:root {
  color-scheme: dark;
  --bg: #0f172a;
  --bg-card: #1e293b;
  --border: #334155;
  --text: #e2e8f0;
  --text-dim: #94a3b8;
  --green: #22c55e;
  --red: #ef4444;
  --yellow: #eab308;
  --blue: #38bdf8;
  /* Dipilih khusus (bukan asal pakai --blue/--red) karena harus kontras satu
     sama lain sebagai PASANGAN 2 warna kategorikal di grafik trafik (rx vs
     tx) — divalidasi lolos cek Lightness band, Chroma floor, CVD separation
     (ΔE 26.8), Normal-vision floor (ΔE 31.8), dan kontras vs background
     (≥3:1). Jangan diganti tanpa validasi ulang. */
  --traffic-rx: #3987e5;
  --traffic-tx: #d95926;
  /* 4 variabel di bawah ini SENGAJA terpisah dari --blue/--red/--green di
     atas, bukan duplikat — nilainya dipilih khusus untuk kombinasi
     foreground+background TERTENTU (teks tombol di atas --blue, pita "down"
     di atas kanvas grafik, flash merah tua kartu down, teks di dalam kotak
     alert yang temanya sendiri sudah bertema merah/hijau), bukan untuk
     dipakai sebagai warna status umum. Lihat README bagian Mode Terang/Gelap
     untuk kenapa dipisah (kalau cuma pakai --blue/--red langsung, versi
     mode terangnya akan gagal kontras — sudah divalidasi ulang dengan
     dataviz/scripts/validate_palette.js + perhitungan WCAG manual). */
  --btn-text: #0f172a;
  --down-band: rgba(239,68,68,0.14);
  --pulse-down-bg: #2a1414;
  --alert-error-text: #fca5a5;
  --alert-ok-text: #86efac;
  /* --status-neutral: aksen border-left NETRAL (status belum diketahui, atau
     elemen yang memang tidak punya konsep status up/down sama sekali, mis.
     kartu interface di iface-traffic.php) — SENGAJA dipisah dari --text-dim
     walau nilai di mode gelap kebetulan sama persis, supaya versi mode
     terangnya bisa dibikin lebih lembut tanpa ikut mengubah --text-dim yang
     dipakai luas sebagai warna TEKS di banyak tempat lain (lihat README
     bagian Mode Terang/Gelap, "Ditemukan waktu uji nyata di HP"). */
  --status-neutral: #94a3b8;
  /* --card-shadow: "none" di mode gelap (kartu sudah cukup terpisah dari
     latar lewat beda value warna gelap-vs-lebih-gelap, border tipis saja
     sudah cukup) — di mode terang diisi shadow lembut supaya kartu terasa
     "mengambang" alih-alih cuma kotak bergaris tegas di atas latar putih
     (lihat README, keluhan pengguna "kayak Windows XP high contrast"). */
  --card-shadow: none;
  /* --stat-box-bg/--stat-box-border: kotak kecil di dalam kartu (mis.
     Download/Upload di iface-traffic.php, psb-value lainnya) — di mode
     gelap tetap rgba tipis seperti semula (TIDAK diubah, supaya halaman
     publik yang masih selalu gelap & ikut makai class yang sama tidak
     kebagian perubahan apa pun). Di mode terang diisi var(--bg) (konsisten
     dengan konvensi "elemen cekung = var(--bg)" yang sudah dipakai di
     input/tooltip lain) + border dihilangkan, karena di atas putih,
     wash rgba setipis itu nyaris tidak kelihatan dan border 1px keras
     jadi satu-satunya yang mendefinisikan kotaknya — itulah kenapa terasa
     kaku/kotak-kotak. */
  --stat-box-bg: rgba(148,163,184,0.08);
  --stat-box-border: var(--border);
  /* Nilai default (dipakai mode gelap) SAMA PERSIS dengan bobot font yang
     sudah ada sebelum variabel ini ada (700/600) — lihat override mode
     terang di bawah untuk kenapa dipisah per tema. */
  --value-weight: 700;
  --name-weight: 600;
  /* --bg-pattern/--bg-glow: dulu CUMA dipasang di mode terang (dot grid
     "none" di sini) — pengguna melaporkan latarnya "masih monotone" walau
     sudah dicoba di produksi sungguhan dengan banyak kartu, dan minta
     tekstur ini dipasang di KEDUA mode, bukan cuma terang. Di mode gelap,
     nilai bawaan ("sebelumnya") disamakan lewat opacity sangat rendah di
     bawah supaya tidak ada yang berubah drastis dari kesan "gelap polos"
     yang sudah disukai — ini PENAMBAHAN tekstur baru, bukan migrasi dari
     nilai lama (dulu benar-benar "none", sekarang dot+glow tipis). */
  /* Dikuatkan dari percobaan pertama (opacity 0.07, radius 1px) — pengguna
     melaporkan masih "belum kelihatan"/"terlalu tipis" di mode gelap
     (README bagian 51). Dot abu-biru muda (148,163,184) di atas navy
     gelap (#0f172a) butuh opacity jauh lebih tinggi dari versi terang
     untuk kontras yang setara — di opacity 0.07 lama, selisih warna dot
     vs latar cuma ~±10 per channel (nyaris tidak kelihatan), di opacity
     0.16 ini selisihnya ~±22 per channel, sebanding dengan selisih di
     mode terang (~±17). Radius ikut dinaikkan sedikit (1px→1.2px) biar
     titiknya juga sedikit lebih besar, bukan cuma lebih kontras. */
  --bg-pattern: radial-gradient(circle, rgba(148,163,184,0.16) 1.2px, transparent 1.7px);
  --bg-glow: radial-gradient(ellipse at 50% -20%, rgba(56,189,248,0.24), transparent 65%);
  /* Wash gelap buat "melesakkan" panel detail per-site yang di-expand di
     Grup Link Publik — lihat override mode terang untuk kenapa dipisah
     jadi variabel (bug yang baru ketahuan di README bagian 50). SEJAK
     redesign "kartu per site" (lihat README), satu-satunya pemakainya
     (.group-row-detail) sudah dihapus — variabel ini SENGAJA dibiarkan
     (tidak dipakai lagi di mana pun) cuma buat nilai historis, tidak
     mengganggu apa pun kalau dibiarkan. */
  --detail-overlay-bg: rgba(15,23,42,0.35);
}

/* ==== Mode Terang ====
   Diaktifkan lewat atribut data-theme="light" di <html> (lihat
   assets/js/theme.js + README bagian "Mode Terang/Gelap"). Cuma dipasang
   di halaman ADMIN (topbar/login) — halaman publik pelanggan (public.php,
   public-group.php) SENGAJA tetap gelap-saja, scope disepakati begitu.
   Semua nilai di bawah sudah divalidasi kontras (WCAG) & CVD-separation
   (untuk pasangan trafik rx/tx) lewat dataviz/scripts/validate_palette.js —
   jangan ganti nilai tanpa validasi ulang yang sama. */
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f1f5f9;
  --bg-card: #ffffff;
  --border: #cbd5e1;
  --text: #0f172a;
  --text-dim: #475569;
  --green: #15803d;
  --red: #dc2626;
  --yellow: #a16207;
  --blue: #0369a1;
  --traffic-rx: #1d4ed8;
  --traffic-tx: #c2410c;
  --btn-text: #f8fafc;
  --down-band: rgba(220,38,38,0.16);
  --pulse-down-bg: #fee2e2;
  --alert-error-text: #b91c1c;
  --alert-ok-text: #15803d;
  /* Lebih terang dari nilai mode gelap (#94a3b8) — di atas putih, abu-biru
     medium itu masih tampil sebagai garis yang cukup "ramai"; versi ini
     dibuat senada dengan --border supaya cuma kerasa sebagai struktur
     halus, bukan aksen yang menonjol (card ini memang tidak punya makna
     status apa-apa, lihat komentar panjang di bagian :root gelap). */
  --status-neutral: #cbd5e1;
  /* Dikuatkan dari percobaan pertama (0.05/0.06) — ternyata kurang
     kelihatan di layar HP/lewat kompresi screenshot, jadi kartu masih
     terasa "kotak bergaris" alih-alih mengambang. Blur & opacity lapisan
     keduanya dinaikkan supaya efek "lift"-nya benar-benar kelihatan,
     tapi warnanya tetap abu gelap transparan (bukan hitam pekat) supaya
     tetap halus, bukan bayangan yang berat. */
  --card-shadow: 0 1px 3px rgba(15,23,42,0.07), 0 10px 24px rgba(15,23,42,0.10);
  /* Angka/nama di kartu (.psb-value, .ip-name) sedikit diringankan di mode
     terang — tebal (700/600) + hitam pekat di atas putih terasa "berat"/
     tinggi-kontras ala tema aksesibilitas lama, walau di mode gelap bobot
     yang sama terasa wajar (lihat README bagian perbaikan susulan). */
  --value-weight: 600;
  --name-weight: 500;
  --stat-box-bg: var(--bg);
  --stat-box-border: transparent;
  /* Pola titik halus di latar halaman (BUKAN di atas kartu — kartu tetap
     polos var(--bg-card) supaya kontennya tidak ikut "ramai") — jarak
     22px (lihat background-size di aturan body). Opacity/radius dikuatkan
     dari percobaan pertama (0.12, radius 1px) — dicoba di produksi nyata
     dengan banyak kartu (lihat screenshot halaman Trafik Interface di
     README bagian 49) dan dirasa masih "monotone"/nyaris tidak kelihatan,
     terutama di layar HP. */
  --bg-pattern: radial-gradient(circle, rgba(100,116,139,0.20) 1.2px, transparent 1.7px);
  /* --bg-glow: lapisan baru — gradasi radial sangat halus di atas halaman
     (warna senada --blue tema ini), supaya latar tidak cuma "warna polos +
     titik" tapi punya kesan kedalaman seperti dashboard modern pada
     umumnya. Cuma terlihat di ~380px teratas halaman (lihat background-size
     di aturan body) lalu memudar — tidak diulang (no-repeat), jadi tidak
     ikut ke-tile sepanjang halaman seperti --bg-pattern. */
  --bg-glow: radial-gradient(ellipse at 50% -20%, rgba(3,105,161,0.16), transparent 65%);
  /* --detail-overlay-bg: nilai mode gelap (rgba(15,23,42,0.35), warna
     --bg gelap sendiri di-overlay transparan) TIDAK cocok dipakai mentah
     di mode terang — itu navy gelap, di atas kartu putih tampil sebagai
     noda abu-abu kotor, bukan "panel melesak" yang halus (ditemukan waktu
     public-group.php pertama kali diperiksa visual, lihat README bagian
     50). Percobaan pertama pakai var(--bg) (konsisten dengan konvensi
     "elemen cekung = var(--bg)" yang sama dipakai --stat-box-bg) — TERNYATA
     bikin masalah baru: chip/kotak DI DALAM panel ini (.pub-chart-subtitle,
     .chart-tabs, .tcs-row) semuanya JUGA pakai var(--stat-box-bg) = var(--bg)
     yang SAMA PERSIS, jadi begitu bersarang di dalam panel ini warnanya
     menyatu total, chip-nya jadi tidak kelihatan bentuknya sama sekali.
     Dipakai warna solid #e2e8f0 (satu tingkat lebih gelap dari --bg
     #f1f5f9, masih satu tingkat lebih terang dari --border #cbd5e1 — di
     antara keduanya) supaya panel ini TETAP kelihatan beda dari kartu putih
     di atasnya, DAN kotak-kotak var(--stat-box-bg) di dalamnya tetap
     kelihatan beda dari panel yang membungkusnya. */
  --detail-overlay-bg: #e2e8f0;
}

* { box-sizing: border-box; }

/* Font "Inter" (bagian 121) — pengguna minta tampilan lebih profesional,
   salah satu penyebab terbesar kesan "generik" adalah font sebelumnya
   cuma ikut bawaan sistem (-apple-system/Segoe UI/Roboto, beda-beda
   tiap HP, tidak ada identitas visual sendiri) — Inter dipilih karena
   dipakai luas di produk-produk dashboard/SaaS profesional (desainnya
   memang dibuat khusus supaya angka & teks padat gampang dibaca di
   layar kecil, cocok buat app monitoring begini).
   FILE FONT DI-HOST SENDIRI (assets/fonts/*.woff2, lisensi SIL Open
   Font License — lihat assets/fonts/LICENSE-inter.txt), BUKAN dimuat
   dari Google Fonts/CDN luar — app ini dari awal 100% self-contained,
   TIDAK ADA dependensi ke domain luar di mana pun (PWA offline-first,
   dipakai juga di lapangan dengan koneksi yang kadang tidak stabil),
   jadi font pun TIDAK BOLEH jadi pengecualian pertama yang bergantung
   ke internet luar cuma supaya teksnya tampil benar.
   Cuma subset "latin" (bukan "latin-ext") yang dipakai — Bahasa
   Indonesia tidak butuh huruf berdiakritik di luar alfabet Latin
   biasa, jadi file lebih kecil (~24KB/berat, total ~130KB utk 5
   berat) tanpa kehilangan satu pun karakter yang dipakai app ini.
   `font-display: swap` supaya teks tetap langsung kelihatan (pakai
   font fallback) selama file font masih dimuat, bukan nge-blank dulu
   (FOIT) — penting di koneksi lambat. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/inter-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/inter-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/inter-600.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/inter-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('../fonts/inter-800.woff2') format('woff2');
}

body {
  margin: 0;
  background-color: var(--bg);
  color: var(--text);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  padding-bottom: env(safe-area-inset-bottom);
}

/* Dua lapis tekstur latar (titik halus + gradasi radial lembut di atas
   halaman) — SENGAJA di-scope ke class .themed-body, BUKAN dipasang
   langsung di selector `body` polos di atas. Dulu (bagian 49) class ini
   bernama ".admin-body" dan SENGAJA tidak dipasang di halaman publik
   (public.php/public-group.php) — scope fitur Mode Terang/Gelap sejak
   awal (bagian 46) disepakati admin-only. Sejak bagian 50, pengguna
   minta halaman publik (link status read-only buat pelanggan) JUGA
   dapat sakelar + tekstur latar ini, jadi class-nya diganti nama jadi
   ".themed-body" (lebih akurat — bukan admin-only lagi) dan sekarang
   DIPASANG JUGA di public.php/public-group.php, bukan cuma dikecualikan.
   Tetap dipisah lewat class (bukan langsung di selector `body` polos)
   supaya kalau nanti ada halaman lain yang sengaja HARUS tetap polos
   (mis. halaman cetak/print, kalau ada), tinggal tidak pasang class ini
   — pola yang sama tetap berguna walau sekarang dipakai lebih luas.
   CATATAN bagian 53: --bg-glow TIDAK LAGI di sini (lihat .bg-glow-fixed
   di bawah) — sekarang body.themed-body cuma bawa --bg-pattern (titik
   halus, tetap tiled & ikut scroll dokumen seperti biasa). Alasannya
   ada di komentar .bg-glow-fixed. */
