/*
 * SocialEar — the design system.
 *
 * Light first. The product is read on paper-coloured ground because that is
 * where a photograph drawn as type, a hairline and a highlighter all look like
 * print rather than like a dashboard theme. Dark is a full theme, not an
 * afterthought, and the console people leave open all night still gets it.
 *
 * Two colours do the work of a brand here, and neither of them is a hue.
 *
 * Ink. Buttons, the active item, the score pill, the eyebrow — everything that
 * used to be an accent colour is now the darkest thing on the page. That is
 * what lets the pictures and the platform marks carry every hue there is,
 * without the product's own colour arguing with them: this thing watches
 * Reddit, YouTube, TikTok and Instagram, and must not look like any of them.
 *
 * The highlighter. One acid yellow, used only as a fill with ink on top, the
 * way a marker is used on a page: the matched word in a mention, the last
 * line of the headline, the ring that crosses a photograph, the ear in the
 * wordmark. It is never text, because at that lightness it cannot be — and
 * that limit is what keeps it a highlight instead of a theme. The `--ember`
 * family keeps its name because a hundred rules read it; what it means now is
 * "the accent that is safe as text", and on paper that accent is ink.
 */

:root {
  --bg: #f6f5f0;
  --surface: #fdfcfa;
  --surface-2: #f0efe9;
  --surface-3: #e8e6df;
  --border: #e2e0d8;
  --border-strong: #cfccc2;
  /*
   * Hairlines, as ink at low opacity rather than as a colour.
   *
   * A solid border is correct against the page and wrong against anything
   * else — nest two surfaces and the line is suddenly a different grey than
   * what it sits on. A translucent ink picks up whatever is behind it, which is
   * what makes a card inside a panel inside a page read as three depths
   * instead of three greys.
   */
  --hair: rgba(15, 15, 14, 0.08);
  --hair-strong: rgba(15, 15, 14, 0.13);

  --text: #0f0f0e;
  --text-2: #55534c;
  --text-3: #8a877e;

  /* The accent that is safe as text: ink. See the header. */
  --ember: #0f0f0e;
  --ember-hot: #2a2a27;
  --ember-dim: #000;
  --ember-wash: rgba(15, 15, 14, 0.07);
  --on-ember: #f6f5f0;

  /* The highlighter. A fill, never a text colour. */
  --hi: #e1f435;
  --hi-ink: #0f0f0e;
  --hi-wash: rgba(225, 244, 53, 0.42);

  /*
   * Positive is a teal-green, and that is a measured decision rather than a
   * taste one.
   *
   * The obvious green (#35c07f) against this red scores ΔE 5.9 under
   * deuteranopia — the most common colour blindness, around one man in twelve —
   * where the floor for "tellable apart" is 8. Negative and positive were very
   * nearly the same colour for those readers, on the one chart whose entire job
   * is telling them apart. Rotating the green toward teal lifts it to 8.8 while
   * staying inside the lightness band and above 3:1 on the surface.
   *
   *   node scripts/validate_palette.js "#d13b40,#12977c" --mode light
   */
  --pos: #12977c;
  --pos-deep: #0c6e59;
  --neg: #d13b40;
  --neu: #7b8794;
  --pos-wash: rgba(18, 151, 124, 0.1);
  --neg-wash: rgba(209, 59, 64, 0.1);

  /*
   * Eight categorical slots for the platforms, in a fixed order — see
   * platformColor() in src/ui/charts.tsx. Validated for colour-vision
   * deficiency as adjacent pairs on this surface; never assigned by rank.
   */
  --cat-1: #2a78d6;
  --cat-2: #eb6834;
  --cat-3: #1baf7a;
  --cat-4: #eda100;
  --cat-5: #e87ba4;
  --cat-6: #008300;
  --cat-7: #4a3aa7;
  --cat-8: #e34948;

  /*
   * Three roles, on purpose.
   *
   * Gabarito carries the personality — it has a slightly squared, humanist
   * geometry that reads as drawn rather than specified, which is what a
   * headline needs. Geist stays underneath it for running text and every
   * control, because a display face set at 15px in a dashboard is fatigue.
   * Geist Mono is not decoration: r/subreddit and u/author are identifiers,
   * and Reddit itself sets them that way, so the mono face is the subject's
   * own vernacular rather than a stylistic tic. It is also the face every
   * eyebrow, caption and credit is set in, which is where the page gets its
   * printed, technical grain.
   */
  --display: "Gabarito", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: "Geist Mono", ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;

  --r-sm: 6px;
  --r: 10px;
  --r-lg: 14px;
  /* The tray. Bigger than the cards it holds, so the nesting is visible. */
  --r-xl: 16px;

  /* Borders do the work of separation. Shadows only lift things that float. */
  --shadow: 0 1px 2px rgba(15, 15, 14, 0.06), 0 8px 24px rgba(15, 15, 14, 0.06);

  /* ------------------------------------------------------------- motion */
  /*
   * One easing curve and three durations, so everything in the product moves
   * the same way. The curve is a soft overshoot-free ease-out: interfaces feel
   * responsive when a change starts fast and settles, and feel cheap when it
   * starts slow. Every transition below names these rather than inventing its
   * own, which is what stops a dashboard reading as a pile of separate widgets.
   */
  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-1: 120ms;
  --dur-2: 200ms;
  --dur-3: 420ms;

  /* -------------------------------------------------------------- space */
  /* A 4px scale. Named, so a padding is a decision from a set rather than a
     number somebody typed — which is the actual difference between a designed
     interface and an assembled one. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;

  /* ---------------------------------------------------------- elevation */
  /*
   * Three levels. On paper they are faint shadows — a light page with heavy
   * shadows reads as a 2014 template — and on dark they become mostly *light*:
   * a black shadow on a near-black ground is invisible, so what separates a
   * surface from the page there is a one-pixel highlight along its top edge.
   */
  --sheen: inset 0 1px 0 rgba(255, 255, 255, 0.9);
  --e-1: 0 1px 2px rgba(15, 15, 14, 0.06);
  --e-2: 0 1px 2px rgba(15, 15, 14, 0.05), 0 6px 16px -6px rgba(15, 15, 14, 0.12);
  --e-3: 0 2px 4px rgba(15, 15, 14, 0.05), 0 16px 40px -12px rgba(15, 15, 14, 0.18);
  /* Focus is the one place the highlighter draws a line rather than a fill. */
  --ring: 0 0 0 3px var(--hi);

  color-scheme: light;
}

/*
 * Dark, twice: once for a system preference nobody has overridden, once for an
 * explicit choice. The two blocks must stay identical — edit one, edit both.
 * The ink accent flips to paper, so the primary button is the lightest thing
 * on a dark page for the same reason it is the darkest on a light one. The
 * highlighter does not change: acid yellow with ink on it works on either
 * ground, which is the property that earned it the job.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0e0e0d;
    --surface: #151514;
    --surface-2: #1b1b19;
    --surface-3: #232320;
    --border: #2a2a27;
    --border-strong: #383834;
    --hair: rgba(255, 255, 255, 0.065);
    --hair-strong: rgba(255, 255, 255, 0.1);

    --text: #f1f0ea;
    --text-2: #aaa89f;
    --text-3: #75736b;

    --ember: #f1f0ea;
    --ember-hot: #fff;
    --ember-dim: #d6d4cc;
    --ember-wash: rgba(241, 240, 234, 0.1);
    --on-ember: #0e0e0d;

    --pos: #1faa8e;
    --pos-deep: #15806b;
    --neg: #f2555a;
    --pos-wash: rgba(31, 170, 142, 0.14);
    --neg-wash: rgba(242, 85, 90, 0.12);
    --cat-1: #3987e5;
    --cat-2: #d95926;
    --cat-3: #199e70;
    --cat-4: #c98500;
    --cat-5: #d55181;
    --cat-6: #008300;
    --cat-7: #9085e9;
    --cat-8: #e66767;

    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.24);
    --sheen: inset 0 1px 0 rgba(255, 255, 255, 0.045);
    --e-1: var(--sheen), 0 1px 2px rgba(0, 0, 0, 0.32);
    --e-2: var(--sheen), 0 2px 4px rgba(0, 0, 0, 0.3), 0 8px 20px -8px rgba(0, 0, 0, 0.5);
    --e-3: var(--sheen), 0 4px 8px rgba(0, 0, 0, 0.34), 0 20px 48px -16px rgba(0, 0, 0, 0.62);

    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg: #0e0e0d;
  --surface: #151514;
  --surface-2: #1b1b19;
  --surface-3: #232320;
  --border: #2a2a27;
  --border-strong: #383834;
  --hair: rgba(255, 255, 255, 0.065);
  --hair-strong: rgba(255, 255, 255, 0.1);

  --text: #f1f0ea;
  --text-2: #aaa89f;
  --text-3: #75736b;

  --ember: #f1f0ea;
  --ember-hot: #fff;
  --ember-dim: #d6d4cc;
  --ember-wash: rgba(241, 240, 234, 0.1);
  --on-ember: #0e0e0d;

  --pos: #1faa8e;
  --pos-deep: #15806b;
  --neg: #f2555a;
  --pos-wash: rgba(31, 170, 142, 0.14);
  --neg-wash: rgba(242, 85, 90, 0.12);
  --cat-1: #3987e5;
  --cat-2: #d95926;
  --cat-3: #199e70;
  --cat-4: #c98500;
  --cat-5: #d55181;
  --cat-6: #008300;
  --cat-7: #9085e9;
  --cat-8: #e66767;

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.24);
  --sheen: inset 0 1px 0 rgba(255, 255, 255, 0.045);
  --e-1: var(--sheen), 0 1px 2px rgba(0, 0, 0, 0.32);
  --e-2: var(--sheen), 0 2px 4px rgba(0, 0, 0, 0.3), 0 8px 20px -8px rgba(0, 0, 0, 0.5);
  --e-3: var(--sheen), 0 4px 8px rgba(0, 0, 0, 0.34), 0 20px 48px -16px rgba(0, 0, 0, 0.62);

  color-scheme: dark;
}

/* ------------------------------------------------------------------ base */

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.025em;
  line-height: 1.15;
  text-wrap: balance;
}

h1 { font-size: 30px; letter-spacing: -0.03em; }
h2 { font-size: 20px; }
h3 { font-size: 16px; }

p { margin: 0 0 12px; }

a {
  color: var(--text);
  text-decoration-color: var(--border-strong);
  text-underline-offset: 3px;
}
a:hover { text-decoration-color: var(--ember); }

code, kbd, .mono { font-family: var(--mono); font-size: 0.92em; }

/* Numbers must not jitter as they update — a live counter that reflows on
   every tick reads as broken even when it is right. */
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/*
 * Icons, from Icon.tsx.
 *
 * `flex: none` because the overwhelmingly common placement is inside a flex
 * row beside a label, and an icon that has agreed to shrink is an icon that
 * turns into an ellipse the moment the label is long.
 *
 * The vertical nudge is the reason this rule exists at all: a 24-grid glyph
 * sitting on the text baseline rides high against lowercase, and `middle`
 * aligns it to the x-height instead. Set here once rather than argued with at
 * each call site.
 */
.icon {
  flex: none;
  vertical-align: middle;
}

/* --------------------------------------------------------------- layout */

.wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
.narrow { max-width: 640px; margin: 0 auto; padding: 0 24px; }

.row { display: flex; align-items: center; gap: 10px; }
.between { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.stack { display: flex; flex-direction: column; gap: 14px; }
.grow { flex: 1 1 auto; min-width: 0; }

.grid { display: grid; gap: 14px; }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

@media (max-width: 900px) {
  .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-3, .grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .grid-4 { grid-template-columns: 1fr; }
  .wrap, .narrow { padding: 0 16px; }
}

/* ---------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-4);
  box-shadow: var(--e-1);
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease);
}

/* Only what is clickable lifts. A card that rises under the cursor and then
   does nothing is a promise the interface does not keep. */
a.card:hover,
.card.interactive:hover {
  border-color: var(--border-strong);
  box-shadow: var(--e-2);
  transform: translateY(-1px);
}

.card-tight { padding: var(--s-3) 14px; }

.stat {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: 14px var(--s-4) var(--s-4);
  box-shadow: var(--e-1);
  overflow: hidden;
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease);
}

/* A one-pixel gradient along the top edge. It is the only ornament on the
   tile, it costs no colour of its own, and it is what stops four grey boxes
   reading as four grey boxes. */
.stat::after {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--border-strong) 22%, var(--border-strong) 78%, transparent);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
}
a.stat:hover {
  border-color: var(--border-strong);
  box-shadow: var(--e-2);
  transform: translateY(-1px);
}
a.stat:hover::after { opacity: 1; }

.stat .label {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-3);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.stat .value {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 29px;
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.1;
  margin-top: var(--s-2);
}

/*
 * The digits spin to the number — see src/client/reel.ts.
 *
 * Every height here is `1.1em` because that is the value's own line-height:
 * the cell tracks the font size instead of hardcoding the 32px it happens to
 * be today, so changing the tile's type size cannot silently misalign the
 * window against the digits inside it.
 *
 * The window is masked rather than hard-cropped, top and bottom, so a digit
 * arrives and leaves rather than appearing at a cut edge. `.stat` already
 * clips its own overflow, so this is about the character above and below the
 * one you are meant to read.
 */
.reel { display: inline-flex; align-items: flex-start; height: 1.1em; }

.reel-col {
  height: 1.1em;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 26%, #000 74%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 26%, #000 74%, transparent);
}

.reel-strip {
  display: flex;
  flex-direction: column;
  will-change: transform;
}

.reel-cell {
  height: 1.1em;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The separator in a value like "3/10". It never moves. */
.reel-fixed { display: inline-flex; align-items: center; height: 1.1em; }

/* Added a frame after the strips are painted, so there is a start state to
   transition from. The easing is the product's overshoot-free settle: a reel
   that bounces past its number and comes back is reading the wrong figure for
   a moment, on a dashboard whose only job is the figure. */
.reel.go .reel-strip {
  transition: transform var(--dur) cubic-bezier(0.16, 1, 0.3, 1) var(--delay);
  transform: translateY(calc(var(--cells) * -1.1em));
}

@media (prefers-reduced-motion: reduce) {
  .reel-strip { transition: none; }
}
.stat .delta {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--text-3);
  margin-top: 2px;
}
.delta.up { color: var(--pos); }
.delta.down { color: var(--neg); }

/* --------------------------------------------------------------- tokens */

/*
 * The r/subreddit chip. This is the atom of the whole interface — it shows up
 * in the sidebar, on every mention, in the filters, in the emails. Mono, so it
 * reads as an identifier rather than as prose, which is exactly how Reddit
 * itself sets it.
 */
.sub {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: var(--mono);
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-2);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 9px;
  white-space: nowrap;
}
.sub:hover { border-color: var(--border-strong); color: var(--text); }
/* A channel avatar inside the pill, the same height as the platform mark. */
.sub .avatar { width: 16px; height: 16px; border-radius: 50%; object-fit: cover; flex: none; }

.user { font-family: var(--mono); font-size: 12.5px; color: var(--text-3); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 500;
  border-radius: 999px;
  padding: 2px 9px;
  border: 1px solid var(--border);
  color: var(--text-2);
  background: var(--surface-2);
}
.pill.pos { color: var(--pos); background: var(--pos-wash); border-color: transparent; }
.pill.neg { color: var(--neg); background: var(--neg-wash); border-color: transparent; }
.pill.ember { color: var(--hi-ink); background: var(--hi); border-color: transparent; }

