/* ═══════════════════════════════════════════════════════════════════════════
   semfilt reader — ported from twitter-reader-extension/reader.css.
   Adaptations from the extension (a slide-in content-script overlay) to a
   standalone full-page app are annotated inline with "PAGE-ADAPT".
   The CSS custom properties + dark-only skin system are kept verbatim.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── CSS CUSTOM PROPERTIES (skin defaults — dark theme) ─────────────────────── */
/* Self-hosted Inter for the reconstructed card (.rtc). Without it the card inherits
   the system stack and falls back to Segoe UI on Windows, which renders heavier/
   larger than the live embed's Chirp font — the "~1px too big" report (task 24). Same
   woff2 the japanese skin ships; absolute asset path since reader.css loads from "/". */
@font-face {
  font-family: 'Inter';
  src: url('/skins/fonts/inter-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  --reader-bg: #000;
  --reader-bg-hover: #080808;
  --reader-bg-selected: rgba(29, 155, 240, 0.04);
  --reader-bg-focus: rgba(245, 197, 24, 0.04);
  --reader-bg-active-header: rgba(245, 197, 24, 0.08);
  --reader-bg-shadow: #16181c;

  --reader-text-primary: #e7e9ea;
  --reader-text-heading: #fff;
  --reader-text-secondary: #71767b;
  --reader-text-accent: #1d9bf0;
  --reader-text-danger: #f4212e;
  --reader-text-success: #00ba7c;

  --reader-border: #2f3336;
  --reader-border-row: #16181c;
  --reader-border-selected: #1d9bf0;
  --reader-border-focus: #f5c518;

  --reader-scrollbar-thumb: #333;

  --reader-badge-bg: #1d9bf0;
  --reader-badge-text: #fff;

  --reader-shadow-avatar-bg: #2f3336;
  --reader-shadow-action-border: #536471;
  --reader-shadow-action-hover: rgba(29, 155, 240, 0.1);
  --reader-shadow-video-badge-bg: #2f3336;

  --reader-read-opacity: 0.55;
  --reader-overlay-bg: #000;

  --reader-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* ── PAGE SHELL (PAGE-ADAPT: replaces the overlay's fixed/transform panel) ──── */
* {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
  height: 100%;
  background: var(--reader-bg);
  color: var(--reader-text-primary);
  font-family: var(--reader-font-family);
}
body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ── STATUS NOTICE (new surface: /api/status.halted) ───────────────────────── */
#reader-status-notice {
  flex-shrink: 0;
  padding: 6px 20px;
  font-size: 13px;
  text-align: center;
  color: var(--reader-badge-text);
  background: var(--reader-text-danger);
  border-bottom: 1px solid var(--reader-border);
}
#reader-status-notice[hidden] {
  display: none;
}
/* task 31: the same bar narrates a running sweep's hydration — a calmer, non-error
   look than the halted (danger-red) default, and a brief green "ready" on completion. */
#reader-status-notice.hydrating {
  background: var(--reader-text-accent);
}
#reader-status-notice.done {
  background: var(--reader-text-success);
}
/* Shared "working" pulse dot (status-bar hydration narration + the posts pane's
   unread-seek sentinel). currentColor, so each site tints it by its own text color —
   the pair had already drifted on color sourcing before this was unified. */
.pulse-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 8px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: middle;
  animation: hyd-pulse 1.1s ease-in-out infinite;
}
@keyframes hyd-pulse {
  0%,
  100% {
    opacity: 0.35;
  }
  50% {
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .pulse-dot {
    animation: none;
  }
}

/* ── APP CONTAINER (three-column row — was #reader-panel) ───────────────────── */
#reader-app {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: row;
}

/* ── COLUMN 1: LIST PANE (authors) ─────────────────────────────────────────── */
#reader-list-pane {
  width: 360px;
  flex-shrink: 0;
  height: 100%;
  background: var(--reader-bg);
  display: flex;
  flex-direction: column;
}
#reader-list-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--reader-border);
  flex-shrink: 0;
}
#reader-list-header h2 {
  color: var(--reader-text-heading);
  font-size: 20px;
  font-weight: 700;
  margin: 0;
}
#reader-total-count {
  color: var(--reader-text-secondary);
  font-size: 20px;
  font-weight: 700;
}
#reader-list {
  flex: 1;
  overflow-y: auto;
}
#reader-list::-webkit-scrollbar {
  width: 4px;
}
#reader-list::-webkit-scrollbar-thumb {
  background: var(--reader-scrollbar-thumb);
  border-radius: 4px;
}

/* ── ADD-AUTHOR FORM (new surface) ─────────────────────────────────────────── */
#reader-add-form {
  flex-shrink: 0;
  padding: 10px 20px;
  border-bottom: 1px solid var(--reader-border-row);
}
#reader-add-form[hidden] {
  display: none;
}
#reader-add-row {
  display: flex;
  gap: 6px;
}
#reader-add-input {
  flex: 1;
  min-width: 0;
  background: var(--reader-bg-shadow);
  border: 1px solid var(--reader-border);
  border-radius: 4px;
  color: var(--reader-text-primary);
  font-family: inherit;
  font-size: 13px;
  padding: 5px 8px;
}
#reader-add-input:focus {
  outline: none;
  border-color: var(--reader-border-selected);
}
/* Shared ghost-button chrome (add-submit / Load-older / modal Cancel — three sites that
   used to carry near-verbatim copies). Sites keep only layout + tint overrides. */
.reader-ghost-btn {
  background: none;
  border: 1px solid var(--reader-border);
  color: var(--reader-text-accent);
  cursor: pointer;
  font-family: inherit;
  font-size: 13px;
  padding: 5px 12px;
  border-radius: 4px;
  transition: border-color 0.15s, color 0.15s;
}
.reader-ghost-btn:hover {
  border-color: var(--reader-text-accent);
}
.reader-ghost-btn:disabled {
  opacity: 0.5;
  cursor: default;
}
#reader-add-msg {
  font-size: 12px;
  margin-top: 6px;
  min-height: 0;
  color: var(--reader-text-secondary);
}
#reader-add-msg.error {
  color: var(--reader-text-danger);
}
#reader-add-msg.ok {
  color: var(--reader-text-accent);
}

/* ── ACTIVE COLUMN HIGHLIGHT (keyboard focus context) ──────────────────────── */
#reader-list-pane.column-active #reader-list-header {
  background: var(--reader-bg-active-header);
}
#reader-detail-pane.column-active #reader-detail-header {
  background: var(--reader-bg-active-header);
}

/* ── PERSON ROW ─────────────────────────────────────────────────────────────── */
.reader-person {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 20px;
  cursor: pointer;
  border-bottom: 1px solid var(--reader-border-row);
  transition: background 0.15s;
  font-size: 14px;
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
}
.reader-person:hover {
  background: var(--reader-bg-hover);
}
.reader-person.selected {
  border: 2px solid var(--reader-border-selected);
  background: var(--reader-bg-selected);
  padding: 4px 18px;
}
.reader-person.selected .reader-person-name {
  color: var(--reader-text-primary);
}
.reader-person.selected .reader-person-handle {
  color: var(--reader-text-secondary);
}
#reader-list-pane.column-active .reader-person.keyboard-focus {
  border: 2px solid var(--reader-border-focus);
  background: var(--reader-bg-focus);
  padding: 4px 18px;
}
#reader-list-pane.column-active .reader-person.keyboard-focus .reader-person-name {
  color: var(--reader-text-primary);
}
#reader-list-pane.column-active .reader-person.keyboard-focus .reader-person-handle {
  color: var(--reader-text-secondary);
}
.reader-person.has-unread {
  font-weight: 600;
}
.reader-person-name {
  color: var(--reader-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-person-handle {
  color: var(--reader-text-secondary);
  flex-shrink: 0;
}
.reader-person-count {
  color: var(--reader-text-secondary);
  font-size: 13px;
  flex-shrink: 0;
  margin-left: auto;
}

/* All-posts pseudo-row: same anatomy as an author row, glyph instead of @handle. */
.reader-person.all-posts .reader-person-glyph {
  color: var(--reader-text-secondary);
  flex-shrink: 0;
}

/* Inactive authors (new state): dimmed, pushed to the bottom, no bold. */
.reader-person.inactive {
  opacity: 0.45;
}
.reader-person.inactive.has-unread {
  font-weight: 400;
}
.reader-authors-divider {
  padding: 8px 20px 4px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--reader-text-secondary);
}

/* The "Hidden" divider is a clickable folder toggle (collapsed by default). It reuses the
   divider look but is a <button>, so reset the native chrome and lay the caret/label/count
   out in a row. Full-width + left-aligned so the whole strip is the hit target. */
button.reader-authors-divider {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  background: none;
  border: none;
  font-family: inherit;
  cursor: pointer;
}
button.reader-authors-divider:hover {
  color: var(--reader-text-primary);
}
.hidden-toggle-caret {
  font-size: 9px;
  line-height: 1;
  transition: transform 0.12s ease;
}
/* ▸ collapsed → rotate to ▾ when the folder is open. */
button.reader-authors-divider:not(.collapsed) .hidden-toggle-caret {
  transform: rotate(90deg);
}
.hidden-toggle-count {
  opacity: 0.7;
}

/* task 32: Hidden-folder rows — dimmed like inactive; the count badge still live-updates
   (updateCountsInPlace covers every rendered row) but never bolds the row: both spellings
   of has-unread go through railHasUnread(), which excludes hidden authors. */
.reader-person.hidden-author {
  opacity: 0.45;
}

/* Per-author backfill-pending badge shown right after adding a handle. */
.reader-person-pending {
  color: var(--reader-text-accent);
  font-size: 11px;
  flex-shrink: 0;
  margin-left: 6px;
}

/* ── COLUMN 2: DETAIL PANE (compact post list) ─────────────────────────────── */
#reader-detail-pane {
  width: 400px;
  flex-shrink: 0;
  height: 100%;
  background: var(--reader-bg);
  border-left: 1px solid var(--reader-border);
  display: flex;
  flex-direction: column;
}
#reader-detail-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--reader-border);
  flex-shrink: 0;
}
#reader-detail-header h2 {
  color: var(--reader-text-heading);
  font-size: 20px;
  font-weight: 700;
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#reader-detail-count {
  color: var(--reader-text-secondary);
  font-size: 20px;
  font-weight: 700;
}
#reader-unread-filter {
  margin-left: auto;
  color: var(--reader-text-secondary);
  font-size: 13px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
