/* =============================================
   Suodrazah Media - orange/purple dark theme
   ============================================= */

/* The `hidden` attribute must actually hide.
   The UA stylesheet's [hidden] { display: none } is a plain element-level rule,
   so ANY author rule that sets `display` outranks it - and this sheet sets
   display on nearly everything: .btn is inline-flex, .card is block, .filter-form
   is flex. Those elements all stay on screen with hidden="" set, and the symptom
   is silent: el.hidden reads true, the DOM agrees the element is hidden, and it
   is sitting there in the page in front of you. It has caught this codebase
   three times already - the header metrics strip, the bulk bar, the months box -
   each patched with another one-off selector further down. This is the general
   rule those were approximating; they are now redundant but harmless.
   !important because it has to outrank whatever a component sets. */
[hidden] { display: none !important; }

:root {
  --bg:          #090910;
  --bg-elevated: #0f0f1a;
  --surface:     #14142a;
  --surface-hover:#1c1c38;
  --surface-alt: #1e1e3a;
  --border:      #2a2a50;
  --accent:      #ff7a00;
  --accent-hover:#e06d00;
  --purple:      #8b5cf6;
  --purple-hover:#7c3aed;
  --text:        #f0f0ff;
  --text-sec:    #8888bb;
  --danger:      #ff3355;
  --warning:     #ff9500;
  --success:     #22c55e;
  --trial-color: #60a5fa;
  --radius-sm:   6px;
  --radius:      10px;
  --radius-lg:   14px;
  --font: system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* Elevation. Cards carry a soft shadow rather than leaning on a hairline
     border alone for separation. These are the DARK-theme values (deeper and
     more diffuse - a dark surface needs more than a 1px line to lift off a dark
     background); the light themes override them below. */
  --shadow-sm:  0 1px 2px rgba(0,0,0,0.30);
  --shadow-md:  0 4px 14px rgba(0,0,0,0.34);
  /* Overlays (dropdowns, popovers, the mobile nav sheet) sit above the page and
     need to read as detached, so they go deeper than a card. */
  --shadow-lg:  0 10px 28px rgba(0,0,0,0.45);

  /* Derived from whatever --accent the active theme sets, so they follow the
     theme instead of fighting it. These were previously hardcoded - a green
     focus ring and an orange nav pill - which looked wrong on every theme whose
     accent isn't that exact colour (emby green, grey steel, sunny amber). */
  --ring:        color-mix(in srgb, var(--accent) 35%, transparent);
  --accent-soft: color-mix(in srgb, var(--accent) 12%, transparent);
  /* Row hairline inside tables: a tint of the theme's own border rather than a
     fixed blue-grey, which was invisible on the light themes. */
  --border-subtle: color-mix(in srgb, var(--border) 55%, transparent);
}

/* Light themes need a tight, low-opacity neutral shadow - the dark values above
   are far too heavy on white and read as a grey smear. */
[data-theme="day"],
[data-theme="sunny"] {
  --shadow-sm: 0 1px 2px rgba(16,24,40,0.06), 0 1px 3px rgba(16,24,40,0.08);
  --shadow-md: 0 4px 14px rgba(16,24,40,0.09);
  --shadow-lg: 0 10px 28px rgba(16,24,40,0.13), 0 2px 6px rgba(16,24,40,0.07);
}

*, *::before, *::after { box-sizing: border-box; }

html { font-size: 16px; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  line-height: 1.6;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); text-decoration: underline; }

/* ---- Header ---- */
.site-header {
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 100;
}

.header-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1.5rem;
  /* min-height, not height. The bar wraps to a second row on narrow screens and
     a fixed height would clip that row rather than making room for it. */
  min-height: 60px;
  /* GRID, not flex, and the reason is a flexbox rule that bit this layout twice:
     a wrapping flex container breaks the line BEFORE it shrinks anything. With
     flex-wrap on, the nav kept its full 740px and pushed first the theme button
     and then the whole right-hand cluster onto a line of its own - so the bar
     grew a wasted row with the logo alone on it, at exactly the widths this work
     was meant to fix.

     A grid says where things go instead of negotiating: the logo takes what it
     needs, the cluster gets the rest and shrinks into it, and the ambient strip
     spans both columns on a row of its own. */
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 0.75rem;
}

.logo {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
}
.logo svg { flex-shrink: 0; }
.logo:hover { text-decoration: none; color: var(--accent); }
.logo-version {
  font-size: 0.65rem;
  font-weight: 600;
  color: var(--text-sec);
  background: var(--surface-alt);
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  letter-spacing: 0.02em;
  align-self: center;
}

/* Right-hand header cluster: nav (when signed in) + the theme switcher, which is
   always present so anyone — even on the login page — can change theme.

   flex-wrap: nowrap is LOAD-BEARING. .header-inner wraps, and a wrapping flex
   container breaks the line before it shrinks anything - so with the theme button
   as a direct child of .header-inner it was pushed onto a line of its own, hard
   left, instead of the nav giving way. Keeping the nav and the button inside one
   non-wrapping box means the nav shrinks (and its links wrap internally) while
   the button stays put. */
.header-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: nowrap;
  min-width: 0;
}
/* The cluster hugs the right of its column. min-width:0 on the logo so the brand
   is what gives way on a 320px screen rather than the controls. */
.header-inner > .header-actions { justify-self: end; }
.header-inner > .logo { min-width: 0; }
.logo > span:first-of-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Never shrinks: a 38px button is not what is costing the row its space. */
.theme-switch { flex-shrink: 0; }

/* The ambient strip - machine metrics and the server clock - on a row of its own,
   under the logo and nav, at EVERY width.

   Its own row unconditionally rather than "inline until it stops fitting", and
   the reason is arithmetic: .header-inner is capped at 1200px, and an admin's nav
   alone is around 740px of that. Add the logo, the strip and the theme button and
   one line was never wide enough on any monitor - which is why the strip used to
   be deleted at 1100px and the bar looked squashed above it. A row that is always
   there also means the numbers never move: they are in the same place on a phone
   as on a desktop, instead of appearing and vanishing at a breakpoint.

   The element is only rendered for a signed-in viewer, so the login page keeps a
   single-row header. */
.header-status {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  min-width: 0;
  grid-column: 1 / -1;
  justify-content: flex-end;
  padding: 0.25rem 0 0.4rem;
  border-top: 1px solid var(--border);
  /* If it ever does run out of room, the STRIP scrolls - never the page body. */
  overflow-x: auto;
  scrollbar-width: none;
}
.header-status::-webkit-scrollbar { display: none; }
/* The clock holds the right edge whether or not the gauges are beside it. It is
   alone on every MEMBER page - the metrics are staff-only - and alone on a staff
   page until the first poll lands; and the <=480px rule below turns the strip
   into space-between, which parks a single child hard left, directly under the
   logo, where it reads as a stray line of text rather than a clock. */
.header-status .header-clock { margin-left: auto; }

/* ── The page title, in the strip ────────────────────────────────────────────
   It used to have a band of its own below the banner, which cost about 5vh on
   every page to say one short phrase. This row was already there and, on a
   member page, nearly empty - it holds only the clock, hard right. So the title
   moved into it.

   :empty is not a nicety. A page that has not been moved over yet renders the
   block empty, and an empty <h1> with margins would leave a hairline gap in the
   strip on exactly those pages. */
.header-title {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  /* Truncated rather than wrapped: a long ticket subject would otherwise push
     the strip to two lines and move everything on the page down. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.header-title:empty { display: none; }

/* Whatever else the old title band held - a Back link, a New button. Kept on the
   page rather than moved into the strip: the strip is chrome, and a control that
   writes or navigates does not belong in furniture that looks identical on every
   page. */
.page-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.6rem;
  margin-bottom: 1rem;
}
.page-actions:empty { display: none; }

/* ---- System clock (server timezone) ---- */
.header-clock {
  display: flex; flex-direction: column; align-items: flex-end; line-height: 1.1;
  flex-shrink: 0; white-space: nowrap;
}
.header-clock-time {
  font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.9rem;
  color: var(--text); letter-spacing: 0.02em;
}
.header-clock-tz { font-size: 0.66rem; color: var(--text-sec); text-transform: uppercase; letter-spacing: 0.04em; }

/* ---- Speed-test result tiles (member status page + admin diagnostics) ---- */
.speedtest-grid { display: flex; gap: 1rem; flex-wrap: wrap; }
.speedtest-metric {
  flex: 1; min-width: 110px; text-align: center;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 0.9rem 0.5rem;
}
.speedtest-value { font-size: 1.7rem; font-weight: 700; color: var(--accent); }
.speedtest-label { font-size: 0.8rem; color: var(--text-sec); margin-top: 0.2rem; }

/* ---- Colour-theme quick switcher (icon-only trigger + dropdown) ---- */
.theme-switch { position: relative; flex-shrink: 0; }
.theme-switch-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; padding: 0;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-sec);
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.theme-switch-btn:hover { color: var(--text); border-color: var(--accent); }
.theme-switch-btn svg { width: 18px; height: 18px; display: block; }
.theme-menu {
  display: none;
  position: absolute; right: 0; top: calc(100% + 6px);
  min-width: 13rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-lg);
  padding: 0.3rem;
  z-index: 300;
  /* The list grows every time a theme is added, and it hangs off a header
     pinned to the top of the window - so on a short viewport (a laptop with the
     window part-height, a phone in landscape, a TV browser) the last entries
     fell past the bottom edge with no way to reach them. They were not merely
     hard to see: the panel is absolutely positioned, so the page behind it does
     not scroll to bring them into view. Cap it against the window and let the
     panel itself scroll. */
  max-height: calc(100vh - 5.5rem);
  overflow-y: auto;
  /* Reaching the end of the list must not then start scrolling the page. */
  overscroll-behavior: contain;
}
.theme-switch.open .theme-menu { display: block; }
.theme-option {
  display: flex; align-items: center; gap: 0.6rem; width: 100%;
  padding: 0.5rem 0.6rem;
  background: transparent; border: 0; border-radius: 7px;
  color: var(--text); font: inherit; font-size: 0.92rem; text-align: left;
  cursor: pointer;
  /* A wrapped label would push the tick onto a second line and make the row
     taller than its neighbours - the list has to read as one column. */
  white-space: nowrap;
}
.theme-option:hover { background: var(--surface-alt); }
.theme-option.active { background: var(--surface-alt); font-weight: 600; }
.theme-option.active::after { content: "\2713"; margin-left: auto; color: var(--accent); }
.theme-dot {
  flex-shrink: 0; width: 18px; height: 18px; border-radius: 50%;
  border: 1px solid rgba(127,127,127,0.4);
  /* The background is clipped to the PADDING box, not the border box.
     By default it paints underneath the border, and because that border is
     semi-transparent the gradient showed through it - and at the gradient's hard
     diagonal stop the two clips disagreed, leaving little square tabs of colour
     poking outside the circle at opposite corners. Clipping to the padding box
     means the colour simply stops before the ring and there is nothing to
     disagree about. box-sizing keeps the dot 18px overall rather than 20px. */
  background-clip: padding-box;
  box-sizing: border-box;
}

/* ---- Nav ---- */
/* min-width:0 lets the nav be the thing that gives way when the bar is tight.
   Without it the nav holds its full intrinsic width and pushes the theme button
   off the row entirely - the button then lands alone on the next line, hard left,
   which is how an admin nav at 1024px used to look. */
.nav-menu { display: flex; align-items: center; min-width: 0; }
/* The links wrap WITHIN the nav rather than the nav wrapping within the header.
   A second line of nav links reads as a nav; a stray theme button on a line of
   its own reads as a bug. */
/* min-width:0 on BOTH boxes. A flex item defaults to min-width:auto, meaning it
   refuses to shrink below its max-content width - so setting it on .nav-menu
   alone changed nothing: the nav inside still held its full width and the theme
   button was still pushed off the row. */
.nav-menu > .header-nav { display: flex; flex-wrap: wrap; justify-content: flex-end; min-width: 0; }

.nav-burger {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 42px;
  height: 42px;
  cursor: pointer;
  border: none;
  background: none;
  padding: 0;
  border-radius: var(--radius);
}
.nav-burger:hover { background: var(--surface-alt); }
.nav-burger span {
  display: block;
  width: 22px;
  height: 2px;
  margin: 0 auto;
  background: var(--text);
  border-radius: 2px;
  transition: transform 0.2s, opacity 0.2s;
}
.nav-menu.open .nav-burger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-menu.open .nav-burger span:nth-child(2) { opacity: 0; }
.nav-menu.open .nav-burger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.header-nav {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.nav-link {
  color: var(--text-sec);
  padding: 0.3rem 0.65rem;
  border-radius: var(--radius);
  font-size: 0.9rem;
  white-space: nowrap;
  transition: color 0.15s, background 0.15s;
}
.nav-link:hover { color: var(--text); background: var(--surface-alt); text-decoration: none; }
.nav-link.active { color: var(--accent); background: var(--accent-soft); text-decoration: none; }

/* Dropdown groups */
.nav-group { position: relative; }

.nav-group-btn {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  color: var(--text-sec);
  padding: 0.3rem 0.65rem;
  border-radius: var(--radius);
  font-size: 0.9rem;
  font-family: var(--font);
  white-space: nowrap;
  background: none;
  border: none;
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
}
.nav-group-btn:hover { color: var(--text); background: var(--surface-alt); }
.nav-group-btn.active { color: var(--accent); background: rgba(255,122,0,0.12); }
.nav-group-btn svg { transition: transform 0.2s; flex-shrink: 0; }
.nav-group.open .nav-group-btn svg { transform: rotate(180deg); }

.nav-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 150px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  z-index: 200;
  padding: 0.25rem;
}
.nav-group.open .nav-dropdown { display: block; }

.nav-dropdown-item {
  display: block;
  color: var(--text-sec);
  padding: 0.45rem 0.7rem;
  border-radius: calc(var(--radius) - 2px);
  font-size: 0.88rem;
  white-space: nowrap;
  transition: color 0.15s, background 0.15s;
}
.nav-dropdown-item:hover { color: var(--text); background: var(--surface-alt); text-decoration: none; }
.nav-dropdown-item.active { color: var(--accent); background: rgba(255,122,0,0.12); text-decoration: none; }

/* The template emits a literal "|" here; render it as a hairline rule instead
   of a text glyph, which sat at the wrong optical weight next to the nav pills. */
.nav-divider {
  width: 1px;
  height: 1.35rem;
  background: var(--border);
  color: transparent;
  overflow: hidden;
  user-select: none;
  padding: 0;
  margin: 0 0.45rem;
  flex-shrink: 0;
}
.nav-user { color: var(--text); font-weight: 600; font-size: 0.88rem; white-space: nowrap; }
/* Who you are and how to stop being them, kept on the same line as each other
   whatever the nav does around them. The left border is the old .nav-divider
   element: as a sibling it was one more place the nav could break, and it broke
   there - leaving a vertical bar alone at the end of a line. */
