:root{
  --bg:#000; --bg2:#080a09; --panel:#0c0f0d;
  --card:rgba(255,255,255,.04); --card2:rgba(255,255,255,.06); --line:rgba(255,255,255,.09); --line2:rgba(255,255,255,.14);
  --green:#1C9136; --mint:#2FD46A; --green-dim:rgba(47,212,106,.14);
  --text:#f1f6f2; --muted:#8fa898; --muted2:#5f7468; --danger:#ff5c5c; --info:#7fb3ff;
  --display:'Unbounded',system-ui,'Segoe UI',sans-serif;
  --sans:system-ui,-apple-system,'Segoe UI',Roboto,Arial,sans-serif;
  --r:14px; --r-lg:20px; --r-xl:28px;
  /* Seam between a docked side panel (editor / modrinth-uMod) and the central
     page — widened to hold the scroll-to-top/bottom arrows (centred in it). */
  --dh-gap:60px;
}
*{box-sizing:border-box}
/* Site-wide custom scrollbar (replaces the default OS/browser bar everywhere —
   the page, scrollable panes, <textarea>s, etc.) so it matches the dark +
   mint-green theme instead of a stock grey/white bar. Firefox gets the same
   look via scrollbar-color; CodeMirror's own gutter/tab-strip scrollbars keep
   their existing narrower rules further down this file. */
*{scrollbar-width:thin;scrollbar-color:var(--line2) transparent}
::-webkit-scrollbar{width:11px;height:11px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--line2);border-radius:20px;border:2px solid var(--bg2);background-clip:padding-box}
::-webkit-scrollbar-thumb:hover{background:var(--mint);background-clip:padding-box}
::-webkit-scrollbar-corner{background:transparent}
html{-webkit-text-size-adjust:100%}
/* `clip`, not `hidden`: setting overflow-x to any scrolling value (hidden/auto/
   scroll) on an element forces its overflow-y to compute as `auto` when it would
   otherwise be `visible` (CSS Overflow §3) — on html+body that turns body into a
   non-viewport scroll container, which breaks `position:sticky` for header.top
   and .dh-notif-bar the moment the page is tall enough to actually scroll (sticky
   elements need the *viewport* to be their scrolling ancestor). `clip` still
   suppresses horizontal overflow but isn't a scrolling value, so it doesn't
   trigger that forced auto and sticky keeps working. */
html,body{overflow-x:clip}
img,canvas,svg{max-width:100%}
body{margin:0;font-family:var(--sans);color:var(--text);line-height:1.6;background:var(--bg);
  background-image:
    radial-gradient(680px 460px at 82% -8%, rgba(47,212,106,.15), transparent 60%),
    radial-gradient(560px 520px at 6% 4%, rgba(28,145,54,.12), transparent 60%);
  background-attachment:fixed;background-repeat:no-repeat;-webkit-font-smoothing:antialiased}
