/* ===========================================================================
   Medusa Proxy — deep-sea. No framework, no external asset, no web font.

   Two rules govern this file:

   1. Nothing with `position: fixed` or `sticky` exists except the decorative
      backdrop (`.bg`, z-index -1, pointer-events none). Every panel — the VPN
      pitch included — lives in the document flow, so no layer can ever cover
      the button a blocked user came here to press.

   2. Exactly one element on the page wears the filled cyan gradient: the
      «Подключиться» button. The VPN pitch is deliberately an outline. The ad
      must never be the brightest object on a screen belonging to the product.

   Scales are declared once below and are not improvised per element:
      type    11 · 12.5 · 14 · 16 · 20 · 26 · fluid-hero      (7 steps)
      weight  400 · 600 · 700                                 (3 steps)
      radius  8 · 12 · 20 · pill                              (4 steps)
      space   4 · 8 · 12 · 16 · 24 · 32 · 48                  (7 steps)
   =========================================================================== */

:root {
  /* --- ink ------------------------------------------------------------- */
  --ink-0: #02060c;
  --ink-1: #050b14;
  --ink-2: #0a1526;
  --ink-3: #102039;

  /* --- text (all ≥ 4.6:1 on --ink-2) ----------------------------------- */
  --text: #dfeaf7;
  --text-2: #a9bcd2;
  --muted: #8ba3bf;

  --line: rgba(142, 194, 238, 0.14);
  --line-2: rgba(142, 194, 238, 0.26);

  /* --- accents --------------------------------------------------------- */
  --cyan: #8fddff;
  --cyan-2: #52b9f2;
  --glow: rgba(120, 205, 255, 0.35);

  /* --- health ---------------------------------------------------------- */
  --up: #5ce3ac;
  --down: #ff8b97;
  --warn: #ffc46b;
  --unknown: #93a8c0;

  /* --- type ------------------------------------------------------------ */
  --t-xs: 11px;
  --t-sm: 12.5px;
  --t-md: 14px;
  --t-base: 16px;
  --t-lg: 20px;
  --t-xl: 26px;
  --t-hero: clamp(26px, 6.2vw, 38px);

  /* --- radius ---------------------------------------------------------- */
  --r-in: 8px;
  --r-btn: 12px;
  --r-tile: 20px;
  --r-pill: 999px;

  /* --- space ----------------------------------------------------------- */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;

  --shell: 1120px;
  --pad: 16px;

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, "Noto Sans", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", "Cascadia Mono", "Segoe UI Mono",
    Menlo, Consolas, monospace;
  --ease: cubic-bezier(0.22, 0.72, 0.28, 1);
}

* { box-sizing: border-box; }

/* author display rules (flex/grid) otherwise beat the UA [hidden] rule —
   this keeps the tg:// fallback and detail blocks genuinely hidden */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  /* svh, not vh: on iOS `100vh` counts the space behind the collapsible URL
     bar, so a page that fits exactly still scrolls a few pixels. */
  min-height: 100vh;
  min-height: 100svh;
  /* The footer used to sit wherever the content happened to end. With one
     server that was mid-screen, leaving a dead band beneath it that read as a
     page cut off rather than a page that is simply short. main now absorbs the
     slack, so the footer always lands on the bottom edge. */
  display: flex;
  flex-direction: column;
  font-family: var(--font);
  font-size: var(--t-base);
  line-height: 1.5;
  color: var(--text);
  background: var(--ink-0);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3 { margin: 0; font-weight: 700; line-height: 1.12; letter-spacing: -0.025em; }
p, dl, dd, ul, ol { margin: 0; }
ul, ol { padding: 0; }
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button, input, select { font: inherit; color: inherit; }
button { background: none; border: 0; }
svg { display: block; }

.mono { font-family: var(--mono); font-size: 0.92em; letter-spacing: -0.01em; }
.break { overflow-wrap: anywhere; }
.trunc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* screen-reader only: SEO/a11y keeps the words, the fold does not pay for them */
.sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--pad);
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: var(--s3) var(--s4);
  background: var(--ink-3); border-radius: 0 0 var(--r-btn) 0;
  font-weight: 600;
}
.skip:focus { left: 0; }

:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: var(--r-in); }

/* ============================================================== backdrop ===
   The only fixed layer on the site. Static: an animated blur under a scrolling
   list is the fastest way to make a mid-range Android feel cheap, and this
   audience is disproportionately on one. */