/* The karma column. Reddit's anatomy, our type. */
.score {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  min-width: 42px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
}
.score .arrow { color: var(--ember); line-height: 1; font-size: 12px; }

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 550;
  line-height: 1;
  padding: 9px 15px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  transition: background 120ms ease, border-color 120ms ease, opacity 120ms ease;
}
.btn {
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.btn:hover { background: var(--surface-3); border-color: var(--border-strong); }
/* A button that moves 1px under the press is the cheapest tactility there is. */
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-primary {
  background: var(--ember);
  border-color: var(--ember);
  color: var(--on-ember);
}
.btn-primary { box-shadow: var(--e-1); }
.btn-primary:hover {
  background: var(--ember-hot);
  border-color: var(--ember-hot);
  box-shadow: var(--e-2);
}

.btn-ghost { background: transparent; border-color: transparent; color: var(--text-2); }
.btn-ghost:hover { background: var(--surface-2); border-color: var(--border); color: var(--text); }

.btn-sm { font-size: 13px; padding: 6px 11px; }

/* --------------------------------------------------------------- inputs */

input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
select,
textarea {
  font-family: inherit;
  font-size: 14px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 9px 11px;
  width: 100%;
}
input::placeholder, textarea::placeholder { color: var(--text-3); }

input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.btn:focus-visible,
a:focus-visible,
.toggle:focus-visible {
  /* A ring rather than a hard outline: the wash reads as the same accent
     without the 2px slab, and it does not fight a rounded corner. */
  outline: 2px solid var(--ember);
  outline-offset: 2px;
  box-shadow: var(--ring);
}

label { font-size: 13px; font-weight: 500; color: var(--text-2); }

/* --------------------------------------------------------------- toggle */

/*
 * The subreddit switch. It carries state that costs money to be wrong about,
 * so it is deliberately large, deliberately animated, and never ambiguous
 * about which way is on.
 */
.toggle {
  position: relative;
  display: inline-block;
  width: 38px;
  height: 22px;
  flex: 0 0 auto;
  border-radius: 999px;
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease;
}
.toggle::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--text-3);
  transition: transform 140ms cubic-bezier(0.3, 0, 0.2, 1), background 140ms ease;
}
.toggle[aria-checked="true"] { background: var(--ember); border-color: var(--ember); }
.toggle[aria-checked="true"]::after { transform: translateX(16px); background: #fff; }
.toggle[aria-busy="true"] { opacity: 0.6; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .toggle, .toggle::after, .btn { transition: none; }
}

/* ------------------------------------------------------------ mentions */

.mention {
  display: flex;
  gap: 14px;
  padding: 14px var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--e-1);
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease);
}
.mention:hover {
  border-color: var(--border-strong);
  box-shadow: var(--e-2);
  transform: translateY(-1px);
}

/*
 * Unread, without a coloured bar down the side.
 *
 * The rail was doing the job and looking like every generated dashboard on the
 * internet — a saturated stripe welded to the left edge of a card. What is
 * actually being said is "you have not read this one", and typography says it
 * better than chrome: a read mention's title drops to secondary ink, an unread
 * one keeps full strength and carries a single small dot.
 *
 * It also survives the thing the rail could not: in a flush list the rail is a
 * bar butted against the panel's own border, which reads as a rendering fault
 * rather than as a state.
 */
.mention .title { color: var(--text); }
.mention:not(.unread) .title { color: var(--text-2); font-weight: 500; }

.unread-dot {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--ember);
  flex: none;
}
.mention .meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12.5px; color: var(--text-3); }

/*
 * The action row wraps.
 *
 * Five controls — Read, Open thread, Mark read, Not relevant, Archive — in a
 * `.row`, which does not wrap. On a 390px screen the card is 356px wide and
 * the row measured 433: "Not relevant" and "Archive" were cut off at the card
 * edge, so the two actions that teach the filter and clear the inbox were the
 * two you could not reach on a phone.
 *
 * Wrapping rather than scrolling on purpose. A row that scrolls sideways hides
 * the same buttons behind a gesture nobody knows is available; a row that
 * wraps costs one line of height and shows all five.
 */
.mention .row { flex-wrap: wrap; }

/*
 * Buying the action row enough width to wrap into two lines instead of three.
 *
 * Wrapping stopped the clipping and cost three lines — 97px of buttons on
 * every card, twenty-five cards to a page. Measured at 390px, the row only
 * ever had 226px to work with, and the five controls need 448 including gaps:
 *
 *   .bulkrow  17→373   padding-left 12, gap 10
 *     checkbox  33→46
 *     .mention  59→373   padding-left 16, gap 14
 *       .score    75→117   42px
 *       .grow    131→357   226px  ← the whole row
 *
 * Two things are reclaimed here, and it takes both. Either one alone still
 * wrapped to three lines, which is worth writing down because the first
 * attempt was the icons on their own and it changed nothing measurable.
 *
 * **The score column.** Atom feeds carry no score, so `m.score` is null for
 * everything this product ingests — the column renders a single "·" and its
 * own title attribute says Reddit does not publish one. On a phone that is
 * 42px plus a 14px gap, a quarter of the row's width, spent on a placeholder
 * that can never become a number. It stays on desktop, where the width is
 * free and the column keeps the cards aligned.
 *
 * **The glyphs.** Each sits beside a word that already says it — "Read",
 * "Open thread", "Not relevant" — so they are decoration here in a way they
 * are not in the nav, where the word is sometimes all there is.
 *
 * Together the row goes 226px → 282px at 390px wide, and the buttons fall as
 * 56+101+87 = 244 on the first line and 101+71 = 172 on the second. Three
 * lines to two, the row 97px to 62, the card 434px to 356. Above the
 * breakpoint nothing moves: at 1280px the score column is still there and the
 * five controls still sit on one line.
 */
@media (max-width: 560px) {
  .mention .score { display: none; }
  .mention .row { gap: 6px; }
  .mention .row .icon { display: none; }
}
.mention .title { font-weight: 560; letter-spacing: -0.01em; margin: 5px 0 4px; }

/*
 * The thread a comment was written under.
 *
 * One line, always — a post title can run to three hundred characters and this
 * is context, not content. Truncating keeps the comment's own text at a
 * predictable distance from the meta row, which is what lets a feed of fifty
 * comments scan as a column rather than as a stack of different shapes.
 */
.mention .under {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 5px 0 3px;
  font-size: 12.5px;
  color: var(--text-3);
  min-width: 0;
}
.mention .under span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mention .under .icon { opacity: 0.55; }

/* `comment` in the meta row, with its glyph. Inline-flex so the icon and the
   word stay one unit when the row wraps. */
.kindtag { display: inline-flex; align-items: center; gap: 4px; }
.mention .body { color: var(--text-2); font-size: 14px; }

/*
 * A mention the plan did not pay for.
 *
 * Built only from tokens, so it follows the theme like everything else — the
 * bars have to read as "covered" on a light surface as well as a dark one, and
 * a hardcoded grey only ever works on one of them.
 *
 * The row keeps its subreddit, author, time and title: the product's job is
 * finding the thing, and proving it was found is what makes the upgrade worth
 * buying. What is covered is the text.
 */
.mention.locked { border-style: dashed; }
.mention.locked .title { color: var(--text-2); }

.locked-body { display: flex; flex-direction: column; gap: 9px; }

.locked-lines { display: flex; flex-direction: column; gap: 5px; }
.locked-lines span {
  height: 8px;
  border-radius: 3px;
  background: var(--surface-3);
}
.locked-lines span:nth-child(1) { width: 96%; }
.locked-lines span:nth-child(2) { width: 88%; }
.locked-lines span:nth-child(3) { width: 54%; }

.locked-cta {
  font-size: 12.5px;
  font-weight: 550;
  color: var(--ember);
  text-decoration: none;
}
.locked-cta:hover { text-decoration: underline; }

/* The matched term, highlighted in place. The whole reason the row is here. */
mark {
  background: var(--hi);
  color: var(--hi-ink);
  border-radius: 2px;
  padding: 0 3px;
  font-weight: 560;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

.dot { width: 3px; height: 3px; border-radius: 50%; background: var(--text-3); flex: 0 0 auto; }

/* ---------------------------------------------------------------- misc */

.muted { color: var(--text-2); }
.dim { color: var(--text-3); }
.small { font-size: 13px; }
.tiny { font-size: 12px; }
hr { border: 0; border-top: 1px solid var(--border); margin: 20px 0; }

.empty {
  text-align: center;
  padding: 48px 24px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r);
  color: var(--text-2);
}

.banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 14px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: 13.5px;
}
.banner.warn { background: var(--ember-wash); border-color: transparent; color: var(--ember); }
.banner.bad { background: var(--neg-wash); border-color: transparent; color: var(--neg); }
/* The third state the set was missing. Payment is the one thing in here that
   goes *right* loudly enough to deserve saying so. */
.banner.good { background: var(--pos-wash); border-color: transparent; color: var(--pos); }

/*
 * "Saved." draws its own tick.
 *
 * A CSS animation and no island: the banner is only rendered when the redirect
 * carries `?saved=1`, so its arrival on screen *is* the event. Nothing needs to
 * watch for anything.
 *
 * The two halves run together — the glyph fades in while righting itself out
 * of a tilt, and the stroke draws over the top of that.
 *
 * **22.627 is measured, not chosen.** It is `getTotalLength()` of Lucide's
 * check — `M20 6 9 17l-5-5`, which is 11√2 for the descender plus 5√2 for the
 * foot — read out of a real browser rather than estimated. A dasharray shorter
 * than the path leaves the tick permanently clipped; longer, and it appears to
 * start already half drawn.
 *
 * **The offset is negative on purpose.** The path is authored from the
 * top-right tip downwards, so animating a positive offset to zero draws the
 * long descender first and the short foot last, which is not how anybody draws
 * a tick. A negative offset reveals from the other end — foot first, then up
 * through the corner. Checked by rendering both signs at half progress and
 * looking at them.
 */
.savedbanner .savedcheck {
  flex: none;
  transform-origin: center;
  animation: saved-check-in 460ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}

.savedbanner .savedcheck path {
  stroke-dasharray: 22.627;
  animation: saved-check-draw 420ms cubic-bezier(0.22, 1, 0.36, 1) 60ms backwards;
}

@keyframes saved-check-in {
  from {
    opacity: 0;
    transform: rotate(-28deg) translateY(5px);
    filter: blur(3px);
  }
}

@keyframes saved-check-draw {
  from { stroke-dashoffset: -22.627; }
  to { stroke-dashoffset: 0; }
}

/* No animation leaves the dasharray at its full length against an offset of
   zero, which is simply the finished tick. */
@media (prefers-reduced-motion: reduce) {
  .savedbanner .savedcheck,
  .savedbanner .savedcheck path { animation: none; }
}

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


/* =========================================================== the wordmark */

.wordmark {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.035em;
  color: var(--text);
  white-space: nowrap;
  line-height: 1;
}

/* The hyphen stays, dimmed. It is a codename and the logo should not pretend
   otherwise — that is the first place a pretence shows. */
.wordmark-ear {
  color: var(--hi-ink);
  background: var(--hi);
  padding: 0 0.14em 0.02em;
  margin-left: 0.04em;
  border-radius: 0.14em;
}

/* ============================================================== the page */

/*
 * The landing's own names for the same neutrals the app uses.
 *
 * `--paper` and `--ink` predate the light-first palette; they stay because
 * seventy rules read them. The values are the app's `--bg` / `--text` family,
 * slightly warm — a paper white with a little yellow in it is what makes the
 * highlighter look like it was drawn on the page rather than pasted over it.
 */
.landing {
  --paper: #f6f5f0;
  --paper-2: #efeee8;
  --paper-3: #e6e4dc;
  --rule: #e0ded6;
  --ink: #0f0f0e;
  --ink-2: #55534c;
  --ink-3: #8a877e;

  background: var(--paper);
  color: var(--ink);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .landing {
    --paper: #0e0e0d;
    --paper-2: #161615;
    --paper-3: #1f1f1d;
    --rule: #2a2a27;
    --ink: #f1f0ea;
    --ink-2: #aaa89f;
    --ink-3: #75736b;
  }
}

:root[data-theme="dark"] .landing {
  --paper: #0e0e0d;
  --paper-2: #161615;
  --paper-3: #1f1f1d;
  --rule: #2a2a27;
  --ink: #f1f0ea;
  --ink-2: #aaa89f;
  --ink-3: #75736b;
}

.landing h1, .landing h2, .landing h3 { color: var(--ink); }
.landing p { color: var(--ink-2); }
.landing a { color: var(--ink); }

.section { padding: 88px 0; border-top: 1px solid var(--rule); }
.section:first-of-type { border-top: 0; }

.eyebrow {
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  /* Inside .landing the secondary ink; on an app page, the app's. */
  color: var(--ink-2, var(--text-2));
}

.lede { font-size: 18px; line-height: 1.55; max-width: 54ch; }

.display {
  font-family: var(--display);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.02;
  font-size: clamp(38px, 6.4vw, 68px);
}

.display em {
  font-style: normal;
  color: var(--hi-ink);
  background: var(--hi);
  padding: 0 0.12em 0.03em;
  margin: 0 -0.04em;
  border-radius: 0.08em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/*
 * The headline arrives a line at a time.
 *
 * An `animation`, deliberately, and not a transition flipped by a class the
 * way this effect is usually built. A class-driven entrance means the lines
 * start at `opacity: 0` and wait for JavaScript to reveal them — on the first
 * screen of a marketing page, that is the entire value proposition rendered
 * blank for anyone whose script does not arrive, and a flash for everyone
 * else. A CSS animation needs nothing to run, and if the stylesheet itself
 * fails the browser's default is text you can read.
 *
 * `backwards` is the load-bearing word: it applies the `from` state during the
 * delay, which is what makes the second and third lines wait their turn rather
 * than sitting there fully drawn until their delay elapses.
 *
 * 70ms apart. Far enough that the eye lands on "They are already" first, close
 * enough that the sentence still reads as one thought — a longer stagger makes
 * a three-line headline feel like three separate announcements.
 */
.hero-line {
  display: block;
  animation: hero-rise 620ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}

.hero-line:nth-child(2) { animation-delay: 70ms; }
.hero-line:nth-child(3) { animation-delay: 140ms; }

@keyframes hero-rise {
  from {
    opacity: 0;
    transform: translateY(14px);
    filter: blur(4px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero-line { animation: none; }
}

@media (max-width: 720px) {
  .section { padding: 56px 0; }
}

/* ------------------------------------------------------------- the hero */

.hero-grid {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: 56px;
  align-items: center;
}

@media (max-width: 940px) {
  .hero-grid { grid-template-columns: 1fr; gap: 40px; }
}

/*
 * The landing header, on a phone.
 *
 * `.row` is a flex row with no wrap, so the six controls in the header did not
 * fold and did not shrink — they simply ran off the side. Measured at 375, 390
 * and 414px the page reported scrollWidth 516 against the viewport, and the
 * nav's right edge was that 516 exactly: the whole marketing page scrolled
 * sideways because of this one row.
 *
 * `display: contents` keeps the group's three links as direct flex children of
 * the nav on a wide screen, so nothing about the desktop layout changes; the
 * wrapper exists only to give this breakpoint one thing to hide.
 */
.nav-sections { display: contents; }

@media (max-width: 640px) {
  .nav-sections { display: none; }
}

/* ---------------------------------------------------------- the marquee */

/*
 * Two rows, opposite directions, paused on hover.
 *
 * The track is duplicated in the markup and translated by exactly -50%, which
 * is the only way this loops without a seam. Percentages of the *track* rather
 * than a pixel distance, so adding a card never desynchronises it.
 */
.marquee {
  position: relative;
  overflow: hidden;
  /* Fades the ends into the page instead of clipping cards mid-word. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.marquee-track {
  display: flex;
  gap: 14px;
  width: max-content;
  animation: marquee 64s linear infinite;
}

.marquee-track.reverse { animation-direction: reverse; }
.marquee:hover .marquee-track { animation-play-state: paused; }

@keyframes marquee {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}

.m-card {
  flex: 0 0 auto;
  width: 330px;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 12px;
  padding: 13px 15px;
}

.m-card .m-head {
  display: flex;
  align-items: center;
  gap: 7px;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-3);
  margin-bottom: 7px;
}

.m-card .m-title {
  font-weight: 560;
  font-size: 14px;
  line-height: 1.35;
  color: var(--ink);
  margin-bottom: 8px;
}

.m-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  border-radius: 999px;
  padding: 2px 8px;
}

.m-chip.buy { color: #0d7a52; background: rgba(13, 122, 82, 0.11); }
.m-chip.neg { color: #c02b30; background: rgba(192, 43, 48, 0.11); }
.m-chip.cmp { color: #8a4b00; background: rgba(179, 98, 0, 0.13); }
.m-chip.ask { color: var(--ink-2); background: var(--paper-3); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .m-chip.buy { color: #47d69a; background: rgba(71, 214, 154, 0.13); }
  :root:not([data-theme="light"]) .m-chip.neg { color: #ff6b70; background: rgba(255, 107, 112, 0.13); }
  :root:not([data-theme="light"]) .m-chip.cmp { color: #ffab4d; background: rgba(255, 171, 77, 0.14); }
}

:root[data-theme="dark"] .m-chip.buy { color: #47d69a; background: rgba(71, 214, 154, 0.13); }
:root[data-theme="dark"] .m-chip.neg { color: #ff6b70; background: rgba(255, 107, 112, 0.13); }
:root[data-theme="dark"] .m-chip.cmp { color: #ffab4d; background: rgba(255, 171, 77, 0.14); }

/* --------------------------------------------- the platform showcase */

/*
 * Four invented mentions, one per platform, in the product's own anatomy.
 * Same paper-and-border language as .m-card and .heropanel, so the section
 * reads as the same product — but its own classes, so a tweak here never
 * restyles the app's real mention rows.
 */
.demo-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
}

@media (max-width: 780px) {
  .demo-grid { grid-template-columns: 1fr; }
}

.demo-card {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 14px;
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* The platform's name leads the card; the grey clause says what is read. */
.demo-plat {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--rule);
  font-size: 14px;
  color: var(--ink);
}
.demo-plat span { font-size: 12px; color: var(--ink-3); }

.demo-mention { display: flex; gap: 14px; align-items: flex-start; }

.demo-score {
  flex: 0 0 auto;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-3);
  padding-top: 2px;
  white-space: nowrap;
}

.demo-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 7px;
  font-size: 11.5px;
  color: var(--ink-3);
  margin-bottom: 7px;
}

.demo-title {
  font-weight: 560;
  font-size: 14.5px;
  line-height: 1.4;
  color: var(--ink);
}

.demo-body {
  margin: 6px 0 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--ink-2);
}