#reader-unread-filter:hover {
  color: var(--reader-text-primary);
}
#reader-unread-only {
  cursor: pointer;
}
#reader-tweets {
  flex: 1;
  overflow-y: auto;
}
#reader-tweets::-webkit-scrollbar {
  width: 4px;
}
#reader-tweets::-webkit-scrollbar-thumb {
  background: var(--reader-scrollbar-thumb);
  border-radius: 4px;
}
#reader-tweets-empty,
#reader-tweets-loading {
  padding: 16px 20px;
  color: var(--reader-text-secondary);
  font-size: 13px;
}
/* Infinite-scroll sentinel: the observer's target, normally empty. It keeps a 1px
   height because a zero-area target is unreliable to observe across browsers. */
#reader-tweets-sentinel {
  min-height: 1px;
}
/* Shown only when auto-fill is bounded out: a pane of nothing but collapsed read runs
   can't grow tall enough to scroll, so paging can't hand back to the user's scroll —
   this is the explicit way onward instead of a silent burst of requests. Chrome comes
   from .reader-ghost-btn; only the full-width layout lives here. */
#reader-tweets-more {
  display: block;
  width: calc(100% - 24px);
  margin: 6px 12px 12px;
  padding: 6px 12px;
}
/* task 33: sentinel state while the unread-seek pages past an all-read backlog — the
   counts promise unread exists, so the pane narrates the silent fetching instead of
   sitting empty. Swapped for the Load-older button on the cap / drift latch. The dot is
   the shared .pulse-dot (currentColor → this block's secondary text). */
#reader-tweets-seeking {
  padding: 10px 12px;
  text-align: center;
  font-size: 13px;
  color: var(--reader-text-secondary);
}

/* ── TWEET ROW (compact, styled like author rows) ──────────────────────────── */
.reader-tweet {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  cursor: pointer;
  border-bottom: 1px solid var(--reader-border-row);
  transition: background 0.15s;
  font-size: 13px;
  line-height: 1.35;
  overflow: hidden;
}
.reader-tweet:hover {
  background: var(--reader-bg-hover);
}
.reader-tweet.unread {
  font-weight: 600;
}
.reader-tweet.read {
  opacity: var(--reader-read-opacity);
}
#reader-detail-pane.column-active .reader-tweet.selected {
  border: 2px solid var(--reader-border-focus);
  background: var(--reader-bg-focus);
  padding: 5px 10px;
  opacity: 1;
}
/* Selection is also visible when the posts column isn't the keyboard-active one. */
.reader-tweet.selected {
  background: var(--reader-bg-selected);
}
.reader-tweet-prefix {
  flex-shrink: 0;
  font-size: 12px;
}
.reader-tweet-prefix.repost {
  color: var(--reader-text-secondary);
}
.reader-tweet-prefix.quote {
  color: var(--reader-text-accent);
}
.reader-tweet-prefix.reply {
  color: var(--reader-text-accent);
}
/* Author handle shown only in the global "All posts" view. */
.reader-tweet-author {
  color: var(--reader-text-secondary);
  flex-shrink: 0;
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reader-tweet-text {
  color: var(--reader-text-primary);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reader-tweet-time {
  color: var(--reader-text-secondary);
  font-size: 12px;
  flex-shrink: 0;
  margin-left: auto;
}

/* Per-row star toggle. Empty (☆) is quiet until row hover; a filled star (★)
   stays visible so starred rows are scannable. Colors come from the skin's
   custom properties (accent = the skin's highlight), never hardcoded. */
.reader-tweet-star {
  background: none;
  border: none;
  padding: 0 2px;
  flex-shrink: 0;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  color: var(--reader-text-secondary);
  opacity: 0;
  transition: opacity 0.15s, color 0.15s;
}
/* Reveal the empty star on row hover. */
.reader-tweet:hover .reader-tweet-star {
  opacity: 0.7;
}
/* A filled star, and any star under the cursor, stays fully lit + accented.
   Prefixed with .reader-tweet so these tie the row-hover rule's specificity and
   win by source order — hover must never dim a filled or hovered star. */
.reader-tweet .reader-tweet-star.starred,
.reader-tweet .reader-tweet-star:hover {
  color: var(--reader-text-accent);
  opacity: 1;
}

/* Per-row copy-permalink button (todo #4). Quiet until row hover — the same
   understated reveal as the empty star — then accents on hover / while confirming
   the copy. Muted secondary icon; colors come from the skin's custom properties. */
.reader-tweet-permalink {
  background: none;
  border: none;
  padding: 0 2px;
  flex-shrink: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  color: var(--reader-text-secondary);
  opacity: 0;
  transition: opacity 0.15s, color 0.15s;
}
.reader-tweet-permalink svg {
  display: block;
}
.reader-tweet:hover .reader-tweet-permalink {
  opacity: 0.7;
}
.reader-tweet .reader-tweet-permalink:hover,
.reader-tweet .reader-tweet-permalink.copied {
  color: var(--reader-text-accent);
  opacity: 1;
}

/* "★ Starred" rail folder — tint its label with the skin accent so it reads as
   a special view (mirrors the "All posts" pseudo-row anatomy otherwise). */
.reader-person.starred-view .reader-person-name {
  color: var(--reader-text-accent);
}

/* ── COLLAPSED READ GROUP ──────────────────────────────────────────────────── */
.reader-read-collapsed {
  padding: 6px 12px;
  color: var(--reader-text-secondary);
  font-size: 12px;
  cursor: pointer;
  border-bottom: 1px solid var(--reader-border-row);
  transition: background 0.15s;
}
.reader-read-collapsed:hover {
  background: var(--reader-bg-hover);
  color: var(--reader-text-primary);
}
#reader-detail-pane.column-active .reader-read-collapsed.selected {
  border: 2px solid var(--reader-border-focus);
  background: var(--reader-bg-focus);
  padding: 4px 10px;
  color: var(--reader-text-primary);
}

/* ── COLUMN 3: PREVIEW PANE (tweet embed) ──────────────────────────────────── */
#reader-preview-pane {
  flex: 1;
  min-width: 400px;
  height: 100%;
  background: var(--reader-bg);
  border-left: 1px solid var(--reader-border);
  display: flex;
  flex-direction: column;
}
#reader-preview-content {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 16px;
}
#reader-preview-content::-webkit-scrollbar {
  width: 4px;
}
#reader-preview-content::-webkit-scrollbar-thumb {
  background: var(--reader-scrollbar-thumb);
  border-radius: 4px;
}
/* ── IFRAME EMBED ──────────────────────────────────────────────────────────── */
.shadow-iframe-wrapper {
  width: 100%;
  max-width: 550px;
  min-height: 250px;
  flex-shrink: 0;
}
.shadow-iframe {
  width: 100%;
  height: 100%;
  min-height: 250px;
  border: none;
  display: block;
}

/* ── PREVIEW-MODE TOGGLE (Live oEmbed | Reconstructed card) ─────────────────── */
/* Sits at the top of the preview pane; aligns with the list/detail headers. */
#reader-preview-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--reader-border);
  flex-shrink: 0;
}
#reader-preview-label {
  color: var(--reader-text-secondary);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* Keeps the render-mode group at the right of the toolbar. */
