/* ── HURUF, DARI FOLDER APLIKASI SENDIRI ──────────────────────────────────
   Baris ini dulu `@import url('https://fonts.googleapis.com/css2?…')`. Di
   kantor yang jaringannya diputus dari internet — persis keadaan banyak NOC —
   Inter & JetBrains Mono tidak pernah sampai, dan seluruh aplikasi jatuh ke
   huruf bawaan browser. Itu bukan "kurang cantik", itu tampak rusak, dan
   yang melihatnya duluan justru calon pembeli di demo.

   Keduanya variable font: SATU berkas memuat seluruh ketebalan (Inter
   300–700, Mono 400–600), jadi tidak ada delapan berkas yang harus dijaga
   cocok satu sama lain.

   `unicode-range` bukan hiasan. Browser hanya mengunduh berkas latin-ext
   kalau ada huruf yang benar-benar memakainya (mis. nama pelanggan berhuruf
   ā atau simbol mata uang di luar € dan $). Halaman biasa hanya menarik
   78 KB dari 172 KB yang tersedia — sekali, lalu dari cache.

   Berkasnya dilayani api.py lewat `_STATIC_EXTS`. Kalau `.woff2` hilang dari
   daftar itu, keempat berkas di bawah jadi 404 dan hurufnya ikut hilang. */
@font-face{
  font-family:'Inter'; font-style:normal; font-weight:300 700; font-display:swap;
  src:url('/fonts/font-inter-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Inter'; font-style:normal; font-weight:300 700; font-display:swap;
  src:url('/fonts/font-inter-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:'JetBrains Mono'; font-style:normal; font-weight:400 600; font-display:swap;
  src:url('/fonts/font-jetbrains-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'JetBrains Mono'; font-style:normal; font-weight:400 600; font-display:swap;
  src:url('/fonts/font-jetbrains-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

:root {
  --bg-0:#060B14; --bg-1:#0D1626; --bg-2:#142035; --bg-3:#1C2D47;
  --border:#1E3A5F; --border2:rgba(30,58,95,0.5);
  --t1:#E2EBF8; --t2:#8AAECB; --t3:#7998B7;
  --green:#22C55E; --red:#F26565; --amber:#F59E0B;
  --blue:#5392F7; --cyan:#06B6D4; --gray:#64748B;
  /* `--orange` dijadikan token 11 September 2026. Ia satu-satunya warna skala
     sinyal yang ditulis mati (`'#F97316'` di signalQuality(), app.js) —
     empat tetangganya sudah `var(--green)`/`var(--amber)`/`var(--red)`.
     Selama warnanya ditulis mati, ia tidak bisa ikut tema mana pun, dan di
     dua tema terang ia jatuh ke 2,74–2,80 tanpa bisa ditolong dari CSS. */
  --orange:#F97316;
  /* Warna TEKS di dalam chip berlatar tint (`rgba(…,.14)`). Kelimanya dulu
     ditulis mati, dan kelimanya dipilih untuk latar GELAP — di tema terang
     tintnya jadi pucat dan teks muda di atasnya ikut pucat: "AP mati"
     terukur 1,03:1, praktis tidak ada. Dijadikan token supaya tema terang
     bisa menukarnya dengan yang gelap, persis pola `--fg-di-terang` di
     lencana Live.

     DUA YANG TERAKHIR DITEMUKAN BELAKANGAN, DAN CARA DITEMUKANNYA PENTING.
     `--fg-chip-hijau` (#86EFAC) & `--fg-chip-biru` (#93C5FD) TIDAK muncul di
     sapuan peramban mana pun — bukan karena mereka lolos, tapi karena data
     demo tidak pernah memunculkan keadaan yang memakainya (`.lentry.ok`,
     `.dis-port-http`, `.dis-jawab-web`). Di tema terang keduanya 1,21–1,54:1.
     Yang menemukan mereka justru laporan pengguna ("informasi aksespoint
     online terlihat samar, berwarna hijau terang") dan pencarian menyeluruh
     `color:#hex` di berkas ini — bukan alat ukurnya.

     Karena itu sekarang ada `test_tidak_ada_warna_teks_yang_ditulis_mati`:
     sapuan peramban hanya melihat yang SEDANG tergambar, jadi ia tidak bisa
     menjaga yang jarang muncul. Yang bisa cuma aturan yang dibaca dari
     berkasnya. */
  --fg-chip-kuning:#FDE68A; --fg-chip-merah:#FCA5A5; --fg-mati:#E4818B;
  --fg-chip-hijau:#86EFAC; --fg-chip-biru:#93C5FD;
  /* `--blue` dinaikkan dari #3B82F6 ke #5392F7 (11 September 2026, putaran
     keempat). 55 dari 74 pemakaiannya `color:` — peran utamanya TEKS, dan
     sebagai teks ia gagal di permukaan yang terangkat: 4,43 di --bg-2 dan
     4,15 di kartu Aktivitas, pada teks 9–11px (.act-btn "Detail" 30x,
     .akt-badge "Ubah", .fbtn "7 hari"). Rona 217° dan saturasi 91%
     DIPERTAHANKAN — dihitung di HLS, hanya terangnya naik 59,8% → 64,8% —
     jadi yang berubah keterbacaannya, bukan warnanya.

     Dinaikkan sampai lolos di KEEMPAT token latar (terburuk 4,50 di --bg-3),
     bukan cuma di permukaan yang kebetulan dipakai hari ini. Nilai yang
     hanya lolos di tempat sekarang akan gagal diam-diam begitu elemennya
     dipindah satu kartu ke dalam.

     Yang TIDAK ikut berubah: 19 literal `rgba(59,130,246,…)` — latar tint
     .06–.22 dan bingkai .28–.4. Mereka memang sudah tidak mengikuti --blue
     sejak ada aksen kustom, dan tidak satu pun membawa teks yang gagal
     kontras. Potret yang disetujui pemilik produk memperlihatkannya persis
     begitu: teksnya naik, bingkainya tetap. */
  /* Warna teks DI ATAS warna aksen. Selama aksennya salah satu dari empat
     preset, ini selalu putih. Yang membuatnya perlu jadi variabel adalah warna
     kustom: pengguna boleh memilih kuning muda, dan teks putih di atasnya
     praktis tidak terbaca. setAksenKustom() menghitungnya dari kecerahan warna
     yang dipilih. */
  --fg-aksen:#fff;
  /* Biru untuk PERMUKAAN, bukan untuk teks. Dipisah 11 September 2026:
     `--blue` dipakai dua cara yang tuntutannya berlawanan — sebagai TEKS di
     latar gelap (butuh terang) dan sebagai LATAR tombol di bawah teks putih
     (butuh gelap). #3B82F6 memberi teks putih cuma 3,68:1; menggelapkannya
     jadi #2563EB memperbaiki tombol (5,17:1) tapi menjatuhkan biru-sebagai-
     teks dari 4,92 ke 3,50 di --bg-1. Satu nilai tidak bisa melayani
     keduanya, jadi yang dipisah nilainya — bukan salah satunya dikorbankan. */
  --blue-permukaan:#2563EB;
  /* Hijau & merah untuk PERMUKAAN tombol, dengan alasan yang sama persis
     seperti --blue-permukaan — dan ditemukan lima hari lebih lambat karena
     halaman yang memakainya tidak pernah masuk daftar sapuan.

     Teks putih di atas --green (#22C55E) cuma 2,28:1, dan di atas --red
     (#F26565) 3,08:1. Yang memakainya bukan hiasan: tombol "Mulai Pointing"
     (14px tebal, tombol utama halaman itu) dan `.btn-bahaya` — tombol yang
     MENGHAPUS. Tombol yang tulisannya paling perlu terbaca justru dua-duanya.

     Seperti --blue-permukaan, keduanya TIDAK diubah tema: kontrasnya diukur
     terhadap teks putih di atasnya, bukan terhadap latar halaman, jadi
     nilainya sama benarnya di keenam tema. Digelapkan di HLS — rona 0,0–0,2°
     bergeser, jadi hijau tetap hijau dan merah tetap merah. */
  --green-permukaan:#167F3D; --red-permukaan:#DA1212;
  /* Teks di atas ketiga *-permukaan. BUKAN --fg-aksen, dan bedanya nyata:
     --fg-aksen ikut warna aksen pilihan pengguna (bisa jadi #111111 kalau
     aksennya kuning muda), sementara hijau & merah permukaan nilainya tetap.
     Memakai --fg-aksen di sana akan menaruh teks GELAP di atas merah tua. */
  --fg-permukaan:#fff;
  /* Teks gelap untuk di atas permukaan TERANG (hijau lencana Live). Hijau
     itu sengaja tidak diubah: warna "hidup" yang sudah dikenal pemakainya,
     dan ia lolos 7,27:1 begitu teksnya yang digelapkan. */
  --fg-di-terang:#06240F;
  /* ⚠️ Namanya menyebut keadaan, bukan warna, dan itu sengaja — tapi ia jadi
     menyesatkan sejak 11 September 2026: di kedua tema TERANG, `--green`
     justru digelapkan supaya terbaca di latar putih, jadi permukaannya tidak
     lagi terang dan token ini bernilai #FFFFFF di sana. Yang dijaga tetap
     sama: "teks yang terbaca DI ATAS --green, apa pun --green-nya hari ini".
     Dijaga per tema oleh test_lencana_hijau_terbaca_di_tiap_tema. */
  /* Cadangan berlapis, bukan sekadar `sans-serif`. Kalau suatu saat berkas
     .woff2 gagal termuat, yang menggantikan adalah huruf antarmuka sistem
     (Segoe UI di Windows, San Francisco di Mac) — bukan huruf bawaan browser
     yang lebarnya jauh berbeda dan membuat tabel padat ini pecah barisnya. */
  --ui:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg-0);color:var(--t1);font-family:var(--ui);font-size:14px;line-height:1.5;min-height:100vh}

/* ── HEADER ── */
header{background:var(--bg-1);border-bottom:1px solid var(--border);padding:0 24px;display:flex;align-items:center;justify-content:flex-start;height:56px;position:sticky;top:0;z-index:100}
.hdr-l{display:flex;align-items:center;gap:12px}
.logo{width:34px;height:34px;background:linear-gradient(135deg,var(--blue),var(--cyan));border-radius:8px;display:flex;align-items:center;justify-content:center;color:var(--fg-aksen);flex-shrink:0}
.logo .ic{width:20px;height:20px}
/* Logo pilihan admin, menutupi logo gelombang bawaan di dalam ubin yang sama.
   `object-fit:contain` supaya logo yang tidak persegi tidak dipaksa gepeng —
   logo perusahaan hampir selalu memanjang. Ubinnya sendiri tidak berubah
   ukurannya, jadi tata letak header tetap persis seperti sebelumnya. */
.merek-logo{width:100%;height:100%;object-fit:contain;display:block;border-radius:inherit}
/* Ubin yang sedang memakai logo pilihan admin: gradiennya dicabut. Gradien itu
   latar untuk ikon gelombang PUTIH bawaan; logo perusahaan punya warnanya
   sendiri, dan gradien di belakangnya membuatnya terbaca seperti stiker. */
.logo.logo-kustom,.login-logo-icon.logo-kustom{background:none}

/* Ikon garis dipakai di seluruh aplikasi, menggantikan emoji. Satu warna:
   `stroke="currentColor"` membuatnya mengambil warna teks di sekitarnya, jadi
   ikon di dalam pesan merah ikut merah tanpa satu pun warna ditulis ulang.

   `vertical-align` perlu karena <svg> adalah elemen inline yang duduk di garis
   dasar teks — tanpa ini ia tampak terangkat setengah huruf di setiap judul
   kartu dan tombol. Di dalam flex, properti ini diabaikan dengan sendirinya. */
.ic{vertical-align:-.15em;flex-shrink:0}
/* Di dashboard logonya sebuah <a> ke /dashboard. Warna & garis bawah bawaan
   tautan harus dimatikan, kalau tidak ikonnya berubah biru bergaris. */
a.logo{text-decoration:none;color:var(--fg-aksen);transition:filter .15s}
a.logo:hover{filter:brightness(1.12)}
.app-name{font-size:15px;font-weight:600;letter-spacing:-.02em}
.app-sub{font-size:10px;color:var(--t3);font-family:var(--mono);text-transform:uppercase;letter-spacing:.08em}
/* `margin-left:auto` MENDORONG blok ini ke tepi kanan header.
   Sampai 19 Agustus 2026 yang mendorongnya `.nav-menu{margin-left:auto}` —
   menu yang duduk di antara logo dan blok ini. Menu itu pindah ke rel kiri,
   pendorongnya ikut hilang, dan jam + lencana Live + profil merapat ke logo
   (bug24). Sekarang blok ini mendorong dirinya sendiri, jadi ia tidak lagi
   bergantung pada ada tidaknya tetangga. */
.hdr-r{display:flex;align-items:center;gap:16px;margin-left:auto}
.badge-live{display:flex;align-items:center;gap:4px;background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.3);border-radius:20px;padding:4px 8px;font-size:10px;font-weight:700;color:var(--green);text-transform:uppercase;letter-spacing:.06em;transition:all .2s}
.dot-live{width:6px;height:6px;background:var(--green);border-radius:50%;animation:blink 1.4s infinite}
.badge-live.offline{background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.35);color:var(--red)}
.badge-live.offline .dot-live{background:var(--red);animation:none}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
#clock{font-family:var(--mono);font-size:12px;color:var(--t2)}

/* ── CONNECTION STATUS ── */
/* Banner status koneksi ke proxy.
   BAWAANNYA TERSEMBUNYI — dulu `display:flex`, sehingga tiap halaman dimuat
   selalu diawali baris biru "Menghubungkan ke proxy server…" yang baru hilang
   sesudah _pingProxy() menjawab (500 ms + waktu request). Di halaman Grafik
   Tren yang sibuk memuat 134 perangkat, jeda itu jadi beberapa detik dan
   bannernya terbaca sebagai gangguan koneksi — dilaporkan pengguna 7 Agustus
   2026 (bug/bug5.png), padahal proxy-nya sehat.
   Yang berarti tetap ditampilkan: keadaan GAGAL. setConnected(false) memasang
   `display:flex` inline, dan inline menang atas aturan ini. */
.conn-bar{padding:8px 24px;font-size:12px;display:none;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
.conn-bar.connecting{background:rgba(59,130,246,.06);color:var(--blue)}
.conn-bar.connected{background:rgba(34,197,94,.06);color:var(--green)}
.conn-bar.error{background:rgba(239,68,68,.08);color:var(--red);display:flex}

/* ── SPANDUK LISENSI (KOMERSIAL Tahap 3) ──────────────────────────────────
   Bentuknya sengaja sama persis dengan .conn-bar: satu baris tipis di bawah
   header, bukan kotak dialog. Keduanya menyampaikan hal yang sama jenisnya —
   keadaan seluruh instalasi, bukan hasil sebuah tindakan — dan dua bentuk
   berbeda untuk satu jenis pesan membuat yang kedua terbaca sebagai sesuatu
   yang lebih genting daripada yang sebenarnya.

   Tidak memakai .conn-bar apa adanya karena keduanya BISA muncul bersamaan
   (langganan hampir habis DAN proxy putus), dan aturan `display:none` di
   .conn-bar akan ikut menyembunyikan yang ini.

   Geseran rel (`margin-left`) ikut DI SINI, bukan sebagai aturan tersendiri
   di bagian rel seperti milik .conn-bar. Aturan terpisah membuat isinya sama
   persis dengan `.conn-bar{margin-left:var(--rail-w)}`, dan dua kelas yang
   deklarasinya sama persis adalah yang dilarang
   `test_gaya_bersama.py::TestTidakAdaKelasKembar` — dengan alasan yang benar:
   kembar yang dibiarkan berkembang biak sampai tidak ada yang tahu mana yang
   dipakai. */
.lisensi-bar{padding:8px 24px;font-size:12px;display:flex;align-items:center;
  gap:8px;border-bottom:1px solid var(--border);margin-left:var(--rail-w)}
.lisensi-bar span{flex:1;min-width:0}
.lisensi-bar.info{background:rgba(59,130,246,.06);color:var(--blue)}
.lisensi-bar.warn{background:rgba(234,179,8,.09);color:var(--amber)}
.lisensi-bar.error{background:rgba(239,68,68,.08);color:var(--red)}
.lisensi-tutup{background:none;border:none;padding:2px;cursor:pointer;
  color:inherit;opacity:.6;display:flex;align-items:center;flex-shrink:0}
.lisensi-tutup:hover{opacity:1}

/* ── MAIN ── */
/* Lebar terbesar isi halaman. Sebagai peubah, bukan angka, karena ia dipakai
   DUA KALI: di sini sebagai batas, dan di aturan penggeser rel di bawah
   sebagai bahan hitungan pemusatannya. Dua angka 1920 yang harus sama persis
   adalah dua angka yang cepat atau lambat berbeda — dan bedanya muncul
   sebagai halaman yang tidak lagi di tengah, tanpa ada yang error. */
:root{--isi-maks:1920px}
main{padding:16px 28px;max-width:var(--isi-maks);margin:0 auto}

/* ── PENCARIAN UTAMA (dashboard, paling atas) ─────────────────────────────
   Diminta pengguna 12 Agustus 2026. Kotaknya sengaja lebih besar daripada
   ketiga kotak pencarian lain di aplikasi ini (11px di dalam tabel): ia bukan
   penyaring sebuah tabel melainkan pintu masuk halaman, dan pintu masuk yang
   ukurannya sama dengan penyaring tidak akan ditemukan orang yang belum tahu
   ia ada.

   `position:relative` di sini yang menahan daftar hasilnya; `z-index` pada
   daftar itu harus di atas kartu statistik yang berada tepat di bawahnya. */
.cari-utama{position:relative;margin-bottom:16px}
.cari-utama input{width:100%;background:var(--bg-1);border:1px solid var(--border);
  border-radius:10px;padding:12px 36px 12px 40px;font-size:14px;color:var(--t1);
  outline:none;font-family:var(--ui)}
.cari-utama input:focus{border-color:var(--blue)}
.cari-utama input::placeholder{color:var(--t3)}
.cari-utama-ic{position:absolute;left:14px;top:50%;transform:translateY(-50%);
  color:var(--t3);display:inline-flex;pointer-events:none}
/* Tombol kosongkan. `top:22px` — bukan 50% — karena begitu daftar hasilnya
   terbuka, tinggi `.cari-utama` ikut bertambah dan tombolnya akan melayang ke
   tengah daftar. 22px = separuh tinggi kotak isiannya sendiri. */
.cari-utama-hapus{display:none;position:absolute;right:12px;top:22px;
  transform:translateY(-50%);background:none;border:none;color:var(--t3);
  cursor:pointer;font-size:14px;line-height:1;padding:4px}
.cari-utama-hapus:hover{color:var(--t1)}

/* Keduanya `display:none` bawaannya dan dibuka JS — daftar hasil saat ada
   yang diketik, tombol kosongkan saat kotaknya berisi. Sama seperti
   `.hal-catatan`: kotak kosong yang selalu ada lebih mengganggu daripada
   kotak yang muncul saat diperlukan. */
.cari-utama-hasil{display:none;position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:60;
  background:var(--bg-1);border:1px solid var(--border);border-radius:10px;
  box-shadow:0 12px 30px rgba(0,0,0,.28);
  /* Delapan baris hasil ±330px — muat di layar mana pun. Batas ini untuk
     baris yang namanya membungkus jadi dua baris di HP: tanpa itu daftarnya
     bisa lebih tinggi daripada layarnya sendiri, dan baris terakhir tidak
     bisa dicapai karena halaman di belakangnya yang ikut tergulung. */
  max-height:min(60vh,420px);overflow:hidden auto}
.cari-baris{display:block;padding:8px 12px;text-decoration:none;
  border-bottom:1px solid var(--border)}
.cari-baris:last-child{border-bottom:none}
/* Satu kelas untuk disorot tetikus MAUPUN papan ketik. Dua gaya terpisah
   (`:hover` sendiri) membuat baris yang sedang dipilih panah bawah tidak
   terlihat lagi begitu tetikus kebetulan lewat di baris lain. */
.cari-baris.sorot{background:var(--bg-3)}
.cari-nama{font-size:13px;font-weight:600;color:var(--t1);margin-bottom:2px}
.cari-ip{display:flex;gap:12px;flex-wrap:wrap;font-family:var(--mono);
  font-size:11px;color:var(--t2)}
.cari-sisi{color:var(--t3);font-family:var(--ui);font-size:9px;
  letter-spacing:.06em;margin-right:3px}
/* Huruf yang cocok. <mark> bawaan browser berlatar kuning terang — di mode
   gelap itu satu-satunya benda putih menyala di layar. */
.cari-tandai{background:rgba(59,130,246,.22);color:var(--blue);
  border-radius:3px;padding:0 1px}
.cari-kosong{padding:12px;font-size:12px;color:var(--t3);
  display:flex;align-items:center;gap:4px}
.cari-kaki{padding:8px 12px;font-size:10px;color:var(--t3);
  border-top:1px solid var(--border);background:var(--bg-2)}

/* ── ALERT ──
   Dihapus 31 Juli 2026 bersama banner "PERANGKAT DI ZONA TERLARANG" yang
   memakainya. Tidak ada satu halaman pun yang memakai kelas .alert* lagi —
   sisa kata "alert" di berkas lain semuanya panggilan alert() JavaScript.
   Nama pelanggarnya kini didapat dengan mengklik kartu Pelanggaran: tabel
   Status Perangkat langsung tersaring ke perangkat yang bersangkutan. */

/* ── STATS ── */
/* Kartu statistik SELALU satu baris.
   Dulu repeat(6,1fr) — jumlah kolomnya dipatok, jadi begitu kartu ke-7
   ("Dinonaktifkan") ditambahkan ia turun ke baris kedua sendirian.
   grid-auto-flow:column membiarkan jumlah kartu berubah tanpa menyentuh CSS
   lagi — penting karena kartu "Dinonaktifkan" muncul/hilang sendiri
   tergantung ada tidaknya perangkat nonaktif.
   minmax(0,1fr) supaya isi kartu yang panjang tidak memaksa kolom melebar. */
.stats{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);gap:12px;margin-bottom:16px}
/* RF-REDESIGN — SATU variabel warna per kartu, bukan tiga daftar sejajar.
   Warna kartu dulu ditulis dua kali (aksen + angka); begitu ikon label
   ditambahkan ia jadi tiga daftar yang harus diubah bersamaan tiap kali ada
   warna baru — dan yang ketinggalan satu tidak terlihat sampai warna itu
   dipakai. `--sc` dinyatakan sekali, yang membacanya boleh bertambah tanpa
   menyentuh daftarnya. Bawaannya abu-abu, bukan tanpa nilai: kartu tanpa
   kelas warna harus tetap punya aksen, bukan aksen tak terlihat. */
.scard{background:var(--bg-1);border:1px solid var(--border);border-radius:10px;padding:12px 16px 12px 20px;position:relative;overflow:hidden;--sc:var(--gray)}
/* Aksen pindah dari tepi ATAS ke tepi KIRI. Mata membaca kartu dari kiri;
   garis di atas berada persis di jalur tepi kartu tetangganya dan terbaca
   sebagai pemisah antar kartu, bukan sebagai penanda milik kartu ini. */
.scard::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;background:var(--sc)}
.scard.green{--sc:var(--green)}.scard.red{--sc:var(--red)}
.scard.amber{--sc:var(--amber)}.scard.blue{--sc:var(--blue)}
.scard.cyan{--sc:var(--cyan)}
.scard.gray{--sc:var(--gray)}
.slabel{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--t3);margin-bottom:4px;display:flex;align-items:center;gap:4px}
/* Ikon mewarisi warna KARTU, bukan warna tulisan labelnya. Labelnya sengaja
   redup (t3) supaya angkanya yang dibaca lebih dulu — dan ikon seredup itu
   hilang sama sekali pada ukuran 12px. */
.slabel .ic{width:12px;height:12px;flex-shrink:0;color:var(--sc)}
.sval{font-family:var(--mono);font-size:26px;font-weight:600;line-height:1;margin-bottom:4px;color:var(--sc)}
.ssub{font-size:11px;color:var(--t3)}

/* Kartu statistik BISA DIKLIK — tiap kartu menyaring tabel Status Perangkat
   ke kategorinya. Kursor & tooltipnya dipasang dari JS (lihat filterDariKartu)
   supaya kartu tanpa `data-filter` tidak ikut terlihat bisa diklik.

   Baris nama pelanggar (.svio-names) yang sempat ada di sini DICABUT: kartu
   statistik satu baris grid, jadi satu kartu yang lebih tinggi meregangkan
   ketujuhnya dan enam sisanya jadi separuh kosong. Namanya kini didapat
   dengan mengklik kartunya. */
.scard[data-filter]{transition:border-color .15s, transform .1s}
.scard[data-filter]:hover{border-color:var(--t3)}
.scard[data-filter]:active{transform:scale(.985)}

/* ── SPECTRUM ── */
/* Tampilan dasarnya pindah ke `.kartu-panel`. Nama ini tetap dipakai
   aturan tata letak di bawah — lihat catatan di `.kartu-panel`. */
.card-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.card-title{font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px}
.pill{font-size:10px;font-family:var(--mono);background:var(--bg-3);border:1px solid var(--border);border-radius:4px;padding:2px 8px;color:var(--t3)}
canvas{display:block;width:100%;height:160px;border-radius:6px}
.spectrum-row{display:flex;align-items:stretch;gap:8px}
.spectrum-row canvas{flex:1;min-width:0}
/* ── Skala warna sinyal (dBm) di sisi kanan, meniru tampilan spectrum analyzer Ubiquiti ── */
.spectrum-scale{display:flex;align-items:center;gap:4px;flex-shrink:0;padding:2px 0 2px 2px}
.spectrum-scale .sc-nums{display:flex;flex-direction:column;justify-content:space-between;height:160px;font-family:var(--mono);font-size:9px;color:var(--t3);text-align:right}
.spectrum-scale .sc-bar{width:9px;height:160px;border-radius:5px;box-shadow:inset 0 0 0 1px var(--border2)}
.spectrum-scale .sc-title{writing-mode:vertical-rl;transform:rotate(180deg);font-size:9px;color:var(--t3);white-space:nowrap;letter-spacing:.03em}
.axis{display:flex;justify-content:space-between;margin-top:4px;padding:0 2px}
.axis span{font-family:var(--mono);font-size:9px;color:var(--t3)}
/* Baris legenda peta spektrum. Nama grupnya TETAP terpisah dari
   `.legenda-grup` milik Grafik Tren: yang ini punya `margin-top` dan
   `display` sendiri, yang itu sengaja tanpa `display` karena JS yang
   menyalakannya. Dua aturan yang memang berbeda, bukan dua nama untuk
   satu hal. */
.legenda-peta{display:flex;gap:12px;margin-top:8px;flex-wrap:wrap}
/* SATU butir legenda — dipakai peta spektrum (Dashboard) DAN Grafik
   Tren. Dulu ditulis dua kali dengan nama berbeda, `.leg` dan
   `.chart-legenda`; disatukan 21 Agustus 2026. Kotak warnanya tidak
   ikut: 8px di peta spektrum, 10px di Tren — beda yang nyata. */
.legenda{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--t2)}
.legenda-kotak{width:8px;height:8px;border-radius:2px}
/* Warna tiap titik keterangan peta spektrum. Dulu ditulis sebagai
   `style="background:var(--red)"` di elemennya — empat gaya sebaris untuk
   empat warna yang sudah punya nama. Ditambah satu warna kelima (amber,
   13 Agustus) itu jadi lima. */
.legenda-aman{background:var(--green)}
.legenda-larang{background:var(--red)}
.legenda-perhatian{background:var(--amber)}
.legenda-radio{background:var(--blue)}
.legenda-mati{background:var(--t3)}

/* ── PERLU TINDAKAN — daftar perangkat yang melanggar & yang down ──
   Duduk tepat di bawah peta spektrum. Spektrum menjawab "di mana" di pita
   frekuensi; panel ini menjawab "siapa".

   `flex:1` pada kartunya membuatnya menyerap sisa tinggi kolom kiri, jadi
   kolom itu berakhir rata dengan kartu Log di sebelahnya alih-alih
   meninggalkan celah kosong yang bentuknya berubah-ubah. */
.act-card{
  background:var(--bg-1);border:1px solid var(--border);border-radius:10px;
  padding:12px 16px;display:grid;grid-template-columns:1fr 1fr;gap:12px;
  height:var(--act-h);flex:0 0 auto;overflow:hidden;
  /* `min-height` di sini LANTAI, bukan 0. Tinggi kartu ini datang dari sisa
     ruang baris grid (lihat `.two` di bawah); kalau suatu saat sisa itu nol,
     panelnya akan lenyap sama sekali — hilang tanpa jejak lebih buruk
     daripada sempit. 132px = judul + tiga baris. Nilai pasti apa pun juga
     mencabut tinggi-minimum-otomatis flexbox, yang tanpa itu tidak mau
     menyusut di bawah tinggi isinya. */
  /* 160px = kepala kartu + judul kolom + dua baris. Naik dari 132px
     21 Agustus 2026 bersama kepala kartunya; angka lama menggambarkan
     kartu yang belum punya kepala. */
  min-height:160px;
  /* `minmax(0,1fr)` — BUKAN `auto` yang jadi bawaannya. Baris grid `auto`
     tumbuh mengikuti isinya, jadi begitu 40 perangkat down bersamaan,
     daftarnya melewati batas kartu dan tumpah ke luar alih-alih digulung.
     `min-height:0` di .act-col dan `overflow-y:auto` di .act-list baru
     berfungsi setelah barisnya sendiri dibatasi. */
  grid-template-rows:auto minmax(0,1fr)
}
/* Kepala kartu: namanya di kiri, angka gabungannya di kanan. Membentang dua
   kolom — `1 / -1` dan bukan `span 2`, supaya susunan satu-kolom di layar
   sempit (@media 640px) ikut benar tanpa aturan tersendiri. */
.act-kepala{
  grid-column:1 / -1;display:flex;align-items:center;justify-content:space-between;
  gap:8px;margin-bottom:12px
}
/* Angka gabungan "berapa perangkat bermasalah sekarang" — jumlah KEDUA kolom.
   Warnanya ikut isinya: nol tidak boleh merah, karena lencana merah yang
   selalu ada berhenti dibaca dalam sehari. Nol justru kabar baik, dan kabar
   baik tidak perlu berteriak. */
.act-total{
  font-family:var(--mono);font-size:16px;font-weight:700;line-height:1;
  padding:4px 8px;border-radius:6px;background:var(--bg-2);color:var(--t3)
}
.act-total.ada{background:rgba(239,68,68,.12);color:var(--red)}
.act-col{display:flex;flex-direction:column;min-height:0;min-width:0}
.act-h{
  font-size:11px;font-weight:700;display:flex;align-items:center;gap:8px;
  padding-bottom:8px;margin-bottom:8px;border-bottom:1px solid var(--border2)
}
.act-h.red{color:var(--red)}
/* RF-REDESIGN — kolom "Perangkat Down" pindah dari abu-abu ke amber. Abu-abu
   adalah warna "tidak ada nilai" di seluruh aplikasi ini (lihat `.badge.off`,
   `.t-redup`); memakainya untuk sebuah KATEGORI berarti daftar yang berisi
   sepuluh perangkat mati terbaca sama redupnya dengan daftar kosong. */
.act-h.amber{color:var(--amber)}
/* Angka di kanan judul: jumlah yang sedang masuk kategori itu.
   RF-REDESIGN — dari lencana 11px berbingkai jadi angka 17px telanjang.
   Bingkainya membuat angka ini terbaca sebagai label, sederajat dengan judul
   di sebelahnya; yang dicari mata di panel ini justru angkanya.

   17px, bukan lebih: tinggi `.act-h` menentukan berapa baris daftar yang muat
   di kartu bertinggi tetap `--act-h`, dan judul 11px-nya sendiri sudah
   setinggi ~16.5px. 17px menambah setengah piksel; 22px akan memakan satu
   baris daftar penuh — ongkos yang sama yang dibayar kepala kartu ini
   21 Agustus 2026, dan tidak perlu dibayar dua kali. */
.act-n{
  margin-left:auto;font-family:var(--mono);font-size:16px;font-weight:700;
  line-height:1;color:var(--t3)
}
/* Nol tidak diwarnai — aturan yang sama dengan `.act-total.ada` di atas, dan
   karena alasan yang sama: angka merah/amber yang SELALU ada berhenti dibaca
   dalam sehari, dan yang dilaporkannya justru kabar baik. */
.act-h.red   .act-n.ada{color:var(--red)}
.act-h.amber .act-n.ada{color:var(--amber)}
/* Daftar digulung DI DALAM panelnya, bukan memanjangkan panel. Sepuluh
   perangkat down sekaligus tidak boleh mengubah tinggi seluruh kolom kiri. */
.act-list{flex:1;overflow-y:auto;min-height:0;display:flex;flex-direction:column;gap:4px}
.act-list::-webkit-scrollbar{width:3px}
.act-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
.act-item{
  display:flex;align-items:baseline;gap:8px;padding:4px 8px;border-radius:6px;
  background:var(--bg-2);border-left:2px solid transparent;cursor:pointer;
  transition:background .12s
}
.act-item:hover{background:var(--bg-3)}
.act-item.vio{border-left-color:var(--red)}
.act-item.down{border-left-color:var(--amber)}
.act-nama{
  font-size:12px;font-weight:600;color:var(--t1);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1
}
.act-info{font-family:var(--mono);font-size:10px;color:var(--t3);white-space:nowrap;flex-shrink:0}
.act-item.vio .act-info{color:var(--red)}
/* RF-REDESIGN — tanda kategori di ujung kanan tiap baris. Nama di kiri
   menjawab SIAPA, angka di tengah menjawab BERAPA, tanda ini menjawab
   KENAPA — dan yang terakhir itulah yang menentukan baris ini pekerjaan
   siapa. Sengaja sependek mungkin: sisa lebar baris milik namanya, dan nama
   perangkat yang terpotong jadi "radio-24 PT. Maha…" tidak bisa dicari. */
.act-tag{
  font-size:9px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  padding:1px 4px;border-radius:4px;flex-shrink:0
}
.act-item.vio  .act-tag{background:rgba(239,68,68,.14);color:var(--fg-chip-merah)}
.act-item.down .act-tag{background:rgba(245,158,11,.14);color:var(--fg-chip-kuning)}
.act-kosong{font-size:11px;color:var(--t3);padding:4px 2px}

/* ── LAYOUT 2-COL ──
   Kiri: kartu statistik, peta spektrum, tabel perangkat — bertumpuk.
   Kanan: Log, memanjang setinggi KETIGANYA, dari batas paling atas.

   Susunan ini dipilih 31 Juli 2026 setelah melihat dashboard dipakai
   sungguhan. Sebelumnya Log sekolom dengan tabel perangkat sehingga keduanya
   berebut tinggi yang sama, sementara spektrum memakan satu baris penuh
   sendirian di atas. Yang benar-benar dilihat sekali pandang adalah kartu
   statistik, spektrum, dan log; tabel perangkat justru yang paling wajar
   dibaca sambil menggulung.

   Log yang memanjang penuh juga menyelesaikan ruang kosong di bawahnya: makin
   tinggi kartunya, makin kecil sisa yang tidak terpakai oleh entri utuh. */
/* Tinggi BARIS-nya pasti — dan ini yang membuat semua batas di dalam kolom
   kiri berfungsi.

   Dilaporkan dua kali (31 Juli 2026, `bug/bug2.png` lalu `bug/bug3.png`):
   panel Perlu Tindakan memanjang ke bawah mengikuti jumlah perangkat down
   dan mendorong tabel di bawahnya. Dua percobaan sebelumnya memasang batas
   makin ke dalam — `overflow-y:auto` di daftarnya, `minmax(0,1fr)` di baris
   kartunya, tinggi pasti 0 di kartunya — dan semuanya lewat.

   Sebabnya satu tingkat lebih luar dari semua itu: baris grid ini bawaannya
   `auto`, jadi tingginya diambil dari max-content `.two-left`. Dan tinggi
   max-content sebuah kolom flex ADALAH tinggi isinya — flexbox menghitung
   baris grid cukup besar untuk memuat tiap itemnya, termasuk yang `flex:1`.
   Baik `flex-basis:0` maupun `height:0` tidak masuk hitungan itu; keduanya
   soal pembagian ruang, bukan soal berapa ruang yang tersedia.

   Jadi rantainya melingkar: satu baris baru di panel → `.two-left` lebih
   tinggi → baris grid lebih tinggi → `1fr` yang ikut tumbuh selalu cukup →
   tidak ada yang pernah digulung. Terukur di bug3.png: kolom kiri 702px,
   kartu Log 680px.

   Nilai pastinya diambil dari sumber yang sama dengan tinggi kartu Log,
   supaya kedua kolom tidak bisa berbeda: `--log-h` yang ditulis
   aturTinggiLog() setelah mengukur tinggi header, dengan rumus CSS-nya
   sebagai cadangan kalau JS tidak jalan. */
/* ── LEBAR KOLOM KANAN ──
   380px dilaporkan pengguna terlalu sempit 15 Agustus 2026: "tabel informasi
   pelanggaran dan down serta tabel log diperlebar sedikit, agar informasi
   terlihat semua".

   Yang terpotong nama perangkat di panel Perlu Tindakan, dan angkanya diukur —
   bukan dikira-kira. Panel itu DUA kolom di dalam kartu, jadi tiap nama cuma
   kebagian seperdua dikurangi seluruh padding:

     (380 - 32 padding kartu - 14 gap) / 2   = 167px per kolom
     - 3 scrollbar - 16 padding baris - 2 border
     - 8 gap - 48 lebar "5600 MHz" (mono 10px)
                                             =  90px untuk namanya

   Diukur langsung dari web/fonts/font-inter-latin.woff2 terhadap 135 nama
   perangkat yang benar-benar terdaftar, pada Inter 12px:

     p50  98px   Jombang-Pinang
     p75 125px   PT. Cempaka (Seruni)
     p90 156px   Ponpes Al-Qurani Al Ghifari
     max 219px   Yellow Bee Tangerang (Quanta/Higen)

   Jadi 88px memotong ±65% nama yang ada — termasuk yang median. 500px
   memberi 148px, cukup utuh sampai p86; itu batas atas clamp di bawah.

   **Kenapa clamp, bukan angka tetap.** Kolom kanan tumbuh dengan mengambil
   dari kolom kiri, dan yang paling dulu terjepit di sana adalah kartu metrik:
   label terpanjang "DINONAKTIFKAN" butuh 93px + 32px padding = 128px per
   kartu, yang baru terpenuhi kalau kolom kiri ≥975px. Di layar 1600px —
   ukuran yang dipakai saat tata letak ini dirancang — 500px menyisakan
   1028px, masih di atasnya. Di layar yang lebih sempit persentasenya yang
   berlaku, jadi kolom kiri tidak pernah diperas demi lebar yang tidak
   dibutuhkan layar sesempit itu. Lantainya 380px = keadaan sebelum ini:
   perubahan ini tidak boleh membuat satu lebar layar pun jadi lebih sempit
   daripada sekarang. */
.two{display:grid;grid-template-columns:1fr clamp(380px, 34%, 500px);gap:16px;margin-bottom:16px;align-items:stretch;
     grid-template-rows:minmax(min-content, calc(var(--act-h) + 16px + var(--log-h, calc(99px + var(--log-entries) * var(--lentry-h)))))}
/* `minmax(min-content, …)` — dan letak `min-content` di batas BAWAH itu yang
   membedakannya dari percobaan yang gagal di bug52 (lihat catatan di bawah).

   bug69, 10 September 2026. Rumus tinggi baris di atas menjumlahkan isi kolom
   KANAN saja. Kolom kiri dipaksa mengikutinya, dan dua kartu teratasnya
   menolak menyusut (`.stats-baris` dan `.spectrum-wrap`, keduanya
   `flex:0 0 auto`). Selama kolom kanan lebih tinggi daripada keduanya, tidak
   ada yang terlihat.
   Yang tidak dipikirkan: kolom kanan bisa jauh LEBIH PENDEK daripada itu.
   Perlu Tindakan kosong + Log 0 entri = baris yang lebih pendek daripada
   kartu statistik + peta spektrum, dan `.zone-card` sudah mentok di 0 jadi
   tidak ada lagi yang bisa diserap. Sisanya meluber keluar baris dan
   menimpa kepala kartu Status Perangkat di bawahnya — gejala yang PERSIS
   sama dengan bug51, dari sisi yang berlawanan.

   Dan itu bukan keadaan langka: instalasi pembeli yang baru dipasang, sebelum
   perangkat pertama ditambahkan, memperlihatkannya di layar pertama.

   Kenapa ini tidak mengulang bug52: yang keliru di sana `minmax(…, auto)` —
   batas ATAS yang berarti "boleh setinggi max-content", sehingga daftar
   Kendala per BTS berhenti menggulung dan barisnya melar melewati kolom
   kanan. Di sini batas atasnya TETAP angka pasti yang sama; yang ditambahkan
   cuma lantai. Baris tidak akan pernah lebih tinggi daripada sebelumnya —
   ia cuma berhenti menjadi lebih pendek daripada isi yang tak bisa menyusut.
   (Dalam `minmax()`, batas atas yang lebih kecil daripada batas bawah memang
   diabaikan — itulah yang membuat lantainya berlaku.) */
/* Angka pasti, dan sengaja tetap begitu — lihat laporan-bug/bug52.png.
   Percobaan pertama menjawab bug51 dengan `minmax(…, auto)`, dan itu keliru:
   batas atas `auto` berarti barisnya boleh setinggi MAX-CONTENT isinya, dan
   max-content kolom kiri adalah seluruh daftar Kendala per BTS tanpa
   digulung. Di produksi (17 BTS) barisnya melar ±230px melewati kolom kanan,
   Kendala per BTS berhenti menggulung, dan di sebelah kartu Log muncul ruang
   kosong selebar kolom — tepat kebalikan dari yang diminta. Dikembalikan.

   Yang benar: barisnya tetap satu angka, dan kolom kirilah yang diajari
   menyusut ke dalamnya. Lihat `.two-left` di bawah. */
/* Tinggi barisnya sekarang menjumlahkan KEDUA kartu di kolom kanan, bukan
   kartu Log saja. Sejak Perlu Tindakan pindah ke sana (13 Agustus 2026),
   `--log-h` hanya menjawab setinggi apa kartu Log — dan baris yang mengikuti
   angka itu saja akan memotong Action Center di atasnya.

   16px di tengah = `gap` .two-right. Ditulis apa adanya, bukan dibaca dari
   variabel: gap grid tidak bisa dirujuk dari calc(). Kalau gap-nya diubah,
   angka ini harus ikut — uji tata letak menjaga keduanya tetap sama. */
/* Kedua kolomnya sekarang satu aturan, dan penyatuan itu sendiri yang
   memperbaiki bug51.

   `min-height:0` dulu cuma dipasang di kolom KANAN. Bawaan sebuah kotak
   flex/grid adalah `min-height:auto` — "jangan pernah lebih pendek daripada
   isiku" — dan kolom kiri masih tunduk padanya. Selama kolom kanan yang
   selalu tertinggi, selisihnya tidak pernah terlihat.

   Tinggi kartu Log mengikuti JUMLAH ENTRI (aturTinggiLog di dashboard.html),
   dan tinggi barisnya diturunkan dari angka yang sama. Jadi pada instalasi
   yang lognya sepi — demo, pemasangan baru, pagi sesudah pembersihan —
   barisnya menyusut sementara kolom kiri menolak ikut. Terukur di bug51: log
   3 entri → baris ≈666px, kolom kiri bertahan ≈742px, dan selisihnya meluber
   menimpa kepala kartu Status Perangkat di bawahnya.

   Sekarang kedua kolom terikat pada tinggi barisnya, dan kelebihan di kolom
   kiri diserap Kendala per BTS yang memang menggulung. Di produksi (log
   penuh) tidak ada kelebihan untuk diserap, jadi tidak ada satu piksel pun
   yang berubah di sana. */
.two-left,
.two-right{display:flex;flex-direction:column;gap:16px;min-width:0;min-height:0}
/* Yang menyerap kekurangan itu HARUS Kendala per BTS, bukan kartu metrik di
   atasnya: `flex:0 1 auto` bawaan membuat kartu angka ikut diperas menurut
   perbandingan tingginya, dan angka besar yang terpotong separuh jauh lebih
   buruk daripada daftar yang menggulung. Peta spektrum sudah `flex:0 0 auto`
   di aturannya sendiri. */
.two-left > .stats-baris{flex:0 0 auto}
/* Jarak antar kartu di kolom kiri diurus `gap`; margin bawaan tiap kartu akan
   menambah jarak KEDUA yang tidak diminta siapa pun — hasilnya 34px di antara
   dua kartu tertentu saja, dan itu terbaca sebagai tata letak yang meleset
   ketimbang sebagai jarak yang disengaja. */
.two-left > .stats-baris,
.two-left > .spectrum-wrap,
.two-left > .zone-card{margin-bottom:0}
/* Peringkat Kendala BTS mengambil alih peran .act-card sebagai pengisi sisa
   ruang kolom kiri.

   `1 1 240px` — dan tiga angkanya menjawab tiga keluhan yang berbeda:

     · `flex-grow:1` menjawab bug18 ("peta utuh, peringkat tersisa beberapa
       baris"). Seluruh sisa ruang kolom kiri jatuh ke sini, jadi di layar
       yang tinggi kartu inilah yang bertambah baris, bukan peta yang
       bertambah kosong. Di produksi ia selalu jauh melewati 240px.

     · `240px` sebagai basis, bukan sebagai `min-height`, adalah perbaikan
       bug51. Angka yang sama, tapi `min-height` berarti "tuntut segini walau
       barisnya tidak sanggup" — dan tuntutan itulah yang meluber menimpa
       kepala tabel Status Perangkat pada instalasi yang lognya sepi. Sebagai
       flex-basis ia cuma PERMINTAAN: dipenuhi kalau ada ruangnya, dilepas
       kalau tidak. Di produksi keduanya menghasilkan tinggi yang persis sama,
       karena yang menentukan di sana `flex-grow`, bukan basisnya.

       240px sendiri bukan angka bebas — ia setinggi kanvas peta spektrum, dan
       itu batas yang ditetapkan bug18: peringkat tidak boleh lebih pendek
       daripada peta di atasnya selama masih ada ruang untuk keduanya.

     · `min-height:0` mencabut `min-height:auto` bawaan flex, yang tanpa itu
       mengembalikan tuntutan yang sama lewat pintu belakang — kali ini
       setinggi SELURUH daftar BTS.

   Informasi kendalanya tidak berkurang sedikit pun: barisnya menggulung di
   dalam kartu (`overflow-y:auto` di baris berikutnya), nama BTS yang panjang
   dipotong elipsis, dan rinciannya tetap lengkap di `title` tiap baris. */
.two-left > .zone-card{flex:1 1 240px;min-height:0;display:flex;flex-direction:column}
.two-left > .zone-card > div:last-child{flex:1;overflow-y:auto;min-height:0}

/* Dua kelompok kartu metrik, berdampingan. Lebarnya 3:4 mengikuti jumlah
   kartunya, jadi seluruh kartu tetap sama lebar seperti saat masih satu baris. */
.stats-baris{display:grid;grid-template-columns:3fr 4fr;gap:16px}
.kolom-sempit{min-width:0}
.stats-judul{font-size:9px;font-weight:700;text-transform:uppercase;
  letter-spacing:.09em;color:var(--t3);margin-bottom:8px;padding-left:2px}
.scard-klik{cursor:pointer}

/* ── DEVICE TABLE CARD ── */
/* `margin-bottom` WAJIB ada di sini sejak tabel dipindah keluar dari `.two`
   (31 Juli 2026). Jarak ke bagian Regulasi di bawahnya dulu datang dari
   `.two{margin-bottom:18px}`; begitu tabelnya berdiri sendiri, jarak itu ikut
   hilang dan kedua kartu menempel. */
/* Tinggi kartu tabel Status Perangkat MENGIKUTI ISINYA, tidak dipatok.
   Dulu 768px, angka yang cocok saat satu baris setinggi ±47px dan halaman
   selalu 10 baris. Dua hal mencabut dasarnya:

     · barisnya jadi lebih tinggi (dua baris IP: AP dan ST, ±66px), jadi 10
       baris tidak lagi muat dan sisanya harus di-scroll DI DALAM kartu —
       padahal halamannya sendiri masih punya ruang di bawah;
     · jumlah baris per halaman sekarang bisa dipilih 10/25/50/100/500, dan
       tidak ada satu angka tinggi pun yang benar untuk kelimanya.

   `height:auto` membuat kartunya persis setinggi berapa pun baris yang
   sedang ditampilkan. Yang di-scroll kembali halamannya, bukan kotak di
   dalam halaman — dan menggulung kotak di dalam kotak itu justru yang
   dikeluhkan (bug/bug7.png, 7 Agustus 2026). */
.tcard{background:var(--bg-1);border:1px solid var(--border);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;height:auto;margin-bottom:16px}
.tcard-hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
.filter-row{display:flex;gap:4px;margin-left:8px}
.fbtn{background:var(--bg-3);border:1px solid var(--border);border-radius:5px;padding:4px 8px;font-size:10px;color:var(--t3);cursor:pointer;font-family:var(--ui);transition:all .12s}
.fbtn:hover,.fbtn.active{background:rgba(59,130,246,.1);border-color:rgba(59,130,246,.4);color:var(--blue)}
.fbtn.warn.active {
  background: rgba(245,158,11,.1);
  border-color: rgba(245,158,11,.4);
  color: var(--amber);   /* ← oranye */
}
.fbtn.safe.active {
  background: rgba(34,197,94,.1);
  border-color: rgba(34,197,94,.3);
  color: var(--green);   /* ← oranye */
}
.fbtn.danger.active{background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.4);color:var(--red)}
.refresh-btn{display:flex;align-items:center;gap:4px;background:none;border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11px;color:var(--t2);cursor:pointer;font-family:var(--ui);transition:all .15s}
.refresh-btn:hover{border-color:var(--blue);color:var(--blue)}
.refresh-btn.spinning svg{animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.tscroll{overflow-x:auto;overflow-y:auto;width:100%;flex:1;min-height:0}
.tscroll::-webkit-scrollbar{width:3px; height:3px}
.tscroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
table{width:100%;border-collapse:collapse}
thead tr{background:var(--bg-2)}
th{padding:8px 12px;text-align:center;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--t3);border-bottom:1px solid var(--border);white-space:normal;line-height:1.3}
th:first-child{text-align:left}
td{padding:12px;font-size:13px;border-bottom:1px solid var(--border2);vertical-align:middle;word-break:break-word;text-align:center}
td:first-child{text-align:left}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover{background:rgba(255,255,255,.02)}
.dname{font-weight:500;color:var(--t1)}
/* Baris nama perangkat: nama, tombol grafik tren, lalu lencana PTMP kalau ada.
   Dipakai di kolom pertama dashboard.

   BARIS INI SENGAJA BUKAN FLEX, dan itu perbaikannya — bukan penyederhanaan.
   Sebagai baris flex (`display:flex;align-items:center;gap:5px`), `.dname`
   menjadi item yang lebarnya SELURUH kolom begitu namanya cukup panjang untuk
   membungkus jadi dua baris — dan tombol trennya terdorong ke tepi kanan
   kolom, ditengahkan pada kedua baris, sejauh mungkin dari nama yang
   dimaksudnya.

   Nama pendek tidak memperlihatkannya sama sekali: di situ nama dan tombolnya
   memang bersebelahan. Itulah kenapa ini tidak pernah terlihat saat
   dikerjakan — yang melihatnya pengguna dengan nama pelanggan sungguhan
   (8 Agustus 2026, laporan-bug/bug11.png).

   Dengan aliran inline biasa, tombolnya mengikuti KATA TERAKHIR namanya,
   sepanjang apa pun namanya. `vertical-align:middle` menggantikan
   `align-items:center` yang hilang bersama flex-nya. */
.dname-baris .btn-ikon{margin-left:4px;vertical-align:middle}
/* Bentuk yang sama tanpa tombol tren — nama + lencana PTMP — di kolom pertama
   Kelola Perangkat dan Detail Perangkat. Bug-nya sama persis dan ditemukan
   dengan menelusuri yang di atas, bukan dari laporan tersendiri: lencananya
   terdorong ke tepi kanan begitu namanya membungkus. */
.nama-baris{font-weight:600;color:var(--t1)}
.dip{font-family:var(--mono);font-size:10px;color:var(--t3);margin-top:2px;
  display:flex;gap:.35em;align-items:baseline}
/* Penanda sisi di depan tiap baris IP ("AP" / "ST"). Lebarnya dipatok supaya
   kedua barisnya berbaris rata — tanpa itu "AP" dan "ST" yang berbeda lebar
   huruf membuat kolom IP-nya bergeser satu-dua piksel dan terbaca goyah. */
.dip-sisi{flex-shrink:0;width:1.8em;color:var(--t3);opacity:.75;font-weight:700}
/* Kelanjutan baris yang membungkus berbaris di bawah IP-nya, bukan di bawah
   penanda sisinya. `min-width:0` wajib: tanpa itu flex item menolak menyusut
   lebih kecil daripada isinya dan tabelnya melebar sendiri. */
.dip-teks{min-width:0;overflow-wrap:anywhere}
.dip-ip{color:var(--cyan);text-decoration:underline;cursor:pointer}

/* Tombol yang isinya CUMA ikon (mis. pintasan grafik tren di kolom Perangkat).
   `color` WAJIB disetel: <button> tanpa warna memakai warna teks bawaan
   browser — hitam — yang terbaca di tema terang tapi nyaris tidak terlihat di
   tema gelap. Dilaporkan pengguna 7 Agustus 2026 (bug/bug4.png), dan bentuk
   kesalahannya persis jenis yang tidak muncul di layar orang yang
   mengerjakannya kalau ia kebetulan memakai tema terang. */
.btn-ikon{background:none;border:none;cursor:pointer;padding:1px 3px;line-height:1;
  flex-shrink:0;color:var(--t3);display:inline-flex;align-items:center;transition:color .15s}
.btn-ikon:hover{color:var(--blue)}
/* ── `white-space:nowrap` di bawah ini: penahan kedua untuk bug9 & bug10 ──
   `td` di atas memakai `word-break:break-word`, yang mengizinkan patahan di
   TENGAH kata. Untuk nama perangkat & IP itu memang yang diinginkan, tapi
   untuk satuan di bawah ini hasilnya "AMA/N", "U/P", "7./4", "3d 01:48/:01" —
   persis yang dilaporkan. Lebar kolom sudah dibetulkan, jadi nowrap ini
   seharusnya tidak pernah terpakai; gunanya kalau kelak ada kolom baru lagi,
   gejalanya muncul sebagai kolom sesak yang masih terbaca, bukan tumpukan
   satu huruf yang tidak.

   Yang diberi nowrap: `.freq`, `.badge`, `.st-up`/`.st-down`/`.st-unknown`,
   dan `.sel-2sisi` (yang letaknya di blok sel dua sisi, jauh di bawah).
   Keenamnya HANYA dipakai dashboard.html, jadi tidak ada halaman lain yang
   ikut berubah. `.dip-teks` sengaja TIDAK diikutkan — di sana patahan
   sembarang tempat memang yang disetel (`overflow-wrap:anywhere`). */
.freq{font-family:var(--mono);font-size:14px;font-weight:600;white-space:nowrap}
.freq.safe{color:var(--green)}.freq.danger{color:var(--red)}.freq.warning{color:var(--amber)}.freq.offline{color:var(--t3)}
.fband{font-size:9px;color:var(--t3);margin-top:2px}
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}
.badge.ok{background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.3);color:var(--green)}
.badge.vio{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:var(--red);animation:vioPulse 1.5s infinite}
@keyframes vioPulse{0%,100%{box-shadow:none}50%{box-shadow:0 0 8px rgba(239,68,68,.4)}}
.badge.warn{background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.3);color:var(--amber)}
/* RF-REDESIGN — down berhenti abu-abu, jadi merah TUMPUL.
   Abu-abu di aplikasi ini berarti "tidak ada nilai" (`.t-redup`, sel kosong,
   kartu Dinonaktifkan). Perangkat yang mati bukan perangkat tanpa nilai —
   ia keadaan terburuk yang bisa dilaporkan baris ini. Merahnya sengaja lebih
   tumpul daripada `.badge.vio` dan TIDAK berdenyut: kalau keduanya sama,
   sepuluh perangkat down akan menenggelamkan satu pelanggaran frekuensi yang
   justru punya tenggat hukum. Dipakai HANYA di tabel dashboard — satu-satunya
   tempat yang menulis `badge off` (lihat bCls di renderTable). */
.badge.off{background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.22);color:var(--fg-mati)}
.bdot{width:4px;height:4px;border-radius:50%;background:currentColor}
/* Nilai monospace di sel tabel. `.sig` (Signal) dan `.sel-lebar` (Lebar
   Channel) tadinya berbeda SETENGAH PIKSEL — 12 lawan 12,5 — dan sesudah
   pembulatan 15 Agustus 2026 ternyata memang hal yang sama. Disatukan jadi
   satu aturan alih-alih dua yang kembar: dua aturan berisi sama bisa
   menyimpang sendiri-sendiri, dan yang menyimpang setengah piksel tidak
   akan ada yang menyadarinya. */
.sig, .sel-lebar{font-family:var(--mono);font-size:12px;color:var(--t2)}
.empty{text-align:center;padding:40px;color:var(--t3);font-size:13px}
/* Keadaan kosong yang menjelaskan (TAMPILAN §4). `.kosong-sebab` menyebut
   penyaring mana yang sedang memotong; `.kosong-bersih` mematikannya. Bukan
   dipisah karena tampilannya beda, melainkan karena isinya beda derajat:
   yang pertama keterangan, yang kedua satu-satunya jalan keluar dari layar
   kosong tanpa harus menebak. */
.kosong-sebab{margin-top:4px;font-size:11px;color:var(--t3);line-height:1.6}
.kosong-bersih{margin-top:12px;background:none;border:1px solid var(--border);border-radius:6px;padding:4px 12px;font-size:11px;color:var(--t2);cursor:pointer;font-family:var(--ui)}
.kosong-bersih:hover{border-color:var(--blue);color:var(--blue)}

/* Kerangka pemuatan (TAMPILAN §4) — kotak abu berdenyut selebar isi yang
   akan datang. Lihat kerangkaBaris() di app.js untuk alasan tingginya.

   `height:.9em` BUKAN angka rasa. Ia relatif terhadap huruf selnya sendiri,
   dan justru itu yang membuatnya aman: berapa pun ukuran huruf kolom itu,
   batangnya tetap lebih pendek daripada tinggi baris (~1.2em) yang
   ditempatinya, jadi ia tidak pernah bisa menumbuhkan barisnya. Tinggi tetap
   dalam piksel akan melampaui baris di kolom yang hurufnya kecil — dan
   kolom seperti itu ada tiga di Log Aktivitas.

   Denyutnya memakai `blink` yang sudah ada (titik hijau "live"), bukan
   keyframes baru yang isinya sama. Satu kosakata gerak, bukan dua yang
   perlahan menyimpang temponya. */
.kerangka{display:inline-block;height:.9em;border-radius:3px;background:var(--border);
  animation:blink 1.4s infinite}
/* Lebar batang. Namanya ukuran, bukan kolom, supaya satu kolom yang berpindah
   halaman tidak menyeret kelasnya sendiri ke mana-mana. */
.k-xs{width:34px}
.k-s{width:58px}
.k-m{width:92px}
.k-l{width:140px}
.k-xl{width:200px}
.k-penuh{width:100%}
.kerangka-blok{display:flex;flex-direction:column;gap:12px;padding:8px 0}
/* `.chip` & keempat variannya DIBUANG 21 Agustus 2026 — nol pemakai di
   keempat belas halaman. Sisa penyaring lama; yang dipakai sekarang `.fbtn`
   (dashboard) dan `.f-select` (Log Aktivitas). Enam kemunculan kata "chip"
   yang tersisa di app.js semuanya ada di dalam komentar. */
/* ST status badges — `white-space:nowrap`, alasannya di komentar `.freq` */
.st-up{display:inline-flex;align-items:center;gap:3px;background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.3);color:var(--green);border-radius:4px;padding:3px 8px;font-size:9px;font-weight:700;white-space:nowrap}
.st-down{display:inline-flex;align-items:center;gap:3px;background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:var(--red);border-radius:4px;padding:3px 8px;font-size:9px;font-weight:700;white-space:nowrap;animation:vioPulse 1.5s infinite}
.st-unknown{display:inline-flex;align-items:center;gap:3px;background:var(--bg-3);border:1px solid var(--border);color:var(--t3);border-radius:4px;padding:3px 8px;font-size:9px;font-weight:700;white-space:nowrap}
.st-dot{width:5px;height:5px;border-radius:50%;background:currentColor}

/* ── FILTER RENTANG WAKTU: input date/time ── */
.dt-input,.tm-input{color-scheme:dark}
.dt-input::-webkit-calendar-picker-indicator,
.tm-input::-webkit-calendar-picker-indicator{
  cursor:pointer;opacity:.6;border-radius:4px;padding:3px;margin-left:2px;
  transition:opacity .15s ease,transform .15s ease,background .15s ease;
}
.dt-input::-webkit-calendar-picker-indicator:hover,
.tm-input::-webkit-calendar-picker-indicator:hover{
  opacity:1;transform:scale(1.18);background:var(--bg-3);
}

/* ── Dropdown filter (menggantikan chip supaya lebih hemat tempat) ── */
.f-select{
  background:var(--bg-1);border:1px solid var(--border);border-radius:7px;
  padding:4px 8px;font-size:11px;font-weight:600;color:var(--t1);
  font-family:var(--ui);cursor:pointer;outline:none;appearance:none;
  -webkit-appearance:none;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat:no-repeat;background-position:right 6px center;background-size:14px;
  padding-right:24px;transition:border-color .15s
}
.f-select:hover,.f-select:focus{border-color:var(--blue)}

/* ── Kotak pencarian nama site di History ── */
.f-search-wrap{
  display:flex;align-items:center;gap:4px;background:var(--bg-1);
  border:1px solid var(--border);border-radius:7px;padding:4px 4px 8px;
  flex:1;min-width:180px;transition:border-color .15s
}
.f-search-wrap:focus-within{border-color:var(--blue)}

/* Ketiga dropdown filter History berbagi lebar sama rata supaya benar-benar
   sejajar, bukan sekadar sebaris. Diberi basis 150px agar teks pilihan
   terpanjang ("Station Online") masih terbaca sebelum dipangkas.

   Dilingkupi #historyFilter, TIDAK diterapkan ke `.f-select` secara umum:
   kelas yang sama dipakai pemilih rentang di halaman Grafik Tren, dan di sana
   ia berdampingan dengan kotak cari yang butuh sisa ruangnya. */
#historyFilter .f-select{flex:1 1 150px;min-width:0}
.f-search-wrap input{
  flex:1;background:none;border:none;outline:none;font-size:12px;
  color:var(--t1);font-family:var(--ui);min-width:0
}
.f-search-btn{
  background:var(--blue-permukaan);border:none;border-radius:5px;width:24px;height:24px;
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  color:var(--fg-aksen);flex-shrink:0;font-size:12px;transition:opacity .15s
}
.f-search-btn:hover{opacity:.85}
.dt-input::-webkit-calendar-picker-indicator:active,
.tm-input::-webkit-calendar-picker-indicator:active{
  transform:scale(.95);
}

/* ── LOG CARD ── */
/* Tinggi kartu diturunkan dari JUMLAH ENTRI, bukan sebaliknya.
   `99px` adalah perkiraan header dua baris; aturTinggiLog() di dashboard.html
   segera menggantinya dengan tinggi header yang benar-benar diukur.

   Yang penting: nilainya PASTI, bukan `auto`. Dengan `auto`, kartu ini
   meregang mengikuti isinya — dan di mode History isinya 200 baris, jadi
   kartunya tumbuh ~23.600px dan menyeret seluruh baris grid ikut memanjang.
   Itu bug yang dilaporkan 31 Juli 2026. Nilai pasti di sini membuat
   `.log-body{overflow-y:auto}` kembali bekerja: 200 baris digulung di dalam
   kartu, bukan memanjangkan halaman — dan tetap begitu walau JS gagal jalan. */
.log-card{background:var(--bg-1);border:1px solid var(--border);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;
          height:var(--log-h, calc(99px + var(--log-entries) * var(--lentry-h)))}
.log-hdr{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;row-gap:8px;column-gap:12px;flex-shrink:0}
/* Kolom flex supaya entri bisa menyerap sisa ruang — lihat .lentry di bawah. */
.log-body{flex:1;overflow-y:auto;min-height:0;display:flex;flex-direction:column}
.log-body::-webkit-scrollbar{width:3px}
.log-body::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
/* Tinggi satu entri log. Ditulis sebagai custom property, bukan angka
   langsung, karena dashboard.html perlu tahu angka yang SAMA untuk menghitung
   berapa entri yang muat di kartu (lihat jumlahEntriMuat()).

   Dulu jumlahnya dipatok 6 dengan alasan "pas persis dengan tinggi kartu".
   Itu benar selama headernya satu baris — dan berhenti benar begitu dropdown
   jenis log ditambahkan 31 Juli 2026: headernya membungkus jadi dua baris di
   layar sempit, ruang isi berkurang 42px, dan entri ke-6 terpotong separuh. */
/* `--log-entries` = berapa entri yang tampil di mode Live, DAN dasar tinggi
   kartunya (lihat aturTinggiLog di dashboard.html). Mengubah angka di sini
   otomatis mengubah tinggi kartunya — tidak ada angka kedua yang harus ikut
   disesuaikan. */
/* Ditulis di `:root`, bukan di `.log-card`. `.two` perlu membaca angka yang
   SAMA untuk tinggi barisnya, dan custom property hanya mengalir ke bawah —
   dari anak ke induk tidak terbaca. */
:root{--lentry-h:118px;--log-entries:5}
/* Tinggi Action Center di kolom kanan. Tetap, bukan mengikuti isi: daftarnya
   berubah tiap siklus polling, dan kartu yang tingginya ikut berubah menyeret
   seluruh baris grid tiap 30 detik. Isinya menggulung di dalam .act-list. */
:root{--act-h:236px}
/* `flex:1 0 var(--lentry-h)` — bukan tinggi tetap.
   jumlahEntriMuat() merender sebanyak entri UTUH yang muat, dan sisa
   pembagiannya (selalu < satu entri) dulu tertinggal sebagai ruang kosong di
   bawah. Sekarang sisa itu dibagi rata ke entri yang ada.

   `max-height` yang membatasinya justru bagian terpenting: tanpa itu, saat log
   baru berisi 2 entri di kartu setinggi 1000px, kedua entri akan menggembung
   jadi 500px masing-masing. Batas ini membuat entri hanya boleh menyerap
   sedikit — kalau memang datanya sedikit, kartunya memang tampil separuh
   kosong, dan itu jawaban yang jujur. */
.lentry{display:grid;grid-template-columns:30px 1fr;column-gap:8px;
        flex:1 0 var(--lentry-h);max-height:calc(var(--lentry-h) + 24px);
        padding:12px 16px;border-left:3px solid transparent;
        border-bottom:1px solid var(--border2);overflow:hidden}
.lentry:last-child{border-bottom:none}
.lentry:hover{background:rgba(255,255,255,.02)}
.lentry.violation{border-left-color:var(--red);background:rgba(239,68,68,.03)}
.lentry.ok{border-left-color:var(--green)}
.lentry.info{border-left-color:var(--blue)}
.lentry.warning{border-left-color:var(--amber);background:rgba(245,158,11,.03)}
.licon{grid-column:1;grid-row:1;width:30px;height:30px;border-radius:7px;background:var(--bg-2);border:1px solid var(--border2);display:flex;align-items:center;justify-content:center;font-size:13px;align-self:start}
.lcontent{grid-column:2;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:4px}
.ltop{display:flex;align-items:center;justify-content:space-between;gap:8px}
.ltop-tags{display:flex;align-items:center;gap:4px;min-width:0;overflow:hidden}
.ltop-tags span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* RF-REDESIGN — chip kejadian, dulu ditulis sebaris di renderLogRows().
   Warnanya diturunkan dari `.lentry.*` yang SUDAH mewarnai tepi kiri entri,
   jadi tidak ada daftar kedua yang bisa berbeda pendapat tentang peristiwa
   mana yang merah. Bawaannya tetap netral: peristiwa yang belum punya kelas
   warna harus tampil sebagai chip biasa, bukan tanpa chip. */
.lchip{font-size:9px;font-weight:700;padding:2px 4px;border-radius:3px;
  background:var(--bg-3);color:var(--t3);text-transform:uppercase;letter-spacing:.05em}
.lentry.violation .lchip{background:rgba(239,68,68,.15);color:var(--fg-chip-merah)}
.lentry.warning   .lchip{background:rgba(245,158,11,.15);color:var(--fg-chip-kuning)}
.lentry.ok        .lchip{background:rgba(34,197,94,.15);color:var(--fg-chip-hijau)}
.lentry.info      .lchip{background:rgba(59,130,246,.15);color:var(--fg-chip-biru)}
.ltime{font-family:var(--mono);font-size:9px;color:var(--t3);white-space:nowrap;flex-shrink:0}
.lname{font-size:12px;font-weight:600;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lchange{font-size:11px;color:var(--t2);line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lentry.violation .lchange{color:var(--fg-chip-merah)}
.lentry.warning .lchange{color:var(--fg-chip-kuning)}
.lmeta{font-size:10px;color:var(--t3);font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* ── KENDALA PER BTS (Dashboard) ──────────────────────────────────────────
   Peringkat, bukan daftar: yang dicari "BTS mana yang paling sering
   bermasalah", jadi tiap baris membawa batang pembanding terhadap BTS
   TERPARAH — angka 23 tidak berarti apa-apa sampai terlihat bahwa yang kedua
   cuma 4. Nama dipatok 34% dan dipotong dengan elipsis supaya batangnya mulai
   di garis yang sama untuk semua baris; nama BTS bisa sangat panjang, dan
   batang yang mulai di tempat berbeda tidak bisa dibandingkan sekilas. */
/* Baris kartu Dashboard — dipakai peringkat Kendala per BTS DAN daftar
   Aturan Zona. Keduanya memang benda yang sama: satu baris di dalam kartu,
   bergaris bawah, yang pertama dan terakhir dipangkas jarak luarnya supaya
   tidak ada ruang menganggur di tepi kartu.

   Dulu dua aturan terpisah yang berbeda BANTALAN 1px saja (7px vs 9px) —
   selisih yang tidak bisa dilihat sendirian, tapi cukup untuk membuat dua
   kartu yang bersebelahan di kolom yang sama tidak berbaris. Pembulatan
   jarak §2.2 (16 Agustus 2026) menyamakan angkanya, dan begitu angkanya
   sama tidak ada lagi alasan aturannya dua. */
.bts-baris,.zone-row{display:flex;align-items:center;gap:8px;padding:8px 0;
  border-bottom:1px solid var(--border2)}
.bts-baris:last-child,.zone-row:last-child{border-bottom:none;padding-bottom:0}
.bts-baris:first-child,.zone-row:first-child{padding-top:0}
.bts-nama{flex:0 0 34%;font-size:12px;font-weight:600;color:var(--t1);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bts-nama.kosong{color:var(--t3);font-weight:500;font-style:italic}
.bts-jumlah{font-family:var(--mono);font-size:12px;color:var(--t1);
  width:34px;text-align:right;flex-shrink:0}
.bts-bar{flex:1;height:8px;min-width:40px;background:var(--bg-2);
  border-radius:5px;overflow:hidden}
/* RF-REDESIGN — bawaannya merah REDUP; `.puncak` (tiga teratas) yang penuh.
   Arahnya sengaja begini dan bukan sebaliknya: kalau bawaannya penuh lalu
   sisanya diredupkan, satu baris yang lupa diberi kelas akan menyala merah
   dan berbohong. Baris yang lupa diberi `.puncak` cuma tampil redup. */
.bts-bar-isi{height:100%;border-radius:5px;background:rgba(239,68,68,.38)}
.bts-bar-isi.puncak{background:var(--red)}
.bts-sub{font-size:10px;color:var(--t3);white-space:nowrap;flex-shrink:0}
/* Kolom BTS — dipakai tabel Status Perangkat DAN Kelola Perangkat. */
.sel-bts{font-size:12px;color:var(--t2)}
/* Di Status Perangkat kolomnya berlebar tetap (`table-layout:fixed`), jadi
   nama BTS yang panjang akan dipatahkan sampai ke tengah kata seperti bug9.
   Di Kelola Perangkat tabelnya bisa digeser, jadi di sana ia boleh utuh. */
.tscroll .sel-bts{font-size:11px;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
/* Keterangan satu baris di dalam kartu (memuat / gagal / tidak ada apa-apa).
   Tiga keadaan yang dulu ditulis sebagai gaya inline di tiap tempatnya. */
.kartu-catatan{font-size:12px;color:var(--t3);padding:8px 0}
.kartu-catatan.gagal{color:var(--red)}
.kartu-catatan.aman{color:var(--green);padding:4px 0 8px}
/* Penyaring BTS di baris pencarian tabel Status Perangkat & Kelola Perangkat.
   Bentuknya disamakan dengan kotak pencarian di sebelahnya. */
.f-bts{background:var(--bg-1);border:1px solid var(--border);border-radius:6px;
  padding:4px 8px;font-size:11px;color:var(--t1);outline:none;
  font-family:var(--ui);max-width:190px;cursor:pointer}

/* ── ZONE TABLE ── */
/* Tampilan dasarnya pindah ke `.kartu-panel` — lihat catatan di sana. */
.zone-card .card-hdr{margin-bottom:8px}
.zone-icon{width:34px;height:34px;border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
.zone-icon.safe{background:rgba(34,197,94,.1)}
.zone-icon.danger{background:rgba(239,68,68,.1)}
.zone-icon.limited{background:rgba(245,158,11,.1)}
/* `.zone-info` DIHAPUS 21 Agustus 2026 — isinya cuma `flex:1`, yang
   sudah punya nama utilitas `.f-kolom`. Kerangka pemuatan kartu Zona
   ikut diubah; dua tempat, satu kelas. */
.zname{font-size:12px;font-weight:600;color:var(--t1)}
.zrange{font-family:var(--mono);font-size:10px;color:var(--t3);margin-top:1px}
.ztag{font-size:9px;font-weight:700;padding:3px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.05em;white-space:normal;line-height:1.3}
.ztag.a{background:rgba(34,197,94,.1);color:var(--green);border:1px solid rgba(34,197,94,.3)}
.ztag.d{background:rgba(239,68,68,.1);color:var(--red);border:1px solid rgba(239,68,68,.3)}
.ztag.w{background:rgba(245,158,11,.1);color:var(--amber);border:1px solid rgba(245,158,11,.3)}

/* ── LOGIN & THEME ── */
/* Latar mengikuti tema, tidak lagi dipatok biru gelap.
   Sebelumnya `rgba(6,11,20,.97)` di tema apa pun, jadi instalasi yang bawaannya
   terang atau hangat membuka layar gelap dulu lalu berkedip jadi terang begitu
   login berhasil. Layar pertama itu justru yang dilihat calon pembeli. */
#loginOverlay{position:fixed;inset:0;background:var(--bg-0);display:flex;align-items:center;justify-content:center;z-index:9999;backdrop-filter:blur(4px)}
#loginOverlay.hidden{display:none}
/* Dua sapuan warna aksen yang sangat tipis — supaya layar login tidak berupa
   satu bidang rata. Dibuat dari var(--blue)/var(--cyan) yang ikut berganti
   bersama pilihan aksen (termasuk warna kustom), bukan warna yang ditulis
   sendiri di sini; dan opacity dipakai alih-alih rgba() karena nilai variabel
   itu bisa berbentuk apa saja.
   pointer-events:none — lapisan ini menutupi seluruh layar, dan tanpa itu ia
   menelan klik ke kotak loginnya sendiri. */
#loginOverlay::before{content:'';position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(900px 480px at 50% -5%, var(--blue), transparent 68%),
             radial-gradient(760px 420px at 50% 105%, var(--cyan), transparent 70%);
  opacity:.14}

/* ── Overlay "memuat" saat memeriksa sesi ──────────────────────────────────
   Dipasang/dilepas oleh app.js (showAuthLoading/hideAuthLoading). Tampil
   selama /api/me diperiksa, MENGGANTIKAN kotak login yang dulu berkedip di
   tiap pindah halaman. z-index di atas #loginOverlay supaya kalau keduanya
   sempat aktif bersamaan, yang terlihat adalah loading — bukan kotak login.
   Pakai var(--bg-0) agar ikut tema terang/gelap. */
#authLoading{position:fixed;inset:0;background:var(--bg-0);display:flex;align-items:center;justify-content:center;z-index:10000}
.auth-loading-box{display:flex;flex-direction:column;align-items:center;gap:12px}
.auth-spinner{width:34px;height:34px;border:3px solid var(--border);border-top-color:var(--blue);border-radius:50%;animation:auth-spin .8s linear infinite}
@keyframes auth-spin{to{transform:rotate(360deg)}}
.auth-loading-text{font-size:12px;color:var(--t3);font-family:var(--ui)}
/* Hormati preferensi pengguna yang sensitif terhadap animasi */
/* position+z-index: ::before di atas adalah elemen terposisikan, dan tanpa ini
   ia digambar DI ATAS kotak login — kotaknya masih terlihat (opacity .14) tapi
   tidak bisa diklik. */
/* Bayangannya lewat variabel, bukan `rgba(0,0,0,.5)` yang dulu ditulis di
   sini. Nilai itu dirancang untuk latar gelap — hitam di atas hitam nyaris
   tidak terlihat. Begitu latar login ikut tema (§1.3), ia jadi gumpalan abu
   yang mencolok di tema terang & hangat; terlihat di bug/tampilan-login-1.png.
   Tema terang menimpanya dengan bayangan yang lebih tipis dan berwarna. */
.login-box{position:relative;z-index:1;background:var(--bg-1);border:1px solid var(--border);border-radius:16px;padding:36px 32px;width:100%;max-width:360px;box-shadow:var(--bayang-login,0 24px 64px rgba(0,0,0,.5))}
.login-logo{display:flex;align-items:center;gap:12px;margin-bottom:24px}
.login-logo-icon{width:42px;height:42px;background:linear-gradient(135deg,var(--blue),var(--cyan));border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--fg-aksen)}
.login-logo-icon .ic{width:22px;height:22px}
.login-title{font-size:16px;font-weight:700;color:var(--t1)}
.login-sub{font-size:11px;color:var(--t3);margin-top:2px}
/* Baris keterangan: apa yang dipantau instalasi ini. Di bawah logo, bukan di
   dalam blok judul, supaya ia boleh selebar kotaknya walau logonya di kiri.
   Kosong berarti hilang sama sekali — display diatur app.js, dan margin di
   sini tidak ikut tertinggal karena elemennya sendiri yang disembunyikan. */
.login-info{margin:0 0 20px;padding:8px 12px;background:var(--bg-2);border:1px solid var(--border2);border-radius:9px;
  font-size:10px;line-height:1.5;color:var(--t2);text-align:center}
.login-info b{color:var(--t1);font-family:var(--mono);font-weight:600}
.login-field{margin-bottom:12px}
.login-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--t3);display:block;margin-bottom:4px}
.login-input{width:100%;background:var(--bg-2);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--t1);font-family:var(--ui);outline:none;transition:border-color .15s}
.login-input:focus{border-color:var(--blue)}
.login-btn{width:100%;padding:8px;background:var(--blue-permukaan);border:none;border-radius:8px;color:var(--fg-aksen);font-size:13px;font-weight:700;font-family:var(--ui);cursor:pointer;margin-top:4px;transition:background .15s}
/* brightness(), bukan biru gelap yang ditulis tangan: tombolnya memakai
   var(--blue) yang ikut pilihan warna aksen, jadi hover yang dipatok satu
   warna akan melompat ke biru begitu aksennya ungu atau magenta. */
.login-btn:hover{filter:brightness(.88)}
.login-err{font-size:12px;color:var(--red);margin-top:8px;text-align:center;min-height:16px}

/* ── Tampil/sembunyi password (15 Agustus 2026) ───────────────────────────
   Diminta pengguna. Password diketik tanpa bisa dilihat, di layar yang juga
   dibuka teknisi dari HP di lapangan — dan salah ketik satu huruf tidak bisa
   dibedakan dari salah password sampai tombol Masuk ditekan.

   `padding-right` pada kotaknya, bukan lebar yang dikurangi: tombolnya
   melayang DI ATAS kotak, jadi kotaknya tetap selebar penuh dan teks yang
   panjang berhenti tepat sebelum ikonnya alih-alih di baliknya. */
.pw-bungkus{position:relative;display:block}
.pw-bungkus .login-input{padding-right:40px}
.pw-mata{
  position:absolute;top:50%;right:6px;transform:translateY(-50%);
  display:flex;align-items:center;justify-content:center;
  width:30px;height:30px;padding:0;
  background:none;border:none;border-radius:6px;
  color:var(--t3);cursor:pointer;transition:color .15s
}
.pw-mata:hover{color:var(--t1)}
/* Cincin fokus TIDAK dihapus. Tombol ini duduk di dalam form login, dan
   satu-satunya cara memakainya lewat papan ketik adalah melihat di mana
   fokusnya berada. */
.pw-mata:focus-visible{outline:2px solid var(--blue);outline-offset:1px;color:var(--t1)}

/* ── "Ingat saya" ─────────────────────────────────────────────────────────
   Seluruh baris jadi label, jadi teksnya ikut bisa diklik — kotak centang
   14px adalah sasaran yang terlalu kecil di layar sentuh.

   Labelnya sengaja SATU baris tanpa keterangan tambahan (diminta pengguna 15
   Agustus 2026). Berapa lama sesinya bertahan pindah ke `title` — layar login
   adalah layar yang dilewati orang, bukan dibaca; kalimat penjelas di sana
   menambah tinggi kotak tanpa pernah benar-benar dibaca siapa pun. */
.login-ingat{
  display:flex;align-items:center;gap:8px;cursor:pointer;
  margin:2px 0 4px;user-select:none
}
.login-ingat input{
  width:14px;height:14px;flex-shrink:0;
  accent-color:var(--blue);cursor:pointer
}
.login-ingat span{font-size:12px;color:var(--t2);line-height:1.4}
/* `.theme-btn` & `.logout-btn` DIBUANG 21 Agustus 2026 — nol pemakai.
   Keduanya tombol lepas di kepala halaman sebelum 19 Agustus; sejak itu
   tema pindah ke halaman Tampilan dan "Keluar" jadi butir NAV_PROFIL. */

/* ── NAV DROPDOWN MENU ── */
/* `.nav-menu` DIBUANG 19 Agustus 2026 bersama elemennya — menu pindah ke rel
   kiri (`.rail`). Aturan yang ditinggalkan untuk elemen yang tidak pernah
   muncul bukan sekadar menganggur: `margin-left:auto` di sini adalah
   pendorong yang dulu menaruh jam & profil di kanan, dan membiarkannya
   membuat orang berikutnya mengira pendorong itu masih bekerja. Sekarang
   `.hdr-r` mendorong dirinya sendiri. */
.nav-item{position:relative}
.nav-btn{background:none;border:none;color:var(--t2);padding:8px 12px;font-size:13px;font-weight:600;cursor:pointer;border-radius:8px;display:flex;align-items:center;gap:8px;transition:all .15s;font-family:var(--ui);white-space:nowrap;text-decoration:none}
.nav-btn:hover{background:var(--bg-3);color:var(--t1)}
.nav-btn.active{background:rgba(59,130,246,.12);color:var(--blue)}
.nav-btn .ic{width:17px;height:17px;flex-shrink:0}
/* `.nav-caret` DIBUANG 21 Agustus 2026 — nol pemakai. Panah dropdown menu
   mendatar lama; menu profil sekarang dibuka tombol ikon tanpa panah. */
.nav-item.open .nav-btn{background:var(--bg-3);color:var(--t1)}
.nav-dropdown{position:absolute;top:calc(100% + 8px);left:0;min-width:210px;background:var(--bg-1);
  border:1px solid var(--border);border-radius:10px;box-shadow:0 16px 40px rgba(0,0,0,.45);
  display:none;z-index:2000;overflow:hidden;padding:4px}
.nav-item.open .nav-dropdown{display:block}
.nav-dropdown a, .nav-dropdown button.nav-opt{
  display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:none;
  padding:8px 12px;font-size:12px;color:var(--t1);cursor:pointer;border-radius:7px;
  font-family:var(--ui);text-decoration:none;transition:background .1s}
.nav-dropdown a:hover, .nav-dropdown button.nav-opt:hover{background:var(--bg-3)}
.nav-dropdown .ic{width:16px;height:16px;flex-shrink:0;color:var(--t2)}
.nav-dropdown a:hover .ic, .nav-dropdown button.nav-opt:hover .ic{color:var(--t1)}
.nav-dropdown button.nav-opt.danger{color:var(--red)}
.nav-dropdown button.nav-opt.danger .ic{color:var(--red)}
#themeIcon{display:inline-flex;align-items:center}
.nav-dropdown button.nav-opt hr, .nav-dropdown hr{border:none;border-top:1px solid var(--border2);margin:4px 4px}
.nav-profile-head{padding:8px 12px 8px;border-bottom:1px solid var(--border2);margin-bottom:4px}
.nav-profile-name{font-size:13px;font-weight:700;color:var(--t1)}
.nav-profile-role{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;
  padding:2px 8px;border-radius:10px;background:var(--bg-3);color:var(--t3);display:inline-block;margin-top:4px}
/* Kaki menu profil: versi yang sedang berjalan. Setenang mungkin — ia jawaban
   untuk pertanyaan yang jarang ditanyakan, bukan menu. */
.nav-versi{font-size:9px;color:var(--t3);padding:4px 12px 2px;border-top:1px solid var(--border2);margin-top:4px}
/* ── Halaman Detail Perangkat — mode LIST (tabel gaya network monitoring) ── */
.det-toolbar{
  display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap
}
.det-search{
  flex:1;min-width:220px;background:var(--bg-1);border:1px solid var(--border);
  border-radius:8px;padding:2px 4px 2px 12px;display:flex;align-items:center;transition:border-color .15s
}
.det-search:focus-within{border-color:var(--blue)}
.det-search input{
  flex:1;background:none;border:none;outline:none;font-size:13px;color:var(--t1);
  font-family:var(--ui);padding:8px 4px;min-width:0
}
.det-refresh{
  background:none;border:1px solid var(--border);border-radius:8px;padding:8px 12px;
  font-size:12px;font-weight:600;color:var(--t2);cursor:pointer;font-family:var(--ui);
  display:inline-flex;align-items:center;gap:4px;transition:all .15s;white-space:nowrap
}
.det-refresh:hover{border-color:var(--blue);color:var(--blue)}
.det-refresh svg{opacity:.75}

.det-tablewrap{
  background:var(--bg-1);border:1px solid var(--border);border-radius:10px;overflow:hidden
}

.det-tablewrap table{width:100%;border-collapse:collapse}
/* Kepala kolom DITENANGKAN 19 Agustus 2026, mengikuti Referensi 2 (UISP).
   Sebelumnya 9px HURUF BESAR tebal berjarak — bentuk yang berteriak lebih
   keras daripada isinya sendiri, padahal judul kolom dibaca sekali lalu tidak
   pernah dilihat lagi. Sekarang 11px huruf biasa, tebal sedang: masih jelas
   sebagai judul karena warnanya lebih redup dan latarnya berbeda, tapi mata
   tidak lagi tertarik ke sana tiap kali memindai daftar.
   Berlaku untuk ketiga halaman yang memakai cangkang ini (Detail Perangkat,
   Kelola Perangkat, Kelola BTS) — menenangkan satu dan meninggalkan dua
   membuat ketiganya terlihat seperti tiga aplikasi. */
.det-tablewrap thead th{
  background:var(--bg-2);text-align:left;padding:12px;font-size:11px;
  font-weight:600;letter-spacing:.01em;color:var(--t3);
  border-bottom:1px solid var(--border);white-space:nowrap
}
.det-tablewrap tbody td{
  padding:12px;font-size:13px;color:var(--t1);border-bottom:1px solid var(--border2);
  /* `text-align:left` di sini BUKAN gaya bawaan yang boleh dihapus.
     Aturan global di baris 121 menyetel SEMUA `td` jadi rata tengah, dan
     `.det-tablewrap thead th` di atas menyetel judulnya jadi rata kiri.
     Tanpa baris ini keduanya lepas: judul di kiri, isi di tengah.

     Bentuk kesalahannya menipu, dan itu yang membuatnya bertahan lama —
     besar simpangannya bergantung pada lebar kolom dikurangi lebar isinya.
     Terukur dari tangkapan layar 31 Juli 2026: kolom Lokasi meleset 143px
     karena kolomnya lebar dan isinya pendek, sementara IP/Frekuensi/Sinyal
     cuma ~13px sehingga terbaca sebagai "kurang rapi sedikit", bukan
     sebagai perataan yang salah.

     Kolom Status/Aktif dan Aksi sengaja menang atas aturan ini lewat kelas
     `.ta-c`/`.ta-r` — judul dan selnya memakai kelas yang sama, jadi keduanya
     mustahil menyimpang. Kelas itu membawa selektor `.det-tablewrap` sendiri
     supaya kekhususannya sepadan dengan aturan ini; sebelum 7 Agustus 2026
     keempatnya ditulis inline justru karena kelas biasa kalah di sini. */
  text-align:left;
  vertical-align:middle
}
.det-tablewrap tbody tr:last-child td{border-bottom:none}
.det-tablewrap tbody tr:hover{background:var(--bg-2)}

/* ══ MODUL TABEL — Aksi yang lengket, lebar yang bisa diseret, menu Kolom ══
   Diminta pengguna 26 Agustus 2026 (butir 3, 4, 5). Pasangannya di app.js
   (cari "MODUL TABEL"); di sini hanya yang tidak bisa dikerjakan JS dengan
   murah. Yang MENYEMBUNYIKAN kolom bukan aturan di berkas ini melainkan
   `<style id="rf-gaya-tabel">` yang disuntik modul — indeks `nth-child`-nya
   bergantung pada kolom mana yang dipilih orangnya, jadi ia tidak bisa
   ditulis di sini. */

/* ── Kolom Aksi tidak ikut hanyut ──
   Tabel di aplikasi ini lebih lebar daripada layar (900–1185px `min-width`),
   jadi kolom Aksi — satu-satunya kolom yang diklik, bukan dibaca — justru
   yang paling sering berada di luar layar. Keluhan aslinya sudah pernah
   ditulis untuk hal lain: "kolom model tidak muat jadi perlu digeser, aksi
   tidak terbaca" (19 Agustus 2026).

   Kelas `.tbl-aksi-lengket` dipasang app.js, BUKAN ditulis di halaman: yang
   menentukan bukan "halaman ini punya tombol" melainkan "tabel ini punya
   kolom bernama aksi", dan itu baru terbaca sesudah `<thead>`-nya dibaca. */
.tbl-aksi-lengket > table > thead > tr > th:last-child,
.tbl-aksi-lengket > table > tbody > tr > td:last-child:not([colspan]){
  position:sticky;right:0;
  /* Latar WAJIB tidak tembus pandang: sel yang melayang di atas sel lain
     tanpa latar sendiri akan menumpuk dua tulisan di tempat yang sama. */
  background:var(--bg-1)
}
.tbl-aksi-lengket > table > thead > tr > th:last-child{background:var(--bg-2)}
/* Sel biasa z-index 1, kepala kolom 2. Keduanya di atas sel yang lewat di
   bawahnya, dan kepala tetap di atas sel — `thead` sendiri sudah lebih dulu
   di urutan sumber, jadi tanpa angka ini sel badan yang menang. */
.tbl-aksi-lengket > table > tbody > tr > td:last-child:not([colspan]){z-index:1}
.tbl-aksi-lengket > table > thead > tr > th:last-child{z-index:2}
/* Sorotan baris hilang di sel yang latarnya sendiri — dipasang ulang, kalau
   tidak kolom Aksi jadi satu-satunya sel yang tidak ikut menyala saat
   barisnya disorot, dan itu terbaca sebagai sel yang bukan bagian barisnya. */
.tbl-aksi-lengket > table > tbody > tr:hover > td:last-child{background:var(--bg-2)}
/* Bayangan tipis di tepi kiri = tanda bahwa ada isi yang lewat di baliknya.
   Tanpa ini kolom yang diam saat sisanya bergeser terbaca sebagai tabel yang
   macet, bukan sebagai kolom yang sengaja ditahan.

   `box-shadow`, bukan `::before` — bayangan digambar DI LUAR kotaknya, jadi
   ia tetap terlihat setelah `.tbl-lebar-tetap` memberi tiap sel
   `overflow:hidden`. Pseudo-elemen di posisi yang sama akan terpotong justru
   di dua tabel yang lebarnya bisa diseret. */
.tbl-aksi-lengket > table > thead > tr > th:last-child,
.tbl-aksi-lengket > table > tbody > tr > td:last-child:not([colspan]){
  box-shadow:-8px 0 8px -8px rgba(0,0,0,.45)
}

/* ── Lebar yang bisa diseret ──
   `table-layout:fixed` HANYA dipasang sesudah orangnya benar-benar menyeret
   (atau membuka halaman yang lebarnya sudah pernah disimpan). Selama belum,
   tabelnya tetap `auto` — jadi halaman yang tidak disentuh tampil persis
   seperti sebelum modul ini ada. Kelasnya dipasang app.js. */
/* Pasangannya `min-width:100%` TIDAK ditulis di sini melainkan sebagai gaya
   sebaris di app.js, dan itu bukan kelalaian: `.det-tablewrap table` dan
   `.mtable table` di berkas ini berkekhususan (0,1,1), jadi sebuah kelas
   (0,1,0) akan kalah dan tidak pernah berlaku — jaring pengaman yang diam
   lebih buruk daripada tidak ada jaring sama sekali. Lihat `_tabelBekukan()`. */
.tbl-lebar-tetap{table-layout:fixed}
/* Dengan lebar tetap, isi yang lebih panjang daripada kolomnya TIDAK
   melebarkan kolom — ia meluber ke sel sebelahnya. Dipotong, dan dipotong
   dengan elipsis supaya "…" berarti "ada lanjutannya", bukan "sisanya
   hilang". Isi lengkapnya tetap ada di `title` tiap sel. */
.tbl-lebar-tetap > thead > tr > th,
.tbl-lebar-tetap > tbody > tr > td{overflow:hidden;text-overflow:ellipsis}

/* Pegangan seret: selebar 9px, menempel di tepi kanan judul kolom. Bukan
   `border-right` yang diberi kursor — sasaran seukuran 1px praktis mustahil
   dikenai, dan yang gagal mengenainya akan mengira fiturnya tidak ada. */
/* Tanpa awalan `.det-tablewrap`: sejak seret-lebar dinyalakan di kedelapan
   tabel (27 Agustus 2026) pembungkusnya tidak lagi cuma kartu itu — Status
   Perangkat memakai `.tscroll`, Log Aktivitas `.akt-tabel`. Pegangannya
   `position:absolute`, jadi judul kolom yang tidak `relative` akan melempar
   pegangannya ke sudut halaman, bukan ke tepi kolomnya. */
th.kol-bisa-seret{position:relative}
.kol-seret{
  position:absolute;top:0;right:0;width:9px;height:100%;
  cursor:col-resize;user-select:none;
  /* `touch-action:none` supaya di layar sentuh seretan mendatar dikirim ke
     pointermove, bukan ditelan browser jadi gulungan halaman. */
  touch-action:none;z-index:3
}
.kol-seret::after{
  content:'';position:absolute;top:25%;bottom:25%;right:4px;width:1px;
  background:var(--border2)
}
.kol-seret:hover::after{background:var(--blue);width:2px;right:3px}
/* Selama menyeret, kursor col-resize berlaku di SELURUH halaman: seretan
   yang keluar dari pegangan selebar 9px tidak boleh terlihat seperti sudah
   dilepas. Pemilihan teks dimatikan karena menyeret di atas judul kolom akan
   menyorotnya biru. */
body.sedang-seret-kolom{cursor:col-resize;user-select:none}

/* ── Menu "Kolom" ──
   Bentuknya mengikuti `.nav-dropdown` yang sudah ada, tapi kelas sendiri:
   yang itu menu navigasi (tautan, satu kolom, tutup sesudah diklik), yang ini
   daftar centang yang memang ditinggal terbuka selagi beberapa kolom
   dimatikan berturut-turut. */
.kol-menu{position:relative;display:inline-flex}
.kol-panel{
  display:none;position:absolute;top:calc(100% + 8px);right:0;min-width:220px;
  background:var(--bg-1);border:1px solid var(--border);border-radius:10px;
  box-shadow:0 16px 40px rgba(0,0,0,.45);padding:4px;z-index:2000;
  max-height:60vh;overflow-y:auto
}
.kol-menu.buka .kol-panel{display:block}
.kol-kepala{
  font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.09em;
  color:var(--t3);padding:8px 8px 4px
}
.kol-opsi{
  display:flex;align-items:center;gap:8px;padding:8px;border-radius:8px;
  font-size:12px;color:var(--t1);cursor:pointer
}
.kol-opsi:hover{background:var(--bg-3)}
.kol-opsi input{cursor:pointer;flex-shrink:0}
.kol-kaki{border-top:1px solid var(--border2);margin-top:4px;padding-top:8px}
.kol-catatan{font-size:10px;color:var(--t3);padding:0 8px 8px;line-height:1.5}
.kol-reset{
  width:100%;background:none;border:1px solid var(--border);border-radius:8px;
  padding:8px;font-size:11px;font-weight:600;color:var(--t2);cursor:pointer;
  font-family:var(--ui)
}
.kol-reset:hover{border-color:var(--blue);color:var(--blue)}
/* Penolakan "setidaknya satu kolom harus tampil". Kuning, bukan merah: tidak
   ada yang rusak, permintaannya saja yang tidak bisa dipenuhi. */
.kol-tolak{
  margin:4px;padding:8px;border-radius:8px;background:rgba(245,158,11,.10);
  color:var(--amber);font-size:11px;line-height:1.5
}
/* Angka "7/9" di tombolnya — satu-satunya tanda bahwa ada kolom yang sedang
   disembunyikan. Tanpa ini, tabel yang kekurangan kolom terbaca sebagai
   kehilangan data, bukan sebagai setelan yang pernah dipilih sendiri. */
.kol-hitung{
  font-family:var(--mono);font-size:10px;background:var(--bg-3);
  border-radius:4px;padding:1px 4px;color:var(--t2)
}
/* Kelola Akun tidak punya toolbar — daftarnya menempel langsung di bawah form
   "Tambah User Baru". Satu baris tersendiri untuk tombol Kolom, dengan jarak
   yang sama dengan bantalan kartu di atasnya. */
.usr-alat{padding:12px 20px 0;justify-content:flex-end}

/* ── Pangkal baris: titik status (Referensi 2) ─────────────────────────────
   Ukurannya 8px dan itu hasil pertimbangan, bukan selera: di bawah 6px ia
   tidak terbaca sebagai warna melainkan sebagai kotoran layar, di atas 10px
   ia menarik perhatian lebih besar daripada nama perangkatnya sendiri.
   `flex-shrink:0` supaya ia tidak ikut memipih saat nama perangkat panjang —
   titik lonjong terbaca sebagai bentuk lain, bukan sebagai titik yang sempit. */
.titik-status{width:8px;height:8px;border-radius:50%;flex-shrink:0}
/* Susunan sel kolom Nama memakai `.baris-isi` yang SUDAH ADA — utilitas
   "sederet elemen sejajar berjarak sama" yang dipakai tiga halaman lain.
   Kelas tersendiri untuk ini sempat ditulis lalu dibuang: deklarasinya sama
   persis, dan test_gaya_bersama menangkapnya. Akibatnya jaraknya jadi milik
   bersama — kalau ada yang mengubahnya demi halaman lain, judul kolom Nama
   ikut melenceng. Itu tidak dibiarkan menggantung: test_detail_table_align
   membaca gap dari kelas ini juga, jadi perubahannya gagal di sana. */
/* Model dilaporkan radio dan panjangnya beragam ("PowerBeam M5 400",
   "airFiber 5XHD"). `nowrap` supaya ia tidak pecah dua baris dan meninggikan
   seluruh baris tabel demi satu sel. */
.sel-model{white-space:nowrap;color:var(--t2)}

/* ── Judul kolom sejajar dengan ISI kolomnya ───────────────────────────────
   Judul dan sel sama-sama berbantalan 12px, jadi kolom yang isinya teks polos
   memang sudah lurus. Tiga kolom tidak: isinya didahului (atau diakhiri)
   elemen lain, sehingga teks yang terlihat mulai lebih ke dalam sementara
   judulnya tetap menempel di tepi.

   Angka di bawah BUKAN kira-kira — diturunkan dari ukuran elemen yang
   menggeser isinya, dan ditulis sebagai penjumlahan supaya asalnya terbaca:

     Nama     ikon 14px + gap 8px           → isi mulai 22px lebih dalam
     Kategori border badge 1px + padding 8px → isi mulai 9px lebih dalam
     Aksi     padding dalam tombol 12px      → teks berakhir 12px lebih masuk

   Ikon 14px SENGAJA tidak ikut pembulatan §2.2 (21 Agustus 2026): ia bukan
   jarak melainkan UKURAN elemen, dan angkanya ditentukan detail.html —
   `ikon(ikonModel(...), 14)`. Membulatkannya di sini membuat penjumlahan ini
   berhenti cocok dengan lebar yang sungguhan, dan judulnya melenceng 2px.

   `tests/test_detail_table_align.py` membandingkan angka ini dengan ukuran
   aslinya di detail.html dan style.css. Kalau ikonnya diganti ukuran atau
   bantalan badge diubah, judulnya melenceng lagi tanpa ada yang memberi
   tahu — uji itu yang memberi tahu. */
.det-tablewrap thead th.th-nama    {padding-left:calc(12px + 8px + 8px + 14px + 8px)}
/* `.th-kategori` sempat DICABUT 19 Agustus 2026 — dan itu keliru separuh.
   Yang kehilangan kolom Kategori cuma **Detail Perangkat** (jadi Model, teks
   polos, memang lurus dengan bantalan sel biasa). **Kelola Perangkat** masih
   punya kolom Tipe berisi `.cat-badge`, dan `<th class="th-kategori">` di
   sana ikut kehilangan penyesuaiannya: judulnya mulai di 14px sementara teks
   badge di bawahnya mulai di 25px (14 bantalan sel + 1 garis + 10 bantalan
   badge). Satu-satunya kolom yang tidak lurus di tabel sembilan kolom itu —
   dikembalikan 21 Agustus 2026.

   Dipagari `.mtable-dev` supaya cabutan yang benar tetap berlaku: aturan ini
   HANYA untuk tabel yang memang punya kolom badge, bukan untuk semua tabel
   yang memakai cangkang `.det-tablewrap`. Penyesuaian yang tertinggal di
   kolom yang sudah tidak ada akan menjorokkan judul kolom LAIN sebesar
   bantalan badge yang tidak ada lagi di sana — itu sebab pencabutannya, dan
   ia tetap benar. */
.mtable-dev thead th.th-kategori    {padding-left:calc(12px + 1px + 8px)}
.det-tablewrap thead th.th-aksi    {padding-right:calc(12px + 12px)}

/* ── Pembungkus tabel yang boleh digeser ke samping ────────────────────────
   Dipakai halaman Kelola Perangkat, Kelola User, dan Riwayat Login. Ketiganya
   dulu memakai `<div style="overflow-y:auto">` saja — tanpa sumbu X, jadi di
   HP tabelnya TIDAK digeser melainkan DIPERAS: 8 kolom dibagi rata ke ~390px
   berarti ~48px per kolom, dan isinya jadi tumpukan kata satu huruf.

   Lebar minimumnya diatur tiap halaman lewat `--tmin`, karena jumlah kolomnya
   berbeda-beda. Nilainya ditaruh di tempat pemakaian supaya yang menambah
   kolom melihat angkanya di baris yang sama, bukan di berkas lain.

   Baik `overflow-x` maupun `min-width` sengaja berlaku di SEMUA lebar, tanpa
   media query. `min-width` sifatnya membatasi diri: selama wadahnya lebih
   lebar dari `--tmin`, aturan ini tidak berpengaruh sama sekali, jadi desktop
   tidak berubah dan tablet sempit ikut tertolong tanpa ambang tersendiri.

   Mengurungnya di `@media(max-width:640px)` justru berbahaya, dan itu
   kesalahan rancangan pertama saya di sini: pada layar 700px `min-width` mati
   sementara tabelnya tetap tidak muat, sehingga kolomnya diperas persis
   seperti sebelum perbaikan — dan untuk `.det-tablewrap` yang `overflow:hidden`
   hasilnya lebih buruk lagi: kolom terakhir TERPOTONG dan tidak bisa dicapai
   sama sekali, bukan sekadar sempit. */
.mtable{overflow-x:auto;-webkit-overflow-scrolling:touch}
.mtable table{min-width:var(--tmin,720px)}

/* Tabel perangkat di dashboard: JANGAN diperas jadi kolom-kolom kecil yang
   tulisannya numpuk & tidak kebaca — kasih lebar minimum, wrapper .tscroll
   sudah overflow-x:auto sejak awal, jadi otomatis jadi scroll ke samping
   begitu min-width ini aktif.

   Dipindahkan keluar dari @media(max-width:640px) 31 Juli 2026, dengan alasan
   yang sama seperti .mtable di atas: di 641–780px tabel 9 kolom ini tetap
   tidak muat, tapi aturannya sudah mati — jadi justru diperas persis seperti
   sebelum ada aturan ini.

   780px → 1080px (7 Agustus 2026, laporan bug9 & bug10). Angka 780 disetel
   saat tabelnya masih 9 kolom; kolom CPU menjadikannya 10. Ini kekeliruan
   yang SAMA dengan `.det-tablewrap` di bawah (680→900 karena kolom itu juga),
   hanya tabel ini yang saat itu terlewat.

   Kenapa lompatannya sebesar ini, padahal cuma tambah satu kolom: tabel ini
   `table-layout:fixed` dengan lebar per kolom dalam PERSEN (lihat <colgroup>
   di dashboard.html). Pada tata letak itu lebar kolom TIDAK pernah mengikuti
   isinya — kolom yang kurang lebar tidak melebar, isinya yang dipatahkan.
   Jadi lebar minimumnya harus dihitung dari kolom yang paling boros
   dibanding jatah persennya, bukan dari jumlah kolom. Di sini itu kolom
   Status: badge "PELANGGARAN" butuh 121px, dan 9% dari 780px hanya 70px.
   Persentasenya sendiri ikut ditata ulang di dashboard.html supaya tiap
   kolom kebagian sesuai isi terpanjangnya; dua-duanya harus cocok, mengubah
   salah satu saja tidak menyelesaikan apa pun.

   1080 = jumlah kebutuhan kesepuluh kolom (959px, diukur dari berkas font di
   web/fonts/ — bukan dikira-kira) + margin ~12% supaya tiap kolom punya sisa
   9–15px. Margin itu ada bukan demi rapi: panah "↔" di kolom Sinyal/LAN/CPU
   TIDAK ada di keempat subset font yang di-bundel, jadi ia dirender font
   sistem yang lebarnya berbeda-beda antar-platform. Sama untuk "✓" di badge
   Aman. Tanpa margin, tabel yang pas di sini bisa pecah di HP orang lain.

   Konsekuensinya di HP 375px geserannya jadi ~2,9 layar (dari ~2,1). Itu
   ditempuh sadar: kolom yang bisa digeser masih terbaca, kolom yang diperas
   tidak — dan ini pertukaran yang sama yang sudah diterima di `.mtable-dev`
   (880px) dan `.det-tablewrap` (900px). */
.tscroll table{min-width:1185px}

/* Detail Perangkat: `overflow:hidden` di atas dipasang demi sudut membulat,
   dan itu membuatnya satu-satunya tabel yang tidak bisa digeser sama sekali —
   kolomnya hanya bisa menyempit sampai tidak terbaca. `auto` tetap memotong
   di sudut, jadi bentuk kartunya tidak berubah. */
.det-tablewrap{overflow-x:auto;overflow-y:visible}
/* 680px sudah tidak cukup sejak kolom CPU ditambahkan (7 Agustus 2026):
   sebelas kolom dibagi 680px berarti ~62px per kolom, dan pasangan angka
   "AP ↔ ST" di kolom Sinyal/LAN/CPU patah di tengah panahnya. Kelola
   Perangkat punya `--tmin` sendiri yang lebih besar dan tidak terpengaruh. */
.det-tablewrap table{min-width:900px}

/* ── Dropdown menu header di layar sempit ──────────────────────────────────
   `.nav-dropdown` memakai `left:0` relatif tombolnya sendiri dengan
   `min-width:210px`. Untuk tombol yang letaknya agak ke kanan, 210px itu
   menjulur ke luar tepi layar — dan `max-width:calc(100vw - 24px)` tidak
   menolong, karena yang salah bukan lebarnya melainkan titik mulainya.
   Hanya Setting & Profile yang dulu ditambal dengan `right:0`; Devices dan
   Tools tidak, dan justru dua itu yang terpotong.

   `position:static` pada .nav-item membuat dropdown berpegang ke <header>
   (yang `position:sticky`, jadi sah sebagai jangkar) alih-alih ke tombolnya.
   Hasilnya satu perilaku untuk keempat menu: melebar penuh tepat di bawah
   header, tidak mungkin keluar layar dari sisi mana pun.

   Ambangnya 768px, bukan 640px seperti blok mobile di bawah: pada 700px
   kelima tombol nav (±550px) plus logo dan badge sudah melebihi lebar layar,
   jadi masalah yang sama sudah muncul sebelum tata letak HP aktif. */
@media(max-width:768px){
  .nav-item{position:static}
  .nav-dropdown{left:12px;right:12px;min-width:0;max-width:none;
                top:calc(100% + 6px)}
  #navSetting .nav-dropdown,
  #navProfile .nav-dropdown{left:12px;right:12px}
}

/* Badge status berbentuk pill dengan titik warna */
.dot-badge{
  display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;
  padding:4px 8px;border-radius:20px;letter-spacing:.02em;white-space:nowrap
}
.dot-badge.up{  background:rgba(34,197,94,.13); color:var(--green); border:1px solid rgba(34,197,94,.35)}
.dot-badge.down{background:rgba(100,116,139,.15);color:var(--t3);  border:1px solid rgba(100,116,139,.4)}
.dot-badge.vio{ background:rgba(239,68,68,.13); color:var(--red);   border:1px solid rgba(239,68,68,.35)}
.dot-badge.warn{background:rgba(245,158,11,.13);color:var(--amber); border:1px solid rgba(245,158,11,.35)}

/* Badge kategori (Ubiquiti / MikroTik) */
.cat-badge{
  display:inline-block;font-size:10px;font-weight:700;padding:3px 8px;
  border:1px solid var(--border);border-radius:20px;letter-spacing:.03em;
  text-transform:uppercase;white-space:nowrap
}

/* Tombol aksi kecil (Detail / Tren) di kolom Aksi — gaya bordered-button
   jelas, warna beda per aksi supaya gampang dibedakan sekilas */
.act-btn{
  display:inline-flex;align-items:center;gap:4px;background:var(--bg-2);border:1.5px solid var(--border);
  border-radius:7px;padding:4px 12px;font-size:11px;font-weight:700;color:var(--t2);
  cursor:pointer;text-decoration:none;font-family:var(--ui);margin-left:4px;
  transition:all .15s;white-space:nowrap
}
.act-btn svg{opacity:.85;flex-shrink:0}
.act-detail{border-color:rgba(59,130,246,.4);color:var(--blue)}
.act-detail:hover{background:rgba(59,130,246,.12);border-color:var(--blue)}
.act-trend{border-color:rgba(6,182,212,.4);color:var(--cyan)}
.act-trend:hover{background:rgba(6,182,212,.12);border-color:var(--cyan)}

/* Mobile: sembunyikan kolom "Lokasi" biar tidak sempit */
@media (max-width: 900px){
  .det-tablewrap thead th:nth-child(3),
  .det-tablewrap tbody td:nth-child(3){display:none}
}

/* ── Kotak saran (autocomplete) pencarian perangkat di halaman Grafik Tren ── */
.trend-suggest{
  display:none;position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:40;
  background:var(--bg-1);border:1px solid var(--border);border-radius:9px;
  box-shadow:0 10px 30px rgba(0,0,0,.35);max-height:260px;overflow-y:auto;padding:4px
}
.trend-suggest-item{
  display:flex;align-items:center;gap:8px;padding:8px;border-radius:6px;
  cursor:pointer;font-size:12px;color:var(--t1);transition:background .1s
}
.trend-suggest-item:hover{background:var(--bg-3)}
.trend-suggest-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
/* Nilai yang menepi ke ujung kanan barisnya: redup, monospace, bukan isi
   utama baris itu. Dipakai IP di daftar saran (Tren/Pointing/Bandwidth) dan
   penghitung karakter kotak Keterangan di halaman Detail. Namanya netral
   karena isinya memang bukan cuma IP — dulu `.trend-suggest-ip`. */
.nilai-ekor{margin-left:auto;font-size:10px;color:var(--t3);font-family:var(--mono)}
/* Mendekati batas — pemberitahuan, bukan larangan. Dipakai penghitung karakter
   kotak Keterangan; daftar saran tidak pernah memasangnya. */
.nilai-ekor.penuh{color:var(--amber)}
.trend-suggest-empty{padding:8px;font-size:12px;color:var(--t3);text-align:center}

/* ═══════════════════════════════════════════════════════════════════════
   TEMA — hanya menimpa variabel warna, tidak satu pun aturan tata letak.
   ═══════════════════════════════════════════════════════════════════════

   Aturan mainnya: sebuah tema BOLEH mengganti --bg-*, --border*, --t1..t3.
   Tema TIDAK boleh mengganti --green/--red/--amber sembarangan, karena tiga
   warna itu bukan hiasan — hijau/merah/kuning adalah bahasa status frekuensi
   dan kondisi perangkat di seluruh aplikasi. Satu-satunya yang menggesernya
   adalah tema `kontras`, dan itu menaikkan kecerahannya, bukan menukar artinya.

   `body.light` dipertahankan sebagai alias `terang`: tab lama yang masih
   memegang app.js versi sebelumnya menyetel class itu, bukan data-tema.

   Daftar id di sini WAJIB sama dengan TEMA di app.js dan _TEMA_SAH di api.py.
   Ketiganya dijaga uji test_tampilan.py::TestDaftarTemaSelaras — kalau tidak,
   sebuah tema bisa lolos disimpan ke database lalu tidak punya warna apa pun
   saat dipakai, dan halamannya tampil tanpa gaya sama sekali. */

/* Gelap — bawaan; sama persis dengan :root di atas, ditulis ulang supaya
   berpindah dari tema lain kembali ke sini benar-benar mengembalikan warnanya. */
body[data-tema="malam"]{--bg-0:#060B14;--bg-1:#0D1626;--bg-2:#142035;--bg-3:#1C2D47;--border:#1E3A5F;--border2:rgba(30,58,95,.5);--t1:#E2EBF8;--t2:#8AAECB;--t3:#7998B7}

/* Gelap netral — abu tanpa rona biru, untuk yang merasa tema bawaan terlalu "biru". */
body[data-tema="grafit"]{--bg-0:#0B0D10;--bg-1:#14171C;--bg-2:#1C2027;--bg-3:#262B33;--border:#2E343D;--border2:rgba(46,52,61,.6);--t1:#E7EAEE;--t2:#A2AAB6;--t3:#8B919E}

/* Ruang NOC — gelap kehijauan, dipakai di layar dinding yang menyala terus. */
body[data-tema="noc"]{--bg-0:#050A07;--bg-1:#0B140E;--bg-2:#0F1D14;--bg-3:#16291C;--border:#1D3D28;--border2:rgba(29,61,40,.6);--t1:#DCF3E4;--t2:#87BF9E;--t3:#629976}

/* Terang — untuk ruangan yang terang atau layar yang dipantulkan proyektor. */
body.light,
body[data-tema="terang"]{--bg-0:#F0F4FA;--bg-1:#FFFFFF;--bg-2:#E8EEF8;--bg-3:#D8E3F0;--border:#C5D3E8;--border2:rgba(197,211,232,.7);--t1:#0F1F3D;--t2:#3A5A8A;--t3:#496588;--bayang-login:0 18px 44px rgba(15,31,61,.14);--green:#147237;--red:#C71111;--amber:#8A5906;--orange:#A54604;--fg-chip-kuning:#775F02;--fg-chip-merah:#BF0606;--fg-mati:#BD2837;--fg-di-terang:#FFFFFF;--fg-chip-hijau:#0F7133;--fg-chip-biru:#045FC5}

/* Kertas hangat — terang tanpa biru, lebih ramah untuk shift panjang. */
body[data-tema="hangat"]{--bg-0:#F5F0E6;--bg-1:#FFFCF5;--bg-2:#EFE7D8;--bg-3:#E3D8C4;--border:#D6C8AE;--border2:rgba(214,200,174,.7);--t1:#2C2418;--t2:#6B5C43;--t3:#685B47;--bayang-login:0 18px 44px rgba(44,36,24,.13);--green:#136E34;--red:#BD1010;--amber:#805305;--orange:#9B4204;--fg-chip-kuning:#725B02;--fg-chip-merah:#BA0606;--fg-mati:#B52735;--fg-di-terang:#FFFFFF;--fg-chip-hijau:#0E682F;--fg-chip-biru:#035ABB}

/* Kontras tinggi — hitam pekat + putih. Satu-satunya tema yang ikut menggeser
   warna status, dan hanya menaikkan kecerahannya supaya tetap terbaca di layar
   luar ruangan atau proyektor murah. Artinya tidak berubah. */
body[data-tema="kontras"]{--bg-0:#000;--bg-1:#0A0A0A;--bg-2:#141414;--bg-3:#1F1F1F;--border:#4D4D4D;--border2:#3A3A3A;--t1:#FFF;--t2:#E0E0E0;--t3:#B0B0B0;--green:#00E676;--red:#FF5252;--amber:#FFD740}

/* ── WARNA AKSEN ──────────────────────────────────────────────────────────
   Ditulis SESUDAH blok tema dengan sengaja: kekhususan selektornya sama
   (`body[atribut]`), jadi yang menang adalah yang belakangan. Kalau ditaruh
   di atas, tema `kontras` akan mengunci --blue dan pilihan aksen diam-diam
   tidak berpengaruh.

   Yang boleh diganti hanya --blue, --cyan & --blue-permukaan (warna tindakan
   & tautan). Merah, hijau, kuning tidak ikut, karena ketiganya menyampaikan
   arti. */
/* Preset "Biru" MENIMPA :root (kekhususan `body[atribut]` lebih tinggi), jadi
   ia harus membawa nilai yang sama. Menaikkan --blue di :root saja tidak
   memperbaiki apa pun untuk pemakai yang aksennya tersetel "biru" — dan itu
   pilihan bawaannya. Ketahuan saat potret pembanding berubah padahal hanya
   :root yang disunting: `str.replace` di skrip potret mengganti KEDUANYA.

   TIAP PRESET WAJIB MENYETEL KETIGANYA, dan kolom ketiga itu yang paling
   mudah lupa. `--blue-permukaan` lahir 11 September 2026 di :root saja —
   sementara preset hanya menimpa --blue. Akibatnya selama beberapa jam:
   memilih Cyan/Ungu/Magenta (atau warna sendiri) tidak lagi mengubah SATU
   PUN tombol utama; latar tombolnya terukur #2563EB untuk keempat aksen.
   Fiturnya diam, tanpa error, dan justru pada elemen yang paling terlihat.

   Nilai permukaan = warna aksennya sendiri yang DIGELAPKAN di HLS sampai
   teks putih lolos 4,5 (rona & saturasi tetap). Jadi tombolnya ikut aksen
   lagi DAN lebih terbaca daripada sebelum pemisahan itu — cyan dulu cuma
   memberi teks putih 2,43:1.

   Kolom --blue juga sudah dinaikkan untuk ungu & magenta: keduanya gagal
   sebagai TEKS (ungu 3,27–4,65 · magenta 4,01–5,70) dan itu tidak pernah
   terlihat karena sapuan peramban pertama cuma menjalankan aksen bawaan.
   Sapuan sekarang menjalankan keempatnya. */
/*                          teks (di latar gelap)   tautan       permukaan (teks putih di atasnya) */
body[data-aksen="biru"]   {--blue:#5392F7;--cyan:#06B6D4;--blue-permukaan:#2563EB}
body[data-aksen="cyan"]   {--blue:#06B6D4;--cyan:#22D3EE;--blue-permukaan:#047F94}
body[data-aksen="ungu"]   {--blue:#A37EF8;--cyan:#A78BFA;--blue-permukaan:#8452F5}
body[data-aksen="magenta"]{--blue:#DE5DF1;--cyan:#F0ABFC;--blue-permukaan:#C513DF}

/* Warna pilihan sendiri. Nilainya TIDAK ditulis di sini — `setAksenKustom()`
   memasang tiga variabel `--aksen-kustom*` langsung di elemen body, lalu
   aturan ini yang menyambungkannya ke --blue/--cyan/--fg-aksen.

   Perantaraan ini yang membuatnya aman: kalau ketiga variabel itu tidak ada —
   localStorage dibersihkan, atau nilai hex-nya rusak — `var(..., cadangan)`
   jatuh ke biru bawaan. Tanpa itu, aksen "kustom" tanpa hex berarti --blue
   kosong, dan SETIAP tombol utama di aplikasi kehilangan warnanya sekaligus. */
body[data-aksen="kustom"]{
  --blue:var(--aksen-kustom,#5392F7);
  --cyan:var(--aksen-kustom-2,#06B6D4);
  --fg-aksen:var(--aksen-kustom-fg,#fff);
  /* Permukaan tombol ikut warna yang dipilih, TANPA digelapkan — beda dari
     keempat preset di atas, dan itu bukan kelalaian. Yang membuat gelap perlu
     di sana adalah teks putih yang dipatok; di sini `--fg-aksen` sudah
     dihitung dari kecerahan warna pilihan (setAksenKustom), jadi warnanya
     apa pun, teksnya yang menyesuaikan. Menggelapkan di sini justru
     membatalkan pilihan orang: kuning yang dipilih muncul sebagai kuning
     tua yang tidak pernah mereka klik. */
  --blue-permukaan:var(--aksen-kustom,#2563EB);
}

/* ── AKSEN DI TEMA TERANG ─────────────────────────────────────────────────
   Blok ini ada karena satu kenyataan yang mudah salah dibaca: **yang menang
   di sini URUTAN BERKAS, bukan kekhususan.** `body[data-tema=…]` dan
   `body[data-aksen=…]` kekhususannya SAMA (0,2,0), dan aksen ditulis
   belakangan — jadi `--blue` & `--cyan` yang disetel blok tema di atas
   DIABAIKAN sepenuhnya. Menaruh nilai terang di blok tema tidak akan pernah
   terlihat, tanpa satu pun galat.

   Jalan keluar yang SALAH: memindahkan blok tema ke bawah blok aksen. Itu
   memang memperbaiki tema terang — dan mematikan pilihan aksen di SELURUH
   tema sekaligus, persis cacat yang baru saja diperbaiki di tombol utama.
   Jalan keluar yang benar menaikkan kekhususannya: dua atribut (0,3,0),
   jadi ia menang atas keduanya tanpa mengubah urutan apa pun.

   Nilainya per (tema x aksen), bukan satu biru gelap untuk semua: aksen yang
   dipilih pengguna harus tetap terbaca SEBAGAI aksen yang mereka pilih.
   Semuanya digelapkan di HLS — rona & saturasi tetap. */
body[data-tema="terang"][data-aksen="biru"]   {--blue:#0A5ADA;--cyan:#046E80}
body[data-tema="terang"][data-aksen="cyan"]   {--blue:#046E80;--cyan:#0A6D7D}
body[data-tema="terang"][data-aksen="ungu"]   {--blue:#6F35F4;--cyan:#6838F6}
body[data-tema="terang"][data-aksen="magenta"]{--blue:#A810BE;--cyan:#A907C5}
body[data-tema="hangat"][data-aksen="biru"]   {--blue:#0A56D0;--cyan:#036576}
body[data-tema="hangat"][data-aksen="cyan"]   {--blue:#036576;--cyan:#096978}
body[data-tema="hangat"][data-aksen="ungu"]   {--blue:#682CF4;--cyan:#612EF6}
body[data-tema="hangat"][data-aksen="magenta"]{--blue:#9F10B5;--cyan:#A007BB}

/* Warna sendiri tidak bisa dipatok nilainya di sini — ia baru diketahui saat
   pengguna memilihnya. Yang menggelapkan `terapkanTampilan()` di app.js,
   yang memang sudah dipanggil ulang tiap kali TEMA berganti, jadi nilainya
   selalu dihitung untuk tema yang sedang berlaku.

   `--blue-permukaan` sengaja TIDAK ikut: ia latar tombol, dan `--fg-aksen`
   sudah menyesuaikan diri dengan kecerahannya. Yang bermasalah di tema
   terang cuma warna yang dipakai SEBAGAI TEKS. */
body[data-tema="terang"][data-aksen="kustom"],
body[data-tema="hangat"][data-aksen="kustom"]{
  --blue:var(--aksen-kustom-teks,#0A5ADA);
  --cyan:var(--aksen-kustom-teks,#046E80);
}

/* ── UKURAN TAMPILAN ──────────────────────────────────────────────────────
   `zoom`, bukan font-size: hampir seluruh ukuran di berkas ini ditulis dalam
   px, jadi mengubah font-size hanya menggeser sebagian teks dan merusak
   perataannya. `zoom` menskalakan semuanya — termasuk padding, ikon, dan
   tinggi baris tabel — sehingga tata letaknya tetap utuh.

   Batasnya sengaja sempit (0,92–1,08). Lebih dari itu, tabel lebar mulai
   memaksa geser menyamping di layar 1366px yang paling banyak dipakai. */
body[data-skala="kecil"]{zoom:.92}
body[data-skala="besar"]{zoom:1.08}
/* ── HALAMAN PENGATURAN ──────────────────────────────────────────────────
   Semua kartu di halaman ini berbagi SATU pembungkus. Sebelumnya tiap kartu
   punya max-width sendiri (520px dan 820px) dan dipusatkan sendiri-sendiri,
   jadi tepi kiri-kanannya tidak segaris — dua kartu yang terbaca seperti dua
   halaman berbeda yang kebetulan ditumpuk. */
.set-wrap{width:100%;max-width:860px;margin:0 auto;display:flex;flex-direction:column;gap:16px}

/* ── Susunan petak halaman Sistem (16 September 2026) ─────────────────────
   Diminta pemilik lewat rancangan docs/laporan-bug/bug75.png. Pembungkusnya
   TETAP `.set-wrap` — satu pembungkus, tepi kiri-kanan segaris, alasan yang
   sama dengan 8 Agustus 2026 — dan `.set-petak` yang menukar kolom
   tunggalnya jadi petak dua belas kolom.

   Dua belas, bukan empat: pembagian yang dipakai rancangannya 5/7 dan 8/4,
   dan keduanya tidak punya penyebut bersama yang lebih kecil. Angka lebar
   tiap kartu ditulis di HTML-nya sebagai `.set-span-N`, bukan di sini per id
   — halaman ini punya sembilan kartu, dan sembilan aturan berid adalah
   sembilan tempat yang harus dicari saat susunannya diubah lagi.

   Lebar maksimumnya naik dari 860px: pada 860px, kolom 5 dari 12 tinggal
   340px dan kedua kolom isian Interval Polling mulai berdesakan. 1440px
   adalah lebar tempat petak 8/4 masih menyisakan 440px untuk donat zona —
   cukup untuk donat beserta legendanya berdampingan. */
.set-petak{max-width:1900px;display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));gap:16px;align-items:start}
.set-span-3{grid-column:span 3}
.set-span-4{grid-column:span 4}
.set-span-5{grid-column:span 5}
.set-span-6{grid-column:span 6}
.set-span-7{grid-column:span 7}
.set-span-8{grid-column:span 8}
.set-span-9{grid-column:span 9}

/* Kartu identitas di kepala halaman. `.app-name` di dalamnya sudah punya
   aturan sendiri untuk kepala halaman (11px); di sini ia judul kartu, jadi
   ukurannya diambil alih. Kekhususannya lebih tinggi, jadi ia menang tanpa
   memindahkan aturan mana pun. */
.set-hero .app-name{font-size:15px;font-weight:700;color:var(--t1)}
.set-hero .app-sub{font-size:11px;color:var(--t3)}
.set-hero-ikon{display:inline-flex;align-items:center;justify-content:center;
  width:40px;height:40px;border-radius:10px;
  background:var(--blue-permukaan);color:var(--fg-aksen)}
/* Empat kartu angka. Membelah jadi dua baris lebih awal daripada petak
   halamannya: di kolom 7-dari-12 pada layar 1100px, empat kartu sebaris
   tinggal 150px masing-masing dan "Polling Interval" melipat jadi dua baris
   di atas angkanya. */

.catatan-tengah{font-size:11px;color:var(--t3);text-align:center}

/* Donat distribusi zona. Angka di TENGAH lubangnya, dan itu bukan hiasan:
   donat tanpa angka total menuntut pembacanya menjumlahkan sendiri
   legendanya. */

.zona-donat-bungkus{position:relative;width:120px;height:120px;flex-shrink:0}
.zona-donat{width:120px;height:120px;display:block}
.zona-donat-tengah{position:absolute;inset:0;display:flex;
  flex-direction:column;align-items:center;justify-content:center;
  pointer-events:none}
.zona-donat-angka{font-size:20px;font-weight:700;color:var(--t1);
  font-family:var(--mono);line-height:1.1}
.zona-donat-ket{font-size:9px;color:var(--t3);text-transform:uppercase;
  letter-spacing:.06em}
.zona-donat-legenda{display:flex;flex-direction:column;gap:8px;
  min-width:0;flex:1}
.zona-donat-baris-legenda{display:flex;align-items:center;gap:8px;
  font-size:11px;color:var(--t2)}

@media(max-width:1100px){
  /* Di bawah ini kolom 4-dari-12 tinggal ±280px — terlalu sempit untuk donat
     beserta legendanya, dan terlalu sempit untuk tabel zona di sebelahnya.
     Petaknya dibelah jadi enam kolom: pembagian 5/7 dan 8/4 jatuh ke 6/6 dan
     tumpukan, dan keduanya masih terbaca. */
  .set-petak{grid-template-columns:repeat(6,minmax(0,1fr))}
  .set-span-3, .set-span-4, .set-span-5, .set-span-7,
  .set-span-8, .set-span-9{grid-column:span 6}
  .set-span-6{grid-column:span 3}
}
@media(max-width:820px){
  /* `minmax(0,1fr)` dan bukan `1fr`: jalur grid bawaannya `minmax(auto,1fr)`,
     dan batas bawah `auto` itu MIN-CONTENT isinya. Kartu Zona Frekuensi memuat
     tabel ber-`--tmin:640px`, jadi dengan `1fr` jalurnya terpaksa selebar
     684px di layar 480px — dan halamannya bisa digeser ke samping. Terukur
     di peramban 16 September 2026: doc 694px lawan layar 480px, satu-satunya
     temuan dari 57 pemuatan yang disapu. */
  .set-petak{grid-template-columns:minmax(0,1fr)}
  .set-span-3, .set-span-4, .set-span-5, .set-span-6,
  .set-span-7, .set-span-8, .set-span-9{grid-column:1 / -1}
  .set-petak .petak-4{grid-template-columns:1fr 1fr}
}
/* `.set-head` DIHAPUS 21 Agustus 2026 — ia kembar dengan `.kartu-kepala`
   sejak pembulatan jarak §2.2, dan settings.html sekarang memakai kelas
   bersama itu. Nasib yang sama dengan `.set-ico`/`.set-title`/`.set-sub`
   di bawah. */
/* `.set-ico`, `.set-title`, dan `.set-sub` DIHAPUS 8 Agustus. Ketiganya
   ternyata kelas bersama yang sudah ada, ditulis ulang dengan nama lain:

     .set-ico   → .ic-inline + .tak-susut   (komposisi persis)
     .set-title → .h-card                    (deklarasi sama persis)
     .set-sub   → .t-sub                     (deklarasi sama persis)

   Dua yang terakhir sudah lama tercatat sebagai kelas kembar. Yang pertama
   tidak, karena ia bukan kembar melainkan kembar-plus-satu-properti — dan
   properti itu persis `.tak-susut`, kelas yang lahir dari pelajaran yang
   sama di `.kartu-kepala`. */
.set-body{padding:16px 20px}
.set-foot{padding:12px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px;align-items:center}
.set-err{font-size:11px;color:var(--red);margin-top:8px;min-height:14px}

/* Tiga kolom: label — isian — keterangan. Kedua isian sejajar dengan
   sendirinya, tanpa lebar yang diatur satu per satu di tiap baris. */
.set-grid{display:grid;grid-template-columns:minmax(140px,180px) 96px 1fr;gap:12px 12px;align-items:center}
/* Label tebal 12px. `.set-label` (halaman Pengaturan) dan `.up-ap` (kolom
   Uptime sisi AP) — sama alasannya dengan `.sig, .sel-lebar` di atas. */
.set-label, .up-ap{font-size:12px;font-weight:600;color:var(--t2)}
.set-num{padding:8px;font-size:13px;font-family:var(--mono);text-align:right}
.set-hint{font-size:11px;color:var(--t3);line-height:1.45}
.set-simpan{background:var(--blue-permukaan);border:none;border-radius:8px;padding:8px 16px;font-size:12px;font-weight:700;color:var(--fg-aksen);cursor:pointer;display:inline-flex;align-items:center;gap:4px}
/* Sakelar centang & kabar "tersimpan". Ketiganya lahir 13 Agustus di kartu
   Notifikasi Telegram dengan awalan `tg-`, dan sejak kartu Pencarian Alamat
   (27 Agustus) dipakai dua halaman — awalan yang menyebut satu halaman
   membuat yang kedua tampak sedang meminjam milik orang lain. Isinya tidak
   berubah sebaris pun, hanya namanya. */
.set-alih{display:flex;align-items:center;gap:8px;font-size:12px;
  color:var(--t2);cursor:pointer;user-select:none}
.set-alih input{width:15px;height:15px;accent-color:var(--blue);cursor:pointer;flex-shrink:0}
.set-alih-kecil{font-size:11px}
.set-ok{font-size:11px;color:var(--green)}
/* Isi kartu sebagai kolom ber-jarak. Ini yang membuat sakelar centang tidak
   perlu margin sendiri — dan margin sendiri itu ternyata kembar persis
   dengan `.login-field`, yang berarti dua kelas berbeda untuk satu jarak
   yang sama. Dipakai kartu Telegram sejak 13 Agustus dan kartu Pencarian
   Alamat sejak 27 Agustus. */
.set-isi{padding:12px 20px;display:flex;flex-direction:column;gap:12px}

/* ── RESPONSIVE ── */
@media(max-width:1024px){
  /* `grid-template-rows:none` mencabut tinggi baris pasti dari aturan di
     atas. Di sini kedua kartu bertumpuk jadi dua baris; angka yang dihitung
     untuk SATU baris berisi dua kolom akan menjepit kolom kiri ke tinggi
     kartu Log. Batas panel di lebar ini dipegang `.act-list{max-height}`.

     Bisa dicabut dari sini karena aturTinggiLog() menulis `--log-h` sebagai
     custom property, bukan langsung ke grid-template-rows — gaya inline akan
     mengalahkan media query ini. */
  .two{grid-template-columns:1fr;height:auto;grid-template-rows:none}
  /* Di satu kolom, Action Center berhenti bertinggi tetap: `--act-h` ada
     untuk menjaga baris grid dua kolom, dan di sini tidak ada baris grid yang
     perlu dijaga. Daftar down yang panjang lebih baik memanjang daripada
     menggulung di dalam kartu setinggi 236px di layar 360px. */
  .act-card{height:auto;flex:0 0 auto}
  .two-left > .zone-card{flex:0 0 auto;min-height:0}
  /* Tinggi Log TIDAK ditimpa di sini: rumus `calc(header + n × entri)` di
     atas sama benarnya untuk satu kolom maupun dua, dan aturTinggiLog() tetap
     mengukur headernya. Menimpanya dengan angka tetap justru mengembalikan
     entri terpotong begitu header membungkus jadi dua baris.

     Urutannya SENGAJA dibiarkan apa adanya. Tata letak HP sudah diperiksa
     pengguna dan dinyatakan sesuai — mengubahnya di sini berarti menumpangkan
     perubahan yang tidak diminta ke perbaikan yang diminta. */
  .two-left{gap:12px}
  /* Dua kolom berdampingan terlalu sempit di bawah 1024px — jadi bertumpuk.
     `grid-template-rows:none` melepas batasan `minmax(0,1fr)` di atas: itu
     dibuat untuk SATU baris berisi dua kolom, dan kalau dibiarkan, baris
     pertama memakan seluruh tinggi kartu sementara baris kedua terjepit. */
  .act-card{grid-template-columns:1fr;grid-template-rows:none;gap:8px;flex:none;
            min-height:0}
  /* Tanpa kolom kiri yang tingginya bisa diikuti, panel ini setinggi isinya.
     `max-height` yang menggantikan peran batas tinggi kartu — tanpa itu,
     40 perangkat down membuat panel ini memanjang sampai ratusan piksel. */
  .act-list{max-height:180px}
  /* Tetap satu baris, tapi mulai bisa digeser ke samping. Dipaksa muat ke
     layar sempit akan membuat tiap kartu terlalu kecil untuk dibaca; digeser
     jauh lebih baik daripada angka yang tidak terbaca. Kartu terakhir sengaja
     terpotong sedikit di tepi — itu isyarat visual bahwa masih ada lanjutannya. */
  .stats{grid-auto-columns:minmax(150px,1fr);overflow-x:auto;padding-bottom:4px;
         scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch}
  .scard{scroll-snap-align:start}
}
@media(max-width:768px){
  header{padding:0 12px;height:52px}
  main{padding:12px}
  .stats{grid-auto-columns:minmax(142px,1fr)}
}

/* ══════════════════════════════════════════
   MOBILE (HP, ≤640px) — tambahan KHUSUS layar sempit.
   Semua rule di sini baru berlaku di bawah 640px, jadi
   TIDAK mengubah tampilan desktop/tablet sama sekali —
   ini murni memperbaiki header & tabel yang tadinya
   tabrakan/terpotong di HP.
   ══════════════════════════════════════════ */
@media(max-width:640px){
  /* Halaman Pengaturan: grid tiga kolom memaksa keterangan jadi satu kata
     per baris di layar sempit. Ditumpuk — label di atas, isian & keterangan
     di bawahnya. */
  .set-grid{grid-template-columns:1fr;gap:4px 0}
  .set-grid > .set-label{margin-top:8px}
  .set-num{width:110px}
  .set-hint{margin-bottom:2px}

  /* Kartu statistik: tetap SATU baris yang digeser ke samping, sama seperti
     menu nav di atasnya — jadi polanya konsisten dan pengguna sudah terbiasa.
     Kartu diperkecil supaya 3 buah terlihat sekaligus; kalau hanya muat 1-2,
     pengguna sulit menduga masih ada lanjutannya. */
  .stats{grid-auto-columns:minmax(122px,1fr);gap:8px;margin-bottom:12px}
  .scard{padding:8px 12px}
  .slabel{font-size:9px;letter-spacing:.05em;margin-bottom:4px}
  .sval{font-size:21px}
  .ssub{font-size:10px}

  /* Header pecah jadi 2 baris: logo+status di baris 1, menu nav
     jadi baris ke-2 yang bisa discroll ke samping — supaya tidak
     lagi saling tindih seperti sebelumnya */
  header{flex-wrap:wrap;height:auto;min-height:auto;padding:8px 12px;row-gap:8px}
  .hdr-l{order:1;gap:8px}
  .logo{width:28px;height:28px;font-size:15px;border-radius:7px}
  .app-name{font-size:13px}
  .app-sub{display:none}              /* subjudul disembunyikan di HP biar header tidak makan tempat */
  .hdr-r{order:2;margin-left:auto;gap:8px}
  #clock{display:none}                /* jam HP sudah ada di status bar sistem, tidak perlu dobel */
  .nav-btn{padding:8px;font-size:12px}
  /* Dua aturan penambal lama DICABUT dari sini — `max-width:calc(100vw-24px)`
     dan `#navSetting/#navProfile{left:auto;right:0}`. Keduanya digantikan oleh
     blok ≤768px di atas yang menangani keempat menu sekaligus. Dibiarkan di
     sini justru membatalkannya: selektor ber-id itu lebih kuat DAN letaknya
     lebih belakang, jadi tepat di layar tersempit perbaikannya tidak berlaku. */

  main{padding:8px}

  /* Pencarian utama di HP — justru di sini ia paling berguna (menu perlu dua
     ketukan untuk dibuka), jadi yang dikecilkan cuma bantalannya, bukan
     hurufnya. 16px pada isian TIDAK boleh turun: Safari iOS memperbesar
     seluruh halaman begitu isian yang difokuskan berhuruf lebih kecil dari
     itu, dan halaman yang melompat saat orang mulai mengetik adalah harga
     yang jauh lebih mahal daripada satu piksel tata letak. */
  .cari-utama{margin-bottom:12px}
  .cari-utama input{font-size:16px;padding:12px 36px 12px 36px}

  .card-hdr{flex-wrap:wrap;row-gap:8px}

  /* Tinggi tetap 640px DICABUT bersama angka-angka di lebar lain: di HP
     barisnya justru paling tinggi (kolom membungkus), jadi kotak bertinggi
     tetap di sana yang paling parah memotongnya. */
  .tcard-hdr{flex-wrap:wrap;row-gap:8px}
  .filter-row{flex-wrap:wrap;row-gap:4px;margin-left:0}   /* semua tombol filter tampil & lanjut ke baris bawah, TIDAK digeser/scroll */
  .fbtn{flex-shrink:0}

  /* Ketiga dropdown filter History jadi selebar layar masing-masing.
     Dengan basis 150px, dua muat berdampingan di 358px dan yang ketiga
     membungkus sendirian — persis ketidaksejajaran yang baru saja
     diperbaiki, hanya berpindah ke layar sempit. */
  #historyFilter .f-select{flex:1 1 100%}

  /* Jarak tabel ke bagian Regulasi ikut dipersempit, seirama dengan
     `main{padding:10px}` di atas. */
  .tcard{margin-bottom:12px}

  /* Kelola Perangkat: dua kolom jejak audit disembunyikan di HP.
     Bukan karena tidak penting, tapi karena keduanya SUDAH ada lengkap di
     modal Riwayat Perubahan pada baris yang sama — jadi tidak ada
     informasi yang benar-benar hilang, hanya tidak diulang. Tanpa ini,
     tabelnya 880px dan butuh 2,3 layar geseran untuk sampai kolom Aksi. */
  /* Nomornya 7 & 8, naik dari 6 & 7 saat kolom kotak centang ditambahkan di
     depan (27 Agustus 2026). Indeks yang dihitung tangan seperti ini adalah
     yang paling gampang tertinggal saat susunan kolom berubah — ada uji yang
     mencocokkan tiap nomor di sini dengan `data-kol` yang ditujunya, jadi
     pergeseran berikutnya gagal di suite, bukan di layar orang. */
  .mtable-dev thead th:nth-child(7), .mtable-dev tbody td:nth-child(7),
  .mtable-dev thead th:nth-child(8), .mtable-dev tbody td:nth-child(8){display:none}
  /* Dua kolom hilang, jadi lebar minimumnya ikut turun. Kalau tetap 880px,
     enam kolom sisanya dipaksa melebar dan geserannya jadi lebih panjang
     daripada yang diperlukan.

     Ditulis sebagai `min-width` langsung, BUKAN dengan menimpa `--tmin`:
     nilai `--tmin` halaman ini ada di atribut style, dan properti kustom
     inline mengalahkan aturan stylesheet — override lewat variabel akan
     diam-diam tidak berlaku. Selektor ini menang karena kekhususannya sama
     dengan `.mtable table` tapi letaknya lebih belakang. */
  .mtable-dev table{min-width:830px}

  /* ── Kelola BTS di HP ──
     Dilaporkan pengguna 12 Agustus 2026: "tampilan jumlah perangkat pada
     mobile tidak ada informasi total perangkat pada salah satu BTS".

     Kolomnya tidak hilang — ia kolom ketiga dari enam pada tabel yang di sini
     selebar 900px (dari `.det-tablewrap table`, yang menang atas `--tmin:720px`
     karena ditulis belakangan dengan kekhususan yang sama). Ia mulai sekitar
     x=340px, satu-dua puluh piksel di luar layar 360px, jadi halamannya
     terbuka dengan angka yang paling dicari tepat di seberang tepi kanan —
     dan tidak ada apa pun yang memberi tahu bahwa ia ada di sana.

     Angkanya dipindahkan JS ke baris kecil di bawah nama (`.hp-saja` di
     _btsJumlahRingkas), jadi kolomnya di sini disembunyikan supaya tidak
     tertulis dua kali. Kedua kolom jejak audit (Dibuat, Terakhir Diubah)
     sengaja TIDAK ikut disembunyikan seperti di Kelola Perangkat: di sana
     isinya masih bisa dibaca lengkap di modal Riwayat, di sini tidak ada
     tempat lain yang memuatnya. */
  /* Nomornya 5, dan perjalanannya sendiri yang menjelaskan kenapa uji
     pencocok indeks itu ada.

     Ditulis `3` pada 13 Agustus 2026, saat Perangkat memang kolom ketiga dari
     enam. Kolom Koordinat disisipkan 19 Agustus (`f982fa8`) dan menggeser
     semuanya satu ke kanan — nomor di sini tidak ikut. Sejak hari itu yang
     disembunyikan di HP adalah KOORDINAT, bukan Perangkat: angka jumlah
     perangkat tertulis dua kali (kolomnya sendiri + baris `.hp-saja` di bawah
     nama), dan kolom koordinat lenyap tanpa ada yang menggantikannya.

     Tidak ada satu pun uji yang gagal selama enam hari itu, dan tidak ada
     error di mana pun — cuma kolom yang salah yang hilang di layar yang
     jarang dibuka. Sekarang ada `TestIndeksKolomTidakBergeser`, yang
     mencocokkan tiap nomor di sini dengan `data-kol` yang ditujunya. */
  .mtable-bts thead th:nth-child(5), .mtable-bts tbody td:nth-child(5){display:none}

  /* ── Detail Perangkat di HP ──
     Kolom BTS/POP (ketiga) disembunyikan dengan alasan yang sama seperti
     kolom Perangkat di atas: namanya sudah ikut menempel di bawah nama
     perangkat lewat `.hp-saja`, dan mengulangnya di kolom yang baru terlihat
     setelah digeser dua layar tidak menambah apa pun. Yang berbeda: di sini
     yang dipindah adalah NAMA BTS-nya, jadi di HP informasinya justru datang
     lebih awal daripada di layar lebar.

     1400 = 1500 dikurangi jatah kolom yang hilang (100px). Tanpa ini kesebelas
     kolom sisanya dipaksa melebar dan geserannya jadi lebih panjang daripada
     yang diperlukan — kekeliruan yang sama sudah pernah terjadi di
     `.mtable-dev` dan dicatat di sana.

     Lebar minimumnya sendiri TIDAK diturunkan di sini melainkan tepat di
     bawah aturan 1500px itu (cari "1400"): aturan di blok ini kekhususannya
     lebih rendah DAN letaknya lebih di depan, jadi ia akan kalah tanpa suara.
     Media query tidak menambah kekhususan apa pun — jebakan yang sudah
     memakan `.mtable-dev table{min-width:830px}` beberapa baris di atas,
     yang sejak `.det-tablewrap.mtable-dev table` ditulis tidak pernah lagi
     berlaku.

     PERHATIAN — aturan ini berbasis URUTAN, bukan nama kolom, dan itu tempat
     paling sunyi untuk salah: menyisipkan kolom di kirinya menggeser artinya
     tanpa satu pun tanda. Aturannya tetap sah, tetap berlaku, dan yang hilang
     di HP cuma berganti jadi kolom lain.

     Sudah terjadi 19 Agustus 2026: kolom Model disisipkan sebagai kolom ke-3
     dan nomor ini sempat jadi 4; begitu Kategori dibuang (Model menggantikan
     tempatnya, bukan menambah) ia kembali 3. Yang menjaganya sekarang
     test_mobile_layout.py::test_yang_disembunyikan_di_hp_memang_bts —
     ia membaca nomornya lalu mencocokkan dengan NAMA kolom di posisi itu,
     jadi angka yang ikut salah pun ketahuan. */
  .mtable-detail thead th:nth-child(3), .mtable-detail tbody td:nth-child(3){display:none}

  .hp-saja{display:inline}

  /* Toolbar pencarian: `min-width:220px` bawaannya memaksa baris tetap
     melebar di layar 360px, lalu tombol di sebelahnya terdorong keluar. */
  .det-toolbar{gap:8px}
  .det-search{min-width:0;flex:1 1 100%}

  /* ── Kartu grafik (halaman Grafik Tren) ──
     Bantalan 18px di kiri+kanan memakan 36px dari layar 360px — 10% lebar
     kanvas hilang untuk ruang kosong. Kanvasnya menggambar sesuai
     offsetWidth, jadi tiap piksel yang dihemat langsung jadi lebar plot. */
  .chart-card{padding:12px !important;margin-bottom:12px !important}

  /* Baris kendali Grafik Tren: kotak cari dan pemilih rentang jadi
     selebar layar, bukan berdesakan di satu baris. */
  #trendSearchWrap{min-width:0 !important;flex:1 1 100%}
  #trendRangeSelect{flex:1 1 100%}

  /* ── Modal: jangan menempel ke tepi layar ──
     Isinya `width:100%` tanpa bantalan di pembungkusnya, jadi di HP kotaknya
     menyentuh kedua tepi dan sudut membulatnya jadi tidak kelihatan. */
  #devAuditOverlay, #devEditOverlay, #disabledOverlay{padding:12px}

  /* ── Form tambah/edit perangkat ──
     Lebar tiap kolom ditulis inline (`width:150px`, `width:120px`, …), jadi
     di layar sempit mereka membungkus jadi kombinasi yang tidak rata — dua
     kotak sempit berdampingan lalu satu kotak sendirian. `!important`
     terpaksa dipakai karena yang dilawan adalah atribut style, bukan aturan
     lain di berkas ini; tanpa itu aturan di sini kalah apa pun urutannya. */
  #devAddPanel .login-input,
  #devEditOverlay .login-input{width:100% !important;min-width:0 !important}
  /* Tiap kolom di baris form dipaksa mengambil satu baris penuh. Ini juga
     yang mengurus pembungkus ber-`width:150px` di sekeliling kotak ID —
     kotaknya sendiri sudah 100%, tapi pembungkusnya belum. */
  #devAddPanel > div{flex-wrap:wrap}
  #devAddPanel > div > *{flex:1 1 100%}
}

/* ══════════════════════════════════════════════════════════════════════════
   KELAS BERSAMA — menggantikan gaya yang ditulis ulang di tiap elemen
   ══════════════════════════════════════════════════════════════════════════

   Diukur 6 Agustus 2026: 719 atribut `style="` tersebar di sebelas halaman,
   dan potongan yang sama persis ditulis ulang puluhan kali — label form 26x,
   kotak isian 22x. Akibatnya bukan berkas yang panjang (itu tidak penting),
   melainkan tidak ada yang bisa memastikan dua label benar-benar berukuran
   sama. Selisih 1px di sepuluh tempat itulah yang membuat sebuah tampilan
   terasa "kurang rapi" tanpa bisa ditunjuk di mana.

   ATURAN YANG DIPEGANG SAAT MEMBUAT KELAS DI SINI:

   1. **Deklarasinya sama persis dengan yang digantikan** — tidak "dirapikan"
      sekalian. Kelas yang menambah satu properti saja mengubah tampilan
      halaman yang dikonversi, dan perubahan itu bercampur dengan konversinya
      sendiri: kalau ada yang bergeser, tidak ada yang tahu itu perbaikan atau
      kerusakan. Merapikan skalanya urusan §2.2 TAMPILAN.md, terpisah dan
      sesudah ini.

   2. **Ditaruh paling akhir di berkas ini.** `.f-input` dipakai bersama
      `.login-input`, dan keduanya sama-sama satu kelas — jadi yang menang
      ditentukan URUTAN, bukan kekhususan. Utilitas harus dibaca terakhir.

   3. **Konversinya satu halaman per commit**, supaya kalau ada yang bergeser,
      yang perlu diperiksa cuma satu halaman.
*/

/* Label kecil di atas kotak isian. 26x ditulis ulang; sengaja BUKAN
   `.login-label` yang sudah ada — kelas itu menambah display:block,
   margin-bottom, dan letter-spacing, yang akan menggeser tata letak form yang
   sedang dikonversi. */
.f-label{font-size:10px;color:var(--t3);text-transform:uppercase;font-weight:700}

/* Ukuran kotak isian di halaman form. Dipakai BERSAMA `.login-input`, yang
   memberi warna, bingkai, dan sudutnya — kelas ini hanya menimpa ukurannya.
   22x ditulis ulang. */
.f-input{width:100%;padding:8px;font-size:12px;margin-top:4px}

/* Angka & alamat yang harus sejajar per karakter (IP, frekuensi, jam). 6x
   ditulis ulang sebagai `style="font-family:var(--mono)"`. Dibaca sesudah
   `.login-input` yang menyetel `--ui`, jadi ia menang di kotak isian. */
.t-mono{font-family:var(--mono)}

/* ── Halaman kedua: users.html (7 Agustus) ───────────────────────────────
   Kelas di bawah lahir dari halaman itu, tapi hampir semuanya sudah tersebar
   di halaman lain — jumlah pemakaian hari ini ditulis di tiap keterangan
   supaya yang berikutnya tahu mana yang tinggal dipasang. */

/* Kaki kotak login (garis pemisah + versi aplikasi). Overlay login disalin
   utuh ke tiap halaman, jadi potongan ini ada 11x — satu per halaman. */

/* Keterangan interval polling di dasar dropdown Setting. 11x. */

/* ── REL NAVIGASI KIRI (19 Agustus 2026) ──────────────────────────────────
   Diminta pengguna: menu utama pindah ke kiri, BENTUK IKON SAJA, dengan
   tooltip nama saat kursor mendekat. Profil pindah ke pojok kanan atas.

   Relnya `position:fixed` dan MULAI DI BAWAH header, bukan dari puncak layar.
   Itu yang membuat header tetap membentang penuh — logo di kiri, jam dan
   profil di kanan — persis seperti yang diminta. Kalau relnya dimulai dari
   atas, headernya ikut terpotong dan logo pindah 56px ke kanan tanpa ada yang
   memintanya.

   Lebarnya 56px: ikon 20px + ruang sentuh 18px di kiri-kanan. Di bawah itu
   sasaran sentuhnya lebih kecil daripada 44px yang dianjurkan untuk jari. */
.rail{position:fixed;left:0;top:56px;bottom:0;width:var(--rail-w);z-index:90;
  background:var(--bg-1);border-right:1px solid var(--border);
  display:flex;flex-direction:column;align-items:center;gap:4px;
  padding:8px 0;overflow:visible}
/* Lebar relnya dan geseran isi halaman WAJIB sama. Ditulis sebagai satu
   variabel, bukan dua angka: dua angka yang harus sama persis adalah dua
   angka yang cepat atau lambat berbeda, dan bedanya muncul sebagai isi
   halaman yang tertimpa relnya — atau jalur kosong di sebelahnya. */
:root{--rail-w:56px}

/* Rel terlipat (20 Agustus 2026), diminta pengguna: "menu yang ada di sebelah
   kiri buatkan agar bisa di hide".

   SELURUHNYA dikurung di layar lebar, dan itu bukan kerapian. Di bawah
   1025px relnya sudah berubah jadi baris mendatar di ATAS halaman — melipatnya
   tidak mengembalikan lebar apa pun, yang dihemat cuma tinggi satu baris.
   Mengurungnya di sini sekaligus membuat aturan `.rail` di media query layar
   sempit tetap yang terakhir dibaca, jadi tidak ada yang perlu dibatalkan
   dua kali.

   Lebarnya jadi nol lewat `--rail-w`, bukan lewat angka kedua: geseran isi
   halaman memang dihitung dari peubah itu, jadi ia ikut sendiri. */
@media (min-width:1025px){
  body.rel-lipat{--rail-w:0px}
  body.rel-lipat .rail{width:0;padding:0;overflow:hidden;border-right:none}
  /* Tombolnya sendiri TIDAK ikut hilang — kalau ikut, tidak ada jalan
     mengembalikan menunya selain membersihkan localStorage. Ia melayang di
     tepi kiri, tepat di bawah header. */
  body.rel-lipat .rail-lipat{position:fixed;left:0;top:60px;width:28px;
    height:34px;overflow:visible;background:var(--bg-1);
    border:1px solid var(--border);border-left:none;
    border-radius:0 8px 8px 0;z-index:91}
}
.rail-lipat{margin-top:auto;flex-shrink:0}

main{margin-left:calc(var(--rail-w)
     + max(0px, (100% - var(--rail-w) - var(--isi-maks)) / 2))}

/* Bilah koneksi TIDAK ikut dipusatkan — ia latar berwarna selebar halaman,
   dan latar berwarna yang dipusatkan dengan celah kosong di kiri-kanannya
   terbaca sebagai kotak yang salah ukuran, bukan sebagai bilah. */
.conn-bar{margin-left:var(--rail-w)}
/* Geseran .lisensi-bar ada di deklarasi utamanya, di bagian atas berkas ini —
   lihat catatan di sana. */

.rail-btn{position:relative;width:40px;height:40px;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  background:none;border:none;border-radius:10px;cursor:pointer;
  color:var(--t2);text-decoration:none;transition:background .15s,color .15s}
.rail-btn:hover{background:var(--bg-3);color:var(--t1)}
/* Butir yang sedang dibuka. Batang aksen di tepi kiri, BUKAN latar penuh:
   latar penuh pada ikon tanpa tulisan terbaca sebagai tombol yang sedang
   ditekan, bukan sebagai halaman yang sedang dibuka. */
.rail-btn.aktif{color:var(--blue)}
.rail-btn.aktif::before{content:'';position:absolute;left:-10px;top:8px;bottom:8px;
  width:3px;border-radius:0 3px 3px 0;background:var(--blue)}
.rail-item{position:relative;display:flex}

/* Tooltip. Elemen sendiri, bukan atribut `title` — lihat _railButir() di
   app.js. `pointer-events:none` supaya ia tidak pernah menghalangi klik pada
   ikon yang justru sedang ditunjuk. */
.rail-tip{position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%);
  background:var(--bg-3);color:var(--t1);border:1px solid var(--border);
  border-radius:7px;padding:4px 8px;font-size:11px;font-weight:600;
  white-space:nowrap;pointer-events:none;opacity:0;z-index:2100;
  box-shadow:0 6px 18px rgba(0,0,0,.35);transition:opacity .12s}
.rail-btn:hover .rail-tip,.rail-btn:focus-visible .rail-tip{opacity:1}
/* Saat flyout-nya terbuka, tooltipnya dimatikan: keduanya muncul di sisi yang
   sama dan saling menimpa, dan yang menang ditentukan urutan penulisan. */
.rail-item.open .rail-tip{opacity:0}

/* Flyout submenu — muncul di SAMPING relnya, bukan di bawah seperti dropdown
   header lama. Selain letaknya, ia memakai `.nav-dropdown` yang sama persis,
   jadi Esc, klik-di-luar, dan penataan isinya tidak ditulis dua kali. */
.rail-flyout{top:0;left:calc(100% + 8px)}
.rail-flyout-judul{padding:4px 12px 8px;font-size:10px;font-weight:700;
  text-transform:uppercase;letter-spacing:.08em;color:var(--t3)}

/* Tombol profil di pojok kanan atas. Bulat, seukuran rel-btn, supaya ia
   terbaca sebagai satu benda dengan ikon-ikon di kiri. */
.hdr-profil{width:34px;height:34px;padding:0;border-radius:50%;
  justify-content:center;border:1px solid var(--border)}
/* `#navProfile{position:relative}` DICABUT 19 Agustus 2026 — ia menyebabkan
   bug yang dilaporkan pengguna: di HP, menekan ikon profil membuat halaman
   bisa digeser ke kanan dan menunya melebar ke samping alih-alih turun.
   Sebabnya aturan itu MENIMPA `.nav-item{position:static}` milik blok layar
   sempit; tanpa `static`, `left:12px;right:12px` diukur dari tombolnya yang
   selebar 34px di ujung kanan, bukan dari layarnya.
   Tidak ada penggantinya, dan memang tidak perlu: `#navProfile` sudah
   berkelas `.nav-item`, yang di layar lebar sudah `position:relative`.
   Aturan yang cuma mengulang yang sudah ada tapi berkekhususan lebih tinggi
   adalah cara paling sunyi membatalkan penimpaan di tempat lain. */
/* Menu profil menempel ke tepi KANAN — ia butir paling kanan di header, dan
   dropdown yang rata kiri menjulur keluar layar.
   Dibungkus @media lebar besar, dan itu WAJIB: di layar sempit sudah ada
   aturan yang membentangkannya `left:12px;right:12px` (lihat blok
   max-width:1024px di atas), dan aturan ini ditulis LEBIH BELAKANG dengan
   kekhususan yang sama — tanpa pembungkus ini ia menang di layar sempit dan
   membatalkan perbaikan itu diam-diam. Dijaga
   test_mobile_layout.py::test_tidak_ada_penambal_lama_yang_menimpa. */
@media (min-width:1025px){
  #navProfile .nav-dropdown{left:auto;right:0}
}

/* Di layar sempit rel tegak memakan lebar yang memang sudah kurang. Ia jadi
   baris mendatar yang bisa digeser, menempel di bawah header — bukan
   disembunyikan di balik tombol hamburger, karena menu yang harus dibuka
   dulu adalah menu yang tidak dipakai. */
@media (max-width:1024px){
  /* `overflow-x:auto` DICABUT 19 Agustus 2026 — ia yang membuat flyout
     Tools, Device Manager, dan Settings TIDAK BISA DIBUKA di HP (dilaporkan
     pengguna: "cuma bisa di klik saja").

     Sebabnya aturan CSS yang mudah dikira tidak berlaku: kalau SATU sumbu
     `overflow` bukan `visible`, sumbu satunya ikut dipaksa jadi `auto` —
     `overflow-y:visible` yang ditulis di sebelahnya diabaikan diam-diam.
     Relnya jadi kotak bergulung, dan flyout yang menggantung di bawahnya
     terpotong habis. Menunya terbuka; yang tidak ada cuma tempat untuk
     menggambarnya.

     Penggantinya `flex-wrap:wrap`: delapan ikon 40px + jarak = 334px, muat
     di layar 360px; yang lebih sempit membungkus jadi dua baris alih-alih
     digulung. Tidak ada yang tersembunyi di seberang tepi, dan tidak ada
     kotak bergulung yang memotong apa pun. */
  .rail{position:sticky;top:56px;left:auto;bottom:auto;width:auto;
    flex-direction:row;justify-content:flex-start;flex-wrap:wrap;
    border-right:none;border-bottom:1px solid var(--border);
    padding:4px 8px;gap:2px}
  main,.conn-bar,.lisensi-bar{margin-left:0}
  .rail-btn.aktif::before{left:8px;right:8px;top:auto;bottom:-6px;
    width:auto;height:3px;border-radius:3px 3px 0 0}
  /* Tooltip di layar sentuh tidak pernah terpicu — dan kalaupun terpicu, ia
     muncul menimpa ikon sebelahnya. Dimatikan; nama menunya ada di flyout. */
  /* Tombol lipat disembunyikan: di sini relnya sudah jadi baris mendatar di
     ATAS halaman, jadi melipatnya tidak mengembalikan lebar apa pun. Kendali
     yang hasilnya tidak terlihat lebih membingungkan daripada yang tiada. */
  .rail-lipat{display:none}
  .rail-tip{display:none}
  /* Flyout membentang selebar layar, sama seperti dropdown header lama —
     dan lewat `position:fixed`, bukan `absolute`. `.rail-item` masih
     `position:relative`, jadi `absolute` akan diukur dari tombol 40px dan
     menjulur keluar layar; `fixed` mengukurnya dari layar itu sendiri.
     `top` dipatok di bawah header (56px) + tinggi rel, bukan dihitung dari
     tombolnya, supaya kedelapan flyout muncul di garis yang sama. */
  .rail-flyout{position:fixed;top:118px;left:12px;right:12px;
    min-width:0;max-width:none}
}

/* Dropdown Profile lebih lebar daripada dropdown lain — isinya nama user dan
   lencana role, bukan cuma daftar tautan. Dibaca SESUDAH `.nav-dropdown` yang
   menyetel 210px: keduanya sama-sama satu kelas, jadi yang menang URUTAN,
   bukan kekhususan. 11x. */
.dd-profil{min-width:220px}

/* Tombol "← Kembali ke Dashboard" di atas isi halaman. 7x. */
.btn-kembali{background:none;border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:12px;color:var(--t2);cursor:pointer;margin-bottom:16px;display:inline-flex;align-items:center;gap:4px;text-decoration:none}

/* Kepala dan kaki kartu berbingkai (Kelola User, Riwayat Login, …). 2x & 4x.
   `.kartu-kaki` sengaja tanpa `line-height` — satu-satunya yang memakainya
   users.html, dan menariknya ke sini akan mengubah tiga kartu lain.

   `flex-shrink:0` DICABUT dari `.kartu-kepala` 7 Agustus. Ia ikut sejak awal
   karena dua halaman pertama kebetulan berkartu flex kolom, dan itu yang
   membuat alat pembukti MENOLAK potongan yang sama persis di device-add.html
   dan tampilan.html (lanjutan 10) — kartu di sana bukan anak flex, jadi
   menambahkannya mengubah nilai akhir delapan belas elemen. Sekarang ia
   `.tak-susut`, kelas terpisah yang dipasang hanya di kartu yang memang
   menggulung. Yang berubah cara menuliskannya, bukan tampilannya: kedua
   halaman lama ikut memasang `.tak-susut` di commit yang sama. */
.kartu-kepala{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
.kartu-kaki{padding:12px 20px;border-top:1px solid var(--border);font-size:10px;color:var(--t3);flex-shrink:0}
/* Di layar sempit kepala kartu MEMBUNGKUS, bukan memeras keterangannya.
   Ditemukan 10 September 2026 dengan menyapu seluruh halaman di peramban:
   kepala "Hasil Pemindaian" (Discovery) punya judul + keterangan + tombol
   Kolom + dua penyaring + tautan "Belum terdaftar" dalam satu baris flex
   tanpa `flex-wrap`. Yang bisa menyusut cuma `.akt-judul` (`min-width:0`),
   jadi ia yang dikorbankan — dan diukur, lebarnya benar-benar NOL sampai
   lebar layar 560px: keterangannya tergambar satu kata per baris, kepala
   kartunya 243px tinggi, dan sisanya TETAP meluber 52px keluar layar.
   Halaman Maps kena lebih ringan (keterangan 56px, kepala 138px).

   `min-width` pada judulnya yang membuat barisnya membungkus — tanpa itu
   `flex-wrap` tidak berbuat apa-apa, karena baris yang isinya boleh
   menyusut sampai nol tidak pernah "kepenuhan". `min(100%,220px)` supaya
   kepala kartu yang isinya cuma judul + lencana kecil tetap satu baris,
   dan supaya di layar 320px angkanya tidak jadi lebih lebar dari layarnya
   sendiri.

   AMBANGNYA 640px, dan itu keputusan yang diukur — bukan yang paling
   longgar. Versi pertama memakai 768px karena pada 720px keterangannya
   masih terperas jadi 151px. Tapi dipotret, pada 768px susunan LAMA justru
   lebih rapi: semuanya muat dalam satu baris, dan yang dihasilkan aturan
   ini di sana cuma "Belum terdaftar" yang turun sendirian. Memperbaiki
   yang tidak rusak ada harganya. */
@media(max-width:640px){
  .kartu-kepala{flex-wrap:wrap}
  .kartu-kepala>.akt-judul{min-width:min(100%,220px)}
}

/* Ikon di sebelah judul kartu. `inline-flex` supaya ia tidak menambah tinggi
   barisnya seperti `inline` pada SVG. 6x. */
.ic-inline{display:inline-flex;color:var(--t2)}

/* Judul kartu dan keterangan sebarisnya di bawahnya. 9x & 20x — `.t-sub`
   potongan gaya terbanyak kedua di seluruh aplikasi. */
.h-card{font-size:15px;font-weight:700;color:var(--t1)}
.t-sub{font-size:11px;color:var(--t3)}

/* Judul bagian di dalam kartu — huruf kecil, dijarangkan. 4x. */
.h-bagian{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--t3);margin-bottom:8px}

/* Kepala tabel di dalam kartu, dipisah TIGA sumbu karena ketiganya memang
   berubah sendiri-sendiri:

     .th-s              hurufnya — sama di semua kolom, semua halaman
     .th-tepi/.th-dalam jaraknya — 22px di kolom pertama & terakhir, 12px di
                        antaranya (kolom tepi sejajar dengan tepi kartu)
     .ta-l/.ta-c/.ta-r  perataannya

   Versi pertama menggabung jarak dan perataan (`.th-l/-c/-r`). Halaman kedua
   yang dikonversi langsung membawa kombinasi keempat (kiri + jarak dalam), dan
   itu memperjelas bahwa daftarnya akan terus bertambah selama dua hal yang
   berubah sendiri-sendiri ditulis sebagai satu kelas. 17x.

   `.ta-*` ditulis `th.`/`td.` — bukan `.` saja — karena
   `th:first-child{text-align:left}` berkekhususan (0,1,1) dan kolom pertama
   yang TIDAK rata kiri akan kalah diam-diam. Alasan lengkapnya di blok
   `td.sel-kosong` di bawah. */
.th-s{font-size:9px;text-transform:uppercase;color:var(--t3);font-weight:700}
.th-tepi{padding:8px 20px}
.th-dalam{padding:8px 12px}
/* Selektor kedua & ketiga tiap baris: kelas ini harus bisa dipakai DI DALAM
   `.det-tablewrap` juga. Kartu tabel itu menyetel perataan sendiri
   (`thead th{text-align:left}` dan `tbody td{text-align:left}`, keduanya
   berkekhususan (0,2,1)) — di atas `th.ta-c` yang cuma (0,1,1). Tanpa dua
   selektor tambahan, kolom Aktif dan Aksi di Kelola Perangkat diam-diam
   kembali rata kiri, dan gejalanya justru paling samar di kolom yang isinya
   pendek. Setelah kekhususannya disamakan, yang menang URUTAN — dan blok ini
   dibaca paling akhir. Sama persis dengan alasan `td.sel-kosong` di bawah. */
th.ta-l,td.ta-l,.det-tablewrap thead th.ta-l,.det-tablewrap tbody td.ta-l{text-align:left}
th.ta-c,td.ta-c,.det-tablewrap thead th.ta-c,.det-tablewrap tbody td.ta-c{text-align:center}
th.ta-r,td.ta-r,.det-tablewrap thead th.ta-r,.det-tablewrap tbody td.ta-r{text-align:right}

/* Baris tunggal yang mengisi seluruh tabel: "Gagal memuat data". 4x.

   Dulu bertiga. Yang dua pergi 16 Agustus 2026 bersama butir kerangka §4,
   dan bukan karena dirapikan — karena tidak ada lagi yang memakainya.
   `.sel-memuat` dan `.sel-kosong` sama-sama dipakai untuk baris "Memuat…"
   (tiga halaman memakai yang satu, satu halaman memakai yang lain — selisih
   yang tidak pernah disengaja), dan keduanya digantikan baris kerangka
   setinggi baris isi. Yang tersisa cuma keadaan gagal, yang memang tetap
   satu baris.

   Ditulis `td.` — bukan `.` saja — karena `td:first-child{text-align:left}`
   berkekhususan (0,1,1) dan sel ini SELALU anak pertama (ia ber-colspan).
   Kelas biasa (0,1,0) akan kalah dan tulisannya menepi ke kiri, padahal gaya
   inline yang digantikan menang mutlak. Setelah disamakan (0,1,1), yang
   menang urutan — dan blok ini dibaca paling akhir.

   Selektor keduanya untuk `.det-tablewrap` (Kelola Perangkat, 7 Agustus):
   `.det-tablewrap tbody td` berkekhususan (0,2,1), di atas `td.sel-gagal`
   (0,1,1). Tanpa itu, pesan gagal di halaman itu diam-diam kembali rata kiri
   dengan bantalan 14px. Deklarasinya tetap SATU, jadi kedua konteks mustahil
   menyimpang. */
td.sel-gagal,.det-tablewrap tbody td.sel-gagal{text-align:center;padding:24px;color:var(--red);font-size:12px}

/* Baris yang STATUSNYA berubah sejak polling sebelumnya (TAMPILAN §5).
   Berkedip SEKALI — `1`, bukan `infinite`. §5 melarang "apa pun yang
   mengulang terus-menerus", dan larangan itu punya alasan: yang berkedip
   terus berhenti dilihat, jadi ia bukan cuma mengganggu, ia juga berhenti
   berguna.

   Kuning, bukan merah/hijau. Yang ditandai adalah PERUBAHANNYA, bukan
   baik-buruknya — perangkat yang baru pulih dan yang baru mati sama-sama
   perlu dilihat, dan warnanya sendiri sudah dijawab kolom Status di baris
   yang sama. Memberi dua arti pada satu kedipan membuat keduanya kabur.

   `ease-out`: paling terang di awal lalu memudar. Kebalikannya (`ease-in`)
   menarik mata SESUDAH orang berhenti memandang, yaitu waktu yang salah. */
.baris-kedip{animation:kedipBaris .9s ease-out 1}
@keyframes kedipBaris{from{background:rgba(245,158,11,.28)}to{background:transparent}}

/* ── Sel dua sisi di tabel Status Perangkat (7 Agustus) ──────────────────
   Sinyal, Status LAN, dan CPU menampilkan hal yang sama bentuknya: satu angka
   sisi AP, panah, satu angka sisi ST, lalu satuannya di bawah. Ketiganya
   ditulis ulang per sel sampai kolom CPU ditambahkan — dan penambahan itu
   yang membuat batas gaya inline §2.1 merah. Menaikkan batasnya berarti
   membiarkan kolom keempat nanti menyalin lagi. */
.sel-2sisi{font-family:var(--mono);font-size:11px;display:flex;align-items:center;justify-content:center;gap:4px;white-space:nowrap}
.t-tebal{font-weight:600}
.sisi-panah{color:var(--t3);font-size:9px}
.sel-satuan{font-size:8px;color:var(--t3);margin-top:1px}

/* Baris duplex Status LAN — satu slot per SISI, bukan satu nilai gabungan
   (20 Agustus 2026). Susunannya sengaja kembar dengan `.sel-2sisi` di atasnya
   supaya FDX/HDX jatuh lurus di bawah angka sisinya masing-masing; yang beda
   cuma ukurannya, dan itu datang dari `.sel-satuan` yang dipakai bersamanya.
   Panahnya ikut mengecil jadi seukuran nilainya — 9px di antara dua nilai 8px
   membuat pemisahnya lebih berat daripada yang dipisahkan. `inherit`, bukan
   angka baru: skala ukuran huruf berhenti di 14 nilai dan dijaga uji. */
.sel-dup{font-family:var(--mono);display:flex;align-items:center;justify-content:center;gap:4px;white-space:nowrap}
.sel-dup .sisi-panah{font-size:inherit}
/* HDX berwarna: kolom ini DIPINDAI, dan setengah-duplex satu-satunya nilai di
   sini yang menuntut tindakan. Kuningnya sejalan dengan lanColor() — "jalan,
   tapi tidak sebagaimana mestinya". */
.dup-hdx{color:var(--amber);font-weight:600}

/* Sel bertumpuk: dua nilai sejenis, satu di atas satunya — Capacity (↑UL /
   ↓DL) dan Uptime (sisi AP / sisi ST). Bentuknya sudah sama persis di kedua
   kolom, tapi dulu ditulis sebagai gaya inline kembar di dashboard.html.

   Yang membuatnya perlu jadi kelas sekarang: `white-space:nowrap` per BARIS.
   Tanpa itu "↑380.2 Mbps" patah jadi "↑380.2" / "Mbps" — terbaca seperti dua
   nilai — dan "3d 01:48:01" patah di tengah jamnya jadi "3d 01:48" / ":01",
   yang bukan cuma jelek tapi salah baca (bug9). nowrap-nya di anak, bukan di
   wadahnya: tumpukan atas-bawahnya justru yang diinginkan. */
/* `.sel-tumpuk` DIBUANG 21 Agustus 2026 — nol pemakai sejak kolom Capacity
   & Uptime berganti bentuk (`.cap-baris`, `.sel-satuan`). */

/* Keterangan kecil di bawah sebuah nilai — dipakai halaman Detail untuk
   menyebut ASAL angka CPU-nya (rata-rata dari pencacah, atau dari cuplikan).
   Nilai tanpa keterangan asalnya adalah nilai yang tidak bisa ditimbang. */
.t-ket{font-size:9px;color:var(--t3);line-height:1.35;margin-top:3px}
/* Varian yang bisa diklik — dipakai ruas CPU di halaman Detail untuk menuju
   grafik trennya. `display:flex` supaya ikonnya sebaris dengan tulisannya
   tanpa menambah tinggi baris. */
.t-tautan{display:inline-flex;align-items:center;gap:4px;color:var(--blue);text-decoration:none}
.t-tautan:hover{text-decoration:underline}

/* ── Kontrol paginasi (7 Agustus) ────────────────────────────────────────
   Dipakai tiga daftar: Status Perangkat, Kelola Perangkat, Detail Perangkat.
   Bentuknya ditulis sekali di app.js (pagerKontrolHTML) dan gayanya sekali di
   sini — kalau tidak, tiga daftar yang sama fungsinya akan pelan-pelan
   terlihat berbeda. */
/* `flex:1` supaya ia mengisi sisa baris: keterangan "1–10 / 134" menempel
   kiri (lewat margin-right:auto di bawah), tombolnya menempel kanan. */
.pager{display:flex;align-items:center;gap:4px;flex-wrap:wrap;flex:1}
.pager-info{font-size:11px;color:var(--t3);white-space:nowrap;margin-right:auto}
.pager-ukuran{background:var(--bg-3);border:1px solid var(--border);border-radius:5px;padding:3px 4px;font-size:11px;color:var(--t2);font-family:var(--ui);cursor:pointer;outline:none}
.pager-btn{padding:3px 8px}
.pager-nomor{font-size:11px;color:var(--t2);padding:3px 8px;background:var(--bg-3);border-radius:5px;border:1px solid var(--border);display:inline-flex;align-items:center;gap:1px}
/* Kotak nomor halaman sengaja tetap terlihat seperti tulisan biasa — yang
   membedakannya cuma kursor dan garis putus tipis saat disorot. Orang yang
   tidak membutuhkannya tidak perlu merasa ada yang berubah. */
.pager-nomor input{width:2.2em;background:none;border:none;border-bottom:1px dashed var(--border2);color:var(--t1);font-family:var(--mono);font-size:11px;text-align:center;padding:0;outline:none;cursor:text}
.pager-nomor span{color:var(--t3)}
/* Varian yang duduk DI BAWAH tabel (halaman Detail Perangkat), bukan di
   baris pencarian: toolbar halaman itu sudah berisi pencarian dan dua
   penyaring, dan baris keempat di sana menenggelamkan kotak pencariannya. */
.pager-bawah{padding:8px 12px;border-top:1px solid var(--border)}
/* Varian yang duduk tepat di bawah kotak pencarian (Kelola Perangkat). */
/* `.pager-bawah-cari` DIBUANG 21 Agustus 2026 — nol pemakai. */

/* ── Cangkang modal (7 Agustus) ──────────────────────────────────────────
   Tiga modal memakai bentuk yang sama: Riwayat Perubahan & Edit Device di
   Kelola Perangkat, dan Perangkat Dinonaktifkan di Dashboard. Ketiganya
   ditulis ulang utuh —
   lapisan gelap, kotak putih, kepala berjudul, tombol ✕, kaki keterangan.

   Yang TIDAK ikut ke kelas: `display` dan `max-width`.

   `display` tinggal inline (`style="display:none"`) karena JS yang membukanya
   menyetel `.style.display='flex'` — memindahkannya ke kelas berarti mengubah
   cara membukanya, dan itu bukan konversi gaya lagi. Ia juga satu-satunya
   properti yang memang berbeda menurut keadaan, bukan menurut halaman.

   `max-width` memang berbeda per modal (560 / 520 / 440 px) — isinya yang
   menentukan, dan menyeragamkannya adalah perubahan tampilan (§2.2). */
.modal-lapis{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;align-items:center;justify-content:center;backdrop-filter:blur(3px)}
.modal-kotak{background:var(--bg-1);border:1px solid var(--border);border-radius:14px;width:100%;box-shadow:0 24px 64px rgba(0,0,0,.5);animation:popup-masuk .14s ease-out}
/* Popup meluncur pendek saat membuka — TAMPILAN §5, 21 Agustus 2026.

   TIDAK ada satu baris JS pun untuk ini, dan itu disengaja: popup
   dibuka dengan `display:none` → `flex` lewat bukaPopup(), dan animasi
   CSS memang MULAI ULANG tiap kali elemennya berubah dari `none` jadi
   terlihat. Menambahkan kelas lewat JS akan menuntut kelas itu dicabut
   lagi saat menutup — satu langkah yang kalau terlewat membuat popup
   berikutnya tidak beranimasi, dan tidak beranimasi itu tidak terlihat
   seperti kesalahan.

   6px, bukan 20px: yang perlu disampaikan "kotak ini baru muncul",
   bukan perjalanannya. Luncuran panjang menunda pembacaan isi popup
   yang justru baru saja diminta orangnya. */
@keyframes popup-masuk{
  from{opacity:0;transform:translateY(6px) scale(.985)}
  to  {opacity:1;transform:none}
}
/* Modal yang isinya bisa lebih panjang daripada layar: tingginya dibatasi dan
   isinya (`.modal-isi`) yang menggulung, bukan halamannya. Modal Edit Device
   TIDAK memakainya — formnya pendek dan tetap sebagaimana adanya sebelum
   konversi ini; menambahkannya sekalian akan mengubah tampilan. */
.modal-tinggi{max-height:80vh;display:flex;flex-direction:column}
.modal-kepala{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
.modal-tutup{background:none;border:none;color:var(--t3);font-size:20px;cursor:pointer;padding:4px 8px;border-radius:6px}
.modal-isi{padding:12px 20px;overflow-y:auto;flex:1}
/* Kaki modal yang berisi tombol, bukan keterangan (keterangan memakai
   `.kartu-kaki` yang sudah ada). 2x. */
.modal-kaki{padding:12px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px}
/* Dipisah dari `.modal-kepala` karena hanya berlaku saat wadahnya flex kolom
   (`.modal-tinggi`) — kepala modal Edit Device memakai potongan yang sama
   PERSIS kecuali properti ini. Digabung, konversi itu akan mengubah nilai
   akhir elemen yang tidak seharusnya tersentuh. Pelajaran yang sama dengan
   `flex-shrink` di `.kartu-kepala` (lanjutan 10). */
.tak-susut{flex-shrink:0}

/* ── Form di dalam modal ─────────────────────────────────────────────────
   `.f-label` & `.f-input` sudah ada sejak halaman pertama; dua di bawah
   melengkapinya supaya satu form utuh tidak lagi separuh kelas separuh
   inline. */
/* Dua kotak isian berdampingan (IP AP/ST, Username/Password). 3x. */
.f-baris{display:flex;gap:8px}
.f-kolom{flex:1}
/* Label kotak centang — teksnya di kanan kotaknya, bukan di atasnya seperti
   `.f-label`. 2x di sini, 1x lagi di device-add.html dengan tambahan sendiri. */
.f-cek{display:flex;align-items:center;gap:4px;font-size:12px;color:var(--t2)}
/* Baris pesan galat di bawah form. `min-height` menahannya tetap memakan
   tempat walau kosong: tanpa itu seluruh form melompat ke atas 14px saat
   galatnya hilang. 3x. */
.f-err{font-size:11px;color:var(--red);min-height:14px}

/* Peringatan di dalam form — sekeluarga dengan `.f-err`, warnanya amber.
   Bedanya bukan kosmetik: merah berarti "ditolak, perbaiki dulu", amber
   berarti "boleh diteruskan, tapi lihat ini dulu". Satu-satunya pemakainya
   sekarang peringatan nama BTS yang beda satu huruf (bts.html), dan di sana
   perbedaan itulah yang menentukan apakah orang menekan Simpan lagi atau
   membetulkan ketikannya. Tanpa `min-height` seperti `.f-err`: kotak ini
   memang tidak selalu ada, dan tempat kosong permanen di dalam modal untuk
   peringatan yang jarang muncul lebih mengganggu daripada modal yang
   meninggi saat ia muncul. */
/* `display:none` bawaannya — JS yang membukanya saat ada yang perlu
   dikatakan, aturan yang sama dengan `.hal-catatan` dan `.dd-unduh`. */
.f-peringatan{display:none;font-size:11px;line-height:1.5;color:var(--amber);
  background:rgba(245,158,11,.10);border:1px solid rgba(245,158,11,.35);
  border-radius:6px;padding:8px}
.f-peringatan .ic{vertical-align:-2px}

/* Pasangan tombol di kaki form. `.btn-utama` 3x, `.btn-batal` 1x — tapi
   keduanya selalu bersebelahan, dan tombol batal yang tertinggal inline
   adalah tombol yang pelan-pelan berbeda tinggi dari tombol di sebelahnya. */
.btn-batal{background:none;border:1px solid var(--border);border-radius:8px;padding:8px 16px;font-size:12px;color:var(--t2);cursor:pointer}
.btn-utama{background:var(--blue-permukaan);border:none;border-radius:8px;padding:8px 16px;font-size:12px;font-weight:700;color:var(--fg-aksen);cursor:pointer}
/* Merah, dan bukan sekadar "tombol utama dengan warna lain": tombol yang
   menghapus 87 perangkat harus terbaca berbeda dari tombol yang menyimpan
   satu form, dari sudut mata sekalipun.

   Ditulis sebagai selektor gabungan karena ia memang cuma berlaku untuk
   tombol utama — kelas warna yang berdiri sendiri akan menjadi kembaran
   `.legenda-larang`, dan kembaran itu berarti mengubah warna kotak legenda
   peta spektrum ikut mengubah tombol Hapus. */
.btn-utama.btn-bahaya{background:var(--red-permukaan);color:var(--fg-permukaan)}
/* Tombol yang menunggu — dipakai kotak konfirmasi hapus, yang baru menyala
   sesudah jumlahnya diketik. Tanpa tanda ini, tombol yang tidak bereaksi
   terbaca sebagai aplikasi yang macet. */
.btn-utama:disabled{opacity:.45;cursor:not-allowed}

/* Dua warna teks yang paling sering ditulis ulang di seluruh aplikasi:
   `color:var(--t3)` 14x dan `color:var(--blue)` 5x. Keduanya sengaja TANPA
   properti lain — begitu sebuah kelas warna ikut membawa ukuran, ia berhenti
   bisa dipasang di tempat berikutnya dan lahirlah kelas kembar. */
.t-redup{color:var(--t3)}
.t-biru{color:var(--blue)}

/* Potongan teks yang hanya ada di HP. Dipakai untuk angka yang di layar lebar
   sudah punya kolom sendiri, tapi di layar sempit kolomnya berada di luar
   layar — jadi angkanya ikut menempel di sel pertama, bukan hilang. Aturan
   yang menyalakannya ada di blok ≤640px, bersama aturan yang menyembunyikan
   kolom aslinya; keduanya harus selalu berpasangan, kalau tidak angkanya
   tampil dua kali.

   Ditulis sebagai ≥641px, bukan `display:none` tanpa syarat: kedua sisi
   ambangnya jadi terbaca berpasangan di sini dan di blok ≤640px, dan tidak
   ada satu pun lebar layar di mana keduanya sama-sama berlaku. */
@media(min-width:641px){
  .hp-saja{display:none}
}
/* Dua lagi dari dashboard: `.t-terang` dipakai tautan di menu unduh — tanpa
   itu ia mengambil warna tautan bawaan, bukan warna teks biasa. */
.t-terang{color:var(--t1)}
.t-merah{color:var(--red)}
/* Dan yang keempat, dari halaman Pengaturan: penjelasan tiga status zona
   menyebut "pelanggaran" merah dan "perhatian" kuning dalam satu kalimat.
   Dua kata sederajat yang gayanya ditulis dengan dua cara berbeda adalah
   persis cara keduanya nanti tidak lagi sederajat. */
.t-amber{color:var(--amber)}
/* Catatan di bawah kolom IP form Tambah Perangkat. Jaraknya diatur
   lewat selektor turunan, bukan kelas baru — `{margin-top:4px}`
   sendirian akan jadi kelas yang isinya sama dengan kelas lain, dan
   TestTidakAdaKelasKembar menolaknya. */
#devPtmpNote div{margin-top:4px}
/* Dan satu dari Bandwidth Test. Ia lahir berpasangan dengan `.t-biru`, bukan
   sendirian: kolom TX rata-rata biru dan RX rata-rata hijau adalah satu
   pasangan yang dibaca bersebelahan, dan dua kolom yang gayanya ditulis
   terpisah adalah persis cara keduanya nanti berbeda tanpa ada yang sadar. */
.t-hijau{color:var(--green)}
/* Waktu kecil di bawah sebuah nama (kolom Ditambahkan & Terakhir Diubah).
   3x — dan ketiganya harus tetap sama, karena kedua kolom itu memang dibaca
   sebagai satu pasangan. */
.t-jejak{font-size:9px;color:var(--t3);font-family:var(--mono)}

/* ── Baris kendali kartu Log, dashboard (7 Agustus) ──────────────────────
   Enam kontrol berjajar di satu baris — pencacah, pemilih jenis, alih
   Live/History, dan tombol unduh dengan menunya. Gayanya ditulis sebagai
   blok inline berbaris-baris, yang bentuk terburuknya: dua tombol alih yang
   HARUS kembar ditulis dua kali, dan tidak ada yang bisa memastikan keduanya
   tetap kembar. */
.log-lencana{background:var(--bg-3);border:1px solid var(--border);border-radius:6px;padding:3px 8px;white-space:nowrap;flex-shrink:0;font-size:10px;font-family:var(--mono);color:var(--t2)}
.log-jenis{background:var(--bg-2);border:1px solid var(--border);border-radius:8px;padding:4px;font-size:11px;font-weight:600;color:var(--t1);font-family:var(--ui);cursor:pointer;outline:none;white-space:nowrap}
.log-alih{display:flex;background:var(--bg-2);border:1px solid var(--border);border-radius:8px;padding:2px;gap:2px}
/* `background` dan `color` TIDAK ikut ke sini: switchLog() menukar keduanya
   lewat `.style` saat tombolnya dipilih, jadi keduanya memang keadaan, bukan
   tampilan diam. Yang di kelas justru yang harus kembar selamanya. */
.log-alih-btn{border:none;border-radius:5px;padding:4px 8px;font-size:11px;font-weight:600;cursor:pointer;font-family:var(--ui);transition:all .15s;white-space:nowrap}
.btn-unduh{display:flex;align-items:center;gap:3px;white-space:nowrap;background:rgba(6,182,212,.1);border:1px solid rgba(6,182,212,.3);border-radius:7px;padding:4px 8px;font-size:12px;font-weight:600;color:var(--cyan);cursor:pointer;font-family:var(--ui);transition:all .15s}
/* `display` tetap inline — toggleDownloadMenu() membukanya dengan
   `.style.display='block'`, alasan yang sama dengan `.modal-lapis`. */
.dd-unduh{position:absolute;right:0;top:34px;background:var(--bg-1);border:1px solid var(--border);border-radius:9px;min-width:170px;z-index:1000;box-shadow:0 8px 24px rgba(0,0,0,.4);overflow:hidden}
.dd-judul{padding:4px 8px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--t3);border-bottom:1px solid var(--border2)}
/* Lima pilihan unduhan. WARNANYA sengaja tidak di sini: tiga hitam, satu biru,
   satu merah — dan warna yang ikut ke kelas berarti dua dari lima harus
   menimpanya lagi, yaitu keadaan yang justru mau dihindari. */
.dd-opsi{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:12px;text-decoration:none;transition:background .1s}
.dd-pisah{border-top:1px solid var(--border2)}

/* Penyaring rentang waktu History. `.dt-input`/`.tm-input` sudah ada di atas
   (keduanya cuma menyetel `color-scheme` dan ikon pemilih tanggal); ukurannya
   yang selama ini ditulis inline ditarik ke sini — dibaca sesudah aturan itu,
   jadi keduanya menumpuk, bukan bertabrakan. Kembar kecuali warna: tanggal
   lebih terang daripada jamnya.

   YANG DI KELAS TELANJANG hanya huruf & warnanya, karena `.dt-input` TIDAK
   cuma dipakai di sini: halaman Tren memakainya juga, dengan kotak yang
   digambar inputnya sendiri. Latar, bingkai, dan lebar adalah keputusan
   `.filter-kotak` — di sini kotaknya digambar induknya, jadi inputnya harus
   polos, dan keempatnya dipatok lebarnya supaya sejajar.

   Ketiga properti itu sempat ikut ke `.dt-input` telanjang (7 Agustus) dan
   diam-diam MEMBERI lebar 110px kepada dua kotak tanggal halaman Tren yang
   sebelumnya selebar isinya — pelajaran `.tak-susut` lagi, dengan cara gagal
   yang baru: kelas yang sudah dipakai halaman LAIN tidak bisa dibuktikan oleh
   pemeriksaan satu halaman. Lihat TestKotakTanggal. */
.filter-kotak{display:flex;align-items:center;gap:4px;background:var(--bg-1);border:1px solid var(--border);border-radius:7px;padding:4px 8px}
.dt-input{font-size:11px;color:var(--t1);font-family:var(--mono);outline:none}
.tm-input{font-size:11px;color:var(--t2);font-family:var(--mono);outline:none}
.filter-kotak .dt-input,.filter-kotak .tm-input{background:none;border:none}
.filter-kotak .dt-input{width:110px}
.filter-kotak .tm-input{width:74px}
.btn-cari{background:var(--blue-permukaan);border:none;border-radius:7px;padding:4px 16px;font-size:12px;font-weight:600;color:var(--fg-aksen);cursor:pointer;font-family:var(--ui);display:flex;align-items:center;gap:4px}
.btn-reset{background:none;border:1px solid var(--border);border-radius:7px;padding:4px 12px;font-size:12px;color:var(--t2);cursor:pointer;font-family:var(--ui)}

/* Satuan kecil di bawah judul kolom ("dBm", "Mbps", "%"). `.t-tipis` dipisah
   karena ia MENCABUT tebal yang datang dari induknya, bukan menyetel ukuran —
   dipakai keterangan "(opsional)" di label halaman Bandwidth Test. */
.t-satuan{font-size:8px;opacity:.7}
/* Sisi yang bisa diklik di dalam satuan itu ("AP"/"ST", "↑ UL"/"↓ DL") — tiap
   sisi mengurutkan kolomnya menurut angkanya sendiri. Garis titik-titik, bukan
   warna aksen: label ini duduk di dalam judul yang SELURUHNYA sudah bisa
   diklik, jadi yang perlu diberitahukan bukan "ini bisa diklik" melainkan
   "yang ini sasaran tersendiri". Garis bawah penuh akan terbaca sebagai
   tautan ke halaman lain. */
.t-sisi{font-weight:inherit;cursor:pointer;border-bottom:1px dotted currentColor}
.t-tipis{font-weight:400}
/* `font-size:10px;color:var(--t3)` — 7x di seluruh aplikasi: satuan di ujung
   angka (dBm, MHz, Mbps, %) di Detail Perangkat, label "Dari"/"Sampai" di
   penyaring dashboard, dan keterangan di bawah kotak isian.

   Kelas ini sempat lahir DUA KALI. `.sat-kecil` sudah ada sejak halaman Tren
   dengan deklarasi yang sama persis, dan tidak ketahuan saat `.t-kecil`
   ditambahkan sehari kemudian — persis kegagalan yang §2.1 ada untuk
   mencegahnya, cuma satu tingkat lebih tinggi: bukan gaya yang kembar, tapi
   KELASNYA. Yang tersisa satu; namanya dipilih yang tidak menyebut "satuan",
   karena setengah pemakaiannya bukan satuan.

   Jangan tertukar dengan `.t-satuan` (8px) — itu satuan di bawah judul kolom
   tabel dashboard, ukuran yang memang berbeda. */
.t-kecil{font-size:10px;color:var(--t3)}

/* ── Kartu berbingkai, dan halaman Tampilan (7 Agustus) ──────────────────
   `.kartu` adalah bingkai yang sama di enam tempat: Kelola User, Riwayat
   Login, Tambah Perangkat, dan tiga kartu di halaman Tampilan. Yang BERBEDA
   di tiap tempat cuma lebarnya (640/900/760/1180px) dan apakah ia flex kolom
   — keduanya tetap inline, karena keduanya memang keputusan per halaman.
   Bayangannya lebih ringan daripada `.modal-kotak`: kartu duduk di halaman,
   modal mengambang di atasnya. */
.kartu{background:var(--bg-1);border:1px solid var(--border);border-radius:14px;box-shadow:0 4px 24px rgba(0,0,0,.2)}
/* Isi kartu. `gap` tidak ikut — 18/14/12px di tiga kartu halaman Tampilan,
   dan menyeragamkannya perubahan tampilan (§2.2). */
.kartu-isi{padding:16px 20px;display:flex;flex-direction:column}

/* Baris kontrol yang membungkus. `.f-bungkus` dipisah dari `.f-baris` karena
   dua dari empat baris di halaman ini TIDAK membungkus. */
.f-bungkus{flex-wrap:wrap}
/* Judul kecil bercetak tebal — nama tema & nama skala di daftar pilihan. 2x. */
.t-judul{font-size:12px;font-weight:700}
/* Label kapital di atas angka besar. Dipakai BERSAMA `.t-kecil` yang memberi
   ukuran & warnanya — sengaja bukan `.h-bagian`, yang letter-spacing-nya .06em
   dan membawa margin bawah. */
.t-kapital{text-transform:uppercase;letter-spacing:.05em}

/* Pratinjau tema: empat lencana warna dan barisnya. Warna tiap lencana tetap
   inline — empat warna berbeda adalah ISI pratinjaunya, bukan gayanya. */
.pil-pratinjau{font-size:9px;font-weight:700;padding:3px 8px;border-radius:4px}
/* `.pratinjau-baris` DIHAPUS 21 Agustus 2026 — kembar dengan
   `.baris-sejajar`, yang isinya sama persis walau urutan deklarasinya
   berbeda. Itu juga sebabnya ia sempat lolos: mata membaca urutan. */
.pratinjau-angka{font-size:24px;font-weight:700;font-family:var(--mono)}

/* ── Kelola Perangkat memakai bentuk Detail Perangkat (7 Agustus) ────────
   Halaman ini dulu satu kartu berbingkai selebar 1100px berisi kepala, kotak
   pencarian, tabel, dan kaki — sementara Detail Perangkat yang isinya sejenis
   memakai toolbar lepas + kartu tabel selebar layar. Dua halaman bersebelahan
   di menu yang sama, dua bentuk yang berbeda.

   Yang dipindahkan cuma bentuknya: kolom, isi, dan tombolnya tetap sama
   persis. */

/* Judul halaman — pengganti kepala kartu. Detail Perangkat tidak punya judul
   sama sekali; di sini ia dipertahankan karena subjudulnya menyebutkan hal
   yang tidak tertulis di tempat lain. */
.hal-judul{display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap;row-gap:8px}
/* Jalan pulang duduk di ujung kanan baris judul, bukan sebagai baris
   tersendiri di atasnya — dua baris kepala membuat halaman ini mulai jauh
   lebih rendah daripada Detail Perangkat yang langsung membuka toolbar.
   `margin-bottom:0` mencabut jarak bawaan `.btn-kembali` yang dirancang
   untuk tautan yang berdiri sendiri; tanpa itu barisnya terlihat miring. */
.btn-kembali-kanan{margin-left:auto;margin-bottom:0;flex-shrink:0}

/* Catatan sebaris di atas tabel (izin role, sorotan perangkat baru).
   `display:none` bawaannya: keduanya baru ditampilkan JS kalau memang ada
   isinya, dan kotak kosong berbingkai lebih mengganggu daripada tidak ada. */
.hal-catatan{display:none;padding:8px 12px;margin-bottom:12px;border:1px solid var(--border);
  border-radius:8px;background:var(--bg-2);font-size:11px;color:var(--t3)}
.hal-catatan-hijau{background:rgba(34,197,94,.10);border-color:rgba(34,197,94,.35);
  color:var(--green);font-size:12px}
/* Keterangan "angka di halaman ini simulasi" pada instansi demo (26 Agustus
   2026). Kuning, bukan hijau dan bukan merah: ini bukan kabar baik, dan bukan
   kerusakan — ia peringatan tentang APA YANG SEDANG DIBACA. Ukuran hurufnya
   12px seperti kerabat hijaunya; 11px bawaan `.hal-catatan` terlalu kecil
   untuk kalimat yang memang harus dibaca sebelum orang mempercayai angkanya.
   Tampil/tidaknya diurus pasangCatatanDemo() di app.js — di instalasi
   sungguhan wadahnya tidak pernah terisi dan `display:none` bawaan berlaku. */
.hal-catatan-demo{background:rgba(245,158,11,.10);border-color:rgba(245,158,11,.35);
  color:var(--amber);font-size:12px;line-height:1.55}
.hal-catatan-demo b{color:var(--amber)}
/* Di halaman Pointing keterangannya duduk DI DALAM kepala halaman (lihat
   alasannya di pointing.html), dan kepala itu sudah mengurus jarak ke bawah
   sendiri — margin bawaan `.hal-catatan` akan menumpuk di atasnya. */
.pt-kepala .hal-catatan-demo{margin-top:8px;margin-bottom:0}

/* Catatan kaki halaman — dulu kaki kartu. */
.hal-kaki{margin-top:12px;font-size:10px;color:var(--t3);display:flex;align-items:center;gap:4px}

/* Ikon di dalam kotak pencarian toolbar. Detail Perangkat tidak punya ikon di
   kotaknya; ditambahkan di sini karena kotak yang sama sudah dipakai
   dashboard dengan ikon, dan dua bentuk untuk satu kotak pencarian adalah hal
   yang sedang dibereskan, bukan ditambah. */
.det-search-ic{display:inline-flex;color:var(--t3);flex-shrink:0}

/* Tombol utama di toolbar (mis. "+ Tambah Perangkat"). Bentuknya `.det-refresh`
   — ukuran dan sudut yang sama — dengan warna terisi, supaya satu tindakan
   yang menambah data tidak terlihat setara dengan tombol biasa. */
/* `text-decoration:none` bukan hiasan: di Detail Perangkat `.det-refresh`
   selalu dipasang pada <button>, yang memang tidak punya garis bawah. Tombol
   ini <a> — ia menuju halaman lain — jadi browser menggarisbawahi tulisannya,
   dan tombol biru bergaris bawah terbaca seperti tautan yang salah gaya.
   Terlihat di tangkapan layar pengguna, bukan dari membaca kodenya. */
.det-utama{background:var(--blue-permukaan);border-color:var(--blue-permukaan);color:var(--fg-aksen);text-decoration:none}
.det-utama:hover{background:var(--blue-permukaan);border-color:var(--blue-permukaan);color:var(--fg-aksen);filter:brightness(1.08)}

/* Tabel 8 kolom ini lebih lebar daripada tabel Detail Perangkat, jadi
   `--tmin`-nya sendiri yang berlaku — bukan 680px milik `.det-tablewrap`.
   Kekhususan (0,2,1) mengalahkan keduanya, jadi hasilnya tidak bergantung
   urutan penulisan. */
.det-tablewrap.mtable-dev table{min-width:var(--tmin,990px)}

/* ── Tabel daftar Detail Perangkat ────────────────────────────────────────
   900px milik `.det-tablewrap` sudah tidak cukup sejak kolom CPU (7 Agustus),
   dan sejak kolom BTS/POP (12 Agustus) ia menjadi dua belas kolom. Yang
   terjadi pada 900px BUKAN kolom yang menyempit rapi: tata letaknya `auto`,
   jadi kolom yang kekurangan tempat memecah ISINYA — nama "APK M2-M1" jatuh
   jadi tiga baris, "10.2.1.26" patah di tengah alamat jadi "10.2.1" + ".26",
   dan SSID "orangtua" jadi tiga suku kata bertumpuk. Dilaporkan pengguna 12
   Agustus 2026 dengan tangkapan layarnya (bug14).

   1500 = jumlah kebutuhan kedua belas kolomnya: Nama 240, Kategori 88,
   BTS 100, Lokasi 180, IP 92, Frekuensi 86, SSID 100, Sinyal 104, LAN 118,
   CPU 110, Status 126, Aksi 150 = 1494, dibulatkan ke atas. Angka per kolom
   diturunkan dari isi TERPANJANG yang mungkin muncul di sana, sama seperti
   `.tscroll table` di dashboard — bukan dari jumlah kolomnya.

   Nama naik 160 → 240 pada 14 Agustus 2026 (dilaporkan pengguna, bug21).
   160 adalah taksiran; 240 diukur dari 135 perangkat yang benar-benar ada:
   separuh namanya 15 karakter, 90% ≤26, terpanjang 35 ("Yellow Bee Tangerang
   (Quanta/Higen)"). 240px dikurangi bantalan 28px dan ikon 22px menyisakan
   190px teks — cukup untuk 26 karakter pada 13px semibold, jadi sembilan
   dari sepuluh nama muat utuh.

   Angka anggaran saja TIDAK cukup, dan itulah sebab bug21 masih terjadi
   sesudah 1360 dipasang: `min-width` hanya lantai untuk TABELNYA, sementara
   tata letak `auto` tetap bebas membagi ruang antar kolom. Kolom Lokasi
   berisi koordinat mentah 40 karakter tanpa tempat patah yang wajar, dan ia
   mengambil jauh lebih banyak daripada jatah 120px-nya — persis dengan
   mengorbankan tetangganya. Karena itu jatah Nama dipatok pada kolomnya
   sendiri, bukan cuma dihitung di komentar ini.

   Konsekuensinya geseran di HP 375px jadi ~3,6 layar. Itu ditempuh sadar,
   dengan pertukaran yang sama yang sudah diterima di seluruh tabel lain di
   berkas ini: kolom yang bisa digeser masih terbaca, kolom yang diperas
   tidak. Yang paling sering dicari — nama, kategori, dan (di HP) BTS-nya —
   tetap berada di layar pertama tanpa digeser sama sekali. */
.det-tablewrap.mtable-detail table{min-width:1500px}

/* Jatah kolom Nama, dipatok di kolomnya sendiri — lihat hitungannya di atas.
   `nowrap` pada baris namanya yang mengurus 10% sisanya: kolomnya melar
   mengikuti nama terpanjang alih-alih memenggalnya. Yang dipenggal bukan
   sekadar jelek — "APK-HMP (Main)" terbaca sebagai tiga baris yang berbeda,
   dan tinggi barisnya ikut menarik seluruh kolom di sebelahnya.
   Baris kedua (radio-id · BTS) sengaja TIDAK ikut nowrap: ia keterangan, dan
   membiarkannya membungkus jauh lebih murah daripada melebarkan kolom untuk
   sesuatu yang tidak dicari orang.
   Dikunci ke `.mtable-detail`: `.nama-baris` juga dipakai Kelola Perangkat
   dan Kelola BTS, yang lebar kolomnya lain lagi. */
.det-tablewrap.mtable-detail thead th:first-child,
.det-tablewrap.mtable-detail tbody td:first-child{min-width:240px}
.det-tablewrap.mtable-detail .nama-baris{white-space:nowrap}
/* Di HP kolom BTS/POP disembunyikan (blok ≤640px), jadi jatahnya 100px ikut
   dilepas. Aturannya ditulis DI SINI, bukan bersama saudara-saudaranya di blok
   ≤640px: `@media` tidak menambah kekhususan, jadi `.mtable-detail table` di
   sana (0,1,1) akan kalah oleh baris di atas ini (0,2,1) betapapun sempit
   layarnya. Selektor yang sama persis, ditulis sesudahnya, adalah satu-satunya
   bentuk yang benar-benar berlaku. */
@media(max-width:640px){
  .det-tablewrap.mtable-detail table{min-width:1400px}
}

/* Sel IP di tabel itu. `nowrap` yang menjadi intinya: sebuah alamat IP tidak
   punya tempat patah yang benar, jadi membiarkannya membungkus berarti satu
   alamat terbaca sebagai dua angka yang berbeda. */
.sel-ip{font-family:var(--mono);font-size:12px;color:var(--t2);white-space:nowrap}

/* Sel Lokasi di tabel itu (bug21, dilaporkan bersama kolom Nama).
   104 dari 135 perangkat mengisinya dengan sepasang koordinat mentah —
   "-6.191040628743995, 106.74142425244503", 38–39 karakter. Aturan global
   `td{word-break:break-word}` mengizinkan patahan DI DALAM angka, dan pada
   kolom sesempit itu hasilnya empat baris pecahan digit yang tidak terbaca
   sebagai koordinat sama sekali.

   `word-break:normal` mengembalikan satu-satunya tempat patah yang masuk
   akal: spasi sesudah koma — jaring pengaman kalau isinya ternyata bukan
   koordinat dan memang panjang.

   180px karena yang ditampilkan sekarang PENDEK. app.js: ringkasKoord()
   memangkas koordinat ke enam desimal untuk layar (±11 cm — jauh lebih
   teliti daripada yang dibutuhkan menemukan sebuah tiang), sementara tautan
   Maps dan tooltip tetap membawa nilai utuhnya. 39 karakter jadi 21, dan
   21 karakter pada 12px ≈ 151px plus bantalan 28px = 179. Jadi satu baris
   utuh, bukan dua — dan itu sebabnya kolomnya melebar 20px tapi barisnya
   justru jadi lebih pendek. */
.sel-lokasi{color:var(--t2);font-size:12px;min-width:180px;
  word-break:normal;overflow-wrap:normal}

/* Sel SSID di tabel yang sama. Masalahnya persis kerabat `.sel-lokasi` di
   atas, dan dilaporkan pengguna 21 Agustus 2026 (bug42): di HP kolomnya
   menyempit sampai satu SSID terbaca sebagai tiga baris pecahan — "kerb / au2
   / 2", "salo / bers / atu".

   Sebabnya `td{word-break:break-word}` yang berlaku untuk semua sel. Aturan
   itu bukan cuma soal tampilan patahannya: ia membuat lebar-minimum kolom ini
   jadi SATU HURUF, jadi tata letak tabel otomatis boleh memerasnya sampai
   sesempit apa pun demi kolom lain. Nama jaringan tidak punya tempat patah
   yang benar — sama seperti alamat IP di `.sel-ip` — jadi yang dikembalikan
   di sini `word-break:normal`: kolomnya kembali menuntut selebar SSID
   terpanjang, dan spasi (kalau ada) tetap jadi tempat patah yang sah.

   Tidak perlu `min-width` seperti `.sel-lokasi`: di sana angkanya menahan
   koordinat yang PANJANG supaya muat satu baris, di sini yang dijaga justru
   lantai bawahnya, dan lantai itu sudah datang dari isinya sendiri. */
.sel-ssid{color:var(--cyan);font-size:12px;
  word-break:normal;overflow-wrap:normal}

/* Baris BTS/POP di kepala kartu mode 1 perangkat. Sebaris dengan lokasinya —
   keduanya menjawab "di mana", dan yang membedakannya cuma ikon menara di
   depan. Nama BTS-nya sedikit lebih terang daripada teks sekitarnya karena
   ia yang dicari mata; kalimat "belum masuk BTS" justru tidak. */
.det-bts{display:flex;align-items:center;gap:4px;flex-wrap:wrap;
  font-size:12px;color:var(--t3);margin-bottom:3px}
.det-bts b{color:var(--t2);font-weight:600}

/* Tombol aksi baris Kelola Perangkat — sekeluarga dengan .act-detail/.act-trend
   di Detail Perangkat, warnanya berbeda per tindakan supaya "hapus" tidak
   pernah terbaca sama dengan "ubah". */
.act-riwayat{border-color:var(--border);color:var(--t2)}
.act-riwayat:hover{border-color:var(--t3);color:var(--t1)}
.act-ubah{border-color:rgba(59,130,246,.4);color:var(--blue)}
.act-ubah:hover{background:rgba(59,130,246,.12);border-color:var(--blue)}
.act-hapus{border-color:rgba(239,68,68,.4);color:var(--red)}
.act-hapus:hover{background:rgba(239,68,68,.12);border-color:var(--red)}

/* Kotak centang "Aktif" — satu-satunya kolom yang bisa diubah langsung dari
   tabel, jadi ia tetap kotak centang dan tidak diganti badge seperti kolom
   Status di Detail Perangkat (badge tidak bisa diklik untuk mematikan). */
.dev-aktif{display:inline-flex;align-items:center;cursor:pointer}
.dev-aktif input{accent-color:var(--green);width:15px;height:15px;cursor:inherit}
.dev-aktif.terkunci{cursor:not-allowed}

/* ── Kartu grafik halaman Tren (7 Agustus) ───────────────────────────────
   Tujuh kartu, satu potongan gaya yang sama ditulis tujuh kali — dan
   penambahan kartu CPU-lah yang membuat batas §2.1 halaman ini merah. `.chart-card`
   sudah ada sebagai kelas sejak lama, tapi isinya cuma penyesuaian layar
   sempit; deklarasi bentuknya tetap tertinggal di tiap elemen. */
/* Cangkang kartu bertepi rapat — 10px radius, berbantalan, berjarak ke
   kartu di bawahnya. SATU aturan untuk tiga tempat yang dulu menulisnya
   sendiri-sendiri: `.chart-card` (Grafik Tren), `.spectrum-wrap` dan
   `.zone-card` (Dashboard). Disatukan 21 Agustus 2026.

   KETIGA NAMA LAMA SENGAJA TETAP DIPASANG di markup sebagai kelas
   kedua, dan itu bukan sisa yang lupa dibuang: belasan aturan tata
   letak memakainya untuk membedakan kartu mana yang mana
   (`.two-left > .spectrum-wrap` lawan `.two-left > .zone-card`,
   `body[data-layout="tembok"] main > .zone-card`, dan bantalan HP
   `.chart-card` di 640px). Menyatukan namanya juga akan membuat aturan
   itu tidak bisa lagi membedakan keduanya — dua kartu yang berbeda
   perlakuannya di layar sempit jadi satu. Yang dibuang duplikasi
   TAMPILANNYA; identitasnya memang berbeda. */
.kartu-panel{background:var(--bg-1);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:16px}
/* Kartu yang hanya tampil di salah satu mode (1 perangkat / semua perangkat).
   JS menyalakannya lewat `style.display`, yang menang atas kelas ini. */
/* `.sembunyi` PINDAH KE DASAR BERKAS INI — lihat catatan di sana. */
/* Judul kecil di atas tiap kanvas, dan kanvasnya sendiri. 8x & 7x.
   `margin-bottom` DICABUT ke `.jarak-bawah` (8 Agustus): tiga dari delapan
   judul duduk di dalam `.chart-kepala` bersama legendanya, dan di sana yang
   berjarak barisnya, bukan judulnya. Pelajaran `.tak-susut` terulang —
   properti yang benar hanya di sebagian pemakai bukan milik kelas itu. */
.chart-judul{font-size:12px;font-weight:700;color:var(--t2)}
.jarak-bawah{margin-bottom:8px}
.chart-kanvas{width:100%;height:260px;display:block;border-radius:6px}

/* ── Potongan yang muncul lagi saat kolom/kartu CPU ditambahkan ──────────
   Ketiganya sudah ditulis ulang 3–4 kali sebelum ini; penambahan CPU cuma
   yang membuatnya melewati batas §2.1. */

/* Keterangan di bawah kanvas grafik ("belum ada rekaman untuk rentang ini").
   Bawaannya tersembunyi; JS menyalakannya lewat `style.display`. 4x. */
.chart-catatan{display:none;margin-top:8px;font-size:11px;color:var(--t3);line-height:1.5}

/* Sel angka dua sisi di tabel Detail Perangkat (sinyal, LAN, CPU) — sejajar
   per karakter dan tidak boleh patah di tengah pasangan "AP ↔ ST". 3x. */
.sel-mono{font-family:var(--mono);font-size:12px;white-space:nowrap}

/* ── Pemilih logo di halaman Tampilan (7 Agustus) ────────────────────────
   Pratinjaunya sengaja seukuran ubin logo di header (34px) dan berlatar sama:
   yang ingin diketahui admin bukan "gambarnya apa" — ia baru saja memilihnya —
   melainkan bagaimana ia tampil di tempat yang sebenarnya. */
.logo-baris{display:flex;align-items:center;gap:12px;margin-top:4px;flex-wrap:wrap}
.logo-pratinjau{width:44px;height:44px;border-radius:9px;flex-shrink:0;overflow:hidden;
  display:flex;align-items:center;justify-content:center;border:1px solid var(--border);
  background:var(--bg-2)}
.logo-pratinjau img{width:100%;height:100%;object-fit:contain;display:block}
/* Keadaan "belum ada logo pilihan" menampilkan logo BAWAAN, bukan kotak
   kosong — itu yang benar-benar akan tampil kalau tidak diisi. */
.logo-pratinjau.kosong{background:linear-gradient(135deg,var(--blue),var(--cyan));color:var(--fg-aksen);border-color:transparent}
.logo-aksi{display:flex;gap:8px;flex-wrap:wrap}
.logo-aksi .det-refresh:disabled{opacity:.45;cursor:not-allowed}

/* ── Halaman Tampilan: setelan kiri, pratinjau kanan (7 Agustus) ─────────
   Sebelumnya pratinjau bertumpuk DI BAWAH setelannya. Menilai sebuah tema
   berarti menggulung turun, melihat, lalu menggulung naik lagi untuk mencoba
   tema berikutnya — dan yang dinilai orang di halaman ini justru akibat
   pilihannya, bukan pilihannya sendiri. Akibat itu harus terlihat pada saat
   yang sama dengan pilihannya (bug/bug8.png).

   `minmax(0,…)` di kedua kolom, bukan `1fr 360px`: tanpa itu kolom grid
   menolak menyusut lebih kecil daripada isinya, dan kartu tema yang lebar
   melebarkan seluruh halaman sampai muncul geseran mendatar. */
.tampilan-dua{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,360px);
  gap:16px;align-items:start}

/* Pratinjau ikut menempel saat setelan di kiri digulung — kolom kiri jauh
   lebih panjang, dan pratinjau yang hilang dari layar sama saja dengan
   pratinjau yang ada di bawah. 74px = tinggi header yang menempel + jarak. */
.pratinjau-kanan{position:sticky;top:74px}

/* Di layar yang tidak cukup untuk dua kolom, keduanya kembali bertumpuk —
   setelan dulu, pratinjau di bawahnya — dan `sticky` dicabut supaya ia tidak
   menutupi setelan yang sedang digulung. */
@media(max-width:1000px){
  .tampilan-dua{grid-template-columns:1fr}
  .pratinjau-kanan{position:static}
}

/* ── Halaman Tren: kepala kartu grafik, legenda, rentang kustom (8 Agustus) ──
   Halaman ketujuh §2.1. Yang tersisa di sini bukan bentuk kartunya — itu sudah
   jadi `.chart-card` kemarin — melainkan apa yang ada DI DALAM kepalanya:
   tiga kartu punya judul + legenda berdampingan, dan potongan gaya barisnya
   ditulis tiga kali sementara enam butir legendanya ditulis enam kali. */

/* Baris kepala kartu: judul di kiri, legenda di sebelahnya. Jaraknya ke
   kanvas dari `.jarak-bawah`, sama seperti judul yang berdiri sendiri. */
.chart-kepala{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
/* Pembungkus legenda. `display` TETAP inline: JS menyalakannya dengan
   `.style.display='flex'` hanya di mode satu perangkat — alasan yang sama
   dengan `.modal-lapis` dan `.dd-unduh`. */
.legenda-grup{align-items:center;gap:12px;flex-wrap:wrap}
/* Butir legendanya sendiri memakai `.legenda` bersama — lihat blok peta
   spektrum di atas. Yang tinggal di sini cuma kotak warnanya, yang
   ukurannya memang berbeda. */
/* Kotak warnanya. WARNANYA sengaja tetap inline: ia harus sama persis dengan
   warna garis yang digambar JS ke kanvas, jadi ia ISI legendanya, bukan
   gayanya — alasan yang sama dengan empat lencana pratinjau tema. */
.legenda-titik{width:10px;height:10px;border-radius:3px}
/* Baris legenda mode semua perangkat — satu butir per perangkat, dirakit JS. */
.legenda-baris{display:flex;flex-wrap:wrap;gap:8px 16px;padding:4px 2px}

/* Rentang tanggal kustom. `.rentang-input` HANYA menggambar kotaknya; huruf,
   warna, dan `outline` datang dari `.dt-input` yang tetap terpasang —
   pasangan yang sama persis dengan `.f-input`/`.login-input`, dan dijaga
   TestKotakTanggal supaya tidak dipakai sendirian. */
.rentang-kotak{align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:16px;background:var(--bg-1);border:1px solid var(--border);border-radius:10px;padding:12px}
.rentang-sel{display:flex;align-items:center;gap:4px}
.rentang-input{background:var(--bg-2);border:1px solid var(--border);border-radius:6px;padding:4px 8px}

/* ── Judul halaman (8 Agustus) ───────────────────────────────────────────
   Tiga halaman memakai `.card-title` sebagai judul HALAMAN, bukan judul
   kartu, dan ketiganya membesarkannya ke 15px lewat gaya inline: Grafik Tren,
   Bandwidth Test, dan Pointing. Dipasang BERSAMA `.card-title`, yang memberi
   tebal dan barisnya — dibaca sesudahnya, jadi ukurannya yang menang.

   Sengaja tidak menyalin seluruh `.card-title` ke sini: yang berbeda di
   ketiganya memang cuma ukurannya, dan kelas yang menyalin sisanya berarti
   dua tempat yang harus berubah bersama. */
.judul-hal{font-size:15px}

/* Panah kecil di tombol unduh. Dua tempat sejak 8 Agustus (dashboard dan
   Detail Perangkat), dan justru itu sebabnya ia jadi kelas: dua tombol unduh
   yang panahnya berbeda 1px adalah bentuk terkecil dari keluhan yang membuka
   §2.1. Jangan tertukar dengan `.t-satuan` (8px) — itu satuan di kepala tabel
   dashboard, ukuran yang memang berbeda. */
.btn-unduh-panah{font-size:9px;opacity:.7}

/* ── Penyaring Riwayat Perubahan di Detail Perangkat (8 Agustus) ─────────
   Bentuknya mengikuti penyaring History di dashboard — kotak cari selebar
   penuh, dropdown di sebelahnya — supaya orang yang sudah tahu satu tidak
   perlu belajar yang lain.

   Yang TIDAK ikut dari sana: penyaring tanggal & status. Keduanya masuk akal
   untuk 42.000 baris seluruh armada, tidak untuk 86 baris satu perangkat, dan
   kotak penyaring yang lebih tinggi daripada isinya adalah cara menutupi
   datanya sendiri. */
.det-hist-filter{display:flex;align-items:center;gap:4px;flex-wrap:wrap;
  padding:8px 16px;border-bottom:1px solid var(--border);
  background:var(--bg-2);flex-shrink:0}
/* Tidak ikut melar bersama kotak cari: dropdown selebar setengah kartu
   membuat kotak carinya sempit justru di layar yang sempit. */
.det-hist-kategori{flex:0 1 168px;min-width:0}
/* `.dd-unduh` berposisi absolut terhadap leluhur berposisi terdekat — tanpa
   pembungkus ini ia menempel ke tepi kartu, bukan ke tombolnya. */
.det-unduh-bungkus{position:relative;flex-shrink:0}

/* ── Tabel Zona Frekuensi, halaman Pengaturan (8 Agustus) ────────────────
   Satu-satunya tabel di aplikasi ini yang selnya berisi kotak isian, bukan
   angka — jadi jarak barisnya memang berbeda dari tabel lain, dan itulah
   kenapa `.th-s`/`.th-tepi` tidak dipakai di sini.

   `.zona-sel` dipasang di TH maupun TD: keduanya harus berjarak sama, kalau
   tidak kolomnya tidak segaris. Dipisah dari `.zona-th` dengan alasan yang
   sama seperti `.th-s` + `.th-tepi` — jarak dan huruf berubah sendiri-
   sendiri, dan kelas yang membawa keduanya tidak bisa dipakai di TD. */
.zona-sel{padding:0 8px 8px 0}
.zona-th{text-align:left;font-size:9px;text-transform:uppercase;letter-spacing:.06em;color:var(--t3)}
/* Tabel Zona Frekuensi adalah FORMULIR yang kebetulan disusun sebagai
   tabel: kotak isian yang melayang di dalam kartu, bukan baris data.
   Bilah abu-abu di baris kepalanya — bawaan `thead tr` yang benar untuk
   tabel data — di sini terbaca sebagai potongan latar yang tertinggal,
   dan mencolok di SEMUA tema karena --bg-2 memang dibuat kontras terhadap
   --bg-1. Dilaporkan pengguna 20 Agustus 2026 (bug27).

   Dimatikan lewat kelas pada tabelnya, bukan lewat `:has()` pada barisnya:
   yang kedua lebih rapi ditulis tapi menuntut dukungan browser yang lebih
   baru untuk sesuatu yang cuma soal latar. */
.zona-tabel thead tr{background:none}
/* Garis bawah kepalanya ikut dilepas — tanpa bilahnya, garis itu
   menggantung sendirian di antara label dan kotak isian. */
.zona-tabel thead th{border-bottom:none}
/* Kotak isian di dalam sel. Seperti `.f-input`, ia HANYA mengurus ukuran —
   warna, bingkai, dan sudutnya dari `.login-input` yang tetap terpasang, dan
   ia dibaca sesudahnya supaya bantalannya yang berlaku. */
.input-penuh{width:100%;padding:8px;font-size:12px}

/* ── Ambang "mendekati" (13 Agustus) ─────────────────────────────────────
   Duduk di bawah keterangan status zona, di dalam kartu yang sama, karena
   angkanya tidak berarti apa-apa lepas dari daftar zona di atasnya.

   Garis di atasnya bukan hiasan: tanpa itu barisnya terbaca sebagai butir
   keempat dari keterangan "dilarang / perhatian / aman" — padahal yang tiga
   itu penjelasan, sedangkan yang ini kotak isian yang mengubah penilaian. */
.zona-ambang{margin-top:12px;padding-top:12px;border-top:1px solid var(--border)}
.zona-ambang>label{display:block;font-size:11px;color:var(--t2);margin-bottom:4px}
/* Selebar angka yang mungkin diisi (0–160) plus panah pengatur bawaan
   browser — bukan 100%, yang membuat kotak tiga digit selebar layar. */
.zona-ambang-isi input{width:90px;padding:8px;font-size:12px}
.zona-ambang-isi span{font-size:11px;color:var(--t3)}
/* Keterangan kecil di bawah sebuah kendali. Dipakai halaman Pengaturan
   (ambang zona) dan halaman Pointing — dulu `.zona-ambang-ket` dan
   `.pt-catatan`, dua nama untuk gaya yang sama persis. Disatukan
   21 Agustus 2026. */
.catatan-kecil{font-size:10px;color:var(--t3);margin-top:8px;line-height:1.6}

/* ── Halaman Log Aktivitas (13 Agustus) ──────────────────────────────────
   Menggantikan Riwayat Login. Isinya lebih lebar dari pendahulunya — enam
   kolom, bukan lima, dan salah satunya memuat "Field: lama → baru" — jadi
   kartunya 1180px, bukan 900px seperti halaman lama.

   Yang TIDAK ditulis ulang di sini: kotak cari, dropdown, menu unduh, kepala
   & kaki kartu. Semuanya kelas bersama yang sudah dipakai halaman Detail dan
   Kelola Perangkat (.f-search-wrap, .f-select, .dd-unduh, .kartu-kaki). */
.akt-kartu{width:100%;max-width:1180px;margin:0 auto;display:flex;flex-direction:column}
.akt-judul{flex:1;min-width:0}

.akt-filter{padding:12px 20px;border-bottom:1px solid var(--border);
  background:var(--bg-2);flex-shrink:0;display:flex;flex-direction:column;gap:8px}
.akt-filter-baris{display:flex;align-items:center;gap:4px;flex-wrap:wrap}
/* Melar bersama-sama sampai batas yang masih terbaca. Tanpa flex-basis,
   dropdown "Semua Objek" menyusut jadi selebar tanda panahnya sendiri di
   layar sempit. */
.akt-sel{flex:1 1 130px;min-width:0}
.akt-tgl{flex:0 1 140px;min-width:0;font-family:var(--mono);font-size:11px}
.akt-reset{background:none;border:1px solid var(--border);border-radius:8px;
  padding:8px 12px;font-size:11px;color:var(--t2);cursor:pointer;
  white-space:nowrap;font-family:var(--ui)}
.akt-reset:hover{border-color:var(--blue);color:var(--t1)}

.akt-tabel{--tmin:1000px;flex:1;overflow-y:auto;padding:8px 0;max-height:62vh}
.akt-baris{border-bottom:1px solid var(--border2)}
.akt-baris:hover{background:var(--bg-2)}
.akt-waktu{text-align:left;padding:8px 20px;font-size:11px;color:var(--t2);font-family:var(--mono);white-space:nowrap}
.akt-user{text-align:left;padding:8px 12px;font-size:12px;font-weight:600;color:var(--t1);white-space:nowrap}
.akt-sel-aksi{padding:8px 12px;text-align:center}
.akt-objek{text-align:left;padding:8px 12px;font-size:11px;line-height:1.5}
/* Jenis di atas, nama di bawah — bukan berdampingan. Nama perangkat di
   instalasi ini sampai 34 karakter; disandingkan dengan jenisnya, kolomnya
   melebar dan mendorong kolom Perubahan keluar layar. */
/* Keterangan kecil di atas nilai utamanya — dipakai kolom Objek (jenisnya)
   dan kolom Perubahan (nama field-nya). Satu kelas, karena keduanya memang
   peran yang sama: label yang menerangkan baris di bawahnya. */
.akt-mini{display:block;font-size:9px;text-transform:uppercase;
  letter-spacing:.06em;color:var(--t3)}
.akt-nama{display:block;color:var(--t1);font-weight:600}
.akt-ubah{text-align:left;padding:8px 12px;font-size:11px;color:var(--t2);line-height:1.5}
.akt-lama{color:var(--t3);text-decoration:line-through}
.akt-ip{text-align:left;padding:8px 20px;font-size:11px;color:var(--t3);font-family:var(--mono);white-space:nowrap}

/* Lencana aksi. Warnanya mengikuti arti yang sudah dipakai di seluruh
   aplikasi: hijau bertambah/berhasil, biru berubah, merah hilang/ditolak,
   kuning perlu diperhatikan tapi bukan kesalahan (kredensial dibuka). */
.akt-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;
  font-weight:700;padding:3px 8px;border-radius:10px;white-space:nowrap}
.akt-b-hijau{background:rgba(34,197,94,.12);color:var(--green)}
.akt-b-biru{background:rgba(59,130,246,.14);color:var(--blue)}
.akt-b-merah{background:rgba(239,68,68,.14);color:var(--red)}
.akt-b-kuning{background:rgba(245,158,11,.14);color:var(--amber)}
.akt-b-abu{background:var(--bg-3);color:var(--t2)}

.akt-kaki{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
/* Sederet elemen sejajar dengan jarak sama — penanda halaman di sini,
   kotak angka + satuan di Setting → Zona Frekuensi. */
.baris-isi{display:flex;align-items:center;gap:8px}
.akt-nav{background:none;border:1px solid var(--border);border-radius:7px;
  padding:4px 8px;font-size:11px;color:var(--t2);cursor:pointer;font-family:var(--ui)}
.akt-nav:hover:not(:disabled){border-color:var(--blue);color:var(--t1)}
.akt-nav:disabled{opacity:.4;cursor:default}
.akt-hal{font-size:11px;color:var(--t3);font-family:var(--mono);white-space:nowrap}

@media (max-width:640px){
  /* Di HP dropdown-nya menumpuk satu per baris. Empat dropdown berdampingan
     di layar 360px menyisakan 80px per dropdown — cukup untuk tanda panah
     dan tiga huruf. */
  .akt-sel,.akt-tgl{flex:1 1 100%}
}

/* ── Peta spektrum: kanvas responsif & tooltip (13 Agustus) ──────────────
   Kanvas berhenti dipatok 160px sejak Perlu Tindakan pindah ke kolom kanan
   dan kolom kiri punya ruang lebih. Ia dan Peringkat Kendala berbagi sisa
   ruang 2:1 — peta spektrum visualisasi utama halaman ini, peringkat kendala
   daftar yang menggulung sendiri kalau kurang. */
/* Kanvasnya BERTINGGI TETAP, dan sisa ruang kolom kiri jatuh seluruhnya ke
   Peringkat Kendala.

   Versi pertama membagi 2:1 dan itu terbalik dari yang dibutuhkan
   (laporan-bug/bug18.png): kanvas jadi ~380px dengan sepertiga atas kosong —
   puncak gunung tertinggi tidak pernah mencapainya — sementara Kendala per
   BTS tersisa 3½ baris di balik scrollbar. Peta spektrum tidak bertambah
   terbaca dengan bertambah tinggi; daftar peringkat bertambah berguna dengan
   bertambah baris.

   240px dipilih di antara 160px lama (terlalu sesak, gunung saling menutup)
   dan 380px yang dikeluhkan. */
.two-left > .spectrum-wrap{flex:0 0 auto;display:flex;flex-direction:column}
.two-left > .spectrum-wrap .spectrum-row canvas{height:240px}
/* Jangkar tooltip. `.spectrum-row` yang diberi position, bukan kartunya:
   koordinat yang dihitung JS relatif terhadap kanvas, dan kartunya punya
   padding 18px yang akan menggeser seluruhnya. */
.two-left > .spectrum-wrap .spectrum-row > div:first-child{position:relative}

.spek-tip{
  position:absolute;z-index:20;pointer-events:none;
  background:var(--bg-1);border:1px solid var(--border);border-radius:8px;
  padding:8px 12px;min-width:186px;box-shadow:0 8px 24px rgba(0,0,0,.45)
}
.spek-tip-nama{font-size:12px;font-weight:700;color:var(--t1);margin-bottom:4px;
  padding-bottom:4px;border-bottom:1px solid var(--border2);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:230px}
.spek-tip-baris{display:flex;align-items:baseline;justify-content:space-between;
  gap:12px;font-size:10px;line-height:1.7}
.spek-tip-baris > span{color:var(--t3)}
.spek-tip-baris > b{color:var(--t1);font-family:var(--mono);font-size:10px;font-weight:600}
.spek-tip-st{font-family:var(--ui)}

/* ── Tabel Status Perangkat: nilai jadi bentuk (13 Agustus) ──────────────
   Diminta pengguna: kurangi visual overload, buat data padat jadi actionable
   tanpa harus membaca tiap nilai teks mentah.

   Prinsip yang dipakai di seluruh blok ini: bentuk MENAMBAH, tidak
   menggantikan. Tiap bilah dan lingkaran tetap membawa angkanya — teknisi
   menyebut "-72 dBm" dan "13 persen" saat berbicara di telepon, dan grafik
   tanpa angka membuat percakapan itu mustahil. Ambang warnanya juga tidak
   ada yang baru: seluruhnya memanggil signalColor()/cpuColor() di app.js. */

/* Nama perangkat. Menggantikan cursor + onmouseover/onmouseout sebaris yang
   dulu menyalakan garis bawahnya — tiga atribut per baris, dikalikan 134. */
.dname-baris .dname{cursor:pointer}
.dname-baris .dname:hover{text-decoration:underline}
/* Rincian AP & ST menjorok ke dalam — hierarki, bukan dua baris sejajar. */
.dsisi{padding-left:16px;border-left:1px solid var(--border2);margin-left:4px;margin-top:3px}
.dip .ic{color:var(--t3);flex-shrink:0}

/* Ikon tautan tiga bilah, digambar CSS tanpa elemen anak — tinggi bilahnya
   menaik ke kanan seperti indikator sinyal ponsel, dan yang menyala mengikuti
   kekuatannya. Tiga latar bergradasi, bukan tiga <span>: barisnya sudah padat,
   dan elemen tambahan per baris dikalikan 134 perangkat. */
.lnk{
  display:inline-block;width:13px;height:11px;flex-shrink:0;align-self:center;
  background:
    linear-gradient(var(--b1) 0 0) 0    100%/3px 4px   no-repeat,
    linear-gradient(var(--b2) 0 0) 5px  100%/3px 7.5px no-repeat,
    linear-gradient(var(--b3) 0 0) 10px 100%/3px 11px  no-repeat;
  --b1:var(--border);--b2:var(--border);--b3:var(--border)
}
.lnk-1{--b1:var(--red)}
.lnk-2{--b1:var(--amber);--b2:var(--amber)}
.lnk-3{--b1:var(--green);--b2:var(--green);--b3:var(--green)}
.lnk-0{opacity:.45}

/* RF-REDESIGN — titik status di paling kiri nama perangkat.
   Ia BUKAN pengulangan `.lnk` di sebelahnya: tiga bilah itu membaca kekuatan
   SINYAL, titik ini membaca STATUS baris (aman/perhatian/pelanggaran/down).
   Keduanya sering berbeda, dan justru selisihnya yang menarik — perangkat
   bersinyal kuat yang duduk di zona terlarang tampil sebagai tiga bilah hijau
   dengan titik merah di depannya.

   Warnanya diambil dari `bCls`, kelas yang sama dengan badge Status di ujung
   kanan baris — jadi kiri dan kanan satu baris tidak bisa menyatakan dua hal
   berbeda. Down memakai merah tumpul yang sama dengan `.badge.off`. */
.dstat{
  display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0;
  margin-right:4px;vertical-align:middle;--dc:var(--gray);background:var(--dc)
}
.dstat-ok{--dc:var(--green)}
.dstat-warn{--dc:var(--amber)}
.dstat-vio{--dc:var(--red)}
.dstat-off{--dc:var(--fg-mati)}

/* Pengukur sinyal linier. --sig = posisi terisi (0–100%), --sigc = warnanya. */
/* Dipakai kolom Signal DAN Capacity: keduanya dua bilah bertumpuk. */
.sel-ukur{display:flex;flex-direction:column;gap:4px}
.sig-ukur{display:flex;align-items:center;gap:4px}
.sig-track{flex:1;min-width:26px;height:5px;border-radius:3px;background:var(--bg-3);overflow:hidden}
.sig-isi{display:block;height:100%;width:var(--sig);background:var(--sigc);border-radius:3px}
/* Lebar TETAP empat karakter, bukan `min-width` — alasannya sama persis
   dengan `.cap-angka` di bawah, dan bacalah di sana. Empat cukup untuk
   "-100"; dBm di sini selalu bulat (kolom `signal` INTEGER). */
.sig-angka{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--sigc);width:4ch;flex-shrink:0;text-align:right}
/* Keadaan tanpa data: rel tetap tergambar, terisi nol, angkanya "—".
   Nilainya selalu sama, jadi ia tinggal di sini — bukan gaya sebaris yang
   ditulis ulang tiap baris tabel. */
.sig-kosong{--sig:0%;--sigc:var(--t3)}

/* Bilah kapasitas. --cap = lebar terisi terhadap CAP_SKALA (300 Mbps). */
.cap-baris{display:flex;align-items:center;gap:4px}
.cap-panah{font-size:10px;color:var(--t3);width:8px;flex-shrink:0}
.cap-track{flex:1;min-width:20px;height:5px;border-radius:3px;background:var(--bg-3);overflow:hidden}
.cap-isi{display:block;height:100%;width:var(--cap);background:var(--capc);border-radius:3px}
/* ⚠️ Lebar TETAP lima karakter, dan itu BUKAN soal kerapian.

   `.cap-track` mengambil sisa ruang (flex:1). Selama slot angkanya melar
   mengikuti isinya, panjang REL ikut berubah dari baris ke baris — dan rel
   yang berbeda panjang berarti persen yang sama tergambar sebagai bilah yang
   berbeda panjang. Akibatnya justru terbalik dari yang dibaca orang: baris
   berangka empat huruf ("75.3") punya rel LEBIH PANJANG daripada baris
   berangka lima huruf ("109.8"), jadi 75,3 Mbps tampak lebih besar daripada
   109,8 Mbps. Dilaporkan pengguna 20 Agustus 2026 (bug31.png), dan terukur
   dari tangkapan layarnya: rel 59px lawan 52px — tepat satu huruf mono 12px.

   Lima karakter cukup untuk "999.9"; yang lebih panjang kehilangan
   desimalnya (dashboard.html: capTeks). Dijaga
   tests/test_status_perangkat_visual.py. */
.cap-angka{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--capc);width:5ch;flex-shrink:0;text-align:right}
.cap-kosong{--cap:0%;--capc:var(--t3)}

/* Pengukur radial CPU. Cincin conic-gradient, angka di tengahnya. Beban CPU
   satu-satunya metrik di tabel ini yang punya batas atas alami (100%), dan
   lingkaran menjawab "seberapa penuh" lebih cepat daripada angka. */
.sel-cpu{display:flex;align-items:center;justify-content:center;gap:8px}
.cpu-radial{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;
  justify-content:center;flex-shrink:0;
  background:conic-gradient(var(--cpuc) var(--cpu), var(--bg-3) 0)}
.cpu-radial > span{width:22px;height:22px;border-radius:50%;background:var(--bg-1);
  display:flex;align-items:center;justify-content:center;
  font-family:var(--mono);font-size:11px;font-weight:700;color:var(--cpuc)}
.cpu-kosong{background:var(--bg-3)}
.cpu-kosong > span{color:var(--t3)}

/* Metrik utama dibesarkan; satuan & angka pendukung tetap kecil. */

.fband-ssid{color:var(--cyan);font-size:10px}
.sel-uptime{display:flex;flex-direction:column;gap:2px;font-family:var(--mono)}
/* nowrap di ANAKNYA, bukan di wadahnya: menumpuk atas-bawah justru yang
   diinginkan di sel ini, yang dilarang cuma satu nilai patah di tengahnya
   ("128d 23h 59m" jadi dua baris).

   Aturan ini sempat HILANG. Ia dulu menempel di `.sel-tumpuk`, kelas yang
   kehilangan seluruh pemakainya saat kolom Capacity & Uptime berganti bentuk —
   dan uji yang menjaganya ikut membaca kelas yang sudah tidak dipakai siapa
   pun, jadi tetap hijau sambil tidak menjaga apa-apa. Ketahuan 21 Agustus
   2026 saat kelas mati disisir. */
.sel-uptime > span{white-space:nowrap}

/* Potongan perintah di dalam banner koneksi. Dulu gaya sebaris tiga properti
   yang ditulis ulang tiap kali pesannya disusun. */
.conn-kode{background:var(--bg-3);padding:1px 4px;border-radius:3px;font-family:var(--mono)}

/* ── Notifikasi Telegram, halaman Pengaturan (13 Agustus) ────────────────
   Kotak isian & tombolnya memakai kelas yang sudah ada (.login-input,
   .input-penuh, .f-label, .akt-reset, .set-simpan). Yang baru di sini hanya tata
   letaknya dan sakelar centangnya. */
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.tg-ket{font-size:10px;color:var(--t3);margin-top:4px;line-height:1.6}
.tg-peristiwa{border-top:1px solid var(--border);padding-top:12px}
/* Dua kolom: delapan peristiwa dalam satu lajur memaksa menggulung, sementara
   empat lajur membuat labelnya terpotong di layar sempit. */
.tg-kotak{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px;margin:8px 0}
/* Kotak yang dimatikan harus TERLIHAT mati — kalau tidak, orang mengetik ke
   dalamnya lalu bingung kenapa tidak tersimpan. `textarea` disebut terpisah
   karena selektor di atas hanya menangkap <input>, dan kotak templat bukan
   salah satunya. */
.set-isi input:disabled,
.set-isi textarea:disabled,
.tg-tag:disabled{opacity:.45;cursor:not-allowed}

/* Kotak templat. Monospace karena isinya penanda `{nama}` dan tag HTML —
   dua hal yang salah ketiknya hanya terlihat kalau lebar hurufnya seragam.
   `resize:vertical` saja: melebar horizontal merusak grid dua kolomnya. */
.tg-templat{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:11px;
  line-height:1.7;resize:vertical;min-height:76px;width:100%}

/* Kotak ambang: isinya paling banyak tiga digit, jadi ia TIDAK melebar penuh
   seperti kotak teks di sekitarnya — kotak selebar setengah kartu untuk
   angka "5" terbaca seperti ada yang belum terisi.
   Sengaja bukan `.input-penuh`: kelas itu milik keempat kotak tabel Zona dan
   jumlahnya dihitung uji sebagai penanda kolom mana yang berangka. */
.tg-num{width:96px;padding:8px;font-size:12px}

.tg-penanda{display:inline-flex;flex-wrap:wrap;gap:4px;margin:4px 0 8px}
.tg-tag{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:10px;
  color:var(--t2);background:var(--bg-2);border:1px solid var(--border);
  border-radius:5px;padding:2px 8px;cursor:pointer;transition:.15s}
.tg-tag:hover:not(:disabled){color:var(--t1);border-color:var(--blue)}

@media (max-width:640px){
  .grid-2,.tg-kotak{grid-template-columns:1fr}
}

/* ══════════════════════════════════════════════════════════════════════════
   SUSUNAN DASHBOARD — tiga pilihan (15 Agustus 2026)
   ══════════════════════════════════════════════════════════════════════════

   Diminta pengguna: "buatkan agar layout dashboard bisa di costumize dengan
   beberapa tampilan". Dipilih di halaman /layout, tersimpan per PERANGKAT di
   localStorage (`rf_layout`), dipasang app.js sebagai `body[data-layout]`.

   Seluruhnya CSS. dashboard.html TIDAK berubah satu baris pun, dan itu yang
   membuat blok ini aman: JS yang mengukur tinggi kartu Log, menggambar peta
   spektrum, dan menyaring tabel semuanya tetap menemukan elemen yang sama di
   tempat yang sama. Kalau susunan ulangnya dikerjakan JS, tiap fungsi itu
   harus tahu ia sedang di susunan mana — tiga cabang di kode pengukuran, dan
   pengukuran yang salah tidak memunculkan error, cuma kartu yang terpotong.

   Alatnya `display:contents` pada `.two-left`/`.two-right`: kedua pembungkus
   itu lenyap dari perhitungan tata letak (kotaknya, bukan isinya), sehingga
   kelima panel di dalamnya jadi anak langsung grid `.two` dan bisa diletakkan
   satu per satu. Tanpa itu, memindahkan satu panel dari kolom kiri ke kanan
   mustahil dilakukan CSS.

   Dibungkus `min-width:1025px` dengan sengaja. Di bawah lebar itu media query
   `max-width:1024px` sudah menumpuk semuanya jadi satu kolom, dan di layar
   selebar HP hanya ADA satu susunan yang masuk akal. Membiarkan aturan di sini
   ikut berlaku berarti dua aturan berebut properti yang sama, dan yang menang
   ditentukan urutan penulisan — bukan oleh keputusan siapa pun. */
@media (min-width:1025px) {

  /* ── FOKUS TABEL ────────────────────────────────────────────────────────
     Untuk orang yang seharian mencari & menyunting perangkat. Tabel naik ke
     paling atas — sebelum kartu angka — karena di sinilah ia bekerja; peta
     spektrum dan peringkat BTS disembunyikan, bukan dikecilkan, sebab yang
     dikecilkan tetap memakan tinggi layar tanpa terbaca. */
  body[data-layout="tabel"] main{display:flex;flex-direction:column}
  body[data-layout="tabel"] main > .cari-utama{order:1}
  body[data-layout="tabel"] main > .tcard{order:2}
  body[data-layout="tabel"] main > .two{order:3}
  /* `main >` WAJIB: kelas `.zone-card` dipakai DUA kartu yang berbeda —
     Aturan Zona di dasar halaman (anak main) dan Peringkat Kendala BTS di
     dalam kolom kiri. Tanpa pembatas anak-langsung, tiap aturan di blok ini
     mengenai keduanya sekaligus. */
  body[data-layout="tabel"] main > .zone-card{order:4}

  body[data-layout="tabel"] .two-left,
  body[data-layout="tabel"] .two-right{display:contents}
  /* Tinggi baris pasti dicabut: rumus `--act-h + gap + --log-h` disusun untuk
     SATU baris berisi dua kolom penuh. Di sini barisnya dua dan isinya lain. */
  body[data-layout="tabel"] .two{grid-template-rows:auto auto;height:auto}
  body[data-layout="tabel"] .stats-baris{grid-column:1 / -1}
  body[data-layout="tabel"] .spectrum-wrap,
  body[data-layout="tabel"] .two-left > .zone-card{display:none}
  /* Sisa tinggi tidak lagi datang dari baris grid, jadi kartunya mengikuti
     isinya — dengan atap, supaya 40 perangkat down tidak memanjangkannya
     melewati kartu Log di sebelahnya.

     `align-items:start` DICABUT dari `.two` di atas, dan itu perbaikan
     laporan-bug/bug50.png. Dengan `start`, kartu Perlu Tindakan berhenti
     tepat di ujung isinya sementara kartu Log di sebelahnya berlanjut
     ratusan piksel ke bawah — terukur di gambar itu: kartu kiri berakhir di
     555px, kartu kanan di 850px, dan 295px di antaranya kosong melompong.
     Ruang itu bukan milik siapa-siapa dan tidak bisa dipakai apa pun.

     Tanpa `start`, kartunya meregang setinggi barisnya (`align-self:stretch`
     bawaan grid) dan `.act-list{overflow-y:auto}` yang sudah ada mengubah
     tinggi tambahan itu jadi BARIS DAFTAR YANG TERBACA — bukan sekadar kotak
     yang lebih tinggi. `max-height` tetap dipertahankan sebagai atap: ia
     sekarang persis setinggi kartu Log, jadi kedua kolom berakhir rata. */
  body[data-layout="tabel"] .act-card{grid-column:1;grid-row:2;
    height:auto;max-height:var(--log-h, 400px);min-height:var(--act-h)}
  body[data-layout="tabel"] .log-card{grid-column:2;grid-row:2}

  /* ── LAYAR TEMBOK ───────────────────────────────────────────────────────
     Monitor NOC yang menyala terus dan dibaca dari seberang ruangan. Yang
     ditanyakan dari jarak itu cuma satu: ada masalah atau tidak.

     Tabel Status Perangkat DISEMBUNYIKAN, dan itu keputusan yang paling
     berani di sini. Sembilan kolom teks 12px tidak terbaca dari 3 meter;
     yang tersisa cuma balok abu-abu yang menghabiskan separuh layar. Yang
     memang terbaca dari jauh — angka besar, nama perangkat bermasalah,
     puncak di peta spektrum — justru dapat ruangnya kembali.

     Aturan Zona ikut disembunyikan karena alasan yang sama tapi lebih tegas:
     isinya rujukan regulasi yang dibaca sekali saat menyetel, bukan keadaan
     yang berubah. Tidak ada gunanya dipandangi seharian. */
  /* ── RF-REDESIGN 3 September 2026 — tabelnya KEMBALI, dan alasan lama
     yang mencabutnya justru yang dijawab.

     Keberatannya dulu: "tabel 9 kolom tidak terbaca dari 3 meter, dan papan
     yang digantung di dinding tidak punya siapa pun yang menggulirnya". Yang
     kedua itu yang berubah — sekarang ADA yang menggulirnya, yaitu halamannya
     sendiri (tembokGulirPasang() di app.js). Tabelnya tidak lagi "di bawah
     lipatan yang tidak pernah terlihat", melainkan bergiliran tampil.

     Yang TIDAK berubah, dan pemiliknya perlu tahu: dari 3 meter huruf 12px
     memang tetap tidak terbaca. Yang didapat dari giliran ini bukan tabel
     yang terbaca dari seberang ruangan, melainkan tabel yang bisa dihampiri
     — operator yang melihat angka merah di kartu atas berjalan mendekat dan
     menemukan barisnya sudah lewat di layar, tanpa menyentuh apa pun.

     Aturan Zona TETAP disembunyikan: isinya rujukan regulasi yang dibaca
     sekali saat menyetel, bukan keadaan yang berubah. Tidak ada gunanya
     ikut digulirkan seharian. */
  body[data-layout="tembok"] main > .zone-card{display:none}

  body[data-layout="tembok"] .two-left,
  body[data-layout="tembok"] .two-right{display:contents}

  /* ── SATU LAYAR, TANPA DIGULIR ──
     Dilaporkan pengguna 21 Agustus 2026: "posisinya tidak tampil dalam 1
     layar full, masih terpotong perlu di scroll". Papan yang digantung di
     dinding tidak punya siapa pun yang menggulirnya — yang di bawah lipatan
     bukan "perlu digulir" melainkan tidak pernah terlihat.

     Rantainya BODY → main → `.two`, tiga kolom flex bersarang, dan tidak satu
     pun memakai angka tinggi. Itu keputusannya, bukan kebetulan:

       · `body` setinggi layar, isinya dibagi flex → main menerima sisa
         SESUDAH header DAN sesudah bilah "Menghubungkan ke proxy server…".
         Bilah itu `display:none` biasanya tapi ikut alur begitu koneksinya
         gagal; rumus `100vh - 56px` yang sempat ditulis di sini melupakannya,
         dan akibatnya justru muncul saat keadaan sedang buruk — papan dinding
         yang kehilangan proxy sekaligus kehilangan 35px paling bawahnya.
       · main sebagai kolom flex → `.two` menerima sisa sesudah kotak
         pencarian, yang tingginya sendiri tidak tetap: ia hilang sama sekali
         di mode tanpa menu (`body.tanpa-menu .cari-utama`).

     Ketiga overlay yang juga anak <body> (#loginOverlay, .modal-lapis) semua
     `position:fixed`, jadi tidak ikut dibagi ruangnya. */
  body[data-layout="tembok"]{
    display:flex;flex-direction:column;height:100vh;overflow:hidden}
  /* Menjadikan <body> kolom flex punya akibat yang tidak diminta siapa pun:
     header dan bilah koneksi ikut jadi ITEM flex, dan bawaan `flex-shrink:1`
     membuat keduanya boleh DIPERAS begitu isi halaman lebih tinggi daripada
     layar.

     Perasannya dibagi menurut basis × faktor, jadi header `height:56px` yang
     berdampingan dengan `main` setinggi isinya kehilangan belasan piksel —
     terukur ±37px tersisa dari 56px pada tangkapan layar bug49. `.rail`
     TIDAK ikut menyusut (ia `position:fixed`, jadi bukan item flex) dan
     tetap dipatok `top:56px`. Yang tersisa: jalur kosong selebar rel, tepat
     di atas ikon pertamanya, hanya di mode ini.

     Tingginya sudah ditulis sejak dulu; yang kurang cuma larangan
     memerasnya. Bilah koneksi ikut dilarang karena alasan yang sama dan
     lebih tajam: ia baru muncul saat proxy-nya gagal, jadi memerasnya
     memotong kalimat yang justru sedang dibutuhkan. */
  body[data-layout="tembok"] > header,
  body[data-layout="tembok"] > .lisensi-bar,
  body[data-layout="tembok"] > .conn-bar{flex-shrink:0}
  /* `overflow-y:auto`, bukan `hidden` lagi. Yang menggulirnya bukan orang
     melainkan tembokGulirPasang() di app.js — dan tanpa wadah yang boleh
     digulir, ia tidak punya apa pun untuk digulir. Sumbu X tetap dikunci:
     papan dinding yang bergeser ke samping adalah papan yang isinya hilang
     separuh tanpa ada yang bisa mengembalikannya.

     Bilah gulungnya disembunyikan (lihat aturan scrollbar di bawah) — ia
     menyatakan "ada yang bisa Anda gulir" kepada ruangan yang tidak punya
     siapa pun untuk melakukannya. */
  body[data-layout="tembok"] main{
    display:flex;flex-direction:column;
    flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;
    scrollbar-width:none}
  body[data-layout="tembok"] main::-webkit-scrollbar{width:0;height:0}
  body[data-layout="tembok"] .two{
    grid-template-columns:1fr 1fr;
    /* `minmax(0,1fr)` — dan BUKAN `auto auto auto` yang dulu di sini. Baris
       `auto` bertinggi sebesar isinya, jadi tiga baris "sebesar isinya" hanya
       kebetulan berjumlah setinggi layar; di sini ia selalu melebihinya, dan
       itu persis bug yang dilaporkan. Minimum 0 wajib DITULIS: bawaan `1fr`
       adalah `minmax(auto,1fr)`, yang tetap menolak menyusut di bawah tinggi
       isi barisnya. */
    grid-template-rows:auto minmax(0,1fr) minmax(0,1fr);
    /* `min-height:100%`, dan itu yang menjaga janji "satu layar" tetap utuh
       sesudah tabelnya kembali: baris atas ini SELALU setinggi layar penuh,
       persis seperti sebelumnya, dan tabelnya mulai tepat di bawah lipatan.
       Persennya menghitung terhadap tinggi `main`, yang pasti — ia anak flex
       dari body setinggi 100vh. `flex:0 0 auto` mencegahnya diperas begitu
       isi di bawahnya menuntut ruang.

       `height`, BUKAN `min-height` — perbaikan laporan-bug/bug67.png. Dengan
       `min-height` saja tinggi kotak ini TIDAK PASTI, dan baris `minmax(0,1fr)`
       di dalam wadah bertinggi tidak pasti berperilaku sebagai max-content:
       tiap kartu tumbuh setinggi ISINYA. Panel Perlu Tindakan dengan 65
       perangkat down jadi sepanjang ribuan piksel, kartu Log ikut, dan "satu
       layar" yang dijanjikan susunan ini hilang sama sekali. Tinggi pasti
       mengembalikan pembagian barisnya. */
    flex:0 0 auto;height:100%;margin-bottom:16px;
    align-items:stretch}
  /* Tabelnya di bawah lipatan, dan tingginya mengikuti isinya — bukan
     diperas ke sisa layar yang sudah nol. */
  body[data-layout="tembok"] main > .tcard{flex:0 0 auto}
  body[data-layout="tembok"] .stats-baris{grid-column:1 / -1}
  /* ── Peta spektrum DIBUANG dari susunan ini, 3 September 2026 ──
     Diminta pemilik sesudah melihatnya di layar sungguhan
     (laporan-bug/bug61.png). Ia memang panel yang paling terbaca dari jauh
     tanpa huruf — tapi yang dibacanya BENTUK PITA, bukan keadaan yang perlu
     ditindaklanjuti, dan di layar yang dipandangi seharian ia memakan
     separuh ruang untuk pertanyaan yang tidak pernah ditanyakan dari jarak
     itu. Yang menggantikannya: nama perangkat bermasalah, yang sebelumnya
     terjepit di satu kolom. Petanya tetap utuh di susunan Operasional. */
  body[data-layout="tembok"] .spectrum-wrap{display:none}
  /* `min-height:0` di keempat kartu bukan gaya penulisan — tanpa itu tiap
     kartu menuntut setinggi ISINYA (bawaan `min-height:auto` untuk item
     grid), barisnya mengalah, dan susunannya kembali melebihi layar lewat
     jalan yang lain. Lantai 160px milik `.act-card` ikut dicabut di sini:
     alasannya ("kalau sisa ruang barisnya nol, panelnya lenyap") tidak
     berlaku pada baris `1fr` yang tingginya dibagi rata, sementara
     akibatnya — kartu yang menolak menyusut — persis yang dihindari. */
  /* Kolom KIRI, dua baris: yang menjawab "siapa yang bermasalah" di atas,
     "BTS mana" tepat di bawahnya. Keduanya dibaca berurutan — dari nama
     perangkat ke tempatnya — dan menaruhnya bersebelahan dengan Log di
     kanan berarti mata tidak perlu melompati layar untuk menghubungkannya. */
  body[data-layout="tembok"] .act-card {grid-column:1;grid-row:2;height:auto;
    min-height:0}
  body[data-layout="tembok"] .two-left > .zone-card{grid-column:1;grid-row:3;
    min-height:0}
  /* `height:auto` mencabut `--log-h` yang ditulis aturTinggiLog(). Angka itu
     jawaban atas "setinggi apa kartu Log supaya lima entri muat utuh" — di
     susunan lain ia yang menentukan tinggi baris grid, di sini pertanyaannya
     terbalik: tinggi barisnya sudah ditentukan layar, dan kartunya mengikuti.
     Entri yang tidak muat digulung `.log-body{overflow-y:auto}` seperti
     biasa; yang TIDAK boleh terjadi cuma kartunya mendorong halaman. */
  /* Log membentang DUA baris di kolom kanan — `2 / 4`, bukan `span 2`,
     supaya susunan satu-kolom di layar sempit tidak perlu aturan tersendiri.
     Ia yang paling panjang isinya dari ketiganya, dan sejak peta spektrum
     dibuang ada ruang untuk memperlihatkan lebih dari lima entri sekaligus. */
  body[data-layout="tembok"] .log-card {grid-column:2;grid-row:2 / 4;
    height:auto;min-height:0;
    /* Penanda untuk jumlahEntriLog() di dashboard.html: DI SINI jumlah entri
       diukur dari tinggi kartunya, bukan diambil dari angka tetap.

       Penandanya ditaruh DI DALAM media query ini, dan itu inti perbaikan
       laporan-bug/bug67.png: di bawah 1025px seluruh blok tembok tidak
       berlaku, kartunya kembali setinggi isinya, dan mengukurnya di sana
       menghasilkan putaran umpan balik — makin banyak entri digambar, makin
       tinggi kartunya, makin banyak entri yang "muat". Dengan penanda di
       sini, JS ikut media query yang sama tanpa menyalin angka 1025px. */
    --log-entries:ukur}

  /* Angka dibesarkan, bukan kartunya. 26px terbaca sampai ±2 m; 44px sampai
     ±4 m — kira-kira jarak meja operator ke layar yang digantung di dinding.
     Label & sub-teks TIDAK ikut dibesarkan: yang perlu terbaca dari jauh cuma
     angkanya, dan label sebesar angka membuat keduanya berebut perhatian. */
  body[data-layout="tembok"] .sval{font-size:44px}
  body[data-layout="tembok"] .scard{padding:16px 20px}
  /* Nama perangkat bermasalah adalah isi terpenting di layar ini — dari jauh
     10,5px tidak terbaca sama sekali. */
  body[data-layout="tembok"] .act-nama{font-size:14px}
  body[data-layout="tembok"] .act-info{font-size:12px}

  /* Tombol layar penuh tanpa menu ditawarkan HANYA di susunan ini. Di dua
     susunan lain ia menu tambahan yang tidak ada gunanya: orang yang sedang
     duduk di depan PC-nya tidak sedang mencari cara menghilangkan menunya.
     Pasangan aturan ini di luar @media — lihat `.tembok-penuh` di bawah. */
  body[data-layout="tembok"] .tembok-penuh{display:inline-flex}

  /* ── Papan "Perlu Diperhatikan" MENGGANTIKAN tabel (18 September 2026) ──
     Diminta pemilik: *"pada mode layar tembok informasinya kurang
     tersampaikan dengan baik khususnya pada tabel status perangkat, karena
     data yang ditampilkan itu semua tapi statusnya aman"*.

     Tabelnya tidak disaring melainkan DIGANTI, dan itu keputusan: 11 kolom
     yang dirancang untuk dibaca dari 50 cm tidak jadi terbaca dari 4 m hanya
     karena barisnya tinggal lima. Yang dibutuhkan dari jarak itu satu nama
     dan satu sebab, sebesar angka kartu statistik di atasnya.

     Keduanya di DALAM media query yang sama dengan sisa blok tembok: di bawah
     1025px susunan ini memang kembali menumpuk seperti Operasional (lihat
     bug67), dan di sana tabel penuh justru yang benar. Menyembunyikan
     `.tcard` di luar @media berarti layar sempit kehilangan tabelnya tanpa
     mendapat papan ini sebagai gantinya. */
  body[data-layout="tembok"] main > .tcard{display:none}
  body[data-layout="tembok"] .pd-card{display:flex;flex:0 0 auto}
}

/* Papan ini milik satu susunan saja — di Operasional & Fokus Tabel tidak ada
   yang meminta ringkasan, dan tabel penuhnya ada di layar yang sama.
   `display:none` bawaan ditulis DI LUAR @media supaya tidak ada satu pun
   keadaan lebar layar yang menampilkannya tanpa aturan tembok di atas. */
.pd-card{display:none;flex-direction:column;background:var(--bg-2);
  border:1px solid var(--border);border-radius:10px;margin-bottom:16px;
  overflow:hidden}
.pd-kepala{display:flex;align-items:center;justify-content:space-between;
  gap:8px;padding:8px 16px;border-bottom:1px solid var(--border)}
/* Angkanya mengikuti kebiasaan `.act-total`: nol TIDAK diwarnai. Lencana yang
   selalu menyala berhenti dibaca dalam sehari, dan yang dilaporkannya di
   papan ini justru kabar baik. */
.pd-total{font-size:20px;font-weight:800;color:var(--t3);
  font-family:var(--mono);line-height:1}
.pd-total.ada{color:var(--amber)}
.pd-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));
  gap:8px;padding:12px 16px}
/* Nama di baris SENDIRI, sebabnya di bawahnya — bukan berdampingan.
   Terukur pada susunan berdampingan: perangkat dengan dua sebab menyisakan
   ±150px untuk namanya, dan "Bengkel Cakra 43" jadi "Bengkel Cak…". Nama
   adalah satu-satunya isi di papan ini yang menentukan siapa yang berangkat
   ke mana; sebabnya bisa dibaca setelah mendekat, namanya tidak bisa ditebak. */
.pd-item{display:flex;flex-direction:column;align-items:flex-start;gap:4px;
  padding:8px 12px;border-radius:8px;background:var(--bg-3);
  border-left:3px solid var(--amber);min-width:0}
.pd-item.kritis{border-left-color:var(--red)}
.pd-nama{font-size:16px;font-weight:700;color:var(--t1);max-width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Sebabnya mono: tiga jenis sebab berdampingan ("SINYAL −81", "CPU 94%",
   "CAP ↓420→120") hanya terbaca sekilas kalau angkanya selebar sama. */
.pd-sebab{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);
  font-size:14px;font-weight:700;color:var(--amber);white-space:nowrap}
.pd-sebab.kritis{color:var(--red)}
/* Sisi mana (AP/ST) — keterangan, bukan sebab. Ia sengaja lebih kecil dan
   lebih redup: yang menentukan perlu-tidaknya didatangi sebabnya, sisinya
   baru dibaca oleh orang yang sudah memutuskan berangkat. */
.pd-sisi{font-size:10px;font-weight:700;color:var(--t3);
  background:var(--bg-1);border-radius:4px;padding:1px 4px;letter-spacing:.04em}
.pd-kosong{grid-column:1 / -1;text-align:center;color:var(--t2);
  font-size:15px;padding:16px 0}
/* Sisa yang tidak muat — satu baris penuh supaya ia tidak terbaca sebagai
   perangkat ke-16. */
.pd-sisa{grid-column:1 / -1;text-align:center;color:var(--t2);font-size:13px;
  padding:4px 0}

/* ── LAYAR PENUH TANPA MENU (TAMPILAN §6) ─────────────────────────────────
   Modenya sendiri dijelaskan di app.js — termasuk kenapa ia SENGAJA tidak
   disimpan di localStorage. Yang perlu dibaca di sini cuma dua keputusan
   yang bentuknya CSS:

   1. Tombolnya `display:none` bawaan, dan dimunculkan dua aturan yang
      berbeda: `body[data-layout="tembok"]` di dalam @media lebar besar (itu
      penawarannya), dan `body.tanpa-menu` DI LUAR @media apa pun (itu jalan
      keluarnya). Pemisahan itu bukan gaya penulisan — kalau keduanya di
      dalam @media, mengecilkan jendela sampai di bawah 1025px saat modenya
      menyala akan ikut menghilangkan satu-satunya tombol keluarnya, dan
      menunya sudah tersembunyi. Persis perangkap yang sedang dihindari.

   2. Yang disembunyikan <nav> dan kotak pencarian, bukan seluruh header.
      Logo, lencana Live, dan jam tetap berdiri — itu isi yang justru dicari
      orang dari kejauhan. Kotak pencarian ikut pergi karena ia kotak isian,
      dan kotak isian tidak dipakai orang yang berdiri tiga meter dari papan
      ketiknya. */
.tembok-penuh{display:none;background:none;border:none;cursor:pointer;
  color:var(--t3);padding:4px;line-height:0;border-radius:6px;
  align-items:center;transition:color .15s,background .15s}
.tembok-penuh:hover{color:var(--t1);background:var(--bg-3)}
body.tanpa-menu .tembok-penuh{display:inline-flex}
/* KETAHUAN 19 Agustus 2026 lewat uji: baris ini dulu menyebut `.nav-menu`,
   dan elemen itu lenyap saat menu pindah ke rel kiri. Modenya tetap menyala,
   tombolnya tetap berganti ikon, dan yang tidak terjadi cuma satu — menunya
   tidak pernah benar-benar hilang. Tidak ada error; layar dinding cuma diam
   saja memampangkan menu yang seharusnya sudah pergi.
   Bersama relnya, geseran isi halaman ikut dilepas — kalau tidak, jalur
   kosong selebar 56px tertinggal di tepi kiri layar TV. */
body.tanpa-menu .rail,
body.tanpa-menu .cari-utama{display:none}
/* `auto`, BUKAN `0` — dan bedanya justru paling terasa di sini. Mode ini
   dipakai di layar TV, yaitu layar yang hampir selalu lebih lebar daripada
   `--isi-maks`. `margin-left:0` akan memaku isi halaman ke tepi kiri dan
   membuang seluruh sisa lebarnya ke kanan; `auto` mengembalikannya ke
   pasangan `margin:0 auto` di bagian MAIN, jadi halamannya di tengah.
   Kerabat bug25 — lihat catatan panjang di atas aturan penggeser rel. */
body.tanpa-menu main{margin-left:auto}
body.tanpa-menu .conn-bar,
body.tanpa-menu .lisensi-bar{margin-left:0}

/* ── KETERANGAN PERANGKAT (halaman Detail, 15 Agustus 2026) ───────────────
   Catatan bebas: S/N, nomor aset, riwayat penggantian. Hanya ada di halaman
   Detail Perangkat — lihat catatan di detail.html.

   `white-space:pre-wrap` bagian intinya: orang menulis catatan sebagai daftar
   berbaris-baris ("S/N: xxx", baris baru, "antena diganti 3 Agu"), dan
   `pre-wrap` menyimpan pergantian barisnya sambil tetap membungkus baris
   panjang. `pre` biasa akan membuat satu baris panjang menggeser halaman ke
   samping; tanpa keduanya, seluruh catatan menyatu jadi satu paragraf dan
   daftar yang ditulis rapi terbaca berantakan. */
.ket-isi{
  white-space:pre-wrap;overflow-wrap:anywhere;
  font-size:12px;line-height:1.75;color:var(--t1);margin:0
}
/* Keadaan kosong. Ditulis miring & redup supaya tidak terbaca sebagai isi
   catatan yang sungguhan — kalimat "Belum ada keterangan" dengan warna yang
   sama seperti catatan asli adalah cara membuat orang mengira ada yang salah. */
.ket-kosong{font-size:12px;color:var(--t3);font-style:italic}
.ket-kotak{
  width:100%;min-height:132px;resize:vertical;
  font-family:var(--ui);font-size:12px;line-height:1.7;
  background:var(--bg-2);color:var(--t1);
  border:1px solid var(--border);border-radius:8px;padding:8px 12px;
  outline:none
}
.ket-kotak:focus{border-color:var(--blue)}
/* Penghitung karakternya memakai `.nilai-ekor` — ia menepi ke kanan sendiri,
   jadi tombol Simpan/Batal tidak perlu dibungkus div tambahan. */
.ket-kaki{display:flex;align-items:center;gap:8px;margin-top:8px;flex-wrap:wrap}

/* ── WPA Pre-shared Key (19 Agustus 2026) ────────────────────────────────
   Kartu penyimpanan kunci WPA di Detail Perangkat, hanya untuk admin.

   `user-select:all` pada nilainya, dan itu justru inti kartunya: kunci WPA
   disalin, tidak dibaca. Satu klik memilih seluruhnya — tanpa itu orang
   menyeret pilihan dengan tetikus dan kehilangan satu karakter di ujung,
   dan kunci yang salah satu karakter tidak bisa dibedakan dari kunci yang
   benar sampai perangkatnya menolak terhubung.

   `overflow-wrap:anywhere` karena 64 digit hex tidak punya spasi untuk
   dipatahkan, dan di layar HP baris itu akan menggeser seluruh halaman ke
   samping. */
/* Sederet kendali yang sejajar dan boleh membungkus. Dipakai kartu WPA di
   Detail Perangkat dan bilah alat di Discovery — dinamai netral justru
   karena dipakai keduanya. */
.baris-sejajar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.wpa-nilai{
  font-family:var(--mono);font-size:13px;color:var(--t1);
  background:var(--bg-2);border:1px solid var(--border);border-radius:8px;
  padding:8px 12px;overflow-wrap:anywhere;user-select:all
}
/* Titik-titik keadaan tertutup. Sengaja SEPANJANG TETAP dan bukan sepanjang
   kunci aslinya: jumlah titik yang mengikuti panjang sebenarnya memberi tahu
   panjang kunci kepada siapa pun yang melihat layar, dan itu keterangan yang
   tidak perlu diberikan. */
.wpa-tertutup{font-family:var(--mono);font-size:15px;color:var(--t3);letter-spacing:2px}
/* Jarak keterangan di kartu ini diatur dari SATU tempat — sebagai selektor
   turunan, bukan kelas baru: `.wpa-nota{margin-top:8px}` akan berisi persis
   sama dengan aturan lain yang cuma berisi `margin-top:8px`, dan
   TestTidakAdaKelasKembar menolak dua kelas berisi hal yang sama.

   Urutannya disengaja — `.f-err` DULU, `.set-hint` belakangan. aturan() di
   test_gaya_bersama.py mencari "<selektor>{" dan mengambil kecocokan
   TERAKHIR; menaruh `.f-err` di ujung membuat aturan ini menyamar sebagai
   deklarasi `.f-err` sendiri, dan uji yang menjaga isinya lalu membaca satu
   margin alih-alih warna merahnya. */
#wpaKartu .f-err,#wpaKartu .set-hint{margin-top:8px}

/* Kartu di halaman Detail Perangkat. Bentuknya persis kartu-kartu yang sudah
   ada di halaman itu (radius 10, tanpa bayangan — beda dari `.kartu` yang
   dipakai halaman berbentuk formulir), tapi sebagai kelas: halaman Detail
   sudah memakai 47 atribut `style="` dan tiap kartu baru yang ditulis sebaris
   menambah empat lagi. */
.det-kartu{background:var(--bg-1);border:1px solid var(--border);
  border-radius:10px;padding:20px;margin-bottom:16px}

/* ── Halaman /layout: denah susunan ───────────────────────────────────────
   Gambar kecil yang menunjukkan susunan sebuah pilihan SEBELUM dipilih.
   Dibuat dari grid + kotak kosong, bukan tangkapan layar: tangkapan layar
   berhenti benar begitu dashboardnya berubah sedikit saja, dan tidak ada
   satu pun uji yang bisa memberi tahu bahwa gambarnya sudah berbohong.

   `grid-template-areas` di sini adalah pernyataan yang SAMA dengan aturan
   `body[data-layout=…]` di atas — dijaga tetap sama oleh
   test_layout_dashboard.py::TestDenahMenggambarkanSusunannya. */
.denah{display:grid;gap:3px;background:var(--bg-2);border:1px solid var(--border);
  border-radius:7px;padding:4px;height:96px}
.denah > i{border-radius:3px;background:var(--border2);display:block;
  min-width:0;min-height:0}
/* Kartu angka & tabel diberi warna sendiri — keduanya yang paling berpindah
   antar susunan, dan denah yang seluruhnya abu-abu tidak menunjukkan apa pun. */
.denah > i.d-s{background:var(--blue);opacity:.5}
.denah > i.d-t{background:var(--t3);opacity:.45}
.denah-operasional{grid-template-columns:1fr 1fr 1.15fr;
  grid-template-areas:"s s a" "k k l" "b b l" "t t t" "z z z"}
.denah-tabel{grid-template-columns:1fr 1fr 1.15fr;
  grid-template-areas:"t t t" "t t t" "s s s" "a a l" "z z z"}
.denah-tembok{grid-template-columns:1fr 1fr 1.15fr;
  grid-template-areas:"s s s" "a a l" "b b l" "p p p"}
.d-s{grid-area:s}.d-k{grid-area:k}.d-b{grid-area:b}
.d-a{grid-area:a}.d-l{grid-area:l}.d-t{grid-area:t}.d-z{grid-area:z}
.d-p{grid-area:p}
/* Papan Perlu Diperhatikan diberi warna sendiri seperti kartu angka & tabel:
   ia yang menggantikan tabel di Layar Tembok, dan denah yang memperlihatkan
   dua kotak abu-abu berpindah tempat tidak menunjukkan apa yang berubah. */
.denah > i.d-p{background:var(--amber);opacity:.45}

.daftar-layout{display:grid;grid-template-columns:repeat(auto-fit,minmax(232px,1fr));gap:12px}
.pilih-layout{text-align:left;background:var(--bg-2);border:2px solid var(--border);
  border-radius:10px;padding:12px;cursor:pointer;color:var(--t1);
  font-family:var(--ui);display:flex;flex-direction:column;gap:8px}
.pilih-layout:hover{border-color:var(--t3)}
.pilih-layout.aktif{border-color:var(--blue)}
.pilih-layout-tanda{margin-left:auto;font-size:11px;color:var(--blue)}
.pilih-layout-ket{font-size:10px;color:var(--t3);line-height:1.45}

/* Pembungkus halaman berbentuk formulir/pengaturan: satu lajur di tengah,
   tidak selebar layar. Halaman Tampilan menulisnya sebaris; kelas ini
   dipakai halaman Layout supaya angka 1180px tidak lahir untuk kedua kali
   di tempat yang berbeda. */
.hal-sempit{width:100%;max-width:1180px;margin:0 auto;
  display:flex;flex-direction:column;gap:16px}
.f-gap18{gap:16px}
/* Kotak keterangan bernada tenang — bukan peringatan merah, bukan teks lepas
   yang hilang di antara kartu. Dipakai untuk syarat & batasan yang perlu
   dibaca sekali, lalu tidak lagi. */
.kotak-catat{font-size:11px;color:var(--t3);background:var(--bg-2);
  border-radius:8px;padding:8px 12px;line-height:1.55}

/* ── Penanda "kolom IP AP/ST tertukar" (15 Agustus 2026) ──────────────────
   Merah, bukan kuning seperti `PTMP`: PTMP itu keterangan tentang bagaimana
   jaringannya memang disusun, ini kekeliruan data yang perlu dibetulkan.

   Ukurannya dikirim pemanggil lewat `font-size` sebaris (badgeTertukar di
   app.js) — sama seperti ptmpBadge, karena penanda ini muncul di baris tabel
   9px dan di kepala kartu Detail 10px. */
.badge-tertukar{
  font-weight:700;padding:1px 4px;border-radius:3px;
  border:1px solid var(--red);color:var(--red);
  white-space:nowrap;flex-shrink:0;vertical-align:middle
}

/* Banner di halaman Detail Perangkat. Penanda kecil cukup untuk MENANDAI di
   daftar; di halaman yang justru menampilkan angka kedua sisi berdampingan,
   yang dibutuhkan kalimat utuh — orang di situ sedang membaca angka yang
   tertukar itu. */
.pesan-tertukar{
  display:flex;align-items:flex-start;gap:8px;
  background:rgba(239,68,68,.08);border:1px solid var(--red);
  border-radius:10px;padding:12px;margin-bottom:16px;
  font-size:12px;line-height:1.55;color:var(--t1)
}
.pesan-tertukar .ic{color:var(--red);flex-shrink:0;margin-top:1px}
.pesan-tertukar b{color:var(--red)}

/* ── Halaman Pointing Sinyal — gaya sebaris jadi kelas (15 Agustus 2026) ──
   TAMPILAN.md §2.1, halaman terakhir yang belum tersentuh. 27 → 11 atribut
   `style="`; sebelas sisanya memang tidak bisa jadi kelas — delapan
   `display:none` yang dibuka JS, dan tiga nilai yang dihitung saat itu juga
   (lebar bilah sinyal, warna menurut kualitas, warna titik online).

   Awalan `.pt-` mengikuti kelas yang sudah ada di halaman ini (`.pt-row`,
   `.pt-big`, `.pt-alert`), bukan nama baru yang berdiri sendiri. */

/* Halaman ini sengaja lebih sempit daripada yang lain: isinya dibaca teknisi
   sambil memutar antena, satu tangan memegang HP. 760px, bukan 1180px seperti
   `.hal-sempit` — jadi ia kelas sendiri, bukan pemakaian ulang yang dipaksakan. */
/* ── Halaman Pointing: dua kolom di layar lebar (21 Agustus 2026) ────────
   Diminta pengguna: *"halaman pointing juga disamakan agar full satu layar
   tidak menyisakan ruang kosong"*.

   Sebelumnya SATU kolom 760px di tengah layar 1920px: enam kartu bertumpuk,
   dan tiga terakhir di bawah lipatan sementara dua pertiga layar kosong.

   Pemisahannya bukan sekadar "belah dua": kiri yang DISETEL (perangkat, sisi,
   panduan suara — disentuh sekali di awal lalu dibiarkan), kanan yang DIBACA
   sambil menggeser antena. Mata tidak perlu melewati kontrol untuk sampai ke
   angkanya.

   DI BAWAH 1100px ia kembali jadi satu kolom 760px, persis seperti sebelumnya
   — dan itu yang penting: halaman ini dipakai DI LAPANGAN sambil memegang
   antena, hampir selalu dari HP. Yang berubah cuma perilaku di layar lebar,
   tempat halaman ini kebetulan juga dibuka. */
.pt-halaman{max-width:760px;margin:0 auto}

@media(min-width:1100px){
  /* TANPA `align-items:start` — dan itu yang tertinggal di perbaikan
     sebelumnya (bug37/38): `align-items:start` dibuang dari `.pt-bawah` tapi
     yang DI SINI tidak. Akibatnya kedua kolom cuma setinggi isinya
     masing-masing, jadi `flex:1` di dalamnya tidak punya sisa untuk diisi dan
     perbaikannya diam-diam tidak berlaku (bug39: kolom kiri berhenti di 765,
     kanan di 845). Satu properti, di satu tempat yang terlewat. */
  .pt-halaman{max-width:none;display:grid;grid-template-columns:392px minmax(0,1fr);
    gap:16px}
  /* Judul halaman MEMBENTANG dua kolom. Tanpa ini ia jadi anak grid PERTAMA
     dan mengambil kolom 1 untuk dirinya sendiri — seluruh isi halaman
     bergeser satu kolom, kartu yang seharusnya di kiri pindah ke kanan, dan
     kartu pembacaan terjepit di kolom 392px sementara dua pertiga layar
     kosong. Persis yang dilaporkan bug35 & bug36. */
  .pt-halaman > .pt-kepala{grid-column:1 / -1;margin-bottom:0}
  /* Kartu terakhir tiap kolom tidak perlu berjarak ke bawah — jaraknya sudah
     jadi urusan `gap` grid-nya. Tanpa ini ada 14px menggantung di bawah
     kolom yang lebih pendek, dan itu terbaca sebagai ruang kosong yang justru
     sedang dihilangkan. */
  .pt-halaman > div > .pt-card:last-child{margin-bottom:0}
  /* Grafik pergerakan, chain, dan detail link berdampingan: ketiganya kartu
     pendek, dan bertumpuk sendiri-sendiri membuat kolom kanan jauh lebih
     panjang daripada kiri — yaitu ruang kosong lagi, cuma pindah tempat. */
  /* `auto-fit`: kartu chain baru muncul pada perangkat berantena banyak, dan
     dua kolom mati membuat kartu ketiga turun sendirian ke baris baru. */
  /* TANPA `align-items:start` — itu yang membuat kartu berdampingan berhenti
     di tinggi masing-masing, dan tepi bawahnya tidak rata (bug38: grafik
     pergerakan berakhir di 680, Detail link di 713). Bawaan grid `stretch`
     membuat keduanya setinggi barisnya. */
  .pt-bawah{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));
    gap:12px}
  /* Baris terakhir kolom kanan MENGISI sisa tingginya, supaya dasar kedua
     kolom halaman ini rata. Kolom yang lebih pendek yang memanjang; di kolom
     yang lebih tinggi `flex:1` tidak punya sisa untuk diisi, jadi aturan yang
     sama aman dipasang di keduanya. */
  .pt-halaman > .kolom-sempit{display:flex;flex-direction:column}
  .pt-kanan > .pt-bawah{flex:1}
  .pt-kiri > .pt-card:last-child{flex:1}
  .pt-bawah > .pt-card{margin-bottom:0}
  /* `.pt-big` SENGAJA tidak disentuh. Ia sudah `clamp(56px,17vw,116px)`,
     jadi di layar lebar angkanya memang sudah 116px — mematoknya di sini cuma
     akan MENGECILKANNYA. Dan aturan itu tinggal di blok <style> pointing.html,
     yang dimuat SESUDAH berkas ini, jadi penimpaan berkekhususan sama dari
     sini tidak akan berlaku sama sekali. Dua alasan, keduanya cukup. */
}
/* Dipakai BERSAMA `.card-hdr`, bukan menggantikannya — keempat properti
   pertamanya sudah ada di sana, dan menyalinnya berarti dua aturan yang bisa
   menyimpang sendiri-sendiri. */
.pt-kepala{gap:12px;flex-wrap:wrap}
.pt-cari{position:relative;margin-bottom:12px}
.jarak-atas{margin-top:12px}
.jarak-atas-l{margin-top:16px}
/* Penggeser volume nada pointing. `accent-color` yang membuat relnya ikut
   warna aksen instalasi alih-alih biru bawaan sistem. */
.pt-geser{flex:1;min-width:140px;accent-color:var(--blue)}
.pt-geser-label{font-size:11px;color:var(--t2);font-weight:600}
/* Lebarnya dipatok supaya angka 5% dan 100% tidak menggeser penggesernya
   saat nilainya berubah — itu terjadi tiap kali slider disentuh. */
.pt-geser-nilai{font-size:11px;color:var(--t3);font-family:var(--mono);
  width:38px;text-align:right}
/* `.pt-catatan` DIHAPUS 21 Agustus 2026 — lihat `.catatan-kecil`. */
/* Tombol Reset. Padding 14px bukan salah ketik: ia ditekan dengan ibu jari di
   atas menara, bukan diklik dengan tetikus. */
.pt-reset{background:none;border:1px solid var(--border);border-radius:10px;
  padding:12px 16px;font-size:12px;font-weight:600;color:var(--t2);
  cursor:pointer;font-family:var(--ui)}
.pt-spark{width:100%;height:110px;display:block}

/* ══════════════════════════════════════════════════════════════════════
   GERAK YANG BISA DIMATIKAN (TAMPILAN §5)
   ══════════════════════════════════════════════════════════════════════
   §5 menulis aturannya: "gerak hanya untuk menunjukkan sesuatu berubah,
   tidak pernah sebagai hiasan", dan melarang "apa pun yang mengulang
   terus-menerus". Aplikasi ini punya enam tempat bergerak, dan sampai
   16 Agustus 2026 hanya DUA yang menghormati setelan sistem — sisanya
   tetap berdenyut selamanya bagi orang yang justru sudah mematikannya.
   Yang paling parah dua lencana merah: pada armada 135 perangkat, layar
   yang dipandangi delapan jam bisa berisi belasan kotak merah berdenyut
   serentak. Itu persis penyiksaan yang §5 sebut.

   Blok ini SATU dan tinggal di akhir berkas, bukan tersebar di dekat tiap
   komponennya. Alasannya bisa diperiksa: yang perlu dijamin di sini bukan
   "komponen X punya pengecualian" melainkan "TIDAK ADA satu pun gerak yang
   terlewat", dan jaminan itu cuma bisa dibaca kalau daftarnya di satu
   tempat. Letaknya di akhir juga yang membuat `animation-duration` di
   bawah menang — kekhususannya sama dengan aslinya, jadi yang menang
   urutan.

   Dua perlakuan, dan bedanya bukan selera:

   · Yang berdenyut sebagai PENEKANAN dimatikan sama sekali. Semuanya
     tetap menyampaikan hal yang sama tanpa gerak — titik hijau/merah
     sudah dibedakan warnanya, lencana pelanggaran sudah merah latar,
     merah garis, dan merah huruf. Denyutnya cuma menambah keras suara
     yang sudah terdengar. Cincin tipis menggantikannya supaya penekanan
     itu tidak hilang sama sekali.

   · Yang berputar sebagai tanda SEDANG BEKERJA tidak dimatikan,
     diperlambat. Pemutar yang berhenti tidak terbaca "gerak dimatikan",
     ia terbaca "macet" — dan itu kabar yang salah, bukan kabar yang
     tenang. */
@media (prefers-reduced-motion: reduce){
  .dot-live,.badge.vio,.st-down{animation:none}
  /* Kedipan baris ikut dimatikan — tapi TIDAK diganti apa pun yang diam.
     Bedanya dengan lencana di atas: lencana menandai KEADAAN, yang tetap
     berlaku selama layarnya dipandangi; kedipan ini menandai PERUBAHAN, yang
     sudah lewat begitu kedipannya selesai. Penanda diam untuk sesuatu yang
     sudah lewat justru berbohong. Yang berubah tetap terbaca di kolom Status
     dan di kartu Log. */
  .baris-kedip{animation:none}
  .badge.vio,.st-down{box-shadow:0 0 0 1px rgba(239,68,68,.45)}
  .kerangka{animation:none;opacity:.65}
  /* Popup muncul langsung, tanpa luncuran. Ia menandai SESUATU YANG
     BARU MUNCUL, dan kemunculannya sendiri sudah menyampaikan itu —
     tidak ada yang hilang saat geraknya dilepas. */
  .modal-kotak{animation:none}
  .refresh-btn.spinning svg,.auth-spinner{animation-duration:2.4s}
}

/* ── Halaman Maps (20 Agustus 2026) ───────────────────────────────────────
   Diminta pengguna: "hitam putih saja, menyesuaikan tema terang/gelap".

   Itu bukan pilihan gaya — ia menentukan bagaimana beda jenis titik
   disampaikan. Warna tidak tersedia, jadi yang membedakan BENTUK: BTS
   segitiga (menara), perangkat lingkaran, perangkat mati lingkaran berongga.
   Peta yang membedakan titiknya dengan warna akan kehilangan seluruh
   perbedaan itu begitu warnanya dilepas.

   Seluruh warna di berkas ini memakai --t1/--t2/--t3/--border, yang di
   palet mana pun berupa abu-abu. Jadi "hitam putih" bertahan sendiri saat
   tema diganti — tanpa aturan khusus per tema. */
.peta-alat{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding:12px 20px;border-bottom:1px solid var(--border)}
.peta-sel{max-width:260px}
.peta-centang{display:flex;align-items:center;gap:4px;font-size:11px;
  color:var(--t2);white-space:nowrap;cursor:pointer}

/* Bidang peta. `touch-action:none` supaya menggeser peta di HP tidak ikut
   menggulung halaman di belakangnya — tanpa itu, satu geseran melakukan dua
   hal sekaligus dan tidak satu pun terasa disengaja.
   `overscroll-behavior` menahan gulir-berantai ke <body> di ujung bidang. */
/* `user-select:none` melengkapi preventDefault() di penggesernya: yang satu
   menahan perilaku bawaan saat tombol ditekan, yang lain memastikan tidak ada
   apa pun di dalam bidang ini yang BISA terpilih sejak awal. Tanpa keduanya,
   menggeser peta meninggalkan jejak teks tersorot biru. */
.peta-wrap{position:relative;overflow:hidden;height:560px;
  background:var(--bg-2);cursor:grab;touch-action:none;user-select:none;
  overscroll-behavior:contain;outline:none}
.peta-wrap:focus-visible{outline:2px solid var(--blue);outline-offset:-2px}

/* ── BIDANGNYA MENGISI LAYAR ───────────────────────────────────────────────
   Dilaporkan pengguna 21 Agustus 2026: "buatkan pada tampilan maps jangan
   sampai ada area kosong, terus perhatikan juga agar tidak terlalu berat".

   Kosongnya ADA DUA, dan yang kedua yang jauh lebih besar.

   **Kosong pertama: di halamannya.** `height:560px` di atas angka mati — di
   layar 1080p ia menyisakan ratusan piksel menganggur di bawah kartunya,
   sementara di layar yang lebih pendek angka yang sama justru memaksa
   halamannya digulir. Sekarang bidangnya mengambil SISA lewat rantai flex
   main → .kartu → .peta-wrap, tanpa satu angka tinggi pun yang harus cocok
   dengan angka lain. 560px dibiarkan berdiri sebagai nilai bawaan: ia yang
   berlaku kalau kelak `.peta-wrap` dipakai di luar halaman ini.

   **Kosong kedua: di dalam bidangnya.** Latar jalan hanya ada di dalam
   KOTAK_UTAMA (1,39° bujur × 0,58° lintang, scripts/kotak_peta.py); di luar
   itu tidak ada yang pernah diunduh, jadi yang tergambar cuma jaring
   lintang-bujur di atas latar kosong. Terukur dari 127 titik yang benar-benar
   berkoordinat di basis data ini (armada 0,80° bujur × 0,46° lintang):
   pada bidang 1808×560 hari ini, **58% bidang berada di luar kotak datanya**.

   Yang menentukan bukan lebar bidangnya melainkan TINGGI. petaPasSemua()
   memilih zoom terbesar yang masih memuat seluruh sebaran, dan sebaran itu
   670px tinggi pada zoom 11 — jadi bidang yang lebih pendek daripada
   670 + tepinya jatuh ke zoom 10, dan di zoom 10 bidang selebar 1808px memuat
   2,48° bujur, hampir dua kali lebar kotak datanya. Begitu bidangnya cukup
   tinggi untuk zoom 11, yang terlihat 1,24° × 0,49° — seluruhnya di dalam
   kotak data, tanpa satu tepi pun yang kosong.

   Ambang itu TIDAK ditulis di sini, dan itu keputusan. Angkanya turunan dari
   tiga hal yang semuanya berubah tanpa ada yang ingat menyesuaikan CSS: lebar
   bidang di layar yang sedang dipakai, sebaran armada yang sungguhan, dan
   kotak yang benar-benar terunduh. petaTinggiMinimal() di maps.html
   menghitungnya tiap kali dan memasangnya sebagai `min-height` sebaris —
   di situ pula seluruh alasannya ditulis. Yang di berkas ini cuma rantai flex
   yang membuat bidangnya mengambil sisa selama lantai itu sudah terpenuhi.

   "Jangan terlalu berat" ikut terjawab, bukan dikorbankan: zoom 11 memakai
   daftar kelas jalan yang SAMA dengan zoom 10 (_JALAN_ZOOM di maps.html),
   sementara wilayah yang terlihat menyusut jadi seperempatnya — ruas yang
   lolos penyaring tiap bingkai jadi jauh lebih SEDIKIT, bukan lebih banyak.
   Jalan lingkungan tetap di luar: ambangnya _DETAIL_ZOOM = 14. */
.hal-peta main{display:flex;flex-direction:column;
  min-height:calc(100vh - 56px)}
/* Kelas KEDUA di kartunya (`class="kartu peta-kartu"`), bukan
   `.hal-peta .kartu`. Aturan yang berakhir dengan `.kartu{` ikut terjaring
   pemeriksa gaya bersama yang mengambil aturan `.kartu` TERAKHIR — dan yang
   terbaca lalu keempat deklarasi flex ini, bukan latar & bingkai kartunya. */
.peta-kartu{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
/* `min-height`, bukan `height`: daftar "belum bisa dipetakan" di kaki kartu
   panjangnya mengikuti berapa perangkat yang kolom lokasinya belum diisi.
   Dengan tinggi mati, daftar yang panjang akan terpotong diam-diam; dengan
   lantai, bidangnya mengalah sampai 360px lalu halamannya boleh digulir —
   dan yang tergulir daftarnya, bukan petanya. */
.hal-peta .peta-wrap{flex:1 1 auto;height:auto;min-height:360px}
.peta-menarik{cursor:grabbing}
.peta-ubin{position:absolute;inset:0}

/* Ubin dibuat abu-abu di sini, bukan diunduh dalam keadaan abu-abu: berkas
   yang sama tetap benar kalau kelak petanya boleh berwarna, dan `filter`
   dihitung GPU tanpa biaya yang terasa.
   Di tema gelap ubinnya dibalik — peta terang di halaman gelap adalah kotak
   putih menyilaukan di tengah layar, dan itu tepat yang dihindari orang yang
   memilih tema gelap. `invert` sesudah `grayscale` supaya yang dibalik
   kecerahannya saja. */
.peta-ubin-img{position:absolute;width:256px;height:256px;
  filter:grayscale(1) contrast(.85) brightness(1.05);
  user-select:none;pointer-events:none}
body[data-tema="malam"] .peta-ubin-img,
body[data-tema="grafit"] .peta-ubin-img,
body[data-tema="noc"] .peta-ubin-img,
body[data-tema="kontras"] .peta-ubin-img{filter:grayscale(1) invert(1) contrast(.8) brightness(.9)}

/* Lapisan garis jalan — paling bawah, di bawah ubin maupun penanda.
   Digambar di canvas karena jumlahnya belasan ribu ruas; sebagai elemen SVG
   ia akan jadi belasan ribu simpul DOM yang dihitung ulang tiap geseran.
   Warnanya TIDAK diatur di sini: canvas tidak mewarisi CSS, jadi warnanya
   dibaca dari tema oleh petaGambarJalan(). */
/* Keduanya lapisan yang menutup bidang peta penuh dan tidak menangkap
   tetikus — jadi SATU aturan, bukan dua yang isinya kebetulan sama.
   `.peta-jalan` ditulis di ujung supaya aturan ini tidak menyamar sebagai
   deklarasi `.peta-svg` bagi aturan() di test_gaya_bersama.py. */
.peta-svg,.peta-jalan{position:absolute;inset:0;pointer-events:none}
.peta-svg circle,.peta-svg g{pointer-events:auto}

/* Garis lintang-bujur. Tipis dan redup: ia acuan, bukan isi — jaring yang
   sama tegasnya dengan garis ST↔AP membuat keduanya tertukar. */
.peta-jaring{stroke:var(--border);stroke-width:1;opacity:.7}
.peta-jaring-teks{fill:var(--t3);font-size:9px;font-family:var(--mono)}
/* Tanpa ubin, jaringnya naik jadi satu-satunya acuan yang ada — jadi ia
   boleh sedikit lebih terbaca. */
.peta-tanpa-ubin .peta-jaring{opacity:1}

/* Garis ST ↔ AP. Putus-putus, bukan utuh: garis utuh di antara 105 titik
   terbaca sebagai jalan di peta, dan yang dimaksud justru bukan jalan —
   ia hubungan radio yang melintas di atas semuanya. */
.peta-tali{stroke:var(--t3);stroke-width:1.2;stroke-dasharray:4 3;opacity:.75}
/* Garis KEDUA di posisi yang sama, tebal dan tak terlihat, yang menerima
   tunjukan kursor. Garis selebar 1,2px praktis mustahil dikenai — dan yang
   gagal mengenainya akan mengira tooltipnya tidak ada. Pola yang sama dengan
   pegangan seret kolom di modul tabel. */
.peta-tali-tunjuk{stroke:transparent;stroke-width:12;fill:none;cursor:help}

/* ══ SURVEI JANGKAUAN (butir 7–9, 27 Agustus 2026) ═════════════════════════
   Panel DI SAMPING peta, bukan halaman tersendiri — permintaan pengguna, dan
   alasannya: yang dibaca teknisi daftar BTS SAMBIL melihat titiknya. */
.peta-bidang{display:flex;gap:12px;align-items:stretch;flex:1 1 auto;min-height:0}
.peta-bidang > .peta-wrap{flex:1 1 auto;min-width:0}
.sv-panel{flex:0 0 320px;max-width:340px;overflow-y:auto;
  background:var(--bg-1);border:1px solid var(--border);border-radius:10px;
  padding:12px;font-size:12px}
.sv-kepala{display:flex;align-items:center;justify-content:space-between;
  font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.09em;
  color:var(--t3);margin-bottom:8px}
.sv-titik{font-size:11px;color:var(--t2);margin-bottom:8px}
.sv-kosong{color:var(--t3);line-height:1.6}
.sv-daftar{list-style:none;margin:0;padding:0;display:flex;
  flex-direction:column;gap:8px}
.sv-item{background:var(--bg-2);border:1px solid var(--border);
  border-radius:8px;padding:8px}
.sv-terdekat{border-color:var(--blue)}
.sv-nama{font-weight:700;color:var(--t1);margin-bottom:4px}
.sv-tag{font-size:9px;font-weight:700;text-transform:uppercase;
  letter-spacing:.09em;color:var(--blue)}
.sv-ukur{display:flex;flex-wrap:wrap;gap:8px;font-size:11px;color:var(--t2);
  line-height:1.6}
.sv-model{font-size:10px}
.sv-kelas{font-family:var(--mono);font-weight:700}
/* Tiga kelas jangkauan — ambangnya sama dengan seluruh aplikasi, lihat
   AMBANG_* di src/survei.py. Panel ini BUKAN bagian peta hitam-putihnya:
   ia daftar angka, dan warna di sini yang membuat "cukup" dan "lemah"
   terbedakan tanpa dibaca satu per satu. */
.sv-kelas.sv-baik{color:var(--green)}
.sv-kelas.sv-cukup{color:var(--amber)}
.sv-kelas.sv-lemah{color:var(--red)}
.sv-kelas.sv-tak{color:var(--t3)}
/* `.catatan-kecil` yang sudah ada dipakai apa adanya untuk keterangannya;
   yang di bawah cuma menambah garis pemisah di catatan penutup. */
.sv-catat-akhir{border-top:1px solid var(--border2);padding-top:8px;margin-top:8px}
.sv-koord{display:inline-flex;gap:4px;align-items:center}
.sv-koord input{width:180px}
/* Pencarian alamat — tambahan yang baru muncul kalau admin menyalakannya
   (src/geokode.py). `position:relative` di sini yang menahan daftar hasilnya,
   pola yang sama dengan `.cari-utama` di atas. */
.sv-alamat{position:relative;display:inline-flex;gap:4px;align-items:center}
.sv-alamat input{width:200px}
/* z-index 60 menyamai `.cari-utama-hasil`: daftarnya jatuh ke ATAS peta, dan
   peta punya penanda, tooltip, serta tombol zoom-nya sendiri di bawah sana. */
.sv-alamat-hasil{position:absolute;top:calc(100% + 6px);left:0;z-index:60;
  min-width:280px;max-width:min(420px,80vw);max-height:min(50vh,300px);
  overflow:hidden auto;background:var(--bg-1);border:1px solid var(--border);
  border-radius:10px;box-shadow:0 12px 30px rgba(0,0,0,.28);padding:4px}
.sv-alamat-opsi{display:flex;flex-direction:column;gap:2px;width:100%;
  text-align:left;background:none;border:0;border-radius:8px;padding:8px;
  font-family:var(--ui);font-size:11px;color:var(--t2);cursor:pointer}
.sv-alamat-opsi:hover,.sv-alamat-opsi:focus{background:var(--bg-3)}
.sv-alamat-nama{font-size:12px;color:var(--t1);line-height:1.4}
.sv-alamat-kabar{padding:8px;font-size:11px;color:var(--t3);line-height:1.5}
/* Atribusi ODbL. Menempel pada DAFTARNYA, bukan di kaki peta: yang di kaki
   peta menyebut sumber ubin, dan ini sumber yang berbeda. */
.sv-alamat-atribusi{padding:8px;border-top:1px solid var(--border2);
  line-height:1.5}
/* Kursor bidik selama mode survei menyala — satu-satunya tanda bahwa klik
   berikutnya akan berarti sesuatu yang lain daripada biasanya. */
.sv-membidik{cursor:crosshair}
/* Lapisan di peta. Lingkaran & garisnya ikut aturan hitam-putih peta: yang
   membedakan kelas jangkauan KETEBALAN dan kerapatan garis putus, bukan
   warna. */
.sv-lingkar{fill:none;stroke:var(--t2);stroke-dasharray:6 5;opacity:.55}
.sv-lingkar-baik{stroke-width:1.6;stroke-dasharray:none;opacity:.75}
.sv-lingkar-cukup{stroke-width:1.2}
.sv-garis{stroke:var(--t1);stroke-width:1.6;opacity:.85}
.sv-garis-cukup{stroke-dasharray:5 4}
.sv-garis-lemah{stroke-dasharray:2 4;opacity:.6}
.sv-garis-tak{stroke-dasharray:1 5;opacity:.4}
.sv-pin{fill:var(--t1);stroke:var(--bg-0);stroke-width:1.5}
.sv-pin-inti{fill:var(--bg-0)}

/* ── Profil medan (LOS & Fresnel) ────────────────────────────────────────
   Diagramnya HITAM-PUTIH seperti sisa peta: yang membedakan tanah, garis
   pandang, dan tepi zona Fresnel adalah ketebalan dan kerapatan garis putus.
   Angka di sekelilingnya (putusan, kelas) bukan bagian gambar — di sana
   warna boleh, sama seperti panel di samping peta. */
.sv-prof-tombol{background:var(--bg-3);border:1px solid var(--border);
  color:var(--t2);border-radius:6px;padding:4px 8px;font-size:10px;
  cursor:pointer;margin-top:8px}
.sv-prof-tombol:hover{color:var(--t1);border-color:var(--t3)}
.sv-prof-putusan{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;
  font-size:11px;color:var(--t2);margin-bottom:12px}
.sv-prof-gambar{background:var(--bg-2);border:1px solid var(--border);
  border-radius:8px;padding:8px;overflow-x:auto}
.sv-prof-svg{display:block;width:100%;min-width:520px;height:auto}
.sv-prof-isi{fill:var(--t3);opacity:.22}
.sv-prof-tanah{fill:none;stroke:var(--t1);stroke-width:1.6}
.sv-prof-los{fill:none;stroke:var(--t1);stroke-width:1.2;stroke-dasharray:none}
.sv-prof-fresnel{fill:none;stroke:var(--t2);stroke-width:1;stroke-dasharray:5 4}
.sv-prof-fresnel60{fill:none;stroke:var(--t2);stroke-width:1;stroke-dasharray:1 4}
.sv-prof-kritis{stroke:var(--t2);stroke-width:1;stroke-dasharray:2 3;opacity:.8}
.sv-prof-jaring{stroke:var(--border2);stroke-width:1}
/* Angka sumbunya memakai .peta-jaring-teks yang sudah ada — bentuknya memang
   sama persis (angka kecil monospace di dalam SVG), dan kelas kedua dengan
   deklarasi yang identik berarti dua tempat yang harus diubah berbarengan
   saat suatu saat ukurannya disetel. */
.sv-prof-kotak{max-width:820px}
.sv-prof-atur{display:flex;flex-wrap:wrap;gap:8px;align-items:center;
  margin-top:12px}
.sv-prof-input{width:96px}
.sv-prof-ukur{margin-top:8px}

@media (max-width:900px){
  /* Panel turun ke bawah peta. Di layar selebar itu, panel 320px di samping
     menyisakan peta yang terlalu sempit untuk dipakai memilih titik. */
  .peta-bidang{flex-direction:column}
  .sv-panel{flex:0 0 auto;max-width:none;max-height:320px}
}
/* Tanpa warna — peta ini hitam putih atas permintaan pengguna, dan tooltip
   ikut di dalamnya. "Tidak online" dibedakan dengan tebal, bukan merah. */
.peta-tip-mati{font-weight:700;color:var(--t1)}

.peta-st{fill:var(--bg-1);stroke:var(--t1);stroke-width:2;cursor:pointer}
.peta-st:hover{stroke-width:3}
/* Perangkat yang sedang tidak online: berongga & putus-putus. Dibedakan
   BENTUK, bukan warna — lihat catatan di kepala bagian ini. */
.peta-mati{stroke:var(--t3);stroke-dasharray:2.5 2.5}
.peta-ap-bentuk{fill:var(--t1);stroke:var(--bg-1);stroke-width:1.5;cursor:pointer}
.peta-ap:hover .peta-ap-bentuk{stroke-width:2.5}
.peta-ap-nama{fill:var(--t1);font-size:10px;font-weight:700;text-anchor:middle;
  paint-order:stroke;stroke:var(--bg-1);stroke-width:3;pointer-events:none}

.peta-zoom{position:absolute;top:12px;right:12px;display:flex;
  flex-direction:column;gap:4px}
.peta-zbtn{width:30px;height:30px;display:flex;align-items:center;
  justify-content:center;font-size:16px;line-height:1;
  background:var(--bg-1);border:1px solid var(--border);border-radius:8px;
  color:var(--t1);cursor:pointer}
.peta-zbtn:hover:not(:disabled){border-color:var(--blue);color:var(--blue)}
.peta-zbtn:disabled{opacity:.4;cursor:not-allowed}

.peta-kaki{position:absolute;left:12px;bottom:12px;display:flex;
  align-items:flex-end;gap:8px;pointer-events:none}
/* Batang skala: garis di tiga sisi, terbuka ke atas — bentuk baku batang
   skala peta, dan lebarnya diisi JS menurut zoom saat itu. */
.peta-skala{border:1px solid var(--t2);border-top:none;height:9px;
  font-size:9px;color:var(--t2);font-family:var(--mono);
  padding-left:3px;line-height:9px;white-space:nowrap}

.peta-tip{position:absolute;z-index:5;pointer-events:none;max-width:280px;
  background:var(--bg-1);border:1px solid var(--border);border-radius:8px;
  padding:8px;font-size:11px;color:var(--t2);
  box-shadow:0 4px 14px rgba(0,0,0,.18)}
.peta-tip-judul{font-size:12px;font-weight:700;color:var(--t1);margin-bottom:4px}
.peta-tip-sisi{line-height:1.6}
/* Label baris tooltip. `width:18px` MATI sejak 27 Agustus 2026 — lebar itu
   lahir saat labelnya masih dua huruf ("AP"/"ST"), dan begitu tooltip ditulis
   ulang dengan kata "Sinyal", "Kapasitas", "Frekuensi", teksnya meluber keluar
   kotaknya lalu ditabrak nilainya: yang terbaca pengguna "Kapasitas↑300 Mbps"
   menyatu jadi satu. Dilaporkan pengguna, dan sebabnya persis lebar tetap yang
   tidak ikut berubah bersama isinya.

   `min-width` menggantikannya: cukup untuk membuat seluruh nilai berbaris rata
   di kolom yang sama, dan MELAR sendiri kalau suatu saat ada label yang lebih
   panjang daripada perkiraan hari ini. `vertical-align:top` supaya nilai yang
   membungkus ke baris kedua tidak menyelinap ke bawah labelnya. */
.peta-tip-sisi b{color:var(--t3);font-size:9px;display:inline-block;
  min-width:56px;vertical-align:top}
.peta-mono{font-family:var(--mono);color:var(--t3)}
.peta-tip-ket{font-size:10px;color:var(--t3);margin-top:4px}

.peta-pesan{position:absolute;top:12px;left:12px;font-size:11px;
  color:var(--t3);background:var(--bg-1);border:1px solid var(--border);
  border-radius:8px;padding:4px 8px;pointer-events:none}

.peta-sisa{padding:12px 20px;border-top:1px solid var(--border)}
/* Baris ringkasnya — satu baris, dan itu seluruh gunanya. Terbuka, bagian ini
   ±157px dan seluruhnya diambil dari bidang peta di atasnya; terlipat ia
   ±42px. Alasan lengkapnya di petaTulisSisa() (maps.html).

   `list-style:none` + `::-webkit-details-marker` — segitiga bawaan <details>
   dibuang karena penandanya di sini kata di ujung kanan, dan dua penanda untuk
   satu tombol membuat keduanya jadi hiasan. Keduanya ditulis: peramban
   berbasis WebKit mengabaikan yang pertama. */
.peta-sisa-ringkas{list-style:none;cursor:pointer;display:flex;
  align-items:center;gap:8px;font-size:11px;font-weight:700;color:var(--t2)}
.peta-sisa-ringkas::-webkit-details-marker{display:none}
.peta-sisa-ringkas:hover{color:var(--t1)}
/* Katanya datang dari CSS, bukan dari HTML-nya, karena ia BERGANTI: label
   yang tetap berbunyi "lihat daftarnya" saat daftarnya sudah terbuka adalah
   tombol yang berbohong. Tanda panah sengaja tidak dipakai — halaman ini
   hitam putih, dan bentuk kecil berujung tajam di ujung baris terbaca sebagai
   ikon status, bukan sebagai tombol. */
.peta-sisa-buka{margin-left:auto;font-size:10px;font-weight:400;
  color:var(--t3);white-space:nowrap;text-decoration:underline}
.peta-sisa-lipat:not([open]) .peta-sisa-buka::after{content:'lihat daftarnya'}
.peta-sisa-lipat[open] .peta-sisa-buka::after{content:'tutup daftarnya'}
.peta-sisa-judul{font-size:11px;font-weight:700;color:var(--t2);margin-bottom:2px}
.peta-sisa-ket{font-size:10px;color:var(--t3);line-height:1.5;margin-bottom:8px}
.peta-sisa-daftar{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px}
.peta-sisa-butir{font-size:10px;color:var(--t2);background:var(--bg-2);
  border:1px solid var(--border);border-radius:8px;padding:4px 8px;
  text-decoration:none}
a.peta-sisa-butir:hover{border-color:var(--blue);color:var(--blue)}

@media (max-width:768px){
  /* Di HP bidangnya dibuat lebih pendek supaya penyaring di atas dan daftar
     "belum bisa dipetakan" di bawah tetap terjangkau tanpa menggulung jauh.
     Yang diturunkan LANTAInya, bukan tingginya: rantai flex di atas tetap
     berlaku di sini, jadi angka pasti apa pun akan mengalahkannya dan
     mengembalikan ruang kosong yang baru saja dibuang. */
  .peta-wrap{height:400px}
  .hal-peta .peta-wrap{min-height:320px}
  .peta-alat{padding:8px 12px}
  .peta-sisa{padding:12px}
}

/* ── Halaman Discovery & Unadopted Devices (20 Agustus 2026) ──────────────
   Diminta pengguna: memindai rentang IP, lalu mengadopsi yang belum
   terdaftar. Bentuknya mengikuti halaman berformulir yang sudah ada
   (.set-wrap, .stats-grup, .input-penuh) supaya tidak ada tata letak baru yang
   harus dipelajari untuk satu fitur. */
/* Dipakai juga oleh kotak konfirmasi aksi massal (app.js) — isinya sama
   persis: tumpukan tegak berjarak 12px di dalam bantalan kartu. */
.dis-isi{padding:16px 20px;display:flex;flex-direction:column;gap:12px}
/* Badge jenis perangkat di hasil Discovery (27 Agustus 2026). Tiga warna, dan
   yang ketiga sama pentingnya dengan dua yang pertama: `unknown` bukan
   kegagalan pembacaan melainkan jawaban — alamat yang cuma menjawab ping
   memang tidak memberi tahu apa pun tentang dirinya, dan menyajikannya
   seolah-olah radio berarti menyatakan sesuatu yang tidak diketahui. */
.dis-badge{display:inline-block;font-size:10px;font-weight:700;padding:1px 8px;
  border:1px solid var(--border);border-radius:20px;white-space:nowrap}
/* Selektor gabungan, bukan kelas warna berdiri sendiri: warnanya cuma
   berarti DI ATAS badge, dan kelas warna yang lepas akan jadi kembaran
   `.act-ubah` / `.t-redup` — kembaran yang berarti mengubah warna tombol Ubah
   ikut mengubah badge di halaman lain. */
.dis-badge.dis-radio{border-color:rgba(59,130,246,.4);color:var(--blue)}
.dis-badge.dis-non-radio{border-color:rgba(245,158,11,.4);color:var(--amber)}
/* Miring, dan itu isyarat KETIGA sesudah warna dan ikon: "Belum diketahui"
   bukan nama jenis melainkan pernyataan bahwa jenisnya belum diketahui, dan
   huruf miring adalah cara tulisan menyatakan dirinya bukan nilai. */
.dis-badge.dis-unknown{color:var(--t3);font-style:italic}
.dis-badge .ic{width:12px;height:12px;vertical-align:-2px;margin-right:4px}

/* Bilah kemajuan. Pemindaian satu /24 makan puluhan detik, dan tombol yang
   cuma berubah jadi "memindai…" tanpa angka membuat orang menekan ulang atau
   menutup halaman. */
.dis-bar{height:4px;background:var(--bg-3);border-radius:2px;overflow:hidden}
.dis-bar-isi{height:100%;width:0;background:var(--blue);transition:width .3s}

.dis-tabel{width:100%;border-collapse:collapse}
.dis-tabel td{padding:8px 12px;font-size:12px;color:var(--t2);
  border-top:1px solid var(--border)}
.dis-tabel td.th-tepi{padding:8px 20px}
.dis-kosong{text-align:center;padding:24px;color:var(--t3);font-size:12px}

/* Halaman Unadopted ditulis ulang jadi TABEL BERPILIHAN 20 Agustus 2026,
   diminta pengguna — dan bentuknya mengikuti Kelola Perangkat: melebar penuh,
   bukan kartu sempit yang dipusatkan dengan area kosong di kiri-kanannya.
   Karena itu hampir seluruh gayanya dipinjam dari kartu tabel yang sudah ada,
   dan yang tersisa di bawah cuma yang memang belum ada. */
.una-kepala-hal{display:flex;align-items:flex-end;justify-content:space-between;
  gap:12px;flex-wrap:wrap;margin-bottom:12px}
.una-ip{font-size:13px;font-weight:700;color:var(--t1);font-family:var(--mono)}
/* Kolom kotak centang: selebar isinya saja, dan tidak ikut menyusut saat
   tabelnya sempit — kotak centang yang terpotong tidak bisa ditekan. */
.una-th-pilih{width:38px;padding-left:20px;padding-right:0;text-align:left}
.una-terpilih td{background:var(--bg-2)}

/* ══ AKSI MASSAL — kotak konfirmasi & bilah pilihan ═════════════════════════
   Diminta pengguna 26 Agustus 2026, berikut kejadiannya: "tidak sengaja
   memilih seluruh perangkat Unadopted, kemudian ikut ter-add/adopt". */

.konf-ringkas{font-size:13px;line-height:1.6;color:var(--t1)}
.konf-ringkas b{color:var(--amber)}
/* Daftar nama yang terkena. Lima saja — daftar 87 baris membuat dialognya
   menggulung, dan yang menggulung dibaca sama saja dengan yang tidak ada. */
.konf-daftar{margin:0;padding:8px 12px 8px 28px;list-style:disc;
  background:var(--bg-2);border-radius:8px;font-size:12px;color:var(--t2);
  line-height:1.8;max-height:160px;overflow-y:auto}
.konf-sisa{list-style:none;margin-left:-16px;color:var(--t3);font-style:italic}
.konf-ketik{background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.35);
  border-radius:8px;padding:12px}
/* Label sendiri, bukan `.f-label` dengan warna ditimpa: aturan seperti
   `.konf-ketik .f-label{…}` membuat definisi `.f-label` tidak lagi bisa dibaca
   di satu tempat — dan ada uji yang memang menjaga itu. */
.konf-ketik-label{display:block;font-size:10px;font-weight:700;
  text-transform:uppercase;color:var(--red)}
.konf-ketik b{font-family:var(--mono);font-size:14px}

/* Bilah aksi massal — muncul HANYA saat ada yang terpilih, tepat di atas
   tabelnya. Di atas, bukan di bawah: yang memilih baris sedang memandang
   tabel, dan tombol yang muncul di luar pandangan itu tidak ditemukan. */
/* TERSEMBUNYI SECARA BAWAAN, dan tampil hanya lewat `.buka`.
   Arahnya sengaja begini — bukan `display:flex` yang ditimpa `.sembunyi`.
   Keduanya berkekhususan (0,1,0), jadi yang menang cuma urutan penulisan; di
   berkas sepanjang ini urutan bukan sesuatu yang bisa diandalkan, dan yang
   dilaporkan pengguna 27 Agustus 2026 persis akibatnya: baris kosong yang
   selalu menganga di atas tabel. Keadaan bawaan yang benar tidak boleh
   bergantung pada baris ke berapa ia ditulis. */
.pilih-bar{display:none;align-items:center;gap:8px;flex-wrap:wrap;
  background:var(--bg-2);border:1px solid var(--blue);border-radius:10px;
  padding:12px 16px;margin-bottom:12px}
.pilih-bar.buka{display:flex}
.pilih-jumlah{font-size:12px;font-weight:700;color:var(--t1);margin-right:4px}
.pilih-jumlah b{color:var(--blue);font-family:var(--mono)}
/* Perluasan ke SELURUH hasil saring sengaja jadi tautan terpisah, bukan
   perilaku bawaan "Pilih semua". Itu klik kedua yang disengaja — permintaan
   pengguna, dan pencegah kejadian yang dilaporkannya. */
.pilih-perluas{background:none;border:none;padding:0;font-size:12px;
  color:var(--blue);cursor:pointer;text-decoration:underline;font-family:var(--ui)}
.pilih-bersih{margin-left:auto}
.pilih-bar select{max-width:200px}
/* Baris yang terpilih. Warnanya sama dengan sorotan hover supaya tidak lahir
   warna kedua untuk "baris yang sedang diperhatikan"; yang membedakan garis
   biru di tepi kiri. */
.baris-terpilih > td{background:var(--bg-2)}
.baris-terpilih > td:first-child{box-shadow:inset 3px 0 0 var(--blue)}
/* Kolom kotak centang di Kelola Perangkat. Selebar isinya, dan tidak ikut
   menyusut — bentuk yang sama dengan `.una-th-pilih` di Unadopted. */
.th-pilih{width:38px;padding-right:0;text-align:left}
.pilih-kemajuan{font-size:11px;color:var(--t3);margin-left:4px}
.una-catatan{margin-top:12px;font-size:11px;color:var(--t3);line-height:1.6}

@media (max-width:640px){
  .dis-isi{padding:12px}
  .una-th-pilih{padding-left:12px}
  /* Di layar sempit tombol Adopt turun ke barisnya sendiri dan melebar —
     sasaran sentuh selebar itu jauh lebih mudah dikenai. */
  /* `.btn-utama` DITULIS LEBIH DULU, `.baris-sejajar` di ujung — dan itu
     bukan selera. aturan() di test_gaya_bersama.py mencari "<selektor>{" lalu
     mengambil kecocokan TERAKHIR; menaruh `.btn-utama` di ujung membuat
     aturan ini menyamar sebagai deklarasi `.btn-utama` sendiri, dan uji yang
     menjaga isinya lalu membaca satu `width` alih-alih seluruh gayanya.
     Kejadian yang sama pernah menimpa `.f-err` (bug25). */
  .una-kepala-hal .btn-utama,
  .una-kepala-hal .baris-sejajar{width:100%}
}

/* ── Halaman Bandwidth Test: dua kolom, satu layar (21 Agustus 2026) ──────
   Diminta pengguna: *"usahakan agar dalam satu tampilan saja, jadi tidak
   perlu di scroll ke bawah, bisa posisikan configurasi pengetesan di sebelah
   kiri, lalu hasilnya ada di sebelah kanan"*.

   Sebelumnya satu kolom 900px di tengah layar 1920px: peringatan, form, dua
   kartu hasil, lalu tabel per detik — dan tabelnya sudah di bawah lipatan.
   Separuh kanan layar kosong sementara isinya perlu digulir.

   Kolom kiri LEBAR TETAP: isinya form, dan form yang melebar tidak jadi
   lebih mudah dibaca — cuma barisnya jadi panjang. Kolom kanan mengambil
   sisanya karena grafik justru makin terbaca makin lebar. */
/* TANPA `align-items:start`: kedua kolom setinggi barisnya, jadi dasarnya
   rata. Yang mengisi sisa tinggi kolom kiri kartu formulirnya (`.bw-form`
   di bawah) — bukan ruang kosong yang menggantung. */
.bw-halaman{display:grid;grid-template-columns:392px minmax(0,1fr);gap:16px}
/* KEDUA kolom kolom-flex, bukan cuma yang kiri. Sampai 16 September 2026
   `.bw-kanan` elemen blok biasa dan `.bw-hasil` di dalamnya memakai
   `height:100%` — benar selama ia anak TUNGGAL. Begitu baris "belum ada
   tes" ikut tampil di atasnya (kerangka hasil yang diminta pemilik),
   "100% dari tinggi induk" berarti melewati dasar induknya sejauh tinggi
   baris itu: terukur 951px pada layar 900px, yaitu halaman yang perlu
   digulir justru karena penambahan yang dimaksudkan menghilangkan
   gulirannya. */
.bw-kiri, .bw-kanan{display:flex;flex-direction:column;gap:12px;min-width:0}

.bw-warn{background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.35);
  border-radius:8px;padding:12px;font-size:11px;color:var(--amber);line-height:1.6}
.bw-kosong{display:none;background:var(--bg-1);border:1px solid var(--border);
  border-radius:10px;padding:16px;font-size:12px;color:var(--t2);line-height:1.7}
.bw-form{display:none;margin-bottom:0;flex:1}
/* Dua kolom di dalam kolom kiri yang sudah sempit: enam medan bertumpuk satu
   per satu membuat tombolnya turun jauh di bawah lipatan — yaitu hal yang
   justru sedang diperbaiki. */
.bw-l{display:block;font-size:10px;font-weight:700;text-transform:uppercase;
  letter-spacing:.06em;color:var(--t3);margin-bottom:4px}
.bw-h{font-size:10px;color:var(--t3);margin-top:4px;line-height:1.4}
.bw-tombol{background:var(--blue-permukaan);border:none;border-radius:8px;padding:8px 20px;
  font-size:12px;font-weight:700;color:var(--fg-aksen);cursor:pointer;
  font-family:var(--ui)}
#bwRun:disabled{pointer-events:none;opacity:.55;cursor:not-allowed}
.bw-err{font-size:11px;color:var(--red);margin-top:8px;min-height:14px;line-height:1.6}

/* Keadaan sebelum tes pertama. SATU BARIS, bukan lagi kotak setinggi 320px:
   sejak 16 September 2026 kerangka hasilnya sendiri yang tergambar sejak
   halaman dibuka (diminta pemilik), jadi yang tersisa untuk dikatakan cuma
   bahwa tanda "–" di bawah berarti "belum diukur" — bukan "terukur kosong". */
.bw-belum{display:flex;align-items:center;gap:8px;padding:8px 12px;
  border:1px solid var(--border);border-radius:8px;background:var(--bg-2);
  color:var(--t3);font-size:11px;line-height:1.6}
.bw-hasil{display:flex;flex-direction:column;gap:12px;flex:1}
/* Empat kotak sebaris, lebar sama. Dipakai kartu angka Bandwidth Test DAN
   kartu angka di kepala halaman Sistem. Yang berbeda cuma kapan keduanya
   membelah jadi dua baris, dan itu ditulis di @media masing-masing halaman —
   lebar wadahnya memang berbeda. */
.petak-4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
/* `height:100%`: dua kartu grafik berdampingan harus setinggi barisnya,
   bukan setinggi isinya masing-masing — legenda satu baris lawan dua baris
   sudah cukup untuk membuat tepi bawahnya meleset. */
.bw-kartu{margin-bottom:0;padding:12px;height:100%}
.bw-judul-grafik{font-size:12px}
/* Tingginya DIPATOK, bukan mengikuti isi: kanvas menggambar sesuai
   offsetHeight, dan tinggi yang berubah-ubah membuat grafiknya melompat tiap
   kali jumlah pembacaan berbeda.

   150 → 116px pada 16 September 2026, dan angka itu DIHITUNG bukan dikira.
   Grafiknya jadi EMPAT (throughput, capacity, sinyal, CPU — dua yang terakhir
   diminta pemilik: *"sekalian tampilkan halaman untuk merekam perubahan
   sinyal … lalu sekalian CPUnya juga"*), tersusun dua baris. Tinggi kolom
   kanan = kartu angka 78 + 2x(kartu grafik 24 padding + 18 judul + 8 jarak +
   kanvas + 18 legenda) + detail link ~156 + baris "belum ada tes" 34 + empat
   jarak 48. Dengan kanvas 116px: 78 + 2x184 + 156 + 34 + 48 = 684px, dan di
   layar 1080 yang tersedia ±850px sesudah header dan padding. Permintaannya
   dua sisi sekaligus — *"tidak perlu scroll down, jangan sampai menyisakan
   ruang kosong"* — jadi sisa 166px itu memang batas yang dicari: cukup untuk
   layar 900px, tidak cukup untuk jadi lubang di layar 1080. */
.bw-kanvas{width:100%;height:116px;display:block}
.bw-titik-tx{background:rgb(59,130,246)}
.bw-titik-rx{background:rgb(34,197,94)}
.bw-titik-ul{background:rgb(6,182,212)}
.bw-titik-dl{background:rgb(245,158,11)}
.bw-titik-sig{background:rgb(56,189,248)}
.bw-titik-noise{background:rgb(148,163,184)}
.bw-titik-cpu{background:rgb(168,85,247)}
/* Membentang seluruh kolom petak induknya. Dipakai pilihan sisi penguji di
   Bandwidth Test — ia yang menentukan arti kolom "Alamat IP tujuan" tepat di
   bawahnya, dan pilihan yang menentukan arti kolom lain tidak pantas
   berdesakan sebagai setengah baris — dan catatan non-admin di halaman
   Sistem. Satu nama untuk satu perilaku; dua nama untuk `grid-column:1 / -1`
   sudah pernah lahir dan langsung tertangkap uji kembar. */
.span-penuh{grid-column:1 / -1}

@media(max-width:1100px){
  /* Di bawah ini kolom kanan jadi terlalu sempit untuk dua grafik
     berdampingan — dan grafik selebar 200px tidak menunjukkan bentuk apa pun.
     Halaman kembali bertumpuk, dan di lebar segitu menggulir memang wajar. */
  .bw-halaman{grid-template-columns:1fr}
}
@media(max-width:640px){
  /* `.grid-2` dipakai form medan DAN pasangan grafik di halaman ini;
     keduanya sama-sama harus bertumpuk di layar sempit. */
  .bw-halaman .grid-2{grid-template-columns:1fr}
  .bw-kanan .petak-4{grid-template-columns:1fr 1fr}
}

/* ── Petak keterangan link — dipakai Pointing DAN Bandwidth Test ─────────
   Pindah dari blok <style> pointing.html 21 Agustus 2026. Gaya yang tinggal
   di satu halaman tidak bisa dipakai halaman lain, dan akibatnya bukan
   "kurang rapi" melainkan TIDAK BERGAYA SAMA SEKALI: di bug34 kelasnya
   terpasang di halaman Bandwidth, aturannya tidak ada, dan dua puluh baris
   label-nilai bertumpuk memanjang ke bawah selebar layar.

   BENTUKNYA ikut berubah atas permintaan pengguna: *"buatkan untuk hasilnya
   menyamping saja mungkin lebih bagus"*. Dulu nilai di BAWAH labelnya — dua
   baris per keterangan, sepuluh keterangan jadi dua puluh baris. Sekarang
   nilai di SEBELAH labelnya, satu baris per keterangan, dan petaknya sendiri
   membelah jadi beberapa kolom kalau kartunya lebar.

   `minmax(220px,1fr)`: di bawah itu "Kapasitas Turun" dan "171.6 Mbps" mulai
   berdesakan di satu baris, dan yang terjadi bukan melipat rapi melainkan
   angkanya terpotong. */
.pt-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
  gap:0 20px}
.pt-grid > div{display:flex;align-items:baseline;justify-content:space-between;
  gap:12px;padding:4px 0;border-bottom:1px solid var(--border2)}
/* Baris terakhir tiap KOLOM tidak bergaris — garis yang menggantung di bawah
   kolom terpendek terbaca sebagai baris kosong yang datanya hilang. */
.pt-grid > div:last-child{border-bottom:none}
.pt-grid .k{font-size:9px;color:var(--t3);text-transform:uppercase;
  letter-spacing:.05em;white-space:nowrap}
/* `min-width:0` + `text-align:right`: nilainya yang menyusut kalau sempit,
   bukan labelnya — label yang terpotong berhenti bisa dibaca sama sekali,
   sementara angka yang terpotong masih menunjukkan besarannya. */
.pt-grid .v{font-size:13px;font-weight:600;font-family:var(--mono);
  color:var(--t1);text-align:right;min-width:0;word-break:break-word}

/* ── Mode kios: penanda kecil di header (21 Agustus 2026, TAMPILAN §6) ────
   TEKSNYA DIAM — tidak ada hitung mundur. §5 melarang apa pun yang mengulang
   terus-menerus, dan angka yang berdetak di layar yang dipandangi delapan jam
   adalah bentuk paling murni dari itu. Yang perlu disampaikan "putaran sedang
   menyala dan berikutnya ke mana", bukan "berapa detik lagi". */
.kios-tanda{display:none;align-items:center;gap:4px;font-size:10px;
  font-family:var(--mono);color:var(--t3);background:var(--bg-2);
  border:1px solid var(--border);border-radius:8px;padding:4px 8px;
  white-space:nowrap}

/* ── Bingkai kios (24 Agustus 2026) ───────────────────────────────────────
   Kenapa ada sama sekali: lihat "SATU DOKUMEN YANG TIDAK PERNAH PERGI" di
   app.js. Ringkasnya — layar penuh milik dokumen, jadi dokumen yang
   memegangnya tidak boleh ikut berpindah halaman.

   `position:fixed` + inset:0, BUKAN width/height 100%: pemegang bingkai masih
   punya header dan rel navigasi di DOM-nya, dan bingkai yang ikut arus tata
   letak akan mulai dari bawah header — menyisakan pita kosong setinggi header
   di bawah layar. */
.kios-bingkai{position:fixed;inset:0;top:0;left:0;width:100%;height:100%;
  border:0;z-index:9000;background:var(--bg-0)}
/* Isi halaman pemegang bingkai tidak ikut tergambar. Ia juga tidak berjalan
   (dashboard.html tidak memulai dirinya saat jadi pemegang), jadi yang
   tersisa cuma kerangka kosong — dan kerangka kosong yang berkedip sepersekian
   detik sebelum bingkainya memuat adalah persis jenis kedipan yang §5
   larang. */
body.kios-pemegang{overflow:hidden}
body.kios-pemegang > *:not(.kios-bingkai){display:none}

/* ══════════════════════════════════════════════════════════════════════════
   HALAMAN DETAIL PERANGKAT — kepala, tab, dan kartu per sisi (RF-FIX-59)
   ══════════════════════════════════════════════════════════════════════════
   Sampai 3 September 2026 halaman ini satu kolom lurus: kepala, lalu 22 ruas
   "Informasi Perangkat" dalam satu petak auto-fill, lalu Keterangan, WPA, dan
   Riwayat. Petak itu yang jadi masalahnya — ruas sisi AP dan sisi ST berselang
   -seling di dalamnya menurut urutan penulisan, bukan menurut sisinya, jadi
   membaca "keadaan sisi ST" berarti memungut delapan ruas yang tersebar di
   antara ruas milik AP. Sekarang tiap sisi punya kartunya sendiri.

   Prefiks `dt-`, bukan `det-`: `det-` sudah dipakai mode DAFTAR halaman ini
   (toolbar, tabel, penyaring riwayat), dan dua kelompok yang tidak pernah
   tampil bersamaan lebih mudah dibaca kalau namanya tidak bisa tertukar. */

/* ── Kepala ── */
.dt-kepala{
  background:var(--bg-1);border:1px solid var(--border);border-radius:10px;
  padding:20px;margin-bottom:16px;display:flex;align-items:flex-start;
  justify-content:space-between;flex-wrap:wrap;gap:20px
}
.dt-kepala-kiri{display:flex;align-items:flex-start;gap:16px;min-width:0;flex:1}
/* Tempat gambar perangkat. Belum ada foto per model di instalasi ini, jadi
   yang mengisinya siluet model dari ikonModel() — ikon yang SAMA dengan yang
   dipakai pil model di sebelah kanan. Kotaknya tetap dipasang walau isinya
   siluet: ia yang memberi kepala kartu ini jangkar visual di kiri, dan begitu
   ada foto sungguhan tinggal diganti isinya tanpa menggeser apa pun. */
.dt-gambar{
  width:88px;height:88px;flex-shrink:0;border-radius:8px;background:var(--bg-2);
  border:1px solid var(--border2);display:flex;align-items:center;
  justify-content:center;color:var(--t3)
}
.dt-gambar .ic{width:40px;height:40px}
.dt-kepala-teks{min-width:0;display:flex;flex-direction:column;gap:4px}
.dt-nama{font-size:20px;font-weight:700;color:var(--t1)}
.dt-titik{width:12px;height:12px;border-radius:50%;flex-shrink:0}
.dt-baris-ket{font-size:12px;color:var(--t3);display:flex;align-items:center;gap:4px}
.dt-baris-ket .ic{color:var(--t3);flex-shrink:0}
.dt-kepala-tombol{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.dt-kepala-kanan{display:flex;flex-direction:column;align-items:flex-end;gap:16px}
/* Empat ruas ringkas — Status, Uptime, Model, Jenis Link. Yang ditanya orang
   dalam dua detik pertama, dan sebelum ini ketiganya harus dipungut dari tiga
   tempat berbeda: badge, pil, dan ruas ke-22 di petak bawah. */
.dt-ringkas{display:flex;gap:24px;flex-wrap:wrap}
.dt-ringkas-label{
  font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;
  color:var(--t3);margin-bottom:4px
}
.dt-ringkas-nilai{font-size:13px;font-weight:600;color:var(--t1);font-family:var(--mono)}

/* ── Bilah tab ── */
.dt-tabbar{
  display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:16px;
  overflow-x:auto
}
.dt-tab{
  background:none;border:none;border-bottom:2px solid transparent;
  padding:12px 16px;font-size:12px;font-weight:600;color:var(--t3);
  cursor:pointer;font-family:var(--ui);white-space:nowrap;
  display:inline-flex;align-items:center;gap:8px;transition:color .12s
}
.dt-tab:hover{color:var(--t2)}
.dt-tab.aktif{color:var(--blue);border-bottom-color:var(--blue)}
.dt-tab .ic{width:14px;height:14px}

/* ── Kartu & petak isi tab ── */
.dt-grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
/* auto-fit, bukan tiga kolom pasti: kartu WPA hanya tergambar untuk admin
   (lihat isAdmin() di detail.html), jadi barisnya kadang berisi dua kartu —
   dan tiga kolom pasti akan menyisakan sepertiga baris kosong di sebelah
   kanan untuk semua orang yang bukan admin. */
.dt-grid3{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-bottom:16px}
.dt-kartu{
  background:var(--bg-1);border:1px solid var(--border);border-radius:10px;
  padding:16px;min-width:0
}
/* Spanduk sisi — pengganti judul biasa. Warnanya yang bekerja: biru selalu
   sisi AP, hijau selalu sisi ST, di kartu mana pun di halaman ini. Sesudah
   dua kali membaca, orang berhenti membaca judulnya. */
.dt-spanduk{
  border-radius:10px;padding:12px 16px;margin-bottom:16px;
  display:flex;align-items:center;gap:12px
}
.dt-spanduk-ap{background:rgba(59,130,246,.08);border:1px solid rgba(59,130,246,.28)}
.dt-spanduk-st{background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.28)}
.dt-spanduk-ikon{
  width:32px;height:32px;border-radius:8px;flex-shrink:0;
  display:flex;align-items:center;justify-content:center
}
.dt-spanduk-ap .dt-spanduk-ikon{background:rgba(59,130,246,.16);color:var(--blue)}
.dt-spanduk-st .dt-spanduk-ikon{background:rgba(34,197,94,.16);color:var(--green)}
.dt-spanduk-judul{font-size:13px;font-weight:700;color:var(--t1);margin-bottom:3px}

/* ── Ruas ── */
.dt-ruas{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:16px}
.dt-ruas-label{
  font-size:9px;color:var(--t3);text-transform:uppercase;letter-spacing:.04em;
  margin-bottom:4px
}
.dt-ruas-nilai{font-size:13px;color:var(--t1);font-family:var(--mono);word-break:break-word}

/* ── Bilah kemajuan performa ── */
.dt-perf{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:20px}
.dt-perf-nilai{font-size:15px;font-weight:700;font-family:var(--mono);margin-bottom:4px}
.dt-bar{height:6px;border-radius:4px;background:var(--bg-3);overflow:hidden;margin-bottom:4px}
.dt-bar-isi{height:100%;border-radius:4px;background:var(--green)}
/* Grafik mini. Tingginya TETAP dan lebarnya penuh: kanvas yang tingginya
   ikut isi membuat baris ruas di sebelahnya naik-turun tiap kali data
   berubah. Digambar gambarSparkline() di app.js — kanvas yang sama dengan
   grafik besar, bukan pustaka tambahan. */
.dt-spark{display:block;width:100%;height:28px;margin-top:4px}

/* ── Tab Tools ── */
/* Alatnya jadi EMPAT pada 16 September 2026 (Cek Latency menyusul Speedtest,
   Pointing, dan Ping), dan tiga kolom tetap berarti satu kartu menggantung
   sendirian di baris kedua dengan dua pertiga lebar kosong di sebelahnya.

   `auto-fit` dan bukan angka kolom: di 1600px keempatnya sebaris, di 1280px
   jadi tiga, di layar sempit menumpuk — dan menambah alat kelima kelak tidak
   menuntut aturan ini disunting lagi. 320px batas bawahnya karena di bawah
   itu kotak isian "Host / IP Address" beserta labelnya mulai berdesakan. */
.dt-tools{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
  gap:16px}
.dt-langkah{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.dt-langkah-n{
  width:20px;height:20px;border-radius:50%;flex-shrink:0;background:var(--bg-3);
  border:1px solid var(--border);color:var(--t3);font-size:10px;font-weight:700;
  display:flex;align-items:center;justify-content:center;font-family:var(--mono)
}
.dt-langkah-t{font-size:11px;font-weight:600;color:var(--t2);flex:1}
.dt-hasil{
  background:var(--bg-2);border:1px solid var(--border2);border-radius:8px;
  padding:12px;margin-top:16px
}
.dt-hasil-kepala{
  display:flex;align-items:center;justify-content:space-between;gap:8px;
  margin-bottom:12px
}
.dt-hasil-judul{
  font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;
  color:var(--t3)
}
.dt-angka-besar{font-size:20px;font-weight:700;font-family:var(--mono)}
.dt-angka-satuan{font-size:11px;font-weight:600;color:var(--t3);margin-left:4px}

/* ── Tab Graph ── */
.dt-graf{display:grid;grid-template-columns:180px 1fr 140px;gap:16px;align-items:start}
.dt-graf-kanvas{width:100%;height:200px;display:block}
.dt-graf-sisi{display:flex;flex-direction:column;gap:12px}
.dt-graf-kotak{
  background:var(--bg-2);border:1px solid var(--border2);border-radius:8px;
  padding:12px
}

/* Satu kolom di layar sempit — dan itu SEMUA petak di halaman ini sekaligus.
   Tiga kartu Tools berdampingan di 520px berarti dropdown selebar 140px yang
   nama perangkatnya terpotong di karakter kesembilan. */
@media (max-width: 1100px){
  .dt-tools,.dt-grid3{grid-template-columns:1fr}
  .dt-graf{grid-template-columns:1fr}
  .dt-graf-sisi{flex-direction:row;flex-wrap:wrap}
  .dt-graf-kotak{flex:1;min-width:120px}
}
@media (max-width: 900px){
  .dt-grid2{grid-template-columns:1fr}
  .dt-kepala{flex-direction:column}
  .dt-kepala-kanan{align-items:flex-start;width:100%}
}

/* Sakelar dua-pilihan di tab Tools (RF-FIX-59). Bentuk yang sama dengan
   sakelar Sisi AP/ST di halaman Pointing; kelasnya sendiri karena yang di
   sana ditulis sebagai gaya sebaris di halamannya dan tidak bisa dipakai
   ulang tanpa memindahkannya lebih dulu. */
.seg-alih{display:flex;gap:4px;background:var(--bg-3);border:1px solid var(--border);
  border-radius:8px;padding:4px}
.seg-alih button{
  flex:1;background:none;border:none;border-radius:4px;padding:4px 8px;
  font-size:11px;font-weight:600;color:var(--t3);cursor:pointer;
  font-family:var(--ui);transition:all .12s
}
.seg-alih button.on{background:var(--bg-1);color:var(--blue)}
.seg-alih button:disabled{opacity:.4;cursor:not-allowed}

/* Hasil ping — petak div, bukan tabel. Alasannya ditulis di detail.html:
   empat berkas uji mencacah th/td di seluruh berkas itu untuk menjaga tabel
   daftar perangkatnya, dan tabel kedua membuat keempatnya gagal menyebut
   kolom yang tidak pernah diubah. */
.dt-ping-tabel{max-height:200px;overflow:auto;margin-top:4px}
.dt-ping-baris{
  display:grid;grid-template-columns:32px 1fr 48px 1fr;gap:8px;
  padding:4px 0;font-size:11px;font-family:var(--mono);color:var(--t2);
  border-bottom:1px solid var(--border2)
}
.dt-ping-kepala{
  color:var(--t3);font-size:9px;font-weight:700;text-transform:uppercase;
  letter-spacing:.06em;font-family:var(--ui);position:sticky;top:0;
  background:var(--bg-2)
}

/* Pelengkap `.jarak-atas-l` yang sudah ada — jarak 16px ke arah sebaliknya.
   Halaman Detail memakainya di antara kartu dan judul di dalamnya; tanpa
   pasangan ini satu-satunya jalan menulis jarak yang sama adalah gaya
   sebaris, dan berkas ini punya batas jumlahnya. */
.jarak-bawah-l{margin-bottom:16px}
/* Tautan yang dibentuk seperti tombol. `.det-refresh` dan `.btn-utama`
   dirancang untuk <button>; dipasang pada <a> keduanya kebagian garis bawah
   bawaan browser. */
.tanpa-garis{text-decoration:none}
/* Kanvas grafik hasil speedtest di tab Tools — lebih pendek daripada grafik
   halaman penuh (.chart-kanvas 260px) karena ia duduk di kartu sepertiga
   lebar layar, dan grafik setinggi itu di sana lebih tinggi daripada seluruh
   angka di atasnya. */
.dt-kanvas-kecil{width:100%;height:120px;display:block;margin-top:16px}
/* Baris mendatar ber-jarak lega: busur pointing beserta angkanya di halaman
   Detail, dan donat distribusi zona beserta legendanya di halaman Sistem.
   Bedanya dengan `.baris-sejajar` cuma jaraknya (16 lawan 8), dan itu memang
   beda yang nyata — keduanya memasangkan GAMBAR dengan tulisan, bukan dua
   potong tulisan. */
.baris-gauge{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
/* Kolom angka di sebelah busur BOLEH menyusut lebih sempit daripada isinya.
   Tanpa ini `min-width:auto` bawaan flexbox menahannya di lebar tulisan
   terpanjang ("Sangat Baik"), dan busurnya terdorong keluar kartu. */
.baris-gauge > div{min-width:0}
.dt-gauge{width:160px;height:100px;flex-shrink:0}

/* ── RF-FIX-60 — hasil Discovery: kolom yang menentukan tindakan ──────────
   Tabel ini bisa 254 baris, dan yang dicari orang di dalamnya cuma SATU hal:
   mana yang belum terdaftar. Sampai 3 September 2026 ketiga kolom penentunya
   (Menjawab, Port, Status) tulisan biasa berhuruf sama dengan sisa tabel,
   jadi menemukannya berarti membaca semuanya. */

/* Menjawab lewat apa. Portnya ikut di lencana web supaya tidak perlu menoleh
   ke kolom sebelah. */
.dis-jawab{
  display:inline-block;font-size:9px;font-weight:700;padding:1px 8px;
  border-radius:4px;white-space:nowrap;font-family:var(--mono)
}
.dis-jawab-ping{background:var(--bg-3);color:var(--t3)}
/* Satu deklarasi untuk dua tempat: lencana web dan lencana port 443 memang
   keping biru yang sama, dan menulisnya dua kali berarti dua tempat yang
   pelan-pelan berbeda birunya. */
.dis-jawab-web,.dis-port-https{background:rgba(59,130,246,.14);color:var(--fg-chip-biru)}
.dis-jawab-diam{background:rgba(239,68,68,.14);color:var(--fg-chip-merah)}

/* Port terbuka. Menumpuk ke bawah, bukan berderet dipisah koma: alamat yang
   membuka empat port membuat kolomnya melebar dan menyeret seluruh tabel. */
.dis-port-baris{display:flex;flex-direction:column;gap:4px;align-items:flex-start}
.dis-port{
  font-size:9px;font-weight:700;font-family:var(--mono);padding:1px 8px;
  border-radius:4px;background:var(--bg-3);color:var(--t3);white-space:nowrap
}
.dis-port-http{background:rgba(34,197,94,.14);color:var(--fg-chip-hijau)}

/* Status di database. Tiga keadaan, tiga warna DAN tiga bentuk ikon — pada
   kecepatan memindai, bentuk terbaca lebih dulu daripada warna. */
.dis-status{
  display:inline-flex;align-items:center;gap:4px;max-width:100%;
  font-size:11px;font-weight:600;text-decoration:none;
  /* Satu variabel warna, bukan tiga kelas warna berdiri sendiri — pola yang
     sama dengan variabel warna kartu statistik dan titik status tabel. Kelas
     warna yang lepas akan jadi kembaran .t-amber/.t-hijau, dan kembaran
     berarti mengubah warna tulisan di satu halaman ikut mengubah status
     Discovery di halaman lain. */
  --sc:var(--t3);color:var(--sc)
}
.dis-status .ic{flex-shrink:0}
/* Nama pelanggan bisa sepanjang "PT. Sumber Rejeki Abadi Sentosa" dan kolom
   ini yang paling kanan — tanpa potongan ini ia mendorong lebar tabel
   melewati tepi kartunya. Nilai utuhnya ada di tooltip barisnya. */
.dis-status-nama{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.dis-status-belum{--sc:var(--amber)}
.dis-status-ap{--sc:var(--green)}
.dis-status-st{--sc:#14B8A6}
.dis-status-ap:hover .dis-status-nama,
.dis-status-st:hover .dis-status-nama{text-decoration:underline}

/* Penanda baris. Tepi kiri 3px, bukan latar sekujur baris: latar berwarna di
   254 baris membuat tabelnya jadi dua blok warna dan justru menghapus
   perbedaan antar barisnya sendiri. Latarnya ada tapi setipis mungkin. */
.dis-tabel tr.dis-belum td:first-child{box-shadow:inset 3px 0 0 var(--amber)}
.dis-tabel tr.dis-belum td{background:rgba(245,158,11,.04)}
.dis-tabel tr.dis-ada td:first-child{box-shadow:inset 3px 0 0 var(--green)}
.dis-tabel tr.dis-belum:hover td{background:rgba(245,158,11,.09)}
.dis-tabel tr.dis-ada:hover td{background:rgba(34,197,94,.06)}

/* ── RF-FIX-60 — antrean adopsi ───────────────────────────────────────── */

/* Bilah ringkasan di atas tabel. Sekaligus jadi keterangan warna untuk tabel
   di bawahnya — amber = belum terdaftar, hijau = sudah. */
.una-ringkas{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
.una-keping{
  display:inline-flex;align-items:center;gap:4px;font-size:11px;
  padding:4px 12px;border-radius:20px;border:1px solid var(--border);
  background:var(--bg-2);color:var(--t3)
}
.una-keping b{font-family:var(--mono);font-weight:700}
.una-keping-belum{border-color:rgba(245,158,11,.4);color:var(--amber)}
/* Digabung dengan keadaan berhasil tombol Salin di halaman Maps — lihat
   .sv-salin-sukses. Keduanya keping hijau bertepi tipis yang menyatakan
   hal yang sama (sudah/berhasil), dan menulisnya dua kali berarti dua
   tempat yang pelan-pelan berbeda hijaunya. */
.una-keping-sudah,.sv-salin-sukses{border-color:rgba(34,197,94,.4);color:var(--green)}
.una-keping-asing{border-color:rgba(148,163,184,.4)}

/* Tebakan jenis. Tanda tanya/siluet membuat "belum diketahui" punya BENTUK,
   bukan cuma huruf abu-abu — dan pada daftar yang mayoritas isinya "menjawab
   ping saja", huruf abu-abu berhenti terbaca. */
.una-tebakan{display:inline-flex;align-items:center;gap:4px;font-size:11px}
.una-tebakan .ic{flex-shrink:0}
.una-tak-tahu{color:var(--t3);font-style:italic}

/* ── RF-COPY-SURVEY — menyalin hasil survei ───────────────────────────────
   Tombolnya sengaja SEKUNDER: yang dituju orang saat membuka panel ini
   membaca angkanya, bukan menyalinnya, dan tombol utama di kepala panel akan
   bersaing dengan daftar BTS-nya sendiri. */
.sv-kepala-aksi{display:flex;align-items:center;gap:4px;flex-shrink:0}
.sv-salin{
  display:inline-flex;align-items:center;gap:4px;background:none;
  border:1px solid var(--border);border-radius:4px;padding:2px 8px;
  font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;
  color:var(--t3);cursor:pointer;font-family:var(--ui);white-space:nowrap;
  transition:all .12s
}
.sv-salin:hover{border-color:var(--t3);color:var(--t2)}
/* Keadaan berhasilnya (hijau dua detik, lalu kembali) dideklarasikan bersama
   .una-keping-sudah di halaman Unadopted — warnanya yang melaporkan, bukan
   tulisannya sendiri: "Tersalin!" tanpa perubahan warna terbaca seperti nama
   tombol yang berubah. */
.sv-salin-kotak{
  width:100%;height:280px;background:var(--bg-2);border:1px solid var(--border);
  border-radius:8px;padding:12px;font-family:var(--mono);font-size:11px;
  color:var(--t2);resize:vertical
}

/* ── RF-TREN-GATE — keping perangkat terpilih ─────────────────────────────
   Di bawah kolom pencarian, bukan di dalamnya: jumlahnya bisa sepuluh, dan
   sepuluh keping di dalam kotak selebar 220px mendorong tempat mengetiknya
   sampai tidak terlihat lagi. */
/* Jaraknya HANYA saat ada isinya — `:not(:empty)`, bukan margin di aturan
   dasarnya. Baris ini kosong sepanjang halaman belum dipilih apa-apa, dan
   jarak 12px di bawah sesuatu yang tidak terlihat adalah celah tanpa sebab
   tepat di atas keterangan "pilih dulu perangkatnya". */
.tren-keping-baris{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-start}
.tren-keping-baris:not(:empty){margin-bottom:12px}
.tren-subjudul{font-size:11px;color:var(--t3);margin-bottom:12px;display:flex;
  align-items:center;gap:8px}
/* Titik warnanya SAMA dengan warna garis perangkat itu di grafik — itu yang
   membuat baris keping ini sekaligus jadi legenda, dan legenda yang bisa
   dihapus satu per satu. */
.tren-keping{
  display:inline-flex;align-items:center;gap:8px;font-size:11px;color:var(--t2);
  background:var(--bg-2);border:1px solid var(--border);border-radius:20px;
  padding:4px 4px 4px 12px
}
.tren-keping-x{
  background:none;border:none;color:var(--t3);cursor:pointer;font-size:10px;
  line-height:1;padding:4px;border-radius:50%;font-family:var(--ui)
}
.tren-keping-x:hover{background:rgba(239,68,68,.14);color:var(--red)}
.tren-keping-batas{font-size:10px;color:var(--amber)}

/* ══════════════════════════════════════════════════════════════════════════
   KARTU VERSI — kabar, bukan tombol (17 September 2026)
   ══════════════════════════════════════════════════════════════════════════

   Seluruh gaya RF-UPGRADE DICABUT di sini bersama alurnya: kotak tarik-jatuh
   paket, lapisan kemajuan, daftar langkah, log pemasangan, dan penguncian
   halaman selama upgrade berjalan — 29 aturan, semuanya untuk satu tombol
   yang sekarang dijalankan dari terminal.

   Yang tersisa dua baris angka. */

/* Label & angkanya memakai kelas yang SUDAH ADA — `.t-sub` untuk labelnya,
   `.h-card` untuk angkanya, `.t-biru` untuk versi yang lebih baru daripada
   yang terpasang. Ketiganya sempat ditulis ulang sebagai `.ver-label`,
   `.ver-nomor`, `.ver-baru`, dan penjaga kelas kembar di
   test_gaya_bersama.py menangkap ketiganya sekaligus. */
.ver-baris{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
/* Perintah terminal di dalam panel keterangan. `inline-block` supaya latar
   dan jarak dalamnya ikut membungkus barisnya, bukan cuma tulisannya. */
.ver-perintah{
  display:inline-block;margin-top:4px;padding:4px 8px;border-radius:4px;
  background:var(--bg-3);border:1px solid var(--border);
  font-size:10px;color:var(--t1);word-break:break-all
}

/* Gambar perangkat di kepala halaman Detail (3 September 2026). Siluet
   berisi, bukan ikon garis — lihat gambarPerangkat() di app.js, termasuk
   kenapa ia digambar sendiri dan bukan foto produk yang diunduh. */
.gbr-perangkat{display:block;color:var(--t2)}
.dt-gambar{padding:8px}
/* Tautan ke antarmuka web radio, di ruas IP Address. Ikon panah-keluar di
   ujungnya yang menyatakan ia membuka tab baru — tanpa itu ia terbaca sebagai
   tautan ke halaman lain di aplikasi ini. */
/* Bentuk dasarnya `.t-tautan` + `.t-mono` yang sudah ada — keduanya dipasang
   di elemennya, bukan disalin ke sini. Yang tersisa cuma dua hal yang tidak
   dimiliki keduanya: garis bawah saat disentuh (ia tautan yang MEMBUKA TAB
   BARU, dan itu perlu terasa) dan ikon panah-keluar yang tidak boleh
   menyusut. */
.dt-tautan-ip:hover{text-decoration:underline}
.dt-tautan-ip .ic{opacity:.7;flex-shrink:0}

/* ── Celah kosong di dasar kartu Informasi AP (laporan-bug/bug63.png) ─────
   Kedua kartu duduk di petak dua kolom dan sama tingginya (bawaan
   `align-items:stretch`), sementara isinya tidak: kartu ST membawa grafik
   mini CPU yang menambah satu baris. Selisihnya muncul sebagai blok kosong
   di dasar kartu AP — bukan ruang yang disengaja, dan terbaca seperti ruas
   yang gagal dimuat.

   Jalan keluarnya BUKAN membiarkan kartunya beda tinggi: dua kartu
   berdampingan yang dasarnya tidak rata justru lebih berantakan. Yang
   diratakan ISINYA — petak ruas menyerap sisa tinggi kartunya, dan barisnya
   dibagi rata di dalamnya. Ruang yang sama, tapi jadi jarak antar baris
   alih-alih blok kosong di ujung. */
.dt-kartu{display:flex;flex-direction:column}
.dt-kartu > .dt-ruas{flex:1;align-content:space-between;row-gap:16px}
/* 132px, turun dari 140: pada kartu selebar separuh layar 1600px, 140px
   menghasilkan empat kolom dengan sisa 60px yang tidak dipakai siapa pun.
   132px memuat lima, dan tiga belas ruas jadi tiga baris rapat alih-alih
   empat baris renggang. */
.dt-ruas{grid-template-columns:repeat(auto-fill,minmax(132px,1fr))}

/* Kartu Log di Layar Tembok: entri bertinggi ISINYA, bukan meregang.
   (laporan-bug/bug64.png)

   Aturan dasarnya `flex:1 0 var(--lentry-h)` — entri MEREGANG mengisi sisa
   ruang kartu, dan itu benar di dua susunan lain: di sana tinggi kartunya
   diturunkan dari jumlah entri, jadi sisa ruangnya selalu kurang dari satu
   entri dan meregangkannya menutup celah pembagian.

   Di sini arahnya terbalik. Kartunya membentang dua baris grid dan tingginya
   datang dari layar, jadi "sisa ruang" bisa setinggi empat entri — dan
   meregangkan dua entri untuk mengisinya menghasilkan dua kotak raksasa
   berisi tiga baris teks, bukan kartu yang penuh. Yang benar: entri sebesar
   isinya, sebanyak yang muat, sisanya digulung. */
body[data-layout="tembok"] .lentry{flex:0 0 auto;max-height:none}

/* ── Popup rentang tanggal & jam di tab Graph (laporan-bug/bug65.png) ─────
   Menempel di tombolnya, bukan baris selebar kartu. Baris selebar kartu
   mendorong seluruh grafik ke bawah untuk tiga kotak isian yang dipakai
   sekali lalu tidak dilihat lagi — dan pada lebar itu isinya berserak
   dengan panah yang jatuh sendirian di baris kedua. */
/* `position:relative` yang penting di sini — ia jangkar popup di bawahnya.
   Tanpa itu popupnya menjangkarkan diri ke kartu terdekat yang berposisi,
   dan mendarat di sudut yang salah. */
.dt-rentang{position:relative;display:inline-flex;gap:4px;align-items:center;
  flex-wrap:wrap;row-gap:4px}
.dt-popup{
  position:absolute;top:100%;right:0;margin-top:8px;z-index:40;
  width:280px;background:var(--bg-1);border:1px solid var(--border);
  border-radius:10px;padding:16px;
  /* Bayangan yang tegas, bukan samar: popup ini melayang di atas kartu yang
     warnanya nyaris sama, dan tanpa bayangan tepinya hilang. */
  box-shadow:0 12px 32px rgba(0,0,0,.45)
}
.dt-popup-judul{
  font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;
  color:var(--t3);margin-bottom:12px
}
/* Label DI ATAS isiannya, bukan di sebelahnya. Berdampingan, label "Sampai"
   mendorong kotak tanggalnya sampai terpotong — kotak datetime-local punya
   lebar minimum sendiri yang tidak bisa ditawar. */
.dt-popup-baris{display:block;margin-bottom:12px}
.dt-popup-label{
  display:block;font-size:10px;color:var(--t3);margin-bottom:4px
}
.dt-popup-isian{width:100%;box-sizing:border-box}
/* Kosong = tidak ada apa-apa, termasuk jaraknya. Kotak galat yang selalu
   memakan 8px membuat kaki popup melompat begitu ada pesan. */
.dt-popup-err{margin-bottom:8px;line-height:1.4}
.dt-popup-err:empty{display:none}
.dt-popup-kaki{display:flex;gap:8px;justify-content:flex-end}
@media (max-width: 640px){
  /* Di layar sempit popup 280px yang dijangkarkan ke kanan bisa keluar dari
     tepi layar. Di sana ia melebar penuh dan jatuh di bawah tombolnya. */
  .dt-popup{position:static;width:auto;margin-top:12px;box-shadow:none}
}

/* ── Beban CPU sisi ST: jalur penuh di kaki kartu (laporan-bug/bug68.png) ──
   Sebelumnya ia satu sel di petak ruas selebar ~132px, dan enam jam pembacaan
   CPU di lebar itu jadi rumput rapat yang tidak bisa dibaca sebagai bentuk
   apa pun — padahal BENTUK persis satu-satunya alasan grafik ini ada. Sel
   setinggi itu juga yang membuat kartu ST lebih tinggi daripada kartu AP di
   sebelahnya, dan selisihnya muncul sebagai ruang kosong di dasar kartu AP. */
.dt-cpu-jalur{
  display:flex;align-items:center;gap:16px;margin-top:16px;padding-top:16px;
  border-top:1px solid var(--border2)
}
.dt-cpu-angka{flex:0 0 auto;min-width:0}
/* 44px, bukan 28px seperti grafik mini di dalam sel: pada lebar sekartu,
   tinggi 28px membuat riak wajar terlihat rata — dan grafik yang selalu rata
   berhenti dibaca. */
.dt-spark-lebar{flex:1;min-width:0;height:44px;display:block}
@media (max-width: 900px){
  /* Angka di ATAS grafiknya begitu kartunya menyempit — berdampingan, sisa
     lebar untuk grafiknya tinggal sepertiga dan masalah aslinya kembali. */
  .dt-cpu-jalur{flex-direction:column;align-items:stretch;gap:8px}
}

/* Kedua kartu di petak dua kolom SAMA TINGGI, dan itu yang menghabiskan
   ruang kosong di dasar kartu AP. Pembungkusnya yang diregangkan grid, bukan
   kartunya — jadi pembungkusnya dijadikan kolom flex dan kartunya menyerap
   sisa tingginya. */
.dt-grid2 > div{display:flex;flex-direction:column}
.dt-grid2 > div > .dt-kartu{flex:1}

/* Kotak tempel kunci lisensi. `resize:vertical` saja — kotak yang bisa
   dilebarkan melampaui kartunya membuat kartu itu sendiri bisa digeser ke
   samping, dan itu persis kelas cacat yang baru dibereskan di Discovery. */
.lis-kunci{resize:vertical;min-height:76px;line-height:1.45;
  word-break:break-all}
.lis-pilih{background:none;border:1px solid var(--border);border-radius:6px;
  padding:3px 8px;font-family:var(--ui);font-size:11px;color:var(--t2);
  cursor:pointer}
.lis-pilih:hover{color:var(--t1);border-color:var(--t3)}

/* ── Halaman Bantuan — tahapan pemakaian (16 September 2026) ───────────────
   Diminta pemilik: panduan tahapan untuk pengguna baru & tim teknis, isinya
   bisa disunting admin. Isinya datang dari /api/bantuan; yang di sini cuma
   bentuknya.

   Nomornya di KOLOM SENDIRI, bukan di depan judul sebagai teks. Judul dua
   baris dengan angka yang ikut mengalir membuat baris kedua menjorok masuk
   di bawah angkanya — dan pada daftar sembilan tahap, sembilan kali menjorok
   yang tidak seragam terbaca sebagai daftar yang tidak rata. */
.bt-daftar{display:flex;flex-direction:column;gap:16px}
.bt-tahap{display:grid;grid-template-columns:44px minmax(0,1fr);
  align-items:start;padding:16px 20px 16px 0;gap:0}
.bt-nomor{display:flex;align-items:center;justify-content:center;
  width:28px;height:28px;margin:2px 0 0 12px;border-radius:50%;
  background:var(--bg-3);color:var(--t2);
  font-family:var(--mono);font-size:12px;font-weight:700}
.bt-ringkas{margin-top:4px;line-height:1.65}
/* `padding-left` dan bukan `list-style-position:inside`: yang kedua membuat
   baris kedua tiap butir rata dengan bulatannya, bukan dengan hurufnya. */
.bt-butir{margin:8px 0 0;padding-left:20px;display:flex;
  flex-direction:column;gap:4px}
.bt-butir li{font-size:12px;color:var(--t2);line-height:1.65}
.bt-tautan-baris{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
.bt-tautan{display:inline-flex;align-items:center;gap:4px;
  border:1px solid var(--border);border-radius:999px;padding:4px 8px;
  font-size:11px;color:var(--t2);text-decoration:none}
.bt-tautan:hover{color:var(--t1);border-color:var(--t3)}

/* Mode sunting. Bentuknya sengaja BEDA dari mode baca — garis putus-putus di
   tepi kiri — supaya tidak ada keraguan sedang di keadaan mana. Halaman yang
   dua keadaannya terlihat sama adalah halaman yang suntingannya ditutup tanpa
   disimpan. */
.bt-tahap-sunting{border-left:2px dashed var(--blue)}
.bt-area{resize:vertical;line-height:1.55;font-family:var(--ui)}
.bt-alat-tahap{display:flex;justify-content:flex-end;gap:4px;margin-bottom:8px}
.bt-mini{background:none;border:1px solid var(--border);border-radius:6px;
  padding:4px 8px;font-family:var(--ui);font-size:11px;color:var(--t2);
  cursor:pointer;display:inline-flex;align-items:center;gap:4px}
.bt-mini:hover{color:var(--t1);border-color:var(--t3)}
.bt-tautan-sunting{display:flex;flex-direction:column;gap:8px}
/* Nama — halaman — hapus. Kolom tengahnya lebar tetap: nama halaman terpanjang
   ("/device-add") memang sependek itu, dan kolom yang ikut isi membuat tiap
   baris tautan berbeda lebarnya. */
.bt-tautan-baris-sunting{display:grid;
  grid-template-columns:minmax(0,1fr) 150px 36px;gap:4px;align-items:center}
@media(max-width:640px){
  .bt-tahap{grid-template-columns:34px minmax(0,1fr);padding-right:12px}
  .bt-nomor{margin-left:4px;width:24px;height:24px;font-size:11px}
  .bt-tautan-baris-sunting{grid-template-columns:minmax(0,1fr)}
}

/* ── Halaman Alur Aplikasi (16 September 2026) ─────────────────────────────
   Diagram alurnya dibuat dengan skill `archify` (spesifikasinya di
   docs/alur-aplikasi.workflow.json) lalu disisipkan ke halaman sebagai SVG
   SEBARIS — bukan <img>, bukan <iframe>.

   Sebaris, karena seluruh warna SVG buatan archify digerakkan variabel CSS:
   `--backend-fill`, `--text`, `--arrow`, dan tujuh belas lainnya. Dengan
   disisipkan, variabel itu bisa diikatkan ke token tema aplikasi ini — dan
   itu yang dikerjakan blok di bawah. Hasilnya diagram yang ikut keenam tema
   dan keempat warna aksen, tanpa satu berkas gambar tambahan pun. Di dalam
   <img> ia tidak mewarisi apa pun dan akan tetap gelap di tema terang.

   NAMA VARIABELNYA milik archify, bukan milik aplikasi ini, dan sengaja
   TIDAK diganti: mengganti nama berarti harus menyunting SVG hasil generator
   tiap kali diagramnya dibuat ulang — dan suntingan tangan pada berkas hasil
   adalah suntingan yang cepat atau lambat terlupa. Yang dilakukan di sini
   cuma mengisinya. Dibatasi ke `.al-bidang` supaya nama-nama umum seperti
   `--text` dan `--grid` tidak bocor ke seluruh halaman.

   Font sengaja tidak disebut: SVG-nya tidak membawa `font-family` sama
   sekali, jadi ia mewarisi huruf halaman — termasuk saat pemilik menggantinya
   di Preferensi → Tampilan. */
.al-bidang{
  --text:var(--t1);
  --text-muted:var(--t2);
  --text-dim:var(--t3);
  --grid:var(--border);
  --mask:var(--bg-1);
  --arrow:var(--t3);
  --arrow-emphasis:var(--blue);
  --lane-fill:var(--bg-2);
  --lane-stroke:var(--border);
  /* Lima jenis kotak, lima warna yang SUDAH dipakai aplikasi ini untuk arti
     yang sama: biru untuk bagian layanannya sendiri, cyan untuk penyimpanan,
     abu untuk yang di luar jangkauan, amber untuk kabar keluar, hijau untuk
     yang dibuka pemakai. Warna baru khusus diagram berarti pembaca harus
     menghafal kamus kedua. */
  --external-fill:var(--bg-3);
  --external-stroke:var(--t3);
  --backend-fill:var(--blue-permukaan);
  --backend-stroke:var(--blue);
  --database-fill:var(--bg-2);
  --database-stroke:var(--cyan);
  --messagebus-fill:var(--bg-2);
  --messagebus-stroke:var(--amber);
  --frontend-fill:var(--bg-2);
  --frontend-stroke:var(--green);
  --security-stroke:var(--red)}
/* `min-width` menahan diagram menyusut sampai tak terbaca di layar sempit;
   yang menggulir BIDANGNYA, bukan halamannya. Angkanya diturunkan dari
   viewBox 1185x660: di bawah ~760px tulisan 8px di dalam kotak mulai hilang. */
/* ── Halaman Sistem: memadatkan supaya muat satu layar (17 Sep 2026) ─────
   Dilaporkan pemilik lewat bug76.png & bug77.png: *"pada halaman sistem masih
   kurang padat tampilannya, buatkan agar bisa menjadi satu halaman saja
   seperti gambar referensi gambar75"*.

   Diukur sebelum dipadatkan: dokumen 1769px pada layar 900px. Tingginya
   TIDAK datang dari jumlah kartunya — empat baris saja — melainkan dari
   prosa keterangan di dalamnya. Kartu Zona sendiri 635px, Lisensi 436px,
   Upgrade 418px, Profil Medan 334px.

   Karena itu yang dikecilkan JARAK dan UKURAN HURUF KETERANGAN, bukan
   keterangannya. Tidak satu kalimat pun dibuang: yang menjelaskan kenapa
   sebuah setelan berbahaya harus tetap terbaca oleh orang yang belum tahu
   ia berbahaya. Yang mengecil cuma ruang di sekitarnya.

   Seluruhnya DIBATASI ke `.set-petak`. Kepadatan ini benar untuk halaman
   yang isinya formulir setelan yang dibaca sekali lalu ditutup; ia tidak
   otomatis benar untuk Dashboard yang dipandangi berjam-jam. */
.set-petak{gap:12px}
.set-petak > .kartu > :where(.kartu-kepala){padding:12px 16px}
.set-petak :where(.set-isi), .set-petak :where(.set-body), .set-petak > .kartu > :where(.kartu-isi){padding:12px 16px}
.set-petak :where(.set-isi){gap:8px}
.set-petak :where(.set-foot){padding:8px 16px}
.set-petak :where(.petak-4){gap:12px}
/* Keterangan panjang: 11px → 10px dengan baris yang lebih rapat. Di bawah
   10px ia berhenti terbaca sama sekali, dan keterangan yang tidak terbaca
   sama saja dengan keterangan yang dibuang. */
.set-petak :where(.catatan-kecil), .set-petak :where(.set-hint){font-size:10px;line-height:1.45}
.set-petak :where(.t-sub){line-height:1.4}
/* Tabel zona: sel yang lebih rapat. Lima baris x 8px = 40px, dan itu
   seperlima dari yang perlu dihemat. */
.set-petak :where(.mtable){padding:8px 16px 4px}
.set-petak :where(.zona-sel){padding-bottom:4px}
.set-petak :where(.zona-tabel) input, .set-petak :where(.zona-tabel) select{padding:4px 8px}
/* Keterangan status & ambang berdampingan — lihat catatan di settings.html. */
.set-petak :where(.zona-kaki){align-items:start;gap:16px}
.set-petak :where(.zona-kaki) > div{margin-top:0}
.set-petak :where(.zona-ambang){margin-top:0;padding-top:0;border-top:none}
/* Kotak tarik-jatuh paket upgrade & kotak tempel kunci lisensi: keduanya
   bidang kosong yang tingginya tidak menambah keterbacaan apa pun. */
.set-petak :where(.lis-kunci){min-height:52px}
.set-petak :where(.set-hero-ikon){width:32px;height:32px}
/* Putaran kedua. Sesudah putaran pertama dokumen turun 1769 → 1524px; yang
   tersisa dikuasai tiga kartu berprosa panjang — Zona 486, Lisensi 404,
   Upgrade 358. Yang dikecilkan lagi jaraknya, bukan kalimatnya. */
.set-petak > .kartu > :where(.kartu-kepala){padding:8px 16px}
.set-petak :where(.kartu-catatan){font-size:10px;line-height:1.45;padding:4px 0}
.set-petak :where(.f-label){margin-bottom:0}
.set-petak :where(.jarak-atas){margin-top:8px}
.set-petak :where(.jarak-atas-l){margin-top:8px}
.set-petak :where(.jarak-bawah-l){margin-bottom:8px}
.set-petak :where(.th-s){font-size:8px}
.set-petak :where(.th-tepi), .set-petak :where(.th-dalam){padding:4px 8px}
.set-petak :where(.f-gap18){gap:8px}
/* Bidang di bawah tabel zona: tombol "Tambah zona" beserta keterangan di
   bawahnya. 22px kiri-kanan diwarisi dari saat kartu ini selebar halaman. */
.set-petak .zona-bawah{padding:0 16px 8px}
.set-petak :where(.zona-ambang-isi){margin-top:4px}


/* ── Satu halaman (17 Sep 2026, putaran ketiga) ──────────────────────────
   Diminta pemilik: *"buatkan jadi satu halaman dulu"*.

   Dua putaran sebelumnya memadatkan JARAK dan berhenti di 1466px — dan di
   situ arah itu memang mentok: empat baris kartu berjumlah 1256px, dan
   tinggi tiap baris ditentukan kartu TERTINGGINYA, bukan rata-ratanya.
   Memadatkan lagi cuma menggeser angka puluhan.

   Yang akhirnya menurunkannya DUA perubahan bentuk, bukan kerapatan:

     1. Baris tengah jadi EMPAT kartu (Interval, Profil Medan, Upgrade,
        Lisensi), bukan dua baris berisi dua. Empat kartu setinggi ±300px
        yang tadinya jadi dua baris sekarang jadi satu — hemat satu baris
        penuh. Itu menuntut petaknya lebih lebar dari 1440px, karena empat
        kolom dari 1440 tinggal 340px dan kotak tarik-jatuh paket upgrade
        tidak muat di sana.
     2. Prosa keterangan panjang dilipat di balik `<details>`. BUKAN
        dihapus: yang menjelaskan kenapa sebuah setelan berbahaya harus
        tetap bisa dibaca orang yang belum tahu ia berbahaya — ia sekarang
        satu klik, bukan nol klik. `<details>` bawaan peramban, jadi tidak
        ada satu baris JavaScript pun yang bisa gagal dan menguncinya.

   Tabelnya sendiri dibatasi tingginya dengan gulir DI DALAM kartunya:
   daftar zona boleh tumbuh sampai puluhan baris, dan kartu yang ikut
   tumbuh akan mengembalikan halaman ini ke keadaan semula tanpa ada yang
   menyadarinya.

   BATASNYA DILONGGARKAN 168px → 480px 17 September 2026, sesudah syarat
   "satu halaman" dicabut pemiliknya. 168px memotong baris kelima TEPAT DI
   TENGAH tulisannya — separuh kotak isian yang menggantung di balik tombol
   "Tambah zona", dan itu terbaca sebagai halaman rusak, bukan sebagai
   daftar yang bisa digulir. 480px = 14 baris; lima baris bawaan regulasi
   Kominfo muat seluruhnya tanpa gulir sama sekali, dan daftar yang benar-
   benar panjang tetap tidak menyeret kartunya sampai berlembar-lembar. */
.set-petak .zona-gulir{max-height:480px;overflow-y:auto}
/* Kartu selebar seperempat halaman: petak label–isian–keterangan tidak muat
   tiga kolom lagi. Keterangannya turun ke bawah membentang dua kolom —
   dipaksakan tetap tiga, kolom keterangannya tinggal ±60px dan satu kalimat
   jadi delapan baris. Terukur: kartu Interval Polling 232px → 423px hanya
   karena pelipatan itu. */
.set-petak :where(.set-grid){grid-template-columns:1fr 88px}
.set-petak :where(.set-grid) :where(.set-hint){grid-column:1 / -1;margin-top:-4px}
.set-petak :where(.lis-kunci){min-height:44px}
.set-petak :where(.pill){font-size:9px}
/* ── Keterangan panjang: tombol bundar kecil + panel mengambang ───────────
   `.ket-lipat` (17 Sep pagi) MELAR KE BAWAH saat dibuka. Di kartu setelan
   yang tingginya sudah disamakan, melar berarti mendorong isi kartu — dan
   selama ia tertutup, barisnya sendiri tetap memakan tinggi. Diminta
   pemilik: keterangannya muncul sebagai "popup kecil saja, jangan sampai
   menutup halamannya".

   Tetap `<details>`: tanpa JavaScript, ia tetap membuka isinya di tempat
   (melar ke bawah seperti kemarin). `.mengambang` dipasang app.js — lihat
   catatan panjang di sana untuk kenapa letaknya dihitung, bukan ditulis. */
.ket-pop{display:inline-block;flex-shrink:0;vertical-align:middle}
.ket-pop > summary{list-style:none;cursor:pointer;user-select:none;
  width:16px;height:16px;border-radius:50%;border:1px solid var(--border);
  display:inline-flex;align-items:center;justify-content:center;
  font-size:10px;font-weight:700;line-height:1;color:var(--t3)}
.ket-pop > summary::-webkit-details-marker{display:none}
/* Tanda tanya digambar dari CSS, bukan ditulis di tiap halaman: satu tempat
   kalau lambangnya berubah, dan tidak ada teks yang ikut tersalin saat
   isinya disalin. Nama yang dibaca pembaca layar dari `aria-label`. */
.ket-pop > summary::before{content:'?'}
.ket-pop > summary:hover{color:var(--t1);border-color:var(--t3)}
.ket-pop[open] > summary{color:var(--fg-aksen);border-color:var(--blue);
  background:var(--blue-permukaan)}
/* Cadangan tanpa JS: prosa biasa di bawah tombolnya.

   `.ket-panel`, BUKAN `.ket-isi` — nama kedua itu sudah dipakai catatan
   bebas perangkat di halaman Detail, dan ia membawa `white-space:pre-wrap`
   supaya daftar berbaris-baris yang diketik orang tetap berbaris. Dipakai di
   sini, tiap pergantian baris di SUMBER HTML jadi pergantian baris di layar:
   panelnya tergambar sebagai tangga, dan tidak ada satu pun galat. Terukur
   sekali di peramban sebelum namanya diganti. */
.ket-pop > .ket-panel{font-size:10px;line-height:1.5;color:var(--t3);margin-top:8px}
/* Dengan JS: panel kecil yang menempel pada tombolnya. `--bg-2` dan bukan
   `--bg-1`: panel sewarna kartunya tidak terbaca sebagai lapisan di atasnya,
   melainkan sebagai bagian kartu yang mendadak melebar. */
.ket-pop.mengambang > .ket-panel{position:fixed;z-index:2100;margin-top:0;
  width:max-content;max-width:min(320px,calc(100vw - 16px));overflow:auto;
  background:var(--bg-2);border:1px solid var(--border);border-radius:10px;
  box-shadow:0 8px 28px rgba(0,0,0,.35);padding:8px 12px;color:var(--t2)}
/* Spanduk satu baris yang diakhiri tombol keterangan: kalimatnya yang
   memanjang, jadi tombolnya berdiri di tepi kanan — sejajar dengan tombol
   keterangan di kepala kartu tepat di atasnya. */
.kartu-catatan.baris-isi > span{flex:1;min-width:0}

/* Varian berlabel. Tanda tanya sendirian cukup di kepala kartu — judul
   kartunya persis di sebelahnya, jadi tidak ada pertanyaan "keterangan
   tentang apa". Di kaki tabel zona tidak ada judul yang menempel, jadi
   labelnya ikut terbaca dan seluruh kepingnya bisa ditekan. */
.ket-pop.ket-teks > summary{width:auto;height:auto;border-radius:9px;
  padding:2px 8px;gap:4px;font-size:10px;font-weight:600;color:var(--t2)}
.ket-pop.ket-teks > summary::before{font-size:9px;opacity:.75}

@media(max-width:1400px) and (min-width:1101px){
  /* Empat kolom tinggal ±330px di sini — di bawah itu label dua kata di
     kartu Interval Polling mulai melipat. Kembali ke dua per baris.

     BATAS BAWAH 1101px ditambahkan 17 September 2026. Tanpa itu aturan ini
     ikut berlaku sampai layar tersempit, dan di sana ia MENANG atas aturan
     `@media(max-width:820px)` di atasnya — bukan karena urutan melainkan
     karena kekhususan: `.set-petak .set-span-3` (0,2,0) mengalahkan
     `.set-span-3` (0,1,0), dan media query tidak menambah kekhususan apa
     pun. Akibatnya di layar 480px: lima kartu ber-`span 6` melebar 464px
     sementara kartu Zona & Distribusi ber-`1 / -1` tinggal 404px — dua tepi
     kanan yang berbeda 60px di halaman yang justru sedang dirapikan.
     Terukur di peramban sebelum & sesudah. */
  .set-petak .set-span-3{grid-column:span 6}
  /* Kartu identitas & deretan empat kartu angka MEMBENTANG PENUH di sini.
     Dengan dua kartu per baris, keduanya tidak lagi bisa berbagi satu baris:
     identitas (6 dari 12) berdiri sendiri dan menyisakan 621px kosong di
     kanannya, lalu deretan angka (9 dari 12) menyisakan 316px lagi. Terukur
     di layar 1366px — lebar laptop yang paling sering dipakai membuka
     halaman ini. Membentang penuh, keduanya jadi dua pita rapat tanpa
     lubang, dan tepi kanannya segaris dengan kartu di bawahnya. */
  .set-petak .set-span-3.set-hero, .set-petak .set-span-9{grid-column:1 / -1}
}

/* ── Tepi bawah yang segaris (17 September 2026, putaran keempat) ─────────
   Diminta pemilik: *"pada halaman sistem tidak apa2 jika tidak bisa satu
   halaman, tetapi tabelnya dibuat rapih, tidak jomplang, dan tidak
   menyisakan ruang kosong, saat ini masih tersedia ruang kosong"*.

   Terukur di peramban sebelum putaran ini, layar 1600px — tinggi tiap kartu
   beserta sisa terhadap kartu tertinggi sebarisnya:

     baris 1  identitas 127 · empat kartu angka  92  (sisa  35)
     baris 2  Interval  277 · Profil 325 · Upgrade 351 · Lisensi 400
              (sisa    123 ·        75 ·          49 ·           0)
     baris 3  Zona      419 · Distribusi 316            (sisa  103)

   Itu ruang kosong yang dimaksud, dan sebabnya bukan kerapatan isi melainkan
   `align-items:start` di petaknya: tiap kartu berhenti setinggi ISINYA
   sendiri, jadi tepi bawah sebaris tidak pernah bertemu dan yang terpendek
   menggantung dengan lubang 123px di bawahnya. Tiga putaran sebelumnya
   memadatkan isi — dan isi yang lebih padat justru MEMPERBESAR lubang itu,
   karena yang menyusut kartu pendeknya, bukan kartu tertingginya.

   Tiga perubahan, dan ketiganya soal tinggi, bukan soal isi:

     1. Kartu sebaris sama tinggi (`stretch`). Tepi bawahnya segaris.
     2. Kaki bertombol menempel ke DASAR kartu. Tanpa ini, kartu yang
        diregangkan cuma memindahkan lubangnya ke bawah tombol Simpan —
        dan ketiga tombol Simpan sebaris berdiri di tiga ketinggian.
     3. Sisa tinggi dibagi ke JARAK ANTAR isi (`space-between`), bukan
        ditumpuk jadi satu bidang kosong di bawah. Kartu Interval Polling
        yang menerima paling banyak: 123px itu jatuh sebagai dua jarak
        antar isian, dan kartunya terbaca lapang — bukan setengah terisi.

   Yang membuat ketiganya mungkin syarat "satu halaman" yang dicabut: kartu
   yang diregangkan tidak boleh dipaksa pendek. */
.set-petak{align-items:stretch}
.set-petak > .kartu{display:flex;flex-direction:column;min-width:0}
/* Bidang isi yang memanjang. Ketiga nama ini bidang isi yang sama di kartu
   yang berbeda — halaman ini memakai `.set-body` (Interval Polling),
   `.set-isi` (Profil Medan, Upgrade, Lisensi), dan `.kartu-isi` (identitas,
   Distribusi Zona). `> ` langsung, jadi kartu di dalam kartu tidak ikut. */
.set-petak > .kartu > :where(.set-isi, .set-body, .kartu-isi, .zona-bawah){
  flex:1 1 auto;display:flex;flex-direction:column}
/* Sisa tinggi DIBAGI hanya di dua kartu yang isinya memang dua kutub:
   Distribusi Zona (donat di atas, ringkasan di bawah) dan kaki tabel zona
   (tombol "Tambah zona" di atas, keterangan di bawah).

   Empat kartu setelan TIDAK ikut, dan itu perubahan 17 September sore:
   `space-between` di sana melebarkan jarak antar isian jadi bidang kosong
   yang dikeluhkan pemilik — *"tidak enak dilihatnya kalau ada gap kosong di
   dalam tabel"*. Yang menutup sisanya sekarang isi keempat kartu yang saling
   mendekat sendiri: prosa panjangnya pindah ke panel mengambang, jadi
   tingginya berselisih puluhan piksel, bukan ratusan. */
.set-petak > .kartu > :where(.kartu-isi, .zona-bawah){justify-content:space-between}
/* Sisa di keempat kartu setelan jatuh SEIMBANG di atas dan di bawah isinya,
   bukan menumpuk jadi satu bidang kosong di bawah baris terakhir. Bedanya
   bukan selera: sisa yang menumpuk di bawah terbaca sebagai kartu yang
   isinya belum selesai digambar, sementara sisa yang seimbang terbaca
   sebagai jarak tepi — dan jaraknya sekarang puluhan piksel, bukan ratusan,
   karena prosa panjangnya sudah pindah ke panel mengambang. */
.set-petak > .kartu > :where(.set-isi, .set-body){justify-content:center}
.set-petak > .kartu > .set-body{gap:12px}
/* Kaki bertombol di dasar kartu. `margin-top:auto` dan bukan hanya
   mengandalkan bidang isi di atasnya: kartu Zona Frekuensi menaruh tabel
   dan tombol "Tambah zona" di antara keduanya. */
.set-petak > .kartu > .set-foot{margin-top:auto}
/* Empat kartu angka ikut diregangkan setinggi kartu identitas di sebelahnya;
   isinya dipusatkan tegak supaya yang bertambah ruang di atas DAN di bawah
   angkanya, bukan bidang kosong di bawah "baris aturan aktif". */
.set-petak .petak-4 > .scard{display:flex;flex-direction:column;justify-content:center}

/* Wadah pesan galat yang SEDANG KOSONG tidak ikut dibagi sisa tinggi.
   Keempat kartu setelan diakhiri wadah galat setinggi 14px yang menunggu
   diisi JS. Sebagai anak terakhir, `space-between` menempelkannya ke dasar
   bidang isi — dan keterangan di atasnya berhenti 75px lebih tinggi. Yang
   terlihat: kartu yang isinya berakhir di tengah, dengan bidang kosong di
   bawahnya. Persis ruang kosong yang dikeluhkan, cuma penyebabnya elemen
   yang tidak terlihat.

   `:empty` dan bukan penghapusan `min-height`-nya: tinggi 14px itu yang
   menahan halaman tidak melompat saat galat muncul, dan itu masih benar di
   halaman lain yang memakai kelas yang sama. Di sini sisanya sudah dibagi
   rata, jadi munculnya galat menggeser isi beberapa piksel saja. */
.set-petak > .kartu :where(.set-err, .f-err):empty,
.set-petak > .kartu > .zona-bawah > div[id$="Err"]:empty{display:none}

/* Donat distribusi zona memanjang & dipusatkan tegak: kartunya setinggi
   kartu tabel di sebelahnya, dan donat yang menempel di tepi atas dengan
   bidang kosong di bawahnya terbaca sebagai kartu yang belum selesai
   digambar.

   Petak isian kartu setelan TIDAK lagi ikut (17 Sep sore) — lihat alasan
   yang sama di aturan `space-between` di atas. */
.set-petak > .kartu > .kartu-isi > .baris-gauge{flex:1 1 auto;align-content:center}

/* Dua isian berdampingan di kartu Profil Medan: kotaknya sejajar meskipun
   labelnya beda jumlah baris. "TINGGI ANTENA PELANGGAN (M)" melipat jadi dua
   baris di kolom selebar 170px, "TINGGI MENARA BTS (M)" tidak — dan kedua
   kotak isian berdiri di dua ketinggian yang berbeda, persis di tengah kartu
   yang dibuat rapi. Labelnya diberi tinggi dua baris dan ditempelkan ke
   DASAR petaknya: yang satu baris duduk di garis yang sama dengan baris
   kedua tetangganya, jadi keduanya berangkat dari tepi bawah yang sama.

   `line-height` ikut ditulis: tanpanya tinggi dua baris ditentukan bawaan
   peramban (±1,3) dan tidak sama dengan `2.4em` yang dipesan di sini —
   terukur meleset 6px, cukup untuk terlihat sebagai dua kotak yang tidak
   sejajar. Dengan 1.2, dua baris PERSIS 2.4em. */
.set-petak .grid-2 > .kolom-sempit > .f-label{display:flex;align-items:flex-end;
  line-height:1.2;min-height:2.4em}

/* Ringkasan zona: SATU kolom berapa pun lebar kartunya. Tiga baris yang
   dibelah jadi dua kolom (`.pt-grid` melakukannya sendiri di atas 440px)
   menyisakan satu petak kosong di kanan bawah — dan di layar 1920px kartu
   ini 595px, jadi justru layar terlebar yang paling terlihat berlubang.
   Satu kolom juga yang memakai sisa tinggi kartunya: tiga baris bergaris
   yang membentang penuh, bukan dua kolom setengah tinggi. */
.set-petak .zona-ringkas{grid-template-columns:minmax(0,1fr)}

/* Kaki tabel zona: SATU baris, bukan dua kolom.
   "Arti status" dan "Ambang mendekati" keduanya pendek — sebagai dua kolom,
   yang kiri habis di satu baris ringkas dan menyisakan bidang kosong setinggi
   kolom kanan di bawahnya, sementara separuh kanan kotak isian juga kosong
   sampai tepi kartu. Berdampingan dalam satu baris, keduanya terbaca sebagai
   kaki tabel — yang kiri di tepi kiri, yang kanan di tepi kanan. */
.set-petak .zona-kaki{display:flex;flex-wrap:wrap;align-items:center;
  justify-content:space-between;gap:8px 24px;margin-top:8px}
.set-petak .zona-kaki > .ket-lipat{margin-top:0}
.set-petak .zona-ambang{display:flex;flex-wrap:wrap;align-items:center;gap:4px 12px}
.set-petak .zona-ambang > label{margin-bottom:0}
.set-petak .zona-ambang .ket-lipat{margin-top:0}

.al-bidang{overflow-x:auto;padding:4px 0 8px}
.al-bidang > svg{display:block;width:100%;height:auto;min-width:760px}

/* Kelas semantik SVG archify. Disalin apa adanya dari berkas hasilnya — yang
   berubah cuma isi variabelnya di blok atas. Tanpa aturan-aturan ini,
   kelasnya terpasang di markup dan tidak menghasilkan apa-apa: bukan "kurang
   rapi" melainkan diagram hitam tanpa bentuk sama sekali, pelajaran yang
   sudah dibayar di bug34.

   Dibatasi ke halaman ini (`.al-bidang`) dan bukan global:
   nama sependek `.t-muted` dan `.c-lane` terlalu mudah bertabrakan dengan
   kelas aplikasi yang lahir kemudian. */
.al-bidang .c-grid{stroke:var(--grid);fill:none}
.al-bidang .c-mask{fill:var(--mask);stroke:none}
.al-bidang .c-lane{fill:var(--lane-fill);stroke:var(--lane-stroke);stroke-dasharray:6,6}
.al-bidang .c-frontend{fill:var(--frontend-fill);stroke:var(--frontend-stroke)}
.al-bidang .c-backend{fill:var(--backend-fill);stroke:var(--backend-stroke)}
.al-bidang .c-database{fill:var(--database-fill);stroke:var(--database-stroke)}
.al-bidang .c-messagebus{fill:var(--messagebus-fill);stroke:var(--messagebus-stroke)}
.al-bidang .c-external{fill:var(--external-fill);stroke:var(--external-stroke)}
.al-bidang .t-primary{fill:var(--text)}
.al-bidang .t-muted{fill:var(--text-muted)}
.al-bidang .t-dim{fill:var(--text-dim)}
.al-bidang .t-backend{fill:var(--backend-stroke)}
.al-bidang .a-default{stroke:var(--arrow);fill:none}
.al-bidang .a-emphasis{stroke:var(--arrow-emphasis);fill:none}
.al-bidang .m-default{fill:var(--arrow)}
.al-bidang .m-emphasis{fill:var(--arrow-emphasis)}
.al-bidang .m-security{fill:var(--security-stroke)}
.al-bidang .m-dashed{fill:var(--database-stroke)}
/* Lambang kecil di pojok tiap kotak. `currentColor` yang diwarnai `.s-*` di
   bawahnya — itu sebabnya `color` dan bukan `stroke` yang disetel di sana. */
.al-bidang .semantic-sigil{fill:none;stroke:currentColor;stroke-width:1.35;
  stroke-linecap:round;stroke-linejoin:round;opacity:.76;pointer-events:none}
.al-bidang .semantic-sigil > *{vector-effect:non-scaling-stroke}
.al-bidang .semantic-sigil .sigil-fill{fill:currentColor;stroke:none}
.al-bidang .s-backend{color:var(--backend-stroke)}
.al-bidang .s-database{color:var(--database-stroke)}
.al-bidang .s-external{color:var(--external-stroke)}
.al-bidang .s-frontend{color:var(--frontend-stroke)}
.al-bidang .s-messagebus{color:var(--messagebus-stroke)}

/* ── bug78: yang digambar DI DALAM kotak backend (18 September 2026) ───────
   Dilaporkan pemilik lewat bug78.png: *"pada halaman alur kerja, warnanya
   terlalu gelap tidak terlihat jadinya"*.

   Kotak backend satu-satunya yang isiannya BERWARNA — `--backend-fill` =
   `--blue-permukaan`; empat jenis kotak lainnya memakai `--bg-2`/`--bg-3`.
   Tapi teks di dalam KELIMANYA memakai token teks HALAMAN (`--text` →
   `--t1`, dan seterusnya), dan token itu disetel untuk latar halaman, bukan
   untuk permukaan aksen. Di empat kotak itu ia jatuh di atas warna yang
   sama sekali lain.

   Terukur di peramban, `fill` teks lawan `fill` rect di grupnya, 30 dari 30
   keadaan tema x aksen GAGAL — bukan cuma tema terang yang dilaporkan:

       judul (t-primary)      3,17 – 4,30
       keterangan (t-muted)   1,25 – 3,92
       label kecil (t-backend) 1,00 – 1,68   ← 1,00 = warnanya PERSIS
                                               warna latarnya, di aksen kustom

   Yang diisikan di sini token yang memang untuk ini dan sudah dipakai tiap
   tombol utama aplikasi: `--fg-aksen` di atas `--blue-permukaan`
   (`.btn-utama`, `.login-btn`). Ia juga satu-satunya yang ikut warna aksen
   PILIHAN SENDIRI — kalau pemilik memilih kuning muda, teksnya menggelap
   sendiri lewat `--aksen-kustom-fg`.

   Yang diisi VARIABEL ARCHIFY-nya untuk subpohon itu, bukan `fill` tiap
   kelas: begitu diagramnya dibuat ulang dan generatornya memakai kelas teks
   yang belum ada hari ini, kelas itu ikut benar tanpa disunting.
   `--backend-stroke` sengaja TIDAK ikut diisi ulang — ia juga garis tepi
   kotaknya, dan tepi putih mengubah bentuk yang tidak ada keluhannya.

   Tingkatan judul/keterangan/label tetap ada, tapi dibawa UKURAN dan TEBAL
   huruf (11px 600 · 8px · 7px), bukan warna: tiga warna berbeda di atas satu
   permukaan aksen berarti dua di antaranya pasti lebih lemah dari yang
   dijaga — dan yang paling lemah justru yang paling kecil hurufnya. */
.al-bidang g[data-node-kind="backend"]{
  --text:var(--fg-aksen);
  --text-muted:var(--fg-aksen);
  --text-dim:var(--fg-aksen)}
.al-bidang g[data-node-kind="backend"] .t-backend{fill:var(--fg-aksen)}
.al-bidang g[data-node-kind="backend"] .s-backend{color:var(--fg-aksen)}

/* ══════════════════════════════════════════════════════════════════════════
   `.sembunyi` — SENGAJA PALING BAWAH
   ══════════════════════════════════════════════════════════════════════════
   Dipakai kartu grafik di Tren, menu unduh di Log Aktivitas, dan delapan
   belas kelas lain. Namanya dulu .chart-sembunyi — khusus grafik, padahal
   isinya `display:none` yang tidak ada urusannya dengan grafik.

   LETAKNYA yang penting, dan itu ditemukan 10 September 2026 lewat laporan
   pengguna: "ada tabel kosong di bawah, yang upgrade sedang berjalan, itu
   fungsinya apa ya?". Yang ia lihat lapisan kemajuan upgrade
   (`#upgLapis`, LOG PEMASANGAN) — panel setinggi 900px yang seharusnya
   hanya muncul SELAMA upgrade berjalan. Ia memang ditulis
   `class="upg-lapis sembunyi"`. Yang tidak berlaku justru `.sembunyi`-nya.

   Dulu ia duduk di baris 2835, sementara `.upg-lapis{display:flex}` di
   4863. Keduanya satu kelas — kekhususannya SAMA — jadi yang menang
   URUTAN, dan yang belakangan menang. Jebakan yang sama persis sudah
   dicatat untuk `.f-input` vs `.login-input` di
   test_gaya_bersama.py: "yang menang ditentukan URUTAN, bukan kekhususan.
   Utilitas yang naik ke atas berhenti berlaku, diam-diam."

   Korbannya dua, dihitung dengan membuka tiap halaman di peramban dan
   mencari elemen ber-`.sembunyi` yang display-nya BUKAN none:

     settings  #upgLapis      display:flex  tinggi 900px
     maps      #svKoordBaris  display:flex  tinggi  33px

   Dari dasar berkas ia menang atas setiap aturan sekelas, berapa pun
   banyaknya aturan baru yang ditambahkan di atasnya nanti. Yang TIDAK ia
   kalahkan: kekhususan yang lebih tinggi (`.a.b`, `#id`) dan `style=`
   sebaris — dan itu memang disengaja, karena beberapa kartu dinyalakan JS
   lewat `style.display`. Dijaga test_sembunyi_paling_bawah.py. */
.sembunyi{display:none}
