/* ============================================================================
   GreyStream companion portal — mobile-first.

   Palette is lifted straight from the TV app's style.css so the phone and the
   television read as one product: #0b0f14 ground, #e9edf2 type, #ff5a5a accent,
   #50d282 for "connected/OK", #9aa6b2 for secondary text.

   No framework, no build step. Two reasons this is hand-written rather than
   Tailwind: the portal is three forms and a tab bar (a utility framework buys
   nothing at that size), and it has to deploy to Hostinger Premium as plain
   static files with no toolchain in the path.
   ========================================================================== */

/* ---- Tokens, taken from the TV app's style.css so the two surfaces are the same
   product rather than two things that merely look similar. Values on the right are
   where each one comes from. ------------------------------------------------- */
:root {
  /* Tells the browser this page is dark, so every piece of native UI it draws for us -
     the document scrollbar, form control internals, the text caret - is rendered from the
     dark set instead of the light one. Without it the page keeps a light scrollbar down a
     #0b0f14 page, which is the same mismatch the language popup had. The portal is dark
     only, deliberately: it is a remote control for a television, and the TV app has no
     light theme to match. */
  color-scheme: dark;

  --bg:        #0b0f14;   /* app: html/body background */
  --bg-panel:  #101722;   /* app: .card / .preview / .type-btn */
  --bg-row:    #0f1620;   /* app: .list-item */
  --bg-btn:    #141c26;   /* app: .actions button */
  --line:      #243243;   /* app: .card / .actions button border */
  --line-row:  #1f2a38;   /* app: .list-item border */
  --line-soft: #1a2432;   /* app: .info-row divider */
  --line-input:#2c3a4c;   /* app: .form input border */
  --hairline:  rgba(255,255,255,0.14);  /* app: .glass-panel border */
  --text:      #e9edf2;   /* app: body colour */
  --text-dim:  #9aa6b2;   /* app: .info-label / labels */
  --text-mute: #7f8b98;   /* app: .pl-ping-hint */
  /* COLOUR MEANS SOMETHING - the same three-way split the TV app uses:
       blue  = general selection / focus / primary action   (app: #3aa0ff focus ring)
       red   = Xtream, plus destructive and error states
       green = M3U
     The portal originally had ONE --accent and it was the Xtream red, so red landed on
     tabs, primary buttons and the plan badge - none of which are about Xtream. That made
     red the dominant colour and drained it of its meaning. --accent stays as the red for
     the rules that are genuinely Xtream or destructive. */
  --sel:       #3aa0ff;   /* app: focus accent */
  --sel-dk:    #2b7fd0;
  --sel-tx:    #dbecff;
  --accent:    #ff5a5a;   /* app: Xtream red - Xtream + danger ONLY */
  --accent-dk: #c93b3b;
  --accent-tx: #ffd6d6;   /* app: .pl-active-chip.type-xtream text */
  --ok:        #50d282;   /* app: M3U green */
  --ok-tx:     #d6f7e4;   /* app: .pl-active-chip.type-m3u text */
  --warn:      #f0b23c;
  --radius:    16px;      /* app: --radius */
  --radius-sm: 10px;      /* app: .list-item / .type-btn / .form input */
  --pill:      999px;     /* app: .pl-active-chip */
  --tap:       48px;      /* minimum comfortable touch target */
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  /* Poppins is the app's display font (loaded the same way in the TV's index.html).
     System fonts follow, so the portal still renders correctly if the webfont is
     blocked or the device is offline. */
  font-family: Poppins, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 16px;                    /* never below 16px: iOS zooms on focus otherwise */
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
}

/* ---- Views --------------------------------------------------------------- */
.view { display: none; }
.view.is-active { display: block; }

/* ---- Brand ---------------------------------------------------------------
   The wordmark is the app's, exactly: uppercase, letterspaced, weight 500, and
   TWO-TONE WHITE — "STREAM" is rgba(255,255,255,0.72) at weight 300. It is not
   red. (The portal originally coloured "Stream" with the accent, which read as a
   different product's logo sitting next to the same app icon.) */
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
  width: 34px; height: 34px;
  border-radius: 8px;
  flex: 0 0 auto;
  display: block;
}
.brand-name {
  margin: 0;
  font-size: 21px;
  font-weight: 500;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: #ffffff;
  white-space: nowrap;
}
.brand-name span { color: rgba(255,255,255,0.72); font-weight: 300; }
.brand-sm .brand-mark { width: 26px; height: 26px; border-radius: 6px; }
.brand-sm .brand-name { font-size: 16px; letter-spacing: 1.2px; }

/* ══════════════════════════════════════════════════════════════════════════
   CONNECT VIEW
   ═══════════════════════════════════════════════════════════════════════ */
.connect-wrap {
  min-height: 100vh;
  min-height: 100dvh;                 /* dvh dodges the mobile URL-bar jump; vh is the fallback */
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 32px 20px calc(32px + env(safe-area-inset-bottom));
  max-width: 460px;
  margin: 0 auto;
}
.connect-wrap .brand { justify-content: center; margin-bottom: 18px; }
.connect-lede {
  text-align: center; color: var(--text-dim);
  margin: 0 0 22px; font-size: 15px;
}
.connect-help {
  text-align: center; color: var(--text-mute);
  font-size: 13px; margin: 18px 4px 0; line-height: 1.6;
}
.connect-help b { color: var(--text-dim); font-weight: 600; }