#reader-preview-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
#reader-preview-mode,
.reader-dd-seg {
  /* .reader-dd-seg: task 36's segmented groups (👎 scope/retro, rubric filters) reuse
     this exact chrome — one recipe, two sites. */
  display: inline-flex;
  border: 1px solid var(--reader-border);
  border-radius: 8px;
  overflow: hidden;
}
.reader-preview-mode-btn {
  background: none;
  border: none;
  color: var(--reader-text-secondary);
  font-family: inherit;
  font-size: 12px;
  line-height: 1.4;
  padding: 5px 12px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.reader-preview-mode-btn + .reader-preview-mode-btn {
  border-left: 1px solid var(--reader-border);
}
.reader-preview-mode-btn:hover {
  color: var(--reader-text-primary);
  background: var(--reader-bg-hover);
}
.reader-preview-mode-btn.active {
  background: var(--reader-bg-active-header);
  color: var(--reader-text-accent);
  font-weight: 600;
}

/* ── RECONSTRUCTED TWEET CARD (self-hosted; built from stored feed data) ─────── */
/* The card is theme-DECOUPLED from the skin: every .rtc* rule below consumes
   card-scoped --rtc-* tokens, NOT the --reader-* skin vars. The card is a
   LIGHT "island" (James's solarized #f0ead6) — matching the tinted light
   oEmbed embed that is the parity target under the japanese skin (X's white
   embed min()'d against the #f0ead6 `mix-blend-mode: darken` overlay,
   skins/japanese.css:45-58). Light-only since task 53 (the Light/Dark toggle
   and its .rtc-theme-dark variant are gone). LOCKED = James's hexes; SAMPLED
   = X's light-mode chrome darken-tinted against #f0ead6 (deterministic min()
   per channel — e.g. the #cfd9de hairline → #cfd9d6, the #1d9bf0 link →
   #1d9bd6). */
.rtc {
  --rtc-bg: #f0ead6;            /* LOCKED (James) — card AND quoted sub-card */
  --rtc-text: #0f1419;         /* LOCKED (James) — body + name */
  --rtc-name: #0f1419;         /* LOCKED (James, follow-up): "almost black" (FDFDFD was a slip) */
  --rtc-handle: #536471;       /* LOCKED (James): X light-mode secondary, chosen over #53649A */
  --rtc-follow: #006fd6;       /* LOCKED (James): the Follow link */
  --rtc-secondary: #536471;    /* SAMPLED: X secondary #536471 (all channels < overlay → unchanged) */
  --rtc-border: #cfd9d6;       /* SAMPLED: X hairline #cfd9de darken-tinted (B 222→214) */
  --rtc-link: #1d9bd6;         /* SAMPLED: X link #1d9bf0 darken-tinted (B 240→214) */
  --rtc-media-bg: #f0ead6;     /* SAMPLED: media/letterbox cell bg = card bg (was #000) */
  --rtc-quote-border: #cfd9d6; /* SAMPLED: quoted sub-card hairline = --rtc-border */
  --rtc-heart: #f01880;        /* SAMPLED: X like-pink #f91880 darken-tinted (R 249→240) */

  width: 100%;
  max-width: 550px;
  box-sizing: border-box;
  background: var(--rtc-bg);
  border: 1px solid var(--rtc-border);
  border-radius: 2px; /* task 23: the live target renders effectively square (measured ≈2px) */
  padding: 14px 16px 16px;
  color: var(--rtc-text);
  /* task 24: give the card its own webfont so Windows Chrome stops falling back to
     Segoe UI (which reads ~1px larger/heavier than the live embed's Chirp). Sizes
     are unchanged — the ladder already matches the embed (measured 20px body). */
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 15px;
  line-height: 1.45;
}
/* "↻ @handle reposted" banner above a reposted tweet's content. */
.rtc-repost {
  color: var(--rtc-secondary);
  font-size: 13px;
  font-weight: 600;
  margin: 0 0 8px 2px;
}

/* task 24/25: self-thread — the author's preceding tweet stacked above the canonical one,
   joined by a vertical connector in the avatar gutter (mirrors the live embed's thread line). */
.rtc-thread {
  position: relative;
}
.rtc-thread-parent {
  position: relative;
  padding-bottom: 10px;
}
/* task 25: the live embed's thread connector — a hairline (color = --rtc-border, sampled to
   the live #cfd9d6) in the avatar gutter, joining the parent avatar to the child avatar. The
   parent body indents into the right-hand content column (below), so the line runs clear of
   the text — superseding task 24's line-less, full-bleed parent per James's live-parity pass. */
.rtc-thread-parent::before {
  content: '';
  position: absolute;
  left: 23px; /* avatar center: 48/2 avatar − 1 (half the 2px stroke) */
  top: 54px; /* just below the 48px avatar */
  bottom: 3px; /* stop just short of the child avatar's top (not crossing into it) */
  width: 2px;
  background: var(--rtc-border);
}
/* Parent body (task 25): indented to align under the header text (48px avatar + 6px head gap),
   set SMALLER than the child body (measured ≈0.83× in the live embed — the parent is context,
   the child is the focus), and CLIPPED to a few lines. The full text stays in the DOM (screen-
   reader / copy get all of it); overflow is hidden with NO ellipsis, cut at an exact line
   multiple (7 × 20px) so no half-line shows, mirroring the live embed's clean note-tweet cut. */
.rtc-thread-parent .rtc-body {
  margin: 10px 0 2px 54px;
  font-size: 16px; /* the live embed's context-tweet size (smaller than the 20px child body) */
  line-height: 20px; /* integer box (20/24 = 0.83× the child's advance, the measured live ratio) */
  max-height: 140px; /* 7 lines → the clip lands exactly on a line boundary, no half-line */
  overflow: hidden;
}
/* task 34: pale-blue ellipsis pill under a CLIPPED parent body — click expands the parent to
   its full text in place. wireParentExpand injects it only when the body actually overflows
   the clip above, so a short parent renders exactly as before (no reserved space, no dead
   button). Shares the body's 54px content-column indent; wash + dots follow the theme blues
   (the rgba lines are the pre-color-mix fallback), so the dark card theme re-tints both. */
.rtc-parent-expand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: 8px 0 2px 54px;
  padding: 0;
  width: 44px;
  height: 22px;
  border: 0;
  border-radius: 999px;
  background: rgba(29, 155, 214, 0.12);
  background: color-mix(in srgb, var(--rtc-link) 12%, transparent);
  color: var(--rtc-follow);
  cursor: pointer;
}
.rtc-parent-expand:hover {
  background: rgba(29, 155, 214, 0.22);
  background: color-mix(in srgb, var(--rtc-link) 22%, transparent);
}
/* Expanded (pill clicked): lift the clip. Direct-child scope so a nested quote's own body
   keeps whatever clip applies to it; (0,3,0) outspecifies the (0,2,0) clip rule above. */
.rtc-thread-parent.rtc-parent-expanded > .rtc-body {
  max-height: none;
}
/* task 26: the parent is a full embedded tweet — its media / link card / nested quote share the
   body's content-column indent (54px = 48px avatar + 6px head gap) so they clear the thread
   connector, and are NOT height-clamped (only the body TEXT is context-clipped above). Direct-
   child (`>`) scope so the child tweet's own media/card/quote are untouched. */
.rtc-thread-parent > .rtc-media,
.rtc-thread-parent > .rtc-card,
.rtc-thread-parent > .rtc-quote {
  margin-left: 54px;
}
/* task 28: the embed's "Replying to @handle" line, shown when the parent is itself a reply to a
   third party. Sits between the header and the body, sharing the body's 54px content-column indent
   (48px avatar + 6px head gap) so header text, this line, and the body all align on one left edge
   — the live embed's arrangement. Sized to the parent's context body (16px), not the child's. */
.rtc-thread-replying {
  margin: 2px 0 0 54px;
  font-size: 16px;
  line-height: 20px;
  color: var(--rtc-secondary);
}
.rtc-thread-replying a {
  /* --rtc-follow (#006fd6), NOT --rtc-link: these handles are link TEXT, so they take the deeper
     blue the .rtc-link rule locks in above — --rtc-link (#1d9bd6) is the lighter action-GLYPH blue. */
  color: var(--rtc-follow);
  text-decoration: none;
}
.rtc-thread-replying a:hover {
  text-decoration: underline;
}
/* The date-only stamp on the parent's name row ("· Jul 6, 2026") — secondary colour, never
   shrinks or wraps (like the badges), so a long display name ellipsizes around it. */
.rtc-thread-date {
  color: var(--rtc-handle);
  font-size: 15px;
  white-space: nowrap;
  flex-shrink: 0;
}
.rtc-thread-date-sep {
  flex-shrink: 0;
}