/* The thread a comment answers, same turned arrow as the app's feed. */
.demo-under {
  font-size: 12.5px;
  color: var(--ink-3);
  margin-bottom: 6px;
}

/*
 * The YouTube card: a video first, because that is what the mention is, and
 * the caption line that put it in the inbox underneath. The thumbnail is a
 * drawn placeholder — a real frame would pin the invented sentence to a real
 * video, which is the thing this section promises not to do.
 */
.yt-video { display: flex; gap: 14px; align-items: flex-start; }

.yt-thumb {
  position: relative;
  flex: 0 0 118px;
  aspect-ratio: 16 / 9;
  border-radius: 8px;
  background:
    radial-gradient(120% 140% at 20% 0%, var(--hi-wash), transparent 55%),
    linear-gradient(135deg, var(--paper-3), var(--paper-2));
  border: 1px solid var(--rule);
}

.yt-play {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 8px 0 8px 14px;
  border-color: transparent transparent transparent var(--ink-2);
}

.yt-dur {
  position: absolute;
  right: 5px;
  bottom: 5px;
  font-size: 10.5px;
  font-weight: 600;
  color: #fff;
  background: rgba(16, 16, 26, 0.78);
  border-radius: 4px;
  padding: 1px 5px;
}

.yt-transcript {
  border-top: 1px dashed var(--rule);
  padding-top: 12px;
}

/* The captions chip, same shape as the app's transcript chip. */
.demo-cc {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-3);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 2px 8px;
}

.yt-jump {
  display: inline-block;
  margin-top: 8px;
  font-size: 11.5px;
  color: var(--ember);
}

/* --------------------------------------------------------- step & cards */

.tile {
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: 14px;
  padding: 22px;
}

.step-n {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--ember);
  letter-spacing: 0.04em;
}

.landing .btn {
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: -0.01em;
  background: var(--paper);
  border-color: var(--rule);
  color: var(--ink);
}
.landing .btn:hover { background: var(--paper-2); border-color: var(--ink-3); }

.landing .btn-primary {
  background: var(--ember);
  border-color: var(--ember);
  color: var(--on-ember);
}
.landing .btn-primary:hover { background: var(--ember-hot); border-color: var(--ember-hot); }

.landing .btn-ghost { background: transparent; border-color: transparent; color: var(--ink-2); }
.landing .btn-ghost:hover { background: var(--paper-2); color: var(--ink); }

/* -------------------------------------------------------------- the faq */

.faq details {
  border-bottom: 1px solid var(--rule);
  padding: 18px 0;
}

.faq summary {
  cursor: pointer;
  font-family: var(--display);
  font-weight: 600;
  font-size: 16.5px;
  letter-spacing: -0.015em;
  color: var(--ink);
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: 16px;
}

.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; color: var(--ember); font-size: 20px; line-height: 1; }
.faq details[open] summary::after { content: "\2013"; }
.faq details p { margin: 12px 0 0; max-width: 68ch; font-size: 14.5px; }

@media (prefers-reduced-motion: reduce) {
  .marquee-track { animation: none; }
}

/* ------------------------------------------------------------- the grid */

.rule-list { display: grid; gap: 0; }
.rule-list > * { padding: 16px 0; border-top: 1px solid var(--rule); }
.rule-list > *:first-child { border-top: 0; }

/* ------------------------------------------------------------- the pager */

/*
 * Built from tokens so it follows the theme, and wrapping rather than
 * scrolling: a history a few hundred pages deep still has to be usable on a
 * phone, where a row of page numbers is the first thing to overflow.
 */
.pager {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 22px;
}
.pager > .btn[aria-disabled='true'] { opacity: 0.4; pointer-events: none; }

.pager-nums { display: flex; align-items: center; gap: 3px; flex-wrap: wrap; margin: 0 auto; }

.pager-num {
  min-width: 30px;
  padding: 5px 7px;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  text-align: center;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  text-decoration: none;
}
.pager-num:hover { border-color: var(--border); color: var(--text); }
.pager-num.current {
  border-color: var(--ember);
  color: var(--ember);
  font-weight: 600;
}
.pager-gap { padding: 0 3px; color: var(--text-3); font-size: 13px; }

/* ═══════════════════════════════════════════════════════ the application */

/*
 * The console chrome.
 *
 * Everything below used to be inline `style=` on the Shell component, which is
 * why nothing quite lined up: a padding written five times is five paddings.
 * It is classes now, off the space scale, so the rhythm is one decision.
 */

.app {
  display: flex;
  min-height: 100vh;
}

/* ------------------------------------------------------------- sidebar */

.sidenav {
  width: 232px;
  flex: 0 0 232px;
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-3) var(--s-4);
  background: var(--surface);
  border-right: 1px solid var(--hair);
  position: sticky;
  top: 0;
  height: 100vh;
}

.sidenav-brand { padding: 0 var(--s-2); text-decoration: none; }

/*
 * The part of the sidebar that gives way.
 *
 * This used to be `display: contents`, which left the groups as direct flex
 * children and the column with no scrolling at all: on a window shorter than
 * the nav, the meter and the log-out button were below the fold of a 100vh
 * box that could not scroll, and there was no way to reach them. Now it is a
 * real box with the column's own gap inside it, it takes what height is left
 * (`min-height: 0` is what lets a flex child shrink below its content), and
 * scrolls on its own — so the navigation gives way and the account controls
 * beneath it stay put. `overscroll-behavior: contain` keeps a scroll that hits
 * its end from carrying on into the page.
 */
.sidenav-scroll {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  /* Room for the scrollbar without the pills touching it. */
  margin-right: calc(-1 * var(--s-1));
  padding-right: var(--s-1);
}

.sidenav-group { display: flex; flex-direction: column; gap: 2px; }
.sidenav-group + .sidenav-group { padding-top: var(--s-4); border-top: 1px solid var(--hair); }

.sidenav-label {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 0 var(--s-2) var(--s-2);
}

/*
 * A nav item.
 *
 * The rail this used to have became a filled pill, which is what the reference
 * actually does — and it is better here: a rail marks an edge, a pill marks a
 * *place*, and a sidebar is a list of places. Rows are 36px rather than 30,
 * because the density that reads as professional is fewer things with room
 * around them, not more things packed in.
 *
 * The leading dot is the same one the subreddit rows use. On the current item
 * it is solid; everywhere else it is absent, so the eye has one mark to find
 * rather than five to compare.
 */
.navlink {
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 8px 10px;
  min-height: 34px;
  border-radius: var(--r);
  font-size: 13.5px;
  font-weight: 480;
  color: var(--text-2);
  text-decoration: none;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.navlink:hover { color: var(--text); background: var(--surface-2); }
.navlink .navicon { flex: none; opacity: 0.7; transition: opacity var(--dur-1) var(--ease); }
.navlink:hover .navicon { opacity: 1; }

.navlink.current {
  color: var(--text);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hair);
  font-weight: 560;
}
.navlink.current .navicon { color: var(--ember); opacity: 1; }

.navlink .navcount {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
  background: var(--surface-3);
  border-radius: 999px;
  padding: 1px 6px;
}
.navlink.current .navcount { color: var(--ember); background: var(--ember-wash); }

/* ---------------------------------------------------------- quota meter */

.meter-card {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--s-3);
  box-shadow: var(--sheen);
}

.meter {
  height: 5px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.meter > i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--text-3), var(--text-2));
  /* Grows into place on load, so the number and the bar arrive together. */
  transition: width var(--dur-3) var(--ease);
}
.meter.tight > i { background: linear-gradient(90deg, var(--ember-dim), var(--ember)); }

.plan-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-2);
  background: var(--surface-3);
  border-radius: 999px;
  padding: 2px 8px;
}
.plan-chip.paid { color: var(--ember); background: var(--ember-wash); }

/* -------------------------------------------------------------- footer */

.sidenav-foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
.sidenav-user {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0 var(--s-2);
  min-width: 0;
}
.sidenav-user .who {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12px;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * The theme toggle ships both glyphs and lets the cascade choose.
 *
 * Dark is the base, so the base state offers the sun — the button always shows
 * where you are going, not where you are. The two overrides below are the same
 * pair the palette uses at the top of this file: the media query for the
 * unstamped default, and the attribute for an explicit choice.
 */
.themetoggle {
  padding: 5px 7px;
  flex: none;
}
.themetoggle .i-sun { display: none; }
.themetoggle .i-moon { display: block; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .themetoggle .i-moon { display: none; }
  :root:not([data-theme="light"]) .themetoggle .i-sun { display: block; }
}
:root[data-theme="dark"] .themetoggle .i-moon { display: none; }
:root[data-theme="dark"] .themetoggle .i-sun { display: block; }

.logout {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
  font-size: 12px;
}
/* Was an inline style, which the phone layout could not override. */
.logout-form { padding: 0 var(--s-2); }
.logout .icon { opacity: 0.65; }
.logout:hover .icon { opacity: 1; }

/* ---------------------------------------------------------------- main */

.appmain {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--s-6) var(--s-6) 80px;
  max-width: 1120px;
  /*
   * Centred in what the sidebar leaves.
   *
   * The column is capped at 1120px so a line of text never runs the width of
   * a 27" display, and it used to sit hard against the sidebar with the whole
   * remainder of the screen empty on the right. Auto margins on a flex item
   * take up that free space equally on both sides, so the dashboard sits in
   * the middle of the screen the sidebar does not occupy — and on a narrow
   * window, where there is no free space, nothing moves.
   */
  margin: 0 auto;
  width: 100%;
}

/* The bar and the drawer exist for the phone only. */
.appbar { display: none; }
.sidenav-close { display: none; }
.sidenav-top { display: flex; align-items: center; justify-content: space-between; }
.navbackdrop[hidden] { display: none; }

@media (max-width: 860px) {
  .app { flex-direction: column; }

  /*
   * The top bar: menu, mark, where you are. Sticky, so the way into the
   * navigation is never scrolled away on a long feed.
   */
  .appbar {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    padding: 8px var(--s-3);
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 30;
  }
  .appbar-brand { display: inline-flex; text-decoration: none; }
  .appbar-brand .wordmark { display: none; }
  .appbar-title { font-size: 14px; font-weight: 600; color: var(--text-2); margin-left: 2px; }

  /*
   * The sidebar becomes a drawer from the left. Same column, same groups,
   * same meter and account controls as the desktop — nothing is hidden, it is
   * just off-canvas until asked for. `transform` rather than `left`, so the
   * slide is on the compositor and the page behind does not reflow.
   */
  .sidenav {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    height: 100dvh;
    width: min(300px, 84vw);
    flex: none;
    z-index: 50;
    overflow-y: auto;
    border-right: 1px solid var(--border);
    box-shadow: 0 0 0 0 transparent;
    transform: translateX(-100%);
    transition: transform 220ms cubic-bezier(0.3, 0, 0.2, 1), box-shadow 220ms ease;
    visibility: hidden;
  }
  body.nav-open .sidenav {
    transform: translateX(0);
    visibility: visible;
    box-shadow: 12px 0 40px rgba(0, 0, 0, 0.25);
  }
  .sidenav-close { display: inline-flex; }

  .navbackdrop {
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgba(16, 16, 26, 0.45);
    backdrop-filter: blur(1px);
  }
  body.nav-open { overflow: hidden; }

  .appmain { padding: var(--s-5) var(--s-4) 64px; }
}
@media (prefers-reduced-motion: reduce) {
  .sidenav { transition: none; }
}

/*
 * The wordmark goes before the navigation does.
 *
 * Restoring the footer above cost the link strip the width the footer now
 * takes. On a 390px screen the lockup was already spending 230px of it, which
 * left the links about forty and clipped "Overview" mid-word. Inside the
 * product the name is the least load-bearing thing on the bar — you know what
 * you opened — so the mark carries it alone until there is room for both.
 */
@media (max-width: 560px) {
  .appbar-title { max-width: 55vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* ═══════════════════════════════════════════════════════════ the pulse */

/*
 * A subreddit that is actually being polled.
 *
 * The product's whole claim is that something is watching while you are not
 * looking, and a static row makes that a promise on a page. A slow ring
 * expanding out of a dot is the smallest honest way to show a process running
 * — and it is slow (2.4s) on purpose: a fast pulse reads as an alarm, and this
 * is not an alarm, it is a heartbeat.
 */
.live {
  position: relative;
  display: inline-flex;
  width: 7px;
  height: 7px;
  flex: none;
}

/* --------------------------------------------------------- spinner */

/* A thin ring with one accented arc. Small enough to sit where the heartbeat
   dot sits, and CSS only, so it shows before any script arrives. */
.spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  flex: none;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  border-top-color: var(--ember);
  animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Reduced motion keeps the accented arc still; the ring alone still reads as
   "in progress" next to the resolving chip. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}
.live::before,
.live::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 999px;
  background: var(--pos);
}
.live::before {
  animation: ping 2.4s var(--ease) infinite;
  opacity: 0.65;
}
.live.paused::before { animation: none; }
.live.paused::after { background: var(--text-3); }
.live.err::before { animation: none; }
.live.err::after { background: var(--neg); }

@keyframes ping {
  0% { transform: scale(1); opacity: 0.6; }
  70% { transform: scale(2.8); opacity: 0; }
  100% { transform: scale(2.8); opacity: 0; }
}