.nav-identity {
  display: inline-flex; align-items: center; gap: 0.5rem; flex-shrink: 0;
  border-left: 1px solid var(--border);
  padding-left: 0.7rem; margin-left: 0.3rem;
}

/* ---- Main content ---- */
.main-content {
  flex: 1;
  max-width: 1200px;
  width: 100%;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

/* Data-dense pages (live streams, analytics) opt into a wider column so their
   tables use the full screen with just the page padding as a buffer, instead of
   being squeezed into 1200px and clipped behind a horizontal scrollbar. */
.main-content.main-wide { max-width: 1800px; }

/* ---- Footer ---- */
.site-footer {
  text-align: center;
  padding: 1.5rem;
  color: var(--text-sec);
  font-size: 0.85rem;
  border-top: 1px solid var(--border);
}

/* ---- Page header ---- */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1.5rem;
  flex-wrap: wrap;
  gap: 1rem;
}
.page-header h1 {
  margin: 0;
  font-size: 1.6rem;
  font-weight: 600;
}
.page-header-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
/* Subtitle under a page title. flex-basis:100% drops it onto its own line below
   the h1 within the flex .page-header (rather than beside it). */
.page-subtitle {
  flex-basis: 100%;
  width: 100%;
  margin: 0.35rem 0 0;
  color: var(--text-sec);
  font-size: 0.9rem;
}
/* Failure reason shown under a subject in the email audit log. */
.email-error { font-size: 0.8rem; margin-top: 0.2rem; word-break: break-word; }

/* Paginator (analytics stream history) */
.pager { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; padding: 0.9rem 1rem; justify-content: center; }
.pager-status { font-size: 0.85rem; color: var(--text-sec); padding: 0 0.4rem; }
/* Both spellings. `.disabled` is the class some links carry (an <a> cannot be
   disabled); `[disabled]` is the real attribute on a <button>, and it was
   styled by nothing at all - so a genuinely dead button rendered in full
   primary orange, looking like the most inviting thing on the page. The one on
   My Cloud has been drawing presses that can only answer with a red flash. */
.btn.disabled,
.btn:disabled { opacity: 0.45; pointer-events: none; cursor: default; }

/* Dashboard "needs attention" lists (open tickets / pending requests) */
.attention-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.7rem; }
.attention-list li { display: flex; flex-direction: column; gap: 0.15rem; }
.attention-link { font-weight: 600; }
.attention-meta { font-size: 0.78rem; color: var(--text-sec); }

/* ---- Cards ---- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.card-narrow { max-width: 480px; }
.card-full { grid-column: 1 / -1; }
/* Vertical spacing for cards stacked in normal document flow. */
.card + .card, .mt-4 { margin-top: 1.5rem; }
/* ...but cards laid out horizontally are spaced by their container's `gap`, so the
   stacking margin must be cancelled there - otherwise every card after the first in
   a row is pushed down 1.5rem and the row no longer lines up at the top. */
.cards-grid > .card + .card,
.cards-masonry > .card + .card { margin-top: 0; }
/* A standalone card directly followed by a card container (e.g. the invitations
   card above the subscription grid) gets the same gap as the cards inside it -
   `.card + .card` can't match a grid/masonry wrapper, so the gap would collapse. */
.card + .cards-grid,
.card + .cards-masonry { margin-top: 1.25rem; }
.mt-2 { margin-top: 0.6rem; }
/* Bottom-margin utilities - the templates lean on these between stacked
   sections (cards, alerts, expanders), so they must actually exist. Adjacent
   top/bottom margins collapse in normal flow, so pairing these with
   `.card + .card` doesn't double the gap. */
.mb-2 { margin-bottom: 0.6rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mb-6 { margin-bottom: 2rem; }

/* First-run setup wizard */
.setup-wrap { max-width: 720px; margin: 2rem auto; padding: 0 1rem; }

/* Key/value description list (user detail) */
.kv { display: grid; grid-template-columns: minmax(110px, max-content) 1fr; gap: 0.5rem 1rem; margin: 0; }
.kv dt { color: var(--text-sec); font-size: 0.85rem; }
.kv dd { margin: 0; }
.btn-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.mb-6 { margin-bottom: 1.5rem; }
.ml-4 { margin-left: 1rem; }

/* ---- Admin user detail ---- */
.user-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem 2rem;
}
.summary-item { display: flex; flex-direction: column; gap: 0.2rem; }
.summary-item-wide { grid-column: 1 / -1; }
.summary-label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-sec); font-weight: 600; }
.summary-value { font-size: 0.95rem; word-break: break-all; }
.summary-mono { font-family: 'SFMono-Regular', 'Consolas', monospace; font-size: 0.8rem; color: var(--text-sec); }

.two-col-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }

.plan-form-row { display: grid; grid-template-columns: auto 1fr; gap: 1.5rem; align-items: start; }
.toggle-group { display: flex; flex-direction: column; }

.credit-form { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; }
.credit-form .field { margin: 0; flex: 0 1 180px; }
.credit-form .field input, .credit-form .field select { width: 100%; }
.field-action label { display: block; }

/* Danger zone */
.danger-zone { margin-top: 2rem; }
.danger-zone-header {
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--danger);
  padding: 0 0.25rem 0.6rem;
  border-bottom: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
  margin-bottom: 1rem;
}
.card-danger { border-color: color-mix(in srgb, var(--danger) 30%, var(--border)); }

/* Emby sync paid toggle */
.sync-paid-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  cursor: pointer;
  color: var(--text-sec);
  font-size: 0.85rem;
  white-space: nowrap;
  user-select: none;
}
.sync-paid-toggle input[type="checkbox"] { accent-color: var(--accent); width: 14px; height: 14px; }

/* Inline request status-update form (admin requests table) */
.request-update-row { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.request-update { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.request-update select,
.request-update input[type="text"] {
  background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text); font-family: var(--font);
  font-size: 0.85rem; padding: 0.35rem 0.5rem;
}
.request-update input[type="text"] { min-width: 140px; }

.card-header {
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Fixed header height so every card's body (and first field) starts at the
     same vertical position, keeping cards in a row lined up at the top. */
  min-height: 3.5rem;
  box-sizing: border-box;
}
/* Sentence case at full contrast. The old all-caps + letter-spacing in the muted
   secondary colour shouted at the reader while being harder to scan, and made
   every card title compete with the page h1 for attention. Size/weight still
   keep it clearly subordinate to the h1. */
.card-header h2 {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text);
}

.card-body { padding: 1.25rem; flex: 1; }
.card-body p:first-child { margin-top: 0; }
.card-body p:last-child { margin-bottom: 0; }

/* ---- Cards grid ---- */
/* Equal-height cards in clean rows. Use .cards-masonry instead for pages whose
   cards vary a lot in height (e.g. the account settings page), so short cards
   don't leave big gaps under them. */
.cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1.25rem;
  align-items: stretch;
}

/* ── The "where to go now" tiles on the dashboard ────────────────────────────
   Big targets, at the top of the page, because someone who has just signed in
   nearly always came to watch something - and before these existed the only
   route to either was a nav link the same size as "Payments".

   auto-fit rather than a fixed three columns: the row holds one, two or three
   tiles depending on what this member is entitled to, and a fixed grid would
   leave a member with only the library staring at a third of a row. */
.watch-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1rem;
  margin-bottom: 1.25rem;
}
.watch-tile {
  --tile: var(--accent);
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 6.5rem;
  padding: 1.15rem 1.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* Two layers: a tinted wash keyed to the tile, over the surface colour. The
     wash is a colour-mix on --tile, which is always defined above - an undefined
     var() inside color-mix voids the whole declaration silently, leaving a
     transparent tile that looks like a CSS file that failed to load. */
  background:
    radial-gradient(120% 140% at 100% 0%, color-mix(in srgb, var(--tile) 22%, transparent), transparent 60%),
    var(--surface);
  color: var(--text);
  text-decoration: none;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.watch-tile:hover, .watch-tile:focus-visible {
  text-decoration: none;
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--tile) 55%, var(--border));
  box-shadow: 0 10px 24px -12px color-mix(in srgb, var(--tile) 70%, transparent);
}
/* The sheen: a soft band that crosses the tile continuously.

   A pseudo-element animated by TRANSFORM, not by `left` or `background-position`.
   A transform is composited - it never repaints the tile's text - which is what
   makes a permanently-running animation cost nothing on a page people sit on.

   Subtle on purpose. At 14% of the tile colour it reads as a slow shimmer rather
   than a highlight sweeping past, which is the difference between something you
   notice once and something that pulls your eye off the text every nine
   seconds. */
.watch-tile::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -35%;
  width: 35%;
  background: linear-gradient(100deg, transparent, color-mix(in srgb, var(--tile) 14%, transparent), transparent);
  animation: watch-sheen 9s linear infinite;
  pointer-events: none;
}
@keyframes watch-sheen {
  /* Both ends of the travel put the band fully OFF the tile - it starts at
     -35%..0% and finishes at 105%..140% - so the loop restarts where nothing is
     drawn and there is no jump to see. A keyframe that merely faded out would
     leave a dark moment each cycle, which is the flicker this shape avoids. */
  from { transform: translateX(0) skewX(-18deg); }
  to { transform: translateX(400%) skewX(-18deg); }
}
.watch-tile-art {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  width: 3rem; height: 3rem;
  color: var(--tile);
  transition: transform 0.18s ease;
}
.watch-tile-art svg { width: 100%; height: 100%; fill: currentColor; stroke: currentColor; }
.watch-tile:hover .watch-tile-art { transform: scale(1.08); }
.watch-tile-text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.watch-tile-text strong { font-size: 1.15rem; }
.watch-tile-text span { color: var(--text-sec); font-size: 0.85rem; }
/* Distinct hues so the three are told apart at a glance rather than by reading.
   Fixed values, not theme tokens: these are identities, and a theme that recoloured
   them would make Live TV and the big screen the same tile in every way that
   matters at arm's length. */
.watch-tile-tv { --tile: #3aa3ff; }
.watch-tile-screen { --tile: #8b5cf6; }

@media (prefers-reduced-motion: reduce) {
  /* The sheen goes entirely - it is decoration and it repeats forever. The hover
     lift stays: it is feedback for a deliberate action, not ambient movement. */
  .watch-tile::after { animation: none; opacity: 0; }
  .watch-tile, .watch-tile-art { transition: none; }
}

/* The second row: everywhere else this member might be going.
   Deliberately QUIETER than the watch tiles - smaller, unfilled, no animation -
   because the eye has to land on the first row first. Nine tiles of equal weight
   is a menu, and the nav bar is already that. */
.go-tiles {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 1.5rem;
}
.go-tile {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: 0.9rem;
  text-decoration: none;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.go-tile:hover, .go-tile:focus-visible {
  text-decoration: none;
  background: var(--surface-hover);
  border-color: var(--accent);
}
/* The glyph is decoration and is hidden from screen readers in the markup. Fixed
   width so nine tiles with glyphs of different widths still line their labels up
   against each other down a wrapped row. */
.go-tile span {
  display: inline-block;
  width: 1.1rem;
  text-align: center;
  color: var(--accent);
}

/* ── The Help page ───────────────────────────────────────────────────────────
   One long page rather than a tree of them: short enough to read straight
   through, searchable with the browser's own find, and a member sent a link to
   one section can scroll to the rest. The contents row is the only navigation it
   needs. */
.help-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}
.help-toc a {
  padding: 0.4rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-sec);
  font-size: 0.85rem;
  text-decoration: none;
}
.help-toc a:hover { text-decoration: none; color: var(--text); border-color: var(--accent); }

/* scroll-margin, not padding: the contents links are in-page jumps, and without
   it the sticky header sits over the heading the reader was just sent to. */
.help-toc + section, .card[id] { scroll-margin-top: 5rem; }

.help-faq dt { font-weight: 600; margin-top: 1.1rem; }
.help-faq dt:first-child { margin-top: 0; }
.help-faq dd { margin: 0.3rem 0 0; color: var(--text-sec); max-width: 70ch; }

/* ---- Cards masonry ---- */
/* Column packing (Pinterest-style) for collections of differently-sized cards:
   each card keeps its natural height and the columns fill tightly, avoiding both
   the stretched-tall look and the ragged inter-row gaps a grid would leave. */
.cards-masonry {
  columns: 340px;
  column-gap: 1.25rem;
}
.cards-masonry > .card {
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  margin: 0 0 1.25rem;
}
/* Cancel the global adjacent-card top margin inside masonry (cards stack via the
   bottom margin above instead). */
.cards-masonry > .card + .card { margin-top: 0; }

/* ---- Stats ---- */
.section-label {
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-sec);
  margin: 0.5rem 0 0.75rem;
}
.stats-row {
  display: flex;
  gap: 1rem;
  margin-bottom: 1.5rem;
  flex-wrap: wrap;
}
/* Revenue figures are longer strings than counts - smaller, single line. */
.stats-row-revenue .stat-value { font-size: 1.5rem; white-space: nowrap; }

/* Live streams - card view */
.stream-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
  margin-bottom: 1.5rem;
}
.stream-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
}
.stream-card-danger { border-color: var(--danger); }

/* Card header: username takes all available space, badge never clips */
.stream-card-head {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  gap: 0.5rem;
  min-width: 0;
}
.stream-card-user {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.88rem;
  min-width: 0;
}
.stream-card-badge { flex-shrink: 0; }
.stream-card-title { font-size: 0.95rem; line-height: 1.35; }
.stream-card-meta { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; font-size: 0.82rem; }
.stream-card-actions { margin-top: 0.15rem; }

/* Sortable table column indicators */
th[data-sort-dir="asc"]::after  { content: " ↑"; font-size: 0.75em; opacity: 0.7; }
th[data-sort-dir="desc"]::after { content: " ↓"; font-size: 0.75em; opacity: 0.7; }

/* Playback progress bar */
.stream-progress {
  height: 3px;
  background: var(--border);
  border-radius: 2px;
  overflow: hidden;
  margin-top: 0.1rem;
}
.stream-progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 2px;
  transition: width 0.3s ease;
}

/* Live Streams "By server" view: each server rendered as a stacked usage bar
   (direct vs transcoding) scaled to a 50-stream capacity. */