.rtc-head {
  display: flex;
  align-items: flex-start;
  gap: 6px; /* task 23: measured ≈5-6px avatar→name in the live embed */
}
.rtc-id {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.4; /* task 23: measured 21.4px line advance at 15px */
}
.rtc-name {
  color: var(--rtc-name);
  font-weight: 700;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rtc-handle {
  color: var(--rtc-handle);
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Handle line (task 23): "@handle · Follow" inline, like the live embed. */
.rtc-handle-row {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
}
.rtc-handle-row .rtc-handle {
  min-width: 0;
}
.rtc-hsep {
  color: var(--rtc-handle);
  flex-shrink: 0;
}
/* ── Verification badges + org affiliation (task 15) ───────────────────────────
   The badge glyph + org square sit on the name line. The name TRUNCATES (its own
   ellipsis) while the badges never shrink — so a long display name never pushes a
   badge off, and a badge is never clipped by the name's overflow. */
.rtc-name-row {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0; /* let the name ellipsize within the row */
}
.rtc-name-row .rtc-name {
  min-width: 0;
}
/* Blue / gold / grey check — a transparent @2x PNG crop of the live X badge
   (data: URI, CSP-allowed), sized to the 15px/700 name like the embed. */
.rtc-badge {
  flex-shrink: 0;
  width: 16px; /* task 23: the EMBED's badge measures 16px CSS (24px @1.4964), not x.com's 1.25em */
  height: 16px;
  display: block;
  object-fit: contain;
}
/* Org-affiliation square: small rounded logo after the check (mirrored media-CF
   URL only — CSP blocks pbs). Hairline border reads it as a distinct chip. */
.rtc-org-badge {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-left: 2px;
  border-radius: 4px;
  object-fit: cover;
  border: 1px solid var(--rtc-border);
}
/* "Follow" link — inline after "@handle ·" like the live embed (task 23 moved
   it off the top-right corner; the X logo lives there now). Targets the
   DISPLAYED author (the ref author on a repost). */
.rtc-follow {
  color: var(--rtc-follow);
  font-size: 15px;
  font-weight: 700;
  text-decoration: none;
  flex-shrink: 0;
  white-space: nowrap;
}
.rtc-follow:hover {
  text-decoration: underline;
}
/* Top-right X logo → status permalink (the embed's chrome). */
.rtc-xlogo {
  margin-left: auto;
  padding-left: 12px;
  margin-top: 2px;
  color: var(--rtc-name);
  flex-shrink: 0;
}
.rtc-xlogo svg {
  display: block;
}
.rtc-xlogo:hover {
  opacity: 0.8;
}

/* Avatar: monogram baseline; upgraded to a background image only if one loads. */
.rtc-avatar {
  flex-shrink: 0;
  border-radius: 50%;
  background-color: hsl(var(--rtc-hue, 20) 42% 45%);
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.rtc-avatar-mono {
  color: rgba(255, 255, 255, 0.95);
  font-weight: 700;
  line-height: 1;
}
.rtc-avatar-has-img .rtc-avatar-mono {
  display: none;
}
.rtc-avatar-lg {
  width: 48px; /* task 23: measured 48px CSS in the live embed */
  height: 48px;
}
.rtc-avatar-lg .rtc-avatar-mono {
  font-size: 21px;
}
.rtc-avatar-sm {
  width: 20px;
  height: 20px;
}
.rtc-avatar-sm .rtc-avatar-mono {
  font-size: 11px;
}

.rtc-body {
  color: var(--rtc-text);
  font-size: 20px; /* task 23: the embed's body is 20px/24px (supersedes task 21's +2px) */
  line-height: 1.2;
  margin: 12px 0 10px;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
}
.rtc-link {
  /* task 24: link TEXT (body URLs + @mentions) uses the deeper #006fd6 to match the
     live embed; the lighter --rtc-link (#1d9bd6) stays for action glyphs (Reply icon,
     Copy hover, focus ring), which are genuinely lighter in the live embed. Dark theme
     is unaffected — both --rtc-follow and --rtc-link resolve to --reader-text-accent. */
  color: var(--rtc-follow);
  text-decoration: none;
}
.rtc-link:hover {
  text-decoration: underline;
}

/* ── Media grid (1–4), Twitter-style rounded mosaic ────────────────────────── */
.rtc-media {
  display: grid;
  gap: 2px;
  margin: 4px 0 12px;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--rtc-border);
}
.rtc-cell {
  position: relative;
  overflow: hidden;
  background: var(--rtc-media-bg);
  min-height: 0;
}
.rtc-media-el {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.rtc-media-1 {
  grid-template-columns: 1fr;
}
.rtc-media-1 .rtc-cell:not(.rtc-cell-video) {
  /* task 24: no forced 16/9 — the cell takes the image's natural ratio (inline
     aspect-ratio stamped from ingest dims) so a tall image (e.g. the Ramp chart) is
     shown in full instead of being top+bottom cropped by object-fit:cover. */
  max-height: 680px;
}
.rtc-media-1 .rtc-cell:not(.rtc-cell-video) .rtc-media-el {
  height: auto;
  max-height: 680px;
  object-fit: contain;
}
/* A lone video keeps its own aspect (no crop) and is capped in height. */
.rtc-media-1 .rtc-cell-video {
  max-height: 510px;
  background: #000;
}
.rtc-media-1 .rtc-cell-video .rtc-media-el {
  height: auto;
  max-height: 510px;
  object-fit: contain;
}
.rtc-media-2 {
  grid-template-columns: 1fr 1fr;
  aspect-ratio: 16 / 9;
}
.rtc-media-3 {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  aspect-ratio: 16 / 9;
}
.rtc-media-3 .rtc-cell:first-child {
  grid-row: span 2;
}
.rtc-media-4 {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  aspect-ratio: 16 / 9;
}
/* Graceful stand-in for pending/blocked media (never a broken-image glyph). */
.rtc-media-missing {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 120px;
  padding: 16px;
  color: var(--rtc-secondary);
  font-size: 12px;
  text-align: center;
  background: var(--rtc-media-bg);
}

/* ── Collapsed transcribed originals (task 35b) ─────────────────────────────── */
/* Once a photo is transcribed, the original image is redundant (James): the principal
   card hides transcribed originals behind the task-34 pale-blue ellipsis pill and the
   text panels below carry the content. Untranscribed siblings (real photos, videos)
   keep rendering; the pill reveals the full grid in place. */
.rtc-ocr-media {
  margin: 4px 0 12px; /* takes over the grid's slot spacing */
}
.rtc-ocr-media .rtc-media {
  margin: 0; /* the wrapper owns the outer spacing */
}
.rtc-ocr-media > .rtc-media + .rtc-ocr-media-expand {
  margin-top: 8px; /* mixed case: visible subset grid above the pill */
}
.rtc-ocr-media-expand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  width: 44px;
  height: 22px;
  border: 0;
  border-radius: 999px;
  /* ORANGE (James 2026-07-18) — distinguishes "hidden transcribed images" from the
     task-34 pale-blue parent-expand pill. Fixed hexes like the white panel: reads the
     same on the cream card, the dark card, and every skin. */
  background: rgba(232, 113, 10, 0.14);
  color: #e8710a;
  cursor: pointer;
}
.rtc-ocr-media-expand:hover {
  background: rgba(232, 113, 10, 0.24);
}

/* ── OCR text from images (task 35) ─────────────────────────────────────────── */
/* A photo that is literally a text passage (book page, article screenshot) renders
   its ingest-stamped transcription as selectable BOLD text on a WHITE panel directly
   below the grid (James 2026-07-18: white background + bold) — full text, never
   clamped (the featured tweet is never re-truncated). The panel reads as the source
   document, so its palette is PINNED to the light card's ink hexes (.rtc:714-724)
   instead of --rtc-* vars: white paper stays white — and stays legible — on the dark
   card and under every skin. */
.rtc-ocr {
  background: #fff;
  border: 1px solid #cfd9d6;
  border-radius: 12px;
  padding: 10px 12px;
  margin: 0 0 12px;
}
.rtc-ocr-label {
  color: #536471;
  font-size: 13px;
  line-height: 1.2;
  margin-bottom: 6px;
}
.rtc-ocr-text {
  color: #0f1419;
  font-weight: 700;
  font-size: 15px;
  line-height: 1.35;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* ── Link-preview card (task 20 — X's og crawl of an external URL) ──────────── */
/* Clone of the live embed's card: bordered 16px-radius block, whole thing one link.
   Large = full-width 1.91:1 image on top, then domain/title/description.
   Summary = a square side thumb; no image = text block alone. */
.rtc-card {
  display: block;
  border: 1px solid var(--rtc-border);
  border-radius: 16px;
  overflow: hidden;
  margin: 4px 0 12px;
  background: var(--rtc-bg);
  color: inherit;
  text-decoration: none;
}
.rtc-card:hover {
  background: rgba(15, 20, 25, 0.03);
}
.rtc-card-media {
  background: var(--rtc-media-bg);
  overflow: hidden;
}
.rtc-card-large .rtc-card-media {
  aspect-ratio: 1.91 / 1;
  border-bottom: 1px solid var(--rtc-border);
}
.rtc-card-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.rtc-card-text {
  padding: 10px 12px;
  min-width: 0;
  align-self: center;
}
.rtc-card-domain {
  color: var(--rtc-secondary);
  font-size: 15px;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rtc-card-title {
  color: var(--rtc-text);
  font-size: 15px;
  line-height: 1.3;
  margin-top: 2px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.rtc-card-desc {
  color: var(--rtc-secondary);
  font-size: 15px;
  line-height: 1.3;
  margin-top: 2px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
/* Summary variant: square thumb left, text column right (arxiv-style 420×420 og images). */
.rtc-card-summary {
  display: flex;
  align-items: stretch;
  min-height: 100px;
}
.rtc-card-summary .rtc-card-media {
  flex: 0 0 118px;
  border-right: 1px solid var(--rtc-border);
}
.rtc-card-summary .rtc-card-media .rtc-card-img {
  width: 118px;
  height: 100%;
  min-height: 118px;
}
.rtc-card-summary .rtc-card-text {
  flex: 1 1 auto;
}
.rtc-card-noimg {
  display: block;
  min-height: 0;
}
/* Nested inside a quote sub-card: slightly tighter to match the smaller container. */
.rtc-quote .rtc-card {
  border-radius: 12px;
  margin: 6px 0 4px;
}

/* ── Nested quoted-tweet sub-card ──────────────────────────────────────────── */
.rtc-quote {
  border: 1px solid var(--rtc-quote-border);
  border-radius: 12px; /* task 23: measured ≈11px in the live embed */
  padding: 12px;
  margin: 4px 0 10px;
}
.rtc-quote-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  margin-bottom: 4px;
  flex-wrap: wrap;
}
.rtc-quote-name {
  color: var(--rtc-name);
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%; /* the wrapping .rtc-name-row caps the width now (task 15) */
}
.rtc-quote-handle {
  color: var(--rtc-secondary);
}
/* Quote sub-card badges: scaled down to the 14px quote name; the name-row is
   capped so a long quoted name truncates instead of shoving the badge across the
   wrap-flex head (task 15). */
.rtc-quote-head .rtc-name-row {
  min-width: 0;
  max-width: 60%;
}
.rtc-quote-head .rtc-badge {
  width: 15.5px;
  height: 15.5px;
}
.rtc-quote-head .rtc-org-badge {
  width: 15px;
  height: 15px;
}
/* task 50: quote sub-card tools — copy the quoted tweet's link / open it on X.
   Right-parked in the wrap-flex head as ONE unit; ALWAYS visible but muted
   (James reads on iPad-Chrome — hover-reveal is unreliable there), full
   opacity + link accent on hover / keyboard focus / while confirming a copy. */
.rtc-quote-tools {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
.rtc-quote-copy,
.rtc-quote-open {
  display: inline-flex;
  align-items: center;
  background: none;
  border: none;
  margin: 0;
  padding: 2px; /* tap slop for touch */
  cursor: pointer;
  color: var(--rtc-secondary);
  opacity: 0.55;
  transition: opacity 0.15s, color 0.15s;
}
.rtc-quote-copy svg,
.rtc-quote-open svg {
  display: block;
  width: 17px;  /* normalizes the 20/22px glyph attrs to the 15px head scale */
  height: 17px;
}
.rtc-quote-copy:hover,
.rtc-quote-copy:focus-visible,
.rtc-quote-copy.copied,
.rtc-quote-open:hover,
.rtc-quote-open:focus-visible {
  opacity: 1;
  color: var(--rtc-link);
}
/* Keyboard focus ring — the .rtc-action idiom (reader.css:1497-1503). */
.rtc-quote-copy:focus,
.rtc-quote-open:focus {
  outline: none;
}
.rtc-quote-copy:focus-visible,
.rtc-quote-open:focus-visible {
  outline: 2px solid var(--rtc-link);
  outline-offset: 2px;
}
/* task 41 contract: stranger snapshot cards are affordance-dead. CSS hide, not a
   markup gate: the <a> navigates with ZERO wiring, and threading a flag through
   quoteCardHtml would ripple 4 call sites of the shared card stack for no gain. */
.rtc.stranger-card .rtc-quote-tools {
  display: none;
}
.rtc-quote-body {
  color: var(--rtc-text);
  font-size: 15px; /* task 23: the embed's quote text is 15px (width-verified) */
  line-height: 1.35;
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
}
.rtc-quote .rtc-media {
  border-radius: 12px;
  margin: 8px 0 2px;
}
.rtc-quote .rtc-media-1 .rtc-cell:not(.rtc-cell-video) {
  /* todo #3: match the main card's 680px cap (was 290px). The old 290px cap combined with
     the inline `aspect-ratio:w/h` to shrink a TALL/portrait QT image's WIDTH — the height
     cap bound and the aspect-ratio then derived a narrow width (width = cap * w/h), so the
     image read as a small, centered portrait instead of filling the sub-card like the live
     embed. The QT sub-card is narrower than the main card, so any chart that fills the main
     card at 680px also fills this sub-card (its full-width height here is even smaller), and
     the cap no longer binds → the image fills the width. Landscape/multi-image QTs are
     unaffected: landscape derives a short height (well under the cap) and multi-image grids
     use .rtc-media-2/3/4, not this .rtc-media-1 rule. */
  max-height: 680px;
}
/* A lone video inside a quote sub-card (task 19 part 2): the video render is the shared
   mediaCellHtml <video> (same as top-level), but cap it to the QT photo height so the sub-card
   stays compact instead of using the top-level 510px cap. */
.rtc-quote .rtc-media-1 .rtc-cell-video,
.rtc-quote .rtc-media-1 .rtc-cell-video .rtc-media-el {
  max-height: 290px;
}
.rtc-quote-missing {
  color: var(--rtc-secondary);
  font-size: 14px;
  text-align: center;
  padding: 12px;
}
/* task 18: a grandchild nested one level deeper (repost-of-quote / QT-of-QT). Same border/radius
   language as the sub-card, tighter padding + type so the extra nesting reads as "inside". Depth is
   capped at one by the schema (ref.quote.quote is never stored), so this is the deepest card. */
.rtc-quote .rtc-quote {
  border-radius: 12px;
  padding: 8px 10px;
  margin: 8px 0 2px;
}
.rtc-quote .rtc-quote-head {
  font-size: 14px;
}
.rtc-quote .rtc-quote-body {
  font-size: 14px;
}

/* ── Full timestamp line (tasks 22/23) — the embed's "3:18 PM · Jul 4, 2026",
   below content, above the actions row. Grey permalink (underline on hover,
   never blue) + the embed's ⓘ glyph pinned at the right edge. */
.rtc-datetime {
  display: flex;
  align-items: center;
  margin: 0 0 12px;
  font-size: 15px;
  line-height: 1.3;
  color: var(--rtc-secondary);
}
.rtc-datetime-link {
  color: inherit;
  text-decoration: none;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rtc-datetime-link:hover {
  text-decoration: underline;
}
.rtc-info {
  margin-left: auto;
  padding-left: 8px;
  display: flex;
  color: var(--rtc-secondary);
  flex-shrink: 0;
}
.rtc-info svg {
  display: block;
}

/* ── Card action row (live parity, task 23; interactivity from task 16) ────────
   Heart+count → like intent, bubble+Reply → reply intent, X link glyph +
   "Copy link to post" → the task-16 copy. A hairline separates it from the
   timestamp line, exactly like the embed. Labels are the embed's 15px/700
   secondary grey; the heart is X pink, the bubble X blue (both theme-tokened). */
.rtc-actions {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 24px;
  border-top: 1px solid var(--rtc-border);
  padding: 11px 0 10px;
  position: relative;
}
.rtc-action {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: none;
  margin: 0;
  padding: 0;
  cursor: pointer;
  color: var(--rtc-secondary);
  font: inherit;
  line-height: 1;
  text-decoration: none;
  -webkit-appearance: none;
  transition: color 0.15s, opacity 0.15s;
}
.rtc-action svg {
  display: block;
}
.rtc-action-like {
  color: var(--rtc-heart);
}
.rtc-action-reply {
  color: var(--rtc-link);
}
.rtc-action-count,
.rtc-action-label {
  font-size: 15px;
  font-weight: 700;
  color: var(--rtc-secondary);
}
.rtc-action-like:hover .rtc-action-count,
.rtc-action-reply:hover .rtc-action-label {
  text-decoration: underline;
}
/* Copy → blue on hover / while confirming; --rtc-link is theme-aware (X blue in
   light, skin accent in dark). */
.rtc-action-copy:hover,
.rtc-action-copy.copied,
.rtc-action-copy:hover .rtc-action-label,
.rtc-action-copy.copied .rtc-action-label {
  color: var(--rtc-link);
}
/* Star — semfilt's own control (no counterpart on the parity target): parked at
   the row's right edge, invisible until card hover / keyboard focus, but always
   shown once starred (the gold state is information). */
.rtc-action-star {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  font-size: 19px; /* text glyph ★/☆ (James) */
  opacity: 0;
}
.rtc:hover .rtc-action-star,
.rtc-action-star:focus-visible,
.rtc-action-star.starred {
  opacity: 1;
}

/* ── "Read N replies" pill (task 23) — the embed's bottom affordance, linking
   out to the full thread on X. Full-round hairline pill, 15px/700 blue. */
.rtc-replies {
  display: block;
  text-align: center;
  border: 1px solid var(--rtc-border);
  border-radius: 9999px;
  padding: 5px 16px;
  color: var(--rtc-follow);
  font-size: 15px;
  font-weight: 700;
  line-height: 1.3;
  text-decoration: none;
}
.rtc-replies:hover {
  background: rgba(0, 111, 214, 0.06);
}
/* Star → gold on the light card; a filled ★ stays lit so it reads as "starred". */
.rtc .rtc-action-star:hover,
.rtc .rtc-action-star.starred {
  color: #c8890a;
}
/* Keyboard focus: visible ring (mouse clicks stay clean via :focus-visible). */
.rtc-action:focus {
  outline: none;
}
.rtc-action:focus-visible {
  outline: 2px solid var(--rtc-link);
  outline-offset: 2px;
}

/* ── FOLLOW MANAGEMENT ─────────────────────────────────────────────────────── */
#reader-add-follow {
  background: none;
  border: 1px solid var(--reader-border);
  color: var(--reader-text-secondary);
  cursor: pointer;
  font-size: 20px;
  line-height: 1;
  padding: 2px 8px;
  border-radius: 4px;
  margin-left: auto;
}
#reader-add-follow:hover {
  color: var(--reader-text-accent);
  border-color: var(--reader-text-accent);
}
#reader-add-follow.open {
  color: var(--reader-text-accent);
  border-color: var(--reader-text-accent);
}
/* task 46: the per-row shelf folded into ONE hover-revealed ⋯ trigger. In-flow at
   opacity 0 (the reveal never shifts layout) — ~125px returned to the name on a 360px
   pane. On touch (hover: none) it stays faintly visible: the old opacity-0 cluster was
   six invisible but TAPPABLE targets on iPad. */
.reader-person-menu-btn {
  background: none;
  border: none;
  color: var(--reader-text-secondary);
  cursor: pointer;
  font-size: 12px;
  padding: 2px 3px;
  line-height: 1;
  opacity: 0;
  transition: opacity 0.15s, color 0.15s;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.reader-person:hover .reader-person-menu-btn,
.reader-person:focus-within .reader-person-menu-btn,
.reader-person-menu-btn[aria-expanded='true'] {
  opacity: 1;
}
.reader-person.inactive .reader-person-menu-btn {
  opacity: 0.8;
}
.reader-person-menu-btn:hover {
  color: var(--reader-text-accent);
}
.reader-person-menu-btn:focus-visible {
  opacity: 1;
  outline: 1px solid var(--reader-border-selected);
  border-radius: 3px;
}
@media (hover: none) {
  .reader-person-menu-btn {
    opacity: 0.55;
  }
}

/* task 46: the anchored per-author menu — body-level fixed singleton (an in-row popover
   would be clipped by the row's overflow:hidden and the rail's scroll clip). z-90: below
   every modal (100) and the lightbox (110) — item activation closes the menu before any
   modal opens, so they never stack. */
#reader-rail-menu {
  position: fixed;
  z-index: 90;
  min-width: 160px;
  max-width: 220px;
  background: var(--reader-bg);
  border: 1px solid var(--reader-border);
  border-radius: 6px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
  padding: 4px 0;
}
#reader-rail-menu[hidden] {
  display: none;
}
.reader-rail-menu-handle {
  padding: 4px 12px 6px;
  font-size: 11px;
  color: var(--reader-text-secondary);
  border-bottom: 1px solid var(--reader-border-row);
  margin-bottom: 4px;
}
#reader-rail-menu .reader-person-action {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  background: none;
  border: none;
  color: var(--reader-text-primary);
  cursor: pointer;
  font-family: inherit;
  font-size: 13px;
  line-height: 1;
  padding: 6px 12px;
  text-align: left;
}
#reader-rail-menu .reader-person-action svg {
  flex-shrink: 0;
  color: var(--reader-text-secondary);
}
#reader-rail-menu .reader-person-action:hover,
#reader-rail-menu .reader-person-action:focus {
  background: var(--reader-bg-hover);
  outline: none;
}
#reader-rail-menu .reader-person-action.danger,
#reader-rail-menu .reader-person-action.danger svg {
  color: var(--reader-text-danger);
}
.reader-rail-menu-sep {
  height: 1px;
  background: var(--reader-border-row);
  margin: 4px 0;
}
/* task 32: persistent (not hover-gated) pause marker beside a paused author's name. */
.reader-person-paused-ind {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  color: var(--reader-text-secondary);
}
.reader-person-paused-ind svg {
  width: 10px;
  height: 10px;
}