/* The same signal as a word, for the rows that have room for one. */
.live-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--pos);
}
.live-label.paused { color: var(--text-3); }
.live-label.err { color: var(--neg); }

/* ═════════════════════════════════════════════════════ arrival & motion */

/*
 * Rows arrive rather than appear.
 *
 * Server-rendered HTML has no loading state to fill, so this is not a
 * spinner-replacement — it is the difference between a page that is *there*
 * and a page that *lands*. Staggered by index, capped at eight, because past
 * that the last row is waiting on an animation instead of being read.
 */
@keyframes rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

.rise > * {
  animation: rise var(--dur-3) var(--ease) both;
}
.rise > *:nth-child(1) { animation-delay: 0ms; }
.rise > *:nth-child(2) { animation-delay: 35ms; }
.rise > *:nth-child(3) { animation-delay: 70ms; }
.rise > *:nth-child(4) { animation-delay: 105ms; }
.rise > *:nth-child(5) { animation-delay: 140ms; }
.rise > *:nth-child(6) { animation-delay: 175ms; }
.rise > *:nth-child(7) { animation-delay: 210ms; }
.rise > *:nth-child(n + 8) { animation-delay: 245ms; }

/*
 * Nobody gets motion sickness from a dashboard on my account.
 *
 * `prefers-reduced-motion` is not a nicety here: the pulse below runs forever,
 * and an infinite animation is exactly what the setting exists to stop.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .live::before { display: none; }
}

/* --------------------------------------------------------- watch rows */

.watch-row .watch-count { text-align: right; flex: none; min-width: 56px; }
/* A resolving channel has no heartbeat yet; a ring spins where the dot would be. */
.watch-row > .spinner { margin: 0 6px 0 5px; }
.watch-row .watch-count .num {
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

/* The remove button stays out of the way until the row is under the cursor —
   a destructive control should not be the brightest thing in a list. */
.watch-row form button { opacity: 0.45; transition: opacity var(--dur-1) var(--ease), color var(--dur-1) var(--ease); }
.watch-row:hover form button { opacity: 1; }
.watch-row form button:hover { color: var(--neg); }

/* ═════════════════════════════════════════════════════════════ charts */

.chart { margin: 0; }
.chart-svg { width: 100%; height: auto; display: block; overflow: visible; }

/* Grid and axis are recessive by construction: they locate a value, they are
   not part of the picture. */
.chart-svg .grid { stroke: var(--border); stroke-width: 1; stroke-dasharray: 2 4; }
.chart-svg .axis-line { stroke: var(--border); stroke-width: 1; }
.chart-svg .axis { fill: var(--text-3); font-family: var(--mono); font-size: 9px; }
.chart-svg .zero { fill: var(--border); }

/* A column lifts slightly under the cursor, which is what makes the tooltip
   feel attached to something rather than floating over it. */
.chart-svg .col rect[fill]:not([fill='transparent']) {
  transition: opacity var(--dur-1) var(--ease);
}
.chart-svg:hover .col { opacity: 0.55; transition: opacity var(--dur-1) var(--ease); }
.chart-svg .col:hover { opacity: 1; }

/* ------------------------------------------------------------- legend */

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 15px;
  margin-top: var(--s-3);
  font-size: 12px;
  color: var(--text-3);
}
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-item .num { color: var(--text); font-weight: 600; }

.legend-swatch {
  width: 9px;
  height: 9px;
  border-radius: 3px;
  flex: none;
}
.legend-swatch.negative { background: var(--neg); }
.legend-swatch.neutral { background: var(--neu); }
.legend-swatch.positive { background: var(--pos); }
/* The same hatch as the chart, so the legend key and the mark are the one
   thing. Absence of a value, drawn as absence. */
.legend-swatch.unknown {
  background: repeating-linear-gradient(
    45deg,
    var(--surface-3) 0 2px,
    var(--border-strong) 2px 3.5px
  );
}

/* ---------------------------------------------------------- split bar */

.splitbar {
  display: flex;
  /* The gap is the secondary encoding. Teal and grey measure ΔE 12 side by
     side even to full colour vision; separated by surface, they never have to
     be told apart by hue. */
  gap: 2px;
  height: 10px;
}
.splitbar-seg {
  border-radius: 3px;
  transition: filter var(--dur-1) var(--ease);
}
.splitbar-seg:hover { filter: brightness(1.15); }
.splitbar-seg.negative { background: var(--neg); }
.splitbar-seg.neutral { background: var(--neu); }
.splitbar-seg.positive { background: var(--pos); }
.splitbar-seg.unknown {
  background: repeating-linear-gradient(45deg, var(--surface-3) 0 3px, var(--border-strong) 3px 4.5px);
}

/* ------------------------------------------------------ horizontal bars */

.hbar { display: block; text-decoration: none; }
.hbar-track {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.hbar-track > i {
  display: block;
  height: 100%;
  border-radius: 999px;
  /* One hue at varying length: this is magnitude, and a colour per row would
     encode nothing while demanding a legend to say so. */
  background: linear-gradient(90deg, var(--ember-dim), var(--ember));
  transition: width var(--dur-3) var(--ease), filter var(--dur-1) var(--ease);
}
.hbar:hover .hbar-track > i { filter: brightness(1.12); }
.hbar .num { color: var(--text-2); }
.hbar:hover .num { color: var(--text); }

/* --------------------------------------------------------------- heat */

.heat { display: grid; grid-template-columns: repeat(24, 1fr); gap: 3px; }
.heat-cell {
  aspect-ratio: 1;
  border-radius: 3px;
  background: var(--surface-3);
  transition: transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
/* Sequential: one hue, light to dark, mixed against the empty-cell colour so
   an empty hour and a quiet hour sit on the same ramp. */
.heat-cell[style*='--fill'] {
  background: color-mix(in srgb, var(--ember) var(--fill), var(--surface-3));
}
.heat-cell:hover { transform: scale(1.28); box-shadow: var(--e-2); }
/* The busiest hour is ringed as well as filled — the peak is the answer the
   chart exists to give, and it should not depend on comparing 24 shades. */
.heat-cell.peak { box-shadow: 0 0 0 1.5px var(--ember); }

/* ═══════════════════════════════════════════════════════════════ panels */

/*
 * The tray.
 *
 * The single most useful thing in the reference: a titled outer container
 * holding rows or cards that are *visibly inside it*. Not a card with a
 * heading — a container with its own header row, its own inset body, and a
 * radius larger than the things it holds, so the nesting reads as depth rather
 * than as two rectangles that happen to be near each other.
 *
 * Everything in this product that used to be `<div class="card"><h3>` is one
 * of these.
 */
.panel {
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: var(--r-xl);
  overflow: hidden;
}

.panel-head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 13px var(--s-4);
  border-bottom: 1px solid var(--hair);
}
.panel-head h3 {
  font-family: var(--font);
  font-size: 13.5px;
  font-weight: 560;
  letter-spacing: -0.005em;
  color: var(--text);
}
.panel-head .panel-note {
  margin-left: auto;
  font-size: 11.5px;
  color: var(--text-3);
}

.panel-body { padding: var(--s-4); }
.panel-body.flush { padding: 0; }

/*
 * A list inside a tray.
 *
 * Hairlines between rows, none at the ends — the tray's own border closes it.
 * Rows are tall on purpose: the reference runs 44–56px, and the density that
 * feels professional is fewer things with room, not more things packed.
 */
.panel-rows { display: flex; flex-direction: column; }
.panel-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 12px var(--s-4);
  border-top: 1px solid var(--hair);
  text-decoration: none;
  color: inherit;
  transition: background var(--dur-1) var(--ease);
}
.panel-rows > .panel-row:first-child { border-top: 0; }
a.panel-row:hover { background: var(--surface-2); }

/*
 * A card nested inside a tray.
 *
 * Smaller radius than its container, and a fill that steps *up* from it, which
 * is the whole trick: a lighter inner surface on a darker outer one is read as
 * closer, and no border is needed to say so.
 */
.subcard {
  background: var(--surface-2);
  border: 1px solid var(--hair);
  border-radius: var(--r);
  padding: var(--s-3) 14px;
}

/* ------------------------------------------------------- inline chips */

/*
 * A bordered token that sits inside a sentence.
 *
 * The reference uses these for everything — a tool name mid-paragraph, AND
 * between two conditions, a warning beside a label. They work because the
 * border is the accent and the fill is a 14% tint of it, so the chip carries
 * colour without becoming a solid block that outshouts the text around it.
 */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 7px;
  border-radius: var(--r-sm);
  border: 1px solid var(--hair-strong);
  background: var(--surface-2);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-2);
  white-space: nowrap;
  vertical-align: baseline;
}
.chip.accent { border-color: color-mix(in srgb, var(--ember) 45%, transparent); background: var(--ember-wash); color: var(--ember); }
.chip.warn { border-color: color-mix(in srgb, var(--ember) 40%, transparent); background: var(--ember-wash); color: var(--ember); }
.chip.good { border-color: color-mix(in srgb, var(--pos) 40%, transparent); background: var(--pos-wash); color: var(--pos); }
.chip.bad { border-color: color-mix(in srgb, var(--neg) 40%, transparent); background: var(--neg-wash); color: var(--neg); }
.chip.mono { font-family: var(--mono); font-size: 11.5px; }

/* ------------------------------------------------------- status dots */

/*
 * Filled means running, hollow means idle.
 *
 * Straight from the reference's agent list, and it is better than a coloured
 * label because it survives at 7px and never needs translating. The pulse
 * defined earlier rides on top of the filled one.
 */
/*
 * `sdot`, not `dot`.
 *
 * `.dot` was already taken — a 3px separator between the meta items on a
 * mention and on the landing page — and defining a 7px one later in the file
 * silently turned every separator on the site into a fat circle. Caught by
 * counting elements in the rendered HTML rather than by looking, which is the
 * only way that class of collision ever shows up.
 */
.sdot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  flex: none;
  background: var(--text-3);
}
.sdot.on { background: var(--pos); }
.sdot.idle { background: transparent; box-shadow: inset 0 0 0 1.5px var(--text-3); }
.sdot.bad { background: var(--neg); }

/* -------------------------------------------------------- the canvas */

/*
 * A dot grid behind the work area.
 *
 * Barely visible on purpose — it is not decoration, it gives the eye something
 * to measure the cards against, which is why the reference's canvas has one
 * and why a flat background makes floating panels look unplaced.
 */
.appmain {
  background-image: radial-gradient(circle at 1px 1px, var(--hair) 1px, transparent 0);
  background-size: 22px 22px;
  background-attachment: local;
}

/* A mention that lives inside a panel: the panel already draws the border, the
   radius and the separation, so the row draws none of them. */
.mention.flat {
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  border-top: 1px solid var(--hair);
}
.panel-rows > .mention.flat:first-child { border-top: 0; }
.mention.flat:hover { transform: none; box-shadow: none; background: var(--surface-2); }

/* ------------------------------------------------------------ toolbar */

/*
 * The filter row.
 *
 * A toolbar, not a card with controls dropped into it. It sits *on* the page
 * rather than floating above it — inset fill, no shadow — because a filter bar
 * that looks like a card competes with the results it is filtering. The fields
 * inside it step back up to --surface, so they read as the raised things within
 * the recess rather than as more of it.
 *
 * Two bands: the controls, then the actions. The divider is what makes the
 * second one a band rather than a stray line of buttons — before it, Filter and
 * Clear shared the controls' wrap and landed wherever the line happened to
 * break.
 */
.toolbar {
  display: flex;
  flex-direction: column;
  margin-bottom: var(--s-4);
  background: var(--surface-2);
  border: 1px solid var(--hair);
  border-radius: var(--r);
  /* Not `overflow: hidden`: the source picker's menu opens past the toolbar's
     bottom edge and was being clipped to the box. The actions row shares the
     toolbar's background, so nothing needs the clip for its corners. */
}

/*
 * A grid, not a wrap.
 *
 * Six controls of six different intrinsic widths in a flex-wrap line up along
 * their left edge and nowhere else: every row breaks at a different place, and
 * the ragged right edge is most of what read as unfinished. `auto-fit` over a
 * fixed minimum gives columns instead — the count changes with the width, but
 * whatever is on row two lands underneath something on row one.
 *
 * The two wide controls claim two columns each. The search field because it is
 * the one people type in, and the date range because two native date inputs and
 * a separator will not fit in a column sized for a select.
 */
.toolbar-controls {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(172px, 1fr));
  gap: var(--s-2);
  padding: 11px;
}
.toolbar-controls > .field-search,
.toolbar-controls > .field-range { grid-column: span 2; }

/* Below two columns the spans would overflow their own grid. */
@media (max-width: 560px) {
  .toolbar-controls { grid-template-columns: 1fr; }
  .toolbar-controls > .field-search,
  .toolbar-controls > .field-range { grid-column: span 1; }
}

.toolbar-actions {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  padding: 11px;
  border-top: 1px solid var(--hair);
  background: var(--surface-2);
}
/* The count is about the state of the bar, not about either button, so it sits
   at the far end rather than trailing Clear like a caption. */
.toolbar-count { margin-left: auto; font-size: 12px; color: var(--text-3); }

/* ---------------------------------------------------------------- fields */

/*
 * One control, six shapes.
 *
 * A select, a search box, a pair of dates and a checkbox each compute their own
 * height from padding and font-size, and left alone they stair-step by two or
 * three pixels — the amount that reads as "off" without being visibly wrong.
 * Height, radius, fill and border are declared once here and every control in
 * the bar takes them.
 */
.toolbar .field,
.toolbar select,
.toolbar input[type="search"] {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  min-width: 0;
  height: 36px;
  padding: 0 10px;
  font-size: 13.5px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--hair-strong);
  border-radius: var(--r-sm);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.toolbar select { padding-right: 26px; }
/* The picker is dressed as the toolbar's other controls, not as a page select. */
.toolbar .picker > summary {
  height: 36px;
  padding: 0 10px;
  font-size: 13.5px;
  background: var(--surface);
  border-color: var(--hair-strong);
}
.toolbar .picker.on > summary, .toolbar .picker[open] > summary { border-color: var(--ember); }
.toolbar .field:hover,
.toolbar select:hover,
.toolbar input[type="search"]:hover { border-color: var(--border-strong); }

/*
 * A filter that is doing something looks like it.
 *
 * This is the whole reason the bar is readable at a glance: without it, a
 * subreddit filter set three days ago and forgotten is visually identical to
 * one that says "All subreddits", and the only clue that a near-empty list is
 * your own doing is the count down in the actions band. The class is written
 * server-side from the same value that populates the control, so it cannot
 * disagree with what the control says.
 *
 * The border carries it alone. An ember fill and ember text were tried first
 * and are too much: four filters is a normal number, and four filled controls
 * out of six turns a bar that is merely doing its job into something that reads
 * as an error. The value in the control already says what it is — "Positive"
 * against "Any sentiment" — so the marker only has to be found, not shouted.
 */
.toolbar .on,
.toolbar .field:has(input:checked) {
  border-color: color-mix(in srgb, var(--ember) 55%, transparent);
}
.toolbar .on:hover,
.toolbar .field:has(input:checked):hover {
  border-color: color-mix(in srgb, var(--ember) 75%, transparent);
}

.toolbar .field:focus-within,
.toolbar select:focus-visible,
.toolbar input[type="search"]:focus-visible {
  outline: 0;
  border-color: var(--ember);
  box-shadow: var(--ring);
}

/* --------------------------------------------------------------- search */

/*
 * The magnifier is inside the field rather than beside it, which is the only
 * arrangement that survives the grid: an icon in its own element would be a
 * column, and the field would stop lining up with everything under it.
 */
