/*
 * Deliberately plain. This is a ledger and a chat, not a storefront — the two
 * things that must be readable at a glance are "when does this expire" and
 * "has this been paid", and everything else gets out of their way.
 *
 * ---------------------------------------------------------------------------
 * Three theme states, not two.
 *
 *   :root                                    the light palette, always defined
 *   @media dark + :root:not([data-theme=light])   system preference wins
 *   :root[data-theme="dark"]                 an explicit choice wins over both
 *
 * Dropping either of the last two breaks one direction of the toggle while the
 * other still appears to work: without the :not() an explicit "light" cannot
 * override a dark system, and without the attribute rule an explicit "dark"
 * does nothing on a light system.
 *
 * The dark values are written once, as --dark-* on :root, and the two rules
 * only map them. A palette kept in two literal copies has already drifted by
 * the time anyone notices.
 *
 * data-theme arrives on <html> from the server, read from a cookie. The CSP
 * has no 'unsafe-inline', so there is no pre-paint script that could set it —
 * and a deferred one would flash the wrong theme first.
 *
 * `color-scheme` is what makes the native controls follow: without it the date
 * picker in the account form, the select arrows and the scrollbars all render
 * in light chrome on a dark page.
 * ---------------------------------------------------------------------------
 */

:root {
  color-scheme: light;

  /* Live tokens — light */
  --bg: #f7f5fb;
  --surface: #ffffff;
  --surface-2: #f0ecf8;
  --border: #e2dcef;
  --text: #1b1526;
  --muted: #6d6479;
  --accent: #7047eb;
  --accent-hover: #5b34cf;
  --accent-soft: #eee8ff;
  --accent-text: #ffffff;
  --danger: #a3352b;
  --danger-soft: #fbeceb;
  --danger-text: #ffffff;
  --warn: #8a6116;
  --warn-bg: #fdf4e3;
  --warn-border: #ecd2a0;
  --focus: #7047eb;
  --shadow-1: 0 1px 2px rgb(16 24 20 / .05);
  --shadow-2: 0 4px 14px rgb(16 24 20 / .09);

  /* The dark palette, defined once. Applied by the two rules below. */
  --dark-bg: #09070f;
  --dark-surface: #151020;
  --dark-surface-2: #20182d;
  --dark-border: #352a47;
  --dark-text: #f2edfa;
  --dark-muted: #aca1bb;
  --dark-accent: #ad8cff;
  --dark-accent-hover: #c2a9ff;
  --dark-accent-soft: #2b1d4a;
  --dark-accent-text: #120c1c;
  --dark-danger: #dd7466;
  --dark-danger-soft: #2c1c1a;
  /*
   * Dark text on the light salmon, not white. #fff on #dd7466 is 3.1:1 — under
   * AA — and it was on every badge in the navigation and every delete button.
   */
  --dark-danger-text: #1b1210;
  --dark-warn: #d7ab5e;
  --dark-warn-bg: #2b2418;
  --dark-warn-border: #67532a;
  --dark-focus: #c2a9ff;
  --dark-shadow-1: 0 1px 2px rgb(0 0 0 / .3);
  --dark-shadow-2: 0 4px 14px rgb(0 0 0 / .45);

  /*
   * Scales. Everything below used to be an ad-hoc value: fifteen distinct font
   * sizes and twenty-one spacing values, none of them named, so "the same size
   * as that other thing" was a guess every time.
   */
  --space-1: .25rem;
  --space-2: .5rem;
  --space-3: .75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.75rem;
  --space-7: 2.5rem;
  --space-8: 4rem;

  --text-xs: .78rem;
  --text-sm: .85rem;
  --text-md: .93rem;
  --text-base: 1rem;
  --text-lg: 1.1rem;
  --text-xl: 1.5rem;
  --text-2xl: 2rem;

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-full: 999px;

  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  --transition: 120ms ease;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: var(--dark-bg);
    --surface: var(--dark-surface);
    --surface-2: var(--dark-surface-2);
    --border: var(--dark-border);
    --text: var(--dark-text);
    --muted: var(--dark-muted);
    --accent: var(--dark-accent);
    --accent-hover: var(--dark-accent-hover);
    --accent-soft: var(--dark-accent-soft);
    --accent-text: var(--dark-accent-text);
    --danger: var(--dark-danger);
    --danger-soft: var(--dark-danger-soft);
    --danger-text: var(--dark-danger-text);
    --warn: var(--dark-warn);
    --warn-bg: var(--dark-warn-bg);
    --warn-border: var(--dark-warn-border);
    --focus: var(--dark-focus);
    --shadow-1: var(--dark-shadow-1);
    --shadow-2: var(--dark-shadow-2);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: var(--dark-bg);
  --surface: var(--dark-surface);
  --surface-2: var(--dark-surface-2);
  --border: var(--dark-border);
  --text: var(--dark-text);
  --muted: var(--dark-muted);
  --accent: var(--dark-accent);
  --accent-hover: var(--dark-accent-hover);
  --accent-soft: var(--dark-accent-soft);
  --accent-text: var(--dark-accent-text);
  --danger: var(--dark-danger);
  --danger-soft: var(--dark-danger-soft);
  --danger-text: var(--dark-danger-text);
  --warn: var(--dark-warn);
  --warn-bg: var(--dark-warn-bg);
  --warn-border: var(--dark-warn-border);
  --focus: var(--dark-focus);
  --shadow-1: var(--dark-shadow-1);
  --shadow-2: var(--dark-shadow-2);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  background-image:
    radial-gradient(circle at 10% -10%, color-mix(in srgb, var(--accent) 15%, transparent), transparent 32rem),
    radial-gradient(circle at 100% 20%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 28rem);
  background-attachment: fixed;
}

