:root{
  --bg-url:url('./static/backround-main.svg?v=20260609');
  --bg-page:#000000;
  --bg-page-overlay:rgba(0,0,0,0);
  --bg-table:transparent;             /* table base shows the .tbl frosted glass */
  --bg-zebra:rgba(255,255,255,.045);  /* zebra/footer/expand are translucent tints */
  --bg-col:rgba(255,255,255,.028);
  --bg-zebra-col:rgba(255,255,255,.07);
  --bg-expand:rgba(0,0,0,.22);
  --bg-footer:rgba(0,0,0,.14);
  --bg-muted:#27272a;
  --bg-modal:rgba(4,3,5,.72);
  --hover-default:#08070a;
  --text-primary:#d4d4d8;
  --text-secondary:#8d8d98;
  --text-hover:#a1a1aa;
  --border-table:#1c1619;
  --border-th:#1f181b;
  --border-td:#161012;
  --border-line:#1e1e24;
  --border-chip:#23232a;
  --text-bright:#ececee;
  --text-white:#fff;
  --text-faint:#71717e;
  --text-subtle:#72727d;
  --text-disabled:#5d5d61;
  --bg-card:#111114;
  --bg-desc:#17171b;
  --border-card:#1f1f24;
}
[data-theme="light"]{
  --bg-url:url('./static/backround-main-light.svg?v=20260609b');
  --bg-page:#eef1f6;
  --bg-page-overlay:rgba(255,255,255,0);
  --bg-table:transparent;
  --bg-zebra:rgba(0,0,0,.035);
  --bg-col:rgba(0,0,0,.022);
  --bg-zebra-col:rgba(0,0,0,.052);
  --bg-expand:rgba(0,0,0,.045);
  --bg-footer:rgba(0,0,0,.025);
  --bg-muted:#ddd8d4;
  --bg-modal:rgba(0,0,0,.35);
  --hover-default:#f4f2f0;
  --text-primary:#1a1412;
  --text-secondary:#585250;
  --text-hover:#3a3230;
  --text-disabled:#908880;
  --border-table:#ddd6d0;
  --border-th:#d4cdc6;
  --border-td:#e8e2dc;
  --border-line:#ddd6d0;
  --border-chip:#ccc5be;
  --text-bright:#1a1412;
  --text-white:#1a1412;
  --text-faint:#7a726c;
  --text-subtle:#605a56;
  --bg-card:#ffffff;
  --bg-desc:#f8f7f5;
  --border-card:#ddd6d0;
}
*{box-sizing:border-box;margin:0;padding:0}
/* dvh для оболочки — возвращён 10.08.2026 по решению владельца.
   Эксперимент с svh (в тот же день) ставился против полевого CLS: dvh
   пересчитывается, когда мобильный браузер прячет адресную строку при скролле,
   и каждый пересчёт меняет высоту оболочки. Гипотеза не подтвердилась и не
   опровергнута — настоящей причиной CLS оказался резерв под таблицу (см.
   #content ниже, 0.672 -> 0), а цена svh видна сразу: оболочка считается по
   вьюпорту с ВИДИМОЙ адресной строкой, и когда браузер её прячет, снизу
   остаётся полоса фона.
   100% — фолбэк для движков без единиц вьюпорта. */
html{height:100%;height:100dvh;background:var(--bg-page);overscroll-behavior-x:contain;-webkit-text-size-adjust:100%;text-size-adjust:100%}
body{display:flex;flex-direction:column;height:100%;height:100dvh;overflow:hidden;background-color:transparent;color:var(--text-primary);font-family:-apple-system,system-ui,'Segoe UI',sans-serif;font-size:16px;-webkit-font-smoothing:antialiased;padding-bottom:env(safe-area-inset-bottom,0px);-webkit-user-select:none;user-select:none}
body.tg-fs{padding-top:calc(var(--tg-safe-area-inset-top,0px) + var(--tg-content-safe-area-inset-top,0px));padding-bottom:calc(var(--tg-safe-area-inset-bottom,0px) + env(safe-area-inset-bottom,0px))}
/* Flex column so `main`'s flex-grow fills leftover height when content is short,
 * pinning `footer` (and absolutely positioned `#sub-w` inside it) to the bottom
 * of the scroll viewport. Without `display:flex` on `.scroll-wrap`, inline
 * `flex:1` on `<main>` has no effect. */
/* overscroll-behavior:contain — цепочку к корню НЕ пускаем.
   Родного pull-to-refresh здесь всё равно быть не может: браузер вешает его
   на корневой скроллер, а документ у нас не прокручивается (body
   overflow:hidden) — именно это и держит шапку неподвижной. Обновление
   делает свой жест (_initPullToRefresh в app.js), и родной отскок ему
   только мешал бы: палец двигал бы две вещи сразу. */