.server-bar-legend {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
  font-size: 0.8rem;
  color: var(--text);
}
.server-bar-key {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  margin-right: 0.35rem;
  vertical-align: middle;
}
.card-header.server-bar-summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.6rem;
}
.server-bar-summary::-webkit-details-marker { display: none; }
.server-bar-line { display: flex; align-items: center; gap: 0.75rem; }
.server-bar-name {
  margin: 0;
  flex: 1;
  font-size: 1rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-sec);
}
.server-bar-counts {
  font-size: 0.82rem;
  font-weight: 400;
  color: var(--text-sec);
  white-space: nowrap;
}
.server-bar-track {
  position: relative;
  display: flex;
  width: 100%;
  height: 12px;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.server-bar-seg { height: 100%; min-width: 2px; transition: width 0.3s ease; }
.server-bar-seg + .server-bar-seg { box-shadow: -1px 0 0 rgba(0,0,0,0.35); }
.server-bar-direct { background: var(--success); }
.server-bar-transcode { background: var(--warning); }

/* Dual-limit server gauge: total streams (/100) stacked over transcodes (/50). */
.server-gauge { display: flex; flex-direction: column; gap: 4px; width: 100%; }
/* Half-capacity tick on the /100 bar (the 50-stream mark). */
.server-bar-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  background: var(--text-sec);
  opacity: 0.45;
  z-index: 1;
}
/* Slimmer transcode gauge against its own 50 ceiling: green → amber → red. */
.server-bar-track-sub { height: 6px; }
.server-bar-track-sub .server-bar-transcode-fill { background: var(--success); }
.server-bar-track-sub.is-warn .server-bar-transcode-fill { background: var(--warning); }
.server-bar-track-sub.is-critical .server-bar-transcode-fill { background: var(--danger); }

/* Invitations: member-home card + admin shareable-link controls */
.invite-send-form { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; margin: 0.5rem 0 0; }
.invite-send-form input[type="email"] { flex: 1; min-width: 220px; }
.invite-list { list-style: none; margin: 1rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.invite-item { padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius, 8px); background: rgba(255,255,255,0.02); }
.invite-item-head { display: flex; align-items: center; gap: 0.5rem; }
.invite-item-link { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.4rem; }
.invite-item-link code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8rem; }
.copy-row { display: flex; gap: 0.5rem; align-items: center; }
.copy-input { flex: 1; font-family: monospace; font-size: 0.82rem; min-width: 0; }
.invite-plan { border: 1px solid var(--border); border-radius: var(--radius, 8px); padding: 0.75rem 1rem 0.25rem; margin: 0.5rem 0 1rem; }
.invite-plan legend { font-size: 0.82rem; color: var(--text-sec); padding: 0 0.4rem; }

/* Site footer - body is a flex column with main flex:1, so this sticks to bottom */
.site-footer { border-top: 1px solid var(--border); padding: 1.25rem 1.5rem; }
.site-footer-inner { max-width: 1200px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.site-footer-name { color: var(--text-sec); font-size: 0.85rem; }
.site-footer-links { display: flex; gap: 1.5rem; }
.site-footer-links a { color: var(--text-sec); font-size: 0.85rem; }
.site-footer-links a:hover { color: var(--accent); }

/* Legal document pages (privacy policy) */
.legal-doc .card-body { line-height: 1.65; max-width: 72ch; }
.legal-doc h2 { margin-top: 1.6rem; font-size: 1.08rem; }
.legal-doc ul { padding-left: 1.2rem; }
.legal-doc li { margin-bottom: 0.35rem; }

/* Live toggle button */
.live-controls { display: flex; align-items: center; gap: 0.75rem; }
.live-btn { display: flex; align-items: center; gap: 0.4rem; }
.live-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--text-sec);
  flex-shrink: 0;
  transition: background 0.2s;
}
.live-dot-pulse { background: var(--danger); animation: live-pulse 1.2s ease-in-out infinite; }
.live-btn.live-active { color: var(--danger); border-color: var(--danger); }
@keyframes live-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.5; transform: scale(0.75); }
}
.stat-card {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 1rem 1.5rem;
  min-width: 140px;
}
.stat-value { font-size: 2rem; font-weight: 700; color: var(--accent); }
.stat-value-unit { font-size: 1.2rem; font-weight: 400; opacity: 0.7; }
.stat-label { color: var(--text-sec); font-size: 0.85rem; margin-top: 0.2rem; }
.stat-sublabel { display: block; font-size: 0.78rem; margin-top: 0.15rem; opacity: 0.75; }

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem 1.1rem;
  border-radius: var(--radius);
  font-size: 0.95rem;
  font-weight: 500;
  font-family: var(--font);
  cursor: pointer;
  border: 1px solid transparent;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
  text-decoration: none;
  gap: 0.4rem;
  /* Never break a label mid-phrase: in the tight header nav, "Sign out" was
     wrapping to two lines and knocking the whole bar out of vertical alignment. */
  white-space: nowrap;
}

.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; text-decoration: none; }

.btn-secondary { background: transparent; color: var(--accent); border-color: var(--accent); }
.btn-secondary:hover { background: rgba(255,122,0,0.12); text-decoration: none; }

.btn-outline { background: transparent; color: var(--text-sec); border-color: var(--border); }
.btn-outline:hover { color: var(--text); background: var(--surface-alt); text-decoration: none; }

.btn-ghost { background: transparent; color: var(--text-sec); border-color: transparent; }
.btn-ghost:hover { color: var(--text); background: var(--surface-alt); text-decoration: none; }

.btn-purple { background: var(--purple); color: #fff; border-color: var(--purple); }
.btn-purple:hover { background: var(--purple-hover); border-color: var(--purple-hover); color: #fff; text-decoration: none; }

.btn-danger { background: transparent; color: var(--danger); border-color: var(--danger); }
.btn-danger:hover { background: rgba(255,51,85,0.12); text-decoration: none; }

.btn-success { background: transparent; color: var(--success); border-color: var(--success); }
.btn-success:hover { background: rgba(34,197,94,0.12); text-decoration: none; }

.btn-sm { padding: 0.25rem 0.7rem; font-size: 0.85rem; }
.btn-full { width: 100%; }

.inline { display: inline; }

/* ---- Forms ---- */
.field { margin-bottom: 1.1rem; }

.radio-group { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.radio-label {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.45rem 0.9rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer; font-size: 0.9rem; color: var(--text-primary);
  transition: border-color 0.15s;
}
.radio-label:has(input:checked) { border-color: var(--accent); color: var(--accent); }
.radio-label input[type="radio"] { accent-color: var(--accent); margin: 0; }

.field label {
  display: block;
  margin-bottom: 0.4rem;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--text-sec);
}

/* ── EVERY text control, themed by default ───────────────────────────────────
   Form controls used to be dressed only inside `.field`, `.tv-filter` and a
   couple of other wrappers. Anything else - the invite form's email box, a
   file picker, a stray <select> - rendered with the browser's own styling,
   which under every dark theme means a WHITE box: a bright bar across a dark
   page, and the brightest thing on it.

   A base rule rather than another wrapper, because the failure mode of a
   wrapper is silent and it recurs: the next control someone adds outside one is
   white again, and it only shows up if the person adding it is using a dark
   theme at the time.

   Listed by type rather than as a bare `input`, on purpose. Checkboxes, radios,
   ranges and colour pickers have to keep their native rendering - painting a
   background on a checkbox makes it a grey square with no tick. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="url"],
input[type="tel"],
input[type="date"],
input[type="time"],
input[type="month"],
select,
textarea {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font);
  font-size: 0.95rem;
  padding: 0.55rem 0.85rem;
  outline: none;
}
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="url"]:focus,
input[type="tel"]:focus,
select:focus,
textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring);
}
/* The placeholder too: the UA default is a mid-grey chosen for a white box and
   is close to unreadable on a dark one. */
input::placeholder, textarea::placeholder { color: var(--text-sec); opacity: 0.75; }
/* A file input's own box cannot be fully restyled, but its BUTTON can - and the
   button is the bright part. */
input[type="file"] { color: var(--text-sec); font-size: 0.9rem; }
input[type="file"]::file-selector-button {
  background: var(--surface-hover);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font);
  font-size: 0.9rem;
  padding: 0.4rem 0.8rem;
  margin-right: 0.6rem;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { border-color: var(--accent); }

.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="number"],
.field select,
.field textarea {
  width: 100%;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font);
  font-size: 0.95rem;
  padding: 0.55rem 0.85rem;
  transition: border-color 0.15s;
  outline: none;
}
.field textarea { resize: vertical; line-height: 1.5; }
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring);
  outline: none;
}
.field input::placeholder, .field textarea::placeholder { color: var(--text-sec); opacity: 0.7; }

.field-hint, .field-help { color: var(--text-sec); font-size: 0.82rem; margin-top: 0.3rem; display: block; }

/* Two fields side by side (e.g. the per-server stream/transcode limits); stacks
   on narrow screens. */
.field-row { display: flex; gap: 1rem; flex-wrap: wrap; }
.field-row .field { flex: 1; min-width: 140px; }

/* Inline checkbox toggle (settings booleans) */
.check-field { display: flex; align-items: center; gap: 0.55rem; }
.check-field input[type="checkbox"],
/* Radios were unstyled: this rule selected checkboxes only, so the scope choice
   on the built-in provider rendered at the browser default size and in the
   browser default colour, next to controls that do neither. */
.check-field input[type="radio"] { width: 18px; height: 18px; accent-color: var(--accent); }
.check-field label { margin: 0; color: var(--text); font-weight: 400; }

/* Toggle switch (IPTV / 4K add-ons) */
.toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.7rem 0;
  margin: 0;
  cursor: pointer;
  border-top: 1px solid var(--border);
}
.toggle-text { display: flex; flex-direction: column; gap: 0.1rem; }
.toggle-title { color: var(--text); font-weight: 500; }
.toggle-sub { color: var(--text-sec); font-size: 0.8rem; }
.toggle { position: relative; flex-shrink: 0; width: 44px; height: 26px; }
.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-track {
  display: block;
  width: 44px;
  height: 26px;
  border-radius: 999px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  transition: background 0.18s, border-color 0.18s;
}
.toggle-thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--text-sec);
  transition: transform 0.18s, background 0.18s;
}
.toggle input:checked + .toggle-track { background: var(--accent); border-color: var(--accent); }
.toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(18px); background: #fff; }
.toggle input:focus-visible + .toggle-track { box-shadow: 0 0 0 3px rgba(255,122,0,0.25); }

/* Secondary auth link (forgot password / back to sign in) */
.auth-alt { text-align: center; margin-top: 1rem; font-size: 0.88rem; }

/* Range slider */
.stream-row { display: flex; align-items: center; gap: 0.8rem; }
.stream-row input[type="range"] { flex: 1; accent-color: var(--accent); }
.stream-value {
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--accent);
  min-width: 1.5rem;
  text-align: center;
}

/* Checkbox */
.checkbox-label {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  color: var(--text) !important;
  font-weight: normal !important;
}
.checkbox-label input[type="checkbox"] { display: none; }
.checkbox-box {
  width: 18px; height: 18px;
  border: 2px solid var(--border);
  border-radius: 4px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  transition: background 0.15s, border-color 0.15s;
}
.checkbox-label input:checked + .checkbox-box {
  background: var(--accent);
  border-color: var(--accent);
}
.checkbox-label input:checked + .checkbox-box::after {
  content: '✓';
  color: white;
  font-size: 12px;
  font-weight: bold;
}

/* Price display */
.price-display {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin: 1rem 0;
  padding: 0.75rem 1rem;
  background: var(--bg-elevated);
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.price-label { color: var(--text-sec); font-size: 0.9rem; }
.price-value { font-size: 1.6rem; font-weight: 700; color: var(--accent); }
.price-currency { color: var(--text-sec); font-size: 0.85rem; }

/* ---- Auth pages ---- */
.auth-wrap {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 3rem 1rem;
}

.auth-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2rem;
  width: 100%;
  max-width: 420px;
}

.auth-logo { text-align: center; margin-bottom: 1.2rem; }
.auth-title { text-align: center; font-size: 1.5rem; font-weight: 700; margin: 0 0 0.4rem; }
.auth-subtitle { text-align: center; color: var(--text-sec); margin: 0 0 1.5rem; }
.auth-form { margin-top: 1.5rem; }

/* ALTCHA bot-protection widget, themed to match the dark UI via its documented
   CSS custom properties. Sits between the last field and the submit button.
   Targets the host's `altcha-field` class (NOT `altcha`) so it doesn't collide
   with ALTCHA's own injected `.altcha` frame - see _altcha.html. The custom
   properties below still inherit into the widget the library renders inside. */
