/* ═══════════════════════════════════════════════════════════════════════════
   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;

  /* task 60: 'Twemoji Country Flags' leads EVERY text stack. It is only registered (by
     /flag-emoji.js) where the platform emoji font cannot draw flags — Windows — and its
     unicode-range covers flag code points alone, so the text font is unaffected: an
     unregistered family is skipped, a registered one yields every other character. */
  --reader-font-family: 'Twemoji Country Flags', -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: 14px;
}
#reader-list::-webkit-scrollbar-thumb {
  background: var(--reader-scrollbar-thumb);
  /* 14px grab target, drawn as an 8px pill inset from the edges */
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 7px;
  min-height: 40px;
}
#reader-list::-webkit-scrollbar-thumb:hover {
  background-color: var(--reader-text-secondary);
}
#reader-list::-webkit-scrollbar-thumb:active {
  background-color: var(--reader-text-accent);
}

/* ── 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;
}
/* The Posts header and the post column's empty header beside it share one fixed height,
   so their bottom borders run as one line whatever the Posts header holds (the demo has
   no Rubric button). */
#reader-detail-header,
#reader-preview-header {
  height: 58px;
  border-bottom: 1px solid var(--reader-border);
  flex-shrink: 0;
}
#reader-detail-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 20px;
}
#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: 14px;
}
#reader-tweets::-webkit-scrollbar-thumb {
  background: var(--reader-scrollbar-thumb);
  /* 14px grab target, drawn as an 8px pill inset from the edges */
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 7px;
  min-height: 40px;
}
#reader-tweets::-webkit-scrollbar-thumb:hover {
  background-color: var(--reader-text-secondary);
}
#reader-tweets::-webkit-scrollbar-thumb:active {
  background-color: var(--reader-text-accent);
}
#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: 14px;
}
#reader-preview-content::-webkit-scrollbar-thumb {
  background: var(--reader-scrollbar-thumb);
  /* 14px grab target, drawn as an 8px pill inset from the edges */
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 7px;
  min-height: 40px;
}
#reader-preview-content::-webkit-scrollbar-thumb:hover {
  background-color: var(--reader-text-secondary);
}
#reader-preview-content::-webkit-scrollbar-thumb:active {
  background-color: var(--reader-text-accent);
}
/* ── 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;
}

/* ── Segmented controls in filter dialogs and the rubric ─────────────────── */
.reader-dd-seg {
  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).
     task 60: the flag family leads here too (see :root) — flags in a card body were
     boxed letters on Windows. */
  font-family: 'Twemoji Country Flags', '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-article, /* task 59: the parent's compact article 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);
}
/* task 58: the same line for a reply shown WITHOUT a parent stacked above it. On the main card
   (a reposted reply whose parent never resolved) it sits between header and body at the body's
   20px, pulling the body up under it; in the quote sub-card (a quoted reply — the embed shows
   the line, never the parent) it takes the sub-card's 15px body size. Grey + link blue as above. */
.rtc-replying {
  margin: 12px 0 0;
  font-size: 20px;
  line-height: 1.2;
  color: var(--rtc-secondary);
}
.rtc-replying + .rtc-body {
  margin-top: 2px;
}
.rtc-quote-replying {
  margin: 0 0 2px;
  font-size: 15px;
  line-height: 1.35;
  color: var(--rtc-secondary);
}
.rtc-replying a,
.rtc-quote-replying a {
  color: var(--rtc-follow);
  text-decoration: none;
}
.rtc-replying a:hover,
.rtc-quote-replying a:hover {
  text-decoration: underline;
}
.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) — a two-way swap (task 63) ────── */
/* Once a photo is transcribed, the original image is redundant (James): the principal
   card hides transcribed originals behind the orange ellipsis pill and the text panels
   below carry the content. Untranscribed siblings (real photos, videos) keep rendering.
   task 63 (James, 2026-09-28): the pill SWAPS the two presentations in place — the
   originals and the panels are the same content twice, and a "Copy screenshot" should
   be able to picture the source rather than the transcription. `.rtc-ocr-originals` on
   the wrapper shows the full grid and hides the subset grid AND the sibling .rtc-ocr
   panels; a second click swaps back. The whole state is that one class, so the
   screenshot clone (a cloneNode of the card) inherits it. DOM order is subset grid,
   full grid, pill: the pill sits below whichever grid is showing, where the swapped-out
   content would be. */