body.themed-body {
  background-image: var(--bg-pattern);
  background-repeat: repeat;
  background-position: 0 0;
  background-size: 22px 22px;
}

/* bagian 53 — "Perbaikan: Efek Scroll-Parallax Glow Tidak Kelihatan Sama
   Sekali di Pemakaian Nyata". Pengguna sudah coba fitur bagian 52 (glow
   ikut geser halus pakai assets/js/bg-parallax.js) dan lapor "seperti
   tidak ada perubahan". Dicek: file JS-nya sendiri sudah termuat & logic-
   nya benar (variabel --glow-shift berubah persis sesuai scroll, sudah
   lolos semua test). Tapi efeknya SECARA DESAIN memang nyaris mustahil
   kelihatan di halaman sungguhan, BUKAN soal deploy/cache:

   Dulu lapisan glow dipasang sebagai salah satu lapis background `body`
   biasa (ikut attachment bawaan "scroll") — artinya glow itu nempel di
   ~380px PALING ATAS dokumen, lalu ikut ter-scroll ke atas & hilang dari
   layar begitu scrollY lewat kira-kira 380-440px, SAMA PERSIS dengan
   jarak scroll yang dipakai geseran --glow-shift itu sendiri (maksimum
   geser 60px baru tercapai di scrollY=500, padahal glow-nya sendiri
   sudah lenyap dari layar jauh sebelum itu). Jadi yang kelihatan cuma
   "glow menghilang pas di-scroll" (perilaku lama, sudah ada sejak bagian
   49, bukan hal baru) — bukan "glow bergeser halus", karena jendela
   waktu buat lihat geserannya ketutup duluan oleh glow-nya sendiri yang
   sudah keluar layar.

   Perbaikan: glow dipindah dari lapisan background `body` ke elemen
   tersendiri yang DIPASANG MELAYANG (position: fixed) relatif ke layar/
   viewport, bukan ke dokumen — pola yang sama persis dengan
   .theme-toggle-btn-fixed di atas (tombol gelap/terang), yang sudah
   terbukti aman & tidak bermasalah di HP (termasuk Safari iOS) di
   aplikasi ini. Dengan begitu glow TIDAK ikut hilang waktu di-scroll —
   dia tetap kelihatan menempel di bagian atas layar, dan geseran halus
   dari --glow-shift sekarang benar-benar kelihatan sebagai "glow turun
   sedikit waktu scroll", bukan ketutup duluan.

   --bg-pattern (titik-titik) TETAP di background `body` biasa seperti
   sebelumnya — titik-titik itu memang dimaksudkan tiled & ikut scroll
   bersama konten, tidak ada masalah di sana. Elemen <div class=
   "bg-glow-fixed" aria-hidden="true"> dipasang sebagai child PERTAMA di
   tiap <body class="themed-body">, SEBELUM konten lain — supaya secara
   urutan dokumen dia otomatis tergambar paling bawah (tanpa perlu
   z-index) dan tidak pernah menutupi/menghalangi klik ke konten (juga
   dibantu pointer-events:none di bawah sebagai jaga-jaga kedua). */
.bg-glow-fixed {
  position: fixed;
  /* bagian 105 — pergeseran parallax var(--glow-shift) DIPINDAH dari
     background-position ke `top` (lihat di bawah). Alasan: dulu geser
     lewat "background-position: center var(--glow-shift,0px)" menggeser
     GAMBAR gradasi ke BAWAH di dalam kotak 520px yang ukurannya (lewat
     background-size) persis sama dengan gambar itu sendiri — begitu
     digeser, bagian ATAS kotak jadi kosong (gambar sudah "pindah",
     tidak ada apa pun buat mengisi celahnya) SEMENTARA tepi atas gambar
     gradasi itu sendiri (dekat pusat elipsnya di 50% -20%) justru masih
     cukup pekat warnanya, belum sempat pudar jadi transparan — jadi yang
     kelihatan persis di batas celah itu adalah garis/pita tegas (dari
     kosong total ke pekat mendadak), BUKAN transisi halus. Itulah
     "belang" yang dilaporkan pengguna di link publik versi desktop, dan
     karena nilainya berubah mengikuti scroll (lewat --glow-shift), pita
     itu kelihatan ikut naik-turun waktu discroll — persis seperti
     dilaporkan. Diverifikasi via repro (viewport lebar 1920px, scroll
     dekat maksimum) + sampling piksel Playwright/Pillow: sebelum
     perbaikan, baris horizontal di sekitar titik pergeseran menunjukkan
     lompatan jelas (area tanpa tint lalu loncat ke tint terpekat dalam
     beberapa piksel); setelah perbaikan, transisinya halus menyatu
     dengan gradasi yang sama seperti biasa, tanpa pita.
     Perbaikannya: TETAP pakai gradasi --bg-glow yang sama persis (tidak
     diubah sama sekali, jadi tampilan saat halaman belum discroll 100%
     identik seperti sebelumnya) dan TETAP background-size 520px (gambar
     gradasi tidak diregangkan/diubah proporsinya), tapi pergeserannya
     sekarang dilakukan dengan menggeser KOTAK elemennya sendiri ke atas
     (top negatif) sebesar --glow-shift, bukan menggeser gambar di dalam
     kotak yang diam. Dengan begini yang terlihat di layar selalu berupa
     jendela/potongan yang berbeda dari SATU gradasi yang sama dan
     kontinu (tidak pernah ada area "di luar gambar" yang terbuka),
     sehingga tidak mungkin muncul garis tegas — hanya pergeseran halus
     seperti efek parallax yang memang dimaksud dari awal. */
  top: calc(-1 * var(--glow-shift, 0px));
  left: 0;
  right: 0;
  height: 520px;
  background-image: var(--bg-glow);
  background-repeat: no-repeat;
  /* Posisi background sekarang statis (center top) — pergeseran parallax
     ditangani lewat `top` di atas, lihat catatan bagian 105. Fallback
     --glow-shift 0px (JS belum jalan/mati/prefers-reduced-motion) =
     top:0px = posisi semula, identik seperti sebelum bagian 52/105,
     tetap aman tanpa JS. */
  background-position: center top;
  background-size: 100% 520px;
  pointer-events: none;
}

.topbar {
  /* Dulu h1 dan nav sebaris (flex row + wrap) — begitu menu bertambah
     (Akun, Kelola User, dst) link-nya numpuk sampai 3 baris teks biru
     polos, makan tempat vertikal & susah di-scan di HP. Sekarang judul
     jadi barisnya sendiri, nav jadi baris di bawahnya — lihat .topbar nav
     untuk kenapa nav-nya sendiri dibuat SATU baris yang bisa digeser,
     bukan ikut wrap. */
  display: flex;
  flex-direction: column;
  padding: 14px 0;
  padding-top: max(14px, env(safe-area-inset-top));
  background: var(--bg-card);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--card-shadow);
  position: sticky;
  top: 0;
  z-index: 10;
  gap: 10px;
}

.topbar h1 { font-size: 18px; margin: 0; padding: 0 16px; display: flex; align-items: center; }

/* Tombol ganti mode terang/gelap (lihat assets/js/theme.js) — SENGAJA
   ditaruh di dalam <h1> (bukan di .topbar nav yang bisa digeser ke
   samping kalau menu banyak), supaya selalu kelihatan tanpa perlu geser,
   didorong ke paling kanan baris judul lewat margin-left:auto karena
   .topbar h1 sudah display:flex. Warna wash disamakan dengan chip nav
   aktif lain (.topbar nav a, #toggle-sensitive) biar senada. */
/* Sakelar geser (bukan tombol ikon lagi) — ikon matahari/bulan SVG asli
   (senada dengan gaya ikon lain di app, mis. .tab-icon), bukan emoji 🌙/☀️
   yang renderingnya beda-beda tiap HP & kadang terlihat "lucu"/kartun
   (lihat README bagian perbaikan susulan). Posisi bundaran (.theme-toggle-
   thumb) digeser murni lewat CSS berdasarkan atribut aria-checked yang
   diset assets/js/theme.js — ikon mana yang tampil DI DALAM bundaran itu
   juga murni CSS (display:none bergantian), jadi JS-nya sendiri tidak
   perlu tahu apa-apa soal ikon, cuma set/lepas aria-checked. */
.theme-toggle-btn {
  margin-left: auto;
  flex-shrink: 0;
  position: relative;
  width: 50px;
  height: 27px;
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg);
  padding: 2px;
  cursor: pointer;
}
.theme-toggle-thumb {
  width: 21px;
  height: 21px;
  border-radius: 50%;
  background: var(--bg-card);
  box-shadow: 0 1px 3px rgba(0,0,0,0.35);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.2s ease;
}
/* 23px = lebar dalam trek (50 − border 2 − padding 4) dikurangi lebar
   bundaran (21) — supaya bundaran pas mentok ke tepi kanan, bukan
   nanggung di tengah. */
.theme-toggle-btn[aria-checked="true"] .theme-toggle-thumb { transform: translateX(23px); }
.theme-toggle-btn:active .theme-toggle-thumb { filter: brightness(0.96); }
.theme-toggle-icon { width: 13px; height: 13px; flex-shrink: 0; }
.theme-toggle-icon.icon-sun { color: var(--yellow); }
.theme-toggle-icon.icon-moon { color: var(--blue); }
.theme-toggle-btn[aria-checked="false"] .icon-moon { display: none; }
.theme-toggle-btn[aria-checked="true"] .icon-sun { display: none; }
/* Varian berdiri sendiri buat login.php (tidak punya .topbar/<h1>) —
   ditempel fixed di pojok kanan atas, dengan aman dari notch/status bar HP. */
.theme-toggle-btn-fixed {
  position: fixed;
  top: max(14px, env(safe-area-inset-top));
  right: 14px;
  z-index: 20;
  background: var(--bg-card);
  box-shadow: var(--card-shadow);
}

.app-logo {
  width: 24px;
  height: 24px;
  border-radius: 7px;
  margin-right: 8px;
  flex-shrink: 0;
}
.app-logo-lg { width: 56px; height: 56px; border-radius: 15px; margin-right: 0; }

/* #conn-badge (status live-poll, lihat assets/js/dashboard.js dkk) —
   bagian 118 sempat menaruhnya di baris TERSENDIRI di bawah judul
   (".topbar-status-row", setelah deretan chip menu lama pindah ke
   panel drawer) — tapi laporan pengguna lewat tangkapan layar: baris
   itu jadi kelihatan "kosong/hampa" (cuma satu pil kecil sendirian di
   baris selebar layar, sisanya putih polos). Bagian 120 memindahkannya
   LAGI, kali ini masuk ke DALAM baris judul <h1> itu sendiri (di
   antara judul & tombol mode gelap/terang) — supaya topbar jadi SATU
   baris yang terasa terisi rapi, bukan dua baris dengan satu baris
   nyaris kosong. Halaman yang tidak live-poll (mis. account.php,
   settings.php) sama sekali tidak mencetak elemen ini, topbar-nya
   tetap satu baris judul polos seperti biasa. */
.topbar h1 .pill {
  flex-shrink: 0;
  margin-left: 10px;
  font-size: 11px;
}

/* Tombol hamburger pembuka panel drawer (bagian 118) — ditaruh ANAK
   PERTAMA di dalam <h1> (lihat render_nav_drawer_button() di
   includes/nav.php), SEBELUM logo/judul — pola umum "tombol menu di
   kiri atas" di app mobile/PWA. */
.nav-drawer-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin-right: 4px;
  background: transparent;
  border: none;
  color: var(--text);
  cursor: pointer;
  padding: 0;
  border-radius: 8px;
}
.nav-drawer-btn:active { background: rgba(148, 163, 184, 0.15); }
.nav-drawer-btn svg { width: 22px; height: 22px; }

/* Panel drawer off-canvas berisi seluruh menu (bagian 118) — REUSE
   pola ".modal-overlay"/".modal-box"/".modal-header"/".modal-close-btn"
   yang sudah ada sejak bagian 85 (modal Detail di sfp.php, lihat
   komentarnya di bawah) buat backdrop+header+tombol tutup (ZERO CSS
   baru di bagian itu, cuma dipakai ulang), cuma kotak panelnya sendiri
   yang beda: geser dari KIRI & tinggi PENUH layar, bukan kotak di
   tengah. ".nav-drawer-overlay[hidden]" WAJIB ada persis alasan yang
   sama dengan ".modal-overlay[hidden]" (lihat komentarnya) — tanpa
   baris ini overlay tetap menghalangi klik ke konten di belakangnya
   walau atribut hidden sudah terpasang. */