.altcha-field {
  display: block;
  margin: 0 0 0.75rem;
  --altcha-max-width: 100%;
  --altcha-border-width: 1px;
  --altcha-border-radius: var(--radius, 8px);
  --altcha-color-base: var(--bg-elevated, #2d3447);
  --altcha-color-border: var(--border, #3d4a6b);
  --altcha-color-text: var(--text, #fff);
  --altcha-color-border-focus: var(--accent, #ff7a00);
  --altcha-color-error-text: var(--danger, #cc3333);
}

/* ---- Badges ---- */
.badge {
  display: inline-block;
  padding: 0.2em 0.65em;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.badge-active  { background: rgba(34,197,94,0.15);   color: var(--success); }
.badge-trial   { background: rgba(96,165,250,0.15);  color: var(--trial-color); }
.badge-warning { background: rgba(255,149,0,0.15);   color: var(--warning); }
.badge-danger  { background: rgba(255,51,85,0.15);   color: var(--danger); }
.badge-muted   { background: rgba(136,136,187,0.12); color: var(--text-sec); }
.badge-4k      { background: rgba(139,92,246,0.22);  color: #c4b5fd; }
.badge-info    { background: rgba(139,92,246,0.15);  color: #c4b5fd; }

.ml-1 { margin-left: 0.25rem; }

/* ── Stream activity page ─────────────────────────────────────────────────── */
.stat-card-link {
  display: block;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s;
}
.stat-card-link:hover {
  background: var(--surface-alt);
  text-decoration: none;
}
.stat-card-active .stat-value { color: var(--accent); }
.stat-card-warn   .stat-value { color: var(--warning); }
.stat-card-danger .stat-value { color: var(--danger); }

/* Per-server load square on the dashboard: a stat-card holding the same capacity
   gauge as the live-activity by-server view, sized to sit in the stats row. */
.stat-card-server { flex: 1 1 200px; max-width: 320px; }
.stat-card-server-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}
.stat-card-server-name {
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stat-card-server-count { font-size: 0.9rem; font-weight: 700; color: var(--accent); white-space: nowrap; }
.stat-card-server .server-gauge { margin-bottom: 0.4rem; }

.row-danger { background: rgba(204,51,51,0.06); }
.row-changed { background: rgba(229,160,13,0.08); }

/* ---- Alerts ---- */
.alert {
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  margin-bottom: 1rem;
  font-size: 0.9rem;
}
.alert-error   { background: rgba(204,51,51,0.15);   border: 1px solid rgba(204,51,51,0.4);   color: #ff7070; }
.alert-success { background: rgba(82,181,75,0.15);   border: 1px solid rgba(82,181,75,0.4);   color: #8de885; }
.alert-warning { background: rgba(229,160,13,0.15);  border: 1px solid rgba(229,160,13,0.4);  color: var(--warning); }
.alert-info    { background: rgba(82,140,229,0.15);  border: 1px solid rgba(82,140,229,0.4);  color: #8db4ff; }

/* ---- Expandable edit rows (details/summary with a real click target) ---- */
.expander {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: rgba(136,136,187,0.06);
  color: var(--text-sec);
  cursor: pointer;
  user-select: none;
  font-size: 0.9rem;
  list-style: none;            /* hide the tiny default disclosure triangle */
}
.expander::-webkit-details-marker { display: none; }
.expander:hover { background: rgba(136,136,187,0.12); color: var(--text); }
.expander-arrow { transition: transform 0.15s ease; font-size: 1rem; }
details[open] > .expander { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
details[open] > .expander .expander-arrow { transform: rotate(180deg); }
/* An expander used as a whole card's header (e.g. "Add a new app"): the card
   already draws the border and radius, so the summary drawing its own box
   produces a double border. Make it read like a card header instead. */
.card > .expander {
  border: none;
  border-radius: 0;
  background: transparent;
  padding: 0.85rem 1.25rem;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text);
}
.card > .expander:hover { background: rgba(136,136,187,0.08); }
details.card[open] > .expander { border-bottom: 1px solid var(--border); }
.card > .expander-body { border: none; border-radius: 0; padding: 1.25rem; }

/* ---- My Cloud price lines ---- */
.price-lines { list-style: none; padding: 0; margin: 0.6rem 0 0.2rem; }
.price-lines li { padding: 0.2rem 0; font-size: 0.9rem; }

/* ---- Settings page: collapsible sections, search, sticky save ---- */
.settings-toolbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1rem;
}
.settings-toolbar input[type="search"] {
  flex: 1;
  max-width: 24rem;
  padding: 0.5rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: 0.9rem;
}
details.settings-group { display: block; }
.settings-group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.85rem 1rem;
  cursor: pointer;
  user-select: none;
  list-style: none;
}
.settings-group-head::-webkit-details-marker { display: none; }
.settings-group-head:hover { background: rgba(136,136,187,0.06); }
.settings-group-head h2 { margin: 0; font-size: 1.02rem; }
.settings-group-side { display: flex; align-items: center; gap: 0.7rem; color: var(--text-sec); }
.settings-group-count { font-size: 0.78rem; }
details[open] > .settings-group-head .expander-arrow { transform: rotate(180deg); }
.settings-group-body {
  padding: 0.9rem 1rem 1rem;
  border-top: 1px solid var(--border);
}
/* Per-group action row. Each group saves itself: one page-wide button meant an
   admin editing "Features" could not tell what else they were about to submit,
   and it made a partial save impossible. */
.settings-group-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1.1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--border);
}
/* "Unsaved" in the group header, so edits are visible on a COLLAPSED section -
   otherwise making a change and shutting the section looks like it saved. */
.settings-group-dirty {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.settings-group-is-dirty { border-color: var(--warning); }

.settings-savebar {
  position: sticky;
  bottom: 0.75rem;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  margin-top: 1.5rem;
  padding: 0.7rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* Themed shadow: the hardcoded dark one this replaced read as a smudge on the
     light themes, where the bar floats over a near-white page. */
  box-shadow: var(--shadow-md);
  z-index: 5;
}

/* ---- Admin app-catalog list ---- */
.catalog-list { display: flex; flex-direction: column; gap: 1rem; }
.catalog-list .card + .card { margin-top: 0; }
.catalog-app-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
}
.catalog-app-title { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
.catalog-app-title h3 { margin: 0; font-size: 1.05rem; }
.catalog-app-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  font-size: 0.85rem;
  color: var(--text-sec);
}
/* Keep each label/value pair on one line - only whole pairs may wrap. */
.catalog-app-meta > span, .catalog-app-meta > a { white-space: nowrap; }
.catalog-meta-label {
  opacity: 0.7;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-right: 0.3rem;
}
.catalog-app-price { font-weight: 700; color: var(--accent); font-size: 0.95rem; }
.catalog-app-desc { color: var(--text-sec); font-size: 0.88rem; line-height: 1.5; margin: 0.65rem 0 0; }
.catalog-app-note { font-size: 0.8rem; line-height: 1.45; margin: 0.5rem 0 0; }
/* Breathing room between the app's text block and its Edit expander. */
.catalog-app .card-body > details { margin-top: 1rem; }
.expander-body {
  border: 1px solid var(--border);
  border-top: none;
  border-radius: 0 0 var(--radius) var(--radius);
  padding: 0.9rem;
}

/* ---- My Cloud app catalog ---- */
.app-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  /* Equal-height rows: every box in the catalog is the same size. */
  grid-auto-rows: 1fr;
  gap: 1rem;
}
/* Cancel the vertical-stacking margin (.card + .card) inside the grid - the
   grid's own gap spaces the cards; the margin was staggering every card after
   the first. */
.app-grid .card + .card { margin-top: 0; }
.app-card { height: 100%; }
.app-card .card-body { display: flex; flex-direction: column; height: 100%; }
.app-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; }
.app-card-price { font-weight: 700; color: var(--accent); margin-top: auto; }

/* ---- Trial notice ---- */
.trial-notice {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  background: rgba(82,181,75,0.08);
  border: 1px solid rgba(82,181,75,0.25);
  border-radius: var(--radius);
  font-size: 0.88rem;
  color: var(--text-sec);
  margin-bottom: 1.2rem;
}
.trial-notice strong { color: var(--text); }

/* Personal note an admin attached to an invite, shown on the registration page.
   pre-wrap keeps the inviter's line breaks; a left accent rule marks it as a
   quoted message rather than a system notice. */
.invite-message {
  padding: 0.75rem 1rem;
  margin-bottom: 1.2rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  font-size: 0.92rem;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-word;
  text-align: left;
}

/* ---- Tables ---- */
.table-wrap { overflow-x: auto; }

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}
.data-table th {
  text-align: left;
  padding: 0.7rem 1rem;
  color: var(--text-sec);
  font-weight: 600;
  font-size: 0.82rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.data-table td {
  padding: 0.8rem 1rem;
  border-bottom: 1px solid var(--border-subtle);
  vertical-align: middle;
}
.data-table tbody tr:hover { background: var(--bg-elevated); }
.data-table tbody tr:last-child td { border-bottom: none; }

.text-center { text-align: center; }
.text-secondary { color: var(--text-sec); }
.text-danger { color: var(--danger); }
.text-warning { color: var(--warning); }
.actions-cell { white-space: nowrap; }

/* ---- Kill stream popup ---- */
.kill-details summary { list-style: none; cursor: pointer; }
.kill-details summary::-webkit-details-marker { display: none; }
.kill-details[open] summary { margin-bottom: 0.5rem; }
.kill-popup {
  position: absolute;
  z-index: 200;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem;
  box-shadow: var(--shadow-lg);
  right: 0;
}
td:last-child { position: relative; }

/* ---- Emby sync ----
   One card per Emby account, holding the two things you can do with it. It was
   two full-width bordered boxes per user, stacked, which read as twice as many
   accounts as there were - and gave equal weight to importing (the common case)
   and linking to an existing member (the rare one). */
.sync-list { display: flex; flex-direction: column; gap: 0.6rem; }
.sync-user {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.03);
  overflow: hidden;
}
.sync-user-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  background: rgba(255, 255, 255, 0.02);
  border-bottom: 1px solid var(--border);
}
.sync-info { flex: 1; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }

/* One action per line, and the lines share a column grid so the email box and
   the account picker start at the same x - which is what stopped this looking
   like two unrelated widgets that happened to be adjacent. */
.sync-act {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.75rem;
}
.sync-act + .sync-act { border-top: 1px solid var(--border); }
/* The rarer path, quieter. Same controls, less contrast - not a different
   shape, which is what made it read as a separate feature.
   Mixed from the theme's own ink rather than a flat black: a black wash is a
   RECESS on a dark theme and a prominent grey band on a light one, which is the
   opposite of what "quieter" is supposed to mean on half the palettes. */
.sync-act-link { background: color-mix(in srgb, var(--text-sec) 7%, transparent); }
.sync-act-label {
  flex: 0 0 7.5rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-sec);
}
.sync-act-field { flex: 1 1 auto; min-width: 0; }
.sync-act-field .input-sm { width: 100%; }

/* The Emby User Sync page uses a bare .card-title instead of the padded
   .card-header/.card-body pattern, so inset its card contents to match the rest
   of the admin UI, and lay the section title out alongside its show/hide toggle. */
.page-emby-sync .card { padding: 1.5rem; }
.page-emby-sync .card-title { margin: 0 0 0.75rem; }
.page-emby-sync .sync-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}
.page-emby-sync .sync-head .card-title { margin: 0; }

/* Per-user hide control on each unsynced row (leading ✕, always visible even
   when the row's right-hand controls scroll off). */
.sync-hide-btn {
  flex: 0 0 auto;
  background: transparent;
  border: none;
  color: var(--text-sec);
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.4rem;
  border-radius: 4px;
}
.sync-hide-btn:hover { color: var(--danger); background: rgba(255,51,85,0.12); }

/* Server-side sortable column headers (e.g. User management). */
.sort-link { color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 0.3rem; white-space: nowrap; }
.sort-link:hover { color: var(--text); text-decoration: none; }
.sort-link.sort-active { color: var(--text); }
.sort-arrow { font-size: 0.7em; color: var(--accent); }

.input-sm {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 0.35rem 0.6rem;
  font-size: 0.88rem;
}
.input-sm:focus { outline: none; border-color: var(--accent); }


/* =============================================
   Theme overrides
   ============================================= */

[data-theme="oled"] {
  --bg:          #000000;
  --bg-elevated: #050505;
  --surface:     #0d0d0d;
  --surface-hover:#141414;
  --surface-alt: #141414;
  --border:      #202020;
  --text:        #ffffff;
  --text-sec:    #aaaaaa;
}

[data-theme="emby"] {
  --bg:          #1c2231;
  --bg-elevated: #232b3a;
  --surface:     #2d3447;
  --surface-hover:#394260;
  --surface-alt: #394260;
  --border:      #3d4a6b;
  --accent:      #52b54b;
  --accent-hover:#42a43b;
  --purple:      #52b54b;
  --text:        #ffffff;
  --text-sec:    #8b9dc3;
}

/* Jellyfin's own palette, taken from the brand rather than approximated: the
   logo's two colours are #aa5cc3 (purple) and #00a4dc (blue), and the app's
   chrome is a near-black with barely-lifted surfaces.

   The accent is the blue because it is the readable one of the pair against this
   background; the purple keeps the secondary role it already had, which is also
   what the logo does with it.

   HOVER GOES LIGHTER, NOT DARKER. The old --accent-hover was #0b8fc0, a DARKER
   blue than the accent it replaced on hover. On a near-black page a control that
   dims when you point at it reads as disabled - the feedback has to move away
   from the background, and here that means up. The surfaces were also too close
   together to separate a card from the page, so the steps are wider now. */
[data-theme="jellyfin"] {
  --bg:          #0d0d0d;
  --bg-elevated: #161616;
  --surface:     #1e1e1e;
  --surface-hover:#2c2c2c;
  --surface-alt: #262626;
  --border:      #333333;
  --accent:      #00a4dc;
  --accent-hover:#33bce8;
  --purple:      #aa5cc3;
  --text:        #ffffff;
  --text-sec:    #a4a4a4;
}

[data-theme="night"] {
  --bg:          #0d0d1a;
  --bg-elevated: #111122;
  --surface:     #18183a;
  --surface-hover:#202050;
  --surface-alt: #202050;
  --border:      #2a2a5a;
  --text:        #e0e0ff;
  --text-sec:    #7878aa;
}

[data-theme="sunny"] {
  --bg:          #ffefc2;   /* warm sunlit gold - much sunnier than the old cream */
  --bg-elevated: #fffbe9;
  --surface:     #fff6d6;
  --surface-hover:#ffe7a0;
  --surface-alt: #ffedb5;
  --border:      #f0d27a;
  --accent:      #d97706;   /* readable amber: btn-primary puts white text on it */
  --accent-hover:#b45309;
  --purple:      #7c3aed;
  --text:        #3a2600;
  --text-sec:    #8a6a12;
  --danger:      #dc2626;
  --warning:     #b45309;
  --success:     #16a34a;
  --trial-color: #2563eb;
}

[data-theme="day"] {
  --bg:          #f8f9fa;
  --bg-elevated: #ffffff;
  --surface:     #ffffff;
  --surface-hover:#f0f2f5;
  --surface-alt: #f0f2f5;
  --border:      #d0d7de;
  --accent:      #ff7a00;
  --accent-hover:#e06d00;
  --purple:      #8b5cf6;
  --text:        #24292f;
  --text-sec:    #57606a;
  --danger:      #cf222e;
  --warning:     #9a6700;
  --success:     #1a7f37;
  --trial-color: #0550ae;
}

[data-theme="sunny"] .site-header,
[data-theme="day"] .site-header {
  background: var(--bg-elevated);
  border-bottom-color: var(--border);
}
[data-theme="sunny"] .field input,
[data-theme="sunny"] .field select,
[data-theme="sunny"] .field textarea,
[data-theme="day"] .field input,
[data-theme="day"] .field select,
[data-theme="day"] .field textarea {
  background: var(--bg-elevated);
  color: var(--text);
}

/* Sunny: a warm sunrise wash across the header to sell the "sunny" feel.
   Placed after the shared light-theme header rule above so it wins. */
[data-theme="sunny"] .site-header {
  background: linear-gradient(180deg, #ffe7a0 0%, var(--bg-elevated) 100%);
}

/* Industrial: a neutral steel/charcoal dark theme with a cool steel-blue accent.
   The brand orange/purple are toned to steel so the whole UI reads monochrome. */
[data-theme="grey"] {
  --bg:          #1c1e21;
  --bg-elevated: #232629;
  --surface:     #2a2d31;
  --surface-hover:#34383d;
  --surface-alt: #313539;
  --border:      #40454b;
  --accent:      #5f97c4;
  --accent-hover:#4d82ac;
  --purple:      #8a93a0;
  --purple-hover:#9aa3b0;
  --text:        #e8eaed;
  --text-sec:    #99a0a8;
  --danger:      #e0686f;
  --warning:     #d6a44c;
  --success:     #7fbf6a;
  --trial-color: #6fa3c7;
}

/* ---- Analytics range picker ---- */
.window-pills {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: center;
  padding: 1rem 0 1.4rem;
}
.window-pill {
  padding: 0.5rem 1.5rem;
  border-radius: 2rem;
  border: 1px solid var(--border);
  color: var(--text-sec);
  text-decoration: none;
  font-size: 0.88rem;
  font-weight: 500;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.window-pill:hover { background: var(--surface-hover); color: var(--text); text-decoration: none; }
.window-pill.active { background: var(--accent); border-color: var(--accent); color: #fff; }

.range-divider { border: none; border-top: 1px solid var(--border); margin: 0 0 1.2rem; }
.range-row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-end; }
.range-sep { color: var(--text-sec); font-size: 0.8rem; align-self: center; padding: 0 0.2rem; margin-top: 1.4rem; }

input[type="datetime-local"] {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font);
  font-size: 0.88rem;
  padding: 0.45rem 0.7rem;
  min-width: 195px;
  transition: border-color 0.15s, box-shadow 0.15s;
}
input[type="datetime-local"]:focus {
  border-color: var(--accent);
  outline: none;
  box-shadow: 0 0 0 3px rgba(255,122,0,0.15);
}
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  filter: invert(0.65);
  cursor: pointer;
  opacity: 0.8;
}
input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }

.sort-arrow { font-size: 0.7rem; opacity: 0.5; }
.sort-arrow.active { opacity: 1; color: var(--accent); }
.report-empty { text-align: center; padding: 3rem 1rem; color: var(--text-sec); }
.report-empty .big { font-size: 2.5rem; opacity: 0.4; margin-bottom: 0.5rem; }

/* ---- Member connection guide (portal) ---- */
.connect-grid { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0.75rem 0 1.25rem; }
.connect-detail {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 0.6rem;
  background: var(--bg-elevated);
  border-radius: var(--radius);
  padding: 0.6rem 0.9rem;
  min-width: 160px;
}
.connect-label {
  flex-basis: 100%;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-sec);
}
.connect-value { font-size: 1.05rem; font-weight: 600; color: var(--text); word-break: break-all; }
.connect-copy { flex-shrink: 0; }
.connect-card h3 { font-size: 0.98rem; margin: 1.2rem 0 0.4rem; }
.connect-card ol { margin: 0 0 0.6rem; padding-left: 1.25rem; }
.connect-card ol li { margin: 0.35rem 0; }
.connect-card p code,
.connect-card li code {
  background: var(--bg-elevated);
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
  font-size: 0.9em;
}
/* "Get the Emby app" store badges, each with a scannable QR of the store link. */
.app-get { margin: 0.25rem 0 1.5rem; }
.app-get h3 { margin: 0 0 0.35rem; }
.app-get-more { font-size: 0.85rem; margin-top: 0.85rem; }
.app-badges { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.65rem; }
.app-badge {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.6rem 1rem 0.6rem 0.6rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--text);
  text-decoration: none;
  transition: border-color 0.15s, background 0.15s;
}
.app-badge:hover { border-color: var(--accent); background: var(--surface-hover); text-decoration: none; }
.app-badge-qr {
  flex-shrink: 0;
  width: 84px; height: 84px;
  background: #fff;            /* light quiet zone so phone cameras lock on */
  color: #000;                /* QR modules use currentColor */
  border-radius: 6px;
  padding: 6px;
}
.app-badge-qr svg { display: block; width: 100%; height: 100%; }
.app-badge-text { display: flex; flex-direction: column; line-height: 1.35; }
.app-badge-text small { color: var(--text-sec); font-size: 0.8rem; }

/* ---- Account credit banner (portal) ---- */
.credit-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  flex-wrap: wrap;
  background: rgba(82,181,75,0.10); border: 1px solid rgba(82,181,75,0.3);
  border-radius: var(--radius); padding: 0.6rem 0.9rem; margin-bottom: 1rem;
}

/* Square Web Payments SDK card field (auto-pay) */
.square-card {
  background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 0.6rem 0.75rem; margin: 0.75rem 0;
  min-height: 48px;
}

/* ---- Member status page ---- */
.status-pill { font-size: 0.82rem; font-weight: 600; padding: 0.3rem 0.8rem; border-radius: 999px; }
.status-up { color: #52b54b; background: rgba(82,181,75,0.14); }
.status-down { color: var(--danger); background: rgba(204,51,51,0.14); }
.status-pending { color: var(--warning); background: rgba(229,160,13,0.14); }
.plain-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.plain-list li { display: flex; align-items: center; gap: 0.6rem; }
.recent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.6rem; }
.recent-item {
  display: flex; align-items: center; gap: 0.5rem;
  background: var(--bg-elevated); border-radius: var(--radius); padding: 0.6rem 0.8rem;
}
.recent-badge {
  flex-shrink: 0; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.04em;
  padding: 0.12rem 0.4rem; border-radius: 4px; background: var(--surface-alt); color: var(--text-sec);
}
.recent-title { flex: 1; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-year { color: var(--text-sec); font-size: 0.8rem; }

/* ---- Uptime heatmaps (status page) ---- */
.uptime-pct { font-size: 0.82rem; font-weight: 600; color: var(--text-sec); }
.heat-block { margin-bottom: 1.25rem; }
.heat-block:last-of-type { margin-bottom: 0; }
.heat-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.5rem; }
.heat-title { font-weight: 600; font-size: 0.9rem; }
.heat-strip { display: flex; gap: 3px; align-items: stretch; height: 30px; }
.heat-cell {
  flex: 1 1 0; min-width: 4px; border-radius: 3px;
  background: var(--surface-alt);
  transition: transform 0.08s ease;
}
.heat-strip .heat-cell:hover { transform: scaleY(1.12); }
.heat-up { background: #52b54b; }
.heat-partial { background: var(--warning, #e5a00d); }
.heat-down { background: var(--danger); }
.heat-none { background: var(--surface-alt); opacity: 0.5; }
.uptime-legend {
  display: flex; justify-content: space-between;
  margin-top: 0.4rem; font-size: 0.72rem; color: var(--text-sec);
}
.heat-legend { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1rem; font-size: 0.75rem; color: var(--text-sec); }
.heat-key { display: flex; align-items: center; gap: 0.35rem; }
.heat-legend .heat-cell { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 3px; }

/* Multi-server uptime blocks inside admin diagnostics */
.uptime-server-block { padding: 0.25rem 0 0.75rem; }
.uptime-server-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
.uptime-server-name { font-weight: 700; font-size: 1rem; }
.uptime-server-divider { border: none; border-top: 1px solid var(--border); margin: 0.5rem 0 1.25rem; }

/* ---- Diagnostics page spacing ----
   These cards have no body padding of their own: only the tables carry an inset
   (from their cell padding), leaving the titles, the configuration list and the
   uptime blocks hugging the card border. Give them the same 1rem inset and a
   little vertical rhythm so the whole page breathes. */
.page-diagnostics .card-title { margin: 0; padding: 1rem 1rem 0.7rem; }
.page-diagnostics .kv,
.page-diagnostics .btn-row,
.page-diagnostics .uptime-server-block { padding-left: 1rem; padding-right: 1rem; }
.page-diagnostics .kv { padding-bottom: 0.5rem; }
.page-diagnostics .btn-row { padding-bottom: 1rem; }
.page-diagnostics .uptime-server-divider { margin-left: 1rem; margin-right: 1rem; }

/* ---- Discovery (TMDB browse) ---- */

.discover-search-bar { display: flex; gap: 0.6rem; margin-bottom: 0.4rem; }
.discover-search-bar input {
  flex: 1; padding: 0.6rem 0.9rem; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius); color: var(--text);
  font-family: var(--font); font-size: 0.95rem;
}
.discover-search-bar input:focus { outline: none; border-color: var(--accent); }

.discover-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.9rem; }
.discover-card {
  display: flex; flex-direction: column;
  background: var(--surface-alt); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
}
.discover-card .poster {
  width: 100%; aspect-ratio: 2 / 3; object-fit: cover; display: block;
  background: var(--surface-hover);
}
.poster-placeholder {
  display: flex; align-items: center; justify-content: center;
  color: var(--text-sec); font-size: 0.85rem; font-weight: 600; letter-spacing: 0.04em;
}
.discover-meta { display: flex; flex-direction: column; gap: 0.35rem; padding: 0.55rem 0.6rem; flex: 1; }
.discover-title { font-size: 0.88rem; font-weight: 600; line-height: 1.25; }
.discover-sub { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; font-size: 0.78rem; }
.discover-badge {
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.04em;
  padding: 0.1rem 0.35rem; border-radius: 4px; background: var(--surface); color: var(--text-sec);
}
.discover-vote { color: var(--accent); font-weight: 600; }
.discover-action { margin-top: auto; }
.discover-request { margin-top: auto; }
.discover-request .btn { width: 100%; }

/* Poster thumbnails beside request rows */
.request-title { display: flex; align-items: flex-start; gap: 0.7rem; }
.poster-thumb {
  width: 42px; height: 63px; object-fit: cover; border-radius: 4px;
  background: var(--surface-hover); flex-shrink: 0;
}
.request-overview {
  font-size: 0.82rem; margin-top: 0.2rem; max-width: 42ch;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ---- Responsive ---- */

/* Tablet / mobile: hamburger collapses the nav; groups expand inline. */
@media (max-width: 860px) {
  .nav-burger { display: flex; }

  .nav-menu > .header-nav { display: none; }
  .nav-menu.open > .header-nav {
    display: flex;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0.1rem;
    padding: 0.5rem;
    background: var(--bg-elevated);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
    /* 70dvh rather than "the viewport minus a 60px header": the header is not
       60px any more once the ambient strip wraps onto its own row, and the old
       arithmetic ran the menu off the bottom of the screen by exactly the height
       it had gained. */
    max-height: 70dvh;
    overflow-y: auto;
  }

  /* Flat links */
  .nav-menu.open > .header-nav .nav-link {
    padding: 0.7rem 0.8rem;
    font-size: 1rem;
  }

  /* Dropdown groups - expand inline (accordion) */
  .nav-menu.open .nav-group { width: 100%; }
  .nav-menu.open .nav-group-btn {
    width: 100%;
    padding: 0.7rem 0.8rem;
    font-size: 1rem;
    justify-content: space-between;
  }
  .nav-menu.open .nav-dropdown {
    position: static;
    display: none;
    box-shadow: none;
    border: none;
    border-left: 2px solid var(--border);
    border-radius: 0;
    background: transparent;
    padding: 0.15rem 0 0.15rem 0.5rem;
    margin: 0 0 0.2rem 0.8rem;
  }
  .nav-menu.open .nav-group.open .nav-dropdown { display: block; }
  .nav-menu.open .nav-dropdown-item {
    padding: 0.6rem 0.8rem;
    font-size: 0.95rem;
  }

  .header-nav .nav-divider { display: none; }
  /* In the open burger menu the nav is a column, so the identity box has to
     stop being a row - otherwise the name and Sign out sit side by side in a
     list of full-width stacked items. */
  .nav-menu.open > .header-nav .nav-identity {
    display: flex; flex-direction: column; align-items: stretch; width: 100%; gap: 0;
    border-left: none; padding-left: 0; margin-left: 0;
  }
  .nav-menu.open > .header-nav .nav-user {
    padding: 0.7rem 0.8rem 0.3rem;
    border-top: 1px solid var(--border);
    margin-top: 0.2rem;
  }
  .nav-menu.open > .header-nav form.inline { display: block; padding: 0 0.3rem 0.3rem; }
  .nav-menu.open > .header-nav .btn-ghost { width: 100%; justify-content: flex-start; padding: 0.7rem 0.5rem; }
}

/* Layout breakpoint: stack grids, shrink chrome, enlarge touch targets. */
@media (max-width: 640px) {
  .main-content { padding: 1rem; }
  .cards-grid { grid-template-columns: 1fr; }
  .header-inner { padding: 0 1rem; }
  .logo span { font-size: 1rem; }
  .page-header { flex-direction: column; align-items: flex-start; }
  .page-header h1 { font-size: 1.35rem; }

  /* Stat cards: two per row, filling the width. */
  .stats-row { gap: 0.6rem; }
  .stats-row .stat-card { flex: 1 1 calc(50% - 0.3rem); min-width: 0; padding: 0.85rem 1rem; }
  .stat-value { font-size: 1.6rem; }

  /* Tables stay readable via horizontal scroll with tighter cells. */
  .table-wrap { -webkit-overflow-scrolling: touch; }
  .data-table th, .data-table td { padding: 0.6rem 0.7rem; }
  .filter-bar { flex-wrap: wrap; }
  .filter-input { min-width: 0; flex: 1 1 120px; }
  .filter-select { min-width: 0; flex: 0 1 120px; }

  /* Primary actions get comfortable tap targets and go full-width in forms. */
  .btn { min-height: 44px; }
  .btn-sm { min-height: 0; }
  .card-body > form > .btn,
  .card-body > form > .btn-row > .btn,
  .auth-card .btn-primary,
  .btn-full { width: 100%; }
  .btn-row { flex-direction: column; }
  .btn-row .btn { width: 100%; }

  /* Description list collapses to a single column. */
  .kv { grid-template-columns: 1fr; gap: 0.15rem 0; }
  .kv dt { margin-top: 0.5rem; }
  .kv dd { margin-bottom: 0.2rem; }

  /* Emby sync rows wrap so the email input gets a full row. */
  /* Narrow: the label goes above its control rather than eating half the row. */
  .sync-act { flex-wrap: wrap; }
  .sync-act-label { flex: 1 1 100%; }
  .sync-act-field { flex: 1 1 100%; }

  /* Keep the kill-stream popup on-screen. */
  .kill-popup { right: 0; max-width: 78vw; }
  .kill-popup textarea { width: 100% !important; max-width: 100%; }

  /* Auth pages: less vertical padding on small screens. */
  .auth-wrap { padding: 1.5rem 0.75rem; }
  .auth-card { padding: 1.5rem; }
}

/* ---- Filter bar (admin users list) ---- */
.filter-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1.25rem;
  border-bottom: 1px solid var(--border);
}
.filter-input,
.filter-select {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.3rem 0.65rem;
  font-size: 0.875rem;
  height: 2rem;
  outline: none;
}
.filter-input { width: 200px; }
.filter-select { width: 140px; }
.filter-input:focus, .filter-select:focus { border-color: var(--accent); }
.filter-input::placeholder { color: var(--text-sec); opacity: 0.7; }

/* ---- Pagination ---- */
.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 0.75rem 1.25rem;
  border-top: 1px solid var(--border);
}
/* ---- Staff machine metrics, in the header ---- */
.header-metrics { display: flex; align-items: center; gap: 0.55rem; }
.header-metrics .metric {
  display: inline-flex; align-items: baseline; gap: 0.25rem;
  font-size: 0.78rem; line-height: 1;
  color: var(--text-sec); cursor: default;
}
.header-metrics .metric-label { opacity: 0.65; font-size: 0.68rem; letter-spacing: 0.03em; }
/* Tabular figures so the header does not twitch sideways every time a value
   crosses from 9% to 10%. */