.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,
.rtc-ocr-media.rtc-ocr-originals > .rtc-ocr-media-expand {
  margin-top: 8px; /* a grid above the pill: the mixed case's subset, or the full grid once the originals show */
}
.rtc-ocr-media.rtc-ocr-originals > .rtc-media {
  display: none; /* the subset grid yields to the full grid */
}
.rtc-ocr-media.rtc-ocr-originals ~ .rtc-ocr {
  display: none; /* the extracted-text panels step aside while the originals show */
}
.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;
}

/* ── X Article (task 59 — the long-form post an article tweet links) ──────── */
/* Rendered where the link card renders (the article is its analog). The official embed
   shows an article tweet as its bare x.com/i/article link (measured 2026-09-13), so there
   is no parity target: the FEATURED slot renders cover + title + the whole body, never
   clipped (task 23's carve-out — the archive renders what the embed will not); the quote
   sub-card / stacked parent / grandchild render the compact summary-card form with a
   "Read article" pill that swaps the full render in place (no animation — the task-34
   pill idiom). Light-only like the rest of the card (task 53). */
.rtc-article {
  margin: 4px 0 12px;
}
/* Cover: natural aspect (inline aspect-ratio from ingest dims), full width, cropped rather
   than letterboxed when the height cap bites — it is a banner, not content. .rtc-media
   supplies the 16px radius, hairline and overflow clip; the cell/img classes make it
   fallback- and lightbox-wired exactly like tweet media. */
.rtc-article-cover {
  margin: 0 0 12px;
}
.rtc-article-cover .rtc-cell {
  max-height: 420px;
}
.rtc-article-cover .rtc-media-el {
  max-height: 420px;
  object-fit: cover;
}
.rtc-article-title {
  color: var(--rtc-text);
  font-size: 23px;
  line-height: 1.25;
  font-weight: 700;
  margin: 0 0 12px;
  overflow-wrap: anywhere;
}
.rtc-article-preview {
  color: var(--rtc-handle);
  font-size: 17px;
  line-height: 1.5;
  margin: 0 0 12px;
  overflow-wrap: anywhere;
}
/* Body: article typography — larger than the 15px card base, smaller than the 20px tweet
   body, with a reading line-height. Block margins drop to zero at the body's edges. */