/*
 * The whole app was built inside a 60rem column. On a 1920px screen that left
 * half the display empty and on 2560px nearly two thirds — a ledger squeezed
 * into a phone-shaped strip in the middle of a desktop.
 *
 * Content now grows with the window up to a point where a table stops being
 * readable, and prose is capped separately (see max-width: 70ch further down)
 * so widening the page does not lengthen the lines.
 */
main {
  max-width: 100rem;
  margin: 0 auto;
  padding: 1.75rem 2rem 4rem;
  width: 100%;
}
main.narrow { max-width: 44rem; }
main.center { text-align: center; }

/* --- admin shell: fixed sidebar, content takes the rest --- */

.shell {
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr);
  min-height: 100vh;
}
.shell-main { min-width: 0; }

.sidenav {
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: var(--space-5) 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}
.sidenav .brand {
  font-weight: 700;
  font-size: var(--text-lg);
  text-decoration: none;
  color: var(--text);
  padding: 0 var(--space-5);
  letter-spacing: -.01em;
}
.sidenav nav { display: flex; flex-direction: column; gap: 1px; }
.sidenav nav a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: .55rem var(--space-5);
  text-decoration: none;
  color: var(--muted);
  font-size: var(--text-md);
  border-left: 3px solid transparent;
  transition: color var(--transition), background var(--transition);
}
.navlabel { margin-right: auto; }
/*
 * The icon is the only thing that moves on hover. It is a cheap way to make a
 * dense list feel responsive without animating a colour, which at this size
 * reads as a flicker rather than as feedback.
 */
.sidenav nav a .icon { color: var(--muted); flex: none; transition: color var(--transition); }
.sidenav nav a:hover { color: var(--text); background: var(--surface-2); }
.sidenav nav a:hover .icon { color: var(--text); }
.sidenav nav a.current {
  color: var(--accent);
  font-weight: 600;
  border-left-color: var(--accent);
  background: var(--accent-soft);
}
.sidenav nav a.current .icon { color: var(--accent); }

.sidenav-foot {
  margin-top: auto;
  padding: 0 var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.sidenav-out { margin: 0; }
.sidenav-out button.link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0;
  color: var(--muted);
  text-decoration: none;
}
.sidenav-out button.link:hover { color: var(--text); text-decoration: underline; }

@media (max-width: 60rem) {
  /* A fixed side column on a phone is stolen width. Back to a row on top. */
  .shell { display: block; }
  .sidenav {
    position: static;
    height: auto;
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    padding: .7rem var(--space-4);
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }
  .sidenav .brand { padding: 0; }
  .sidenav nav { flex-direction: row; flex-wrap: wrap; gap: var(--space-1) var(--space-4); margin-right: auto; }
  .sidenav nav a { padding: .3rem 0; border-left: 0; border-bottom: 2px solid transparent; background: none; }
  .sidenav nav a:hover { background: none; }
  .sidenav nav a.current { border-left: 0; border-bottom-color: var(--accent); background: none; }
  .sidenav-foot { margin: 0; padding: 0; flex-direction: row; align-items: center; gap: var(--space-4); }
  main { padding: var(--space-5) var(--space-4) 3rem; }
}

h1 { font-size: 1.5rem; margin: 0 0 1rem; }
h2 { font-size: 1.1rem; margin: 1.75rem 0 .6rem; }
h3 { font-size: .95rem; margin: 1.25rem 0 .4rem; }

a { color: var(--accent); }