/* ── DELETE-CONFIRM MODAL (task 32 — the app's first modal) ─────────────────── */
/* task 36's 👎 dialog shares the overlay + shell chrome (comma'd below) and keeps only
   its own width/field layout — the two-dialog version of the ghost-btn dedupe. */
#reader-modal-overlay,
#reader-dislike-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
}
#reader-modal-overlay[hidden],
#reader-dislike-overlay[hidden] {
  display: none;
}
#reader-delete-modal,
#reader-dislike-modal {
  width: 340px;
  max-width: calc(100vw - 40px);
  background: var(--reader-bg);
  border: 1px solid var(--reader-border);
  border-radius: 8px;
  padding: 18px 20px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.8);
}
#reader-delete-title {
  margin: 0 0 8px;
  font-size: 16px;
  font-weight: 700;
  color: var(--reader-text-heading);
}
#reader-delete-text {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--reader-text-secondary);
}
#reader-delete-label {
  display: block;
  font-size: 12px;
  color: var(--reader-text-secondary);
  margin-bottom: 6px;
}
#reader-delete-label b {
  color: var(--reader-text-primary);
  font-weight: 600;
}
#reader-delete-input {
  width: 100%;
  background: var(--reader-bg-shadow);
  border: 1px solid var(--reader-border);
  border-radius: 4px;
  color: var(--reader-text-primary);
  font-family: inherit;
  font-size: 13px;
  padding: 6px 8px;
}
#reader-delete-input:focus {
  outline: none;
  border-color: var(--reader-text-danger);
}
#reader-delete-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 14px;
}
/* Both modal buttons share the layout; Cancel's chrome is .reader-ghost-btn with a
   neutral tint, Delete keeps its filled danger look below. */
