/* ------------------------------------------------------------------- type
 * Self-hosted rather than linked from Google's CDN: no third-party request on
 * every page load, nothing to leak about who is reading, and the fonts arrive
 * from the same origin Cloudflare is already caching. Both are OFL 1.1, which
 * permits exactly this.
 *
 * VARIABLE files, not static instances. The stylesheet asks for weights like
 * 620 and 650 in a dozen places; a static family would round each of those to
 * 600 or 700 and quietly flatten the hierarchy the design was built on.
 *
 * `font-display: swap` shows the fallback immediately and repaints when the
 * file lands - a dashboard that is blank for 300ms is worse than one that
 * reflows once. The stacks below name the old system fonts as the fallback, so
 * that first paint is the metrics this design was originally drawn against.
 *
 * latin-ext is a separate file behind its own unicode-range, so an English
 * page never downloads it. */

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("/static/fonts/jakarta-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("/static/fonts/jakarta-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --bg: #0f1115;
  --panel: #171a21;
  --panel-2: #1e222b;
  --line: #2a2f3a;
  --text: #e6e8ee;
  --muted: #99a1b3;
  --accent: #e1306c;
  --accent-2: #8134af;
  --ok: #2ea043;
  --warn: #d29922;
  --danger: #da3633;
  /* Platform colours, used only by that platform's skin. The brand gradient
     stays the identity of the site itself. */
  --youtube: #ff0033;

  /* Two faces, because they are asked to do different jobs. Inter is drawn for
     dense interface text and holds up at 12px in a table of counts; Jakarta
     has the character the headings were missing, and only ever appears large
     enough for it to read as character rather than noise. The old system stack
     stays on the end of both as the swap-in fallback. */
  --font-ui: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-display: "Plus Jakarta Sans", var(--font-ui);
}

/* The controls the browser draws itself - number spinners, select popups,
   scrollbars, date pickers - follow this rather than our CSS. Without it they
   arrive light-on-light in the middle of a dark page. */
:root { color-scheme: dark; }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.55 var(--font-ui);
  /* `cv11` gives Inter the single-storey `l` with a tail, which is what stops
     lI1 reading as three of the same character in a handle or a video id. */
  font-feature-settings: "cv11";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Headings, and the numbers that behave like them. */
h1, h2, h3, h4, .brand, .stepno, .section-head, .page-head h1, .hero-copy h1 {
  font-family: var(--font-display);
}

/* Counts are read down a column and compared against each other, so the digits
   have to be one width. Proportional figures make 1,111 visibly narrower than
   8,888 and a table of them look ragged. */
table, .stat .num, .post-engagement, .profile-counts, .count, .counter,
.chart-svg, .tile-cap, .ig-stats, .step-list {
  font-variant-numeric: tabular-nums;
}

a { color: inherit; }

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 24px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 10;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  text-decoration: none;
  letter-spacing: .2px;
}

.brand .dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--warn), var(--accent) 55%, var(--accent-2));
}

.topbar nav a {
  color: var(--muted);
  text-decoration: none;
  margin-left: 18px;
  font-size: 14px;
}
.topbar nav a:hover { color: var(--text); }

main {
  max-width: 1180px;
  margin: 0 auto;
  padding: 24px 24px 64px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

footer {
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 12px;
  padding: 18px 24px;
  text-align: center;
}

/* ------------------------------------------------------------------ panels */
.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 18px 20px;
}

.panel h2 {
  margin: 0 0 14px;
  font-size: 15px;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .08em;
}

.status-panel {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px;
}

.stat {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.stat .num { font-family: var(--font-display); font-size: 26px; font-weight: 650; letter-spacing: -0.4px; }
.stat .lab { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }

/* ------------------------------------------------------------------ tables */
table { width: 100%; border-collapse: collapse; }

th {
  text-align: left;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
  font-weight: 600;
  padding: 6px 10px;
  border-bottom: 1px solid var(--line);
}

td {
  padding: 9px 10px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}

tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--panel-2); }