.rtc-article-body {
  color: var(--rtc-text);
  font-size: 17px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.rtc-article-body > :first-child {
  margin-top: 0;
}
.rtc-article-body > :last-child {
  margin-bottom: 0;
}
.rtc-article-body p {
  margin: 0 0 16px;
}
.rtc-article-h1,
.rtc-article-h2,
.rtc-article-h3 {
  color: var(--rtc-text);
  font-weight: 700;
}
.rtc-article-h1 {
  font-size: 22px;
  line-height: 1.25;
  margin: 24px 0 12px;
}
.rtc-article-h2 {
  font-size: 19px;
  line-height: 1.3;
  margin: 20px 0 10px;
}
.rtc-article-h3 {
  font-size: 17px;
  line-height: 1.35;
  margin: 16px 0 8px;
}
.rtc-article-body ul,
.rtc-article-body ol {
  margin: 0 0 16px;
  padding-left: 24px;
}
.rtc-article-body li {
  margin: 0 0 6px;
}
.rtc-article-body blockquote {
  margin: 0 0 16px;
  padding: 0 0 0 16px;
  border-left: 3px solid var(--rtc-border);
  color: var(--rtc-handle);
}
/* Code: --rtc-media-bg equals the card ground since task 23's sampling, so a block on it would
   vanish — a faint ink wash (the rgba line is the pre-color-mix fallback) plus the hairline
   sets it off instead. Never wrapped: long lines scroll inside the block, the card never widens. */
.rtc-article-code {
  margin: 0 0 16px;
  padding: 12px 14px;
  border: 1px solid var(--rtc-border);
  border-radius: 8px;
  background: rgba(15, 20, 25, 0.05);
  background: color-mix(in srgb, var(--rtc-text) 5%, transparent);
  color: var(--rtc-text);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
  font-size: 14px;
  line-height: 1.45;
  overflow-x: auto;
  white-space: pre;
}
.rtc-article-code code {
  font: inherit;
}
.rtc-article-figure {
  margin: 16px 0;
}
.rtc-article-figure .rtc-media.rtc-article-media {
  margin: 0;
  border-radius: 12px;
}
.rtc-article-figure figcaption {
  margin: 6px 2px 0;
  color: var(--rtc-handle);
  font-size: 14px;
  line-height: 1.35;
}
.rtc-article-hr {
  border: 0;
  border-top: 1px solid var(--rtc-border);
  margin: 20px 0;
}
/* "Read on X" (stub / unavailable) and "View post on X" (an embedded tweet block): quiet
   bordered pills in the link-text blue. */
.rtc-article-open,
.rtc-article-tweet {
  display: inline-block;
  margin: 0 0 12px;
  padding: 6px 14px;
  border: 1px solid var(--rtc-border);
  border-radius: 999px;
  color: var(--rtc-follow);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
  text-decoration: none;
}
.rtc-article-open:hover,
.rtc-article-tweet:hover {
  background: rgba(15, 20, 25, 0.03);
}
.rtc-article-body .rtc-article-tweet {
  margin: 0 0 16px;
}
.rtc-article-unavailable {
  display: flex;
  align-items: center;
  gap: 10px;
}
.rtc-article-unavailable .rtc-article-open {
  margin: 0;
}
.rtc-article-label {
  color: var(--rtc-handle);
  font-size: 15px;
}
/* Inside a stacked parent / quote sub-card the expanded full render steps down to that
   container's own type scale (the parent body is 16px, the sub-card body 15px). */
.rtc-thread-parent .rtc-article-title,
.rtc-quote .rtc-article-title {
  font-size: 19px;
}
.rtc-thread-parent .rtc-article-body,
.rtc-thread-parent .rtc-article-preview {
  font-size: 16px;
}
.rtc-quote .rtc-article-body,
.rtc-quote .rtc-article-preview {
  font-size: 15px;
}
/* Compact form: the link card's summary silhouette — square thumb left, text right, one
   hairline box. Title and preview each clamp to two lines; the pill / "Read on X" sits under.
   These rules follow the scale rules above on purpose: same specificity, later wins. */
.rtc-article-compact {
  display: flex;
  align-items: stretch;
  margin: 8px 0 4px;
  border: 1px solid var(--rtc-border);
  border-radius: 12px;
  overflow: hidden;
}
.rtc-article-thumb {
  flex: 0 0 96px;
  min-height: 96px;
  border-right: 1px solid var(--rtc-border);
}
.rtc-article-thumb .rtc-card-img {
  width: 96px;
  height: 100%;
  min-height: 96px;
}
.rtc-article-compact .rtc-article-text {
  flex: 1 1 auto;
  min-width: 0;
  padding: 10px 12px;
  align-self: center;
}
.rtc-article-compact .rtc-article-title {
  font-size: 15px;
  line-height: 1.3;
  margin: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.rtc-article-compact .rtc-article-preview {
  color: var(--rtc-secondary);
  font-size: 14px;
  line-height: 1.3;
  margin: 2px 0 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.rtc-article-compact .rtc-article-open {
  margin: 8px 0 0;
  padding: 4px 12px;
  font-size: 14px;
}
/* The "Read article" pill: the task-34 pale-blue wash (rgba = pre-color-mix fallback) with a
   text label instead of the ellipsis, since the swap brings in a whole document, not lines. */
.rtc-article-expand {
  display: inline-flex;
  align-items: center;
  margin: 8px 0 0;
  padding: 0 12px;
  height: 24px;
  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);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.rtc-article-expand:hover {
  background: rgba(29, 155, 214, 0.22);
  background: color-mix(in srgb, var(--rtc-link) 22%, transparent);
}

/* ── 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;
  /* task 62: a size container, so the row can answer its OWN width (the card is 367px on
     an iPad in portrait, 401px landscape, 487px in a 1280px window, 550px at most). */
  container-type: inline-size;
}
.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);
}
/* task 62: the camera — "Copy screenshot", icon-only, right after "Copy link to post"
   (James, 2026-09-26; it began life in the preview toolbar). A normal flex child (46px of
   row: gap + glyph). Always visible, like the quote tools (hover-reveal is unreliable on
   iPad-Chrome); link-blue on hover / focus / while confirming, danger red while reporting
   a failure, dimmed while the capture renders. card-screenshot.js strips it from the
   picture it takes. */