.header-metrics .metric-val { font-variant-numeric: tabular-nums; min-width: 2.4em; text-align: right; }
.header-metrics .metric-warn .metric-val { color: var(--warning, #f59e0b); }
.header-metrics .metric-crit .metric-val { color: var(--danger, #ef4444); font-weight: 600; }
/* `hidden` has to actually hide, and neither rule above lets it: an author
   `display` beats the UA sheet's [hidden]{display:none}. So the strip painted
   "CPU -- RAM -- DISK --" from first paint, and KEPT it for the life of the page
   for a staff member whose /admin/metrics.json is refusing - the poller stops
   dead on a 401/403, and three frozen dashes then sit there being read as a
   measurement.

   The per-gauge selector is not belt-and-braces either: there is no CPU figure
   until the sampler has two samples to difference, and no disk figure on a host
   statvfs cannot answer for, so the paint step hides individual gauges too. */
.header-metrics[hidden] { display: none; }
.header-metrics .metric[hidden] { display: none; }
/* The strip has a row to itself, so the metrics can breathe rather than being
   packed tight against each other the way they were when they shared a line. */
@media (max-width: 1100px) { .header-metrics { gap: 0.85rem; } }

/* Phone: spread the strip across its row and drop the type a notch, so three
   metrics and the clock sit on one line at 320px instead of scrolling. */
@media (max-width: 480px) {
  .header-status { justify-content: space-between; gap: 0.5rem; }
  .header-metrics { gap: 0.6rem; }
  .header-metrics .metric { font-size: 0.72rem; }
  .header-metrics .metric-val { min-width: 2.2em; }
  .header-clock-time { font-size: 0.82rem; }
  .header-clock-tz { font-size: 0.6rem; }
}

.pagination-info { color: var(--text-sec); font-size: 0.875rem; }
.pagination-disabled { opacity: 0.35; cursor: default; pointer-events: none; }

/* The pager now carries a numbered strip, an optional jump box and an optional
   size row, so it has to wrap rather than overflow the card on a narrow screen. */
.pagination { flex-wrap: wrap; }
.pagination-pages,
.pagination-sizes,
.pagination-jump { display: flex; align-items: center; gap: 0.3rem; }
/* Digits must not jitter as you move through the strip - proportional figures
   make "9" and "10" different widths, so every button shuffles sideways. */
.pagination-pages .btn { font-variant-numeric: tabular-nums; min-width: 2.1rem; justify-content: center; }
.pagination-gap { color: var(--text-sec); padding: 0 0.15rem; user-select: none; }
.pagination-jump label { color: var(--text-sec); font-size: 0.875rem; }
/* Wide enough for four digits; a number input that clips its own value is worse
   than a plain text one. */
.pagination-jump input { width: 5rem; }
@media (max-width: 640px) {
  /* The jump box is the first thing to cost more than it gives on a phone -
     prev, next, the numbers and the row count still answer "where am I, and is
     there more". */
  .pagination { gap: 0.5rem; }
  .pagination-jump { display: none; }
}

/* Prevent iOS Safari from zooming when focusing inputs (needs >=16px). */
@media (max-width: 860px) {
  .field input,
  .field select,
  .field textarea,
  .input-sm,
  .request-update select,
  .request-update input[type="text"] {
    font-size: 16px;
  }
}


/* =============================================
   Interaction polish
   Appended last so these win over the component rules above without !important.
   ============================================= */

/* Keyboard focus. Previously only text inputs showed any focus affordance, so
   tabbing through the admin pages was close to invisible. :focus-visible keeps
   mouse clicks clean while making keyboard navigation obvious. */
:where(a, button, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Short, cheap transitions on the properties that actually change on hover.
   Deliberately not `all` - that animates layout properties too and causes
   visible reflow jank on the bigger admin tables. */
.btn,
.nav-link,
.field input,
.field select,
.field textarea,
.data-table tbody tr {
  transition: background-color 140ms ease, border-color 140ms ease,
              color 140ms ease, box-shadow 140ms ease;
}

/* Honour the OS "reduce motion" setting: keep the state changes, drop the
   animation. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

/* =============================================
   Live TV channel browser (/portal/tv)
   A lineup is thousands of channels, so the browser is a category rail plus a
   dense card grid rather than a table: the logo and "what's on now" are what
   people actually scan for, and neither reads well in a table cell.
   ============================================= */

/* Horizontally scrolling category rail. Wrapping thousands of categories onto
   the page would push the channels below the fold on every load. */
.channel-rail {
  display: flex;
  gap: 0.4rem;
  overflow-x: auto;
  padding-bottom: 0.4rem;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}
.channel-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-elevated);
  color: var(--text-sec);
  font-size: 0.8125rem;
  text-decoration: none;
  white-space: nowrap;
}
.channel-chip:hover { color: var(--text); border-color: var(--accent); }
.channel-chip.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.channel-chip-count { opacity: 0.65; font-size: 0.75rem; }

.channel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 0.75rem;
}
.channel-card {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
  min-width: 0;
}
.channel-card-head { display: flex; align-items: center; gap: 0.55rem; min-width: 0; }
.channel-logo {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  object-fit: contain;
  border-radius: 6px;
  /* Provider logos are overwhelmingly white-on-transparent, which is invisible
     on a dark card - so they get their own light plate rather than the card's. */
  background: rgba(255, 255, 255, 0.06);
  padding: 2px;
}
.channel-logo-blank { background: rgba(136, 136, 187, 0.12); }
.channel-card-title { min-width: 0; }
.channel-name {
  font-weight: 600;
  font-size: 0.875rem;
  /* Channel names run long ("UK | SKY SPORTS MAIN EVENT FHD"); one line each
     keeps every card the same height so the grid stays scannable. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.channel-number {
  display: inline-block;
  margin-right: 0.35rem;
  color: var(--text-sec);
  font-variant-numeric: tabular-nums;
}
.channel-group { font-size: 0.75rem; color: var(--text-sec); }
.channel-now { font-size: 0.8125rem; min-width: 0; }
.channel-now-empty { color: var(--text-sec); font-style: italic; }
.channel-prog-title {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.channel-prog-sub,
.channel-prog-time {
  color: var(--text-sec);
  font-size: 0.75rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.channel-progress {
  height: 3px;
  margin-top: 0.35rem;
  border-radius: 999px;
  background: rgba(136, 136, 187, 0.2);
  overflow: hidden;
}
.channel-progress > span { display: block; height: 100%; background: var(--accent); }
.channel-next {
  font-size: 0.75rem;
  color: var(--text-sec);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}


/* Subheading inside a settings group. Used where a section holds several
   distinct blocks (the Live TV panel: providers, add, defaults) that would
   otherwise need nested cards - a card inside a card reads as a boxed-in mess. */
.settings-subhead {
  margin: 1.4rem 0 0.6rem;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text);
}
.settings-group-body > .settings-subhead:first-child { margin-top: 0; }

/* ---- Live TV filter bar ----
   A GET form so every filtered view is a shareable link and the page works with
   scripting off. Replaced a horizontally-scrolling category rail, which hid most
   of a real lineup's dozens of categories behind a drag. */
.tv-filters { padding: 0.85rem 1rem; }
.tv-filter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.7rem;
}
.tv-filter { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
.tv-filter label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-sec);
}
/* `input` unqualified, not a list of types. Scoping this to input[type="search"]
   meant the library's Year box - the one control in either bar that is not a
   search field - matched NOTHING here and fell all the way to the UA stylesheet:
   browser font, square corners, white ground inside a dark card, a native
   spinner, and none of the 2.1rem height, so it sat visibly shorter than the
   selects beside it. The markup was always right, which is why no render test
   could have caught it. Any control type added to a filter bar in future is now
   covered by construction; :not([type="hidden"]) so a hidden input can never
   acquire a border. */
.tv-filter input:not([type="hidden"]),
.tv-filter select {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.35rem 0.6rem;
  font-size: 0.875rem;
  height: 2.1rem;
  outline: none;
  max-width: 100%;
}
.tv-filter input[type="search"] { min-width: 13rem; }
/* Four digits or blank. At the text-input default it would be ~20 characters
   wide and dominate a row it contributes least to. */
.tv-filter input[type="text"] { width: 6.5rem; }
/* Category names run long ("LATINO LOCALS | MEXICO HD"), so the select needs a
   ceiling or it pushes every other control off the row. */
.tv-filter select { max-width: 16rem; }
/* The same focus ring the rest of the app uses (.field, ~line 935), rather
   than a lone border colour - colour alone is not a signal everyone receives. */
.tv-filter input:focus, .tv-filter select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring);
}
.tv-filter-actions { flex-direction: row; align-items: center; gap: 0.4rem; }

.tv-filter-status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
}
.tv-result-count { font-size: 0.875rem; }
/* Active filters as removable chips: on a 6,000-channel lineup it is very easy
   to forget a category is still applied and conclude the search is broken. */
.tv-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: rgba(255, 122, 0, 0.12);
  color: var(--text);
  font-size: 0.78rem;
  text-decoration: none;
}
.tv-chip:hover { background: rgba(255, 122, 0, 0.22); }

/* ---- The two shell switches ----
   "Admin" on the member nav and "User interface" on the console nav are the same
   control pointing opposite ways: they change WHICH INTERFACE you are in, not
   which page of it. Bordered rather than plain, so the eye does not read them as
   one more tab in a row of tabs - which is exactly what happened when they were
   styled like their neighbours, and made the switch invisible on the nav it
   mattered most on. */
.nav-link-admin,
.nav-link-portal {
  border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
  border-radius: 999px;
  padding-inline: 0.7rem;
}
.nav-link-admin:hover,
.nav-link-portal:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* ---- How to show the lineup ----
   Its own row of real targets, not three small words at the end of the filter
   bar. The TV guide is what most people open this page for and it used to be
   the least visible control on it: a link the size of "Clear", among the chips.
   Each one carries a line saying what it gives you, because Grid/List/Guide are
   not self-explaining until you have tried all three. */
.tv-modes {
  display: grid;
  /* auto-fit rather than three fixed columns, so this becomes one column on a
     phone without a second set of rules to say so. */
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.6rem;
  margin-top: 0.75rem;
}
.tv-mode {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elevated);
  color: var(--text);
  text-decoration: none;
}
.tv-mode:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.tv-mode svg { width: 22px; height: 22px; flex: 0 0 auto; color: var(--text-sec); }
.tv-mode-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.tv-mode-name { font-weight: 600; font-size: 0.95rem; }
.tv-mode-hint { color: var(--text-sec); font-size: 0.78rem; }
.tv-mode.is-active {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, var(--bg-elevated));
}
.tv-mode.is-active svg { color: var(--accent); }

/* ---- The guide's time controls ----
   flex-flow rather than flex-wrap alone: .card sets `flex-direction: column` for
   its header and body, and this card IS one row. Setting only `display:flex`
   inherited the column and stacked the three groups down the middle. */
.tv-guidebar {
  display: flex;
  flex-flow: row wrap;
  align-items: center;
  gap: 0.6rem 0.9rem;
  padding: 0.7rem 1rem;
}
.tv-guidebar-when { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.tv-guidebar-window { font-weight: 600; font-variant-numeric: tabular-nums; }
.tv-guidebar-move { display: flex; gap: 0.35rem; }
/* Pushed to the far end, so "how far through time" and "how much of it" do not
   read as one row of six equivalent buttons. */
.tv-guidebar-spans { margin-left: auto; display: flex; gap: 0.25rem; }
.tv-span {
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-sec);
  font-size: 0.8125rem;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
.tv-span:hover { color: var(--text); }
.tv-span.is-active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* A range typed rather than stepped to. Its own line: it is a form, and putting
   a form's fields in the same row as the buttons that move the window made both
   read as one undifferentiated strip of controls. */
.tv-guidebar-range {
  flex: 1 1 100%;
  display: flex;
  flex-flow: row wrap;
  align-items: flex-end;
  gap: 0.5rem 0.7rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
}
.tv-range-field { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.tv-range-field label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-sec);
}
/* Dressed explicitly, like .tv-filter's controls: a datetime input inherits
   none of the app's form styling from a bare selector, and a native one sits
   visibly shorter and squarer than the buttons beside it. */
.tv-guidebar-range input[type="datetime-local"] {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.3rem 0.5rem;
  font-size: 0.85rem;
  height: 2.1rem;
  outline: none;
  max-width: 100%;
}
.tv-guidebar-range input[type="datetime-local"]:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring);
}
/* The browser's own calendar button is drawn dark, which is invisible against a
   dark field. Inverted here and put back on the two light themes, rather than
   inverted only in dark - a viewer on the default (no data-theme attribute) is
   on a dark palette, so dark is the case to style for. */
.tv-guidebar-range input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  filter: invert(0.7);
  cursor: pointer;
}
[data-theme="day"] .tv-guidebar-range input[type="datetime-local"]::-webkit-calendar-picker-indicator,
[data-theme="sunny"] .tv-guidebar-range input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  filter: none;
}
.tv-range-help { flex: 1 1 100%; margin: 0; }

/* ---- Live TV list view ---- */
.tv-list-num { font-variant-numeric: tabular-nums; color: var(--text-sec); }
.tv-list-channel { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.tv-list-logo {
  width: 26px; height: 26px; object-fit: contain; border-radius: 4px;
  background: rgba(255, 255, 255, 0.06); padding: 1px; flex: 0 0 auto;
}

/* ---- Live TV guide: the timeline ----
   Channels down the side, time across the top, and the whole thing scrolls
   sideways. This replaced a stack of cards per channel, which could show three
   programmes at a width that could have shown six, never lined up between one
   row and the next, and had no way to say "later this evening" at all.

   The geometry comes from the server: routes::iptv divides the window into
   five-minute columns and gives every programme its start and end line, so the
   ruler and the rows are placed on the SAME gridlines. Two implementations of
   "where does 21:30 sit" is how a timeline drifts from its own labels. */
.tv-epg {
  /* The floor for one five-minute column. Below about this the blocks stop
     holding a readable title, so the grid stops shrinking and starts scrolling
     instead - which is what the sideways scrollbar is for. */
  --epg-cell: 14px;
  --epg-ch: 15rem;
}
.tv-epg-scroll {
  overflow: auto;
  /* Bounded, so the ruler and the channel column have something to stick to and
     a 60-row guide does not push the pager a screen and a half down the page. */
  max-height: min(74vh, 900px);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
  /* A visible track rather than an overlay bar: sideways scrolling that nothing
     advertises is sideways scrolling nobody finds. */
  scrollbar-color: var(--accent) transparent;
}
.tv-epg-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tv-epg-scroll::-webkit-scrollbar { height: 12px; width: 12px; }
.tv-epg-scroll::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--accent) 60%, transparent);
  border-radius: 999px;
}
.tv-epg-scroll::-webkit-scrollbar-track { background: rgba(136, 136, 187, 0.08); }
/* min-content, so the inner block is as wide as the widest row rather than the
   viewport - without it the rows scroll but the ruler above them does not. */