.scroll-wrap{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
.scroll-wrap>main{flex:1 1 auto}
.scroll-wrap>footer{flex-shrink:0}

/* z-index:3 keeps the label above `.tab-slider` (z-index:2) so the
   active tab's text doesn't disappear behind the glass pill. */
/* Vertical padding is sized so the button fills the full nav-bar height:
 * `nav` has no padding of its own, so `.tab-btn` padding IS the bar. A
 * generous vertical padding means a finger can tap anywhere in the bar's
 * strip and still land on the correct tab (see annotated screenshot). */
/* .tab-btn применяется и к <a> (вкладки стали ссылками ради перелинковки).
   text-decoration и font-family задаём явно: у ссылки и у кнопки они разные
   по умолчанию, и без этого вкладки поехали бы шрифтом. */
.tab-btn{position:relative;z-index:3;text-decoration:none;font-family:inherit;padding:14px 14px;font-size:16px;font-weight:500;border:none;border-bottom:2px solid transparent;color:var(--text-secondary);cursor:pointer;white-space:nowrap;background:none;transition:color .25s ease}
.tab-btn:hover{color:var(--text-hover)}
.tab-btn.tab-active{color:#fff}

/* Tab bar slider — full-height glass pill that slides behind the active
   `.tab-btn` inside a `.tab-track` (mirrors `.chip-slider`: same
   backdrop-filter + gradient recipe). Tabs and regions swapped their
   slider styles when the two rows swapped place in the header.

   The active pill has to visually overpaint BOTH grey divider lines
   that bracket the nav row — `header>.wrap::after` above and
   `nav>.wrap::after` below — so its top and bottom edges blend into
   the borders instead of stopping 1px short of them. The bottom edge
   is handled by `.tab-slider { z-index:2 }` (lifts it above the
   nav-local ::after). For the top edge we pull the whole `nav` up by
   1px with `margin-top:-1px` and give it `position:relative; z-index:1`
   so nav and its descendants stack above `header` in the body-level
   stacking context. The slider still uses `top:0; height:100%`, but
   because nav now sits 1px into header's bottom, that top pixel lands
   exactly on the header's grey divider and repaints it. We can't
   achieve the same by setting `top:-1px` on the slider itself —
   `tab-track` is `overflow-x:auto`, which per spec forces overflow-y
   to clip, so a slider sticking out of the track gets cropped. */
.sticky-hdr{flex-shrink:0;z-index:20}
nav{position:relative;z-index:1;margin-top:-1px}
.tab-track{position:relative;scrollbar-width:none}
.tab-track::-webkit-scrollbar{display:none}
/* `transform:translate3d(0,0,0)` seeds a concrete "from" value so Safari
 * can interpolate when the first `translate3d(Xpx,0,0)` is written from
 * JS. Without it WebKit sees `none → translate3d(...)` and skips the
 * transition (user-visible as a teleport of the slider bar). The 3d
 * variant also pins the element to its own compositor layer, which
 * sidesteps another Safari bug where transitions inside a
 * `-webkit-overflow-scrolling:touch` scroll container render as jumps.
 * `will-change:transform` only — `width` is a layout prop, advertising
 * it just confuses Safari's layer promotion without helping perf on a
 * 2px bar. */
.tab-slider{position:absolute;top:0;left:0;width:0;height:100%;background:linear-gradient(135deg,rgba(197,15,53,.9),rgba(105,11,31,.85));box-shadow:0 8px 24px -8px rgba(197,15,53,.95),inset 0 1px 0 rgba(255,255,255,.12);opacity:0;pointer-events:none;z-index:2;transform:translate3d(0,0,0);transition:transform .38s cubic-bezier(.32,.72,0,1),width .38s cubic-bezier(.32,.72,0,1),opacity .25s ease;will-change:transform}
.tab-slider::after{content:'';position:absolute;inset:0;background:linear-gradient(180deg,rgba(255,255,255,.14),rgba(255,255,255,0) 50%,rgba(0,0,0,.12));pointer-events:none}
.tab-slider.no-anim{transition:none}

/* Telegram Mini App overrides: lift the tab bar into the safe-area strip
 * between the native Close and menu buttons. That strip's height equals
 * `--tg-content-safe-area-inset-top` and sits just below the device
 * notch (`--tg-safe-area-inset-top`). `position:fixed` ties the bar to
 * viewport coordinates, the only positioning that survives Telegram's
 * WebView (sticky there scrolls with content). `pointer-events:none` on
 * the wrapper keeps the empty side-strips click-through; we re-enable
 * pointer-events on the actual capsule. */
.tg-fs nav{position:fixed;top:var(--tg-safe-area-inset-top,0px);left:50%;transform:translateX(-50%);z-index:30;margin-top:0;height:var(--tg-content-safe-area-inset-top,40px);display:flex;align-items:center;pointer-events:none}
.tg-fs nav .wrap{padding:0!important;display:inline-flex;pointer-events:auto}
.tg-fs nav>.wrap::after{display:none}
.tg-fs .tab-track{display:inline-flex!important;gap:0;flex:none!important;overflow-x:auto!important;overflow-y:hidden;scrollbar-width:none;overscroll-behavior-x:contain;max-width:calc(100vw - 168px);min-width:0;background:rgba(255,255,255,.08);border-radius:8px;padding:2px}
/* overflow-x:auto форсирует overflow-y:clip, но .tg-fs .tab-slider — это
   top:2px;height:calc(100% - 4px) внутри padding:2px, то есть целиком внутри
   бокса: срезать нечего. overscroll-behavior-x:contain обязателен — без него
   свайп на конце трека уходит в горизонтальный жест Telegram и закрывает
   Mini App. 168px — резерв под нативные back/⋯/✕, ИЗМЕРИТЬ на устройстве. */
.tg-fs .tab-track::-webkit-scrollbar{display:none}
[data-theme="light"] .tg-fs .tab-track{background:rgba(0,0,0,.06)}
/* В капсуле поля кнопки уже, чем на сайте: 14px с каждой стороны давали 28px
   между соседними подписями, и четыре вкладки занимали почти всю доступную
   ширину (168px и так отданы нативным back/⋯/✕). 10px — ряд читается плотнее
   и раньше перестаёт прокручиваться. Ширину ползунка мерит _moveTabSlider по
   самой кнопке, поэтому подгонять её отдельно не нужно. */
.tg-fs .tab-btn{padding:5px 10px!important;font-size:13px!important;font-weight:600;border-bottom:none!important;border-radius:6px}
.tg-fs .tab-slider{border-radius:6px!important;top:2px!important;height:calc(100% - 4px)!important;overflow:hidden;box-shadow:0 4px 12px -4px rgba(197,15,53,.6),inset 0 1px 0 rgba(255,255,255,.15)!important}
.tg-fs .rating-tab-chips{display:none!important}
.tg-rating-pill{display:none}
.tg-fs .tg-rating-pill{display:inline-flex;align-items:center;margin-left:6px;padding:5px 10px;font-size:12px;font-weight:600;color:#fff;background:linear-gradient(135deg,#C50F35,#690B1F);border:none;border-radius:6px;cursor:pointer;letter-spacing:.02em;box-shadow:0 4px 12px -4px rgba(197,15,53,.6),inset 0 1px 0 rgba(255,255,255,.15)}

table{border-collapse:separate;border-spacing:0;width:100%}
th{text-align:left;padding:11px 10px;font-size:13px;line-height:1.25;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--text-secondary);border-bottom:none;box-shadow:inset 0 -1px 0 var(--border-th);white-space:nowrap}
td{padding:10px 10px;border-bottom:none;box-shadow:inset 0 -1px 0 var(--border-td);font-size:15px;line-height:1.3;white-space:nowrap}
/* Direct-row height anchor. The «active provider» row inside `.tbl-vpn`
 * tbody (rendered when `activeDirect` exists) carries this class on
 * BOTH the single-word («Ростелеком») and multi-word («Сибирский
 * Медведь») branches. Pinning every cell in that row to a fixed 48px
 * box-box height keeps the row's pixel height identical regardless of
 * which provider is active, even when the multi-word branch swaps in
 * its tighter padding override (6/6 instead of the default 10/10).
 * Without this pin, sub-pixel rounding of the font leading inside the
 * sec-name cell shifts the row height by ±1px between providers, and
 * that ±1px cascades into the <tfoot> below as a visible jump of the
 * «ПРОВАЙДЕР:» label and chip-track slider on every provider switch. */
.tbl-vpn tbody tr.direct-row>td{height:53px;box-sizing:border-box;vertical-align:middle}
th.c,td.c{text-align:center}
/* Zebra striping. Two mechanisms coexist:
 *   - automatic `tr:nth-child(even)` for simple tables (proxy list,
 *     expanded server tables, admin nodes/profiles) where every tr is
 *     visible and the parity is stable;
 *   - explicit `tr.row-alt` class for tables with hidden sibling rows
 *     (e.g. VPN main table where expandable groups get a paired hidden
 *     `.expand-row`). The render code assigns `.row-alt` based on the
 *     index among VISIBLE rows so the stripes stay aligned regardless
 *     of which groups are expandable.
 * Tables marked `.tbl-vpn` opt out of the nth-child rule entirely. */
table:not(.tbl-vpn) tbody tr:nth-child(even) td{background:var(--bg-zebra)}
tbody tr.row-alt td{background:var(--bg-zebra)}
td.sec-name,th.sec-name{background:var(--bg-col)}
td.sec-meta,th.sec-meta{background:var(--bg-col)}
table:not(.tbl-vpn) tbody tr:nth-child(even) td.sec-name,table:not(.tbl-vpn) tbody tr:nth-child(even) td.sec-meta{background:var(--bg-zebra-col)}
tbody tr.row-alt td.sec-name,tbody tr.row-alt td.sec-meta{background:var(--bg-zebra-col)}
.tbl-proxy td.sec-name,.tbl-proxy th.sec-name{background:transparent}
table.tbl-proxy tbody tr:nth-child(even) td.sec-name{background:var(--bg-zebra)}
.tbl-proxy tbody tr.row-alt td.sec-name{background:var(--bg-zebra)}
/* Row hover. Base tone is neutral grey; rows carrying an `edge-*`
 * status class swap in a green / orange / red wash so the hover reads
 * the row's signal state at a glance (working / partial / failed).
 *
 * Why the verbose selector list: the zebra rules above assign
 * higher-specificity backgrounds to `td.sec-name` / `td.sec-meta` on
 * `nth-child(even)` / `.row-alt` rows (specificity 0,3,4 / 0,2,3). A
 * plain `tr:hover td` (0,1,2) loses those fights, and the hover ended
 * up patchy — side columns kept their zebra tint while the middle
 * flipped to the hover colour. We match each zebra scope cell by cell
 * so every cell in the hovered row lands on `var(--hover-bg)`. */
tr{--hover-bg:var(--hover-default)}
tr.edge-ok{--hover-bg:rgba(34,197,94,.05)}
tr.edge-half{--hover-bg:rgba(255,88,10,.05)}
tr.edge-fail{--hover-bg:rgba(239,68,68,.05)}
tbody tr:hover td,
tbody tr:hover td.sec-name,
tbody tr:hover td.sec-meta,
table:not(.tbl-vpn) tbody tr:nth-child(even):hover td,
table:not(.tbl-vpn) tbody tr:nth-child(even):hover td.sec-name,
table:not(.tbl-vpn) tbody tr:nth-child(even):hover td.sec-meta,
tbody tr.row-alt:hover td,
tbody tr.row-alt:hover td.sec-name,
tbody tr.row-alt:hover td.sec-meta{background:var(--hover-bg)}

.st{font-weight:700;font-size:15px}
.st-ok{color:#22c55e}
.st-warn{color:#eab308}
.st-fail{color:#ef4444}
.st-none{color:var(--text-disabled,#48484f);display:inline-block;min-width:17px;line-height:17px;text-align:center;vertical-align:middle;font-weight:400}

.dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex-shrink:0}
.dot-g{background:#22c55e}.dot-y{background:#eab308}.dot-r{background:#ef4444}.dot-0{background:var(--bg-muted)}

/* Colored left edge on VPN rows. Replaces the per-row score dot with a
 * 1px-wide vertical gradient stripe that spans the full row height and
 * sits flush with the table wrapper's visual edge (the VPN wrapper drops
 * its left border/radius so the stripe itself forms the edge). Gradients
 * mirror assets/yes.svg, assets/half.svg and assets/no.svg. */
/* VPN table drops its left border (the coloured edge stripe per row forms
   the visible left edge) and moves its bottom border from the real
   border slot into an `inset` box-shadow one pixel inside the
   padding-box. Why: the real border sits outside `.tbl`'s padding-box,
   and `.tbl`'s `overflow-x:auto` clips descendants at the padding-box
   — so the tfoot slider could never physically sit on top of the real
   border. Relocating the line inside the clip lets the slider (also
   inside padding-box) land directly on top of it.
   `padding-bottom:1px` reserves that strip so the `tfoot td`'s own
   background (`#040304`) stops before the shadow instead of painting
   over it — inset shadows render above element background but under
   descendants, so without the padding the td would mask the line.
   Height budget stays identical: the dropped `border-bottom:1px` is
   compensated by the new `padding-bottom:1px`. */
/* All-inset borders so they follow the 8px border-radius → rounded TOP corners
   (the cap row now reads as peeking out from under the rounded table). */
.tbl:has(> .tbl-vpn){padding-bottom:1px;box-shadow:inset 0 1px 0 var(--border-table),inset -1px 0 0 var(--border-table),inset 0 -1px 0 var(--border-table)}
/* Footer row hosts the provider chips as a single full-width band that
 * seals the bottom of the VPN table. No left stripe, uniform background,
 * and it must not react to row hover.
 *
 * Layout notes:
 * - The footer row uses TWO tds: the first one carries the "Провайдер:"
 *   label and inherits `.sec-name`'s `width:1%` so it shrinks to the
 *   same width as the name column in tbody; the second one (`.prov-cell`)
 *   spans every remaining column (`colspan = 3 + targets.length`) so the
 *   chip track starts exactly at the boundary between the name column
 *   and the "scores" columns. No pixel magic needed — the browser's
 *   column sizing keeps label and chips aligned with the rows above
 *   regardless of how long the VPN names get.
 * - `padding-bottom:0` on the chips cell drops the slider flush against
 *   the table's inner bottom edge, so it reads as the table's bottom
 *   line lighting up under the active provider (see the upward-glow
 *   `box-shadow` on `.chip-track-prov > .chip-slider` further below).
 * - `border-top` used to be `2px solid #201719` which read as a bold
 *   seam between the last row and the chips; dimmed to a single-pixel
 *   hairline matching the rest of the table's row dividers. */
.tbl-vpn tfoot td{background:var(--bg-footer);padding:8px 10px;border-top:none;border-bottom:0;box-shadow:inset 0 1px 0 var(--border-td);white-space:normal;vertical-align:middle}
.tbl-vpn tfoot tr:hover td{background:var(--bg-footer)}
/* Label cell sits inside the `.sec-name` column. The "Провайдер:" span
   now carries its own leading `ico-sm` icon, so the cell's padding only
   needs to match tbody's base left padding — the icon + gap recipe
   inside the span lines things up with the provider rows for free
   (10px desktop, 7px mobile, mirroring `.tbl-vpn td.sec-name`).
   `padding-bottom:0` mirrors the chips cell so both cells share the
   same effective content area height; otherwise the label would sit
   4px higher than the chip track's vertical centre because `prov-cell`
   uses `padding-bottom:0` (slider hugs the table's bottom border). */
.tbl-vpn tfoot td.sec-name{padding:8px 10px 0 10px}
/* Chips cell: kill the bottom padding so the slider at the track's
   bottom lands flush with the table's inner bottom edge, and drop the
   left padding so chips start exactly at the scores-column boundary. */
.tbl-vpn tfoot td.prov-cell{padding:8px 10px 0 0}
.tbl-vpn tfoot .chip-track-prov{padding:0}
/* "Набалдашник" — provider chips as a full-width cap ROW inside the table
   `<thead>` (visually part of the table, mirror of the tfoot chips row). One
   colspanned cell (label + chips flex) so it doesn't affect column widths; its
   top corners round with the table via the frame clip + this cell radius. The
   lifted `.ovl-slider` drives its underline (see `_syncTblSlider`). */
.tbl-vpn thead tr.prov-head td{background:transparent;padding:2px 10px 6px;border-radius:7px 7px 0 0;box-shadow:none;position:relative}
.prov-head-row{display:flex;align-items:center;gap:10px}
.prov-head-lbl{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;font-size:10px;line-height:1;color:var(--text-secondary);font-weight:600;text-transform:uppercase;letter-spacing:.04em}
.prov-head-lbl .ico-sm{width:14px;height:14px}
.prov-head-row .chip-track-prov{padding:0;min-width:0}
/* The "VPN + сервисы" row (`.vpn-head`) rounds its top corners — its frosted
   outer columns curve, revealing the darker cap (bg-footer) behind, so the cap
   reads as flowing into a rounded table. */
.tbl-vpn thead tr.vpn-head th:first-child{border-top-left-radius:8px}
.tbl-vpn thead tr.vpn-head th:last-child{border-top-right-radius:8px}
/* VPN row top edge = EXACTLY the table's own top edge (line ~378):
   `1px solid --glass-border` + `inset 0 1px 0 --glass-sheen`. Both the border
   and the inset sheen follow the 8px corner radius, so the row's top edge reads
   identical to the table/cap top frame. Plus the existing dark bottom line. */
/* Denser fill for JUST the VPN header row: a second fill layer (alpha .25) over
   the panel's own --glass-fill makes this one row a touch less transparent.
   Per-theme colour (dark tint / white) so light theme isn't muddied by a dark
   overlay. Clipped by the cell radius so the rounded corners still reveal the cap. */
.tbl-vpn thead tr.vpn-head th{background:rgba(20,21,27,.1);border-top:1px solid var(--glass-border);box-shadow:inset 0 -1px 0 var(--border-th)}
[data-theme="light"] .tbl-vpn thead tr.vpn-head th{background:rgba(255,255,255,.1)}
/* tfoot bottom corners rounded via the cells (clip handles it too). */
.tbl-vpn tfoot td:first-child{border-bottom-left-radius:7px}
.tbl-vpn tfoot td:last-child{border-bottom-right-radius:7px}
/* VPN column sizing: shrink the name + meta columns to their content so
 * the target-icon columns get to breathe horizontally instead of
 * hugging each other in the middle. Proxy table uses the same three-
 * section layout (name | provider scores | last-check) so it shares
 * the rule. */
.tbl-vpn th.sec-name,.tbl-vpn td.sec-name,
.tbl-vpn th.sec-meta,.tbl-vpn td.sec-meta{width:1%}
/* Proxy table: lock the whole table geometry so switching operator filters
 * (which changes which proxy rows are visible) can't reflow the columns.
 * `table-layout:fixed` freezes column widths based on the explicit values
 * below; the name column is pinned to match `.proxy-name-clip`'s 21ch cap,
 * the clock column gets a fixed numeric-safe slot, and provider score
 * columns auto-distribute the remainder evenly. */
.tbl-proxy{table-layout:fixed}
.tbl-proxy th.sec-name,.tbl-proxy td.sec-name{width:22ch}
.tbl-proxy th.sec-meta,.tbl-proxy td.sec-meta{width:56px}
/* VPN expanded server table: same fixed-layout approach as proxy table.
 * Name column capped at 22ch (matches proxy-name-clip), ping at 56px,
 * target icons auto-distribute the rest evenly. */
.tbl-srv{table-layout:fixed}
.tbl-srv th:first-child,.tbl-srv td:first-child{width:22ch}
.tbl-srv th:nth-child(2),.tbl-srv td:nth-child(2){width:56px;text-align:center}
/* Proxy name clipper: hard-cap the visible name at 21ch and fade the tail
 * from 15ch to 21ch so long IPs/domains don't bloat the name column. Uses
 * `ch` (width of the "0" glyph) which is monospace-ish enough for numeric
 * IPs and short domains. The mask extends past the element, so names
 * shorter than 15ch render fully opaque. Element must be inline-block for
 * max-width + overflow to take effect inside the `<td>`. */
.proxy-name-clip{display:inline-block;max-width:21ch;overflow:hidden;white-space:nowrap;vertical-align:bottom;-webkit-mask-image:linear-gradient(to right,#000 0,#000 15ch,transparent 21ch);mask-image:linear-gradient(to right,#000 0,#000 15ch,transparent 21ch)}
td.sec-name{position:relative}
td.sec-name.edge-ok::before,
td.sec-name.edge-half::before,
td.sec-name.edge-fail::before{content:'';position:absolute;left:0;top:0;bottom:0;width:1px;pointer-events:none}
td.sec-name.edge-ok::before{background:linear-gradient(180deg,#33783E,#07593E)}
td.sec-name.edge-half::before{background:linear-gradient(180deg,#DE3513,#FF580A)}
td.sec-name.edge-fail::before{background:linear-gradient(180deg,#6E0C20,#C10F34)}

.badge{padding:1px 6px;border-radius:9999px;font-size:14px;font-weight:500;white-space:nowrap}
.badge-ok{background:#052e16;color:#22c55e}
.badge-fail{background:#450a0a;color:#ef4444}
.badge-info{background:#172554;color:#60a5fa}

.wrap{width:100%;max-width:860px;margin:0 auto}

/* Divider lines under header / nav (tabs) / region chips.
 *
 * Constraints:
 *   1. The line must end exactly where the table inside <main class="wrap">
 *      starts, i.e. inset by main's horizontal padding (16px desktop,
 *      12px mobile) so it never protrudes past the table's visible edge.
 *   2. Vertically the line must sit at the BOTTOM of the wrap element
 *      (= top of the next section), not at the bottom of the content-box,
 *      so the wrap's padding-bottom stays as breathing room *above* the
 *      line rather than below it.
 *
 * A `border-bottom` fails (1): it spans the full border-box width.
 * A `background-image` clipped to content-box fails (2): the line would
 *   float `padding-bottom` pixels above the element's real bottom edge.
 * A pseudo-element absolutely positioned to the padding-box bottom with
 *   explicit left/right inset satisfies both constraints and needs no
 *   extra DOM. */
header>.wrap,
nav>.wrap,
.region-chips>.wrap{position:relative}
header>.wrap::after,
nav>.wrap::after,
.region-chips>.wrap::after{
  content:'';
  position:absolute;
  left:16px;
  right:16px;
  bottom:0;
  height:1px;
  background:var(--border-line);
  pointer-events:none;
}
/* NB: no `-webkit-overflow-scrolling:touch` here — in WebKit it turns the
   scroll container into a layer that does NOT clip content to border-radius,
   so the table's corners rendered square. Modern iOS has momentum scroll by
   default, so dropping it costs nothing and lets `overflow` clip the rounded
   corners properly. */
.tbl{overflow-x:auto;overflow-y:hidden;scrollbar-width:none;border-radius:8px;border:1px solid var(--glass-border);box-shadow:var(--glass-shadow),inset 0 1px 0 var(--glass-sheen);background:var(--glass-grain),linear-gradient(135deg,var(--glass-sheen),transparent 38%),var(--glass-fill);}
.tbl::-webkit-scrollbar{display:none}

.fchip{display:inline-flex;align-items:center;padding:3px 8px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid var(--border-chip);color:var(--text-secondary);background:transparent;transition:color .18s ease,border-color .18s ease;user-select:none;text-transform:uppercase;letter-spacing:.03em;touch-action:manipulation;-webkit-tap-highlight-color:transparent}
.fchip:hover{border-color:var(--text-secondary);color:var(--text-hover)}
/* Multi-toggle proxy filter states keep their signal colours. */
.fchip.f-ok{background:rgba(34,197,94,.08);border-color:rgba(34,197,94,.4);color:#22c55e}
.fchip.f-fail{background:rgba(239,68,68,.08);border-color:rgba(239,68,68,.4);color:#ef4444}

/* Region + single-select provider chips: borderless text labels. The
 * glass-pill slider provides the only button-like affordance — idle
 * chips must not look like clickable buttons on their own. */
.region-chip{display:inline-flex;align-items:center;padding:12px 12px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:1px solid transparent;color:var(--text-secondary);background:transparent;transition:color .18s ease;user-select:none;white-space:nowrap}
.region-chip:hover{color:var(--text-hover)}
.region-chip-active{color:#fff}
.region-chip-active:hover{color:#fff}

/* ── Chip slider (full-bar-height rectangle that slides between chips) ──
 * `.chip-slider` is positioned absolutely inside each `.chip-track` and
 * spans the full vertical height of the track. Only the horizontal
 * transform + width animate between chips. No border-radius — this is a
 * tab-style indicator, not a button sitting on top of a button.
 *
 * NB: NO `isolation:isolate` here. The slider's backdrop-filter has to be
 * able to see the page background (body bg image) underneath, otherwise
 * the glass effect collapses into a solid red rectangle.
 */
.chip-track{position:relative;display:inline-flex;align-items:center;gap:2px}
/* Hit-area: the chips absorb the track's former vertical padding so the
 * tap target equals the full bar height. Track padding moves to
 * `.region-chip` (4+8=12). Slider still anchors to the track's bottom
 * edge, which now coincides with the chip's bottom edge — distance from
 * text baseline to the 2 px underline stays identical to the previous
 * layout (12 px). */
.chip-track-bar{padding:0}
/* Fixed-height invisible bar for provider chips so the underline slider
 * doesn't jitter when chips have different natural heights (icon-only
 * vs text). Same shape as `.chip-track-bar` so the underline lands in
 * the same spot on the region row and the provider row.
 *
 * Provider chips are icon-only tappable targets, so they're a touch
 * larger than the generic `.fchip` default — both the hit area (padding)
 * and the glyph (`.ico-sm` override) grow modestly, with the track's
 * `min-height` following so the slider still anchors to a stable
 * baseline. */
/* Same hit-area trick as `.chip-track-bar`: strip vertical padding from
 * the track and push it onto the chip buttons so fingers register on
 * the full provider-bar strip (4+6=10). */
.chip-track-prov{min-height:30px;align-items:center;padding:0;gap:4px}
.prov-sep{width:1px;align-self:center;background:var(--bg-muted);flex-shrink:0;margin:0 4px;height:14px}
.chip-track-prov>button{display:inline-flex;align-items:center}
.chip-track-prov>button.fchip{padding:10px 10px;border-radius:9px;transition:filter .3s cubic-bezier(.32,.72,0,1),color .18s ease,border-color .18s ease}
.chip-track-prov>button.fchip .ico-sm{width:20px;height:20px}
/* Inactive provider chips fade back: saturation drops + slight
   brightness cut so the active chip reads as the "lit" selection.
   Filter is cheap to animate and plays nicely with the slider's
   position transition when the user picks a different provider. */
.chip-track-prov>button.fchip:not(.f-ok){filter:saturate(.55) brightness(.82)}
/* Chip labels sit at z-index:3 so they render ABOVE their slider
   (region slider z-index:2, VPN footer slider z-index:0) — the glow
   then pools behind the chip content instead of tinting it from
   the front. Mirrors `.tab-btn`'s z-index:3 vs `.tab-slider` z-index:2
   relationship in the nav bar. Background/border are forced
   transparent so the slider (and its glow) visibly show through. */
.chip-track>button{position:relative;z-index:3;background:transparent!important;border-color:transparent!important}
.chip-track.chip-track-prov>button.fchip.f-ok{color:#fff}
/* Same Safari-friendly seeding as `.tab-slider` — see note above for
 * why translate3d seed + `will-change:transform` (only) are required
 * to get a smooth region/provider slide in WebKit. */
.chip-slider{position:absolute;top:0;left:0;width:0;height:100%;background:linear-gradient(135deg,rgba(197,15,53,.85),rgba(105,11,31,.8));box-shadow:0 8px 24px -8px rgba(197,15,53,.75),inset 0 1px 0 rgba(255,255,255,.12);opacity:0;pointer-events:none;z-index:0;transform:translate3d(0,0,0);transition:transform .38s cubic-bezier(.32,.72,0,1),width .38s cubic-bezier(.32,.72,0,1),opacity .25s ease;will-change:transform}
.chip-slider::after{content:'';position:absolute;inset:0;background:linear-gradient(180deg,rgba(255,255,255,.14),rgba(255,255,255,0) 50%,rgba(0,0,0,.12));pointer-events:none}
.chip-slider.no-anim{transition:none}

/* Region row AND provider rows (VPN + Proxy) use the underline style
   (2px bottom line). The default `.chip-slider` recipe above is
   currently unused, but kept in case we reintroduce a pill-style track.

   The region row's track sits flush against the grey 1px divider line
   drawn by `.region-chips>.wrap::after`. Both elements are positioned
   descendants of the same `.wrap` with z-index 0 (slider) and auto
   (::after); since ::after comes later in DOM order it was painting
   ON TOP of the slider and masking its bottom pixel, so the red
   underline looked 1px tall instead of 2px.

   We keep the slider at `bottom:0; height:2px` (so it's aligned with
   the divider's bottom edge) and bump its `z-index` to 2 — one pixel
   of the red line now floats above the grey divider and the other
   pixel repaints on top of the grey divider, covering it exactly
   under the active chip. `.chip-track>button` then sits at z-index:3
   so chip labels (and icons) stay above the slider and its glow. */
.chip-track-bar>.chip-slider,
.chip-track-prov>.chip-slider{top:auto;bottom:0;height:2px;background:linear-gradient(135deg,#C50F35,#690B1F);box-shadow:0 0 10px -2px rgba(197,15,53,.55);border-radius:2px 2px 0 0;z-index:2}
/* Region slider borrows the VPN footer's upward multi-layer light wash.
   Kept at z-index:2 (unlike the VPN variant) because it still has to
   cover the grey `region-chips>.wrap::after` divider under the active
   chip — dropping to z-index:0 would let that divider paint on top
   (::after has z-index:auto, later in DOM) and leave a visible gap in
   the red line. Region chips are text-only, so having the glow render
   in front of them just reads as a "lit" selection rather than an
   obscuring tint. */
.chip-track-bar>.chip-slider{box-shadow:0 -2px 8px rgba(197,15,53,1.3),0 -8px 20px rgba(197,15,53,1),0 -18px 28px rgba(197,15,53,.85),0 -30px 44px rgba(197,15,53,.8)}
/* Provider chip tracks (proxy header + VPN footer) share the same upward
 * multi-layer wash as region chips — reads as a halo of light blooming
 * upward from the active provider instead of a flat 2px bar. The VPN
 * footer variant below still overrides position (`bottom:-1px`) and
 * swaps the shadow to the status-tinted edge-* gradients. */
.chip-track-prov>.chip-slider{box-shadow:0 -2px 8px rgba(197,15,53,1.3),0 -8px 20px rgba(197,15,53,1),0 -18px 28px rgba(197,15,53,.85),0 -30px 44px rgba(197,15,53,.8)}
.chip-track-bar>.chip-slider::after,
.chip-track-prov>.chip-slider::after{display:none}
/* Inside the VPN footer the slider sits directly on the table's bottom
   line. `.tbl` paints that line as an `inset` box-shadow one pixel
   above its padding-box bottom (see `.tbl:has(> .tbl-vpn)` above).
   The track ends at the `tfoot td`'s content-box bottom, which lines
   up one pixel above the shadow (because `.tbl` has
   `padding-bottom:1px`), so `bottom:-1px` on the slider drops its
   lower pixel onto the shadow exactly — under the active chip the
   coloured bar replaces the grey line, and the grey line is visible
   everywhere else.
   Upward-biased multi-layer shadow reads as a bright wash of light
   from below. Colours come from the active provider's status class
   (edge-ok / edge-half / edge-fail) set on the track itself, so the
   slider matches the provider's signal colour. */
/* Slider in the VPN footer sits BELOW the chips on the z-axis (z-index:0
   vs the chip buttons' z-index:1) so its upward glow pools behind the
   provider icons rather than tinting them from the front. The slider
   bar itself is still visible because it occupies the bottom strip of
   the track where the chips don't overlap vertically.
   Shadow is a soft, evenly-spaced three-layer wash — no hot spot right
   above the bar — so the light reads as a diffuse halo, not a flame. */
.tbl-vpn tfoot .chip-track-prov>.chip-slider{bottom:0;z-index:0;box-shadow:0 -2px 8px rgba(197,15,53,1.25),0 -8px 20px rgba(197,15,53,.95),0 -18px 28px rgba(197,15,53,.85),0 -30px 44px rgba(197,15,53,.7)}
.tbl-vpn tfoot .chip-track-prov.edge-ok>.chip-slider{background:linear-gradient(135deg,#33783E,#07593E);box-shadow:0 -2px 8px rgba(34,197,94,1.25),0 -8px 20px rgba(34,197,94,.95),0 -18px 28px rgba(34,197,94,.85),0 -30px 44px rgba(34,197,94,.7)}
.tbl-vpn tfoot .chip-track-prov.edge-half>.chip-slider{background:linear-gradient(135deg,#DE3513,#FF580A);box-shadow:0 -2px 8px rgba(255,88,10,1.25),0 -8px 20px rgba(255,88,10,.95),0 -18px 28px rgba(255,88,10,.85),0 -30px 44px rgba(255,88,10,.7)}
.tbl-vpn tfoot .chip-track-prov.edge-fail>.chip-slider{background:linear-gradient(135deg,#C10F34,#6E0C20);box-shadow:0 -2px 8px rgba(193,15,52,1.25),0 -8px 20px rgba(193,15,52,.95),0 -18px 28px rgba(193,15,52,.85),0 -30px 44px rgba(193,15,52,.7)}
/* Brand-coloured provider sliders */
.chip-track-prov[data-brand="beeline"]>.chip-slider{background:linear-gradient(135deg,#FCB533,#C89020)!important;box-shadow:0 -2px 8px rgba(252,181,51,1.25),0 -8px 20px rgba(252,181,51,.95),0 -18px 28px rgba(252,181,51,.85),0 -30px 44px rgba(252,181,51,.7)!important}
.chip-track-prov[data-brand="mts"]>.chip-slider{background:linear-gradient(135deg,#FF0000,#CC0000)!important;box-shadow:0 -2px 8px rgba(255,0,0,1.25),0 -8px 20px rgba(255,0,0,.95),0 -18px 28px rgba(255,0,0,.85),0 -30px 44px rgba(255,0,0,.7)!important}
.chip-track-prov[data-brand="megafon"]>.chip-slider{background:linear-gradient(135deg,#00985F,#006B42)!important;box-shadow:0 -2px 8px rgba(0,152,95,1.25),0 -8px 20px rgba(0,152,95,.95),0 -18px 28px rgba(0,152,95,.85),0 -30px 44px rgba(0,152,95,.7)!important}
.chip-track-prov[data-brand="rostelecom"]>.chip-slider{background:linear-gradient(135deg,#EC5D2F,#B8421E)!important;box-shadow:0 -2px 8px rgba(236,93,47,1.25),0 -8px 20px rgba(236,93,47,.95),0 -18px 28px rgba(236,93,47,.85),0 -30px 44px rgba(236,93,47,.7)!important}
.chip-track-prov[data-brand="t2"]>.chip-slider{background:linear-gradient(135deg,#EB4994,#B8306F)!important;box-shadow:0 -2px 8px rgba(235,73,148,1.25),0 -8px 20px rgba(235,73,148,.95),0 -18px 28px rgba(235,73,148,.85),0 -30px 44px rgba(235,73,148,.7)!important}
.chip-track-prov[data-brand="belymedved"]>.chip-slider{background:linear-gradient(135deg,#008FA8,#006B7F)!important;box-shadow:0 -2px 8px rgba(0,143,168,1.25),0 -8px 20px rgba(0,143,168,.95),0 -18px 28px rgba(0,143,168,.85),0 -30px 44px rgba(0,143,168,.7)!important}

.legend{display:inline-flex;flex-wrap:wrap;gap:10px;font-size:14px;color:var(--text-secondary)}
.legend>span{display:inline-flex;align-items:center;gap:3px}
/* Tint the legend's check-icon (a white-filled SVG used as <img>) down to
 * the same neutral grey as the surrounding text — without touching the
 * source SVG. `brightness(.45)` on pure white ≈ #737373, visually a hair
 * off but indistinguishable from `color:#7d7d87`. */
.legend .ico-sm{filter:brightness(.45)}

.vpn-row{cursor:pointer;transition:background .1s,box-shadow .15s;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation;-webkit-user-select:none;user-select:none}
.vpn-row *{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
.vpn-row.vpn-row-pressing td{background:rgba(34,197,94,.10);transition:background .15s}
/* Hover fill for VPN rows lives in the unified `tr:hover td` block above
 * so the colour can flow from the row's `edge-*` status class via
 * `--hover-bg`. A local `.vpn-row:hover td{background:#0c0b0c}` override
 * used to sit here but it pinned every VPN row to grey on hover,
 * defeating the tint. */
.expand-row td{background:var(--bg-expand)!important;padding:12px}

.ico{width:19px;height:19px;object-fit:contain;vertical-align:middle;border-radius:3px}
.ico-st{width:17px;height:17px}
th.c .ico,td.c .ico{display:block;margin:0 auto}
.ico-sm{width:16px;height:16px}
th .ico{opacity:.85}

@media(max-width:640px){
  body{font-size:15px}
  .wrap{max-width:100%}
  th{padding:8px 4px;font-size:12px}
  td{padding:8px 4px;font-size:13px}
  /* Keep a 2-3px gap between the colored stripe and the VPN name / header
   * text on mobile so the content doesn't hug the edge. */
  .tbl-vpn td.sec-name,.tbl-vpn th.sec-name{padding-left:7px}
  /* Mirror desktop but with tighter gutters and the mobile offset for
   * the label (24px = 7 sec-name pad + 12 ico-sm + 5 gap). */
  .tbl-vpn tfoot td{padding:8px 4px}
  .tbl-vpn tfoot td.sec-name{padding:8px 4px 0 7px}
  .tbl-vpn tfoot td.prov-cell{padding:8px 4px 0 0}
  .tab-btn{padding:12px 10px;font-size:15px}
  /* All top-bar wrappers share the same horizontal gutter as <main> so the
   * divider lines end exactly where the table starts. !important is needed
   * to beat inline `padding:0 16px` on nav/region-chips and
   * `padding:14px 16px 10px` on the header. */
  main{padding:8px 12px!important}
  header .wrap{padding:10px 12px 8px!important}
  nav .wrap,.region-chips .wrap{padding-left:12px!important;padding-right:12px!important}
  /* Match the narrower mobile gutter so the divider line still ends
   * where the table starts. */
  header>.wrap::after,
  nav>.wrap::after,
  .region-chips>.wrap::after{left:12px;right:12px}
  .expand-row td{padding:8px}
  /* Keep the mobile hit-area trick alive: bump chip vertical padding
   * so tap zone fills the full bar (track padding is 0 on both sides). */
  .fchip{padding:2px 6px;font-size:14px}
  .chip-track-prov>button.fchip{padding:8px 6px}
  .region-chip{padding:10px 10px;font-size:13px}
  .ico{width:16px;height:16px}
  .ico-st{width:14px;height:14px}
  .ico-sm{width:14px;height:14px}
  .st-none{min-width:14px;line-height:14px}
  /* Slash the 4-layer upward wash down to a single soft shadow on phones:
   * the desktop recipe re-rasterises four stacked blurs on every frame
   * while the slider's `width` + `transform` animate between chips, and
   * mobile GPUs choke on that workload — user-visible symptom is a
   * stuttery region-slider transition ("переключается рывками"). One
   * shadow layer keeps the "bloom of light from below" silhouette
   * without the per-frame compositing cost. Tfoot edge-* variants get
   * the same colour-tinted single-layer treatment. */
  .chip-track-bar>.chip-slider,
  .chip-track-prov>.chip-slider,
  .tbl-vpn tfoot .chip-track-prov>.chip-slider{box-shadow:0 -3px 14px rgba(197,15,53,.85)}
  .tbl-vpn tfoot .chip-track-prov.edge-ok>.chip-slider{box-shadow:0 -3px 14px rgba(34,197,94,.85)}
  .tbl-vpn tfoot .chip-track-prov.edge-half>.chip-slider{box-shadow:0 -3px 14px rgba(255,88,10,.85)}
  .tbl-vpn tfoot .chip-track-prov.edge-fail>.chip-slider{box-shadow:0 -3px 14px rgba(193,15,52,.85)}
  .chip-track-prov[data-brand="beeline"]>.chip-slider{box-shadow:0 -3px 14px rgba(252,181,51,.85)!important}
  .chip-track-prov[data-brand="mts"]>.chip-slider{box-shadow:0 -3px 14px rgba(255,0,0,.85)!important}
  .chip-track-prov[data-brand="megafon"]>.chip-slider{box-shadow:0 -3px 14px rgba(0,152,95,.85)!important}
  .chip-track-prov[data-brand="rostelecom"]>.chip-slider{box-shadow:0 -3px 14px rgba(236,93,47,.85)!important}
  .chip-track-prov[data-brand="t2"]>.chip-slider{box-shadow:0 -3px 14px rgba(235,73,148,.85)!important}
  .chip-track-prov[data-brand="belymedved"]>.chip-slider{box-shadow:0 -3px 14px rgba(0,143,168,.85)!important}
  /* VPN header on phones: force the dynamic legend (provider name /
   * connection type / last-check timestamp) to drop below the "Какой
   * VPN работает сегодня · N сервисов" caption instead of competing
   * with it for horizontal space. The desktop flex row still wraps
   * when items don't fit, but on mobile we lock the break so the
   * legend always gets its own line. `!important` is needed because
   * the wrapper carries inline `align-items:center` from its style
   * attribute — without it the column layout inherits center cross-
   * axis alignment and the caption ends up centred on mobile. */
  .vpn-hdr{flex-direction:column!important;align-items:flex-start!important;gap:4px}
  .vpn-hdr>.legend{width:100%}
  /* Proxy OVERVIEW rows: clip the name harder so the status-icon columns fit
   * and the row breathes (fade tail moves inward). */
  .proxy-name-clip{max-width:14ch;-webkit-mask-image:linear-gradient(to right,#000 0,#000 9ch,transparent 14ch);mask-image:linear-gradient(to right,#000 0,#000 9ch,transparent 14ch)}
  .tbl-proxy th.sec-name,.tbl-proxy td.sec-name{width:14ch}
  /* Nested server table (.tbl-srv): drop the fixed 22ch name column. The name
   * gets a SHORT cap and the СЕРВЕР + ПИНГ columns shrink to content; the 5
   * status-icon columns carry NO fixed width, so they share all the leftover
   * space and spread out evenly with plenty of air — no more stuck-together
   * icons, no overflow. */
  .tbl-srv{table-layout:auto;width:100%}
  .tbl-srv th:first-child,.tbl-srv td:first-child{width:1%;white-space:nowrap}
  .tbl-srv th:nth-child(2),.tbl-srv td:nth-child(2){width:1%;white-space:nowrap;padding-left:6px;padding-right:6px}
  .tbl-srv th.c,.tbl-srv td.c{text-align:center}
  .tbl-srv .proxy-name-clip{display:block;max-width:11ch;-webkit-mask-image:linear-gradient(to right,#000 0,#000 7ch,transparent 11ch);mask-image:linear-gradient(to right,#000 0,#000 7ch,transparent 11ch)}
}
/* Very narrow phones (iPhone SE / Galaxy S8 portrait): trim name columns and
 * gutters another notch so the icon grid never overflows the table frame. */
@media(max-width:380px){
  th{padding:8px 3px}
  td{padding:8px 3px}
  .ico{width:15px;height:15px}
  .proxy-name-clip{max-width:11ch;-webkit-mask-image:linear-gradient(to right,#000 0,#000 7ch,transparent 11ch);mask-image:linear-gradient(to right,#000 0,#000 7ch,transparent 11ch)}
  .tbl-proxy th.sec-name,.tbl-proxy td.sec-name{width:11ch}
  .tbl-srv .proxy-name-clip{max-width:9ch;-webkit-mask-image:linear-gradient(to right,#000 0,#000 6ch,transparent 9ch);mask-image:linear-gradient(to right,#000 0,#000 6ch,transparent 9ch)}
}

/* Modal overlay: semi-opaque backdrop with blur. Clicking the backdrop
 * closes the modal (see `_modalBackdropClick`). The inner card keeps its
 * own background and lives in the markup so we only style the chrome
 * here. `.hidden` is the on/off switch. */
.modal-bg{background:var(--bg-modal);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.modal-bg.hidden{display:none!important}

/* Preloader */
/* ── Заставка (#preloader, .pl-*) ────────────────────────────────────────
   Её CSS живёт НЕ ЗДЕСЬ, а критическим блоком в <head> index.html. Причина
   измеренная, а не стилистическая: этот файл блокирует рендер, и пока он не
   приехал, у заставки нет ни размеров, ни раскладки — показать логотип она
   не может. Правьте там; дублировать сюда нельзя, иначе два источника правды
   разъедутся на первой же правке. */

/* Subscribe widget icon pulse */
@keyframes subPulse{0%,100%{filter:drop-shadow(0 0 4px rgba(197,15,53,.4))}50%{filter:drop-shadow(0 0 12px rgba(197,15,53,.7)) drop-shadow(0 0 24px rgba(197,15,53,.4))}}

/* ── Theme toggle button ── */
.theme-btn{background:none;border:1px solid var(--border-chip);border-radius:8px;cursor:pointer;padding:4px;display:inline-flex;align-items:center;justify-content:center;color:var(--text-secondary);transition:color .2s,border-color .2s}
.theme-btn:hover{color:var(--text-hover);border-color:var(--text-secondary)}
.theme-btn svg{width:18px;height:18px}
/* Переключатель языка (.theme-btn.lang-btn, текст «RU»/«EN» вместо svg): без явного
   размера он был 30×24 против 28×28 у соседей. Квадрат задаётся числом на каждом
   брейкпоинте рядом с размером svg; padding:0 — иначе текст не влезает в бокс. */
.lang-btn{width:28px;height:28px;padding:0!important;font-size:12px}
.theme-ico-moon,.theme-ico-sun{display:none}
:root .theme-ico-moon{display:block}
[data-theme="light"] .theme-ico-moon{display:none}
[data-theme="light"] .theme-ico-sun{display:block}

/* ── Light-theme overrides for inline styles ──
   Inline `color:` and `background:` in JS templates can't use var()
   without touching every template string. These selectors catch the
   most frequent dark-only colours and swap them. */
/* Light theme uses its own SVG with white fill + warm rose lines */
[data-theme="light"] .tab-btn.tab-active{color:#fff}
[data-theme="light"] .region-chip-active,[data-theme="light"] .region-chip-active:hover{color:var(--text-primary)}
[data-theme="light"] .badge-ok{background:#dcfce7;color:#16a34a}
[data-theme="light"] .badge-fail{background:#fee2e2;color:#dc2626}
[data-theme="light"] .badge-info{background:#dbeafe;color:#2563eb}
[data-theme="light"] .legend .ico-sm{filter:brightness(.55)}
/* White SVG icons need darkening on light bg */
[data-theme="light"] th.sec-meta .ico{filter:invert(1) brightness(.3)}
[data-theme="light"] #health-indicator .ico-sm,[data-theme="light"] #snapshot-indicator .ico-sm{filter:invert(1) brightness(.3)}
[data-theme="light"] #modal>div>div:last-child{background:var(--glass-grain),linear-gradient(135deg,var(--glass-sheen),transparent 38%),var(--glass-fill)!important;border-color:var(--glass-border)!important}
#vpn-modal-stats-body svg{width:100%;max-width:100%;height:96px;display:block}
[data-theme="light"] .proxy-name-clip{color:var(--text-bright)!important}
[data-theme="light"] .expand-row td div{border-color:var(--border-line)!important}
/* Header SVG logo: white fill → dark on light bg */
[data-theme="light"] header svg path[fill="white"]{fill:var(--text-primary)}
[data-theme="light"] header svg g path{fill:var(--text-primary)}
/* Responsive header brand: the wordmark scales by height (width:auto keeps
 * aspect) so it never overruns the action buttons on narrow portrait phones,
 * and the 4 icon buttons shrink a notch on small screens to free up room. */
.brand-wordmark{height:16px;width:auto;display:block;flex-shrink:0}
/* Brand cell can shrink (min-width:0) and clips the wordmark rather than
   letting it overrun the favicon / push the action buttons on tiny screens. */
.sticky-hdr header>.wrap>div:first-child{min-width:0;overflow:hidden}
@media (max-width:480px){.brand-wordmark{height:15px}}
@media (max-width:430px){.brand-wordmark{height:14px}}
/* Tighten the whole header on small phones — favicon, wordmark, the gaps AND
   the icon buttons all step down so nothing feels oversized on an iPhone SE.
   Favicon size + the inner gaps are inline styles, so they need !important. */
@media (max-width:400px){
  .sticky-hdr header>.wrap>div{gap:6px!important}
  .sticky-hdr header .wrap>div:first-child>img{width:22px!important;height:22px!important}
  .brand-wordmark{height:12px}
  .theme-btn,.rating-btn,.help-btn{padding:3px!important}
  .theme-btn svg,.rating-btn svg,.help-btn svg{width:15px!important;height:15px!important}
  .lang-btn{width:23px;height:23px;padding:0!important;font-size:11px}
}
@media (max-width:368px){
  .sticky-hdr header .wrap>div:first-child>img{width:21px!important;height:21px!important}
  .brand-wordmark{height:11px}
  .theme-btn,.rating-btn,.help-btn{padding:2px!important}
  .theme-btn svg,.rating-btn svg,.help-btn svg{width:14px!important;height:14px!important}
  .lang-btn{width:20px;height:20px;padding:0!important;font-size:10px}
}
@media (max-width:340px){
  .sticky-hdr header>.wrap>div{gap:5px!important}
  .sticky-hdr header .wrap>div:first-child>img{width:19px!important;height:19px!important}
  .brand-wordmark{height:10px}
  .theme-btn svg,.rating-btn svg,.help-btn svg{width:13px!important;height:13px!important}
  .lang-btn{width:19px;height:19px;padding:0!important;font-size:10px}
}
[data-theme="light"] .chip-track-prov>button.fchip:not(.f-ok){filter:saturate(.55) brightness(1.1)}
/* Slider glows — punchy on light backgrounds */
[data-theme="light"] .tab-slider{box-shadow:0 4px 24px -2px rgba(197,15,53,.65)}
[data-theme="light"] .chip-track-bar>.chip-slider{box-shadow:0 -2px 10px rgba(197,15,53,.9),0 -8px 22px rgba(197,15,53,.6)}
[data-theme="light"] .chip-track-prov>.chip-slider{box-shadow:0 -2px 10px rgba(197,15,53,.9),0 -8px 22px rgba(197,15,53,.6)}
[data-theme="light"] .tbl-vpn tfoot .chip-track-prov>.chip-slider{box-shadow:0 -2px 10px rgba(197,15,53,.9),0 -10px 28px rgba(197,15,53,.55)}
[data-theme="light"] .tbl-vpn tfoot .chip-track-prov.edge-ok>.chip-slider{box-shadow:0 -2px 10px rgba(34,197,94,.9),0 -10px 28px rgba(34,197,94,.55)}
[data-theme="light"] .tbl-vpn tfoot .chip-track-prov.edge-half>.chip-slider{box-shadow:0 -2px 10px rgba(255,88,10,.9),0 -10px 28px rgba(255,88,10,.55)}
[data-theme="light"] .tbl-vpn tfoot .chip-track-prov.edge-fail>.chip-slider{box-shadow:0 -2px 10px rgba(193,15,52,.9),0 -10px 28px rgba(193,15,52,.55)}
/* Brand slider glows — upward on light bg */
[data-theme="light"] .chip-track-prov[data-brand="beeline"]>.chip-slider{box-shadow:0 -2px 10px rgba(252,181,51,.9),0 -8px 22px rgba(252,181,51,.6)!important}
[data-theme="light"] .chip-track-prov[data-brand="mts"]>.chip-slider{box-shadow:0 -2px 10px rgba(255,0,0,.9),0 -8px 22px rgba(255,0,0,.6)!important}
[data-theme="light"] .chip-track-prov[data-brand="megafon"]>.chip-slider{box-shadow:0 -2px 10px rgba(0,152,95,.9),0 -8px 22px rgba(0,152,95,.6)!important}
[data-theme="light"] .chip-track-prov[data-brand="rostelecom"]>.chip-slider{box-shadow:0 -2px 10px rgba(236,93,47,.9),0 -8px 22px rgba(236,93,47,.6)!important}
[data-theme="light"] .chip-track-prov[data-brand="t2"]>.chip-slider{box-shadow:0 -2px 10px rgba(235,73,148,.9),0 -8px 22px rgba(235,73,148,.6)!important}
[data-theme="light"] .chip-track-prov[data-brand="belymedved"]>.chip-slider{box-shadow:0 -2px 10px rgba(0,143,168,.9),0 -8px 22px rgba(0,143,168,.6)!important}
/* Table wrapper: subtle shadow instead of harsh ring */
[data-theme="light"] .tbl{box-shadow:0 1px 3px rgba(0,0,0,.08),0 0 0 1px var(--border-table)}
[data-theme="light"] .tbl:has(> .tbl-vpn){padding-bottom:1px;box-shadow:0 1px 3px rgba(0,0,0,.08),inset 0 0 0 1px var(--border-table)}
/* Hover tints — slightly stronger on light so they're visible */
[data-theme="light"] tr{--hover-bg:#f0ebe6}
[data-theme="light"] tr.edge-ok{--hover-bg:rgba(34,197,94,.08)}
[data-theme="light"] tr.edge-half{--hover-bg:rgba(255,88,10,.08)}
[data-theme="light"] tr.edge-fail{--hover-bg:rgba(239,68,68,.08)}
/* SVG logo text — make dark on light bg */
[data-theme="light"] header svg path[fill="white"]{fill:var(--text-primary)}

/* ── Rating mode (admin parity) ── */
.rating-btn{background:none;border:1px solid var(--border-chip);border-radius:8px;cursor:pointer;padding:4px;display:inline-flex;align-items:center;justify-content:center;color:var(--text-secondary);transition:color .2s,border-color .2s,background .25s,box-shadow .25s}
.rating-btn:hover{color:var(--text-hover);border-color:var(--text-secondary)}
.rating-btn svg{width:18px;height:18px;display:block}
.rating-btn.active{background:linear-gradient(135deg,#C50F35,#690B1F);color:#fff;border-color:transparent;box-shadow:0 2px 10px -2px rgba(197,15,53,.55)}
.rating-btn.active:hover{border-color:transparent;color:#fff}
.rating-tab-chips{display:flex;align-items:stretch;flex-shrink:0;padding:0}
.rating-tab-chips .chip-track-bar{padding:0;align-items:flex-end;height:100%}
.rating-tab-chips .region-chip{padding:10px 10px 14px;font-size:14px;font-weight:500}
@media (max-width:540px){
  .rating-tab-chips .region-chip{padding:8px 8px 12px;font-size:13px}
}
.rating-fade{animation:rating-fade .35s ease-out}
@keyframes rating-fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
.rating-skel{background:linear-gradient(90deg,var(--bg-card) 0%,var(--hover-default,rgba(255,255,255,.06)) 50%,var(--bg-card) 100%);background-size:200% 100%;animation:rating-skel 1.4s ease-in-out infinite;border-radius:6px}
@keyframes rating-skel{0%{background-position:200% 0}100%{background-position:-200% 0}}
.pick-modal-block{margin-bottom:16px}
.pick-modal-block h4{font-size:13px;color:var(--text-bright);margin-bottom:8px;font-weight:600}
.pick-modal-options{display:flex;flex-wrap:wrap;gap:6px}
.pick-opt{background:var(--bg-card);border:1px solid var(--border-line);color:var(--text-bright);padding:8px 12px;border-radius:8px;cursor:pointer;font-size:13px;transition:background .2s,border-color .2s}
.pick-opt:hover{border-color:var(--text-secondary)}
.pick-opt.selected{background:linear-gradient(135deg,#C50F35,#690B1F);color:#fff;border-color:transparent}
.pick-modal-cta{margin-top:18px;width:100%;background:linear-gradient(135deg,#C50F35,#690B1F);color:#fff;border:none;border-radius:8px;padding:12px;font-size:14px;font-weight:700;cursor:pointer;letter-spacing:.04em;text-transform:uppercase}
.rating-section-h{font-size:11px;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.06em;margin:16px 0 6px}
.rating-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin:12px 0}
.rating-stats-cell{background:var(--bg-card);border:1px solid var(--border-line);border-radius:8px;padding:10px 12px}
.rating-stats-cell .rs-label{font-size:10px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.04em;margin-bottom:4px}
.rating-stats-cell .rs-value{font-size:18px;color:var(--text-bright);font-weight:700;line-height:1.1;font-variant-numeric:tabular-nums}
.rating-stats-cell .rs-sub{font-size:11px;color:var(--text-secondary);margin-top:2px}
.rating-prov-bar{display:grid;grid-template-columns:80px 1fr 50px 60px;align-items:center;gap:8px;padding:5px 0;font-size:12px;color:var(--text-bright)}
.rpb-bar{height:8px;border-radius:4px;background:var(--bg-card);overflow:hidden;position:relative;border:1px solid var(--border-line)}
.rpb-fill{height:100%;background:linear-gradient(90deg,#22c55e,#15803d);border-radius:4px;transition:width .35s ease}
.rpb-fill.f-warn{background:linear-gradient(90deg,#eab308,#a16207)}
.rpb-fill.f-fail{background:linear-gradient(90deg,#ef4444,#991b1b)}
/* Availability bar loading state (modal opens instantly, uptime fills async). */
.rpb-fill.vpn-uptime-loading{background:var(--border-line);animation:vpnUptimePulse 1.1s ease-in-out infinite}
@keyframes vpnUptimePulse{0%,100%{opacity:.3}50%{opacity:.65}}
.rating-prov-bar .rpb-pct{font-size:11px;color:var(--text-bright);text-align:right;font-variant-numeric:tabular-nums}
.rating-prov-bar .rpb-lat{font-size:11px;color:var(--text-secondary);text-align:right;font-variant-numeric:tabular-nums}
@media (max-width:400px){
  .rating-prov-bar{grid-template-columns:64px 1fr 42px 52px;font-size:11px;gap:6px}
}
.rc{padding:6px 4px;text-align:center;vertical-align:middle;font-variant-numeric:tabular-nums}
.rc-th{font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.04em;font-weight:600;padding:11px 4px;text-align:center}
.rc-th-score,.rc-score{width:58px}
.rc-th-best,.rc-th-avg,.rc-best,.rc-avg{width:78px}
.rc-th-stab,.rc-stab{min-width:160px}
.rc-score-pill{display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:22px;padding:0 8px;border-radius:6px;font-size:12px;font-weight:700;background:var(--bg-card);color:var(--text-bright);border:1px solid var(--border-line)}
.rc-score-pill.s-top{background:linear-gradient(135deg,#22c55e,#15803d);color:#fff;border-color:transparent}
.rc-score-pill.s-mid{background:linear-gradient(135deg,#eab308,#a16207);color:#fff;border-color:transparent}
.rc-score-pill.s-low{background:linear-gradient(135deg,#ef4444,#991b1b);color:#fff;border-color:transparent}
.rc-stab-wrap{display:flex;align-items:center;gap:8px;justify-content:center}
.rc-spark{flex:1 1 auto;display:flex;align-items:center;min-width:80px;max-width:260px}
.rc-spark svg{display:block;width:100%;height:38px}
.rc-pct{flex:0 0 auto;font-weight:700;font-size:13px;color:var(--text-bright);min-width:36px;text-align:right}
.rc-pct.r-ok{color:var(--status-ok,#22c55e)}
.rc-pct.r-warn{color:var(--status-warn,#f59e0b)}
.rc-pct.r-fail{color:var(--status-fail,#ef4444)}
.rc-best-v{font-weight:600;color:var(--status-ok,#22c55e);font-size:12px;white-space:nowrap}
.rc-avg-v{font-weight:500;color:var(--text-bright);font-size:12px;white-space:nowrap}
.rc-empty{padding:6px 12px;text-align:center}
.rc-na{font-size:11px;color:var(--text-disabled);font-style:italic;opacity:.7}
@media (max-width:720px){
  .rc-spark svg{height:28px}
  .rc-th-stab,.rc-stab{min-width:120px}
  .rc-pct{font-size:12px}
}
@media (max-width:480px){
  .tbl{overflow-x:hidden}
  .tbl-vpn,.tbl-proxy-rating{table-layout:auto;width:100%}
  .rc{padding:4px 2px}
  .rc-th{padding:8px 2px;font-size:9px;letter-spacing:.02em}
  .rc-th-score,.rc-score{width:36px}
  .rc-th-best,.rc-th-avg,.rc-best,.rc-avg{width:42px}
  .rc-th-stab,.rc-stab{min-width:64px}
  .rc-stab-wrap{gap:3px}
  .rc-spark{min-width:32px;max-width:80px}
  .rc-spark svg{height:22px}
  .rc-pct{font-size:10px;min-width:24px}
  .rc-best-v,.rc-avg-v{font-size:10px}
  .rc-score-pill{min-width:24px;height:18px;font-size:10px;padding:0 4px}
}
.tbl-proxy-rating td.rc,.tbl-proxy-rating th.rc{padding:2px 3px;vertical-align:middle;line-height:1}
.tbl-proxy-rating td.sec-name,.tbl-proxy-rating th.sec-name{padding-top:6px;padding-bottom:6px}
.tbl-proxy-rating .rc-stab-wrap{align-items:center;gap:4px;min-height:0}
.tbl-proxy-rating .rc-spark{min-width:48px;max-width:120px;flex:0 1 auto}
.tbl-proxy-rating .rc-spark svg{height:17px!important;width:auto!important;max-width:100%;display:block}
.tbl-proxy-rating .rc-pct{font-size:11px;line-height:1;min-width:26px}
.tbl-proxy-rating .rc-score-pill{height:17px;min-width:24px;padding:0 5px;font-size:10px;line-height:1;border-radius:4px}
.tbl-proxy-rating .rc-best-v,.tbl-proxy-rating .rc-avg-v{font-size:11px;line-height:1}
.tbl-proxy-rating .rc-th{font-size:10px;padding:8px 2px;line-height:1;letter-spacing:.03em}
.tbl-proxy-rating .rc-th-stab,.tbl-proxy-rating .rc-stab{min-width:100px}
.tbl-proxy-rating .rc-th-score,.tbl-proxy-rating .rc-score{width:44px}
.tbl-proxy-rating .rc-th-best,.tbl-proxy-rating .rc-th-avg,.tbl-proxy-rating .rc-best,.tbl-proxy-rating .rc-avg{width:64px}
.tbl-proxy-rating .rc-empty{padding:2px 6px}
.tbl-proxy-rating .rc-na{font-size:10px}
@media (max-width:720px){
  .tbl-proxy-rating .rc-spark svg{height:15px!important}
  .tbl-proxy-rating .rc-th-stab,.tbl-proxy-rating .rc-stab{min-width:72px}
}
@media (max-width:480px){
  .tbl-proxy-rating .rc-th{font-size:9px;padding:6px 1px}
  .tbl-proxy-rating .rc-th-score,.tbl-proxy-rating .rc-score{width:30px}
  .tbl-proxy-rating .rc-th-best,.tbl-proxy-rating .rc-th-avg,.tbl-proxy-rating .rc-best,.tbl-proxy-rating .rc-avg{width:36px}
  .tbl-proxy-rating .rc-th-stab,.tbl-proxy-rating .rc-stab{min-width:48px}
  .tbl-proxy-rating .rc-spark{min-width:24px;max-width:64px}
  .tbl-proxy-rating .rc-pct{font-size:9px;min-width:20px}
  .tbl-proxy-rating .rc-best-v,.tbl-proxy-rating .rc-avg-v{font-size:9px}
  .tbl-proxy-rating .rc-score-pill{min-width:20px;height:16px;font-size:9px;padding:0 3px}
}
/* SCORE column is dropped on every phone (kept on desktop). */
@media (max-width:640px){
  .rc-th-score,.rc-score{display:none}
}
/* The uptime sparkline is kept ONLY on the largest phones (iPhone Pro Max,
 * 430px) and desktop. Below 430px the 220px graph crowds the row, so drop it
 * and show just the uptime % — centered in its column, clear of TOP/AVG.
 * These rules sit after EVERY base/responsive .rc-* rule (incl. the
 * .tbl-proxy-rating overrides) so they win on source order without
 * !important. */
@media (max-width:429px){
  .rc-spark,.tbl-proxy-rating .rc-spark{display:none}
  .rc-th-stab,.rc-stab,
  .tbl-proxy-rating .rc-th-stab,.tbl-proxy-rating .rc-stab{min-width:0}
  .rc-stab-wrap{gap:0;justify-content:center}
  .rc-pct,.tbl-proxy-rating .rc-pct{min-width:0;text-align:center;font-size:12px}
}

/* ── data-tip (tap-tooltip) ── */
[data-tip]{cursor:help;-webkit-tap-highlight-color:transparent}
.data-tip-pop{position:absolute;z-index:60;background:#0c0a0d;color:var(--text-bright);border:1px solid var(--border-line);border-radius:8px;padding:8px 10px;font-size:12px;font-weight:500;line-height:1.35;letter-spacing:.01em;text-transform:none;box-shadow:0 8px 28px -6px rgba(0,0,0,.6),0 4px 12px -4px rgba(0,0,0,.4);pointer-events:none;animation:dataTipIn .15s ease-out}
[data-theme="light"] .data-tip-pop{background:#ffffff;color:#1a1412;border-color:#ddd6d0;box-shadow:0 8px 28px -6px rgba(0,0,0,.18),0 4px 12px -4px rgba(0,0,0,.1)}
.data-tip-pop::after{content:"";position:absolute;left:var(--tip-arrow-x,16px);transform:translateX(-50%);width:10px;height:10px;background:inherit;border:1px solid var(--border-line);border-top:0;border-left:0;rotate:45deg}
[data-theme="light"] .data-tip-pop::after{border-color:#ddd6d0}
.data-tip-pop[data-placement="top"]::after{bottom:-6px}
.data-tip-pop[data-placement="bottom"]::after{top:-6px;rotate:-135deg}
@keyframes dataTipIn{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}

/* ── Help button (header) ── */
.help-btn{background:none;border:1px solid var(--border-chip);border-radius:8px;cursor:pointer;padding:4px;display:inline-flex;align-items:center;justify-content:center;color:var(--text-secondary);transition:color .2s,border-color .2s,background .2s,box-shadow .25s;position:relative}
.help-btn:hover{color:var(--text-hover);border-color:var(--text-secondary)}
.help-btn svg{width:18px;height:18px;display:block}
.help-btn .help-ico-close{display:none}
.help-btn.tour-active{background:linear-gradient(135deg,#C50F35,#690B1F);color:#fff;border-color:transparent;box-shadow:0 2px 10px -2px rgba(197,15,53,.55);z-index:90}
.help-btn.tour-active .help-ico-q{display:none}
.help-btn.tour-active .help-ico-close{display:block}

/* ── Tour overlay ── */
html.tour-on{overflow:hidden}
.tour-backdrop{position:fixed;inset:0;z-index:80;background:transparent;cursor:pointer}
.tour-spot{position:absolute;z-index:81;border-radius:8px;box-shadow:0 0 0 4px rgba(197,15,53,.55),0 0 0 9999px rgba(0,0,0,.62);pointer-events:auto;cursor:pointer;transition:top .2s ease,left .2s ease,width .2s ease,height .2s ease;animation:tourPulse 2s ease-in-out infinite}
[data-theme="light"] .tour-spot{box-shadow:0 0 0 4px rgba(197,15,53,.5),0 0 0 9999px rgba(0,0,0,.45)}
@keyframes tourPulse{0%,100%{box-shadow:0 0 0 4px rgba(197,15,53,.55),0 0 0 9999px rgba(0,0,0,.62)}50%{box-shadow:0 0 0 6px rgba(197,15,53,.7),0 0 0 9999px rgba(0,0,0,.62)}}
.tour-card{position:absolute;z-index:82;background:var(--bg-card);color:var(--text-bright);border:1px solid var(--border-card);border-radius:12px;padding:14px 16px 12px;box-shadow:0 12px 36px -8px rgba(0,0,0,.7),0 4px 16px -4px rgba(0,0,0,.45);max-width:320px;width:calc(100vw - 32px);cursor:pointer;animation:tourCardIn .22s ease-out;transition:top .22s ease,left .22s ease}
[data-theme="light"] .tour-card{box-shadow:0 12px 36px -8px rgba(0,0,0,.18),0 4px 16px -4px rgba(0,0,0,.1)}
.tour-card-title{font-size:16px;font-weight:700;color:var(--text-white);margin-bottom:6px;letter-spacing:.02em}
.tour-card-body{font-size:15px;color:var(--text-primary);line-height:1.45}
.tour-card-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px;font-size:13px;color:var(--text-secondary);font-weight:600;text-transform:uppercase;letter-spacing:.06em}
.tour-card-progress{flex:0 0 auto}
.tour-card-hint{display:inline-flex;align-items:center;gap:8px;font-size:12px;color:var(--text-bright);text-transform:none;letter-spacing:.02em;font-weight:500;line-height:1.25;text-align:right}
.tour-card-hint-text{display:inline-block;text-align:right}
.tour-tap-pulse{position:relative;display:inline-flex;width:18px;height:18px;flex:0 0 auto;align-items:center;justify-content:center}
.tour-tap-pulse::before,.tour-tap-pulse::after{content:"";position:absolute;border:2px solid #C50F35;border-radius:50%;animation:tourTap 1.4s ease-out infinite;opacity:0}
.tour-tap-pulse::before{width:18px;height:18px}
.tour-tap-pulse::after{width:18px;height:18px;animation-delay:.7s}
.tour-tap-pulse>span{width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#C50F35,#690B1F);box-shadow:0 0 0 2px rgba(255,255,255,.18)}
@keyframes tourTap{0%{transform:scale(.4);opacity:.9}80%{opacity:0}100%{transform:scale(1.6);opacity:0}}
.tour-dots{position:fixed;left:50%;bottom:max(18px,env(safe-area-inset-bottom,0));transform:translateX(-50%);z-index:82;display:flex;gap:6px;background:rgba(12,10,13,.78);padding:8px 12px;border-radius:999px;border:1px solid var(--border-line);cursor:default}
[data-theme="light"] .tour-dots{background:rgba(255,255,255,.85)}
.tour-dot{width:8px;height:8px;border-radius:999px;background:var(--text-faint);transition:background .2s,transform .2s;cursor:pointer}
.tour-dot.done{background:var(--text-secondary)}
.tour-dot.active{background:linear-gradient(135deg,#C50F35,#690B1F);transform:scale(1.25)}
@keyframes tourCardIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
@media (max-width:480px){
  .tour-card{padding:12px 14px 10px;width:calc(100vw - 24px);max-width:340px}
  .tour-card-title{font-size:15px}
  .tour-card-body{font-size:14px}
  .tour-card-meta{font-size:12px}
  .tour-card-hint{font-size:11px}
}

/* Narrow-viewport guards for system zoom (iOS Display Zoom, Android
 * Display size 125%+, Windows DPI 125% in Telegram WebView2). These
 * limit the worst overflow / overlap; full clamp()-based fluid sizing
 * is a larger rework. */
@media (max-width:360px){
  body{font-size:14px}
  .tab-btn{font-size:14px!important;padding:6px 10px!important}
  header .wrap{padding:10px 12px!important}
  /* Squeeze the data table so its 9 columns (name + 5 status icons + 3 meta)
   * fit a 320–360px frame WITHOUT horizontal clipping. Clipping/reflow is what
   * pushes the measured status overlay out of place ("полоски уезжают вниз"). */
  main{padding:8px 8px!important}
  th,td{padding:7px 2px}
  .tbl-vpn td.sec-name,.tbl-vpn th.sec-name{padding-left:5px}
  .ico{width:14px;height:14px}
  .ico-st{width:13px;height:13px}
  .proxy-name-clip{max-width:9ch;-webkit-mask-image:linear-gradient(to right,#000 0,#000 6ch,transparent 9ch);mask-image:linear-gradient(to right,#000 0,#000 6ch,transparent 9ch)}
  .tbl-proxy th.sec-name,.tbl-proxy td.sec-name{width:9ch}
  .tbl-srv .proxy-name-clip{max-width:8ch;-webkit-mask-image:linear-gradient(to right,#000 0,#000 5ch,transparent 8ch);mask-image:linear-gradient(to right,#000 0,#000 5ch,transparent 8ch)}
}
@media (max-width:320px){
  body{font-size:13px}
  th,td{padding:6px 1px}
  .ico{width:13px;height:13px}
  .ico-st{width:12px;height:12px}
  .proxy-name-clip{max-width:8ch}
  .tbl-srv .proxy-name-clip{max-width:7ch}
}

/* ============================================================
   Quantum Glass — matte (frosted) panels
   Surface material ported from the "Ateo Digital Quantum Glass" theme.
   This file is the shared source of truth: central-server/app/api/web.py
   inlines it into the admin dashboard, so the tokens + bloom + grain below
   reach admin for free (admin-only card classes are frosted in
   dashboard.html). The card panel (.tour-card), the modal content box and
   the data table (.tbl + translucent surface tints) are frosted here.
   Page background, typography and colors are otherwise unchanged.
   ============================================================ */
:root{
  --glass-fill:rgba(20,21,27,.55);
  --glass-border:rgba(255,255,255,.10);
  --glass-border-strong:rgba(255,255,255,.16);
  --glass-sheen:rgba(255,255,255,.07);
  --glass-blur:10px;
  --glass-shadow:0 10px 40px -16px rgba(0,0,0,.85), 0 2px 8px -4px rgba(0,0,0,.6);
  --bg-bloom-a:rgba(197,15,53,.20);
  --bg-bloom-b:rgba(40,12,60,.1);
  --grain-op:.05;
}
[data-theme="light"]{
  --glass-fill:rgba(255,255,255,.72);
  --glass-border:rgba(255,255,255,.9);
  --glass-border-strong:rgba(0,0,0,.08);
  --glass-sheen:rgba(255,255,255,.95);
  --glass-shadow:0 18px 48px -24px rgba(20,28,55,.32), 0 4px 12px -6px rgba(20,28,55,.1);
  --bg-bloom-a:rgba(197,15,53,.07);
  --bg-bloom-b:rgba(70,90,160,.06);
  --grain-op:.025;
}
/* page background moved off the body element onto a fixed ::before (z-index:-2)
   so the negative-z bloom below is not occluded by an opaque body box.
   (Matches the dozor/ateo pattern; required for the bloom to be visible here.) */
body::before{
  content:"";position:fixed;inset:0;z-index:-2;pointer-events:none;
  opacity:.25;                      /* яркость паттерна фона (тёмный остаётся чёрным) */
  background-color:var(--bg-page);
  background-image:linear-gradient(var(--bg-page-overlay),var(--bg-page-overlay)),var(--bg-url);
  background-position:center;background-size:cover;background-repeat:no-repeat;
}
/* colored bloom — above the page background, below content.
   Spread across the viewport with a large central glow (not just a corner). */
body::after{
  content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;opacity:.65;
  background:
    radial-gradient(80% 70% at 22% 6%,var(--bg-bloom-a),transparent 72%),
    radial-gradient(70% 62% at 82% 16%,var(--bg-bloom-b),transparent 72%),
    radial-gradient(115% 100% at 46% 46%,var(--bg-bloom-a),transparent 78%);
}
/* fine grain — matte top overlay (below modals/tour cards at higher z) */
html::after{
  content:"";position:fixed;inset:0;z-index:60;pointer-events:none;
  opacity:var(--grain-op);mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.5' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* frosted panel material (geometry/radius left untouched) */
.tour-card{
  background:var(--glass-grain),linear-gradient(135deg,var(--glass-sheen),transparent 38%),var(--glass-fill);
  border:1px solid var(--glass-border);
  box-shadow:var(--glass-shadow),inset 0 1px 0 var(--glass-sheen);
}
/* frost the modal content box (its bg is set inline in index.html → !important).
   The .modal-bg overlay keeps its own blur; this frosts the panel itself. */
#modal>div>div:last-child{
  background:var(--glass-grain),linear-gradient(135deg,var(--glass-sheen),transparent 38%),var(--glass-fill)!important;
  border:1px solid var(--glass-border)!important;
  box-shadow:var(--glass-shadow),inset 0 1px 0 var(--glass-sheen)!important;
}

/* === FC table edge overlay (lifts status accent + provider slider out of
   the .tbl frosted frame, which clips its own children at the padding box).
   `.tbl-ovl` is a non-clipped sibling layer above `.tbl`; JS (_syncTblOverlay)
   measures each edge-row + the active provider chip and paints 1px/2px bars
   that straddle the frame border so they read as sitting ON the grань. === */
.tbl-frame{position:relative}
.tbl-ovl{position:absolute;inset:0;z-index:6;pointer-events:none;overflow:visible}
.tbl-ovl>.ovl-edges{position:absolute;inset:0;pointer-events:none}
.ovl-edge{position:absolute;left:0;width:1px;pointer-events:none}
.ovl-edge.e-ok{background:linear-gradient(180deg,#33783E,#07593E)}
.ovl-edge.e-half{background:linear-gradient(180deg,#DE3513,#FF580A)}
.ovl-edge.e-fail{background:linear-gradient(180deg,#6E0C20,#C10F34)}
/* Light theme's table frame sits 1px differently — nudge the status bars
   right so they stay on the border line. */
[data-theme="light"] .ovl-edge{left:1px}
.ovl-slider{position:absolute;left:0;height:2px;bottom:0;border-radius:2px 2px 0 0;pointer-events:none;opacity:0;
  transition:transform .38s cubic-bezier(.32,.72,0,1),width .38s cubic-bezier(.32,.72,0,1),opacity .25s ease;will-change:transform,width}
.ovl-slider.no-anim{transition:none}
/* When the overlay drives these, suppress the in-table originals so we don't
   double up (the overlay copies sit 1px outward, over the frame border). */
.tbl-frame.has-ovl td.sec-name.edge-ok::before,
.tbl-frame.has-ovl td.sec-name.edge-half::before,
.tbl-frame.has-ovl td.sec-name.edge-fail::before{display:none}
.tbl-frame.has-ovl .chip-track-prov>.chip-slider{opacity:0!important}
/* On phones the JS-measured overlay status stripes mis-position (they escaped
   to the viewport's left edge and slid down the page). Drop the lifted copies
   and restore the in-table ::before stripes — those render at the correct cell
   edge with zero measurement. The provider slider (.ovl-slider) is untouched.
   Placed after the suppression rule above so it wins on source order. */
@media (max-width:640px){
  .tbl-ovl>.ovl-edges{display:none}
  .tbl-frame.has-ovl td.sec-name.edge-ok::before,
  .tbl-frame.has-ovl td.sec-name.edge-half::before,
  .tbl-frame.has-ovl td.sec-name.edge-fail::before{display:block}
}
/* ── Cross-promo: other Ateo tools + our projects (compact cards) ── */
/* CLS: резервируем место под таблицу, пока её нет. #content пуст в разметке и
   наполняется из JS — он и есть источник сдвига: всё, что ниже (intro с
   единственным H1, кросс-промо, футер), сталкивается вниз, когда он вырастает.
   Резерв в один экран уводит нижние блоки под сгиб, а CLS считает только
   сдвиги В ПРЕДЕЛАХ вьюпорта — поэтому угадывать высоту таблицы не нужно, и
   она заведомо выше экрана (~1700px). Снимается тем же app-ready, так что на
   коротких вкладках пустой дыры не остаётся.

   Раньше здесь стоял `body:not(.app-ready){display:none}` на .xp-section,
   .fc-intro и footer. Это резервировало ноль и просто прятало контент: до
   app-ready у страницы не было ни H1, ни единой исходящей ссылки, а app-ready
   зависит от успешного ответа API — то есть весь SEO-контент страницы был
   лотереей на терпение краулера. Внешний аудит поймал проигрышный билет
   («Page has no outgoing links», «Text to code ratio < 10%»), 2026-08-10. */
/* Резерв высоты под таблицу — ТОЛЬКО пока её нет.
   Безусловный min-height пробовали 10.08.2026 и откатили по живому телефону:
   `.tbl-ovl` — абсолютный слой поверх таблицы (в нём живёт нижняя строка
   «ПРОВАЙДЕР»), его геометрию JS синхронизирует с контейнером. Раздутый
   контейнер растягивал оверлей, и он ложился поверх блока промокодов.
   Резерв держится, пока #content пуст ИЛИ пока в нём короткое состояние —
   ошибка загрузки, «Нет данных по прокси». Без второго условия он схлопывался
   на медленном API и давал CLS 0.332: коротких путей четыре, и свой catch
   есть не только в loadTab, но и внутри renderVpnTab. */
#content:empty,
#content.load-error{min-height:100svh}
.xp-section{width:100%;max-width:860px;margin:44px auto 0;padding:0 16px}
.xp-section>h2{font-size:18px;font-weight:700;letter-spacing:-.01em;margin:0 0 14px;color:var(--text-bright)}
.xp-tools{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
.xp-tool{display:block;padding:14px 16px;border-radius:11px;text-decoration:none;color:inherit;background:var(--glass-fill);border:1px solid var(--glass-border);box-shadow:var(--glass-shadow),inset 0 1px 0 var(--glass-sheen);transition:transform .12s,border-color .12s}
.xp-tool:hover{transform:translateY(-2px);border-color:#C50F35}
.xp-tool__name{display:flex;align-items:center;gap:9px;font-weight:600;font-size:15px;color:var(--text-bright)}
.xp-tool__fav{width:22px;height:22px;border-radius:6px;flex-shrink:0;display:block}
.xp-tool__desc{font-size:12.5px;line-height:1.45;color:var(--text-secondary);margin-top:4px}
.xp-tool__cta{display:inline-block;margin-top:8px;font-size:12.5px;font-weight:600;color:#C50F35}
.xp-projects{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:10px}
.xp-project{display:block;padding:12px 16px;border-radius:10px;text-decoration:none;color:inherit;background:var(--glass-fill);border:1px solid var(--glass-border);box-shadow:var(--glass-shadow),inset 0 1px 0 var(--glass-sheen);transition:transform .12s,border-color .12s}
.xp-project:hover{transform:translateY(-2px);border-color:#C50F35}
.xp-project__name{font-weight:600;font-size:14px;color:var(--text-bright)}
.xp-project__type{font-size:11.5px;color:var(--text-secondary);margin-top:3px}

/* ── Promocodes: row icon, modal/card reveal block, "Промокоды" section ── */
/* `aspect-ratio` matches the promo SVG's own 250×145 viewBox and is NOT
 * cosmetic: with `width:auto` alone the browser has no intrinsic ratio until
 * the data-URI decodes, so it lays the <img> out at the CSS default 300px
 * wide. In `.promo-reveal` — a 100%-wide flex button inside a ~230px grid
 * card — a 300px icon leaves negative room for "Показать промокод", the label
 * wraps to several lines, the button grows, and everything below it moves.
 * That single reflow WAS the site's whole CLS: Lighthouse desktop measured
 * 0.481, attributed to `section.xp-section`, cause "Media element lacking an
 * explicit size". Declaring the ratio gives the used width (27.6px) before
 * the first layout, so nothing ever moves. Keep the ratio in sync if the
 * icon's viewBox changes. */
.promo-row-ico{height:13px;width:auto;aspect-ratio:250/145;vertical-align:-2px;margin-left:1px}
.promo-block{margin-top:20px;display:flex;flex-direction:column;gap:8px}
/* In the "Промокоды" cards the block fills the (grid-equal-height) card and its
   reveal/code control is pinned to the bottom — so buttons line up across cards
   regardless of a 1- vs 2-line description. */
.promo-block.promo-compact{margin-top:8px;flex:1}
.promo-compact .promo-reveal,.promo-compact .promo-code-box{margin-top:auto}
.promo-desc{font-size:12.5px;color:var(--text-secondary)}
.promo-reveal{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:13px 16px;border-radius:10px;background:var(--glass-fill);border:1px dashed var(--glass-border);color:var(--text-bright);font-weight:600;font-size:13px;cursor:pointer;transition:border-color .12s,transform .1s}
.promo-reveal:hover{border-color:#16a34a}
.promo-reveal:active{transform:scale(.99)}
.promo-reveal .ico-sm{height:16px;width:auto;aspect-ratio:250/145}
/* Click/tap anywhere on the code field to copy (icon lives inside it),
   apex-style. Hidden until "Показать промокод" reveals it. */
.promo-code-box{display:none;align-items:center;justify-content:center;position:relative;width:100%;cursor:pointer;background:var(--glass-fill);border:1px solid var(--glass-border);border-radius:9px;padding:12px 40px;font:inherit;transition:border-color .12s,background .12s}
.promo-code-box:hover{border-color:#16a34a}
.promo-code-box:active{transform:scale(.995)}
.promo-code{min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;font-weight:700;letter-spacing:.04em;color:var(--text-bright);text-align:center;word-break:break-all}
.promo-copy-ico{position:absolute;right:14px;top:0;bottom:0;display:inline-flex;align-items:center;color:var(--text-secondary);transition:color .12s}
.promo-copy-ico svg{width:18px;height:18px;display:block}
.promo-copy-ico .ic-check{display:none}
.promo-code-box.copied{border-color:#16a34a;background:rgba(22,163,74,.12)}
.promo-code-box.copied .promo-copy-ico{color:#16a34a}
.promo-code-box.copied .ic-copy{display:none}
.promo-code-box.copied .ic-check{display:block}
/* Activation link — a filled green CTA under the revealed code (shown by
   revealPromo). Only rendered when a valid http(s)/tg promo_url is set. */
.promo-activate{align-items:center;justify-content:center;gap:8px;width:100%;padding:13px 16px;border-radius:10px;background:#16a34a;color:#fff;font-weight:700;font-size:13px;text-decoration:none;text-align:center;transition:background .12s,transform .1s}
.promo-activate:hover{background:#15903f}
.promo-activate:active{transform:scale(.99)}
.promo-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}
.promo-card{display:flex;flex-direction:column;padding:14px 16px;border-radius:11px;background:var(--glass-fill);border:1px solid var(--glass-border);box-shadow:var(--glass-shadow),inset 0 1px 0 var(--glass-sheen)}
.promo-card__name{display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px;color:var(--text-bright)}

/* ── VPN tariffs & discounts ──────────────────────────────────────────────
   Table price cell: struck old price (small, red, above) + green bold new.
   Modal: a segmented "О сервисе / Тарифы" switcher over the info panel, with
   tariff pills and a per-tariff price/description (+ CTA deep-link swap). */
.price-old{display:block;font-size:10px;color:#ef4444;text-decoration:line-through;text-decoration-color:var(--text-faint);text-decoration-color:color-mix(in srgb,var(--text-primary) 55%,transparent);line-height:1.1;opacity:.85;font-weight:500}
.price-new{display:block;color:#22c55e;font-weight:700;line-height:1.15}
/* Modal tariff pills — shown directly (no О сервисе/Тарифы switch), centered
   above the description panel. Selecting a pill updates the header metrics row
   (price/devices/traffic) + CTA link, NOT the description. */
.vpn-tpills{display:flex;flex-wrap:wrap;gap:6px}
.vpn-tpills-top{justify-content:center;margin-top:18px;margin-bottom:2px}
.vpn-tpill{border:1px solid var(--border-chip);background:var(--bg-col);color:var(--text-secondary);font:inherit;font-size:12px;font-weight:600;padding:7px 13px;border-radius:8px;cursor:pointer;transition:border-color .12s,color .12s,background .12s}
.vpn-tpill:hover{border-color:#16a34a}
.vpn-tpill.active{border-color:#16a34a;color:var(--text-bright);background:rgba(22,163,74,.1)}
/* Header price cell with a discount: struck old (small red, above) + green new. */
.tf-old-h{display:block;font-size:11px;color:#ef4444;text-decoration:line-through;text-decoration-color:var(--text-faint);text-decoration-color:color-mix(in srgb,var(--text-primary) 55%,transparent);line-height:1;opacity:.85;text-align:center;margin-bottom:2px}
.tf-new-h{display:block;color:#22c55e;font-weight:700;line-height:1.05;text-align:center}

/* ════════════════════════════════════════════════════════════════════
   Glass-panel film grain — a faint static noise tile baked into a data-URI
   (feTurbulence rasterised ONCE by the browser, cached + tiled — ~free per
   frame; NOT a live filter). Theme-dependent: light specks on the dark theme,
   dark specks on the light theme. Added as the top background layer on the
   panels (the gradient recipe gets `var(--glass-grain),` prepended).
   ════════════════════════════════════════════════════════════════════ */
:root{
  --glass-grain:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='1.5' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 .015 .015 .015 0 0'/></filter><rect width='100%25' height='100%25' filter='url(%23g)'/></svg>");
}
[data-theme="light"]{
  --glass-grain:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='1.5' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .02 .02 .02 0 0'/></filter><rect width='100%25' height='100%25' filter='url(%23g)'/></svg>");
}
/* No-JS safety net: freeze animation/transition for reduced-motion users. */
@media (prefers-reduced-motion: reduce){
  *{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
}

/* ==GEN:CSS:CARDS== блок каталога; до 2026-10-07 генерировался из админки, теперь исходник здесь. == */
/* ── Таблица сервисов виртуальных карт («Карты») ──
   Ключевое: `table-layout:fixed` + `width:100%` + проценты в <colgroup>.
   При этом таблица физически не может выйти за ширину контейнера — колонки
   делят её пропорционально, а содержимое переносится внутри ячейки. Именно из-за
   отсутствия fixed-раскладки первая версия уезжала в горизонтальный скролл, а
   бейджи пополнения наезжали на соседнюю колонку.

   Плотность берётся не числом колонок, а высотой строки: каждая ячейка несёт
   две строки — главное значение и уточнение мелким. Так 10 параметров влезают
   в 8 колонок. */
/* Стеклянную рамку даёт общий класс .tbl (grain + sheen + fill из темы) —
   таблица оборачивается в него, а не в свою обёртку.

   Базовый td в теме — white-space:nowrap и 15px. Для этой таблицы и то и другое
   переопределяется: без переноса текст физически вылезал в соседнюю колонку,
   из-за чего таблица «не влезала», а 15px в семи колонках не помещаются. */
.cs-tbl{width:100%;min-width:760px;table-layout:fixed;font-size:13px}
.cs-tbl th{padding:9px 12px;font-size:10px;letter-spacing:.05em;color:var(--text-faint)}
.cs-tbl td{padding:12px;white-space:normal;vertical-align:top;font-size:13px;line-height:1.35;box-shadow:inset 0 -1px 0 var(--border-td)}
.cs-tbl tbody tr:last-child td{box-shadow:none}
.cs-tbl tbody tr{cursor:pointer}
.cs-tbl tr.cs-dead{opacity:.45}
.cs-nm{color:var(--text-bright);font-weight:600;font-size:14px;overflow-wrap:anywhere}
.cs-nm>.dot{margin-right:7px;vertical-align:1px}
.cs-sub{font-size:11px;color:var(--text-faint);line-height:1.45;margin-top:3px;overflow-wrap:anywhere}
.cs-num{color:var(--text-bright);font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
.cs-note{font-size:11px;color:var(--text-faint);margin-top:3px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
.cs-warn{color:#c96a52}
.cs-amber{color:#b0893f}
/* Фичи — моно-текст вместо цветных плашек: отсутствие показывается зачёркнутым
   и притушенным, а не красным. Семь цветных бейджей в строке читались как шум. */
/* Явный горизонтальный ряд иконок. Раньше чипы были инлайновыми <span> в
   обычном <div>, склеенными без пробелов: точек переноса между ними не
   возникало, и строка ломалась в произвольных местах, разваливаясь в столбик.
   Флекс сам задаёт переносы — иконки идут в строку и уходят вниз только когда
   место действительно кончилось. */
.cs-icrow{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;line-height:1}
.cs-icrow>span{display:inline-flex;align-items:center;white-space:nowrap}
.cs-icrow .cs-ic{margin-right:2px;vertical-align:0}
.cs-icrow .cs-f{margin-right:0}
/* Ряд фич внутри ячейки сервиса: KYC, кошельки, офлайн, возвраты.
   Переехал сюда из отдельной колонки — она стоила 14% ширины ради
   четырёх значков. */
.cs-nmf{margin-top:6px;gap:3px 6px}
/* Фичи — вторичный слой рядом с названием, поэтому значки мельче основных
   22px: в узкой колонке они иначе встают по одному в строку. */
.cs-nmf .cs-ic{width:22px;height:22px}
.cs-nmf .cs-f{font-size:10px;margin-right:0}
/* KYC — исключение: в значке мелкие буквы и четыре полосы, при 18px он
   перестаёт читаться, а это самый важный признак из ряда. */
.cs-nmf img[src*="kyc_"]{width:24px;height:24px}
/* Процент за пополнение — приписка к иконке, а не самостоятельный факт:
   мельче и тусклее, чтобы ряд читался как ряд платёжных систем. */
.cs-fee{font-style:normal;font-size:10px;color:var(--text-faint);margin-left:1px}
/* Пополнение — ровно три значка в ряду, максимум два ряда. Грид, а не
   переносы флекса: у флекса число значков в ряду плавало от ширины колонки,
   и высота строк таблицы скакала от сервиса к сервису. */
/* Колонки РОВНО по ширине значка. С `max-content` колонка растягивалась по
   самому длинному проценту («4.5%»), и все три значка расползались — процент
   оставался в потоке и добавлял ширину, хотя визуально был поднят. */
.cs-tprow{display:grid;grid-template-columns:repeat(3,22px);gap:5px 18px;align-items:start;justify-content:start}
.cs-tprow>span{position:relative;display:block;width:22px}
.cs-tprow .cs-ic{margin-right:0}
/* Процент — надстрочный бейдж ВНЕ потока: ширины не занимает совсем и висит
   в промежутке справа от своего значка. Промежуток (18px) подобран так, чтобы
   самый длинный процент до соседнего значка не доставал. */
.cs-tprow .cs-fee{position:absolute;left:100%;top:-3px;margin-left:-2px;font-size:9px;line-height:1;white-space:nowrap}
/* Бета-метка вкладки. `sup` внутри кнопки не влияет на замер слайдера —
   он читает ширину самой кнопки, а не текстового узла. */
.tab-beta{font-size:9px;margin-left:3px;color:#C50F35;letter-spacing:0;font-weight:700;top:-.5em}
.cs-f{display:inline-block;margin-right:7px;font-size:11px;letter-spacing:.04em;color:var(--text-secondary);white-space:nowrap}
.cs-f-no{color:var(--text-disabled);text-decoration:line-through}
/* Переключатель в духе темы: стеклянная дорожка, накатывающийся кружок,
   включённое состояние — фирменным градиентом. Сам <input> остаётся в
   разметке и только прячется: он даёт клавиатуру, фокус и чтение экранным
   диктором, чего рисованный div не даёт. */
.cs-tg{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--text-secondary);cursor:pointer;user-select:none}
.cs-tg input{position:absolute;opacity:0;width:0;height:0}
.cs-sw{position:relative;flex:none;width:34px;height:19px;border-radius:999px;background:var(--glass-fill);border:1px solid var(--border-chip);transition:background .2s ease,border-color .2s ease}
.cs-sw::after{content:'';position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:var(--text-faint);transition:transform .2s cubic-bezier(.4,0,.2,1),background .2s ease}
.cs-tg input:checked + .cs-sw{background:linear-gradient(135deg,rgba(197,15,53,.9),rgba(105,11,31,.85));border-color:transparent}
.cs-tg input:checked + .cs-sw::after{transform:translateX(15px);background:#fff}
.cs-tg input:focus-visible + .cs-sw{outline:2px solid var(--text-secondary);outline-offset:2px}
/* Поле суммы — то же стекло, что у панелей, вместо системного вида. Стрелки
   спиннера убраны: шаг в один доллар мышью никто не крутит, а места они
   занимают больше самого числа. */
.cs-amt{width:74px;padding:5px 9px;border-radius:8px;background:var(--glass-fill);border:1px solid var(--border-chip);color:var(--text-bright);font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;transition:border-color .2s ease,box-shadow .2s ease;-moz-appearance:textfield}
.cs-amt::-webkit-outer-spin-button,.cs-amt::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.cs-amt:focus{outline:none;border-color:#C50F35;box-shadow:0 0 0 3px rgba(197,15,53,.18)}
/* ── Панель контролов ───────────────────────────────────────────────────
   Вопросы, которые читатель задаёт таблице, собраны в одну рамку с
   подписями, а не рассыпаны строкой голых полей вперемешку с заголовком.
   Живёт в ядре: пользователей уже двое — «Карты» и «eSIM», — и у вертикали
   остаётся только своя дельта (у eSIM это выбор страны и погашенный объём
   при безлимите).

   Чипов с готовыми значениями здесь нет намеренно. Сетки у вендоров разные,
   а метрика подбирает БЛИЖАЙШИЙ покрывающий вариант, а не совпадающий: ряд
   чипов утверждал бы общую сетку, которой не существует, и половина его
   значений не совпала бы ни с одним тарифом. Число вводится любое, а стрелки
   ходят по лестнице привычных величин — это подсказка навигации, а не
   список доступного. */
.cs-panel{display:flex;flex-wrap:wrap;align-items:flex-end;gap:12px 20px;margin:0 0 14px;
  padding:12px 14px;border-radius:14px;background:var(--glass-fill);
  border:1px solid var(--border-chip);font-size:12px;color:var(--text-secondary)}
.cs-fld{display:flex;flex-direction:column;gap:6px;min-width:0}
.cs-flab{font-size:10px;letter-spacing:.07em;text-transform:uppercase;color:var(--text-disabled)}
/* Высота ряда равна высоте степпера: иначе поле с тумблерами ниже соседнего
   на 17px, панель выровнена по низу — и подписи полей разъезжаются по высоте. */
.cs-frow{display:flex;align-items:center;gap:10px;min-height:36px}
.cs-step{display:inline-flex;align-items:center;height:36px;border-radius:10px;
  background:var(--glass-fill);border:1px solid var(--border-chip);overflow:hidden}
.cs-step:focus-within{border-color:rgba(197,15,53,.65);box-shadow:0 0 0 3px rgba(197,15,53,.14)}
.cs-step button{width:30px;height:100%;border:none;background:none;color:var(--text-secondary);
  font-size:16px;line-height:1;cursor:pointer;transition:background .15s,color .15s}
/* Подсветка своим красным, а не белой плёнкой: --hover-bg объявлен только на
   строках таблицы, снаружи он не определён — и белые 7% на светлой теме
   невидимы. Тон один на обе темы, отличается только плотностью. */
.cs-step button:hover{background:rgba(197,15,53,.15);color:var(--text-primary)}
.cs-step button:disabled{opacity:.3;cursor:not-allowed;background:none}
input.cs-stnum{width:42px;height:100%;padding:0;border:none;border-radius:0;background:none;
  color:var(--text-primary);font-size:14px;font-weight:600;text-align:right;
  font-variant-numeric:tabular-nums;-moz-appearance:textfield}
input.cs-stnum:focus{outline:none;box-shadow:none}
.cs-stnum::-webkit-outer-spin-button,.cs-stnum::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
/* Подпись единицы читается как часть поля, поэтому и ведёт себя как поле:
   курсор текстовый, клик выделяет число. Промахнуться по ней легче всего,
   когда цифра одна — число прижато вправо, вплотную к подписи. */
.cs-stunit{padding:0 4px 0 3px;color:var(--text-disabled);font-size:11px;cursor:text;user-select:none}
.cs-lim{margin-top:3px}
/* Ряд под названием: центрируем по вертикали и разводим промежутком, а не
   точками. Значки тут разной природы (наш глиф, логотип платёжной системы,
   эмодзи-флаг), и по базовой линии они не выравниваются. */
.cs-subrow{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.cs-subrow .cs-ic{margin-right:0}
.cs-flag{font-size:15px;line-height:1}
/* Формат карты — вспомогательный признак, и его глиф заливает весь квадрат,
   тогда как у Visa/Mastercard внутри поля есть воздух. При равном размере
   он выглядел вдвое крупнее логотипа, поэтому задаём меньше явно. */
/* Селекторы СКОПИРОВАНЫ ПОД .cs-subrow намеренно: базовое .cs-ic объявлено
   ниже по файлу и при равной специфичности побеждало — пополняемая карта
   оставалась 22px, тогда как предоплаченная слушалась правила и была 15px,
   и пара выглядела разнокалиберной. Замер это и показал: 22x22 против 15x15.
   Оба состояния формата обязаны быть одного размера — это одна и та же
   карта, отличается только наличие креста. */
.cs-subrow .cs-ic-fmt,.cs-subrow .cs-ic-fmt .cs-ic{width:18px;height:18px}
/* Логотип платёжной системы — главный опознавательный знак строки. */
.cs-subrow .cs-ic-net{width:26px;height:26px}
/* Скидочная цена берёт ЦВЕТ у .price-new, но не её метрику: у price-new своя
   line-height, и подмена класса меняла высоту строки на 4px — таблица дёргалась
   при прокрутке тарифов ровно там, где у одного тарифа скидка есть, а у
   соседнего нет. Меняем только цвет. */
/* Место под старую цену держится неразрывным пробелом, но у .price-old есть
   line-through — и над ценой без скидки проступала чёрточка на пустом месте.
   Заполнитель зачёркивать нечего. */
.cs-old-hole{text-decoration:none}
/* Светлая тема мерилась: --text-faint давал 4,17:1, --text-disabled 3,08:1
   на фоне #eef1f6 при норме 4,5. Переопределяем токены ВНУТРИ карточной
   таблицы — они наследуются всем её содержимым, а соседние вкладки не
   задеваются. #6b635d даёт 5,20:1. */
[data-theme="light"] .cs-tbl,[data-theme="light"] .cs-tg,[data-theme="light"] .cs-amt{--text-faint:#6b635d;--text-disabled:#6b635d}
[data-theme="light"] .cs-step button:hover{background:rgba(197,15,53,.11)}
/* Белый текст на пилюле при прозрачности .62 давал 3,50:1 — градиент
   разбавлялся светлым фоном до бледно-розового. На .88 выходит 5,32:1. */
[data-theme="light"] .cs-car-on,[data-theme="light"] .cs-car-on:hover{background:linear-gradient(135deg,rgba(197,15,53,.88),rgba(105,11,31,.85))}
.cs-num-sale{color:#22c55e}
.cs-negwrap{display:inline-flex;align-items:center}
.cs-ic-neg{color:var(--text-disabled)}
.cs-gi{width:17px;height:17px;vertical-align:-4px;margin-right:5px;opacity:.8}
.cs-more{font-size:13px;color:var(--text-faint);cursor:help;letter-spacing:1px}
.cs-so{cursor:pointer;user-select:none}
.cs-so:hover{color:var(--text-hover)}
/* ── Карточка сервиса ──
   Заголовки разделов с фирменной красной засечкой, выбор тарифа — общим
   слайдером темы (.chip-track + _moveSlider из shared-render.js), сравнение
   тарифов — компактной таблицей с подсветкой выбранного. */
.cs-sec{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-faint);margin:16px 0 6px}
.cs-sec::before{content:'';width:3px;height:11px;border-radius:2px;background:linear-gradient(180deg,#C50F35,#690B1F);flex-shrink:0}
.cs-tf-track{overflow-x:auto;scrollbar-width:none;margin:0 -2px 8px}
.cs-tf-track::-webkit-scrollbar{display:none}
.cs-tf-track .region-chip{padding:7px 11px;font-size:13px;white-space:nowrap}
.cs-mt{width:100%;table-layout:fixed;font-size:12px;border-radius:8px;overflow:hidden;background:var(--glass-grain),linear-gradient(135deg,var(--glass-sheen),transparent 38%),var(--glass-fill);border:1px solid var(--glass-border)}
.cs-mt th{padding:6px 8px;font-size:9px;letter-spacing:.04em;color:var(--text-faint);white-space:nowrap}
.cs-mt td{padding:7px 8px;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--text-secondary);box-shadow:inset 0 -1px 0 var(--border-td);cursor:pointer;font-variant-numeric:tabular-nums}
.cs-mt tbody tr:last-child td{box-shadow:none}
.cs-mt tbody tr:hover td{background:var(--hover-default)}
.cs-mt tr.cs-mt-on td{color:var(--text-white);font-weight:600;background:linear-gradient(135deg,rgba(197,15,53,.16),rgba(105,11,31,.08))}
.cs-mt tr.cs-mt-on td:first-child{box-shadow:inset 3px 0 0 #C50F35,inset 0 -1px 0 var(--border-td)}
.cs-kv{display:grid;grid-template-columns:auto minmax(0,1fr);gap:3px 12px;font-size:12px;margin:0}
.cs-kv dt{color:var(--text-secondary)}
.cs-kv dd{margin:0;text-align:right;color:var(--text-bright);overflow-wrap:anywhere}
.cs-flow{font-size:12px;line-height:1.65;color:var(--text-secondary);overflow-wrap:anywhere}
.cs-flow b{color:var(--text-bright);font-weight:600}
/* Иконки платёжных систем и токенов. object-fit:contain — потому что у логотипов
   разные пропорции внутри общего квадрата 512×512 (карты широкие, монеты
   круглые), и без него их бы растянуло. */
.cs-ic{width:22px;height:22px;object-fit:contain;vertical-align:-6px;margin-right:3px}
/* Выключенную функцию нельзя «зачеркнуть» как текст, поэтому обесцвечиваем. */
.cs-ic-off{opacity:.28;filter:grayscale(1)}
/* Кнопки правки проявляются на наведении: строка кликабельна сама по себе,
   и две плашки в каждой строке только шумели. */
.cs-act{display:flex;gap:4px;margin-top:6px;opacity:0;transition:opacity .15s ease}
.cs-tbl tbody tr:hover .cs-act,.cs-act:focus-within{opacity:1}
.cs-act button{font-size:11px;line-height:1.4;padding:1px 8px;border-radius:6px;border:1px solid var(--border-chip);background:transparent;color:var(--text-secondary);cursor:pointer;font-family:inherit}
.cs-act button:hover{color:var(--text-bright);border-color:var(--text-faint)}
.cs-act button.cs-del:hover{color:#c96a52;border-color:#c96a52}
/* Вертикальная карусель тарифов: выбранный по центру, соседние приглушены.
   Лента едет трансформом, поэтому при клике реально прокручивается — ради этого
   строка при выборе тарифа НЕ перерисовывается целиком, а обновляются только
   ячейки с цифрами (см. cardsRotate). */
/* Вся ячейка тарифов — зона прокрутки: попасть в 22-пиксельную пилюлю мышью,
   а тем более пальцем, неудобно. Отрицательные поля растягивают карусель на всю
   ячейку, чтобы кликабельным был каждый пиксель. */
.cs-td-car{cursor:pointer}
.cs-td-car:hover .cs-car-i:not(.cs-car-on){opacity:.55}
.cs-car{--h:22px;position:relative;overflow:hidden;margin:-2px -8px;pointer-events:none}
.cs-car-strip{position:absolute;left:0;right:0;top:0;transition:transform .3s cubic-bezier(.4,0,.2,1)}
/* Ширина ОДИНАКОВАЯ у всех пилюль — 25 символов плюс внутренние отступы.
   fit-content подгонял каждую под свой текст, и колонка превращалась в
   лесенку из плашек разной длины. min(100%) нужен, чтобы на узкой
   колонке пилюля не вылезала за её край. */
.cs-car-i{display:block;width:min(100%,calc(18ch + 16px));height:var(--h);font-size:12px;line-height:var(--h);padding:0 8px;border-radius:6px;color:var(--text-faint);opacity:.3;white-space:nowrap;overflow:hidden;transition:opacity .3s ease,color .3s ease}
/* Обрезка фейдом, а не многоточием — тем же приёмом, что у адресов прокси
   (.proxy-name-clip). Маска висит на самой пилюле, поэтому к правому краю
   растворяется и фон: у всех пилюль ширина одинаковая, и правый край
   получается одинаково мягким независимо от длины подписи. */
.cs-car-i{-webkit-mask-image:linear-gradient(to right,#000 0,#000 14ch,transparent 18ch);
  mask-image:linear-gradient(to right,#000 0,#000 14ch,transparent 18ch)}
/* В горизонтальной ленте ширина элемента — треть окна, а не 18 символов,
   поэтому маска в ch там либо не сработает, либо срежет не там. Проценты
   привязаны к самому элементу, и текст в ней выровнен по центру — гасим
   симметрично с обеих сторон. */
.cs-hcar-i{-webkit-mask-image:linear-gradient(to right,transparent 0,#000 12%,#000 88%,transparent 100%);
  mask-image:linear-gradient(to right,transparent 0,#000 12%,#000 88%,transparent 100%)}
/* Выбранный тариф — фирменным красным, той же градиентной идиомой, что и
   .tab-slider (#C50F35 -> #690B1F с подсветкой снизу). */
.cs-car-one{display:flex;align-items:center}
/* Приглушено намеренно. Пилюля обозначает выбранный тариф — это ориентир
   внутри строки, а не главный объект страницы; при прежней насыщенности и
   внешнем свечении семь красных пятен перетягивали внимание с цифр, ради
   которых таблицу и читают. Внешнее свечение убрано совсем, осталась только
   тонкая внутренняя подсветка сверху — она даёт объём, но не светится. */
.cs-car-on,.cs-car-on:hover{opacity:1;color:#fff;font-weight:600;background:linear-gradient(135deg,rgba(197,15,53,.62),rgba(105,11,31,.58));box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}

/* ── Узкий экран: колонки уходят в подстрочник ──
   Таблица остаётся таблицей ровно до тех пор, пока колонки в неё влезают.
   Всё, что не влезло, переезжает в подстрочник — строку без сетки под
   строкой сервиса, визуально с ней слитную. Порог не про устройство, а про
   число колонок: каждая ступень снимает по одной справа налево, начиная с
   наименее важной.
   Содержимое ячеек в подстрочнике то же самое — _cardRow кладёт его и в
   <td>, и в подстрочник, а CSS решает, что из этого показать. Второго
   рендерера нет, поэтому админка и публичный сайт не могут разъехаться. */
/* Зебра — по СЕРВИСУ, а не по строке. Тема красит каждую вторую <tr>
   (table tbody tr:nth-child(even)), а на сервис у нас приходится две:
   строка и её подстрочник. Из-за этого полосатыми выходили ровно
   подстрочники, и подстрочник читался как отдельная запись. Полосу задаёт
   класс cs-alt, который _cardRow вешает на ОБЕ строки нечётного сервиса, —
   на чётность <tr> опираться нельзя.
   Селекторы намеренно начинаются с table.cs-tbl: у правил темы вес (0,2,4),
   и без ведущего типа наши бы им проиграли. */
table.cs-tbl tbody tr:not(.cs-alt)>td{background:transparent}
table.cs-tbl tbody tr.cs-alt>td{background:var(--bg-zebra)}

/* Ховер строки — тот же механизм, что у VPN и прокси: строка объявляет
   --hover-bg, а заливка одна на всех. Здесь она НЕ работала: правила зебры
   выше весят (0,2,4) и (0,3,4), а тема красит ховер селектором `tbody
   tr:hover td` весом (0,1,2) — он проигрывал, и строка, открывающая
   карточку, ничем не отзывалась на курсор.
   Зелёный — база: у «Карт» сигнальных классов нет, потому что цена там
   известна всегда. Вертикаль, у которой сигнал есть (eSIM: es-ok/es-half/
   es-fail), переопределяет переменную у себя весом (0,2,1).
   Правила стоят ЗДЕСЬ, а не ниже: блок @media (hover:none) гасит залипший
   ховер на тач-экранах, и он обязан идти после — при равном весе (0,3,4)
   решает порядок. */
table.cs-tbl tbody tr{--hover-bg:rgba(34,197,94,.05)}
table.cs-tbl tbody tr.cs-r:hover>td,
table.cs-tbl tbody tr.cs-r:hover + tr.cs-r2>td,
table.cs-tbl tbody tr.cs-r2:hover>td{background:var(--hover-bg)}

/* Тап на телефоне не должен ни мигать системным прямоугольником, ни
   оставлять залипшую подсветку: на тач-экране :hover не снимается до
   касания в другом месте, и строка выглядит выделенной. Возвращаем ей её
   же фон покоя. */
.cs-tbl tbody tr{-webkit-tap-highlight-color:transparent}
@media (hover:none){
  /* :not(.cs-alt) поднимает вес до (0,3,4) — ровно как у темы
     (`table:not(.tbl-vpn) tbody tr:nth-child(even):hover td`). Без него
     правило весило (0,2,4) и проигрывало на ЧЁТНЫХ строках, а чётные — это
     как раз подстрочники нечётных сервисов: тап по карусели подсвечивал
     всю полосу. */
  table.cs-tbl tbody tr:not(.cs-alt):hover>td{background:transparent}
  table.cs-tbl tbody tr.cs-alt:hover>td{background:var(--bg-zebra)}
  /* Кнопки «Ред.»/«Уд.» в админке проявляются по ховеру, которого тут нет. */
  .cs-act{opacity:1}
}
.cs-r2{display:none}
.cs-r2>td{padding:0 12px 12px}
/* Горизонтальная карусель подстрочника — зеркало табличной: то же окно в три
   слота, тот же приглушённый вид соседей, тот же фирменный градиент на
   выбранном. Отличие только в оси. */
/* Лента ПОЗИЦИОНИРУЕТСЯ АБСОЛЮТНО — как и в вертикальной карусели. Без этого
   её элементы участвуют в расчёте ширины ячейки, и таблица растягивалась на
   всю длину ленты: замер показывал 1661px вместо 334. */
/* Своё --h: переменная объявлена на .cs-car и на соседний класс не
   распространяется — без этого высота ленты схлопывалась в ноль и карусель
   была невидима, хотя разметка присутствовала. */
.cs-hcar{--h:26px;position:relative;overflow:hidden;height:var(--h);width:100%}
.cs-hcar-strip{position:absolute;left:0;top:0;width:100%;display:flex;transition:transform .3s cubic-bezier(.4,0,.2,1)}
.cs-hcar-i{flex:0 0 33.3333%;height:var(--h);line-height:var(--h);font-size:12px;padding:0 8px;border-radius:6px;color:var(--text-faint);opacity:.3;white-space:nowrap;overflow:hidden;text-align:center;transition:opacity .3s ease,color .3s ease}
/* Выбранный элемент горизонтальной ленты нужно поднимать ОТДЕЛЬНЫМ правилом:
   у .cs-car-on та же специфичность, что у .cs-hcar-i, а объявлен он выше —
   и приглушение с блёклым цветом перебивали фирменный градиент. */
.cs-hcar-i.cs-car-on{opacity:1;color:#fff;font-weight:600}
/* Ступени. Числа — это ширина, при которой очередная колонка перестаёт
   помещаться в свою долю colgroup, а не размер какого-то телефона. */
@media (max-width:820px){
  /* С этого момента таблица уже не семиколоночная: фиксированная раскладка
     и проценты colgroup рассчитаны на все пять, поэтому в усечённом режиме
     ширину раздаёт браузер. Заодно снимается min-width:840px — из-за него
     таблица уезжала за край экрана. */
  .cs-tbl{min-width:0;table-layout:auto}
  .cs-tbl colgroup{display:none}
  .cs-r2{display:table-row}
  /* Разделитель строки рисует подстрочник, а не строка над ним: иначе между
     ними шла бы линия и они читались бы как две разные записи. */
  .cs-r>td{box-shadow:none}
  .cs-r2>td{box-shadow:inset 0 -1px 0 var(--border-td)}
  .cs-tbl tbody tr.cs-r2:last-child>td{box-shadow:none}
  /* Уводим ТАРИФ, и только его. Он единственная колонка с текстом переменной
     длины: в узкой таблице подписи резались до «Для подпи…», а освобождённой
     ширины ровно хватает остальным четырём. Цифровые колонки уводить не надо —
     они узкие и в экран помещаются. */
  .cs-tbl th:nth-child(2),.cs-r>td:nth-child(2){display:none}
}
/* Телефон: четыре колонки обязаны поместиться, поэтому ужимаем, а не прячем.
   На 375px без этого имени сервиса доставалось 48px при «Wanttopay» в 86 —
   слова ломались посередине. Экономия набирается по частям: отступы, кегль
   имени и — главное — ряд пополнения. */
@media (max-width:560px){
  /* В таблице остаются три опоры: кто, какой тариф и сколько это стоит.
     Ниже этого порога уводить нечего — подстрочник уже забрал всё
     остальное на ступени 760. */
  .cs-tbl th{padding:9px 8px}
  .cs-tbl td{padding:12px 8px}
  .cs-r2>td{padding:0 8px 12px}
  /* Колонка стоимости несёт больше всего текста (сумма, курс, комиссия),
     поэтому долю отдаём ей, а не имени сервиса. */
  .cs-tbl th:nth-child(1),.cs-r>td:nth-child(1){width:31%}
  .cs-panel{gap:10px 14px;padding:11px 12px}
}
@media (max-width:440px){
  .cs-tbl th{padding:8px 4px}
  .cs-tbl td{padding:11px 4px}
  .cs-r2>td{padding:0 4px 11px}
  /* Имени нужна нижняя граница: автораскладка отдавала ему 55px, а самое
     длинное название требует 76 — короче него колонка быть не может, иначе
     слова ломаются посередине. Соседи ужимаются переносом, им есть куда. */
  .cs-tbl th:nth-child(1),.cs-r>td:nth-child(1){min-width:94px}
  .cs-nm{font-size:12.5px}
  .cs-num{font-size:13px}
  /* Процент уходит ПОД значок. Бейджу справа нужен промежуток в 18px, чтобы
     не наезжать на соседа, — на телефоне это 36px из ~90, отданные пустоте.
     Под значком он ничего не раздвигает, и промежуток падает до 5px. */
  /* Замер на 375px: колонка пополнения занимала 102px из 334 — больше всех.
     Значок 20px вместо 22 и промежуток 4 вместо 18 (процент под ним ничего
     не раздвигает) отдают соседям больше двадцати пикселей. */
  /* Ширину колонки пополнения задают НЕ значки, а подпись «9% за пополнение»
     у одного сервиса: колонка тянется по самой длинной строке во всех рядах.
     Мельче и с переносом она перестаёт диктовать ширину всей колонке. */
  .cs-r>td:nth-child(4) .cs-note{font-size:9px;line-height:1.25}
  .cs-tprow{grid-template-columns:repeat(3,20px);gap:4px 4px}
  .cs-tprow>span{width:20px;text-align:center}
  .cs-tprow .cs-ic{width:20px;height:20px}
  .cs-tprow .cs-fee{position:static;display:block;margin:1px 0 0;font-size:8px;line-height:1.1;white-space:nowrap}
}
@media (max-width:360px){
  /* Совсем узкие экраны. Имя ужимается ещё на полкегля — этого хватает,
     чтобы «Wanttopay» помещался в 82px вместо 94. */
  .cs-tbl th:nth-child(1),.cs-r>td:nth-child(1){min-width:82px}
  .cs-nm{font-size:11.5px}
  .cs-tprow{grid-template-columns:repeat(2,20px)}
}
/* ==/GEN:CSS:CARDS== */

/* ==GEN:CSS:GAMES== блок каталога; до 2026-10-07 генерировался из админки, теперь исходник здесь. == */
/* Строка-маркер выше обязана быть ОДНОЙ строкой: порт режет блок от конца
   строки с маркером, и вторая строка того же комментария уезжала в
   status-node/styles.css голым хвостом с закрывающей скобкой комментария, но
   без открывающей. esbuild ругался на каждом деплое (css-syntax-error), а
   браузер склеивал мусор со следующим селектором и выбрасывал правило
   .cs-v-games .cs-num (2026-08-03 … 09-30). И внутри комментария нельзя
   писать закрывающую скобку буквально — она закрывает его на месте. */
/* Дельты скоупятся классом-модификатором, а не порядком объявления:
   .cs-v-games .cs-num весит (0,2,0) против (0,1,0) у ядра, поэтому кто выше
   в файле — уже неважно. В этом файле дважды ловили баг «порядок при равной
   специфичности»; третий раз заводить не будем. */
.cs-v-games .cs-num{white-space:nowrap;font-variant-numeric:tabular-nums;font-size:15px}
.cs-v-games .cs-tprow{grid-template-columns:repeat(2,22px);gap:4px 18px}
/* Канал словами под именем — вместо колонки с каруселью. У четырёх сервисов
   из пяти канал ровно один, и крутить там нечего. */
.gm-ch{display:block;font-size:10px;color:var(--text-secondary);margin-top:2px;line-height:1.3}
/* Сигнал цветом — полоской слева, как edge-ok у VPN и прокси. Одно правило в
   одной функции (_gameEdge), в разметке цвет не выбирается руками. */
.cs-v-games tbody tr.cs-r>td:first-child{position:relative}
.cs-v-games tbody tr.cs-r>td:first-child::before{content:'';position:absolute;left:0;top:7px;bottom:7px;width:2px;border-radius:2px;background:transparent}
.cs-v-games tr.gm-ok>td:first-child::before{background:linear-gradient(180deg,#33783E,#07593E)}
.cs-v-games tr.gm-half>td:first-child::before{background:linear-gradient(180deg,#DE3513,#FF580A)}
.cs-v-games tr.gm-fail>td:first-child::before{background:linear-gradient(180deg,#6E0C20,#C10F34)}
.cs-v-games tr.gm-ok{--hover-bg:rgba(34,197,94,.05)}
.cs-v-games tr.gm-half{--hover-bg:rgba(255,88,10,.05)}
.cs-v-games tr.gm-fail{--hover-bg:rgba(239,68,68,.05)}
/* Глиф ответственности — тот же размер, что у светофора VPN. */
.gm-resp{display:inline-flex;align-items:center;min-height:19px}
.gm-resp .st-none{color:var(--text-disabled)}
/* Калькулятор ключей: два поля вместо каталога цен, который протух бы за часы.
   Живёт ПОД таблицей отдельным блоком, а не режимом: «₽ за 1000 ₽ на кошельке»
   и «₽ за один тайтл» не сравнимы, и переключатель, обнулявший половину
   таблицы, выражал это хуже, чем два отдельных блока. */
.gm-keys{margin-top:22px;padding-top:16px;border-top:1px solid var(--border-td)}
.gm-keys-h{font-size:13px;font-weight:600;margin-bottom:2px}
.gm-keycalc{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:10px 0 4px;font-size:12px;color:var(--text-secondary)}
.gm-keycalc input{width:92px;padding:5px 9px;border-radius:8px;background:var(--glass-fill);border:1px solid var(--border-chip);color:var(--text-primary);font-size:12px;-moz-appearance:textfield}
.gm-keycalc input::-webkit-outer-spin-button,.gm-keycalc input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.gm-verdict{font-weight:600}
.gm-verdict.win{color:#22c55e}
.gm-verdict.lose{color:#e0a44a}
.gm-verdict.na{color:var(--text-disabled)}
/* Правило третьей стороны. Отдельный вид от «запретов сервиса»: там сказано,
   что запрещает вендор, а здесь — чьё правило и чем оно грозит читателю. */
.gm-rule{border-left:2px solid #c96a52;padding:2px 0 2px 9px;margin:4px 0;font-size:12px;line-height:1.45}
.gm-rule b{color:#c96a52;font-weight:600}
[data-theme="light"] .cs-v-games{--text-faint:#6b635d;--text-disabled:#6b635d}
/* Три колонки влезают в 390px без горизонтальной прокрутки, поэтому ни одна
   не сбрасывается: раньше «Чек · возврат» уезжала за экран вместе с двумя
   признаками «нет данных», и таблица на телефоне выглядела полнее, чем есть.
   Ступени идут ПО УБЫВАНИЮ — при равной специфичности побеждает последняя. */
/* Ядро на 820px прячет вторую колонку — у карт это карусель тарифов, и там
   это верно. У «Игр» вторая колонка теперь ГЛАВНОЕ ЧИСЛО, и наследование этого
   правила стирало с мобильного ровно то, ради чего вкладка существует.
   Специфичность (0,2,2) против (0,2,1) у ядра — порядок объявления ни при чём. */
@media (max-width:820px){
  .cs-v-games thead th:nth-child(2),
  .cs-v-games tbody tr.cs-r>td:nth-child(2){display:table-cell}
  .gm-keycalc input{width:80px}
}
@media (max-width:440px){
  .cs-v-games .cs-tprow{grid-template-columns:repeat(2,20px)}
  .cs-v-games .cs-num{font-size:14px}
}
/* ==/GEN:CSS:GAMES== */

/* ==GEN:CSS:EMIG== блок каталога; до 2026-10-07 генерировался из админки, теперь исходник здесь. == */
/* Заголовочного числа у вкладки нет: цена услуги зависит от кейса, поэтому
   колонка «Цены» показывает два честных факта (консультация, «от») с пометкой
   прозрачности, а порядок строк редакторский. Вторая колонка здесь — страны,
   и на телефоне её прятать нельзя (ядро прячет th:nth-child(2) ради карусели
   тарифов «Карт»); вместо неё уходит третья, услуги. */
.em-ccs{display:flex;flex-wrap:wrap;gap:4px 8px}
.em-cc{white-space:nowrap;font-size:12.5px}
.em-more{color:var(--text-secondary)}
.em-chips{display:flex;flex-wrap:wrap;gap:4px}
.em-chip{font-size:11px;line-height:1;padding:4px 7px;border-radius:999px;border:1px solid var(--border-chip);color:var(--text-secondary);white-space:nowrap}
.em-chiprow{flex-wrap:wrap;gap:6px}
.em-sep>td{padding:14px 12px 6px;font-size:13px;color:var(--text-bright);border-top:1px solid var(--border-chip);cursor:default}
.em-sep .cs-note{margin-left:6px}
.em-row.em-dead>td:first-child{box-shadow:inset 2px 0 0 #ef4444}
.em-row.em-warn>td:first-child{box-shadow:inset 2px 0 0 #f59e0b}
.em-offer{padding:8px 10px;border:1px solid var(--border-chip);border-radius:8px;margin:6px 0;font-size:13px}
.em-offer b{color:var(--text-bright)}
.em-kv{margin:0 0 6px;font-size:13px;line-height:1.5}
.em-kv b{color:var(--text-bright);font-weight:600}
@media (max-width:820px){
  .cs-v-emig table.cs-tbl th:nth-child(2),.cs-v-emig table.cs-tbl td:nth-child(2){display:table-cell}
  .cs-v-emig table.cs-tbl th:nth-child(3),.cs-v-emig table.cs-tbl td:nth-child(3){display:none}
}
/* ==/GEN:CSS:EMIG== */

/* ==GEN:CSS:ESIM== блок каталога; до 2026-10-07 генерировался из админки, теперь исходник здесь. == */
.cs-v-esim .cs-num{white-space:nowrap;font-variant-numeric:tabular-nums}
/* Флаги покрытия: строка эмодзи, обрезаемая фейдом — та же идиома, что у
   адресов прокси. Список стран бывает в 130 позиций, и обрывать его резко
   значит утверждать, что там ровно столько, сколько видно. */
.cs-v-esim .cs-nmf .cs-f,#modal .cs-nmf .cs-f{margin-right:7px}
.es-flags{display:block;font-size:13px;line-height:1.4;letter-spacing:1px;overflow:hidden;
  -webkit-mask-image:linear-gradient(to right,#000 0,#000 80%,transparent 100%);
  mask-image:linear-gradient(to right,#000 0,#000 80%,transparent 100%)}
.es-more{font-size:10px;color:var(--text-disabled);letter-spacing:0}
/* Виджет корзины стоит на общей `.cs-panel` из ядра; здесь только дельта.
   Безлимит выключает объём: цифра остаётся видна как последняя выбранная,
   но гаснет и стрелки прячутся. Клик по погашенной плашке возвращает пакет. */
.cs-step.off{cursor:pointer}
.cs-step.off input.cs-stnum,.cs-step.off .cs-stunit{opacity:.3}
.cs-step.off button{visibility:hidden}
.es-plan{font-size:12px;line-height:1.35}
.es-where{display:flex;align-items:center;gap:6px;margin-top:3px}
.es-where .cs-ic{width:18px;height:18px}
/* Ровно та же геометрия, что у .cs-tprow в «Картах»: 22px и три в ряд.
   Пятый способ уходит на вторую строку, а не растягивает колонку. */
.es-pay{display:grid;grid-template-columns:repeat(3,22px);gap:5px 18px;
  align-items:start;justify-content:start}
.es-pay .cs-ic{width:22px;height:22px}
/* Способ без значка — это текст, и в колонку шириной 22px он не помещается.
   Отдаём ему всю ширину сетки отдельной строкой: «Uzcard / Humo через Click»
   иначе расплющивало бы в столбик по букве. */
.es-pay .cs-f{grid-column:1/-1;white-space:normal;line-height:1.35}
/* Сигнал цветом — полоской слева, одно правило в одной функции (_esimEdge). */
.cs-v-esim tbody tr.cs-r>td:first-child{position:relative}
.cs-v-esim tbody tr.cs-r>td:first-child::before{content:'';position:absolute;left:0;top:7px;bottom:7px;
  width:2px;border-radius:2px;background:transparent}
.cs-v-esim tr.es-ok>td:first-child::before{background:linear-gradient(180deg,#33783E,#07593E)}
.cs-v-esim tr.es-half>td:first-child::before{background:linear-gradient(180deg,#DE3513,#FF580A)}
.cs-v-esim tr.es-fail>td:first-child::before{background:linear-gradient(180deg,#6E0C20,#C10F34)}
.cs-v-esim tr.es-ok{--hover-bg:rgba(34,197,94,.05)}
.cs-v-esim tr.es-half{--hover-bg:rgba(255,88,10,.05)}
.cs-v-esim tr.es-fail{--hover-bg:rgba(239,68,68,.05)}
.cs-v-esim .cs-num{white-space:nowrap;font-variant-numeric:tabular-nums;font-size:15px}
.es-unl{height:36px;border:1px solid var(--border-chip);background:var(--glass-fill);
  color:var(--text-secondary);font-size:12px;padding:0 12px;border-radius:10px;cursor:pointer;
  white-space:nowrap;transition:color .15s,background .15s,border-color .15s}
.es-unl:hover{color:var(--text-primary);border-color:rgba(197,15,53,.5)}
.es-unl.on{color:#fff;font-weight:600;border-color:transparent;
  background:linear-gradient(135deg,rgba(197,15,53,.9),rgba(105,11,31,.85))}
/* ── Выбор страны ───────────────────────────────────────────────────────
   Был нативный <select> на 154 позиции: чтобы добраться до Турции, читатель
   листал список руками. Стало поле с поиском — три буквы вместо прокрутки.
   Рядом с каждой строкой — сколько сервисов реально называют там цену:
   это единственный способ заранее понять, почему направление даст прочерк. */
.es-cwrap{position:relative}
.es-cbtn{display:flex;align-items:center;gap:9px;width:230px;max-width:100%;height:36px;
  padding:0 11px;border-radius:10px;background:var(--glass-fill);border:1px solid var(--border-chip);
  color:var(--text-primary);font-size:13px;cursor:pointer;text-align:left;
  transition:border-color .15s,background .15s}
.es-cbtn:hover{border-color:rgba(197,15,53,.5)}
.es-cbtn.on{border-color:rgba(197,15,53,.65);box-shadow:0 0 0 3px rgba(197,15,53,.14)}
.es-cfl{font-size:17px;line-height:1;flex:none}
.es-cnm{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.es-cch{flex:none;color:var(--text-disabled);font-size:9px;transition:transform .15s}
.es-cbtn.on .es-cch{transform:rotate(180deg)}
.es-pop{position:absolute;z-index:60;top:calc(100% + 6px);left:0;width:270px;max-width:84vw;
  padding:8px;border-radius:12px;border:1px solid var(--border-chip);
  background:var(--bg-card,#141014);box-shadow:0 20px 44px rgba(0,0,0,.5);display:none}
.es-pop.on{display:block}
input.es-psearch{width:100%;height:32px;padding:0 10px;border-radius:8px;font-size:13px;
  background:var(--glass-fill);border:1px solid var(--border-chip);color:var(--text-primary)}
input.es-psearch:focus{outline:none;border-color:rgba(197,15,53,.65)}
.es-plist{max-height:264px;overflow-y:auto;margin-top:6px;scrollbar-width:thin}
.es-pitem{display:flex;align-items:center;gap:9px;width:100%;padding:7px 8px;border:none;
  border-radius:8px;background:none;color:var(--text-primary);font-size:13px;cursor:pointer;
  text-align:left}
.es-pitem:hover{background:rgba(197,15,53,.16)}
.es-pitem.on{color:#fff;font-weight:600;
  background:linear-gradient(135deg,rgba(197,15,53,.88),rgba(105,11,31,.82))}
.es-pnm{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.es-pcnt{flex:none;font-size:10px;color:var(--text-disabled);font-variant-numeric:tabular-nums}
.es-pitem.on .es-pcnt{color:rgba(255,255,255,.75)}
.es-pnone{padding:16px 8px;text-align:center;color:var(--text-disabled);font-size:12px}
/* Строка «не годится как резерв прямо сейчас». Не ошибка и не отсутствие
   данных — цена посчитана, просто сценарий другой. */
.es-notready{display:block;font-size:10px;color:#e0a44a;margin-top:2px;line-height:1.3}
/* Приписка к главному числу. Нормальным весом и мелко: она поясняет число,
   а не спорит с ним. Своя строка — в колонке с nowrap иначе распирает. */
.es-minfee{display:block;font-size:10px;font-weight:400;color:var(--text-disabled);
  margin-top:2px;line-height:1.3;white-space:nowrap}
/* Фразы вроде «переживает шатдаун» длиннее любой другой ячейки, а .cs-f по
   умолчанию nowrap: при table-layout:fixed это распирало таблицу на 52px за
   пределы обёртки на 360px. Здесь чипу разрешено переноситься — замерено. */
.es-rf{font-size:11px;line-height:1.5}
.es-rf .cs-f{white-space:normal;margin-right:0;display:block}
.es-rf b{font-weight:600;color:var(--text-primary)}
.es-cool{color:#e0a44a}
[data-theme="light"] .cs-v-esim{--text-faint:#6b635d;--text-disabled:#6b635d}
[data-theme="light"] .es-pitem:hover{background:rgba(197,15,53,.11)}
@media (max-width:820px){.es-cbtn{width:196px}}
/* Что остаётся на телефоне. Ядро на 820px убирает вторую колонку («Тариф»),
   здесь на 560px уходит четвёртая («Оплата») — на 390px остаются «Сервис» и
   цена, то есть вопрос и ответ. Оба вычтенных факта есть в карточке строки.
   Это решение, а не наследство: до переписи колонок было пять, и nth-child(4)
   значил «Покрытие». Ступени по УБЫВАНИЮ — иначе 560 вернёт колонку на 375px. */
@media (max-width:560px){
  .cs-v-esim thead th:nth-child(4),.cs-v-esim tbody tr.cs-r>td:nth-child(4){display:none}
  /* Страна получает всю ширину — это самый длинный текст в виджете и то, что
     чаще всего меняют. Трафик и срок встают парой под ней. Сама рамка сжата
     ядром: правило про её отступы одно на обе вертикали. */
  .cs-fld.es-fc{flex:1 1 100%}
  .es-cbtn{width:100%}
  .es-pop{width:100%}
}
/* ==/GEN:CSS:ESIM== */
/* ==GEN:CSS:END== ниже этой строки ничего не дописывать вручную == */

/* Индикатор потягивания и затемнение переехали в общий
   scripts/pull-to-refresh.js: он подключает свои стили сам, и жест целиком
   (разметка, поведение, вид) живёт одним файлом на все четыре сайта. */

/* ── Tor: матрица операторы × транспорты и история (2026-09-30). Свои правила,
   а не .cs-tbl: у каталогов зебра и скрытие 2-й колонки на телефоне, здесь
   колонки — операторы, и прятать их нельзя. Цвета столбиков — те же, что у
   edge-* в shared-render.js. ── */
.tor-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
table.tor-tbl{width:100%;border-collapse:collapse;font-size:14px}
table.tor-tbl th,table.tor-tbl td{padding:10px 8px;text-align:center;border-bottom:1px solid var(--glass-border,rgba(255,255,255,.08));white-space:nowrap}
table.tor-tbl th{font-weight:600;font-size:13px;color:var(--text-secondary)}
table.tor-tbl td.tor-tr,table.tor-tbl th:first-child{text-align:left}
.tor-prov{display:block}.tor-type{display:block;font-size:11px;font-weight:400;opacity:.7}
.tor-cell .ico-st{width:18px;height:18px;vertical-align:middle}
.tor-nodata{opacity:.45}
.tor-h3{font-size:15px;margin:22px 0 8px}
.tor-hist-grid{display:flex;flex-direction:column;gap:6px}
.tor-hist-row{display:flex;align-items:center;gap:10px;font-size:13px}
.tor-hist-label{flex:0 0 150px;color:var(--text-secondary)}
.tor-hist{display:flex;gap:2px;flex:1;min-width:0}
.tor-bar{display:block;flex:1;min-width:3px;max-width:14px;height:18px;border-radius:2px;background:rgba(127,127,127,.18)}
.tor-bar-ok{background:#33783E}.tor-bar-partial{background:#FF580A}.tor-bar-fail{background:#C10F34}
.tor-note,.tor-empty{color:var(--text-secondary);font-size:13px;line-height:1.5;margin:14px 0 0}
.tor-empty{padding:20px 0;text-align:center}
@media (max-width:540px){.tor-hist-label{flex-basis:96px;font-size:12px}table.tor-tbl th,table.tor-tbl td{padding:8px 5px}}