.field-search { position: relative; padding: 0 !important; }
.field-search svg {
  position: absolute;
  left: 10px;
  width: 15px;
  height: 15px;
  color: var(--text-3);
  pointer-events: none;
}
.toolbar .field-search input[type="search"] {
  height: 100%;
  padding: 0 10px 0 32px;
  border: 0;
  background: transparent;
  border-radius: inherit;
}
.toolbar .field-search input[type="search"]:hover { border-color: transparent; }
.toolbar .field-search input[type="search"]:focus-visible { box-shadow: none; border-color: transparent; }
.field-search:focus-within svg { color: var(--ember); }

/* ----------------------------------------------------------- date range */

/*
 * Two dates as one field.
 *
 * Every other control here carries its own label inside it — "All subreddits",
 * "Any sentiment", "Search text…" — so two bare inputs with the words "From"
 * and "to" loose beside them were the only thing the eye had to assemble out of
 * parts. The border belongs to the group; the inputs inside have none, and the
 * arrow between them says what the two ends are. "From" and "to" survive as
 * `aria-label`s, where they are doing real work.
 */
.field-range { justify-content: center; gap: 1px; }
.toolbar .field-range input[type="date"] {
  width: auto;
  min-width: 0;
  flex: 0 1 auto;
  height: 100%;
  padding: 0 2px;
  border: 0;
  background: transparent;
  border-radius: 0;
  font-family: var(--mono);
  font-size: 12px;
  color: inherit;
}
.toolbar .field-range input[type="date"]:focus-visible { outline: 0; box-shadow: none; }
/* An empty date is a placeholder, not a value, and recedes like one — except
   inside an active range, where the whole field is already ember. */
.toolbar .field-range input[type="date"]:not(:valid) { color: var(--text-3); }
.field-range .sep { flex: none; font-size: 11px; color: var(--text-3); }

/* The native indicator is drawn in black and disappears against a dark fill. */
.toolbar .field-range input[type="date"]::-webkit-calendar-picker-indicator {
  margin: 0;
  padding: 0;
  opacity: 0.55;
  cursor: pointer;
  filter: invert(1);
}
.toolbar .field-range input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
:root[data-theme="light"] .toolbar .field-range input[type="date"]::-webkit-calendar-picker-indicator {
  filter: none;
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) .toolbar .field-range input[type="date"]::-webkit-calendar-picker-indicator {
    filter: none;
  }
}

/* --------------------------------------------------------------- toggle */

/*
 * The checkbox is a field like the rest, so it sits in a grid cell without
 * being the one short thing in the row. The native input stays — it is the
 * label, the hit target and the keyboard behaviour, all of which a styled div
 * would have to reimplement badly.
 */
.field-toggle { cursor: pointer; user-select: none; font-size: 13px; }
.toolbar .field-toggle input[type="checkbox"] {
  width: 15px;
  height: 15px;
  flex: none;
  margin: 0;
  accent-color: var(--ember);
}

/* ---------------------------------------------- keyword scope checkboxes */

/*
 * The same idea as .field-toggle, sized as a chip rather than a field, because
 * these come in lists of ten rather than one to a grid cell.
 */
.checks { display: flex; flex-wrap: wrap; gap: var(--s-2); }

.check {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-2);
  cursor: pointer;
  user-select: none;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}
.check:hover { border-color: var(--border-strong); color: var(--text); }

/* Width is set explicitly: the checkbox inherits the 100% that the input rules
   above give every field, and without this it stretches across the chip. */
.check input[type="checkbox"] { width: auto; margin: 0; accent-color: var(--ember); flex: none; }

/* :has, so the whole chip lights up rather than just the box inside it. */
.check:has(input:checked) {
  border-color: color-mix(in srgb, var(--ember) 45%, transparent);
  background: var(--ember-wash);
  color: var(--ember);
}
.check:has(input:disabled) { opacity: 0.5; cursor: not-allowed; }

/* A scope pointing at a subreddit that is no longer watched. Kept visible
   precisely because it does nothing — unticking it is how it goes away. */
.check.stale { border-style: dashed; }
.check.stale .tiny { color: var(--text-3); }

/* A fieldset is the right element for a checkbox group and the wrong one by
   default: the browser's border, padding and min-width all have to go before it
   can just be a heading over some chips. */
.field-group { border: 0; padding: 0; margin: 0; min-width: 0; }
.field-group > legend {
  padding: 0;
  margin-bottom: 7px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-2);
}

.scope { margin-top: 7px; }
.scope > summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-3);
  cursor: pointer;
  list-style: none;
  border-radius: var(--r-sm);
}
.scope > summary::-webkit-details-marker { display: none; }
.scope > summary::before {
  content: '';
  width: 0;
  height: 0;
  /* A triangle from borders rather than a glyph, so it rotates cleanly and
     does not change size with the font. */
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform var(--dur-1) var(--ease);
}
.scope[open] > summary::before { transform: rotate(90deg); }
.scope > summary:hover { color: var(--text-2); }
.scope[open] > summary { color: var(--text-2); margin-bottom: var(--s-2); }

.scope-form {
  padding: var(--s-3);
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}


/* ═══════════════════════════════════════════════════ the time series */

/*
 * The chart panel while a new range is on its way.
 *
 * The same treatment the subreddit switch gets — dimmed and inert, so a second
 * click cannot race the first. Deliberately no spinner: the fragment is one
 * query, and something that appears and vanishes inside a hundred milliseconds
 * reads as a flicker rather than as progress.
 */
[data-swap][aria-busy="true"] { opacity: 0.6; pointer-events: none; }

/*
 * And the new one arrives rather than appears.
 *
 * The swap used to be a bare `innerHTML` assignment: one frame the old chart,
 * the next a completely different set of lines, with nothing connecting them.
 * At the size this panel is, that reads as a glitch rather than as an answer.
 *
 * The dim above is the wait and this is the arrival, so the two are separate
 * rules — and note that this is a *reveal*, not a skeleton. A skeleton belongs
 * to a first load with nothing on screen; here there is always a previous
 * chart, and swapping real data for a grey box on the way to different real
 * data would be a downgrade dressed as a loading state.
 *
 * `is-entering` kills the transition so the incoming markup snaps to hidden;
 * src/client/swap.ts forces the layout and drops the class, and only then is
 * there something to transition from.
 */
[data-swap] {
  transition: opacity var(--dur-2) var(--ease), filter var(--dur-2) var(--ease);
}

[data-swap].is-entering {
  transition: none;
  opacity: 0;
  filter: blur(3px);
}

/* The blur is the part that costs something to read; without motion the chart
   should simply be the new chart. */
@media (prefers-reduced-motion: reduce) {
  [data-swap] { transition: none; }
  [data-swap].is-entering { filter: none; }
}

.ts { position: relative; }

.ts-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-3);
}

/*
 * A legend key is also the control that hides its series.
 *
 * These were checkboxes driving CSS, which was instant and could not survive a
 * stacked bar — hiding a middle layer leaves the ones above it floating over a
 * gap. They are links now, and the state lives in the query string: correct in
 * both chart modes, kept when the range or the chart type changes, and a
 * filtered view is something you can send to somebody.
 */
.ts-key {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 10px 5px 8px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: 12px;
  color: var(--text-2);
  text-decoration: none;
  user-select: none;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
    opacity var(--dur-1) var(--ease);
}
.ts-key:hover { border-color: var(--hair-strong); color: var(--text); }
.ts-key .ts-swatch { width: 9px; height: 9px; border-radius: 3px; flex: none; }
.ts-key .ts-total { color: var(--text); font-weight: 600; font-size: 11.5px; }

/*
 * A series carries its colour as `--s`, set inline by the server, because the
 * keys are no longer a closed set: split by source and the series are whatever
 * the workspace watches. A series with no colour — unclassified, everything
 * else — is `.hatch`, and the hatch is reserved for an absence.
 */
.ts-key .ts-swatch { background: var(--s); }
.ts-key.hatch .ts-swatch {
  background: repeating-linear-gradient(45deg, var(--surface-3) 0 2px, var(--border-strong) 2px 3.5px);
}
.ts-key .picon { color: var(--s); flex: none; }

/* Switched off: the swatch hollows out rather than fading to nothing — the
   same filled-means-on language the status dots use. */
.ts-key.off { opacity: 0.5; }
.ts-key.off .ts-swatch {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--text-3);
}
.ts-key.off .ts-total { color: var(--text-3); font-weight: 500; }

/*
 * Lines, and no fills.
 *
 * The overlaid translucent areas this replaced were the compromise between two
 * charts: three fills piled on one baseline made the small series unreadable
 * and the whole thing look like a smear. An area chart earns its fill when
 * there is one series and loses it when there are four.
 */
.ts-line { stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; fill: none; }
.ts-end { stroke: var(--surface); stroke-width: 2; }

.ts-series .ts-line { stroke: var(--s); }
.ts-series.hatch .ts-line { stroke: var(--border-strong); stroke-dasharray: 3 3; }

.ts-series .ts-end { fill: var(--s); }
.ts-series.hatch .ts-end { fill: var(--border-strong); }

/* Stacked bars: 2px of surface between segments, so adjacent teal and grey
   never have to be told apart by hue. Same rule as everywhere else here. */
.ts-bar { fill: var(--s); }
.ts-bar.hatch { fill: url(#hatch); }

/* No hover-dimming of the other columns here: the hit targets are a separate
   sibling layer from the bars, so `:has` would dim the hovered column along
   with the rest. The crosshair and the tooltip already say which one it is. */

/* The crosshair appears only under the cursor's own column. */
.ts-crosshair { stroke: var(--hair-strong); stroke-width: 1; opacity: 0; }
.ts-hit:hover .ts-crosshair { opacity: 1; }

/* ------------------------------------------------------- range picker */

.range-picker { position: relative; display: inline-flex; gap: 2px; padding: 2px; background: var(--surface-2); border: 1px solid var(--hair); border-radius: 999px; }
.range-picker a {
  position: relative;
  z-index: 1;
  padding: 4px 11px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-3);
  text-decoration: none;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.range-picker a:hover { color: var(--text); }
.range-picker a.current { background: var(--surface); color: var(--text); box-shadow: inset 0 0 0 1px var(--hair); }

/*
 * The pill that slides between options — see src/client/tabs.ts.
 *
 * It is created by the island, never rendered by the server, and the static
 * background above is only turned off once it exists. That order matters: a
 * script that fails to load must leave a picker with its active option still
 * marked, not a bar with nothing selected on it.
 *
 * Pinned at `left: 0` and moved entirely by `translateX`, which the island
 * measures as the gap between the picker's left edge and the active link's.
 * Anchoring it at the 2px padding instead would double-count that padding,
 * because the measured distance already contains it.
 */
.range-picker.has-pill a.current { background: transparent; box-shadow: none; }

.rp-pill {
  position: absolute;
  left: 0;
  top: 2px;
  bottom: 2px;
  z-index: 0;
  width: 0;
  border-radius: 999px;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--hair);
  pointer-events: none;
  will-change: transform, width;
  transition: transform var(--dur-2) cubic-bezier(0.22, 1, 0.36, 1),
    width var(--dur-2) cubic-bezier(0.22, 1, 0.36, 1);
}

@media (prefers-reduced-motion: reduce) {
  .rp-pill { transition: none; }
}

/*
 * The credits line, under the mention meter.
 *
 * Deliberately not a second progress bar. Two bars of equal weight say the
 * product has two currencies; mentions are the promise the plan makes, and
 * credits are what the API and the model work draw from. A single row of
 * numerals reads as a reading rather than as a second limit to worry about.
 */
.creditline {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 9px;
  padding-top: 9px;
  border-top: 1px solid var(--hair);
  font-size: 12px;
  color: var(--text-2);
}
.creditline .icon { opacity: 0.6; }
.creditline .num { color: var(--text); }

/* ------------------------------------------------------------------ api */

/*
 * The one-time secret.
 *
 * Ember-washed rather than green: this is not a success message, it is the
 * only moment the plaintext exists. The colour is doing the same job it does
 * on the primary action — "this is the thing to act on" — and a reassuring
 * green would say the opposite.
 */
.revealkey { border-color: var(--ember-dim); background: var(--ember-wash); }
.revealkey .icon { color: var(--ember); }

.secretrow {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 9px 10px;
}
.secretrow .secret {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  white-space: nowrap;
  font-size: 12.5px;
  /* Selectable in one gesture for anybody who reaches for the mouse before
     the button. */
  user-select: all;
}
.btn.ok { color: var(--pos); border-color: var(--pos); }

.keylist { display: flex; flex-direction: column; gap: 2px; }
.keyrow {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 0;
  border-bottom: 1px solid var(--hair);
}
.keyrow:last-child { border-bottom: 0; }
.keyname { font-weight: 550; font-size: 14px; }
.btn.danger:hover { color: var(--neg); border-color: var(--neg); }

/* The price list beside the balance, so "how long will these last" is
   arithmetic the customer can actually do. */
.pricelist {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--hair);
  font-size: 12.5px;
}
.priceitem { display: flex; justify-content: space-between; gap: 10px; padding: 4px 0; }

/* Any table that could outgrow its column. Scrolls itself rather than the
   page — a dashboard whose body scrolls sideways has lost its nav. */
.scrolltable { overflow-x: auto; }
table.ledger { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 420px; }
table.ledger th {
  text-align: left;
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 0 10px 8px 0;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
table.ledger td { padding: 8px 10px 8px 0; border-bottom: 1px solid var(--hair); }
table.ledger tr:last-child td { border-bottom: 0; }

.banner-bad {
  display: flex;
  align-items: center;
  gap: 8px;
  border-color: var(--neg);
  color: var(--neg);
  background: var(--neg-wash);
}

/* ============================================================ API docs */

/*
 * Two columns, and the split is the whole idea: prose reads top-to-bottom
 * while code stays put beside it. A reader is understanding an endpoint and
 * copying a call at the same time, and one column makes them scroll between
 * the two.
 */
.docs { display: flex; min-height: 100vh; }

.docnav {
  flex: none;
  width: 248px;
  border-right: 1px solid var(--border);
  background: var(--surface);
  padding: var(--s-4) 0 var(--s-4) var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}
.docnav-brand { display: block; padding-right: var(--s-4); }
.docnav nav { display: flex; flex-direction: column; gap: var(--s-4); flex: 1 1 auto; }
.docnav-group { display: flex; flex-direction: column; gap: 1px; }

.docnav-section {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
  text-decoration: none;
  padding: 4px 0 5px;
  font-family: var(--mono);
}
.docnav-section:hover { color: var(--text-2); }

.docnav-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-2);
  text-decoration: none;
  padding: 4px 10px 4px 0;
  border-left: 2px solid transparent;
  padding-left: 9px;
  margin-left: -11px;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
}
.docnav-item:hover { color: var(--text); background: var(--surface-2); }
.docnav-item.on { color: var(--ember); border-left-color: var(--ember); background: var(--ember-wash); }

/* The method, as a fixed-width tag so the titles beside them line up. */
.dot-method {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.03em;
  width: 26px;
  flex: none;
  text-align: right;
  color: var(--text-3);
}
.dot-method.get { color: var(--pos); }
.dot-method.post { color: var(--ember); }
.dot-method.delete { color: var(--neg); }

.docnav-foot { display: flex; gap: 6px; padding-right: var(--s-4); }

.docmain { flex: 1 1 auto; min-width: 0; padding: var(--s-6) var(--s-6) 120px; max-width: 1180px; }
.docheader { max-width: 720px; margin-bottom: var(--s-6); }
.docheader h1 { font-size: 32px; margin: 0 0 10px; }
.docheader .lede { color: var(--text-2); font-size: 16px; margin: 0 0 var(--s-5); }

/* The key box leads, because every Test button below is inert without it and
   a reader who scrolls past will click one, get a 401, and conclude the API
   is broken. */
.keybox {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--s-3);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.keybox label { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-2); margin: 0; }
.keybox input { flex: 1 1 auto; font-family: var(--mono); font-size: 12.5px; }
.keybox p { margin: 0; }