.bg {
  position: fixed; inset: 0; z-index: -1;
  pointer-events: none; overflow: hidden;
  background: var(--ink-0);
}
.bg__deep {
  position: absolute; inset: 0;
  background: radial-gradient(122% 80% at 50% -16%,
    #16375d 0%, #0a1a30 38%, #050c16 68%, #02060c 100%);
}
.bg__rays {
  position: absolute; inset: -18% -12% auto -12%; height: 128%;
  background:
    linear-gradient(101deg, transparent 17%, rgba(168, 218, 255, 0.10) 20.5%, transparent 24%),
    linear-gradient(96deg, transparent 37%, rgba(168, 218, 255, 0.13) 41.5%, transparent 47%),
    linear-gradient(88.5deg, transparent 57%, rgba(168, 218, 255, 0.07) 60.5%, transparent 65%),
    linear-gradient(84deg, transparent 73%, rgba(168, 218, 255, 0.11) 77.5%, transparent 82%);
  filter: blur(16px);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.5) 46%, transparent 84%);
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.5) 46%, transparent 84%);
}
.bg__dots {
  position: absolute; inset: 0;
  background-image: radial-gradient(rgba(196, 228, 255, 0.07) 1px, transparent 1.2px);
  background-size: 24px 24px;
  -webkit-mask-image: radial-gradient(125% 85% at 50% 0%, #000 0%, transparent 72%);
  mask-image: radial-gradient(125% 85% at 50% 0%, #000 0%, transparent 72%);
}
.bg__jelly {
  position: absolute; left: 50%; top: 6%;
  width: min(86vw, 660px);
  transform: translateX(-50%);
  opacity: 0.07;
  filter: drop-shadow(0 0 70px rgba(140, 205, 255, 0.4));
}
.bg__floor {
  position: absolute; inset: 45% 0 0 0;
  background: linear-gradient(180deg, transparent, rgba(2, 6, 12, 0.72) 55%, var(--ink-0) 100%);
}

/* ================================================================ topbar ===
   Not sticky. Paid for once, then the viewport belongs to the cards. */

.topbar { padding: var(--s2) 0; }
.topbar__in { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s3); }

.brand { display: inline-flex; align-items: center; gap: var(--s2); min-width: 0; }
.brand__mark {
  width: 30px; height: 30px; flex: none;
  filter: drop-shadow(0 2px 10px rgba(120, 200, 255, 0.5));
}
.brand__word { font-size: 13px; font-weight: 700; letter-spacing: 0.14em; white-space: nowrap; }
.brand__word i { font-style: normal; font-weight: 400; color: var(--muted); }

/* Ambient VPN presence: on the fold, always, at a hairline weight. 44px tall so
   it is a real tap target, not decoration you cannot hit. */
.vpill {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: 44px; padding: 0 var(--s3);
  border: 1px solid var(--line-2); border-radius: var(--r-pill);
  background: rgba(14, 32, 56, 0.5);
  font-size: var(--t-sm); font-weight: 600; color: var(--text-2);
  white-space: nowrap;
  transition: border-color 0.18s var(--ease), color 0.18s var(--ease),
              background 0.18s var(--ease);
}
.vpill b { color: var(--cyan); font-weight: 700; }
.vpill:hover { border-color: var(--cyan-2); color: var(--text); background: rgba(20, 48, 82, 0.6); }
.vpill__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 10px var(--cyan); }

.topbar__fall { flex: 1 0 100%; margin-top: var(--s2); }

.head__admin { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.head__admin form { margin: 0; }

/* ============================================================== masthead ===
   Full-bleed hairline band: the rule runs to the viewport edges so a 1920px
   screen reads as a composed band rather than a narrow column in a void. */

.masthead { border-bottom: 1px solid var(--line); padding: var(--s3) 0 var(--s4); margin-bottom: var(--s4); }
.masthead__in { display: flex; flex-direction: column; gap: var(--s3); }

.lede h1 { font-size: var(--t-hero); line-height: 1.04; letter-spacing: -0.035em; }
.lede h1 em { font-style: normal; color: var(--cyan); }
.lede p {
  margin-top: var(--s2);
  max-width: 44ch;
  font-size: var(--t-md);
  line-height: 1.45;
  color: var(--text-2);
}
.lede p b { color: var(--text); font-weight: 600; }

/* --- honest health summary ------------------------------------------------
   One 12.5px line on mobile — the only furniture between the promise and the
   first proxy. It states the live count and the age of the newest check, and
   goes amber on its own once that check is older than ten minutes. */

.status { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s1) var(--s2); font-size: var(--t-sm); color: var(--muted); }
.status__now { display: inline-flex; align-items: center; gap: var(--s2); color: var(--text-2); }
.status__now b { color: var(--text); font-weight: 700; }
.status__sep { color: var(--line-2); }

.pulse { position: relative; width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--up); box-shadow: 0 0 12px rgba(92, 227, 172, 0.7); }
.pulse::after { content: ""; position: absolute; inset: -4px; border-radius: 50%; border: 1px solid var(--up); opacity: 0; }
.pulse--none { background: var(--down); box-shadow: 0 0 12px rgba(255, 139, 151, 0.7); }
.pulse--none::after { border-color: var(--down); }
.status.is-stale .pulse { background: var(--warn); box-shadow: 0 0 12px rgba(255, 196, 107, 0.7); }
.status.is-stale .pulse::after { border-color: var(--warn); }
.status.is-stale .status__when { color: var(--warn); }

/* ================================================================ mosaic ===
   One grid. Proxy cards, the VPN pitch and the manual-setup panel are all
   tiles in it; the pitch simply spans every column. Same DOM at every width,
   no `order` tricks, reading order == visual order. */

/* Items align to the start, never stretch. A tile is allowed to be taller than
   the one beside it; it is not allowed to drag its neighbour's height up with
   it and leave a void inside a card that has nothing to put there. */
.mosaic { display: grid; gap: var(--s3); }
/* Cards no longer expand inline (the details panel is a modal), so nothing
   can inflate a row any more — stretch keeps paired tiles the same height,
   which is what the ragged empty state was missing. */