.nav-drawer-overlay {
  position: fixed;
  inset: 0;
  background: rgba(2, 6, 23, 0.6);
  display: flex;
  justify-content: flex-start;
  z-index: 100;
}
.nav-drawer-overlay[hidden] { display: none; }
.nav-drawer-box {
  background: var(--bg-card);
  border-right: 1px solid var(--border);
  box-shadow: var(--card-shadow);
  width: 100%;
  max-width: 300px;
  height: 100%;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.nav-drawer-body { padding: 8px 0 16px; }
.nav-drawer-group { margin-top: 10px; }
.nav-drawer-group:first-child { margin-top: 0; }
.nav-drawer-group-title {
  margin: 0;
  padding: 10px 20px 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.nav-drawer-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 20px;
  color: var(--text);
  text-decoration: none;
  font-size: 14px;
}
.nav-drawer-link:active { background: rgba(56, 189, 248, 0.12); }
/* Ikon (bagian 119) — stroke="currentColor" di markup-nya (lihat
   render_nav_drawer_link() di includes/nav.php) jadi warnanya otomatis
   ikut warna teks link di sebelahnya, tanpa aturan warna terpisah di
   sini (abu-abu biasa, atau biru begitu link-nya jadi aktif lewat
   .nav-drawer-link-active di bawah). */
.nav-drawer-link-icon { width: 18px; height: 18px; flex-shrink: 0; }
.nav-drawer-link-active {
  color: var(--blue);
  font-weight: 600;
  background: rgba(56, 189, 248, 0.08);
  cursor: default;
}
.nav-drawer-divider {
  height: 1px;
  margin: 10px 20px;
  background: var(--border);
}
/* Body dikunci (tidak ikut scroll di belakang drawer) selagi drawer
   terbuka, lihat assets/js/nav-drawer.js — isi drawer sendiri tetap
   bisa di-scroll terpisah lewat ".nav-drawer-box { overflow-y: auto }"
   di atas kalau menu lebih tinggi dari layar. */
body.nav-drawer-open { overflow: hidden; }

.container { max-width: 720px; margin: 0 auto; padding: 16px; }

.summary-row { display: flex; gap: 10px; margin-bottom: 14px; }
.summary-box {
  flex: 1;
  background: var(--bg-card);
  border: 1px solid var(--border);
  box-shadow: var(--card-shadow);
  border-radius: 12px;
  padding: 10px;
  text-align: center;
}
.summary-box span { display: block; font-size: 24px; font-weight: 700; }
.summary-box small { color: var(--text-dim); font-size: 12px; }
.summary-box.up span { color: var(--green); }
.summary-box.down span { color: var(--red); }
/* Ikon kecil di atas angka (bagian 122) — pakai currentColor karena ini
   <svg> inline sungguhan di markup (beda dari ikon search di atas yang
   background-image), jadi warnanya otomatis ikut warna teks kotaknya
   masing-masing lewat 3 aturan warna di bawah, tanpa perlu diulang per
   tema seperti ikon search. */
.summary-icon { display: block; width: 20px; height: 20px; margin: 0 auto 4px; color: var(--text-dim); }
.summary-box.up .summary-icon { color: var(--green); }
.summary-box.down .summary-icon { color: var(--red); }

/* Kotak ringkasan Down (dashboard.js)/Idle (bgp.js) yang bisa DIKLIK untuk
   memfilter daftar kartu di bawahnya — dipasang jadi elemen <button>
   sungguhan (bukan cuma div+onclick) supaya dapat semantik & aksesibilitas
   tombol gratis (fokus keyboard, Enter/Space, dibaca screen reader),
   makanya butuh reset beberapa properti UA default (font/border/background)
   di sini supaya tetap kelihatan identik dengan .summary-box biasa yang
   tidak bisa diklik (mis. kotak "Total"/"Online"). */
.summary-box.filterable {
  font: inherit;
  border: 1px solid var(--border);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.summary-box.filterable:hover { border-color: rgba(248,113,113,0.5); }
.summary-box.filterable.filter-active {
  background: rgba(239,68,68,0.15);
  border-color: var(--red);
}
.summary-box.filterable.filter-active small { color: var(--red); }

/* flex-wrap supaya tidak overflow di HP sempit kalau isi baris ini (tombol
   filter + toggle sensitif) lebih lebar dari layar. */
.toolbar-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; margin-bottom: 14px; }
/* Kotak cari cepat dashboard (index.php) ditaruh di baris sendiri (TERPISAH
   dari .toolbar-row) supaya bisa benar-benar ditengahkan secara horizontal,
   lepas dari tombol-tombol pill di .toolbar-row yang rata kanan. */
.search-row { display: flex; justify-content: center; margin-bottom: 10px; }
/* Kotak cari cepat (dashboard index.php & tabel Kelola IP manage.php) —
   gaya disamakan dengan .table-input/.form-grid input (background gelap
   #0f172a, bukan kotak putih bawaan browser <input type=search>), tapi
   dipakai di luar .form-grid/tabel jadi perlu class sendiri. flex:1 1 200px
   supaya dia melebar mengisi induknya (.search-row di index.php) sampai
   max-width, lalu sisa ruang di kiri-kanannya otomatis jadi margin yang
   menengahkan kotaknya (justify-content:center di induknya). */
.search-input {
  flex: 1 1 200px;
  min-width: 140px;
  max-width: 320px;
  padding: 8px 10px 8px 34px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 13px;
  /* Ikon kaca pembesar (bagian 122) ganti emoji 🔍 yang tadinya ditulis di
     dalam atribut placeholder — pengganti bukan currentColor karena SVG di
     background-image TIDAK ikut alur warna dokumen (beda dari <svg> inline
     biasa), jadi warnanya di-hardcode manual meniru var(--text-dim) tiap
     tema (dark #94a3b8 di sini, light #475569 lewat override di bawah) —
     kalau var(--text-dim) ini berubah suatu saat, dua warna hex di sini
     (dark & light) juga perlu disamakan manual. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 10px center;
  background-size: 16px 16px;
}
.search-input::placeholder { color: var(--text-dim); }
/* margin-right:auto menempel tombol ini ke KIRI baris, terlepas dari
   justify-content:flex-end di .toolbar-row (yang tetap dipakai supaya
   #toggle-sensitive apa adanya tetap di kanan) — lihat index.php/bgp.js. */
#clear-filter-btn {
  margin-right: auto;
  background: rgba(239,68,68,0.15);
  color: var(--red);
}
/* Gotcha yang sama dengan .chart-tabs[hidden] di atas: ".pill" juga sudah
   nyetel "display", jadi atribut hidden="" bawaan JS butuh override eksplisit
   di sini juga, kalau tidak tombolnya tetap kelihatan walau clearFilterBtn.hidden
   di-set true dari dashboard.js/bgp.js. */
#clear-filter-btn[hidden] { display: none; }
/* ".toolbar-row" juga nyetel display:flex — bgp.js pakai hidden="" di DIV
   #bgp-toolbar (bukan cuma tombolnya) buat nyembunyiin seluruh baris
   (termasuk marginnya) waktu filter Idle tidak aktif. */
#bgp-toolbar[hidden] { display: none; }
/* Warna disamakan dengan chip menu di .topbar nav (rgba biru + var(--blue))
   supaya senada dengan tombol-tombol lain, bukan abu-abu redup seperti
   sebelumnya — lihat juga .form-toggle > summary di manage.php untuk
   perlakuan serupa. Saat aktif (IP & Kapasitas sedang disembunyikan),
   latarnya dibuat lebih pekat supaya tetap kelihatan ada perbedaan status. */
#toggle-sensitive {
  background: rgba(56,189,248,0.12);
  color: var(--blue);
}
#toggle-sensitive.toggle-active {
  background: rgba(56,189,248,0.25);
  color: var(--blue);
}

/* Disembunyikan kalau tombol "Sembunyikan IP & Kapasitas" diaktifkan — lihat
   dashboard.js. Pakai display:none (bukan blur) supaya benar-benar tidak
   kebaca, bukan cuma disamarkan. */
body.hide-sensitive .ip-addr,
body.hide-sensitive .sensitive-capacity {
  display: none;
}

/* Tombol "Tampilan Ringkas" (bagian 122) — warna disamakan PERSIS dengan
   #toggle-sensitive di atas (biru chip yang sama) supaya kedua tombol
   toggle ini kelihatan satu keluarga, bukan dibedakan warnanya tanpa
   alasan. */
#toggle-compact {
  background: rgba(56,189,248,0.12);
  color: var(--blue);
}
#toggle-compact.toggle-active {
  background: rgba(56,189,248,0.25);
  color: var(--blue);
}

/* Mode ringkas kartu IP (bagian 122, tombol "Tampilan Ringkas" di
   dashboard.js) — SENGAJA cuma menyembunyikan/memampatkan lewat CSS di
   sini, TIDAK mengubah fungsi cardHtml() di dashboard.js sama sekali,
   supaya markup kartu yang sudah rumit (lihat komentar panjang di
   cardHtml) tidak perlu disentuh/berisiko pecah cuma demi tampilan
   ringkas. Yang disembunyikan: Site ID/Sub ID/Capacity (.ip-meta),
   Catatan (.ip-notes), dan rentang min/max latency (.latency-range) —
   info sekunder yang jarang perlu dilihat sekilas. Yang TETAP
   ditampilkan: nama, IP, status, latency avg, loss%, "down sejak",
   waktu cek terakhir, dan tombol Detail/Link Publik — semua info/aksi
   penting tetap ada, cuma kartunya jadi lebih pendek & rapat. */
body.compact-cards .ip-card {
  padding: 8px 12px;
  gap: 6px;
}
body.compact-cards .ip-meta,
body.compact-cards .ip-notes,
body.compact-cards .latency-range {
  display: none;
}

.cards { display: flex; flex-direction: column; gap: 10px; }

.group-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin: 14px 0 2px;
  padding: 0 2px;
  font-size: 13px;
  font-weight: 700;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.group-header:first-child { margin-top: 0; }
.group-count { font-weight: 500; text-transform: none; letter-spacing: normal; font-size: 12px; }

.ip-card {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-left: 5px solid var(--status-neutral);
  box-shadow: var(--card-shadow);
  border-radius: 12px;
  padding: 12px 14px;
}
.ip-card.status-up { border-left-color: var(--green); }
.ip-card.status-down { border-left-color: var(--red); animation: pulse-down 1.6s ease-in-out infinite; }
.ip-card.inactive { opacity: 0.45; }

@keyframes pulse-down {
  0%, 100% { background: var(--bg-card); }
  50% { background: var(--pulse-down-bg); }
}

.ip-card-main {
  /* Tanpa ini, flex item defaultnya tidak mau menyempit (min-width: auto),
     jadi teks panjang (mis. Site ID gabungan "S0073636 & S0065835") malah
     mendorong kartu melebar ke samping dan kepotong di layar HP, bukan
     turun baris. */
  flex: 1 1 auto;
  min-width: 0;
}

.ip-name { font-weight: var(--name-weight); font-size: 15px; }
/* overflow-wrap:anywhere WAJIB — tanpa ini, string panjang tanpa spasi
   (mis. alamat IPv6 peer BGP, bisa 20+ karakter) tidak punya titik wrap
   alami sama sekali dan malah OVERFLOW ke luar kartu, nabrak/numpuk
   dengan kolom status di sebelah kanan (lihat bagian 21/24 — ini
   ketahuan di halaman Status BGP yang alamatnya bisa IPv6, beda dari IP
   pelanggan di dashboard yang selalu IPv4 pendek). */
.ip-addr { font-family: ui-monospace, monospace; color: var(--text-dim); font-size: 13px; overflow-wrap: anywhere; }
.ip-notes { color: var(--text-dim); font-size: 12px; margin-top: 2px; overflow-wrap: anywhere; }
.ip-meta { color: var(--text-dim); font-size: 11px; margin-top: 2px; overflow-wrap: anywhere; }

.ip-card-side { text-align: right; min-width: 96px; flex-shrink: 0; }
.status-badge { font-weight: 700; font-size: 12px; letter-spacing: 0.04em; }
.status-up .status-badge { color: var(--green); }
.status-down .status-badge { color: var(--red); }
.status-unknown .status-badge { color: var(--text-dim); }

.down-since {
  display: block;
  color: var(--red);
  font-size: 11px;
  font-weight: 600;
  margin-top: 2px;
  white-space: normal;
  overflow-wrap: anywhere;
}

.latency { display: block; font-size: 13px; margin-top: 2px; }
.latency-range { display: block; font-size: 11px; color: var(--text-dim); margin-top: 1px; }
.loss-pct { display: block; font-size: 11px; margin-top: 1px; }
.lat-good { color: var(--green); }
.lat-ok { color: var(--yellow); }
.lat-bad { color: var(--red); }

.checked-at { color: var(--text-dim); font-size: 11px; margin-top: 4px; }
.streak { display: block; color: var(--red); font-size: 11px; margin-top: 2px; }

/* Link "Detail" di kartu dashboard (lihat dashboard.js::cardHtml()) — buka
   ping-test.php buat tes ping manual (fitur bukti komplain ISP, lihat
   README). display:inline-block di dalam .ip-card-side (text-align:right)
   supaya padding kotaknya tidak ikut melebar penuh selebar kolom, cuma
   sebesar teksnya sendiri (seperti pill kecil), konsisten dengan baris lain
   di kolom ini (.checked-at/.streak) yang juga ringkas. */
.ip-detail-link {
  display: inline-block;
  margin-top: 6px;
  font-size: 11px;
  color: var(--blue);
  text-decoration: none;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 10px;
  white-space: nowrap;
}
.ip-detail-link:hover { background: rgba(56,189,248,0.12); }