#reader-delete-actions button {
  font-family: inherit;
  font-size: 13px;
  padding: 6px 14px;
  border-radius: 4px;
  cursor: pointer;
  transition: opacity 0.15s, border-color 0.15s;
}
#reader-delete-cancel {
  color: var(--reader-text-primary);
}
#reader-delete-cancel:hover {
  border-color: var(--reader-text-secondary);
}
#reader-delete-confirm {
  background: var(--reader-text-danger);
  border: 1px solid var(--reader-text-danger);
  color: #fff;
}
#reader-delete-confirm:disabled {
  opacity: 0.35;
  cursor: default;
}
#reader-delete-msg {
  min-height: 16px;
  margin-top: 8px;
  font-size: 12px;
  color: var(--reader-text-danger);
}

/* ── MEDIA LIGHTBOX (task 39 — click a photo on the reconstructed card) ─────── */
/* x.com's full-screen image viewer, adapted: the image at NATURAL size capped to
   the viewport — the grid mosaic cover-crops (a 1200×157 strip renders as a
   sliver), so this is where "full glory" lives. Backdrop is a CONSTANT near-black
   in both card themes (x.com's lightbox is dark everywhere); z-110 sits above the
   z-100 dialogs and under the deploy toast (99999). */
#reader-media-viewer {
  position: fixed;
  inset: 0;
  z-index: 110;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
}
#reader-media-viewer[hidden] {
  display: none; /* the base display:flex would defeat the hidden attribute */
}
#reader-media-viewer-img {
  max-width: calc(100vw - 48px);
  max-height: calc(100vh - 48px);
  width: auto;
  height: auto;
  object-fit: contain; /* natural size capped by the viewport — never upscaled */
}
/* The near-impossible in-viewer load failure (app.js): the standard placeholder,
   recolored — the card-scoped --rtc-* vars don't reach a body-level overlay. */
#reader-media-viewer .rtc-media-missing {
  background: transparent;
  color: #71767b;
}
#reader-media-viewer .rtc-media-missing[hidden] {
  display: none; /* the class's display:flex would defeat the hidden attribute */
}
/* x.com's own lightbox button treatment: 34px circles over the backdrop. Close is
   top-LEFT (as on x.com); chevrons vertically centered at the edges. */
#reader-media-viewer button {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(15, 20, 25, 0.75);
  color: #fff;
  cursor: pointer;
}
#reader-media-viewer button:hover {
  background: rgba(39, 44, 48, 0.75);
}
#reader-media-viewer button[hidden] {
  display: none; /* clamped-out arrows (and both, for a single photo) */
}
#reader-media-viewer-close {
  top: 12px;
  left: 12px;
}
#reader-media-viewer-prev,
#reader-media-viewer-next {
  top: 50%;
  transform: translateY(-50%);
}
#reader-media-viewer-prev {
  left: 12px;
}
#reader-media-viewer-next {
  right: 12px;
}
/* Grid affordance: photos open the viewer; video cells keep their native controls. */
.rtc-cell:not(.rtc-cell-video) .rtc-media-el {
  cursor: pointer;
}