.tile {
  position: relative;
  border: 1px solid var(--line); border-radius: var(--r-tile);
  background: linear-gradient(158deg, rgba(19, 38, 64, 0.62), rgba(6, 13, 24, 0.74));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 22px 50px -34px #000;
}
.tile--wide { grid-column: 1 / -1; }

/* ------------------------------------------------------------------ cards */

.card { display: flex; flex-direction: column; padding: var(--s4) var(--s4) var(--s4); }
.card__head { display: flex; align-items: center; gap: var(--s2); }
.card__flag { font-size: var(--t-lg); line-height: 1; flex: none; }
/* the country/label is the identifying field, so it wraps rather than
   truncating — a card is allowed to be 20px taller before a user is allowed to
   be unable to tell two servers apart */
.card__title { font-size: var(--t-lg); flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.card__head .health { flex: none; }

.health { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-xs); font-weight: 600; color: var(--muted); white-space: nowrap; }
.health i { width: 8px; height: 8px; border-radius: 50%; background: var(--unknown); flex: none; }
.health--up { color: var(--up); }
.health--up i { background: var(--up); }
.health--down { color: var(--down); }
.health--down i { background: var(--down); box-shadow: 0 0 8px rgba(255, 139, 151, 0.55); }
/* A green "работает" that outlives its own check is the dishonest state the
   whole page exists to avoid — the client flips this per card, not just on the
   summary line. */
.card.is-stale .health--up { color: var(--warn); }
.card.is-stale .health--up i { background: var(--warn); animation: none; }
.health__old { display: none; }
.card.is-stale .health__old { display: inline; }
.card.is-stale .health__now { display: none; }

.card__meta { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: var(--s1); font-size: var(--t-sm); color: var(--muted); }
.card__meta .sep { opacity: 0.45; }
.badge {
  padding: 2px var(--s2); border-radius: var(--r-pill);
  border: 1px solid var(--line-2); background: rgba(70, 140, 200, 0.12);
  font-size: var(--t-xs); font-weight: 600; letter-spacing: 0.06em; color: var(--text-2);
}
.ping { color: var(--up); font-variant-numeric: tabular-nums; }
.card.is-stale .ping { color: var(--muted); }

.card__host { margin-top: var(--s1); font-size: var(--t-sm); color: var(--text-2); overflow-wrap: anywhere; }
.card__host b { font-weight: 600; color: var(--text); }

.card__note {
  margin-top: var(--s2); padding: var(--s2) var(--s3);
  border: 1px solid rgba(255, 139, 151, 0.22); border-radius: var(--r-in);
  background: rgba(90, 24, 34, 0.2);
  font-size: var(--t-sm); line-height: 1.4; color: #f4cfd4;
}

.card__acts { display: flex; gap: var(--s2); margin-top: var(--s2); }
.card__acts .btn--go { flex: 1 1 auto; }
.ico { opacity: 0.7; }

/* A server that did not answer is dimmed and demoted, never hidden and never
   silently promoted: its data stays one tap away and it keeps an escape hatch,
   because some operators still reach it. */
.card--down { opacity: 0.82; }
.card--down .card__title { color: var(--text-2); }

/* raw values, revealed on demand — the manual path needs no network at all */
.kv { display: grid; gap: 6px; }
.kv > div { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: var(--s2); align-items: baseline; font-size: var(--t-md); }
.kv dt { color: var(--muted); font-size: var(--t-sm); }
.kv dd { min-width: 0; color: var(--text); }

/* ============================================================= data sheet ===
   The «Данные» panel. It used to expand inside the card, which made the card
   grow inside a grid row and stretched the tile next to it into a tall empty
   box. A detail view must not be able to touch the list it came from, so it is
   a modal: one native <dialog> for the whole page, filled from the card that
   opened it. The dialog element itself is the full-viewport scrim and the panel
   is its child — that makes "click outside" a plain identity check, and it is
   the one fixed layer allowed to cover a card, because it only exists while the
   user is holding it open.

   Desktop: a centred panel. Phone: a bottom sheet, where a thumb already is. */

.sheet {
  position: fixed; inset: 0; z-index: 90;
  width: 100%; max-width: 100%; height: 100%; max-height: 100%;
  margin: 0; padding: 0; border: 0;
  display: none;
  align-items: center; justify-content: center;
  overflow: hidden; overscroll-behavior: contain;
  color: var(--text);
  /* the scrim proper is ::backdrop; this tint also covers browsers that show
     the dialog without one */
  background: rgba(3, 8, 16, 0.28);
}
.sheet[open] { display: flex; }
/* Deliberately no viewport units on the dialog itself. This box is a scrim and
   a tap target and nothing else — on a phone it no longer places the panel (see
   .sheet__in in the 639px block), so it has no reason to track the visible area,
   and dvh here was actively harmful: it is re-resolved on every toolbar
   movement, and anything anchored to the bottom of a box whose height is
   re-resolved that way inherits each intermediate value on the way. 100% of the
   initial containing block is the one figure iOS never revises, and being a
   little taller than the visible area costs nothing when the only jobs are
   "tint" and "catch a tap outside". */
.sheet::backdrop {
  background: rgba(3, 8, 16, 0.52);
  -webkit-backdrop-filter: blur(10px) saturate(0.9);
  backdrop-filter: blur(10px) saturate(0.9);
}