/* [bagian 94] Tombol "Link Publik" di kartu dashboard — reuse ".ip-detail-link"
   apa adanya (pill kecil yang sama), cuma ditambah jarak kiri lewat modifier
   ini supaya tidak nempel langsung ke tombol "Detail" kalau keduanya
   dirender bersebelahan (lihat assets/js/dashboard.js::cardHtml()). */
.ip-public-link { margin-left: 6px; }

/* [bagian 96] Checkbox "Balik arah RX/TX" per interface di manage-interfaces.php — rata tengah biar sejajar dengan kolom lain. */
.text-center { text-align: center; }

/* Bar pemakaian CPU/memori/disk — halaman Kesehatan Router (bagian 22).
   Warna fill-nya pakai class yang sama dengan lat-good/lat-ok/lat-bad
   (lewat background, bukan color) supaya ambang batasnya konsisten
   dengan warna latensi di tempat lain. */
.usage-row { margin-top: 6px; }
.usage-label {
  display: flex; justify-content: space-between; font-size: 11px;
  color: var(--text-dim); margin-bottom: 2px;
}
.usage-bar {
  height: 6px; border-radius: 3px; background: rgba(148,163,184,0.15);
  overflow: hidden;
}
.usage-bar-fill { height: 100%; border-radius: 3px; }
.usage-bar-fill.lat-good { background: var(--green); }
.usage-bar-fill.lat-ok { background: var(--yellow); }
.usage-bar-fill.lat-bad { background: var(--red); }

/* Kartu Kesehatan Router (health.php) TIDAK pakai pola ip-card-main +
   ip-card-side (dua kolom) seperti kartu ping/BGP — .ip-card-main di sini
   jadi SATU-SATUNYA anak flex (lihat health.js::cardHtml()), jadi otomatis
   melebar penuh selebar kartu, bukan dipersempit oleh kolom sisi kanan yang
   dulu dijatah lebar tetap (min-width:96px) padahal isinya cuma "baru saja"
   1 baris — sisa lebar itu dulu kebuang percuma, sekarang dipakai penuh
   buat grid sensor di bawahnya. */
.health-card-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
}
.health-card-header .checked-at { margin-top: 0; flex-shrink: 0; }

/* Kartu Trafik Interface (iface-traffic.php) — ringkasan (nama + angka
   download/upload) selalu kelihatan, tapi grafik+statistik di dalam
   .if-detail baru dirender/ditampilkan setelah kartunya DIKLIK, supaya
   daftar interface yang banyak tidak langsung menggambar puluhan grafik
   SVG sekaligus (lihat assets/js/iface-traffic.js — chart juga baru benar2
   di-render ke DOM saat expand, bukan cuma disembunyikan lewat CSS). */
.if-summary { cursor: pointer; }
.if-summary:hover .ip-name { color: var(--blue); }
.if-chevron {
  display: inline-block;
  margin-left: 6px;
  color: var(--text-dim);
  font-size: 11px;
  transition: transform 0.15s ease;
}
.if-expanded .if-chevron { transform: rotate(180deg); }
.if-detail { margin-top: 4px; }
.if-hint {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 10px;
  text-align: center;
}

/* Sensor papan ditampilkan sebagai grid kotak kecil (label di atas, nilai
   di bawah) supaya SEJAJAR rapi di HP — bukan daftar satu per baris yang
   tingginya tidak menentu (sebelumnya pakai flex-wrap "chip" yang jumlah
   per barisnya berubah-ubah tergantung panjang teks, jadi terlihat
   berantakan di layar sempit). auto-fill+minmax biar otomatis 2 kolom di
   HP sempit, lebih banyak kolom di layar lebih lebar, tanpa breakpoint. */
.health-sensors {
  margin-top: 10px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 6px;
}
.health-sensor {
  background: rgba(148, 163, 184, 0.08);
  border-radius: 8px;
  padding: 5px 8px;
  min-width: 0;
}
.health-sensor-label {
  display: block;
  font-size: 10px;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}
.health-sensor-value {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin-top: 1px;
  overflow-wrap: anywhere;
}

/* Trafik download/upload per-IP, dashboard admin (hanya muncul kalau
   "Interface Mikrotik" diisi di Kelola IP) — cuma angka saat ini, satu baris
   ringkas (avg/maks ada di halaman status publik, kartu Grafik Trafik, lihat
   .traffic-chart-stats di bawah). */
.traffic-rate {
  display: block;
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 2px;
  white-space: nowrap;
}

.pill {
  display: inline-block;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  border: none;
  cursor: pointer;
}
.pill-on { background: rgba(34,197,94,0.15); color: var(--green); }
.pill-off { background: rgba(148,163,184,0.15); color: var(--text-dim); }

.alert-error {
  background: rgba(239,68,68,0.12);
  border: 1px solid var(--red);
  color: var(--alert-error-text);
  padding: 10px 12px;
  border-radius: 10px;
  margin-bottom: 12px;
  font-size: 13px;
}
.alert-ok {
  background: rgba(34,197,94,0.12);
  border: 1px solid var(--green);
  color: var(--alert-ok-text);
  padding: 10px 12px;
  border-radius: 10px;
  margin-bottom: 12px;
  font-size: 13px;
}

.empty { color: var(--text-dim); text-align: center; padding: 24px 0; }
/* Ikon kecil inline di teks "Tidak ada IP yang cocok dengan pencarian"
   (bagian 122, ganti emoji 🔍 di #search-empty-msg) — currentColor aman di
   sini karena ini <svg> inline di markup, bukan background-image, jadi
   otomatis ikut warna var(--text-dim) dari .empty di atasnya. */
.empty-icon { display: inline-block; width: 15px; height: 15px; vertical-align: -3px; margin-right: 3px; }

/* ==== Kelola Grup Link Publik (manage-groups.php) ==== */
.help-text { font-size: 13px; color: var(--text-dim); line-height: 1.6; margin: 0; }
.help-text a { color: var(--blue); }
.empty-inline { color: var(--text-dim); font-style: italic; }
.member-count { color: var(--text-dim); font-size: 12px; }

/* ==== Login ==== */
/* flex-direction DINAIKKAN dari default "row" ke "column" (+ gap) supaya
   footer baru di login.php (lihat README bagian 57) tersusun DI BAWAH
   kartu form, bukan berdampingan. Aman untuk public.php/public-group.php
   yang juga pakai class body ini — keduanya cuma punya SATU flex item
   langsung (div .public-status-page, lihat komentar di bawah), dan satu
   item yang dipusatkan tetap kelihatan sama persis di row atau column
   (align-items & justify-content cuma tukar sumbu, hasil visualnya
   identik untuk 1 item). .bg-glow-fixed & .theme-toggle-btn-fixed TIDAK
   ikut terhitung sebagai flex item sama sekali karena position:fixed
   (dikeluarkan dari flow, termasuk flex layout, oleh spesifikasi CSS). */
.login-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-height: 100vh;
  padding: 16px;
}
/* public.php punya DUA kartu bertumpuk (status + grafik latensi) — dibungkus
   wrapper flex-column ini supaya tidak ikut kena "display:flex" baris
   (row) dari .login-body di atas, yang didesain untuk SATU kartu
   form login dan akan menaruh dua kartu berdampingan kalau tidak dibungkus. */
.public-status-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: 100%;
}
/* Pembungkus #chart-card + #uptime-bar-card + #incident-card di balik
   tombol "Lihat Detail" (lihat README "Fitur: Tombol Detail di Link Status
   Publik" & public.php/assets/js/public-status.js) — PERSIS aturan
   .public-status-page di atas (flex-column + gap 12px + center), supaya
   ketiga kartu di dalamnya tetap berjarak & center sama seperti waktu
   masih jadi flex-item LANGSUNG punya .public-status-page (sebelum dibungkus
   div ini, jaraknya datang dari "gap" milik .public-status-page; sekarang
   wrapper-nya sendiri butuh gap yang sama karena ketiga kartu itu jadi flex
   item punya WRAPPER ini, bukan lagi punya .public-status-page). */
.public-detail-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: 100%;
}
/* Atribut "hidden" bawaan browser ("[hidden] { display: none }" di UA
   stylesheet) kekalahan SPESIFISITASNYA SAMA (0,1,0) dengan ".public-detail-section
   { display: flex }" di atas — kalau sama-sama (0,1,0), yang menang cuma
   urutan terakhir, dan aturan author (punya kita) SELALU dianggap setelah
   UA stylesheet, jadi "display: flex" di atas akan MENIMPA "display: none"
   bawaan walau atribut hidden-nya masih terpasang di elemennya (beda dari
   #chart-card/#uptime-bar-card di atas yang classnya TIDAK pernah set
   "display" sendiri, jadi [hidden] bawaan tetap menang tanpa perlu aturan
   tambahan ini). Override eksplisit ini WAJIB supaya tombol "Lihat Detail"
   benar-benar menyembunyikan/menampilkan — tanpa ini, detail-section selalu
   kelihatan walau atribut hidden-nya ada. */
.public-detail-section[hidden] { display: none; }

/* position:relative + z-index:1 — bug & perbaikan PERSIS sama dengan
   ".card" di bawah (lihat komentar panjang & README bagian 91: "Glow
   Latar Belakang Bocor Tembus Kartu"), cuma kelewat dipasang di bagian
   91 itu karena laporannya waktu itu soal kartu ADMIN (Redaman SFP,
   class ".card"), bukan kartu LINK PUBLIK (".login-card", dipakai
   public.php/public-group.php/login.php). Akar masalahnya identik:
   ".login-card" dari awal cuma mengandalkan URUTAN DOM (muncul SETELAH
   .bg-glow-fixed di HTML) supaya otomatis tergambar di atas glow TANPA
   z-index — ternyata tidak selalu cukup di semua browser/compositor,
   terutama kelihatan di layar LEBAR (PC/desktop, laporan pengguna: link
   publik kelihatan "belang" yang ikut bergeser naik-turun waktu scroll
   — belang itu = glow .bg-glow-fixed yang bocor tembus bagian atas
   kartu, posisinya ikut geser karena var(--glow-shift) dari parallax
   bagian 52/53). Dikonfirmasi lewat Playwright + baca piksel (pola
   verifikasi sama dengan README bagian 91, diukur di ".chart-card" yang
   SENGAJA dikecualikan dari aksen ::before di atas supaya tidak
   tercampur efek lain): area atas kartu kosong terbukti rgb(32,58,81)
   meluruh ke rgb(30,41,59) murni (var(--bg-card)) di bagian kartu yang
   lebih bawah — SELISIH NYATA (glow bocor), hilang total (FLAT
   rgb(30,41,59) di semua titik ukur) setelah z-index:1 ini ditambahkan.
   Nilai 1 dipilih
   sama persis dengan .card (lihat alasan lengkap di komentar sana) —
   cukup menang lawan .bg-glow-fixed (auto), masih jauh di bawah elemen
   lain yang sudah ada z-index-nya sendiri. */