a{color:var(--mint);text-decoration:none}
a:hover{text-decoration:underline}
h1,h2,h3{font-family:var(--display);font-weight:700;color:#fff;letter-spacing:-.02em;line-height:1.1;margin:0}
.muted{color:var(--muted)} .tiny{font-size:13px} .mint{color:var(--mint)}

/* notification bar — sticks directly under the always-sticky header (67px tall
   at every breakpoint: 66px .top-in content + header.top's 1px border-bottom),
   so it stays pinned in view instead of scrolling away. Using 66px here left
   the notif bar 1px too high, so the header's higher z-index clipped a 1px
   sliver off the top of the notif bar while scrolling. */
.dh-notif-bar{position:sticky;top:67px;z-index:29}
.dh-notif{display:flex;gap:10px;align-items:flex-start;padding:11px 22px;
  font-size:14px;line-height:1.4;backdrop-filter:blur(12px);border-bottom:1px solid var(--line)}
.dh-notif-body{flex:1}
.dh-notif b{font-family:var(--display);font-weight:700}
.dh-notif-x{flex-shrink:0;align-self:flex-start;background:none;border:0;color:inherit;
  opacity:.65;cursor:pointer;font-size:19px;line-height:1;padding:0 2px}
.dh-notif-x:hover{opacity:1}
.dh-notif.info{background:rgba(230,196,40,.24);color:#fff3c4}
.dh-notif.warning{background:rgba(255,140,20,.26);color:#ffdcae}
.dh-notif.error{background:rgba(255,60,60,.26);color:#ffc7c7}
@media(max-width:560px){
  .dh-notif{padding:10px 14px;font-size:13px}
}

/* header */
header.top{position:sticky;top:0;z-index:30;background:rgba(0,0,0,.62);backdrop-filter:blur(14px);border-bottom:1px solid var(--line)}
.top-in{display:flex;align-items:center;gap:18px;max-width:1080px;margin:0 auto;height:66px;padding:0 22px}
.brand{display:flex;align-items:center;gap:9px;font-family:var(--display);font-weight:700;font-size:18px;color:#fff;flex-shrink:0}
.brand img{width:auto;height:31px;aspect-ratio:1631/1462}
.brand .bn{white-space:nowrap}
header.top nav{margin-left:auto;display:flex;gap:18px;align-items:center}
header.top nav a{color:var(--muted);font-size:15px;white-space:nowrap}
header.top nav a:hover{color:#fff;text-decoration:none}
/* nav links that are buttons (e.g. Регистрация) must keep button text colour —
   the generic `nav a` rule above is more specific than `.btn` and would
   otherwise paint the label muted-green on the mint pill (unreadable). */
header.top nav a.btn{color:#04140a}
header.top nav a.btn:hover{color:#04140a}
header.top nav a.btn.ghost{color:#fff}
header.top nav a.btn.ghost:hover{color:var(--mint)}
header.top nav a.btn.nav-open-panel{color:var(--mint)}
header.top nav a.btn.nav-open-panel:hover{color:#fff}

.sys-pill{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);white-space:nowrap;
  background:rgba(255,255,255,.03);border:1px solid var(--line);padding:7px 13px;border-radius:999px}
.sys-pill:hover{color:#fff;text-decoration:none}
.sys-pill .dot{width:8px;height:8px;border-radius:50%;background:var(--mint);box-shadow:0 0 8px rgba(47,212,106,.8);animation:pulse 2.4s infinite}
.sys-pill.bad .dot{background:var(--danger);box-shadow:0 0 8px rgba(255,92,92,.8)}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(47,212,106,.5)}70%{box-shadow:0 0 0 7px rgba(47,212,106,0)}100%{box-shadow:0 0 0 0 rgba(47,212,106,0)}}

.balance-pill{display:inline-flex;align-items:center;font:600 13px/1 ui-monospace,monospace;color:var(--mint);white-space:nowrap;
  background:rgba(47,212,106,.08);border:1px solid var(--line);padding:7px 13px;border-radius:999px}
.balance-pill[hidden]{display:none}
.balance-pill:hover{color:#fff;text-decoration:none;border-color:var(--mint)}
.balance-pill.neg{color:var(--danger);background:rgba(255,92,92,.08)}
.balance-pill.neg:hover{border-color:var(--danger)}
/* Balance pill sits in the header bar, right of the logo, at every width.
   The duplicate copy inside <nav> (.nav-balance) is never shown. */
.hdr-balance{display:inline-flex;margin-left:10px}
.nav-balance{display:none !important}

.nav-social{display:flex;gap:10px;align-items:center}
.nav-social>a,.nav-social>.nav-email-btn{flex:0 0 20px;width:20px;height:20px}
.nav-social a{color:var(--muted);display:flex;transition:color .15s}
.nav-social svg{display:block}
.nav-social a:hover{color:var(--mint)}
.nav-email-btn{background:none;border:0;cursor:pointer;color:var(--muted);display:flex;align-items:center;justify-content:center;transition:color .15s;padding:0;position:relative}
.nav-email-btn:hover{color:var(--mint)}
/* Masked glyph (not a filtered <img>) so the email icon takes currentColor and
   matches the Discord/Telegram SVGs exactly — muted, mint on hover. */
.nav-email-ico{display:block;width:20px;height:20px;background-color:currentColor;
  -webkit-mask:url(icons/email.svg) center/contain no-repeat;mask:url(icons/email.svg) center/contain no-repeat}
.nav-email-tip{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);background:var(--panel);border:1px solid var(--line2);border-radius:8px;padding:5px 10px;font-size:12px;color:#fff;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .2s;z-index:50}
.nav-email-tip.show{opacity:1}

/* logout confirm popover — Выйти never submits directly; it opens this panel,
   only "Подтвердить выход" inside actually signs out. */
.logout-wrap{position:relative}
.logout-confirm{position:absolute;top:calc(100% + 8px);right:0;z-index:40;min-width:210px;max-width:280px;
  background:var(--bg2);border:1px solid var(--line2);border-radius:var(--r);
  box-shadow:0 16px 30px rgba(0,0,0,.4);padding:12px}
.logout-confirm p{margin:0 0 10px;font-size:13.5px;color:var(--muted);word-break:break-word}
.logout-confirm .btn{width:100%}

/* burger toggle — hidden on desktop, shown (see mobile media query below) once
   the nav no longer fits on one line */
.burger{display:none;flex:0 0 auto;flex-direction:column;justify-content:center;align-items:center;
  gap:5px;width:38px;height:38px;margin-left:auto;padding:0;background:transparent;
  border:1px solid var(--line2);border-radius:9px;cursor:pointer}
.burger span{display:block;width:18px;height:2px;background:#fff;border-radius:2px;transition:transform .2s,opacity .2s}
.burger[aria-expanded="true"] span:nth-child(1){transform:translateY(7px) rotate(45deg)}
.burger[aria-expanded="true"] span:nth-child(2){opacity:0}
.burger[aria-expanded="true"] span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}

/* layout */
/* Content column width is a variable so the invisible edge-resize zones on an
   expanded server (.sp-width-grip) can widen/narrow it; it stays centred via margin
   auto. Default keeps the old 1080px. When a side panel (editor/Modrinth) is
   docked the cap is dropped so the column fills right up to the panel and the
   seam gap is always a uniform --dh-gap instead of scaling with the window. */
/* max-width + side paddings are transitioned so a docking side panel eases the
   central column from its centred cap to the docked width in lock-step with the
   body padding (below) — instead of max-width snapping to `none` (full width)
   first and only then shrinking. Killed mid-drag by body.dh-col-resizing. */
main.container{max-width:var(--dh-mid-w,1080px);margin:0 auto;padding:36px 22px 60px;
  transition:max-width .42s cubic-bezier(.4,0,.2,1),padding-left .32s cubic-bezier(.4,0,.2,1),padding-right .32s cubic-bezier(.4,0,.2,1)}
.auth{max-width:440px;margin:40px auto}
h1{font-size:30px} h2{font-size:21px}
.sub{color:var(--muted);margin:10px 0 0}
.row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}

/* buttons */
/* line-height:1 is load-bearing: without it <a class="btn"> inherits body's line-height
   while a native <button class="btn"> uses UA 'normal' — so a green anchor (e.g. "+ Создать
   сервер") renders ~3px taller than an adjacent ghost <button> ("Глобальные действия"). This
   is the recurring "thick green + thin black" mismatch; pinning line-height equalizes them. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;font-family:var(--display);font-weight:600;line-height:1;font-size:15px;
  padding:12px 22px;border-radius:999px;border:1px solid transparent;cursor:pointer;transition:transform .15s,background .15s,border-color .15s,box-shadow .15s;white-space:nowrap}
.btn{background:var(--mint);color:#04140a;box-shadow:0 8px 24px rgba(47,212,106,.2)}
/* `.btn{display:inline-flex}` is author CSS and beats the UA `[hidden]{display:none}`
   regardless of source order/specificity (the recurring trap in this file — see
   .pr-note-view[hidden] etc.) — needed the moment a `.btn` is ever toggled via
   the `hidden` attribute (e.g. the per-port "Отменить"/"Применить" buttons) instead
   of a JS-driven class. */
.btn[hidden]{display:none}
.btn:hover{transform:translateY(-2px);text-decoration:none;box-shadow:0 12px 32px rgba(47,212,106,.3)}
.btn.sm{padding:8px 15px;font-size:14px}
.btn.full{width:100%}
.btn.ghost{background:rgba(255,255,255,.03);border-color:var(--line2);color:#fff;box-shadow:none}
.btn.ghost:hover{border-color:var(--mint);color:var(--mint)}
.btn.danger{background:transparent;border:1px solid var(--danger);color:var(--danger);box-shadow:none}
.btn.danger:hover{background:var(--danger);color:#fff}
/* "Открыть панель" — black background, green outline (standalone console header) */
.btn.nav-open-panel{background:#000;border-color:var(--mint);color:var(--mint);box-shadow:none}
.btn.nav-open-panel:hover{background:#000;border-color:var(--mint);color:#fff;box-shadow:0 0 0 1px var(--mint)}

/* SFTP password reveal/copy controls (server settings tab) */
.sftp-pw-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.sftp-pw-text{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.5px;color:var(--text)}
.sftp-pw-btn{display:inline-flex;align-items:center;justify-content:center;padding:0;width:24px;height:24px;flex:none;
  background:transparent;border:1px solid var(--line2);border-radius:7px;color:var(--muted);cursor:pointer;
  transition:color .15s,border-color .15s}
.sftp-pw-btn:hover{color:#fff;border-color:var(--mint)}
.sftp-pw-btn svg{width:15px;height:15px}

/* cards */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:24px;margin:16px 0}
.empty{text-align:center;padding:34px;color:var(--muted)}

/* forms */
label{display:block;margin:16px 0 7px;font-weight:600;font-size:14px}
label .slider-val{float:right;color:var(--mint);font-family:var(--display);font-weight:700}
input[type=text],input[type=email],input[type=password],input[type=number],input[type=tel],select{width:100%;padding:12px 14px;border-radius:12px;
  border:1px solid var(--line2);background:#0a0d0b;color:var(--text);font-size:15px;font-family:var(--sans)}
input:focus,select:focus{outline:none;border-color:var(--mint)}
input[type=number]{appearance:textfield;-moz-appearance:textfield}
input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
select{appearance:none;-webkit-appearance:none;padding-right:38px;cursor:pointer;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238fa898' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat:no-repeat;background-position:right 13px center}
select option{background:#0a0d0b;color:#fff}
select option:disabled{color:#46564c}
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:6px;border-radius:6px;background:rgba(255,255,255,.08);padding:0}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;background:var(--mint);
  border:3px solid #06140a;box-shadow:0 0 0 1px var(--mint),0 4px 12px rgba(47,212,106,.5);cursor:pointer}
input[type=range]::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:var(--mint);border:3px solid #06140a;cursor:pointer}
/* locked-zone slider wrapper — positions a gray "already used" bar on the left */
.slider-lock-wrap{position:relative;display:flex;align-items:center}
.slider-lock-wrap input[type=range]{width:100%;position:relative;z-index:1}

/* resize-resources modal (wider than the default confirm box) */
.dh-resize-ov{align-items:flex-start;padding-top:5vh}
.dh-resize-box{width:100%;max-width:520px;max-height:88vh;overflow-y:auto}
.dh-resize-box .res-sliders{display:flex;flex-direction:column;gap:14px;margin:16px 0}
.dh-resize-box label{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;font-size:14px;font-weight:600}
.dh-resize-box .slider-val{color:var(--mint);font-family:var(--display);font-weight:700;font-size:14px}
.slider-value-edit{cursor:text;outline:0;border:0;background:transparent;padding:0;margin:0;box-shadow:none}
.slider-value-edit:focus{outline:0;border:0;box-shadow:none}
.dh-resize-box .slider-note{font-size:12px;color:var(--muted);margin-top:4px}
.dh-resize-box .disk-warn{color:#ffe08a;font-size:12px;margin-top:4px;display:none}
.dh-resize-box .disk-warn.show{display:block}
.sp-res-quote{border-top:1px solid var(--line);margin-top:4px;padding-top:14px}.sp-res-quote-head{display:flex;justify-content:space-between;align-items:center;gap:10px;font-weight:700}.sp-res-quote .price-list{margin:10px 0 8px}.sp-res-quote-fixed{border-top:1px dashed var(--line2);margin-top:5px;padding-top:7px}.sp-res-quote-total{display:block;color:var(--mint);font-family:var(--display);font-size:19px;text-align:right}.sp-res-quote-period{flex:0 0 auto}

/* schedule create/edit + task modals — overflow visible so the custom dropdowns
   inside can extend past the box instead of being clipped */
.dh-sched-box{width:100%;max-width:480px;overflow:visible}
.dh-sched-box .sch-field{margin:14px 0}
.dh-sched-box .sch-field>label{display:block;margin:0 0 6px;font-size:13px;font-weight:600;color:var(--muted)}
.dh-sched-box .sch-field input[type=text]{width:100%}
.sch-editor{margin:14px 0;padding:14px;background:#0a0d0b;border:1px solid var(--line);border-radius:12px}
.sch-editor .sch-field:first-child{margin-top:0}
.sch-time{display:flex;align-items:center;gap:8px}
.sch-time .csel{flex:1;min-width:0}
.sch-colon{color:var(--muted);font-weight:700}
.sch-preview{margin-top:12px;padding:9px 12px;border-radius:9px;background:var(--green-dim);color:var(--mint);
  font-family:var(--display);font-weight:600;font-size:14px;text-align:center}
.sch-raw{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
.sch-raw-cell label{display:block;font-size:11px;color:var(--muted);margin-bottom:4px;text-align:center}
.sch-raw-inp{width:100%;text-align:center;padding:8px 4px}
.sch-active{display:flex;align-items:center;gap:9px;margin:14px 0 0;font-size:14px;cursor:pointer}
.sch-active input{width:16px;height:16px;flex-shrink:0}
/* segmented knob (task type) + power-signal buttons */
.sch-knob{display:flex;gap:4px;background:#0a0d0b;border:1px solid var(--line2);border-radius:10px;padding:4px}
.sch-knob-btn{flex:1;border:0;background:none;color:var(--muted);font-family:var(--sans);font-weight:600;font-size:14px;
  padding:9px 8px;border-radius:7px;cursor:pointer;transition:background .15s,color .15s}
.sch-knob-btn:hover{color:#fff}
.sch-knob-btn.active{background:var(--mint);color:#04140a}
.sch-power{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.sch-power-btn{border:1px solid var(--line2);background:#0a0d0b;color:#cdd6cf;font-family:var(--sans);font-weight:600;
  font-size:14px;padding:11px 8px;border-radius:9px;cursor:pointer;transition:border-color .15s,background .15s,color .15s}
.sch-power-btn:hover{border-color:var(--mint);color:#fff}
.sch-power-btn.active{border-color:var(--mint);background:var(--green-dim);color:var(--mint)}
.sch-power-btn.danger.active{border-color:rgba(255,92,92,.6);background:rgba(255,92,92,.12);color:#ffb3b3}
.sp-drive-bar{display:flex;flex-direction:column;align-items:stretch;gap:8px;margin:4px 0 14px;min-width:0}
.sp-backup-top,.sp-drive-settings{display:flex;align-items:center;gap:8px;min-width:0}
.sp-drive-settings{position:relative;flex-wrap:wrap}
.sp-drive-btn{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;transform:none!important;box-shadow:none!important;transition:background .15s,border-color .15s,color .15s}
.sp-drive-btn:hover{transform:none!important;box-shadow:none!important}
.sp-drive-logo{width:24px;height:24px;object-fit:contain;flex:0 0 24px}
.sp-drive-name{white-space:nowrap}
.sp-drive-google{background:#fff;color:#202124;border-color:#dadce0}.sp-drive-google:hover{background:#f4f8ff!important;color:#174ea6!important;border-color:#4285f4!important}
.sp-drive-yandex{background:#fff;color:#202124;border-color:#dadce0}.sp-drive-yandex:hover{background:#f4f8ff!important;color:#174ea6!important;border-color:#4285f4!important}
.sp-drive-btn,.sp-backup-create{height:38px}.sp-backup-create{white-space:nowrap;flex:0 0 auto}.sp-drive-compact{display:none;font-size:18px;line-height:1}.sp-drive-btn.compact .sp-drive-action-wide,.sp-backup-create.compact .sp-drive-wide{display:none}.sp-drive-btn.compact .sp-drive-compact,.sp-backup-create.compact .sp-drive-compact{display:inline}.sp-backup-create.compact{width:36px;height:36px;border-radius:50%;padding:0;display:inline-flex;align-items:center;justify-content:center}
.sp-backup-hdd{position:relative;flex:1 1 180px;min-width:150px;margin:0;padding:9px 14px;border:1px solid var(--line);border-radius:999px;background:#090d0a}.sp-backup-hdd-top,.sp-backup-hdd-measure{display:flex;justify-content:space-between;gap:8px;font-size:12px;font-variant-numeric:tabular-nums}.sp-backup-hdd-measure{position:absolute;visibility:hidden;white-space:nowrap}.sp-backup-hdd-track{height:6px;background:rgba(255,255,255,.1);border-radius:999px;overflow:hidden;margin-top:7px}.sp-backup-hdd-track i{display:block;height:100%;background:var(--mint);border-radius:inherit}
.sp-drive-menu{position:absolute;z-index:30;top:42px;left:0;width:min(520px,calc(100vw - 36px));padding:12px;background:#101511;border:1px solid var(--line2);border-radius:12px;box-shadow:0 18px 45px rgba(0,0,0,.5)}
.sp-drive-account{padding:10px 0;border-bottom:1px solid var(--line)}
.sp-drive-account:last-of-type{border-bottom:0}.sp-drive-account-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.sp-drive-account-actions{justify-content:flex-end;gap:2px;flex:0 0 auto}.sp-drive-check.active{color:var(--mint);cursor:default}.sp-drive-check.active:hover{color:var(--mint);background:none}
.sp-drive-email{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
.sp-drive-add{display:inline-flex;margin:10px 0 0;transform:none!important;box-shadow:none!important}.sp-drive-add:hover{transform:none!important;box-shadow:none!important}
.sp-drive-quota{position:relative;height:24px;margin:9px 0 2px;background:#070b08;border:1px solid rgba(47,212,106,.28);border-radius:7px;overflow:hidden}.sp-drive-quota-fill{position:absolute;inset:0 auto 0 0;background:linear-gradient(90deg,rgba(47,212,106,.48),rgba(47,212,106,.3))}.sp-drive-quota span{position:absolute;z-index:1;inset:0;display:flex;align-items:center;justify-content:center;padding:0 7px;font-family:var(--sans);font-size:11px;font-weight:600;line-height:1;color:#e9f3ec}
.sp-backup-targets{display:flex;gap:7px;flex-wrap:wrap;margin:12px 0}.sp-backup-target{border:1px solid var(--line2);background:#090d0a;color:var(--muted);padding:9px 12px;border-radius:9px;cursor:pointer}.sp-backup-target.active{border-color:var(--mint);background:var(--green-dim);color:var(--mint)}.sp-backup-target.disabled{opacity:.4;cursor:not-allowed}
.sp-backup-name{display:flex;align-items:center;gap:8px;min-width:0}.sp-backup-name>span:last-child{min-width:0;overflow:hidden;text-overflow:ellipsis}.sp-backup-locs{display:inline-flex;align-items:center;gap:4px;flex:0 0 auto}.sp-backup-loc-wrap{position:relative;display:inline-flex;width:17px;height:17px;align-items:center;justify-content:center}.sp-backup-loc{width:16px;height:16px;object-fit:contain}.sp-backup-loc.local{display:inline-flex;align-items:center;justify-content:center;color:#8f9a92}.sp-backup-loc.local .btn-icon{width:16px;height:16px}.sp-backup-loc-wrap.pending .sp-backup-loc{filter:grayscale(1);opacity:.32}.sp-backup-loc-wrap.failed .sp-backup-loc{filter:grayscale(1);opacity:.38}.sp-backup-loc-spin{position:absolute;inset:1px;border:2px solid rgba(255,255,255,.3);border-top-color:var(--mint);border-radius:50%;animation:sp-drive-spin .75s linear infinite}@keyframes sp-drive-spin{to{transform:rotate(360deg)}}
.sp-backup-choices{display:flex;flex-direction:column;gap:7px;margin:14px 0}.sp-backup-choice{width:100%;display:grid;grid-template-columns:22px 30px 1fr;align-items:center;gap:11px;padding:11px 12px;text-align:left;border:1px solid var(--line2);border-radius:11px;background:#090d0a;color:var(--text);cursor:pointer;transition:border-color .15s,background .15s}.sp-backup-choice:hover{border-color:rgba(47,212,106,.55)}.sp-backup-choice.active{border-color:var(--mint);background:rgba(47,212,106,.07)}.sp-backup-choice.disabled{opacity:.4;cursor:not-allowed}.sp-backup-choice.disabled:hover{border-color:var(--line2)}.sp-backup-knob{width:20px;height:20px;border:1px solid #67706a;border-radius:5px;display:flex;align-items:center;justify-content:center}.sp-backup-knob-dot{width:10px;height:10px;border-radius:2px;background:transparent;transform:scale(.35);transition:background .15s,transform .15s}.sp-backup-choice.active .sp-backup-knob{border-color:var(--mint)}.sp-backup-choice.active .sp-backup-knob-dot{background:var(--mint);transform:scale(1)}.sp-backup-choice-logo{width:28px;height:28px;object-fit:contain}.sp-backup-choice-logo.local{display:flex;align-items:center;justify-content:center;color:#aab5ad}.sp-backup-choice-logo.local .btn-icon{width:25px;height:25px}.sp-backup-choice-copy{display:flex;flex-direction:column;gap:2px;min-width:0}.sp-backup-choice-copy strong{font-size:14px;color:#fff}.sp-backup-choice-copy small{font-size:11px;color:var(--muted)}.sp-backup-create-error{margin:-5px 0 14px;padding:10px 12px;border:1px solid rgba(255,120,120,.5);border-radius:9px;background:rgba(255,92,92,.09);color:#ffc1c1;font-size:13px;line-height:1.4}
/* Compact against the actual expanded-server content width, not the viewport:
   the same page can have a wide browser but a ~780px server pane. */
/* The HDD readout is useful only at a readable width. Before the action buttons
   themselves compact, give it a full first row so it cannot collide with them. */
@container srv (max-width:1040px){.sp-backup-hdd{min-width:0}}
@supports not (container-type:inline-size){@media(max-width:1100px){.sp-backup-hdd{min-width:0}}}
/* Preserve the compact spacing in narrow server panes; the create button itself
   switches only when its measured first row can no longer fit. */
@container srv (max-width:840px){.sp-drive-bar,.sp-backup-top{gap:6px}.sp-drive-menu{top:40px}.sp-backup-hdd{min-width:0}}
@supports not (container-type:inline-size){@media(max-width:950px){.sp-drive-bar,.sp-backup-top{gap:6px}.sp-drive-menu{top:40px}.sp-backup-hdd{min-width:0}}}
.sch-tasks{margin-top:6px;display:flex;flex-direction:column;gap:3px}
.sch-task{display:flex;align-items:center;justify-content:space-between;gap:8px}
.sch-task-x{background:none;border:0;color:var(--muted);cursor:pointer;font-size:13px;line-height:1;padding:2px 6px;border-radius:6px}
.sch-task-x:hover{color:#ffb3b3;background:rgba(255,92,92,.1)}
@media(max-width:560px){.sch-raw{grid-template-columns:repeat(2,1fr)}}

/* avatar cropper modal */
.dh-avatar-box{width:100%;max-width:340px}
.av-stage{position:relative;width:264px;max-width:100%;height:264px;margin:16px auto 0;border-radius:12px;overflow:hidden;background:#050706;touch-action:none}
.av-canvas{display:block;width:264px;height:264px;cursor:grab;touch-action:none}
.av-canvas:active{cursor:grabbing}
/* rounded-square framing guide overlaid on the square canvas — matches the
   panel avatar's actual shape (.sp-set-avatar: 16px radius on a 116px box,
   i.e. 16/116 × 264 ≈ 36px at this 264px stage), not a circle */
.av-ring{position:absolute;inset:0;pointer-events:none;box-shadow:0 0 0 9999px rgba(0,0,0,.35);border-radius:36px}
.av-controls{display:flex;align-items:center;gap:12px;margin-top:14px}
.av-zoom{flex:1;-webkit-appearance:none;appearance:none;height:5px;border-radius:999px;background:var(--line2);outline:none}
.av-zoom::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:var(--mint);cursor:pointer;box-shadow:0 0 8px rgba(47,212,106,.5)}
.av-zoom::-moz-range-thumb{width:18px;height:18px;border:0;border-radius:50%;background:var(--mint);cursor:pointer}

/* clone-server modal — same box/slider chrome as resize, plus a name field and
   a read-only "Тип сервера: ..." line (type can't be changed, only name/resources). */
.dh-clone-box{max-width:560px}
.dh-clone-box .clone-name{margin-bottom:14px}
.dh-clone-box .clone-name label{display:block;color:var(--muted);font-size:13px;margin:0 0 7px;font-weight:600}
.dh-clone-box .clone-name input{width:100%}
.dh-clone-box .clone-type-info{margin:-6px 0 16px}

/* new-server configurator: build selectors + custom logo dropdown (mirrors landing) */
/* Stage-1 server-type picker: looped carousel (drag or arrow buttons), one big
   logo+title focused at a time. JS renders a window of BUFFER items either side
   of the active one (see initGroupCarousel) so the strip never runs out to drag. */
.cfg-carousel{display:flex;align-items:center;justify-content:center;gap:16px;margin:0 0 6px;user-select:none;min-width:0}
.cfg-car-arrow{flex:0 0 auto;width:38px;height:38px;border-radius:50%;background:#0a0d0b;color:#cfe;
  border:1px solid var(--line2);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:border-color .15s,color .15s,transform .1s}
.cfg-car-arrow:hover{border-color:var(--mint);color:#fff}
.cfg-car-arrow:active{transform:scale(.92)}
.cfg-car-arrow svg{width:18px;height:18px;display:block;pointer-events:none}
/* min-width:0 lets this shrink below its 280px target on narrow panels — without it,
   a grid-item ancestor's automatic min-width blows the panel wider than the viewport
   (grid/flex "min-content" overflow bug). */
.cfg-car-viewport{overflow:hidden;width:100%;max-width:280px;min-width:0;touch-action:pan-y}
.cfg-car-track{display:flex;cursor:grab}
.cfg-car-track.dragging{cursor:grabbing}
.cfg-car-item{flex:0 0 100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;
  padding:22px 10px 6px;box-sizing:border-box}
.cfg-car-logo{width:88px;height:88px;object-fit:contain;opacity:.4;transform:scale(.8);pointer-events:none;-webkit-user-drag:none;
  filter:drop-shadow(0 10px 26px rgba(47,212,106,.12));transition:opacity .38s cubic-bezier(.22,.9,.32,1),transform .38s cubic-bezier(.22,.9,.32,1)}
.cfg-car-title{font-family:var(--display);font-weight:700;font-size:19px;color:#7d9b8c;text-align:center;
  opacity:.5;transition:opacity .38s,color .38s}
.cfg-car-item.active .cfg-car-logo{opacity:1;transform:scale(1);filter:drop-shadow(0 10px 26px rgba(47,212,106,.3))}
.cfg-car-item.active .cfg-car-title{opacity:1;color:#fff}
.cfg-car-dots{display:flex;align-items:center;justify-content:center;gap:2px;margin-bottom:2px}
/* Each dot is a real <button> with a larger invisible hit area (16x16) around the
   visible 6px circle, so they're comfortably clickable/tappable, not just decorative. */
.cfg-car-dot{width:16px;height:16px;padding:0;border:0;background:transparent;display:flex;
  align-items:center;justify-content:center;cursor:pointer}
.cfg-car-dot::after{content:'';width:6px;height:6px;border-radius:50%;background:var(--line2);
  transition:background .2s,transform .2s;display:block}
.cfg-car-dot:hover::after{background:#9fb}
.cfg-car-dot.active::after{background:var(--mint);transform:scale(1.3)}
/* Stage 1 only: the «1 Тип сервера» label lives below the carousel/dots and is pulled
   up to overlap that footer whitespace, instead of sitting in its own row above the
   carousel like stages 2/3's headers — trims the stage's overall height. Left-anchored
   (not centered) to match stages 2/3's headers. */
.cfg-step-h-type{margin:-9px 0 0}
.cfg-selects{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px}
.cfg-sel label{display:block;color:var(--muted);font-size:13px;margin:0 0 7px;font-weight:600}
.csel{position:relative}
.csel-trigger{width:100%;display:flex;align-items:center;gap:10px;background:#0a0d0b;color:#fff;border:1px solid var(--line2);
  border-radius:12px;padding:11px 36px 11px 12px;font-size:15px;font-family:var(--sans);cursor:pointer;text-align:left;min-height:46px}
.csel.open .csel-trigger,.csel-trigger:focus{outline:none;border-color:var(--mint)}
.csel-logo{width:26px;height:26px;object-fit:contain;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(47,212,106,.4))}
.csel-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.csel-caret{position:absolute;right:14px;top:50%;width:9px;height:9px;border-right:2px solid var(--muted);
  border-bottom:2px solid var(--muted);transform:translateY(-70%) rotate(45deg);transition:transform .15s}
.csel.open .csel-caret{transform:translateY(-30%) rotate(225deg)}
.csel-menu{position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:25;background:#0c0f0d;border:1px solid var(--line2);
  border-radius:12px;padding:6px;box-shadow:0 18px 44px rgba(0,0,0,.65);max-height:300px;overflow:auto}
/* opens upward when there isn't room below in the viewport (JS adds .up) */
.csel.up .csel-menu{top:auto;bottom:calc(100% + 6px)}
.csel-opt{display:flex;align-items:center;gap:11px;padding:9px 10px;border-radius:9px;cursor:pointer;font-size:15px;color:#fff}
.csel-opt:hover{background:rgba(255,255,255,.06)}
.csel-opt.sel{background:var(--green-dim);color:var(--mint)}
.csel-opt.disabled{opacity:.3;cursor:not-allowed}
.csel-opt.disabled:hover{background:transparent}
.csel-group{padding:9px 10px 4px;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.csel-group:not(:first-child){margin-top:4px;border-top:1px solid var(--line2);padding-top:9px}
.csel-disabled{opacity:.5}
.csel-disabled .csel-trigger{cursor:not-allowed}
/* Collapsible group headers for makeVersionSel's «Любое ядро» version tree. */
.csel-group-tree{cursor:pointer;display:flex;align-items:center;gap:6px;user-select:none;border-radius:7px}
.csel-group-tree:hover{color:#fff;background:rgba(255,255,255,.04)}
.csel-group-chev{display:inline-block;font-size:9px;transition:transform .15s ease}
.csel-group-chev.open{transform:rotate(90deg)}
.csel-opt-child{padding-left:22px}
.dh-mr-vgate .csel{max-width:220px;margin:10px auto 0;text-align:left}
@media(max-width:560px){ .cfg-selects{grid-template-columns:1fr} }

/* new-server: staged two-column configurator — visually identical to the landing
   page's (panel of numbered stages + sticky summary sidebar). */
/* Header row mirrors .config's three columns (panel / modpack-mid / summary) so
   each box gets a title that lines up with it exactly: h1 uses the same
   flex-basis as .panel, .cfg-head-modpack the same as .modpack-mid (including
   its open/closed grow animation), .cfg-head-price the same as .summary. Same
   gap as .config so the math matches at any viewport width. On the mobile
   stacked layout this row makes no sense (titles would sit far above their
   boxes) — JS reparents .cfg-head-modpack/.cfg-head-price back next to their
   own box there (see placeHeaderMids() in new_server.ejs). */
.cfg-head{margin:0 0 26px;display:flex;align-items:center;gap:22px;flex-wrap:wrap}
.cfg-head h1{font-size:clamp(26px,3.4vw,38px);color:#fff;flex:1.55 1 340px;min-width:0}
.cfg-head-modpack{flex:0 1 0px;min-width:0;opacity:0;overflow:hidden;max-height:0;
  display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;
  transition:flex-grow .42s cubic-bezier(.4,0,.2,1),flex-basis .42s cubic-bezier(.4,0,.2,1),
    opacity .3s ease,max-height .42s cubic-bezier(.4,0,.2,1)}
.cfg-head-modpack.open{flex:2 1 520px;opacity:1;max-height:none;overflow:visible}
/* On the shared header row (desktop) the source buttons must stay beside the
   title, never wrap under it: `overflow:visible` above applies the instant
   `.open` is added (it isn't one of the transitioned properties), so while
   flex-basis is still animating from 0→520px the row is already un-clipped —
   with the default flex-wrap:wrap that meant the buttons visibly wrapped onto
   their own line for the first part of the animation, then snapped back up
   once the column had grown wide enough to fit everything on one line. Force
   nowrap here (title truncates via ellipsis in the very unlikely event of a
   still-too-narrow row instead of wrapping/overflowing). Scoped to `.cfg-head`
   specifically — the mobile-reparented copy inside `.modpack-section` (below)
   is much narrower and wrapping there is the better fallback. */
.cfg-head > .cfg-head-modpack{flex-wrap:nowrap}
.cfg-head > .cfg-head-modpack .modpack-section-h{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.cfg-head > .cfg-head-modpack .cfg-modpack-sources{flex-shrink:0}
.cfg-head-price{font-family:var(--display);font-weight:700;letter-spacing:-.02em;line-height:1.1;
  color:#fff;font-size:clamp(26px,3.4vw,38px);flex:1 1 280px;min-width:0}
/* Reparented onto mobile (inside their own box instead of the shared header row)
   — reset the row-mirroring flex-basis, which would otherwise size HEIGHT inside
   .modpack-section's column flow instead of width. */
.modpack-section .cfg-head-modpack{flex:0 0 auto;opacity:1;max-height:none;overflow:visible;margin-bottom:12px}
/* Sibling BEFORE .summary now (placeHeaderMids in new_server.ejs), not a child
   inside it — .summary is itself the bordered/background box (unlike
   .modpack-section above, a bare flex wrapper around the real box .mpb), so a
   child here would render padded/boxed-in alongside the price instead of
   sitting on top of the box like every other section heading on this page.
   Only ever matches once JS has actually reparented it under .config — in
   the true row layout it lives in .cfg-head instead, this selector doesn't
   see it there at all. */
.config > .cfg-head-price{flex:0 0 auto;width:100%;max-width:680px;margin:0 auto 14px}
/* "Скопировать сервер" dropdown — sits under «Создать и запустить», so it's a
   full-width control and the menu matches the button's width exactly. */
.copy-existing{position:relative;width:100%}
.copy-existing .btn{width:100%;display:inline-flex;align-items:center;justify-content:center;gap:8px}
.copy-existing-menu{position:absolute;left:0;right:0;top:calc(100% + 8px);z-index:30;width:100%;
  max-height:360px;overflow-y:auto;background:#0c0f0d;border:1px solid var(--line2);border-radius:12px;padding:8px;
  box-shadow:0 20px 50px rgba(0,0,0,.6)}
.cem-head{padding:8px 10px 6px;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.cem-item{display:flex;align-items:center;gap:11px;padding:9px 10px;border-radius:9px;color:#fff;cursor:pointer;text-decoration:none}
.cem-item[hidden]{display:none}
.cem-item:hover{background:rgba(47,212,106,.1)}
.cem-item img{width:24px;height:24px;object-fit:contain;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(47,212,106,.4))}
.cem-item span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cem-empty{padding:12px 10px;text-align:center;color:var(--muted);font-size:13px}
.cem-empty[hidden]{display:none}
/* Flex, not grid: the «Сборка модов» mid column (below) needs to grow/shrink its
   own width while its panel/summary siblings reflow around it every animation
   frame — grid tracks don't renegotiate like that, flex-basis/flex-grow do.
   nowrap: the row's 3-column widths only add up at ≥1761px viewports (below
   that, the 861-1760px tier further down forces a full column stack instead)
   — flex-wrap:wrap used to let the browser split panel/modpack-mid/summary
   across two lines on its own whenever they didn't fit, which produced an
   unpredictable HYBRID layout (2 items on one line, 1 on the next, in either
   order depending on their exact frozen pixel widths) instead of a clean
   single-column stack. */
.config{display:flex;flex-wrap:nowrap;gap:22px;align-items:flex-start}
/* min-width:0 overrides the flex item's automatic min-width:auto, which otherwise
   sizes off the widest descendant's min-content (the stage-1 carousel row) and can
   blow the panel wider than its share of the row on narrow viewports. */
.config .panel{background:var(--card);border:1px solid var(--line);border-radius:var(--r-xl);padding:30px;min-width:0;flex:1.55 1 340px}
.cfg-step+ .cfg-step{margin-top:30px;padding-top:30px;border-top:1px solid var(--line)}
.cfg-step-h{display:flex;align-items:center;gap:10px;font-family:var(--display);font-weight:600;color:#fff;margin-bottom:18px;font-size:17px}
.cfg-step-h .num{width:24px;height:24px;border-radius:50%;background:var(--green-dim);color:var(--mint);font-size:13px;
  display:flex;align-items:center;justify-content:center}
.res+ .res{margin-top:24px}
.res-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px}
.res-head .name{color:var(--muted);font-size:15px}
.res-head .val{font-family:var(--display);font-weight:700;font-size:24px;color:#fff;font-variant-numeric:tabular-nums}
.minmax{display:flex;justify-content:space-between;color:var(--muted2);font-size:12px;margin-top:8px;font-variant-numeric:tabular-nums}
/* CS2 shared-base overlay badge — big green letters right after the NVMe value */
.res-right{display:flex;align-items:baseline;gap:12px}
.ov-badge{position:relative;color:var(--mint);font-family:var(--display);font-weight:700;
  font-size:24px;line-height:1;white-space:nowrap}
.ov-badge[hidden]{display:none}
.ov-info{vertical-align:super;margin-left:2px;background:transparent;border:0;color:var(--mint);
  font-family:var(--sans);font-weight:700;font-size:12px;line-height:1;cursor:pointer;padding:0}
.ov-info:hover{opacity:.75}
.ov-pop{position:absolute;right:0;top:calc(100% + 8px);z-index:30;width:290px;white-space:normal;text-align:left;
  background:#0c0f0d;border:1px solid var(--line2);border-radius:12px;padding:11px 13px;color:var(--muted);
  font-family:var(--sans);font-weight:400;font-size:13px;line-height:1.5;box-shadow:0 18px 44px rgba(0,0,0,.65)}
.ov-pop[hidden]{display:none}
@media(max-width:560px){
  /* On a phone the popup is anchored to the tiny badge (right:0, width:290px),
     so its left edge ran off the screen ("half off-screen"). Anchor it to the
     full-width slider row instead and cap its width to the viewport so it can't
     overflow either edge. */
  .res{position:relative}
  .ov-badge{position:static}
  .ov-pop{width:min(290px, calc(100vw - 40px))}
}
/* «Ассистент DristHosting» toggle — sits in the top-right corner of the «Ядро»
   stage header (label + ⓘ + switch). */
.cfg-step-h-core{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.cfg-step-h-title{display:inline-flex;align-items:center;gap:10px}
.cfg-assist{display:inline-flex;align-items:center;gap:8px}
.cfg-assist[hidden]{display:none}
.cfg-assist-label{color:var(--muted);font-size:13.5px;font-weight:600;white-space:nowrap;letter-spacing:0}
/* «Сборка модов» mode. The loader/version/java row is genuinely hidden until a
   pack is picked (the explicit rule below is required — `.cfg-selects{display:grid}`
   otherwise beats the UA `[hidden]{display:none}` at equal specificity). Once a pack
   is picked it reappears with loader+version read-only (.csel-readonly) and Java
   still editable. */
.cfg-selects[hidden]{display:none}
.csel-readonly .csel-caret{display:none}
.csel-readonly .csel-trigger{cursor:default;background:var(--bg2);border-style:dashed}
.cfg-modpack-hint{margin:6px 0 0;color:var(--muted);font-size:13.5px}
.cfg-modpack-hint[hidden]{display:none}
.cfg-modpack-hint-warn{color:var(--danger);font-weight:600}
.cfg-modpack-selected{display:flex;align-items:center;gap:12px;padding:12px;margin-top:2px;
  background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg)}
.cfg-modpack-selected[hidden]{display:none}
.cfg-modpack-selected-icon{width:48px;height:48px;border-radius:10px;object-fit:cover;background:#0a0d0b;flex-shrink:0}
.cfg-modpack-selected-info{flex:1;min-width:0}
.cfg-modpack-selected-title{font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cfg-modpack-selected-version{color:var(--muted);font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cfg-modpack-remove{background:none;border:0;color:var(--muted);font-size:15px;cursor:pointer;padding:6px 10px;border-radius:8px;flex-shrink:0}
.cfg-modpack-remove:hover{background:var(--danger);color:#fff}

/* «Сборка модов» mid column: sits BETWEEN the configurator panel and the price
   summary, pushing both to the sides as it grows in rather than unhiding a block
   below them. `.open` is the only state driven by JS ([hidden] would just cut the
   width/opacity transition short); flex-basis+flex-grow animate together so the
   panel/summary siblings reflow smoothly every frame without needing their own
   transitions. Height is pinned to the panel's via JS (syncModpackHeight) so this
   column's own content scrolls internally instead of stretching the row. */
/* max-height:0 while closed keeps this item's cross-axis (row) contribution at
   zero too — without it, once the browser has been opened once, the results
   grid mounted inside stays in the DOM (only the pick, not the markup, is
   cleared on close) and its real content height would inflate the row even at
   zero width/opacity. `.open` lifts the cap so the JS-set exact height (synced
   to the panel) isn't fought by it. */
.modpack-mid{flex:0 1 0px;min-width:0;opacity:0;overflow:hidden;max-height:0;
  transition:flex-grow .42s cubic-bezier(.4,0,.2,1),flex-basis .42s cubic-bezier(.4,0,.2,1),
    opacity .3s ease,max-height .42s cubic-bezier(.4,0,.2,1)}
/* overflow stays hidden only while closed/animating — needed to clip the collapsing
   width down to nothing. Once open it must switch to visible, or the filter dropdown
   menus (position:absolute, can be wider than the column) get clipped at its edge. */
.modpack-mid.open{flex:2 1 520px;opacity:1;max-height:none;overflow:visible}
/* Content stays invisible until the column's width animation has actually
   finished (JS adds .ready on the .modpack-mid transitionend — see
   new_server.ejs's setCoreMode) — the card grid re-wrapping its text at every
   intermediate width while .modpack-mid is still growing is what made the open
   animation look rough; hiding it for those ~0.4s and fading it in only once
   the column has settled removes the visible reflow entirely. */
.modpack-section{height:100%;display:flex;flex-direction:column;min-width:420px;
  opacity:0;transition:opacity .2s ease}
.modpack-section.ready{opacity:1}
/* Title text itself — sized like the page's own «Создать сервер» h1. Its wrapper
   (.cfg-head-modpack, styled above near .cfg-head) is what actually gets
   positioned/animated, either in the shared header row or reparented next to
   this box on mobile. */
.modpack-section-h{font-family:var(--display);font-weight:700;letter-spacing:-.02em;line-height:1.1;color:#fff;font-size:clamp(26px,3.4vw,38px)}
.cfg-modpack-sources{display:flex;gap:8px}
.cfg-modpack-src-btn{display:flex;align-items:center;gap:9px;padding:9px 16px;
  background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);cursor:pointer;color:var(--muted);font-family:var(--sans);font-weight:600;font-size:14px}
.cfg-modpack-src-btn:hover{border-color:var(--line2);background:var(--card2);color:#fff}
.cfg-modpack-src-btn.active{border-color:var(--mint);background:var(--green-dim);color:var(--mint)}
.cfg-modpack-src-btn img{width:22px;height:22px;object-fit:contain}

/* Inline modpack browser (public/modpack-browser.js) — now the width of the mid
   column rather than the full page, so it's a flex column filling that column's
   height (pinned to the configurator panel's, see syncModpackHeight) with its
   results/detail panes scrolling internally instead of a viewport-relative cap. */
.cfg-modpack-browser{flex:1 1 auto;min-height:0;display:flex}
.cfg-modpack-browser[hidden]{display:none}
.mpb{border:1px solid var(--line);border-radius:var(--r-xl);background:var(--bg2);flex:1 1 auto;min-width:0;display:flex;flex-direction:column}
/* column-reverse (not a DOM reorder — the browser still appends search then
   filters) puts the LAST child on top: filters row first, search full-width
   underneath. */
.mpb-toolbar{display:flex;flex-direction:column-reverse;gap:10px;padding:12px;border-bottom:1px solid var(--line);background:var(--panel);border-radius:var(--r-xl) var(--r-xl) 0 0}
.mpb-search{width:100%;box-sizing:border-box;background:#0a0d0b;border:1px solid var(--line2);border-radius:10px;
  padding:10px 13px;color:var(--text);font-size:14px;font-family:var(--sans)}
.mpb-search:focus{outline:none;border-color:var(--mint)}
.mpb-filters{display:flex;gap:8px}
/* custom multi-select filter dropdown (icons + checkboxes) */
.mpb-dd{position:relative}
.mpb-dd-trig{display:inline-flex;align-items:center;gap:7px;box-sizing:border-box;height:40px;padding:0 11px;cursor:pointer;
  background:#0a0d0b;border:1px solid var(--line2);border-radius:10px;color:var(--text);font-family:var(--sans);font-size:13px;white-space:nowrap}
.mpb-dd-trig:hover{border-color:var(--muted2)}
.mpb-dd-trig.has-sel{border-color:var(--mint);color:var(--mint)}
.mpb-dd.open .mpb-dd-trig{border-color:var(--mint)}
.mpb-dd-ic{display:inline-flex;width:16px;height:16px;opacity:.85}
.mpb-dd-ic svg,.mpb-ic svg{width:100%;height:100%;fill:currentColor;display:block}
.mpb-dd-caret{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid currentColor;opacity:.7}
.mpb-dd-menu{position:absolute;z-index:20;top:calc(100% + 5px);left:0;min-width:210px;max-height:320px;overflow:auto;
  background:var(--panel);border:1px solid var(--line2);border-radius:12px;padding:5px;box-shadow:0 18px 44px rgba(0,0,0,.5)}
.mpb-dd-empty{padding:12px;text-align:center;color:var(--muted);font-size:13px}
.mpb-dd-opt{display:flex;align-items:center;gap:9px;padding:8px 9px;border-radius:8px;cursor:pointer;color:var(--text);font-size:13px}
.mpb-dd-opt:hover{background:var(--card2)}
.mpb-dd-opt.sel{color:var(--mint)}
.mpb-dd-box{flex:0 0 auto;width:16px;height:16px;border-radius:5px;border:1.5px solid var(--line2);position:relative}
.mpb-dd-box.on{background:var(--mint);border-color:var(--mint)}
.mpb-dd-box.on::after{content:"";position:absolute;left:4.5px;top:1.5px;width:4px;height:8px;border:solid #06210f;border-width:0 2px 2px 0;transform:rotate(45deg)}
.mpb-dd-oic{width:18px;height:18px;flex:0 0 auto;object-fit:contain;color:var(--muted)}
.mpb-dd-oic svg{width:100%;height:100%;fill:currentColor}
.mpb-dd-otext{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media(max-width:720px){ .mpb-filters{flex-wrap:wrap} }

.mpb-body{position:relative;overflow:hidden;border-radius:0 0 var(--r-xl) var(--r-xl);flex:1 1 auto;min-height:0;display:flex}
.mpb-results,.mpb-detail{flex:1 1 auto;min-height:0;width:100%;overflow:auto}
.mpb-results{display:grid;grid-template-columns:1fr;gap:8px;padding:10px}
.mpb-results[hidden]{display:none}
.mpb-detail{padding:18px}
.mpb-detail[hidden]{display:none}
.mpb-msg{grid-column:1/-1;padding:30px 12px;text-align:center;color:var(--muted)}
/* result card */
.mpb-card{display:flex;align-items:center;gap:12px;padding:11px;border-radius:12px;cursor:pointer;border:1px solid var(--line);background:var(--card)}
.mpb-card:hover{border-color:var(--mint);background:var(--card2)}
.mpb-ricon{width:52px;height:52px;border-radius:10px;flex-shrink:0;object-fit:cover;background:#0a0d0b}
.mpb-rinfo{flex:1;min-width:0}
.mpb-rtitle{font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mpb-rdesc{color:var(--muted);font-size:12.5px;line-height:1.4;margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.mpb-rmeta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:7px}
.mpb-rstats{display:flex;align-items:center;gap:11px}
.mpb-stat{display:inline-flex;align-items:center;gap:4px;color:var(--muted2);font-size:12px;font-weight:600}
.mpb-ic{display:inline-flex;width:14px;height:14px;color:var(--mint);opacity:.9}
.mpb-rtags{display:flex;gap:5px;flex-wrap:wrap;min-width:0}
.mpb-chip{font-size:11px;color:var(--muted);background:rgba(255,255,255,.05);border-radius:6px;padding:2px 7px;white-space:nowrap}
/* detail view */
.mpb-back{display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.03);border:1px solid var(--line2);
  border-radius:10px;color:#fff;font-family:var(--sans);font-size:13px;font-weight:600;cursor:pointer;padding:8px 14px;margin-bottom:14px}
.mpb-back:hover{border-color:var(--mint);color:var(--mint)}
.mpb-dhead{display:flex;gap:14px;align-items:flex-start}
.mpb-dhead .mpb-pick{margin-left:auto;flex-shrink:0}
.mpb-dicon{width:64px;height:64px;border-radius:14px;object-fit:cover;background:#0a0d0b;flex-shrink:0}
.mpb-dhinfo{flex:1;min-width:0}
.mpb-dtitle{font-family:var(--display);font-size:20px;font-weight:700;color:#fff;line-height:1.2}
.mpb-dsummary{color:var(--text);font-size:14px;margin:6px 0 0;line-height:1.45}
.mpb-ddesc{color:var(--muted);font-size:13.5px;line-height:1.6;margin:16px 0;border-top:1px solid var(--line);padding-top:16px;overflow-wrap:break-word}
.mpb-ddesc img{max-width:100%;height:auto;border-radius:8px;margin:6px 0}
.mpb-ddesc h1,.mpb-ddesc h2,.mpb-ddesc h3{color:#fff;font-family:var(--display);margin:16px 0 8px;line-height:1.25}
.mpb-ddesc h1{font-size:19px} .mpb-ddesc h2{font-size:17px} .mpb-ddesc h3{font-size:15px}
.mpb-ddesc a{color:var(--mint);text-decoration:none} .mpb-ddesc a:hover{text-decoration:underline}
.mpb-ddesc p{margin:8px 0} .mpb-ddesc ul,.mpb-ddesc ol{margin:8px 0;padding-left:22px} .mpb-ddesc li{margin:3px 0}
.mpb-ddesc code{background:rgba(255,255,255,.07);border-radius:5px;padding:1px 5px;font-size:12.5px}
.mpb-ddesc pre{background:#0a0d0b;border:1px solid var(--line2);border-radius:10px;padding:12px;overflow:auto;margin:10px 0}
.mpb-ddesc pre code{background:none;padding:0}
.mpb-ddesc blockquote{border-left:3px solid var(--line2);margin:10px 0;padding:2px 0 2px 14px;color:var(--muted2)}
.mpb-ddesc table{border-collapse:collapse;margin:10px 0;font-size:12.5px} .mpb-ddesc td,.mpb-ddesc th{border:1px solid var(--line);padding:5px 9px}
.mpb-col-h{color:var(--muted2);text-transform:uppercase;letter-spacing:.1em;font-size:11px;margin:6px 0 8px}
.mpb-vlist{display:flex;flex-direction:column;gap:4px}
.mpb-vrow{display:flex;align-items:center;gap:10px;text-align:left;background:var(--card);border:1px solid transparent;border-radius:10px;
  padding:10px 12px;color:#fff;font-family:var(--sans);font-size:13px;cursor:pointer}
.mpb-vrow:hover{border-color:var(--mint);background:var(--card2)}
.mpb-vrow-name{flex:0 1 auto;min-width:40px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mpb-vrow-meta{flex:1;min-width:0;color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mpb-vrow-badge{flex:0 0 auto;font-size:11px;font-weight:600;color:var(--mint);background:var(--green-dim);border-radius:6px;padding:2px 7px}
/* «Выбрать ▾» split pick control — main click picks the newest version/file,
   caret opens a dropdown of all of them (replaces the old «Версии» detail tab,
   .mpb-vrow rows above are reused unchanged inside .mpb-pick-menu). Sits at the
   right edge of a result card and, identically, of the detail header. */
.mpb-pick{position:relative;display:flex;flex-shrink:0}
.mpb-pick-main{font-family:var(--sans);font-size:12.5px;font-weight:700;color:#06210f;background:var(--mint);
  border:1px solid var(--mint);border-radius:9px 0 0 9px;padding:0 13px;height:34px;cursor:pointer;white-space:nowrap}
.mpb-pick-main:hover{filter:brightness(1.08)}
.mpb-pick-main:disabled{opacity:.6;cursor:default}
.mpb-pick-caret{display:flex;align-items:center;justify-content:center;width:26px;height:34px;flex-shrink:0;
  background:var(--mint);border:1px solid var(--mint);border-left:1px solid rgba(6,33,15,.35);border-radius:0 9px 9px 0;cursor:pointer;color:#06210f}
.mpb-pick-caret svg{width:12px;height:12px;fill:currentColor;display:block}
.mpb-pick.open .mpb-pick-caret{background:#eafff2}
.mpb-pick-menu{position:absolute;z-index:20;top:calc(100% + 6px);right:0;min-width:340px;max-width:460px;max-height:320px;overflow:auto;
  background:#0c0f0d;border:1px solid var(--line2);border-radius:12px;padding:6px;box-shadow:0 18px 44px rgba(0,0,0,.55)}
/* .mpb-vlist{display:flex} is author CSS and beats the UA [hidden]{display:none}
   regardless of source order/specificity (same trap as .cfg-selects[hidden] in
   new_server.ejs) — .mpb-pick-menu carries that class for its flex-column list
   styling, so it needs its own explicit override while menu.hidden is true. */
.mpb-pick-menu[hidden]{display:none}
/* infinite-scroll load-more indicator */
.mpb-more{grid-column:1/-1;padding:14px;text-align:center;color:var(--muted);font-size:13px}
/* version-tree filter (groups + leaves) */
.mpb-dd-tree{min-width:250px}
.mpb-dd-box.part{background:var(--green-dim);border-color:var(--mint)}
.mpb-dd-box.part::after{content:"";position:absolute;left:3px;top:6px;width:8px;height:0;border-top:2px solid var(--mint)}
.mpb-tree-group{font-weight:600}
.mpb-tree-count{color:var(--muted2);font-size:11px;font-weight:600}
.mpb-tree-caret{margin-left:auto;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--muted2);transition:transform .15s}
.mpb-tree-caret.open{transform:rotate(180deg)}
/* Generous max-height cap (idiom used for .modpack-mid's own mobile reveal
   too) — real content per group is short, so the eased transition to this cap
   reads as a quick slide rather than a hard snap. */
.mpb-tree-leaves{max-height:0;overflow:hidden;transition:max-height .22s ease}
.mpb-tree-leaves.open{max-height:600px}
.mpb-tree-leaf{padding-left:30px}
.mpb-tree-leaf .mpb-dd-otext{color:var(--muted)}
.mpb-tree-leaf:hover .mpb-dd-otext{color:#fff}

.cfg-name{margin-bottom:24px}
.cfg-name label{display:block;color:var(--muted);font-size:15px;margin:0 0 12px}
.cfg-java{margin-top:13px;color:var(--muted);font-size:13px}
.config .summary{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-xl);padding:30px;min-width:0;flex:1 1 280px}
.summary .spec{color:var(--text);font-size:15px;margin:0 0 22px;line-height:1.7}
.summary .btn{margin-bottom:10px}
/* create-server live price quote (mirrors the landing configurator's price block) */
.summary .price{font-family:var(--display);font-weight:800;font-size:38px;color:#fff;line-height:1;margin-bottom:16px;display:flex;flex-direction:column;align-items:flex-start;gap:12px}
.summary .price .g{background:linear-gradient(120deg,#2FD46A,#1C9136);-webkit-background-clip:text;background-clip:text;color:transparent}
.price-period{display:inline-flex;gap:4px;background:rgba(255,255,255,.04);border:1px solid var(--line2);border-radius:10px;padding:3px;margin-top:2px}
.price-period-btn{font-family:var(--sans);font-size:12px;font-weight:600;color:var(--muted);background:none;border:0;border-radius:7px;
  padding:6px 12px;cursor:pointer;transition:background .15s,color .15s}
.price-period-btn:hover{color:#fff}
.price-period-btn.active{background:var(--green-dim);color:var(--mint)}

/* «Ядро | Сборка модов» toggle — same segmented-control idiom as .price-period above. */
.core-mode-toggle{display:inline-flex;gap:4px;background:rgba(255,255,255,.04);border:1px solid var(--line2);border-radius:10px;padding:3px}
.core-mode-toggle[hidden]{display:none}
.core-mode-btn{font-family:var(--sans);font-size:12px;font-weight:600;color:var(--muted);background:none;border:0;border-radius:7px;padding:6px 12px;cursor:pointer}
.core-mode-btn:hover{color:#fff}
.core-mode-btn.active{background:var(--green-dim);color:var(--mint)}
.price-list{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:14px 0;margin-bottom:18px;display:flex;flex-direction:column;gap:9px}
.price-row{display:flex;justify-content:space-between;align-items:baseline;font-size:13px;color:var(--muted)}
.price-row b{color:#fff;font-weight:600;font-variant-numeric:tabular-nums}
@media(max-width:860px){
  .config{flex-direction:column}
  .config .panel,.config .summary{flex-basis:auto;width:100%}
  /* Stacked layout: the mid column can't grow "between" two sides that no longer sit
     side by side, so it falls back to a full-width block that reveals via height
     (same collapse idiom as .sp-selbar) between the configurator and price stack —
     already in DOM order between them, so no re-ordering needed here. */
  /* flex:0 0 auto resets the desktop numeric flex-basis (which sizes the row's
     WIDTH) back to auto now that the main axis is vertical (column direction) —
     otherwise it would size this item's HEIGHT instead, fighting the max-height
     reveal below. */
  .modpack-mid{flex:0 0 auto;width:100%;height:0;opacity:0;overflow:hidden;
    transition:height .38s cubic-bezier(.4,0,.2,1),opacity .3s ease}
  /* A definite height (not max-height:2000px, which — combined with the general
     .open rule's overflow:visible below — let the box grow to fit EVERY result
     card unclipped, stretching the whole page ~3x taller than this). height:100%
     down the .modpack-section→.mpb→.mpb-body→.mpb-results chain only resolves
     against a definite ancestor height, so giving one here also switches results
     to scrolling INSIDE this box (.mpb-results already has overflow:auto) instead
     of inside the page. overflow must be re-pinned to hidden — the un-media-
     queried .modpack-mid.open rule (desktop) sets overflow:visible unconditionally. */
  .modpack-mid.open{height:min(650px,70vh);opacity:1;overflow:hidden}
  .modpack-section{min-width:0}
}
/* 861-1345px, «Сборка модов» OPEN: the usable page width is below two 640px
   columns plus their 22px gutter (1302px; 1346px once main.container's 44px
   horizontal padding is included). Stack the sections here rather than letting
   either the picker or the configuration controls become cramped.
   Scoped to body.mp-open: with the mid column CLOSED, main.container is only
   ever 1080px wide (its un-widened default — see main.container above), which
   panel+summary fit side by side in at any width down to the phone tier, so
   the plain (unmedia-queried) .config row rule already handles that case —
   forcing it to stack here too just because the *viewport* (not the actually-
   rendered, much narrower container) fell under 1761px produced an awkward
   centered single column with acres of empty space on both sides. */
@media(min-width:861px) and (max-width:1345px){
  /* The open modpack picker changes the configurator into a centred column.
     Keep the otherwise standalone page title on that same column too; without
     this it remains against the widened container's left edge, detached from
     the server form below. */
  body.mp-open .cfg-head{width:100%;max-width:680px;margin-left:auto;margin-right:auto}
  body.mp-open .config{flex-direction:column;align-items:center}
  body.mp-open .config .panel,body.mp-open .config .summary,body.mp-open .modpack-mid{flex:0 0 auto;width:100%;max-width:680px}
  body.mp-open .modpack-mid{height:0;opacity:0;overflow:hidden;
    transition:height .38s cubic-bezier(.4,0,.2,1),opacity .3s ease}
  body.mp-open .modpack-mid.open{height:min(650px,70vh);opacity:1;overflow:hidden}
  body.mp-open .modpack-section{min-width:0}
}
/* 1346-1760px: the configurator and picker each retain at least 640px, so they
   fit side by side even though the price summary does not. Put the summary on a
   centred second row; this uses the otherwise wasted intermediate desktop width
   without compromising either interactive column. */
@media(min-width:1346px) and (max-width:1760px){
  body.mp-open .config{flex-wrap:wrap}
  body.mp-open .config .panel,body.mp-open .modpack-mid{flex:1 1 calc((100% - 22px)/2);width:calc((100% - 22px)/2);max-width:none}
  /* Match the heading's two-column geometry to the boxes below. The generic
     1.55:2 header flex ratio otherwise starts «Сборка модов» too far left. */
  body.mp-open .cfg-head h1,body.mp-open .cfg-head > .cfg-head-modpack{flex:1 1 calc((100% - 22px)/2);width:calc((100% - 22px)/2)}
  /* Keep the price heading and card as one deliberately narrow, centred
     column. A fixed basis avoids flex distributing this final row differently
     from its title at some widths. */
  body.mp-open .config .summary{flex:0 0 500px;width:500px;margin-left:auto;margin-right:auto}
  /* The title occupies a complete row before the centred price card. The
     negative bottom margin compensates for flex's 22px row gap, preserving the
     page's usual 14px title-to-card spacing. */
  /* Take a complete flex row: otherwise this 500px title can share the row
     with the 500px card, placing one at each edge instead of stacking them. */
  body.mp-open .config > .cfg-head-price{flex:0 0 100%;width:100%;max-width:none;margin:-8px 0 0;text-align:center}
}
/* «Сборка модов» open: the two-column configurator + a mid column both need room,
   so the page widens beyond its normal centred reading width (same idea as the
   server-panel's editor/Modrinth side docks resizing main.container elsewhere).
   Only the VALUE is scoped to body.mp-open — the transition duration now lives
   on the unconditional main.container rule above (matching .modpack-mid's own
   .42s) so it survives .mp-open being REMOVED too. It used to be redeclared
   here at .42s (overriding the base rule's .32s, used for the unrelated
   editor/Modrinth docks) only while .mp-open was present: opening looked fine
   (the override applies to the new state being transitioned TO), but on close
   this rule stops matching THE INSTANT .mp-open is removed, so the "after"
   state that CSS transitions look up its duration from was to the base rule's
   .32s the whole time — 100ms faster than .modpack-mid's own .42s shrink,
   which right up to that gap let panel/summary (width-frozen below) get
   briefly squeezed by flex-shrink because the container had already reached
   its final (narrower) width before .modpack-mid finished collapsing. */
body.mp-open main.container{max-width:1760px}

/* oauth row (register) */
.oauth{display:flex;gap:10px;margin:12px 0;flex-wrap:wrap}
.oauth a{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;text-align:center;padding:11px;border:1px solid var(--line2);border-radius:12px;color:var(--text);font-weight:600;font-size:14px}
.oauth a:hover{border-color:var(--mint);color:var(--mint);text-decoration:none}
.oauth-logo{width:18px;height:18px;flex:none;border-radius:4px}

/* alerts */
.alert{padding:13px 16px;border-radius:12px;margin:14px 0;white-space:pre-wrap;font-size:14px}
.alert.err{background:rgba(255,92,92,.1);border:1px solid rgba(255,92,92,.4);color:#ffb3b3}
.alert.ok{background:var(--green-dim);border:1px solid rgba(47,212,106,.4);color:#bff5cf}
.alert.info{background:rgba(127,179,255,.1);border:1px solid rgba(127,179,255,.4);color:#cfe2ff}
.alert.warn{background:rgba(255,196,0,.1);border:1px solid rgba(255,196,0,.42);color:#ffe08a}
.sp-warn{display:flex;align-items:center;gap:12px;justify-content:space-between}
.sp-warn-text{flex:1;min-width:0}
.sp-warn-btn{display:inline-flex;align-items:center;gap:6px;background:rgba(255,196,0,.06);border:1px solid rgba(255,196,0,.42);
  color:#ffe08a;cursor:pointer;border-radius:10px;padding:6px 11px;font-size:13px;font-family:var(--sans);white-space:nowrap;flex-shrink:0;
  transition:border-color .15s,color .15s,background .15s}
.sp-warn-btn:hover{border-color:#ffc400;color:#fff2c2;background:rgba(255,196,0,.14)}
.sp-warn-btn svg{width:16px;height:16px;fill:currentColor;flex-shrink:0}


/* pool: bars (CPU/RAM/disk/HDD/ports), one per line. Now lives inside the
   info-pop popover (see below) rather than always on the page. */
.pool-bars{display:flex;flex-direction:column;gap:12px}
.metric-bar{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:12px 16px}
.metric-bar .l{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;line-height:1.2}
.metric-bar .n{font-size:15px;font-weight:700;color:#fff;font-family:var(--display);white-space:nowrap;line-height:1.2;
  flex-shrink:0;width:132px;text-align:right}
.metric-bar .n .muted{font-size:13px;font-family:var(--sans);font-weight:400}
.metric-bar .bar-row{display:flex;align-items:center;gap:14px}
.metric-bar .bar-row .l{flex-shrink:0;width:70px}
.metric-bar .bar{flex:1;height:8px;background:#0a0d0b;border-radius:4px;overflow:hidden;border:2px solid rgba(47,212,106,.4)}
.metric-bar .bar > i{display:block;height:100%;background:var(--mint)}

/* resource-pool info popover: an (i) button next to "Создать сервер" reveals the
   pool breakdown on hover/focus instead of it taking permanent space on the page. */
.info-pop{position:relative;display:inline-flex}
.info-btn{width:30px;height:30px;border-radius:50%;border:1px solid var(--line2);background:var(--card);
  color:var(--muted);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}
.info-btn:hover,.info-btn:focus-visible{color:#fff;border-color:var(--mint)}
.info-pop-panel{position:absolute;top:calc(100% + 10px);right:0;z-index:40;width:380px;max-width:calc(100vw - 32px);
  background:var(--bg2);border:1px solid var(--line2);border-radius:var(--r);padding:14px 16px;
  box-shadow:0 16px 30px rgba(0,0,0,.4);
  opacity:0;visibility:hidden;transform:translateY(-4px);transition:opacity .15s,transform .15s,visibility .15s;pointer-events:none}
.info-pop:hover .info-pop-panel,.info-pop:focus-within .info-pop-panel{opacity:1;visibility:visible;transform:none;pointer-events:auto}
.info-pop-title{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px}
.info-pop-panel .metric-bar{padding:0;border:0;background:none}
.info-pop-panel .pool-bars{gap:10px}
@media (max-width:640px){.info-pop-panel{right:auto;left:0}}

/* table */
table{width:100%;border-collapse:collapse}
th,td{text-align:left;padding:13px 14px;border-bottom:1px solid var(--line);vertical-align:middle}
tr:last-child td{border-bottom:0}
th{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.05em;font-weight:600}
td strong{font-family:var(--display);font-weight:600}
.srv-name{display:flex;align-items:center;gap:12px}
.core-box{position:relative;flex-shrink:0;width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:transparent;
  border:1px solid rgba(47,212,106,.55);border-radius:11px;box-shadow:0 0 12px rgba(47,212,106,.32),inset 0 0 10px rgba(47,212,106,.1)}
.core-box img{width:28px;height:28px;object-fit:contain;filter:drop-shadow(0 0 4px rgba(47,212,106,.4))}
/* custom avatar fills the whole box (cover), no glyph drop-shadow. No overflow
   clip here (the img rounds itself) so the presence dot isn't cut off. */
.core-box.has-avatar{padding:0}
.core-box.has-avatar img{width:100%;height:100%;object-fit:cover;border-radius:10px;filter:none}

/* billing page */
.billing-top{display:grid;grid-template-columns:1.1fr 1fr;gap:22px;align-items:start}
.billing-top .card{margin:16px 0}
.billing-scroll{height:340px;overflow-y:auto}
.billing-scroll table{margin:0}
@media(max-width:760px){.billing-top{grid-template-columns:1fr}}

.billing-spend{display:grid;grid-template-columns:2fr 1fr;gap:22px;align-items:start}
/* minmax(0,1fr) (not the default minmax(auto,1fr)) so the column can shrink
   below its content — otherwise the nowrap mobile legend forces the whole
   chart card wider than the viewport instead of scrolling inside it. */
@media(max-width:760px){.billing-spend{grid-template-columns:minmax(0,1fr)}}
.billing-chart-card{min-width:0}
.billing-chart-legend{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:14px}
.bc-chip{display:inline-flex;align-items:center;gap:7px;font-size:13px;color:var(--muted);
  border:1px solid var(--line2);border-radius:999px;padding:6px 12px;line-height:1}
.bc-toggle{cursor:pointer;transition:border-color .15s,color .15s}
.bc-toggle input{position:absolute;opacity:0;width:1px;height:1px}
.bc-toggle:hover{border-color:var(--mint);color:#fff}
.bc-swatch{width:9px;height:9px;border-radius:50%;flex-shrink:0}
.billing-chart-wrap{position:relative;height:280px}
.billing-chart-wrap canvas{width:100%;height:100%;display:block;cursor:crosshair}
/* Admin stats: chart card sits beside the revenue card (.billing-top), which has
   variable content height — chart-card.js pins this card's height to that sibling's
   rendered height, so the chart-wrap (flex:1) fills whatever room is left instead of
   using its own fixed 280px and leaving a gap under the shorter card. */
.chart-card{display:flex;flex-direction:column}
.chart-card > h2,.chart-card .billing-chart-legend{flex-shrink:0}
.chart-card .billing-chart-wrap{flex:1;height:auto;min-height:0}
.billing-chart-tooltip{position:absolute;pointer-events:none;background:var(--bg2);border:1px solid var(--line2);
  border-radius:var(--r);padding:9px 12px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.4);white-space:nowrap;z-index:2}
.bc-tt-date{color:var(--muted);font-size:12px;margin-bottom:4px}
.bc-tt-row{display:flex;align-items:center;gap:14px;justify-content:space-between}
.bc-tt-key{display:flex;align-items:center;gap:0}
.bc-tt-val{font-weight:600;color:#fff;font-family:var(--display);margin-left:14px}
.billing-forecast{display:flex;flex-direction:column;gap:14px;background:var(--card);border:1px solid var(--line);
  border-radius:var(--r);padding:16px}
.bf-head{margin:0;font-family:var(--display);font-weight:700;color:#fff;font-size:14px}
.bf-items{display:flex;flex-direction:column;gap:14px}
.bf-item{display:flex;flex-direction:column;gap:2px}
.bf-label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.bf-value{font-size:20px;font-weight:700;font-family:var(--display);color:var(--mint)}

/* ── Balance page: two-column layout ──────────────────────────────────────
   Left (.bill-main) = balance + always-expanded history. Right (.pay-panel) =
   the top-up flow — a STATIC, always-present column (no open/close). The grid
   is two columns on desktop unconditionally; the panel (taller of the two)
   sets the row height and .hist-card's flex:1 stretches the history card so
   its bottom aligns with the panel's bottom. */
/* spacing between the «Баланс» page title and the grid below it (global
   h1 is margin:0, which left them flush). */
.bill-h1{margin-bottom:22px}
.bill-grid{display:grid;grid-template-columns:1fr;gap:22px;min-width:0}
.bill-main{display:flex;flex-direction:column;gap:22px;min-width:0}
.bill-grid > .card,.bill-main .card{margin:0}
@media(min-width:861px){
  /* fixed-width left column; the panel claims the remaining track. Both
     columns size to their natural content and the grid stretches the shorter
     one to match the taller — no min-height floor, so neither carries a big
     block of empty space. */
  .bill-grid{grid-template-columns:minmax(0,460px) minmax(0,1fr)}
  /* Let history consume every remaining pixel of the stretched left column.
     The old max-height:180px capped only the inner body while .hist-card kept
     stretching, producing an invisible empty barrier halfway down the card.
     A zero flex basis prevents a long table from making the whole grid taller;
     excess rows scroll inside the fully-height body instead. */
  .bill-main{min-height:0}
  .bill-main .hist-card{min-height:0}
  .bill-main .hist-body{height:0;max-height:none;min-height:0;flex:1 1 auto}
}
@media(max-width:860px){
  /* Single column on mobile: the top-up panel must sit ABOVE the payment
     history. .bill-main only wraps balance+history for the desktop left
     column, so promote its children (display:contents) into the grid and
     reorder the three cards balance → top-up panel → history. */
  .bill-grid{display:flex;flex-direction:column;gap:22px}
  .bill-main{display:contents}
  .bill-grid > .card{width:100%;min-width:0}
  .bal-card{order:1}
  .pay-panel{order:2}
  .hist-card{order:3}
}

/* balance card */
.bal-card{position:relative}
.bal-label{margin:0 0 5px;text-transform:uppercase;letter-spacing:.05em}
.bal-amount{font:700 42px/1 ui-monospace,'SFMono-Regular',Menlo,monospace;color:var(--mint);margin:0}
.bal-amount.neg{color:var(--danger)}
.bal-cur{font-size:26px;color:var(--muted);font-weight:600}
.bal-breakdown{position:absolute;top:20px;right:22px;width:150px;display:flex;flex-direction:column;gap:7px}
.bal-breakdown .price-row{gap:10px}
.bal-breakdown .price-row span{display:inline-flex;align-items:center;gap:7px}
.bal-breakdown .price-row b{font-size:13px}
.bal-breakdown .bal-real{color:var(--mint)}
.bal-breakdown .bal-promo{color:#e6c428}
.bal-breakdown .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.dot.real{background:var(--mint)} .dot.promo{background:#c98500}
.bal-neg{margin:18px 0 0}
.bal-topup-btn{margin-top:22px;font-size:16px;padding:14px 22px}
.bal-topup-btn .ic{margin-right:-2px}
/* The billing layout becomes one column below 861px. Keep the breakdown in
   normal flow there too: absolutely anchoring it at the top-right reserves a
   visually empty strip beside the total on tablet/mobile viewports. */
@media(max-width:860px){
  .bal-breakdown{position:static;width:auto;margin-top:16px}
}
/* Phones can report a wider CSS viewport in landscape, so viewport width alone
   is not enough to select this layout. On any coarse-pointer device, keep the
   billing cards in one column through the same width used by the mobile dock;
   desktop layouts are unaffected. */
@media(pointer:coarse) and (max-width:1300px){
  .bill-grid{display:flex;flex-direction:column;gap:22px}
  .bill-main{display:contents}
  .bill-grid > .card{width:100%;min-width:0}
  .bal-card{order:1}
  .pay-panel{order:2}
  .hist-card{order:3}
  .bal-breakdown{position:static;width:auto;margin-top:16px}
}

/* payment history (always expanded). flex:1 so that when .bill-main is
   stretched to match a taller open .pay-panel, this card (not empty page
   background) absorbs any remaining height difference — the min-height
   above handles the common case, this is just the exact-match fallback. */
.hist-card{padding:0;overflow:hidden;display:flex;flex-direction:column;flex:1 1 auto}
.hist-sum{display:flex;align-items:center;gap:10px;padding:20px 22px 14px;
  font-family:var(--display);font-weight:700;color:#fff}
.hist-title{font-size:16px}
.hist-count{font:700 12px/1 var(--sans);background:var(--card2);border:1px solid var(--line);color:var(--muted);
  border-radius:999px;padding:4px 9px}
.hist-body{padding:0 22px 20px;max-height:420px;overflow:auto}
.hist-table{margin:0;width:100%}
.hist-amt{font-variant-numeric:tabular-nums;font-weight:600}

/* payment-status pills (history table) */
.pay-badge{display:inline-block;font-size:12px;font-weight:600;padding:3px 10px;border-radius:999px;
  border:1px solid var(--line2);color:var(--muted);white-space:nowrap}
.pay-badge.ok{background:var(--green-dim);border-color:transparent;color:var(--mint)}
.pay-badge.bad{background:rgba(255,92,92,.12);border-color:transparent;color:var(--danger)}
.pay-badge.warn{background:rgba(230,196,40,.14);border-color:transparent;color:#e6c428}
.pay-badge.pend{background:rgba(127,179,255,.12);border-color:transparent;color:var(--info)}

/* ── Top-up panel ─────────────────────────────────────────────────────────
   Amount + five payment-method tiles share #payForm. Each tile is a
   .pay-method-slot wrapping a fallback submit button (today's redirect to
   T-Bank's hosted checkout) and an empty .pm-widget-mount box; T-Bank's real
   embedded PaymentIntegration widget (billing-payment.js) mounts into the
   latter per-tile and only then hides the fallback — so a tile a terminal
   doesn't have enabled, or a widget load failure, just leaves the redirect
   fallback visible instead of a dead button. Collecting raw PAN on our own
   server would put us in PCI-DSS scope; the widget's card entry happens
   entirely inside T-Bank's own iframe, so that never applies here. */
/* a plain stretched grid column now (no align-self:start / sticky) so it and
   .bill-main share the row's height in both directions — whichever column is
   taller sets the height and the other fills to it, which is what keeps the
   history card's bottom aligned with the panel's bottom. */
.pay-panel{border-color:var(--line);background:var(--card);position:relative;overflow:hidden;min-width:0}
.pay-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:4px}
.pay-close{background:none;border:0;color:var(--muted);cursor:pointer;padding:7px;border-radius:9px;display:flex;line-height:0}
.pay-close:hover{color:#fff;background:rgba(255,255,255,.06)}
.pay-panel .pay-label{margin:16px 0 8px;font-weight:600;font-size:14px;display:block}
.pay-panel form:first-of-type > .pay-label:first-child{margin-top:8px}
.pay-section{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin:24px 0 12px}

/* amount input with a fixed ₽ suffix */
.amount-field{position:relative}
.amount-field input{font:700 26px/1 var(--display);padding:15px 46px 15px 16px;background:#101010;border-color:#3f3f3f}
.amount-field input:focus{border-color:#686868}
.amount-cur{position:absolute;right:17px;top:50%;transform:translateY(-50%);color:var(--muted);
  font:700 20px/1 var(--display);pointer-events:none}
.amt-chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.amt-chip{font-family:var(--display);font-weight:600;font-size:13px;padding:8px 14px;border-radius:999px;
  background:#292929;border:1px solid #484848;color:#bdbdbd;cursor:pointer;
  transition:border-color .15s,color .15s,background .15s}
.amt-chip:hover{color:#fff}
.amt-chip.sel{border-color:var(--mint);color:var(--mint);background:var(--green-dim)}
.amt-err{color:var(--danger);font-size:13px;margin:9px 0 0}
input.field-invalid{border-color:var(--danger)}
input.field-invalid:focus{border-color:var(--danger)}

/* payment methods: SBP alone on row 1, SberPay/T-Pay/Mir Pay on row 2, the
   card widget alone on row 3 — plain flex column of rows, not a uniform grid. */
.pay-methods{display:flex;flex-direction:column;gap:8px}
.pm-wide{width:100%}
/* Acquirers are full-width rows. Each row explains which methods are available
   in that provider's embedded checkout. */
.pay-wallets{display:grid;grid-template-columns:1fr;gap:10px}
/* Mir Pay is mobile-only. On desktop hide it and let the card span both columns.
   billing-payment.js also skips mounting Mir Pay's widget on desktop (it would
   render a stray card button). The card slot is display:contents, so the span
   goes on whichever visible child (fallback button XOR widget mount) it shows. */
@media(pointer:fine){
  .pay-wallets .pay-method-slot[data-widget-type="mirpay"]{display:none}
}
/* Each card button (T-Bank «Мир», ЮKassa Visa/Mastercard) is its own full-width
   row — on every viewport now, since there can be two of them and their labels
   are long. The slot (not its children) is the grid item that spans. */
.pm-card-slot{grid-column:1 / -1}
/* the slot itself isn't a layout box — display:contents hands its one visible
   child (fallback button XOR widget-mount box, toggled via [hidden] in JS)
   straight to its parent flex row, so both share the exact same .pay-method
   sizing with no extra wrapper markup to account for. */
.pay-method-slot{display:contents}
/* SberPay + Card are the exceptions: each must be a real positioned box (not
   display:contents) so its .pm-skin can absolutely cover the mounted widget
   button. The one visible in-flow child (fallback button XOR widget mount) fills
   the grid cell as before; the button is no longer the grid item so it needs an
   explicit width (see .pm-sber-official / .pm-card). */
.pm-sber-slot,.pm-card-slot{display:block;position:relative}
/* row layout (logo beside label, not stacked above it) keeps these close to
   the ~50-56px height of a standard payment button instead of the tall
   square tiles a column layout produced. */
.pay-method{position:relative;display:flex;flex-direction:row;align-items:center;justify-content:center;gap:10px;
  padding:13px 16px;border-radius:12px;border:1px solid var(--line2);background:rgba(255,255,255,.03);
  color:#fff;cursor:pointer;font-family:var(--sans);transition:border-color .15s,transform .15s,background .15s}
/* .pay-method's own display:flex (author-origin) beats the UA stylesheet's
   [hidden]{display:none} regardless of specificity, so a hidden fallback/mount
   stayed visible — both the fallback button and an empty widget box rendered
   at once, doubling that slot's height. Force it back off explicitly. */
.pay-method[hidden]{display:none}
.pay-method:hover{border-color:var(--mint);transform:translateY(-2px);background:rgba(47,212,106,.06)}
.pay-method:active{transform:none}
.pm-logo{height:24px;display:block;object-fit:contain}
/* each brand's logo sits on its own official backdrop color, not a shared box */
.pm-logo-sbp{height:30px;background:#fff;border-radius:7px;padding:5px 12px;box-sizing:border-box}
/* larger СБП logo — used ONLY in the QR overlay's dark modal, where the white
   box keeps the mark legible against #121820. */
.pm-logo-sbp-lg{height:50px;padding:7px 20px}
/* СБП tile logo: no white box (the sbp.png is already transparent) so it sits
   flush on the white tile and blends when the tile darkens on hover — a boxed
   logo stayed pure-white and stood out. Sized to the other brand logos (30px)
   so the СБП tile is the same height as the rest, not the tall square it was. */
.pm-logo-sbp-btn{height:30px}
.pm-logo-mir{height:30px;background:#fff;border-radius:7px;padding:5px 10px;box-sizing:border-box}
/* T-Pay brand mark for the QR overlay's dark modal. The vector t-pay-logo.svg is
   just the dark "[shield-Т] PAY" pill (transparent bg), so we sit it on a crisp
   #FFDD2D field — the official yellow-button look — mirroring how .pm-logo-sbp
   gets a white box here. Replaces the old tpay.png, a lopsided crop whose yellow
   field didn't actually enclose the pill (the "broken logo"). */
.pm-logo-t{height:30px;border-radius:9px;background:#FFDD2D;padding:6px 11px;box-sizing:border-box}
/* larger T-Pay mark — used ONLY in the QR overlay's dark modal; height matches
   .pm-logo-sbp-lg (50px) so both methods' overlay marks are the same size. */
.pm-logo-t-lg{height:50px;border-radius:14px;padding:10px 18px}
.pm-name{font-size:13px;font-weight:600}
.pm-tag{position:absolute;top:50%;right:14px;transform:translateY(-50%);background:var(--green-dim);color:var(--mint);
  font:700 10px/1 var(--display);padding:3px 7px;border-radius:999px}
/* sberpay.png is a complete branded badge (gradient pill + checkmark + "Pay"
   already baked in) unlike the other three logos, so no backdrop box here —
   just scale it to match their height. */
.pm-logo-sber{height:30px}
.pm-cardicon{display:flex;align-items:center;justify-content:center;height:24px;color:var(--text)}
/* T-Bank's widget renders its OWN bordered button (its own cross-origin
   iframe) in here, so the mount must be a bare box with NO border/background/
   padding of ours — otherwise its border stacks OUTSIDE T-Bank's button and
   you see a doubled outline (the card double-box bug). This is why the mount
   no longer carries the .pay-method class. The iframe is height:100%, which
   only resolves against a DEFINITE parent height — without one it falls back
   to the plain <iframe> HTML default of 150px — so give the mount an explicit
   height matching the other tiles. */
/* Just tall enough to hug T-Bank's own button. Its button (top-aligned in the
   iframe) measures 56px with the bottom border intact — measured by mounting
   the real widget (prod terminal key + integration.js) and pixel-scanning: the
   full border still renders down to a 56px mount and only starts clipping at
   54px. The earlier 64px left ~7px of dead space UNDER the button in every
   mount, which stacked with the grid row-gap into a ~16px chasm between the
   SberPay/T-Pay row and the Card tile. 58px keeps a 2px safety margin over the
   button while closing that gap to a uniform ~10px. */
.pm-widget-mount{height:58px;padding:0;border:0;background:none;overflow:visible}
.pm-widget-mount iframe{width:100%;height:100%;border:0;display:block}

/* neutral fallback tint for a tile with no brand class of its own (the card
   tile). Declared BEFORE the brand tints so a wallet .pm-fallback that ALSO
   carries a brand class (.pm-sber etc.) keeps its brand color — the later
   brand rule wins on equal specificity. */
.pm-fallback{background:rgba(127,179,255,.08);border-color:rgba(127,179,255,.22)}
.pm-fallback:hover{background:rgba(127,179,255,.14);border-color:rgba(127,179,255,.4)}

/* each method tinted toward its own brand color instead of a flat black tile
   — low-alpha so it still reads as a dark-theme button, not a colored badge.
   SBP and Mir Pay use two-tone gradients since both brands' marks are
   themselves a color transition rather than one flat hue. */
/* СБП tile: fully white (matches the СБП logo's own white background) so the
   enlarged logo reads as one seamless mark rather than sitting in a tinted box. */
.pm-sbp{background:#fff;border-color:rgba(0,0,0,.12)}
/* only the background darkens on hover — no translateY lift (the T-Bank widget
   tiles beside it don't animate either, so the СБП tile shouldn't jump). */
.pm-sbp:hover{background:#f2f2f6;border-color:rgba(0,0,0,.26);transform:none}
/* Official SberPay button (from Sber's SberPay brand guide): solid #21A038 green
   field with the white "Оплатить [Sber] Pay" lockup (sberpay-lockup.svg — the
   guide's button SVG with its own green field stripped so it sits on ours). This
   is BOTH the redirect fallback button (.pm-sber, shown if the widget can't
   mount) AND, via .pm-sber-skin below, the cover over the mounted widget button.
   Guide: min height 30px, min clear-space 10px, white on #21A038. */
/* full-width: the slot (not this button) is now the grid item, so the button no
   longer auto-stretches to the cell — make it fill the cell explicitly. */
.pm-sber-official{background:#21A038;border:1px solid transparent;color:#fff;gap:0;width:100%;box-sizing:border-box}
.pm-sber-official:hover{background:#1c8a30;border-color:transparent;transform:none}
.pm-sber-official:active{background:#187a2a;transform:none}
.sber-lockup{height:21px;width:auto;display:block;flex:none;max-width:100%}
/* Shared skin: an OPAQUE cover (our own branded button) over a mounted T-Bank
   widget button (SberPay, Card). pointer-events:none lets the real click pass
   THROUGH to T-Bank's button underneath (opening the native overlay) — it just
   skins the widget with our button. Covers the whole slot cell so it lines up
   with the mount regardless of the mount's height. Per-method background below. */
.pm-skin{position:absolute;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;
  border:1px solid transparent;border-radius:12px;pointer-events:none;box-sizing:border-box;
  transition:background .15s,background-color .15s,border-color .15s}
.pm-skin[hidden]{display:none}
.pm-sber-skin{background:#21A038}
/* The skin is pointer-events:none (so the click reaches T-Bank's button beneath),
   which means it can't match :hover itself — the pointer hovers the iframe under
   it. But :hover DOES propagate to the iframe's ancestors, so drive the hover-dim
   off the slot instead, mirroring each method's own :hover (SberPay #1c8a30, card
   the .14 tint). Only the visible skin is affected; the hidden fallback keeps its
   own :hover. */
.pm-sber-slot:hover .pm-sber-skin{background:#1c8a30}
/* Official T-Bank T-Pay button (from T-Bank's T-Pay brand guide): the solid
   #FFDD2D field with "Оплатить с" + the vector mark — NOT the dark tinted tile
   the other wallets use, exactly as the СБП button beside it is its own brand's
   white button. Colors are the guide's prescribed states (default #FFDD2D /
   hover #FCC521 / active #FAB619); label is 14px medium on #000 @ .8, logo 21px,
   gap 7px — the guide's "Button M" (40–60px) sizing. No mint/lift hover: on-brand
   hover is a flat color change only. Border kept 1px transparent so the box height
   matches the bordered tiles in the same grid row. */
.pm-tpay{background:#FFDD2D;border:1px solid transparent;color:rgba(0,0,0,.8);gap:7px}
.pm-tpay:hover{background:#FCC521;border-color:transparent;transform:none}
.pm-tpay:active{background:#FAB619;transform:none}
.tpay-cta{font-family:var(--sans);font-size:14px;font-weight:500;line-height:1;color:rgba(0,0,0,.8);white-space:nowrap}
.tpay-logo{height:21px;width:auto;display:block;flex:none}
.pm-mir{background:linear-gradient(120deg,rgba(56,168,108,.16),rgba(37,140,219,.16));border-color:rgba(56,168,108,.34)}
.pm-mir:hover{background:linear-gradient(120deg,rgba(56,168,108,.26),rgba(37,140,219,.26));border-color:rgba(56,168,108,.54)}
/* card tile (card glyph + "Оплатить картой"): neutral blue tint. Card is a
   top-level redirect to T-Bank's hosted checkout (PaymentURL), NOT an embedded
   widget, so this button is the whole thing. width:100% because the .pm-card-slot
   wrapper (not this button) is the grid item. */
.pm-card{background:rgba(127,179,255,.08);border-color:rgba(127,179,255,.22);width:100%;box-sizing:border-box}
/* card buttons only tint on hover — no translateY lift (the .pay-method base
   rule adds one; the branded wallet tiles beside them don't animate, so the card
   buttons shouldn't jump either). This is the "card button lifts on hover" fix. */
.pm-card:hover{background:rgba(127,179,255,.14);border-color:rgba(127,179,255,.4);transform:none}
.pm-card:active{transform:none}
/* Мир badge sitting after the label in the T-Bank «Мир» card button. */
.pm-card-badge{height:18px;background:#fff;border-radius:4px;padding:2px 5px;box-sizing:border-box;display:block}
/* Acquirer rows use an opaque surface and explicit high-contrast type. The old
   translucent grey card blended into .pay-panel and made its muted text look
   blurred, especially on lower-density screens. */
.pm-provider{width:100%;min-height:96px;box-sizing:border-box;justify-content:flex-start;gap:15px;padding:14px 15px;
  text-align:left;background:#1b1b1b;border-color:#343434;box-shadow:0 1px 0 rgba(255,255,255,.04) inset}
.pm-provider:hover{transform:none;background:#222;border-color:#505050}
.pm-provider:active{transform:none;background:#171717}
.pm-provider-mark{width:66px;height:66px;border-radius:16px;display:flex;align-items:center;justify-content:center;
  flex:0 0 66px;overflow:hidden;font:800 34.5px/1 var(--display)}
.pm-provider-mark-tpay{background:#ffdd2d}
.pm-provider-mark-tpay img{width:46.5px;height:auto;display:block}
.pm-provider-mark-yk{background:#7b3ff2;color:#fff}
.pm-provider-mark-sbp{background:#fff}
.pm-provider-mark-sbp img{width:51px;height:auto;display:block}
.pm-provider-copy{display:flex;flex:1 1 auto;min-width:0;flex-direction:column;gap:4px}
.pm-provider-copy strong{color:#fff;font:700 16px/1.2 var(--display)}
.pm-provider-copy span{color:#c7c7c7;font-size:12.5px;line-height:1.4;text-wrap:balance}
.pm-provider-arrow{flex:0 0 auto;color:#aaa;font-size:20px;line-height:1}
/* ЮKassa Visa/Mastercard card form fills the overlay; the widget sizes its own
   iframe — a min-height stops the box collapsing before it renders. */
#yookassa-card-form{min-height:200px}
/* loading skeleton shown in a wallet tile while its T-Bank widget mounts */
.pm-skeleton{height:60px;border-radius:12px;border:1px solid var(--line2);
  background:linear-gradient(100deg,rgba(255,255,255,.02) 30%,rgba(255,255,255,.09) 50%,rgba(255,255,255,.02) 70%);
  background-size:200% 100%;animation:pmShimmer 1.15s ease-in-out infinite}
.pm-skeleton[hidden]{display:none}
@keyframes pmShimmer{from{background-position:200% 0}to{background-position:-200% 0}}
.pay-secure{display:flex;align-items:flex-start;gap:7px;color:var(--muted2);font-size:12px;line-height:1.5;margin:13px 0 0}
.pay-secure svg{flex-shrink:0;margin-top:2px;color:var(--mint);opacity:.8}
@media(max-width:560px){
  .bal-amount{font-size:36px}
  /* chart: halve the plot height (surrounding legend/forecast untouched) */
  .billing-chart-wrap{height:140px}
  /* legend: one horizontal scrollable row, chip widths unchanged */
  .billing-chart-legend{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:4px;margin-bottom:12px}
  .billing-chart-legend .bc-chip{flex:0 0 auto}
  /* forecast: the two figures side-by-side, slightly smaller to fit */
  .bf-items{flex-direction:row;gap:12px}
  .bf-items .bf-item{flex:1;min-width:0}
  .billing-forecast .bf-value{font-size:16px}
  .billing-forecast .bf-label{font-size:10.5px}
  /* payment history: ~1.5× shorter scroll area */
  .hist-body{max-height:280px}
  /* Amount chips must not be a horizontal scroller: swiping this row on
     Android can chain its overscroll to the document and pan the whole billing
     page into an empty area on the right. A compact grid keeps every option
     reachable without creating a second horizontal scroll surface. */
  .amt-chips{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));overflow:visible;padding-bottom:0}
  .amt-chips .amt-chip{width:100%;min-width:0;white-space:nowrap}
}

/* dedicated T-Bank SuccessURL/FailURL landing pages */
.pay-result{text-align:center}
.pay-result-icon{width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 18px}
.pay-result-icon.ok{background:var(--green-dim);color:var(--mint);box-shadow:0 0 0 1px rgba(47,212,106,.4),0 10px 30px rgba(47,212,106,.25)}
.pay-result-icon.err{background:rgba(255,92,92,.12);color:var(--danger);box-shadow:0 0 0 1px rgba(255,92,92,.4),0 10px 30px rgba(255,92,92,.2)}
.pay-result .sub{margin:10px 0 0}
.pay-result-card{text-align:left}
.pay-result-hint{margin:14px 0 0;line-height:1.6}
.pay-result-tips ul{margin:0;padding-left:18px;color:var(--muted);font-size:14px;line-height:1.8}
.pay-result-tips li{margin:0}

/* SBP QR page (/billing/topup/sbp) */
.pay-qr{text-align:center}
.pay-qr-card{display:flex;flex-direction:column;align-items:center;gap:14px}
.pay-qr-img{background:#fff;border-radius:16px;padding:18px;line-height:0}
.pay-qr-img svg{width:100%;max-width:220px;height:auto;display:block}
.pay-qr-status{display:flex;align-items:center;gap:8px;margin:0}
.pay-qr-dot{width:8px;height:8px;border-radius:50%;background:var(--mint);box-shadow:0 0 8px rgba(47,212,106,.8);animation:pulse 2.4s infinite;flex-shrink:0}

/* ── Page-level QR overlay (#payQrOverlay, billing-payment.js) ──────────────
   Shared by СБП + T-Pay. Rendered at document level (position:fixed inset:0),
   so it covers the WHOLE viewport — a blurred backdrop over the entire page +
   a centred modal with the QR, the sum below, and a live "waiting" status that
   reloads on payment. This is the "QR in the middle of the page" the redesign
   asked for (it used to be docked inside .pay-panel). */
.pay-qr-overlay{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;
  padding:16px;overflow:auto}
/* .pay-qr-state sets its own display:flex, which (author-origin) beats the UA
   [hidden]{display:none} regardless of specificity — without this explicit
   override every state (loading + QR + error) rendered at once. */
.pay-qr-overlay[hidden],.pay-qr-state[hidden]{display:none}
/* lock the page behind the overlay so the backdrop doesn't scroll away */
body.pay-qr-open{overflow:hidden}
.pay-qr-backdrop{position:absolute;inset:0;background:rgba(6,9,14,.62);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
.pay-qr-modal{position:relative;z-index:1;width:100%;max-width:352px;text-align:center;padding:24px 22px;
  background:#181818;border:1px solid #444;border-radius:var(--r);
  box-shadow:0 24px 60px rgba(0,0,0,.6);animation:payIn .22s ease}
@keyframes payIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.pay-qr-close{position:absolute;top:10px;right:10px}
.pay-qr-brand{display:flex;justify-content:center;margin-bottom:14px}
.pay-qr-state{display:flex;flex-direction:column;align-items:center;gap:14px}
.pay-qr-spinner{width:44px;height:44px;border-radius:50%;border:3px solid var(--line2);border-top-color:var(--mint);
  animation:spin .8s linear infinite;margin-top:8px}
@keyframes spin{to{transform:rotate(360deg)}}
.pay-qr-code{background:#fff;border-radius:16px;padding:16px;line-height:0}
.pay-qr-code svg{width:100%;max-width:210px;height:auto;display:block}
.pay-qr-sum{margin:0;font-size:15px}
.pay-qr-sum b{font-size:18px}
.pay-qr-status{display:flex;align-items:center;justify-content:center;gap:8px;margin:0}
/* comfortable padding so the label doesn't crowd the button edges */
.pay-qr-app-link{margin:4px 0 0;padding:14px 24px;line-height:1.25;white-space:normal;height:auto;min-height:0}
.pay-qr-err-msg{color:var(--danger);margin:6px 0 0;line-height:1.5}

/* Card payment overlay: reuses the QR overlay shell (.pay-qr-overlay/-backdrop/
   -modal/-state/-spinner/-close) but wider — T-Bank's EMBEDDED card FORM
   (features.iframe) needs room. */
.pay-card-modal{max-width:440px}
.pay-card-head{display:flex;align-items:center;justify-content:center;gap:8px;font-weight:600;color:var(--text);margin-bottom:16px}
.pay-card-form-wrap{display:flex;flex-direction:column;gap:14px}
/* T-Bank checkout only: its iframe is explicitly forced to the light theme in
   billing-payment.js. Match the surrounding shell to the form's #F6F7F8 and
   use dark copy so there is no dark frame around an almost-white widget. This
   is intentionally scoped to #payCardOverlay; payment-method buttons keep the
   neutral-grey menu treatment above. */
#payCardOverlay .pay-card-modal{background:#F6F7F8;border-color:#D9DDE1;color:#202124}
#payCardOverlay .pay-card-head{color:#202124}
#payCardOverlay .muted{color:#50555A}
#payCardOverlay .pay-close{color:#555B61}
#payCardOverlay .pay-close:hover{color:#111;background:rgba(0,0,0,.06)}
#payCardOverlay #tbank-card-form{background:#F6F7F8;color:#202124;border-radius:12px}
/* T-Bank injects its own <iframe> in here; it self-sizes via postMessage (it
   starts at height 0), so min-height keeps the spinner->form swap from
   collapsing to nothing while the form loads. */
#tbank-card-form{width:100%;min-height:120px}
#tbank-card-form iframe{width:100%;border:0;display:block}

/* footer */
footer{border-top:1px solid var(--line);padding:26px 22px;text-align:center;color:var(--muted);font-size:13px;margin-top:40px}

/* support widget */
.support{position:fixed;right:22px;bottom:22px;z-index:50}
.support-btn{width:54px;height:54px;border-radius:50%;border:0;background:var(--mint);color:#04140a;cursor:pointer;
  display:flex;align-items:center;justify-content:center;box-shadow:0 10px 30px rgba(0,0,0,.5);transition:transform .15s}
.support-btn:hover{transform:scale(1.07)}
/* On the dashboard's mobile bottom dock bar (see .srv-dock's own mobile skin
   above), the support launcher moves into the same footer instead of floating
   over it — flush in the reserved right-hand gutter, same height as the bar,
   sharing its background (no border of its own either — same call as the
   dock's) so the two read as one continuous strip with no seam between them.
   dh-has-dock is set by buildServerDock (server-panel.js) — only pages with a
   dock get this; everywhere else the floating circular button is unaffected. */
@media(max-width:1300px){
  body.dh-has-dock .support{right:0;bottom:0;width:64px;height:61px;
    display:flex;align-items:center;justify-content:center;
    background:var(--panel);border:0;box-shadow:none}
  body.dh-has-dock .support-btn{width:42px;height:42px;box-shadow:none}
  /* Room for the fixed bottom dock bar so page content — and the "Пространство:
     X / Отобразить все серверы" banner at the foot of the server list — never
     sits underneath it. */
  body.dh-has-dock main.container{padding-bottom:78px}
}
/* Mobile Chatwoot: force true full-bleed coverage + lock the page behind it
   so nothing scrolls/peeks in underneath (see footer.ejs chatScrollLock). */
@media(max-width:667px){
  body.dh-chat-lock{position:fixed;left:0;right:0;width:100%;overflow:hidden}
  .woot-widget-holder{top:0!important;left:0!important;right:0!important;bottom:0!important;
    width:100%!important;height:100vh!important;height:100dvh!important;border-radius:0!important}
  .woot-widget-holder iframe{min-height:100%!important}
}
.support-panel{position:absolute;right:0;bottom:68px;width:300px;background:var(--panel);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:18px;box-shadow:0 20px 50px rgba(0,0,0,.6)}
.sup-head{display:flex;gap:11px;align-items:center;margin-bottom:14px}
.sup-head img{width:34px;height:34px}
.sup-head b{display:block;font-family:var(--display);font-size:15px;color:#fff}
.sup-head .on{color:var(--mint);font-size:12px}
.msg{font-size:13px;line-height:1.5;padding:11px 13px;border-radius:13px;background:rgba(255,255,255,.05);color:#cdddd2;border-top-left-radius:4px;margin-bottom:14px}
.sup-actions .btn{margin-bottom:8px}
.sup-input{display:flex;gap:8px;margin-top:12px;opacity:.55}
.sup-input input{flex:1;background:#0a0d0b;border:1px solid var(--line);border-radius:10px;padding:9px 12px;color:var(--text);font-size:13px}
.sup-input button{background:var(--line2);border:0;border-radius:10px;width:38px;color:var(--muted);cursor:not-allowed}

/* ====================== in-dashboard server panels ====================== */
/* global actions dropdown (start all / stop all) in the dashboard header */
/* "Мои серверы" header row: title + Глобальные действия pinned left, the
   Создать сервер button + pool-info (i) pushed to the right edge.
   The wrapper is a query container so the header reflows on its OWN width, not
   the viewport's — when the docked editor squeezes the page to half-width the
   header still gets the correct narrow (2-row) layout. */
/* inline-size containment implies layout containment (a new stacking context),
   which would otherwise trap the pool-info popup's z-index below the server
   cards that follow it in the DOM. Lift the whole wrapper above those cards
   (auto/0) while staying under the sticky site header (z-index:30). */
.action-bar-wrap{container:actbar / inline-size;position:relative;z-index:5}
.action-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 18px}
.action-bar h1{margin:0}
.action-bar .create-srv{margin-left:auto}
/* Keep the two buttons the same height. Создать сервер is an <a> and rendered
   taller than the <button> because an anchor inherits the body line-height
   while a button resets to `normal`; pinning line-height makes them equal. */
.action-bar .create-srv,.action-bar #gactionBtn{padding:11px 20px;line-height:1.15}
/* Below this the header can't fit title + Глобальные действия + Создать on one
   row without an ugly wrap (the (i) breaks onto its own line), so switch to a
   3-row layout: centred title, then Создать + (i) sharing one full-width row
   (Создать grows to fill it, (i) keeps its own fixed 30px), then Глобальные
   действия full-width below that. Пространства used to live in this bar too
   (a 3rd narrow-viewport-only button, .ws-bar-btn) — it's gone now, folded
   into the same .ws-dock-btn folder button the wide-screen sidebar dock always
   had, which the mobile bottom dock bar below makes reachable at every width
   instead of just ≥1300px (see .srv-dock's mobile skin). One tier for the
   whole range (not two with a gap between) so title-centring is consistent
   throughout. Container-query (not viewport) so the editor-squeezed page
   reflows too. */
@container actbar (max-width:880px){
  .action-bar{gap:10px}
  .action-bar h1{flex:1 0 100%;text-align:center;font-size:24px}
  .action-bar .create-srv{flex:1 1 auto;margin-left:0}
  .action-bar #globalActions{order:3;flex:1 0 100%;width:100%}
  .action-bar #globalActions #gactionBtn{width:100%;justify-content:center}
  .action-bar #globalActions #gactionBtn .gaction-label{margin-left:14px}
}
.gaction{position:relative}
.gaction .gaction-car{font-size:11px;opacity:.8}
/* The optical-centering nudge below only matters once the button is stretched
   wider than its own content (mobile action-bar row, burger dropdown row) —
   there, the trailing chevron's flex gap has no left-side counterpart, so
   centering the "Действия ▾" group in the slack leaves the word itself sitting
   left of the pill's true visual center, and nudging the label right by the
   chevron's own footprint (gap+glyph width, measured ~13.5px) re-centers the
   WORD instead of the group. On the default header pill the button is
   shrink-to-fit (no slack to redistribute), so the same margin only added dead
   space on the left and made the pill wider/lopsided vs. its siblings
   (Выйти, Статус систем) — keep it scoped to the two width:100% contexts only. */
.gaction-btn.busy,#gactionBtn.busy{opacity:.6;cursor:progress}
.gaction-menu{position:absolute;left:0;top:calc(100% + 8px);z-index:30;min-width:250px;
  background:#0c0f0d;border:1px solid var(--line2);border-radius:12px;padding:6px;box-shadow:0 20px 50px rgba(0,0,0,.6)}
.gaction-item{display:flex;align-items:center;gap:10px;width:100%;text-align:left;background:none;border:0;color:#fff;
  font-family:var(--sans);font-size:14px;font-weight:600;padding:11px 12px;border-radius:9px;cursor:pointer}
.gaction-item:hover:not(:disabled){background:rgba(255,255,255,.06)}
.gaction-item:disabled{opacity:.45;cursor:default}
.gaction-item .gaction-ico{width:18px;text-align:center;flex-shrink:0}
.gaction-item.start .gaction-ico{color:var(--mint)}
.gaction-item.stop .gaction-ico{color:#ffb3b3}
.gaction-item.collapse .gaction-ico,.gaction-item.expand .gaction-ico{color:var(--muted)}
.gaction-sep{height:1px;margin:5px 10px;background:var(--line)}
@media(max-width:640px){.gaction-menu{left:auto;right:0}}
.srv-list{display:flex;flex-direction:column;gap:12px;margin:14px 0}
/* server-panel.js restores saved order and expanded rows before revealing this
   list, avoiding a first-paint flash of the wrong panel geometry. */
html.dh-dashboard-pending .srv-list{visibility:hidden}
/* The server card is a query container so its own content reflows to the CARD's
   real width, not the viewport's. This matters once the content column is narrowed
   independently of the window — by the resizable --dh-mid-w width and by docking the
   editor/Modrinth side panel — where viewport media queries never fire and the
   header/tabs/power row/stats would otherwise clip. See the @container block below. */
.srv{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden;transition:border-color .15s;
  container:srv / inline-size}
.srv.open{border-color:rgba(47,212,106,.4)}
.srv.srv-file-target{border-color:rgba(47,212,106,.45);box-shadow:inset 0 0 0 1px rgba(47,212,106,.16)}
.srv.srv-file-target .srv-head:after{content:'Перетащите сюда для копирования';margin-left:auto;color:var(--mint);font-size:12px;font-weight:700;white-space:nowrap;opacity:.8}
.srv.srv-file-target .srv-quick{margin-left:0}
.srv.srv-file-drop{border-color:var(--mint);box-shadow:0 0 0 3px rgba(47,212,106,.2)}
.srv.srv-file-drop .srv-head{background:rgba(47,212,106,.16)}
.srv.srv-file-drop .srv-head:after{content:'Отпустите, чтобы скопировать';opacity:1}
@media(max-width:700px){.srv.srv-file-target .srv-head:after{display:none}}
.srv-head{display:flex;align-items:center;gap:14px;padding:14px 18px;cursor:pointer;user-select:none}
.srv-head:hover{background:rgba(255,255,255,.02)}
.srv-meta{display:flex;flex-direction:column;min-width:0}
.srv-meta .srv-name{font-family:var(--display);font-weight:600;font-size:21px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.srv-quick{margin-left:auto;white-space:nowrap}
/* drag handle (3×3 grip) — reorder servers. Sits leftmost in the header; the
   grabbing cursor + hover tint signal it's draggable. Hidden on shared rows. */
.srv-grip{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:20px;height:26px;
  color:var(--muted2);cursor:grab;border-radius:6px;transition:color .15s,background .15s;touch-action:none}
.srv-grip:hover{color:var(--mint);background:rgba(47,212,106,.08)}
.srv.srv-dragging{opacity:.6;border-color:rgba(47,212,106,.55);box-shadow:0 12px 30px rgba(0,0,0,.45)}
.srv-list.srv-drag-active .srv-head{cursor:grabbing}

/* ---- server dock (avatar quick-nav) ---------------------------------------
   ≥1300px: a slim vertical strip of translucent server-avatar buttons, left
   of the content, vertically centred, meant to sit exactly midway between the
   viewport edge and the Modrinth/editor panel's own resting left edge. That
   target isn't a single constant across all widths: below SINGLE_FS_BP
   (1180px, server-panel.js) an open panel goes full-screen (`left:0`, see the
   media block further down), so hugging the edge (default rule below) IS the
   midpoint; above it the panel docks at `left:var(--dh-gap)` (60px) instead,
   so the midpoint sits further in — the `min-width:1181px` override recentres
   it there. z-index sits BELOW the Modrinth/editor docks (25) so their
   slide-in animation passes in front of it instead of looking like it dives
   underneath; only the hover popup (.srv-dock-tip below) stays above
   everything. max-height+overflow-y is a safety net for a user with enough
   servers to overflow the viewport height — without it the strip just grew
   past the screen edge with no way to reach the rest.
   <1300px: same DOM (server-panel.js builds it once, unconditionally — see
   buildServerDock), but reflowed by the media query below into a horizontal
   bar fixed along the BOTTOM edge instead — the narrow-viewport entry point
   to Пространства/avatars, always on screen (not just while scrolled to top),
   with its own overflow-x scroll once the avatars don't fit. Reserves a gutter
   on the right for the support-chat launcher (see .support's own mobile rule
   below) so the two together read as one footer bar. Mirrors the srv-list DOM
   order in both orientations — server-panel.js rebuilds it after a
   drag-reorder — so it always reflects the same order the list itself is in. */
.srv-dock{position:fixed;top:50%;left:1px;max-height:calc(100vh - 40px);overflow-y:auto;
  transform:translateY(-50%);z-index:20;display:flex;flex-direction:column;gap:10px;
  padding:10px 7px;border-radius:var(--r-lg);background:var(--panel);border:1px solid var(--line2);
  box-shadow:0 10px 28px rgba(0,0,0,.5)}
@media(min-width:1181px){
  .srv-dock{left:calc(var(--dh-gap, 60px) / 2 - 2px);transform:translate(-50%,-50%)}
}
.srv-dock[hidden]{display:none}
@media(max-width:1300px){
  /* No box-shadow here: it and overflow-x:auto on the same box is a classic
     CSS trap — a shadow is normally painted OUTSIDE the border box, but once
     the box scrolls, the browser clips the shadow to the scrollport too, so
     it renders as a ragged partial band instead of a clean glow. No border
     either (the vertical strip's own border:1px solid var(--line2), reset
     here) — the panel background against the page behind it is separation
     enough on its own. */
  .srv-dock{top:auto;bottom:0;left:0;right:64px;max-height:none;transform:none;
    flex-direction:row;align-items:center;overflow-x:auto;overflow-y:hidden;
    border-radius:0;border:0;box-shadow:none}
}
/* Same corner radius as the avatar everywhere else it appears (.core-box.has-avatar
   img) — a rounded rectangle, not a circle. */
.srv-dock-item{flex:0 0 auto;width:38px;height:38px;padding:0;border-radius:10px;border:2px solid transparent;
  background:transparent;cursor:pointer;opacity:.5;transition:opacity .18s,border-color .18s,transform .18s}
.srv-dock-item:hover,.srv-dock-item:focus-visible{opacity:1;border-color:rgba(47,212,106,.5);transform:scale(1.08);outline:none}
.srv-dock-item img{width:100%;height:100%;object-fit:cover;border-radius:8px;display:block;pointer-events:none}
/* Drag-to-reorder from the dock itself (wireDockDrag) — same treatment as the
   main list's own .srv-dragging. */
.srv-dock-item.srv-dock-dragging{opacity:1;border-color:rgba(47,212,106,.7);box-shadow:0 8px 20px rgba(0,0,0,.5)}
.srv-dock.srv-dock-drag-active{cursor:grabbing}
/* Name popup — styled like .dh-fixed-tip below, but positioned by JS to the
   LEFT of the hovered avatar (it only flips right if the gutter is too
   narrow to hold it — see showSrvDockTip in server-panel.js). Never the
   browser's native title tooltip. */
.srv-dock-tip{position:fixed;z-index:9999;max-width:220px;background:var(--panel);border:1px solid var(--line2);
  border-radius:8px;padding:7px 11px;font-family:var(--sans);font-size:12.5px;font-weight:600;line-height:1.4;
  color:#fff;white-space:nowrap;pointer-events:none;box-shadow:0 10px 28px rgba(0,0,0,.5);
  opacity:0;transform:translateY(4px);transition:opacity .15s,transform .15s}
.srv-dock-tip.show{opacity:1;transform:translateY(0)}

/* ---- workspaces ("Пространства") ----------------------------------------
   Subsets of the owned server list. The folder button (.ws-dock-btn) always
   lives at the head of the server dock above — server-panel.js builds it once
   (addWsDockButton) and the dock's own responsive skin (vertical strip / fixed
   bottom bar) carries it along, so there's a single trigger and a single
   .ws-pop popup implementation at every width now, not a second narrow-only
   copy. */
.ws-dock-btn{display:flex;align-items:center;justify-content:center;width:38px;height:38px;padding:0;
  margin-bottom:2px;padding-bottom:12px;border-bottom:1px solid var(--line2);
  background:transparent;border-left:0;border-right:0;border-top:0;cursor:pointer;color:var(--muted)}
.ws-dock-btn .btn-icon{width:20px;height:20px}
.ws-dock-btn:hover,.ws-dock-btn.active{color:var(--mint)}
/* Mobile bottom-bar skin: the divider rotates from underneath the icon (a
   column of items, folder button on top) to its right (a row of items,
   folder button first) — border-bottom/padding-bottom swap for border-right/
   padding-right. This has to come AFTER the base rule above: same selector,
   same specificity, so source order alone decides the winner, and this needs
   to beat the unconditional border-bottom/padding-bottom it's replacing. */
@media(max-width:1300px){
  .ws-dock-btn{margin:0 2px 0 0;padding:0 12px 0 0;border-bottom:0;border-right:1px solid var(--line2)}
}
/* Trailing "+" tile below the avatars — only present while a non-default
   workspace is active (see updateDockAddBtn) — same footprint as an avatar
   tile but dashed, so it reads as an "add" slot rather than another server. */
.ws-dock-add{flex:0 0 auto;width:38px;height:38px;padding:0;border-radius:10px;border:2px dashed var(--line2);
  background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;
  color:var(--muted);opacity:.8;transition:opacity .18s,border-color .18s,color .18s}
.ws-dock-add:hover{opacity:1;border-color:var(--mint);color:var(--mint)}
.ws-dock-add svg{width:18px;height:18px;fill:currentColor}
.ws-pop{position:fixed;z-index:120;width:300px;max-width:calc(100vw - 24px);padding:8px;
  background:var(--panel);border:1px solid var(--line2);border-radius:var(--r-lg);box-shadow:0 10px 28px rgba(0,0,0,.5);
  /* Smoothly track the sidebar dock's own top edge when it (and cascaded
     tier-2 popups anchored off this one) get re-positioned — see
     repositionWsPopups in server-panel.js — as the server count in the
     active workspace changes the dock's height (it's vertically centred). */
  transition:top .22s ease,left .22s ease}
.ws-pop[hidden]{display:none}
.ws-pop-head{padding:6px 10px 8px;font-family:var(--display);font-weight:700;font-size:14px;color:#fff;border-bottom:1px solid var(--line);margin-bottom:4px}
.ws-list{max-height:50vh;overflow-y:auto;display:flex;flex-direction:column;gap:2px}
.ws-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--text);font-size:14px}
.ws-item:hover{background:var(--green-dim)}
.ws-item.active{color:var(--mint);background:var(--green-dim)}
/* Leading folder glyph (svgIcon('root_folder'), a direct child — not wrapped in
   its own span) — same muted-until-active treatment as the row's own text.
   Sized up from an earlier 16px: at 16 the Material-symbol paths (folder/lock/
   rename/select/delete) read as slightly blocky/pixelated — 18-20px gives the
   anti-aliasing enough room to look clean. */
.ws-item>svg{width:19px;height:19px;flex:0 0 auto;fill:currentColor;color:var(--muted2)}
.ws-item.active>svg{color:var(--mint)}
.ws-item .ws-item-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ws-item input{flex:1;background:#0a0d0b;border:1px solid var(--mint);border-radius:6px;color:#fff;font-size:14px;padding:4px 7px;min-width:0}
/* 6-dot drag grip (custom rows only, in place of the folder glyph — see
   wsGripIcon in server-panel.js). Wrapped in its own span rather than left as
   a bare svg so the DRAG HITBOX can be widened without moving the visible
   icon: padding grows the box outward (left/top/bottom, up to the row's own
   edges) while equal negative margins cancel it back out of the flex flow,
   so layout/gap with the next sibling and the row's own height are both
   unaffected. Only the right edge is left at the icon's own 19px box, per
   design. The row's height is set by the 27px-tall .ws-item-act buttons
   (not this 19px icon), so reaching its top/bottom takes the 8px row
   padding PLUS the 4px of align-items:center slack around the icon within
   that taller lane — 12px each side, not just the row's own 8px padding. */
.ws-item-grip{display:flex;align-items:center;justify-content:center;flex:0 0 auto;
  width:19px;height:19px;padding:12px 0 12px 10px;margin:-12px 0 -12px -10px;box-sizing:content-box;
  color:var(--muted2);cursor:grab;touch-action:none}
.ws-item-grip svg{width:19px;height:19px;flex:0 0 auto;fill:currentColor}
.ws-item-grip:hover{color:var(--text)}
.ws-item.active .ws-item-grip{color:var(--mint)}
.ws-item.ws-dragging{opacity:.6;background:var(--green-dim)}
.ws-list.ws-drag-active{cursor:grabbing}
/* Lock glyph replacing the action buttons on the un-deletable default row. */
/* Same 27px box as .ws-item-act (below) so the glyph itself lines up with the
   delete button's glyph on other rows — a bare icon flush against the row's
   right edge sat a few px further right than a glyph centred in a 27px
   button's own padding would. */
.ws-item-lock{display:flex;align-items:center;justify-content:center;width:27px;height:27px;flex:0 0 auto;color:var(--muted2)}
.ws-item-lock svg{width:17px;height:17px;fill:currentColor}
/* Per-row rename/servers/delete buttons — always visible (not hover-reveal,
   since the mobile action-bar entry point has no hover at all). */
/* No gap between the three buttons — even a 1px seam let the cursor dip
   into unhovered space while sliding across them, instantly hiding/showing
   the next button's data-tip tooltip (info-tip.js) and reading as flicker. */
.ws-item-acts{display:flex;align-items:center;gap:0;flex:0 0 auto;margin-left:2px}
.ws-item-act{display:flex;align-items:center;justify-content:center;width:27px;height:27px;padding:0;flex:0 0 auto;
  border-radius:6px;background:transparent;border:0;color:var(--muted2);cursor:pointer;transition:color .15s,background .15s}
.ws-item-act svg{width:16px;height:16px;fill:currentColor}
.ws-item-act:hover{color:var(--mint);background:rgba(255,255,255,.07)}
.ws-item-act.danger:hover{color:var(--danger);background:rgba(255,92,92,.14)}
.ws-add-row{margin-top:4px;padding-top:6px;border-top:1px solid var(--line)}
.ws-add-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 10px;border-radius:8px;
  background:none;border:0;color:var(--muted);cursor:pointer;font-size:14px;transition:color .15s,background .15s}
.ws-add-btn:hover{color:var(--mint);background:var(--green-dim)}
.ws-add-btn svg{width:16px;height:16px;fill:currentColor}
/* Bottom banner — shown while viewing a non-default workspace. Centred in the
   normal document flow below the server list (not fixed/sticky), matching the
   rest of the page's "everything on one page" scroll convention. */
.ws-footer{margin:40px 0;display:flex;flex-direction:column;align-items:center;gap:16px;text-align:center}
.ws-footer[hidden]{display:none}
.ws-footer-title{font-family:var(--display);font-weight:700;font-size:19px;color:#fff}
.ws-footer-all{padding:16px 38px;font-size:17px}
/* Members-picker flyout (openWsMembersFlyout) — a second .ws-pop cascading to
   the right of whatever anchor opened it, NOT a centred modal (see
   positionRightOf in server-panel.js). */
.ws-members-pop{width:300px}
.ws-member-list{max-height:min(50vh,420px);overflow-y:auto;display:flex;flex-direction:column;gap:2px}
.ws-member-row{display:flex;align-items:center;gap:10px;padding:6px;border-radius:8px;cursor:pointer}
.ws-member-row:hover{background:rgba(255,255,255,.04)}
.ws-member-avatar{width:30px;height:30px;border-radius:8px;object-fit:cover;background:#0a0d0b;flex:0 0 auto}
.ws-member-name{font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Both buttons stretch to fill the row equally (rather than sitting glued to
   the right at their own text-driven widths), which centers the pair as a
   group without needing justify-content:center. */
.ws-member-actions{display:flex;gap:8px}
.ws-member-actions .btn{flex:1 1 0}
/* Delete-confirmation popup (openWsConfirmPop) — same cascading-flyout idea as
   the members-picker above, replacing the centred confirmAsk modal so a
   destructive action reads as part of the same popup chain instead of jumping
   to the middle of the screen. */
.ws-confirm-pop{width:280px}
.ws-confirm-msg{padding:2px 10px 10px;color:var(--muted);font-size:13.5px;line-height:1.5}
/* Custom dark, larger checkbox — same unchecked/checked SVG-swap as the file
   manager's .sp-check, just bigger (24px vs 22px) and always visible instead
   of collapsed to zero width outside select-mode. Dark idle colour (muted2)
   per "dark themed" — .sp-check's default is lighter, meant to sit next to a
   filename rather than stand on its own like this one does. */
.ws-cb{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;flex:0 0 auto;
  color:var(--muted2);cursor:pointer;border-radius:6px;transition:color .15s}
.ws-cb svg{width:24px;height:24px;display:block}
.ws-cb .check{display:none}
.ws-cb.checked .uncheck{display:none}
.ws-cb.checked .check{display:block}
.ws-cb.checked{color:var(--mint)}
.ws-cb:hover{color:var(--text)}
.ws-cb.checked:hover{color:var(--mint)}

/* Name takes the free space so the address + chevron sit at the right edge
   (the status pill used to do this with margin-left:auto; it's now a dot). */
.srv-meta{flex:1 1 auto;min-width:0}
.srv-addr{margin-left:auto;display:inline-flex;align-items:center;gap:8px;background:var(--bg2);border:1px solid var(--line2);
  color:#cfe9d8;font-family:ui-monospace,'Cascadia Code','Consolas',monospace;font-size:13.5px;padding:7px 12px;border-radius:9px;
  cursor:pointer;min-width:0;max-width:min(46vw,320px);transition:border-color .15s,background .15s,color .15s}
.srv-addr-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.srv-addr:hover{border-color:rgba(47,212,106,.5);background:rgba(47,212,106,.08)}
.srv-addr.copied{border-color:rgba(47,212,106,.6);color:#bff5cf}
.srv-addr .srv-addr-ico{width:15px;height:15px;fill:currentColor;opacity:.7;flex-shrink:0}
@media(max-width:560px){ .srv-addr{padding:7px 10px;font-size:12.5px;gap:6px} }
/* Detach-console icon in the header — hidden by default; a narrow card shows it
   (only while the row is open + a fine pointer is present) in place of the
   console toolbar's text "Открепить". Same square icon-button look as the IP
   button's compact form; custom [data-tip] tooltip wired in server-panel.js. */
.srv-head-detach{display:none;flex:0 0 auto;width:36px;height:36px;align-items:center;justify-content:center;
  background:var(--bg2);border:1px solid var(--line2);border-radius:9px;color:var(--muted);cursor:pointer;padding:0;
  transition:border-color .15s,color .15s,background .15s}
.srv-head-detach:hover{border-color:rgba(47,212,106,.5);color:var(--mint);background:rgba(47,212,106,.08)}
.srv-head-detach svg{width:17px;height:17px;display:block;pointer-events:none}

/* Server status is shown ONLY as a coloured presence dot on the logo (see
   .status-dot). The old text pill is retained in the DOM for JS but hidden. */
.srv-state{display:none}

/* Presence dot at the bottom-right of the server logo (social-network style).
   Its label tooltip is NOT a CSS ::after (that gets clipped by the card's
   overflow:hidden once it reaches the card edge); instead server-panel.js
   renders a position:fixed .dh-fixed-tip on <body> so it's never clipped and
   always paints above every other layer. */
.status-dot{position:absolute;bottom:-2px;right:-2px;width:13px;height:13px;border-radius:50%;
  border:2.5px solid var(--bg2);background:var(--muted2);cursor:help;transition:background .2s,box-shadow .2s;z-index:2}
.status-dot[data-status="running"]{background:var(--mint);box-shadow:0 0 7px rgba(47,212,106,.75)}
.status-dot[data-status="offline"]{background:var(--danger)}
.status-dot[data-status="pending"]{background:#f2cc60}
.status-dot[data-status="unknown"]{background:var(--muted2)}
/* Floating tooltip (status dot, and any other popup that must escape a clipping
   ancestor). Fixed-positioned by JS; a very high z-index keeps it on top of the
   docked editor/Modrinth panels, the chat bubble, everything. */
/* Short labels stay on one line; long explanations wrap to a readable column
   (was white-space:nowrap, which forced them off-screen). */
.dh-fixed-tip{position:fixed;z-index:9999;max-width:280px;background:var(--panel);border:1px solid var(--line2);
  border-radius:8px;padding:8px 11px;font-family:var(--sans);font-size:12.5px;font-weight:500;line-height:1.45;
  color:#fff;white-space:pre-line;text-align:left;pointer-events:none;box-shadow:0 10px 28px rgba(0,0,0,.5);
  opacity:0;transform:translateY(4px);transition:opacity .15s,transform .15s}
.dh-fixed-tip.show{opacity:1;transform:translateY(0)}
/* ── Unified info glyph (ⓘ) + iOS switch — shared by both configurators, the
   server panel's Настройки tab and the file manager (see also info-tip.js). ── */
.info-dot{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;flex:0 0 18px;
  padding:0;border:0;background:transparent;color:var(--mint);cursor:help;opacity:.8;vertical-align:middle;
  -webkit-tap-highlight-color:transparent;transition:opacity .15s}
.info-dot:hover,.info-dot:focus-visible{opacity:1;outline:none}
.info-dot svg{width:18px;height:18px;display:block;pointer-events:none}
.dh-switch{position:relative;display:inline-flex;flex:0 0 auto;width:38px;height:22px;cursor:pointer;vertical-align:middle}
.dh-switch input{position:absolute;inset:0;width:100%;height:100%;opacity:0;margin:0;cursor:pointer;z-index:1}
.dh-switch-track{position:absolute;inset:0;border-radius:999px;background:rgba(255,255,255,.14);
  border:1px solid var(--line2);transition:background .18s,border-color .18s}
.dh-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;
  box-shadow:0 1px 3px rgba(0,0,0,.4);transition:transform .18s}
.dh-switch input:checked ~ .dh-switch-track{background:var(--mint);border-color:var(--mint)}
.dh-switch input:checked ~ .dh-switch-track .dh-switch-knob{transform:translateX(16px)}
.dh-switch input:focus-visible ~ .dh-switch-track{box-shadow:0 0 0 3px rgba(47,212,106,.35)}
.chev{transition:transform .2s;color:var(--muted);font-size:21px;line-height:1}
.srv.open .chev{transform:rotate(180deg)}

/* node status block */
.node-status{width:calc(100vw - var(--dh-ed-w,0px) - var(--dh-mr-w,0px) - 44px);
  margin:38px 0 0 50%;transform:translateX(-50%);container:node-status / inline-size}
.node-status .empty{text-align:left;padding:18px}
.nstat-head{display:flex;justify-content:center;margin-bottom:14px}
.nstat-pill{display:inline-flex;align-items:center;gap:10px;font-family:var(--display);font-size:15px;color:#fff;
  background:var(--card);border:1px solid var(--line);padding:11px 20px;border-radius:999px}
.nstat-pill .dot{width:11px;height:11px;border-radius:50%;flex-shrink:0;background:var(--mint);box-shadow:0 0 10px rgba(47,212,106,.8)}
.nstat-pill.bad{border-color:var(--danger)} .nstat-pill.bad .dot{background:var(--danger);box-shadow:0 0 10px rgba(255,92,92,.8)}
.comp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
.comp-grid .comp{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:14px 16px;display:flex;flex-direction:column;gap:5px}
.comp-grid .comp .nm{display:flex;align-items:center;gap:9px;color:#fff;font-size:14px}
.comp-grid .comp .nm .dot{width:9px;height:9px;border-radius:50%;flex-shrink:0;background:var(--mint);box-shadow:0 0 8px rgba(47,212,106,.7)}
.comp-grid .comp.bad .nm .dot{background:var(--danger);box-shadow:0 0 8px rgba(255,92,92,.7)}
.comp-grid .comp .mt{padding-left:18px;font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
.comp-grid .comp.bad .mt{color:#ffb3b3}
.node-charts{display:grid;grid-template-columns:repeat(2,minmax(0,480px));justify-content:center;gap:14px;margin-top:14px}
.node-charts .nc{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:18px}
.node-charts .nc h3{font-family:var(--display);font-size:15px;color:#fff;margin-bottom:10px}
.node-charts .nc canvas{width:100%;display:block}
.node-charts .nc-legend{grid-column:1/-1;display:flex;gap:22px;justify-content:center;color:var(--muted);font-size:13px}
.node-charts .nc-legend i{display:inline-block;width:14px;height:4px;border-radius:2px;margin-right:6px;vertical-align:middle}
.node-charts .nc-legend i.cpu{background:var(--mint)} .node-charts .nc-legend i.ram{background:var(--info)}
@media(max-width:720px){.node-charts{grid-template-columns:minmax(0,480px)}}
@container node-status (max-width:720px){.node-charts{grid-template-columns:minmax(0,480px)}}
.srv-body{border-top:1px solid var(--line);padding:18px;position:relative}

/* Invisible horizontal resize zones: only the ew-resize cursor reveals them.
   They sit over the left/right border of the expanded server-management body. */
.sp-width-grip{position:absolute;top:0;bottom:0;width:12px;z-index:4;cursor:ew-resize;
  touch-action:none;background:transparent}
.sp-width-grip-left{left:-6px}.sp-width-grip-right{right:-6px}
/* In split view the central server panel can resize from the edge adjacent to
   each open side panel, not only from the side panel's own seam hitbox. */
body.editor-open:not(.modrinth-open) .sp-width-grip-left,
body.modrinth-open:not(.editor-open) .sp-width-grip-right{display:none}
@media(max-width:760px){.sp-width-grip{display:none}
  .sp-pane-body,.sp-pane>.sp-drop{margin-right:0;padding-right:0}}

/* big load charts, shown between the console and the secondary tabs — always
   visible once a row is expanded (the small/big toggle handle was removed).
   The three charts sit in a row while there's width for it; below the width
   thresholds in server-panel.js (buildBigCharts → applyChartsMode) the wrap
   flips to a looped carousel — 2 charts, then 1 — rather than stacking into
   several short rows. */
.sp-bigcharts-wrap{height:120px;margin-bottom:10px;overflow:hidden;display:flex;flex-direction:column}
.sp-bc-car{flex:1;min-height:0;display:flex;align-items:stretch;gap:10px}
.sp-bc-viewport{flex:1;min-width:0;overflow:hidden;padding-bottom:1px}
.sp-bigcharts{display:flex;align-items:stretch;gap:14px;height:100%}
.sp-bigchart{flex:1;min-width:0;position:relative;display:flex;flex-direction:column;background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:14px 16px}
/* Label overlaid on the chart (not on its own row, which cost ~20px of height)
   with an infill hugging just the word, so the grid/trend lines never run
   visibly through the text. The infill must be OPAQUE and match the chart card's
   rendered colour — var(--card) is translucent white, so it would let the ~.09
   grid line show straight through. #141414 is the two stacked .04 --card layers
   (the .srv card + this chart card) composited over the black page. */
/* The label doubles as the live current-usage readout ("CPU: 125%"), refreshed
   every stats sample — tabular-nums keeps the digits from jittering the label's
   width around as the value changes each second. */
.sp-bigchart b{position:absolute;top:9px;left:12px;z-index:2;margin:0;font-size:12px;font-weight:700;letter-spacing:.03em;
  font-variant-numeric:tabular-nums;background:#141414;padding:1px 7px;border-radius:7px;line-height:1.35}
.sp-bigchart.cpu b{color:var(--mint)} .sp-bigchart.mem b{color:var(--info)} .sp-bigchart.net b{color:#f2a94e}
.sp-bigchart canvas{width:100%;flex:1;min-height:0;height:auto;display:block}
/* Desktop-only custom hover popup (non-native tooltip) for the load charts. Fixed
   to the viewport and anchored above the marked sample; server-panel.js sets its
   left/top. The ::after triangle points down at the sample dot. */
.sp-chart-tip{position:fixed;z-index:60;transform:translate(-50%,calc(-100% - 10px));display:none;
  flex-direction:column;align-items:center;gap:1px;padding:5px 9px;pointer-events:none;white-space:nowrap;
  background:#0c100e;border:1px solid var(--line2);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.5)}
.sp-chart-tip.show{display:flex}
.sp-chart-tip .sp-chart-tip-v{font-size:13px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1.15}
.sp-chart-tip::after{content:'';position:absolute;top:100%;left:50%;transform:translateX(-50%);
  border:5px solid transparent;border-top-color:var(--line2)}

/* Carousel chrome (side arrows + dots below) — hidden in the default 3-across
   row mode, revealed by the mode-car1/mode-car2 classes server-panel.js toggles
   by measured width. Same look/feel as the server configurator carousel. */
.sp-bc-arrow{flex:0 0 auto;width:34px;height:34px;align-self:center;border-radius:50%;background:#0a0d0b;color:#cfe;
  border:1px solid var(--line2);display:none;align-items:center;justify-content:center;cursor:pointer;transition:border-color .15s,color .15s,transform .1s}
.sp-bc-arrow:hover{border-color:var(--mint);color:#fff}
.sp-bc-arrow:active{transform:scale(.92)}
.sp-bc-arrow svg{width:16px;height:16px;display:block;pointer-events:none}
.sp-bc-dots{display:none;align-items:center;justify-content:center;gap:2px;height:16px;margin-top:6px;flex:0 0 auto}
.sp-bc-dot{width:16px;height:16px;padding:0;border:0;background:transparent;display:flex;align-items:center;justify-content:center;cursor:pointer}
.sp-bc-dot::after{content:'';width:6px;height:6px;border-radius:50%;background:var(--line2);transition:background .15s,transform .15s}
.sp-bc-dot:hover::after{background:#9fb}
.sp-bc-dot.active::after{background:var(--mint);transform:scale(1.3)}
.sp-bigcharts-wrap.mode-car1 .sp-bc-arrow,.sp-bigcharts-wrap.mode-car2 .sp-bc-arrow{display:flex}
.sp-bigcharts-wrap.mode-car1 .sp-bc-car,.sp-bigcharts-wrap.mode-car2 .sp-bc-car{margin-bottom:22px}
.sp-bigcharts-wrap.mode-row .sp-bigcharts{justify-content:center}
.sp-bigcharts-wrap.mode-row .sp-bigchart{max-width:360px}
.sp-bigcharts-wrap.mode-car1 .sp-bigcharts,.sp-bigcharts-wrap.mode-car2 .sp-bigcharts{cursor:grab}
.sp-bigcharts-wrap.mode-car1 .sp-bigcharts.dragging,.sp-bigcharts-wrap.mode-car2 .sp-bigcharts.dragging{cursor:grabbing}
/* one visible chart = 100%, two = half each (the 14px track gap makes 2 fit exactly) */
.sp-bigcharts-wrap.mode-car1 .sp-bigchart{flex:0 0 100%}
.sp-bigcharts-wrap.mode-car2 .sp-bigchart{flex:0 0 calc(50% - 7px)}
/* permanent console section (always shown at the top of an expanded server) */
.sp-console-slot{margin-bottom:6px}

/* The fixed-height, scrollable tabs region — every tab shares one height so
   switching tabs never jerks the panel. */
/* No longer the scroll area — each pane scrolls its own body below its pinned
   header (see the .sp-pane flex rules), so the container never shows a scrollbar
   of its own (which used to span over every tab's header). overflow:visible (not
   hidden) so the scroll body's scrollbar, nudged out into the right margin via a
   negative margin, isn't clipped. The panes fill it exactly, so it never scrolls. */
.sp-panes-container{overflow:visible}

/* VISIBLE resize grips — a centred pill below each resizable section (console
   and tabs). ns-resize cursor; brightens to mint on hover/drag. */
.sp-grip{height:18px;margin:2px 0 8px;cursor:ns-resize;display:flex;align-items:center;justify-content:center;
  touch-action:none;user-select:none}
.sp-grip::after{content:"";width:46px;height:5px;border-radius:3px;background:var(--line2);transition:background .15s,width .15s}
.sp-grip:hover::after,.sp-grip.dragging::after{background:var(--mint);width:66px}
/* The last grip belongs visually to the server card's bottom edge. Pull its
   trailing margin into the body's padding so the handle hugs that edge instead
   of floating a full grip+padding block above it. */
.sp-grip-bottom{margin-bottom:-14px}

.sp-console-section{display:flex;flex-direction:column;gap:12px}
/* console toolbar: power controls on the left, live stats on the right */
.sp-con-bar{display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:space-between}
.sp-power-wrap{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.sp-power{display:flex;gap:8px;flex-wrap:wrap}
.btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
/* «Запустить» disabled because the server is already running carries a live-uptime
   [data-tip] (set in onStats) — reads as "hover for info", not a dead end, so it gets
   a help cursor instead of the usual not-allowed. Scoped to [data-tip] specifically so
   a start button disabled for another reason (action in flight, no tip set) still
   shows the normal not-allowed cursor. */
.btn.power-start:disabled[data-tip]{cursor:help}

/* Power button colour variants: restart = yellow, stop = red, kill = black/dark.
   All are ghost outlines so a disabled state still reads clearly. */
.btn.power-restart{border-color:rgba(242,204,96,.6);color:#f2cc60}
.btn.power-restart:hover:not(:disabled){color:#fff;border-color:#f2cc60;background:rgba(242,204,96,.12)}
.btn.power-stop{border-color:rgba(255,92,92,.55);color:#ffb3b3}
.btn.power-stop:hover:not(:disabled){color:#fff;border-color:var(--danger);background:rgba(255,92,92,.12)}
.btn.power-kill{border-color:var(--line2);color:#cdd3cd;background:rgba(0,0,0,.35)}
.btn.power-kill:hover:not(:disabled){color:#fff;border-color:#888;background:rgba(0,0,0,.6)}
/* Power action in progress: the acting button hides its label and spins, staying
   bright even though it's locked; the other buttons dim via .btn:disabled. «Убить»
   is left enabled during start/restart/stop (see _applyPowerButtons). */
.btn.is-loading{position:relative;color:transparent!important;pointer-events:none}
.btn.is-loading:disabled{opacity:1}
.btn.is-loading::after{content:"";position:absolute;top:50%;left:50%;width:14px;height:14px;margin:-7px 0 0 -7px;
  border-radius:50%;border:2px solid rgba(255,255,255,.32);border-top-color:#fff;animation:spin .6s linear infinite}
.btn:not(.ghost).is-loading::after{border-color:rgba(4,20,10,.3);border-top-color:#04140a}

/* Shared "(i)" info button + custom hover/focus tooltip (power commands, editor
   help). Pure CSS — focus-within reveals it on touch/keyboard too. */
.sp-info-wrap{position:relative;display:inline-flex;align-items:center}
/* A proper info glyph (masked info.svg), not the literal italic letter "i" —
   the letter rendered off-centre inside the circle. */
.sp-info-btn{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;
  background:none;border:0;color:var(--muted);cursor:help;transition:color .15s}
.sp-info-btn:hover{color:var(--mint)}
.sp-info-ico{display:block;width:21px;height:21px;background-color:currentColor;
  -webkit-mask:url(icons/info.svg) center/contain no-repeat;mask:url(icons/info.svg) center/contain no-repeat}
/* Tooltip opens DOWNWARD from the button — both (i) buttons sit near the top of
   their containers (which clip overflow), so opening up would get cut off. */
.sp-info-tip{position:absolute;top:calc(100% + 9px);left:50%;transform:translateX(-50%) translateY(-4px);z-index:9999;
  background:var(--panel);border:1px solid var(--line2);border-radius:9px;padding:10px 13px;
  font-size:12.5px;color:var(--muted);line-height:1.55;width:max-content;max-width:min(300px,86vw);
  box-shadow:0 14px 34px rgba(0,0,0,.6);opacity:0;pointer-events:none;transition:opacity .15s,transform .15s}
.sp-info-tip b{color:#fff;font-weight:600}
.sp-info-tip .sp-info-tip-head{margin-top:6px;color:var(--muted);font-weight:600}
.sp-info-wrap:hover .sp-info-tip,.sp-info-wrap:focus-within .sp-info-tip{opacity:1;transform:translateX(-50%) translateY(0)}
/* Editor info button anchors its tooltip to the right edge (it sits at the far
   right of the foot bar, so a centred tooltip would clip off-screen). */
.dh-ed-info-wrap .sp-info-tip{left:auto;right:0;transform:translateY(-4px)}
.dh-ed-info-wrap:hover .sp-info-tip,.dh-ed-info-wrap:focus-within .sp-info-tip{transform:translateY(0)}
.dh-ed-foot-spacer{flex:1 1 auto;min-width:8px}
.sp-stats{display:flex;gap:16px;flex-wrap:wrap;align-items:center;font-size:13px;color:var(--muted)}
.sp-stats b{color:#fff;font-family:var(--display);font-weight:600;margin-left:4px}
.sp-con-right{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
/* "Открепить" — open the console in a separate browser window — desktop only */
.sp-detach{display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.04);border:1px solid var(--line2);
  color:var(--muted);cursor:pointer;border-radius:10px;padding:6px 11px;font-size:13px;font-family:var(--sans);white-space:nowrap;
  transition:border-color .15s,color .15s,background .15s}
.sp-detach:hover{border-color:var(--mint);color:var(--mint);background:var(--green-dim)}
.sp-detach svg{width:16px;height:16px;fill:currentColor;flex-shrink:0}
/* Overlay "update available" icon (Rust/CS2) — a small accented corner badge
   next to the power buttons; a pulsing dot draws the eye without being loud. */
.sp-update-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;border-radius:10px;background:rgba(255,255,255,.04);border:1px solid var(--line2);
  color:var(--amber,#e5c07b);cursor:pointer;transition:border-color .15s,color .15s,background .15s}
.sp-update-btn:hover{border-color:var(--amber,#e5c07b);background:rgba(229,192,123,.12)}
.sp-update-btn svg{width:18px;height:18px;fill:currentColor}
.sp-update-btn::after{content:'';position:absolute;top:-3px;right:-3px;width:9px;height:9px;border-radius:50%;
  background:var(--amber,#e5c07b);box-shadow:0 0 0 2px var(--card);animation:sp-update-pulse 1.6s ease-in-out infinite}
@keyframes sp-update-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.25)}}
/* thin rounded bar shown in place of a console that's been popped out into a window */
.sp-detached-note{display:flex;align-items:center;justify-content:center;gap:16px;flex-wrap:wrap;
  min-height:46px;background:var(--card);border:1px solid var(--line2);border-radius:999px;padding:7px 20px}

/* standalone /console/<id> page — one server shown as a console that fills the
   window exactly: a full-height flex column from <body> down to the console
   output, which is the only thing that grows/scrolls. No page scroll, no gap. */
.dh-console-page{height:100vh;display:flex;flex-direction:column;overflow:hidden}
.dh-console-page main.container{flex:1;min-height:0;display:flex;flex-direction:column;max-width:none;margin:0;padding:14px 18px}
.dh-console-page .srv-list{flex:1;min-height:0;display:flex;margin:0}
.dh-console-page .srv{flex:1;min-height:0;display:flex;flex-direction:column;background:none;border:0;overflow:visible}
.dh-console-page .srv-head{display:none}        /* page header already names the server */
.dh-console-page .srv-body{flex:1;min-height:0;display:flex;flex-direction:column;border-top:0;padding:0}
.dh-console-page .sp-console-slot{flex:1;min-height:0;display:flex;margin:0}
.dh-console-page .sp-console-section{flex:1;min-height:0}
.dh-console-page .sp-console-wrap{flex:1;min-height:0}
.dh-console-page .sp-console{flex:1;height:auto;min-height:0} /* the only growing/scrolling element */
/* Header server icon reuses the green rounded-square core-box; shows the custom
   avatar (cover) when set, else the core glyph. */
.dh-console-page .brand .core-box{width:38px;height:38px;border-radius:10px}
.dh-console-page .brand .core-box img{width:24px;height:24px}
.dh-console-page .brand .core-box.has-avatar img{width:100%;height:100%}

/* tabs */
/* Single non-wrapping row that scrolls horizontally when the tabs don't fit
   (like the mobile behaviour, now the default) — never a second row. The wheel
   is redirected to horizontal scroll in server-panel.js. Scrollbar hidden. */
.sp-tabs{display:flex;gap:4px;flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;border-bottom:1px solid var(--line);margin-bottom:16px}
.sp-tabs::-webkit-scrollbar{display:none}
.sp-tab{background:none;border:0;border-bottom:2px solid transparent;color:var(--muted);font-family:var(--sans);font-size:14px;font-weight:600;padding:9px 13px;cursor:pointer;margin-bottom:-1px;flex:0 0 auto;white-space:nowrap}
.sp-tab:hover{color:#fff}
.sp-tab.on{color:var(--mint);border-bottom-color:var(--mint)}
.sp-pane.hide{display:none}

/* console */
/* line-height is a fixed 19px, not a unitless multiplier: 12.5px * 1.5 = 18.75px,
   a non-integer line-box height. Browsers round each row to a whole device pixel,
   and that rounding error accumulates across rows and periodically snaps back —
   the "every Nth row is 1px short" artifact. A fixed integer line-height removes
   the fractional math entirely, so every row is exactly the same height. */
.sp-console{position:relative;background:#050706;border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;height:510px;overflow-y:auto;cursor:text;
  font-family:'SF Mono',ui-monospace,'Cascadia Code',Consolas,monospace;font-size:12.5px;line-height:19px;color:#cdd6cf;white-space:pre-wrap;word-break:break-word}
/* The command row stays hidden while connecting, so the status label owns the
   first console column and never competes with an inert prompt marker. */
.sp-console[data-loading]::before{content:'Подключение к консоли…';position:absolute;top:12px;left:14px;color:var(--muted);pointer-events:none}
/* An empty .cl (blank console line) generates no inline content, so browsers can
   collapse it below the parent's line-height strut. min-height must match that
   19px (not the old 1em/1.5em, both of which are relative to font-size and would
   reintroduce fractional heights) or blank rows render shorter than text rows. */
.sp-console .cl{min-height:19px}
.sp-console a{color:#79c0ff;text-decoration:underline;text-underline-offset:2px}
.sp-console a:hover{color:#a5d6ff}
/* the input row is the permanent last line of the console output itself —
   one continuous scrollback, cmd.exe-style, instead of a separate input box */
.sp-prompt-row{display:flex;align-items:flex-start;gap:8px;position:relative}
.sp-prompt-row[hidden]{display:none}
.sp-prompt-row .sp-prompt{flex-shrink:0;color:#2FD46A;user-select:none}
.sp-prompt-row .sp-prompt-text{flex:1;min-width:0;white-space:pre-wrap;word-break:break-word}
/* wrapper holding the textarea + its gray ghost-suffix mirror overlay */
.sp-cmd-wrap{position:relative;flex:1;min-width:0;display:block}
.sp-cmd{width:100%;display:block;resize:none;overflow:hidden;background:transparent;border:0;outline:0;padding:0;margin:0;
  position:relative;z-index:1;
  font:inherit;color:inherit;white-space:pre-wrap;word-break:break-word;caret-color:#cdd6cf}
/* mirror layer behind the textarea: typed text is transparent (reserves space),
   the trailing .g span is the gray completion hint shown after the caret */
.sp-cmd-ghost{position:absolute;inset:0;z-index:0;pointer-events:none;overflow:hidden;
  font:inherit;color:transparent;white-space:pre-wrap;word-break:break-word}
.sp-cmd-ghost .g{color:#5f6b62}
/* alphabetical, scrollable options popup floating above the prompt line */
.sp-comp-pop{position:absolute;left:18px;bottom:calc(100% + 5px);z-index:20;
  min-width:170px;max-width:min(440px,92%);max-height:220px;overflow-y:auto;
  background:#0c110d;border:1px solid var(--line2);border-radius:9px;padding:4px;
  box-shadow:0 10px 30px rgba(0,0,0,.6)}
.sp-comp-item{padding:4px 11px;border-radius:6px;cursor:pointer;white-space:nowrap;
  color:#cdd6cf;font-family:inherit;font-size:12.5px;line-height:19px}
.sp-comp-item:hover{background:rgba(255,255,255,.06)}
.sp-comp-item.sel{background:var(--green-dim);color:var(--mint)}

/* ⓘ marker on the embedded console-completion agent jar in the files list —
   uses the shared .info-dot glyph; this only adds the inline spacing. */
.sp-fi-info{margin-left:5px}

/* console wrapper (positioning context for the floating jump-to-bottom arrow) */
.sp-console-wrap{position:relative;display:flex;flex-direction:column;min-height:0}
.sp-scrolldown{position:absolute;right:16px;bottom:14px;z-index:6;width:38px;height:38px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;background:var(--green-dim);border:1px solid rgba(47,212,106,.55);
  color:var(--mint);cursor:pointer;opacity:0;transform:translateY(8px);pointer-events:none;
  transition:opacity .18s ease,transform .18s ease,background .15s;box-shadow:0 6px 18px rgba(0,0,0,.5)}
.sp-scrolldown.show{opacity:1;transform:none;pointer-events:auto}
.sp-scrolldown:hover{background:rgba(47,212,106,.24);border-color:var(--mint)}
.sp-scrolldown-ico svg{width:20px;height:20px;fill:currentColor;display:block}

/* generic pane bits */
.sp-sec{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:4px 0 14px}
.sp-sec h3{font-family:var(--display);font-size:16px;color:#fff;margin:0}
.sp-loading,.sp-empty{padding:26px;text-align:center;color:var(--muted)}
.sp-kv{padding:14px 16px;margin:10px 0}
.sp-kv.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r)}

/* --- Databases tab: per-database cards with a storage usage bar ------------- */
.db-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px;margin-top:12px}
.db-card{margin:0;padding:16px;display:flex;flex-direction:column;gap:13px}
.db-card-over{border-color:rgba(255,92,92,.45)}
.db-card-head b{font-size:15.5px;word-break:break-all}
.db-conn{display:flex;flex-direction:column;gap:6px}
.db-conn-row{display:flex;align-items:center;gap:8px;min-width:0}
.db-conn-key{color:var(--muted);font-size:12.5px;flex:0 0 92px}
.db-conn-val{font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
.db-conn-val.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.db-copy{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;
  background:transparent;border:1px solid var(--line2);border-radius:7px;color:var(--muted);cursor:pointer;transition:.15s}
.db-copy:hover{color:var(--mint);border-color:var(--mint)}
.db-copy svg{width:16px;height:16px}
.db-usage{display:flex;flex-direction:column;gap:7px;border-top:1px solid var(--line);margin-top:1px;padding-top:12px}
.db-usage-head{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
.db-usage-label{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;color:var(--text)}
.db-usage-edit{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;background:transparent;border:0;border-radius:6px;color:var(--muted);cursor:pointer;transition:.15s}
.db-usage-edit:hover{color:var(--mint);background:var(--card2)}
.db-usage-edit svg{width:15px;height:15px}
.db-usage-track{height:8px;border-radius:6px;background:rgba(255,255,255,.08);overflow:hidden}
.db-usage-fill{display:block;height:100%;border-radius:6px;transition:width .3s ease;background:var(--mint)}
.db-usage-fill.warn{background:#e6b800}
.db-usage-fill.crit{background:var(--danger)}
.db-usage-meta{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:12.5px}
.db-over-badge{flex:0 0 auto;font-size:11px;font-weight:600;color:var(--danger);background:rgba(255,92,92,.12);
  border:1px solid rgba(255,92,92,.3);border-radius:6px;padding:2px 7px}
.db-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:2px}
.db-actions .btn{flex:1 1 auto}

/* --- storage configurator modal (single slider + В месяц/В день knob) ------- */
.dbcfg-box{max-width:420px}
.dbcfg-head{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-top:18px}
.dbcfg-lab{font-size:13px;font-weight:600;color:var(--text)}
.dbcfg-size{font-size:18px;font-weight:700;color:var(--mint);font-family:var(--display)}
.dbcfg-slider-wrap{margin:12px 0 4px}
.dbcfg-slider{width:100%}
.dbcfg-minmax{display:flex;justify-content:space-between;color:var(--muted2);font-size:12px}
.dbcfg-price-block{margin-top:16px;padding-top:14px;border-top:1px solid var(--line)}
.dbcfg-price-row{display:flex;justify-content:space-between;align-items:center;gap:10px}
.dbcfg-price{font-size:19px;font-weight:700;color:var(--text)}
.dbcfg-per-group{display:inline-flex;border:1px solid var(--line2);border-radius:9px;overflow:hidden;flex:0 0 auto}
.dbcfg-per{background:transparent;border:0;color:var(--muted);font-size:12.5px;padding:6px 12px;cursor:pointer;transition:.15s}
.dbcfg-per+.dbcfg-per{border-left:1px solid var(--line2)}
.dbcfg-per.active{background:var(--green-dim);color:var(--mint)}
/* ── Запуск tab ──────────────────────────────────────────────────────────────
   Card 1 «Параметры запуска»: title on the left, the JVM-flag dropdown alone on
   the right of the same line, resolved command underneath. */
.sp-run-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0 0 10px;flex-wrap:wrap;position:relative;z-index:1}
/* The preset menu sometimes opens upward over the JVM-chip editor, which is a
   later sibling in the card. Lift the whole header's stacking context while its
   selector is open, not just the menu itself, so both opening directions render
   above every element in the startup card. */
.sp-run-head.sp-run-head-menu-open{z-index:40}
.sp-run-head .csel.open{z-index:41}
.sp-run-head b{font-family:var(--display);font-size:15px;color:#fff}
.sp-run-head-act{flex:0 0 auto;min-width:230px}
/* Widened preset dropdown: each option carries its own description line (see
   .csel-opt-desc below), so the trigger and the open menu both need more room
   than the plain single-line selects elsewhere on this tab. */
.sp-run-head-act-wide{min-width:360px}
.sp-run-head-act-wide .csel-menu{min-width:360px}
.sp-run-cmd{display:block;background:#050706;border:1px solid var(--line);border-radius:var(--r);padding:10px 13px;
  font-family:ui-monospace,Consolas,monospace;font-size:12.5px;color:#bff5cf;word-break:break-all}
@media(max-width:560px){.sp-run-head-act,.sp-run-head-act-wide{width:100%;min-width:0}}
/* Per-option description inside a dropdown menu (csel-opt-desc), used by the
   preset selector above. Plain callers of makeSpSel that never set o.desc get
   nothing extra: .csel-opt-txt just wraps the single label span as before. */
.csel-opt-txt{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.csel-opt-desc{font-size:11.5px;line-height:1.35;color:var(--muted);white-space:normal}
.csel-opt.sel .csel-opt-desc{color:var(--mint)}

/* ── JVM flag editor (Запуск tab) ────────────────────────────────────────────
   One wrapping command box: fixed fragments are text, every editable flag is a
   pill with its own value control and remove button. Pills drag to reorder; the
   add-flag search lives in the same flow. */
.jvmf-editor{margin-top:14px;position:relative;background:#050706;border:1px solid var(--line);border-radius:var(--r);padding:10px 13px}
.jvmf-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;min-height:42px;font-family:ui-monospace,Consolas,monospace;font-size:12.5px;color:#bff5cf;line-height:1.7}
.jvmf-static{white-space:pre-wrap;overflow-wrap:anywhere}
.jvmf-chip{display:flex;align-items:center;gap:8px;background:var(--card);border:1px solid var(--line2);
  border-radius:var(--r);padding:7px 8px 7px 12px;cursor:grab}
.jvmf-chip.drag{opacity:.4}
.jvmf-chip:hover{border-color:var(--line2);background:var(--card2)}
.jvmf-name{display:flex;align-items:center;gap:5px;font-size:13px;color:#fff;white-space:nowrap}
.jvmf-prefix{color:var(--muted2);font-family:ui-monospace,Consolas,monospace}
.jvmf-info{opacity:.6}
.jvmf-info:hover,.jvmf-info:focus-visible{opacity:1}
.jvmf-val{font-size:13px;border-radius:7px;border:1px solid var(--line2);background:#0a0d0b;color:#fff}
.jvmf-bool{padding:5px 10px;cursor:pointer;font-family:ui-monospace,Consolas,monospace;font-weight:600}
.jvmf-bool.on{color:var(--mint);border-color:rgba(47,212,106,.4);background:var(--green-dim)}
.jvmf-bool.off{color:var(--danger)}
.jvmf-select{padding:5px 8px;cursor:pointer}
.jvmf-marker{padding:5px 9px;color:var(--muted);font-family:ui-monospace,Consolas,monospace;cursor:default}
.jvmf-numwrap{display:flex;align-items:center;border:1px solid var(--line2);border-radius:7px;background:#0a0d0b;overflow:hidden}
.jvmf-num{border:0!important;background:transparent!important;width:60px;padding:5px 4px 5px 8px;
  font-family:ui-monospace,Consolas,monospace;text-align:right}
.jvmf-num:focus{outline:none}
.jvmf-unit{padding:5px 8px 5px 2px;color:var(--muted);font-size:11.5px}
.jvmf-x{flex-shrink:0;background:none;border:0;color:var(--muted2);cursor:pointer;font-size:11px;line-height:1;
  padding:5px 6px;border-radius:5px}
.jvmf-x:hover{background:var(--danger);color:#fff}
.jvmf-add{position:relative;flex:1 1 180px;min-width:160px}
.jvmf-search{width:100%;height:100%;min-height:34px;background:transparent;border:1px dashed var(--line2);
  border-radius:var(--r);padding:7px 12px;font-size:13px;color:#fff}
.jvmf-search:focus{outline:none;border-style:solid;border-color:var(--mint)}
.jvmf-results{position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:20;max-height:340px;overflow-y:auto;
  background:#0c0f0d;border:1px solid var(--line2);border-radius:var(--r);padding:6px;box-shadow:0 14px 34px rgba(0,0,0,.5)}
.jvmf-result{padding:8px 10px;border-radius:8px;cursor:pointer}
.jvmf-result:hover{background:rgba(255,255,255,.06)}
.jvmf-result-name{font-size:13px;color:#fff;font-family:ui-monospace,Consolas,monospace}
.jvmf-result-desc{font-size:11.5px;color:var(--muted);line-height:1.35;margin-top:2px}
.jvmf-noresults{padding:10px}

/* Card 2 «Конфигурация»: columns side by side, like the Настройки tab. Each
   column is a coherent group (Ядро / Среда выполнения / Мир / Доступ …) and the
   grid reflows to fewer columns as the panel narrows. */
.sp-cfg-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(255px,1fr));gap:20px 30px;margin-top:14px}
.sp-field{margin:0 0 15px;min-width:0}
.sp-field:last-child{margin-bottom:0}
.sp-field label{display:block;margin:0 0 7px;font-size:13.5px;font-weight:600;color:#fff}
.sp-field label .muted{display:block;font-weight:400;margin-top:3px;line-height:1.35}
.sp-field input,.sp-field .csel{width:100%}
.sp-field input[type=number]{appearance:textfield;-moz-appearance:textfield}
.sp-cfg-stack{display:flex;flex-direction:column;gap:8px}
.sp-cfg-row{display:flex;gap:8px;align-items:center}
.sp-cfg-row input{flex:1;min-width:0}
.sp-cfg-dice{flex:0 0 auto;font-size:15px;line-height:1}
.sp-cfg-switch{padding-top:2px}
.sp-cfg-static{padding:8px 12px;border-radius:8px;border:1px solid var(--line2);background:#0a0d0b;color:var(--muted);font-size:13.5px}
/* «Применить» for the core/version pair sits at the bottom of its column so the
   two columns' controls still line up. */
.sp-cfg-act{margin-top:auto;padding-top:6px}
.sp-cfg-act .btn{width:100%}
/* Modpack-created servers' Конфигурация column: icon + title + source logo. */
.sp-cfg-mp-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}
.sp-cfg-mp-icon{width:44px;height:44px;border-radius:10px;object-fit:cover;background:#0a0d0b;flex-shrink:0}
.sp-cfg-mp-info{min-width:0}
.sp-cfg-mp-title{font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sp-cfg-mp-srclogo{height:14px;margin-top:4px;opacity:.8}
/* A field that just saved flashes its edge mint — quieter than a toast per field. */
.sp-saved,.sp-saved .csel-trigger,.sp-saved input{border-color:var(--mint)!important;box-shadow:0 0 0 3px rgba(47,212,106,.16)}
.csel.busy{opacity:.55;pointer-events:none}

/* Filename completion (Файл запуска / главный файл скрипта). */
.sp-fc{position:relative}
.sp-fc input{width:100%}
.sp-fc-pop{position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:26;background:#0c0f0d;border:1px solid var(--line2);
  border-radius:8px;padding:4px;max-height:210px;overflow:auto;box-shadow:0 18px 40px rgba(0,0,0,.55)}
.sp-fc-opt{padding:6px 9px;border-radius:5px;font-family:ui-monospace,Consolas,monospace;font-size:12.5px;color:#fff;cursor:pointer;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sp-fc-opt:hover,.sp-fc-opt.on{background:var(--green-dim);color:var(--mint)}
/* Настройки tab — grouped cards so related controls read as one block. */
.sp-set-card{padding:16px 18px;margin:14px 0}
.sp-set-card:first-of-type{margin-top:4px}
.sp-set-card-h{margin:0 0 4px}
.sp-set-card-h b{font-family:var(--display);font-size:15px;color:#fff}
.sp-set-div{height:1px;margin:16px 0;background:var(--line);border:0}
.sp-set-toggle{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-top:12px}
.sp-set-toggle-main{display:inline-flex;align-items:center;gap:8px;min-width:0}
.sp-set-toggle-label{font-size:14.5px;color:#fff;font-weight:600}
.sp-set-card.danger{border-color:rgba(255,92,92,.4)}
.sp-set-card.danger .sp-set-card-h b{color:var(--danger)}
/* two-column card body (identity | resources, clone | assistant) */
.sp-set-2col{display:grid;grid-template-columns:minmax(0,0.85fr) minmax(0,1.15fr);gap:26px;margin-top:14px}
.sp-set-2col.even{grid-template-columns:1fr 1fr}
@media(max-width:760px){.sp-set-2col,.sp-set-2col.even{grid-template-columns:1fr;gap:22px}}
/* a card with no title starts its body flush with the padding */
.sp-set-card>.sp-set-2col:first-child{margin-top:0}
/* Профиль | Ресурсы: two separate rectangles, same row/widths as .sp-set-2col.
   Cards stretch to equal height so the «Изменить ресурсы» button stays pinned. */
.sp-set-row{display:grid;grid-template-columns:minmax(0,0.85fr) minmax(0,1.15fr);gap:14px;margin:4px 0 0}
.sp-set-row .sp-set-card{margin:0;display:flex;flex-direction:column}
.sp-set-row .sp-set-card>.sp-set-col{flex:1}
@media(max-width:760px){.sp-set-row{grid-template-columns:1fr;gap:14px}}
.sp-set-col{min-width:0;display:flex;flex-direction:column}
.sp-set-col-h{font-size:12.5px;color:var(--muted);font-weight:600;margin-bottom:12px;text-transform:uppercase;letter-spacing:.05em}
.sp-set-col-vsep{position:relative}
/* identity: avatar + name + id */
.sp-set-avatar{position:relative;width:116px;height:116px;border-radius:16px;overflow:hidden;cursor:pointer;
  border:1px solid var(--line2);background:#0c0f0d;flex:0 0 auto}
.sp-set-avatar img{width:100%;height:100%;object-fit:cover;display:block}
.sp-set-avatar-ov{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:6px;flex-direction:column;
  background:rgba(0,0,0,.5);opacity:0;transition:opacity .15s;color:#fff;font-size:11px;font-weight:600}
.sp-set-avatar:hover .sp-set-avatar-ov,.sp-set-avatar:focus-visible .sp-set-avatar-ov{opacity:1}
.sp-set-avatar-ov svg{width:26px;height:26px;fill:currentColor}
.sp-set-name{display:flex;align-items:center;gap:6px;margin-top:16px}
.sp-set-name-text{font-family:var(--display);font-size:18px;color:#fff;font-weight:600;word-break:break-word;min-width:0}
.sp-set-name-input{font-size:17px;padding:6px 10px;border-radius:8px;background:#0c0f0d;color:#fff;border:1px solid var(--mint);width:100%;min-width:0}
.sp-set-editbtn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:8px;
  border:0;background:transparent;color:var(--muted);cursor:pointer;flex:0 0 auto}
.sp-set-editbtn:hover{color:var(--mint);background:rgba(255,255,255,.07)}
.sp-set-editbtn svg{width:17px;height:17px;fill:currentColor}
.sp-set-id{display:inline-flex;align-items:center;gap:8px;margin-top:14px;padding:8px 14px;border-radius:999px;align-self:flex-start;
  border:1px solid var(--line2);background:#0c0f0d;color:var(--muted);font-size:13px;cursor:pointer;transition:border-color .15s,color .15s}
.sp-set-id:hover{border-color:var(--mint);color:#fff}
.sp-set-id svg{width:15px;height:15px;fill:var(--mint);flex:0 0 auto}
/* resource bars */
.sp-res-row{display:flex;align-items:center;gap:12px;margin:0 0 16px}
.sp-res-row:last-child{margin-bottom:0}
.sp-res-ic{flex:0 0 22px;height:22px;color:var(--mint);display:inline-flex;align-items:center}
.sp-res-ic svg{width:22px;height:22px;fill:currentColor}
.sp-res-body{flex:1;min-width:0}
.sp-res-top{display:flex;justify-content:space-between;align-items:baseline;font-size:12.5px;margin-bottom:6px;gap:8px}
.sp-res-top .k{color:var(--muted)} .sp-res-top .v{color:#fff;font-variant-numeric:tabular-nums;white-space:nowrap}
.sp-res-bar{height:7px;border-radius:999px;background:rgba(255,255,255,.09);overflow:hidden}
.sp-res-bar.alloc{position:relative}
/* the slice of the pool already allocated to OTHER servers (right, grayed) */
.sp-res-others{position:absolute;right:0;top:0;bottom:0;background:rgba(255,255,255,.2)}
.sp-res-fill{height:100%;border-radius:999px;background:var(--mint);width:0;transition:width .5s ease}
.sp-res-fill.warn{background:#f2cc60}
.sp-res-fill.hot{background:var(--danger)}
.sp-set-res-btn{margin-top:auto;padding-top:16px}
/* collapsible fold (SFTP) — NB: .sp-drop is taken (file-manager drop zone). */
.sp-fold-head{display:flex;align-items:center;justify-content:space-between;gap:10px;cursor:pointer;user-select:none}
.sp-fold-head .chev{transition:transform .2s;color:var(--muted);display:inline-flex}
.sp-fold-head .chev svg{width:22px;height:22px;fill:currentColor;display:block}
/* The fold lives inside the open server row, so `.srv.open .chev` (0,3,0) would
   rotate this chevron up in *both* states. Reset it at equal specificity (later
   in source wins) so closed points down; only .open (0,4,0) rotates it up. */
.sp-fold .sp-fold-head .chev{transform:none}
.sp-fold.open .sp-fold-head .chev{transform:rotate(180deg)}
.sp-fold-body{margin-top:14px;display:none}
.sp-fold.open .sp-fold-body{display:block}

/* Сеть tab — per-port firewall rule row (.sp-fold; see the generic fold rules
   above). Each port is its own card so the TCP/UDP editor doesn't need a table. */
.pr-row{padding:12px 16px}
/* nowrap — the fold header title is one row, full stop; a wrap fallback
   (tried briefly) pushed the note down onto a visible second row inside the
   header instead, which read as broken, not "responsive". The note must
   instead SHRINK (down to its own min-width floor, see .pr-note-wrap) and
   ellipsis-truncate on that single line rather than ever wrap. */
.pr-head-main{display:flex;align-items:center;flex-wrap:nowrap;gap:8px;min-width:0;flex:1 1 auto}
/* Editable note, inline in the fold header. Collapsed to a plain span + pencil
   glyph (.pr-note-view) until clicked; the entry box (.pr-note-inp) only exists
   while actually editing (2026-08-01 network-tab rework #3 — replaces the
   always-a-dark-entry-box look). Both share .pr-note-wrap's flex-grow so
   whichever is visible stretches across the header row's leftover width;
   min-width:15ch (not the old 40px) is the floor it shrinks to before the
   text itself starts ellipsis-truncating (.pr-note-text) — still one row,
   never a second row — ~15 Cyrillic characters, enough to read a short note
   without opening the editor. */
.pr-note-wrap{flex:1 1 15ch;min-width:15ch;display:flex}
.pr-note-view{display:flex;align-items:center;gap:8px;width:100%;min-height:32px;
  padding:6px 10px;border-radius:8px;cursor:pointer;color:var(--text);font-size:13px}
/* An explicit `display` on the element beats the browser's built-in
   `[hidden]{display:none}` (author rules win ties against the UA sheet), so
   without this override toggling .hidden left the view showing right next to
   the now-also-visible input instead of actually hiding it. */
.pr-note-view[hidden]{display:none}
.pr-note-view:hover{background:rgba(255,255,255,.05)}
.pr-note-view:hover .btn-icon,.pr-note-view:focus-visible .btn-icon{background-color:var(--mint)}
.pr-note-text{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pr-note-text.muted{color:var(--muted)}
.pr-note-view .btn-icon{flex:0 0 auto;width:15px;height:15px;background-color:var(--muted)}
.pr-note-inp{flex:1 1 auto;width:100%;background:transparent;
  border:1px solid var(--line2);border-radius:8px;padding:6px 10px;font-size:13px;color:var(--text)}
.pr-note-inp:focus{outline:none;border-color:var(--mint)}
.pr-note-inp::placeholder{color:var(--muted)}
.pr-proto-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.pr-proto-col{display:flex;flex-direction:column;gap:10px;padding:12px;border:1px solid var(--line2);border-radius:10px}
/* Protocol and its on/off switch form one compact, vertically-centred pair.
   The list mode and subnet field deliberately occupy their own rows below. */
.pr-proto-h{display:flex;align-items:center;gap:8px;font-weight:600}
.pr-proto-label{flex:0 0 auto}
.pr-proto-h .dh-switch{display:inline-flex;align-items:center;align-self:center;margin:0}
.pr-proto-h .dh-switch-knob{top:50%;transform:translateY(-50%)}
.pr-proto-h .dh-switch input:checked ~ .dh-switch-track .dh-switch-knob{transform:translate(16px,-50%)}
.pr-mode-row{display:flex;align-items:center}
.pr-cidr-label{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--text);margin-bottom:-4px}
.pr-cidr-info{flex:0 0 auto}
.pr-proto-col .dh-field{margin:0}
.pr-owner-chk{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);cursor:pointer;user-select:none}
.pr-owner-chk[hidden]{display:none}
.pr-body .dh-field{margin-top:12px}
/* Multi-line CIDR field — a plain `<textarea>` (2026-08-01 rework #4 drops
   the chip-tag widget rework #3 tried; just a text field, per feedback).
   Auto-grows to fit its content (see the .el('textarea',...) input listener
   in buildCidrField) instead of showing a native resize handle/scrollbar, so
   it still reads as custom-designed rather than a bare default box. The
   `.pr-proto-col` prefix beats `.dh-field textarea`'s tag+class specificity
   so these overrides actually win. */
.pr-proto-col .pr-cidr-inp{display:block;width:100%;resize:none;overflow:hidden;
  min-height:40px;padding:9px 12px;border:1px solid var(--line2);border-radius:10px;
  background:#0a0d0b;color:var(--text);font-size:13px;font-family:var(--sans);line-height:1.5}
.pr-proto-col .pr-cidr-inp:focus{outline:none;border-color:var(--mint)}
.pr-proto-col .pr-cidr-inp::placeholder{color:var(--muted)}
/* Whole-server rate cap, below the per-port fold rows (not nested in any one
   port's settings — see buildServerRate). Label, entry box and "Применить" all
   share ONE line — never wrap (rework #4) — with the entry box stretching to
   fill whatever the label+button don't need, and "Мбит/с" drawn as a non-
   interactive suffix inside it (rework #3 — replaces the old
   label-states-the-unit + fixed-width box). */
.pr-server-rate .pr-rate-row{gap:10px;align-items:center;flex-wrap:nowrap}
.pr-server-rate .pr-rate-row .btn{flex:0 0 auto}
.pr-rate-label{flex:0 0 auto;font-weight:600;font-size:13px;color:var(--text);white-space:nowrap}
.pr-rate-wrap{position:relative;flex:1 1 120px;min-width:80px}
.pr-rate-wrap .pr-rate-inp{width:100%;padding-right:56px}
.pr-rate-suffix{position:absolute;right:12px;top:50%;transform:translateY(-50%);
  color:var(--muted);font-size:12.5px;pointer-events:none}
/* Wide screens show TCP/UDP side by side; shallow/mobile stacks TCP over UDP.
   Moving the whitelist/blacklist toggle into .pr-proto-h (above) widened each
   column's header, so this tier had to move up from the old 640px (rework #3). */
@container srv (max-width: 900px) {
  .pr-proto-grid{grid-template-columns:1fr}
}
.sp-sftp-act-wrap{margin-top:2px}
.sp-sftp-act{display:flex;align-items:baseline;gap:10px;padding:6px 0;border-top:1px solid rgba(255,255,255,.05)}
.sp-sftp-act:first-of-type{border-top:0}
.sp-sftp-ip{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;color:var(--mint);flex:0 0 auto}
.csel-sm .csel-trigger{min-height:36px;padding:7px 30px 7px 10px;font-size:13.5px;border-radius:8px}
.csel-sm .csel-caret{right:11px}
.csel-sm .csel-menu{border-radius:8px;padding:4px}
.csel-sm .csel-opt{font-size:13.5px;padding:7px 10px;border-radius:5px}
.csel-sm .csel-logo{width:20px;height:20px}
/* server-resource pills: black fill, green edges (matches .btn family). The
   spec pills are display-only; only the "ID" pill is interactive. */
.sp-specs{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0}
.sp-spec{display:inline-flex;align-items:center;gap:8px;font-family:var(--display);font-weight:600;font-size:14px;
  padding:8px 15px;border-radius:999px;border:1px solid var(--mint);background:#000;color:var(--mint)}
.sp-spec.static{cursor:default}
.sp-spec.copy{position:relative;padding-right:30px;cursor:pointer;transition:transform .15s,box-shadow .15s,border-color .15s}
.sp-spec.copy:hover{transform:translateY(-2px);box-shadow:0 12px 32px rgba(47,212,106,.3)}
.sp-spec .sp-spec-copy{position:absolute;top:4px;right:7px;width:13px;height:13px;opacity:.75}
.sp-spec.copy:hover .sp-spec-copy{opacity:1}
.pill{display:inline-block;font-size:11px;font-weight:600;padding:2px 9px;border-radius:999px}
.pill.ok{background:var(--green-dim);color:#bff5cf;border:1px solid rgba(47,212,106,.4)}
.pill.pend{background:rgba(242,204,96,.12);color:#f2cc60;border:1px solid rgba(242,204,96,.35)}

/* file manager */
.sp-filebar{display:flex;align-items:center;gap:12px;margin-bottom:10px}
/* Explorer-style address bar: one full-width row, brighter than the
   surrounding black, with rounded edges. Clicking bare space in the row
   (not a crumb) flips it into an editable raw-path text field. */
.sp-pathbar{position:relative;width:100%;border-radius:var(--r);background:var(--card2);
  border:1px solid var(--line2);padding:9px 14px;cursor:text;
  display:flex;align-items:center;gap:12px}
.sp-crumbs{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:4px;flex-wrap:wrap;font-size:13.5px;color:var(--muted);min-height:20px;transition:opacity .1s}
.sp-crumbs.off{opacity:0;position:absolute;pointer-events:none}
.sp-crumb{display:inline-flex;align-items:center;height:23px;line-height:1;color:var(--text);padding:3px 8px;border-radius:7px;cursor:pointer;transition:color .12s,background .12s}
.sp-crumb svg{width:17px;height:17px;display:block}
.sp-crumb:hover{color:var(--mint);background:var(--green-dim)}
.sp-crumb:last-child{color:#fff;font-weight:600}
.sp-crumb-sep{display:inline-flex;align-items:center;height:23px;line-height:1;color:var(--muted2);font-size:13px}
.sp-crumb.crumb-drop{background:var(--green-dim);outline:1px dashed var(--mint)}
/* The pathbar fills the whole filebar row (the disk meter now lives inside it). */
.sp-filebar>.sp-pathbar{flex:1 1 auto;min-width:0}
/* Occupied/allocated disk meter, pinned to the right end of the path bar and
   sitting OVER the address bar rather than beside it. flex:0 0 auto keeps its
   size while the crumbs (flex:1) shrink, so the path never renders under it. */
.sp-fstore{flex:0 0 auto;display:flex;align-items:center;gap:8px;color:var(--muted);cursor:default;user-select:none}
.sp-fstore-track{width:66px;height:7px;border-radius:4px;background:var(--bg2);border:1px solid var(--line2);overflow:hidden;flex:0 0 auto}
.sp-fstore-fill{display:block;height:100%;width:0;border-radius:inherit;background:linear-gradient(90deg,var(--green),var(--mint));transition:width .3s ease}
.sp-fstore.hot .sp-fstore-fill{background:linear-gradient(90deg,#e5a13a,var(--danger))}
.sp-fstore-txt{font-family:ui-monospace,'Cascadia Code','Consolas',monospace;font-size:11.5px;white-space:nowrap}
.sp-crumbs-edit{display:none;flex:1 1 auto;min-width:0;background:transparent;border:0;outline:0;padding:0;margin:0;
  font:inherit;font-size:13.5px;color:var(--text);font-family:ui-monospace,Consolas,monospace}
.sp-crumbs-edit.on{display:block}
.sp-drop{position:relative;border:1px dashed transparent;border-radius:var(--r);height:560px;overflow-y:auto;transition:border-color .15s,background .15s}
.sp-drop.over{border-color:var(--mint);background:rgba(47,212,106,.06)}
.sp-drop.marqueeing{user-select:none}
/* PC-only rubber-band file select: a child of .sp-drop, positioned in "content
   space" (see the JS) so it scrolls along with the file list naturally instead
   of drifting off its anchor row — see server-panel.js's mousedown handler for
   why fixed+viewport coordinates broke on scroll. z-index only needs to clear
   the file table itself, not the whole page — .sp-drop's own overflow already
   clips the box to the file list, which is the box's natural boundary anyway. */
.sp-marquee{position:absolute;z-index:5;pointer-events:none;border:1px solid var(--mint);background:rgba(47,212,106,.15)}
/* Every tab pane fills the panes container as a flex column: its header stays
   pinned (flex:0 0 auto) and a single scroll body below it (.sp-pane-body for most
   tabs, the file list .sp-drop for Files) is the ONLY scroll area. So the header is
   always visible, the scrollbar starts *below* it (never over the header's action
   button), and it appears only when the body overflows — short tabs get none. The
   panes container itself no longer scrolls. The scroll body fills the pane to the
   bottom (no reserved gap): the corner resize grip is position:absolute and z-index'd
   OVER the content, so it takes no layout space — an earlier padding-bottom that
   reserved a band for it just left a dead gap that clipped the last file row. */
.sp-panes{height:100%}
.sp-pane:not(.hide){display:flex;flex-direction:column;height:100%;min-height:0}
.sp-pane>.sp-sec,
.sp-pane>.sp-filebar,
.sp-pane>.sp-selbar,
.sp-pane>.sp-modrinth-bar{flex:0 0 auto}
.sp-pane-body,
.sp-pane>.sp-drop,
.sp-pane>.sp-drop.has-mr-bar{flex:1 1 auto;min-height:0;overflow-y:auto}
.sp-pane>.sp-drop,
.sp-pane>.sp-drop.has-mr-bar{height:auto}
/* Match .sp-drop's 1px border so the body scrollbar sits at the same x on every
   tab (keeps the corner grip aligned across Files and the rest). */
.sp-pane-body{border:1px solid transparent}
/* Push the scrollbar out into the right margin so its thumb lands ~14px from the
   card's right border — clear of the tab content (whose right edge sits ~20px from
   the border) and directly above the corner grip (inset 15px). The body extends
   11px right into the padding (negative margin, shown because the panes container is
   overflow:visible); an equal padding-right insets the content back so nothing else
   moves — only the scrollbar rides out into the margin.
   NOTE the −9 (thumb ~16px) vs the grip's 15px: on paper both should be 15, but at
   100% display scaling the native scrollbar thumb rendered exactly 1px RIGHT of the
   grip (a real, integer-pixel offset — the browser seats a native scrollbar thumb
   and a CSS box on slightly different pixels). −9 nudges the thumb 1px left so it
   sits directly over the grip at 100% (confirmed by the user on their 100% screen).
   headless overlay bars are 0-width and misreport this — see scratchpad/align.js. */
.sp-pane-body,
.sp-pane>.sp-drop{margin-right:-9px;padding-right:9px}
/* Gentle 100ms fade as the files list (re)renders — keeps background refreshes
   from snapping in. Scoped to the files drop so other tabs' tables are unaffected. */
@keyframes sp-refresh-in{from{opacity:0}to{opacity:1}}
.sp-drop>.sp-ftable,.sp-drop>.sp-empty,.sp-drop>.alert{animation:sp-refresh-in .1s ease}
.sp-ftable{width:100%;border-collapse:separate;border-spacing:0 6px}
.sp-ftable th{font-size:11px;text-transform:uppercase;color:var(--muted);text-align:left;padding:8px 10px;font-weight:600}
.sp-ftable td{padding:9px 10px;vertical-align:middle;border-bottom:0}
.sp-ftable td:first-child{border-radius:8px 0 0 8px}
.sp-ftable td:last-child{border-radius:0 8px 8px 0}
.sp-ftable tbody tr,.sp-ftable tr{user-select:none}
.sp-ftable tr:hover td{background:rgba(255,255,255,.03)}
/* Click-to-copy chip (network tab IP / port). The copy glyph sits to the right
   of the value and both flash mint on a successful copy. */
.sp-copy{display:inline-flex;align-items:center;gap:6px;cursor:pointer;border-radius:7px;padding:2px 7px;margin:-2px 0;
  border:1px solid transparent;color:var(--text);font-family:ui-monospace,Consolas,monospace;transition:background .12s,border-color .12s,color .12s}
.sp-copy:hover{background:rgba(47,212,106,.08);border-color:rgba(47,212,106,.35)}
.sp-copy:focus-visible{outline:none;border-color:var(--mint)}
.sp-copy svg{width:13px;height:13px;flex-shrink:0;opacity:.6;transition:opacity .12s}
.sp-copy:hover svg{opacity:1}
.sp-copy.copied{background:rgba(47,212,106,.16);border-color:var(--mint);color:var(--mint)}
.sp-copy.copied svg{opacity:1}
.fn{display:inline-flex;align-items:center;gap:11px;color:var(--text)}
.fn .fi{opacity:.85}
/* file-type glyph from the vscode-icons pack: one SVG per type, pulled in as a
   background so the browser only fetches the types a listing actually shows.
   Full opacity — these are colour logos, not monochrome glyphs to dim. Sized
   well above the vscode-icons pack's own fine detail floor (~17px read as
   blurry/pixelated at that size) rather than right at it. */
.fn .fi-t{width:30px;height:30px;flex-shrink:0;opacity:1;
  background-repeat:no-repeat;background-position:center;background-size:contain}
/* Installed Modrinth/CurseForge/uMod files use a real <img> in place of the
   file-type background. Keep the same 30px footprint as .fi-t after its recent
   enlargement, while fitting the artwork inside it without changing its aspect
   ratio (especially important for uMod's wide wordmark). */
.fn .fi-source{width:30px;height:30px;flex:0 0 30px;display:inline-flex;align-items:center;justify-content:center;opacity:1}
.fn .fi-source>img{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}
.fn-nav{cursor:pointer}
.fn-nav:hover,.fn-edit:hover{color:var(--mint)}
/* Explorer-style selection: mint wash + a left accent bar */
.sp-ftable tr.sel td{background:var(--green-dim)}
.sp-ftable tr.sel:hover td{background:rgba(47,212,106,.2)}
.sp-ftable tr.sel td:first-child{box-shadow:inset 3px 0 0 var(--mint)}
.sp-ftable tr.dragging{opacity:.4}
.sp-ftable tr.drop-target td{background:rgba(47,212,106,.22);box-shadow:inset 0 0 0 1px var(--mint)}
/* Browsable mods/plugins folders (Modrinth- or uMod-badged) get a dark-green
   tint so they stand out from regular files — darker than the active-selection
   highlight, lighter than the plain row background. The .sel combos below
   restate the existing selection rules with extra specificity so a selected
   mods folder still reads as selected, regardless of CSS source order. */
.sp-ftable tr.sp-modfolder td{background:rgba(47,212,106,.055)}
.sp-ftable tr.sp-modfolder:hover td{background:rgba(47,212,106,.09)}
.sp-ftable tr.sp-modfolder.sel td{background:var(--green-dim)}
.sp-ftable tr.sp-modfolder.sel:hover td{background:rgba(47,212,106,.2)}
.sp-drop:focus{outline:none}
/* .row's own default (flex-wrap:wrap) let this handful of small icon buttons
   split onto two lines whenever their row ran short on space — most visible
   in the Сеть tab's narrower .pr-net-left column, where "Сделать основным" +
   "Удалить" (2 icons, plenty small enough to always fit together) were
   stacking instead of staying on one line next to the note/chevron. */
.sp-fact{justify-content:flex-end;gap:2px;flex-wrap:nowrap}
.ta-r{text-align:right}
/* Port picker (Network tab) — iOS-timer-style scroll wheel */
.sp-note{margin:2px 0 12px}
/* Сеть tab overall layout (rework #4): the per-port config rows (.pr-net-left)
   sit next to ONE right-hand column (.pr-net-right) holding, top to bottom,
   the port wheel, "Добавить порт", the rate cap, and the price line — those
   four never sit side by side with EACH OTHER, only the two columns
   (left vs. right) are side by side. .pr-net-left is flex:1 1 720px so it
   takes whatever space the fixed-width right column doesn't need; once the
   container can't fit both at a reasonable width, flex-wrap drops the right
   column below the port rows instead of squeezing either one.
   justify-content:center only visibly matters once wrapped: side by side,
   .pr-net-left's flex-grow already fills the whole line so centering has no
   effect; wrapped, .pr-net-right is alone on its own flex line and centering
   is what actually centers that column under the port rows. */
.pr-net-row{display:flex;align-items:flex-start;justify-content:center;gap:28px;flex-wrap:wrap}
/* min-width used to be 0 (the usual flex boilerplate to let a child shrink
   past its content), which let the flex-wrap decision above use the smaller
   460px flex-basis to decide "fits side by side" and then squeeze .pr-row
   itself narrower than what a port row can actually render at — address+port
   chips don't shrink (unbreakable, no ellipsis) and the note has its own
   15ch floor (see .pr-note-wrap), so a squeezed row just overflowed
   rightward, overlapping .pr-net-right instead of ever wrapping below it.
   720px covers both the port header's real minimum (2 chips — the address chip can be a
   domain, not just a short IP, via NODE_IP_LABEL — + a 15ch note + the
   star/delete icons + chevron + row padding) and the expanded TCP/UDP editor
   without wrapping «Также открыть доступ всем своим серверам». Matching
   min-width to flex-basis means the wrap decision and the actual rendered
   floor are the same number: nothing to squeeze, so nothing to overlap; the
   right column now falls to the bottom before the row ever runs out of room. */
.pr-net-left{flex:1 1 720px;min-width:min(720px,100%)}
.pr-net-right{width:100%;max-width:380px;flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:14px;padding-top:2px}
.pr-net-right .sp-pw-confirm{width:100%;text-align:center}
/* Strip the card chrome (background/border/padding) here — with the wheel/
   button above and the price line below, a bordered card just reads as a
   disconnected extra box instead of the whole column reading as one group. */
.pr-net-right .pr-server-rate{width:100%;margin:0;background:none;border:0;padding:0;box-shadow:none}
.pr-ports-price{text-align:center;font-size:16px;font-weight:700;color:var(--mint)}
.sp-pw-viewport{position:relative;width:100%;max-width:380px;overflow:hidden;cursor:grab;touch-action:none;outline:none;
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 22%, #000 78%, transparent 100%);
  mask-image:linear-gradient(to bottom, transparent 0, #000 22%, #000 78%, transparent 100%)}
.sp-pw-viewport.dragging{cursor:grabbing}
.sp-pw-track{position:relative;will-change:transform}
.sp-pw-row{display:flex;align-items:center;justify-content:center;gap:10px;user-select:none;cursor:pointer}
.sp-pw-row .sp-pw-num{font-weight:700;font-size:19px;color:var(--text);font-variant-numeric:tabular-nums;min-width:64px;text-align:right}
.sp-pw-row .sp-pw-st{font-size:11.5px;color:var(--muted);min-width:96px}
.sp-pw-row.free .sp-pw-st{color:var(--mint)}
.sp-pw-row.occ{opacity:.85}
.sp-pw-row.lock .sp-pw-st{color:var(--danger)}
.sp-pw-row.mine .sp-pw-st{color:var(--info)}
.sp-pw-highlight{position:absolute;left:0;right:0;pointer-events:none;border-top:1px solid var(--line2);border-bottom:1px solid var(--line2);
  background:rgba(47,212,106,.05)}
/* The number used to be its own bordered/filled pill nested inside the
   button's own pill — a dark box sitting inside the green one, which read as
   a redundant "double box" (and grew taller once swapped for the `<input>`,
   whose own border+padding stacked on top of the button's). Fixed: no
   separate background/border/padding on either state, just a dashed
   underline in `currentColor` (works on both the default green pill and the
   dark ghost/disabled state without separate color rules) — the button
   itself stays the only actual box. */
/* min-width sized so "Добавить порт [5-digit port]… (NNN₽/мес)" fits on one
   line without ellipsis-clipping at the common widths this button actually
   renders at (.pr-net-right is 380px wide, min() keeps it from overflowing
   a genuinely narrower mobile viewport instead). */
.sp-pw-confirm{min-width:min(340px, 100%);cursor:pointer;white-space:nowrap}
/* Disabled (port unavailable): dim the "Добавить порт"/price chrome only —
   NOT the port number itself (.sp-pw-confirm-num-wrap stays at full opacity)
   so it's still easy to read which port is being looked at while it's
   greyed out as unavailable. A blanket opacity on the whole button used to
   dim the number along with everything else. */
.sp-pw-confirm.disabled{cursor:not-allowed;box-shadow:none}
.sp-pw-confirm.disabled:hover{transform:none;box-shadow:none}
.sp-pw-confirm.disabled .sp-pw-confirm-text,
.sp-pw-confirm.disabled .sp-pw-confirm-price{opacity:.4}
.sp-pw-confirm.disabled .sp-pw-confirm-num-wrap{cursor:pointer;opacity:1}
/* numWrap (plain text) and the swapped-in <input> share the exact same fixed
   width/line-height/border so the button's total size never changes when you
   click the number to edit it — no reflow, no resize, only the content
   inside that fixed slot changes (a reflow here used to be able to shift the
   input out from under the pointer between mousedown/mouseup, so the "click"
   landed on confirmBtn instead and fired an immediate assign). */
/* align-self:stretch (confirmBtn is a flex row) extends the clickable area to
   the button's full height top-to-bottom, not just the 16px-tall number
   itself — the number/pencil stay visually centered via this element's own
   flex centering, only the hit-target grows. */
.sp-pw-confirm-num-wrap{display:inline-flex;align-items:center;justify-content:center;gap:4px;
  align-self:stretch;cursor:pointer;vertical-align:baseline}
.sp-pw-confirm-num-wrap:hover{opacity:1}
.sp-pw-confirm-num-wrap .btn-icon{width:13px;height:13px;opacity:.7}
.sp-pw-confirm-num-wrap:hover .btn-icon{opacity:1}
.sp-pw-confirm-num{cursor:pointer;display:inline-block;width:74px;height:16px;line-height:16px;
  border-bottom:1px dashed currentColor;opacity:.85;padding:0;vertical-align:baseline;
  overflow:hidden;text-overflow:ellipsis}
/* input[type=number] (global form-control rule, ~55 lines up) sets its own
   width:100%/padding/border and beats a same-specificity single-class
   selector by source order — `.sp-pw-confirm-input[type=number]` matches it
   in specificity (0,2,0 vs 0,1,1) and comes later, so it's what actually has
   to win here, not the bare class alone. Fixed height (not line-height alone)
   because a text `<input>`'s intrinsic box carries a few px of UA-controlled
   height beyond its content-box regardless of line-height/padding/border —
   without pinning height explicitly this still came out ~4px taller than
   .sp-pw-confirm-num and reflowed confirmBtn on every edit-toggle. */
.sp-pw-confirm-input,.sp-pw-confirm-input[type=number]{width:74px;height:16px;background:transparent;border:0;
  border-bottom:1px dashed currentColor;color:inherit;
  font:inherit;font-size:inherit;font-weight:inherit;line-height:16px;padding:0;margin:0;outline:none;
  text-align:center;cursor:text;vertical-align:baseline;box-sizing:border-box;border-radius:0}
.sp-pw-confirm-input::-webkit-outer-spin-button,.sp-pw-confirm-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.sp-pw-confirm-input[type=number]{-moz-appearance:textfield;appearance:textfield}
.lk{background:none;border:0;cursor:pointer;color:var(--muted);font-size:15px;padding:5px 7px;border-radius:8px;line-height:1}
.lk:hover{color:#fff;background:rgba(255,255,255,.06)}
.lk.danger:hover{color:#fff;background:var(--danger)}
/* Primary port's star: filled yellow indicator, not a button (no hover/click
   affordance) — replaces the old green "основной" text pill. */
.pr-star-primary{color:var(--amber,#e5c07b);cursor:default}
.pr-star-primary:hover{color:var(--amber,#e5c07b);background:none}
/* Primary port's delete icon stays in the row (so the icon slot never shifts
   between primary/non-primary rows) but is greyed out and inert. */
.lk[disabled]{color:var(--muted);opacity:.35;cursor:not-allowed}
.lk[disabled]:hover{color:var(--muted);background:none}
/* Material Symbols glyphs (services/dashboard/public/icons/*.svg) recolored via
   mask so they track .lk's text color — including its hover/danger states —
   instead of being stuck at whatever fill the SVG file itself has. */
.btn-icon{display:inline-block;width:16px;height:16px;background-color:currentColor;
  -webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;vertical-align:-3px}

/* Windows-Explorer-style right-click / kebab context menu */
.dh-ctx{position:fixed;z-index:120;min-width:210px;padding:6px;
  background:rgba(12,15,13,.97);backdrop-filter:blur(14px);border:1px solid var(--line2);
  border-radius:12px;box-shadow:0 18px 50px rgba(0,0,0,.6);font-size:14px;color:var(--text);
  animation:ctxIn .12s ease-out}
@keyframes ctxIn{from{opacity:0;transform:scale(.97) translateY(-4px)}to{opacity:1;transform:none}}
.dh-ctx-head{padding:6px 12px 8px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--line);margin:0 0 4px}
.dh-ci{display:flex;align-items:center;gap:11px;padding:8px 12px;border-radius:8px;cursor:pointer;white-space:nowrap;color:var(--text)}
.dh-ci:hover{background:var(--green-dim)}
/* per-action tones: green=select, white=neutral (default), red=delete */
.dh-ci.green{color:var(--mint)}
.dh-ci.danger{color:var(--danger)}
.dh-ci.danger:hover{background:rgba(255,92,92,.16);color:#ffb3b3}
.dh-ci .ico{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
.dh-ci .ico svg{width:18px;height:18px;display:block}
.dh-ci .lbl{flex:1}
.dh-ci .key{margin-left:18px;color:var(--muted2);font-size:12px}
.dh-ci-sep{height:1px;background:var(--line);margin:5px 6px}

/* mobile select-mode: checkboxes slide in from the left, names shift right, and
   a rounded bulk-action bar drops down under the file toolbar */
.sp-check{display:inline-flex;align-items:center;justify-content:center;width:0;opacity:0;overflow:hidden;flex-shrink:0;
  vertical-align:middle;cursor:pointer;color:var(--muted);
  transition:width .28s cubic-bezier(.4,0,.2,1),opacity .24s,margin-right .28s}
.sp-drop.selmode .sp-check{width:24px;opacity:1;margin-right:8px}
.sp-check svg{width:22px;height:22px;display:block}
.sp-check .check{display:none}
.sp-check.checked .uncheck{display:none}
.sp-check.checked .check{display:block}
.sp-check.checked{color:var(--mint)}
.sp-check:hover{color:var(--text)}
.sp-check.checked:hover{color:var(--mint)}
.sp-drop.selmode .sp-ftable tr{cursor:pointer}

.sp-selbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;
  max-height:0;opacity:0;overflow:hidden;transform:translateY(-6px);margin:0;
  transition:max-height .3s cubic-bezier(.4,0,.2,1),opacity .25s,transform .3s,margin .3s}
.sp-selbar.open{max-height:160px;opacity:1;transform:none;margin:0 0 10px}
.sp-selbar-info{font-size:13px;color:var(--muted);white-space:nowrap}
.sp-selcount{color:var(--mint);font-weight:700}
.sp-selbar-actions{display:flex;gap:8px;flex-wrap:wrap}
.sp-selbtn{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:12px;
  background:var(--card2);border:1px solid var(--line2);color:var(--text);cursor:pointer;font-size:13.5px;white-space:nowrap;
  transition:background .15s,border-color .15s,color .15s}
.sp-selbtn:hover{background:var(--green-dim);border-color:rgba(47,212,106,.45)}
.sp-selbtn svg{width:17px;height:17px;flex-shrink:0}
.sp-selbtn.ghost{background:transparent}
.sp-selbtn.ghost:hover{background:rgba(255,255,255,.06);border-color:var(--line2);color:#fff}
.sp-selbtn.danger{color:#ffb3b3;border-color:rgba(255,92,92,.3)}
.sp-selbtn.danger:hover{background:rgba(255,92,92,.16);border-color:rgba(255,92,92,.55);color:#fff}

/* modal editor */
.sp-modal{position:fixed;inset:0;z-index:80;background:rgba(0,0,0,.66);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;padding:24px}
.sp-modal-box{background:var(--panel);border:1px solid var(--line2);border-radius:var(--r-lg);padding:18px;width:100%;max-width:520px;box-shadow:0 30px 70px rgba(0,0,0,.6)}
.sp-modal-box.big{max-width:900px}
.sp-modal-h{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px;word-break:break-all}
.sp-editor{width:100%;height:60vh;background:#050706;border:1px solid var(--line2);border-radius:10px;padding:12px;
  color:#cdd6cf;font-family:ui-monospace,Consolas,monospace;font-size:12.5px;line-height:1.5;resize:vertical}
.sp-editor:focus{outline:none;border-color:var(--mint)}

/* custom confirmation modal (replaces window.confirm) */
.dh-confirm-ov{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;padding:24px;
  background:rgba(0,0,0,.64);backdrop-filter:blur(4px);opacity:0;transition:opacity .18s}
.dh-confirm-ov.show{opacity:1}
.dh-confirm-box{width:100%;max-width:430px;background:var(--panel);border:1px solid var(--line2);border-radius:var(--r-lg);
  padding:24px;box-shadow:0 30px 80px rgba(0,0,0,.65);transform:translateY(10px) scale(.97);transition:transform .18s,opacity .18s;opacity:0}
.dh-confirm-ov.show .dh-confirm-box{transform:none;opacity:1}
.dh-confirm-title{font-family:var(--display);font-weight:700;font-size:18px;color:#fff;margin-bottom:9px}
.dh-confirm-msg{color:var(--muted);font-size:14px;line-height:1.55;word-break:break-word}
.dh-confirm-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:22px}
/* prompt-modal fields */
.dh-fields{margin-top:16px;display:flex;flex-direction:column;gap:14px}
.dh-field label{display:block;margin:0 0 6px;font-weight:600;font-size:13px;color:var(--text)}
.dh-field input,.dh-field select,.dh-field textarea{width:100%}
.dh-field textarea{padding:12px 14px;border-radius:12px;border:1px solid var(--line2);background:#0a0d0b;color:#fff;
  font-family:var(--sans);font-size:15px;resize:vertical;min-height:74px}
.dh-field textarea:focus{outline:none;border-color:var(--mint)}
.dh-field-hint{margin-top:6px}
.dh-field-bad{border-color:var(--danger)!important}
/* subuser permission checklist modal */
.dh-perms-box{max-width:560px}
.dh-pemail{padding:11px 14px;border-radius:12px;border:1px solid var(--line2);background:#0a0d0b;color:var(--muted);font-size:15px}
.dh-perms-bar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:16px}
.dh-perms{margin-top:10px;max-height:46vh;overflow-y:auto;display:flex;flex-direction:column;gap:14px;padding-right:4px}
.dh-pgroup-h{font-weight:700;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin-bottom:6px}
.dh-pgroup-items{display:grid;grid-template-columns:1fr 1fr;gap:4px 12px}
.dh-pitem{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:9px;cursor:pointer;font-size:13.5px;color:var(--text);user-select:none}
.dh-pitem:hover{background:var(--card)}
.dh-pitem.on{color:#fff}
.dh-pcheck{display:inline-flex;flex:0 0 auto;color:var(--line2)}
.dh-pitem.on .dh-pcheck{color:var(--mint)}
.dh-pcheck svg{width:19px;height:19px}

/* toasts — host stacks them in a column, pinned to the bottom centre */
.dh-toasts{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);z-index:90;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;max-width:90vw}
.dh-toast{pointer-events:auto;background:var(--panel);border:1px solid var(--line2);
  border-radius:12px;padding:11px 18px;color:var(--text);font-size:14px;box-shadow:0 16px 40px rgba(0,0,0,.5);opacity:0;transform:translateY(20px);transition:opacity .25s,transform .25s;max-width:100%;text-align:center}
.dh-toast.show{opacity:1;transform:translateY(0)}
.dh-toast.ok{border-color:rgba(47,212,106,.5)} .dh-toast.err{border-color:rgba(255,92,92,.5);color:#ffb3b3}

/* Near-mobile (just above the burger breakpoint): tighten the top bar's own
   gap/padding (brand + balance + burger — the nav itself is already off in
   the burger dropdown by this width, see the max-width:1000px block below),
   and keep the server power buttons on a single compact row instead of a
   lonely 2×2. */
@media(min-width:681px) and (max-width:860px){
  .top-in{gap:12px;padding:0 16px}
  .hdr-balance{margin-left:8px;padding:6px 10px;font-size:12px}
  /* power buttons fill the full width (no lonely gap on the right); stats wrap
     onto the next line rather than sharing the row. */
  .sp-power-wrap{width:100%;flex-wrap:nowrap;gap:8px}
  .sp-power{flex:1;flex-wrap:nowrap;gap:8px}
  .sp-power .btn{flex:1;min-width:0;padding:9px 6px;font-size:13px}
  /* copy-IP: its own full-width row so it isn't squeezed to a thin sliver */
  .srv-head{flex-wrap:wrap}
  .srv-head .srv-addr{order:5;flex:1 0 100%;justify-content:center;max-width:none;margin-left:0}
}

/* Header nav's own breakpoint — deliberately wider than the ≤680px tier below
   (which is about the server-row/panel layout, an unrelated concern that
   happens to share the round number). Below this width the full desktop nav
   (Действия + Статус систем + Discord/Telegram/email + В панель + Выйти)
   doesn't fit even with every gap/padding trimmed: measured naturally
   overflowing (Discord/Telegram SVGs — the only replaced elements in the row —
   getting silently flex-shrunk into illegible dots, then Выйти itself pushed
   past the viewport edge with nothing left to shrink) from ~975px down to the
   old 680px cutoff. Collapsing into the burger dropdown here instead — same
   dropdown UI that already worked fine below 680px — replaces that whole dead
   zone with a layout that actually fits. */
@media(max-width:1000px){
  /* header: collapse the nav behind a burger button instead of wrapping/
     squeezing links onto the top bar — dropdown panel below the brand row. */
  .burger{display:flex;margin-left:auto}
  .top-in{position:relative}
  /* balance pill lives in the header bar (not the burger) on mobile, pinned to
     the right next to the burger. */
  .hdr-balance{display:inline-flex;margin-left:auto}
  .hdr-balance + .burger{margin-left:12px}
  .nav-balance{display:none}
  /* burger dropdown: ONE control per row, right-aligned, panel only as wide as
     its content (not full-screen). Order matches the DOM: [Действия] ·
     Статус систем · соцсети · [В панель] · Выйти. [Действия] only renders on
     pages with a server list (dashboard). Same on every page (incl. billing). */
  header.top nav{position:absolute;top:calc(100% + 6px);right:8px;left:auto;display:none;flex-direction:column;
    align-items:stretch;gap:2px;margin:0;padding:6px;width:max-content;min-width:190px;max-width:min(74vw,300px);
    background:var(--bg2);border:1px solid var(--line);border-radius:12px;box-shadow:0 18px 40px rgba(0,0,0,.5)}
  header.top nav.open{display:flex}
  header.top nav a{padding:11px 14px;white-space:nowrap;border-radius:8px}
  header.top nav a:hover{background:rgba(255,255,255,.04)}
  /* .sys-pill is an <a>, so the generic `nav a{border-radius:8px}` rule above
     (more specific: header.top+nav+a) was clobbering its own border-radius:999px
     and its padding was 11px 14px vs Выйти's .btn.sm 8px 15px — same pill shape
     as Выйти needs both re-asserted here, not just left to `.sys-pill`'s base rule.
     Font/color/width are also pulled in line with Действия/Выйти (.btn.ghost.sm)
     here: margin mirrors those buttons' own wrapper padding (.gaction/.logout-wrap,
     4px 6px) so the bare <a> lines up at the same width without a wrapper of its
     own. The status dot doesn't read as one more pill in this stacked list, so its
     color moves to the pill's own edge instead (default = ok; .bad = same red the
     dot used to turn on a failed health check) — border PLUS an outer glow, so the
     whole row reads as "this button is the status" rather than needing its own dot. */
  header.top nav .sys-pill{margin:4px 6px;padding:8px 15px;line-height:1;justify-content:center;border-radius:999px;
    font-family:var(--display);font-weight:600;font-size:14px;color:#fff;border-color:var(--mint);
    box-shadow:0 0 10px rgba(47,212,106,.35)}
  header.top nav .sys-pill:hover{color:var(--mint);border-color:var(--mint)}
  header.top nav .sys-pill.bad{border-color:var(--danger);box-shadow:0 0 10px rgba(255,92,92,.35)}
  header.top nav .sys-pill.bad:hover{color:var(--danger);border-color:var(--danger)}
  header.top nav .sys-pill .dot{display:none}
  /* icons centered as a group (not spread across the row); gap stays the fixed
     20px it always had — only the flex-end→center alignment changes. */
  header.top nav .nav-social{gap:20px;padding:11px 14px;justify-content:center}
  header.top nav .nav-social a,header.top nav .nav-social .nav-email-btn{padding:0}
  /* `header.top nav a:hover{color:#fff}` (base rule, desktop+mobile) is more
     specific than `.nav-social a:hover{color:var(--mint)}`, so Discord/Telegram
     silently hovered white while the email button (a <button>, unaffected by that
     `nav a` rule) hovered mint — the one the icon row was actually designed for
     (see .nav-email-ico above). Re-assert mint here, scoped to the burger dropdown
     only, so the three icons hover identically without touching the desktop row. */
  header.top nav .nav-social a:hover{color:var(--mint)}
  /* Match the other standalone controls in the burger menu. The generic nav
     link hover rule otherwise wins over the mint button styling and makes this
     control appear to vanish against the dropdown. */
  header.top nav .nav-panel-btn{display:flex;width:calc(100% - 12px);margin:4px 6px;padding:8px 15px;
    border-radius:999px;text-align:center;color:#04140a;background:var(--mint);border-color:var(--mint);box-shadow:none}
  header.top nav .nav-panel-btn:hover{color:#04140a;background:var(--mint);border-color:var(--mint);transform:none;box-shadow:0 0 10px rgba(47,212,106,.35)}
  header.top nav form{width:100%}
  header.top nav form .btn{width:100%}
  header.top nav .logout-wrap{padding:4px 6px}
  header.top nav .logout-wrap .btn{width:100%}
  header.top nav .gaction{padding:4px 6px}
  header.top nav .gaction #gactionBtn{width:100%}
  header.top nav .gaction #gactionBtn .gaction-label{margin-left:14px}
  header.top nav .gaction-menu{left:auto;right:0}
}
@media(max-width:680px){
  .srv-quick{display:none}
  .srv-addr{min-width:0;padding:6px 9px;font-size:12.5px}
  .srv-addr .srv-addr-txt{overflow:hidden;text-overflow:ellipsis}
}
@media(max-width:560px){
  main.container{padding:22px 14px 48px}
  .auth{margin:22px auto}
  h1{font-size:25px} h2{font-size:19px}
  .card{padding:18px;border-radius:var(--r)}
  .top-in{gap:10px}
  /* server rows */
  .srv-head{gap:11px;padding:13px 14px}
  .core-box{width:38px;height:38px;border-radius:9px}
  .core-box img{width:24px;height:24px}
  .srv-body{padding:14px 12px}
  .sp-bar{gap:10px}
  .sp-power>.btn{padding:10px 6px;font-size:12.5px}
  .sp-stats{width:100%;gap:10px 14px;font-size:12px}
  .sp-tab{padding:8px 10px;font-size:13px}
  .sp-console{height:450px;font-size:12px}
  /* file manager: keep names from forcing horizontal page scroll */
  .sp-filebar{gap:8px}
  .sp-drop{height:380px}
  .sp-drop.has-mr-bar{height:calc(380px - 62px)}
  .sp-drop .sp-ftable td:nth-child(2){display:none} /* hide size column (file list only) */
  .fn{max-width:52vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .sp-fact{gap:0}
  .lk{padding:6px 6px;font-size:16px}
  /* modals fill the small screen with comfortable padding */
  .sp-modal{padding:12px}
  .sp-modal-box{padding:16px}
  .sp-editor{height:64vh}
  .dh-confirm-ov,.dh-confirm-box{padding:18px}
  .dh-pgroup-items{grid-template-columns:1fr}
  .dh-ctx{min-width:min(220px,86vw)}
  .node-status{width:auto;margin-left:0;transform:none}
  .node-charts{grid-template-columns:minmax(0,480px)}
  .comp-grid{grid-template-columns:1fr}
}

/* ===================== mobile server panel redesign (phones only — desktop
   layout above is untouched by these rules) ===================== */
@media(max-width:680px){
  /* Collapsed row header: logo/name/chevron on the top line, the copyable
     address on its own line below. Status is a dot on the logo now, so it no
     longer needs its own row. */
  .srv-head{display:grid;grid-template-columns:auto 1fr auto;
    grid-template-areas:"logo name chev" "addr addr addr";
    column-gap:12px;row-gap:8px}
  .srv-head .core-box{grid-area:logo}
  .srv-head .srv-meta{grid-area:name;min-width:0}
  .srv-head .chev{grid-area:chev}
  .srv-head .srv-addr{grid-area:addr;width:100%;justify-content:center;max-width:none;margin-left:0}

  /* Power controls fill the whole width as a 2×2 grid with the (i) button in the
     dead centre, between the four buttons. `display:contents` promotes the four
     buttons to grid items of the wrap so the (i) can sit among them. */
  .sp-power-wrap{display:grid;grid-template-columns:1fr auto 1fr;
    grid-template-areas:"b1 info b2" "b3 info b4";gap:8px;width:100%}
  .sp-power{display:contents}
  .sp-power>.btn:nth-child(1){grid-area:b1}
  .sp-power>.btn:nth-child(2){grid-area:b2}
  .sp-power>.btn:nth-child(3){grid-area:b3}
  .sp-power>.btn:nth-child(4){grid-area:b4}
  .sp-power>.btn{width:100%;min-width:0}
  .sp-power>.btn:nth-child(n+5){grid-column:1/-1}  /* update button (CS2/Rust) full-width below */
  .sp-info-wrap{grid-area:info;align-self:center;justify-self:center}
  .sp-info-btn{width:36px;height:36px}
  .sp-info-btn .sp-info-ico{width:26px;height:26px}

  /* Live resource indicators (CPU/RAM/disk/network/uptime): one horizontally
     scrollable row instead of wrapping across multiple lines. */
  .sp-stats{display:flex;flex-wrap:nowrap;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;
    scrollbar-width:none;gap:16px;padding-bottom:2px}
  .sp-stats::-webkit-scrollbar{display:none}
  .sp-stats span{flex:0 0 auto}

  /* Big load charts stay a fixed-height single row here too; below the width
     thresholds server-panel.js flips them to the looped carousel (2→1 chart)
     rather than stacking — so no mobile-specific stacking override is needed. */

  /* Section tabs (Файлы…Настройки): one scrollable row instead of wrapping
     across 3 lines. */
  .sp-tabs{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding-bottom:2px}
  .sp-tabs::-webkit-scrollbar{display:none}
  .sp-tab{flex:0 0 auto}

  /* File manager: a continuous list with no gaps between rows (the desktop
     "chip" rows waste vertical space on a small screen), separated by a thin
     divider instead. Opening a folder/file now takes one tap — see the
     `!IS_PC` branch in fileRow()'s click handler in server-panel.js. */
  .sp-ftable{border-spacing:0}
  .sp-ftable td{border-bottom:1px solid var(--line)}
  .sp-ftable tr:last-child td{border-bottom:0}
  .sp-ftable td:first-child,.sp-ftable td:last-child{border-radius:0}
}

/* ===================== narrow-CARD reflow (container queries) ===========
   The same reflow as the phone breakpoints above, but triggered by the server
   card's OWN width instead of the viewport's. On a real phone the card ≈ the
   viewport, so these fire alongside the matching @media rules (identical result).
   The point is the desktop cases the media queries miss: a narrowed --dh-mid-w
   column, or the editor/Modrinth panel docked beside a wide window — there the card
   can be ~460px while the viewport is still 1400px, so only these @container rules
   fire and the header / power row / stats stop clipping. Breakpoints are ~40px under
   the viewport ones (the card is the column minus main.container's padding). Card
   content layout only — the header bar/burger stay viewport-driven above. */
@container srv (max-width: 820px) {
  /* Narrow card: the header stays a SINGLE non-wrapping row. Rather than dropping
     the IP address onto its own full-width row (and "Открепить" onto its own row
     in the console toolbar), the IP button collapses to just its copy icon and —
     with the detach icon — sits at the right next to the chevron. Explicit `order`
     so it wins over the leftover order:5/grid-area rules from the viewport tiers,
     and `display:flex` so it overrides those tiers' `display:grid`. Custom
     [data-tip] tooltips (server-panel.js) name the icons. */
  .srv-head{display:flex;flex-wrap:nowrap}
  .srv-head .core-box{order:0}
  .srv-head .srv-meta{order:1;flex:1 1 auto;min-width:0}
  .srv-head .srv-addr{order:4;flex:0 0 auto;width:36px;height:36px;padding:8px;gap:0;margin-left:0;max-width:none;
    justify-content:center}
  .srv-head .srv-addr .srv-addr-txt{display:none}
  .srv-head .srv-addr .srv-addr-ico{width:17px;height:17px;opacity:.85}
  .srv-head .srv-head-detach{order:5}
  .srv-head .chev{order:6}
  /* the toolbar's text "Открепить" is replaced by the header icon at this width */
  .sp-detach{display:none}
  /* power buttons still fill the full width (mirrors the 681–860px viewport tier) */
  .sp-power-wrap{width:100%;flex-wrap:nowrap;gap:8px}
  .sp-power{flex:1;flex-wrap:nowrap;gap:8px}
  .sp-power .btn{flex:1;min-width:0;padding:9px 6px;font-size:13px}
}
/* The header detach icon is meaningful only with a real mouse (a popped-out
   window is pointless on touch) and only while the row is open (there's a live
   console to move). Nest the two conditions so both must hold. */
@media (pointer: fine) {
  @container srv (max-width: 820px) {
    .srv.open .srv-head-detach{display:inline-flex}
  }
}
@container srv (max-width: 640px) {
  /* Full narrow layout (mirrors the ≤680px viewport tier). The header keeps the
     single-row icon layout from the ≤820 tier above (IP + detach icons at the
     right) — it no longer drops the address onto its own row. */
  .srv-quick{display:none}
  /* Disk meter: drop the "used / total" text on a very narrow card, keep the bar. */
  .sp-fstore-txt{display:none}

  /* Power controls: 2×2 grid with the (i) button centred between them. */
  .sp-power-wrap{display:grid;grid-template-columns:1fr auto 1fr;
    grid-template-areas:"b1 info b2" "b3 info b4";gap:8px;width:100%;flex-wrap:nowrap}
  .sp-power{display:contents}
  .sp-power>.btn:nth-child(1){grid-area:b1}
  .sp-power>.btn:nth-child(2){grid-area:b2}
  .sp-power>.btn:nth-child(3){grid-area:b3}
  .sp-power>.btn:nth-child(4){grid-area:b4}
  .sp-power>.btn{width:100%;min-width:0}
  .sp-power>.btn:nth-child(n+5){grid-column:1/-1}
  .sp-info-wrap{grid-area:info;align-self:center;justify-self:center}
  .sp-info-btn{width:36px;height:36px}
  .sp-info-btn .sp-info-ico{width:26px;height:26px}

  /* Live resource row + section tabs: one horizontally scrollable row each,
     instead of wrapping/overflowing the narrow card. */
  .sp-stats{display:flex;flex-wrap:nowrap;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;
    scrollbar-width:none;gap:16px;padding-bottom:2px}
  .sp-stats::-webkit-scrollbar{display:none}
  .sp-stats span{flex:0 0 auto}
  .sp-tabs{flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;padding-bottom:2px}
  .sp-tabs::-webkit-scrollbar{display:none}
  .sp-tab{flex:0 0 auto}
}

/* ===================== docked code editor (Notepad++-ish) =============== */
.fn-edit{cursor:pointer}
.fn-edit:hover{color:var(--mint);text-decoration:none}

/* When the editor is open the page content is pushed into the left half so the
   seam between the two "windows" sits at screen centre. The header bar and footer,
   however, must stay full-width and screen-centred — the body padding would squeeze
   them too, so we counter it with a negative right margin that restores their box to
   the full viewport width (their centred inner content then re-centres on screen). */
body{transition:padding-left .32s cubic-bezier(.4,0,.2,1),padding-right .32s cubic-bezier(.4,0,.2,1)}
/* Editor column width is a variable so the page push, the panel width and the
   drag handle all derive from one value and stay in lock-step when resized. */
body.editor-open{--dh-ed-w:50vw;padding-right:var(--dh-ed-w)}
header.top,footer{transition:margin-left .32s cubic-bezier(.4,0,.2,1),margin-right .32s cubic-bezier(.4,0,.2,1)}
body.editor-open header.top,
body.editor-open footer{margin-right:calc(-1 * var(--dh-ed-w))}
/* Seam between the left-half content and the docked editor: shrink main's own
   right padding while the editor is open so the gap matches the editor's own
   inset from the screen edge (.dh-editor right/width below) on the other side. */
body.editor-open main.container{padding-right:var(--dh-gap,10px)}
/* Docked: lift the width cap to fill right up to the seam (uniform gap) — but as
   an explicit calc of the exact docked width (100vw minus the reserved panel
   column[s]) rather than `none`. `none` would resolve to the *current* full body
   width the instant the panel opens (before the padding has shrunk it), so the
   column snapped to full width and only then eased down. A concrete max-width that
   equals the final available width eases (transition on main.container above) in
   step with the body padding, so width = min(max-width, available) glides straight
   from the centred cap to the docked width with no full-width overshoot. */
body.editor-open:not(.modrinth-open) main.container{max-width:calc(100vw - var(--dh-ed-w))}
/* Unlike Modrinth (docks right, pushing content into the right half — nowhere
   near the left-edge .srv-dock sidebar), the editor pushes content into the
   LEFT half, landing main.container flush against the viewport edge, directly
   under the fixed sidebar. Reserve the same --dh-gap inset Modrinth already
   gives its own content (see the padding-left rule below, `.modrinth-open
   main.container`) so the sidebar clears the content identically either way. */
body.editor-open:not(.modrinth-open) main.container{padding-left:var(--dh-gap,10px)}
body.modrinth-open:not(.editor-open) main.container{max-width:calc(100vw - var(--dh-mr-w))}
body.editor-open.modrinth-open main.container{max-width:calc(100vw - var(--dh-ed-w) - var(--dh-mr-w))}
/* While a column handle is being dragged, kill transitions so it tracks the
   cursor 1:1 instead of easing behind it. */
body.dh-col-resizing,body.dh-col-resizing *{transition:none !important}

/* Floating card docked to the right half. top/bottom are set inline by JS so it
   stays within the page body (between the heading row and the last server card,
   never the sticky header or the footer); inset 16px from the right edge with
   rounded corners like the rest of the UI — matching the 16px seam main.container
   keeps on its own right edge above. Its left edge lands at 50vw, exactly the
   seam with the left-shifted page. */
.dh-editor{position:fixed;top:0;right:var(--dh-gap,10px);bottom:0;width:calc(var(--dh-ed-w, 50vw) - var(--dh-gap,10px));z-index:25;display:flex;flex-direction:column;
  background:var(--bg2);border:1px solid var(--line2);border-radius:var(--r-lg);overflow:hidden;
  box-shadow:0 24px 70px rgba(0,0,0,.6);transform:translateX(calc(100% + var(--dh-gap,60px) + 24px));transition:transform .32s cubic-bezier(.4,0,.2,1)}
.dh-editor.show{transform:translateX(0)}
/* Smoothly glide the (JS-set inline) top/bottom when a content-driven resize
   occurs — e.g. the files list auto-refreshes to a different height. Toggled on
   only for those updates (see updateBounds); scroll keeps top/bottom instant. */
.dh-editor.bounds-anim{transition:transform .32s cubic-bezier(.4,0,.2,1),top .2s ease,bottom .2s ease}

/* Column resize handles — a wide invisible grab-zone sitting exactly on the seam
   between a docked panel and the page (never on the page-edge gap). The horizontal
   resize cursor signals it's draggable. Positioned from
   the same width variables as the panels, so the handle tracks the seam. */
.dh-col-grip{position:fixed;top:0;bottom:0;width:14px;z-index:26;cursor:ew-resize;transform:translateX(-50%);
  background:transparent;touch-action:none}
/* The seam is draggable but visually invisible — no line is drawn even on
   hover/drag (the ::after is kept only so the grab-zone element has geometry). */
.dh-col-grip::after{content:"";position:absolute;top:0;bottom:0;left:50%;width:2px;transform:translateX(-50%);
  background:transparent}
.dh-col-grip.grip-ed{left:calc(100vw - var(--dh-ed-w, 50vw))}
.dh-col-grip.grip-mr{left:var(--dh-mr-w, 50vw)}
@media(max-width:1180px){ .dh-col-grip{display:none} }  /* panels go full-screen; no seam to drag (= SINGLE_FS_BP) */

/* Scroll-to-top / scroll-to-bottom arrows, parked in the widened seam that opens
   up beside a docked side panel. The container spans the full-height gutter; the
   two buttons sit near the top and bottom of the viewport (clear of the sticky
   header / footer). Each fades out when the page is already pinned to that edge. */
.dh-snav{position:fixed;top:0;bottom:0;width:var(--dh-gap,30px);z-index:27;display:none;pointer-events:none}
.dh-snav.on{display:block}
.dh-snav.side-right{right:var(--dh-ed-w,50vw)}
.dh-snav.side-left{left:var(--dh-mr-w,50vw)}
/* No side panel docked: the arrows sit in the same natural whitespace beside the
   centred content column (mirroring the docked-editor seam position) instead of
   only existing while a panel is open. Parked close to the content's edge — a
   flat 10px, measured to the visible .dh-snav-btn itself, not the invisible
   --dh-gap box around it — instead of centred in the often much wider margin.
   The button sits 16px inside its 60px-wide box ((60-28)/2), so for the button
   to land 10px off the content, the box's right edge must sit 10+16+... = 54px
   off the content's right edge: button_left = content_right+10; box_left =
   button_left-16 = content_right-6; box_right = box_left+60 = content_right+54.
   `right` (measured from the viewport edge) is then `contentGap - 54`, where
   contentGap = (100vw - --dh-mid-w)/2. Confirmed with a headless render (the
   on-screen button-to-content gap measured exactly 10px). */
.dh-snav.side-mid{right:calc((100vw - var(--dh-mid-w,1080px)) / 2 - 54px)}
.dh-snav-btn{position:absolute;left:50%;transform:translateX(-50%);pointer-events:auto;
  width:28px;height:28px;border-radius:50%;border:1px solid var(--line2);background:var(--panel);color:var(--muted);
  cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 6px 18px rgba(0,0,0,.5);
  transition:opacity .18s,transform .18s,color .15s,border-color .15s,background .15s}
.dh-snav-up{top:78px}
.dh-snav-down{bottom:26px}
.dh-snav-btn:hover{color:var(--mint);border-color:var(--mint);background:var(--bg2);transform:translateX(-50%) scale(1.1)}
.dh-snav-btn.hide{opacity:0;pointer-events:none;transform:translateX(-50%) scale(.6)}
.dh-snav-ico{display:flex}
.dh-snav-ico svg{width:16px;height:16px;fill:currentColor;display:block}

/* tab bar */
.dh-ed-bar{display:flex;align-items:stretch;background:var(--panel);border-bottom:1px solid var(--line);min-height:42px}
.dh-tabs{display:flex;align-items:stretch;flex:1;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin}
.dh-tabs::-webkit-scrollbar{height:6px}
.dh-tabs::-webkit-scrollbar-thumb{background:var(--line2);border-radius:3px}
.dh-tab{display:flex;align-items:center;gap:7px;padding:0 8px 0 13px;min-width:150px;flex-shrink:0;
  border-right:1px solid var(--line);color:var(--muted);cursor:pointer;font-size:13px;white-space:nowrap;user-select:none}
.dh-tab:hover{background:rgba(255,255,255,.03);color:#fff}
.dh-tab.on{background:var(--bg2);color:#fff;box-shadow:inset 0 2px 0 var(--mint)}
.dh-tab.drag{opacity:.45}
/* Tabs never shrink server/file names down to unreadable slivers — each tab
   sizes to its own content (flex-shrink:0 here and on both labels below) and
   the strip scrolls horizontally (.dh-tabs overflow-x:auto + the wheel handler
   in server-panel.js) once tabs no longer fit, instead of cramming everything
   in and ellipsizing. The per-label max-width below is only a safety cap
   against a pathologically long name/server, not a normal-case shrink. */
.dh-tab-name{overflow:hidden;text-overflow:ellipsis;font-family:var(--sans);flex-shrink:0;max-width:220px}
.dh-tab-srv{flex-shrink:0;font-family:var(--sans);font-size:10px;color:var(--mint);border:1px solid rgba(47,212,106,.4);
  border-radius:5px;padding:0 5px;max-width:120px;overflow:hidden;text-overflow:ellipsis}
.dh-tab-dot{width:7px;height:7px;border-radius:50%;background:transparent;flex-shrink:0}
.dh-tab.dirty .dh-tab-dot{background:#ff5c5c;box-shadow:0 0 7px rgba(255,92,92,.75)}
.dh-tab-x{background:none;border:0;color:var(--muted2);cursor:pointer;font-size:12px;line-height:1;padding:3px 5px;border-radius:5px;flex-shrink:0}
.dh-tab-x:hover{background:var(--danger);color:#fff}
.dh-ed-x{background:none;border:0;border-left:1px solid var(--line);color:var(--muted);cursor:pointer;font-size:14px;padding:0 17px;flex-shrink:0}
.dh-ed-x:hover{background:var(--danger);color:#fff}
/* Path shown in the bar instead of the tab strip — mobile only (see 920px
   override); a multi-tab strip is desktop-only since mobile never opens more
   than one file at a time. Hidden by default so it doesn't double up with
   .dh-tabs on desktop. */
.dh-ed-path-top{display:none;flex:1;align-items:center;padding:0 14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}

/* editor host + footer */
.dh-ed-host{flex:1;min-height:0;position:relative}
/* Desktop: the control bar now sits right under the tab/path row (DOM order
   is bar, foot, find, host) instead of docked to the bottom — see the
   2026-07-14 UI note. Mobile restores the old bottom-docked position via
   `order` in the 920px override below, without needing separate markup. */
.dh-ed-foot{display:flex;align-items:center;gap:12px;padding:8px 12px;border-bottom:1px solid var(--line);background:var(--panel)}
/* Scrollable control strip: holds save / close-all / toggles and scrolls
   horizontally when the editor column is too narrow to show them all (wheel or
   swipe). The info button stays OUTSIDE it so its downward tooltip isn't clipped
   by this strip's overflow. Visible thin scrollbar — same treatment as the tab
   strip above it (.dh-tabs) — so the overflow is discoverable, not just a
   silent wheel/swipe affordance. */
.dh-ed-foot-scroll{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:12px;
  overflow-x:auto;scrollbar-width:thin}
.dh-ed-foot-scroll::-webkit-scrollbar{height:6px}
.dh-ed-foot-scroll::-webkit-scrollbar-thumb{background:var(--line2);border-radius:3px}
/* Path label absorbs all the slack (shrinking to an ellipsis first) so the
   hint text and toggles stay a fixed group, without needing a margin-left:auto
   hack on whichever item happens to be first after Save. */
.dh-ed-path{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.dh-ed-hint{flex-shrink:0}
/* Toggle controls (word wrap, Minecraft preview) sit in one row on desktop;
   stacked into a column on mobile — see the 920px override below. */
.dh-ed-toggles{display:flex;align-items:center;gap:14px;flex-wrap:nowrap;flex-shrink:0}
/* Save (desktop): one solid-green pill — "Сохранить" label plus two small
   round icon buttons in front of it, where a single leading save icon used
   to sit. The pill as a whole (label included) saves just the active file;
   the save-all icon is the one carved-out exception (its own click handler,
   see server-panel.js's dh-ed-savebar onclick). Mobile turns the two icons
   into standalone round buttons — see the 920px override below. */
.dh-ed-savebar{flex-shrink:0;box-sizing:border-box;display:inline-flex;align-items:center;height:38px;gap:6px;
  padding:0 16px 0 6px;border-radius:999px;background:var(--mint);color:#04140a;cursor:pointer}
.dh-ed-save-label{font-family:var(--display);font-weight:700;font-size:14px;line-height:1;white-space:nowrap}
/* Icon buttons sit on the green pill, so they get a subtle dark tint instead
   of their own background. No hover/active transform "pop" here — these sit
   inside a bar that's already a single click target, and the scale/lift read
   as a second, competing button trying to draw attention to itself. */
.dh-ed-save-ico{flex-shrink:0;box-sizing:border-box;width:26px;height:26px;padding:0;display:inline-flex;align-items:center;justify-content:center;
  border:0;border-radius:50%;background:rgba(4,20,10,.16);color:#04140a;cursor:pointer;
  transition:background .15s ease}
.dh-ed-save-ico svg{width:15px;height:15px;display:block}
.dh-ed-save-ico:hover{background:rgba(4,20,10,.28)}
/* "Закрыть все" — always danger-red (outline + text), filling solid red on hover.
   Same box model as the save pill so the two are exactly the same height. */
.dh-ed-closeall{flex-shrink:0;box-sizing:border-box;height:38px;line-height:1;padding:0 15px;border-radius:999px;background:transparent;
  border:1px solid var(--danger);color:var(--danger);font-family:var(--display);font-weight:600;font-size:13px;cursor:pointer;
  transition:background .15s,color .15s,border-color .15s}
.dh-ed-closeall:hover{background:var(--danger);border-color:var(--danger);color:#fff}

/* find / replace panel (Find + Replace tabs; scoped to the active file only) */
.dh-find{background:var(--panel);border-bottom:1px solid var(--line);padding:8px 10px;display:flex;flex-direction:column;gap:7px}
.dh-find[hidden]{display:none}
.dh-find-tabs{display:flex;align-items:center;gap:4px}
.dh-find-tab{background:none;border:0;border-bottom:2px solid transparent;color:var(--muted);font-family:var(--sans);font-size:13px;font-weight:600;padding:3px 9px;cursor:pointer}
.dh-find-tab:hover{color:#fff}
.dh-find-tab.on{color:var(--mint);border-bottom-color:var(--mint)}
.dh-find-close{margin-left:auto;background:none;border:0;color:var(--muted);cursor:pointer;font-size:13px;padding:3px 7px;border-radius:5px}
.dh-find-close:hover{background:var(--danger);color:#fff}
.dh-find-row{display:flex;align-items:center;gap:6px}
.dh-find-replace-row{display:none}
.dh-find.replace .dh-find-replace-row{display:flex}
.dh-find-q,.dh-find-r{flex:1;min-width:0;background:#0a0d0b;border:1px solid var(--line2);border-radius:8px;padding:7px 10px;color:var(--text);font-size:13px;font-family:ui-monospace,Consolas,monospace}
.dh-find-q:focus,.dh-find-r:focus{outline:none;border-color:var(--mint)}
.dh-find-btn{background:rgba(255,255,255,.04);border:1px solid var(--line2);color:#fff;cursor:pointer;border-radius:8px;padding:6px 10px;font-size:13px;line-height:1;flex-shrink:0}
.dh-find-btn:hover{border-color:var(--mint);color:var(--mint)}
.dh-find-btn.wide{font-family:var(--sans);font-weight:600}
.dh-find-case{background:rgba(255,255,255,.04);border:1px solid var(--line2);color:var(--muted);cursor:pointer;border-radius:8px;padding:6px 9px;font-size:12px;font-weight:700;flex-shrink:0}
.dh-find-case.on{border-color:var(--mint);color:var(--mint);background:var(--green-dim)}
.dh-find-count{flex-shrink:0;min-width:92px;text-align:right}
.cm-dh-find{background:rgba(229,192,123,.32);border-radius:2px}

/* CodeMirror sizing + DristHosting ("drist") theme */
.dh-editor .CodeMirror{height:100%;font-family:'SF Mono',ui-monospace,'Cascadia Code',Consolas,monospace;font-size:13px;line-height:1.55}
.cm-s-drist.CodeMirror{background:var(--bg);color:#cdd6cf}
.cm-s-drist .CodeMirror-gutters{background:#070a08;border-right:1px solid var(--line)}
.cm-s-drist .CodeMirror-linenumber{color:#46564c}
.cm-s-drist .CodeMirror-cursor{border-left:1px solid var(--mint)}
.cm-s-drist div.CodeMirror-selected{background:rgba(255,214,10,.34)}
.cm-s-drist .CodeMirror-line::selection,.cm-s-drist .CodeMirror-line>span::selection{background:rgba(255,214,10,.42)}
.cm-s-drist .CodeMirror-line::-moz-selection,.cm-s-drist .CodeMirror-line>span::-moz-selection{background:rgba(255,214,10,.42)}
/* YAML: the standalone word `false` painted red (see YAML_FALSE_OVERLAY) —
   themed selector so it beats the base .cm-atom colour. */
.cm-s-drist .cm-dh-bool-true{color:#2fd46a!important;font-weight:600}
.cm-s-drist .cm-dh-bool-false{color:#ff5c5c!important;font-weight:600}
/* Painted booleans are click-to-toggle — hint it with a pointer cursor. */
.cm-s-drist .cm-dh-bool-true,.cm-s-drist .cm-dh-bool-false{cursor:pointer}
.cm-s-drist .CodeMirror-activeline-background{background:rgba(255,255,255,.028)}
.cm-s-drist .CodeMirror-matchingbracket{color:var(--mint)!important;border-bottom:1px solid var(--mint)}
.cm-s-drist .CodeMirror-guttermarker-subtle{color:#46564c}
.cm-s-drist .cm-comment{color:#5f7468;font-style:italic}
.cm-s-drist .cm-keyword{color:var(--mint);font-weight:600}
.cm-s-drist .cm-atom{color:#e5c07b}
.cm-s-drist .cm-number{color:#d19a66}
.cm-s-drist .cm-def{color:#79c0ff}
.cm-s-drist .cm-variable-2{color:#7fb3ff}
.cm-s-drist .cm-variable-3,.cm-s-drist .cm-type{color:#56b6c2}
.cm-s-drist .cm-property{color:#9cdcfe}
.cm-s-drist .cm-string{color:#98c379}
.cm-s-drist .cm-string-2{color:#56b6c2}
.cm-s-drist .cm-meta{color:#c678dd}
.cm-s-drist .cm-tag{color:var(--mint)}
.cm-s-drist .cm-attribute{color:#d19a66}
.cm-s-drist .cm-bracket{color:#8fa898}
.cm-s-drist .cm-error{color:#fff;background:var(--danger)}
.cm-s-drist .cm-header{color:var(--mint);font-weight:700}
.cm-s-drist .cm-link{color:#61afef;text-decoration:underline}

@media(max-width:1180px){  /* = SINGLE_FS_BP in server-panel.js */
  body.editor-open{padding-right:0}
  body.editor-open header.top,
  body.editor-open footer{margin-right:0}
  /* Panel is full-screen here (covers the page) — no split, so restore the
     normal centred cap on the hidden column instead of a docked calc width. */
  body.editor-open:not(.modrinth-open) main.container{max-width:var(--dh-mid-w,1080px)}
  /* A floating support bubble has no business hovering over a full-screen
     editor — hide it outright instead of just moving it aside (desktop's
     split-view only repositions it; see the non-mobile rule above). */
  body.editor-open .support,
  body.editor-open .woot-widget-bubble,
  body.editor-open .woot-widget-holder,
  body.editor-open .woot--bubble-holder{display:none!important}
  .dh-editor{width:100vw;right:0;border-radius:0;border-left:0;border-right:0;transform:translateX(100%);
    /* Fill the viewport instead of chasing the underlying page's scroll
       position — updateBounds() keeps setting inline top/bottom for the
       desktop split-view, so these need !important to win. */
    top:0!important;bottom:0!important;height:100vh;height:100dvh}
  /* Keyboard shortcuts are a desktop-only affordance on a phone. */
  .dh-ed-hint{display:none}
  /* A multi-file tab strip is redundant on mobile — only one file is ever
     open there at a time — so the bar shows the current path instead, with
     the close button staying pinned to the top-right corner. */
  .dh-tabs{display:none}
  .dh-ed-path-top{display:flex}
  /* The path now lives in the bar above instead of duplicating it down here;
     the footer holds the toggle column on the left with Save beside it on
     the right (not below — that just left dead space under a short column). */
  .dh-ed-path{display:none}
  /* Desktop moved the control bar up top (see the non-mobile rules above);
     mobile keeps it bottom-docked, so push it after find/host via `order`
     rather than duplicating markup. */
  .dh-ed-foot{align-items:center;gap:12px;order:1;border-bottom:0;border-top:1px solid var(--line)}
  /* One horizontal control strip (like desktop) that SWIPES sideways to reveal
     anything that overflows a narrow phone, instead of the old stacked toggle
     column (which can't be swiped and forced the footer to grow tall). */
  .dh-ed-toggles{flex-direction:row;align-items:center;gap:14px;flex:0 0 auto;min-width:0}
  /* Save on mobile: drop the pill/label — just the two round green buttons
     (save-one, save-all), same glyphs as desktop. No hover on touch, so
     _bindHoldTip (server-panel.js) shows the same [data-tip] popup on a
     tap-and-hold instead. */
  .dh-ed-savebar{background:transparent;padding:0;gap:10px}
  .dh-ed-save-label{display:none}
  .dh-ed-save-ico{width:34px;height:34px;background:var(--mint);color:#04140a;box-shadow:none}
  .dh-ed-save-ico svg{width:17px;height:17px}
  .dh-ed-save-ico:active{transform:scale(.92);box-shadow:0 6px 16px rgba(47,212,106,.4)}
  /* Compact the "Закрыть все" pill so it fits beside the toggle column. */
  .dh-ed-closeall{height:34px;padding:0 11px;font-size:12px}
  /* Each checkbox+label pair still reads left-to-right in its own row (the
     wrap must NOT go column here — that's what stacked the checkbox on top
     of its own label). The hide-formatting sub-toggle is a nested child of
     the preview row, so it's pushed onto a second, wrapped line instead. */
  /* Single-line strip now (see toggles rule above): keep the preview controls on
     one row and let the "Скрыть символы" sub-toggle slide in horizontally like
     desktop, so the whole footer is one swipeable line. */
  .mc-preview-wrap{flex-wrap:nowrap}
  .mc-hide-wrap{flex-basis:auto;width:auto;max-height:none;max-width:0;margin-top:0;overflow:hidden;
    transition:max-width .22s ease,opacity .18s ease}
  .mc-hide-wrap.mc-hide-visible{max-width:240px;max-height:none;margin-top:0}
  /* Locks the underlying dashboard from scrolling behind the full-screen
     editor (see CodeEditor's _lockMobileScroll in server-panel.js) — mirrors
     the mobile Chatwoot scroll-lock further up this file. */
  body.dh-editor-lock{position:fixed;left:0;right:0;width:100%;overflow:hidden}
}

/* ---- Minecraft preview mode ---- */
/* Footer preview controls */
.mc-preview-wrap{display:flex;align-items:center;gap:4px;flex-shrink:0;white-space:nowrap}
/* Custom square instead of the native checkbox: dark fill + green outline so
   it stands out against the panel background even unchecked, filling solid
   mint (with a small dark check) once ticked. */
.mc-preview-wrap input[type=checkbox]{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin:0;flex-shrink:0;
  border:1.5px solid var(--mint);border-radius:4px;background:#0a0d0b;cursor:pointer;position:relative;
  transition:background .15s,border-color .15s}
.mc-preview-wrap input[type=checkbox]:hover{border-color:#fff}
.mc-preview-wrap input[type=checkbox]:checked{background:var(--mint)}
.mc-preview-wrap input[type=checkbox]:checked::after{content:'';position:absolute;left:4.5px;top:1.5px;width:4px;height:8px;
  border:solid #04140a;border-width:0 2px 2px 0;transform:rotate(45deg)}
.mc-preview-wrap label{font-size:11px;color:var(--muted);cursor:pointer;margin:0;font-weight:400;user-select:none}
.mc-preview-wrap label:hover{color:#fff}
/* Separator dot between the two checkbox groups */
.mc-preview-sep{color:var(--muted2);font-size:11px;flex-shrink:0}

/* "Скрыть символы" wrap — slides in when preview is checked */
.mc-hide-wrap{display:flex;align-items:center;gap:4px;overflow:hidden;max-width:0;opacity:0;
  transition:max-width .22s ease,opacity .18s ease}
.mc-hide-wrap.mc-hide-visible{max-width:240px;opacity:1}

/* ===================== Modrinth integration ============================ */
/* file-manager badges: a root plugins/mods folder the core can browse, and the
   Modrinth glyph that replaces 📄 on a Modrinth-installed jar. */
.folder-modrinth{width:18px;height:18px;border-radius:4px;flex-shrink:0;object-fit:contain}
.fi-modrinth{border-radius:5px}
/* green per-file update button (left of the kebab) */
.sp-upd-btn{color:var(--mint)!important}
.sp-upd-btn svg{width:18px;height:18px;display:block}

/* bottom action bar inside the Файлы tab */
.sp-modrinth-bar{display:flex;align-items:center;gap:10px;margin-top:14px;padding-top:13px;border-top:1px solid var(--line);flex-wrap:wrap;min-height:34px}
/* the bar's height is taken back from the file list so the tab stays the same height */
.sp-drop.has-mr-bar{height:calc(560px - 62px)}
.sp-mr-check .dh-spin{margin-right:7px}
.sp-mr-all svg{width:16px;height:16px}

/* ---- uMod integration (Rust) ---- reuses the Modrinth widget + bar styling,
   with an amber accent (vs Modrinth's mint) so the two browsers read as distinct. */
/* uMod's logo is a ~3:1 wordmark — keep its aspect (fixed height, auto width) so it
   isn't squished into a square box. */
.folder-umod{width:32px;height:18px;border-radius:2px;flex-shrink:0;object-fit:contain}
.fi-umod{border-radius:2px}
.dh-umod .dh-mr-bar .dh-mr-logo{height:16px;width:auto;border-radius:0}
/* a plugin with no icon falls back to the wordmark: letterbox it (contain) inside
   the square result/detail icon box instead of cropping/squishing it. */
.um-logo-fallback{object-fit:contain!important;background:#0a0d0b;padding:7px}
.dh-umod .dh-mr-q:focus{border-color:#d18b4f}
.dh-mr-rlist{display:block}
.dh-um-more{display:flex;justify-content:center;padding:14px}
.dh-um-sort{display:flex;gap:6px;margin-top:9px}
.dh-um-sortb{flex:1;background:#0a0d0b;border:1px solid var(--line2);color:var(--muted);border-radius:8px;padding:6px 8px;font-size:12px;cursor:pointer;font-family:var(--sans)}
.dh-um-sortb:hover{color:#fff;border-color:var(--line)}
.dh-um-sortb.on{background:#1a130c;border-color:#d18b4f;color:#fff}
.dh-um-install.done{opacity:.7}
.dh-umod .dh-mr-dl-main[disabled]{opacity:.6;cursor:default;color:var(--muted)}

/* ---- CurseForge integration ---- reuses the Modrinth widget + bar styling,
   with CurseForge's orange accent (vs Modrinth's mint / uMod's amber) so all
   three browsers read as distinct at a glance. The logo is an SVG that already
   carries its own rounded-rect #EB622B plate, so it needs no extra background —
   just the same square footprint the Modrinth .webp gets. */
.folder-curseforge{width:18px;height:18px;border-radius:4px;flex-shrink:0;object-fit:contain}
.fi-curseforge{border-radius:5px}
/* a plugins/mods folder can be filled from more than one source, so its badges
   sit in a small row rather than as a single mark. */
.sp-fmarks{display:inline-flex;align-items:center;justify-content:center;gap:4px;min-height:30px;flex-shrink:0}
.dh-curseforge .dh-mr-q:focus{border-color:#EB622B}
.dh-curseforge .dh-mr-tab.on{color:#f0864f;border-bottom-color:#EB622B}
.dh-curseforge .dh-mr-more{display:flex;justify-content:center;padding:14px}
/* a release whose author forbids third-party downloads: visibly inert, but still
   clickable so the click can explain why (toast) instead of doing nothing. */
.dh-curseforge .dh-mr-dl.is-blocked .dh-mr-dl-main{opacity:.55;color:var(--muted)}
.dh-mr-vitem.is-blocked{opacity:.5}
.dh-mr-vitem.is-blocked .dh-mr-vnum{text-decoration:line-through}
.dh-curseforge .dh-mr-dl-main[disabled],
.dh-curseforge .btn[disabled]{opacity:.6;cursor:default}

/* ---- shared content-source bar (Modrinth + CurseForge + uMod, one line) ----
   Each source is a bare logo: no border, no background, no label, and it never
   changes size. The ONLY hover feedback is the logo glowing in its own brand
   colour, plus the source's name as a floating popup (the site-wide [data-tip]
   tooltip in info-tip.js). Deliberately nothing else — an inline expanding label,
   a border fading in and a shrink-on-hover were each tried and removed: they all
   made the row twitch on a pointer that was only passing through. The glow is a
   drop-shadow on the logo (not a box-shadow on the button) so it traces the mark
   itself rather than an invisible rectangle around it. */
.sp-srcs{display:flex;align-items:center;gap:10px;margin-right:auto}  /* pushes the action buttons right */
.sp-modrinth-bar .sp-src-btn{display:inline-flex;align-items:center;justify-content:center;gap:0;
  padding:4px;color:#fff;background:transparent;box-shadow:none;border:0;transform:none}
/* .btn's lift would move the logo on hover — the one thing this bar must not do. */
.sp-modrinth-bar .sp-src-btn:hover{transform:none;box-shadow:none;background:transparent}
.sp-src-logo{width:30px;height:30px;border-radius:5px;flex-shrink:0;display:block;
  transition:filter .2s ease}
.sp-modrinth-bar .sp-src-btn.sp-mr-add:hover .sp-src-logo,
.sp-modrinth-bar .sp-src-btn.sp-mr-add:focus-visible .sp-src-logo{filter:drop-shadow(0 0 7px rgba(47,212,106,.9))}
.sp-modrinth-bar .sp-src-btn.sp-cf-add:hover .sp-src-logo,
.sp-modrinth-bar .sp-src-btn.sp-cf-add:focus-visible .sp-src-logo{filter:drop-shadow(0 0 7px rgba(235,98,43,.95))}
.sp-modrinth-bar .sp-src-btn.sp-um-add:hover .sp-src-logo,
.sp-modrinth-bar .sp-src-btn.sp-um-add:focus-visible .sp-src-logo{filter:drop-shadow(0 0 7px rgba(209,139,79,.9))}
/* uMod's logo is a ~3:1 wordmark — keep its aspect instead of squaring it */
.sp-um-add .sp-src-logo{height:24px;width:auto;border-radius:0}

/* download progress toast */
.dh-prog{position:relative;display:flex;flex-direction:column;gap:7px;min-width:250px}
.dh-prog-cancel{position:absolute;top:-4px;right:-4px;width:22px;height:22px;border:0;border-radius:50%;background:transparent;color:var(--muted);font-size:20px;line-height:20px;cursor:pointer}.dh-prog-cancel:hover{background:var(--danger);color:#fff}.dh-prog-cancel:disabled{opacity:.45;cursor:default}
.dh-prog-label{font-size:13px}
.dh-prog-bar{height:6px;border-radius:4px;background:rgba(255,255,255,.13);overflow:hidden}
.dh-prog-bar i{display:block;height:100%;width:2%;background:var(--mint);border-radius:4px;transition:width .35s ease}
.dh-prog.ok .dh-prog-bar i{background:var(--mint)}
.dh-prog.err .dh-prog-bar i{background:var(--danger);width:100%}
.dh-prog.indeterminate .dh-prog-bar i{width:38%;animation:dh-indet 1.1s ease-in-out infinite}
@keyframes dh-indet{0%{margin-left:-38%}100%{margin-left:100%}}

/* in-button download progress (Modrinth/uMod "Скачать" buttons) — a translucent
   sweep grows left-to-right over whatever the button's own background is, so
   it reads on the mint Modrinth button and the uMod detail's .btn.primary alike
   without needing per-button colors. z-index:-1 relies on the button itself
   being the positioned ancestor (added by .dh-dl-active) so it paints above the
   button's own background but under its (unpositioned) label text/icon. */
button.dh-dl-active{position:relative;overflow:hidden;cursor:default}
/* .btn.sm (2 classes) would otherwise outrank the generic rule above on
   padding-right, since the uMod detail's install button carries that class
   pair too — repeat the selector with matching specificity so there's
   always room for the ✕ regardless of which button variant is downloading. */
button.dh-dl-active,.btn.dh-dl-active{padding-right:24px}
button.dh-dl-active .dh-dl-fill{position:absolute;top:0;left:0;bottom:0;width:0;z-index:-1;
  background:rgba(255,255,255,.32);transition:width .3s ease;pointer-events:none}
button.dh-dl-active.dh-dl-indet .dh-dl-fill{width:42%!important;animation:dh-indet 1.1s ease-in-out infinite}
/* uMod's list button dims+greys out on [disabled] by default (already-installed
   state) — override that while actively downloading so the fill stays legible. */
.dh-mr-dl-main.dh-dl-active[disabled]{opacity:1;color:#04140a}
.dh-dl-cancel{position:absolute;top:50%;right:4px;transform:translateY(-50%);width:15px;height:15px;border-radius:50%;
  background:rgba(4,20,10,.55);color:#fff;border:0;display:flex;align-items:center;justify-content:center;
  font-size:9px;line-height:1;cursor:pointer;z-index:2;padding:0}
.dh-dl-cancel:hover{background:var(--danger)}

/* markdown tables + extra heading levels in the Modrinth detail body */
.dh-mr-md h5,.dh-mr-md h6{font-family:var(--sans);color:#fff;margin:12px 0 6px}
.dh-mr-md table.dh-mr-mdtable{border-collapse:collapse;width:100%;margin:10px 0;font-size:13px;display:block;overflow-x:auto}
.dh-mr-md .dh-mr-mdtable th,.dh-mr-md .dh-mr-mdtable td{border:1px solid var(--line);padding:6px 9px;text-align:left;vertical-align:top}
.dh-mr-md .dh-mr-mdtable th{background:rgba(255,255,255,.05);color:#fff;font-weight:600}
.btn.loading{opacity:.85;pointer-events:none}
.dh-spin{display:inline-block;width:13px;height:13px;border:2px solid rgba(255,255,255,.28);border-top-color:#fff;border-radius:50%;animation:dh-spin .7s linear infinite;vertical-align:-2px}
@keyframes dh-spin{to{transform:rotate(360deg)}}

/* ---- the docked Modrinth browser (mirror of the editor, on the LEFT) ---- */
/* page push: content shifts into the right half so the seam sits at centre.
   Header/footer keep full width via a negative left margin (same trick the
   editor uses on the right). */
body{transition:padding-left .32s cubic-bezier(.4,0,.2,1),padding-right .32s cubic-bezier(.4,0,.2,1)}
header.top,footer{transition:margin-left .32s cubic-bezier(.4,0,.2,1),margin-right .32s cubic-bezier(.4,0,.2,1)}
body.modrinth-open{--dh-mr-w:50vw;padding-left:var(--dh-mr-w)}
body.modrinth-open header.top,
body.modrinth-open footer{margin-left:calc(-1 * var(--dh-mr-w))}
body.modrinth-open main.container{padding-left:var(--dh-gap,10px)}
/* BOTH open: three equal columns — modrinth : page(console) : editor = 1:1:1,
   each one third of the viewport, so the console sits dead centre. Widths and
   paddings all flow from the two column-width variables. */
body.editor-open.modrinth-open{--dh-ed-w:33.333vw;--dh-mr-w:33.333vw}

.dh-modrinth{position:fixed;top:0;left:var(--dh-gap,10px);bottom:0;width:calc(var(--dh-mr-w, 50vw) - var(--dh-gap,10px));z-index:25;display:flex;flex-direction:column;
  background:var(--bg2);border:1px solid var(--line2);border-radius:var(--r-lg);overflow:hidden;
  box-shadow:0 24px 70px rgba(0,0,0,.6);transform:translateX(calc(-100% - var(--dh-gap,60px) - 24px));
  transition:transform .32s cubic-bezier(.4,0,.2,1),width .32s cubic-bezier(.4,0,.2,1)}
.dh-modrinth.show{transform:translateX(0)}
.dh-modrinth.dh-bedrock-addons{z-index:31}

.dh-mr-bar{display:flex;align-items:center;gap:8px;padding:0 12px;min-height:44px;background:var(--panel);border-bottom:1px solid var(--line)}
.dh-mr-bar .dh-mr-logo{width:20px;height:20px;border-radius:4px}
.dh-mr-title{font-family:var(--display);font-weight:700;color:#fff}
.dh-mr-dir{color:var(--muted);font-size:13px;font-family:ui-monospace,Consolas,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dh-mr-x{margin-left:auto;background:none;border:0;color:var(--muted);font-size:15px;cursor:pointer;padding:6px 10px;border-radius:8px;flex-shrink:0}
.dh-mr-x:hover{background:var(--danger);color:#fff}

.dh-mr-search{padding:10px 12px;border-bottom:1px solid var(--line);background:var(--panel)}
.dh-mr-q{width:100%;background:#0a0d0b;border:1px solid var(--line2);border-radius:10px;padding:9px 12px;color:var(--text);font-size:14px;font-family:var(--sans)}
.dh-mr-q:focus{outline:none;border-color:var(--mint)}

.dh-mr-body{flex:1;min-height:0;position:relative}
.dh-mr-results{position:absolute;inset:0;overflow:auto;padding:8px}
.dh-mr-result{display:flex;gap:11px;align-items:center;padding:10px;border-radius:12px;cursor:pointer;border:1px solid transparent}
.dh-mr-result:hover{background:var(--card);border-color:var(--line)}
.dh-mr-ricon{width:48px;height:48px;border-radius:10px;flex-shrink:0;object-fit:cover;background:#0a0d0b}
.dh-mr-ricon.bedrock-provider-icon,.sp-ba-add .sp-src-logo{object-fit:contain}
.dh-mr-rinfo{flex:1;min-width:0}
.dh-mr-rtitle{font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dh-mr-rdesc{color:var(--muted);font-size:13px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.dh-mr-ract{flex-shrink:0}
.dh-mr-dl{display:inline-flex;align-items:stretch;border-radius:9px;overflow:hidden}
.dh-mr-dl-main{background:var(--mint);color:#04140a;border:0;font-weight:600;font-family:var(--sans);font-size:13px;padding:7px 11px;cursor:pointer;white-space:nowrap}
.dh-mr-dl-caret{background:var(--mint);color:#04140a;border:0;border-left:1px solid rgba(0,0,0,.22);padding:0 9px;cursor:pointer;font-size:11px}
.dh-mr-dl-main:hover,.dh-mr-dl-caret:hover{filter:brightness(1.08)}
.dh-mr-dl.changed .dh-mr-dl-main,.dh-mr-dl.changed .dh-mr-dl-caret{background:var(--card2);color:#fff;border-left-color:var(--line)}
.dh-mr-rstats{display:flex;gap:13px;color:var(--muted2);font-size:12px;margin-top:4px}

/* custom animated version dropdown (replaces the plain context menu) */
.dh-mr-vmenu{position:fixed;z-index:60;min-width:184px;max-width:300px;max-height:340px;overflow:auto;
  background:var(--panel);border:1px solid var(--line2);border-radius:12px;padding:5px;
  box-shadow:0 18px 50px rgba(0,0,0,.55);opacity:0;transform:translateY(-6px) scale(.98);
  transform-origin:top right;transition:opacity .16s ease,transform .16s cubic-bezier(.4,0,.2,1)}
.dh-mr-vmenu.show{opacity:1;transform:translateY(0) scale(1)}
.dh-mr-vitem{display:flex;align-items:center;gap:8px;padding:7px 9px;border-radius:8px;cursor:pointer}
.dh-mr-vitem:hover{background:var(--card2)}
.dh-mr-vnum{font-weight:600;color:#fff;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
.dh-mr-vbadge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:1px 6px;border-radius:20px;flex-shrink:0}
.dh-mr-vbadge.rel{background:var(--green-dim);color:var(--mint)}
.dh-mr-vbadge.beta{background:rgba(229,192,123,.16);color:#e5c07b}
.dh-mr-vbadge.alpha{background:rgba(255,92,92,.16);color:var(--danger)}
.dh-mr-vdate{color:var(--muted2);font-size:11px;flex-shrink:0}

.dh-mr-detail{position:absolute;inset:0;background:var(--bg2);overflow:auto;padding:14px}
.dh-mr-detail[hidden]{display:none}
.dh-mr-detail-head{display:flex;gap:12px;align-items:center;position:relative;padding-right:34px}
.dh-mr-dicon{width:64px;height:64px;border-radius:14px;object-fit:cover;background:#0a0d0b;flex-shrink:0}
.dh-mr-dinfo{flex:1;min-width:0}
.dh-mr-dtitle{font-family:var(--display);font-size:20px;font-weight:700;color:#fff;line-height:1.2}
.dh-mr-dsub{margin-top:4px}
/* The install/change-version action button, same as the search-row one, next
   to the name at the top of the detail page instead of buried in a tab. */
.dh-mr-detail-head .dh-mr-dl,.dh-mr-detail-head .dh-um-install{flex-shrink:0}
.dh-mr-detail-x{position:absolute;top:0;right:0;background:none;border:0;color:var(--muted);font-size:16px;cursor:pointer;padding:4px 8px;border-radius:8px}
.dh-mr-detail-x:hover{background:var(--danger);color:#fff}
.dh-mr-detail-cols{display:flex;gap:18px;margin-top:14px;align-items:flex-start}
.dh-mr-detail-main{flex:1;min-width:0}
.dh-mr-aside{width:236px;flex-shrink:0}
@media(max-width:1100px){.dh-mr-detail-cols{flex-direction:column}.dh-mr-aside{width:100%}}
.dh-mr-tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-bottom:12px}
.dh-mr-tab{background:none;border:0;border-bottom:2px solid transparent;color:var(--muted);font-weight:600;font-family:var(--sans);font-size:14px;padding:7px 12px;cursor:pointer}
.dh-mr-tab:hover{color:#fff}
.dh-mr-tab.on{color:var(--mint);border-bottom-color:var(--mint)}
.dh-mr-md{color:#cdd6cf;font-size:14px;line-height:1.65;word-wrap:break-word;overflow-wrap:anywhere}
.dh-mr-md img{max-width:100%;height:auto;border-radius:8px}
.dh-mr-md h1,.dh-mr-md h2,.dh-mr-md h3,.dh-mr-md h4{font-family:var(--sans);color:#fff;margin:14px 0 8px}
.dh-mr-md a{color:var(--info)}
.dh-mr-md pre{background:#0a0d0b;border:1px solid var(--line);border-radius:8px;padding:10px;overflow:auto}
.dh-mr-md code{background:rgba(255,255,255,.06);padding:1px 5px;border-radius:5px;font-family:ui-monospace,Consolas,monospace;font-size:12px}
.dh-mr-md pre code{background:none;padding:0}
.dh-mr-md ul,.dh-mr-md ol{padding-left:22px;margin:8px 0}
.dh-mr-md blockquote{border-left:3px solid var(--line2);margin:8px 0;padding-left:12px;color:var(--muted)}
.dh-mr-changelog .dh-mr-cl-item{border-bottom:1px solid var(--line);padding:12px 0}
.dh-mr-cl-head{margin-bottom:6px}
.dh-mr-vtable td{vertical-align:middle}
.dh-mr-vtable .btn.sm svg{width:16px;height:16px}
.dh-mr-ablock{margin-bottom:16px}
.dh-mr-atitle{font-weight:700;color:#fff;font-size:12px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px}
.dh-mr-abody{display:flex;flex-direction:column;gap:6px}
.dh-mr-link{color:var(--info);font-size:13px}
.dh-mr-tags{display:flex;flex-direction:row;flex-wrap:wrap;gap:6px}
.dh-mr-tag{background:var(--card);border:1px solid var(--line);border-radius:20px;padding:2px 10px;font-size:12px;color:var(--muted)}
.dh-mr-person{display:flex;align-items:center;gap:8px;color:var(--text)}
.dh-mr-person:hover{text-decoration:none}
.dh-mr-avatar{width:24px;height:24px;border-radius:50%;object-fit:cover;background:#0a0d0b;flex-shrink:0}

body.modrinth-open:not(.editor-open) .support,
body.modrinth-open:not(.editor-open) .woot-widget-bubble,
body.modrinth-open:not(.editor-open) .woot-widget-holder,
body.modrinth-open:not(.editor-open) .woot--bubble-holder{left:auto!important;right:22px!important}

/* Both panels open: three columns need even more room than a lone panel, so fall
   back to full-screen sooner (a bigger viewport than the editor-only case).
   (= BOTH_FS_BP in server-panel.js) */
@media(max-width:1600px){
  body.editor-open.modrinth-open{padding-left:0;padding-right:0}
  body.editor-open.modrinth-open header.top,
  body.editor-open.modrinth-open footer{margin-left:0;margin-right:0}
  body.editor-open.modrinth-open main.container{padding-left:var(--dh-gap,10px);padding-right:var(--dh-gap,10px);max-width:var(--dh-mid-w,1080px)}
  body.editor-open.modrinth-open .dh-editor{width:100vw;left:0;right:0;border-radius:0;border-left:0;border-right:0}
  body.editor-open.modrinth-open .dh-modrinth{width:100vw;left:0;right:auto;border-radius:0;border-left:0;border-right:0}
  /* Can't fit all three columns → only the LAST-opened panel (dh-stack-*, set in
     server-panel.js) fills the screen; the other is tucked off-screen behind it.
     So which panel covers depends on OPEN ORDER, not "modrinth always on top". */
  body.dh-stack-mr.editor-open.modrinth-open .dh-editor{transform:translateX(100%)}
  body.dh-stack-ed.editor-open.modrinth-open .dh-modrinth{transform:translateX(-100%)}
}

@media(max-width:1180px){  /* = SINGLE_FS_BP in server-panel.js */
  body.modrinth-open{padding-left:0}
  body.modrinth-open header.top,body.modrinth-open footer{margin-left:0}
  body.modrinth-open:not(.editor-open) main.container{max-width:var(--dh-mid-w,1080px)}
  body.editor-open.modrinth-open{padding-left:0;padding-right:0}
  body.editor-open.modrinth-open header.top,body.editor-open.modrinth-open footer{margin-left:0;margin-right:0}
  .dh-modrinth{width:100vw;left:0;border-radius:0;border-left:0;border-right:0;transform:translateX(-100%)}
  body.editor-open.modrinth-open .dh-modrinth,body.editor-open.modrinth-open .dh-editor{width:100vw}
}