.sheet__in {
  position: relative;
  display: flex; flex-direction: column; min-height: 0;
  width: min(520px, calc(100% - 2 * var(--pad)));
  max-height: calc(100% - 2 * var(--pad));
  margin: var(--pad);
  padding: var(--s4);
  border: 1px solid var(--line-2); border-radius: var(--r-tile);
  background: linear-gradient(158deg, rgba(21, 43, 71, 0.97), rgba(6, 13, 24, 0.98));
  box-shadow: 0 34px 90px -30px #000, inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

/* the panel is only ever focused programmatically, to keep the ring off the
   close button; it is not a control, so it draws no ring of its own */
.sheet__in:focus, .sheet__in:focus-visible { outline: none; }

/* app.js holds this class for the first two animation frames after showModal().
   The panel is not painted during them — on iOS the frames immediately after a
   dialog enters the top layer are laid out against a viewport the engine has not
   finished settling, and the panel was visibly placed wrong in one of them
   before snapping into position. Which of the several candidate mechanisms is
   to blame could not be established without the device, so this fixes the class
   of bug instead of one instance of it: no unsettled frame is ever shown.
   The scrim is deliberately NOT held back, so the tap still gets instant
   feedback and the delay is imperceptible. */
.sheet.is-settling .sheet__in { visibility: hidden; }

.sheet__grip { display: none; }
.sheet__head { display: flex; align-items: flex-start; gap: var(--s3); flex: none; }
.sheet__title { font-size: var(--t-lg); flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.sheet__x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; margin: -8px -8px 0 0; flex: none; cursor: pointer;
  border: 1px solid transparent; border-radius: var(--r-in); color: var(--muted);
  transition: color 0.18s var(--ease), background 0.18s var(--ease), border-color 0.18s var(--ease);
}
.sheet__x:hover { color: var(--text); border-color: var(--line); background: rgba(24, 47, 78, 0.65); }

/* the long ee-secret gets room to wrap, and the panel scrolls rather than
   truncating anything into uselessness */
.sheet__body {
  margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--line);
  overflow-y: auto; overscroll-behavior: contain; min-height: 0;
  -webkit-overflow-scrolling: touch;
}
.sheet__warn { margin-top: 0; margin-bottom: var(--s3); }
.sheet__acts { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s4); }
.sheet__acts .btn { flex: 1 1 170px; }
.sheet__hint { margin-top: var(--s3); font-size: var(--t-sm); line-height: 1.4; color: var(--muted); }

/* Phone: a bottom sheet — full width, rounded at the top, clear of the home
   indicator. Authored here rather than in the breakpoint section because it
   overrides nothing but the six rules directly above it. */
@media (max-width: 639px) {
  .sheet { align-items: flex-end; }
  .sheet__in {
    width: 100%; max-height: 92%; margin: 0;
    padding-bottom: calc(var(--s4) + env(safe-area-inset-bottom, 0px));
    border-radius: var(--r-tile) var(--r-tile) 0 0;
    border-bottom: 0;
  }
  .sheet__grip {
    display: block; width: 42px; height: 4px; margin: 0 auto var(--s3);
    border-radius: var(--r-pill); background: var(--line-2);
  }
}

/* Revealed only after an open attempt demonstrably did not leave the page.
   The wording never diagnoses ("Telegram не установлен") — a slow cold start or
   a confirmation sheet looks identical from here, and telling someone their
   working app is missing at peak anxiety is the one unforgivable lie. */
.fallback {
  margin-top: var(--s3); padding: var(--s3);
  border: 1px solid rgba(255, 196, 107, 0.26); border-radius: var(--r-in);
  background: rgba(74, 56, 22, 0.24);
}
.fallback p { font-size: var(--t-sm); line-height: 1.45; color: #f3e0c2; }
.fallback p b { color: #ffd9a8; font-weight: 600; }
.fallback__acts { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s2); }

/* ------------------------------------------------------------- the receipt
   Grafted from the "moment" direction. After a tg:// link demonstrably opened
   the app, the card the user just pressed grows a receipt in place: it tells
   them the one thing they still have to do inside Telegram, and only then
   mentions the VPN. It pushes, never covers; it is capped so the card grows
   rather than detonates; it fires once per session; and it is hard-suppressed
   after any failed attempt, so it can never claim a success that did not
   happen. */
.receipt {
  margin-top: var(--s3); padding: var(--s3);
  border: 1px solid rgba(92, 227, 172, 0.28); border-radius: var(--r-in);
  background: linear-gradient(150deg, rgba(16, 58, 46, 0.5), rgba(8, 26, 34, 0.5));
}
.receipt__top { display: flex; align-items: flex-start; gap: var(--s2); font-size: var(--t-md); line-height: 1.35; color: var(--text); }
.receipt__tick { color: var(--up); font-weight: 700; flex: none; }
.receipt__acts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); margin-top: var(--s3); }
.receipt__hide { font-size: var(--t-sm); color: var(--muted); text-decoration: underline; cursor: pointer; padding: var(--s2) 0; }
.receipt__hide:hover { color: var(--text-2); }

/* ------------------------------------------------------------- VPN pitch --
   Third in the flow, after the two best live servers. Not sticky (that covered
   a card), not appended after the list (that was unreachable on a phone, and
   drifted further down with every server added). A tile in the same grid is
   the only position that is neither, and it is stable at any list length.
   Chrome is identical to a proxy card — same border, same background, no
   brighter edge, no blend mode — so it reads as the next section, not an ad. */