.login-card {
  position: relative;
  z-index: 1;
  background: var(--bg-card);
  border: 1px solid var(--border);
  box-shadow: var(--card-shadow);
  border-radius: 16px;
  padding: 28px 24px;
  width: 100%;
  max-width: 340px;
}
.login-card h1 {
  margin: 0 0 4px;
  font-size: 20px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.login-card .subtitle { text-align: center; color: var(--text-dim); font-size: 13px; margin-bottom: 20px; }
.login-card label { display: block; margin-bottom: 14px; font-size: 13px; color: var(--text-dim); }
.login-card input {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 15px;
}
.login-card button, .form-actions button, .inline-form button {
  background: var(--blue);
  color: var(--btn-text);
  border: none;
  padding: 10px 16px;
  border-radius: 8px;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
}
.login-card button { width: 100%; padding: 12px; }

/* ==== Manage page ==== */
/* position:relative + z-index:1 BARU ditambah di bagian 91 — AKAR MASALAH
   SEBENARNYA dari rangkaian laporan "kotak putih/gelap" bagian 87-90:
   .card TIDAK PERNAH diposisikan (tidak ada position/z-index sejak awal
   dibuat), jadi urutan tumpukannya cuma ikut urutan DOM (lihat komentar
   .bg-glow-fixed) — SEHARUSNYA cukup, TAPI dikonfirmasi lewat screenshot
   pengguna (mode gelap & terang, kartu DEKAT bagian atas halaman, masih
   dalam jangkauan 520px .bg-glow-fixed) PLUS dites ulang sendiri lewat
   Playwright (ambil screenshot asli, bukan cuma getComputedStyle — nilai
   computed-nya SAMA identik di semua sel, tapi WARNA PIKSEL SUNGGUHAN di
   area kartu yang TIDAK ditutupi elemen ber-z-index (mis. sel Status/TX,
   BUKAN kolom beku) kelihatan lebih terang/kebiruan — radial-gradient
   glow dari .bg-glow-fixed (position:fixed, z-index:auto, lapisan paling
   bawah body) TEMBUS/BOCOR ke sel-sel itu di beberapa
   browser/compositor, PADAHAL .card ada di DOM SETELAH .bg-glow-fixed
   (seharusnya otomatis di atas tanpa z-index, lihat komentar di
   .bg-glow-fixed) — kolom beku (sticky-col1, PUNYA z-index:2 eksplisit +
   position:sticky, jadi stacking context sendiri) KEBETULAN tidak kena
   bug ini sama sekali, makanya warnanya "bersih" dibanding sisa
   kartunya sendiri — itulah "kotak" yang terus dilaporkan: bukan kolom
   beku-nya yang salah, tapi SISA KARTUNYA yang kebocoran glow, kolom
   beku cuma kebetulan satu-satunya bagian yang warnanya BENAR (sesuai
   var(--bg-card) murni tanpa campuran apa pun). Perbaikannya: beri
   .card stacking context SENDIRI yang EKSPLISIT (bukan lagi
   mengandalkan urutan DOM implisit) dengan z-index pasti di atas
   .bg-glow-fixed (z-index:auto) — ini dijamin oleh spesifikasi CSS,
   tidak bergantung urutan DOM atau quirk compositor browser tertentu
   lagi. z-index:1 dipilih SENGAJA kecil (bukan asal besar) — cukup
   menang lawan .bg-glow-fixed (auto), masih jauh di bawah .topbar
   (10), .theme-toggle-btn-fixed (20), tooltip chart (5), dan
   .modal-overlay (100), jadi tidak mengganggu urutan tumpuk elemen lain
   yang sudah ada. Lihat README bagian 91 buat bukti before/after & cara
   verifikasinya. */
.card {
  position: relative;
  z-index: 1;
  background: var(--bg-card);
  border: 1px solid var(--border);
  box-shadow: var(--card-shadow);
  border-radius: 12px;
  padding: 16px;
  margin-bottom: 16px;
}
.card h2 { margin-top: 0; font-size: 16px; }
/* Modifier ".card-bare" (hapus total background/border/shadow kartu
   pembungkus tabel) SEMPAT dibuat & dipasang di bagian 89, TAPI
   DIBATALKAN lagi di bagian 90 — setelah kartu pembungkusnya dihapus,
   kolom pertama yang dibekukan (sticky-col1, WAJIB tetap punya
   background SOLID — lihat komentar di ".table-wrap.sticky-col1"
   sebelum ini) jadi satu-satunya elemen opaque di tengah tabel yang
   sudah transparan, sehingga JUSTRU makin menonjol sebagai "kotak
   putih" tersendiri — dikonfirmasi lewat tangkapan layar HP pengguna
   yang melingkari PERSIS kolom itu ("ini malah semakin jelas"). Kartu
   "card" POLOS (bukan modifier apa pun) dikembalikan di semua 7 tempat
   yang sempat diubah bagian 89 (manage.php, manage-groups.php,
   manage-routers.php, users.php, manage-interfaces.php, manage-sfp.php,
   sfp.php lewat assets/js/sfp.js) SUPAYA background kolom beku
   (var(--bg-card)) kembali SAMA PERSIS dengan background kartu di
   sekelilingnya — seamless, tidak ada tepi/kontras yang kelihatan sama
   sekali, lebih "halus" daripada opsi lain (warna mendekati tapi tidak
   identik) yang masih berpotensi sisa tepi tipis. Lihat README bagian
   90 buat riwayat lengkap & alasan tiap percobaan sebelumnya. */
/* Form tambah/edit IP di manage.php — dibungkus <details> supaya bisa
   disembunyikan (collapsed) secara default dan tidak menghalangi tabel di
   bawahnya. <summary>-nya dibuat seperti chip/pill menu di .topbar nav
   (warna & bentuk sama persis) supaya kelihatan rapi, BUKAN seperti judul
   section biasa — dan tanda segitiga bawaan browser dimatikan karena
   pill-nya sendiri sudah cukup jadi penanda "bisa diklik". */
.form-toggle { margin-top: 0; }
.form-toggle > summary {
  display: inline-block;
  list-style: none;
  cursor: pointer;
  background: rgba(56, 189, 248, 0.12);
  color: var(--blue);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  padding: 6px 14px;
  border-radius: 999px;
}
.form-toggle > summary::-webkit-details-marker,
.form-toggle > summary::marker { display: none; content: ''; }
.form-toggle > summary:hover,
.form-toggle > summary:active { background: rgba(56, 189, 248, 0.22); }
.form-toggle[open] > summary { margin-bottom: 14px; }
.form-grid label { display: block; margin-bottom: 12px; font-size: 13px; color: var(--text-dim); }
.form-grid input[type=text],
.form-grid input[type=password],
.form-grid select {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 9px 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
}
.form-grid .checkbox { display: flex; align-items: center; gap: 8px; }
.form-actions { display: flex; gap: 10px; align-items: center; }
/* Kalau .form-actions ini PERSIS nempel sesudah .table-wrap (tombol
   "Simpan Pilihan Interface" di manage-interfaces.php sejak bagian 69,
   DAN tombol "Simpan Pilihan Port SFP" di manage-sfp.php — pola yang
   sama persis, selector di bawah otomatis kena ke keduanya tanpa perlu
   diubah) — biasanya .form-actions dapat jarak atas gratis
   dari margin-bottom label terakhir di .form-grid (lihat .form-grid label di
   atas), tapi tabel TIDAK PUNYA margin-bottom seperti itu, jadi tombolnya
   kelihatan mepet/nabrak langsung ke garis bawah baris terakhir tabel kalau
   tidak diberi jarak tambahan secara eksplisit di sini. Dibuat selector
   ADJACENT SIBLING (.table-wrap + .form-actions) biar HANYA kasus ini yang
   kena, tidak mengubah .form-actions di halaman lain (manage.php,
   manage-routers.php, account.php, dst) yang sudah cukup jaraknya. */
.table-wrap + .form-actions { margin-top: 14px; }
.btn-secondary { color: var(--text-dim); text-decoration: none; font-size: 14px; }

/* Input teks biasa yang dipakai LANGSUNG di dalam sel tabel (bukan di dalam
   .form-grid) — mis. kolom Label di manage-interfaces.php — supaya tetap
   konsisten gaya gelapnya dengan input lain, bukan kotak putih bawaan browser. */
.table-input {
  width: 100%;
  min-width: 130px;
  padding: 6px 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-size: 13px;
}
/* "Kotak" abu-abu yang dilaporkan pengguna lewat tangkapan layar HP asli
   (bukan browser desktop) di kolom yang dibekukan ternyata BUKAN bug warna
   lagi (itu sudah dibereskan di bagian 87, hapus box-shadow) — dibuktikan
   lewat crop close-up tangkapan layarnya sendiri: batangnya SOLID abu-abu,
   TEBAL, posisinya PERSIS di baris paling bawah .table-wrap, lebarnya
   proporsional sama porsi tabel yang lagi kelihatan (kolom Aksi paling
   kanan masih kepotong di luar layar) — ini scrollbar bawaan Android
   Chrome buat area yang overflow-x:auto, BUKAN elemen custom bikinan CSS
   di sini sama sekali. .topbar nav (atas) & .router-filter-row (baris
   tab router) SUDAH lama dikasih "scrollbar-width: thin" biar scrollbar
   browser-nya tipis & senada (lihat komentar masing-masing), tapi
   .table-wrap (dipakai tabel Redaman SFP & Kelola IP) KELEWATAN belum
   pernah dikasih perlakuan sama sejak awal dibuat — makanya scrollbar
   BAWAAN (lebih tebal, abu-abu solid, tidak ikut tema gelap/terang) di
   sinilah yang kelihatan seperti "kotak" aneh dibanding dua area geser
   lain di app yang scrollbar-nya sudah tipis & nyaris tidak kelihatan.
   Baris di bawah ini MENYAMAKAN .table-wrap dengan pola yang sudah ada,
   bukan fitur baru. */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
/* Kolom pertama tabel "dibekukan" (tetap kelihatan) waktu tabel digeser ke
   samping di layar kecil — dipakai tabel yang kolom pertamanya jadi
   penanda utama baris (mis. Nama di manage.php), supaya tidak hilang arah
   soal baris mana yang sedang dilihat waktu geser jauh ke kanan (mis. cari
   tombol Edit/Hapus di kolom paling kanan). Opt-in lewat class
   "sticky-col1" (bukan semua .table-wrap) — belum semua tabel di app ini
   kolom pertamanya cukup berguna dibekukan. background solid wajib di sini
   (bukan transparan) supaya konten kolom lain yang sudah digeser tidak
   kelihatan "tembus" di belakang kolom yang dibekukan.
   "box-shadow" yang DULU ada di sini (sejak kolom ini pertama kali dibuat)
   SUDAH DIHAPUS — dilaporkan pengguna langsung lewat tangkapan layar
   halaman Redaman SFP: shadow itu menimpa bagian tepi kolom SEBELAHNYA
   (bukan kolom yang dibekukan sendiri), membuat kolom beku jadi terlihat
   seperti "kotak putih" yang warnanya beda dari sisa barisnya — padahal
   SEBENARNYA sama persis var(--bg-card) di kedua sisi, cuma shadow-nya
   yang menggelapkan tepi kolom sebelah sampai kelihatan seperti beda
   warna. Dikonfirmasi langsung lewat inspeksi computed style + screenshot
   close-up sebelum diperbaiki — tanpa box-shadow ini, kolom beku tetap
   berfungsi (tetap "position: sticky" + background solid), cuma tidak
   lagi punya aksen bayangan dekoratif waktu digeser. */
.table-wrap.sticky-col1 table th:first-child,
.table-wrap.sticky-col1 table td:first-child {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--bg-card);
}
/* Kolom pertama (Nama di manage.php, Port di sfp.php sejak bagian 84 —
   dua tabel yang pakai sticky-col1 saat ini) SENGAJA dikecualikan dari
   "white-space: nowrap"
   global th/td di bawah — nama pelanggan yang panjang (mis. "PTP Site IQ
   Service kom ( Kandangmas, Kudus)") membuat kolom yang "dibekukan" ini
   ikut melebar mengikuti satu baris teksnya, mendorong kolom-kolom lain
   (termasuk kolom Link Publik/Aksi paling kanan) jadi perlu digeser jauh
   lebih dalam sebelum terlihat sama sekali — dikeluhkan pengguna langsung
   lewat tangkapan layar (tombol "Salin Link"/"Buat Link" ikut terpotong
   di tepi layar HP). max-width + word-break di sini membuat nama panjang
   TURUN ke baris berikutnya di DALAM kolom yang sama (bukan melebarkan
   kolomnya), jadi lebar kolom ini tetap wajar berapa pun panjang namanya
   — kolom lain jadi lebih cepat terlihat tanpa perlu geser sejauh
   sebelumnya, apa pun nama IP-nya.
*/
.table-wrap.sticky-col1 table th:first-child,
.table-wrap.sticky-col1 table td:first-child {
  white-space: normal;
  word-break: break-word;
  min-width: 110px; /* tanpa ini, table-layout:auto bisa menyusutkan kolom ini sampai SATU HURUF per baris (lebar minimumnya ikut word-break) — lihat hasil uji di README */
  max-width: 150px;
}
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--border); white-space: nowrap; }
th { color: var(--text-dim); font-weight: 600; }
td code { font-size: 12px; }
.actions { display: flex; gap: 10px; }
.actions a { color: var(--blue); text-decoration: none; }
/* Selektor ".inline-form .link-danger" (2 class) SENGAJA dipakai, bukan
   ".link-danger" saja — tombol "Cabut"/"Hapus" di manage.php,
   manage-routers.php & manage-groups.php semuanya ada di dalam
   <form class="inline-form">, dan aturan ".inline-form button { background:
   var(--blue); ... }" di atas spesifisitasnya (1 class + 1 elemen) lebih
   tinggi dari class tunggal — tanpa ini, tombolnya akan tetap kelihatan
   kartu biru solid, bukan link teks merah tipis seperti seharusnya (sama
   persis gotcha-nya dengan ".public-status-card .detail-toggle-btn" di
   bagian Link Status Publik). */
.inline-form .link-danger { background: none; border: none; color: var(--red); padding: 0; font-size: 13px; cursor: pointer; }
.inline-form { display: inline; }
.pill-on.pill { background: rgba(34,197,94,0.15); color: var(--green); border: none; }
.pill-off.pill { background: rgba(239,68,68,0.12); color: var(--red); border: none; }

.app-footer {
  max-width: 720px;
  margin: 18px auto 0;
  padding: 14px 16px calc(14px + env(safe-area-inset-bottom));
  text-align: center;
  border-top: 1px solid var(--border);
}
.live-clock {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.footer-copy {
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-dim);
}

/* ==== Link Status Publik (manage.php) ==== */
/* Class ganda ".pill-neutral.pill" sengaja ditulis supaya spesifisitasnya
   menyamai ".pill-on.pill"/".pill-off.pill" di atas — kalau cuma ".pill-neutral"
   (satu class), aturan ".pill-off.pill" yang lebih spesifik bisa menang duluan
   kalau suatu saat tombol ini ikut dikasih class pill-off secara tidak sengaja. */