/* ── task 40: full-archive corpus modal (preview → armed approve → progress) ── */
#reader-corpus-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
}
#reader-corpus-overlay[hidden] {
  display: none;
}
#reader-corpus-modal {
  width: 400px;
  max-width: calc(100vw - 40px);
  background: var(--reader-bg);
  border: 1px solid var(--reader-border);
  border-radius: 8px;
  padding: 18px 20px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.8);
}
#reader-corpus-title {
  margin: 0 0 10px;
  font-size: 16px;
  font-weight: 700;
  color: var(--reader-text-heading);
}
.reader-corpus-line {
  margin: 0 0 8px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--reader-text-primary);
}
.reader-corpus-line b {
  font-weight: 700;
}
.reader-corpus-line .muted,
.reader-corpus-line.muted {
  color: var(--reader-text-secondary);
  font-size: 12px;
}
.reader-corpus-line.done {
  color: var(--reader-text-success);
}
.reader-corpus-line.warn {
  color: var(--reader-text-danger);
}
.reader-corpus-line .pulse-dot {
  margin-right: 6px;
}
.reader-corpus-actions {
  display: flex;
  flex-direction: column; /* stacked, full-width purchase buttons (spec §8.2) */
  gap: 8px;
  margin: 12px 0 10px;
}
.reader-corpus-btn {
  width: 100%;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 8px 12px;
  border-radius: 6px;
  cursor: pointer;
  background: var(--reader-bg-shadow);
  border: 1px solid var(--reader-border);
  color: var(--reader-text-primary);
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.reader-corpus-btn:hover:not(:disabled) {
  border-color: var(--reader-text-secondary);
}
/* Armed confirm (and the cancel) go filled-danger — the money click looks like one. */
.reader-corpus-btn.danger {
  background: var(--reader-text-danger);
  border-color: var(--reader-text-danger);
  color: #fff;
}
.reader-corpus-btn:disabled {
  opacity: 0.35;
  cursor: default;
}
.reader-corpus-foot {
  margin-top: 10px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--reader-text-secondary);
}
.reader-corpus-foot b {
  color: var(--reader-text-primary);
}

/* ── task 36: SEMANTIC FILTERS ──────────────────────────────────────────────── */
/* Four surfaces: the card's 👎 action, the "Low Signal (NN) 😂" section, the
   restore-tuning panel, and the rubric editor — plus the 👎 dialog, whose overlay/
   shell/segment chrome is shared with the task-32 modal and the preview toggles
   above (see the comma'd selectors there). */

/* Card 👎 — parked beside the star with the same hover-reveal contract; grayscale
   until hovered so the emoji reads as quiet chrome, not a lit control. */
.rtc-action-dislike {
  position: absolute;
  right: 34px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 16px;
  opacity: 0;
  filter: grayscale(1);
}
.rtc:hover .rtc-action-dislike,
.rtc-action-dislike:focus-visible {
  opacity: 1;
}
.rtc-action-dislike:hover {
  filter: none;
}

/* Ghost-button danger tint (tuning "Delete rule", rubric Delete) — the rail's
   accent-vs-danger split, on the shared ghost chrome. */
.reader-ghost-btn.danger {
  color: var(--reader-text-danger);
}
.reader-ghost-btn.danger:hover {
  border-color: var(--reader-text-danger);
}

/* Top-bar Rubric button lights while its view is open (updateRailSelection). */
#reader-rubric-btn {
  flex-shrink: 0;
  padding: 4px 10px;
  font-size: 12px;
}
#reader-rubric-btn.active {
  color: var(--reader-text-accent);
  border-color: var(--reader-text-accent);
}

/* ── Low Signal section ── pinned above the sequence; the toggle reuses the rail's
   Hidden-folder chrome (button.reader-authors-divider + caret rotate) but drops the
   divider's uppercase transform: the header copy `Low Signal (NN) 😂` is spec-EXACT
   and must render exactly as written. */
.reader-lowsig {
  border-bottom: 1px solid var(--reader-border);
}
button.reader-lowsig-toggle {
  text-transform: none;
  letter-spacing: normal;
  font-size: 12px;
  padding: 7px 12px 6px;
}
.reader-lowsig-label {
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-lowsig-item {
  border-bottom: 1px solid var(--reader-border-row);
}
.reader-lowsig-item:last-child {
  border-bottom: none;
}
/* Row anatomy mirrors .reader-tweet (it reuses the text/time spans) at the read-dim —
   hidden is settled content. NOT .reader-tweet itself: these rows must never join
   keyboard nav, mark-read, or the collapse machinery (see lowSignalRowHtml). */
.reader-lowsig-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px 2px;
  cursor: pointer;
  font-size: 13px;
  line-height: 1.35;
  overflow: hidden;
  opacity: var(--reader-read-opacity);
  transition: background 0.15s;
}
.reader-lowsig-item:hover .reader-lowsig-row {
  background: var(--reader-bg-hover);
  opacity: 1;
}
/* Restore+star — the row star's quiet-until-hover contract. */
.reader-lowsig-star {
  background: none;
  border: none;
  color: var(--reader-text-secondary);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 2px;
  flex-shrink: 0;
  opacity: 0;
  transition: opacity 0.15s, color 0.15s;
}
.reader-lowsig-item:hover .reader-lowsig-star,
.reader-lowsig-star:focus-visible {
  opacity: 1;
}
.reader-lowsig-star:hover {
  color: var(--reader-text-accent);
}
/* The annotation line — what hid it (rule text + scope, or "manual"). */
.reader-lowsig-reason {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px 7px 30px;
  font-size: 11px;
  color: var(--reader-text-secondary);
}
.reader-lowsig-why {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-style: italic;
}
.reader-lowsig-restore {
  margin-left: auto;
  flex-shrink: 0;
  font-size: 11px;
  padding: 1px 8px;
  opacity: 0;
  transition: opacity 0.15s;
}
.reader-lowsig-item:hover .reader-lowsig-restore,
.reader-lowsig-restore:focus-visible {
  opacity: 1;
}

/* ── Restore-tuning panel ── an inline dismissible card at the top of the posts pane
   (decision #7): edit text / narrow / delete rule / leave as-is. */
.reader-tuning {
  margin: 10px 12px;
  border: 1px solid var(--reader-border-selected);
  border-radius: 8px;
  padding: 10px 12px;
  background: var(--reader-bg-selected);
}
.reader-tuning-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
  font-weight: 600;
  color: var(--reader-text-primary);
  margin-bottom: 8px;
}
.reader-tuning-close {
  background: none;
  border: none;
  color: var(--reader-text-secondary);
  cursor: pointer;
  font-size: 12px;
  padding: 2px 4px;
  line-height: 1;
}
.reader-tuning-close:hover {
  color: var(--reader-text-primary);
}
.reader-tuning-rule + .reader-tuning-rule {
  margin-top: 10px;
  border-top: 1px solid var(--reader-border-row);
  padding-top: 10px;
}
/* Shared task-36 form chrome (simplify pass): ONE textarea recipe (tuning edit, rule
   edit/create, the 👎 why box), ONE small-input recipe (tags inputs, tag filter), ONE
   focus ring for the lot. Per-site rules below keep only layout/size deltas. */
.reader-tuning-text,
.reader-rule-edit-text,
#reader-dislike-why {
  width: 100%;
  background: var(--reader-bg-shadow);
  border: 1px solid var(--reader-border);
  border-radius: 4px;
  color: var(--reader-text-primary);
  font-family: inherit;
  font-size: 13px;
  line-height: 1.4;
  padding: 6px 8px;
  resize: vertical;
}
#reader-dislike-tags-input,
.reader-rubric-tagsel,
.reader-rule-edit-tags {
  background: var(--reader-bg-shadow);
  border: 1px solid var(--reader-border);
  border-radius: 4px;
  color: var(--reader-text-primary);
  font-family: inherit;
  font-size: 12px;
  padding: 5px 8px;
}
.reader-tuning-text:focus,
.reader-rule-edit-text:focus,
#reader-dislike-why:focus,
.reader-rule-edit-tags:focus,
#reader-dislike-tags-input:focus {
  outline: none;
  border-color: var(--reader-border-selected);
}
.reader-tuning-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.reader-tuning-actions .reader-ghost-btn {
  font-size: 12px;
  padding: 3px 10px;
}
.reader-tuning-msg {
  min-height: 14px;
  margin-top: 6px;
  font-size: 11px;
  color: var(--reader-text-accent);
}

/* ── 👎 dialog ── shell/overlay chrome shared with the delete modal (comma'd there);
   only the wider box and its field layout live here. */
#reader-dislike-modal {
  width: 440px;
}
#reader-dislike-title {
  margin: 0 0 8px;
  font-size: 16px;
  font-weight: 700;
  color: var(--reader-text-heading);
}
#reader-dislike-text {
  margin: 0 0 10px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--reader-text-secondary);
}
/* #reader-dislike-why chrome + focus: the shared form recipe (tuning section above). */
/* Chips land here async: a pulse-dot "suggesting…" while pending, three suggestion
   buttons on arrival, a quiet degraded line on failure — never a blocked dialog. */