.promo { padding: var(--s4); overflow: hidden; }
.promo__aura {
  position: absolute; right: -20%; top: -60%; width: 70%; height: 220%;
  background: radial-gradient(45% 45% at 50% 50%, rgba(120, 200, 255, 0.16), transparent 70%);
  pointer-events: none;
}
.promo__body { position: relative; }
.promo__eyebrow { display: inline-flex; align-items: center; gap: var(--s2); font-size: var(--t-xs); font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--cyan); }
/* the one place the jellyfish is fully itself: sharp, opaque, small */
.promo__mark { width: 20px; height: 20px; flex: none; }
.promo__title { margin-top: var(--s3); font-size: var(--t-lg); line-height: 1.2; }
.promo__title em { font-style: normal; color: var(--cyan); }
.promo__perks { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s3); margin-top: var(--s2); font-size: var(--t-sm); color: var(--text-2); list-style: none; }
.promo__perks li { display: inline-flex; align-items: center; gap: 6px; }
.promo__perks li::before { content: ""; width: 4px; height: 4px; border-radius: 50%; background: var(--cyan-2); flex: none; }
.promo__cta { position: relative; display: flex; flex-direction: column; gap: var(--s2); margin-top: var(--s3); }

.promocode {
  display: flex; align-items: center; gap: var(--s2);
  min-height: 44px; padding: 0 var(--s3); cursor: pointer; text-align: left;
  border: 1px solid var(--line-2); border-radius: var(--r-btn);
  background: rgba(10, 30, 52, 0.6);
  transition: border-color 0.18s var(--ease), background 0.18s var(--ease);
}
.promocode:hover { border-color: var(--cyan-2); background: rgba(14, 40, 70, 0.7); }
.promocode__k { font-family: var(--mono); font-size: var(--t-md); font-weight: 700; letter-spacing: 0.08em; color: var(--cyan); }
.promocode__v { font-size: var(--t-sm); color: var(--muted); line-height: 1.2; }
.promocode__ico { margin-left: auto; color: var(--muted); flex: none; }
.promocode.is-done { border-color: var(--up); }
.promocode.is-done .promocode__k { color: var(--up); }
.promocode.is-done .promocode__v { color: var(--up); }

/* ------------------------------------------------------------- help tile */


/* -------------------------------------------------------------- show more */

.more { margin-top: var(--s3); }
.more__btn { width: 100%; }

/* ------------------------------------------------------------ empty state */

.empty { padding: var(--s6) var(--s4); text-align: center; }
.empty__mark { margin: 0 auto var(--s3); opacity: 0.7; }
.empty__title { font-size: var(--t-lg); }
.empty__text { max-width: 46ch; margin: var(--s3) auto 0; color: var(--text-2); font-size: var(--t-md); }

/* =============================================================== buttons ===
   One filled style, used once per card, for one job. */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 44px; padding: 0 var(--s4);
  font-size: var(--t-md); font-weight: 600; white-space: nowrap;
  border: 1px solid transparent; border-radius: var(--r-btn); cursor: pointer;
  transition: transform 0.14s var(--ease), box-shadow 0.2s var(--ease),
              background 0.2s var(--ease), border-color 0.2s var(--ease), color 0.2s var(--ease);
}
.btn:active { transform: translateY(1px); }