.pill-neutral.pill { background: rgba(148,163,184,0.15); color: var(--text-dim); border: none; }
.public-link-cell { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.btn-copy-link { font-family: inherit; }

/* ==== Halaman Status Publik (public.php) ==== */
.public-status-card { text-align: center; max-width: 420px; }
.public-status-card .app-logo-lg { margin: 0 auto 14px; display: block; }
.pub-name { margin: 0 0 10px; font-size: 19px; }
.pub-status-badge {
  display: inline-block;
  padding: 6px 16px;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.5px;
  margin: 6px 0 10px;
}
.pub-status-badge.status-up { background: rgba(34,197,94,0.15); color: var(--green); }
.pub-status-badge.status-down { background: rgba(239,68,68,0.15); color: var(--red); }
.pub-status-badge.status-unknown { background: rgba(148,163,184,0.15); color: var(--text-dim); }
.pub-metric { font-size: 14px; color: var(--text); margin-top: 6px; }
.pub-metric strong { color: var(--blue); }
.pub-stale-warning {
  margin-top: 12px;
  font-size: 12px;
  color: var(--alert-error-text);
  background: rgba(239,68,68,0.1);
  border: 1px solid rgba(239,68,68,0.3);
  border-radius: 8px;
  padding: 8px 10px;
}

/* Aksen warna di tepi ATAS kartu status = kelihatan sekilas tanpa perlu baca
   teks dulu (pola umum di status page profesional ala statuspage.io). Pakai
   border-top TEBAL + glow radial tipis di baliknya biar tidak terasa cuma
   garis biasa. SENGAJA di-exclude dari #chart-card lewat ":not(.chart-card)"
   — kartu itu juga pakai class .public-status-card (lihat public.php), tapi
   aksen status cuma relevan buat kartu status di atasnya, bukan kartu grafik. */
.public-status-card:not(.chart-card) {
  position: relative;
  overflow: hidden;
  border-top: 4px solid var(--green);
  padding-top: 26px;
}
.public-status-card:not(.chart-card).status-down { border-top-color: var(--red); }
.public-status-card:not(.chart-card).status-unknown { border-top-color: var(--text-dim); }
.public-status-card:not(.chart-card)::before {
  content: '';
  position: absolute;
  top: -40px; left: 50%; transform: translateX(-50%);
  width: 260px; height: 160px;
  background: radial-gradient(ellipse, rgba(34,197,94,0.16), transparent 72%);
  pointer-events: none;
}
.public-status-card:not(.chart-card).status-down::before {
  background: radial-gradient(ellipse, rgba(239,68,68,0.16), transparent 72%);
}
.public-status-card:not(.chart-card).status-unknown::before {
  background: radial-gradient(ellipse, rgba(148,163,184,0.14), transparent 72%);
}
.public-status-card:not(.chart-card) .checked-at { margin-top: 14px; }

/* Tombol "Lihat Detail"/"Sembunyikan Detail" — lihat komentar
   .public-detail-section di atas & render()/toggleDetail() di
   assets/js/public-status.js. Selektor 2-class ".public-status-card
   .detail-toggle-btn" SENGAJA dipakai (bukan ".detail-toggle-btn" saja),
   sama gotcha-nya dengan ".inline-form .link-danger" di bagian Kelola —
   tanpa ini, aturan umum ".login-card button { background:
   var(--blue); width: 100%; ...}" (1 class + 1 elemen) menang lawan class
   tunggal dan tombolnya akan kelihatan jadi CTA biru selebar kartu, bukan
   pil kecil di tengah. Tombol ini SEKUNDER (bukan ajakan utama seperti
   tombol Login), makanya gayanya pil transparan tipis, bukan solid biru. */
.public-status-card .detail-toggle-btn {
  display: flex;
  width: fit-content;
  align-items: center;
  gap: 6px;
  margin: 14px auto 0;
  padding: 7px 16px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-dim);
  font-family: inherit;
  font-weight: 600;
  font-size: 12px;
  cursor: pointer;
}
.public-status-card .detail-toggle-btn:hover,
.public-status-card .detail-toggle-btn.active {
  color: var(--text);
  border-color: var(--text-dim);
}
.detail-toggle-chevron {
  width: 14px;
  height: 14px;
  transition: transform 0.15s ease;
}
.detail-toggle-btn.active .detail-toggle-chevron { transform: rotate(180deg); }

/* Titik kecil berkedip di sebelah teks badge ONLINE/DOWN — sinyal visual
   "data ini hidup/live", bukan cuma status statis. Dipakai juga di detail
   per-site halaman Grup Link Publik (lihat assets/js/public-group.js). */
.pub-live-dot {
  display: inline-block;
  vertical-align: middle;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: currentColor;
  margin-right: 7px;
  animation: live-pulse-ring 1.8s ease-in-out infinite;
}
@keyframes live-pulse-ring {
  0%, 100% { box-shadow: 0 0 0 0 currentColor; opacity: 1; }
  50% { box-shadow: 0 0 0 5px transparent; opacity: 0.7; }
}

/* Baris kotak statistik (Latensi/Kapasitas/Uptime) menggantikan baris teks
   bertumpuk ".pub-metric"/".ip-meta" yang lama — dipakai juga di detail
   per-site halaman Grup Link Publik lewat skeleton .gd-stat-* yang sudah
   ditoggle per kotak (lihat assets/js/public-group.js: rowDetailTemplate()). */
.pub-stat-row { display: flex; gap: 8px; margin: 18px 0 2px; }
.pub-stat-box {
  flex: 1;
  background: var(--stat-box-bg);
  border: 1px solid var(--stat-box-border);
  border-radius: 10px;
  padding: 10px 4px;
}
.pub-stat-box .psb-label {
  display: block;
  font-size: 10px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: .03em;
  margin-bottom: 4px;
}
.pub-stat-box .psb-value { display: block; font-size: 16px; font-weight: var(--value-weight); color: var(--text); }
.pub-stat-box .psb-value.accent-blue { color: var(--blue); }
.pub-stat-box .psb-value.accent-green { color: var(--green); }
.pub-stat-box .psb-value.accent-red { color: var(--red); }

/* ==== Grafik Latensi + Grafik Trafik, satu kartu dengan tab (public.php) ====
   Digabung jadi satu kartu (bukan 2 kartu terpisah seperti sebelumnya) supaya
   lebih muat di satu layar HP — lihat assets/js/public-status.js:
   initChartTabs(). */
.chart-card {
  margin: 12px auto 0;
  text-align: left;
  /* max-width SENGAJA tidak ditimpa lagi di sini — biar sama persis dengan
     kartu status di atasnya (.public-status-card), supaya lebar kedua kartu
     konsisten (sebelumnya beda: 340px vs 480px, bikin tampilan jadi aneh). */
}
/* Dibungkus jadi "segmented control" (track abu-abu + tombol aktif timbul
   di dalamnya), bukan dua tombol bertepi terpisah seperti sebelumnya —
   kesannya lebih seperti kontrol switch yang dipakai aplikasi modern,
   bukan dua tombol lepas yang kebetulan bersebelahan. */
.chart-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 14px;
  /* var(--stat-box-bg)/var(--stat-box-border), BUKAN rgba(148,163,184,0.08)
     + var(--border) langsung — kalau dipasang mentah, di mode terang wash
     setipis itu nyaris tidak kelihatan di atas kartu putih, jadi yang
     mendefinisikan track ini cuma border 1px keras (persis gejala "Windows
     XP high contrast" yang sudah ditemukan & diperbaiki untuk .pub-stat-box
     di README bagian 47 — class ini tidak sempat ikut diperiksa waktu itu
     karena baru dipakai di halaman publik yang belum masuk scope mode
     terang). */
  background: var(--stat-box-bg);
  border: 1px solid var(--stat-box-border);
  border-radius: 10px;
  padding: 4px;
}
/* PENTING: atribut "hidden" biasanya mengandalkan aturan bawaan browser
   "[hidden] { display: none }", tapi aturan ".chart-tabs { display: flex }"
   di atas sama spesifisitasnya dan datang belakangan, jadi MENIMPA itu —
   tanpa baris ini, tombol tab akan tetap kelihatan walau elemen-nya diberi
   hidden=true oleh JS (lihat pollSeries() di public-status.js). */
.chart-tabs[hidden] { display: none; }
/* Selector SENGAJA ".chart-tabs .chart-tab-btn" (bukan cuma ".chart-tab-btn"
   saja) — bug lama yang baru ketahuan waktu halaman publik ini pertama kali
   benar-benar diperiksa visual (dulu tidak pernah di-screenshot/diperiksa
   pixel-per-pixel sebelum fitur mode terang/gelap diperluas ke sini, lihat
   README bagian 50): tombol tab yang TIDAK aktif (mis. "Trafik" saat
   "Latensi" sedang aktif) kelihatan terisi penuh warna biru solid alih-alih
   transparan — soalnya kartu pembungkusnya (.chart-card) juga pakai class
   .login-card (dipakai bareng kartu status & form login), dan aturan umum
   ".login-card button { background: var(--blue); color: var(--btn-text); }"
   (buat tombol submit login) spesifisitasnya (0,1,1) LEBIH TINGGI dari
   ".chart-tab-btn" polos (0,1,0), jadi menang duluan untuk tombol tab yang
   TIDAK punya class tambahan ".active" (yang spesifisitasnya baru menang di
   0,2,0). Dengan discope ke ".chart-tabs .chart-tab-btn" (0,2,0), aturan ini
   sekarang SAMA ATAU LEBIH spesifik dari aturan ".login-card button", dan
   karena datang belakangan di file ini, menang seperti seharusnya — tombol
   tab tidak aktif kembali transparan seperti desain aslinya. */
.chart-tabs .chart-tab-btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px 10px;
  border-radius: 7px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.chart-tab-btn .tab-icon { width: 15px; height: 15px; flex-shrink: 0; }
.chart-tab-btn.active {
  background: var(--bg-card);
  color: var(--blue);
  box-shadow: 0 1px 4px rgba(0,0,0,0.3);
}
/* Filter dropdown (bagian 124) — GANTI TOTAL baris chip yang bisa digeser
   ke samping (overflow-x: auto, dipakai sejak bagian 72/98/99) dengan
   SATU tombol pemicu + panel mengambang vertikal, mirip menu drawer
   (bagian 118) tapi lebih kecil & tanpa backdrop gelap. Alasan: baris
   geser lama kelihatan "telanjang" (scrollbar bawaan browser nongol) &
   sebagian pilihan selalu tersembunyi di luar layar begitu grup/router-nya
   banyak (laporan pengguna, tangkapan layar Dashboard dengan 5+ grup POP)
   — makin banyak grup/router makin parah, beda dari menu nav yang jumlah
   itemnya tetap (makanya solusinya juga sama: pola drawer, bukan chip).
   Dipakai di 5 halaman: Dashboard & Riwayat (filter grup lokasi/POP, PUNYA
   opsi "Semua"), Trafik Interface/Kesehatan Router/Redaman SFP (filter
   router, TIDAK PUNYA "Semua" — selalu tepat satu router aktif, lihat
   komentar activeRouterId di masing-masing JS-nya). Markup per halaman:

     <div class="filter-dropdown" id="xxx-filter-wrap" hidden>
       <button type="button" class="filter-dropdown-trigger" ...>
         <svg class="filter-dropdown-icon" aria-hidden="true">...</svg>
         <span class="filter-dropdown-trigger-label">Semua</span>
         <svg class="filter-dropdown-chevron" aria-hidden="true">...</svg>
       </button>
       <div id="xxx-filter-row" class="filter-dropdown-panel">...</div>
     </div>

   `hidden` di WRAPPER (bukan lagi di panelnya) yang sekarang jadi
   satu-satunya penanda "fitur ini tidak relevan sama sekali" (mis. cuma
   1 POP/1 router, tidak ada gunanya memfilter) — begitu wrapper hidden,
   SELURUH isinya (tombol pemicu & panel) otomatis ikut tidak tergambar
   (attribut hidden di elemen induk membungkus seluruh anak-anaknya, tidak
   peduli class apa pun di anak-anaknya) — dashboard.js/iface-traffic.js/
   health.js/sfp.js CUMA perlu dialihkan targetnya (dari elemen panel ke
   elemen wrapper) buat baris `xxxEl.hidden = true/false` yang SUDAH ADA,
   TIDAK ADA logika baru yang ditambah di sana. Buka/tutup panel &
   sinkronisasi label tombol pemicu (teks pilihan yang sedang aktif)
   ditangani SATU file baru generik assets/js/filter-dropdown.js, dipakai
   bareng di semua 5 halaman — isi chip & logika filter sendiri di
   masing-masing *.js/history-filter.js SAMA SEKALI TIDAK disentuh (masih
   delegasi klik + toggle class "active" seperti sebelumnya, filter-
   dropdown.js cuma "menonton" dari luar lewat MutationObserver). */
.filter-dropdown {
  position: relative;
  display: inline-block;
  margin-bottom: 14px;
}
.filter-dropdown[hidden] { display: none; }
.filter-dropdown-trigger {
  display: flex;
  align-items: center;
  gap: 7px;
  font: inherit;
  padding: 8px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-card);
  box-shadow: var(--card-shadow);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.filter-dropdown-trigger:hover { border-color: var(--blue); }
.filter-dropdown-icon,
.filter-dropdown-chevron { width: 15px; height: 15px; flex-shrink: 0; color: var(--text-dim); }
.filter-dropdown-chevron { transition: transform 0.15s ease; }
.filter-dropdown-trigger[aria-expanded="true"] .filter-dropdown-chevron { transform: rotate(180deg); }
.filter-dropdown-trigger-label {
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Panel-nya SENGAJA position:absolute (bukan ikut alur dokumen biasa) —
   supaya daftar/kartu di BAWAH baris filter ini tidak ikut "terdorong"
   turun-naik tiap kali panel dibuka/ditutup, pola standar dropdown di
   mana pun. display:none default (TERTUTUP) diganti flex oleh class
   "open" (ditambah/dihapus assets/js/filter-dropdown.js) — SENGAJA bukan
   atribut `hidden` (itu sudah dipakai WRAPPER buat arti lain, lihat
   komentar panjang di atas). */
.filter-dropdown-panel {
  display: none;
  flex-direction: column;
  gap: 2px;
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 40;
  min-width: 220px;
  max-width: min(320px, calc(100vw - 32px));
  max-height: 55vh;
  overflow-y: auto;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--card-shadow);
  padding: 6px;
}
.filter-dropdown-panel.open { display: flex; }
/* Tiap pilihan di dalam panel — baris penuh selebar panel (bukan pil
   sependek teksnya seperti dulu), gaya "daftar vertikal" disamakan PERSIS
   dengan .nav-drawer-link (bagian 118/119) — lihat .nav-drawer-link:active
   & .nav-drawer-link-active — supaya dropdown ini terasa satu keluarga
   visual dengan menu drawer, bukan komponen asing. */
.filter-dropdown-panel .router-filter-chip {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  padding: 9px 12px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  white-space: normal;
}
.filter-dropdown-panel .router-filter-chip:active { background: rgba(56, 189, 248, 0.12); }
.filter-dropdown-panel .router-filter-chip.active {
  color: var(--blue);
  font-weight: 700;
  background: rgba(56, 189, 248, 0.08);
}