td.actions { white-space: nowrap; text-align: right; }
td.actions form { display: inline-block; margin-left: 6px; }
td.detail { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.acct { display: inline-flex; align-items: center; gap: 8px; text-decoration: none; font-weight: 550; }
.acct:hover { color: var(--accent); }

.avatar { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; }
.avatar.big { width: 84px; height: 84px; border-radius: 50%; }

/* ------------------------------------------------------------------- forms */
.row-form { display: flex; gap: 10px; flex-wrap: wrap; }
/* Direct children only. Without the `>` this also matched the checkboxes
   nested inside the type boxes, giving each one a 200px flex basis - which is
   why a row of four small toggles spread across the whole line. */
.row-form > input { flex: 1 1 200px; }

/* Every text-like input, named by what it is NOT. Listing the types that
   should be styled is a list somebody has to remember to extend, and nobody
   did: `type="number"` was missing, so the caption limit and every field on
   the limits page rendered as a white browser default in the middle of a dark
   form. Checkboxes, radios and the button types are genuinely different
   controls and keep their own appearance. */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):not([type="color"]):not([type="submit"]):not([type="button"]):not([type="image"]) {
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--text);
  border-radius: 8px;
  padding: 8px 11px;
  font: inherit;
  font-size: 14px;
}
/* A number field's own digits should line up with every other count. */
input[type="number"] { font-variant-numeric: tabular-nums; }
input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.btn {
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--text);
  border-radius: 8px;
  padding: 8px 14px;
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
.btn:hover { border-color: var(--muted); }
.btn.small { padding: 4px 10px; font-size: 12.5px; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { filter: brightness(1.08); }
.btn.danger { color: #ff8a85; border-color: #4a2523; }
.btn.danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }

.controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.controls form.inline { display: flex; gap: 8px; align-items: center; }
.check { color: var(--muted); font-size: 13px; display: inline-flex; gap: 5px; align-items: center; }

/* ------------------------------------------------------------------- pills */
.pill {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--panel-2);
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}
.pill.tiny { font-size: 10.5px; padding: 1px 7px; }
.pill.ok { color: #7ee2a8; border-color: #1d4429; background: #12241a; }
.pill.warn { color: #f0c674; border-color: #4a3a17; background: #241d10; }
.pill.busy { color: #8ab4ff; border-color: #1d3049; background: #101a26; }
.pill.post { color: #ff9ec0; }
.pill.reel { color: #c79bff; }
.pill.story { color: #ffd08a; }
.pill.highlight { color: #ffe08a; }
.pill.igtv { color: #ffb08a; }
.pill.tagged { color: #a5a0ff; }
.pill.video { color: #ff8f8f; }

/* -------------------------------------------------------------------- grid */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
}

.tile {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  border-radius: 10px;
  overflow: hidden;
  background: var(--panel-2);
  border: 1px solid var(--line);
  text-decoration: none;
}
.tile img, .tile video { width: 100%; height: 100%; object-fit: cover; display: block; }
.tile.nothumb::after {
  content: "no preview";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--muted);
  font-size: 12px;
}
.tile-meta { position: absolute; top: 7px; left: 7px; display: flex; gap: 4px; }
.tile-cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 16px 8px 6px;
  font-size: 11px;
  color: #dfe3ea;
  background: linear-gradient(transparent, rgba(0, 0, 0, .82));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.tab {
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  text-decoration: none;
  font-size: 13px;
  color: var(--muted);
}
.tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }

.pager { display: flex; align-items: center; gap: 14px; margin-top: 16px; }

/* ----------------------------------------------------------------- profile */
.profile { display: flex; gap: 20px; align-items: flex-start; }
.profile h1 { margin: 0 0 4px; font-size: 22px; display: flex; align-items: center; gap: 10px; }
.profile .name { margin: 0 0 6px; color: var(--text); }
.profile .bio { margin: 0 0 8px; color: var(--muted); max-width: 62ch; white-space: pre-wrap; }
.profile-body { flex: 1; }

/* ------------------------------------------------------------------ typography */
.hint { color: var(--muted); font-size: 13px; margin: 10px 0 0; }
.small { font-size: 12.5px; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.error { color: #ff8a85; font-size: 13px; margin: 6px 0 0; }
code {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 1px 5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
}

@media (max-width: 640px) {
  main { padding: 16px 12px 48px; }
  .profile { flex-direction: column; }
  td.detail { max-width: 140px; }
}

/* ------------------------------------------------------------- brand mark */
.mark { width: 22px; height: 22px; flex-shrink: 0; }
.brand { gap: 9px; }

/* ------------------------------------------------------- public home page */
body.home { display: flex; flex-direction: column; min-height: 100vh; }

.home-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 32px;
  max-width: 1080px;
  width: 100%;
  margin: 0 auto;
  box-sizing: border-box;
}
.home-nav .brand { display: flex; align-items: center; font-weight: 600; text-decoration: none; }
.home-nav nav { display: flex; align-items: center; gap: 14px; }
.home-nav nav a:not(.btn) { color: var(--muted); text-decoration: none; font-size: 14px; }
.home-nav nav a:not(.btn):hover { color: var(--text); }

.home-main {
  flex: 1;
  max-width: 1080px;
  width: 100%;
  margin: 0 auto;
  padding: 24px 32px 72px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 56px;
}

/* The hero is a rounded card rather than a full-bleed band: bleeding to the
   edge needs 100vw, which counts the scrollbar and scrolls the page sideways
   on every desktop browser. The card keeps the same effect and cannot do that. */
.hero {
  position: relative;
  overflow: hidden;
  border-radius: 20px;
  padding: 60px 48px 96px;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr);
  align-items: center;
  gap: 32px;
  background:
    radial-gradient(circle at 12% 18%, rgba(255, 255, 255, .16), transparent 42%),
    radial-gradient(circle at 82% 78%, rgba(255, 255, 255, .12), transparent 46%),
    linear-gradient(125deg, var(--warn), var(--accent) 52%, var(--accent-2));
  color: #fff;
}
/* Ornament, drawn rather than marked up so a screen reader never meets it. */
.hero::before, .hero::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .18);
  pointer-events: none;
}
.hero::before { width: 210px; height: 210px; top: -70px; right: 20%; }
.hero::after { width: 120px; height: 120px; bottom: 40px; left: -40px; }

.hero-copy { position: relative; z-index: 1; }
.hero h1 {
  margin: 0 0 14px;
  font-size: clamp(34px, 5.2vw, 52px);
  line-height: 1.04;
  letter-spacing: -1.4px;
  font-weight: 700;
}
.hero .lede {
  margin: 0 0 26px;
  max-width: 44ch;
  font-size: 17px;
  line-height: 1.55;
  color: rgba(255, 255, 255, .88);
  text-wrap: pretty;
}
.hero-cta { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.hero-cta .hint { color: rgba(255, 255, 255, .8); }
/* Which services this works with, said by showing their marks rather than
   describing them: somebody deciding whether this is for them is looking for
   their own logo. Sits on the hero's colour, so the chips are glass rather
   than each platform's brand fill - eight saturated colours fighting the
   gradient behind them reads as a sponsor wall. The mark keeps its colour. */
.platforms {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.platform {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px 7px 11px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .28);
  background: rgba(255, 255, 255, .12);
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
}
.plogo { width: 17px; height: 17px; flex: none; }
/* White on the hero rather than each brand's own red or pink: the marks sit
   on a gradient built from those same colours and disappear into it. */
.platform .plogo { color: #fff; }

.hero-points {
  list-style: none;
  margin: 26px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  font-size: 13.5px;
  color: rgba(255, 255, 255, .85);
}
.hero-points li { display: flex; align-items: center; gap: 7px; }
.hero-points li::before {
  content: "";
  width: 15px; height: 15px;
  flex: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, .22)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8.4l2.6 2.6L12 5.6' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/12px no-repeat;
}
.hero-mark {
  position: relative;
  z-index: 1;
  justify-self: center;
  width: min(220px, 100%);
}
.hero-mark .mark { width: 100%; height: auto; filter: drop-shadow(0 12px 28px rgba(0, 0, 0, .28)); }

.hero-wave { position: absolute; left: 0; right: 0; bottom: -1px; line-height: 0; }
.hero-wave svg { width: 100%; height: 84px; display: block; }
.hero-wave path { fill: var(--bg); }

.btn.big { padding: 12px 26px; font-size: 15px; text-decoration: none; display: inline-flex;
           align-items: center; gap: 10px; }

/* Discord's own blurple, for the one button that hands you to Discord. Anything
   else here would make it look like a generic sign-in. */
.btn.discord {
  background: #5865f2;
  border-color: #5865f2;
  color: #fff;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.btn.discord:hover { background: #4752c4; border-color: #4752c4; }
.dlogo { width: 1.15em; height: 1.15em; flex: none; }

.section-head {
  margin: 0 0 22px;
  text-align: center;
  font-size: clamp(24px, 3.4vw, 32px);
  font-weight: 680;
  letter-spacing: -0.6px;
}

.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 16px;
}
.feature {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 20px 22px 22px;
  transition: border-color .15s, transform .15s;
}
.feature:hover { border-color: #3a4150; transform: translateY(-2px); }
.feature h3 { margin: 14px 0 8px; font-size: 15.5px; font-weight: 620; letter-spacing: -0.1px; }
.feature p { margin: 0; color: var(--muted); font-size: 13.5px; line-height: 1.6; text-wrap: pretty; }

.ficon {
  display: grid;
  place-items: center;
  width: 40px; height: 40px;
  border-radius: 11px;
  color: #fff;
}
.ficon svg { width: 21px; height: 21px; }
.ficon.pink   { background: #e1306c; }
.ficon.indigo { background: #5865f2; }
.ficon.purple { background: #8134af; }
.ficon.amber  { background: #d29922; }
.ficon.green  { background: #2ea043; }
.ficon.blue   { background: #2f81f7; }
.ficon.teal   { background: #12b5a5; }
.ficon.red    { background: #da3633; }
.ficon.slate  { background: #55607a; }

.step-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
  counter-reset: step;
}
.step-list li {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  gap: 2px 14px;
  align-items: center;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 18px 20px;
}
.stepno {
  grid-row: span 2;
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: #fff;
  font-weight: 650;
  font-size: 14px;
}
.step-list strong { font-size: 14.5px; font-weight: 620; }
.step-list span:not(.stepno) { color: var(--muted); font-size: 13.5px; }
.step-list code {
  background: var(--panel-2);
  border-radius: 5px;
  padding: 1px 5px;
  font-size: 12.5px;
}

.note h2 {
  margin: 0 0 10px;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted);
}
.note .hint { margin: 0; max-width: 78ch; line-height: 1.65; }

.home-footer {
  border-top: 1px solid var(--line);
  padding: 20px 32px;
  max-width: 1080px;
  width: 100%;
  margin: 0 auto;
  box-sizing: border-box;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--muted);
}

@media (max-width: 860px) {
  /* One column, mark above the copy - at this width the two-column hero gives
     the headline about twelve characters a line. */
  .hero {
    grid-template-columns: 1fr;
    text-align: center;
    padding: 40px 26px 84px;
  }
  .hero .lede { margin-left: auto; margin-right: auto; }
  .hero-cta, .hero-points { justify-content: center; }
  .hero-mark { order: -1; width: min(150px, 60%); margin-bottom: 6px; }
}

@media (max-width: 640px) {
  .home-nav, .home-main, .home-footer { padding-left: 16px; padding-right: 16px; }
  .home-main { gap: 44px; }
}

/* ------------------------------------------------ signed-in app chrome */
body.app { min-height: 100vh; }

.app-nav {
  display: flex;
  align-items: center;
  gap: 26px;
  padding: 14px 28px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 10;
}
.app-nav .brand { display: flex; align-items: center; text-decoration: none; }
.app-nav nav { display: flex; gap: 20px; }
.app-nav nav a { color: var(--muted); text-decoration: none; font-size: 14px; }
.app-nav nav a:hover { color: var(--text); }
.app-user { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.app-user .who { color: var(--muted); font-size: 13.5px; }
.app-user .avatar { border-radius: 50%; }

.app-main {
  max-width: 1180px;
  margin: 0 auto;
  padding: 28px 28px 72px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.page-head { display: flex; align-items: center; gap: 14px; }
.page-head h1 { margin: 0; font-size: 30px; letter-spacing: -0.6px; font-weight: 650; }
.page-head .count {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 3px 11px;
  font-size: 13px;
  color: var(--muted);
}
.icon-btn {
  display: inline-grid;
  place-items: center;
  width: 32px; height: 32px;
  border-radius: 8px;
  color: var(--muted);
  text-decoration: none;
}
.icon-btn:hover { color: var(--text); background: var(--panel-2); }

.crumb { margin: 0; font-size: 13.5px; }
.crumb a { color: var(--muted); text-decoration: none; }
.crumb a:hover { color: var(--text); }

/* ------------------------------------------------------- server picker */
.server-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 14px;
}

.server-card {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  text-decoration: none;
  color: var(--text);
  transition: border-color .12s ease, background .12s ease;
}
.server-card:hover { background: var(--panel-2); border-color: var(--muted); }
/* Servers the bot is already in are the actionable ones, so they read brighter. */
.server-card.active { border-color: #33405a; background: #171d29; }
.server-card.active:hover { border-color: var(--accent); }

.server-icon {
  width: 44px; height: 44px;
  border-radius: 10px;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--panel-2);
}
.server-icon.initials {
  display: grid;
  place-items: center;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--text);
  background: linear-gradient(135deg, var(--warn), var(--accent) 55%, var(--accent-2));
}
.server-icon.big { width: 64px; height: 64px; border-radius: 14px; font-size: 20px; }

.server-name {
  flex: 1;
  min-width: 0;
  font-size: 14.5px;
  font-weight: 550;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.server-action { color: var(--muted); display: grid; place-items: center; flex-shrink: 0; }
.server-card:hover .server-action { color: var(--text); }
.server-card.active .server-action { color: #8ab4ff; }

.grid-note { margin-top: 4px; }
.guild-head { gap: 18px; }
.guild-head h1 { font-size: 24px; }
.guild-head .hint { margin: 4px 0 0; }

@media (max-width: 640px) {
  .app-nav { gap: 14px; padding: 12px 16px; flex-wrap: wrap; }
  .app-user .who { display: none; }
  .app-main { padding: 20px 16px 56px; }
  .server-grid { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------- tab layout */
.tab-layout { display: grid; grid-template-columns: 236px 1fr; gap: 20px; align-items: start; }
.side-tabs { display: flex; flex-direction: column; gap: 6px; position: sticky; top: 78px; }
/* A row per platform, each carrying its own mark. The logo is what a server
   admin actually scans for - the word beside it confirms rather than leads. */
.side-tab {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 13px;
  border-radius: 10px;
  border: 1px solid transparent;
  text-decoration: none;
  color: var(--text);
}
.side-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.side-tab span { color: var(--muted); font-size: 12px; line-height: 1.35; }
.side-tab strong { font-size: 14px; font-weight: 550; }
.side-tab:hover { background: var(--panel); }
.side-tab.active { background: var(--panel); border-color: var(--line); }
.side-tab.active strong { color: var(--accent); }

/* Each platform's own colour, which is the point of showing the mark at all.
   Muted until selected, so the strip reads as one list rather than a row of
   competing brand badges. */
.side-mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 30px; height: 30px;
  border-radius: 8px;
  background: var(--panel-2);
  color: var(--muted);
}
.side-mark svg { width: 17px; height: 17px; }
.side-tab.active .side-mark.instagram {
  background: linear-gradient(135deg, #d29922, #e1306c, #8134af);
  color: #fff;
}
.side-tab.active .side-mark.youtube { background: var(--youtube); color: #fff; }
.side-tab.active .side-mark.tiktok { background: #000; color: #fff; }
.side-tab.active .side-mark.logs { background: var(--line); color: var(--text); }
/* A hint of it on hover, so the strip responds before it is clicked. */
.side-tab:hover .side-mark { color: var(--text); }

/* Announced, not offered. It must not look clickable, so it takes no hover
   and no pointer - the label under it is the whole explanation. */
.side-tab.upcoming { opacity: .5; cursor: default; }
.side-tab.upcoming:hover { background: none; }
.side-tab.upcoming:hover .side-mark { color: var(--muted); }
.side-tab.upcoming strong { font-weight: 500; }
.tab-body { display: flex; flex-direction: column; gap: 20px; min-width: 0; }

.flash { padding: 11px 15px; border-radius: 10px; font-size: 13.5px; border: 1px solid; }
.flash.ok { color: #7ee2a8; border-color: #1d4429; background: #12241a; }
.flash.bad { color: #f0a0a0; border-color: #4a2523; background: #241214; }

/* --------------------------------------------------------------- forms */
.add-form, .settings-form { display: flex; flex-direction: column; gap: 14px; }
.add-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); align-items: end; }
.add-form label, .settings-form label { display: flex; flex-direction: column; gap: 5px; }
.add-form label > span, .settings-form label > span { font-size: 12.5px; color: var(--muted); }
.settings-form label.wide { max-width: 640px; }
.settings-form label em.hint { font-style: normal; font-size: 12px; }
.settings-form label.checkbox { flex-direction: row; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.settings-form label.checkbox > span { color: var(--text); font-size: 14px; }
.settings-form label.checkbox em.hint { flex-basis: 100%; margin-left: 22px; }
.settings-form button { align-self: flex-start; }

select {
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--text);
  border-radius: 8px;
  padding: 8px 10px;
  font: inherit;
  font-size: 14px;
}
select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
input:disabled, select:disabled, button:disabled { opacity: .5; cursor: not-allowed; }

.inline-form { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.inline-form input { width: 190px; }

@media (max-width: 860px) {
  .tab-layout { grid-template-columns: 1fr; }
  .side-tabs { position: static; flex-direction: row; overflow-x: auto; }
  .side-tab span { display: none; }
}

/* --------------------------------------------------------- kind checkboxes */
/* Content types are a fixed set, so they are ticked, not typed. One outline
   per box and nothing else: a coloured label inside a bordered chip reads as
   two nested pills, which is noise on a control this small. */
fieldset.kinds { border: 0; padding: 0; margin: 0; min-width: 0; }
.add-form fieldset.kinds { grid-column: 1 / -1; }
fieldset.kinds legend { padding: 0; font-size: 12.5px; color: var(--muted); }
.kind-boxes { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 5px; }
/* `display: flex` above is an author rule, and author rules beat the browser's
   built-in `[hidden] { display: none }` whatever the specificity - so without
   this the `hidden` attribute did nothing and the unselected platform's types
   stayed on screen, merely greyed out by `disabled`. */
.kind-boxes[hidden] { display: none; }
.kind-boxes.tight { gap: 5px; margin-top: 0; }

.kind-boxes .kind-box {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  gap: 6px;
  padding: 5px 10px 5px 8px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: transparent;
  color: var(--muted);
  font-size: 13px;
  cursor: pointer;
  user-select: none;
}
.kind-boxes.tight .kind-box { padding: 3px 8px 3px 6px; gap: 5px; font-size: 12.5px; }
.kind-boxes .kind-box input { margin: 0; accent-color: var(--accent); cursor: inherit; }
.kind-boxes.tight .kind-box input { width: 12px; height: 12px; }
/* The add-form styles its label spans as field captions; these are not that. */
.kind-boxes .kind-box > span { color: inherit; font-size: inherit; line-height: 1.4; }
.kind-boxes .kind-box:hover { border-color: #3a4150; color: var(--text); }
.kind-boxes .kind-box:has(input:checked) { background: var(--panel-2); color: var(--text); }
.kind-boxes .kind-box:has(input:disabled) { opacity: .45; cursor: not-allowed; }
.kind-boxes .kind-box:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ------------------------------------------------------------- feeds table */
/* Positioned so an absolutely-placed child (the visually-hidden column
   label) resolves against this box. Without it the span escapes to the
   viewport and a wide table scrolls the whole page sideways. */
.table-scroll { overflow-x: auto; position: relative; }

/* Every cell holds a control of a different natural height, so the row is
   pinned to a single baseline rather than left to each control's default. */
table.subs th, table.subs td { padding: 10px 8px; vertical-align: middle; }
table.subs th:first-child, table.subs td:first-child { padding-left: 4px; }
table.subs th:last-child, table.subs td:last-child { padding-right: 4px; }
table.subs td .acct { white-space: nowrap; }
table.subs th.num, table.subs td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.subs select { width: 100%; min-width: 120px; max-width: 210px; height: 34px; padding: 0 9px; }
table.subs td.actions { text-align: right; }
/* Four chips on one line keeps every row the same height; the wrapper
   scrolls sideways before the chips are allowed to wrap. */
table.subs .kind-boxes { flex-wrap: nowrap; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* --------------------------------------------------------------- save bar */
/* One button per tab, below the panels: the page is saved as a whole, so a
   per-row control would imply edits land one at a time. */
.save-bar { display: flex; align-items: center; gap: 12px; }

.btn.save {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--ok);
  border-color: var(--ok);
  color: #fff;
  font-weight: 550;
}
.btn.save:hover { filter: brightness(1.1); border-color: var(--ok); }
.btn.save:disabled { opacity: .5; cursor: not-allowed; filter: none; }
.btn.save .icon { width: 16px; height: 16px; }

/* ===================================================== public post page === */
/* One archived item, readable by anyone holding the signed link. Deliberately
   its own layout: no nav, no dashboard, nothing but this post. */
body.public {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
.public-main {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  /* Load-bearing. The global `main` rule carries `margin: 0 auto`, and an auto
     margin on a flex item turns off the stretch that would otherwise give this
     the width of the page - so it shrank to fit its own contents instead. A
     photo has an intrinsic width and hid that; an embedded player is 300x150
     until it loads, which left the card the width of its title. */
  width: 100%;
}
.public-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 16px;
  border-top: 1px solid var(--line);
}
.public-foot .brand { text-decoration: none; font-weight: 600; }
/* `.hint` carries a top margin for use in prose. Here it is a flex item, and
   `align-items: center` centres the *margin box* - so that margin shifts the
   text down half its size relative to the brand beside it. */
.public-foot .hint { margin: 0; font-size: 12px; }

/* NOT `.post`: kind pills carry the bare kind as a class (`pill tiny post`),
   so a `.post` selector would style every post pill on every page. */
.postcard {
  width: 100%;
  max-width: 560px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: hidden;
}

.post-head {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--line);
}
.post-avatar {
  width: 38px; height: 38px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  /* The brand gradient stands in for a missing picture. */
  background: linear-gradient(135deg, #d29922, #e1306c, #8134af);
}
.post-avatar.initials {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 700; color: #fff;
}
.post-who { display: flex; flex-direction: column; min-width: 0; flex: 1; }
/* A flex container blockifies an inline-block child, and `flex-basis: auto`
   then resolves to `width: auto` - which fills the row and squeezes the
   username to nothing. The kind pill has to opt out of growing. */
.post-head .pill { flex: none; }
.post-user { font-weight: 600; text-decoration: none; }
.post-user:hover { color: var(--accent); }
.post-name {
  font-size: 12.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Who was tagged, kept subordinate to whoever actually posted it. */
.post-tagged { font-size: 11.5px; color: var(--muted); margin-top: 1px; }
.post-tagged a { color: var(--accent); text-decoration: none; font-weight: 550; }
.post-tagged a:hover { text-decoration: underline; }

/* ------------------------------------------------------------- the viewer */
.viewer { position: relative; background: #000; }

/* Scroll-snap carries the carousel, so a swipe works and the page still
   functions with no JavaScript at all. */
.frames {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.frames::-webkit-scrollbar { display: none; }

.frame {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 260px;
  max-height: 78vh;
}
.frame img, .frame video {
  max-width: 100%;
  max-height: 78vh;
  display: block;
  object-fit: contain;
}

.frame .download {
  position: absolute;
  right: 10px; bottom: 10px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 11px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .22);
  background: rgba(0, 0, 0, .62);
  backdrop-filter: blur(6px);
  color: #fff;
  font-size: 12px;
  text-decoration: none;
}
.frame .download:hover { background: rgba(0, 0, 0, .82); border-color: rgba(255, 255, 255, .45); }
.frame .download .icon { width: 14px; height: 14px; }
/* A video's own controls sit along the bottom; keep clear of them. */
.frame:has(video) .download { bottom: 52px; }

.viewer .nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .2);
  background: rgba(0, 0, 0, .55);
  color: #fff;
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.viewer .nav:hover:not(:disabled) { background: rgba(0, 0, 0, .8); }
.viewer .nav:disabled { opacity: 0; pointer-events: none; }
.viewer .prev { left: 10px; }
.viewer .next { right: 10px; }

.viewer .counter {
  position: absolute; top: 10px; right: 10px;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(0, 0, 0, .62);
  color: #fff;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.viewer .dots {
  position: absolute; left: 0; right: 0; bottom: 10px;
  display: flex; justify-content: center; gap: 6px;
  pointer-events: none;
}
.viewer .dot {
  width: 6px; height: 6px;
  padding: 0;
  border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, .38);
  cursor: pointer;
  pointer-events: auto;
}
.viewer .dot.on { background: #fff; }

/* --------------------------------------------------------------- the foot */
/* The global `footer` rule centres its text; this one is a reading column. */
.post-foot { padding: 13px 15px 15px; text-align: left; font-size: inherit; }
/* The attached track, in the header where the kind badge used to be. The media
   itself says what kind of post this is; a word for it was redundant. */
.post-music {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: none;
  max-width: 46%;
  min-width: 0;
  padding: 5px 9px 5px 5px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel-2);
}
.post-music.is-playable { cursor: pointer; user-select: none; }
.post-music.is-playable:hover { border-color: #3a4150; }
.post-music.is-playable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.music-art { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; flex: none; }
/* Spun while playing, so it is obvious sound is coming from here. */
.post-music.is-playing .music-art { animation: music-spin 6s linear infinite; }
@keyframes music-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .post-music.is-playing .music-art { animation: none; }
}

.music-note { color: var(--accent); font-size: 15px; line-height: 1; flex: none; padding-left: 4px; }
.music-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.music-text strong {
  font-size: 12.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.music-text span {
  font-size: 11px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Two bars that animate only while playing - the whole state indicator. */
.music-state { display: flex; align-items: flex-end; gap: 2px; height: 12px; flex: none; }
.music-state::before, .music-state::after {
  content: ""; width: 2px; height: 4px; border-radius: 1px; background: var(--muted);
}
.post-music.is-playing .music-state::before,
.post-music.is-playing .music-state::after {
  background: var(--accent);
  animation: music-bar .6s ease-in-out infinite alternate;
}
.post-music.is-playing .music-state::after { animation-delay: .3s; }
@keyframes music-bar { from { height: 3px; } to { height: 12px; } }
@media (prefers-reduced-motion: reduce) {
  .post-music.is-playing .music-state::before,
  .post-music.is-playing .music-state::after { animation: none; height: 8px; }
}

@media (max-width: 480px) {
  /* Below this the three columns fight; the track drops to its own line. */
  .post-head { flex-wrap: wrap; }
  .post-music { max-width: 100%; width: 100%; order: 3; }
}

.post-caption {
  margin: 0 0 10px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.55;
  font-size: 14.5px;
}
.post-meta { margin: 0; font-size: 12.5px; color: var(--muted); }
.post-meta a { color: var(--muted); }
.post-meta a:hover { color: var(--accent); }
.empty { padding: 40px 16px; text-align: center; }

/* ------------------------------------------------------------ platform skins
 * An archive is easiest to check against the source when it reads like the
 * source, so a YouTube video gets YouTube's shape and a photo post keeps
 * Instagram's. The markup is identical either way - the same header, viewer
 * and foot - and a skin only reorders them and changes their weight, so the
 * two can no more drift apart than the page and the modal can.
 *
 * The default, everything above, is the Instagram shape: a card with the
 * account row on top and a square-ish picture under it. A new platform needs
 * a `.skin-<name>` block here only if its own page reads differently.
 *
 * Ordering is `order` on a flex column rather than a second template: one
 * DOM, one set of scripts, and nothing to keep in step. */
.postcard { display: flex; flex-direction: column; }

/* The handle in front of the caption, which is how Instagram prints one. */
.caption-user { font-weight: 600; text-decoration: none; color: var(--text); }
.caption-user:hover { color: var(--accent); }

/* A headline, for platforms whose caption is really a title. */
.post-title {
  margin: 0;
  padding: 14px 2px 0;
  font-size: 20px;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -.01em;
  overflow-wrap: anywhere;
}

/* The link to the source, sitting where a channel page keeps its subscribe
   button. Red because that is the button being stood in for. */
.post-visit {
  flex: none;
  align-self: center;
  padding: 8px 15px;
  border-radius: 999px;
  background: var(--youtube);
  color: #fff;
  font-size: 13.5px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.post-visit:hover { background: #d40029; color: #fff; }
.post-visit:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.post-subs { font-size: 12.5px; color: var(--muted); margin-top: 1px; }

/* ---------------------------------------------------------------- youtube */
/* Video first, then the title, then the channel, then the numbers - which is
   the order YouTube's own page puts them in, and the reverse of Instagram's. */
.skin-youtube {
  /* A 16:9 frame wants more width than a square photo does. */
  max-width: 900px;
  background: transparent;
  border: 0;
  border-radius: 0;
  /* The card no longer clips anything; the viewer rounds its own corners. */
  overflow: visible;
}
.skin-youtube .viewer {
  order: 1;
  border-radius: 12px;
  overflow: hidden;
  /* Every video here is 16:9, whether it is our copy or the fallback player,
     so the frame is the same height before the file has loaded. */
  aspect-ratio: 16 / 9;
}
.skin-youtube .frames { height: 100%; }
.skin-youtube .frame { height: 100%; max-height: none; }
.skin-youtube .frame img,
.skin-youtube .frame video { width: 100%; height: 100%; object-fit: contain; }

.skin-youtube .post-title { order: 2; }

.skin-youtube .post-head {
  order: 3;
  padding: 13px 2px 14px;
  /* The rule belongs to the card's edge, and this skin has no card. */
  border-bottom: 0;
  gap: 12px;
}
.skin-youtube .post-avatar { width: 40px; height: 40px; }
.skin-youtube .post-user { font-size: 15px; }

/* The numbers, in the grey block a video page keeps them in. */
.skin-youtube .post-foot {
  order: 4;
  padding: 12px 14px 13px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 12px;
}
.skin-youtube .post-engagement { margin: 0 0 4px; font-size: 14px; color: var(--text); }
.skin-youtube .post-engagement strong { font-weight: 700; }
.skin-youtube .empty { border-radius: 12px; background: var(--panel-2); }

@media (max-width: 480px) {
  /* The button drops below the channel row rather than squeezing the name. */
  .skin-youtube .post-head { flex-wrap: wrap; }
  .skin-youtube .post-visit { order: 4; width: 100%; text-align: center; }
}

/* ------------------------------------------------- the post page on a phone */
/* One screenful, no page scrolling: the media takes whatever the header and
   caption leave, and a long caption scrolls inside its own strip rather than
   pushing the picture off the top.
 *
 * Every rule is scoped to `body.public` on purpose. `.postcard` is the same
 * partial the admin grid drops into a lightbox, and a card told to be
 * `height: 100%` inside a modal collapses.
 *
 * Two conditions, because a phone held sideways is 812px WIDE and 375 tall.
 * A width-only rule misses that entirely - which is the case with the least
 * height to spare. The second arm is capped at 1024px so a desktop window
 * dragged short keeps the ordinary centred card. */
@media (max-width: 700px), (max-width: 1024px) and (max-height: 560px) {
  body.public {
    /* dvh is the viewport actually visible, so the card is not sized to a
       height the browser's address bar is covering. vh first as the fallback. */
    height: 100vh;
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
  }
  .public-main {
    padding: 0;
    align-items: stretch;
    /* Without min-height:0 a flex child refuses to shrink below its content,
       and the whole point here is that it must. */
    min-height: 0;
  }
  body.public .postcard {
    border: 0;
    border-radius: 0;
    max-width: none;
    height: 100%;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  /* A tighter header: it was taking 120px of an 812px screen, because the
     track wrapped onto its own line below the name. */
  body.public .post-head { flex: none; padding: 9px 12px; gap: 9px; }
  body.public .post-avatar { width: 32px; height: 32px; }
  body.public .post-user { font-size: 14px; }
  body.public .post-name { font-size: 11.5px; }
  body.public .post-music { padding: 5px 8px; gap: 7px; max-width: 46%; }
  body.public .post-music .music-art,
  body.public .post-music .music-note { width: 26px; height: 26px; }
  body.public .music-text strong,
  body.public .music-text span { font-size: 11px; }

  /* The media gets everything left over. */
  body.public .viewer { flex: 1; min-height: 0; }
  body.public .frames {
    height: 100%;
    /* Declaring the axis lets the browser commit to a horizontal swipe on the
       first touch instead of waiting to see whether the page will scroll. Safe
       only here, where the page has no vertical scrolling to give up. */
    touch-action: pan-x pinch-zoom;
    /* A fast flick should advance one slide, not skate past three. */
    scroll-snap-stop: always;
  }
  body.public .frame { height: 100%; min-height: 0; max-height: none; }
  body.public .frame img,
  body.public .frame video { max-height: 100%; height: 100%; object-fit: contain; }

  /* Capped, and scrolls within itself. `flex-shrink` lets it give way to the
     picture first when the screen is short. */
  body.public .post-foot {
    flex: 0 1 auto;
    max-height: 26%;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding: 10px 14px 12px;
  }
  body.public .post-caption { max-height: none; }

  .public-foot { flex: none; padding: 8px 14px; }
  .public-foot .brand { font-size: 13px; }
  .public-foot .mark { width: 16px; height: 16px; }
}

/* A phone held sideways has almost no height. Give the caption less still, so
   the picture is not reduced to a letterbox. */
@media (max-width: 900px) and (max-height: 500px) {
  body.public .post-foot { max-height: 24%; }
  body.public .post-head { padding: 6px 12px; }
  body.public .post-avatar { width: 28px; height: 28px; }
  body.public .public-foot { display: none; }
}

/* ------------------------------------------------------------- admin: users */
.app-nav nav a.on { color: var(--text); border-bottom: 2px solid var(--accent); padding-bottom: 2px; }
table.users th, table.users td { padding: 10px 8px; vertical-align: middle; }
table.users td.actions { text-align: right; white-space: nowrap; }
table.users td.actions .inline-form { display: inline-flex; margin-left: 8px; }
table.users select { height: 30px; padding: 0 8px; font-size: 12.5px; }
/* A blocked row is dimmed, but never so far that its own controls are hard to
   read - this is the row you most need to act on. */
table.users tr.blocked td:not(.actions) { opacity: .6; }
table.users .who-cell { min-width: 190px; }
table.users .who-line { display: flex; align-items: center; gap: 7px; }
table.users .acct { display: inline; }
table.users .hint.small { display: block; font-size: 11.5px; }
table.users td.nowrap { white-space: nowrap; }
.live {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--ok); margin-right: 5px; vertical-align: 1px;
}

/* --------------------------------------------------------------- kill switch */
/* Loud on purpose: if the service is stopped, that has to be the first thing
   seen on the page, not a badge among others. */
.panel.killed {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  border-color: #4a2523;
  background: #241214;
}
.panel.killed strong { color: #f0a0a0; }
.panel.killed .hint { margin: 4px 0 0; }

/* --------------------------------------------------------------- analytics */
/* Exactly two, not auto-fit: with two children auto-fit resolves to three
   columns on a wide screen and leaves an empty one. */
.two-up { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
@media (max-width: 760px) { .two-up { grid-template-columns: 1fr; } }

.chart-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.chart-head h3 { margin: 0 0 2px; font-size: 15px; font-weight: 600; }

/* Bars are sized against the busiest day so the shape reads at any scale. */
.chart {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 90px;
  margin: 10px 0 4px;
  padding-top: 2px;
}
.chart .bar {
  flex: 1 1 0;
  min-width: 3px;
  min-height: 2px;
  border-radius: 2px 2px 0 0;
  background: linear-gradient(180deg, var(--accent), var(--accent-2));
}
/* An empty day still draws a tick, so gaps are visible rather than invisible. */
.chart .bar.zero { background: var(--line); }
.chart-axis {
  display: flex; justify-content: space-between;
  font-size: 11px; color: var(--muted);
}

/* ===================================================== account: profile === */
.profile-head { display: flex; gap: 32px; align-items: flex-start; margin-bottom: 26px; }
.profile-pic {
  width: 130px; height: 130px; flex: none;
  border-radius: 50%; object-fit: cover;
  background: linear-gradient(135deg, #d29922, #e1306c, #8134af);
}
.profile-pic.missing { display: inline-block; }
.profile-meta { min-width: 0; flex: 1; }
.profile-line { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.profile-line h1 { margin: 0; font-size: 20px; font-weight: 400; }

.profile-counts {
  display: flex; gap: 28px; flex-wrap: wrap;
  list-style: none; margin: 16px 0 14px; padding: 0;
  font-size: 14px; color: var(--muted);
}
.profile-counts strong { color: var(--text); font-weight: 600; }
.profile-name { margin: 0; font-weight: 600; font-size: 14px; }
.profile-bio { margin: 2px 0 0; font-size: 14px; line-height: 1.5; white-space: pre-wrap; }
.profile-status { margin-top: 12px; }

.tabs.profile-tabs {
  display: flex; gap: 0; justify-content: center; flex-wrap: wrap;
  border-top: 1px solid var(--line); margin-bottom: 20px;
}
/* Every pill property from the base `.tab` has to be undone here, or the
   rounded outline and the accent fill show through the underline style. */
.profile-tabs .tab {
  padding: 15px 22px;
  border: 0;
  border-top: 1px solid transparent;
  border-radius: 0;
  background: none;
  margin-top: -1px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
}
.profile-tabs .tab:hover { color: var(--text); }
.profile-tabs .tab.active {
  background: none;
  border-color: transparent;
  border-top-color: var(--text);
  color: var(--text);
}
.profile-tabs .tab span {
  margin-left: 6px;
  color: var(--muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
.profile-tabs .tab.active span { color: var(--text); }

/* Square tiles, three across, the way the source lays them out. */
/* Capped at the width the source uses, so tiles stay a believable size on a
   wide screen instead of stretching past 400px each. */
.profile-head, .profile-tabs, .ig-grid, .pager {
  /* `width: 100%` matters: the page shell is a column flex container, where
     auto margins make a child shrink to its content instead of filling. */
  width: 100%;
  max-width: 935px;
  margin-left: auto;
  margin-right: auto;
}
.ig-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; }
.ig-tile {
  position: relative;
  aspect-ratio: 1;
  padding: 0; border: 0;
  background: var(--panel-2);
  cursor: pointer;
  overflow: hidden;
}
.ig-tile img, .ig-tile video { width: 100%; height: 100%; object-fit: cover; display: block; }
.ig-tile:hover img, .ig-tile:hover video { opacity: .82; }
.ig-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ig-tile.nothumb::after {
  content: "no preview";
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; color: var(--muted);
}
.ig-badges { position: absolute; top: 8px; right: 8px; display: flex; gap: 5px; }
.ig-badge {
  color: #fff; font-size: 13px; line-height: 1;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .6);
}
@media (min-width: 900px) { .ig-grid { gap: 6px; } }

/* A video grid is rows of 16:9 stills, not a three-wide square mosaic: a
   square crop of a widescreen thumbnail throws away a third of the frame,
   which is most of what makes one thumbnail tell itself from another. */
.ig-grid.skin-youtube {
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 14px;
}
.ig-grid.skin-youtube .ig-tile { aspect-ratio: 16 / 9; border-radius: 10px; }

details.ops { margin-top: 28px; }
details.ops summary { cursor: pointer; font-weight: 600; }
details.ops > * { margin-top: 14px; }
details.ops h3 { margin-bottom: 0; font-size: 14px; }

/* ---------------------------------------------------------------- lightbox */
.lightbox {
  position: fixed; inset: 0; z-index: 50;
  display: flex; align-items: center; justify-content: center;
  padding: 28px 16px;
  background: rgba(6, 8, 12, .8);
  backdrop-filter: blur(7px);
  overflow-y: auto;
}
.lightbox[hidden] { display: none; }
.lightbox-body { width: 100%; max-width: 560px; margin: auto; }
/* A 16:9 frame in a column sized for a square photo is a third the height it
   should be. The fragment is fetched after the modal is built, so the width
   has to follow what came back rather than being decided when it opened. */
.lightbox-body:has(.skin-youtube) { max-width: 900px; }
.lightbox-close {
  position: fixed; top: 14px; right: 20px;
  width: 38px; height: 38px;
  border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, .1);
  color: #fff; font-size: 24px; line-height: 1;
  cursor: pointer;
}
.lightbox-close:hover { background: rgba(255, 255, 255, .2); }
/* Stops the page scrolling behind the overlay. */
body.modal-open { overflow: hidden; }

/* Engagement, shown on the card and over the tile on hover. */
.post-engagement { margin: 0 0 8px; font-size: 13.5px; color: var(--muted); }
.post-engagement strong { color: var(--text); font-weight: 600; }

.ig-stats {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center; gap: 18px;
  background: rgba(0, 0, 0, .45);
  color: #fff; font-size: 14px; font-weight: 600;
  opacity: 0; transition: opacity .12s;
}
.ig-tile:hover .ig-stats, .ig-tile:focus-visible .ig-stats { opacity: 1; }

/* --- profile history ---------------------------------------------------- */
/* Reuses the post lightbox, so only the card's own layout is new. */
.history-body { max-width: 640px; padding: 22px 24px 26px; }
.history-body h2 { margin: 0 0 4px; }
.history { list-style: none; margin: 16px 0 0; padding: 0; }
.history li {
  padding: 12px 0;
  border-top: 1px solid var(--line);
}
.history-when {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--muted);
}
.history-values { margin-top: 6px; display: grid; gap: 4px; font-size: 14px; }
/* The old value is struck through rather than hidden: what a bio said before
   is the point of keeping the history at all. */
.history-values del { color: var(--muted); text-decoration-color: var(--muted); }
.history-values ins { text-decoration: none; }
.history-pics { margin-top: 8px; display: flex; gap: 12px; }
.history-pics figure { margin: 0; text-align: center; }
.history-pics img {
  width: 96px; height: 96px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--line);
}
.history-pics figcaption { font-size: 11px; color: var(--muted); margin-top: 4px; }
.btn .count {
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 8px;
  background: var(--panel-2);
  font-size: 11px;
}
/* The updates column carries either a badge or a button; neither should
   stretch the row. */
.subs .profile-cell { white-space: nowrap; }

/* --- follower chart ----------------------------------------------------- */
.chart-body { max-width: 720px; padding: 22px 24px 24px; }
.chart-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 12px;
}
.chart-head h2 { margin: 0; }
.chart-metrics { display: flex; gap: 4px; }
/* `.tab` was written for anchors, so a button wearing it keeps the browser's
   own background, font and cursor unless each is undone here. */
.chart-metrics .tab {
  background: none;
  font: inherit;
  font-size: 12.5px;
  padding: 4px 10px;
  cursor: pointer;
}
.chart-metrics .tab.active { background: var(--accent); color: #fff; }
.chart-ranges .btn { font-size: 12.5px; padding: 4px 10px; }
.chart-ranges { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 10px; }
#chart { min-height: 220px; }
.chart-svg { width: 100%; height: auto; display: block; overflow: visible; }
.chart-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.chart-area { fill: rgba(225, 48, 108, .14); stroke: none; }
.chart-svg .grid { stroke: var(--line); stroke-width: 1; }
.chart-svg .ytick, .chart-svg .xtick {
  fill: var(--muted);
  font-size: 11px;
  font-family: inherit;
}
.chart-svg .ytick { text-anchor: end; }
.chart-svg .xtick.end { text-anchor: end; }
.chart-empty { padding: 80px 0; text-align: center; }
#chart-note { margin: 10px 0 0; }

/* The counts in the profile header become buttons when there is a history to
   show. They must not look like buttons - they are still the profile line. */
.statlink {
  background: none; border: 0; padding: 0; margin: 0;
  color: inherit; font: inherit; cursor: pointer;
  border-bottom: 1px dashed transparent;
}
.statlink:hover { border-bottom-color: var(--muted); }
.statlink:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* --- an embedded platform player ---------------------------------------- */
/* 16:9, and it fills the viewer the same way a frame would, so the phone
   layout that gives the media whatever is left over still works. */
.viewer.player { aspect-ratio: 16 / 9; }
.viewer.player iframe { width: 100%; height: 100%; border: 0; display: block; }
/* This once dropped the ratio on `body.public` so the phone layout could flex
   the player to fill whatever was left. The only platform that embeds one now
   has a skin that keeps its shape and lets the page scroll instead, and the
   rule was not scoped to a phone - so it was flattening the desktop player to
   the 300x150 an iframe is before it loads. */

/* A 16:9 video does not want a whole portrait screen the way a picture does,
   and YouTube's own page scrolls under a player that keeps its shape. So this
   skin opts out of the one-screenful layout above: the viewer holds its
   aspect ratio and everything below it scrolls. */
@media (max-width: 700px), (max-width: 1024px) and (max-height: 560px) {
  body.public .skin-youtube {
    height: auto;
    min-height: 100%;
    overflow-y: auto;
    padding: 0 12px 14px;
  }
  body.public .skin-youtube .viewer {
    flex: none;
    /* Full-bleed, the way a phone player is, against the card's own padding. */
    margin: 0 -12px;
    border-radius: 0;
  }
  body.public .skin-youtube .frames { height: auto; }
  body.public .skin-youtube .frame img,
  body.public .skin-youtube .frame video { height: 100%; }
  body.public .skin-youtube .post-title { padding: 12px 0 0; font-size: 17px; }
  body.public .skin-youtube .post-head { padding: 11px 0 12px; }
  body.public .skin-youtube .post-foot {
    flex: none;
    max-height: none;
    overflow: visible;
    padding: 11px 13px 12px;
  }
}

/* A phone held sideways is all width and no height: the player would fill the
   screen and the title would never be reached. Let it scroll as it does in
   portrait, and keep the video at a size the text can share the screen with. */
@media (max-width: 900px) and (max-height: 500px) {
  body.public .skin-youtube .viewer { max-height: 62vh; }
}

/* ------------------------------------------------------- the alert preview
 * What the personalization settings actually produce, on Discord's own dark
 * surface rather than ours - a colour picked against the wrong background is
 * the whole reason a preview is worth having. Its labels and colours come
 * from the bot, so the two cannot disagree about what "auto" means. */
.alert-preview { margin-top: 22px; border-top: 1px solid var(--line); padding-top: 16px; }
.preview-label {
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Discord's own greys, not ours: the point is how it looks over there. */
.dc-message {
  display: flex;
  gap: 12px;
  padding: 14px 16px;
  border-radius: 10px;
  background: #313338;
  color: #dbdee1;
  font-size: 14px;
  line-height: 1.45;
  max-width: 560px;
}
.dc-avatar {
  flex: none;
  width: 38px; height: 38px;
  border-radius: 50%;
  overflow: hidden;
  display: grid; place-items: center;
  /* The disc the mark is drawn on, and the same colour the SVG uses for the
     ring that separates the dot from the bell. */
  background: #1a1c22;
}
.dc-avatar .bot-avatar { width: 100%; height: 100%; display: block; }
.dc-body { min-width: 0; flex: 1; }
.dc-head { margin: 0 0 3px; display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.dc-name { color: #f2f3f5; font-weight: 600; }
.dc-tag {
  background: #5865f2; color: #fff;
  font-size: 10px; font-weight: 600;
  padding: 1px 5px; border-radius: 4px;
  text-transform: uppercase; letter-spacing: .02em;
}
.dc-when { color: #949ba4; font-size: 11.5px; }
.dc-content { margin: 0 0 6px; overflow-wrap: anywhere; white-space: pre-wrap; }

/* The embed, with its accent stripe down the left. */
.dc-embed {
  --pv-accent: #e1306c;
  border-radius: 4px;
  background: #2b2d31;
  border-left: 4px solid var(--pv-accent);
  max-width: 440px;
}
.dc-embed-inner { padding: 11px 14px 13px; }
.dc-embed-title { margin: 0; color: #00a8fc; font-weight: 600; font-size: 14.5px; }
.dc-embed-desc {
  margin: 6px 0 0;
  font-size: 13.5px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.dc-fields { display: flex; gap: 26px; margin-top: 9px; }
.dc-fields span { display: flex; flex-direction: column; font-size: 13.5px; }
.dc-fields b { font-size: 12.5px; color: #f2f3f5; }
.dc-embed-art {
  margin-top: 10px;
  height: 116px;
  border-radius: 4px;
  /* Stands in for the uploaded file without pretending to be a real photo. */
  background:
    repeating-linear-gradient(45deg, #1f2124 0 8px, #26282c 8px 16px);
  border: 1px solid #3a3d43;
}
.dc-embed-foot { margin: 9px 0 0; font-size: 11.5px; color: #949ba4; }

/* A platform Discord unfurls itself: its card, not ours. */
.dc-unfurl {
  margin-top: 2px;
  padding: 10px 12px 12px;
  border-radius: 4px;
  background: #2b2d31;
  border-left: 4px solid #4f545c;
  max-width: 440px;
}
.dc-unfurl-site { display: block; font-size: 11.5px; color: #949ba4; }
.dc-unfurl-title {
  display: block; margin-top: 2px;
  color: #00a8fc; font-weight: 600; font-size: 14px;
}
.dc-unfurl-art {
  margin-top: 9px;
  height: 132px;
  border-radius: 4px;
  display: grid; place-items: center;
  color: #f2f3f5; font-size: 30px;
  background: repeating-linear-gradient(45deg, #1f2124 0 8px, #26282c 8px 16px);
  border: 1px solid #3a3d43;
}
.preview-note { margin: 10px 0 0; max-width: 560px; }

@media (max-width: 620px) {
  .dc-message { padding: 11px 12px; gap: 9px; }
  .dc-avatar { width: 32px; height: 32px; }
}

/* --------------------------------------------------------------- credit
 * What is left on the metered account. Four readings side by side, because
 * "$18 left" and "9 days left" answer different questions - one is what you
 * have, the other is when you have to act. */
.credit { margin-top: 18px; border-top: 1px solid var(--line); padding-top: 16px; }
.credit-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px;
  margin-bottom: 14px;
}
.credit-figures .stat { gap: 2px; }
.credit-figures .num { font-family: var(--font-display); font-size: 25px; font-weight: 650; }
/* The unit rides along at text size so the figure keeps the eye. */
.credit-figures .unit { font-size: 13px; font-weight: 500; color: var(--muted); margin-left: 3px; }
.credit .flash { margin: 12px 0; }
.credit .inline-form { margin-top: 14px; align-items: center; }
.credit .inline-form input { width: 130px; }

/* ==================================================== the operator skin ===
 * Everything behind /admin, scoped to `body.ops` so the guild dashboard and
 * the public archive pages are untouched. They are a product; this is a tool,
 * and the two should not have to look the same.
 *
 * Almost all of it is variable overrides. The old palette was #0f1115 - a
 * blue-slate near-black that is the default of every generated dashboard and
 * recognisable as one before you have read anything on the page. This is a
 * true neutral with no cast at all, which leaves status as the only colour.
 */
body.ops {
  --bg:      #0b0b0c;
  --panel:   #131315;
  --panel-2: #191a1c;
  --line:    #232326;
  --text:    #e9e9eb;
  --muted:   #8a8b91;
  --ok:      #4ec77f;
  --warn:    #e0a63a;
  --danger:  #e5544b;
  /* No brand accent here. The gradient appears once, in the mark, and every
     other colour on the page means a state. */
  --accent:  #e9e9eb;

  --faint:   #6b6c72;
  --line-2:  #2e2f33;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;

  font-size: 13px;
}

/* -------------------------------------------------------------- chrome */
body.ops .app-nav { gap: 18px; padding: 0 16px; height: 44px; }
body.ops .app-nav .brand { font-size: 14px; gap: 8px; font-weight: 600; }
body.ops .app-nav nav { gap: 2px; }
body.ops .app-nav nav a { padding: 5px 10px; border-radius: 4px; font-size: 12.5px; }
body.ops .app-nav nav a:hover { background: var(--panel-2); color: var(--text); }
/* The underline treatment belonged to a page with more air than this one. */
body.ops .app-nav nav a.on {
  background: var(--panel-2); color: var(--text);
  border-bottom: 0; padding-bottom: 5px;
}
body.ops .app-user .who { font-size: 12.5px; }
body.ops .app-user .avatar { width: 22px; height: 22px; }

body.ops .app-main { max-width: 1500px; margin: 0; padding: 16px; gap: 18px; }
body.ops footer {
  padding: 22px 16px 30px; font-size: 11.5px;
  color: var(--faint); text-align: left;
}

/* --------------------------------------------------------- the section */
/* No card. A ruled label, then the content. Every section used to be the same
   bordered box, which is most of why nothing on the page had any weight. */
body.ops .panel { background: none; border: 0; border-radius: 0; padding: 0; }
body.ops .panel > h2 {
  margin: 0 0 9px; font-size: 11px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--faint);
  padding-bottom: 7px; border-bottom: 1px solid var(--line);
}
/* The few that really are notices keep being boxes. */
body.ops .flash { border-radius: 5px; padding: 10px 13px; font-size: 12.5px; }
body.ops .panel.killed {
  background: #1b1113; border: 1px solid #3a1f22;
  border-radius: 5px; padding: 12px 14px;
}

/* --------------------------------------------------------- the figures */
/* One thing is big; the rest are small. Four equal 26px numbers all shouting
   at once is why nothing landed before. */
body.ops .stats {
  display: flex; gap: 34px; align-items: flex-end; flex-wrap: wrap;
  background: none; border: 0; padding: 0;
}
body.ops .stat { gap: 5px; }
body.ops .stat .num {
  font-family: var(--mono); font-size: 22px; font-weight: 500;
  letter-spacing: -.02em; color: var(--muted);
}
body.ops .stats .stat:first-child .num { font-size: 44px; color: var(--text); line-height: 1; }
body.ops .stat .lab { font-size: 9.5px; letter-spacing: .1em; color: var(--faint); }

/* ---------------------------------------------------------- the ledger */
body.ops table { font-size: 12.5px; }
body.ops th {
  padding: 0 10px 7px; font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--faint); border-bottom: 0;
}
body.ops td { padding: 6px 10px; border-top: 1px solid var(--line); border-bottom: 0; }
body.ops tbody tr:hover td { background: var(--panel); }
body.ops td.num, body.ops th.num { text-align: right; }
body.ops .figure {
  font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right;
}
body.ops .detail, body.ops td.mono { color: var(--muted); }

/* Pills become tags: a hairline outline, not a filled lozenge. */
body.ops .pill {
  font-family: var(--mono); font-size: 10.5px; padding: 1px 5px; border-radius: 3px;
  background: none; border: 1px solid var(--line-2); color: var(--muted);
}
body.ops .pill.ok { color: var(--ok); border-color: rgba(78, 199, 127, .3); background: none; }
body.ops .pill.warn { color: var(--warn); border-color: rgba(224, 166, 58, .3); background: none; }
body.ops .pill.busy { color: var(--text); border-color: var(--line-2); background: none; }

/* --------------------------------------------------------- the controls */
body.ops .btn {
  font-size: 12px; padding: 4px 10px; border-radius: 4px;
  background: var(--panel-2); border-color: var(--line-2);
}
body.ops .btn.primary {
  background: var(--text); color: #0b0b0c; border-color: var(--text); font-weight: 550;
}
body.ops .btn.danger { background: none; color: #f0a6a0; border-color: rgba(229, 84, 75, .32); }
body.ops input, body.ops select { border-radius: 4px; padding: 5px 8px; font-size: 12px; }
/* Not checkboxes: a monospace font on one does nothing, and the rule reads as
   though it might. */
body.ops input:not([type="checkbox"]) { font-family: var(--mono); }
body.ops .kind-box span { font-size: 11px; }
body.ops .row-form { gap: 8px; align-items: center; }

/* ------------------------------------------------------------ the strip */
/* Live state on one hairline rather than in a panel: it is glanced at, not
   read, and it should cost the page as little height as that implies. */
.ops-strip {
  display: flex; margin: -16px -16px 0;
  border-bottom: 1px solid var(--line); background: #0e0e10; overflow-x: auto;
}
.ops-strip .cell {
  padding: 9px 16px; border-right: 1px solid var(--line); white-space: nowrap;
}
.ops-strip .cell:last-child { border-right: 0; }
.ops-strip .k {
  display: block; font-size: 9.5px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--faint);
}
.ops-strip .v {
  display: block; margin-top: 3px; font-size: 12.5px; color: var(--text);
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}
.ops-strip form { display: inline; margin-left: 8px; }

.dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; margin-right: 6px; }
.dot.ok { background: var(--ok); box-shadow: 0 0 0 3px rgba(78, 199, 127, .14); }
.dot.warn { background: var(--warn); box-shadow: 0 0 0 3px rgba(224, 166, 58, .14); }
.dot.bad { background: var(--danger); box-shadow: 0 0 0 3px rgba(229, 84, 75, .14); }
.dot.idle { background: var(--faint); }

/* -------------------------------------------------------------- the log */
/* Runs read as a tail, not as a six-column table. */
.ops-log { font-family: var(--mono); font-size: 11.5px; line-height: 1.9; }
.ops-log > div { display: flex; gap: 12px; align-items: baseline; }
.ops-log .t { color: var(--faint); flex: none; }
.ops-log .a { color: var(--muted); flex: none; min-width: 190px; }
.ops-log .m { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ops-log .m.quiet { color: var(--faint); }
.ops-log .m.bad { color: #f0a6a0; }

/* A glance at what is arriving, not a gallery. */
body.ops .grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 6px; }
body.ops .tile { border-radius: 5px; }
body.ops .tile-cap { font-size: 10px; padding: 14px 6px 4px; }

@media (max-width: 700px) {
  body.ops .app-main { padding: 12px; }
  .ops-strip { margin: -12px -12px 0; }
  body.ops .stats { gap: 20px; }
  body.ops .stats .stat:first-child .num { font-size: 34px; }
  .ops-log .a { min-width: 0; }
}

/* The figures are text on the page, not six more cards. `.stat` carries its
   own panel background from the default skin, so `.stats` losing its box was
   not enough - each child had one too. */
body.ops .stat {
  background: none; border: 0; border-radius: 0; padding: 0;
}
/* The add row is a row. `.row-form input { flex: 1 1 200px }` made the name
   field eat the line and pushed the type boxes onto their own, which is how a
   one-line form became four. */
body.ops #admin-add { flex-wrap: nowrap; align-items: center; }
/* `>` throughout. A bare `input` here also matched every checkbox inside the
   type boxes - this codebase puts checkboxes inside form rows, so any rule
   sizing "the input" in a row has to say which one it means. Caught three
   times in this file before the selector was tightened. */
body.ops #admin-add > input { flex: 0 0 210px; width: 210px; }
body.ops #admin-add .kind-boxes { margin-top: 0; gap: 6px; }
body.ops #admin-add .kind-box { padding: 3px 8px; }
@media (max-width: 900px) {
  body.ops #admin-add { flex-wrap: wrap; }
}

/* Only the operator page's headline gets the big figure. Elsewhere `.stats` is
   a row of equals and should stay one - the rule used to key off "first child
   of any .stats", which made "0 servers" the largest thing on Analytics. */
body.ops .stats .stat:first-child .num { font-size: 22px; color: var(--muted); }
body.ops .stats.headline .stat:first-child .num {
  font-size: 44px; color: var(--text); line-height: 1;
}

/* Charts in neutral. The gradient is the product's identity and means nothing
   as a bar fill; here the only colour on the page is a state. */
body.ops .chart .bar { background: #34353a; border-radius: 1px 1px 0 0; }
body.ops .chart .bar:last-child { background: #4a4c52; }
body.ops .chart .bar.zero { background: var(--line); }
body.ops .chart { gap: 2px; height: 62px; }
body.ops .chart-axis { font-family: var(--mono); font-size: 10px; color: var(--faint); }
body.ops .chart-head h3 {
  font-size: 11px; font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--faint);
}
body.ops .chart-head { padding-bottom: 7px; border-bottom: 1px solid var(--line); }
body.ops .credit-figures .num { font-family: var(--mono); font-weight: 500; }

/* ===================================================== the product skin ===
 * The guild-facing pages, scoped to `body.product`. `base.html` carries `ops`
 * instead: a Discord mod who visits once a month and somebody running the
 * service want different things from a screen, and making both look the same
 * is what made the whole interface read as generated.
 *
 * Warm dark, with a faint plum cast that ties to the brand gradient. Not the
 * operator page's near-black, and not the old blue-slate either.
 */
body.product {
  --bg:      #16141a;
  --panel:   #1e1b24;
  --panel-2: #26222e;
  --line:    #2f2939;
  --text:    #f1eef4;
  --muted:   #a9a2b6;
  --faint:   #7a7288;
  --line-2:  #3d3549;
}

body.product .app-nav { padding: 15px 24px; background: transparent; border-bottom-color: var(--line); }
body.product .app-main { max-width: 1320px; padding: 26px 24px 70px; gap: 24px; }

/* ------------------------------------------------------------ feed cards */
.feed-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 15px;
}
.feed-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 15px;
  padding: 15px 16px 13px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}
.feed-card:hover { border-color: var(--line-2); }

.fc-who { display: flex; align-items: center; gap: 11px; }
.fc-av {
  width: 38px; height: 38px; border-radius: 50%; flex: none; object-fit: cover;
  background: linear-gradient(135deg, #d29922, #e1306c, #8134af);
}
.fc-name { display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
.fc-name b { font-size: 15px; font-weight: 600; }
.fc-meta { font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
.fc-remove {
  margin-left: auto; flex: none;
  width: 26px; height: 26px; border-radius: 7px;
  background: none; border: 0; color: var(--faint);
  font-size: 18px; line-height: 1; cursor: pointer;
}
.fc-remove:hover { background: var(--panel-2); color: #f0a6a0; }

/* The strip is why this is a card and not a row: it shows what the feed
   actually does, which no column of numbers can. */
.fc-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.fc-strip .shot {
  aspect-ratio: 1; border-radius: 7px; overflow: hidden;
  background: var(--panel-2); display: block;
}
.fc-strip .shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.fc-strip .shot.blank img { display: none; }
.fc-strip .shot.more {
  display: grid; place-items: center;
  color: var(--faint); font-size: 12px; font-variant-numeric: tabular-nums;
}
/* Nothing archived yet: one wide tile saying so beats three empty squares. */
.fc-strip .shot.more.wide { grid-column: 1 / -1; aspect-ratio: auto; padding: 14px 0; }

/* ------------------------------------------------------------ the sentence */
/* Each feed reads as a line of English with the editable parts inline. */
.fc-says {
  margin: 0; font-size: 13.5px; line-height: 2.1; color: var(--faint);
}
.fc-says select {
  font-size: 12.5px; padding: 3px 7px; border-radius: 7px;
  background: var(--panel-2); border: 1px solid var(--line);
  color: var(--text); max-width: 100%;
}
.fc-says select:hover { border-color: var(--line-2); }
.fc-pick { display: inline-flex; vertical-align: middle; }
.fc-says .kind-boxes { margin: 0; gap: 4px; }
.fc-says .kind-box {
  padding: 2px 8px; border-radius: 7px; font-size: 12px; gap: 5px;
}

.fc-foot {
  display: flex; align-items: center; gap: 9px;
  padding-top: 11px; border-top: 1px solid var(--line);
}
.badge {
  font-size: 11px; padding: 2px 8px; border-radius: 20px;
  border: 1px solid var(--line-2); color: var(--muted);
}
.badge.ok { color: #56c98a; border-color: rgba(86, 201, 138, .32); }
.badge.quiet { color: var(--faint); border-color: transparent; padding-left: 0; }
body.product .btn.tiny {
  font-size: 11.5px; padding: 3px 9px; border-radius: 7px;
  background: transparent; border-color: var(--line-2); color: var(--muted);
}
body.product .btn.tiny:hover { color: var(--text); border-color: var(--muted); }

/* Adding a feed belongs where the feeds are. */
.feed-card.add {
  border-style: dashed; align-items: center; justify-content: center;
  text-align: center; gap: 8px; min-height: 200px;
  color: var(--faint); text-decoration: none;
}
.feed-card.add:hover { border-color: var(--muted); color: var(--muted); }
.feed-card.add .plus {
  width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center;
  border: 1px solid var(--line-2); font-size: 18px;
}
.feed-card.add b { color: var(--text); font-weight: 550; font-size: 14px; }
.feed-card.add span:last-child { font-size: 12.5px; }

/* A section that is a heading and its content, with no box of its own - the
   cards below already are boxes, and a box of boxes reads as neither. */
body.product .panel.bare { background: none; border: 0; padding: 0; }
body.product .panel.bare > h2 {
  display: flex; align-items: baseline; gap: 11px;
  font-size: 17px; font-weight: 650; margin: 0 0 14px;
}
body.product .panel.bare > h2 em {
  font-style: normal; font-weight: 400; font-size: 13px; color: var(--faint);
}

/* ------------------------------------------------------------ the chrome */
body.product .side-tabs { flex-direction: row; flex-wrap: wrap; position: static; gap: 7px; }
body.product .tab-layout { grid-template-columns: 1fr; gap: 22px; }
body.product .side-tab {
  padding: 8px 15px 8px 12px; border-radius: 11px; gap: 9px;
  border: 1px solid transparent;
}
body.product .side-tab .side-text { flex-direction: row; align-items: baseline; gap: 8px; }
body.product .side-tab strong { font-size: 13.5px; font-weight: 500; }
body.product .side-tab.active strong { color: var(--text); }
body.product .side-mark { width: 22px; height: 22px; border-radius: 6px; background: none; }
body.product .side-mark svg { width: 15px; height: 15px; }
body.product .side-tab.active .side-mark { background: none; }
body.product .side-tab.active .side-mark.instagram { color: #e1306c; }
body.product .side-tab.active .side-mark.youtube { color: #ff0033; }
body.product .side-tab.active .side-mark.logs { color: var(--text); }

body.product .page-head h1 { font-size: 24px; font-weight: 680; }
body.product .settings-form, body.product .add-form { gap: 15px; }
body.product input, body.product select { border-radius: 9px; }
body.product .btn { border-radius: 9px; }
body.product .btn.primary, body.product .btn.save {
  background: var(--text); color: #16141a; border-color: var(--text); font-weight: 600;
}

@media (max-width: 700px) {
  .feed-cards { grid-template-columns: 1fr; }
  body.product .app-main { padding: 18px 14px 60px; }
}

/* A tab per content type inside a platform's alert settings. A story is gone
   in a day and a premiere is an appointment; they do not want the same ping,
   colour or caption limit. The dot marks a type already set differently, so
   nobody has to click through every tab to find out. */
.kind-tabs { display: flex; gap: 4px; flex-wrap: wrap; margin: 0 0 14px; }
.kind-tab {
  padding: 5px 12px; border-radius: 8px;
  font-size: 12.5px; color: var(--muted);
  border: 1px solid transparent; text-decoration: none;
}
.kind-tab:hover { background: var(--panel-2); color: var(--text); }
.kind-tab.active {
  background: var(--panel-2); border-color: var(--line-2); color: var(--text);
}
.kind-tab .edited { color: var(--warn); margin-left: 3px; font-size: 15px; line-height: 0; }

/* A label and its control per row. The sentence form read well only while the
   line fitted: once a channel name or a role wrapped, the commas and the full
   stop orphaned onto their own lines and looked like a mistake. */
.fc-rows {
  display: grid; grid-template-columns: auto 1fr;
  gap: 7px 12px; margin: 0; align-items: center;
}
.fc-rows dt {
  font-size: 11px; font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}
.fc-rows dd { margin: 0; min-width: 0; }
.fc-rows dd select { width: 100%; font-size: 12.5px; padding: 5px 9px; }
.fc-rows .kind-boxes { margin: 0; gap: 4px; }
.fc-rows .kind-box { padding: 2px 8px; border-radius: 7px; font-size: 12px; gap: 5px; }

/* Where an account's profile updates go: asked once per account, not once per
   tile. A per-card button put the same question on every feed of the same
   account and let each answer it differently. */
.updates-note { margin: -6px 0 14px; max-width: 62ch; }
.updates { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 9px; }
.updates-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 13px; border-radius: 11px;
  background: var(--panel); border: 1px solid var(--line);
}
.updates-who { font-weight: 550; font-size: 13.5px; flex: none; }
.updates-row select { flex: 1; min-width: 0; font-size: 12.5px; padding: 5px 9px; }
.updates-row select:disabled { opacity: .75; cursor: default; }
.updates-row .only { flex: none; font-size: 11.5px; color: var(--faint); }

/* The card ends with its last row: no footer, no rule, no trailing space. The
   old badge repeated what the profile-updates panel says, and its footer left
   an empty strip on every card that did not carry one - so three feeds of the
   same account looked like one of them had something extra. */
.feed-card { padding-bottom: 15px; }

/* A video we hold no still for. Left as a bare square it reads as a broken
   image; a play mark says the archive has the file and not a picture of it. */
.fc-strip .shot.blank { display: grid; place-items: center; }
.fc-strip .shot.blank::after {
  content: ""; width: 0; height: 0;
  border-left: 9px solid var(--faint);
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
  margin-left: 3px;
}
.fc-strip .shot.blank.video::after { border-left-color: var(--muted); }

/* ---------------------------------------------------- operator: manual push
   Status is the only colour, as everywhere in this skin: the chips and the
   preview are neutral, and only "will post" / "already posted" carry a state. */
.push-feeds { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.push-feed {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 6px 11px; border: 1px solid var(--line-2); border-radius: 6px;
  color: var(--text); text-decoration: none; font-size: 13.5px;
}
.push-feed:hover { border-color: var(--muted); }
.push-feed.on { border-color: var(--text); background: var(--panel-2); }
.push-plat {
  font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted);
}
/* The URL field is the one input that should take the line; `>` so the rule
   cannot reach anything nested, the mistake this stylesheet has made before. */
.push-find > input[type="url"] { flex: 1 1 320px; min-width: 0; }
.push-missing {
  margin-top: 12px; padding: 12px 14px; border: 1px dashed var(--line-2);
  border-radius: 8px; display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
}
.push-missing p { margin: 0; flex: 1 1 280px; color: var(--muted); font-size: 13.5px; }

.push-item { display: flex; gap: 16px; align-items: flex-start; margin-bottom: 18px; }
.push-thumb {
  width: 112px; aspect-ratio: 1; object-fit: cover; border-radius: 6px;
  background: var(--panel-2); flex: none;
}
/* Removed, not hidden: `visibility` keeps the box, which left a 112px hole
   above the caption on a phone. */
.push-thumb.blank { display: none; }
.push-meta { min-width: 0; }
.push-meta p { margin: 0 0 6px; }
.push-meta a { word-break: break-all; }
.push-caption { color: var(--muted); font-size: 13.5px; white-space: pre-wrap; }

.push-targets { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.push-targets th {
  text-align: left; font-weight: 500; color: var(--muted); font-size: 12px;
  padding: 0 10px 8px 0; border-bottom: 1px solid var(--line);
}
.push-targets td { padding: 9px 10px 9px 0; border-bottom: 1px solid var(--line); }
.push-targets tr.muted td { color: var(--faint); }

.push-send { margin-top: 16px; display: flex; flex-direction: column; gap: 12px; align-items: flex-start; }
.grid .tile.on { outline: 2px solid var(--text); outline-offset: 2px; }

@media (max-width: 640px) {
  .push-item { flex-direction: column; }
  /* Nothing hidden: "who gets pinged" is the column a push most needs, and
     dropping it at phone width made a 616px pane show no pings at all. */
  .push-targets td, .push-targets th { padding-right: 8px; overflow-wrap: anywhere; }
}

/* The operator header at phone width. A fixed 44px height let the wrapped
   rows spill over the page below - "Sign out" sat on top of each page's
   heading - and adding a sixth nav link pushed the last one off the screen,
   scrolling the whole page sideways. The links get a row of their own that
   scrolls within itself, so a new page never widens the page again. */
@media (max-width: 640px) {
  body.ops .app-nav { height: auto; min-height: 44px; flex-wrap: wrap; row-gap: 6px; padding: 8px 16px; }
  body.ops .app-nav nav {
    order: 3; flex: 1 0 100%; overflow-x: auto; scrollbar-width: none;
  }
  body.ops .app-nav nav::-webkit-scrollbar { display: none; }
  body.ops .app-nav nav a { flex: none; }
}

/* ---------------------------------------- operator: push, write your own */
.push-modes {
  display: flex; gap: 2px; margin: 4px 0 16px; border-bottom: 1px solid var(--line);
}
.push-modes a {
  padding: 7px 12px; color: var(--muted); text-decoration: none; font-size: 13.5px;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.push-modes a:hover { color: var(--text); }
.push-modes a.on { color: var(--text); border-bottom-color: var(--text); }

.push-compose { display: flex; flex-direction: column; gap: 14px; max-width: 640px; }
.push-compose label { display: flex; flex-direction: column; gap: 5px; }
.push-compose label > span { font-size: 12.5px; color: var(--muted); }
.push-compose em.hint { font-style: normal; font-size: 12px; }
/* Styled to match the inputs, which the skin covers and a textarea it does
   not - it fell back to the browser's light grey. Values read off a
   rendered input, not guessed. */
.push-compose textarea {
  resize: vertical; min-height: 96px; font: inherit; color: var(--text);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 8px 11px;
}
.push-compose textarea:focus { outline: none; border-color: var(--muted); }
.push-compose input[type="file"] { font: inherit; font-size: 13px; color: var(--muted); }
.push-compose > .btn { align-self: flex-start; }
.push-meta .btn.tiny { margin-top: 6px; }

/* The preview is Discord's own palette, as on the guild page, so a role
   mention gets Discord's blurple rather than this skin's neutral. */
.push-dc { max-width: 560px; margin: 8px 0 18px; }
.dc-mention {
  color: #c9cdfb; background: rgba(88, 101, 242, .3); border-radius: 3px; padding: 0 2px;
}
a.dc-embed-title { display: block; color: #00a8fc; text-decoration: none; }
a.dc-embed-title:hover { text-decoration: underline; }
.push-dc-image { display: block; max-width: 100%; max-height: 300px; border-radius: 4px; margin: 8px 0; }
.push-dc-video { display: block; max-width: 400px; width: 100%; border-radius: 4px; margin: 4px 0 8px; }
.badge.bad { color: var(--danger); border-color: rgba(229, 84, 75, .35); }