.docsection { margin-bottom: var(--s-7, 56px); scroll-margin-top: 20px; }
.docsection > h2 {
  font-size: 22px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
  margin: 0 0 var(--s-4);
}
.prose { max-width: 720px; display: flex; flex-direction: column; gap: 11px; }
.prose p { color: var(--text-2); font-size: 14.5px; line-height: 1.65; margin: 0; }

/* One endpoint: description left, code right. */
.endpoint {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s-5);
  padding: var(--s-5) 0;
  border-bottom: 1px solid var(--hair);
  scroll-margin-top: 20px;
  align-items: start;
}
.endpoint:last-child { border-bottom: 0; }
.ep-left { min-width: 0; }
.ep-left h3 { font-size: 17px; margin: 0 0 10px; display: flex; align-items: center; gap: 7px; }

.anchor { color: var(--text-3); opacity: 0; transition: opacity var(--dur-1) var(--ease); line-height: 0; }
.endpoint:hover .anchor, .anchor:focus-visible { opacity: 1; }

.ep-sig { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.ep-sig code { font-size: 13px; color: var(--text); }

.method {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.05em;
  padding: 2px 7px;
  border-radius: var(--r-sm);
}
.method.get { color: var(--pos); background: var(--pos-wash); }
.method.post { color: var(--ember); background: var(--ember-wash); }
.method.delete { color: var(--neg); background: var(--neg-wash); }

.costchip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--text-3);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 8px;
}
.costchip.free { color: var(--pos); border-color: var(--pos); }

.ep-desc { color: var(--text-2); font-size: 14.5px; line-height: 1.65; margin: 0 0 var(--s-4); }

.paramgroup { margin-bottom: var(--s-4); }
.paramhead {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0 0 10px;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--hair);
  font-weight: 500;
}
.params { margin: 0; display: flex; flex-direction: column; }
.params dt { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.params dt:first-child { margin-top: 0; }
.params dd { margin: 4px 0 0; font-size: 13.5px; color: var(--text-2); line-height: 1.6; }
.pname { font-size: 12.5px; color: var(--text); font-weight: 550; }
.ptype { font-family: var(--mono); font-size: 11px; color: var(--text-3); }
.preq {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--ember);
}

/* The code column sticks, so it is still beside the prose that describes it
   after a long parameter list. */
.ep-right { position: sticky; top: var(--s-4); min-width: 0; }

.codecard {
  border: 1px solid var(--border);
  border-radius: var(--r);
  overflow: hidden;
  background: var(--surface-2);
}
.codehead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 8px 6px 10px;
  background: var(--surface-3);
  border-bottom: 1px solid var(--border);
  min-height: 38px;
}
.resphead { border-top: 1px solid var(--border); }
.respname {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}

.tabs { display: flex; gap: 2px; }
.tab {
  font-family: inherit;
  font-size: 12px;
  color: var(--text-3);
  background: transparent;
  border: 0;
  padding: 4px 9px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.tab:hover { color: var(--text-2); }
.tab.on { color: var(--text); background: var(--surface); }

.codeblock {
  margin: 0;
  padding: 13px 14px;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.7;
  color: var(--text-2);
  /* Wide code scrolls inside its own box; the page body never scrolls
     sideways, because a dashboard whose body scrolls sideways has lost its
     nav. */
  max-height: 340px;
  overflow-y: auto;
}
.codeblock.hidden { display: none; }
.codeblock code { font-family: inherit; font-size: inherit; white-space: pre; }
.codeblock.resp { background: var(--surface); }

/* A response that came back from the reader's own workspace, rather than the
   example. Marked, because otherwise there is no way to tell whether Test
   did anything. */
.codeblock.live { border-left: 2px solid var(--pos); }
.codeblock.live.bad { border-left-color: var(--neg); }

.runbtn { gap: 5px; }

@media (max-width: 1080px) {
  .endpoint { grid-template-columns: minmax(0, 1fr); }
  .ep-right { position: static; }
}
@media (max-width: 820px) {
  .docnav {
    position: static;
    height: auto;
    width: 100%;
    border-right: 0;
    border-bottom: 1px solid var(--border);
    padding: var(--s-3);
  }
  .docs { flex-direction: column; }
  .docmain { padding: var(--s-4) var(--s-4) 80px; }
}

/* -------------------------------------------------- share of voice */

/*
 * One stacked bar, because share of voice is a division of one whole. Separate
 * bars per brand would read as absolute volumes, which is what the time series
 * above it already shows.
 */
.sovbar {
  display: flex;
  height: 10px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--surface-3);
  gap: 2px;
}
.sovbar > i { display: block; height: 100%; min-width: 2px; }
/* Ember for us, neutral greys for them. The accent is already "the thing to
   look at" everywhere else in the product, and here the thing to look at is
   your own share. */
.sovbar > i.mine { background: var(--ember); }
.sovbar > i.theirs { background: var(--text-3); }
.sovbar > i.theirs:nth-of-type(3) { background: var(--neu); }
.sovbar > i.theirs:nth-of-type(4) { background: var(--surface-3); box-shadow: inset 0 0 0 1px var(--border-strong); }

.sovlist { display: flex; flex-direction: column; margin-top: 14px; }
.sovrow {
  display: grid;
  grid-template-columns: 9px minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid var(--hair);
  font-size: 13.5px;
}
.sovrow:last-child { border-bottom: 0; }

.sovdot { width: 9px; height: 9px; border-radius: 3px; }
.sovdot.mine { background: var(--ember); }
.sovdot.theirs { background: var(--text-3); }

.sovlabel { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sovpct { font-size: 14px; font-weight: 550; min-width: 46px; text-align: right; }
.sovn { min-width: 40px; text-align: right; }

.sovdelta { display: inline-flex; align-items: center; gap: 3px; min-width: 62px; justify-content: flex-end; }
.sovdelta.up { color: var(--pos); }
.sovdelta.down { color: var(--neg); }
.sovdelta.flat { color: var(--text-3); }

.sov-empty .icon { color: var(--text-3); }

/* Role chips on the keywords page. Only "competitor" is coloured: it is the
   one that changes what a number means, and colouring all three would make
   the list read as three categories of equal weight. */
.chip.role-competitor { color: var(--ember); background: var(--ember-wash); }
.chip.role-topic { color: var(--text-3); }

/* ------------------------------------------------------ keyboard triage */

/*
 * The cursor row.
 *
 * A left rail rather than a background tint or an outline. The feed already
 * uses a background shift for unread and a border for the card itself, so a
 * third treatment on either of those would be read as a variation of them.
 * A rail is unambiguously "you are here".
 */
.mention.cursor {
  position: relative;
  background: var(--surface-2);
}
.mention.cursor::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--ember);
  border-radius: 0 2px 2px 0;
}
/* The island focuses the row so screen readers announce it; the browser's own
   focus ring on top of the rail is two things saying the same thing. */
.mention.cursor:focus { outline: none; }
.mention.cursor:focus-visible { outline: 2px solid var(--ember); outline-offset: -2px; }

.sheet {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 60;
  padding: var(--s-4);
}
.sheet[hidden] { display: none; }

.sheet-card {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r);
  padding: var(--s-4);
  width: min(380px, 100%);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
}

.keys { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 10px 16px; align-items: center; }
.keys dt { display: flex; align-items: center; gap: 5px; }
.keys dd { margin: 0; font-size: 13.5px; color: var(--text-2); }
.keysep { font-size: 11px; color: var(--text-3); }

kbd {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--text);
  background: var(--surface-3);
  border: 1px solid var(--border);
  /* The bottom edge darker than the others, so it reads as a key with a
     little height rather than as a tag. */
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  white-space: nowrap;
}

/* ---------------------------------------------------------- live badge */

/*
 * Floating rather than in the flow.
 *
 * The pill appears while somebody is already reading, and a badge that pushes
 * the list down the moment it arrives moves the row under their cursor — which
 * is precisely the thing this feature exists to avoid.
 */
.livepill {
  position: sticky;
  top: var(--s-3);
  z-index: 20;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0 auto var(--s-3);
  padding: 6px 14px;
  border: 1px solid var(--ember);
  background: var(--ember);
  color: var(--on-ember);
  border-radius: 999px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 550;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);
  /* Centres itself without the parent needing to become a flex container,
     which would relayout the panel underneath it. */
  left: 50%;
  transform: translateX(-50%);
  width: fit-content;
}
.livepill[hidden] { display: none; }
.livepill:hover { background: var(--ember-hot); border-color: var(--ember-hot); }
.livepill .num { color: inherit; }

@media (prefers-reduced-motion: no-preference) {
  .livepill { animation: livein var(--dur-2) var(--ease); }
  @keyframes livein {
    from { opacity: 0; transform: translateX(-50%) translateY(-6px); }
    to { opacity: 1; transform: translateX(-50%) translateY(0); }
  }
}

/* --------------------------------------------------- relevance feedback */

.banner.suggest {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--s-3);
  border-color: var(--border-strong);
}
.banner.suggest .icon { color: var(--ember); flex: none; }
.banner.suggest strong { color: var(--text); font-weight: 600; }

/* A pressed state on a ghost button, for the "Noise" toggle. Muted rather
   than ember: this is a fact about a row the customer has already dealt with,
   not somewhere to look. */
.btn.btn-ghost.on {
  color: var(--text);
  background: var(--surface-3);
  border-color: var(--border-strong);
}

/* -------------------------------------------------------- keyword health */

.kwhealth {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 7px;
  font-size: 12px;
  color: var(--text-3);
}
.kwhealth .icon { opacity: 0.7; }
/* Ember, not red. A noisy keyword is a thing to tune, not a fault — and the
   product is the one that let it get noisy. */
.kwhealth.loud { color: var(--ember); }
.kwhealth.loud .icon { opacity: 1; }
.kwhint {
  margin-left: 4px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--ember-wash);
  font-size: 11px;
}

/* ------------------------------------------------------- views and bulk */

.viewrail {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: var(--s-3);
}

.viewchip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: 12.5px;
  overflow: hidden;
}
.viewchip > a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  color: var(--text-2);
  text-decoration: none;
}
.viewchip:hover { border-color: var(--border-strong); }
.viewchip:hover > a { color: var(--text); }
.viewchip.on { border-color: var(--ember); background: var(--ember-wash); }
.viewchip.on > a { color: var(--ember); }

.viewx {
  border: 0;
  background: transparent;
  color: var(--text-3);
  cursor: pointer;
  padding: 5px 8px 5px 2px;
  line-height: 0;
}
.viewx:hover { color: var(--neg); }

.viewsave { display: inline-flex; gap: 6px; align-items: center; }
.viewsave input { width: 150px; font-size: 12.5px; padding: 4px 9px; }

/*
 * The action bar sticks to the top of the list rather than the bottom of the
 * screen. What it acts on is above it and below it; anchoring to the viewport
 * puts the verb a long way from the nouns.
 */
.bulkbar {
  position: sticky;
  top: 0;
  z-index: 15;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 9px 12px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-strong);
}
.bulkbar[hidden] { display: none; }
.bulkcount { font-size: 13px; color: var(--text-2); }

.bulkrow {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding-left: 12px;
  cursor: default;
}
.bulkrow > .mention { flex: 1 1 auto; min-width: 0; }
.bulkrow > input[type="checkbox"] { margin-top: 18px; flex: none; width: auto; }
/* Keeps the cards aligned when a locked row has no checkbox to occupy the
   column. */
.bulkspacer { width: 13px; flex: none; }
.bulkrow.picked { background: var(--ember-wash); }

/* ------------------------------------------------------------ confetti */

/*
 * It falls from above and it happens once — see src/client/confetti.ts.
 *
 * The pieces are `<i>` elements with no content, positioned by custom
 * properties the island rolls per piece. Keeping the animation here rather
 * than in the Web Animations API means the motion sits with the rest of the
 * product's motion, and the reduced-motion guard below is in the same place as
 * every other one in this file.
 *
 * Above the drawer's 70 because it is the topmost thing on screen for three
 * seconds, and `pointer-events: none` because it must never be something you
 * can click on or have to wait out.
 */
.confetti {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 90;
}

.confetti i {
  position: absolute;
  top: -8vh;
  left: var(--x);
  width: var(--w);
  height: var(--h);
  border-radius: 1px;
  will-change: transform;
  animation: confetti-fall var(--dur) linear var(--delay) both;
}

@keyframes confetti-fall {
  from { transform: translate3d(0, 0, 0) rotate(0deg); }
  to { transform: translate3d(var(--dx), 118vh, 0) rotate(var(--rot)); }
}

/* The island already declines to build the nodes; this is the belt to that
   pair of braces, and it also covers a stylesheet loaded without the script. */
@media (prefers-reduced-motion: reduce) {
  .confetti { display: none; }
}

/* -------------------------------------------------------------- drawer */

.drawer {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.42);
  z-index: 70;
  display: flex;
  justify-content: flex-end;
}
.drawer[hidden] { display: none; }
/* Stops the page behind from scrolling under the panel, which is what makes a
   drawer feel like a surface rather than a floating div. */
body.drawer-open { overflow: hidden; }

.drawer-panel {
  width: min(620px, 100%);
  background: var(--bg);
  border-left: 1px solid var(--border-strong);
  display: flex;
  flex-direction: column;
  box-shadow: -20px 0 60px rgba(0, 0, 0, 0.4);
}

@media (prefers-reduced-motion: no-preference) {
  .drawer-panel { animation: drawerin var(--dur-2) var(--ease); }
  @keyframes drawerin {
    from { transform: translateX(24px); opacity: 0; }
    to { transform: translateX(0); opacity: 1; }
  }
}

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--border);
  flex: none;
}
.dr-title {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}

.drawer-body { padding: var(--s-4); overflow-y: auto; flex: 1 1 auto; }

/*
 * The thread reads first and the mention second — the order the text needs,
 * since a comment answers a post. That is the opposite of the feed's order,
 * where the mention leads because it is what you are scanning for.
 *
 * The thread is set back visually so the eye still lands on the mention: it is
 * context, not the subject.
 */
.dr-thread {
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: var(--r);
  padding: var(--s-3);
  margin-bottom: var(--s-4);
}
.dr-thread h3 { font-size: 15px; margin: 6px 0 4px; }
.dr-thread .dr-prose { color: var(--text-3); font-size: 13.5px; }

.dr-mention h3 { font-size: 17px; margin: 8px 0 6px; }
.dr-mention .meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12.5px; color: var(--text-3); }

.dr-label {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-3);
  display: block;
  margin-bottom: 8px;
}

.dr-prose { font-size: 14.5px; line-height: 1.68; color: var(--text-2); margin-top: 10px; }
.dr-prose p { margin: 0 0 11px; }
.dr-prose p:last-child { margin-bottom: 0; }

@media (max-width: 640px) {
  .drawer-panel { width: 100%; border-left: 0; }
}

/* ----------------------------------------------------------- site scan */

.scanbox { border-color: var(--border-strong); }
.scanbox .icon { color: var(--ember); }

/*
 * The wait while a homepage is read and a model runs over it.
 *
 * `thinking-orbs` does exactly this and is React 18+; this product is
 * server-rendered with zero hydration and islands measured in hundreds of
 * bytes, so the idea is borrowed and the implementation is three spans.
 *
 * Three dots on one baseline, breathing out of phase. Not a spinner: a spinner
 * says "loading", where an out-of-phase pulse says "working" — which is the
 * honest description of a model call that takes an unpredictable few seconds.
 */
.orb {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-right: 2px;
}
.orb[hidden] { display: none; }
.orb > i {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.35;
}