/* Narrow rows yield the copy-link LABEL, not the camera: heart+count, Reply and "Copy link
   to post" (~312px, more with a five-character count) plus the camera (46px) plus the
   hover-revealed dislike/star parked at the right edge (54px) need ~450px; below that the
   iPad's 333/367px rows wrapped the label and put the star on top of the camera. Icon-only
   "Copy link" beside the camera keeps every action; the title/aria-label still say "Copy
   link to post", and the copied check swaps in the same way. Wide cards keep the task-23
   parity label. */
@container (max-width: 450px) {
  .rtc-action-copy .rtc-action-label {
    display: none;
  }
}
.rtc-action-shot:hover:not(:disabled),
.rtc-action-shot:focus-visible,
.rtc-action-shot.copied,
.rtc-action-shot.saved {
  color: var(--rtc-link);
}
.rtc-action-shot.failed {
  color: #f0212e; /* X danger #f4212e darken-tinted (R 244→240), the --rtc-heart recipe */
}
.rtc-action-shot:disabled {
  cursor: progress;
  opacity: 0.5;
}
/* 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);
}
/* task 61: the built-in rule block — the rule-card box, dashed and quieter, because it is
   stated, not owned: no actions, no counts. Deliberately NOT .reader-rule (counts key on it). */
.reader-rule-builtin {
  border: 1px dashed var(--reader-border-row);
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 10px;
}
.reader-rule-builtin .reader-rule-text {
  color: var(--reader-text-secondary);
}
.reader-rule-builtin-tag {
  color: var(--reader-text-secondary);
}
.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;
}

/* ═══ task 67: the public demo's loading screen ══════════════════════════════
   Over the app from first paint until app.js liftLoader(): the pen-sketch owl in
   the dark theme of its fullscreen study (pale ink on #0d1326, orange eyes), in
   the same 80% box, with the name in the eye's orange in the band beneath it.
   Only the demo page carries the node (index.html's demo-only block), so these
   rules match nothing in the gated app.

   The element is one screen, measured in viewport units from the top-left
   corner; ::before is the sheet behind it, over everything a viewer could pan or
   zoom to. They differ on a screen narrower than the app (1160px of columns): a
   phone answers that overflow by widening the box that `inset: 0` fills, and
   the owl would be centred somewhere off screen. */
#reader-loader {
  --owl-eye: #f07f13;
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  z-index: 1000;
  display: grid;
  place-items: center;
  background: #0d1326;
  color: #e9ecf5; /* the owl draws its ink in the element's color */
  transition: opacity 240ms ease; /* app.js LOADER_FADE_MS */
}
#reader-loader::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  background: inherit;
}
#reader-loader.lifting {
  opacity: 0;
  pointer-events: none; /* the app underneath is live while the screen fades */
}
/* Other browsers answer the same overflow by zooming the whole page out to fit,
   loading screen included. So until the lift starts the app is clipped at the
   body — html takes the viewport's overflow, which leaves body's own `hidden`
   on its box — and the page is exactly one screen wide. From the first frame of
   the lift the document is what it always was. */
html:has(> body > #reader-loader:not(.lifting)) {
  overflow: hidden;
}
#reader-loader-owl {
  display: block;
  width: 80vmin;
  height: 80vmin;
  width: min(80vw, 80dvh);
  height: min(80vw, 80dvh);
}
/* The band under the owl's box: a tenth of the height, which in landscape is all
   the room there is. System font, so the name is set once and never swaps. */
#reader-loader-name {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 10vh;
  height: 10dvh;
  margin: 0;
  display: grid;
  place-items: center;
  color: var(--owl-eye);
  font: 700 clamp(20px, 3.6vmin, 30px) / 1 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}