.muted { color: var(--muted); font-size: .9rem; }
.mono { font-family: var(--font-mono); font-size: .9rem; }
.break { word-break: break-all; }

/* --- panels and cards --- */

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 1rem 1.1rem;
  margin: 1rem 0;
  box-shadow: var(--shadow-1);
}
.panel.highlight { border-color: var(--accent); }
.panel > summary { cursor: pointer; font-weight: 600; }

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: .75rem;
  margin-bottom: 1.75rem;
}
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: .9rem 1rem;
  text-decoration: none;
  color: var(--text);
  display: flex;
  flex-direction: column;
  gap: .15rem;
}
.card b { font-size: 1.6rem; font-weight: 600; }
.card span { color: var(--muted); font-size: .85rem; }
.card.warn { background: var(--warn-bg); border-color: var(--warn-border); }

/* --- forms --- */

label { display: block; margin: .6rem 0; font-size: .88rem; color: var(--muted); }
input, textarea, select {
  display: block;
  width: 100%;
  margin-top: .2rem;
  padding: .5rem .6rem;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
textarea { resize: vertical; }

button, .button {
  font: inherit;
  padding: .5rem .9rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-text);
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
button.secondary { background: transparent; border-color: var(--border); color: var(--text); }
button.link {
  background: none;
  border: none;
  color: var(--accent);
  padding: 0 .35rem;
  text-decoration: underline;
}
button.danger, button.link.danger { color: var(--danger); }
button.danger:not(.link) { background: var(--danger); color: var(--danger-text); }

form.inline { display: inline; }
.row { display: flex; gap: .75rem; flex-wrap: wrap; align-items: center; margin: .6rem 0; }

.rowform {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 0 .75rem;
  align-items: end;
  margin: .5rem 0;
}

/*
 * The buttons get a row of their own, spanning every column.
 *
 * Without the span they were just another grid item, so they were squeezed
 * into one ~9rem cell alongside the inputs: the labels wrapped onto three
 * lines and the row grew taller than the panel, pushing the buttons out past
 * its edge. Nothing about that was visible in the markup — it only showed up
 * once there were enough fields beside them.
 */
.rowform-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  align-items: center;
  margin-top: .4rem;
  padding-bottom: .6rem;
}

/*
 * Tables scroll sideways inside themselves rather than pushing the panel wide.
 *
 * A table is the one element here that cannot shrink below its content — on a
 * 375px screen the payments table is ~150px too wide. Clipping it with
 * overflow:hidden on the panel would have hidden the buttons in the last
 * column instead, which is worse than a scrollbar: the content becomes
 * unreachable rather than merely awkward.
 */
.tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: .5rem 0 1rem; }
.tablewrap table { margin: 0; }

/* --- tables --- */

table { width: 100%; border-collapse: collapse; margin: .5rem 0 1rem; font-size: .93rem; }
th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--border); }
th { font-weight: 600; color: var(--muted); font-size: .82rem; text-transform: uppercase; letter-spacing: .03em; }
td.actions { white-space: nowrap; }

/* --- status pills --- */

.pill {
  display: inline-block;
  padding: .1rem .5rem;
  border-radius: var(--radius-full);
  font-size: .78rem;
  border: 1px solid var(--border);
  color: var(--muted);
  white-space: nowrap;
}
/*
 * Filled, not outlined. The whole point of these pages is answering "is this
 * still working?" and "has this been paid?" at a glance — an outline reads as
 * decoration, a filled chip reads as a state.
 */
.pill.active, .pill.paid {
  background: var(--accent-soft); border-color: transparent; color: var(--accent); font-weight: 600;
}
.pill.expiring, .pill.pending {
  background: var(--warn-bg); border-color: transparent; color: var(--warn); font-weight: 600;
}
.pill.expired, .pill.unpaid {
  background: var(--danger-soft); border-color: transparent; color: var(--danger); font-weight: 600;
}

/* --- accounts --- */