@media (prefers-reduced-motion: no-preference) {
  .orb > i { animation: orbpulse 1.25s ease-in-out infinite; }
  .orb > i:nth-child(2) { animation-delay: 0.18s; }
  .orb > i:nth-child(3) { animation-delay: 0.36s; }
  @keyframes orbpulse {
    0%, 100% { opacity: 0.3; transform: translateY(0) scale(1); }
    45% { opacity: 1; transform: translateY(-2px) scale(1.25); }
  }
}
/* Reduced motion still needs to say something is happening, so the dots hold
   at full strength rather than animating. A static grey row would read as a
   broken control. */
@media (prefers-reduced-motion: reduce) {
  .orb > i { opacity: 0.9; }
}

.checks .check .chip { margin-left: 4px; }

/* The draft: an editable box, never a send button. */
.dr-draft { margin-top: var(--s-4); }
.dr-draft[hidden] { display: none; }
.dr-draft textarea {
  width: 100%;
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.6;
  resize: vertical;
}

/* ----------------------------------------------------------- weekly report */

/* Read by someone with no product context, so: one column, generous type,
   nothing that needs a legend. */
.report { max-width: 760px; margin: 0 auto; padding: 56px 24px 80px; }
.report-head .eyebrow {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ember);
}
.report-head h1 { font-size: 34px; margin: 10px 0 6px; text-wrap: balance; }
.report-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 12px; margin-top: var(--s-5);
}
.report-stats .stat {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  padding: 16px; display: flex; flex-direction: column; gap: 4px;
}
.report-stats .k { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-3); }
.report-stats .v { font-size: 26px; font-weight: 600; }
.report .up { color: var(--pos); }
.report .down { color: var(--neg); }
.report-top { margin-top: var(--s-6); }
.report-top h2 { font-size: 20px; margin: 0 0 var(--s-3); }
.report-mention { padding: 16px 0; border-bottom: 1px solid var(--hair); }
.report-mention .meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12.5px; color: var(--text-3); }
.report-mention .title { font-weight: 560; margin: 6px 0 2px; }
.report-mention .body { color: var(--text-2); font-size: 14.5px; line-height: 1.6; margin: 6px 0 8px; }
.report-foot {
  margin-top: var(--s-6); padding-top: var(--s-4); border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}

/* Adding a place — a platform picker beside one field. */
.card.sources { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.card.sources .icon { color: var(--text-3); }

/*
 * A row of options under a source: the comments toggle, YouTube's filters.
 *
 * One shape for the add form and for the rows beneath it, so the same choice
 * looks the same wherever it is made. The checkboxes are `.check` chips — the
 * ones the keyword scopes use — rather than bare inputs beside bare words,
 * which is what these were: a checkbox with a hairline of its own reads as a
 * control, and lights up when it is on. The note at the end takes what is
 * left of the row and says, in the quiet colour, what will happen — it is
 * where the number that used to be a field now lives as a sentence.
 */
.source-options {
  flex: 1 1 100%;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 10px;
  padding: 8px 10px;
  border: 1px solid var(--hair);
  border-radius: var(--r);
  background: color-mix(in srgb, var(--surface-2) 55%, transparent);
  font-size: 12.5px;
}
.watch-row .source-options { margin-top: 9px; padding: 7px 9px; }
.source-options .check { padding: 4px 9px; font-size: 12.5px; }
.source-options-label {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-right: 2px;
}
.source-options-note { color: var(--text-3); flex: 1 1 auto; min-width: 12em; line-height: 1.45; }
.source-options[hidden] { display: none; }
/* The row's rules for its remove button (faded until hover, red on it) are not for these. */
.watch-row .source-options button { opacity: 1; }
.watch-row .source-options button:hover { color: var(--text); }
.watch-row .source-options button.btn-primary:hover { color: var(--on-ember); }

/* ───────────────────────────────────────────────── platform marks */

/*
 * The Reddit / Hacker News / Lobsters marks.
 *
 * Filled, where every other icon in this interface is a 1.6-weight outline —
 * because a logo is a solid shape and an outlined one reads as a bad copy. The
 * two are never asked to stand in for each other: a platform mark always sits
 * beside the place's own label, never alone as an affordance.
 *
 * Recessive by default. The mark is there to be *scanned* — to let somebody
 * tell three platforms apart down a column without reading — and a row of
 * full-contrast logos would pull rank on the thing they label. It comes up to
 * full strength with the row it sits in.
 */
.picon {
  flex: none;
  color: var(--text-3);
  vertical-align: -0.12em;
  transition: color var(--dur-1) var(--ease);
}
.sub .picon,
.mention:hover .picon,
.panel-row:hover .picon,
a:hover > .picon { color: var(--text-2); }

/* In the picker, the mark is the choice rather than a label on one, so it is
   drawn at full strength and takes the accent when its platform is selected. */
.platform-pick { display: inline-flex; align-items: center; gap: 7px; }
.platform-pick .picon { color: var(--text-2); }
.platform-pick input:checked + .platform-pick-face .picon,
.platform-pick input:checked ~ .platform-pick-face { color: var(--ember); }

/* --------------------------------------------------- landing: real examples */

/* The cards are links to the real threads now, so they must not look like
   links — the whole card is the target, and underlined titles in a moving
   marquee are noise. */
a.m-card { text-decoration: none; color: inherit; display: block; }
a.m-card:hover { border-color: var(--ember); }
a.m-card:focus-visible { outline: 2px solid var(--ember); outline-offset: 2px; }

/* Where the conversation is: measured activity per niche. Numbers in mono,
   right-aligned, so "175" and "7.3" can be compared down a column. */
.niches { display: flex; flex-direction: column; gap: 18px; }
.niche-name {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 6px;
}
.niche-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 3.5em auto 3.5em auto;
  align-items: baseline;
  gap: 6px 10px;
  padding: 6px 0;
  border-bottom: 1px solid var(--rule);
  font-size: 14px;
}
.niche-row .num { text-align: right; font-weight: 600; }

/* What is in it: a grid of twelve, each an icon, a name and one sentence. */
.features {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 28px 32px;
}
.feature h3 { font-size: 16px; margin: 12px 0 6px; }
.feature p { margin: 0; color: var(--ink-2); }
.feature-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  color: var(--ember);
  background: var(--paper-3);
}

/* The orb host holds either three CSS dots or thinking-orbs' canvas. Fixed
   height so swapping one for the other does not move the button's label. */
.orb { min-height: 20px; }
.orb > canvas { display: block; }

/* ------------------------------------------------- forms inside flex rows */

/*
 * A form wrapping a single button, sitting in a flex row beside other buttons.
 *
 * A flex container centres the *margin box*, not the border box. The browser's
 * default stylesheet gives `<form>` a bottom margin, so a form whose content
 * is 27px tall occupies 42px in the row — and centring that box lifts the
 * button inside it by half the margin. Measured on the dashboard header: the
 * "Share this week" button sat 7.5px above "All mentions", which is exactly
 * half of the 15px the browser had added.
 *
 * The button is the thing being laid out here; the form is only there to carry
 * the POST. So it stops contributing spacing of its own.
 */
.row > form,
.between > form,
.keyrow > form,
.viewrail > form,
.sources > form,
.bulkbar > form,
.banner > form,
.panel-row > form,
.toolbar-actions > form { margin: 0; }

/* ------------------------------------------------------- the hero panel */

/*
 * The product's own summary, as the hero visual.
 *
 * Markup rather than a screenshot: sharp at any pixel density, follows the
 * theme, reflows on a phone, and cannot drift out of date the way an exported
 * PNG of a UI does. It borrows the dashboard's anatomy — stat tiles above, the
 * share-of-voice bar below — in the landing's own paper-and-ink palette rather
 * than the app's, so it belongs to this page instead of looking pasted in.
 */
.heropanel {
  margin: 0;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: 14px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-width: 460px;
  /* Sits slightly proud of the page, the way the app's cards do. */
  box-shadow: 0 1px 2px rgba(16, 16, 26, 0.04), 0 18px 40px -24px rgba(16, 16, 26, 0.28);
}

.hp-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.hp-title { font-family: var(--display); font-weight: 700; font-size: 17px; color: var(--ink); }
.hp-chip {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-3);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 3px 9px;
}
.hp-chip b { color: var(--ember); font-weight: 600; }

/* Three readings across the top, exactly as the dashboard opens. */
.hp-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.hp-stat {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 12px;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 10px;
  min-width: 0;
}
.hp-k {
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.hp-v {
  font-size: 24px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--ink);
  /* r/ClaudeAI is longer than "35": it shrinks rather than breaking the grid. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hp-stat .hp-v.mono { font-size: 15px; font-weight: 600; }
.hp-n { font-size: 11px; line-height: 1.35; color: var(--ink-3); }

.hp-sov { display: flex; flex-direction: column; gap: 9px; }
/* The denominator belongs to the bar, not to a tile of its own: putting it here
   is what let the tiles stop repeating the number the bar already states. */
.hp-sov-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }

/* One bar divided, not three bars — share of voice is a division of a whole. */
.hp-bar { display: flex; height: 10px; gap: 2px; border-radius: 999px; overflow: hidden; background: var(--paper-3); }
.hp-bar > i { display: block; height: 100%; min-width: 3px; }
.hp-bar > i.mine { background: var(--ember); }
.hp-bar > i.theirs { background: var(--ink-3); opacity: 0.55; }
.hp-bar > i.theirs:last-child { opacity: 0.3; }

.hp-legend { display: flex; flex-direction: column; }
.hp-row {
  display: grid;
  grid-template-columns: 9px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  border-bottom: 1px solid var(--rule);
  font-size: 13.5px;
  color: var(--ink-2);
}
.hp-row:last-child { border-bottom: 0; }
.hp-dot { width: 9px; height: 9px; border-radius: 3px; }
.hp-dot.mine { background: var(--ember); }
.hp-dot.theirs { background: var(--ink-3); opacity: 0.55; }
.hp-term { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hp-share { font-weight: 650; color: var(--ink); min-width: 42px; text-align: right; }
.hp-count { color: var(--ink-3); min-width: 30px; text-align: right; font-size: 12px; }

.hp-foot { font-size: 11px; line-height: 1.5; color: var(--ink-3); margin: 0; }

@media (max-width: 940px) {
  .heropanel { max-width: none; }
}
@media (max-width: 420px) {
  /* Three tiles at 390px leaves ~100px each, which clips the subreddit name. */
  .hp-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .hp-stat:last-child { grid-column: 1 / -1; }
}

/* ------------------------------------------------------------ video page */

/*
 * The player and the words, side by side.
 *
 * Two cards rather than one: the stage is a dark 16:9 block and the quote is
 * running text, and a single card holding both reads as a video with a
 * caption. They are two different things — where it was said, and what was
 * said — and the answer somebody came for is the second one.
 */
.video-page {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(280px, 1fr);
  gap: var(--s-4);
  align-items: start;
}
@media (max-width: 960px) {
  .video-page { grid-template-columns: 1fr; }
}

.video-stage {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--r);
  overflow: hidden;
  background: #000;
}
.video-stage iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.video-stage.video-gone {
  background: var(--surface-2);
  display: grid;
  place-items: center;
  padding: var(--s-4);
  text-align: center;
}

/* The poster is a link: with no script it is the way to YouTube at the right
   second, so it has to look pressable before any script has loaded. */
.video-poster {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #fff;
  text-decoration: none;
}
.video-poster img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.82;
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-3) var(--ease);
}
.video-poster:hover img,
.video-poster:focus-visible img { opacity: 1; transform: scale(1.02); }
.video-poster:focus-visible { outline: 2px solid var(--ember); outline-offset: -2px; }

.video-play {
  position: relative;
  width: 64px;
  height: 64px;
  border-radius: 999px;
  background: var(--ember);
  color: var(--on-ember);
  display: grid;
  place-items: center;
  box-shadow: var(--e-3);
  transition: transform var(--dur-2) var(--ease);
}
.video-play .icon { margin-left: 3px; }
.video-poster:hover .video-play { transform: scale(1.06); }

.video-play-label {
  position: absolute;
  left: 12px;
  bottom: 12px;
  font-family: var(--mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  padding: 4px 8px;
  border-radius: var(--r-sm);
  background: rgba(0, 0, 0, 0.66);
  color: #fff;
}

.video-facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
  margin-top: 12px;
  font-size: 12.5px;
  color: var(--text-3);
}

/* The window of transcript, set as a quotation — it is somebody's speech,
   and the rule down the side is what keeps it from reading as our copy. */
.video-quote {
  margin: 10px 0 14px;
  padding: 2px 0 2px 14px;
  border-left: 2px solid var(--ember);
  font-size: 15px;
  line-height: 1.72;
  color: var(--text);
}

/* A transcript's title is a link to its page and looks like the title it is;
   the underline arrives on hover, which is the only time it needs saying. */
.mention .title a { color: inherit; text-decoration: none; }
.mention .title a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* --------------------------------------------------------- video mention */

/*
 * A transcript row leads with the frame.
 *
 * The score column is Reddit's anatomy — karma, an arrow — and a view count
 * does not belong in it: 104,356,039 under a dot read as an id, not a count.
 * The thumbnail takes that column instead, at the width of a small YouTube
 * card, and the views move up into the meta line as "104M views".
 */
.mention .thumb {
  position: relative;
  display: block;
  flex: 0 0 120px;
  align-self: flex-start;
  aspect-ratio: 16 / 9;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: #000;
}
.mention .thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.9;
  transition: opacity var(--dur-2) var(--ease);
}
.mention .thumb:hover img { opacity: 1; }
.mention .thumb-play {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  display: grid;
  place-items: center;
}
.mention .thumb-play .icon { margin-left: 2px; }
.mention .thumb-dur {
  position: absolute;
  right: 5px;
  bottom: 5px;
  font-size: 10.5px;
  font-weight: 600;
  color: #fff;
  background: rgba(0, 0, 0, 0.72);
  border-radius: 4px;
  padding: 1px 5px;
}
@media (max-width: 640px) {
  .mention .thumb { flex-basis: 96px; }
}

/* The second it was said, and who wrote the words down. */
.mention .said-at { display: flex; align-items: center; gap: 8px; margin: 2px 0 7px; }
.mention .stamp {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--ember);
  background: var(--ember-wash);
  border-radius: 4px;
  padding: 1px 7px;
  text-decoration: none;
}
.mention .stamp:hover { text-decoration: underline; text-underline-offset: 2px; }

/* The window of speech, set as a quotation rather than as our own copy. */
.mention .body.said { padding-left: 12px; border-left: 2px solid var(--border-strong); }

/* An avatar beside a place name, wherever a place is named: chips, bars, scope checks. */
.avatar { width: 16px; height: 16px; border-radius: 50%; object-fit: cover; flex: none; vertical-align: -3px; }

/* ---------------------------------------------------------- source picker */

/*
 * The mentions toolbar's source filter, drawn as a menu so each source can
 * carry its platform mark and avatar. The summary is dressed as the select
 * it replaced, so the toolbar reads as one row of controls.
 */
.picker { position: relative; }
.picker > summary {
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 14px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 9px 11px;
  cursor: pointer;
  user-select: none;
}
.picker > summary::-webkit-details-marker { display: none; }
.picker > summary:hover { border-color: var(--border-strong); }
.picker.on > summary { border-color: var(--ember); color: var(--text); }
.picker[open] > summary { border-color: var(--ember); }
.picker-label { display: inline-flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-caret { color: var(--text-3); flex: none; transition: transform var(--dur-1) var(--ease); }
.picker[open] .picker-caret { transform: rotate(180deg); }

.picker-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  min-width: 100%;
  max-height: 320px;
  overflow-y: auto;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r);
  box-shadow: var(--e-3);
}
.picker-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  border-radius: var(--r-sm);
  font-size: 13.5px;
  color: var(--text-2);
  cursor: pointer;
  white-space: nowrap;
}
.picker-item:hover { background: var(--surface-2); color: var(--text); }
/* The radio is the value, not the picture: the row itself shows the state. */
.picker-item input { position: absolute; opacity: 0; pointer-events: none; width: 0; height: 0; margin: 0; }
.picker-item:has(input:checked) { color: var(--ember); background: var(--ember-wash); }
.picker-item:has(input:focus-visible) { outline: 2px solid var(--ember); outline-offset: -2px; }
.picker-face { display: inline-flex; align-items: center; gap: 6px; }
.picker-face .picon, .picker-label .picon { color: inherit; }