/* ═══ The All posts knockout ═════════════════════════════════════════════════
   A click on All posts plays this in the post column, "Loading new posts"
   beneath the drawing, until the drawing has played through once and the posts
   are in, or until a post opens there (app.js playKnockout; the canvas is drawn
   by knockout-worker.js): the pen-and-ink Tyson knockout from
   animations/mtbttf, on that study's dark paper in this reader's colours. The
   worker reads its paper and ink from this element's background and colour, so
   the drawing's edges and its end-of-pass fade resolve into the column. It
   takes the post's place while it shows; the rail and the posts stay live. */
#reader-knockout {
  flex: 1;
  min-height: 0;
  container-type: size; /* the canvas fits itself to this box (cqw, cqh) */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 16px;
  background: var(--reader-bg);
  color: var(--reader-text-primary); /* the drawing's ink */
  transition: opacity 240ms ease; /* app.js KNOCKOUT_FADE_MS */
}
#reader-knockout[hidden] {
  display: none;
}
#reader-knockout.faded {
  opacity: 0;
}
#reader-knockout:not([hidden]) + #reader-preview-content {
  display: none;
}
/* The drawing's own proportions (its 1306 × 1038 bounds), as large as fits
   above the line of text: 14px of gap and the note's 20px line. */
#reader-knockout-canvas {
  display: block;
  flex: none;
  aspect-ratio: 1306 / 1038;
  width: min(100cqw, (100cqh - 34px) * 1306 / 1038);
}
#reader-knockout-note {
  margin: 0;
  font-size: 15px;
  line-height: 20px;
  color: var(--reader-text-secondary);
}

/* ═══ task 68: the phone layout ══════════════════════════════════════════════
   Small viewports in either orientation, with touch or a mouse — the query
   app.js mirrors as MOBILE_MQ (keep the two strings identical). Larger windows
   and iPads keep the three fixed columns above. James (2026-10-08): names and
   counts but no handles; the
   authors take all but a 44px posts strip on opening and slide to a narrow strip once an author
   is picked (an expand button brings them back); a picked post shows in the top
   two thirds with the posts beneath it.

   #reader-app becomes a two-column, two-row grid. The rail spans both rows on
   the left and its own width (viewport minus 44px, or the 44px strip under
   .rail-collapsed) sizes the `auto` track, so the slide is one width
   transition; the posts fill the right column until .post-open moves the
   preview into the top cell and the posts into the bottom one, as
   .knockout-open does for the All posts drawing. All three classes are
   app.js's (setRailCollapsed / syncPostOpen / playKnockout) and mean nothing
   outside this block. The media query, not a JS-set class, drives the block so the gated
   app — which has no loading screen — never paints the 1160px layout first. */