#reader-dislike-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  min-height: 26px;
  margin: 8px 0 2px;
  font-size: 11px;
  color: var(--reader-text-secondary);
}
.reader-dislike-chip {
  background: none;
  border: 1px solid var(--reader-border);
  border-radius: 9999px;
  color: var(--reader-text-primary);
  cursor: pointer;
  font-family: inherit;
  font-size: 11px;
  line-height: 1.35;
  padding: 3px 10px;
  text-align: left;
  transition: border-color 0.15s, background 0.15s;
}
.reader-dislike-chip:hover {
  border-color: var(--reader-text-accent);
}
.reader-dislike-chip.selected {
  border-color: var(--reader-text-accent);
  background: var(--reader-bg-selected);
}
/* "Matches your existing rule X" chips get the ≈ mark — picking one strengthens. */
.reader-dislike-chip.existing .reader-dislike-chip-mark {
  color: var(--reader-text-accent);
  font-weight: 700;
  margin-right: 4px;
}
#reader-dislike-chips.failed {
  font-style: italic;
}
.reader-dislike-field {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
}
.reader-dislike-field[hidden] {
  display: none;
}
.reader-dislike-field-label {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--reader-text-secondary);
  flex-shrink: 0;
  min-width: 52px;
}
#reader-dislike-tags-input {
  flex: 1; /* chrome + focus from the shared input recipe (tuning section) */
}
#reader-dislike-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
}
.reader-dislike-spacer {
  flex: 1;
}
#reader-dislike-actions button {
  font-size: 12px;
  padding: 5px 12px;
}
/* Primary = filled accent (the badge pair — the delete modal's filled-danger sibling). */
#reader-dislike-save {
  background: var(--reader-badge-bg);
  border: 1px solid var(--reader-badge-bg);
  color: var(--reader-badge-text);
  border-radius: 4px;
  font-family: inherit;
  cursor: pointer;
  transition: opacity 0.15s;
}
#reader-dislike-save:disabled {
  opacity: 0.35;
  cursor: default;
}

/* ── Rubric editor ── the posts pane re-purposed (Starred-pattern main-area swap). */
.reader-rubric {
  padding: 10px 12px;
}
.reader-rubric-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--reader-border);
  margin-bottom: 10px;
}
.reader-rubric-tagsel {
  padding: 4px 6px; /* chrome from the shared input recipe (tuning section) */
}
.reader-rubric-authorchip {
  background: var(--reader-bg-selected);
  border: 1px solid var(--reader-border-selected);
  border-radius: 9999px;
  color: var(--reader-text-accent);
  cursor: pointer;
  font-family: inherit;
  font-size: 11px;
  padding: 3px 10px;
}
.reader-rubric-new {
  margin-left: auto;
  font-size: 12px;
  padding: 4px 10px;
}
.reader-rule {
  border: 1px solid var(--reader-border-row);
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 10px;
}
.reader-rule.editing {
  border-color: var(--reader-border-selected);
}
.reader-rule-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
  font-size: 11px;
}
.reader-rule-polarity {
  border-radius: 3px;
  padding: 1px 6px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 10px;
}
.reader-rule-polarity.blacklist {
  color: var(--reader-text-danger);
  border: 1px solid var(--reader-text-danger);
}
.reader-rule-polarity.whitelist {
  color: var(--reader-text-success);
  border: 1px solid var(--reader-text-success);
}
.reader-rule-scope {
  color: var(--reader-text-primary);
  font-weight: 600;
}
.reader-rule-tag {
  color: var(--reader-text-accent);
}
/* Decision #2: whitelist entries are stored, not enforced — say so on the card. */
.reader-rule-inert {
  color: var(--reader-text-secondary);
  font-style: italic;
}
.reader-rule-stats {
  margin-left: auto;
  color: var(--reader-text-secondary);
  flex-shrink: 0;
}
.reader-rule-text {
  font-size: 13px;
  line-height: 1.4;
  color: var(--reader-text-primary);
}
/* Sweep progress line ("sweeping — N hidden" while running, polled). */
.reader-rule-sweep {
  display: flex;
  align-items: center;
  margin-top: 6px;
  font-size: 11px;
  color: var(--reader-text-secondary);
}
.reader-rule-sweep.running {
  color: var(--reader-text-accent);
}
.reader-rule-sweep.error {
  color: var(--reader-text-danger);
}
.reader-rule-examples {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  font-size: 11px;
}
.reader-rule-ex-label {
  color: var(--reader-text-secondary);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 10px;
}
.reader-rule-ex {
  color: var(--reader-text-secondary);
  border: 1px solid var(--reader-border-row);
  border-radius: 4px;
  padding: 1px 6px;
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-decoration: none;
}
a.reader-rule-ex:hover {
  color: var(--reader-text-accent);
  border-color: var(--reader-text-accent);
}
.reader-rule-ex.cex {
  border-style: dashed;
}
.reader-rule-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
}
.reader-rule-actions .reader-ghost-btn {
  font-size: 11px;
  padding: 2px 8px;
}
.reader-rule-edit-tags {
  width: 100%; /* chrome + focus from the shared input recipe (tuning section) */
  margin-top: 6px;
}
.reader-rule-edit-text {
  margin-top: 2px;
}
.reader-rule-edit-note {
  font-size: 10px;
  color: var(--reader-text-secondary);
  font-style: italic;
}
.reader-rule-create-retro {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 6px;
}

/* ── task 41: Stranger Mode (Spectacles) ─────────────────────────────────────
   The confirm modal reuses the delete-modal chrome (overlay/dialog geometry); the
   view mounts in col-2 as a stack of full reconstructed cards with a header card
   above — no row list, no actions row, nothing selectable. */
#reader-stranger-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
}
#reader-stranger-overlay[hidden] {
  display: none;
}
#reader-stranger-modal {
  width: 380px;
  max-width: calc(100vw - 40px);
  background: var(--reader-bg);
  border: 1px solid var(--reader-border);
  border-radius: 8px;
  padding: 18px 20px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.8);
}
#reader-stranger-title {
  margin: 0 0 8px;
  font-size: 16px;
  font-weight: 700;
  color: var(--reader-text-heading);
}
.stranger-modal-ask {
  margin: 0 0 10px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--reader-text-primary);
}
.stranger-modal-ask b {
  color: var(--reader-text-heading);
}
.stranger-modal-cost {
  font-size: 12px;
  line-height: 1.5;
  color: var(--reader-text-secondary);
  background: var(--reader-bg-shadow);
  border: 1px solid var(--reader-border);
  border-radius: 6px;
  padding: 8px 10px;
}
.stranger-modal-capped,
.stranger-modal-existing {
  margin-top: 8px;
  font-size: 11px;
  color: var(--reader-text-secondary);
  font-style: italic;
}
.stranger-modal-loading {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--reader-text-secondary);
  padding: 6px 0 2px;
}
#reader-stranger-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 14px;
}
#reader-stranger-actions button {
  font-family: inherit;
  font-size: 12px;
  padding: 5px 14px;
  border-radius: 4px;
  cursor: pointer;
}
#reader-stranger-go {
  background: var(--reader-text-accent);
  border: 1px solid var(--reader-text-accent);
  color: #fff;
  font-weight: 600;
}
#reader-stranger-go.armed {
  background: var(--reader-text-danger);
  border-color: var(--reader-text-danger);
}
#reader-stranger-go:disabled {
  opacity: 0.55;
  cursor: default;
}
#reader-stranger-msg {
  min-height: 14px;
  margin-top: 8px;
  font-size: 11px;
  color: var(--reader-text-danger);
}

/* the view */
#reader-stranger {
  padding: 10px 14px 20px;
  max-width: 620px;
  margin: 0 auto;
}
#reader-stranger-head {
  border: 1px solid var(--reader-border);
  border-radius: 8px;
  background: var(--reader-bg-shadow);
  padding: 10px 12px;
  margin-bottom: 14px;
}
.stranger-meta {
  font-size: 12px;
  color: var(--reader-text-secondary);
}
.stranger-progress {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--reader-text-primary);
}
.stranger-status.error {
  font-size: 12px;
  color: var(--reader-text-danger);
}
.stranger-empty {
  font-size: 12px;
  color: var(--reader-text-secondary);
  display: flex;
  align-items: center;
  gap: 8px;
}
.stranger-head-actions {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.stranger-head-actions .reader-ghost-btn {
  font-size: 11px;
  padding: 2px 10px;
}
.stranger-card-wrap {
  margin-bottom: 14px;
}
.stranger-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 12px;
  color: var(--reader-text-secondary);
  padding: 12px 0;
}
#reader-stranger-sentinel {
  min-height: 1px;
}

/* ═══ task 48: Finite Scroll public demo (DEMO_MODE) ═════════════════════════
   app.js stamps data-demo on <html> at boot when the demo build flag is on.
   Curator-only STATIC nodes are removed outright at boot (add-author, rubric
   button, status notice) and the per-author ⋯ trigger is simply never rendered;
   this rule is the belt-and-suspenders guard on that trigger. The unread
   badges/counts task 48 hid here are BACK since task 54 — they're viewer-local
   now (post_count minus the viewer's localStorage reads), so the demo shows the
   prod chrome. */
html[data-demo] .reader-person-menu-btn {
  display: none !important;
}
/* The friendly demo empty state ("nothing shared yet") — a touch roomier than
   the reader's terse "No posts yet." so a public landing doesn't look broken. */
html[data-demo] .reader-demo-empty {
  padding: 42px 24px;
  text-align: center;
  line-height: 1.6;
  max-width: 420px;
  margin: 0 auto;
}