.tv-epg-inner { min-width: min-content; }

/* Both the ruler and every row are the same two-part shape: a fixed channel
   column that sticks to the left edge, then the timeline. */
.tv-epg-head, .tv-epg-row { display: flex; align-items: stretch; }
.tv-epg-head {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--border);
}
.tv-epg-corner, .tv-epg-ch {
  position: sticky;
  left: 0;
  z-index: 2;
  flex: 0 0 var(--epg-ch);
  width: var(--epg-ch);
  background: var(--bg-elevated);
  border-right: 1px solid var(--border);
}
.tv-epg-corner {
  z-index: 4;
  display: flex;
  align-items: center;
  padding: 0.4rem 0.6rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-sec);
}
/* The ruler and the track share one gridline definition, set on .tv-epg by the
   template - which is what makes a label and the programme under it agree. */
.tv-epg-times, .tv-epg-track {
  display: grid;
  grid-template-columns: var(--epg-cols);
  flex: 1 1 auto;
  min-width: calc(var(--epg-cells) * var(--epg-cell));
}
.tv-epg-tick {
  padding: 0.4rem 0 0.35rem 0.4rem;
  font-size: 0.75rem;
  color: var(--text-sec);
  font-variant-numeric: tabular-nums;
  border-left: 1px solid var(--border);
  white-space: nowrap;
  overflow: hidden;
}
/* The hour gets the heavier line, so the eye has something coarser than a wall
   of half-hour ticks to navigate by. */
.tv-epg-tick.is-hour { color: var(--text); border-left-color: color-mix(in srgb, var(--text-sec) 45%, var(--border)); }

.tv-epg-row { border-top: 1px solid var(--border); }
.tv-epg-ch {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  min-width: 0;
  overflow: hidden;
  color: inherit;
  text-decoration: none;
}
.tv-epg-ch:hover .channel-name { color: var(--accent); }
/* min-width:0 on the flex child too, or the ellipsis never engages: a grid or
   flex item refuses to shrink below its content's intrinsic width, and channel
   names on a real IPTV line are sentences - "AU (STAN 01) | Wests Scarborough v
   Kalamunda WA Premier Grade Round 15 2026 (2026-08-01 17:25:24)". */
.tv-epg-ch-text { min-width: 0; overflow: hidden; }
.tv-epg-ch .channel-name {
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.2;
  font-size: 0.85rem;
}
.tv-epg-ch .channel-logo { width: 28px; height: 28px; flex: 0 0 auto; }

.tv-epg-track { position: relative; padding: 3px 0; gap: 3px; }
/* The live marker, drawn per row rather than as one overlay: an absolutely
   positioned line over a scrolling grid has to be told the grid's width, and
   only the browser knows that. A percentage of each track is the same line. */
.tv-epg-live .tv-epg-track::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--epg-now);
  width: 2px;
  background: var(--danger);
  pointer-events: none;
  z-index: 1;
}
.tv-epg-block {
  position: relative;
  min-width: 0;
  overflow: hidden;
  padding: 0.3rem 0.45rem;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: color-mix(in srgb, var(--text-sec) 8%, transparent);
  font-size: 0.78rem;
  /* Each block sizes its own contents. A quarter-hour programme in a twelve-hour
     window is about forty pixels wide, and on a news channel that runs them
     back to back the row was a wall of "Ay u… 12:15-" with a record dot on top
     of it - three things competing for space that fits one. */
  container-type: inline-size;
}
/* Below this the times cost more than they tell: the block's position in the
   row already says roughly when it is, and the title is what identifies it. */
@container (max-width: 5.5rem) {
  .tv-epg-block-time { display: none; }
  .tv-epg-block-title { -webkit-line-clamp: 3; }
}
/* And below THIS there is no room for a control at all. The block still tunes
   to the channel; recording is a wider window away (2h), which is the same
   press that makes it readable. */