.btn--go {
  color: #04121f;
  background: linear-gradient(118deg, #b7e9ff 0%, var(--cyan) 42%, var(--cyan-2) 100%);
  box-shadow: 0 10px 28px -14px var(--glow), inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.btn--go:hover { filter: brightness(1.05); }

/* the admin's one primary action per screen. Same ink as --go: there is no
   second "important" button on those pages, so a distinct treatment would only
   invent a hierarchy that does not exist. */
.btn--primary {
  color: #04121f; font-weight: 700;
  background: linear-gradient(118deg, #b7e9ff 0%, var(--cyan) 42%, var(--cyan-2) 100%);
  box-shadow: 0 10px 28px -14px var(--glow), inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.btn--primary:hover { filter: brightness(1.05); }

/* the VPN call to action: cyan, but never filled */
.btn--out { color: var(--cyan); border-color: var(--line-2); background: rgba(12, 34, 58, 0.5); }
.btn--out:hover { border-color: var(--cyan); background: rgba(18, 48, 80, 0.66); }

.btn--quiet, .btn--ghost { color: var(--text-2); background: rgba(16, 32, 55, 0.55); border-color: var(--line); }
.btn--quiet:hover, .btn--ghost:hover { color: var(--text); border-color: var(--line-2); background: rgba(24, 47, 78, 0.65); }

.btn--danger { color: #ffb9c0; background: rgba(120, 30, 42, 0.32); border-color: rgba(255, 117, 131, 0.28); }
.btn--danger:hover { color: #fff; background: rgba(160, 38, 54, 0.5); border-color: var(--down); }

.btn--sm { min-height: 36px; padding: 0 var(--s3); font-size: var(--t-sm); }
.btn--lg { min-height: 48px; padding: 0 var(--s5); }
.btn--full { width: 100%; }

/* copy feedback lands on the control itself — there is no floating toast to
   momentarily sit over a card */
.btn.is-done, .btn--go.is-done { color: #04121f; background: linear-gradient(118deg, #a9f0cd, var(--up)); border-color: transparent; }

.btn.is-off {
  color: var(--muted); background: rgba(16, 28, 46, 0.55); border-color: var(--line);
  pointer-events: none;
}

/* ================================================================ footer === */

main { flex: 1 0 auto; }
.foot { margin-top: var(--s6); border-top: 1px solid var(--line); }
.foot__in {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  align-items: center; justify-content: space-between;
  padding-top: var(--s5); padding-bottom: var(--s5);
  font-size: var(--t-sm); color: var(--muted);
}
.foot__in a { color: var(--text-2); border-bottom: 1px solid var(--line-2); }
.foot__in a:hover { color: var(--cyan); }
.foot__dot { opacity: 0.5; }
.foot__fall { flex-basis: 100%; }

/* ================================================================= forms === */

.field { display: block; }
.field__label { display: block; margin-bottom: 6px; font-size: var(--t-sm); color: var(--text-2); }
.field__hint { color: var(--muted); font-size: var(--t-xs); font-weight: 400; }

.input {
  width: 100%; min-height: 44px; padding: var(--s3); color: var(--text);
  background: rgba(4, 11, 21, 0.72); border: 1px solid var(--line);
  border-radius: var(--r-btn); outline: none;
  transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease), background 0.2s var(--ease);
}
.input::placeholder { color: rgba(140, 165, 195, 0.45); }
.input:focus { border-color: var(--cyan); background: rgba(8, 20, 36, 0.85); box-shadow: 0 0 0 3px rgba(120, 205, 255, 0.14); }
select.input { appearance: none; cursor: pointer; }

.check { display: flex; align-items: center; gap: var(--s2); font-size: var(--t-md); color: var(--text-2); cursor: pointer; }
.check input { width: 18px; height: 18px; accent-color: #6cc9f5; cursor: pointer; }

.alert { padding: var(--s3) var(--s4); margin-bottom: var(--s4); font-size: var(--t-md); border-radius: var(--r-btn); border: 1px solid var(--line); }
.alert--err { color: #ffc0c7; background: rgba(120, 30, 42, 0.28); border-color: rgba(255, 117, 131, 0.3); }
.alert--ok { color: #b8f5da; background: rgba(30, 110, 80, 0.24); border-color: rgba(92, 227, 172, 0.3); }

/* ================================================================= login === */

.login { display: grid; place-items: center; min-height: 60vh; padding: var(--s5) var(--pad); }
/* the card carried no surface of its own — it inherited nothing, so the form
   floated on the page background. It is a .tile like every other panel. */
.login__card {
  width: 100%; max-width: 380px; padding: var(--s6) var(--s5); text-align: center;
  border: 1px solid var(--line); border-radius: var(--r-tile);
  background: linear-gradient(158deg, rgba(19, 38, 64, 0.62), rgba(6, 13, 24, 0.74));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 22px 50px -34px #000;
}
.login__mark { margin: 0 auto var(--s4); opacity: 0.85; }
.login__title { font-size: var(--t-lg); }
.login__sub { margin-bottom: var(--s5); font-size: var(--t-xs); letter-spacing: 0.2em; color: var(--muted); }
.login__form { display: grid; gap: var(--s3); text-align: left; }

/* ================================================================= admin === */

.admin { padding: var(--s5) 0 var(--s7); }
.admin__title { font-size: var(--t-xl); margin-bottom: var(--s5); }

/* An empty list is a state, not an accident: it gets a drawn container so the
   page still reads as "the list lives here" instead of a lost sentence. */
.admin__empty {
  padding: var(--s6) var(--s4); text-align: center; color: var(--muted); font-size: var(--t-md);
  border: 1px dashed var(--line-2); border-radius: var(--r-tile);
  background: rgba(10, 22, 40, 0.35);
}

/* .panel was padding and nothing else, so the form had no edges — it read as
   loose text on the page. It is the same surface as a public .tile. */
.panel {
  padding: var(--s5); margin-bottom: var(--s6);
  border: 1px solid var(--line); border-radius: var(--r-tile);
  background: linear-gradient(158deg, rgba(19, 38, 64, 0.62), rgba(6, 13, 24, 0.74));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 22px 50px -34px #000;
}
.panel__title { font-size: var(--t-lg); }
.form__head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  margin-bottom: var(--s4); padding-bottom: var(--s3); border-bottom: 1px solid var(--line);
}

/* Twelve tracks instead of auto-fit: a port is not as wide as a secret, and
   auto-fit gave every field the same arbitrary width whatever it held. */
.form__grid { display: grid; gap: var(--s4) var(--s3); grid-template-columns: repeat(12, 1fr); margin-bottom: var(--s4); }
.form__grid > .field { grid-column: span 12; }
.field--wide { grid-column: span 12; }
.field--full { grid-column: 1 / -1; }
.form__foot { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; }

/* Paste-a-link shortcut. It sits above the fields it fills and is separated by
   a rule, so it reads as "or start from a link" rather than as a tenth field
   someone has to work out whether to fill in. */
.paste { margin-bottom: var(--s4); padding-bottom: var(--s4); border-bottom: 1px solid var(--line); }
.paste .field__label { display: block; margin-bottom: 6px; }
.paste__row { display: flex; gap: var(--s2); align-items: stretch; }
.paste__row .input { flex: 1 1 auto; min-width: 0; }
.paste__row .btn { flex: none; }
.paste__msg { margin-top: var(--s2); font-size: var(--t-sm); }
.paste__msg--ok { color: var(--up); }
.paste__msg--err { color: #ffc0c7; }
.form__foot .btn--primary { min-width: 190px; }

/* minmax(0, 1fr), not the default auto: an auto track is sized to max-content,
   so a row kept every action button on one line and pushed itself past the
   viewport on a phone instead of letting them wrap. */
.rows { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s3); }
.row {
  min-width: 0;
  display: flex; flex-wrap: wrap; gap: var(--s3); align-items: flex-start; justify-content: space-between;
  padding: var(--s4); border: 1px solid var(--line); border-radius: var(--r-tile);
  background: linear-gradient(158deg, rgba(19, 38, 64, 0.5), rgba(6, 13, 24, 0.6));
}
.row--off { opacity: 0.55; }
.row__main { min-width: 0; flex: 1 1 320px; }
.row__head { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; margin-bottom: var(--s2); }
.row__addr { font-size: var(--t-base); font-weight: 600; overflow-wrap: anywhere; }
.row__sub { font-size: var(--t-sm); color: var(--muted); margin-top: var(--s1); }
.row__link { font-size: var(--t-xs); color: var(--muted); margin-top: var(--s1); max-width: 100%; }
.row__acts { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }
.row__acts form { margin: 0; }

/* admin keeps the legacy status/type chips */
.tag { padding: 2px var(--s2); font-size: var(--t-xs); font-weight: 600; letter-spacing: 0.06em; border-radius: var(--r-pill); border: 1px solid var(--line-2); color: var(--text-2); background: rgba(70, 140, 200, 0.12); }
.tag--off { color: var(--muted); background: rgba(120, 140, 165, 0.12); }
.dot { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-sm); color: var(--muted); white-space: nowrap; }
.dot i { width: 8px; height: 8px; border-radius: 50%; background: var(--unknown); flex: none; }
.dot--up { color: var(--up); }
.dot--up i { background: var(--up); }
.dot--down { color: var(--down); }
.dot--down i { background: var(--down); }

/* ========================================================== breakpoints ====
   Authored after every base rule they override — the reverse order silently
   kills half of them at equal specificity. */

/* Narrow phones: the wordmark and the VPN pill must share one row, or the
   header steals 26px from the fold. The pill drops "Medusa" (the wordmark two
   centimetres to its left already said it) rather than wrapping. */
@media (max-width: 399px) {
  .vpill__long { display: none; }
  .brand__word { letter-spacing: 0.1em; }
}

@media (min-width: 400px) {
  :root { --pad: 18px; }
}

/* two columns: the pitch spans both and becomes a horizontal band */
/* Once a card is wider than a phone it stops being a stack and becomes a dense
   row: identity on the left, actions on the right. This starts well before the
   two-column mosaic so a tablet in portrait never shows a 700px-wide card with
   a full-width button glued to the bottom of it. */
@media (min-width: 620px) {
  .card { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: var(--s4); }
  .card__head, .card__meta, .card__host, .card__note { grid-column: 1; }
  /* the actions stack on the right so the identity column keeps enough width
     for the country name and the meta line to stay on one line each */
  .card__acts { grid-column: 2; grid-row: 1 / span 3; flex-direction: column; align-items: stretch; margin-top: 0; }
  .card__note { grid-column: 1 / -1; }
  .fallback, .receipt { grid-column: 1 / -1; }
  .card__acts .btn--go { flex: none; }
}

@media (min-width: 780px) {
  :root { --pad: 24px; }

  .mosaic { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); }
  .promo { grid-column: 1 / -1; }

  .masthead { padding: var(--s5) 0; margin-bottom: var(--s5); }
  .masthead__in { display: grid; grid-template-columns: minmax(0, 1fr) 340px; align-items: center; gap: var(--s5); }
  .lede p { font-size: var(--t-base); max-width: 46ch; }
  .status {
    align-content: center; gap: var(--s2);
    padding-left: var(--s5); border-left: 1px solid var(--line);
    min-height: 88px;
  }
  .status__sep { display: none; }
  .status__now, .status__when { flex-basis: 100%; }

  .promo { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--s3) var(--s5); padding: var(--s5); }
  .promo__title { font-size: var(--t-xl); margin-top: var(--s3); }
  .promo__cta { align-items: stretch; margin-top: 0; min-width: 240px; }
  .promo .fallback { grid-column: 1 / -1; margin-top: 0; }

  .foot__in { padding-top: var(--s5); padding-bottom: var(--s6); }
}

@media (min-width: 1080px) {
  .promo { gap: var(--s3) var(--s7); padding: var(--s5) var(--s6); }
}

/* Very wide screens: the band must not become a short headline stranded beside
   a small panel. The hero grows, its measure grows, and the status panel gains
   mass, so the two columns meet in the middle instead of clinging to the edges.
   The full-bleed rules above and below do the rest of the anchoring. */
@media (min-width: 1400px) {
  :root { --shell: 1280px; --pad: 32px; --t-hero: 44px; }
  .masthead__in { grid-template-columns: minmax(0, 1fr) 400px; }
  .lede p { max-width: 52ch; }
}

/* An empty list renders no counter at all, so the band must not keep a 340px
   column reserved for one. Two classes, and after the breakpoints, so it wins
   at every width. */
.masthead__in.is-solo {
  display: flex; flex-direction: column; grid-template-columns: none;
  /* the wide breakpoint centres the two columns against each other; with one
     column that would centre the headline while every tile below it stays
     left-aligned */
  align-items: stretch;
}

/* ======================================================== reduced motion ===
   Animation stops; meaning does not. The liveness dot degrades to a static
   ring instead of vanishing, because it carries information. */

@media (prefers-reduced-motion: no-preference) {
  .pulse::after { animation: ping 2.6s var(--ease) infinite; }
  .health--up i { animation: beat 2.4s var(--ease) infinite; }
  .fallback.is-in, .receipt.is-in { animation: slide 0.24s var(--ease) both; }

  /* the sheet arrives, it does not perform: one short move, no bounce */
  .sheet[open], .sheet[open]::backdrop { animation: fade 0.2s var(--ease) both; }
  .sheet[open]:not(.is-settling) .sheet__in { animation: rise 0.26s var(--ease) both; }
}
@media (prefers-reduced-motion: no-preference) and (max-width: 639px) {
  .sheet[open]:not(.is-settling) .sheet__in { animation: sheetup 0.28s var(--ease) both; }
}

@keyframes ping {
  0%   { opacity: 0.7; transform: scale(0.6); }
  70%  { opacity: 0; transform: scale(1.5); }
  100% { opacity: 0; transform: scale(1.5); }
}
@keyframes beat {
  0%   { box-shadow: 0 0 0 0 rgba(92, 227, 172, 0.45); }
  70%  { box-shadow: 0 0 0 7px rgba(92, 227, 172, 0); }
  100% { box-shadow: 0 0 0 0 rgba(92, 227, 172, 0); }
}
@keyframes slide {
  from { opacity: 0; transform: translateY(-5px); }
  to   { opacity: 1; transform: none; }
}
@keyframes fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes rise {
  from { opacity: 0; transform: translateY(12px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes sheetup {
  from { transform: translateY(100%); }
  to   { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .pulse::after { opacity: 0.55; transform: none; }
}

/* forced-colors: nothing on this page may rely on a gradient or a shadow to be
   readable, and the health colours must survive */
@media (forced-colors: active) {
  .tile, .btn, .promocode, .input, .sheet__in { border-color: CanvasText; }
  .sheet { background: Canvas; }
  .btn--go { background: Highlight; color: HighlightText; forced-color-adjust: none; }
  .health i, .pulse { forced-color-adjust: none; }
  .bg { display: none; }
}

/* ------------------------------------------------------------------ copy swap
   Both labels occupy the SAME grid cell, so the control is always as wide as
   the longer of the two and confirming cannot resize it (or shove its
   neighbours around). Only opacity changes. */
.swap { display: inline-grid; }
.swap > span { grid-area: 1 / 1; }
.swap__a, .swap__b { transition: opacity .18s ease; }
.swap__b { opacity: 0; }   /* colour is inherited: the button that turns green
                              already sets dark ink on itself, and hard-coding
                              --up here printed green on green */
.is-done .swap__a { opacity: 0; }
.is-done .swap__b { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .swap__a, .swap__b { transition: none; }
}

/* ============================================================ admin layout ==
   Authored last on purpose: these override base rules at equal specificity, so
   anywhere earlier in the file half of them would lose silently. */

/* Phone: one field per row, and a proxy row becomes a stack whose actions sit
   on their own line — four buttons cannot share a 390px row with an address. */
@media (max-width: 719px) {
  /* nowrap matters here: the base rule wraps, and a WRAPPING column container
     is multi-line, so `stretch` sizes an item to its own flex line (max-content)
     instead of to the row. That is what pushed the list past a 390px screen. */
  .row { flex-direction: column; flex-wrap: nowrap; align-items: stretch; }
  /* the flex items here are a mix of bare buttons and <form>-wrapped ones, so
     the sizing has to sit on the direct children — putting it on .btn left the
     two wrapped actions sized by content and unable to share a line */
  .row__acts { width: 100%; }
  .row__acts > * { flex: 1 1 auto; min-width: 0; }
  .row__acts > form { display: flex; }
  .row__acts > form > .btn { width: 100%; }
  .form__foot .btn--primary { width: 100%; }
}

/* From here the 12-track grid is real: a port gets a quarter, a host a half. */
@media (min-width: 720px) {
  .form__grid > .field--3 { grid-column: span 3; }
  .form__grid > .field--6 { grid-column: span 6; }
  .form__grid > .field--wide { grid-column: span 6; }
  .panel { padding: var(--s5) var(--s5) var(--s5); }
  /* the checkbox is a control, not a label: align it to the field baseline
     rather than letting it float in the middle of its own track */
  .form__grid > .check { align-self: center; padding-top: var(--s1); }
}


/* ------------------------------------------------- fine print & recovery ---
   What used to be a five-line "set it up by hand" tile is now one muted line
   here plus one line inside the failure panel, which only ever renders after a
   tg:// link demonstrably failed to open the app. */
.fine {
  margin: var(--s2) 0 0; padding: 0 var(--pad);
  font-size: var(--t-sm); line-height: 1.5; color: var(--muted); text-align: center;
}
.fallback__where { margin-top: var(--s2); font-size: var(--t-sm); color: var(--text-2); }
.fallback__where b { color: var(--text); font-weight: 600; }