.account {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: .85rem 1rem;
  margin: .7rem 0;
  background: var(--surface);
  box-shadow: var(--shadow-1);
}
.account-head { display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
.account dl { display: grid; grid-template-columns: max-content 1fr; gap: .3rem 1rem; margin: .6rem 0 0; }
.account dt { color: var(--muted); font-size: .85rem; }
.account dd { margin: 0; word-break: break-all; }

/* --- chat --- */

.chat { display: flex; flex-direction: column; gap: .6rem; margin: .5rem 0 1rem; }
.msg { max-width: 85%; padding: .5rem .8rem; border-radius: var(--radius-md); border: 1px solid var(--border); }
.msg p { margin: 0; white-space: pre-wrap; word-break: break-word; }
.msg time { display: block; margin-top: .25rem; font-size: .75rem; color: var(--muted); }
.msg.customer { align-self: flex-start; background: var(--surface); }
.msg.admin { align-self: flex-end; background: var(--warn-bg); border-color: var(--warn-border); }

.chatform { display: flex; flex-direction: column; gap: .5rem; align-items: flex-start; }
.chatform textarea { width: 100%; }

/* --- thread list --- */

.threads { list-style: none; padding: 0; margin: 0; }
.threads li { border-bottom: 1px solid var(--border); }
.threads a {
  display: flex; align-items: center; gap: .75rem;
  padding: .7rem .2rem; text-decoration: none; color: var(--text);
}
.thread-name { font-weight: 500; margin-right: auto; }
.badge {
  background: var(--danger); color: var(--danger-text);
  border-radius: var(--radius-full); padding: 0 .45rem; font-size: .78rem;
}

/* --- admin preview of a customer page --- */

/*
 * Loud on purpose. The whole page below it is a faithful copy of what someone
 * else sees, and mistaking it for the real admin view is exactly the error
 * this bar exists to prevent.
 */
.previewbar {
  background: var(--warn-bg);
  border-bottom: 1px solid var(--warn-border);
  padding: .55rem 1rem;
  font-size: .9rem;
  text-align: center;
}

/* --- misc --- */

.error {
  background: var(--warn-bg);
  border: 1px solid var(--danger);
  border-radius: var(--radius-sm);
  padding: .6rem .8rem;
  margin: .8rem 0;
}
.code {
  font-family: var(--font-mono);
  font-size: 2rem;
  letter-spacing: .18em;
  margin: .3rem 0;
}
hr { border: none; border-top: 1px solid var(--border); margin: 1rem 0; }
details summary { cursor: pointer; }

/* A crypto payment the customer has to act on: an amount and an address that
   must be copied exactly. Given room rather than squeezed into a table cell. */
.cryptopay {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  padding: .5rem .7rem;
  border: 1px solid var(--warn-border);
  background: var(--warn-bg);
  border-radius: var(--radius-md);
  font-size: .9rem;
  max-width: 26rem;
}
.cryptopay .break { word-break: break-all; }

/* --- admin chat: thread beside the customer's context --- */

/*
 * Two columns where there is room, stacked where there is not. The context
 * column comes second in the source so a narrow screen shows the conversation
 * first — on a phone the reason you opened the page is the message, not the
 * account list.
 */
.threadlayout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.25rem;
  align-items: start;
}
@media (min-width: 56rem) {
  .threadlayout { grid-template-columns: minmax(0, 1fr) 18rem; }
  .threadside { position: sticky; top: 1rem; }
}

.threadside h2 { font-size: .82rem; text-transform: uppercase; letter-spacing: .03em;
                 color: var(--muted); margin: 0 0 .5rem; }
.threadside h2 + h2, .sidecard + h2, p + h2 { margin-top: 1.25rem; }

.sidecard {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: .6rem .7rem;
  margin-bottom: .5rem;
  font-size: .88rem;
  display: flex;
  flex-direction: column;
  gap: .15rem;
}
.sidecard-head { display: flex; justify-content: space-between; align-items: center; gap: .5rem; }

/* The thread scrolls on its own so the context column stays put beside it. */
.threadmain .chat { max-height: 60vh; overflow-y: auto; }

/* Guides are the one place author-written prose is rendered, so it gets the
   vertical rhythm the rest of the app does not need. */
.guide h2 { margin: 0; }
.guide h3 { margin: 1.25rem 0 .3rem; }
.guide p { margin: .6rem 0; }
.guide ul, .guide ol { margin: .6rem 0; padding-left: 1.3rem; }
.guide li { margin: .25rem 0; }
.guide code { background: var(--bg); padding: .1rem .3rem; border-radius: var(--radius-sm); }
.guide a { word-break: break-word; }

/* --- phones --- */

@media (max-width: 40rem) {
  /*
   * Text-style buttons ("betald", "ta bort", "kolla") render at 25px, which is
   * fine for a mouse and too small for a thumb. They keep looking like links —
   * turning them into real buttons would make a row of four read as four
   * primary actions — but get enough vertical padding to be hittable, and
   * enough spacing that adjacent ones are not one target.
   */
  button.link {
    padding: .55rem .5rem;
    display: inline-block;
    line-height: 1.2;
  }
  td.actions { white-space: normal; }
  td.actions form { display: inline-block; }

  /* Status pills sat at 12.5px — readable on a desktop, a squint on a phone. */
  .pill { font-size: .82rem; padding: .15rem .55rem; }

  /* A grid of ~9rem columns fits two per row at 375px, which puts a label and
     an unrelated field side by side. One field per row is easier to read and
     to fill in. */
  .rowform { grid-template-columns: 1fr; }

  /* A checkbox renders at 13px — a fifth of a fingertip. */
  label.checkbox input[type="checkbox"] { width: 1.35rem; height: 1.35rem; }
  label.checkbox { padding: .5rem 0; }

  /* The unread counter in the nav. Small by design, but not a squint. */
  .badge { font-size: .82rem; padding: .1rem .5rem; }
}