/* ========================================================================
   bagian 101 (opsi "Halus & Rapi") — Pemolesan Desain konservatif
   ========================================================================
   Arah alternatif dari opsi "Futuristik" — tujuannya tetap bersih &
   profesional seperti sekarang, proporsi/kontras/tipografi dirapikan &
   ditajamkan sedikit, elevasi kartu dibuat konsisten di kedua mode, TAPI
   TIDAK ADA efek mencolok/baru secara warna: tidak ada glow warna-warni
   di latar/kartu, tidak ada teks gradient, tidak ada animasi berdenyut.
   Sama seperti opsi Futuristik, aturan di bawah ini otomatis ikut
   terlihat di bgp.php/health.php/iface-traffic.php (berbagi class CSS
   yang sama — lihat komentar panjang di .router-filter-card di atas),
   dan .login-card SENGAJA dikecualikan lewat
   :not(.public-status-card):not(.public-group-card) supaya halaman
   publik pelanggan (public.php/public-group.php) tidak ikut berubah —
   lihat alasan detail pola pengecualian ini di versi "Futuristik". */

.summary-box, .ip-card, .filter-dropdown-trigger, .filter-dropdown-panel, .login-card:not(.public-status-card):not(.public-group-card), .card {
  border-radius: 14px;
  box-shadow:
    0 1px 2px rgba(0,0,0,0.24),
    0 10px 24px rgba(2,6,23,0.28),
    inset 0 1px 0 rgba(255,255,255,0.03);
}
/* Varian terang ikon kaca pembesar .search-input (bagian 122) — lihat
   komentar panjang di definisi .search-input kenapa warnanya harus
   diulang manual di sini, bukan otomatis ikut var(--text-dim). */
:root[data-theme="light"] .search-input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
}

:root[data-theme="light"] .summary-box,
:root[data-theme="light"] .ip-card,
:root[data-theme="light"] .filter-dropdown-trigger,
:root[data-theme="light"] .filter-dropdown-panel,
:root[data-theme="light"] .login-card:not(.public-status-card):not(.public-group-card),
:root[data-theme="light"] .card {
  box-shadow:
    0 1px 3px rgba(15,23,42,0.06),
    0 10px 22px rgba(15,23,42,0.08),
    inset 0 1px 0 rgba(255,255,255,0.6);
}

/* Angka ringkasan: tabular-nums (digit sejajar rapi tiap kali berubah),
   sedikit lebih besar & lebih tegas supaya jadi fokus mata pertama. TIDAK
   ada gradient teks di sini (beda dari opsi Futuristik) — angka Total
   tetap warna teks biasa var(--text), konsisten dengan Online/Down yang
   solid hijau/merah. */
.summary-box span {
  font-variant-numeric: tabular-nums;
  font-size: 27px;
  font-weight: 800;
  letter-spacing: -0.01em;
}
.summary-box small {
  text-transform: uppercase;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
}

/* Badge status jadi pil kecil berlatar tipis + titik indikator, bukan
   cuma teks berwarna polos — lebih gampang dikenali sekilas, konsisten
   dengan bahasa "pill" yang sudah dipakai di .pill/.router-filter-chip.
   TIDAK ada box-shadow glow/animasi berdenyut (beda dari opsi
   Futuristik) — warna latar pil jauh lebih redup, murni bentuk, bukan
   efek cahaya. */
.status-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px 3px 8px;
  border-radius: 999px;
  font-size: 11px;
}
.status-badge::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.status-up .status-badge { background: rgba(34,197,94,0.13); }
.status-down .status-badge { background: rgba(239,68,68,0.13); }
.status-unknown .status-badge { background: rgba(148,163,184,0.12); }

/* Data numerik (IP, latency, jam) pakai tabular-nums supaya kolomnya
   tidak "bergoyang" tiap kali angkanya berubah di putaran poll berikutnya. */
.ip-addr, .latency, .latency-range, .loss-pct, .checked-at, .live-clock {
  font-variant-numeric: tabular-nums;
}
.ip-name { letter-spacing: -0.005em; }

/* Garis bawah tipis polos (BUKAN gradient, beda dari opsi Futuristik) di
   header grup — pemisah antar-POP/grup yang lebih jelas dibanding cuma
   margin/teks kecil saja. */
.group-header {
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}

/* Tombol aktif dapat sedikit gradasi SATU keluarga warna biru saja (bukan
   warna baru/ungu seperti opsi Futuristik) — terasa lebih "dibuat", tetap
   konsisten dengan var(--blue). Sudah dicek, halaman publik pakai class
   tombol lain, aman tanpa pengecualian tambahan. (".router-filter-chip.
   active" dulu ikut di sini juga sebelum bagian 124 — sekarang warna
   aktifnya diatur sendiri lewat ".filter-dropdown-panel .router-filter-
   chip.active", gaya highlight tipis seperti .nav-drawer-link-active,
   BUKAN lagi gradasi pekat ini — lihat komentar di sana.) */
.login-card button, .form-actions button, .inline-form button {
  background: linear-gradient(135deg, var(--blue), #0ea5e9);
}

/* Judul per-grafik ("📈 Grafik Latensi"/"📶 Grafik Trafik") disembunyikan
   kalau tab lagi aktif (dua grafik tersedia) — tombol tab-nya sendiri sudah
   cukup jelas menunjukkan grafik mana yang sedang tampil, jadi judul ganda
   di dalamnya cuma makan tempat percuma. Kalau cuma 1 grafik yang tersedia
   (tab disembunyikan, lihat JS), judulnya tetap tampil seperti biasa. */
.chart-card.tabs-active .chart-title-standalone { display: none; }
.pub-chart-title {
  margin: 0 0 6px;
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  text-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
}
.pub-chart-title .tab-icon { width: 15px; height: 15px; color: var(--blue); flex-shrink: 0; }
/* Subjudul rentang waktu ("24 jam terakhir") ditampilkan sebagai chip kecil,
   bukan teks polos — biar tidak kalah "menyatu" dengan kontrol tab segmented
   di atasnya yang juga punya nuansa pill/chip. */
.pub-chart-subtitle {
  display: block;
  width: fit-content;
  margin: 0 auto 12px;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--text-dim);
  /* Sama seperti .chart-tabs di atas — var(--stat-box-bg/border), bukan
     rgba mentah, supaya chip ini tidak "menghilang" di mode terang. */
  background: var(--stat-box-bg);
  border: 1px solid var(--stat-box-border);
  border-radius: 999px;
  padding: 3px 10px;
  text-align: center;
}
.latency-chart-wrap { position: relative; width: 100%; }
.latency-svg { display: block; overflow: visible; }
.latency-gridline { stroke: var(--border); stroke-width: 1; }
.latency-axis-label {
  fill: var(--text-dim);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}
.latency-down-band { fill: var(--down-band); }
.latency-area { fill: var(--blue); opacity: 0.12; stroke: none; }
.latency-line { fill: none; stroke: var(--blue); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.latency-end-dot { fill: var(--blue); stroke: var(--bg-card); stroke-width: 2; }
.latency-end-label { fill: var(--blue); font-size: 11px; font-weight: 700; }
.latency-crosshair { stroke: var(--text-dim); stroke-width: 1; }
.latency-crosshair-dot { fill: var(--blue); stroke: var(--bg-card); stroke-width: 2; }
.latency-hit-area { cursor: crosshair; }
.latency-tooltip {
  position: absolute;
  transform: translateX(-50%);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px 9px;
  font-size: 12px;
  color: var(--text);
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 4px 12px rgba(0,0,0,0.35);
  z-index: 5;
}
.latency-tooltip-time { color: var(--text-dim); margin-right: 6px; }
.latency-tooltip-down { color: var(--red); }
.latency-empty { color: var(--text-dim); font-size: 12px; text-align: center; padding: 18px 0; }
.latency-chart-caption {
  display: flex;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 8px;
  font-size: 11px;
  color: var(--text-dim);
}
.latency-chart-caption .key { display: inline-flex; align-items: center; gap: 5px; }
.latency-chart-caption .key-line { display: inline-block; width: 14px; height: 2px; background: var(--blue); border-radius: 1px; }
.latency-chart-caption .key-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; background: rgba(239,68,68,0.5); }

/* ==== Grafik Trafik Download/Upload (public.php) ====
   Kartu pembungkusnya sudah dipakaikan .chart-card bareng grafik latensi di
   atas (lihat blok "Grafik Latensi + Grafik Trafik" di atas) — di sini
   tinggal style elemen-elemen di DALAM grafiknya. */
.traffic-chart-wrap { position: relative; width: 100%; }
.traffic-svg { display: block; overflow: visible; }
.traffic-gridline { stroke: var(--border); stroke-width: 1; }
.traffic-axis-label {
  fill: var(--text-dim);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}
/* Dua garis (bukan garis+area seperti grafik latensi) — dua area yang
   ditumpuk saling menutupi dan malah bikin lebih susah dibaca mana rx mana
   tx, jadi di sini cukup garis + titik akhir yang diberi warna beda. */