.input-pin {
  font-size: 26px !important;
  letter-spacing: 0.32em;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* ══════════════════════════════════════════════════════════════════════════
   DASHBOARD
   ═══════════════════════════════════════════════════════════════════════ */
/* ONE COLUMN WIDTH FOR THE WHOLE DASHBOARD.
   .dash-body was already capped at 560px and centred, but the header row, the device
   line and the tab strip were not - so on a desktop browser the tabs ran the full width
   of the window while the content sat in a narrow column beneath them, and nothing lined
   up. The sticky BACKGROUNDS stay full-bleed (they should span the window); only their
   contents are constrained, which is what puts every edge on the same two verticals. */
/* THE COLUMN, DEFINED ONCE. Everything that should share a left and right edge is in
   this one selector list and reads one gutter token. It was three separate definitions
   before - the header bars had no side padding, the tab strip 16px, and the body 24px
   above 620px - so on a desktop the sync bar, the tabs and the cards each started at a
   different x. Add a new full-width element to THIS list rather than giving it its own
   max-width, which is how the three came about. */
:root { --gs-gutter: 16px; }
.dash-col,
.dash-head-row,
.dash-device,
.dash-bars,
.tabs,
.dash-body {
  max-width: 560px;
  margin-left: auto; margin-right: auto;
  padding-left: var(--gs-gutter); padding-right: var(--gs-gutter);
}

.dash-head {
  position: sticky; top: 0; z-index: 20;
  background: var(--bg);
  border-bottom: 1px solid var(--line-soft);
  padding: calc(12px + env(safe-area-inset-top)) 0 10px;   /* sides come from the column rule */
}
.dash-head-row { display: flex; align-items: center; justify-content: space-between; }
.dash-device {
  display: flex; align-items: center; gap: 7px;
  margin-top: 6px; font-size: 12.5px; color: var(--text-mute);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.dot-ok { background: var(--ok); box-shadow: 0 0 8px rgba(80,210,130,0.6); }

/* Session countdown. Quiet by default (it is reassurance, not a deadline — the
   session lasts far longer than the 10-minute PIN), amber near the end. */
.dash-expiry {
  margin-left: auto;
  color: var(--text-mute);
  white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.dash-expiry.is-low { color: var(--warn); }
.dash-expiry.is-expired { color: var(--accent); }

/* Tabs: the app's settings tab strip, scaled to a phone — hairline-bordered
   buttons with top-rounded corners sitting flush on the content below, and the
   .type-selected red tint on the active one. Sizes are reduced from the TV's
   20px/12-24px padding so three fit across a 360px screen. */
.tabs {
  position: sticky; z-index: 19;
  top: 0;                              /* JS sets the real offset once the header is measured */
  display: flex;
  gap: 4px;
  /* Centred on the same 560px column as everything else. The horizontal padding is kept
     inside the max-width (box-sizing is border-box globally) so the tab edges align with
     the card edges below them rather than sitting 16px wider. */
  background: var(--bg);
  border-bottom: 1px solid var(--hairline);
}
.tab {
  flex: 1 1 33.333%;
  min-height: 44px;
  min-width: 0;
  background: transparent;
  border: 1px solid var(--hairline);
  border-bottom: none;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  color: var(--text);
  font-size: 14px; font-weight: 700;
  font-family: inherit;
  padding: 11px 4px;
  margin-bottom: -1px;                 /* sit ON the strip's border, like the app's */
  cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  -webkit-tap-highlight-color: transparent;
}
.tab.is-active {
  background: rgba(58,160,255,0.16);
  border-color: var(--sel);
  color: #ffffff;
}

.dash-body {
  /* Bottom padding clears BOTH sticky bars (sync ~73px + upgrade banner ~52px)
     plus the home indicator, so the last control in a tab — the Cancel button at
     the foot of the playlist form — is never trapped underneath them. Keep this
     comfortably above 125px if either bar's height changes. */
  /* The sync bar and the upgrade banner moved into the header, so the body no longer
     has to reserve room beneath itself for them. */
  padding-top: 16px; padding-bottom: calc(32px + env(safe-area-inset-bottom));
}

/* Shown in place of an empty saved-items list, so the first thing a newly paired
   user sees is an instruction rather than a blank screen. */
.empty {
  text-align: center;
  color: var(--text-mute);
  font-size: 14px;
  line-height: 1.6;
  padding: 28px 16px;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  margin-bottom: 14px;
}
.tab-panel { display: none; }
.tab-panel.is-active { display: block; }

.panel-lede { color: var(--text-dim); font-size: 14px; margin: 0 0 16px; }
.panel-lede b { color: var(--text); font-weight: 600; }
.panel-note { color: var(--text-mute); font-size: 13px; line-height: 1.6; margin: 10px 0 0; }
.panel-note b { color: var(--text-dim); }

/* ---- Cards ---------------------------------------------------------------
   Mirrors the app's .glass-panel: a near-transparent fill with a hairline border,
   not a raised grey block. That restraint is most of what makes the TV UI read as
   premium, and it survives the move to a phone unchanged. */
.card {
  background: var(--bg-panel);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 18px 16px;
}
.card-form { margin-top: 14px; }
/* Matches the app's .glass-panel > h2 weight/spacing, scaled for a phone. */
.card-title { font-size: 17px; font-weight: 700; margin-bottom: 14px; }

/* ---- Saved-item list ----------------------------------------------------- */
.list { display: block; margin-bottom: 14px; }
.list:empty { display: none; }

/* Same construction as the app's .list-item: #0f1620 on a #1f2a38 hairline, 10px
   radius, with the 4px type stripe (red = Xtream, green = M3U) it uses everywhere. */
.item {
  display: flex; align-items: center; gap: 12px;
  background: var(--bg-row);
  border: 1px solid var(--line-row);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 13px 14px;
  margin-bottom: 10px;
}
.item.is-m3u { border-left-color: var(--ok); }
/* Radio-only m3u lists: the app's blue, not the M3U green. Green means "an M3U that feeds
   Live TV", which is the one thing this list is not. */
.item.is-radio { border-left-color: var(--sel); }
/* The word behind the stripe. On a list of ten, a colour on its own is a puzzle. */
.item-tag {
  display: inline-block;
  vertical-align: 1px;
  margin-left: 2px;
  padding: 1px 7px;
  border-radius: var(--pill);
  background: rgba(58,160,255,0.18);
  border: 1px solid rgba(58,160,255,0.55);
  color: var(--sel-tx);
  font-size: 11px; font-weight: 700; letter-spacing: 0.3px;
}
.item-main { flex: 1 1 auto; min-width: 0; }
.item-name {
  font-size: 15.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.item-sub {
  font-size: 12.5px; color: var(--text-mute); margin-top: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.item-actions { display: flex; gap: 8px; flex: 0 0 auto; }

/* ---- Fields -------------------------------------------------------------- */
.field { display: block; margin-bottom: 14px; }
.field-label {
  display: block; font-size: 13px; font-weight: 600;
  color: var(--text-dim); margin-bottom: 6px;
}
.field-hint { color: var(--text-mute); font-weight: 400; }

/* Frame-only fields, exactly like the app's .form input — transparent fill on a
   #2c3a4c border. The app deliberately avoids filled inputs ("no fill - frame
   only"), and the portal follows. */
input[type="text"], input[type="url"], input[type="password"] {
  width: 100%;
  min-height: var(--tap);
  background: transparent;
  border: 1px solid var(--line-input);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 16px;                     /* holds the line against iOS focus zoom */
  font-family: inherit;
  padding: 11px 13px;
  -webkit-appearance: none;
  appearance: none;
}
input::placeholder { color: #4e5966; }
/* The app's focus treatment is a light ring plus a faint grey fill (.focused +
   "inputs get the same grey fill shading on focus as list rows"), not a coloured
   border — carried over so focus feels the same on both surfaces. */
input:focus {
  outline: none;
  border-color: rgba(255,255,255,0.65);
  background: rgba(255,255,255,0.06);
  box-shadow: 0 0 0 2px rgba(255,255,255,0.18);
}

.input-row { display: flex; gap: 8px; align-items: stretch; }
.input-row input { flex: 1 1 auto; min-width: 0; }

/* ---- Checkbox ------------------------------------------------------------
   The app's own tick, rebuilt: a native input hidden behind a drawn box, because a
   platform checkbox is a small grey square on Android and a blue lozenge on iOS and
   neither belongs on this surface. The whole row is the <label>, so the tap target is
   the text as well as the box - on a phone that matters more than the box's own size.
   Selected state is the app's blue (--sel), the same one the TV's checkbox uses; the
   red/green pair here means Xtream/M3U and must not be borrowed for "on". */
.check {
  display: flex; align-items: flex-start; gap: 12px;
  margin: 2px 0 14px;
  padding: 12px 12px;
  background: var(--bg-row);
  border: 1px solid var(--line-row);
  border-radius: var(--radius-sm);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Not display:none - that takes the input out of the focus order, and a keyboard user
   would have no way to reach the control at all. */
.check input { position: absolute; opacity: 0; width: 0; height: 0; }
.check-box {
  flex: 0 0 auto;
  width: 22px; height: 22px;
  margin-top: 1px;
  border: 1px solid var(--line-input);
  border-radius: 6px;
  background: transparent;
  position: relative;
}
.check input:checked + .check-box {
  background: var(--sel);
  border-color: var(--sel);
}
/* The tick is drawn rather than typed: a ✓ glyph renders at a different weight and
   baseline in every font the phones fall back to. */
.check input:checked + .check-box::after {
  content: "";
  position: absolute; left: 7px; top: 3px;
  width: 5px; height: 10px;
  border: solid #06121e;
  border-width: 0 2.5px 2.5px 0;
  transform: rotate(45deg);
}
.check input:focus-visible + .check-box { box-shadow: 0 0 0 3px rgba(58,160,255,0.45); }
.check-text { min-width: 0; }
.check-label { display: block; font-size: 14.5px; font-weight: 600; color: var(--text); }
.check-hint { display: block; font-size: 12.5px; line-height: 1.5; color: var(--text-mute); margin-top: 4px; }

/* ---- Segmented control (Xtream / M3U) ------------------------------------ */
.seg {
  display: flex; gap: 8px; margin-bottom: 16px;
}
/* Straight copy of the app's .type-btn / .type-selected, including the exact
   rgba(255,90,90,0.16) tint and white bold label on the selected one. */
.seg-btn {
  flex: 1 1 50%;
  min-height: var(--tap);
  background: var(--bg-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: #aeb9c5;
  font-size: 14.5px; font-weight: 600; font-family: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.seg-btn.is-on {
  background: rgba(255,90,90,0.16);
  border-color: var(--accent);
  color: #ffffff;
  font-weight: 700;
}
/* The M3U side takes the app's green treatment, so format is colour-coded the same
   way here as it is in every list on the TV. */
.seg-btn.is-on[data-type="m3u"] {
  background: rgba(80,210,130,0.16);
  border-color: var(--ok);
}

/* ---- Buttons -------------------------------------------------------------
   Base is the app's .actions button: #141c26 on #243243, 10px radius. */
.btn {
  min-height: var(--tap);
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--bg-btn);
  color: var(--text);
  font-size: 15px; font-weight: 600; font-family: inherit;
  padding: 11px 16px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity 0.12s ease;
}
.btn:active { opacity: 0.7; }
.btn[disabled] { opacity: 0.45; cursor: default; }

/* Primary is the app's SELECTED-state treatment (red tint + red border + white
   bold text), not a solid red slab. The app never fills a control with flat
   #ff5a5a, and a solid one here looked like a different product's button. */
.btn-primary {
  background: rgba(58,160,255,0.16);
  border-color: var(--sel);
  color: #ffffff;
  font-weight: 700;
}
.btn-ghost   { background: transparent; }
.btn-quiet   { background: transparent; border-color: transparent; color: var(--text-mute); }
/* DELETE IS AN ORDINARY BUTTON. It was outlined and lettered in the Xtream red, which made
   it the loudest control on a list of ten rows - and red already MEANS Xtream in both
   surfaces, so a red Delete beside a green M3U row was saying two things at once. The TV app
   settles this the same way: its Delete sits in the action row looking exactly like Test,
   Save and Set Active. The safety is the confirm dialog every delete already goes through,
   not the colour. Kept as its own class rather than swapped for .btn-ghost at each call
   site, so the intent stays visible and one rule can restyle every delete in the portal. */
.btn-danger  { background: transparent; }
.btn-block   { display: block; width: 100%; }
.btn-sm      { min-height: 36px; padding: 6px 12px; font-size: 13.5px; }
.btn-row     { display: flex; gap: 10px; margin-top: 4px; }
.btn-row .btn { flex: 1 1 50%; }

/* Paste / Show sit beside their input, so they must not grow with it. */
.btn-paste, .btn-eye {
  flex: 0 0 auto;
  padding: 0 12px;
  font-size: 13.5px;
  color: var(--text-dim);
}

/* ---- Messages ------------------------------------------------------------ */
.msg {
  margin: 0; padding: 0; font-size: 13.5px; line-height: 1.5;
  min-height: 0; color: var(--text-dim);
  /* pre-line so the per-mirror Test-connection breakdown keeps its line breaks
     without needing markup. */
  white-space: pre-line;
}
.msg:not(:empty) { padding: 10px 0 4px; }
.msg.is-error { color: var(--accent); }
.msg.is-ok    { color: var(--ok); }

/* ---- Activation ---------------------------------------------------------- */
.status-card { padding: 6px 16px; }
.status-line {
  display: flex; justify-content: space-between; align-items: center;
  gap: 12px; padding: 13px 0;
  border-bottom: 1px solid var(--line-soft);
}
.status-line:last-child { border-bottom: none; }
.status-label { color: var(--text-dim); font-size: 14px; font-weight: 600; }
.status-value { font-size: 14px; text-align: right; }
.status-value.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
.status-value.is-trial { color: var(--warn); }
.status-value.is-active { color: var(--ok); }
.status-value.is-expired { color: var(--accent); }

/* What Premium includes. Deliberately plain: a list of names, no marketing copy, no
   colour. It sits between the status card and the prices to answer "what am I paying
   for" before the figure appears. */
.feature-card { margin-top: 16px; }
.feature-list {
  list-style: none; margin: 0; padding: 0;
  /* Two columns on a phone - six single-line names in one column made the card taller
     than the prices below it, which is the wrong emphasis. */
  display: flex; flex-wrap: wrap;
}
.feature-list li {
  /* box-sizing so the 50% basis is the OUTER width; without it the padding pushes two
     items past 100% and they stop pairing. */
  box-sizing: border-box;
  flex: 0 0 50%;
  display: flex; align-items: baseline; gap: 8px;
  padding: 5px 10px 5px 0;
  font-size: 13.5px; color: var(--text);
  min-width: 0;
}
/* Green because it means "included", the same way green means M3U elsewhere: a state,
   not decoration. Decorative to a screen reader - the feature name carries the meaning. */
.feature-tick { flex: 0 0 auto; color: var(--ok); font-size: 12px; font-weight: 700; }

/* Radio never locks. On its own row, above the border, because "what still works when
   the trial ends" is the question people actually have. */
.feature-free {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  margin-top: 12px; padding-top: 12px;
  border-top: 1px solid var(--line-soft);
  font-size: 13.5px;
}
.feature-free[hidden] { display: none; }
.feature-free-k { color: var(--text); font-weight: 600; }
.feature-free-v { color: var(--ok); font-weight: 600; }

/* ---- Product model ------------------------------------------------------
   Two halves side by side. On a phone they stack, and the free half comes first
   deliberately: it is what the user already has, and reading "here is what you have,
   here is what you'd add" is a fairer sequence than leading with the upsell. */
.model-card { margin-top: 16px; }
.model-cols {
  display: flex;
  gap: 18px;
  margin-top: 4px;
}
.model-col { flex: 1 1 50%; min-width: 0; }
/* The divider only makes sense while they are side by side; at one column it would be a
   line across the middle of nothing. */
.model-col + .model-col {
  padding-left: 18px;
  border-left: 1px solid var(--line-soft);
}
@media (max-width: 520px) {
  .model-cols { flex-direction: column; gap: 20px; }
  .model-col + .model-col { padding-left: 0; border-left: 0; padding-top: 18px; border-top: 1px solid var(--line-soft); }
}
.model-head {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; font-weight: 800; letter-spacing: 0.4px;
  text-transform: uppercase;
}
/* Green for the free half, blue for premium - the same two senses the rest of the app
   uses, so the colour is doing work rather than decorating. */
.model-head-free { color: var(--ok); }
.model-head-premium { color: var(--sel); }
.model-ic { flex: 0 0 auto; display: inline-flex; }
.model-ic svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.model-sub { margin: 4px 0 10px; font-size: 12.5px; color: var(--text-mute); }
.model-list { list-style: none; margin: 0; padding: 0; }
.model-list li {
  display: flex; align-items: baseline; gap: 8px;
  padding: 4px 0;
  font-size: 13.5px; line-height: 1.45;
  color: var(--text);
}
.model-list .feature-tick { flex: 0 0 auto; font-size: 12px; font-weight: 700; }
.model-free-tick { color: var(--ok); }
.model-premium-tick { color: var(--sel); }
.model-note {
  margin: 14px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--line-soft);
  font-size: 12.5px; line-height: 1.6;
  color: var(--text-mute);
}
/* A button that reads as a link - it navigates, and there is no href yet. */
.link-btn {
  background: none; border: 0; padding: 0;
  font: inherit; color: var(--sel);
  cursor: pointer; text-decoration: none;
  white-space: nowrap;
}
.link-btn:hover { text-decoration: underline; }

.plans { display: flex; gap: 12px; margin-top: 16px; }
/* display:flex would otherwise win over the hidden attribute - a lifetime owner is shown
   no plans at all, and the block has to actually disappear. */
.plans[hidden] { display: none; }
.plan {
  position: relative;
  flex: 1 1 50%;
  background: var(--bg-panel);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  /* Top padding clears the badge that straddles the border on .plan-best. Both cards
     carry it so the two stay the same height and the prices line up. */
  padding: 24px 14px 14px;
  text-align: center;
}
.plan-best { border-color: rgba(58,160,255,0.5); }
/* Built like the app's .pl-active-chip: pill, tinted fill, tinted border, light
   text — rather than a solid red badge. */
.plan-badge {
  position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
  /* The badge sits ON the card's top border, so its fill has to be OPAQUE: a tinted
     translucent fill let the border line run visibly straight through the badge. Paint
     the card colour first, then the tint on top of it. */
  background:
    linear-gradient(rgba(58,160,255,0.22), rgba(58,160,255,0.22)),
    var(--bg-panel);
  border: 1px solid rgba(58,160,255,0.6);
  color: var(--sel-tx);
  font-size: 10.5px; font-weight: 800; letter-spacing: 0.6px; text-transform: uppercase;
  padding: 3px 11px; border-radius: var(--pill); white-space: nowrap;
  max-width: calc(100% - 16px);
  overflow: hidden; text-overflow: ellipsis;
}
.plan-name {
  font-size: 14px; color: var(--text-dim); font-weight: 600;
  letter-spacing: 0.3px; text-transform: uppercase;
}
/* THE PRICE IS THE POINT OF THE CARD, so it gets the size. 34px against the 14px plan
   name is roughly the 2.5x contrast a promotional page needs for the figure to be read
   first; at the old 25px it competed with the button below it.
   The line is a flex ROW - struck reference, the figure, then the period - because the
   reference has to sit BESIDE the price to read as a comparison. Stacked, it just reads
   as two prices. baseline alignment keeps the three different sizes sitting on one line
   rather than centred against each other. */
.plan-price {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 2px;
}
.plan-now {
  font-size: 34px; font-weight: 700; line-height: 1.05;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* The reference price, on its OWN line above the figure.
   It was inline with the price, which is how the reference layout is usually drawn - but
   at 430px the two 50% cards are ~180px wide and "$19.99 $14.99 once" does not fit, so
   each card wrapped at a different point and the pair looked broken. A full-width line
   wraps identically in both cards at every width and still reads as a comparison,
   because the struck figure sits directly above the one being charged.
   Muted and smaller so it supports the price instead of competing: a strikethrough at
   full size reads as though the higher number were the offer. */
.plan-was {
  flex: 0 0 100%;
  font-size: 15px; font-weight: 500;
  color: var(--text-mute);
  text-decoration: line-through;
  text-decoration-thickness: 1.5px;
  line-height: 1.2;
}
.plan-was[hidden] { display: none; }
.plan-per { font-size: 13px; color: var(--text-mute); font-weight: 400; }

/* The saving, stated as a number. Green because it is a gain, and the one place on this
   panel where colour is doing that job. */
.plan-save {
  margin: 4px 0 0;
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.2px;
  color: var(--ok);
}
.plan-save[hidden] { display: none; }

/* "Launch price" is a factual label for what is being charged right now. It is what
   makes the struck figure above it lawful copy rather than a fabricated saving - see the
   note on showStandardPriceRef in config.js. Muted, so the claim sits under the numbers
   rather than shouting alongside them. */
.plan-tag {
  font-size: 11px; font-weight: 600; letter-spacing: 0.2px;
  color: var(--text-mute);
  margin: 6px 0 12px;
  /* Long translations ("Precio de lanzamiento hasta …") must wrap inside the card rather
     than widen it - the two plans are 50/50 and any overflow breaks that. */
  line-height: 1.35;
  overflow-wrap: break-word;
}
.plan-tag[hidden] { display: none; }

.plan-billing { margin-top: 2px; font-size: 12px; color: var(--text-mute); }
/* The commitment, in the user's own terms, directly under the saving. "Renews annually"
   next to a subscription price is the single most useful thing a pricing card can say. */
.plan-terms { margin-top: 4px; font-size: 12px; color: var(--text-dim); }

/* ---- Trial offer, and the telemetry offer, share a shape ------------------
   Both are opt-in cards with a leading mark, two lines of text and one button. Giving
   them the same shape says they are the same KIND of thing - optional, actionable - and
   keeps them clearly below the plans. */
/* WHY THE BUTTON IS NOT A FIXED WIDTH.
   ────────────────────────────────────────────────────────────────────────────────
   A fixed width is the obvious fix and the wrong one for a UI that ships in 14 languages.
   "Start 14-Day Trial" measures 168px; the Polish "Rozpocznij 14-dniowy okres próbny" is
   306px - 1.8x - and Chinese is barely half the English. Any single number either clips
   the long labels or strands whitespace beside the short ones, and it has to be re-guessed
   every time a translation changes.
   The defect was never the width. It was that the row could not BREAK: the button was
   `flex: 0 0 auto` on a nowrap row, so it took its content width out of the middle of the
   card and left the heading whatever remained. Measured before the fix, at 280-360px
   viewports, that remainder was 0px in every language including English, and for Polish
   and Russian it was negative - the button overflowed the card, which is the overlap in
   the screenshot.
   The rule instead: the text keeps a MINIMUM READABLE MEASURE, and when the button no
   longer fits beside it the button takes a row of its own. That is driven by the space
   actually available rather than by a breakpoint guess, so it holds on a phone, a tablet
   and a desktop alike, and no future translation can break it. */
.act-offer,
.act-telemetry {
  display: flex;
  align-items: center;
  flex-wrap: wrap;          /* the button may drop to a row of its own - see above */
  gap: 12px;
  margin-top: 16px;
  padding: 12px 14px;
  background: var(--bg-panel);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
}
.act-offer[hidden] { display: none; }
/* The day count as the card's mark - it is the offer, so it may as well be the icon. */
.act-offer-ic {
  flex: 0 0 auto;
  width: 42px; height: 42px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 10px;
  background: rgba(80,210,130,0.12);
  border: 1px solid rgba(80,210,130,0.45);
  color: var(--ok);
  font-size: 17px; font-weight: 800;
  font-variant-numeric: tabular-nums;
}
/* Once the trial is over the badge reads "0", and the free-tier green made that look like
   good news sitting next to "has ended". Neutral instead - it is a fact, not a state to
   celebrate or to alarm anyone about. */
.act-offer.is-done .act-offer-ic {
  background: rgba(255,255,255,0.04);
  border-color: var(--line);
  color: var(--text-mute);
}
/* …but a trial still RUNNING keeps the green: the number there is days you still have. */
.act-offer.is-done.is-live .act-offer-ic {
  background: rgba(80,210,130,0.12);
  border-color: rgba(80,210,130,0.45);
  color: var(--ok);
}
/* 180px is the MEASURE, not a guess at a device: it is the width below which a 14px
   heading starts breaking into the one-word-per-line column from the bug report. When the
   button cannot fit beside 180px of text, the flex line runs out of room and the BUTTON
   wraps rather than the text being squeezed.
   On the narrowest phones the badge takes a row of its own as well - at a 287px viewport
   the card has 220px of content width, so only 166px remains beside the 42px badge. That
   is deliberate: 166px is inside the range where the heading starts fragmenting, and a
   badge stacked above a full-width heading reads better than a heading squeezed beside
   it. From roughly 300px up, badge and text share a row again. */
.act-offer-tx { flex: 1 1 180px; min-width: 0; }
.act-offer-h { font-size: 14px; font-weight: 700; color: var(--text); }
.act-offer-sub { margin-top: 2px; font-size: 12.5px; line-height: 1.5; color: var(--text-mute); }

/* `order` keeps the button last in RTL as well - it is the action, so it comes after the
   thing it acts on in both directions.
   It is NOT stretched to the full card width when it wraps onto its own row. A button
   spanning the card looked heavy next to the text it belongs to, and the two offer cards
   then disagreed with each other by exactly the difference in their label lengths. Both
   are instead sized to the WIDER of the two labels, measured at runtime, by
   equaliseOfferButtons() in app.js - which is the only rule that survives fifteen
   languages. min-width keeps a short label (Chinese, or "Joined") from collapsing into a
   control too small to aim at with a thumb. */
/* Deliberately NOT scoped to `.act-offer >`. These are properties of the accept button
   itself, and tying them to an ancestor class made them silently dependent on nobody ever
   reassigning that ancestor's className - which is exactly what renderActivation did to
   the telemetry card, un-centring its button with no error anywhere. */
.btn-accept { order: 3; min-width: 140px; }
/* Applied by markWrappedOfferButtons() in app.js, and only when the button has actually
   dropped onto a line of its own - which CSS has no way to detect. Auto margins on both
   sides centre it in the line; beside the text it keeps its natural end position. */
.btn-accept.is-wrapped { margin-left: auto; margin-right: auto; }

/* ---- Merchant-of-record trust line --------------------------------------- */
.act-trust {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin-top: 18px;
  font-size: 12.5px;
  color: var(--text-mute);
}
.act-trust[hidden] { display: none; }
.act-trust-ic { display: inline-flex; color: var(--ok); }
.act-trust-ic svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

/* ---- Footnotes ------------------------------------------------------------
   Deliberately the quietest text on the panel, but present: each one qualifies a claim
   made above it, and a caveat nobody can find is the same as one that was never made. */
.act-footnotes {
  display: flex; gap: 18px; flex-wrap: wrap;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--line-soft);
}
.act-footnote {
  flex: 1 1 220px;
  display: flex; gap: 8px;
  margin: 0;
  font-size: 11.5px; line-height: 1.55;
  color: var(--text-mute);
}
.act-footnote-mark { flex: 0 0 auto; display: inline-flex; color: var(--text-dim); }
.act-footnote-mark svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

/* Optional telemetry offer. A CARD with its own button: as a paragraph it read as a
   statement about something already decided, and there was nothing to press. Still
   deliberately quieter than the plans above - a bonus must not compete with the price,
   and declining it costs the user nothing. */
/* Shape comes from .act-offer, which it now shares with the trial card. Only the
   hidden rule is its own. */
.act-telemetry[hidden] { display: none; }
/* Green = accept, matching the M3U/"included" sense colour already used on this panel.
   Deliberately NOT the blue primary: that belongs to the purchase buttons, and an
   optional extra should not look like the main action. */
.btn-accept {
  /* `0 1 auto`, not `0 0 auto`: content width by default, but allowed to give ground
     before it overflows the card. The last-resort guards are max-width (it can never be
     wider than the card) and white-space: normal (a very long label wraps INSIDE the
     button rather than sticking out of it). */
  flex: 0 1 auto;
  max-width: 100%;
  white-space: normal;
  background: rgba(80,210,130,0.14);
  border-color: rgba(80,210,130,0.55);
  color: #bff0d4;
  font-weight: 700;
  min-height: var(--tap);   /* stays a full-size tap target even when the label is short */
  padding: 8px 16px;
}
.btn-accept:hover { background: rgba(80,210,130,0.22); border-color: var(--ok); }
/* "This has already happened." Was the telemetry card's alone; the trial card takes it too
   now that its button reports the trial's state instead of pretending to start one. Keyed on
   .is-done rather than on either card, so the next card that ends up in this position gets
   the same treatment for free. */
.act-offer.is-done .btn-accept,
.act-telemetry.is-done .btn-accept {
  background: transparent;
  border-color: var(--line);
  color: var(--text-mute);
  cursor: default;
}
/* Overrides .btn[disabled]'s blanket 0.45: at that opacity the remaining time - which is the
   point of the chip - was too faint to read on a phone in daylight. The muted colour above
   already says "not a button"; it does not need to be half invisible as well. */
.act-offer.is-done .btn-accept[disabled] { opacity: 1; }
.act-offer.is-done .btn-accept:hover { background: transparent; border-color: var(--line); }

/* Build note, not a customer message. Boxed and dimmed so it can never be mistaken for
   part of the offer; disappears entirely once GS_CONFIG.paymentsEnabled is true. */
.dev-note {
  margin-top: 16px;
  padding: 10px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm, 10px);
  color: var(--text-mute);
  font-size: 12.5px;
}
.dev-note[hidden] { display: none; }

.danger-card { margin-top: 24px; border-color: var(--line); }

/* ---- Status bars, in the header ------------------------------------------
   Both were `position: fixed` against the bottom of the window. They are statements
   ABOUT THIS DEVICE, so they now sit with the device row at the top, in normal flow -
   which also means they no longer cover the last of the content, and the body no longer
   needs 172px of dead padding to stay clear of them. They still travel with the sticky
   header, so "Sync to TV" is reachable from anywhere without hunting. */
.dash-bars { margin-top: 10px; }
.sync-bar {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 12px;
  background: var(--bg-row);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
}
.sync-note { flex: 1 1 auto; font-size: 13px; color: var(--text-mute); min-width: 0; }
.sync-note.is-dirty { color: var(--warn); }
.sync-note.is-ok { color: var(--ok); }
.btn-sync { flex: 0 0 auto; padding: 9px 18px; }

.upsell {
  display: flex; align-items: center; gap: 10px;
  margin-top: 8px;
  padding: 9px 12px;
  background: rgba(255,90,90,0.12);
  border: 1px solid rgba(255,90,90,0.28);
  border-radius: var(--radius-sm);
  font-size: 13.5px;
}
.upsell[hidden] { display: none; }
.upsell span { flex: 1 1 auto; min-width: 0; }
.upsell-x {
  flex: 0 0 auto; width: 30px; height: 30px;
  background: none; border: 0; color: var(--text-mute);
  font-size: 21px; line-height: 1; cursor: pointer;
}

/* ---- Toast --------------------------------------------------------------- */
/* The app's own toast: rgba(0,0,0,0.55) on a rgba(255,255,255,0.18) hairline. */
.toast {
  position: fixed; left: 50%; transform: translateX(-50%) translateY(20px);
  bottom: calc(32px + env(safe-area-inset-bottom));   /* nothing pinned at the foot any more */
  z-index: 60;
  max-width: calc(100vw - 40px);
  background: rgba(0,0,0,0.55);
  border: 1px solid rgba(255,255,255,0.18);
  color: #eaf2fb;
  border-radius: var(--pill);
  padding: 11px 20px;
  font-size: 14px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.55);
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast.is-on { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.is-error { border-color: var(--accent-dk); }

/* ---- Larger phones / small tablets --------------------------------------- */
@media (min-width: 620px) {
  /* ONE TOKEN, so the tabs and the header bars widen WITH the cards instead of staying
     behind at 16px - which is exactly how the three edges came apart. */
  :root { --gs-gutter: 24px; }
  .plans { gap: 16px; }
}

/* Respect the OS reduced-motion setting. */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ── Language picker ───────────────────────────────────────────────────────────
   Sits above the connect card and in the dashboard header. Deliberately quiet:
   it is a fallback for when the browser's own language guess is wrong, not a
   primary control. Each language is listed in its OWN script, so someone who
   cannot read the current UI language can still find theirs.                    */
.lang-row { display: flex; justify-content: flex-end; margin: 0 0 12px; }
.lang-row-sm { margin: 0 8px 0 0; }
/* Same shell as .btn-sm - #141c26 on #243243, 10px radius, 36px tall - so it reads as
   one of the portal's controls rather than a raw browser <select>. The first version
   used its own ad-hoc greys and a red focus ring, and looked bolted on. */
.lang-select {
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text-dim);
  background: var(--bg-btn);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  min-height: 36px;
  padding: 6px 30px 6px 12px;
  max-width: 11rem;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  /* Chevron drawn in CSS - no asset, and it tints with the text colour. */
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-mute) 50%),
    linear-gradient(135deg, var(--text-mute) 50%, transparent 50%);
  background-position: right 14px center, right 9px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.lang-select:hover { color: var(--text); border-color: var(--sel-dk); }
.lang-select:focus-visible { outline: 2px solid var(--sel); outline-offset: 2px; }
/* ---- The open list -------------------------------------------------------
   This is OUR popup, not the browser's. Styling `option` only ever worked on desktop
   Chromium - Android draws the native list itself and ignored it completely, which is
   why the dropdown was still a white sheet on a phone. See i18n.js for the keyboard and
   screen-reader behaviour that had to be rebuilt along with it. */
.lang-picker { position: relative; display: inline-block; }
.lang-select-tx { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lang-picker.is-open .lang-select { border-color: var(--sel-dk); color: var(--text); }

.lang-list {
  position: absolute;
  z-index: 40;
  top: calc(100% + 4px);
  left: 0;
  min-width: 100%;
  max-height: 60vh;
  overflow-y: auto;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--bg-panel);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 34px rgba(0,0,0,0.55);
}
.lang-list[hidden] { display: none; }
/* THE SCROLLBAR IS PART OF THE PANEL, not part of the operating system.
   Left alone the browser paints its default bar - a light trough with a grey thumb -
   straight down the edge of a near-black popup, which is the mismatch in the report.

   THE STANDARD PROPERTIES ONLY, and that is deliberate rather than lazy. The obvious
   belt-and-braces version, `scrollbar-width` plus a set of `::-webkit-scrollbar` rules,
   does NOT work: as soon as either standard property is set to something other than
   `auto`, Chromium ignores the `::-webkit-scrollbar` pseudo-elements completely. Written
   together, the webkit block is dead code that silently does nothing - measured here as a
   thumb 6px of inset wide inside a 2px bar, i.e. invisible. It is genuinely either/or, so
   this picks the one with a future: `scrollbar-color` is supported by Firefox, by Chrome
   121+ and by Safari 18.2+.
   Older engines are not left with the light bar either - `color-scheme: dark` on :root
   already makes them paint the native scrollbar from the dark set. That is the fallback,
   and it is a good one: the wrong shade of dark beats a white stripe. */
.lang-list {
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.22) transparent;
}
.lang-option {
  padding: 9px 12px;
  border-radius: 7px;
  font-size: 14px;
  color: var(--text);
  white-space: nowrap;
  cursor: pointer;
}
/* Keyboard cursor and mouse hover are the SAME highlight - two different ones on screen
   at once is the usual way a custom listbox ends up confusing. */
.lang-option[data-active] { background: rgba(255,255,255,0.08); }
.lang-option.is-selected { color: var(--sel-tx); font-weight: 700; }
.lang-option.is-selected[data-active] { background: rgba(58,160,255,0.22); }
/* The list opens leftward from a right-aligned trigger so it cannot run off the edge. */
.lang-row-sm .lang-list { left: auto; right: 0; }
[dir="rtl"] .lang-list { left: auto; right: 0; }

/* Arabic: the sticky bars and the field rows are flex, so they mirror for free;
   these are the few places that need a hand. */
[dir="rtl"] .lang-row { justify-content: flex-start; }
[dir="rtl"] .lang-select {
  padding: 6px 12px 6px 30px;
  background-position: left 9px center, left 14px center;
}
[dir="rtl"] .lang-row-sm { margin: 0 0 0 8px; }
[dir="rtl"] .input-pin { direction: ltr; text-align: center; }
[dir="rtl"] input[type="url"],
[dir="rtl"] #in-device-id { direction: ltr; text-align: left; }