/*
 * A checkbox and its text on one line.
 *
 * The blanket `input { width: 100% }` above would otherwise stretch it across
 * the form, so it needs an explicit size rather than an inline style — an
 * inline style wins over the media query and would leave it 13px on a phone.
 */
label.checkbox {
  display: flex;
  align-items: center;
  gap: .5rem;
  color: var(--text);
}
label.checkbox input[type="checkbox"] {
  width: 1.1rem;
  height: 1.1rem;
  margin: 0;
  flex: none;
}

/* --- linked accounts drawn as one unit --- */

/*
 * A MAG box and the M3U line it shares a subscription with are one thing to
 * the person reading. Listed as two separate cards they read as two unrelated
 * accounts, and the relationship becomes a line of text you have to notice.
 *
 * The group is joined by removing the border between members and running a
 * coloured rail down the left, so the eye takes it as a single block.
 */
.account.grouped { border-left: 3px solid var(--accent); }
.account.grouped-first { margin-bottom: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.account.grouped-rest {
  margin-top: 0;
  border-top: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.grouplink {
  font-size: .8rem;
  color: var(--muted);
  margin-bottom: .4rem;
  padding-bottom: .3rem;
  border-bottom: 1px dashed var(--border);
}

/* The customer's own note on an account: a field and a button on one line. */
.notefield { display: flex; gap: .5rem; align-items: flex-end; margin-top: .6rem; }
.notefield label { flex: 1; margin: 0; }
.notefield button { flex: none; }

/* Choosing how to pay. Given room because the explanation is the point — the
   two buttons alone would be a coin toss. */
.choosepay { display: flex; flex-direction: column; gap: .5rem; max-width: 30rem; }
.choosepay form { display: inline-block; }
.choosepay p { margin: 0; }

@media (max-width: 40rem) {
  .notefield { flex-direction: column; align-items: stretch; }
}

/* The "how do I…" box on the account page.
   Collapsed, because it is read once and then in the way — unlike a form,
   which has to be visible to be used. */
.helpbox > summary {
  font-weight: 600;
  cursor: pointer;
  padding: .2rem 0;
}
.helpbox[open] > summary { margin-bottom: .5rem; border-bottom: 1px solid var(--border); padding-bottom: .5rem; }

/* --- copy buttons --- */

/*
 * Every credential on these pages exists to be pasted somewhere else. On a
 * phone, dragging a selection across a 42-character address is the worst part
 * of the flow — and a mistyped payment address means money that never arrives.
 */
.copywrap {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  max-width: 100%;
}
.copybtn {
  font: inherit;
  font-size: .78rem;
  padding: .25rem .55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--bg);
  color: var(--muted);
  cursor: pointer;
  flex: none;
  white-space: nowrap;
}
.copybtn:hover { color: var(--text); border-color: var(--muted); }
.copybtn.copied { color: var(--accent); border-color: var(--accent); }

/* --- portals --- */

.portal .account-head h2 { margin: 0; font-size: 1rem; }
.urlrow {
  display: flex;
  flex-direction: column;
  gap: .2rem;
  padding: .6rem 0;
  border-top: 1px solid var(--border);
}
.urlrow:first-of-type { border-top: 0; }
.urllabel { font-size: .82rem; color: var(--muted); font-weight: 600; }

@media (max-width: 40rem) {
  /* Full-width tap target on a phone rather than a pill beside the text. */
  .copybtn { padding: .45rem .8rem; font-size: .85rem; }
}

/* --- account read view on the customer card --- */

/*
 * One line per account instead of eight form fields.
 *
 * The form is still there, behind a Redigera summary — but seeing an account
 * is the common case and editing is the rare one, and the page was built the
 * other way round. Six accounts rendered 4.7 screens of inputs.
 */
.acctline {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
}
.acctmain { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
.acctmeta { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.acctnote { margin: .3rem 0 0; }

.acctedit > summary {
  cursor: pointer;
  font-size: .85rem;
  color: var(--accent);
  margin-top: .5rem;
  width: max-content;
}
.acctedit[open] > summary { margin-bottom: .3rem; }
.acctactions { margin: .4rem 0 0; gap: .5rem; }

/* --- readable line length --- */

/*
 * Prose was running 111–129 characters on the customer pages. Past roughly 85
 * the eye loses the line on the way back to the left margin.
 *
 * Scoped to running text only: tables, account cards, addresses and anything
 * monospaced keep the full width, because for those the constraint is fitting
 * the content, not reading a sentence.
 */
/*
 * Every paragraph and list item under main, at any depth. Chasing specific
 * parents (.panel > p, form > p, section > p) missed one each time — the
 * property is "this is running text", not "this sits in a particular box".
 */
main p,
main li {
  max-width: 70ch;
}

/* The exceptions, where fitting the content beats reading a sentence. */
main p.mono,
main p.break,
main .mono,
main .break,
main td,
main th,
main .chat p,
main .cryptopay {
  max-width: none;
}

/* Details used as a panel needs its summary to look like the heading it replaces. */
details.panel > summary {
  font-size: 1.1rem;
  font-weight: 600;
  list-style-position: outside;
}
details.panel[open] > summary {
  margin-bottom: .6rem;
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--border);
}
details.guide > summary { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }

/*
 * Summaries are tap targets.
 *
 * The read-view/edit split turned several headings into <summary>, which
 * renders at the height of its text — around 20px, well under a fingertip.
 * They were not targets before this change, so this is fixing a regression the
 * change introduced rather than pre-empting the mobile pass.
 */
summary { padding: .45rem 0; }
@media (max-width: 40rem) {
  summary { padding: .6rem 0; }
}

/* --- column layouts --- */

/*
 * Two columns where the content genuinely splits in two, one column where it
 * does not. The breakpoint is 75rem rather than a device size: below it two
 * columns are each narrower than a comfortable reading measure, which is worse
 * than scrolling.
 */
.cols { display: grid; gap: 1.25rem; align-items: start; }
.col { min-width: 0; }

@media (min-width: 75rem) {
  /* Identity and actions are a fixed-ish column; the working lists take the
     rest, because that is where the width is actually needed. */
  .cols-side { grid-template-columns: minmax(0, 22rem) minmax(0, 1fr); }
  .cols-even { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Panels inside a column should not each carry an outer margin — the grid gap
   already spaces them, and doubling it makes the columns drift apart. */
.col > .panel:first-child { margin-top: 0; }

/*
 * Repeated cards flow into a grid rather than stacking one per row.
 *
 * On a wide screen a single column of accounts meant scrolling past a lot of
 * empty space to the right of every card. 24rem is roughly where an account
 * card stops looking cramped.
 */
.cardgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
  gap: .75rem;
  align-items: start;
}
.cardgrid > .account { margin: 0; }

/* A linked pair must stay a pair: it spans the full row rather than being split
   across two grid cells where the shared rail would read as two unrelated ones. */
.cardgrid > .account.grouped { grid-column: 1 / -1; }

@media (max-width: 40rem) {
  .cardgrid { grid-template-columns: 1fr; }
}

/* Payment form in the chat side panel: narrow column, so fields stack. */
.sidepay label { margin: .4rem 0; }
.sidepay-actions { display: flex; flex-direction: column; gap: .4rem; margin-top: .5rem; }
.sidepay-actions button { width: 100%; }

/* "Hur gör jag?" beside a payment — a link, not a third button competing
   with the one that actually takes the money. */
.guidelink { display: inline-block; margin-top: .35rem; font-size: .88rem; }

/* ==========================================================================
   Added with the design system: focus, icons, the theme toggle, tags,
   notices, and the table and card treatments that go with them.
   ========================================================================== */

/*
 * Focus. There was none — not one `:focus` or `outline` rule in the whole
 * file, on an app whose destructive actions ("ta bort", "Neka", "Markera
 * betald") are all `button.link`: no background, no border, styled to look
 * like text. Tabbing across a table row with four of them was a guess.
 *
 * :focus-visible rather than :focus, so a mouse click on a button does not
 * leave a ring behind it.
 */
:where(a, button, .button, input, textarea, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/*
 * For text that only a screen reader should reach. Not `display: none` and not
 * `visibility: hidden` — both remove the element from the accessibility tree,
 * which is the opposite of what this is for.
 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/*
 * Straight past the navigation. Seven links stand between the top of every
 * admin page and its content, and a keyboard user meets all seven on every
 * page load. Hidden until focused, then it lands on top of everything.
 */
.skiplink {
  position: absolute;
  left: var(--space-2);
  z-index: 10;
  padding: var(--space-2) var(--space-3);
  background: var(--accent);
  color: var(--accent-text);
  border-radius: var(--radius-sm);
  text-decoration: none;
  /*
   * Moved with `top`, and with no transition on it.
   *
   * A transform is composited: on a page the browser is not painting, both
   * getBoundingClientRect() and getComputedStyle() keep reporting the parked
   * position however you move it, which makes this link impossible to verify.
   * A transition has the same problem for a different reason — it does not run
   * while the page is hidden, so the value stays at where it started.
   *
   * Neither is worth having here anyway. The one person who needs this link is
   * navigating by keyboard and wants it immediately, not after 120ms of it
   * sliding into place.
   */
  top: -4rem;
}
.skiplink:focus { top: var(--space-2); }

.icon { flex: none; vertical-align: -.15em; }

/*
 * The theme toggle: three states in a segmented control.
 *
 * Three, not two, because a two-state switch can only say "not what my system
 * says" and gives no way back to following it.
 */
.themetoggle {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  width: max-content;
}
.themebtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: .3rem .5rem;
  background: none;
  border: 0;
  border-radius: var(--radius-full);
  color: var(--muted);
  cursor: pointer;
  transition: color var(--transition), background var(--transition);
}
.themebtn:hover { color: var(--text); }
.themebtn.current { background: var(--surface); color: var(--accent); box-shadow: var(--shadow-1); }

/*
 * A tag is not a pill.
 *
 * `.pill` was carrying three unrelated jobs: state (active/expired/paid),
 * type (MAG/M3U) and category (a guide's section, an operator's name). Colour
 * meant "state" in one of those and nothing at all in the other two, so a
 * neutral grey chip beside a red one read as a state nobody had defined. Tags
 * are now visibly a different object.
 */
.tag {
  display: inline-block;
  padding: .1rem .5rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--muted);
  white-space: nowrap;
}

/*
 * Notices. "Sparat." used to be a `.panel.highlight` — the same box as a
 * content panel with a green border, which reads as a section rather than as
 * an answer to what you just did.
 */
.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: .7rem var(--space-4);
  margin: var(--space-4) 0;
  border-radius: var(--radius-md);
  border: 1px solid var(--accent);
  background: var(--accent-soft);
  color: var(--text);
  font-size: var(--text-md);
}
.notice.warn { border-color: var(--warn-border); background: var(--warn-bg); }
.notice.error { border-color: var(--danger); background: var(--danger-soft); }

/*
 * Cards lift slightly on hover. Every one of them is a link to the list it
 * counts, which was not discoverable before: they looked exactly like the
 * static panels beside them.
 */
.card {
  transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition);
}
.card:hover { border-color: var(--accent); box-shadow: var(--shadow-2); transform: translateY(-1px); }
.card b { letter-spacing: -.02em; }
.card span { text-transform: uppercase; letter-spacing: .04em; font-size: var(--text-xs); }