.traffic-line-rx { fill: none; stroke: var(--traffic-rx); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.traffic-line-tx { fill: none; stroke: var(--traffic-tx); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.traffic-end-dot-rx { fill: var(--traffic-rx); stroke: var(--bg-card); stroke-width: 2; }
.traffic-end-dot-tx { fill: var(--traffic-tx); stroke: var(--bg-card); stroke-width: 2; }
.traffic-end-label-rx { fill: var(--traffic-rx); font-size: 11px; font-weight: 700; }
.traffic-end-label-tx { fill: var(--traffic-tx); font-size: 11px; font-weight: 700; }
.traffic-crosshair { stroke: var(--text-dim); stroke-width: 1; }
.traffic-crosshair-dot-rx { fill: var(--traffic-rx); stroke: var(--bg-card); stroke-width: 2; }
.traffic-crosshair-dot-tx { fill: var(--traffic-tx); stroke: var(--bg-card); stroke-width: 2; }
.traffic-hit-area { cursor: crosshair; }
.traffic-tooltip {
  position: absolute;
  transform: translateX(-50%);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px 9px;
  font-size: 12px;
  color: var(--text);
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 4px 12px rgba(0,0,0,0.35);
  z-index: 5;
}
.traffic-tooltip-time { color: var(--text-dim); margin-right: 6px; display: block; }
.traffic-tooltip-rx { color: var(--traffic-rx); }
.traffic-tooltip-tx { color: var(--traffic-tx); margin-left: 8px; }
.traffic-empty { color: var(--text-dim); font-size: 12px; text-align: center; padding: 18px 0; }

/* Baris "Rata-rata"/"Puncak" di dalam kartu Grafik Trafik — angka SAAT INI
   sengaja tidak diulang di sini, sudah ada di titik ujung garis pada grafik
   di atasnya (lihat assets/js/traffic-chart.js: endMarker()). Diisi JS
   (assets/js/public-status.js: renderTrafficStats()), kosong/disembunyikan
   kalau belum ada histori sama sekali. */
/* Tiap baris (Rata-rata/Puncak) jadi kotak tile sendiri — sama bahasa
   visualnya dengan .pub-stat-box di kartu status, supaya seluruh halaman
   kelihatan satu sistem desain, bukan dua gaya kotak yang beda. */
.traffic-chart-stats {
  display: flex;
  gap: 8px;
  margin-top: 14px;
  font-size: 12px;
}
.tcs-row {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  /* Sama seperti .chart-tabs/.pub-chart-subtitle di atas. */
  background: var(--stat-box-bg);
  border: 1px solid var(--stat-box-border);
  border-radius: 10px;
  padding: 8px 6px;
}
.tcs-label {
  color: var(--text-dim);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .03em;
}
.tcs-rx, .tcs-tx { font-weight: 700; font-size: 12px; }
.tcs-rx { color: var(--traffic-rx); }
.tcs-tx { color: var(--traffic-tx); }

/* bagian 125 — tile "Saat Ini" (trafik LIVE, bukan agregat) ditaruh paling
   kiri di antara tile Rata-rata/Puncak, dibikin sedikit menonjol (border
   biru + titik berdenyut reuse .pub-live-dot dari badge ONLINE/DOWN) supaya
   gampang dibedakan sekilas dari dua tile lain yang sifatnya ringkasan
   jendela waktu. Munculnya cuma waktu item.rx_bps/tx_bps ada isinya (lihat
   renderTrafficStats() di public-status.js & renderTrafficStatsBlock() di
   public-group.js) — otomatis tersembunyi di sub-tab "30 Hari" karena objek
   traffic_daily_stats memang tidak punya field rx_bps/tx_bps. */
.tcs-row-current {
  border-color: var(--blue);
}
.tcs-row-current .tcs-label {
  color: var(--blue);
  display: flex;
  align-items: center;
  justify-content: center;
}

.traffic-chart-caption {
  display: flex;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 8px;
  font-size: 11px;
  color: var(--text-dim);
}
.traffic-chart-caption .key { display: inline-flex; align-items: center; gap: 5px; }
.traffic-chart-caption .key-line-rx { display: inline-block; width: 14px; height: 2px; background: var(--traffic-rx); border-radius: 1px; }
.traffic-chart-caption .key-line-tx { display: inline-block; width: 14px; height: 2px; background: var(--traffic-tx); border-radius: 1px; }
/* bagian 108 — catatan penjelasan di bawah Grafik Trafik (public.php &
   kartu Link Grup di public-group.js): dipasang var(--stat-box-bg)/
   var(--stat-box-border) seperti .pub-chart-subtitle di atas (BUKAN rgba
   mentah) supaya kotaknya tidak "menghilang" di mode terang — alasan
   persis sama seperti kenapa .router-filter-card dipakai di bagian 100/107,
   lihat README. */
.traffic-chart-note {
  margin-top: 10px;
  padding: 8px 12px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-dim);
  text-align: center;
  background: var(--stat-box-bg);
  border: 1px solid var(--stat-box-border);
  border-radius: 8px;
}
/* bagian 109 — tombol penunjuk visual di dalam .traffic-chart-note:
   laporan pengguna, kalimat "badge di atas" di catatan bagian 108 masih
   ambigu buat pengunjung awam (badge yang mana?) — tombol ini men-scroll
   HALAMAN ke badge status (.pub-status-badge di public.php / .sc-badge di
   public-group.js, lihat jumpToStatusBadge()/listener di bindDetailTabs())
   lalu menyalakan cincin berdenyut di badge itu sendiri, jadi "yang mana"
   terjawab lewat AKSI, bukan cuma kata-kata. Dibuat seperti pil kecil
   inline di dalam kalimat (bukan tombol CTA penuh/biru) — ini cuma
   pelengkap catatan, bukan ajakan utama.
   SELEKTOR 2-CLASS (".traffic-chart-note .traffic-note-jump-btn", BUKAN
   ".traffic-note-jump-btn" saja) — gotcha YANG SAMA PERSIS dengan
   ".public-status-card .detail-toggle-btn" di atas: tanpa ini, aturan umum
   ".login-card button{background:var(--blue);width:100%;...}" (1 class + 1
   elemen) menang lawan class tunggal, dan tombolnya akan jadi CTA biru
   SELEBAR KARTU alih-alih pil kecil inline seperti yang dimaksud. */
.traffic-chart-note .traffic-note-jump-btn {
  display: inline-flex;
  align-items: center;
  width: auto;
  gap: 3px;
  margin-left: 2px;
  padding: 2px 9px;
  background: transparent;
  border: 1px solid var(--stat-box-border);
  border-radius: 999px;
  color: var(--blue);
  font-family: inherit;
  font-weight: 600;
  font-size: 11px;
  line-height: 1.6;
  cursor: pointer;
  white-space: nowrap;
  vertical-align: middle;
}
.traffic-chart-note .traffic-note-jump-btn:hover { border-color: var(--blue); }

/* Cincin berdenyut yang disalakan SEMENTARA (lihat JS) di badge status
   (.pub-status-badge / .sc-badge) waktu .traffic-note-jump-btn diklik —
   pola IDENTIK dengan @keyframes live-pulse-ring di atas (box-shadow dari
   currentColor→transparent, warnanya otomatis ikut currentColor badge
   yang SEDANG aktif: hijau/merah/abu-abu — TANPA perlu tahu status
   apa pun di CSS ini), cuma radiusnya dibuat jauh lebih besar (14px,
   bukan 5px) & 5 kali putaran (bagian 117 — sebelumnya 2 kali, dinaikkan
   atas permintaan supaya "sorotan sesaat" ini lebih jelas kelihatan dan
   tidak keburu hilang sebelum sempat diperhatikan) supaya jelas kelihatan
   sebagai "sorotan sesaat", bukan sekadar live-dot kecil yang gampang
   terlewat. */
@keyframes badge-attention-ring {
  0% { box-shadow: 0 0 0 0 currentColor; }
  100% { box-shadow: 0 0 0 14px transparent; }
}
.pub-status-badge.badge-attention-pulse {
  animation: badge-attention-ring 0.8s ease-out 5;
}

/* ==== Halaman Grup Link Publik (public-group.php) ====
   REDESIGN: tiap site SEKARANG kartu status TERPISAH (login-card sendiri,
   persis gaya kartu status public.php — badge, down-since, baris
   Latensi/Kapasitas/Uptime langsung kelihatan TANPA tap), bukan baris
   ringkas di dalam satu kartu besar seperti sebelumnya (.group-row/
   .group-row-btn/.group-row-detail versi lama, lihat README bagian
   redesign "kartu per site" di halaman grup untuk riwayatnya). Grafik
   Latensi/Trafik + Grafik Uptime Harian + Riwayat Gangguan tiap site
   digabung di balik SATU tombol "Lihat Detail" per kartu — REUSE TOTAL
   markup & CSS kartu status/tombol detail/chart-card dari public.php
   (.public-status-card, .detail-toggle-btn, .public-detail-section, dst,
   lihat aturan masing-masing di bagian lain file ini) — TIDAK ada style
   baru buat elemen-elemen itu di sini, cuma dua aturan tata-letak di bawah
   buat membariskan kartu-kartu itu, lihat assets/js/public-group.js
   (buildSiteCards()). */
.public-group-card { max-width: 480px; text-align: center; }
.public-group-card .app-logo-lg { margin: 0 auto 10px; display: block; }
/* Daftar seluruh kartu site — flex-column + gap SAMA pola dengan
   .public-status-page/.public-detail-section di atas (bukan lagi
   "text-align:left" baris bertumpuk versi lama), supaya tiap site-card-group
   di dalamnya (lihat di bawah) berjejer vertikal dengan jarak rapi &
   center, konsisten dengan kartu-kartu lain di halaman publik ini. */
.group-list {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: 100%;
}
/* Pembungkus SATU site: kartu status (selalu kelihatan) + panel detail
   (.public-detail-section, hidden sampai tombol "Lihat Detail" ditekan) —
   wrapper flex-column + gap ini PERSIS alasan yang sama dengan
   .public-detail-section sendiri (lihat komentarnya): supaya status-card &
   detail-section tetap berjarak sama walau dua-duanya jadi flex-item punya
   WRAPPER ini, bukan lagi langsung punya .group-list. */
.site-card-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: 100%;
}

/* ==== Sub-tab "15 Menit"/"30 Hari" di dalam Grafik Trafik (public.php &
   detail Grup Link Publik) — lihat README "Fitur: Grafik Trafik 30 Hari di
   Link Publik". SENGAJA reuse class .chart-tabs/.chart-tab-btn APA ADANYA
   (bukan bikin class baru seperti .router-filter-row) — beda dari kasus
   filter router (bagian 72) yang jumlah chip-nya variabel/bisa banyak, di
   sini SELALU PERSIS 2 pilihan (jendela pendek vs 30 hari), jadi "segmented
   control kaku buat 2 tombol" itu justru pas, tidak perlu pola chip baru.
   Discope ke ".traffic-range-tabs" (bukan #chart-tabs/#tab-btn-* yang sudah
   dipakai tab Latensi/Trafik di level ATAS) supaya dua pasang tab yang
   bersarang ini tidak saling tertukar id/listener-nya. */
.traffic-range-tabs {
  margin-bottom: 12px;
}
.traffic-range-tabs .chart-tab-btn { font-size: 12px; padding: 6px 8px; }

/* ==== Grafik Batang Uptime 30 Hari (public.php & detail Grup Link Publik)
   ====
   Satu batang = satu hari (sumbu kategorikal) — lihat README "Fitur: Grafik
   Uptime Harian di Link Publik" & assets/js/uptime-bar-chart.js. Dibangun
   dari elemen HTML/CSS biasa (bukan SVG seperti latency-chart.js/
   traffic-chart.js) karena sumbunya kategorikal (per hari), bukan waktu
   kontinu. */
.uptime-bar-chart {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 90px;
  padding: 0 1px;
}
.uptime-bar {
  flex: 1;
  min-width: 3px;
  border-radius: 2px 2px 0 0;
  cursor: pointer;
  transition: opacity 0.1s ease;
}
.uptime-bar:hover,
.uptime-bar:active { opacity: 0.75; }
.uptime-bar.ok { background: var(--green); }
.uptime-bar.warn { background: var(--yellow); }
.uptime-bar.bad { background: var(--red); }
/* "Tidak ada data" — batang nyaris rata/samar (bukan warna status apa pun),
   supaya jelas bedanya dari uptime rendah sungguhan (.bad, yang warnanya
   tegas merah). rgba abu-abu TETAP (bukan var(--stat-box-border)) — border
   itu SENGAJA transparent di mode terang (lihat :root[data-theme=light] di
   atas), jadi batang ini akan ikut hilang total/tidak kelihatan sama
   sekali di mode terang kalau dipakai di sini (baru ketahuan waktu
   di-screenshot langsung, bukan cuma dites lewat kode). */
.uptime-bar.nodata { background: rgba(148, 163, 184, 0.35); }
.uptime-bar-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 10px;
  color: var(--text-dim);
}
.uptime-bar-info {
  margin-top: 10px;
  text-align: center;
  font-size: 12px;
  color: var(--text-dim);
  min-height: 16px; /* jaga tinggi tetap stabil walau teksnya kosong sesaat */
}
.uptime-bar-empty { color: var(--text-dim); font-size: 12px; text-align: center; padding: 18px 0; }

/* ==== Riwayat Gangguan (public.php & detail Grup Link Publik) ====
   Daftar ringkas kejadian down->pulih dalam 30 hari terakhir — lihat README
   "Fitur: Riwayat Gangguan di Link Publik" & ping_get_recent_incidents() di
   PingService.php. Kartu pembungkusnya pakai .login-card.public-status-card
   yang SAMA dengan kartu status/grafik lain, cuma isinya daftar baris
   biasa, bukan grafik. */
.incident-list { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.incident-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  background: var(--stat-box-bg);
  border: 1px solid var(--stat-box-border);
  border-radius: 10px;
  padding: 8px 12px;
  font-size: 12px;
}
.incident-date { color: var(--text); text-align: left; }
.incident-duration { color: var(--text-dim); white-space: nowrap; }
.incident-duration.incident-ongoing { color: var(--red); font-weight: 700; }
.incident-empty {
  color: var(--text-dim);
  font-size: 13px;
  text-align: center;
  padding: 10px 0 2px;
}
/* bagian 103 — Riwayat Gangguan diringkas ke N terbaru + tombol
   "Tampilkan semua"/"Sembunyikan" (lihat README). Class-nya SENGAJA
   ".incident-toggle-btn" (bukan dipakaikan ulang ".detail-toggle-btn"
   yang sudah ada) — supaya listener klik ".detail-toggle-btn" yang sudah
   ada (buat tombol "Lihat Detail" di kartu status, kartu YANG BEDA) tidak
   ikut menangkap klik di tombol ini secara keliru (lihat
   assets/js/public-status.js & public-group.js). Gaya visualnya DIBUAT
   SAMA (pil tipis transparan, tombol sekunder) dengan ".detail-toggle-btn"
   — cuma duplikat definisi CSS-nya, bukan class-nya — termasuk gotcha
   spesifisitas yang SAMA: ".public-status-card .incident-toggle-btn"
   (bukan ".incident-toggle-btn" saja) supaya menang lawan aturan umum
   ".login-card button { width:100%; background:var(--blue); ... }". */
.public-status-card .incident-toggle-btn {
  display: block;
  width: fit-content;
  margin: 10px auto 0;
  padding: 6px 14px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-dim);
  font-family: inherit;
  font-weight: 600;
  font-size: 11.5px;
  cursor: pointer;
}
.public-status-card .incident-toggle-btn:hover {
  color: var(--text);
  border-color: var(--text-dim);
}

/* ==== Modal "Detail" (baru bagian 85, sfp.php/sfp.js) ====
   Dipakai PERTAMA KALI oleh tombol Detail di halaman Redaman SFP — belum
   ada modal/dialog sama sekali di aplikasi ini sebelum ini, jadi ini CSS
   baru untuk BUNGKUSNYA (overlay + kotak + header + tombol tutup). ISI
   modalnya sendiri (grid field label/value) REUSE TOTAL class
   .health-sensors/.health-sensor dari Kesehatan Router (bagian 22), tidak
   ada CSS baru di situ. Ditulis generik (".modal-*", bukan ".sfp-modal-*")
   supaya bisa dipakai ulang kalau nanti fitur lain juga butuh modal
   serupa, tidak terikat ke Redaman SFP saja. */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(2, 6, 23, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  z-index: 100;
}
/* PENTING: ".modal-overlay[hidden]" (selector lebih spesifik, class+atribut)
   WAJIB ada supaya attribut [hidden] bawaan browser benar-benar
   menyembunyikan overlay ini — tanpa baris ini, "display: flex" di atas
   (sama spesifisitasnya dengan default UA stylesheet "[hidden]{display:
   none}") menang lewat urutan cascade, jadi overlay tetap full-screen &
   TETAP MENGHALANGI KLIK ke tabel di belakangnya walau atribut hidden-nya
   sudah terpasang (ditemukan langsung lewat tes Playwright sebelum rilis
   bagian 85 — tombol Detail di baris tabel sama sekali tidak bisa diklik
   kalau baris ini tidak ada). */
.modal-overlay[hidden] { display: none; }
.modal-box {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--card-shadow);
  width: 100%;
  max-width: 480px;
  max-height: calc(100vh - 32px);
  overflow-y: auto;
}
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  background: var(--bg-card);
}
.modal-header h3 { margin: 0; font-size: 15px; overflow-wrap: anywhere; }
.modal-close-btn {
  flex: 0 0 auto;
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 6px;
}
.modal-close-btn:hover { background: rgba(148, 163, 184, 0.15); color: var(--text); }
.modal-body { padding: 16px; }

/* Tombol "Detail" di tabel Redaman SFP REUSE class .ip-detail-link yang
   sudah ada (dashboard.php, link "Detail" ke ping-test.php) — TAPI di
   sana elemennya <a>, di sfp.js dipasang di <button> (buka modal di
   halaman yang sama, bukan pindah halaman) jadi perlu override kecil
   supaya bentuknya tetap identik (browser kasih background/border/font
   bawaan ke <button>, <a> tidak). */
button.ip-detail-link {
  background: transparent;
  cursor: pointer;
  font: inherit;
}
