/* ================= Site-wide font (IBM Plex Sans Thai) ================= */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Thai:wght@300;400;500;600;700&display=swap');

/* ================= Global Variables & Reset ================= */
:root {
    --primary: #f97316;        /* orange */
    --primary-hover: #ea580c;
    --amber: #fbbf24;
    /* Paper theme - warm ivory/kraft tones instead of the previous cool
       white/blue-gray surfaces, so panels, cards and hover states all read
       as "paper" together rather than one warm background behind cool-gray
       cards. */
    --bg-color: #f3ecdd;
    --panel-bg: #faf6ec;
    --sidebar-bg: #16171c;     /* near-black */
    --text-main: #2b3345;
    --text-light: #6b7280;
    --border: #e3d9c4;
    --danger: #ef4444;
    --success: #10b981;
    --warning: #f59e0b;
    --text-strong: #0f172a;
    --surface-2: #f2ebdc;
    --surface-3: #ece2cd;
    --line-soft: #cdbfa0;
    --muted-3: #94a3b8;
    --alert-info-bg: #eff6ff;
    --alert-info-fg: #1e40af;
    --alert-warn-bg: #fffbeb;
    --alert-warn-border: #fde68a;
    --alert-warn-fg: #b45309;
    --badge-success-bg: #d1fae5;
    --badge-success-fg: #047857;
    --badge-danger-bg: #fee2e2;
    --badge-danger-fg: #b91c1c;
    --badge-warn-bg: #fef3c7;
    --badge-warn-fg: #b45309;

    /* ===== Category colors (color-coded nav/cards across admin + dashboard) =====
       users=blue, server=orange, finance=green, support=purple, network=cyan,
       energy=gold, equipment=rose. See the .cat-* marker classes below. */
    --cat-users: #3b82f6;
    --cat-server: #f97316;
    --cat-finance: #10b981;
    --cat-support: #8b5cf6;
    --cat-network: #06b6d4;
    --cat-energy: #eab308;
    --cat-equipment: #ec4899;
}

* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'IBM Plex Sans Thai', sans-serif; }
body { background-color: var(--bg-color); color: var(--text-main); overflow-x: hidden; width: 100%; }

/* ================= Minimal Network Canvas ================= */
#networkCanvas { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: -1; background-color: var(--surface-2); }

/* ================= Mobile Header ================= */
.mobile-header {
    display: none; background: var(--sidebar-bg); padding: 15px 20px;
    align-items: center; justify-content: space-between; color: white;
    position: sticky; top: 0; z-index: 99;
}
.menu-toggle-btn { background: transparent; border: none; color: white; font-size: 1.5rem; cursor: pointer; }
.mobile-logo { font-size: 1.5rem; font-weight: bold; }
.mobile-logo .text-idc { color: var(--primary); font-weight: 300; }

/* ================= Layout & Sidebar (ปรับให้เต็มจอ 100%) ================= */
.app-container {
    display: flex;
    min-height: 100vh;
    width: 100%; /* เปลี่ยนจาก max-width ให้กลายเป็น 100% เต็มจอ */
    background: transparent;
}
.sidebar {
    width: 260px; background-color: var(--sidebar-bg); color: #fff;
    display: flex; flex-direction: column; padding: 20px 0;
    box-shadow: 4px 0 15px rgba(0,0,0,0.05); z-index: 100;
    transition: transform 0.3s ease;
    /* Hints the browser to promote this to its own GPU compositor layer
       ahead of time, before the slide-in/out transform starts. Without
       this, WebKit/Safari (seen on a narrow-window mobile-style open) can
       briefly paint the pre-animation and post-animation states together
       for a frame or two when it promotes the layer right as the
       transition begins, showing a smeared/duplicated ghost of the menu
       text mid-slide that resolves once the animation settles. */
    will-change: transform;
    /* Fixed in place while the page content scrolls - stays pinned to the
       viewport instead of scrolling away with a tall content-section.
       Using position:fixed (not sticky) because .app-container has
       overflow:hidden in the portal-skin section below, and any ancestor
       with overflow != visible hijacks position:sticky's scroll container,
       silently breaking the stick-to-viewport effect. Fixed positioning
       is relative to the viewport regardless of ancestor overflow, so it
       isn't affected. main-content gets a matching margin-left below so
       it isn't hidden underneath.
       No overflow-y here (see .menu below) - with this many nav items the
       list alone can be taller than the viewport, and putting the scroll
       on the whole sidebar let a wheel-scroll over the nav silently carry
       the logo/brand header off the top of the screen. Scoping the scroll
       to just .menu keeps the brand header and the user card at the
       bottom always visible. */
    position: fixed; top: 0; left: 0; height: 100vh;
}
.close-sidebar { display: none; text-align: right; padding: 0 20px; font-size: 1.5rem; cursor: pointer; }
.brand { text-align: center; padding: 10px 20px 30px 20px; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 20px; flex-shrink: 0; }
.logo { font-size: 2.2rem; letter-spacing: 1px; }
.text-cnp { font-weight: 700; color: #fff; }
.text-idc { font-weight: 300; color: var(--primary); }
.brand-slogan { font-size: 0.85rem; color: var(--muted-3); margin-top: 5px; }

.menu { display: flex; flex-direction: column; padding: 0 15px; flex: 1 1 auto; overflow-y: auto; min-height: 0; }
.menu a { padding: 15px 20px; color: var(--muted-3); text-decoration: none; border-radius: 10px; margin-bottom: 5px; transition: 0.3s; display: flex; align-items: center; font-weight: 400; }
.menu a i { width: 25px; font-size: 1.1rem; }
.menu a:hover, .menu a.active { background-color: rgba(255,255,255,0.05); color: var(--primary); }

.sidebar-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 90; }

/* ================= Main Content & Topbar ================= */
.main-content { flex: 1; display: flex; flex-direction: column; width: calc(100% - 260px); margin-left: 260px; }
.topbar { background: var(--panel-bg); padding: 15px 40px; display: flex; justify-content: flex-end; align-items: center; border-bottom: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.02); }
.user-info { display: flex; align-items: center; gap: 15px; }
.user-avatar { width: 35px; height: 35px; background: #e0f2fe; color: var(--primary); border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.logout-btn { border: 1px solid var(--border); background: transparent; padding: 6px 12px; border-radius: 6px; cursor: pointer; color: var(--text-light); }

/* ================= Content Sections & Panels ================= */
.content-section { padding: 40px; display: none; animation: fadeIn 0.4s; }
.content-section.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }

.section-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 25px; color: var(--text-strong); }

.panel {
    background-color: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 25px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.03);
    margin-bottom: 20px;
}
.panel h3 { margin-bottom: 20px; font-size: 1.1rem; color: var(--text-strong); border-bottom: 1px solid var(--border); padding-bottom: 10px; }
.h-100 { height: 100%; }

/* ================= Dashboard VPS Cards ================= */
.vps-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 20px; }
.vps-card {
    background-color: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 25px;
    box-shadow: 0 4px 15px rgba(0,0,0,0.04);
    transition: 0.3s;
}
.vps-card:hover { box-shadow: 0 8px 25px rgba(0,0,0,0.08); transform: translateY(-3px); border-color: var(--line-soft); }
.vps-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
.vps-title { display: flex; align-items: center; gap: 10px; }
.text-orange { color: #f97316; font-size: 1.3rem;}
.vps-details p { display: flex; justify-content: space-between; margin-bottom: 10px; font-size: 0.95rem; padding-bottom: 8px; border-bottom: 1px dashed var(--surface-3); }
.vps-details p span { color: var(--text-light); }
.vps-actions { margin-top: 20px; display: flex; flex-wrap: wrap; gap: 10px; }

/* ================= Buttons ================= */
/* display:inline-flex + align-items:center so an icon's own glyph metrics
   never sit a half-pixel off the label's text baseline (previously relied
   on default inline/baseline flow, which is where the icon-above-text
   "wrapped" look and other small text-position issues came from); gap
   replaces the literal space char between `<i>` and the label as the actual
   spacing so it stays consistent even where a button is squeezed narrow. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.3s ease; font-size: 0.95rem; text-align: center; }
.btn-primary { background: var(--primary); color: #fff; box-shadow: 0 4px 6px rgba(14, 165, 233, 0.2); }
.btn-primary:hover { background: var(--primary-hover); box-shadow: 0 6px 12px rgba(14, 165, 233, 0.3); transform: translateY(-1px); }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text-main); }
.btn-outline:hover { background: var(--surface-2); border-color: var(--line-soft); }
.btn-full { width: 100%; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; }

/* ================= Modern Form Inputs ================= */
.form-group { margin-bottom: 20px; }
.form-group label { display: block; margin-bottom: 8px; font-size: 0.95rem; font-weight: 500; color: var(--text-light);}

.form-input {
    width: 100%;
    padding: 14px 18px;
    border: 1.5px solid var(--border);
    border-radius: 12px;
    background-color: var(--surface-2);
    color: var(--text-main);
    font-size: 0.95rem;
    transition: all 0.3s ease;
    outline: none;
}
.form-input:hover { border-color: var(--line-soft); }
.form-input:focus {
    background-color: var(--panel-bg);
    border-color: var(--primary);
    box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.15);
}

select.custom-select {
    appearance: none; -webkit-appearance: none;
    background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
    background-repeat: no-repeat; background-position: right 1rem center; background-size: 1.2em; padding-right: 2.5rem;
}
.form-layout-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }

/* ================= Ordering Packages ================= */
.package-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 25px; margin-bottom: 30px; }
.package-card {
    background-color: var(--panel-bg); border: 2px solid transparent; border-radius: 20px; padding: 35px 25px; text-align: center; cursor: pointer;
    transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.4s ease, border-color 0.4s ease;
    box-shadow: 0 4px 15px rgba(0,0,0,0.03); position: relative;
}
.package-card:hover, .package-card.selected {
    border-color: var(--primary); box-shadow: 0 20px 40px rgba(14, 165, 233, 0.15); transform: translateY(-12px);
}
.package-card.dedicated { border: 2px solid var(--primary); }
.pkg-badge { position: absolute; top: 15px; right: -30px; background: var(--primary); color: #fff; font-size: 0.75rem; padding: 4px 30px; transform: rotate(45deg); font-weight: 600; }
.pkg-icon { font-size: 2.5rem; color: var(--primary); margin-bottom: 15px; }
.price { font-size: 1.6rem; font-weight: 600; color: var(--text-strong); margin-bottom: 25px; }
.features { list-style: none; text-align: left; margin-bottom: 25px; font-size: 0.95rem;}
.features li { padding: 10px 0; border-bottom: 1px solid var(--surface-3); }
.features li i { color: var(--success); margin-right: 8px; }

.pkg-btn {
    background-color: var(--surface-3); color: var(--text-light); border-radius: 12px; padding: 12px; font-weight: 600; transition: all 0.3s ease;
}
.package-card:hover .pkg-btn, .package-card.selected .pkg-btn {
    background-color: var(--primary); color: #ffffff; box-shadow: 0 8px 20px rgba(14, 165, 233, 0.3); transform: scale(1.05);
}

/* ================= Billing & Support ================= */
.billing-layout, .support-layout { display: grid; grid-template-columns: 1fr 1.2fr; gap: 25px; }
.billing-col { display: flex; flex-direction: column; }
.status-badge { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 500; }
.status-badge.running, .status-badge.success { background: var(--badge-success-bg); color: var(--badge-success-fg); }
.status-badge.stopped { background: var(--badge-danger-bg); color: var(--badge-danger-fg); }
.status-badge.pending { background: var(--badge-warn-bg); color: var(--badge-warn-fg); }
.invoice-box { background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
.invoice-header { display: flex; justify-content: space-between; margin-bottom: 15px; }
.inv-id { font-weight: 600; color: var(--text-strong); }
.inv-total { font-size: 1.2rem; margin-top: 15px; text-align: right; border-top: 1px dashed var(--line-soft); padding-top: 10px;}
.inv-total strong { color: var(--danger); font-size: 1.4rem; }
.bank-details { display: flex; align-items: center; gap: 15px; background: var(--alert-warn-bg); border: 1px solid var(--alert-warn-border); padding: 20px; border-radius: 12px; margin-bottom: 20px; }
.bank-logo { width: 45px; height: 45px; object-fit: contain; }
.bank-text h4 { font-size: 1.4rem; color: var(--alert-warn-fg); letter-spacing: 1px; margin: 4px 0;}
.bank-text p { font-size: 0.95rem; color: var(--alert-warn-fg); }
.history-table { width: 100%; border-collapse: collapse; }
.history-table th, .history-table td { padding: 15px 10px; text-align: left; border-bottom: 1px solid var(--border); font-size: 0.95rem; }

/* Support Chat */
.chat-panel { display: flex; flex-direction: column; height: 100%; padding: 0; overflow: hidden;}
.chat-header { padding: 20px; border-bottom: 1px solid var(--border); background: var(--surface-2); display: flex; justify-content: space-between; align-items: center;}
.chat-header h3 { border: none; margin: 0; padding: 0; }
.chat-box { flex: 1; padding: 20px; overflow-y: auto; background: var(--panel-bg); min-height: 350px; max-height: 450px; display: flex; flex-direction: column; gap: 15px; }
.chat-msg { display: flex; gap: 10px; max-width: 85%; }
.chat-msg.admin { align-self: flex-start; }
.chat-msg.user { align-self: flex-end; justify-content: flex-end; }
.msg-avatar { width: 38px; height: 38px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 0.85rem; flex-shrink: 0;}
.admin .msg-avatar { background: #fee2e2; color: #dc2626; }
.user .msg-avatar { background: #e0f2fe; color: var(--primary); }
.msg-name { font-size: 0.85rem; color: var(--text-light); margin-bottom: 4px; }
.msg-time { font-size: 0.75rem; color: var(--muted-3); margin-left: 5px; }
.msg-bubble { padding: 12px 16px; border-radius: 16px; font-size: 0.95rem; line-height: 1.5; }
.admin .msg-bubble { background: var(--surface-3); border-top-left-radius: 0; }
.user .msg-bubble { background: var(--primary); color: white; border-top-right-radius: 0; box-shadow: 0 4px 10px rgba(14, 165, 233, 0.2); }
.user .msg-name { text-align: right; }
.chat-input-area { padding: 20px; border-top: 1px solid var(--border); display: flex; gap: 10px; background: var(--surface-2);}

/* ================= Responsive (รองรับมือถือ แท็บเล็ต และจอเล็ก) ================= */
@media (max-width: 1200px) {
    .billing-layout, .support-layout { grid-template-columns: 1fr; }
}

@media (max-width: 768px) {
    .mobile-header { display: flex; }
    .topbar { display: none; }

    .sidebar { position: fixed; top: 0; left: -280px; height: 100vh; width: 260px; }
    .sidebar.open { transform: translateX(280px); }
    .close-sidebar { display: block; margin-top: 10px; flex-shrink: 0; }
    /* On a short phone screen the admin menu (13 links) is taller than the
       space left after .brand + .sidebar-user-card, so it needs to be the
       one part of the sidebar that scrolls internally while the logo up
       top and the user card at the bottom both stay put. flex:1 1 auto
       (the base/desktop rule) sizes .menu from its own CONTENT first and
       only grows/shrinks from there, and .sidebar-user-card's margin-top:
       auto competes for the same leftover space - on a tall desktop
       sidebar there's slack either way so it goes unnoticed, but on a
       short one that combination could let the menu (or the items past
       whatever fits) get pushed out from under the user card with nothing
       left to scroll it back into view. flex-basis 0 makes the grow/shrink
       math exact instead of content-dependent, and dropping the auto
       margin (flex-grow:1 alone already pins the card to the bottom once
       .menu has taken all it needs) removes that competition entirely -
       so .menu is always exactly "whatever height is left", scrollable,
       and every link in it stays reachable no matter how many there are. */
    .menu { flex: 1 1 0; }
    .sidebar-user-card { margin-top: 0; }
    .brand { flex-shrink: 0; }

    .main-content { width: 100%; margin-left: 0; }
    .content-section { padding: 20px; }

    .form-layout-grid { grid-template-columns: 1fr; }
    .bank-details { flex-direction: column; align-items: flex-start; }
}

/* ================= Fullstack additions ================= */
.text-primary { color: var(--primary); }
.empty-state { color: var(--text-light); padding: 20px 0; text-align: center; }

/* ================= Loading indicator (replaces plain "กำลังโหลด..." text) =================
   A small "download box" progress bar + live percentage, per direct
   reference (a Uiverse.io loader with a percentage readout) - see
   cnpLoaderHtml()/the auto-starting MutationObserver in ui.js for how the
   percentage actually moves. display:inline-flex so it drops into any
   existing .empty-state <p>/<td> wrapper (already text-align:center) with
   no layout changes needed at each call site.
   Built from <span>s, not <div>s, even though it visually reads as boxes -
   a LOT of call sites are `<p class="empty-state">...</p>`, and <div> is on
   HTML5's list of tags that force-close an open <p> (the browser silently
   splits the markup into an empty, un-styled <p></p> followed by the div
   sitting OUTSIDE it, breaking the centering) - <span> is inline/phrasing
   content so it can't trigger that. display:block below is what actually
   makes the track/fill spans behave like boxes instead of inline text.
   display:flex + width:100% (not inline-flex) so this centers itself via
   its own justify-content, regardless of whether the wrapping element uses
   text-align:center (a <p class="empty-state">) or not (a <div> inside a
   CSS grid, e.g. #packageGrid/.vps-grid) - relying on the parent's
   text-align was inconsistent across call sites. */
.cnp-loader { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; width: 100%; text-align: center; }
.cnp-loader-label { font-size: 0.85rem; font-weight: 600; color: var(--text-light); }
.cnp-loader-bar { display: flex; align-items: center; gap: 12px; }
.cnp-loader-track {
    display: block; width: 160px; height: 10px; border-radius: 999px;
    background: rgba(249, 115, 22, 0.14); overflow: hidden; position: relative;
}
.cnp-loader-fill {
    display: block; height: 100%; width: 0%; border-radius: 999px; position: relative; overflow: hidden;
    background: linear-gradient(90deg, var(--primary, #f97316), var(--amber, #fbbf24));
}
.cnp-loader-fill::after {
    content: ''; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.55), transparent);
    animation: cnpLoaderShine 1.1s linear infinite;
}
@keyframes cnpLoaderShine { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
.cnp-loader-pct { font-size: 0.9rem; font-weight: 700; color: var(--primary-hover, #ea580c); min-width: 36px; text-align: left; }

/* Animated server-rack artwork above the bar - a bouncing isometric box
   with a fading/scaling "ghost" shadow beneath it and a couple of
   strobing LED accents, per direct reference (a Uiverse.io "SevMini"
   loader by Juanes200122). Markup is two <svg><use> tags pointing at the
   <symbol>s ensureCnpLoaderSprite() injects once (see ui.js) - keeps this
   from duplicating the ~30-path SVG at every one of the many loading spots
   across the site. */
.cnp-loader-art { position: relative; width: 100px; height: 96px; display: flex; align-items: flex-end; justify-content: center; }
.cnp-loader-server {
    width: 82px; height: auto; position: relative; z-index: 2;
    filter: drop-shadow(0 8px 10px rgba(15, 23, 42, 0.2));
    animation: cnpLoaderBounce 4s infinite ease-in-out;
}
.cnp-loader-ghost {
    width: 68px; height: auto; position: absolute; bottom: 2px; z-index: 1;
    animation: cnpLoaderGhost 4s infinite ease-in-out;
}
@keyframes cnpLoaderBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-12px); } }
@keyframes cnpLoaderGhost {
    0% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.5; transform: scale(0.88); }
    100% { opacity: 1; transform: scale(1); }
}
/* Targets ids living inside the <symbol> sprite - a CSS id selector applies
   to <use>-instantiated content the same as if it were inlined, so this
   still animates correctly even though the actual elements only exist once
   (hidden) in the sprite. */
#strobe_led1 { animation: cnpLoaderStrobeLed 0.5s infinite; }
#strobe_color1, #strobe_color1b { animation: cnpLoaderStrobeGreen 0.8s infinite; }
#strobe_color3 { animation: cnpLoaderStrobeRed 0.8s infinite; animation-delay: 3s; }
@keyframes cnpLoaderStrobeLed { 0% { opacity: 1; } 50% { opacity: 0; } 51% { opacity: 1; } 100% { opacity: 1; } }
@keyframes cnpLoaderStrobeGreen { 0%, 50%, 100% { fill: #17e300; } 25%, 75% { fill: #17e300b4; } }
@keyframes cnpLoaderStrobeRed { 0%, 50%, 100% { fill: #ff5f4a; } 25%, 75% { fill: #103573; } }

/* Auth pages (login/register) - split hero + form layout */
.auth-body {
    min-height: 100vh;
    display: flex;
    align-items: stretch;
    justify-content: center;
    background: #0a0f1c;
    padding: 0;
    position: relative;
}
/* Animated network canvas fills the whole auth page (see auth-net.js) */
#authNet {
    position: fixed; inset: 0; z-index: 0; display: block;
}
.auth-body::after {
    content: '';
    position: fixed; inset: 0; z-index: 0; pointer-events: none;
    background: radial-gradient(900px 600px at 80% 10%, rgba(249,115,22,0.14), transparent 60%),
                radial-gradient(700px 500px at 5% 90%, rgba(251,191,36,0.10), transparent 60%);
}
.auth-shell {
    display: flex;
    width: 100%;
    min-height: 100vh;
    position: relative;
    z-index: 1;
}
.auth-hero {
    flex: 1;
    background: transparent;
    color: #fff;
    padding: 56px 60px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    position: relative;
    overflow: hidden;
}
#heroArt { display: none; }
.auth-hero > * { position: relative; z-index: 1; }
.auth-hero .hero-logo { font-size: 2rem; font-weight: 700; margin-bottom: 22px; letter-spacing: 0.5px; }
.auth-hero .hero-logo .text-cnp { color: #fff; }
.auth-hero .hero-logo .text-idc { color: var(--primary); font-weight: 300; }
.auth-hero h2 { font-size: 1.7rem; line-height: 1.4; margin-bottom: 14px; font-weight: 600; color: #fff; }
.auth-hero p { color: #c8cdd6; font-size: 1.05rem; line-height: 1.7; max-width: 430px; }
.auth-hero .hero-features { margin-top: 30px; display: flex; flex-direction: column; gap: 14px; }
.auth-hero .hero-features div { display: flex; align-items: center; gap: 12px; color: #e2e5ea; font-size: 1.02rem; }
.auth-hero .hero-features i { color: var(--primary); font-size: 1.15rem; width: 24px; }
.auth-main {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 40px 24px;
    position: relative;
    overflow: hidden;
    background: transparent;
}
.auth-card {
    position: relative; z-index: 1;
    background: rgba(255,255,255,0.94);
    backdrop-filter: blur(20px) saturate(160%);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    border: 1px solid rgba(255,255,255,0.7);
    border-radius: 24px;
    padding: 44px 40px;
    width: 100%;
    max-width: 430px;
    box-shadow: 0 24px 70px rgba(2,6,23,0.5), 0 0 0 1px rgba(249,115,22,0.14), inset 0 1px 0 rgba(255,255,255,0.8);
    overflow: hidden;
    animation: cardPop 0.55s cubic-bezier(.2,.8,.2,1) both;
}
@keyframes cardPop {
    from { opacity: 0; transform: translateY(18px) scale(0.96); }
    to { opacity: 1; transform: none; }
}
/* shimmer sweep across the card (วิบวับ) */
.auth-card::before {
    content: '';
    position: absolute; top: 0; left: -60%;
    width: 45%; height: 100%;
    background: linear-gradient(100deg, transparent, rgba(255,255,255,0.55), transparent);
    transform: skewX(-18deg);
    animation: cardShine 6s ease-in-out infinite;
    pointer-events: none;
}
@keyframes cardShine {
    0%, 65% { left: -60%; }
    85%, 100% { left: 130%; }
}
/* glowing top accent line */
.auth-card::after {
    content: '';
    position: absolute; top: 0; left: 12%; right: 12%; height: 2px;
    background: linear-gradient(90deg, transparent, var(--amber), var(--primary), transparent);
    opacity: 0.8;
    animation: accentGlow 3s ease-in-out infinite;
}
@keyframes accentGlow { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.9; } }
.auth-card > * { position: relative; z-index: 2; }
.auth-card .auth-title { font-size: 1.6rem; font-weight: 600; color: var(--sidebar-bg); text-align: center; }
.auth-card .auth-sub { text-align: center; color: var(--text-light); margin: 6px 0 28px; font-size: 0.95rem; }
.otp-inputs { display: flex; gap: 10px; justify-content: center; margin: 8px 0 6px; }
.otp-inputs input {
    width: 48px; height: 58px; text-align: center; font-size: 1.5rem; font-weight: 600;
    border: 2px solid var(--border); border-radius: 12px; background: var(--surface-2); color: var(--sidebar-bg);
    transition: border-color .2s, background .2s;
}
.otp-inputs input:focus { outline: none; border-color: var(--primary); background: var(--panel-bg); }
.otp-meta { text-align: center; color: var(--text-light); font-size: 0.88rem; margin-top: 14px; }
.otp-meta a { color: var(--primary); font-weight: 600; text-decoration: none; cursor: pointer; }
.otp-meta a.disabled { color: var(--line-soft); pointer-events: none; }
.auth-back { background: none; border: none; color: var(--text-light); cursor: pointer; font-size: 0.9rem; margin-bottom: 14px; }
.auth-back:hover { color: var(--primary); }
@media (max-width: 860px) {
    .auth-hero { display: none; }
    .auth-shell { min-height: 100vh; }
}

/* Toast notifications */
.cnp-toast {
    position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px);
    background: var(--text-strong); color: #fff; padding: 12px 22px; border-radius: 10px;
    font-size: 0.9rem; opacity: 0; pointer-events: none; transition: all 0.3s ease; z-index: 999;
    box-shadow: 0 8px 20px rgba(0,0,0,0.2); max-width: 90vw;
}
.cnp-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.cnp-toast.error { background: var(--danger); }

/* Admin: summary cards */
.summary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 300px)); gap: 20px; }
.summary-card { display: flex; align-items: center; gap: 16px; }
.summary-card i { font-size: 1.8rem; color: var(--primary); background: #e0f2fe; padding: 16px; border-radius: 14px; }
.summary-num { font-size: 1.6rem; font-weight: 700; color: var(--text-strong); }

/* Ticket list rows (customer support tab) */
.ticket-row {
    display: flex; justify-content: space-between; align-items: center;
    padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px;
    margin-bottom: 10px; cursor: pointer; transition: 0.2s;
}
.ticket-row:hover, .ticket-row.active { border-color: var(--primary); background: #f0f9ff; }

/* Admin tables: allow selects inside cells to not overflow */
.history-table select { width: auto; padding: 8px 10px !important; }
.history-table td, .history-table th { vertical-align: middle; }

/* condition-alert (info banners used on order/support pages) */
.condition-alert {
    background: var(--alert-info-bg); color: var(--alert-info-fg); border-left: 4px solid var(--primary);
    padding: 14px 18px; border-radius: 8px; margin-bottom: 20px; font-size: 0.92rem;
}

/* =====================================================================
   LIQUID GLASS THEME  (white / orange / black tone)
   Frosted translucent surfaces with glossy highlights + soft motion.
   Appended last so these rules win over the base styles above.
   ===================================================================== */
:root {
    --glass-bg: rgba(255, 255, 255, 0.55);
    --glass-bg-strong: rgba(255, 255, 255, 0.72);
    --glass-border: rgba(255, 255, 255, 0.65);
    --glass-blur: 18px;
    --glass-shadow: 0 10px 34px rgba(23, 35, 58, 0.10), 0 2px 6px rgba(249, 115, 22, 0.06);
    --glass-ring: inset 0 1px 0 rgba(255, 255, 255, 0.85), inset 0 0 0 1px rgba(255, 255, 255, 0.25);
}

/* Warm, brighter backdrop so the frosted surfaces have something to refract */
body { background: linear-gradient(160deg, #fff7ef 0%, #f6f8fc 46%, #fef3e6 100%); }
#networkCanvas { background: transparent; }
.app-container::before {
    content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
    background: radial-gradient(720px 520px at 88% -6%, rgba(251, 191, 36, 0.22), transparent 60%),
                radial-gradient(640px 480px at -6% 108%, rgba(249, 115, 22, 0.16), transparent 60%);
}

/* --- Frosted panels & cards --- */
.panel,
.vps-card,
.package-card,
.infra-card,
.invoice-box,
.chat-panel,
.summary-card.panel {
    background: var(--glass-bg) !important;
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border: 1px solid var(--glass-border) !important;
    box-shadow: var(--glass-shadow), var(--glass-ring) !important;
    position: relative;
    overflow: hidden;
}
/* .infra-card never had its own padding/radius rule in this file - it used
   to get both ONLY from a duplicate style block injected by admin.js, which
   was removed (it was also duplicating the background/border above and
   fighting this glass look). Removing it dropped padding/radius entirely,
   so the CPU/RAM gauges and WAN chart inside ran edge-to-edge instead of
   sitting inset like every other card - restoring just the layout half here
   (the color half correctly stays owned by the glass rule above). */
.infra-card { padding: 20px; border-radius: 20px; }

/* glossy sheen that lives on the top-left of every glass surface */
.panel::after,
.vps-card::after,
.package-card::after,
.infra-card::after {
    content: ''; position: absolute; top: 0; left: 0; right: 0; height: 45%;
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0));
    pointer-events: none; opacity: 0.9;
}
.panel > *, .vps-card > *, .package-card > *, .infra-card > * { position: relative; z-index: 1; }

.vps-card:hover,
.infra-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 18px 44px rgba(23, 35, 58, 0.16), 0 4px 14px rgba(249, 115, 22, 0.14), var(--glass-ring) !important;
    border-color: rgba(249, 115, 22, 0.45) !important;
}

/* --- Sidebar: dark liquid glass --- */
.sidebar {
    background: linear-gradient(180deg, rgba(22, 23, 28, 0.82), rgba(22, 23, 28, 0.72)) !important;
    -webkit-backdrop-filter: blur(22px) saturate(150%);
    backdrop-filter: blur(22px) saturate(150%);
    border-right: 1px solid rgba(255, 255, 255, 0.08);
    box-shadow: 4px 0 30px rgba(0, 0, 0, 0.18);
}
.menu a:hover, .menu a.active {
    background: rgba(249, 115, 22, 0.14);
    box-shadow: inset 0 0 0 1px rgba(249, 115, 22, 0.25);
}

/* --- Topbar & mobile header: frosted --- */
.topbar {
    background: rgba(255, 255, 255, 0.6) !important;
    -webkit-backdrop-filter: blur(16px) saturate(160%);
    backdrop-filter: blur(16px) saturate(160%);
    border-bottom: 1px solid var(--glass-border);
    position: sticky; top: 0; z-index: 80;
}
.mobile-header {
    background: rgba(22, 23, 28, 0.82) !important;
    -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
}
.user-avatar {
    background: rgba(249, 115, 22, 0.15); color: var(--primary);
    box-shadow: inset 0 0 0 1px rgba(249, 115, 22, 0.3);
}

/* --- Buttons: 3D "press" style (site-wide) ---
   Every .btn/.btn-primary/.btn-outline everywhere (dashboard, admin, login,
   register, forgot-password all share this one stylesheet) now reads as a
   raised block: a hard-edged drop shadow stands in for the block's own
   "thickness", the button lifts a little further on hover, and presses
   flush (shadow collapses, button shifts down into it) on click.
   Shadow/border/lift are all in `em` (not px) so they scale down with each
   button's own font-size - small icon-only buttons (tables, admin node
   power/refresh, filter chips) that set a smaller inline font-size get a
   proportionally thin shadow instead of the same fixed px slab as a big
   CTA button, which was making the small ones look swollen/oversized. */
.btn {
    position: relative;
    transform: translateY(0);
    transition: transform .12s ease, box-shadow .12s ease, background .2s ease, color .2s ease, border-color .2s ease;
}
/* Note: none of these use !important (deliberately, unlike the first version
   of this block). A bunch of buttons across the codebase set their own
   inline color/border-color for a specific meaning (delete = red, cancel
   order = red, node shutdown = red, etc.) - forcing this block with
   !important silently stomped every one of those back to the same neutral
   color. Plain (non-!important) rules here still win over the older base
   .btn-outline/.btn-primary declared earlier in this file (later same-
   specificity rule wins in the cascade), while any button's own inline
   style - which always outranks a non-!important stylesheet rule - keeps
   showing through, in both the normal and :hover states. */
.btn-outline {
    background: var(--panel-bg);
    border: 1px solid var(--border);
    box-shadow: 0 0.2em 0 0 var(--line-soft);
}
.btn-outline:hover {
    background: var(--surface-2); border-color: rgba(249, 115, 22, 0.4); color: var(--primary-hover);
    transform: translateY(-0.14em); box-shadow: 0 0.32em 0 0 var(--line-soft);
}
.btn-outline:active { transform: translateY(0.12em); box-shadow: 0 0.06em 0 0 var(--line-soft); }
.btn-primary {
    background: linear-gradient(135deg, var(--amber), var(--primary) 55%);
    box-shadow: 0 0.2em 0 0 var(--primary-hover);
}
.btn-primary:hover { transform: translateY(-0.14em); box-shadow: 0 0.32em 0 0 var(--primary-hover); }
.btn-primary:active { transform: translateY(0.12em); box-shadow: 0 0.06em 0 0 var(--primary-hover); }
.btn:disabled, .btn:disabled:hover, .btn:disabled:active {
    transform: none !important; box-shadow: none !important;
}

/* Start/Stop mini-buttons on the compact dashboard VM cards (vpsCard() in
   app.js) and the admin VM table row (vpsRow() in admin.js) still use plain
   .btn-outline with no per-action color, unlike the detail page's
   Console/Terminal/Start/Stop/Delete row (.cb-btn .bf-*) which already keeps
   Start green / Stop red as a real, meaningful color instead of the shared
   neutral outline - bring these in line so "Stop" actually reads as the
   red/dangerous action it is (not scoped to .vps-actions so it also applies
   to the admin table's icon-only buttons). */
.btn-action-start:not(:disabled) {
    color: #3f7a56 !important; border-color: #a8d4b6 !important;
    box-shadow: 0 0.2em 0 0 #a8d4b6 !important;
}
.btn-action-start:not(:disabled):hover { background: #f2f8f4 !important; }
.btn-action-stop:not(:disabled) {
    color: #b1615c !important; border-color: #e3b3af !important;
    box-shadow: 0 0.2em 0 0 #e3b3af !important;
}
.btn-action-stop:not(:disabled):hover { background: #fbf1f0 !important; }
/* Matches the indigo used for Terminal on the detail page's action row
   (.cb-btn.bf-terminal, --button_color:#4f46e5) so the "Terminal" quick
   button on the compact dashboard card reads as the same action/color. */
.btn-action-terminal:not(:disabled) {
    color: #4f46e5 !important; border-color: #c7d2fe !important;
    box-shadow: 0 0.2em 0 0 #c7d2fe !important;
}
.btn-action-terminal:not(:disabled):hover { background: #eef2ff !important; }

/* Icon-only action buttons packed in a row (admin VM table's per-row
   eye/start/stop/reboot/terminal/backup/.../delete strip, and each infra
   node card's reboot/shutdown pair) inherited the site-wide 999px pill
   radius meant for labeled buttons - on a plain square icon that just
   reads as a big circle, and 9 of them side by side looked oversized.
   Smaller fixed square + rounded-square (not full pill) corners instead. */
.vps-row-actions .btn, .infra-head .btn {
    border-radius: 10px !important;
    width: 32px; height: 32px; padding: 0 !important;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 0.78rem;
    box-shadow: 0 0.16em 0 0 var(--line-soft);
}
.vps-row-actions .btn:hover, .infra-head .btn:hover { transform: translateY(-0.1em); box-shadow: 0 0.26em 0 0 var(--line-soft); }
.vps-row-actions .btn:active, .infra-head .btn:active { transform: translateY(0.08em); box-shadow: 0 0.04em 0 0 var(--line-soft); }
.vps-row-actions .btn-action-start:not(:disabled),
.infra-head .btn-action-start:not(:disabled) { box-shadow: 0 0.16em 0 0 #a8d4b6 !important; }
.vps-row-actions .btn-action-stop:not(:disabled),
.infra-head .btn-action-stop:not(:disabled) { box-shadow: 0 0.16em 0 0 #e3b3af !important; }
.vps-row-actions .btn-action-terminal:not(:disabled) { box-shadow: 0 0.16em 0 0 #c7d2fe !important; }

/* --- Form inputs on glass --- */
.form-input {
    background: rgba(255, 255, 255, 0.6) !important;
    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
    border: 1.5px solid rgba(255, 255, 255, 0.75) !important;
}
.form-input:focus {
    background: rgba(255, 255, 255, 0.92) !important;
    border-color: var(--primary) !important;
    box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.15) !important;
}

/* --- Status badges keep their solid colors but sit on glass nicely --- */
.summary-card i { background: rgba(249, 115, 22, 0.14); color: var(--primary); box-shadow: inset 0 0 0 1px rgba(249, 115, 22, 0.25); }

/* --- Generic glass modal (admin upgrade / dialogs) --- */
.cnp-modal-overlay {
    position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center;
    background: rgba(15, 23, 42, 0.42); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
    opacity: 0; pointer-events: none; transition: opacity 0.25s ease; padding: 20px;
}
.cnp-modal-overlay.show { opacity: 1; pointer-events: auto; }
.cnp-modal {
    position: relative; width: 100%; max-width: 460px;
    background: var(--glass-bg-strong);
    -webkit-backdrop-filter: blur(26px) saturate(170%); backdrop-filter: blur(26px) saturate(170%);
    border: 1px solid var(--glass-border); border-radius: 22px; padding: 30px 28px;
    box-shadow: 0 30px 70px rgba(2, 6, 23, 0.35), var(--glass-ring);
    transform: translateY(16px) scale(0.97); transition: transform 0.3s cubic-bezier(.2,.8,.2,1);
}
.cnp-modal-overlay.show .cnp-modal { transform: none; }
.cnp-modal::after {
    content: ''; position: absolute; top: 0; left: 12%; right: 12%; height: 2px;
    background: linear-gradient(90deg, transparent, var(--amber), var(--primary), transparent);
    animation: accentGlow 3s ease-in-out infinite;
}
.cnp-modal-title { font-size: 1.2rem; font-weight: 700; color: var(--sidebar-bg); margin-bottom: 16px; padding-right: 30px; border: none; }
.cnp-modal-x {
    position: absolute; top: 16px; right: 16px; width: 34px; height: 34px; border-radius: 50%;
    border: none; background: rgba(15, 23, 42, 0.06); color: var(--text-light); cursor: pointer; font-size: 0.95rem;
    transition: background 0.2s;
}
.cnp-modal-x:hover { background: rgba(239, 68, 68, 0.14); color: var(--danger); }

/* ================= Compact VM card (click through to detail page) ================= */
.vps-card.compact { cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
.vps-card.compact:hover { transform: translateY(-4px); }
.vps-card.compact .vps-header { margin-bottom: 14px; }
.vps-spec-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
.spec-chip {
    display: inline-flex; align-items: center; gap: 6px;
    background: rgba(249, 115, 22, 0.08); color: #9a3412;
    border: 1px solid rgba(249, 115, 22, 0.18);
    padding: 5px 12px; border-radius: 100px; font-size: 0.82rem; font-weight: 600;
}
.spec-chip i { color: var(--primary); font-size: 0.78rem; }
.vps-card.compact .vps-actions { margin-top: 0; gap: 8px; }
/* 4 buttons (Start/Stop/Restart/Terminal) at the base .btn padding/font-size
   didn't fit a card's width on one line, so the longest label wrapped and
   flex-stretch made every button in the row match that wrapped height -
   the "bloated" pill look. A smaller inline-scoped font-size/padding here
   (shadow/border already scale with it via the em-based 3D press rules)
   plus a 2-up grid (each button ~half width) keeps every label on one line
   at a much more compact, proportionate size. */
.vps-card.compact .vps-actions .btn {
    flex: 1 1 calc(50% - 4px);
    padding: 8px 10px;
    font-size: 0.82rem;
    gap: 6px;
    white-space: nowrap;
}

/* ================= VM detail page ================= */
.vps-detail-back {
    display: inline-flex; align-items: center; gap: 8px; background: none; border: none;
    color: var(--text-light); font-size: 0.95rem; font-weight: 600; cursor: pointer; margin-bottom: 18px; padding: 6px 0;
}
.vps-detail-back:hover { color: var(--primary); }
.vps-detail-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px; margin-bottom: 4px; }
.vps-detail-head h2 { display: flex; align-items: center; gap: 12px; font-size: 1.5rem; margin: 0; }
.vps-detail-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 22px 0; }
.vdt-tabs { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 18px; }

/* =====================================================================
   FLAT INVERT-ON-HOVER VM ACTION BUTTONS (Console/Start/Stop/.../tab row/
   Delete on the VM detail page, both customer app.js and admin
   adminVpsDetail.js). Replaced the earlier 3D-press "Voxybuns" button with
   a Uiverse.io "kamehame-ha" style instead, per direct reference: a solid
   color pill (background + matching outline) that inverts to a transparent/
   outline-only button with colored text+icon on hover, rather than
   pressing down. Markup is untouched (still .cb-btn > .button_top, same
   .bf-* modifier classes) - only what CSS does with that structure changed,
   so no HTML/JS edits were needed anywhere this renders. */
.cb-btn {
    --button_color: var(--primary, #f97316);
    display: inline-flex;
    border: none; padding: 0; cursor: pointer;
    font-family: inherit; font-size: 0.84rem; font-weight: 700;
    border-radius: 0.65em;
    background: var(--button_color);
    outline: 2px solid var(--button_color);
    outline-offset: -2px;
    transition: background-color 0.3s ease;
}
.cb-btn .button_top {
    display: flex; align-items: center; gap: 8px;
    box-sizing: border-box;
    border: none;
    border-radius: 0.65em;
    padding: 0.65em 1.15em;
    background: transparent;
    color: #fff;
    white-space: nowrap;
    transition: color 0.3s ease;
}
.cb-btn:hover:not(:disabled) { background: transparent; }
.cb-btn:hover:not(:disabled) .button_top { color: var(--button_color); }
.cb-btn:disabled { cursor: not-allowed; opacity: 0.45; }
/* .active marks the currently-open tab among the ports/files/activity
   panel toggle buttons (toggleVpsDetailTab() in app.js) - held statically
   in the same inverted/outline look :hover uses, since a click there
   toggles a panel open rather than firing an action, so it needs a
   persistent "this one is selected" state instead of just a hover cue. */
.cb-btn.active { background: transparent; }
.cb-btn.active .button_top { color: var(--button_color); }

/* Back to one distinct, fully-saturated color per action (not the muted/
   dusty tones from the previous pass, and not all-orange either) - each
   button is its own color so the whole row reads as a varied palette at a
   glance instead of blending together. Start/Stop/Delete still keep their
   inherently meaningful green/rose/red; everything else just gets its own
   spot on the wheel so no two adjacent buttons repeat a hue. */
.bf-start       { --button_color: #16a34a; }
.bf-stop        { --button_color: #e11d48; }
.bf-reboot      { --button_color: #2563eb; }
.bf-console     { --button_color: #7c3aed; }
.bf-terminal    { --button_color: #4f46e5; }
.bf-backup      { --button_color: #0891b2; }
.bf-backupfiles { --button_color: #0d9488; }
.bf-autobackup  { --button_color: #0284c7; }
.bf-resetpw     { --button_color: #db2777; }
.bf-reinstall   { --button_color: #d97706; }
.bf-upgrade     { --button_color: #9333ea; }
.bf-delete      { --button_color: #b91c1c; }
.bf-ports       { --button_color: #059669; }
.bf-files       { --button_color: #ea580c; }
.bf-log         { --button_color: #ca8a04; }
.bf-traffic     { --button_color: #65a30d; }

/* Press-and-hold delete confirmation button (deleteMyVps in app.js) - fills
   left-to-right over 3s via the --hold-pct CSS var, releasing early cancels. */
.hold-delete-btn {
    position: relative;
    width: 100%;
    padding: 15px 18px;
    border-radius: 12px;
    border: 2px solid #fecaca;
    background: #fff5f5;
    color: #b91c1c;
    font-weight: 600;
    font-size: 0.95rem;
    cursor: pointer;
    overflow: hidden;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
    --hold-pct: 0%;
}
.hold-delete-btn .hold-delete-fill {
    position: absolute;
    inset: 0;
    width: var(--hold-pct);
    background: linear-gradient(90deg, #ef4444, #b91c1c);
    z-index: 0;
}
.hold-delete-btn.holding .hold-delete-fill { transition: none; }
.hold-delete-btn:not(.holding) .hold-delete-fill { transition: width 0.2s ease; }
.hold-delete-btn .hold-delete-label {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    transition: color 0.15s ease;
}
.hold-delete-btn.holding .hold-delete-label { color: #fff; }

/* PromptPay "waiting for payment" spinner */
.pp-spinner {
    width: 14px; height: 14px; border-radius: 50%;
    border: 2px solid rgba(249, 115, 22, 0.25); border-top-color: var(--primary);
    display: inline-block; animation: ppSpin 0.8s linear infinite;
}
@keyframes ppSpin { to { transform: rotate(360deg); } }

/* =====================================================================
   NETWORK MAP (see js/networkMap.js) - real-time topology diagram shared
   by the admin "เครือข่าย" tab and the customer VPS detail page. Positions
   are computed in JS (x = depth column, y = post-order leaf slot) and
   painted as an absolutely-positioned SVG of smooth bezier curves with a
   junction dot at every real branch point, underneath plain HTML cards -
   the same visual language as UniFi's own topology view, requested
   directly by the client with UniFi topology-view screenshots as
   reference. `.netmap-canvas` is a fixed-height pan/zoom viewport (see
   networkMap.js's viewStates/fitView()/applyView()/zoomBy()) - it opens
   fitted to show the whole diagram, and a corner toolbar + ctrl/cmd+wheel +
   drag + pinch let the user zoom in/pan around from there. Each card shows
   only icon/label/status by default; metrics/notes live in a single shared
   `.nm-detail-float` popover revealed on hover/focus/click (see below) so a
   long chain still fits comfortably instead of every card being full-size.
   ===================================================================== */
/* .netmap-canvas is a real pan/zoom "porthole" onto the (often much
   wider/taller) diagram: fixed height + overflow:hidden, with
   .netmap-canvas-inner absolutely positioned inside it and moved/scaled via
   a single translate()+scale() transform kept in networkMap.js's
   viewStates (see fitView()/applyView()/zoomBy() there) - initial view
   fits the whole diagram on load, then the toolbar/wheel/drag/pinch
   handlers in attachInteractions() adjust it. cursor:grab hints that
   dragging pans; touch-action:none stops the browser's own touch
   scroll/pinch from fighting with the custom pointer-based pan/pinch
   handling on mobile. */
.netmap-canvas {
    position: relative; width: 100%; height: 480px; overflow: hidden;
    border-radius: 12px; cursor: grab; touch-action: none; background: var(--body-bg, #faf7f0);
}
.netmap-canvas.nm-grabbing { cursor: grabbing; }
.netmap-canvas-inner { position: absolute; top: 0; left: 0; }
.netmap-toolbar {
    position: absolute; top: 10px; right: 10px; z-index: 50; display: flex; gap: 4px;
    background: var(--panel-bg, rgba(255, 255, 255, 0.92)); backdrop-filter: blur(6px);
    padding: 4px; border-radius: 10px; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.14);
    border: 1px solid var(--border, #e2e8f0);
}
.nm-zoom-btn {
    width: 30px; height: 30px; border-radius: 7px; border: none; background: transparent;
    color: var(--text-strong, #1e293b); display: flex; align-items: center; justify-content: center;
    cursor: pointer; font-size: 0.8rem; transition: background .15s ease;
}
.nm-zoom-btn:hover { background: rgba(148, 163, 184, 0.2); }
.netmap-svg { position: absolute; top: 0; left: 0; pointer-events: none; overflow: visible; transition: filter .18s ease, opacity .18s ease; }
.netmap-link { fill: none; stroke: rgba(148, 163, 184, 0.45); stroke-width: 2; }
.netmap-link.flowing { stroke: rgba(34, 197, 94, 0.4); }
.netmap-branch-dot { fill: var(--primary, #f97316); }
.netmap-flow-dot { fill: #22c55e; }

.netmap-card-pos { position: absolute; width: 172px; transform: translateY(-50%); transition: filter .18s ease, opacity .18s ease; }
.netmap-card {
    position: relative; border-radius: 16px; padding: 12px 14px; background: var(--panel-bg, #fff);
    border: 1px solid var(--border, #e2e8f0); box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
    width: 100%; box-sizing: border-box; text-align: center; cursor: default;
    transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.netmap-card[tabindex] { cursor: pointer; }
.netmap-card:hover, .netmap-card:focus-within, .netmap-card.nm-open { box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12); transform: translateY(-2px); }
/* Spotlight: while any one card's popover is showing (hover, keyboard
   focus, or tapped-open via nm-open - see attachInteractions() in
   networkMap.js), blur+dim every OTHER card and the connector lines so the
   active card's detail reads clearly instead of competing with a busy
   diagram, and raise that one card above the rest so its popover always
   paints on top. :has() scoped to .netmap-canvas-inner (not :hover on the
   canvas itself) so this also fires for the click-to-pin path on touch. */
.netmap-canvas-inner:has(.netmap-card:hover, .netmap-card.nm-open) .netmap-card-pos:not(:has(.netmap-card:hover, .netmap-card.nm-open)) {
    filter: blur(2.5px); opacity: 0.45;
}
.netmap-canvas-inner:has(.netmap-card:hover, .netmap-card.nm-open) .netmap-svg { filter: blur(2.5px); opacity: 0.5; }
.netmap-card-pos:has(.netmap-card:hover, .netmap-card.nm-open) { z-index: 45; }
.nm-icon {
    width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center;
    margin: 0 auto 8px; font-size: 1.05rem; color: #fff;
    background: linear-gradient(135deg, #94a3b8, #64748b); /* unknown = default */
}
.netmap-card.status-up .nm-icon { background: linear-gradient(135deg, #4ade80, #16a34a); box-shadow: 0 8px 18px rgba(22, 163, 74, 0.28); }
.netmap-card.status-down .nm-icon { background: linear-gradient(135deg, #f87171, #dc2626); box-shadow: 0 8px 18px rgba(220, 38, 38, 0.28); }
/* Real product-photo hops (ISP logo, MikroTik, Proxmox host) - a wider flat
   strip instead of the small round icon, since a landscape product shot
   turns into an unrecognizable sliver squeezed into a 40px circle. Status
   shows as a colored border around the photo instead of a colored fill
   (the photo itself needs a plain white background to read correctly). */
.nm-icon-photo {
    width: 100%; height: 54px; border-radius: 12px; margin: 0 auto 8px; box-sizing: border-box;
    display: flex; align-items: center; justify-content: center; overflow: hidden;
    background: #fff; border: 2px solid rgba(148, 163, 184, 0.35); padding: 4px;
}
.nm-icon-photo img { max-width: 100%; max-height: 100%; object-fit: contain; }
.netmap-card.status-up .nm-icon-photo { border-color: #22c55e; }
.netmap-card.status-down .nm-icon-photo { border-color: #ef4444; }
.nm-label { font-weight: 700; font-size: 0.9rem; color: var(--text-strong, #1e293b); overflow-wrap: break-word; }
/* ISP badge (e.g. "3BB") shown next to the Internet hop's label - a small
   text badge in the ISP's brand color rather than a scraped logo image, so
   this never depends on hotlinking/storing a third-party trademarked asset. */
.nm-isp-badge { display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 999px; font-size: 0.65rem; font-weight: 800; letter-spacing: 0.02em; color: #fff; background: linear-gradient(135deg, #f7941d, #ec6608); vertical-align: middle; }
.nm-status { display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 0.76rem; color: var(--text-light, #64748b); margin-top: 3px; }
.nm-dot { width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; flex-shrink: 0; }
.netmap-card.status-up .nm-dot { background: #22c55e; box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18); }
.netmap-card.status-down .nm-dot { background: #ef4444; box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18); }

/* Single shared floating popover per canvas (see attachInteractions() /
   positionDetail() in networkMap.js) - a sibling of .netmap-canvas-inner,
   positioned in screen-space px relative to .netmap-canvas itself, so
   panning/zooming the diagram never stretches or clips it, and it doesn't
   need to live (and get pan/zoom-transformed) inside every single card.
   JS sets .style.left/.top directly and toggles .show; the --nm-arrow-x
   custom property re-aims the little pointer triangle at whichever card is
   currently active after JS clamps the box within the canvas bounds. */
.nm-detail-float {
    position: absolute; left: 0; top: 0; width: max-content; min-width: 200px; max-width: 250px; text-align: left;
    background: var(--panel-bg, #fff); border: 1px solid var(--border, #e2e8f0); border-radius: 14px;
    box-shadow: 0 16px 40px rgba(2, 6, 23, 0.18); padding: 12px 14px;
    opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .16s ease;
    z-index: 60;
}
.nm-detail-float.show { opacity: 1; visibility: visible; pointer-events: auto; }
.nm-detail-float::before {
    content: ''; position: absolute; top: -6px; left: var(--nm-arrow-x, 20px); transform: translateX(-50%) rotate(45deg);
    width: 12px; height: 12px; background: var(--panel-bg, #fff);
    border-left: 1px solid var(--border, #e2e8f0); border-top: 1px solid var(--border, #e2e8f0);
}
/* Flipped above the card instead (see positionDetail()) when there isn't
   room below within the fixed-height viewport - arrow moves to the bottom
   edge and points down instead of up. */
.nm-detail-float.nm-flip::before { top: auto; bottom: -6px; transform: translateX(-50%) rotate(225deg); }
.nm-metrics { display: flex; flex-direction: column; gap: 3px; }
.nm-metrics + .nm-note { margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border, #e2e8f0); }
.nm-metric { display: flex; justify-content: space-between; gap: 10px; font-size: 0.76rem; color: var(--text-light, #64748b); }
.nm-metric b { color: var(--text-strong, #1e293b); font-weight: 700; }
.nm-note { font-size: 0.72rem; color: #94a3b8; font-style: italic; line-height: 1.4; }

/* Respect reduced-motion preferences */
@media (prefers-reduced-motion: reduce) {
    .auth-card::before, .cnp-modal::after, .btn-primary::after, .pp-spinner { animation: none !important; }
    .stat-card.stat-card-anim, .app-container { animation: none !important; }
    .stat-ring-outer { transition: none !important; }
    .panel, .vps-card, .package-card, .infra-card, .invoice-box, .chat-panel, .summary-card.panel { animation: none !important; }
}

/* Entrance animation for every "card" surface (extends the stat-card-anim
   pattern already used on the stat row to the rest of the site) - these are
   freshly-created DOM nodes each time their container's innerHTML is
   re-rendered, so a plain CSS animation on the selector itself replays
   automatically with no JS wiring needed (same mechanism .content-section's
   fadeIn already relies on). Staggered per grid position so a row of VM
   cards or summary cards cascades in instead of popping in all at once. */
@keyframes cardFadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.panel, .vps-card, .package-card, .infra-card, .invoice-box, .chat-panel, .summary-card.panel {
    animation: cardFadeUp 0.4s cubic-bezier(.22, .9, .3, 1) both;
}
.vps-grid .vps-card:nth-child(2), .summary-grid .summary-card:nth-child(2), .infra-grid .infra-card:nth-child(2) { animation-delay: .05s; }
.vps-grid .vps-card:nth-child(3), .summary-grid .summary-card:nth-child(3), .infra-grid .infra-card:nth-child(3) { animation-delay: .1s; }
.vps-grid .vps-card:nth-child(4), .summary-grid .summary-card:nth-child(4), .infra-grid .infra-card:nth-child(4) { animation-delay: .15s; }
.vps-grid .vps-card:nth-child(5), .summary-grid .summary-card:nth-child(5), .infra-grid .infra-card:nth-child(5) { animation-delay: .2s; }
.vps-grid .vps-card:nth-child(6), .summary-grid .summary-card:nth-child(6), .infra-grid .infra-card:nth-child(6) { animation-delay: .25s; }

/* Table rows: a soft hover highlight so a dense admin table (VM list, order
   list, logs) is easier to track across many columns at a glance. */
.history-table tbody tr { transition: background-color 0.15s ease; }
.history-table tbody tr:hover { background-color: var(--surface-2); }

/* Modal "popup": a slight overshoot on the way in (classic "back" easing)
   reads as a satisfying pop instead of a flat slide/fade, without being
   cartoonish - reserved for the modal since it's the one surface on the
   site that's genuinely meant to feel like a popup. */
.cnp-modal { transition: transform 0.32s cubic-bezier(0.34, 1.56, 0.64, 1); }

/* =====================================================================
   LANGUAGE SWITCHER (see js/i18n.js)
   Shared markup/classes reused across every page's topbar/nav.
   ===================================================================== */
.i18n-switch { position: relative; }
.i18n-btn {
    display: flex; align-items: center; gap: 6px; padding: 8px 12px; border-radius: 10px;
    background: var(--surface-2); border: 1px solid var(--border); color: var(--text-main);
    cursor: pointer; font-size: 0.95rem; font-family: inherit; transition: background .2s, border-color .2s;
}
.i18n-btn:hover { background: var(--surface-3); border-color: var(--line-soft); }
.i18n-menu {
    position: absolute; right: 0; top: calc(100% + 8px); min-width: 150px; padding: 6px;
    background: var(--panel-bg); border: 1px solid var(--border); border-radius: 12px;
    box-shadow: 0 16px 40px rgba(2,6,23,0.18); opacity: 0; pointer-events: none;
    transform: translateY(-6px); transition: opacity .18s, transform .18s; z-index: 300;
}
.i18n-switch.open .i18n-menu { opacity: 1; pointer-events: auto; transform: none; }
.i18n-menu button {
    display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 9px 10px;
    border: none; background: none; border-radius: 8px; color: var(--text-main); font-family: inherit;
    font-size: 0.92rem; font-weight: 600; cursor: pointer; transition: background .15s;
}
.i18n-menu button:hover { background: var(--surface-2); }
.i18n-menu button.active { color: var(--primary); background: rgba(249,115,22,0.08); }
.topbar-tools { display: flex; align-items: center; gap: 10px; }

/* =====================================================================
   PORTAL SKIN (dark-sidebar / orange-accent redesign - brand orange kept,
   variable names below (--accent-blue/--accent-lime) are historical from
   an earlier color pass but now just resolve to orange shades)
   Appended last so it wins over the base + liquid-glass rules above.
   Applies to dashboard.html + admin.html (both share this stylesheet
   and the same .app-container/.sidebar/.topbar/.menu markup).
   ===================================================================== */
:root {
    --ink: #12151c;
    --ink-soft: #1b1f29;
    --accent-lime: #ffe3c2;
    --accent-lime-strong: #ffcf94;
    --accent-blue: #f97316;
    --accent-blue-soft: #fff1e6;
    --page-shell-bg: #ede1c8;
}
body { background: var(--page-shell-bg) !important; }
#networkCanvas { display: none; }

/* --- Full-bleed app shell (fills the browser, no floating margin/gap) --- */
.app-container {
    margin: 0;
    min-height: 100vh;
    border-radius: 0;
    overflow: hidden;
    background: var(--panel-bg);
}

.sidebar {
    background: var(--ink) !important;
    -webkit-backdrop-filter: none; backdrop-filter: none;
    border-right: none;
    box-shadow: none;
    padding: 22px 0 16px 0;
}
.brand { border-bottom: 1px solid rgba(255,255,255,0.08); text-align: left; padding: 0 22px 22px 22px; margin-bottom: 18px; display: flex; align-items: center; gap: 10px; }
.brand::before {
    content: ''; width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0;
    background: conic-gradient(from 200deg, var(--primary), var(--amber), var(--primary));
}
.logo { font-size: 1.35rem; }
.brand-slogan { margin-top: 1px; }

.menu { padding: 0 14px; gap: 2px; }
.menu a { color: #aab0be; border-radius: 14px; padding: 12px 16px; font-weight: 500; }
.menu a i { color: inherit; }
.menu a:hover { background: rgba(255,255,255,0.06); color: #fff; }
.menu a.active {
    background: var(--primary) !important;
    color: #fff !important;
    font-weight: 700;
    box-shadow: none;
}

/* --- Sidebar user card, pinned to the bottom via margin-top:auto --- */
.sidebar-user-card {
    margin: auto 14px 4px 14px;
    background: var(--ink-soft);
    border-radius: 16px;
    padding: 12px 14px;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
}
.sidebar-user-avatar {
    width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
    background: var(--accent-blue); color: #fff;
    display: flex; align-items: center; justify-content: center; font-size: 0.85rem;
}
.sidebar-user-meta { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.sidebar-user-meta strong { color: #fff; font-size: 0.86rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-user-badge {
    align-self: flex-start; background: var(--accent-lime); color: var(--ink);
    font-size: 0.66rem; font-weight: 700; padding: 2px 9px; border-radius: 999px;
}

/* --- Topbar --- */
.topbar { background: var(--panel-bg) !important; -webkit-backdrop-filter: none; backdrop-filter: none; border-bottom: 1px solid var(--border); }
.notif-bell-btn {
    background: var(--ink); border: none; cursor: pointer; position: relative;
    width: 38px; height: 38px; border-radius: 12px; color: #fff; font-size: 1rem;
    display: inline-flex; align-items: center; justify-content: center;
}
.notif-bell-btn .notif-badge {
    display: none; position: absolute; top: -3px; right: -3px; background: var(--danger); color: #fff;
    font-size: 0.62rem; font-weight: 700; border-radius: 10px; padding: 1px 5px; min-width: 15px;
}
.user-avatar { background: var(--accent-blue-soft) !important; color: var(--accent-blue) !important; box-shadow: none !important; }
.logout-btn { border-radius: 999px; }

/* --- Buttons: original orange, just pill-shaped like the video's "Add new task" --- */
.btn-primary { border-radius: 999px !important; }
.btn-outline { border-radius: 999px !important; }

/* --- Stat card row (dashboard / admin summary overview) ---
   Plain flexbox, not CSS grid auto-fit/auto-fill - in production this ended
   up leaving cards clumped narrow with a dead gap on the right (a grid
   auto-fit/auto-fill track-collapsing quirk that didn't reproduce the same
   way in preview). flex-grow on each card guarantees they always split the
   full row width evenly, with no ambiguity, while flex-wrap still lets them
   reflow to fewer per line on narrow viewports. */
.stat-row {
    display: flex;
    flex-wrap: wrap;
    gap: clamp(10px, 2vw, 16px);
    margin-bottom: 30px;
}
/* max-width caps how far flex-grow:1 can stretch each card - without it,
   a row with only 3-4 cards on a wide monitor stretches every card to fill
   the leftover space evenly, leaving a small fixed-size icon+number looking
   lost inside an oversized slab. Capped, cards grow to a comfortable size
   and then just leave gap at the end of the row instead of over-stretching. */
.stat-row > .stat-card { flex: 1 1 180px; max-width: 320px; }
.stat-row.bw-stat-row > .stat-card { flex-basis: 200px; max-width: 260px; }
.stat-row.quick-links-row > .stat-card { flex-basis: 220px; max-width: 300px; }

.stat-card {
    border-radius: 20px;
    padding: clamp(14px, 2.5vw, 18px) clamp(16px, 3vw, 20px);
    background: var(--surface-2);
    min-height: 108px;
    min-width: 0; /* grid items default to min-width:auto (content size), which
        blocks shrinking to the track width and lets long unbroken Thai
        labels overflow past the card's rounded border - this opts back in
        to normal shrink-to-fit behavior. */
    overflow: hidden; /* hard visual guarantee - whatever the track/content
        width math works out to, text can never spill past this card's own
        rounded box into a neighboring card. */
    display: flex; flex-direction: column; justify-content: center; gap: 4px;
}
.stat-card .stat-num { font-size: clamp(1.5rem, 1.15rem + 1.2vw, 1.9rem); font-weight: 700; line-height: 1.1; overflow-wrap: break-word; word-break: break-word; max-width: 100%; }
.stat-card .stat-label { font-size: 0.85rem; opacity: 0.85; overflow-wrap: break-word; word-break: break-word; max-width: 100%; }
.stat-card .stat-sub { font-size: 0.72rem; opacity: 0.65; overflow-wrap: break-word; word-break: break-word; max-width: 100%; }

.quick-link-card { text-decoration: none; cursor: pointer; text-align: center; gap: 8px; }
.quick-link-card .stat-num { color: var(--primary); font-size: 1.6rem; }
.quick-link-card .stat-label { color: var(--text-strong); font-weight: 600; }

.stat-card.blue { background: var(--accent-blue); color: #fff; }
.stat-card.blue .stat-sub { color: rgba(255,255,255,0.75); }
.stat-card.white { background: var(--panel-bg); color: var(--text-strong); border: 1px solid var(--border); }

/* ================= Category color-coding =================
   Any element with a .cat-users / .cat-server / .cat-finance / .cat-support /
   .cat-network / .cat-energy / .cat-equipment marker class sets --cat-color;
   every rule below just reads that variable, so a new colored component is
   a one-line addition rather than a new color to invent each time. */
.cat-users      { --cat-color: var(--cat-users); }
.cat-server     { --cat-color: var(--cat-server); }
.cat-finance    { --cat-color: var(--cat-finance); }
.cat-support    { --cat-color: var(--cat-support); }
.cat-network    { --cat-color: var(--cat-network); }
.cat-energy     { --cat-color: var(--cat-energy); }
.cat-equipment  { --cat-color: var(--cat-equipment); }

.menu a[class*="cat-"] i { color: var(--cat-color); opacity: 0.9; }
.menu a[class*="cat-"]:hover i { color: #fff; opacity: 1; }
.menu a[class*="cat-"].active { background: var(--cat-color) !important; }
.menu a[class*="cat-"].active i { color: #fff; opacity: 1; }

.stat-card[class*="cat-"] { border-left: 4px solid var(--cat-color); }
.stat-card[class*="cat-"] .stat-num i { color: var(--cat-color); }
.quick-link-card[class*="cat-"] .stat-num { color: var(--cat-color) !important; }
.section-title[class*="cat-"] { border-left: 4px solid var(--cat-color); padding-left: 12px; }

/* ================= Feature guide ("วิธีการใช้งาน") =================
   .guide-block picks up --cat-color from its own .cat-* marker class
   (inherited by children automatically since custom properties cascade),
   so the icon box and numbered step badges just read var(--cat-color)
   without needing their own cat-* class. */
.guide-block {
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-left: 4px solid var(--cat-color, var(--primary));
    border-radius: 20px;
    padding: 22px 24px;
    margin-bottom: 20px;
}
.guide-block-head { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
.guide-block-icon {
    width: 48px; height: 48px; border-radius: 14px; flex-shrink: 0;
    background: var(--cat-color, var(--primary)); color: #fff;
    display: flex; align-items: center; justify-content: center; font-size: 1.25rem;
}
.guide-block-text { flex: 1; min-width: 180px; }
.guide-block-text h3 { font-size: 1.1rem; margin-bottom: 4px; color: var(--text-strong); }
.guide-block-text p { font-size: 0.85rem; color: var(--text-light); }
.guide-block-cta { white-space: nowrap; text-decoration: none; }
.guide-steps { list-style: none; counter-reset: guide-step; display: flex; flex-direction: column; gap: 12px; margin: 0; padding: 0; }
.guide-steps li {
    counter-increment: guide-step;
    display: flex; gap: 12px; align-items: flex-start;
    font-size: 0.9rem; color: var(--text-main); line-height: 1.55;
}
.guide-steps li::before {
    content: counter(guide-step);
    flex-shrink: 0; width: 24px; height: 24px; border-radius: 50%;
    background: var(--cat-color, var(--primary)); color: #fff;
    font-size: 0.75rem; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
    margin-top: 1px;
}
.guide-steps li strong { color: var(--text-strong); }

.guide-block-img {
    display: block;
    width: 100%;
    max-width: 640px;
    height: auto;
    border-radius: 14px;
    border: 1px solid var(--border);
    margin: 0 0 18px 0;
    box-shadow: 0 4px 16px rgba(0,0,0,0.06);
}

.stat-card.promo {
    background: var(--accent-lime);
    color: var(--ink);
    justify-content: space-between;
}
.stat-card.promo .stat-title { font-size: 1.05rem; font-weight: 700; line-height: 1.35; overflow-wrap: break-word; }
.stat-card.promo .stat-title b { color: var(--primary-hover); }
.stat-card.promo a { color: var(--primary-hover); font-weight: 700; font-size: 0.82rem; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }

.stat-card.ring { align-items: center; text-align: center; }
.stat-ring-outer {
    width: 76px; height: 76px; border-radius: 50%;
    background: conic-gradient(var(--accent-blue) calc(var(--pct, 0) * 1%), var(--surface-3) 0);
    display: flex; align-items: center; justify-content: center; margin-bottom: 6px;
}
.stat-ring-inner {
    width: 58px; height: 58px; border-radius: 50%; background: var(--stat-ring-hole-bg, var(--panel-bg));
    display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.95rem; color: var(--text-strong);
}

/* --- Full-width top summary bar (ลูกค้า / VPS ออนไลน์ / รายรับเดือนนี้) ---
   Each figure is its own translucent pill rather than divider-separated
   columns, so auto-fit can reflow 3 → 2 → 1 per row at any width without
   a stray border-left ending up mid-row. */
.top-summary-bar {
    display: grid;
    /* Capped at 320px (was an unbounded 1fr) - with only 3 pills, 1fr made
       each one stretch to fill the whole row on a wide monitor, leaving a
       tiny fixed-size icon+number looking lost inside a huge slab of
       color. Capping the track means each pill stays a sensible,
       comfortable size and any leftover width on a very wide screen is
       just left as breathing room after the last one, instead of forcing
       everything to stretch to fill it. */
    grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 320px));
    background: linear-gradient(120deg, var(--primary), var(--primary-hover));
    color: #fff;
    border-radius: 20px;
    padding: clamp(16px, 3vw, 22px) clamp(18px, 3vw, 26px);
    margin-bottom: 20px;
    gap: 14px;
}
.tsb-item {
    display: flex; align-items: center; gap: 14px;
    background: rgba(255,255,255,0.1); border-radius: 16px; padding: 12px 16px;
    min-width: 0;
}
.tsb-icon { width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.18); display: flex; align-items: center; justify-content: center; font-size: 1.2rem; flex-shrink: 0; }
.tsb-item > div:not(.tsb-icon) { min-width: 0; flex: 1; }
.tsb-num { font-size: clamp(1.3rem, 1.05rem + 1vw, 1.6rem); font-weight: 700; line-height: 1.1; overflow-wrap: break-word; }
.tsb-label { font-size: 0.8rem; opacity: 0.85; margin-top: 2px; overflow-wrap: break-word; }
.tsb-sub { font-size: 0.74rem; opacity: 0.8; margin-top: 4px; overflow-wrap: break-word; }
.tsb-progress { width: 100%; max-width: 140px; height: 5px; border-radius: 999px; background: rgba(255,255,255,0.25); margin-top: 6px; overflow: hidden; }
.tsb-progress-fill { height: 100%; background: #fff; border-radius: 999px; transition: width 0.8s cubic-bezier(0.22, 0.9, 0.3, 1); }

/* --- Dashboard charts (revenue trend + VPS status donut) ---
   Fixed 2:1 proportion (not a repeating card grid, so auto-fit doesn't
   apply) - stacks to one column once the narrower column would drop
   below a usable width instead of at an arbitrary fixed viewport size. */
.chart-row { display: grid; grid-template-columns: minmax(0, 2fr) minmax(240px, 1fr); gap: 16px; margin-top: 4px; }
@media (max-width: 860px) { .chart-row { grid-template-columns: 1fr; } }
.chart-card h3 { font-size: 0.95rem; font-weight: 600; color: var(--text-strong); margin-bottom: 14px; border: none; padding: 0; }
.chart-canvas-wrap { position: relative; height: 220px; overflow: hidden; }
.chart-canvas-wrap-donut { height: 180px; }
.chart-canvas-wrap canvas { max-width: 100%; max-height: 100%; }

/* --- Dedicated-server equipment photos --- */
.ded-thumb {
    width: 44px; height: 44px; border-radius: 12px; object-fit: cover; flex-shrink: 0;
    background: var(--surface-2);
}
.ded-thumb-placeholder {
    display: flex; align-items: center; justify-content: center;
    color: var(--muted-3); font-size: 1.1rem;
}
.ded-thumb-lg { width: 76px; height: 76px; border-radius: 16px; }

/* --- Device catalog page (a-catalog) --- */
.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; }
.catalog-card {
    background: var(--panel-bg); border: 1px solid var(--border); border-radius: 20px;
    overflow: hidden; transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.catalog-card:hover { transform: translateY(-4px); box-shadow: 0 14px 30px rgba(15, 20, 30, 0.12); }
.catalog-card-photo { width: 100%; height: 150px; object-fit: cover; display: block; background: var(--surface-2); }
.catalog-card-photo-placeholder { display: flex; align-items: center; justify-content: center; color: var(--muted-3); font-size: 2.2rem; }
.catalog-card-body { padding: 16px 18px; }
.catalog-card-body h4 { margin: 0 0 6px; font-size: 1rem; color: var(--text-strong); }
.catalog-card-spec { font-size: 0.8rem; color: var(--text-light); margin: 0; }

/* --- Live per-node bandwidth tiles (nodeCard() / loadInfra() in admin.js) --- */
.node-bandwidth { display: flex; gap: 14px; margin: 10px 0; flex-wrap: wrap; }
.nb-item {
    display: inline-flex; align-items: center; gap: 6px;
    background: var(--surface-2); border-radius: 999px; padding: 5px 12px;
    font-size: 0.78rem; color: var(--text-light);
    transition: background-color 0.3s ease;
}
.nb-item i { font-size: 0.7rem; color: var(--primary); }
.nb-item b { color: var(--text-strong); font-weight: 600; }

/* --- Softer, more rounded corners across the board (matches the reference
   layout's rounder cards/buttons/inputs) --- */
.panel, .vps-card, .summary-card.panel, .invoice-box, .bank-details, .chart-card { border-radius: 20px; }
.package-card { border-radius: 24px; }
.form-input { border-radius: 14px; }
.btn { border-radius: 12px; }
.cnp-modal { border-radius: 26px; }
.msg-bubble { border-radius: 18px; }
.otp-inputs input { border-radius: 14px; }

/* =====================================================================
   MOTION (portal skin)
   Entrance animation for the app shell + stat-row, hover/press feedback
   on the new interactive surfaces, and a smooth fill animation on the
   progress ring. Disabled under prefers-reduced-motion (see above).
   ===================================================================== */
@property --pct {
    syntax: '<number>';
    inherits: true;
    initial-value: 0;
}
@keyframes shellIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes statCardIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* opacity-only fade (no transform) - .sidebar inside is position:fixed, and
   an ancestor with an active transform/transform-animation becomes the
   containing block for fixed descendants, silently turning "fixed relative
   to the viewport" into "fixed relative to this box" instead - which then
   scrolls away with the page along with everything else. Dropped the
   translateY from this keyframe (see above) so .app-container never
   triggers that. */
.app-container { animation: shellIn 0.5s ease both; }

.stat-card {
    transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.stat-card:hover { transform: translateY(-4px); box-shadow: 0 14px 30px rgba(15, 20, 30, 0.12); }
/* .stat-card-anim is added only on the first render of a stat-row (see
   app.js/admin.js) - auto-refresh re-renders the row every ~12s and we
   don't want the pop-in to replay every cycle, just on first paint. */
.stat-card.stat-card-anim { animation: statCardIn 0.5s cubic-bezier(0.22, 0.9, 0.3, 1) both; }
.stat-row .stat-card.stat-card-anim:nth-child(1) { animation-delay: 0.02s; }
.stat-row .stat-card.stat-card-anim:nth-child(2) { animation-delay: 0.08s; }
.stat-row .stat-card.stat-card-anim:nth-child(3) { animation-delay: 0.14s; }
.stat-row .stat-card.stat-card-anim:nth-child(4) { animation-delay: 0.2s; }

.stat-ring-outer { transition: --pct 1.1s cubic-bezier(0.22, 0.9, 0.3, 1); }

.stat-card.promo a { transition: gap 0.2s ease, color 0.2s ease; }
.stat-card.promo a i { transition: transform 0.2s ease; }
.stat-card.promo a:hover i { transform: translateX(3px); }

.menu a { transition: background-color 0.25s ease, color 0.25s ease, transform 0.15s ease; }
.menu a:active { transform: scale(0.97); }

.notif-bell-btn { transition: transform 0.2s ease, background-color 0.2s ease; }
.notif-bell-btn:hover { transform: translateY(-2px) scale(1.05); }
.notif-bell-btn:active { transform: scale(0.94); }

.sidebar-user-card { transition: background-color 0.2s ease; }
.sidebar-user-card:hover { background: rgba(255, 255, 255, 0.08); }

/* ================= Announcement popup (js/announcements.js) =================
   A centered modal injected straight into <body> by JS - not tied to
   .sidebar/.main-content/.topbar at all, so it renders identically
   whether it's shown on the landing page, the customer dashboard, or the
   admin panel, regardless of which of those layouts is on screen. */
body.cnp-announce-open { overflow: hidden; }
.cnp-announce-overlay {
    position: fixed; inset: 0; z-index: 10000;
    background: rgba(15, 18, 26, 0.6);
    display: flex; align-items: center; justify-content: center;
    padding: 20px;
}
.cnp-announce-modal {
    background: #fff; border-radius: 20px; width: 100%; max-width: 440px;
    max-height: 88vh; overflow-y: auto; position: relative;
    box-shadow: 0 24px 60px rgba(0,0,0,0.3);
}
.cnp-announce-close {
    position: absolute; top: 12px; right: 12px; z-index: 1;
    width: 32px; height: 32px; border-radius: 50%; border: none;
    background: rgba(0,0,0,0.35); color: #fff; font-size: 1.2rem; line-height: 1;
    cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.cnp-announce-close:hover { background: rgba(0,0,0,0.55); }
.cnp-announce-img { width: 100%; max-height: 240px; object-fit: cover; display: block; }
.cnp-announce-body { padding: 24px 24px 8px 24px; text-align: center; }
.cnp-announce-icon { font-size: 1.8rem; margin-bottom: 10px; }
.cnp-announce-info .cnp-announce-icon { color: #0284c7; }
.cnp-announce-success .cnp-announce-icon { color: #16a34a; }
.cnp-announce-warning .cnp-announce-icon { color: #d97706; }
.cnp-announce-danger .cnp-announce-icon { color: #dc2626; }
.cnp-announce-title { font-size: 1.15rem; font-weight: 700; margin-bottom: 8px; color: var(--text-strong); }
.cnp-announce-message { color: var(--text-light); font-size: 0.95rem; line-height: 1.6; white-space: pre-wrap; }
.cnp-announce-footer {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 16px 24px 24px 24px;
}
.cnp-announce-more { font-size: 0.8rem; color: var(--text-light); }
.cnp-announce-ack { padding: 10px 24px; border-radius: 10px; }

/* =====================================================================
   ANALYTICS DASHBOARD COMPONENTS
   Shared between the customer dashboard and admin overview page - a
   welcome banner, a richer stat-card with a colored icon avatar + trend
   pill (vs. the plain number+label stat-card used elsewhere), and a
   compact activity-feed list. Visual language borrowed from a popular
   "analytics dashboard" template pattern (icon-avatar stat cards, a
   greeting banner, a recent-activity list) but built with this site's own
   orange/paper identity and real data - no borrowed colors or demo content.
   ===================================================================== */

.welcome-banner {
    position: relative; overflow: hidden; isolation: isolate;
    background: linear-gradient(135deg, var(--amber), var(--primary) 70%);
    border-radius: 20px; padding: 26px 30px; margin-bottom: 22px;
    color: #fff; display: flex; align-items: center; justify-content: space-between; gap: 20px;
    box-shadow: 0 14px 34px rgba(234, 88, 12, 0.22);
    animation: cardFadeUp 0.45s cubic-bezier(.22, .9, .3, 1) both;
}
.welcome-banner::after {
    content: ''; position: absolute; top: -50px; right: -30px; width: 190px; height: 190px;
    border-radius: 50%; background: rgba(255, 255, 255, 0.14); z-index: -1;
}
.welcome-banner::before {
    content: ''; position: absolute; bottom: -60px; right: 90px; width: 120px; height: 120px;
    border-radius: 50%; background: rgba(255, 255, 255, 0.08); z-index: -1;
}
.welcome-banner-text h2 { font-size: 1.35rem; font-weight: 800; margin: 0 0 6px; color: #fff; border: none; padding: 0; }
.welcome-banner-text p { margin: 0; opacity: 0.94; font-size: 0.92rem; max-width: 520px; line-height: 1.5; }
.welcome-banner-art { font-size: 3.6rem; opacity: 0.3; flex-shrink: 0; }
@media (max-width: 640px) { .welcome-banner-art { display: none; } }

.stat-card-v2-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px; margin-bottom: 24px;
}
.stat-card-v2 {
    background: var(--panel-bg); border: 1px solid var(--border); border-radius: 18px;
    padding: 20px; display: flex; flex-direction: column; gap: 14px;
    transition: transform 0.25s ease, box-shadow 0.25s ease;
    animation: cardFadeUp 0.4s cubic-bezier(.22, .9, .3, 1) both;
    text-decoration: none;
}
.stat-card-v2:hover { transform: translateY(-3px); box-shadow: 0 14px 30px rgba(15, 20, 30, 0.10); border-color: var(--line-soft); }
/* Cards on dashboard/a-summary re-render on a periodic auto-refresh - JS adds
   this class from the second render onward (same gating idea as the older
   .stat-card-anim system) so the pop-in only plays once, not every refresh. */
.stat-card-v2.no-anim { animation: none; }
.stat-card-v2-grid .stat-card-v2:nth-child(2) { animation-delay: .05s; }
.stat-card-v2-grid .stat-card-v2:nth-child(3) { animation-delay: .1s; }
.stat-card-v2-grid .stat-card-v2:nth-child(4) { animation-delay: .15s; }
.stat-card-v2-top { display: flex; align-items: flex-start; justify-content: space-between; }
.stat-card-v2-icon {
    width: 46px; height: 46px; border-radius: 14px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center; font-size: 1.15rem;
}
.stat-card-v2-icon.cat-users { background: rgba(59, 130, 246, 0.14); color: #3b82f6; }
.stat-card-v2-icon.cat-server { background: rgba(249, 115, 22, 0.14); color: #f97316; }
.stat-card-v2-icon.cat-finance { background: rgba(34, 197, 94, 0.14); color: #16a34a; }
.stat-card-v2-icon.cat-network { background: rgba(6, 182, 212, 0.14); color: #0891b2; }
.stat-card-v2-icon.cat-energy { background: rgba(245, 158, 11, 0.14); color: #f59e0b; }
.stat-card-v2-icon.cat-support { background: rgba(139, 92, 246, 0.14); color: #8b5cf6; }
.stat-card-v2-trend {
    font-size: 0.76rem; font-weight: 700; padding: 3px 9px; border-radius: 999px;
    display: inline-flex; align-items: center; gap: 4px; white-space: nowrap;
}
.stat-card-v2-trend.up { background: rgba(34, 197, 94, 0.12); color: #16a34a; }
.stat-card-v2-trend.down { background: rgba(239, 68, 68, 0.12); color: #dc2626; }
.stat-card-v2-num { font-size: 1.75rem; font-weight: 800; color: var(--text-strong); line-height: 1; }
.stat-card-v2-label { font-size: 0.85rem; color: var(--text-light); margin-top: 4px; }
.stat-card-v2-link { font-size: 0.78rem; font-weight: 600; color: var(--primary); display: inline-flex; align-items: center; gap: 4px; }

/* Compact activity feed (recent orders/log entries) - an icon-avatar list
   row, same visual family as the stat-card icon avatars above. */
.activity-list { display: flex; flex-direction: column; }
.activity-row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.activity-row:last-child { border-bottom: none; padding-bottom: 0; }
.activity-row-icon {
    width: 34px; height: 34px; border-radius: 10px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center; font-size: 0.84rem;
}
.activity-row-body { flex: 1; min-width: 0; }
.activity-row-msg { font-size: 0.86rem; color: var(--text-strong); line-height: 1.4; }
.activity-row-meta { font-size: 0.74rem; color: var(--text-light); margin-top: 3px; }