/* The platform radios on the add-a-source form: mark and name on one line,
   centred on each other, which they were not while the face had no layout. */
.platform-pick-face { display: inline-flex; align-items: center; gap: 6px; line-height: 1; }
.platform-pick-face .picon { vertical-align: middle; }

/* ------------------------------------------------------ pricing: the switch */

/*
 * Monthly or yearly is a switch, not two tabs: one control that flips every
 * card at once. The cards carry both prices; the wrapper's data-interval says
 * which is visible, so the page is right before its script runs.
 */
.interval-switch {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 28px;
  font-size: 14px;
}
.interval-label { color: var(--ink-3, var(--text-3)); cursor: pointer; transition: color var(--dur-1, 140ms) ease; display: inline-flex; align-items: center; gap: 8px; }
.interval-label.on { color: var(--ink, var(--text)); font-weight: 600; }

[data-pricing] .price-year { display: none; }
[data-pricing][data-interval="year"] .price-year { display: block; }
[data-pricing][data-interval="year"] .price-month { display: none; }

.price-per { font-size: 14px; font-weight: 400; color: var(--ink-3, var(--text-3)); margin-left: 2px; }
.price-sub { color: var(--ink-3, var(--text-3)); min-height: 17px; }

.save-badge {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ember);
  background: var(--ember-wash, rgba(15, 15, 14, 0.08));
  position: relative;
  white-space: nowrap;
}

/*
 * The sparkle: two glyphs that rise and fade at the badge's corners, on a slow
 * loop — a flash, then most of the cycle dark — so the saving keeps catching
 * the eye without ever flickering. The badge itself breathes a little with
 * them. Off entirely for anyone who asked for reduced motion.
 */
.save-badge.sparkle { animation: badge-glow 3200ms ease-in-out infinite; }
.save-badge.sparkle::before,
.save-badge.sparkle::after {
  content: "✦";
  position: absolute;
  font-size: 10px;
  color: var(--ember);
  opacity: 0;
  pointer-events: none;
  animation: sparkle 3200ms ease-out infinite;
}
.save-badge.sparkle::before { left: -9px; top: -8px; }
.save-badge.sparkle::after { right: -8px; bottom: -8px; animation-delay: 700ms; }
@keyframes sparkle {
  0% { opacity: 0; transform: scale(0.4) translateY(3px); }
  10% { opacity: 1; transform: scale(1.2) translateY(-1px); }
  28% { opacity: 0; transform: scale(0.7) translateY(-6px); }
  100% { opacity: 0; transform: scale(0.7) translateY(-6px); }
}
@keyframes badge-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(225, 244, 53, 0); }
  12% { box-shadow: 0 0 0 4px rgba(225, 244, 53, 0.55); }
  40% { box-shadow: 0 0 0 0 rgba(225, 244, 53, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .save-badge.sparkle, .save-badge.sparkle::before, .save-badge.sparkle::after { animation: none; }
}

/* ------------------------------------------------- landing: platform strip */

/* Five chips under the lede: the marks at full strength, the names beside
   them, each a link down to that platform's card. */
.platform-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 24px;
}
.platform-strip-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px 7px 10px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--paper);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--ink-2);
  text-decoration: none;
  transition: border-color var(--dur-1, 140ms) ease, color var(--dur-1, 140ms) ease, transform var(--dur-1, 140ms) ease;
}
.platform-strip-item .picon { color: var(--ink); }
.platform-strip-item:hover { border-color: var(--ember); color: var(--ink); transform: translateY(-1px); }
.platform-strip-item:hover .picon { color: var(--ember); }

/* ---------------------------------------------------- landing: real cards */

/* "live" or "drawn", at the far end of the platform line, so a reader knows
   which kind of card they are looking at before they read it. */
.demo-plat .demo-tag {
  margin-left: auto;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 1px 7px;
}
/*
 * `is-live`, not `live`. The app's `.live` is the 7px heartbeat dot with the
 * pinging ring, and a tag that borrowed the word borrowed the rules too: the
 * word LIVE and a green disc drawn on top of each other, on every real card.
 */
.demo-plat .demo-tag.is-live { color: #0d7a52; border-color: rgba(13, 122, 82, 0.35); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .demo-plat .demo-tag.is-live { color: #47d69a; border-color: rgba(71, 214, 154, 0.4); }
}
:root[data-theme="dark"] .demo-plat .demo-tag.is-live { color: #47d69a; border-color: rgba(71, 214, 154, 0.4); }

/* An Instagram post's, a TikTok video's or an X post's cover on a real card:
   square, portrait and wide — the shapes the platforms use themselves. */
.demo-cover {
  flex: none;
  display: block;
  width: 64px;
  height: 64px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--paper-3);
}
.demo-cover.portrait { width: 52px; height: 70px; }
.demo-cover.wide { width: 84px; height: 52px; }
.demo-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
.demo-meta .avatar { width: 16px; height: 16px; border-radius: 50%; object-fit: cover; }
.demo-card.real a.yt-video { color: inherit; text-decoration: none; }
.demo-card.real a.yt-video:hover .demo-title { text-decoration: underline; }
.yt-thumb.has-frame { overflow: hidden; background: var(--paper-3); }
.yt-thumb .yt-frame { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.yt-thumb.has-frame .yt-play { z-index: 1; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)); border-color: transparent transparent transparent #fff; }
.yt-thumb.has-frame .yt-dur { z-index: 1; }
a.yt-jump { text-decoration: none; }
a.yt-jump:hover { text-decoration: underline; }


/* ══════════════════════════════════════════════════════ by platform */

/*
 * A platform's mark, in the platform's colour.
 *
 * The marks are monochrome everywhere they sit beside a label (see
 * PlatformIcon.tsx for why). These are the places where the colour is doing
 * a job — the same slot the platform has on the chart, so the chip in the
 * sidebar, the header on the sources list and the legend on the overview all
 * agree about which one is which.
 */
.picon.tinted { color: var(--s, currentColor); }

/*
 * The chart's second band: the split, then the filters.
 *
 * Shares the toolbar's recessed fill so it reads as controls *on* the panel
 * rather than a card inside one, and wraps rather than scrolls — a control
 * hidden behind a horizontal gesture is one nobody finds.
 */
.chart-filters {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  padding: 9px var(--s-4);
  border-bottom: 1px solid var(--hair);
  background: var(--surface-2);
}
.chart-filter-label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-3);
}
.chart-clear {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  font-size: 12px;
  color: var(--text-3);
  text-decoration: none;
}
.chart-clear:hover { color: var(--text); }

/* A picker whose rows are links rather than radios: same menu, same rows,
   marked by a class instead of a checked input. Sized to the band it sits in. */
.picker-links > summary {
  height: 30px;
  padding: 0 9px;
  font-size: 12.5px;
  border-radius: 999px;
  background: var(--surface);
  border-color: var(--hair-strong);
}
.picker-links .picker-item { text-decoration: none; }
.picker-links .picker-item.on { color: var(--ember); background: var(--ember-wash); }
.picker-face .avatar { width: 16px; height: 16px; border-radius: 50%; object-fit: cover; }

/* A platform chip: mark in the platform's colour, name in ink, count. On, it
   takes the accent like every other active filter in the product. */
.pchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 10px;
  border: 1px solid var(--hair-strong);
  border-radius: 999px;
  background: var(--surface);
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-2);
  text-decoration: none;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.pchip:hover { border-color: var(--border-strong); color: var(--text); }
.pchip .num { font-size: 11.5px; color: var(--text-3); }
.pchip.on { border-color: var(--ember); color: var(--text); }
.pchip.on .num { color: var(--ember); }

/*
 * A platform's group of rows, folding.
 *
 * `details` gives the fold for free; what the CSS does is make the summary
 * the group header it already was, take the browser's own marker off it, and
 * put a chevron in that turns when the group is open. The rows inside keep
 * their own hairlines, so a closed group is one header line and an open one
 * is exactly what the flat list was.
 */
.panel-fold > summary { list-style: none; cursor: pointer; user-select: none; }
.panel-fold > summary::-webkit-details-marker { display: none; }
.panel-fold > summary:hover { background: var(--surface-3); }
.panel-fold > summary:focus-visible { outline: 2px solid var(--ember); outline-offset: -2px; }
.panel-fold .fold-mark {
  flex: none;
  display: inline-flex;
  color: var(--text-3);
  transform-origin: center;
  transition: transform var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.panel-fold[open] .fold-mark { transform: rotate(90deg); }
.panel-fold > summary:hover .fold-mark { color: var(--text); }
.panel-rows > .panel-fold:first-child > summary { border-top: 0; }

/* A group header inside a tray of rows. */
.panel-group {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px var(--s-4) 7px;
  border-top: 1px solid var(--hair);
  background: var(--surface-2);
  font-size: 12.5px;
  color: var(--text-2);
}
.panel-rows > .panel-group:first-child { border-top: 0; }
.panel-group strong { font-weight: 600; color: var(--text); }
.panel-group .panel-note { margin-left: auto; font-size: 11.5px; color: var(--text-3); }

/* ------------------------------------------------------------- pulse */

/*
 * The sidebar's heartbeat — see PulseCard in Shell.tsx.
 *
 * Quieter than the meter below it: no border, the recessed fill only, so the
 * sidebar keeps one card that looks like a card. It grows into the column the
 * nav used to leave empty.
 */
.pulse-card {
  margin-top: var(--s-4);
  padding: var(--s-3);
  border-top: 1px solid var(--hair);
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.pulse-head { display: flex; align-items: center; gap: 8px; }
.pulse-head .live { margin-left: 2px; }
.pulse-title {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
}
.pulse-count { margin-left: auto; font-size: 11.5px; color: var(--text-2); }
.pulse-plats { display: flex; flex-wrap: wrap; gap: 5px; }
.pulse-plat {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px 3px 7px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: 11.5px;
  color: var(--text-2);
  text-decoration: none;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.pulse-plat:hover { border-color: var(--hair-strong); color: var(--text); }
.pulse-plat.off { opacity: 0.55; }
.pulse-plat.off .picon { color: var(--text-3); }
.pulse-today {
  display: flex;
  align-items: baseline;
  gap: 7px;
  text-decoration: none;
  color: var(--text-2);
}
.pulse-today:hover .pulse-what { color: var(--text); }
.pulse-n {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--text);
}
.pulse-what { font-size: 12px; }
.pulse-neg { color: var(--neg); }
.pulse-last { margin-top: -3px; }
.pulse-cta { font-size: 12.5px; color: var(--ember); text-decoration: none; }
.pulse-cta:hover { text-decoration: underline; }

/* ------------------------------------------------------- mention tags */

/*
 * The platform tag on a mention row.
 *
 * The mark in the platform's colour, the name in ink — text wears text
 * colour, the coloured thing beside it carries the identity. The fill is a
 * faint wash of the same colour, so a column of rows shows its platforms as
 * a pattern before a single word is read.
 */
.ptag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px 2px 6px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--s) 35%, transparent);
  background: color-mix(in srgb, var(--s) 10%, transparent);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-2);
  white-space: nowrap;
}
.ptag .picon { color: var(--s); }
.mention:hover .ptag { color: var(--text); }

/*
 * Post or comment, on every platform.
 *
 * Two shapes, not two colours: the post tag is filled and the comment tag is
 * outlined, so a column of rows shows which are threads and which are voices
 * in them without either wearing a semantic colour it has not earned.
 */
.kind {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  border: 1px solid var(--hair-strong);
  color: var(--text-2);
  background: transparent;
}
.kind-post,
.kind-transcript {
  color: var(--text);
  background: var(--surface-3);
  border-color: transparent;
}
.kind-comment { border-style: dashed; }
.kind .icon { opacity: 0.8; }

/* An Instagram, TikTok or X cover in the score's column. */
.mention .thumb.cover { flex-basis: 68px; aspect-ratio: 1 / 1; background: var(--surface-3); }
.mention .thumb.cover.portrait { flex-basis: 54px; aspect-ratio: 3 / 4; }
.mention .thumb.cover.wide { flex-basis: 84px; aspect-ratio: 16 / 10; }
.mention .thumb.cover .thumb-play { width: 22px; height: 22px; }
@media (max-width: 640px) {
  .mention .thumb.cover { flex-basis: 56px; }
  .mention .thumb.cover.portrait { flex-basis: 46px; }
}

/* ======================================================= the photographs */

/*
 * Real pictures, drawn as type.
 *
 * `.ascii-stage` is a box with a fixed ratio holding the picture and the
 * canvas that redraws it (src/client/ascii.ts). Until the script has drawn,
 * the picture shows as itself, desaturated, so a slow connection sees a
 * photograph rather than a blank; once drawn it falls back to a whisper under
 * the glyphs: the type carries the texture, the photograph carries the
 * likeness, and neither has to do the other's job. The two colours the script reads are declared
 * here and nowhere else, so the treatment follows the theme like any text.
 */
.ascii {
  margin: 0;
  --ascii-ink: var(--ink);
  --ascii-accent: var(--hi);
  --ascii-paper: var(--paper);
}

.ascii-stage {
  position: relative;
  overflow: hidden;
  border-radius: 14px;
  background: var(--paper-2);
  border: 1px solid var(--rule);
}

.ascii-stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.06);
  transition: opacity var(--dur-3) var(--ease);
}

.ascii-stage.is-drawn img { opacity: 0.3; }

:root[data-theme="dark"] .ascii-stage.is-drawn img { opacity: 0.22; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ascii-stage.is-drawn img { opacity: 0.22; }
}

.ascii-stage canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

.ascii-cap {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
  color: var(--ink-2);
}

.ascii-credit { color: var(--ink-3); }

/* The wide band runs edge to edge; its caption keeps to the column. */
.ascii-wide .ascii-stage { border-radius: 0; border-left: 0; border-right: 0; }
.ascii-wide .ascii-cap { max-width: 1160px; margin: 10px auto 0; padding: 0 24px; }

@media (max-width: 940px) {
  .hero-grid .ascii { max-width: 520px; }
}

/*
 * The thumbnails on the platform cards are somebody else's pixels on somebody
 * else's server, so the canvas cannot read them. They get the same language
 * by other means: desaturated, and a grid of plus signs laid over them in the
 * page's ink. One SVG tile, eight pixels square, multiplied on paper and
 * screened on dark. `position: relative` is what lets the overlay sit inside
 * the rounded frame instead of on the card.
 */
.demo-cover, .yt-thumb { position: relative; }
.demo-cover img, .yt-thumb .yt-frame { filter: grayscale(0.9) contrast(1.05); }
.demo-cover::after, .yt-thumb::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath d='M4 1.5v5M1.5 4h5' stroke='%230f0f0e' stroke-width='0.9' stroke-linecap='round'/%3E%3C/svg%3E");
  background-size: 8px 8px;
  opacity: 0.42;
  mix-blend-mode: multiply;
}
:root[data-theme="dark"] .demo-cover::after,
:root[data-theme="dark"] .yt-thumb::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath d='M4 1.5v5M1.5 4h5' stroke='%23f1f0ea' stroke-width='0.9' stroke-linecap='round'/%3E%3C/svg%3E");
  mix-blend-mode: screen;
  opacity: 0.3;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .demo-cover::after,
  :root:not([data-theme="light"]) .yt-thumb::after {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath d='M4 1.5v5M1.5 4h5' stroke='%23f1f0ea' stroke-width='0.9' stroke-linecap='round'/%3E%3C/svg%3E");
    mix-blend-mode: screen;
    opacity: 0.3;
  }
}