/*
 * Zebra striping only.
 *
 * There was a sticky header here. It could not work: `.tablewrap` has
 * `overflow-x: auto`, which computes `overflow-y` to `auto` as well, so the
 * wrapper — not the viewport — becomes the sticky containing block. It has no
 * height cap, so it never scrolls vertically and the header never sticks;
 * page scroll moves the whole wrapper instead. Making it work would mean
 * capping the wrapper's height and living with a second scrollbar, which is a
 * worse trade on a page whose tables are mostly short.
 *
 * The header row also needed a background only to cover rows sliding under it.
 * Without the sticky it needs none, and the one it had (`--bg`) was wrong in
 * the five tables that sit inside a `.panel`: it painted page background
 * through the card, darker than both the panel and the striped rows below.
 */
tbody tr:nth-child(even) { background: var(--surface-2); }
tbody tr:hover { background: var(--accent-soft); }
td.actions { text-align: right; }

/*
 * The scroll container needs to be focusable, or the horizontal scroll it
 * exists to provide cannot be reached from a keyboard at all.
 */
.tablewrap:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* Buttons: one height, one radius, and a hover that is actually visible. */
button, .button {
  border-radius: var(--radius-sm);
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
button:hover:not(.link):not(:disabled), .button:hover { background: var(--accent-hover); }
button.secondary:hover:not(:disabled) { background: var(--surface-2); border-color: var(--muted); }
button.danger:not(.link):hover:not(:disabled) { filter: brightness(1.1); }
button:disabled { opacity: .5; cursor: not-allowed; }

/*
 * The chat is polled, so messages arrive without a page load. Without a live
 * region a screen reader is never told; the thread simply grows behind it.
 */
.chat { scroll-behavior: smooth; }

/* Inputs pick up the accent while they are being typed in. */
input:focus, textarea:focus, select:focus { border-color: var(--accent); }

/* An empty live region should occupy nothing; see partials/chat.ejs. */
.chatstatus:empty { margin: 0; }

/* --- TeamTV private customer experience and N1 approval flow --- */
.eyebrow {
  display: block;
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  margin-bottom: .35rem;
}
.page-title h1 { margin-bottom: 0; }
.auth-shell {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--space-5);
}
.auth-card {
  width: min(100%, 29rem);
  padding: clamp(1.5rem, 5vw, 2.7rem);
  border: 1px solid var(--border);
  border-radius: 1.4rem;
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  box-shadow: 0 1.5rem 5rem color-mix(in srgb, var(--accent) 18%, transparent);
  text-align: left;
}
.auth-card h1 { font-size: var(--text-2xl); line-height: 1.15; letter-spacing: -.04em; }
.auth-card button { width: 100%; padding: .7rem 1rem; margin-top: .4rem; }
.brand-mark {
  width: 3.1rem;
  height: 3.1rem;
  display: grid;
  place-items: center;
  margin-bottom: var(--space-5);
  border-radius: 1rem;
  background: linear-gradient(145deg, var(--accent), #d768ff);
  color: #fff;
  font-size: var(--text-xl);
  font-weight: 800;
  letter-spacing: -.08em;
  box-shadow: 0 .7rem 2rem color-mix(in srgb, var(--accent) 34%, transparent);
}
.subscription-hero {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding: clamp(1.3rem, 4vw, 2.2rem);
  margin-bottom: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: linear-gradient(130deg, var(--surface), var(--accent-soft));
  box-shadow: var(--shadow-2);
}
.subscription-hero h1 { margin-bottom: .3rem; font-size: clamp(1.5rem, 4vw, 2.3rem); }
.subscription-hero p { margin: 0; color: var(--muted); }
.subscription-stat { text-align: center; min-width: 5.5rem; }
.subscription-stat b { display: block; font-size: 2.4rem; line-height: 1; color: var(--accent); }
.subscription-stat span { color: var(--muted); font-size: var(--text-sm); }
.secret { display: flex; align-items: center; gap: .2rem; flex-wrap: wrap; }
.secret [data-secret-label] { min-width: 5rem; letter-spacing: .12em; }
.planstrip { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .55rem; margin-top: var(--space-4); }
.plan-card { padding: .75rem; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-2); }
.plan-card > span { display: block; color: var(--accent); font-weight: 700; margin: .15rem 0 .55rem; }
.plan-card button { width: 100%; }
.searchbar { display: flex; align-items: end; gap: .6rem; margin-bottom: var(--space-4); }
.searchbar label { flex: 1; margin: 0; }
.candidate, .queue-row, .action-plan {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: .75rem .85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  margin: .5rem 0;
}
.candidate .rowform { flex: 1; margin: 0; }
.integration-meta { display: flex; align-items: center; flex-wrap: wrap; gap: .45rem; margin-top: .4rem; font-size: var(--text-sm); }
.queue-row { color: var(--text); text-decoration: none; }
.queue-row:hover { border-color: var(--accent); }
.queue-row.urgent { border-color: var(--warn-border); background: var(--warn-bg); }
.queue-row.danger-row { border-color: var(--danger); background: var(--danger-soft); }
.approval-card { padding: clamp(1rem, 4vw, 2rem); }
.approval-head { display: flex; justify-content: space-between; align-items: start; gap: var(--space-4); }
.approval-facts { display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1rem; padding: var(--space-4) 0; }
.approval-facts dt { color: var(--muted); }
.approval-facts dd { margin: 0; font-weight: 600; }
.action-plan { align-items: start; flex-wrap: wrap; }
.approval-button { width: 100%; padding: .8rem 1rem; font-weight: 700; }

@media (max-width: 40rem) {
  .subscription-hero, .candidate, .queue-row, .approval-head { align-items: stretch; flex-direction: column; }
  .subscription-stat { text-align: left; }
  .searchbar { align-items: stretch; flex-direction: column; }
  .searchbar button { width: 100%; }
}

/* Live updates must respect hidden zero badges and remain usable on small screens. */
[hidden] { display: none !important; }
.chat { max-height: 60vh; overflow-y: auto; overscroll-behavior: contain; }
.threads a { flex-wrap: wrap; }
.thread-name { overflow-wrap: anywhere; min-width: 0; }
@media (prefers-reduced-motion: reduce) { .chat { scroll-behavior: auto; } }