#reader-rail-toggle,
#reader-posts-expand {
  display: none;
}
@media (max-width: 700px), (max-height: 500px) {
  html {
    overflow: hidden; /* nothing may widen a phone's layout viewport (see task 67 above) */
  }
  #reader-app {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: minmax(0, 2fr) minmax(0, 1fr);
    overflow: hidden;
  }
  #reader-list-pane,
  #reader-detail-pane,
  #reader-preview-pane {
    width: auto;
    min-width: 0;
    height: auto;
    min-height: 0;
  }

  /* ── The rail: almost the full width, or the strip ── */
  #reader-list-pane {
    grid-column: 1;
    grid-row: 1 / -1;
    width: calc(100vw - 44px);
    overflow: hidden;
    transition: width 0.22s ease;
  }
  #reader-app.rail-collapsed #reader-list-pane {
    width: 44px;
  }
  @media (prefers-reduced-motion: reduce) {
    #reader-list-pane,
    #reader-rail-toggle svg {
      transition: none;
    }
  }
  #reader-list-header {
    padding: 10px 12px;
    gap: 8px;
  }
  #reader-list-header h2,
  #reader-total-count {
    font-size: 17px;
    white-space: nowrap;
  }
  #reader-add-form {
    padding: 10px 12px;
  }
  /* The expand/collapse button: a double chevron that points at where the rail is going. */
  #reader-rail-toggle,
  #reader-app:not(.rail-collapsed) #reader-posts-expand {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    padding: 0;
    margin-left: auto;
    background: none;
    border: 1px solid var(--reader-border);
    border-radius: 4px;
    color: var(--reader-text-secondary);
    cursor: pointer;
  }
  #reader-add-follow + #reader-rail-toggle {
    margin-left: 0; /* the + already pushes right; the demo has no + */
  }
  #reader-rail-toggle svg {
    transition: transform 0.22s ease;
  }
  #reader-app.rail-collapsed #reader-rail-toggle svg {
    transform: rotate(180deg);
  }

  /* Expanded rows: name + count, no handle; the name takes what the count and the ⋯ leave
     and clips with its ellipsis — that is how a long name is abridged. Taller for a thumb. */
  #reader-app .reader-person {
    padding: 10px 12px;
    gap: 8px;
    font-size: 15px;
  }
  #reader-app .reader-person.selected,
  #reader-list-pane.column-active .reader-person.keyboard-focus {
    padding: 8px 10px; /* the 2px border keeps the row's height */
  }
  #reader-app .reader-person-handle {
    display: none;
  }
  #reader-app .reader-person-name {
    flex: 1 1 auto;
    min-width: 0;
  }
  #reader-app .reader-person-count {
    font-size: 14px;
  }

  /* The strip: each row is its initial (data-initial, set by the row templates), so the
     strip stays a one-tap author switcher rather than a blank bar. Unread = bold and bright;
     the selected row keeps its box; dividers thin to a rule. */
  #reader-app.rail-collapsed #reader-list-header {
    padding: 8px 0;
    justify-content: center;
  }
  #reader-app.rail-collapsed #reader-list-header h2,
  #reader-app.rail-collapsed #reader-add-follow,
  #reader-app.rail-collapsed #reader-add-form {
    display: none;
  }
  #reader-app.rail-collapsed #reader-rail-toggle {
    margin: 0;
  }
  #reader-app.rail-collapsed .reader-person {
    justify-content: center;
    height: 40px;
    padding: 0;
    gap: 0;
  }
  #reader-app.rail-collapsed .reader-person > * {
    display: none;
  }
  #reader-app.rail-collapsed .reader-person::before {
    content: attr(data-initial);
    font-size: 15px;
    font-weight: 400;
    color: var(--reader-text-secondary);
  }
  #reader-app.rail-collapsed .reader-person.has-unread::before {
    font-weight: 700;
    color: var(--reader-text-primary);
  }
  #reader-app.rail-collapsed .reader-person.starred-view::before {
    color: var(--reader-text-accent);
  }
  #reader-app.rail-collapsed .reader-person.selected,
  #reader-app.rail-collapsed #reader-list-pane.column-active .reader-person.keyboard-focus {
    padding: 0;
  }
  #reader-app.rail-collapsed .reader-authors-divider {
    width: auto;
    height: 9px;
    padding: 0;
    margin: 0 8px;
    font-size: 0;
    line-height: 0;
    border-bottom: 1px solid var(--reader-border);
  }
  #reader-app.rail-collapsed .reader-authors-divider > * {
    display: none;
  }

  /* ── The right column: posts, and the top/bottom split once a post is open ── */
  #reader-detail-pane {
    grid-column: 2;
    grid-row: 1 / -1;
  }
  #reader-preview-pane {
    display: none;
  }
  #reader-app.post-open #reader-preview-pane,
  #reader-app.knockout-open #reader-preview-pane {
    display: flex;
    grid-column: 2;
    grid-row: 1;
  }
  #reader-app.post-open #reader-detail-pane,
  #reader-app.knockout-open #reader-detail-pane {
    grid-row: 2;
    border-top: 1px solid var(--reader-border);
  }
  #reader-detail-header {
    height: auto;
    padding: 10px 12px;
    gap: 8px;
    overflow: hidden; /* header controls never widen the page */
  }
  /* The post sits above the posts list here: no header row to line up with. */
  #reader-preview-header {
    display: none;
  }
  /* Keep the list laid out so pagination geometry stays valid. Visibility also
     removes its controls from keyboard focus while authors occupy the screen. */
  #reader-app:not(.rail-collapsed) #reader-tweets {
    visibility: hidden;
    pointer-events: none;
  }
  #reader-app:not(.rail-collapsed) #reader-preview-pane {
    display: none;
  }
  #reader-app:not(.rail-collapsed) #reader-detail-pane {
    grid-row: 1 / -1;
    border-top: none;
  }
  #reader-app:not(.rail-collapsed) #reader-detail-header {
    padding: 8px 0;
    justify-content: center;
  }
  #reader-app:not(.rail-collapsed) #reader-detail-header > :not(#reader-posts-expand) {
    display: none;
  }
  #reader-app:not(.rail-collapsed) #reader-posts-expand {
    margin: 0;
  }
  #reader-detail-header h2,
  #reader-detail-count {
    font-size: 17px;
  }
  #reader-app .reader-tweet {
    padding: 9px 12px;
  }
  #reader-detail-pane.column-active .reader-tweet.selected {
    padding: 7px 10px;
  }
  #reader-preview-content {
    padding: 10px;
    gap: 10px;
  }

  /* Dialogs had no height cap; the on-screen keyboard halves a phone's viewport. */
  #reader-delete-modal,
  #reader-dislike-modal,
  #reader-corpus-modal,
  #reader-stranger-modal {
    max-height: calc(100vh - 40px);
    max-height: calc(100dvh - 40px);
    overflow-y: auto;
  }
  /* A 14px gutter is wasted on touch. */
  #reader-list::-webkit-scrollbar,
  #reader-tweets::-webkit-scrollbar,
  #reader-preview-content::-webkit-scrollbar {
    width: 0;
  }

  /* The card's 👎 is gone here (James, 2026-10-09: on an iPhone 13 mini "the thumbs down
     button overlaps the snapshot button"). The actions row is 276px on that phone, and its
     flex children — heart + count, Reply, the icon-only copy link, the camera — reach 262px
     with a five-character count, straight through the 👎 parked absolutely at right 34px and
     into the star at right 0. So: no 👎 on phones, 16px gaps instead of 24px, and the star
     leaves its absolute perch for the end of the flex row (margin-left: auto) — a flex child
     cannot sit on another, and a row too narrow for all five wraps instead of overlapping. */
  .rtc-action-dislike {
    display: none;
  }
  .rtc-actions {
    gap: 16px;
    flex-wrap: wrap;
  }
  .rtc-action-star {
    position: static;
    transform: none;
    margin-left: auto;
  }
}

