: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}
/* height:100dvh (dynamic viewport) is more reliable than 100% on mobile
   Firefox / iOS — with plain height:100% the overflow:hidden shell can fail to
   establish (page becomes one tall scroll, the fixed preloader then covers only
   the first screen). 100% is the fallback for engines without dvh. */
html{height:100%;height:100dvh;background:var(--bg-page);overscroll-behavior:none;-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. */
.scroll-wrap{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;overscroll-behavior:none;-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{position:relative;z-index:3;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}
/* `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:visible!important;min-width:0;background:rgba(255,255,255,.08);border-radius:8px;padding:2px}
[data-theme="light"] .tg-fs .tab-track{background:rgba(0,0,0,.06)}
.tg-fs .tab-btn{padding:5px 14px!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`. */
.tbl:has(> .tbl-vpn){padding-bottom:1px;box-shadow:0 -1px 0 0 var(--border-table),1px 0 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}
/* 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;
}
.tbl{overflow-x:auto;-webkit-overflow-scrolling:touch;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{position:fixed;inset:0;width:100%;height:100%;z-index:99999;background:#000000;display:flex;align-items:center;justify-content:center;opacity:1;transition:opacity .6s cubic-bezier(.4,0,.2,1)}
#preloader::before{content:'';position:absolute;inset:0;background:url('./static/backround-main.svg?v=20260609') center/cover no-repeat;opacity:1;pointer-events:none;animation:plBgDim 1.8s cubic-bezier(.4,0,.2,1) .2s forwards}
/* animated bloom on the preloader — same dark-theme radials as the live site,
   fading/settling in to the site's final state (opacity .65). Sits above the
   bg (::before) and below the logo (.pl-wrap has z-index:1). */
#preloader::after{content:'';position:absolute;inset:0;pointer-events:none;opacity:.65;background:radial-gradient(80% 70% at 22% 6%,rgba(197,15,53,.20),transparent 72%),radial-gradient(70% 62% at 82% 16%,rgba(40,12,60,.1),transparent 72%),radial-gradient(115% 100% at 46% 46%,rgba(197,15,53,.20),transparent 78%);animation:plBloomIn 1.8s cubic-bezier(.4,0,.2,1) .2s both}
@keyframes plBloomIn{from{opacity:.4;transform:scale(1.08)}to{opacity:.65;transform:scale(1)}}
#preloader.done{opacity:0;pointer-events:none}
/* Fluid sizing (clamp) so the preloader scales smoothly with the viewport and
   the wide .pl-title SVG can NEVER overflow a narrow screen — supersedes the
   old fixed px + the ≤540 breakpoint. max-width caps the row to the screen. */
.pl-wrap{display:flex;align-items:stretch;gap:clamp(12px,4vw,28px);position:relative;z-index:1;max-width:calc(100vw - 28px)}
.pl-icon{position:relative;width:clamp(64px,22vw,160px);height:clamp(64px,22vw,160px);flex-shrink:0;overflow:hidden;border-radius:12px;box-shadow:0 10px 40px -16px rgba(0,0,0,.85),0 2px 8px -4px rgba(0,0,0,.6)}
.pl-rect{position:relative;width:clamp(64px,22vw,160px);height:clamp(64px,22vw,160px);clip-path:inset(100% 0 0 0);animation:plRevealRect .9s cubic-bezier(.22,1,.36,1) .15s forwards;border-radius:12px;overflow:hidden}
.pl-rect svg{width:100%;height:100%;display:block;opacity:.62}
.pl-rect::after{content:'';position:absolute;inset:0;border-radius:12px;background:linear-gradient(135deg,rgba(255,255,255,.22),rgba(255,255,255,.06) 34%,transparent 60%);box-shadow:inset 0 1px 0 rgba(255,255,255,.20),inset 0 0 0 1px rgba(255,255,255,.14);pointer-events:none}
.pl-fist{position:absolute;bottom:0;left:50%;height:92%;aspect-ratio:166/231;transform:translate(-50%,40px);opacity:0;animation:plFistUp .85s cubic-bezier(.22,1,.36,1) .45s forwards}
.pl-fist svg,.pl-fist img{width:100%;height:100%;display:block;object-fit:contain;image-rendering:-webkit-optimize-contrast}
.pl-text{display:flex;flex-direction:column;justify-content:space-between;padding:6px 0}
.pl-title{height:clamp(38px,11vw,115px);min-width:0;opacity:0;transform:translateX(60px);animation:plSlideText .85s cubic-bezier(.22,1,.36,1) .75s forwards}
.pl-title svg{height:100%;width:auto;max-width:100%;display:block}
.pl-sub{height:clamp(8px,2.6vw,16px);align-self:flex-end;opacity:0;transform:translateX(40px);animation:plSlideText .85s cubic-bezier(.22,1,.36,1) 1.05s forwards}
.pl-sub svg{height:100%;width:auto;max-width:100%;display:block}
@keyframes plRevealRect{to{clip-path:inset(0 0 0 0)}}
@keyframes plFistUp{to{opacity:1;transform:translate(-50%,0%)}}
@keyframes plSlideText{to{opacity:1;transform:translateX(0)}}
@keyframes plBgDim{to{opacity:.25}}
@media(max-width:540px){
  .pl-fist{transform:translate(-50%,24px)}
  .pl-text{padding:4px 0}
}

/* 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-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}
}
@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}
}
@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}
}
[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)}
.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: keep the below-content blocks out of layout until the app has rendered
   the table (body.app-ready, set in app.js). Otherwise they get shoved down as
   #content fills → big layout shift. A load/timeout backstop in index.html sets
   app-ready even if the render hook misfires, so they always appear. */
body:not(.app-ready) .xp-section,
body:not(.app-ready) footer{display:none}
.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}

/* ════════════════════════════════════════════════════════════════════
   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}
}