@container (max-width: 3.6rem) {
  .tv-epg-rec { display: none; }
  .tv-epg-block:has(.tv-epg-rec) .tv-epg-block-title { padding-right: 0; }
}
.tv-epg-block:hover { border-color: var(--accent); }
.tv-epg-block.is-now {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.tv-epg-hit { position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.tv-epg-block-title {
  font-weight: 600;
  line-height: 1.2;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
/* Room for the record button, but only where there is one - reserving it for
   every block would cost width on an account with no DVR. */
.tv-epg-block:has(.tv-epg-rec) .tv-epg-block-title { padding-right: 1.1rem; }
.tv-epg-block-time { color: var(--text-sec); font-size: 0.7rem; font-variant-numeric: tabular-nums; }
/* Already underway when the window opened. */
.tv-epg-cont { color: var(--text-sec); margin-right: 0.15rem; }
.tv-epg-block .channel-progress { margin-top: 0.2rem; }
/* Icon-sized, and over the block's own hit area. A button with a word on it
   would set the minimum width of every programme in the guide. */
.tv-epg-rec { position: absolute; top: 2px; right: 2px; z-index: 2; margin: 0; }
.tv-epg-rec button {
  border: 0;
  border-radius: 999px;
  width: 18px;
  height: 18px;
  line-height: 1;
  font-size: 0.6rem;
  padding: 0;
  cursor: pointer;
  background: color-mix(in srgb, var(--danger) 22%, var(--bg-elevated));
  color: var(--danger);
}
.tv-epg-rec button:hover { background: var(--danger); color: #fff; }
.tv-epg-empty {
  padding: 0.5rem 0.6rem;
  color: var(--text-sec);
  font-size: 0.8rem;
}

@media (max-width: 900px) {
  /* Narrower channel column and a wider cell: on a phone the guide is scrolled
     rather than scanned, so legibility beats how much fits. */
  .tv-epg { --epg-ch: 8.5rem; --epg-cell: 17px; }
  .tv-epg-scroll { max-height: 70vh; }
}
@media (max-width: 700px) {
  .tv-filter input[type="search"] { min-width: 0; width: 100%; }
  .tv-filter { flex: 1 1 100%; }
  .tv-guidebar-spans { margin-left: 0; }
}

/* ---- Backup section picker ---- */
.backup-sections { display: flex; flex-direction: column; gap: 0.4rem; }
.backup-section {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.9rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
  cursor: pointer;
}
.backup-section-fixed { cursor: default; opacity: 0.85; }
.backup-section-main { display: flex; align-items: flex-start; gap: 0.6rem; min-width: 0; }
.backup-section-main input { margin-top: 0.2rem; flex: 0 0 auto; }
/* Tabular so the sizes line up in a column and the big one is obvious at a
   glance - which is the entire point of showing them. */
.backup-size {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-size: 0.8125rem;
  color: var(--text-sec);
  white-space: nowrap;
}

/* ---- Live TV curation (featured + tags) ---- */
.channel-featured { color: var(--accent); margin-right: 0.3rem; }
.channel-tags { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.3rem; }
.channel-tags-inline { display: inline-flex; flex-wrap: wrap; gap: 0.25rem; margin-left: 0.4rem; }
.channel-tag {
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: rgba(139, 92, 246, 0.16);
  color: #c4b5fd;
  font-size: 0.7rem;
  white-space: nowrap;
}
/* Staff-only curation, collapsed to a cog in the card corner. It sits on a
   member-facing page listing thousands of channels, so it must cost NO vertical
   space until used - a control row per card doubled the page height for something
   touched on a handful of them. */
.channel-card { position: relative; }
.channel-curate > summary {
  position: absolute;
  top: 0.35rem;
  right: 0.35rem;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 5px;
  color: var(--text-sec);
  cursor: pointer;
  list-style: none;
  opacity: 0.5;
}
.channel-curate > summary::-webkit-details-marker { display: none; }
.channel-curate > summary:hover,
.channel-curate[open] > summary { opacity: 1; background: rgba(136, 136, 187, 0.16); color: var(--text); }
/* A panel over the card rather than inside it, so opening one does not reflow
   the grid and shove every other card down the page. */
.channel-curate-body {
  position: absolute;
  top: 2rem;
  right: 0.35rem;
  left: 0.35rem;
  z-index: 3;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: var(--shadow-md);
}

/* Featured strip. Bordered in the accent so it reads as a distinct section
   rather than the first rows of the lineup below it. */
.tv-featured { border-color: rgba(255, 122, 0, 0.35); }
.tv-featured .card-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.channel-card-featured { border-color: rgba(255, 122, 0, 0.35); }

/* ---- Live TV player ---- */
.watch-body { padding: 0; }
.watch-shell { position: relative; background: #000; }
.watch-video { display: block; width: 100%; max-height: 75vh; background: #000; }
/* Over the video, not under it: a message below the fold of a 16:9 player is a
   message nobody reads. */
.watch-status {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  max-width: 32rem;
  padding: 0.7rem 1rem;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.78);
  color: #fff;
  font-size: 0.9rem;
  text-align: center;
  pointer-events: none;
}
.watch-status-error { background: rgba(185, 28, 28, 0.9); }
.watch-meta { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.9rem 1.25rem; }
.watch-now { display: flex; flex-direction: column; gap: 0.15rem; align-items: flex-start; }
/* Record buttons sit beside what they record, not in a toolbar. `align-self:
   flex-start` keeps the button its own width inside the column flex parent,
   which otherwise stretches it across the card. */
.record-form { margin-top: 0.35rem; align-self: flex-start; }
.watch-next { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.watch-next .record-form { margin-top: 0; }
.watch-later > summary { cursor: pointer; color: var(--text-sec); font-size: 0.9rem; }
.watch-later-list { list-style: none; margin: 0.5rem 0 0; padding: 0; display: flex;
  flex-direction: column; gap: 0.35rem; }
.watch-later-list li { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.watch-later-list .record-form { margin-top: 0; margin-left: auto; }

/* Whole-card click target. An overlay rather than wrapping the card in an <a>,
   because the curation cog is inside it and must stay independently clickable -
   nesting interactive elements inside a link is invalid and behaves erratically. */
.channel-card-hit { position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.channel-card-link { display: block; color: inherit; text-decoration: none; }
.channel-card:hover, .channel-card-link:hover { border-color: var(--accent); }
/* The raw player event under the friendly message. Small and monospace: it is for
   copying into a bug report, not for reading. */
.watch-detail {
  position: absolute;
  left: 50%;
  bottom: 0.75rem;
  transform: translateX(-50%);
  max-width: 90%;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.7);
  color: rgba(255, 255, 255, 0.7);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.7rem;
  text-align: center;
  pointer-events: none;
}

/* ---- Scheduled-job schedule editor ----
   A <details> inside the actions cell of the jobs table, so the row stays a row
   until someone actually wants to change a schedule. The popup is constrained in
   width because the cell it hangs off is the narrowest column on the page. */
.job-schedule { margin-top: 0.4rem; }
.job-schedule > summary { display: inline-flex; list-style: none; cursor: pointer; }
.job-schedule > summary::-webkit-details-marker { display: none; }
.job-schedule-form {
  margin-top: 0.5rem;
  padding: 0.7rem;
  min-width: 17rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}
/* Monospace, because this is an expression whose field positions carry meaning -
   proportional digits make it genuinely harder to count the fields. */
.job-cron {
  width: 100%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.job-schedule-toggle {
  display: flex; align-items: center; gap: 0.45rem;
  margin-top: 0.6rem; font-size: 0.9rem; cursor: pointer;
}
.job-schedule-actions { display: flex; gap: 0.4rem; margin-top: 0.7rem; }

/* ── Library ────────────────────────────────────────────────────────────────
   The member's own media library, browsed in the portal. The previous version
   of this page used four class names - library-grid, library-poster,
   library-item, library-view - that were defined NOWHERE in this file, so it
   rendered as an unstyled column of full-size posters. Everything below is new.

   Posters are 2:3, which is what every film poster is and what the media server
   returns. Fixing the aspect ratio on the WRAPPER rather than the image is what
   keeps a grid of mixed-artwork items on a straight baseline: a server with one
   4:3 poster in it would otherwise push its whole row down. */

.lib-views { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.9rem 0 0.2rem; }
/* Centred only where this row is the PAGE'S OWN switcher. `.lib-views` and
   `.lib-view-chip` are shared with the season strip on the item page, and
   centring the base rule would centre "Season 1 … Season 20" into a ragged
   block that reads as a mistake rather than as a heading. */
.lib-views-main { justify-content: center; margin: 1.1rem 0 0.4rem; }
.lib-view-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-sec);
  font-size: 0.85rem;
  text-decoration: none;
  white-space: nowrap;
  /* Colour AND border move together on hover/active, so the transition names
     both rather than `all` - which would also animate the layout properties
     above and make a wrapping row jitter as it reflows. */
  transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
/* currentColor, so one rule carries the icon through every chip state. */
.lib-view-icon { width: 15px; height: 15px; flex: none; opacity: 0.85; }
.lib-view-chip:hover { color: var(--text); border-color: var(--accent); text-decoration: none; }
.lib-view-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.lib-view-chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.lib-view-chip.active .lib-view-icon { opacity: 1; }

.lib-filters { padding: 0.9rem 1.1rem; margin-top: 0.6rem; }
.lib-filter-row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; }

.lib-result-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem;
  margin: 1rem 0 0.6rem; color: var(--text-sec); font-size: 0.9rem;
}

/* auto-fill, not auto-fit: auto-fit collapses empty tracks, so a search that
   returns one result stretches that single poster across the whole row. */
.lib-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 1rem 0.9rem;
}

/* A rail scrolls sideways and the PAGE never does. scroll-snap so a flick lands
   on a card rather than between two. */
.lib-rail { margin-top: 1.6rem; }
.lib-rail-head { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.6rem; }
.lib-rail-head h2 { margin: 0; font-size: 1.05rem; }
.lib-rail-head .btn { margin-left: auto; }
.lib-rail-track {
  display: flex; gap: 0.9rem;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  padding-bottom: 0.5rem;
  scrollbar-width: thin;
}
.lib-rail-track > * { flex: 0 0 150px; scroll-snap-align: start; }

.lib-card { display: block; color: inherit; text-decoration: none; min-width: 0; }
.lib-card:hover { text-decoration: none; }
.lib-card:hover .lib-poster-wrap { border-color: var(--accent); transform: translateY(-2px); }

.lib-poster-wrap {
  position: relative;
  aspect-ratio: 2 / 3;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-alt);
  transition: border-color 0.12s ease, transform 0.12s ease;
}
.lib-poster { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A tile with an initial, not a broken <img>. An image element with no src is a
   grey box with an error glyph in it, which reads as a fault rather than as an
   item whose artwork has not been fetched. */
.lib-poster-blank {
  display: flex; align-items: center; justify-content: center;
  background: rgba(136, 136, 187, 0.14);
  color: var(--text-sec); font-size: 1.8rem; font-weight: 600;
}

.lib-tick, .lib-count {
  position: absolute; top: 0.35rem; right: 0.35rem;
  min-width: 1.35rem; height: 1.35rem; padding: 0 0.3rem;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px; font-size: 0.72rem; font-weight: 700;
  background: var(--accent); color: #fff;
}
.lib-tick { background: var(--success, #16a34a); }

/* The bar sits ON the poster's bottom edge, which is where every media app puts
   it - below the poster it competes with the title for the same glance. */
.lib-progress {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 4px; background: rgba(0, 0, 0, 0.55);
}
.lib-progress > span { display: block; height: 100%; background: var(--accent); }
.lib-progress-wide { position: static; margin: 0.6rem 0; border-radius: 999px; max-width: 22rem; }

.lib-card-meta { display: flex; flex-direction: column; gap: 0.1rem; margin-top: 0.45rem; }
/* Two lines, then ellipsis. Titles run long and a card that grows to fit one
   drags its whole grid row with it. */
.lib-card-title {
  font-size: 0.88rem; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.lib-card-sub, .lib-card-facts {
  font-size: 0.76rem; color: var(--text-sec);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lib-dot { margin: 0 0.3rem; opacity: 0.5; }

/* ── Detail page ─────────────────────────────────────────────────────────── */
.lib-detail { margin-top: 0.8rem; border-radius: var(--radius); overflow: hidden; }
/* The backdrop is decoration, so it is a background rather than an <img>: it must
   not be announced to a screen reader, and it has to crop rather than letterbox
   at every width. The gradient is what keeps the text readable over an image
   nobody chose for its contrast. */
.lib-detail-hero {
  background-image:
    linear-gradient(to right, var(--bg-elevated) 0%, rgba(0, 0, 0, 0.72) 55%, rgba(0, 0, 0, 0.55) 100%),
    var(--hero);
  background-size: cover;
  background-position: center 20%;
}
.lib-detail-hero .lib-detail-body { color: #f4f6fb; }
.lib-detail-hero .lib-detail-body .text-secondary,
.lib-detail-hero .lib-detail-body .field-help { color: rgba(244, 246, 251, 0.72); }
.lib-detail-inner {
  display: flex; gap: 1.5rem; padding: 1.5rem;
  background: var(--bg-elevated);
}
.lib-detail-hero .lib-detail-inner { background: transparent; }
.lib-detail-poster {
  flex: 0 0 200px; width: 200px; aspect-ratio: 2 / 3;
  object-fit: cover; border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}
.lib-detail-body { min-width: 0; flex: 1; }
.lib-detail-body h1 { margin: 0 0 0.3rem; font-size: 1.7rem; }
.lib-tagline { margin: 0 0 0.6rem; font-style: italic; opacity: 0.85; }
.lib-detail-facts { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; margin: 0.5rem 0; }
.lib-genres { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.6rem 0; }
.lib-overview { margin-top: 0.9rem; max-width: 62ch; line-height: 1.55; }

.lib-cast > * { flex: 0 0 110px; }
.lib-person { display: flex; flex-direction: column; gap: 0.15rem; font-size: 0.78rem; text-align: center; }
.lib-person-img {
  width: 110px; height: 110px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--border);
}
.lib-person .text-secondary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lib-tech > summary { padding: 0.9rem 1.25rem; cursor: pointer; font-weight: 600; }

@media (max-width: 640px) {
  .lib-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.8rem 0.6rem; }
  .lib-rail-track > * { flex-basis: 120px; }
  /* The hero stacks: a 200px poster beside text is most of a phone's width. */
  .lib-detail-inner { flex-direction: column; gap: 1rem; padding: 1rem; }
  .lib-detail-poster { flex-basis: auto; width: 140px; }
  .lib-detail-body h1 { font-size: 1.3rem; }
}

/* The add-to-playlist control on an item page. A row, so it reads as one
   sentence with a control in the middle rather than a stacked form. */
.lib-addto { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.lib-addto label { color: var(--text-sec); font-size: 0.85rem; }
/* Dressed EXPLICITLY, matching .tv-filter. A bare <select> inherits none of the
   app's form styling - the rules for that are scoped to .field and .tv-filter -
   so it renders as the operating system's own control, which on this page sits
   in the middle of a dark hero and looks like a browser widget that has landed
   on the page by accident. */
.lib-addto select {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.35rem 0.6rem;
  font-size: 0.875rem;
  height: 2.1rem;
  outline: none;
  min-width: 11rem;
  max-width: 16rem;
}
.lib-addto select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }

/* A name is a few words. At the card's full width the box invites a sentence and
   then refuses one at 120 characters. */
.pl-new .field { max-width: 24rem; }

/* ── One playlist's contents ──────────────────────────────────────────────────
   A LIST, not the poster grid the rest of the library uses. Order is the whole
   content of a playlist, and a grid that reflows from six columns to two at a
   narrower width destroys the one thing this page exists to show. A row also
   leaves somewhere for the reorder controls to live that does not move as the
   list changes.

   An <ol>, stripped of its marker: the number beside each row is styled, and a
   double number is worse than none. */
.pl-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.pl-item {
  display: flex; align-items: center; gap: 0.85rem;
  padding: 0.6rem 0.85rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
}
/* Tabular figures so 9 → 10 does not shift the row beside it. */
.pl-num {
  flex: 0 0 1.6rem; text-align: right;
  color: var(--text-sec); font-size: 0.85rem; font-variant-numeric: tabular-nums;
}
.pl-art { flex: 0 0 44px; display: block; }
.pl-art img { width: 44px; height: 66px; object-fit: cover; border-radius: 4px; display: block; }
/* The no-artwork case, sized to match the poster exactly so a mixed list keeps
   one baseline. */
.pl-art-blank {
  width: 44px; height: 66px; border-radius: 4px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-hover); color: var(--text-sec);
  font-weight: 600; text-transform: uppercase;
}
/* min-width: 0 is what lets the title ellipsis work: a flex child defaults to
   min-width:auto and refuses to shrink below its content, which pushes the
   controls off the row instead of truncating the title. */
.pl-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.pl-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pl-title-dim { color: var(--text-sec); }
.pl-facts { color: var(--text-sec); font-size: 0.8rem; }
.pl-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 0.15rem; }

@media (max-width: 640px) {
  /* The row wraps rather than scrolls: the controls drop under the title and
     keep their tap targets at full size. */
  .pl-item { flex-wrap: wrap; }
  .pl-body { flex-basis: 55%; }
  .pl-actions { margin-left: auto; }
}

/* The quality control, beside the badge that says what the choice produced. */
.lib-play-bar { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.lib-play-bar label { color: var(--text-sec); font-size: 0.85rem; }
.lib-play-bar select { min-width: 11rem; }

/* The quick action on a tile. Overlaid on the poster rather than placed under it,
   so a card with an action is exactly as tall as one without and the grid keeps
   its baseline. Only appears on hover or keyboard focus - a row of × buttons over
   every poster is visual noise on a page whose job is showing artwork - but
   focus-within is what keeps it reachable without a mouse. */
.lib-card-wrap { position: relative; min-width: 0; }
.lib-card-action {
  position: absolute; top: 0.35rem; left: 0.35rem; z-index: 2;
  opacity: 0; transition: opacity 0.12s ease;
}
.lib-card-wrap:hover .lib-card-action,
.lib-card-wrap:focus-within .lib-card-action { opacity: 1; }
.lib-card-action button {
  width: 1.6rem; height: 1.6rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(0, 0, 0, 0.72); color: #fff; font-size: 1rem;
}
.lib-card-action button:hover { background: var(--danger, #ef4444); }
/* Touch has no hover, so the control would be unreachable if it stayed hidden. */
@media (hover: none) { .lib-card-action { opacity: 1; } }

.lib-state-row { flex-wrap: wrap; gap: 0.4rem; }

/* ── Bulk selection on admin lists ───────────────────────────────────────── */
.bulk-col { width: 2.2rem; text-align: center; }
.bulk-col input { width: 1.05rem; height: 1.05rem; cursor: pointer; }
/* Sticky, because the point of a selection bar is that it stays reachable while
   you scroll a long list ticking rows - one that scrolls away means going back
   to the top to act on what you just chose. */
.bulk-bar {
  position: sticky; top: 60px; z-index: 5;
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
  margin: 0.6rem 0; padding: 0.6rem 0.9rem;
  background: var(--surface-alt); border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
}
.bulk-bar[hidden] { display: none; }
.bulk-months { display: inline-flex; align-items: center; gap: 0.4rem; }
.bulk-months[hidden] { display: none; }
.bulk-months input { width: 4.5rem; }

/* ── Collapsible cards ──────────────────────────────────────────────────────
   A <details> styled to look exactly like a .card, so folding a section away is
   a native disclosure rather than a bespoke widget: keyboard-operable, announced
   correctly, and open-by-default with no JavaScript. */
.card-collapsible > summary {
  cursor: pointer;
  /* The default triangle sits outside the padding box and cannot be positioned;
     the chevron below replaces it. list-style covers Firefox, ::-webkit-details-
     marker covers older Safari - both are needed. */
  list-style: none;
  user-select: none;
}
.card-collapsible > summary::-webkit-details-marker { display: none; }
.card-collapsible > summary:hover { background: var(--surface-2, rgba(127, 127, 127, 0.06)); }
/* A visible focus ring: the summary IS the control, and losing the outline would
   leave a keyboard user with no idea what they are about to toggle. */
.card-collapsible > summary:focus-visible {
  outline: 2px solid var(--primary, #4a9eff);
  outline-offset: -2px;
}
/* Rotates to point down when open. A transform on a pseudo-element rather than
   swapping glyphs, so the two states animate into each other. */
.card-collapsible > summary::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  margin-left: 0.75rem;
  flex: none;
  border-right: 2px solid var(--text-secondary, #888);
  border-bottom: 2px solid var(--text-secondary, #888);
  transform: rotate(-45deg) translate(-0.1rem, -0.1rem);
  transition: transform 0.15s ease;
}
.card-collapsible[open] > summary::after {
  transform: rotate(45deg) translate(-0.1rem, -0.1rem);
}
/* Closed, the header IS the whole card, so its bottom border would draw a rule
   under nothing. */
.card-collapsible:not([open]) > summary { border-bottom: none; }

/* ── Live list regions ──────────────────────────────────────────────────────
   Fetching a page of rows takes a moment. Dimming rather than blanking keeps the
   old rows readable and the layout still, so the table does not jump on every
   keystroke; pointer-events off stops a click landing on a row that is about to
   be replaced by a different one. */
[data-live-region].is-loading {
  opacity: 0.55;
  pointer-events: none;
  /* Only after a beat. A fast response should not flash. */
  transition: opacity 0.12s ease 0.15s;
}
@media (prefers-reduced-motion: reduce) {
  .card-collapsible > summary::after { transition: none; }
  [data-live-region].is-loading { transition: none; }
}

/* ── In-page form validation ────────────────────────────────────────────────
   The browser's validation bubble is a transient tooltip anchored to a field
   that may be scrolled off-screen; on a phone it is routinely never seen, and
   the refusal to submit reads as a dead button. These style the replacement
   that lives IN the page. */
.form-errors { margin-bottom: 1rem; }
.auth-form .is-invalid,
.field .is-invalid {
  border-color: var(--danger);
  /* A ring as well as a border colour: colour alone is not a signal everyone
     receives, and the message above says the same thing in words. */
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 28%, transparent);
}

/* ── Loading states ─────────────────────────────────────────────────────────
   For panels whose content arrives after the shell paints - the Live TV lineup
   above all, where nineteen thousand channels take long enough that a static
   line of text is indistinguishable from a page that has stopped.

   Two parts, and both earn their place: an indeterminate bar that says the page
   is still working, and a skeleton in the SHAPE of the content so the layout
   does not jump when it lands.

   The bar is indeterminate on purpose. The fetch is a single request, so there
   is no honest percentage available; a fabricated one that races to 90% and then
   sits there is a worse lie than showing no number at all. */
.loading-bar {
  height: 3px;
  overflow: hidden;
  background: var(--accent-soft);
  border-top-left-radius: var(--radius);
  border-top-right-radius: var(--radius);
}
.loading-bar i {
  display: block;
  height: 100%;
  width: 40%;
  background: var(--accent);
  animation: loading-slide 1.1s ease-in-out infinite;
}
/* -100% to 230%, not 350%. The bar is 40% of the track, so a translate of 350%
   puts its LEFT edge at 140% of the track - fully off the right for roughly a
   fifth of every cycle, which reads as the animation having stopped. 230% lands
   its left edge at 92%, so some part of it is on screen at all times. */
@keyframes loading-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(230%); }
}

.skeleton-grid { margin-top: 0.75rem; }
.skeleton-card { pointer-events: none; }
/* One shared shimmer, so every placeholder in a panel moves as one surface
   rather than as a dozen independently-animating boxes. */
.skeleton-block, .skeleton-line {
  /* Solid theme tokens, never color-mix, and never a variable that does not
     exist. The first draft of this shimmered between --bg-elevated and a
     color-mix over `--text-secondary` - a variable this sheet has never had; it
     is called --text-sec. An undefined var() makes the color-mix invalid, an
     invalid color-mix makes the whole `background` declaration invalid, and the
     browser drops it silently. The result rendered as a grid of empty boxes with
     no error anywhere, which is how it was caught - by looking at it. */
  /* --border as the base, not --surface. In the light themes --surface is
     #ffffff and --surface-alt is #f0f2f5 - a five-percent step that is simply
     not there on a real screen, so the placeholders vanished and the panel
     looked empty rather than loading. --border is the token that draws the card
     outlines, so it is visible against a card by definition, in every theme. */
  background: linear-gradient(
    90deg,
    var(--border) 25%,
    var(--surface-hover) 37%,
    var(--border) 63%
  );
  background-size: 400% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
  border-radius: 4px;
}
.skeleton-block { border-radius: 6px; }
.skeleton-line { height: 0.7rem; margin-top: 0.4rem; }
.skeleton-line-lg { width: 78%; height: 0.8rem; margin-top: 0; }
.skeleton-line-sm { width: 45%; }
@keyframes skeleton-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: 0 0; }
}

/* Motion is the whole mechanism here, so with it suppressed the placeholders
   have to say "loading" some other way - a steady tint that is clearly not
   content, rather than frozen bars that look like a broken render. */
@media (prefers-reduced-motion: reduce) {
  .loading-bar i {
    animation: none;
    width: 100%;
    opacity: 0.55;
  }
  .skeleton-block, .skeleton-line {
    animation: none;
    background: var(--border);
  }
}