/* ═══ Touch: hover-revealed controls stay faintly visible ════════════════════
   A control that waits for a hover is invisible on a touch screen — and on iOS
   it costs a tap. WebKit's ContentChangeObserver (Source/WebCore/page/cocoa/
   ContentChangeObserver.cpp) turns a tap that makes content appear into a
   hover instead of a click: an element at opacity 0 counts as hidden
   (isVisuallyHidden), and when the tap's synthetic mouseover reveals a
   clickable one — through an opacity transition it tracks, or a plain style
   change (StyleChangeScope) — the tap ends there and no click is dispatched.
   That is why a post row on an iPhone took one tap to "select" (its star and
   copy-link icons fading in) and a second to open (James, 2026-10-08); a tap
   inside the card (its star and 👎) paid the same price.

   The task-46 ⋯ trigger already sits at a fixed 0.55 on touch (its block
   above, which is why author rows opened on one tap). Here every other
   hover-revealed control follows: the SAME 0.55 in every state, hovered or
   not, and no opacity transition, so a tap changes nothing WebKit watches. A
   starred star keeps its full strength (information, not chrome), a copied
   link its check. Pointer devices (hover: hover — a desktop, an iPad with a
   trackpad) are untouched: the quiet-until-hover contract stays theirs. */
@media (hover: none) {
  .reader-tweet .reader-tweet-star:not(.starred),
  .reader-tweet:hover .reader-tweet-star:not(.starred),
  .reader-tweet .reader-tweet-permalink:not(.copied),
  .reader-tweet:hover .reader-tweet-permalink:not(.copied),
  .reader-lowsig-item .reader-lowsig-star,
  .reader-lowsig-item:hover .reader-lowsig-star,
  .reader-lowsig-item .reader-lowsig-restore,
  .reader-lowsig-item:hover .reader-lowsig-restore {
    opacity: 0.55;
    transition: color 0.15s;
  }
  .rtc .rtc-action-star:not(.starred),
  .rtc:hover .rtc-action-star:not(.starred),
  .rtc .rtc-action-dislike,
  .rtc:hover .rtc-action-dislike {
    opacity: 0.55;
    transition: color 0.15s; /* .rtc-action's own transition names opacity */
  }
}
