:root {
  --bg: #f8fafc;
  --surface: #ffffff;
  --surface-2: #f8fafc;     /* slightly off-surface (cards inside cards, post-preview, etc.) */
  --surface-hover: #f1f5f9; /* hover state on buttons / menu items */
  --border: #e2e8f0;
  --border-strong: #cbd5e1;
  --text: #0f172a;
  --text-muted: #475569;
  --text-faint: #94a3b8;
  --accent: #0085ff;
  --accent-hover: #0066cc;
  --accent-soft: #eff6ff;
  --danger: #e11d48;
  --danger-hover: #be123c;
  --danger-soft: #fff1f2;
  --danger-border: #fecdd3;
  --danger-text: #9f1239;
  --warning-soft: #fffbeb;
  --warning-border: #fde68a;
  --warning-text: #92400e;
  /* Notification row tinting per reason — visible 2px border that
   * inherits dark-mode tones via the [data-theme="dark"] block.
   * Custom-theme slots can override these on a per-slot basis. The
   * verified row also paints a rainbow gradient over the border —
   * the single-color token below is the fallback for the inline
   * SVG glyph (which uses currentColor). */
  --notif-like-border: #f9a8d4;
  --notif-repost-border: #86efac;
  --notif-follow-border: #93c5fd;
  --notif-starterpack-border: #fdba74;
  --notif-verified-border: #8b5cf6;
  /* Card tint variants — themable per slot via the custom-theme
   * modal. Default values match the pre-token-isation appearance:
   *   - repost: solid lime, border falls back to --border
   *   - masked: precomputed alpha-over-white wash of the masked
   *     accent (rgba(212,24,61,0.08) over #ffffff = #fbedef; the
   *     0.30 border = #f2bac5)
   *   - repost+masked: solid orange-200, warm muted border. */
  --card-repost-bg: #ecfccb;
  --card-repost-border: #e2e8f0;
  --card-masked-bg: #fbedef;
  --card-masked-border: #f2bac5;
  --card-repost-masked-bg: #fed7aa;
  --card-repost-masked-border: #d97f43;
  /* 멀티 컬럼 의 프로필 컬럼 배너 — 리포스트 lime 보다 파랑빛.  연한 sky
   * blue (= 리포스트 hue 73 → blue hue 210, 같은 saturation/lightness).
   * 사용자 spec 2026-05-27. */
  --column-profile-banner-bg: #cfe5fb;
  /* 검색 컬럼 배너 — 다크 모드 기준 남색 (사용자 spec 2026-05-27).
   * light 는 그 대응 연한 indigo. */
  --column-search-banner-bg: #dde0fa;
  /* 알림 컬럼 배너 — 라이트 모드 기준 연한 핑크 (사용자 spec 2026-05-27). */
  --column-notif-banner-bg: #fce7f3;
  /* 좋아요 / 북마크 컬럼 배너 — 알림 컬럼 과 동일 색 (사용자 spec 2026-05-27
   * 5 차).  세 종류 모두 "내 활동 기반" 분류 라서 시 각 적 으로 묶 임. */
  --column-likes-banner-bg: #fce7f3;
  --column-bookmarks-banner-bg: #fce7f3;
  /* 커스텀 피드 컬럼 배너 — 라이트 기준 연한 주황 (사용자 spec 2026-05-27 5 차). */
  --column-feed-banner-bg: #fde4c6;
  /* 커스텀 리스트 컬럼 배너 — 라이트 기준 연한 보라 (사용자 spec 2026-05-27 9 차). */
  --column-list-banner-bg: #ede1fc;
  --toast-bg: rgba(15, 23, 42, 0.92);
  --toast-fg: #ffffff;
  --shadow-md: 0 8px 24px rgba(15, 23, 42, 0.12);
  --shadow-toast: 0 4px 12px rgba(0, 0, 0, 0.18);
  --radius: 10px;
  --radius-lg: 14px;
  color-scheme: light;
}

[data-theme="dark"] {
  --bg: #0b1220;
  --surface: #15213a;
  --surface-2: #0f1a2e;
  --surface-hover: #23304d;
  --border: #25334f;
  --border-strong: #3b4a6b;
  --text: #e2e8f0;
  --text-muted: #94a3b8;
  --text-faint: #64748b;
  --accent: #4aa8ff;
  --accent-hover: #7cbcff;
  --accent-soft: #19355c;
  --danger: #fb7185;
  --danger-hover: #f43f5e;
  --danger-soft: #3f0e1a;
  --danger-border: #6b1b2d;
  --danger-text: #fda4af;
  --warning-soft: #3a2c0a;
  --warning-border: #b45309;
  --warning-text: #fde68a;
  --notif-like-border: #831843;
  --notif-repost-border: #14532d;
  --notif-follow-border: #1e3a8a;
  --notif-starterpack-border: #9a3412;
  --notif-verified-border: #6d28d9;
  /* Card tint variants — dark counterparts. Masked uses a redder
   * tint (R-dominant) so the card reads clearly as "danger / masked"
   * rather than the previous neutral-warm purple. Precomputed from
   * rgba(220, 60, 70, 0.30) over surface = rgb(21,33,58):
   *   - bg: ~#51293e  (R 81, G 41, B 62 — R clearly dominant)
   *   - border: rgba(220, 60, 70, 0.55) over surface ≈ #823041
   * repost+masked = warm amber-brown (orange-900 area). */
  --card-repost-bg: #15353a;
  --card-repost-border: #25334f;
  --card-masked-bg: #51293e;
  --card-masked-border: #823041;
  --card-repost-masked-bg: #48260e;
  --card-repost-masked-border: #7c4a1f;
  /* 다크 모드 프로필 컬럼 배너 — 리포스트 #15353a (cyan-teal) 의 hue 만
   * 파랑 (210) 으로 shift, same saturation/lightness.  어두운 navy. */
  --column-profile-banner-bg: #1a2f4c;
  /* 다크 모드 검색 컬럼 배너 — 남색 (사용자 spec 2026-05-27).
   * indigo-blue 의 어두운 톤. */
  --column-search-banner-bg: #1c2150;
  /* 다크 모드 알림 컬럼 배너 — 라이트 핑크 의 어두운 대응. */
  --column-notif-banner-bg: #4a1d3a;
  /* 다크 모드 좋아요 / 북마크 / 커스텀 피드 — 알림 과 같은 핑크 / 어두운
   * 주황 (사용자 spec 2026-05-27 5 차). */
  --column-likes-banner-bg: #4a1d3a;
  --column-bookmarks-banner-bg: #4a1d3a;
  --column-feed-banner-bg: #3a2515;
  --column-list-banner-bg: #2c1e4a;
  /* Dark mode toasts: keep the capsule dark (not light slate) so a
   * notification doesn't strobe brightness against a dark page. Bg is
   * slightly elevated from the page bg (#0b1220) so the edges still
   * read; fg stays near-white for clear contrast. */
  /* 사용자 spec 2026-05-27 : 다크모드 토스트 는 밝 은 색 (light theme 반전).
   * 다크 배경 위 의 어두 운 토스트 visibility 낮 던 사용자 보고. */
  --toast-bg: rgba(241, 245, 249, 0.96);
  --toast-fg: #0f172a;
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5);
  --shadow-toast: 0 4px 16px rgba(0, 0, 0, 0.6);
  color-scheme: dark;
}

* { box-sizing: border-box; }

/* Suppress the browser's auto-focus / mouse-click focus outline app-
 * wide. Keyboard navigation (Tab) still shows the outline via
 * :focus-visible so screen-reader / keyboard users keep their cue,
 * but a dialog or button receiving focus from showModal() / a tap /
 * programmatic .focus() no longer paints a stray blue ring. */
:focus:not(:focus-visible) { outline: none; }

html, body {
  margin: 0;
  padding: 0;
  /* Disable iOS double-tap-to-zoom (and the ~300ms tap delay that
   * Safari uses to wait for a possible second tap). `manipulation`
   * specifically opts out of the double-tap-zoom heuristic while
   * keeping single-finger panning. Pinch is already blocked via
   * meta viewport + gesturestart preventDefault. */
  touch-action: manipulation;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Pretendard Variable",
    Pretendard, system-ui, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
  line-height: 1.5;
  /* dvh tracks the dynamic viewport (shrinks with keyboard / browser
   * chrome). Old browsers without dvh fall back to vh. */
  height: 100vh;
  height: 100dvh;
  /* Kill the user-agent default 8px body margin. With the margin in
   * place, content in the main pane (container padding-top: 12px)
   * starts at viewport-Y 20px while the sidebar's position:fixed
   * top:12px rail sits at viewport-Y 12px — a visible 8px misalignment
   * between the home button and the first notification / post card. */
  margin: 0;
  /* Body itself doesn't scroll — the scrollbar belongs to <main>, so
   * it visually sits between the header and footer instead of running
   * the full height of the viewport across them. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
}
/* Lightbox lock — disables main's scroll without resorting to
 * position: fixed / scroll-position juggling (we don't need it now
 * that the page scroll already lives on main, not body). */
html.lightbox-open main { overflow: hidden !important; }

a {
  color: var(--accent);
  text-decoration: none;
}
a:hover { text-decoration: underline; }

button {
  font: inherit;
  cursor: pointer;
}

button:disabled { cursor: not-allowed; opacity: 0.5; }

input, button {
  font-family: inherit;
}

/* 사용자 spec 2026-05-27 : 오토포커스 시각 표시 모두 안 보이게.  universal
 * :focus-visible 의 outline 제거 + specific selector override 차단 위해
 * !important.  키보드 접근성 cue 도 같이 사라 짐 (사용자 명시 요청). */
:focus-visible {
  outline: none !important;
  outline-offset: 0 !important;
}

/* Layout — header and footer are normal block-flex children of the
 * full-height body; <main> is the scrolling region between them. */
.site-header {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
  z-index: 50;
}
.site-header .inner {
  max-width: 960px;
  margin: 0 auto;
  padding: 12px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 17px;
  font-weight: 600;
  color: var(--text);
}
.brand-emoji {
  font-size: 22px;
  line-height: 1;
}
.brand-version {
  margin-left: 4px;
  color: var(--text-faint);
  font-size: 11px;
  font-weight: 400;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  opacity: 0.7;
  letter-spacing: 0;
}
.nav { display: flex; align-items: center; gap: 6px; font-size: 13px; }
.nav a, .nav .pill {
  padding: 6px 10px;
  border-radius: 8px;
  color: var(--text-muted);
}
.nav a:hover { background: var(--surface-hover); text-decoration: none; }
.nav .session {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  padding: 4px 8px;
  border-radius: 8px;
}
.nav .session img {
  width: 22px; height: 22px; border-radius: 50%;
}
.nav .session button {
  background: transparent;
  border: 0;
  color: var(--text-muted);
  padding: 2px 4px;
  font-size: 12px;
}
.nav .session button:hover { color: var(--text); }
.nav .session-name {
  color: var(--text);
  font-weight: 600;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nav .session-handle {
  color: var(--text-muted);
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (max-width: 520px) {
  .nav .session-name { display: none; }
}

/* Language picker (globe icon + popover menu). */
.lang-picker { position: relative; display: inline-flex; }
.lang-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  border-radius: 8px;
  cursor: pointer;
  padding: 0;
}
.lang-btn:hover { color: var(--text); background: var(--surface-hover); }
.lang-btn[aria-expanded='true'] { color: var(--text); background: var(--surface-hover); }
.lang-btn svg { display: block; }
.lang-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 50;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 4px;
  box-shadow: var(--shadow-md);
  min-width: 140px;
  display: flex;
  flex-direction: column;
  /* 11 locales is too tall to show all at once; cap height to about
   * 5.5 options (rows ≈ 37px, +8px container padding) so the partial
   * row signals there's more to scroll. */
  max-height: calc(5.5 * 37px + 8px);
  overflow-y: auto;
  overscroll-behavior: none;
}
.lang-menu[hidden] { display: none; }
/* Inside the reader-settings dialog the lang picker sits at the LEFT
 * of the quick row, so the default right-anchored popover opens off
 * the dialog edge. Anchor to the left instead so it unfolds toward
 * the rest of the row. */
.reader-settings-quickrow .lang-menu {
  left: 0;
  right: auto;
}
/* When the picker lives inside a <dialog> with internal scroll, JS
 * swaps absolute → fixed so the menu escapes the scroll container.
 * Coordinates are set inline from getBoundingClientRect(). */
.lang-menu.is-fixed {
  position: fixed;
  top: auto;
}
.lang-option {
  display: flex;
  align-items: center;
  gap: 6px;
  background: transparent;
  border: 0;
  border-radius: 6px;
  padding: 8px 10px;
  color: var(--text);
  font-size: 14px;
  cursor: pointer;
  text-align: left;
}
.lang-option:hover { background: var(--surface-hover); }
.lang-option.active { font-weight: 600; }
.lang-option .lang-check {
  display: inline-block;
  width: 14px;
  color: var(--accent);
  font-weight: 700;
}

/* Tool picker — solid down-triangle button + dropdown of tool links. */
.tool-picker { position: relative; display: inline-flex; }
.tool-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  border-radius: 8px;
  cursor: pointer;
  padding: 0;
  font-size: 11px;
  line-height: 1;
}
.tool-btn:hover,
.tool-btn[aria-expanded='true'] {
  color: var(--text);
  background: var(--surface-hover);
}
.tool-btn-tri { transform: translateY(1px); }
.tool-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 50;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 4px;
  box-shadow: var(--shadow-md);
  min-width: 180px;
  display: flex;
  flex-direction: column;
  /* 사용자 spec : 10.5 개 정도 만 보이고 스크롤.  각 row 가 38-40px
   * 라 ~400px max-height 면 ~10 개 + half 1 row 보임. */
  max-height: 400px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.tool-menu[hidden] { display: none; }
.tool-option {
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: 0;
  border-radius: 6px;
  padding: 8px 10px;
  color: var(--text);
  font-size: 14px;
  cursor: pointer;
  text-align: left;
  text-decoration: none;
}
.tool-option:hover { background: var(--surface-hover); text-decoration: none; }
.tool-option.active { font-weight: 600; background: var(--accent-soft); color: var(--accent-hover); }
.tool-option.disabled { color: var(--text-faint); cursor: not-allowed; }
.tool-option.disabled:hover { background: transparent; }
.tool-option .tool-icon { font-size: 16px; width: 20px; text-align: center; }
.tool-option .tool-tag {
  margin-left: auto;
  font-size: 11px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--border);
  color: var(--text-muted);
}
/* experimental tag — 다른 색 (보라 / accent 계열) 으로 planned 와 시각
 * 구분.  이 태그 가 붙은 도구 는 누구 나 접근 가능 — allowlist 는 도구
 * 내부 의 신기능 분기 에만 작용. */
.tool-option .tool-tag-exp {
  background: color-mix(in srgb, #a855f7 28%, transparent);
  color: #a855f7;
}

/* Theme toggle (moon / sun) — same shape as the language picker. */
.theme-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  border-radius: 8px;
  cursor: pointer;
  padding: 0;
}
.theme-btn:hover { color: var(--text); background: var(--surface-hover); }
.theme-btn svg { display: block; }

/* Account picker — person-pictogram button + popover with name and
 * sign-out. Replaces the inline "avatar + name + @handle + logout"
 * pill that overflowed on narrow viewports. */
.session-picker { position: relative; display: inline-flex; }
.session-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  border-radius: 8px;
  cursor: pointer;
  padding: 0;
}
.session-btn:hover,
.session-btn[aria-expanded='true'] {
  color: var(--text);
  background: var(--surface-hover);
}
.session-btn svg { display: block; }
.session-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 50;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px;
  box-shadow: var(--shadow-md);
  min-width: 220px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.session-menu[hidden] { display: none; }
.session-menu-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px 8px;
  border-bottom: 1px solid var(--border);
}
.session-menu-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}
.session-menu-who {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.session-menu-who strong {
  font-size: 14px;
  color: var(--text);
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.session-menu-handle {
  color: var(--text-muted);
  font-size: 12px;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.session-menu-section-label {
  margin: 6px 6px 2px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.session-menu-account-wrap {
  display: flex;
  align-items: stretch;
  gap: 2px;
}
.session-menu-account {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 6px 10px;
  margin: 0;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font: inherit;
  min-width: 0;
}
.session-menu-account:hover { background: var(--surface-hover); }
.session-menu-account-out { opacity: 0.55; }
.session-menu-account-out:hover { opacity: 1; }
/* Active row: full opacity, outlined with the accent so it reads as
   "selected" rather than "disabled". The wrap loses its gap-induced
   strip too, so the highlight feels intentional. */
.session-menu-account-active {
  cursor: default;
  background: color-mix(in srgb, var(--accent, #0085ff) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent, #0085ff) 55%, transparent);
  opacity: 1;
}
.session-menu-account-active:hover {
  background: color-mix(in srgb, var(--accent, #0085ff) 14%, transparent);
}
.session-menu-account-badge {
  margin-left: auto;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--accent, #0085ff);
  flex-shrink: 0;
}
.session-menu-account-forget {
  flex-shrink: 0;
  background: transparent;
  border: 0;
  border-radius: 6px;
  width: 28px;
  font-size: 18px;
  line-height: 1;
  color: var(--text-muted);
  cursor: pointer;
}
.session-menu-account-forget:hover {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}
.session-menu-account-avatar {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
}
.session-menu-account-avatar-fallback {
  background: linear-gradient(135deg, var(--accent, #0085ff), #6ee7b7);
}
.session-menu-account-who { display: flex; flex-direction: column; min-width: 0; }
.session-menu-account-who strong {
  font-size: 13px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.session-menu-account-handle {
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.session-menu-add {
  background: transparent;
  border: 0;
  border-radius: 6px;
  padding: 8px 10px;
  margin-top: 4px;
  text-align: left;
  font-size: 14px;
  color: var(--accent, #0085ff);
  cursor: pointer;
}
.session-menu-add:hover { background: var(--surface-hover); }
.session-menu-logout {
  background: transparent;
  border: 0;
  border-radius: 6px;
  padding: 8px 10px;
  text-align: left;
  font-size: 14px;
  color: var(--danger);
  cursor: pointer;
}
.session-menu-logout:hover { background: var(--surface-hover); }

.auth-section-head { margin-bottom: 14px; }
.auth-section-head h2 { margin: 0 0 4px; font-size: 18px; }
.auth-section-head .hint { margin: 0 0 10px; }
.auth-section-cancel { margin-top: 4px; }
.auth-section-notice { margin: 4px 0 14px; }
.auth-add-row {
  display: flex;
  justify-content: center;
  margin: 14px 0 22px;
}

/* Signed-in summary on the home page — avatar + name + switch
   button, sized to feel like a compact card. */
.home-account-card {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 18px auto 22px;
  padding: 14px 18px;
  max-width: 480px;
  border-radius: 12px;
  background: var(--surface-2, rgba(0, 0, 0, 0.04));
  border: 1px solid var(--border, rgba(0, 0, 0, 0.08));
}
.home-account-avatar {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
}
.home-account-avatar-fallback {
  background: linear-gradient(135deg, var(--accent, #0085ff), #6ee7b7);
}
.home-account-who {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}
.home-account-who strong {
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-account-handle {
  font-size: 13px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-account-switch { flex-shrink: 0; }

.btn.danger {
  background: var(--danger);
  color: #fff;
  border: 1px solid var(--danger);
}
.btn.danger:hover { background: var(--danger-hover); border-color: var(--danger-hover); }

.account-switcher-modal {
  max-width: 460px;
  width: min(92vw, 460px);
}
.account-switcher-modal h3 { margin: 0 0 12px; }
.account-switcher-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 60vh;
  overflow-y: auto;
  margin: 0 -6px 14px;
  padding: 4px 6px;
}
/* Action row at the bottom of the account-switcher modal. When both
 * buttons are present (add + logout), they split the row at equal
 * width; with only "add" present, it takes the full row. */
.account-switcher-actions {
  display: flex;
  gap: 8px;
  margin-top: 4px;
}
.account-switcher-actions > .btn { flex: 1; }
.confirm-dialog {
  max-width: 460px;
  width: min(92vw, 460px);
}

main {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* `contain` — 안 쪽 의 over-scroll bounce 는 visual 로 보이 도록 살리
   * 면서 (사용자 가 pull-to-refresh 의도 를 시각 적 으 로 인식), 스크
   * 롤 chain 이 <html>/<body> 로 전파 안 되도록 차단 → navigator-level
   * pull-to-refresh (페이지 reload) 트리거 도 안 일어남.  사용자 spec
   * (2026-05-26) : "피드 모달 처럼 아래 로 같이 드래그 도 되 게".
   * `none` 이 던 시절 은 visual bounce 자체 가 차단 돼 pull-to-refresh
   * gesture 가 사용자 에 게 안 보이 던 root cause. */
  overscroll-behavior: contain;
}
.container {
  max-width: 960px;
  margin: 0 auto;
  padding: 32px 16px;
}

.site-footer {
  background: var(--surface);
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-muted);
  flex: 0 0 auto;
  z-index: 50;
}
.site-footer .inner {
  max-width: 960px;
  margin: 0 auto;
  padding: 14px 16px;
}
.site-footer .foot-line { margin: 0; }
.site-footer .foot-line + .foot-line { margin-top: 4px; }
.site-footer .attribution { color: var(--text-faint); }
.site-footer .attribution a { color: var(--text-muted); }
.site-footer .attribution a:hover { color: var(--accent); }

/* Cloud (블스구름) */
/* 블스구름 — mode toggle + progress bar */
.cloud-mode-toggle { margin-bottom: 8px; }
.cloud-count-field { margin-top: 0; }
.cloud-count-field[hidden] { display: none; }
.cloud-mode-warning {
  margin: 6px 0 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--warning, #f59e0b) 12%, transparent);
  color: var(--text);
}
.cloud-progress {
  margin: 8px 0 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.cloud-progress[hidden] { display: none; }
.cloud-progress-bar {
  height: 8px;
  border-radius: 4px;
  background: var(--surface-2);
  overflow: hidden;
  position: relative;
}
.cloud-progress-fill {
  height: 100%;
  background: var(--accent, #0085ff);
  border-radius: 4px;
  transition: width 0.12s linear;
}
.cloud-progress-bar.indeterminate .cloud-progress-fill {
  width: 30% !important;
  position: absolute;
  left: 0;
  animation: cloud-progress-slide 1.4s ease-in-out infinite;
}
@keyframes cloud-progress-slide {
  0%   { transform: translateX(-100%); }
  50%  { transform: translateX(120%); }
  100% { transform: translateX(-100%); }
}
.cloud-progress-label {
  font-size: 12px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.cloud-advanced { margin-top: 12px; }
.cloud-advanced > summary {
  cursor: pointer;
  padding: 4px 0;
  user-select: none;
}
.cloud-tokenizer-options {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 4px;
}
.cloud-tokenizer-option {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  cursor: pointer;
}
.cloud-tokenizer-option:hover { background: var(--surface-hover); }
.cloud-tokenizer-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.cloud-tokenizer-head strong { font-size: 14px; }
.cloud-tokenizer-option .hint { margin: 0; }
.cloud-canvas-wrap {
  margin-top: 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
}
.cloud-canvas {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
}
.cloud-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  flex-wrap: wrap;
}
.cloud-actions .hint.ok { color: #059669; }
[data-theme="dark"] .cloud-actions .hint.ok { color: #6ee7b7; }

/* Reaction tool (블스리액션) — two-pane layout: posts left, reactions
 * stream right. Stacks vertically on phones. */
.reaction-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-top: 16px;
}
@media (max-width: 760px) {
  .reaction-grid { grid-template-columns: 1fr; }
}
/* Single-post mode (opened from the post-action "+" menu): the
 * left pane (my-posts list) is irrelevant because the caller
 * picked a specific post, so collapse to one column and hide it. */
.reaction-grid.reaction-grid-single {
  grid-template-columns: 1fr;
}
.reaction-grid.reaction-grid-single .reaction-left {
  display: none;
}
/* Single-post root tweaks — title row is hidden (reaction.js
 * skips rendering it), so tighten the gap between the lone
 * description paragraph and the reactions grid. The default
 * header margin would leave a 16+ px gap that reads as empty
 * space in a small modal. */
.reaction-is-singlepost .cleaner-toolbar { margin-bottom: 4px; }
.reaction-is-singlepost .cleaner-toolbar > div > p { margin: 0; }
.reaction-is-singlepost .reaction-grid { margin-top: 8px; }
.reaction-left,
.reaction-right {
  padding: 14px;
  max-height: 70vh;
  overflow-y: auto;
}
.reaction-pane-title { margin: 0 0 10px; font-size: 16px; }
.reaction-post-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.reaction-post {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 8px 10px;
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.reaction-post:hover,
.reaction-post:focus {
  background: var(--surface-hover);
  outline: none;
}
.reaction-post.selected {
  background: var(--accent-soft);
  border-color: var(--accent);
}
.reaction-post-meta,
.reaction-item-head {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--text-muted);
}
/* Match the cleaner sample list — solid pill background around each
 * tag (reply / images / video / external / quote / repost) so they
 * read as distinct chips rather than runs of plain text. */
.reaction-post-meta .badge,
.reaction-item-head .badge,
.reaction-item .badge {
  background: var(--surface-hover);
  color: var(--text-muted);
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.6;
}
.reaction-foot {
  margin: 6px 0 0;
  font-size: 11px;
  color: var(--text-faint);
}
.reaction-time { font-variant-numeric: tabular-nums; }
.reaction-link {
  margin-left: auto;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 14px;
  padding: 0 4px;
}
.reaction-link:hover { color: var(--accent); text-decoration: none; }
.reaction-text {
  margin: 4px 0;
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.reaction-text-sm {
  margin: 4px 0;
  font-size: 12px;
  color: var(--text-muted);
  white-space: pre-wrap;
  word-break: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.reaction-text.empty { color: var(--text-faint); font-style: italic; }
.reaction-post-counts {
  display: flex;
  gap: 6px;
  font-size: 12px;
  color: var(--text-muted);
}
.reaction-post-counts .sep { color: var(--text-faint); }
.reaction-selected-head {
  padding: 6px 0 10px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 10px;
}
.reaction-stream {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.reaction-item {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  background: var(--surface-2);
}
.reaction-item-who {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}

/* Limit tool */
.limit-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: flex-end;
  margin-bottom: 14px;
}
.limit-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}
@media (max-width: 600px) {
  .limit-grid { grid-template-columns: 1fr; }
}
.limit-card { padding: 14px; }
.limit-card-title { margin: 0 0 2px; font-size: 14px; }
.limit-card-nsid {
  margin: 0 0 10px;
  font-size: 11px;
  color: var(--text-muted);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.limit-bar {
  height: 10px;
  background: var(--surface-2);
  border-radius: 999px;
  overflow: hidden;
  margin: 8px 0;
}
.limit-bar-fill {
  height: 100%;
  transition: width 0.3s ease;
}
.limit-bar-green { background: #10b981; }
.limit-bar-yellow { background: #f59e0b; }
.limit-bar-red { background: var(--danger); }
.limit-numbers { margin: 0 0 4px; font-size: 13px; }
.limit-numbers strong { font-size: 18px; font-variant-numeric: tabular-nums; }
.limit-detail {
  margin: 4px 0 0;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.limit-cors-banner {
  margin-bottom: 14px;
  border-left: 4px solid var(--warning, #f59e0b);
}
.limit-cors-banner h3 { margin: 0 0 8px; font-size: 15px; }
.limit-cors-banner p { margin: 6px 0; }
.limit-probe-result {
  margin-bottom: 14px;
  border-left: 4px solid var(--accent, #0085ff);
}
.limit-probe-result h3 { margin: 0 0 8px; font-size: 15px; }
.limit-probe-result .hint.ok { color: #059669; }
[data-theme="dark"] .limit-probe-result .hint.ok { color: #6ee7b7; }
.limit-probe-result .hint.err { color: var(--danger); }
.limit-probe-headers {
  margin: 8px 0 0;
  padding-left: 0;
  list-style: none;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  max-height: 240px;
  overflow-y: auto;
  background: var(--surface-2);
  border-radius: 6px;
  padding: 8px 12px;
}
.limit-probe-headers li { padding: 2px 0; word-break: break-all; }
.limit-probe-header-name { color: var(--text-muted); }

/* Masking tool — composer + public decoder route */
.panel-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.panel-head h2 { margin: 0; }
.row.gap { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.inline-row { display: inline-flex; gap: 6px; align-items: center; cursor: pointer; }
.hint.ok { color: #059669; }
[data-theme="dark"] .hint.ok { color: #6ee7b7; }
.hint.err { color: var(--danger); }
.pill.warn { background: var(--danger); color: #fff; }

.composer-extra-section {
  padding-top: 6px;
}
.masking-body-hint {
  margin: 4px 0 8px;
  font-size: 12px;
}
.masking-section-divider {
  border: 0;
  border-top: 1px dashed var(--border);
  margin: 14px 0 12px;
}
.masking-modes {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 12px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 12px;
}
@media (max-width: 600px) {
  .masking-modes { grid-template-columns: 1fr; }
}
.masking-modes legend {
  padding: 0 6px;
  font-size: 12px;
  color: var(--text-muted);
}
.masking-mode-opt {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-size: 13px;
}
.masking-mode-sample {
  display: inline-block;
  min-width: 4.5em;
  text-align: center;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  padding: 2px 6px;
  background: var(--surface-2);
  border-radius: 4px;
}
.masking-mode-custom-input {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  padding: 4px 8px;
}
.masking-mode-opt:has(.masking-mode-custom-input) {
  flex: 1 1 100%;
}

/* The "읽기 제한" fieldset reuses .masking-modes for the wrapper +
   .masking-mode-opt for each radio row, but stacks vertically and
   slots the matching input directly under its radio. */
.masking-restrict-modes {
  grid-template-columns: 1fr;
  gap: 8px;
}
.masking-restrict-opt {
  font-size: 14px;
}
.masking-restrict-opt-label { flex: 1; }
.masking-restrict-opt .info-btn { margin-left: auto; }
/* Each "form input goes here" row lives inside a wrapper whose
   padding-left handles the indent under the radio. The inputs
   themselves stay at width:100% (inherited from .input) so they
   honour box-sizing and don't fight grid-item sizing. */
.masking-restrict-row {
  padding-left: 24px;
  box-sizing: border-box;
  min-width: 0;
}
.masking-restrict-row > .input { width: 100%; min-width: 0; }
.masking-restrict-select {
  /* Long option labels would otherwise stretch the closed control
     past the row; clip overflowing text inside the select. */
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}
.masking-restrict-note { margin-left: 24px; }

.backup-mobile-warn {
  margin: 8px 0 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--warning, #f59e0b) 12%, transparent);
  color: var(--text);
  font-size: 13px;
}

/* 블스돋보기 */
.magnifier-form {
  margin: 12px 0 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: stretch;
}
/* Toggle hugs its own width (inline-flex), proceed button rides
   along to the right of the same row. */
.magnifier-top-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.magnifier-scope { flex: 0 0 auto; }
.magnifier-scope .mode {
  /* Even widths on the two scope buttons so "내 계정" / "다른 계정"
     read as a balanced segmented control. */
  min-width: 90px;
  text-align: center;
}
.magnifier-proceed { flex: 0 0 auto; }
.magnifier-search-row { display: flex; gap: 8px; }
.magnifier-search-row[hidden] { display: none; }
.magnifier-input { flex: 1; min-width: 0; }
.magnifier-status { margin: 6px 0 0; min-height: 1.2em; }
.magnifier-status.err { color: var(--danger); }
.magnifier-stack {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.magnifier-results {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.magnifier-results:empty { display: none; }
.magnifier-section { padding: 16px; }
.magnifier-section-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
}
.magnifier-section-head h2 { margin: 0; font-size: 16px; }

.magnifier-id-head {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 14px;
}
.magnifier-avatar {
  flex-shrink: 0;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
}
.magnifier-avatar-fallback {
  background: linear-gradient(135deg, var(--accent, #0085ff), #6ee7b7);
}
.magnifier-id-who { display: flex; flex-direction: column; min-width: 0; }
.magnifier-id-who strong { font-size: 16px; }
.magnifier-handle { color: var(--text-muted); font-size: 14px; }
.magnifier-id-rows { display: flex; flex-direction: column; gap: 6px; }
.magnifier-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  font-size: 13px;
}
.magnifier-row-label {
  flex-shrink: 0;
  width: 110px;
  color: var(--text-muted);
}
.magnifier-row-value {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  word-break: break-all;
}
.magnifier-labels {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}
.magnifier-label {
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-muted);
}
.magnifier-timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.magnifier-timeline li {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 12px;
  align-items: baseline;
  font-size: 13px;
}
.magnifier-timeline time {
  color: var(--text-muted);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
}
@media (max-width: 600px) {
  .magnifier-timeline li { grid-template-columns: 1fr; }
}

.magnifier-pw-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.magnifier-pw-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.magnifier-pw-info { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.magnifier-pw-revoke { font-size: 12px; padding: 4px 10px; }

.magnifier-clients {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.magnifier-client {
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 13px;
  text-decoration: none;
  color: var(--text);
}
.magnifier-client:hover { background: var(--surface-2); }

.magnifier-aturi { display: flex; flex-direction: column; gap: 12px; }
.magnifier-aturi-fields { display: flex; flex-direction: column; gap: 8px; }
.magnifier-aturi-result {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 10px;
  border-radius: 8px;
  background: var(--surface-2);
}
.magnifier-aturi-output {
  flex: 1;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12px;
  word-break: break-all;
}

/* Recent-posts dropdown anchored under the URL input. */
.magnifier-aturi-url-wrap { position: relative; }
.magnifier-aturi-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 20;
  margin-top: 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  max-height: 320px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.magnifier-aturi-dropdown[hidden] { display: none; }
.magnifier-aturi-dropdown-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  width: 100%;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 8px 12px;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font: inherit;
  border-bottom: 1px solid var(--border);
}
.magnifier-aturi-dropdown-item:last-child { border-bottom: 0; }
.magnifier-aturi-dropdown-item:hover { background: var(--surface-2); }
.magnifier-aturi-dropdown-snippet {
  font-size: 13px;
  line-height: 1.35;
}
.magnifier-aturi-dropdown-time {
  font-size: 11px;
  color: var(--text-muted);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
.masking-add-btn { border-color: var(--border); }

/* Inline highlight + × overlay on each masked span in the body */
.mirror-mask {
  background: rgba(212, 24, 61, 0.20);
  border-radius: 3px;
  /* Confine the background paint to each line-fragment's glyph box,
   * so wraps don't paint a strip into the trailing whitespace area. */
  -webkit-box-decoration-break: clone;
          box-decoration-break: clone;
}
[data-theme="dark"] .mirror-mask {
  background: rgba(248, 113, 133, 0.28);
}
.masking-overlay-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.masking-span-target {
  position: absolute;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  pointer-events: auto;
  border-radius: 3px;
  transition: background-color 0.12s;
  z-index: 3;
}
.masking-span-target:hover,
.masking-span-target:focus-visible {
  background: rgba(212, 24, 61, 0.22);
  outline: 0;
}
[data-theme="dark"] .masking-span-target:hover,
[data-theme="dark"] .masking-span-target:focus-visible {
  background: rgba(248, 113, 133, 0.28);
}

.masking-preview-text {
  position: relative;
  white-space: normal;
  background: var(--surface-2);
  border-radius: 8px;
  padding: 12px;
  font-size: 14px;
  line-height: 1.5;
  min-height: 3em;
}
.masking-preview-body {
  white-space: pre-wrap;
}
.masking-preview-over {
  background: rgba(225, 29, 72, 0.22);
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(225, 29, 72, 0.25);
}
.masking-preview-count {
  position: absolute;
  top: 8px;
  right: 12px;
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  background: var(--surface);
  border-radius: 4px;
  padding: 1px 6px;
}
.masking-preview-count.over { color: #fff; background: var(--danger); }
.masking-linkcard-preview {
  display: flex;
  gap: 0;
  margin-top: 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface);
  max-width: 480px;
}
.masking-linkcard-preview-thumb {
  width: 96px;
  height: 96px;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--surface-2);
}
.masking-linkcard-preview-body {
  padding: 8px 12px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  font-size: 13px;
  line-height: 1.4;
}
.masking-linkcard-preview-domain {
  color: var(--text-faint);
  font-size: 11px;
  margin-bottom: 2px;
}
.masking-linkcard-preview-title {
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.masking-linkcard-preview-desc {
  color: var(--text-muted);
  font-size: 12px;
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.masking-anchor-preview {
  color: var(--accent, #0085ff);
  text-decoration: underline;
}
.masking-unmask-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 14px 0;
}
.masking-unmask-reply {
  margin-top: 16px;
}
.masking-target-prompt h3 { margin: 0 0 6px; font-size: 16px; }
.masking-target-prompt p.hint { margin: 0 0 10px; }

/* Rhythm — day-of-week × hour heatmap */
.rhythm-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin: 12px 0;
}
.rhythm-progress { margin: 0; min-height: 1.2em; }
.rhythm-progress.err { color: var(--danger); }
.rhythm-heatmap { margin: 14px 0 8px; overflow-x: auto; }
.rhythm-grid {
  border-collapse: separate;
  border-spacing: 2px;
  font-variant-numeric: tabular-nums;
}
.rhythm-grid th {
  font-weight: 500;
  color: var(--text-muted);
  font-size: 11px;
  text-align: center;
  padding: 0;
}
.rhythm-th-corner { width: 32px; height: 16px; }
.rhythm-th-hour { width: 18px; height: 16px; }
.rhythm-th-day {
  width: 32px;
  text-align: right !important;
  padding-right: 6px !important;
  font-size: 12px !important;
}
.rhythm-cell {
  width: 18px;
  height: 18px;
  border-radius: 3px;
  background: var(--surface-2);
  transition: transform 0.1s;
}
.rhythm-cell.filled:hover {
  transform: scale(1.25);
  outline: 1px solid var(--accent);
}
.rhythm-legend {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 10px;
  font-size: 12px;
}
.rhythm-legend-swatch {
  width: 14px;
  height: 14px;
  border-radius: 3px;
  display: inline-block;
}
.rhythm-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px;
  margin-top: 14px;
}
.rhythm-stat {
  background: var(--surface-2);
  border-radius: 8px;
  padding: 10px 14px;
}
.rhythm-stat-label { font-size: 12px; color: var(--text-muted); }
.rhythm-stat-value {
  font-size: 18px;
  font-weight: 600;
  margin: 2px 0;
}
.rhythm-stat-sub { font-size: 12px; }
.rhythm-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  margin-top: 16px;
}
.rhythm-actions .hint.ok { color: #059669; }
.rhythm-actions .hint.err { color: var(--danger); }
[data-theme="dark"] .rhythm-actions .hint.ok { color: #6ee7b7; }

/* Social — follow-graph analytics */
.social-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin: 12px 0;
}
.social-progress { margin: 0; min-height: 1.2em; }
.social-progress.err { color: var(--danger); }

/* 블스스탯 */
.stat-status { margin: 0 0 12px; min-height: 1.2em; }
.stat-status.err { color: var(--danger); }
.stat-preview {
  display: flex;
  justify-content: center;
  margin: 16px 0;
}
.stat-preview canvas {
  max-width: 100%;
  height: auto;
  border-radius: 14px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
}
.stat-actions {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin: 12px 0 28px;
}
.stat-actions[hidden] { display: none; }
.stat-import-badge {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 13px;
  font-weight: 500;
}
.stat-import-badge[hidden] { display: none; }
.stat-import-clear {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 13px;
  padding: 2px 6px;
  cursor: pointer;
}
.stat-import-clear:hover { color: var(--text); }
.stat-import-clear[hidden] { display: none; }
.stat-font-picker {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 16px 0;
  padding: 12px 16px;
  border-radius: 10px;
  background: var(--surface-2);
}
.stat-font-picker[hidden] { display: none; }
.stat-font-picker .field-label { margin: 0; font-weight: 600; }
.stat-font-select { max-width: 280px; }
.social-sections {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.social-section { padding: 16px; }
.social-section-head { margin-bottom: 12px; }
.social-section-head h2 {
  margin: 0;
  font-size: 16px;
}
.social-section-head .hint { margin: 4px 0 0; }
.social-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 420px;
  overflow-y: auto;
}
.social-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  text-decoration: none;
  color: inherit;
  transition: background-color 0.12s;
}
.social-row:hover { background: var(--surface-2); }
.social-avatar {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
}
.social-avatar-fallback {
  background: linear-gradient(135deg, var(--accent, #0085ff), #6ee7b7);
}
.social-row-body { flex: 1; min-width: 0; }
.social-row-name {
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.social-row-name strong { font-weight: 600; }
.social-row-handle { color: var(--text-muted); font-weight: 400; }
.social-row-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-muted);
}
.social-stat {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-variant-numeric: tabular-nums;
}
.social-stat-score {
  color: var(--accent, #0085ff);
  font-weight: 600;
}
.social-tag {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1.4;
  vertical-align: middle;
  background: var(--surface-2);
  color: var(--text-muted);
}
.social-tag-mutual {
  background: color-mix(in srgb, var(--accent, #0085ff) 22%, var(--surface-2));
  color: var(--accent, #0085ff);
}
.social-tag-follows-me {
  background: color-mix(in srgb, #16a34a 22%, var(--surface-2));
  color: #16a34a;
}
[data-theme="dark"] .social-tag-follows-me { color: #4ade80; }
.social-tag-i-follow {
  background: color-mix(in srgb, #a855f7 22%, var(--surface-2));
  color: #a855f7;
}
[data-theme="dark"] .social-tag-i-follow { color: #c084fc; }
.social-sort-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.social-section-progress {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  padding: 8px 12px;
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text-muted);
  font-size: 13px;
}
.social-section-progress::before {
  content: '⏳';
  animation: social-section-progress-pulse 1.6s ease-in-out infinite;
  display: inline-block;
}
@keyframes social-section-progress-pulse {
  0%, 100% { opacity: 0.45; transform: scale(0.95); }
  50%      { opacity: 1;    transform: scale(1.08); }
}
.social-share-row {
  display: flex;
  justify-content: flex-end;
  margin-top: 12px;
}
.social-share-btn {
  font-size: 0.9em;
  padding: 6px 12px;
}
.share-preview-modal {
  max-width: 560px;
  width: min(92vw, 560px);
}
.share-preview-image-wrap { margin: 10px 0 14px; }
.share-preview-image {
  display: block;
  width: 100%;
  max-width: 100%;
  border-radius: 10px;
  border: 1px solid var(--border, #e5e7eb);
  background: var(--surface-2);
}
.share-preview-image-locked {
  margin: 6px 0 0;
  font-size: 0.85em;
}
.share-preview-alt-label,
.share-preview-text-label {
  display: block;
  margin: 0 0 4px;
  font-size: 12px;
  color: var(--text-muted);
}
.share-preview-alt {
  width: 100%;
  box-sizing: border-box;
  min-height: 56px;
  resize: vertical;
  font-family: inherit;
  font-size: 13px;
  line-height: 1.45;
  margin-bottom: 12px;
}
.share-preview-text {
  width: 100%;
  box-sizing: border-box;
  min-height: 140px;
  resize: vertical;
  font-family: inherit;
  font-size: 14px;
  line-height: 1.5;
  padding: 10px 12px;
}
.share-preview-counter { margin: 4px 0 0; text-align: right; font-variant-numeric: tabular-nums; }
.social-sort-select {
  width: auto;
  max-width: 200px;
  padding: 6px 10px;
  font-size: 13px;
}
.social-sort-dir {
  padding: 4px 10px;
  font-size: 14px;
  min-width: 36px;
}

.masking-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}
.masking-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px 4px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 13px;
  max-width: 100%;
}
.masking-chip.orphaned {
  border-color: var(--danger);
  color: var(--danger);
  text-decoration: line-through;
}
.masking-chip-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 200px;
}
.masking-chip-remove {
  border: 0;
  background: transparent;
  cursor: pointer;
  padding: 0 4px;
  font-size: 16px;
  line-height: 1;
  color: var(--text-muted);
}
.masking-chip-remove:hover { color: var(--danger); }

.masking-preview-text {
  white-space: pre-wrap;
  background: var(--surface-2);
  border-radius: 8px;
  padding: 12px;
  font-size: 15px;
  line-height: 1.5;
  min-height: 4em;
}
.masking-anchor-preview {
  color: var(--link, #0085ff);
  text-decoration: underline;
}
.masking-revealed-body {
  white-space: pre-wrap;
  font-size: 15px;
  line-height: 1.6;
  padding: 8px 4px;
}

/* Bluesky-style post card on the unmask page */
.masking-post-card {
  display: flex;
  gap: 12px;
  padding: 4px 0;
}
.masking-post-avatar-link {
  flex-shrink: 0;
  display: block;
}
.masking-post-avatar {
  display: block;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
}
.masking-post-avatar-fallback {
  background: linear-gradient(135deg, var(--accent, #0085ff), #6ee7b7);
}
.masking-post-main {
  flex: 1;
  min-width: 0;
}
.masking-post-name-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 4px;
}
.masking-post-displayname {
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
}
.masking-post-displayname:hover { text-decoration: underline; }
.masking-post-handle {
  color: var(--text-muted);
  text-decoration: none;
  font-size: 14px;
}
.masking-post-handle:hover { text-decoration: underline; }
.masking-post-time {
  color: var(--text-muted);
  font-size: 14px;
}
.masking-post-time-sep { margin-right: 2px; }
.masking-post-restrict-tag {
  font-size: 11px;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
  line-height: 1.5;
}
.masking-post-restrict-row {
  display: flex;
  margin: 4px 0 6px;
}
.masking-post-card-denied {
  background: var(--surface-2);
  border-radius: 10px;
  padding: 12px;
}
.masking-denied-message {
  margin: 6px 0 0;
  font-size: 14px;
  color: var(--text);
}
.masking-denied-login {
  margin: 4px 0 0;
  font-size: 12px;
}
.masking-restrict-select { margin-top: 8px; max-width: 360px; }
.masking-restrict-note { margin: 4px 0 0; }

.masking-post-card .masking-revealed-body {
  padding: 0;
  margin: 0;
}
.masking-revealed {
  background: #fde68a;
  color: #111;
  padding: 0 3px;
  border-radius: 3px;
}
[data-theme="dark"] .masking-revealed {
  background: #b45309;
  color: #fff;
}

/* Footer share row */
.site-footer .inner { text-align: center; }
.footer-share {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.share-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  border-radius: 8px;
  padding: 0;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.share-btn:hover {
  color: var(--text);
  background: var(--surface-hover);
}
.share-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.share-btn.share-bluesky:hover { color: #0085ff; border-color: #0085ff; }
.share-btn.share-x:hover { color: #000; border-color: #000; }
.share-btn.share-threads:hover { color: #000; border-color: #000; }
.share-btn.share-instagram:hover { color: #c13584; border-color: #c13584; }

/* Toast */
.toast {
  position: fixed;
  left: 50%;
  bottom: 90px;
  transform: translate(-50%, 8px);
  background: var(--toast-bg);
  /* Themed contrast: light theme = white text on dark slate bg, dark
   * theme = near-black text on light slate bg. The error variant
   * overrides to white below so dark text doesn't disappear against
   * the rose-800 background. */
  color: var(--toast-fg);
  padding: 8px 14px;
  border-radius: 999px;
  font-size: 13px;
  z-index: 200;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
  max-width: calc(100% - 32px);
  text-align: center;
  box-shadow: var(--shadow-toast);
  /* pre-line so toasts with \n in their message (e.g. the iOS
   * Safari setup hint) render as multi-line instead of joining
   * the lines into one wrapping blob. */
  white-space: pre-line;
}
.toast.visible {
  opacity: 1;
  transform: translate(-50%, 0);
}
.toast.error { background: #9f1239; color: #fff; }

/* Components */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 20px;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 1px solid transparent;
  background: var(--surface);
  color: var(--text);
  padding: 8px 14px;
  border-radius: var(--radius);
  font-size: 13px;
  font-weight: 500;
  transition: background 0.15s, border-color 0.15s;
}
.btn:hover { background: var(--surface-hover); }
.btn[hidden] { display: none; }
.btn.primary { background: var(--accent); color: #fff; }
.btn.primary:hover { background: var(--accent-hover); }
.btn.secondary { background: var(--surface-hover); border-color: var(--border); }
.btn.secondary:hover { background: var(--border); }
.btn.danger { background: var(--danger); color: #fff; }
.btn.danger:hover { background: var(--danger-hover); }
.btn.ghost { background: transparent; color: var(--text-muted); }
.btn.ghost:hover { background: var(--surface-hover); color: var(--text); }

.input {
  width: 100%;
  box-sizing: border-box; /* keep border+padding inside the declared width */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  font-size: 13px;
  background: var(--surface);
  color: var(--text);
  min-width: 0; /* let flex/grid shrink the input below content-min-width */
}
.input::placeholder { color: var(--text-faint); }
.input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 133, 255, 0.15);
}
.input.mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }

.field { display: block; margin-bottom: 12px; }
.field-label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 4px;
}

.hint { font-size: 12px; color: var(--text-muted); }

.input-with-button {
  display: flex;
  gap: 8px;
  align-items: stretch;
}
.input-with-button .input { flex: 1 1 auto; min-width: 0; }
.input-with-button .btn { flex: 0 0 auto; white-space: nowrap; }

.field-error {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--danger);
}
.field-error[hidden] { display: none; }

.banner {
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: 13px;
}
.banner.error {
  background: var(--danger-soft);
  border-color: var(--danger-soft);
  color: var(--danger-text);
}
.banner.warn {
  background: var(--warning-soft);
  border-color: var(--warning-border);
  color: var(--warning-text);
}
.banner.ok {
  background: #ecfdf5;
  border-color: #a7f3d0;
  color: #065f46;
}

/* ───── Composer ───── */
.composer { padding: 16px; position: relative; }
/* Drag-and-drop visual cue — 파일 / 이미지 를 카드 위로 끌어 왔 을 때
 * 아우트라인 + accent-soft 배경 으로 "여기 에 드롭 가능" 신호.  drag
 * 가 끝나면 (drop / leave / dragend) 자동 제거. */
.composer.is-drag-over {
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
  background: var(--accent-soft);
}
/* submit (특히 video upload) 진행 중 — inert 와 짝.  시각 적 으로 약간
 * dim + cursor: progress 로 사용자 가 "기다리 는 중" 인지.  inert 가
 * 모든 인터랙션 차단, 이 클래스 는 그 시각 표시 만 담당. */
.composer.is-busy {
  opacity: 0.7;
  cursor: progress;
}
.composer-section { margin-bottom: 16px; }
.composer-section:last-of-type { margin-bottom: 0; }
.composer-top { margin-bottom: 16px; }
/* Layered textarea: mirror behind, transparent textarea on top so the
 * red over-limit highlight shines through. Mirror and textarea MUST share
 * the same padding / font / line-height for offsets to line up. */
.composer-textarea-wrap {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
/* Mention typeahead — dropdown floats below the textarea inside
 * the body section. The host gets position:relative so we can
 * anchor the dropdown absolutely; bodyWrap above has overflow:
 * hidden which would clip us if we lived inside it. */
.composer-section-mention-host { position: relative; }
.composer-mention-dropdown {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  z-index: 30;
  margin-top: 4px;
  max-height: 280px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.composer-mention-dropdown[hidden] { display: none; }
.composer-mention-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  cursor: pointer;
  user-select: none;
}
.composer-mention-row:hover,
.composer-mention-row.is-active { background: var(--surface-hover); }
.composer-mention-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--surface-hover);
}
.composer-mention-avatar-fallback { background: var(--surface-hover); }
.composer-mention-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}
.composer-mention-name {
  font-weight: 600;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.composer-mention-handle {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Read-only marker the host tool tacks onto the post text at publish.
 * Rendered in a faded box that visually nests against the textarea so
 * the user can see exactly what's being prepended/appended. */
.composer-locked-affix {
  font-family: inherit;
  font-size: 15px;
  line-height: 1.55;
  color: var(--text-faint);
  background: var(--surface-2);
  padding: 6px 14px;
  border: 1px solid var(--border);
  user-select: none;
  cursor: not-allowed;
  white-space: pre;
  font-style: italic;
}
.composer-locked-affix.prefix {
  border-top-left-radius: var(--radius);
  border-top-right-radius: var(--radius);
  border-bottom: 0;
  margin-bottom: 0;
}
.composer-locked-affix.suffix {
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
  border-top: 0;
  margin-top: 0;
}
.composer-locked-affix[hidden] { display: none; }
/* Pull the textarea's own corners flat against the adjacent affix box.
 * Using ~ (general sibling) instead of + so the rule covers both the
 * lockedAffix and the markerAffix pairs — either can sit between the
 * wrap edge and another hidden affix. */
.composer-locked-affix.prefix:not([hidden]) ~ .composer-textarea-wrap {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.composer-textarea-wrap:has(~ .composer-locked-affix.suffix:not([hidden])) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.composer-textarea-wrap:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 133, 255, 0.15);
}
.composer-textarea-mirror,
.composer-textarea {
  /* shared layout — keep in lockstep */
  padding: 10px 12px;
  font-family: inherit;
  font-size: 14px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow-wrap: break-word;
  /* 캐럿 정렬 fix (사용자 보고 2026-06-05, Windows Chrome) — textarea 는
   * overflow:auto 라 클래식 스크롤바(Windows)가 뜨면 텍스트 폭이 ~15px 좁아
   * 지는데 미러(overflow:hidden)는 전체 폭이라 줄바꿈 지점이 달라져 캐럿이
   * 어긋났음.  두 레이어 모두 스크롤바 거터를 '항상' 예약(stable)해 줄바꿈 폭을
   * 일치시킨다.  스크롤바는 그대로 유지(스크롤 시 거터 안에 표시).  오버레이
   * 스크롤바 환경(mac/iOS)에선 거터가 0 이라 영향 없음. */
  scrollbar-gutter: stable;
}
.composer-textarea-mirror {
  position: absolute;
  inset: 0;
  margin: 0;
  border: 0;
  color: transparent;          /* invisible; only backgrounds show */
  pointer-events: none;
  overflow: hidden;             /* scroll synced via JS to textarea */
}
.mirror-ok { background: transparent; }
.mirror-over {
  background: rgba(225, 29, 72, 0.22);
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(225, 29, 72, 0.25);
}
/* An empty over-limit span (the usual state, when the user is under
 * 300 chars) still paints the box-shadow as a 1px dot at its inline
 * insertion point. That dot reads as a stray pink mark next to the
 * caret — or at the start of the next line when the body ends with
 * a newline. Suppress the painting when there's nothing to mark. */
.mirror-over:empty { background: transparent; box-shadow: none; }
.mirror-link {
  background: rgba(0, 133, 255, 0.10);
  border-radius: 2px;
  box-shadow: 0 1px 0 0 var(--accent);
}
/* mirror-link-auto = 자동 감지된 https?:// URL 미리보기 표시.  발행
 * 시 BSKY.detectUrlFacets 가 link facet 을 붙이므로, 사용자가 발행
 * 후 보게 될 모습 — 글자 색이 accent 인 평범한 링크 — 그대로 미리
 * 보여줌.  링커 도구 manual facet (.mirror-link 의 배경 톤 + 밑줄)
 * 과 시각적으로 구분됨.
 *
 * 표시는 reader 의 컴포저처럼 mirror 가 var(--text) 로 보이는 환경
 * 에서만 유효.  standalone 컴포저 (mirror text: transparent) 에선
 * 글자 색 변화가 안 보임 — 그땐 facet 자체는 submit 시 여전히
 * 부착되지만 시각 highlight 는 없음. */
.mirror-link-auto {
  color: var(--accent);
}
/* Picked @mention — paints a chunky pill underneath the textarea
 * text so the @handle reads as a single atomic tag rather than
 * editable prose. Coordinated with the beforeinput atomic-mention
 * guard (blocks edits inside the range) and the selectionchange
 * snap-out (caret can't land between letters). Box-shadow gives
 * a 1px outline so the pill stands clear on busy backgrounds.
 * Note: we can't add padding to the span because the mirror text
 * has to align character-for-character with the textarea above —
 * we get the chunky look purely via background + radius. */
.mirror-mention {
  background: var(--accent-soft);
  border-radius: 999px;
  box-shadow: 0 0 0 1px var(--accent);
}
.composer-textarea {
  position: relative;           /* sit on top of the mirror */
  width: 100%;
  min-height: 120px;
  resize: vertical;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--text);
  display: block;
}
.composer-textarea::placeholder { color: var(--text-faint); }
.body-counter {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.body-counter.over { color: var(--danger); font-weight: 600; }
.composer-section .section-head {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}
.composer-section .section-head .field-label { margin: 0; }

/* Image grid: 2x2 thumbnails with alt-text input below each. */
.composer-images {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 10px;
  margin-bottom: 10px;
}
.composer-images:empty { display: none; }
/* 사진 5장 이상 — 미리보기 카드를 가로 카루셀(좌우 슬라이드)로.  카드가 많아
 * 세로로 길어지는 문제 해결(사용자 보고 2026-06-09).  4장 이하는 위 그리드.
 * 카드 폭 고정 → 한 화면에 2장+다음 장 peek, 자유 가로 스크롤. */
.composer-images.composer-images-carousel {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  padding-bottom: 6px;
}
.composer-images-carousel .image-cell {
  flex: 0 0 min(180px, 72%);
}
/* 미리보기 카루셀(5장+) 썸네일의 "n/총" 번호 배지 — 피드 캐러셀 배지와 동일
 * 모양.  카드(.image-cell, position:relative) 좌상단(✕ 는 우상단).  4장 이하
 * (그리드)면 JS 가 배지를 안 만들어 표시 안 됨. */
.image-index-badge {
  position: absolute;
  top: 10px;
  left: 10px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  padding: 1px 7px;
  border-radius: 9px;
  pointer-events: none;
  z-index: 2;
}
/* 블스북리뷰 (composer-lock-one-book): one cover + one star
 * widget per post, so center both instead of letting them sit
 * left-aligned like a generic post composer. */
.composer-lock-one-book .composer-images {
  grid-template-columns: minmax(0, 240px);
  justify-content: center;
}
.composer-lock-one-book .composer-book-row {
  margin-left: 0;
  justify-content: center;
  width: 100%;
}
.composer-lock-one-book .composer-rating {
  justify-content: center;
  width: 100%;
}
.image-cell {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.image-thumb {
  width: 100%;
  aspect-ratio: 4/3;
  object-fit: cover;
  border-radius: 6px;
  background: var(--border);
}
.image-alt {
  font-size: 12px;
  padding: 4px 6px;
}
.image-meta {
  font-size: 11px;
  color: var(--text-faint);
  display: block;
  margin-top: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.image-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 0;
  background: rgba(15, 23, 42, 0.75);
  color: #fff;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.image-remove:hover { background: rgba(15, 23, 42, 0.9); }

/* 이미지 cell 아래쪽 워터마크 chip — 압축적 pill 모양으로 alt-text
 * 입력 바로 밑에 자리.  is-on 상태에선 accent 배경으로 "활성화" 시각화. */
.composer-watermark-chip {
  align-self: center;
  padding: 4px 10px;
  margin-top: 4px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  background: transparent;
  border: 1px dashed var(--border);
  border-radius: 999px;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.composer-watermark-chip:hover {
  background: var(--card-bg, transparent);
  color: var(--text);
  border-color: var(--text-muted);
}
.composer-watermark-chip.is-on {
  color: #fff;
  background: var(--accent);
  border-style: solid;
  border-color: var(--accent);
}
.composer-watermark-chip.is-on:hover {
  filter: brightness(1.05);
}

/* 워터마크 입력 / 편집 모달 */
.composer-watermark-modal .modal-body { min-width: 320px; max-width: 560px; }
.composer-watermark-input { width: 100%; box-sizing: border-box; margin: 6px 0 12px; }
.composer-watermark-preview {
  margin: 8px 0 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px;
  background: var(--card-bg, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 140px;
}
.composer-watermark-preview-img {
  max-width: 100%;
  max-height: 360px;
  border-radius: 6px;
  display: block;
}
.composer-watermark-preview-loading {
  margin: 0;
  font-size: 12px;
  color: var(--text-muted);
}
.composer-watermark-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 8px;
}
.composer-watermark-actions .btn { min-width: 88px; }
/* Opacity slider — sits right under the preview image. */
.composer-watermark-opacity-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-top: 12px;
  font-size: 13px;
}
.composer-watermark-opacity-label {
  color: var(--text-muted);
}
.composer-watermark-opacity-value {
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.composer-watermark-opacity-slider {
  width: 100%;
  margin: 4px 0 0;
  accent-color: var(--accent);
}
/* Book-cover image cells are locked: no × button, no alt-text
 * input, and a subtle ring so the user reads "this image is
 * fixed". The grid still shows the preview. */
.image-cell.is-locked {
  outline: 1.5px solid var(--accent-soft, rgba(0, 133, 255, 0.35));
  outline-offset: -1.5px;
}
.image-thumb.is-locked { cursor: default; }

/* Post-link preview card */
.post-preview-slot:empty { display: none; }
.post-preview-slot { margin-top: 8px; }
/* When the post-link section is the FIRST card child (reply / quote
 * lock mode hoists it to the top) and the label + URL row are hidden,
 * drop the preview's gap-from-URL — there's no URL above it anymore. */
.composer-section-postref-locked .post-preview-slot { margin-top: 0; }

/* Book-scanner button — sits next to the image add button when
 * opts.bookScanner is on (📚 블스북리뷰). Inherits .btn.secondary
 * styling so no extra rules needed beyond layout. */
.composer-book-btn { margin: 0; }
/* "@" quick-button — sits inline next to the image-add button.
 * Inherits ALL metrics from base .btn (padding, font-size,
 * line-height) so heights AND inline baselines match the sibling
 * "미디어 추가" exactly. Only override is font-weight: 700 so
 * the @ glyph reads as a symbol despite using the same body
 * font-size as the sibling's label, plus a min-width to keep
 * the button from collapsing to a narrow single-char column. */
/* 미디어 추가 + 도구 버튼을 한 줄에 묶는 flex 행.  세로 정렬(align-items
 * center)과 간격(gap)을 여기서 한 번에 잡아 — 도구 버튼이 맨 버튼이든
 * inline-flex 래퍼로 감싼 것이든 모두 같은 높이·같은 간격으로 정렬된다.
 * (예전엔 맨 버튼=baseline, 래퍼 버튼=middle 이라 세로가 어긋났다.) */
.composer-tools-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
/* 컴포저 도구 버튼 공통 치수 — @·이모지·상호작용·라벨·번역 등 모든 도구
 * 버튼이 똑같은 사각형(폭·높이·패딩·폰트)으로 보이도록 한 곳에서 관리한다.
 * 폭/높이/패딩/마진을 명시해 .btn 기본값 + 글리프 폭 차이로 버튼마다
 * 크기가 달라지던 문제를 없앤다.  세로 위치/간격은 .composer-tools-row 가,
 * 크기는 이 클래스가 책임진다.
 * ⚠ 앞으로 추가하는 도구 버튼도 반드시 이 클래스를 붙일 것 — 개별 CSS 로
 * 크기를 다시 잡지 말 것(그러면 또 어긋난다). */
.composer-tool-btn {
  box-sizing: border-box;
  min-width: 40px;
  height: 38px;
  margin: 0;
  padding: 0 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  line-height: 1;
}
.composer-tool-btn[hidden] { display: none; }
.composer-mention-trigger { font-weight: 700; }
.composer-mention-trigger[hidden] { display: none; }
/* Row that bundles the book button + the 5-star rating widget. */
.composer-book-row {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  margin-left: 8px;
  flex-wrap: wrap;
}
.composer-rating {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
/* hidden attribute on .composer-rating needs to override the
 * inline-flex display rule above — same specificity, source order
 * decides, so an explicit selector wins. */
.composer-rating[hidden] { display: none; }
.composer-rating-star {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 2px;
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  color: var(--text-faint);
  transition: color 0.15s ease, background 0.15s ease, transform 0.05s ease;
}
.composer-rating-star:hover {
  background: var(--surface-hover);
  color: var(--text-muted);
}
.composer-rating-star:active { transform: scale(0.94); }
.composer-rating-star.is-on { color: var(--accent); }
/* Suppress the focus ring on stars — tap-target only; the ring
 * showed on iOS after every tap because :focus-visible counts
 * touch as keyboard-like there. Keyboard nav still works (Tab +
 * Space activate the star); we just don't draw the outline. */
.composer-rating-star:focus,
.composer-rating-star:focus-visible {
  outline: none;
}
.composer-rating-star svg { display: block; }
.composer-rating-clear {
  margin-left: 4px;
  padding: 4px 8px;
  background: transparent;
  border: 1px solid var(--surface-stroke, var(--text-faint));
  border-radius: 6px;
  color: var(--text-muted);
  font-size: 12px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.composer-rating-clear:hover {
  background: var(--surface-hover);
  color: var(--text);
}
.composer-rating-clear[hidden] { display: none; }

/* Book scanner modal — camera preview + ISBN input + result card. */
.bookscan-modal .modal-body {
  min-width: 320px;
  max-width: 480px;
}
.bookscan-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.bookscan-tab {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  padding: 8px 12px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.bookscan-tab.is-active {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}
.bookscan-tab:hover:not(.is-active) { color: var(--text); }
.bookscan-panel[hidden] { display: none; }
.bookscan-panel-camera { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.bookscan-video {
  width: 100%;
  max-width: 360px;
  aspect-ratio: 4 / 3;
  background: #000;
  border-radius: 8px;
  object-fit: cover;
}
.bookscan-camera-status {
  margin: 0;
  font-size: 12px;
  text-align: center;
}
/* Surfaces only when a recoverable camera failure (NotAllowedError /
 * NotFoundError) needs the user to re-trigger getUserMedia. Hidden
 * by default + during the next attempt; setCameraError() flips it on. */
.bookscan-camera-retry {
  align-self: center;
  font-size: 12px;
  padding: 4px 12px;
}
.bookscan-camera-retry[hidden] { display: none; }
.bookscan-manual-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}
.bookscan-manual-input {
  flex: 1;
  min-width: 0;
  font-variant-numeric: tabular-nums;
}
.bookscan-result { margin-top: 16px; }
.bookscan-result[hidden] { display: none; }
.bookscan-result-card {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-2);
}
.bookscan-cover {
  flex: 0 0 auto;
  width: 80px;
  height: auto;
  max-height: 120px;
  border-radius: 4px;
  object-fit: cover;
  background: var(--surface);
}
.bookscan-meta { flex: 1; min-width: 0; }
.bookscan-title { margin: 0 0 6px; font-size: 15px; }
.bookscan-meta .bookscan-meta-line {
  /* Parent-scoped so the (0,2,0) specificity beats .modal-body p's
   * default 16px bottom margin — without this the author / publisher
   * lines drift apart visually. */
  margin: 2px 0;
  font-size: 12px;
  color: var(--text);
  line-height: 1.4;
}
.bookscan-meta-label {
  color: var(--text-muted);
}
.bookscan-result-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 12px;
}
/* `display: flex` above out-specs the [hidden] attribute's default
 * `display: none`, so the JS `resultActionsRow.hidden = true` toggle
 * (used to defer the action buttons until the cover finishes painting)
 * had no visual effect — the buttons appeared instantly. Force the
 * collapse explicitly. */
.bookscan-result-actions[hidden] { display: none; }
/* The `hidden` attribute on the label + input-with-button row would
 * normally collapse to display:none, but .field-label sets
 * display:block and .input-with-button sets display:flex which both
 * win in the cascade. Override here so the lock-mode hide actually
 * takes effect. */
.composer-section-postref-locked .field-label[hidden],
.composer-section-postref-locked .input-with-button[hidden] {
  display: none;
}
.post-preview {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  background: var(--surface-2);
}
.post-preview-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}
.post-preview-avatar {
  width: 24px;
  height: 24px;
  border-radius: 50%;
}
.post-preview-who {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}
.post-preview-handle {
  color: var(--text-muted);
  font-size: 12px;
}
.post-preview-text {
  margin: 0;
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--text);
}
.postref-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 8px;
  flex-wrap: wrap;
}
.postref-actions[hidden] { display: none; }

.composer-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 12px;
}
/* Some Korean web fonts (Diphylleia, Nanum Myeongjo etc.) push the
 * compose buttons' single-line text past the cell width and force a
 * second line ("게시" / "중…").  Clamping every button in the
 * actions row to nowrap keeps the labels on a single line at every
 * font. If a label is genuinely too long for the layout it'll just
 * overflow horizontally — visually preferable to a stacked vertical
 * fragment. */
.composer-actions .btn { white-space: nowrap; }
.reader-compose-thread-publish,
.reader-compose-thread-add { white-space: nowrap; }
/* Split layout when the host wires up a clear button — clear sticks
 * to the left, every other action (load draft / save draft / submit)
 * clusters on the right. */
.composer-actions-split { justify-content: flex-end; }
.composer-actions-split > .composer-clear { margin-right: auto; }

/* 나빌레라 링크카드 행 — SUPER_DIDS 전용 dev quick-action.  미디어
 * 버튼 줄 (imagesSection) 과 비우기 / 제출 줄 (composer-actions)
 * 사이에 자기 자신만의 한 줄을 차지한다.  나머지 .btn 들이 inline-
 * flex 인 것과 달리 여기 button 은 display:flex 로 띄워서 row 전체
 * 폭을 차지하게 (양 옆으로 늘어남). */
.composer-nabilera-card-row {
  margin-top: 8px;
  margin-bottom: 16px;
}
.composer-nabilera-card-btn {
  display: flex;
  width: 100%;
}

/* Link-facet editor (블스링커) */
.composer-link-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  flex-wrap: wrap;
}
.composer-link-toolbar .hint { color: var(--warning-text); }
.link-edit-panel {
  border: 1px solid var(--accent);
  background: var(--accent-soft);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 8px;
}
.link-edit-panel[hidden] { display: none; }
.link-edit-panel .link-selected {
  margin: 0 0 8px;
  font-size: 13px;
  word-break: break-word;
}
.link-edit-panel .field { margin-bottom: 0; }
.link-edit-panel .modal-actions { margin-top: 10px; }
.link-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 4px;
}
.link-chips:empty + .hint { display: block; }
.link-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  color: var(--text);
  font-size: 12px;
  cursor: pointer;
  max-width: 100%;
}
.link-chip:hover { background: #dbeafe; }
.link-chip.orphaned {
  background: var(--warning-soft);
  border-color: var(--warning-border);
  color: var(--warning-text);
}
.link-chip .chip-text {
  font-weight: 600;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.link-chip .chip-sep { color: var(--text-faint); }
.link-chip .chip-url {
  color: var(--text-muted);
  max-width: 240px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11px;
}
.link-chip .chip-warn { color: var(--warning-text); font-size: 11px; }

/* ───── Time machine: date/time block ───── */
.datetime-block {
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--accent-soft);
  margin-bottom: 4px;
}
.datetime-body { margin: 10px 0; }
.datetime-picker-row {
  /* Two equal-width columns ("vertical strips") on one row. Each strip
   * gets exactly half the container; the date/time inputs themselves
   * are sized to ~2/3 of their strip (sitting left-aligned) so the
   * native pickers don't visually crash into each other.
   * box-sizing + min-width:0 on the cell keeps the native chrome from
   * overflowing the column. */
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.datetime-cell {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.datetime-cell .field-label { font-size: 12px; }
.datetime-cell .input[type="date"],
.datetime-cell .input[type="time"] {
  width: 66.67%;
}
.datetime-format-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  flex-wrap: wrap;
}
.datetime-manual-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
  gap: 8px;
}
.datetime-manual-row .input { padding: 6px 8px; }
.ampm-cell { justify-content: flex-end; }
.datetime-tz-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.datetime-tz-row .input { flex: 1; min-width: 200px; }
.datetime-preview {
  margin: 10px 0 0;
  font-size: 12px;
}
.datetime-preview code { font-size: 12px; }
.field-label.inline { margin: 0; font-weight: 500; color: var(--text-muted); }

/* ───── Drawing (raffle) ───── */
.drawing-stats {
  margin-top: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  flex-wrap: wrap;
}
.drawing-stats .stat-label { color: var(--text-muted); margin-right: 2px; }
.drawing-stats .sep { color: var(--text-faint); }
.drawing-stats .hint { color: var(--text-faint); font-size: 12px; }
.drawing-results { margin-top: 16px; padding: 16px; }
.drawing-results h2 { margin: 0 0 10px; font-size: 18px; }
.winners-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.winners-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  background: var(--surface-2);
}
.winners-list .post-preview-avatar { width: 28px; height: 28px; }
.winners-list strong { font-size: 14px; }

.count-clamp {
  margin: 6px 0 0;
  color: var(--warning-text);
  background: var(--warning-soft);
  border: 1px solid var(--warning-border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 12px;
}
.count-clamp[hidden] { display: none; }

/* Recent-posts picker modal */
.picker-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 50vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.picker-item {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  background: var(--surface-2);
  cursor: pointer;
}
.picker-item:hover, .picker-item:focus {
  background: var(--accent-soft);
  border-color: var(--accent);
  outline: none;
}
.picker-text {
  margin: 0 0 6px;
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.picker-meta {
  display: flex;
  gap: 6px;
  font-size: 12px;
  color: var(--text-muted);
  flex-wrap: wrap;
}
.picker-meta .sep { color: var(--text-faint); }

/* Home grid */
.hero { margin-bottom: 28px; }
.hero h1 {
  margin: 0 0 6px;
  font-size: 28px;
  letter-spacing: -0.01em;
}
.hero p { margin: 0; color: var(--text-muted); }

/* 블스리더 */
.reader-prefs {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--text-muted);
}
.reader-prefs label { cursor: pointer; }
/* Author / repost / reply-target spans. Currently inert — the
 * planned profile modal will attach a click handler that reads
 * data-did and pops the profile. Style as default cursor until then. */
.reader-profile-target { cursor: default; }
.reader-card-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}
.reader-status { text-align: center; margin: 8px 0; }
.reader-status.err { color: var(--danger); }
/* Settings dialog body width. The two-column grid wants room, so
 * we let it grow up to a wider cap than the default modal. */
/* Settings dialog runs slightly wider than the default 420 px modal
 * cap so the two-column basic-settings grid + the lang/theme/bug-
 * report/brand quick row breathe. The modal-body's min-width(480)
 * used to overshoot the dialog's max-width(420) and forced a 60 px
 * horizontal overflow scrollbar — Chrome auto-scrolled to hide it
 * but content on either edge ended up clipped depending on
 * scrollLeft. Pinning the dialog itself to 480 px aligns it with
 * the body. */
dialog.modal.reader-settings-dialog { max-width: 480px; }
.reader-settings-dialog .modal-body { min-width: min(480px, 92vw); }
/* Compose dialog — host the shared Composer module inside a wider
 * modal so the textarea has room to breathe. The "본문" field-label
 * is hidden per spec (the textarea itself is self-evident); the
 * dialog only carries the body + composer's own action row. */
.reader-compose-dialog { max-width: 560px !important; }
.reader-compose-dialog .modal-body {
  min-width: min(520px, 92vw);
  position: relative;  /* anchors the absolute .reader-compose-close */
}
/* Conditional × close — only ACTIVATED when the dialog actually
 * has overflowing content (the bottom action row is reachable on
 * its own in the short-content case, so the floating × would just
 * be noise there). reader.js toggles .is-scrollable on the dialog
 * as a ResizeObserver fires.
 *
 * Positioning history:
 *   v0.9.762 — `position: sticky; float: right` to stay visible
 *     during scroll AND not take a full layout line. Side effect:
 *     the float reserves ~14 CSS px on the right of modal-body's
 *     content area. `.composer-textarea-wrap` is a BFC (via
 *     overflow: hidden) so it narrows to avoid the float → input
 *     ends ~14px short of the right edge. The reply preview's
 *     `.reader-card` is NOT a BFC so it extends to full width →
 *     parent post extends past input on the right. User reported.
 *   v0.9.765 — switch to plain `position: absolute`. No float
 *     reservation → all composer content fills modal-body width
 *     identically. Trade-off: when modal scrolls vertically, the
 *     close button scrolls with the content (no longer sticky).
 *     For compose-mode the action row + ESC key remain available
 *     so this is acceptable.
 *
 * IMPORTANT — we still ALWAYS render the button in its full
 * 28×28 box (display: inline-flex), and only flip visibility +
 * pointer-events when .is-scrollable lands. The display: none ↔
 * inline-flex flip caused an earthquake (ResizeObserver retoggle
 * loop). With position: absolute, the box doesn't affect siblings'
 * layout anyway — but we preserve the visibility-only pattern
 * since it's harmless and keeps the rendering predictable. */
.reader-compose-close {
  position: absolute;
  top: 10px;
  right: 14px;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg) 80%, var(--text));
  color: var(--text);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.reader-compose-close:hover {
  background: color-mix(in srgb, var(--bg) 60%, var(--text));
}
/* "🧵 타래" toggle in the compose modal — sits in the modal chrome
 * Thread mode inside the compose modal — additional composers
 * stack BELOW the first one with a per-slot chrome row (label,
 * up/down arrows, ✕ remove).  The first composer keeps full
 * features (masking, reply target); subsequent slots are
 * deferredSubmit + quote-only.  When threadSlots.length >= 2 the
 * dialog gets .is-thread-mode, which hides the first composer's
 * primary submit so the modal-level "타래로 올리기" button takes
 * over publishing. */
/* Spacing between the inline-mounted slot 0 wrapper and the
 * "타래 추가하기" + "타래로 올리기" tail block.  Matches the
 * inter-slot gap below so the whole stack reads with a uniform
 * rhythm rather than slot 0 sitting tight against the rest. */
.reader-compose-thread-extra { margin-top: 20px; }
/* Slot 0 also needs breathing room above it inside the modal body
 * — without this it butts right up against the closeBtn / reply
 * preview / etc.  Only takes effect once thread mode is active
 * (single-composer layout doesn't render the wrapper). */
.reader-compose-dialog.is-thread-mode .reader-compose-thread-slot-zero {
  margin-bottom: 8px;
}
/* "1/N" label that pins above the first composer when thread mode
 * is on. Slots 2..N carry the same label inside their slot-head;
 * slot 0 doesn't HAVE a slot-head (the first composer fills the
 * modal body chrome itself), so we paint a small floating label
 * just below the × close to mirror the chain numbering. */
.reader-compose-first-slot-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  margin: 0 0 8px 0;
}
.reader-compose-first-slot-label[hidden] { display: none; }
/* Floating scroll-to-top inside the compose modal — same pattern
 * as the profile modal's scrolltop button. Sticks to the bottom-
 * right of the dialog's scroll viewport so a long thread mode
 * composer can jump back without scrolling all the way up. */
.reader-compose-scrolltop {
  position: sticky;
  bottom: 16px;
  margin-left: auto;
  margin-right: 12px;
  margin-top: -56px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  color: var(--text);
  cursor: pointer;
  font-size: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  transition: opacity 0.2s ease;
}
.reader-compose-scrolltop[hidden] { display: none; }
/* Bottom-LEFT counterpart to the scroll-top button: discard the
 * entire thread (returns the modal to single-composer mode + wipes
 * every slot's content). Same circular chip shape so the two
 * affordances read as a matched pair. Coloured danger-tinted so the
 * destructive intent is signaled before the confirm dialog fires. */
.reader-compose-thread-reset {
  position: sticky;
  bottom: 16px;
  margin-right: auto;
  margin-left: 12px;
  margin-top: -56px;
  width: 40px;
  height: 40px;
  /* No internal padding — the inner SVG (22px square) is centered
   * by flex.  Removed font-size since the icon is no longer text. */
  padding: 0;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--danger, #e85a5a);
  color: var(--danger, #e85a5a);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  transition: opacity 0.2s ease, transform 0.18s ease;
}
.reader-compose-thread-reset svg {
  /* Ensure the SVG itself contributes no extra baseline space; the
   * 22x22 viewBox sits precisely in the centre of the 40x40 chip. */
  display: block;
}
.reader-compose-thread-reset:hover { transform: scale(1.06); }
.reader-compose-thread-reset[hidden] { display: none; }
.reader-compose-scrolltop:hover {
  background: color-mix(in srgb, var(--surface-2) 70%, var(--text));
}
.reader-compose-thread-list {
  display: flex;
  flex-direction: column;
  /* Inter-slot gap restored to the original 20px so chained
   * composers don't feel cramped against each other.  Matches the
   * margin-top above .reader-compose-thread-extra for a single
   * visual rhythm. */
  gap: 20px;
}
.reader-compose-thread-list:empty { display: none; }
.reader-compose-thread-slot {
  position: relative;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}
/* 설정 힌트 — 문장 영역을 누르면 페이드아웃 후 다른 힌트가 페이드인.
 * 전구 / ⓘ 클릭은 텍스트 핸들러에서 closest 로 걸러져서 영향 없음. */
.reader-settings-hint { transition: opacity 700ms ease; will-change: opacity; }
.reader-settings-hint.is-fading { opacity: 0; }
.reader-settings-hint-clickable { cursor: pointer; }
.reader-settings-hint-clickable .reader-hint-bulb,
.reader-settings-hint-clickable .reader-hint-info { cursor: default; }
/* In SINGLE-composer mode (no .is-thread-mode on the dialog) the
 * slot 0 wrapper is invisible — no chrome at all so the composer
 * card looks identical to the pre-thread plain client composer.
 * Without this rule the user would see a boxed empty container
 * around the composer even after a thread-reset. */
.reader-compose-dialog:not(.is-thread-mode) .reader-compose-thread-slot-zero {
  padding: 0;
  border: 0;
  background: transparent;
}
.reader-compose-thread-slot-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 13px;
  color: var(--text-muted);
}
/* `display: flex` above overrides the UA stylesheet's
 * `[hidden] { display: none }`.  Without this explicit rule,
 * slot0Head.hidden = true was silently ignored — the 1/N + ↑↓✕
 * row stayed visible after a thread-reset even though only one
 * composer remained.  Explicit + same-class specificity wins. */
.reader-compose-thread-slot-head[hidden] {
  display: none;
}
.reader-compose-thread-slot-label {
  flex: 1 1 auto;
  font-weight: 600;
}
.reader-compose-thread-slot-up,
.reader-compose-thread-slot-down,
.reader-compose-thread-slot-remove {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 6px;
}
.reader-compose-thread-slot-up:hover:not(:disabled),
.reader-compose-thread-slot-down:hover:not(:disabled),
.reader-compose-thread-slot-remove:hover {
  background: var(--surface-hover);
  color: var(--text);
}
.reader-compose-thread-slot-up:disabled,
.reader-compose-thread-slot-down:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.reader-compose-thread-slot-remove { color: var(--danger); }
.reader-compose-thread-slot-host { display: block; }
.reader-compose-thread-add {
  margin-top: 12px;
  width: 100%;
}
.reader-compose-thread-publish-row {
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: stretch;
}
.reader-compose-thread-publish {
  width: 100%;
}
.reader-compose-thread-status { margin: 0; }
.reader-compose-thread-status.err { color: var(--danger); }
.reader-compose-thread-status.ok  { color: var(--success, var(--accent)); }
/* In thread mode, the first composer's submit (.btn.primary inside
 * .composer-actions) is hidden — the modal-level "타래로 올리기"
 * button handles publish for all slots. Clear / draft buttons
 * (.btn.secondary) stay visible. */
.reader-compose-dialog.is-thread-mode .composer-actions > .btn.primary {
  display: none;
}
.reader-thread-template-input {
  flex: 0 0 auto;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  width: 140px;
}
.reader-thread-delay-input {
  flex: 0 0 auto;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  width: 80px;
}
/* Post-detail modal — opened from notification rows when the subject
 * is a published post (like / repost reasons). Sized like the
 * compose modal so card embeds (images, link cards, quotes) don't
 * get cramped. */
.reader-post-detail-modal { max-width: 560px !important; }
.reader-post-detail-modal .modal-body {
  min-width: min(520px, 92vw);
  /* position: relative anchors the absolute .reader-post-detail-close
   * to the modal-body chassis (same as .reader-compose-dialog). */
  position: relative;
  /* Modal body itself has no scroll — the center slot scrolls its
   * own content.  Padding 0 / margin 0 lets the viewport fill the
   * full chassis so swipe distances match what the user sees. */
  padding: 0;
  overflow: hidden;
}
/* 5-slot cross for thread navigation.  Viewport has explicit
 * height set in JS to MIN(center-card-height, 75vh); when the
 * card is taller than 75vh, the center slot scrolls internally
 * instead of growing the modal.  Track fills the viewport; all
 * 5 slots are absolutely positioned around it.
 *
 * Width is locked to the modal-body so horizontal slides stay
 * clean.  touch-action 의 default 는 'none' 으로 — JS 가 모든 vertical
 * gesture 를 소유.  긴 카드 (75vh cap 도달) 일 때 만 applyViewportHeight
 * 가 inline 'pan-y' 로 올려 native scroll 회복.
 *
 * 이전 의 default 'pan-y' 는 모달 mount ~ 첫 applyViewportHeight 사이
 * 의 짧은 window 동안 mobile browser 가 swipe-up 을 native pan 으로
 * claim → 사용자 의 첫 swipe-up 이 무시 되는 버그.  CSS 기본 을 none
 * 으로 바꿔 race 자체 가 사라짐. */
.reader-pcm-viewport {
  position: relative;
  overflow: hidden;
  width: 100%;
  max-height: 75vh;
  touch-action: none;
  overscroll-behavior: none;
}
.reader-pcm-viewport.is-animating {
  transition: height 0.28s cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* ── Post detail modal — navigation indicators (▲ ▼ ◀ ▶) ───────────
 * One triangle per edge of the viewport; only the directions with a
 * navigable neighbor are visible (.is-visible toggled by JS based on
 * canGo()).  Pure-CSS triangle via border tricks — keeps the shape
 * crisp at any pixel ratio without an inline SVG.  Click target =
 * the indicator itself (10x10 px hit zone is enough on touch). */
/* navIndicators 는 사용자 spec 으로 삼각형 button — 큰 44x44 hit zone
 * 의 transparent outer button + 가운데 small CSS triangle (border 트릭).
 * 이전 은 0x0 div 의 border-only 라 hit target 이 ~16x10 으로 좁아 사용자
 * 가 빗나가는 케이스 → button + inner triangle 구조 로 hit zone 보강. */
.reader-pcm-nav {
  position: absolute;
  z-index: 5;
  width: 44px;
  height: 44px;
  padding: 0;
  margin: 0;
  background: transparent;
  border: 0;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease-out;
  display: flex;
  align-items: center;
  justify-content: center;
}
.reader-pcm-nav.is-visible {
  opacity: 0.65;
  pointer-events: auto;
}
.reader-pcm-nav.is-visible:hover { opacity: 1; }
/* :active 상태 의 transform 은 per-direction translateX(-50%) /
 * translateY(-50%) 를 덮어쓰면 indicator 가 튕기는 위치 로 깜빡 — opacity
 * 만 으로 tap feedback. */
.reader-pcm-nav.is-visible:active { opacity: 0.85; }
/* Triangle positions — visible triangle 은 viewport 가장자리 (≈6px) 에
 * 붙여 두되, 44x44 hit zone 은 가장자리에서 안쪽으로 펼침.  inner span
 * (triangle) 을 button 의 edge 쪽 으로 flex-align 해 시각 위치 = 가장
 * 자리, hit target = 충분 한 크기 를 동시 만족. */
.reader-pcm-nav-top {
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  align-items: flex-start;
  padding-top: 6px;
}
.reader-pcm-nav-bottom {
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  align-items: flex-end;
  padding-bottom: 6px;
}
.reader-pcm-nav-left {
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  justify-content: flex-start;
  padding-left: 6px;
}
.reader-pcm-nav-right {
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  justify-content: flex-end;
  padding-right: 6px;
}
/* Inner triangle — pure CSS border 트릭.  pointer-events none → click 은
 * 항상 outer button 으로. */
.reader-pcm-nav-tri {
  display: block;
  width: 0;
  height: 0;
  pointer-events: none;
}
.reader-pcm-nav-tri-top {
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-bottom: 10px solid var(--accent);
}
.reader-pcm-nav-tri-bottom {
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-top: 10px solid var(--accent);
}
.reader-pcm-nav-tri-left {
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  border-right: 10px solid var(--accent);
}
.reader-pcm-nav-tri-right {
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  border-left: 10px solid var(--accent);
}
/* (old ::after pad rules — no longer needed, 44x44 outer button covers
 * the click zone.  keep harmless empty stubs to avoid breaking selectors
 * elsewhere if any). */
.reader-pcm-nav-tri::after {
  content: none;
}
.reader-pcm-nav-right::after { transform: translate(-12px, -50%); }

/* ── Feed: 새 포스트 push-in 애니메이션 ─────────────────────────────
 * prependItems 가 새 카드 추가 시 .reader-card-push-in 을 stamp,
 * keyframe 이 max-height 0 → 1200px 로 grow.  Sibling 들은 layout
 * 변화로 자연스럽게 밀려남.  Animation 끝 (animationend) 에 JS 가
 * 클래스 제거 → max-height cap 풀려 1200px 보다 큰 카드도 다 보임.
 * overflow:hidden 은 max-height 가 actual content 보다 작은 동안
 * 안의 그림자 / dropdown 이 카드 밖으로 새지 않게 막음. */
@keyframes reader-card-push-in {
  from {
    max-height: 0;
    opacity: 0;
    margin-top: 0;
    padding-top: 0;
    padding-bottom: 0;
    transform: translateY(-6px);
  }
  to {
    max-height: 1200px;
    opacity: 1;
    transform: translateY(0);
  }
}
.reader-card-push-in {
  animation: reader-card-push-in 0.38s cubic-bezier(0.2, 0.7, 0.2, 1);
  overflow: hidden;
}
.reader-pcm-track {
  position: absolute;
  inset: 0;
}
.reader-pcm-track.is-animating {
  transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.reader-pcm-slot {
  position: absolute;
  width: 100%;
  /* Symmetric 16px padding all sides — close button (absolute
   * top: 10px, z-index: 2) is allowed to overlay the card's top
   * edge per user preference (much better than the old 36px gap
   * reserved exclusively for the close button). */
  padding: 16px;
  box-sizing: border-box;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* Kill the rubber-band overscroll at the scroll boundaries.
   * Without this, iOS Safari at scrollTop=0 reacts to a finger
   * pull-down with a native elastic shift that confuses the JS
   * gesture detector (the slot's content visually moves while
   * scrollTop stays at 0 — and the pointer events that arrive
   * during the elastic phase have been observed to disagree with
   * the user's finger direction in our repros). */
  overscroll-behavior: none;
  /* Disable native image drag inside the modal so a press-and-drag
   * on a post image is interpreted as a swipe instead of starting a
   * browser drag-and-drop. */
  -webkit-user-drag: none;
}
.reader-pcm-slot img,
.reader-pcm-slot a {
  -webkit-user-drag: none;
  user-select: none;
}
/* Center fills the entire viewport; if the card overflows 75vh the
 * slot scrolls internally (modal stays at 75vh, swipe-to-parent /
 * swipe-to-child engages only at scroll boundaries). */
.reader-pcm-slot-center { top: 0; left: 0; height: 100%; }
/* Top / bottom sit OUTSIDE the vertical extent of the track at their
 * own natural (card) height.  Sliding the track ±dest-height brings
 * them into the viewport. */
.reader-pcm-slot-top    { bottom: 100%; left: 0; max-height: 75vh; }
.reader-pcm-slot-bottom { top: 100%;    left: 0; max-height: 75vh; }
/* Left / right share the track's height so a horizontal slide stays
 * vertically clean.  Their cards may still scroll inside if taller. */
.reader-pcm-slot-left   { right: 100%; top: 0; height: 100%; }
.reader-pcm-slot-right  { left: 100%;  top: 0; height: 100%; }
/* Timeline / list post cards get a pointer cursor because the
 * whole card now opens the post detail modal.
 *
 * Hover background-tint is gated to mouse-only devices (`@media
 * (hover: hover)`):
 *   - On touch, :hover is sticky AFTER tap — the card would stay
 *     painted opaque even after the modal opened/closed, which
 *     reads as a stuck "selected" state and (when wallpaper is
 *     on) makes the card unexpectedly OPAQUE (wallpaper showed
 *     through the alpha-blended surface previously, then the
 *     hover override replaced background with a solid color).
 *   - On mouse, hover is a useful affordance; we still respect
 *     the wallpaper card-alpha so toggling it doesn't surprise. */
.reader-card[data-post-uri] { cursor: pointer; }
.reader-quote[data-post-uri] { cursor: pointer; }
@media (hover: hover) {
  .reader-card[data-post-uri]:hover {
    background: color-mix(in srgb, var(--surface) 96%, var(--text));
  }
  .reader-quote[data-post-uri]:hover {
    background: color-mix(in srgb, var(--surface-2) 90%, var(--text));
  }
  /* Wallpaper-aware variant: nest the hover-tint color inside
   * the same alpha-mix the non-hover rule already uses, so the
   * card stays semi-transparent (wallpaper visible) even while
   * hovered.  Without this, hover stomps the alpha and the card
   * snaps to solid mid-interaction. */
  body[data-reader-bg="1"] .reader-card[data-post-uri]:hover {
    background: color-mix(
      in srgb,
      color-mix(in srgb, var(--surface) 96%, var(--text))
        calc(var(--reader-card-alpha, 1) * 100%),
      transparent
    );
  }
}
/* (Previously two conditional rules — `.reader-post-detail-modal
 * .reader-card.is-own-post .reader-actions` and `.reader-card.is-
 * own-post .reader-actions:has(> .reader-time.is-absolute)` — that
 * picked specific scenarios where the action row would overflow.
 * Replaced by the universal flex-wrap above which lets the time
 * chip wrap whenever it doesn't fit, regardless of the location
 * / author / time-format combo. The .is-own-post class still
 * lives on the card for other future uses, but no longer drives
 * the action-row layout.) */
/* Positioning history:
 *   v0 — `position: sticky; float: right` to keep the close button
 *     visible during scroll AND not consume a full layout line. Same
 *     pattern as the original .reader-compose-close.
 *   v1 (this rule) — switched to `position: absolute` for the exact
 *     same reason the compose-close was switched in 859e032: the
 *     float reserves ~28px on the right of modal-body's content
 *     area, and .reader-card (BFC via display:flex) shrinks to
 *     avoid the float → the post card ends up ~30+px short of the
 *     modal's right edge with an empty band on the right. With
 *     `position: absolute` (anchored to modal-body's
 *     position:relative chassis), no float reservation, the card
 *     fills the full modal width.
 *   Trade-off: close no longer sticks during vertical scroll.
 *     Acceptable — modal closes via ESC and backdrop click too. */
.reader-post-detail-close {
  position: absolute;
  top: 10px;
  right: 14px;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg) 80%, var(--text));
  color: var(--text);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.reader-post-detail-close:hover {
  background: color-mix(in srgb, var(--bg) 60%, var(--text));
}

/* ── 2026-05-25 실험 모드 : 타래 보기 모달 + expand / collapse 버튼 ── */
/* expand 버튼 (detail 모달) : × 옆.  collapse 와 같은 chassis. */
.reader-post-detail-expand,
.reader-thread-collapse {
  position: absolute;
  top: 10px;
  right: 50px;   /* × 가 right:14 + 28px 너비 + 8px gap */
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg) 80%, var(--text));
  color: var(--text);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.reader-post-detail-expand:hover,
.reader-thread-collapse:hover {
  background: color-mix(in srgb, var(--bg) 60%, var(--text));
}

/* 타래 모달 — 화면 가운데 의 길고 narrow 한 모달.  내부 는 scrollable
 * vertical list.  포스트 카드 들 이 자연 스럽게 쌓임. */
/* 2026-05-25 v2 fix : .reader-thread-modal (specificity 0,1,0) 만 으로는
 * 전역 dialog.modal (specificity 0,1,1) 의 height: max-content + max-
 * height: calc(100dvh - 16px) 가 win 해 모달 크기 가 0 ish 으로 줄어들
 * 어 사용자 가 backdrop 만 보고 본체 invisible.  combined selector
 * dialog.modal.reader-thread-modal 로 specificity 올려 height / max-
 * height / width 가 의도 대로 적용 되게.  다른 reader 모달 들도 같은
 * 패턴 (dialog.modal.reader-xxx-modal). */
/* 2026-05-25 사용자 spec : sibling swap 시 모달 크기 가 바뀌면 부드럽게.
 * pcm-viewport 의 .is-animating 패턴 과 동일 — JS 가 swap 전 후 의
 * height 를 측정 해 inline 으로 lock + transition 으로 보간.  margin:
 * auto 가 매 frame 재 center 계산 → 모달 자체 가 부드럽게 grow/shrink
 * + 안 의 카드 들 도 dialog 위치 따라 동기 적 으로 slide.
 *
 * + 추가 (사용자 보고 : 모달 이 max-height cap 상태 라 dialog 자체 의
 * height 가 안 변하는 경우 — 카드 내용 만 instant 으로 바뀌어 부드럽지
 * 않음).  새 wraps 에 .is-entering / .is-entered fade-in 으로 보완 :
 * dialog 변화 0 인 케이스 에서 도 카드 등장 이 transition 으로 부드럽게.
 * duration 0.40s — 사용자 가 "시간 좀 늘려야 할 것 같다" 한 데 맞춰
 * 0.28s → 0.40s 로 일관 (dialog + card 둘 다). */
dialog.modal.reader-thread-modal.is-morphing {
  transition: height 0.40s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.reader-thread-card-wrap.is-entering {
  opacity: 0;
}
.reader-thread-card-wrap.is-entering.is-entered {
  opacity: 1;
  transition: opacity 0.40s ease;
}
dialog.modal.reader-thread-modal {
  max-width: 560px;
  width: min(92vw, 560px);
  max-height: 88dvh;
  /* 2026-05-25 v3 : height 명시 안 함 → 전역 dialog.modal 의 height:
   * max-content 가 적용 됨.  단, modal-body / scroller 가 height: 100%
   * 면 circular collapse → 함께 auto / content-driven 으로 (아래).
   * 짧은 thread 면 dialog 가 컨텐츠 에 맞춰 자동 으로 작아짐, 길면
   * scroller max-height: 88dvh 에 닿아 overflow scroll. */
  padding: 0;
  border: 0;
  border-radius: 16px;
}
.reader-thread-modal .modal-body {
  position: relative;
  padding: 0;
  /* No height: 100% — content-driven (= scroller height).  dialog 의
   * max-content 가 cycle 없이 fit content. */
  overflow: hidden;
}
.reader-thread-close {
  /* × 와 같은 chassis 의 thread 모달 변종. */
  position: absolute;
  top: 10px;
  right: 14px;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg) 80%, var(--text));
  color: var(--text);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.reader-thread-close:hover {
  background: color-mix(in srgb, var(--bg) 60%, var(--text));
}
.reader-thread-scroller {
  /* No height: 100% — content-driven.  max-height 가 cap 역할 (dialog
   * 의 max-height 88dvh 와 의도 적 으로 일치 — scroller 가 직접 cap
   * 으로 modal 의 max-content 가 cycle 없이 동작). */
  max-height: 88dvh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* 2026-05-25 사용자 spec : 포스트카드 모달 의 .reader-pcm-slot 과
   * 동일 한 대칭 16px padding.  close / collapse 버튼 은 absolute top:10
   * 으로 첫 카드 의 top 가장자리 를 overlay (포스트카드 모달 의 close
   * 버튼 과 같은 디자인 — 카드 top 의 빈 여백 36px 보다 자연 스러움). */
  padding: 16px;
  box-sizing: border-box;
  /* 2026-05-25 incremental fetch 때 위 에 카드 가 prepend 되면 native
   * scroll anchor 가 scrollTop 을 paint cycle 의 일부 로 자동 보정 →
   * momentum scroll 끊기 지 않고 보이는 카드 위치 보존.  Chrome / Safari
   * 17+ 지원.  default 가 auto 지만 명시. */
  overflow-anchor: auto;
}
.reader-thread-card-wrap {
  margin: 8px 0;
  /* sibling indicator (◀/▶) 가 absolute 로 카드 양쪽 에 떠야 해서
   * relative 컨테이너 필요. */
  position: relative;
  transition: background 200ms ease, box-shadow 200ms ease, transform 200ms ease;
  /* native overflow-anchor 가 각 카드 를 후보 anchor 로 고려 하도록. */
  overflow-anchor: auto;
}
/* 첫 / 마지막 wrap 은 scroller padding 과 중복 되는 margin 제거 — top /
 * bottom 의 빈 공간 누적 방지. */
.reader-thread-card-wrap:first-child { margin-top: 0; }
.reader-thread-card-wrap:last-child { margin-bottom: 0; }
.reader-thread-card-wrap.is-focal {
  /* focal post 강조 — 사용자 가 누른 글 임을 시각 적 으로 표시. */
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 12px;
}

/* 2026-05-25 사용자 spec : 타래 모달 안 에선 has-parent-peek / has-
 * child-peek 의 shadow card 가 시각 적 으로 중복 (이미 카드 들 이 세로
 * 로 stack 됨).  pseudo-element peek 와 그 margin offset 둘 다 무력화 —
 * .reader-thread-card-wrap 의 12 px 간격 만 시각 적 으로 남도록. */
.reader-thread-modal .reader-card.has-parent-peek::before,
.reader-thread-modal .reader-card.has-child-peek::after {
  display: none;
}
.reader-thread-modal .reader-card.has-parent-peek,
.reader-thread-modal .reader-card.has-child-peek {
  margin-top: 0;
  margin-bottom: 0;
}

/* ── 타래 트리 시각화 (실험, treeview) ────────────────────────────────
 * 헤더 토글 버튼 = collapse(right:50) 왼쪽 = right:86.  collapse 와 같은
 * 동그란 chassis.  팝오버 는 dialog 자식(top layer) + position:fixed —
 * 드래그 가능, 모달 위 에 뜸. */
.reader-thread-treebtn {
  position: absolute;
  top: 10px;
  right: 86px;   /* collapse(right:50) + 28px + 8px gap */
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg) 80%, var(--text));
  color: var(--text);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.reader-thread-treebtn:hover {
  background: color-mix(in srgb, var(--bg) 60%, var(--text));
}
.reader-thread-treebtn[aria-pressed="true"] {
  background: var(--accent);
  color: #fff;
}
.reader-tree-pop {
  position: fixed;
  width: 280px;
  max-width: calc(100vw - 16px);
  max-height: min(60vh, 460px);
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 1px solid color-mix(in srgb, var(--bg) 70%, var(--text));
  border-radius: 12px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);
  z-index: 5;
  overflow: hidden;
  transition: opacity 0.2s ease;
}
/* 노드 선택 / 본 타래 누름·스와이프 시 비켜 둠.  반반 보다 더 투명(0.3).
 * 딤 상태 에서 팝오버 를 누르면 깨어나 다시 불투명(JS 가 is-dimmed 제거). */
.reader-tree-pop.is-dimmed { opacity: 0.3; }
.reader-tree-pop.is-dragging { user-select: none; }
.reader-tree-pop-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  cursor: move;
  touch-action: none;
  background: color-mix(in srgb, var(--bg) 88%, var(--text));
  border-bottom: 1px solid color-mix(in srgb, var(--bg) 78%, var(--text));
}
.reader-tree-pop-title { font-weight: 700; font-size: 13px; flex: 1 1 auto; }
.reader-tree-pop-close {
  flex: none;
  width: 22px; height: 22px;
  border: 0; border-radius: 50%;
  background: color-mix(in srgb, var(--bg) 70%, var(--text));
  color: var(--text);
  font-size: 15px; line-height: 1;
  cursor: pointer;
}
.reader-tree-pop-close:hover { background: color-mix(in srgb, var(--bg) 55%, var(--text)); }
.reader-tree-pop-legend {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: wrap;
  padding: 6px 10px;
  font-size: 11px;
  color: var(--text-faint);
  border-bottom: 1px solid color-mix(in srgb, var(--bg) 84%, var(--text));
}
.reader-tree-pop-legend .reader-tree-dot { margin-left: 6px; }
.reader-tree-pop-legend .reader-tree-dot:first-child { margin-left: 0; }
.reader-tree-pop-body {
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  padding: 6px 0;
  flex: 1 1 auto;
}
.reader-tree-dot {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 50%;
  flex: none;
  background: var(--text-faint);
}
.reader-tree-dot.is-green { background: #3fb950; }
.reader-tree-dot.is-orange { background: #f0883e; }
.reader-tree-node {
  position: relative;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 3px 10px 3px 2px;
  font-size: 12px;
  line-height: 1.3;
  cursor: pointer;
  white-space: nowrap;
}
.reader-tree-node:hover { background: color-mix(in srgb, var(--bg) 90%, var(--text)); }
/* 부모→자식 연결선 (옅게).  각 자식 item 이 세로선(::before) + 가로선
 * (::after) 을 그린다.  세로선 은 item 높이 전체(다음 형제 까지 이어짐),
 * 단 마지막 자식 은 가로선(노드 중앙, ~11px)까지만 → 그 아래 로는 안 그림. */
.reader-tree-children {
  position: relative;
  padding-left: 18px;
}
.reader-tree-item { position: relative; }
.reader-tree-children > .reader-tree-item::before {
  content: '';
  position: absolute;
  left: -10px; top: 0;
  width: 1px; height: 100%;
  background: color-mix(in srgb, var(--text) 24%, transparent);
}
.reader-tree-children > .reader-tree-item:last-child::before {
  height: 11px;   /* 마지막 자식 : 가로선 까지만(노드 세로 중앙), 아래 끊김 */
}
.reader-tree-children > .reader-tree-item::after {
  content: '';
  position: absolute;
  left: -10px; top: 11px;
  width: 12px; height: 1px;
  background: color-mix(in srgb, var(--text) 24%, transparent);
}
.reader-tree-node .reader-tree-dot { background: var(--text-faint); }
.reader-tree-node.is-green .reader-tree-dot { background: #3fb950; }
.reader-tree-node.is-orange .reader-tree-dot { background: #f0883e; }
.reader-tree-node.is-loading .reader-tree-dot { animation: reader-tree-pulse 0.8s ease-in-out infinite; }
.reader-tree-node.is-focal { background: color-mix(in srgb, var(--accent) 16%, var(--bg)); }
.reader-tree-node.is-focal .reader-tree-label b { color: var(--accent); }
.reader-tree-label {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 1 1 auto;
}
.reader-tree-label b { font-weight: 600; }
.reader-tree-snip { color: var(--text-faint); }
.reader-tree-count { color: #f0883e; font-weight: 600; }
@keyframes reader-tree-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
.reader-card.reader-tree-flash {
  animation: reader-tree-flash-kf 0.9s ease;
}
@keyframes reader-tree-flash-kf {
  0%, 100% { box-shadow: none; }
  30% { box-shadow: 0 0 0 3px var(--accent); }
}

/* 2026-05-25 사용자 spec : 타래 모달 의 카드 양쪽 에 sibling indicator
 * (◀ 이전 형제, ▶ 다음 형제).  카드 가장자리 위 에 absolute 로 떠
 * scroller padding 안 으로 살짝 overflow.  scroller padding 이 16 px
 * 이라 -10 px 위치 가 가장자리 6 px 안쪽 까지만 침범.  클릭 시
 * swapToSibling 트리거.  cache 에 sibling 정보 가 있 을 때만 보임. */
.reader-thread-sibling-prev,
.reader-thread-sibling-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.08);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  padding: 0;
  color: var(--text);
  font-size: 12px;
  line-height: 1;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.reader-thread-sibling-prev { left: -10px; }
.reader-thread-sibling-next { right: -10px; }
.reader-thread-sibling-prev:hover,
.reader-thread-sibling-next:hover {
  background: rgba(0, 0, 0, 0.18);
}
[data-theme="dark"] .reader-thread-sibling-prev,
[data-theme="dark"] .reader-thread-sibling-next {
  background: rgba(255, 255, 255, 0.12);
}
[data-theme="dark"] .reader-thread-sibling-prev:hover,
[data-theme="dark"] .reader-thread-sibling-next:hover {
  background: rgba(255, 255, 255, 0.22);
}

.reader-thread-loading {
  padding: 32px 16px;
  text-align: center;
}
.reader-compose-dialog .field-label[for="composer-body"] { display: none; }
/* (display: flow-root on .composer was attempted v0.9.763 to make
 * the reply preview narrow consistently with the textarea-wrap
 * around the close-button float. Reverted v0.9.764: bench repro
 * at multiple viewports showed identical border-box widths with
 * AND without the rule, while the user reported the fix made
 * production look weirder — so the rule wasn't helping the actual
 * cause, and was changing some unrelated layout in production.
 * Root cause still pending — need a clearer repro from the user
 * before re-attempting.) */
/* Drafts slot-picker modal. 5 rows, each with [preview / time] on
 * the left and [primary action + delete] on the right. Empty rows
 * dim their preview line and show no action in load mode. */
.reader-drafts-dialog .modal-body { min-width: min(420px, 92vw); }

/* ── DM hosted inside the reader dialog ────────────────────────── */
dialog.modal.reader-dm-dialog {
  max-width: min(900px, 96vw);
  width: calc(100% - 24px);
}
.reader-dm-dialog .modal-body {
  min-width: min(820px, 92vw);
  position: relative;
  /* Top padding clears the absolute-positioned X (right) and
   * back (left) buttons — both at top:10 with ~22px glyph
   * height, so they end at y=32. 44 leaves a 12px breathing
   * gap before the panes' top edge so the rounded corners
   * don't kiss the buttons. */
  padding: 44px 16px 16px;
}
/* Tighter shell inside the dialog — DM's own .dm-title-row already
 * supplies a heading, so we just need the close × pinned to the
 * top-right of the dialog body. */
.reader-dm-dialog .reader-settings-close {
  position: absolute;
  top: 10px;
  right: 14px;
  z-index: 2;
}
.reader-dm-mount { display: block; }
/* When DM is mounted inside the dialog, give the two-pane layout
 * a dialog-relative height so it fills the modal vs. the page. */
.reader-dm-dialog .dm-layout {
  height: min(620px, calc(100dvh - 160px));
}

/* DM access-denied notice — replaces the two-pane layout when the
 * pre-check finds the user's app password lacks DM scope. */
.dm-layout.dm-noaccess { display: block; height: auto; }
.dm-noaccess-card {
  max-width: 480px;
  margin: 24px auto;
  padding: 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  text-align: center;
}
.dm-noaccess-title { margin: 0 0 12px; font-size: 17px; }
.dm-noaccess-body {
  margin: 0 0 16px;
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1.55;
  text-align: left;
}
.dm-noaccess-steps {
  margin: 0 0 16px;
  padding-left: 22px;
  text-align: left;
  color: var(--text);
  font-size: 13px;
  line-height: 1.6;
}
.dm-noaccess-steps li { margin-bottom: 4px; }

/* Login screen DM hint — sits between the App-Password intro and
 * the form, calling out the separate DM-scope checkbox the user
 * needs to enable when creating their app password. */
.login-dm-hint {
  margin: 6px 0 14px;
  padding: 8px 12px;
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 8px;
  font-size: 12px;
  line-height: 1.5;
}

/* ── 블스DM — two-pane DM client ─────────────────────────────────
 * Convos list on the left (32% width), messages + send on the
 * right. On narrow viewports (<640 px) collapses to a stacked
 * column so each pane gets full width. Polling-based — no real-
 * time chrome; a small clock on each row's timestamp is the only
 * temporal cue. */
.dm-title-row { margin-bottom: 12px; }
.dm-title { margin: 0; font-size: 20px; }
.dm-subtitle { margin: 4px 0 0; color: var(--text-muted); font-size: 13px; }
.dm-layout {
  display: grid;
  /* The back button is now absolutely positioned over the modal
   * (mirroring the × close in the opposite corner), so the
   * layout no longer reserves a header row — the convo list /
   * messages panes get the full vertical real-estate. */
  grid-template-columns: minmax(220px, 32%) 1fr;
  grid-template-areas: "convos messages";
  grid-template-rows: 1fr;
  gap: 12px;
  height: calc(100dvh - 240px);
  min-height: 380px;
}
.dm-convos-pane { grid-area: convos; }
.dm-messages-pane { grid-area: messages; }
/* The header wrapper still exists so the JS keeps a stable
 * anchor for the back button, but `display: contents` makes
 * it disappear from layout — its absolute-positioned back-btn
 * child finds the modal-body (position:relative) as its
 * positioning context and renders in the top-left corner,
 * symmetric to the X in the top-right. */
.dm-header { display: contents; }
.dm-back-btn {
  /* Desktop default: always hidden. Back-to-list only makes
   * sense on the mobile single-pane navigation since on desktop
   * the convo list is permanently visible alongside the chat. */
  display: none;
  padding: 0 4px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.dm-back-btn:hover { color: var(--text); background: transparent; }
@media (max-width: 640px) {
  /* Mobile: show when the JS hasn't explicitly hidden it
   * (state.mobileView === 'chat' clears the [hidden] attr). */
  .dm-back-btn:not([hidden]) { display: inline-block; }
}
/* When DM is inside the host modal, anchor the back button to
 * the modal-body's top-left corner — mirror of the × close at
 * top-right. Both use the same coordinate space (modal-body is
 * position:relative). z-index keeps it above the panes' rounded
 * corners. */
.reader-dm-dialog .dm-back-btn {
  position: absolute;
  top: 10px;
  left: 14px;
  z-index: 2;
}
@media (max-width: 640px) {
  /* Single-pane navigation on mobile — only one of [convos,
   * messages] is visible at a time, controlled by the
   * .dm-mobile-list / .dm-mobile-chat modifier on .dm-layout.
   * Each pane gets the full width AND a tall fixed height so the
   * convo list / messages list actually fill the viewport. */
  .dm-layout {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    height: calc(100dvh - 120px);
  }
  .dm-layout.dm-mobile-list { grid-template-areas: "convos"; }
  .dm-layout.dm-mobile-list .dm-messages-pane { display: none; }
  .dm-layout.dm-mobile-chat { grid-template-areas: "messages"; }
  .dm-layout.dm-mobile-chat .dm-convos-pane { display: none; }
  /* The list view fills the full viewport — no more 240px cap.
   * No special inner padding needed for the panes any more:
   * the modal-body's 44px padding-top already places them
   * BELOW the back/X buttons, so the corner buttons sit on a
   * blank strip and never cross into pane content. */
  .dm-convos-pane { max-height: none; }
}
.dm-convos-pane,
.dm-messages-pane {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  overflow: hidden;
}
.dm-convos-list {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 4px;
}
.dm-convos-more { margin: 8px; align-self: center; }
.dm-empty {
  margin: 12px;
  text-align: center;
  color: var(--text-faint);
}
.dm-convo-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  background: transparent;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--text);
}
.dm-convo-row:hover { background: var(--surface-hover); }
.dm-convo-row.is-active {
  background: var(--accent-soft);
}
.dm-convo-row.is-unread .dm-convo-name { font-weight: 600; }
.dm-convo-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--surface-2);
}
.dm-convo-avatar-placeholder {
  background: var(--surface-hover);
}
.dm-convo-meta {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.dm-convo-name-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.dm-convo-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 14px;
}
/* The display-name span carries the row's weight. The handle
 * span next to it is dimmed so the eye still reads name-first
 * but the @handle is right there for disambiguation. */
.dm-convo-display { color: var(--text); }
.dm-convo-handle { color: var(--text-faint); font-weight: normal; }
.dm-convo-time { font-size: 11px; color: var(--text-faint); flex-shrink: 0; }
.dm-convo-preview {
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dm-convo-unread-badge {
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  border-radius: 9px;
  background: var(--accent);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
}
.dm-messages-header {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.dm-messages-with { font-size: 14px; font-weight: 500; }
/* Rich profile row in the chat-pane header — same shape as a
 * follower-list-modal row (avatar + display + handle), minus
 * the relationship pill / count slot. */
.dm-chat-profile {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.dm-chat-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-hover);
  flex: 0 0 36px;
}
.dm-chat-avatar-fallback {
  background: var(--border-strong);
  color: var(--text-muted);
}
.dm-chat-meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.dm-chat-display {
  font-weight: 600;
  font-size: 15px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text);
}
.dm-chat-handle {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dm-messages-list {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.dm-message {
  display: flex;
  flex-direction: column;
  max-width: 76%;
}
.dm-message-mine { align-self: flex-end; align-items: flex-end; }
.dm-message-theirs { align-self: flex-start; align-items: flex-start; }
.dm-message-bubble {
  padding: 8px 12px;
  border-radius: 14px;
  font-size: 14px;
  line-height: 1.4;
  word-break: break-word;
  white-space: pre-wrap;
}
.dm-message-mine .dm-message-bubble {
  background: var(--accent);
  color: #fff;
  border-bottom-right-radius: 4px;
}
.dm-message-theirs .dm-message-bubble {
  background: var(--surface-2);
  color: var(--text);
  border-bottom-left-radius: 4px;
}
.dm-message-time {
  font-size: 11px;
  color: var(--text-faint);
  margin-top: 2px;
}
.dm-send-form {
  display: flex;
  gap: 8px;
  padding: 10px;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  /* textarea 가 여러 줄로 자라면 보내기 버튼은 아래쪽(마지막 줄)에
   * 정렬 — 한 줄일 땐 차이 없음. */
  align-items: flex-end;
}
.dm-send-input {
  flex: 1 1 auto;
  min-width: 0;
  /* 오른쪽 패딩을 16px 로 — 한 줄에서 커서가 마지막 글자에 겹쳐 보이던
   * 현상 방지(사용자 보고 2026-05-31).  textarea 라 긴 글은 가로 스크롤
   * 없이 줄바꿈되므로 커서가 항상 글자 뒤 여백에 놓인다. */
  padding: 8px 16px;
  border: 1px solid var(--border);
  border-radius: 18px;
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
  /* auto-grow textarea : 수동 리사이즈 핸들 제거, 한 줄(약 20px)에서
   * 시작해 JS(autoGrowSendInput)가 scrollHeight 로 높이를 키운다.
   * max-height 를 넘으면 내부 스크롤. */
  font-family: inherit;
  line-height: 1.4;
  resize: none;
  overflow-y: auto;
  max-height: 120px;
  box-sizing: border-box;
}
.dm-send-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 133, 255, 0.15);
}

/* (Composer video upload progress bar was here — removed v0.9.752.
 * The server's progress field jumps backwards mid-encoding which
 * read as failures rather than ongoing work; the submit button
 * label now reflects the phase name instead.) */

/* ── Composer video preview ──────────────────────────────────────
 * Single column: video on top, meta strip + × remove button below.
 * NO overlay buttons — every native <video controls> chrome
 * (play / scrubber / volume / fullscreen / picture-in-picture /
 * download) sits ON the video element, so any overlay button
 * collides with at least one of them somewhere. Push the × out
 * to the meta strip where there's no collision. */
.composer-video-preview {
  margin: 8px 0;
  border-radius: 10px;
  overflow: hidden;
  background: #000;
}
.composer-video-preview[hidden] { display: none; }
.composer-video-preview-el {
  display: block;
  width: 100%;
  max-height: 320px;
  background: #000;
}
/* 미리보기 메타 — 세로 3줄(정보 / 편집됨 태그 / 액션). */
.composer-video-preview-meta {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 10px;
  font-size: 11px;
  color: var(--text-faint);
  background: var(--surface-2);
}
.composer-video-preview-inforow {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* 액션 줄 — 우측 정렬(저장 · 편집 · ×). */
.composer-video-preview-actionrow {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 6px;
}
.composer-video-preview-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1 1 auto;
  min-width: 0;
}
.composer-video-preview-size { flex: 0 0 auto; }
/* GIF 변환 결과 표기 — 파일명 옆 의 inline " (GIF)" 토 큰.  size /
 * dim 메타 와 같은 색/굵기 라 산만 하 지 않 게. */
.composer-video-preview-gif-tag {
  margin-left: 4px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
/* 영상 편집(실험) — 미리보기 '편집' 버튼 + 편집 모달. */
.composer-video-preview-edit {
  flex: 0 0 auto;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 12px;
  cursor: pointer;
}
.composer-video-preview-edit:hover { background: var(--surface-hover); }
.composer-video-preview-edit + .composer-video-preview-remove { margin-left: 6px; }
/* 편집 결과 영구 태그 — 영상 바로 아래에 자기 줄을 차지(flex-basis 100%)해서
 * 절대 놓치지 않게.  오디오 있으면 초록, 없으면 주황(미지원 경고). */
.composer-video-preview-edited {
  padding: 4px 10px;
  border-radius: 8px;
  font-size: 12px;
  font-weight: 600;
  background: color-mix(in srgb, var(--ok, #1a8917) 14%, transparent);
  color: var(--ok, #1a8917);
}
.composer-video-preview-edited.no-audio {
  background: color-mix(in srgb, var(--warn, #b26a00) 16%, transparent);
  color: var(--warn, #b26a00);
}
.composer-video-preview-save {
  flex: 0 0 auto;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 12px;
  text-decoration: none;
  cursor: pointer;
}
.composer-video-preview-save:hover { background: var(--surface-hover); }
/* 폭은 dialog 에 (CLAUDE.md: body 에 걸면 오른쪽 공백). */
dialog.modal.composer-videoedit-modal { max-width: 520px; }
.composer-videoedit-video { width: 100%; max-height: 320px; border-radius: 8px; background: #000; display: block; margin-bottom: 8px; }
.composer-videoedit-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.composer-videoedit-rowlabel { flex: 0 0 auto; min-width: 40px; font-size: 13px; color: var(--text-muted); }
.composer-videoedit-range { flex: 1 1 auto; min-width: 0; }
.composer-videoedit-trimlabel { margin: 6px 0 0; text-align: center; font-variant-numeric: tabular-nums; }
.composer-videoedit-subhead { margin: 14px 0 2px; font-size: 13px; }
.composer-videoedit-pos { flex: 0 0 auto; width: auto; }
.composer-videoedit-color { flex: 0 0 auto; width: 36px; height: 28px; padding: 0; border: 1px solid var(--border); border-radius: 6px; background: none; }
.composer-videoedit-color::-webkit-color-swatch-wrapper { padding: 2px; }
.composer-videoedit-progress { width: 100%; margin-top: 10px; }
.composer-videoedit-playrange { margin-top: 8px; }
.composer-videoedit-imgname { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* GIF 편집 모달 — 프레임 썸네일 그리드. 폭은 dialog 에(CLAUDE.md). */
dialog.modal.composer-gifedit-modal { max-width: 560px; }
.composer-gifedit-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 6px;
  max-height: 46vh;
  overflow-y: auto;
  padding: 6px;
  margin-top: 6px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-2);
}
.composer-gifedit-cell {
  position: relative;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 6px;
  background: #000;
  cursor: pointer;
  overflow: hidden;
  line-height: 0;
}
.composer-gifedit-thumb { width: 100%; height: auto; display: block; }
.composer-gifedit-num {
  position: absolute; left: 2px; top: 2px;
  font-size: 10px; line-height: 1; padding: 1px 3px;
  background: rgba(0, 0, 0, 0.6); color: #fff; border-radius: 3px;
}
.composer-gifedit-mark {
  position: absolute; right: 2px; top: 2px;
  font-size: 11px; line-height: 1;
}
/* 구간 밖: 흐리게. 제외: 빨강 오버레이 + ✕. 시작/끝 경계: 강조 테두리. */
.composer-gifedit-cell.out-range { opacity: 0.32; filter: grayscale(0.7); }
.composer-gifedit-cell.excluded::after {
  content: '✕'; position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; color: #fff; background: rgba(200, 30, 30, 0.55);
}
.composer-gifedit-cell.is-start { border-color: var(--accent, #0085ff); }
.composer-gifedit-cell.is-end { border-color: var(--accent, #0085ff); }
.composer-gifedit-info { margin: 6px 0 0; font-variant-numeric: tabular-nums; }
.composer-gifedit-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.composer-gifedit-rowlabel { flex: 0 0 auto; font-size: 13px; color: var(--text-muted); }
.composer-gifedit-fps { flex: 0 0 auto; width: 84px; }
.composer-gifedit-numbox { flex: 0 0 auto; width: 56px; text-align: center; padding: 4px 6px; font-variant-numeric: tabular-nums; }
.composer-gifedit-row .composer-gifedit-range { flex: 1 1 auto; min-width: 0; }

/* 변형/필터 공용 컨트롤(영상·GIF 편집). */
.composer-xform { margin-top: 4px; }
.composer-xform-label { margin: 10px 0 4px; font-size: 12px; color: var(--text-muted); }
.composer-xform-btnrow { display: flex; flex-wrap: wrap; gap: 6px; }
.composer-xform-chip {
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  cursor: pointer;
}
.composer-xform-chip:hover { background: var(--surface-hover); }
.composer-xform-chip.on { background: var(--accent, #0085ff); color: #fff; border-color: var(--accent, #0085ff); }
.composer-xform-checkrow { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: 13px; cursor: pointer; }
.composer-xform-check { flex: 0 0 auto; }

/* 이미지 편집 모달 — 맨 위 미리보기 항상 표시.  폭은 dialog 에(CLAUDE.md). */
dialog.modal.composer-imgedit-modal { max-width: 520px; }
.composer-imgedit-previewwrap {
  display: flex; align-items: center; justify-content: center;
  background: repeating-conic-gradient(#0001 0% 25%, transparent 0% 50%) 50% / 16px 16px, #000;
  border-radius: 10px; padding: 8px; margin: 6px 0 4px;
}
.composer-imgedit-preview-img { max-width: 100%; max-height: 300px; border-radius: 6px; display: block; }
/* 가림 모드(실험): 미리보기 위에 드래그 캡처용 투명 오버레이 */
.composer-imgedit-previewwrap.has-redact { position: relative; }
.composer-imgedit-redact-overlay {
  position: absolute; touch-action: none; cursor: crosshair;
  border-radius: 6px;
}
/* 가림 컨트롤 — 다섯 버튼(모자이크·블러·얼굴블러·실행취소·지우기)을 한 줄에.
 * 한 flex 컨테이너에서 gap 으로 간격을 통일(여러 div 로 쪼개 두 줄 되던 실수 방지). */
.composer-imgredact-row {
  display: flex; align-items: center; gap: 6px; margin: 4px 0 0;
  flex-wrap: nowrap;            /* 항상 한 줄 */
  overflow-x: auto;             /* 좁으면 가로 스크롤(버튼은 안 줄임) */
  -webkit-overflow-scrolling: touch;
  padding-bottom: 2px;
}
.composer-imgredact-btn { padding: 5px 10px; font-size: 13px; white-space: nowrap; flex: 0 0 auto; }
.composer-imgredact-mode.is-on,
.composer-imgredact-face.is-on { background: color-mix(in srgb, var(--accent) 18%, var(--surface)); color: var(--accent); border-color: var(--accent); }
.composer-imgedit-size { margin: 0 0 6px; font-variant-numeric: tabular-nums; }
.composer-imgedit-size.is-over { color: var(--warn, #b26a00); font-weight: 600; }

/* 영상 편집 크롭 미리보기 래퍼 — overflow hidden + aspect-ratio(크롭 시). */
.composer-videoedit-cropwrap {
  width: 100%; max-height: 320px; overflow: hidden; border-radius: 8px;
  background: #000; margin-bottom: 8px;
  display: flex; align-items: center; justify-content: center;
}
.composer-videoedit-cropwrap > .composer-videoedit-video { margin-bottom: 0; max-height: 320px; }

/* GIF 편집 실시간 미리보기 — 결과(변형/워터마크 적용)를 루프 재생. */
.composer-gifedit-previewwrap {
  display: flex; align-items: center; justify-content: center;
  background: repeating-conic-gradient(#0001 0% 25%, transparent 0% 50%) 50% / 16px 16px, #000;
  border-radius: 10px; padding: 8px; margin: 4px 0 10px;
}
.composer-gifedit-preview { max-width: 100%; max-height: 240px; border-radius: 6px; display: block; }

/* 다중 GIF 이어붙이기 — 클립 목록 + 크로스페이드. */
.composer-gifedit-concat { margin: 4px 0 2px; }
.composer-gifedit-cliplist { display: flex; flex-direction: column; gap: 6px; margin-bottom: 6px; }
.composer-gifedit-cliprow { display: flex; align-items: center; gap: 8px; }
.composer-gifedit-clipthumb { flex: 0 0 auto; width: 44px; height: 44px; border-radius: 6px; background: #000; }
.composer-gifedit-clipname { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--text-muted); }

/* 합치기 모달 — 클립 썸네일 캐러셀 + 선택 클립 편집. */
dialog.modal.composer-merge-modal { max-width: 560px; }
.composer-merge-carousel {
  display: flex; gap: 8px; overflow-x: auto; padding: 8px 4px;
  margin: 4px 0; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2);
}
.composer-merge-thumb {
  position: relative; flex: 0 0 auto; padding: 0; border: 2px solid transparent;
  border-radius: 6px; background: #000; cursor: pointer; line-height: 0; overflow: hidden;
}
.composer-merge-thumb canvas { display: block; }
.composer-merge-thumb.is-sel { border-color: var(--accent, #0085ff); }
.composer-merge-thumb .composer-gifedit-num { position: absolute; left: 2px; top: 2px; }
.composer-merge-editpanel { margin-top: 8px; }

/* 접이식 섹션(변형·필터 / 워터마크) — 헤더 클릭으로 펼침/접힘. */
.composer-collapse { margin-top: 12px; border-top: 1px solid var(--border); }
.composer-collapse-header {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: 10px 2px; background: none; border: 0; cursor: pointer;
  font-size: 14px; font-weight: 600; color: var(--text); text-align: left;
}
.composer-collapse-arrow { flex: 0 0 auto; width: 14px; color: var(--text-muted); }
/* '이미지로 GIF 만들기' — 이미지 그리드 바로 아래 전체폭 버튼. */
.composer-img2gif-btn { display: block; width: 100%; margin: 8px 0 4px; }

/* 편집 모달이 열려 있으면 우하단 플로팅 '맨 위로' 버튼류 숨김(적용 버튼과
 * 겹쳐 불편 — 사용자 보고).  position:fixed FAB 가 dialog 위로 떠서 가림. */
body.composer-editmodal-open .reader-compose-scrolltop,
body.composer-editmodal-open .reader-modal-scrolltop,
body.composer-editmodal-open .reader-profile-scrolltop,
body.composer-editmodal-open .reader-compose-discard { display: none !important; }
.btn.btn-sm { padding: 4px 10px; font-size: 12px; }
.composer-video-preview-remove {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 0;
  background: var(--surface-hover);
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.composer-video-preview-remove:hover {
  background: var(--danger-soft);
  color: var(--danger);
}

/* ── Per-post Google Translate toggle ────────────────────────────
 * Subtle inline affordance under the body text; only renders when
 * the post's lang tag mismatches the user's UI base lang. Button
 * is muted by default + accent on hover; result paragraph sits
 * indented with a left border so the user sees it as derived
 * content (not part of the original post). */
.reader-translate-wrap {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 6px;
}
.reader-translate-btn {
  align-self: flex-start;
  background: transparent;
  border: 0;
  padding: 2px 6px;
  margin: 0;
  color: var(--text-faint);
  font-size: 12px;
  cursor: pointer;
  border-radius: 6px;
}
.reader-translate-btn:hover:not(:disabled) {
  color: var(--accent);
  background: var(--accent-soft);
}
.reader-translate-btn:disabled { cursor: wait; opacity: 0.6; }
.reader-translate-result {
  margin: 4px 0 0;
  padding: 8px 12px;
  border-left: 3px solid var(--accent-soft);
  background: color-mix(in srgb, var(--accent-soft) 50%, transparent);
  font-size: 14px;
  color: var(--text);
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  border-radius: 0 6px 6px 0;
}
.reader-translate-result[hidden] { display: none; }

/* ── Muted-words modal ───────────────────────────────────────────── */
.reader-muted-words-dialog .modal-body { min-width: min(420px, 92vw); }
.reader-muted-words-list {
  /* position: relative so the JS-packed chips (position: absolute,
   * with computed left/top from packChips in reader.js) anchor to
   * this container. Without this, the chips would anchor to the
   * nearest positioned ancestor (the dialog or worse). */
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  min-height: 48px;
  align-content: flex-start;
}
.reader-muted-words-empty {
  margin: 0 !important;
  padding: 4px;
  width: 100%;
  text-align: center;
  color: var(--text-faint);
  font-style: italic;
}
.reader-muted-words-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 4px 2px 4px;
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 999px;
  font-size: 13px;
  max-width: 100%;
}
.reader-muted-words-chip.is-open {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* Chip body is a button now — clicking opens the info popover. */
.reader-muted-words-chip-body {
  appearance: none;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  min-width: 0;
}
.reader-muted-words-chip-body:hover {
  background: rgba(0, 0, 0, 0.06);
}
[data-theme="dark"] .reader-muted-words-chip-body:hover {
  background: rgba(255, 255, 255, 0.08);
}
/* Suppress the browser's mouse-click focus ring — the popover-open
 * .is-open accent outline on the parent chip is our visual feedback
 * for "this chip is selected". Without this rule, clicking a chip
 * leaves a stacked ring (focus-visible + .is-open) that reads as a
 * UI bug. Keyboard tab still draws focus-visible so a11y stays
 * intact unless the popover is open (where .is-open takes over). */
.reader-muted-words-chip-body:focus:not(:focus-visible) { outline: none; }
.reader-muted-words-chip.is-open .reader-muted-words-chip-body:focus,
.reader-muted-words-chip.is-open .reader-muted-words-chip-body:focus-visible {
  outline: none;
}
.reader-muted-words-chip-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 220px;
}
.reader-muted-words-chip-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 0;
  background: transparent;
  color: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  flex: 0 0 22px;
}
.reader-muted-words-chip-x:hover {
  background: rgba(0, 0, 0, 0.08);
}
[data-theme="dark"] .reader-muted-words-chip-x:hover {
  background: rgba(255, 255, 255, 0.1);
}
/* Input + 추가 button share one row — input expands, button auto.
 * margin-top gives breathing room from the chip list above (we
 * dropped the explicit <hr> divider; this margin is the visual
 * separator). */
.reader-muted-words-add-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
  margin-top: 14px;
}
/* Chip info popover. Position:fixed against the viewport so it
 * lives in the same top-layer as the dialog (a body-level
 * popover would render under the modal backdrop). z-index above
 * chips inside the modal but below toasts. */
.reader-muted-words-popover {
  position: fixed;
  z-index: 50;
  min-width: 180px;
  max-width: min(300px, 80vw);
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  font-size: 12px;
  color: var(--text);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.reader-muted-words-popover-row {
  display: flex;
  gap: 12px;
  justify-content: space-between;
  align-items: baseline;
}
.reader-muted-words-popover-key {
  color: var(--text-muted);
  flex: 0 0 auto;
}
.reader-muted-words-popover-val {
  color: var(--text);
  text-align: right;
  min-width: 0;
  word-break: keep-all;
}
/* ✓ / ✗ for the exclude-following row. Accent green when on,
 * dimmed when off — a glanceable binary indicator. */
.reader-muted-words-popover-icon {
  font-size: 14px;
  line-height: 1;
  font-weight: 700;
}
.reader-muted-words-popover-icon.is-on { color: var(--accent); }
.reader-muted-words-popover-icon.is-off { color: var(--text-faint); }
/* Push the save/cancel actions row clear of the input + chip list
 * above. .modal-actions has no top margin by default, so the
 * buttons rendered right under the add-row visually overlapped
 * with input on narrow viewports. */
.reader-muted-words-dialog .modal-actions {
  margin-top: 16px;
}
/* Stack the control rows (기간 / 뮤트 범위 / 팔로잉 제외하기)
 * with comfortable vertical breathing room — matches the rhythm
 * of the main settings dialog where each row sits in its own
 * surface-2 capsule. */
.reader-muted-words-dialog .reader-settings-row {
  margin: 10px 0;
}
/* Override the global flex:1-on-first-child rule for this modal:
 * we want the label to hug its content so the dotted leader (the
 * actual visual "spacer") starts right after it. Without this,
 * the label and the leader split leftover width 50/50 and the
 * 라벨—— gap looks far too wide. */
.reader-muted-words-dialog .reader-settings-row > :first-child {
  flex: 0 0 auto;
}
.reader-muted-words-dialog .reader-settings-row input[type="checkbox"] {
  width: 18px;
  height: 18px;
  margin: 0;
  cursor: pointer;
  flex: 0 0 18px;
}
/* Full-width settings action button (keyword mute / user mute
 * entries in the basic-settings left column). Plain row layout
 * — no label / leader / button split. */
.reader-settings-action-wide {
  width: 100%;
  justify-content: center;
}

/* ── User-mute modal ──────────────────────────────────────────── */
/* Activity-subscription per-profile modal (bell on profile actions).
 * Two-checkbox dialog: New posts / Replies, with Save + Cancel. */
.reader-subscribe-dialog .modal-body { min-width: min(380px, 92vw); }
.reader-subscribe-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 4px; cursor: pointer;
}
.reader-subscribe-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 12px;
}
/* Cancel + Save are both compact — they take their natural label
 * width (저장 / 취소 sit on the right edge as small adjacent
 * buttons).  Earlier the cancel button was getting stretched to a
 * full-width bar by `.reader-settings-row > :first-child { flex: 1 }`,
 * fixed at the markup level too (the actions container no longer
 * carries .reader-settings-row).  Equal min-width keeps the pair
 * symmetric regardless of label glyph widths. */
.reader-subscribe-actions > .btn {
  flex: 0 0 auto;
  min-width: 88px;
}

/* Thread-settings sub-modal opened from the main settings dialog.
 * Vertical rhythm in this small modal needs more breathing room than
 * the main settings grid — the three rows (position / template /
 * delay) read claustrophobic at the default 0px row gap. */
/* ─── Shared modal action-row layout ───
 *
 * Every confirm/cancel button pair across the app's modals (account
 * deactivate / delete, thread reset confirm, thread settings close,
 * subscribe save, …) was previously dropped into
 * `class: 'reader-settings-row reader-settings-row-actions'`.
 * That dragged in TWO buggy behaviours from .reader-settings-row:
 *   (1) `> :first-child { flex: 1 }` stretched the FIRST button to
 *       fill the row, so the cancel button kept ending up wider
 *       than the danger / save button.
 *   (2) The row had no top padding, so it sat flush against the
 *       previous paragraph / picker — visually claustrophobic.
 *
 * Use .reader-modal-actions for ALL future modal button rows.  It
 * keeps the buttons compact (their natural label width, equal
 * min-width) and adds the breathing room the previous structure
 * was missing.  Never combine with .reader-settings-row. */
.reader-modal-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  /* 28px top margin so the row breathes after dense content above
   * (paragraphs / select rows / inputs).  Specifically called out
   * by the deactivate-account flow where the deleteAfter select
   * row used to sit ~6px above the cancel/confirm buttons. */
  margin-top: 28px;
}
.reader-modal-actions > .btn {
  flex: 0 0 auto;
  min-width: 96px;
}
/* Standalone single-button row (e.g. "Request delete code" inside
 * the account-delete modal) — left-aligned + extra bottom space
 * so the inputs that follow don't crowd it. */
.reader-modal-actions-standalone {
  justify-content: flex-start;
  margin-top: 12px;
  margin-bottom: 8px;
}

/* Auto-installed scroll-to-top chip — appears on every <dialog>
 * that grows past 240px of scroll travel.  Installer lives in
 * app.js as part of the showModal monkey-patch.  Same visual chip
 * shape as the compose modal's bespoke scroll-top so the two
 * affordances read as a pair across the app. */
.reader-modal-scrolltop {
  position: sticky;
  bottom: 16px;
  margin-left: auto;
  margin-right: 16px;
  margin-top: -56px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  color: var(--text);
  cursor: pointer;
  font-size: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  transition: opacity 0.2s ease, transform 0.18s ease;
}
.reader-modal-scrolltop:hover { transform: scale(1.06); }
.reader-modal-scrolltop[hidden] { display: none; }

/* ── 블스투데이 tool ─────────────────────────────────────────
 * Streams the user's posts/reposts from "this day in past years"
 * top-to-bottom (newest year first). */
.today-root { display: flex; flex-direction: column; gap: 12px; }
.today-intro { color: var(--text-muted); margin: 4px 0 0 0; }
.today-mode-toggle {
  display: flex;
  gap: 6px;
  justify-content: center;
  margin: 12px 0 4px;
  flex-wrap: wrap;
}
.today-mode-toggle .btn { padding: 6px 14px; font-size: 14px; }
.today-follow-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 6px;
  background: var(--card-bg, transparent);
}
.today-follow-avatar {
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--border);
}
.today-follow-avatar-empty { display: inline-block; }
.today-follow-meta { min-width: 0; flex: 1; }
.today-follow-name { font-size: 14px; font-weight: 600; }
.today-follow-handle { font-size: 12px; color: var(--text-muted); }
.today-section-info { padding: 8px 0; }
.today-sections { display: flex; flex-direction: column; }
.today-section { display: flex; flex-direction: column; gap: 10px; padding: 14px 0; }
.today-section-head {
  margin: 0;
  /* "n년 전" — big, the marquee per spec.  Centred so the year +
   * date stack reads as a chapter title. */
  font-size: clamp(28px, 5vw, 44px);
  font-weight: 700;
  color: var(--accent);
  text-align: center;
}
.today-section-date {
  margin: 0;
  color: var(--text-faint);
  font-size: 13px;
  text-align: center;
}
.today-section-list { display: flex; flex-direction: column; gap: 10px; }
.today-separator { border: 0; border-top: 1px solid var(--border); margin: 0; }
.today-status {
  margin: 12px 0 0 0;
  text-align: center;
  color: var(--text-muted);
  font-size: 14px;
}
.today-status.done { font-size: 18px; font-weight: 600; color: var(--text); }
.today-status.err  { color: var(--danger); }
/* Foot block — appears below "끝" with the timezone used + a picker.
 * Hidden during loading / paused; only shows once phase === 'done'.
 * Per spec the picker is local-to-this-tool: writes only to
 * bsky-tools:today/timezone/v1 and never mutates the reader's prefs. */
.today-tzfoot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin: 16px 0 24px 0;
  color: var(--text-muted);
  font-size: 13px;
}
.today-tzfoot[hidden] { display: none; }
.today-tzfoot-msg { margin: 0; text-align: center; }
.today-tzfoot-select {
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
}

.reader-thread-settings-dialog .modal-body { min-width: min(440px, 92vw); }
/* Breathing room between options without separator lines (per spec
 * "옵션과 옵션 사이엔 수평선이 있을 필요 없음. 지워"). */
.reader-thread-settings-dialog .modal-body > .reader-settings-row {
  padding: 12px 4px;
}
/* Label hugs its text — earlier the global `.reader-settings-row >
 * :first-child { flex: 1 }` rule was stretching it, pushing the
 * dashed leader line off to the right of empty whitespace and
 * making the "라벨 ㅡㅡㅡ" gap read enormous.  Forcing 0-0-auto on
 * the label restores a tight "label ——— control" rhythm. */
.reader-thread-settings-dialog .modal-body > .reader-settings-row > :first-child {
  flex: 0 0 auto;
}
.reader-thread-settings-dialog .modal-body > .reader-settings-row-actions {
  padding-top: 16px;
}

/* Activity-subscription LIST modal (open from settings → 구독 목록).
 * Paged list of avatar + handle + Post/Reply toggles. Row collapses
 * with a height transition on unsubscribe (both flags off). */
.reader-subscriptions-dialog .modal-body { min-width: min(500px, 92vw); }
.reader-subscriptions-status { margin: 8px 0; color: var(--text-faint); }
.reader-subscriptions-list {
  display: flex; flex-direction: column; gap: 6px;
  max-height: min(60dvh, 480px); overflow-y: auto; padding: 4px 0;
}
.reader-subscriptions-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: 8px;
}
.reader-subscriptions-row:hover { background: var(--surface-hover); }
.reader-subscriptions-avatar {
  width: 36px; height: 36px; border-radius: 50%; flex: 0 0 36px;
  object-fit: cover; background: var(--surface-2);
}
.reader-subscriptions-name { flex: 1 1 auto; min-width: 0; }
.reader-subscriptions-display {
  font-weight: 600; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;
}
.reader-subscriptions-handle {
  font-size: 12px; color: var(--text-faint);
  white-space: nowrap; text-overflow: ellipsis; overflow: hidden;
}
.reader-subscriptions-controls { display: flex; gap: 10px; flex: 0 0 auto; }
.reader-subscriptions-flag {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px; cursor: pointer;
}

.reader-muted-actors-dialog .modal-body { min-width: min(460px, 92vw); }
.reader-muted-actors-status { margin: 8px 0; }
.reader-muted-actors-status.err { color: var(--danger); }
.reader-muted-actors-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: min(60dvh, 480px);
  overflow-y: auto;
  padding: 4px 0;
}
.reader-muted-actors-empty {
  margin: 12px;
  text-align: center;
  color: var(--text-faint);
}
.reader-muted-actors-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
}
.reader-muted-actors-row:hover { background: var(--surface-hover); }
.reader-muted-actors-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex: 0 0 36px;
  object-fit: cover;
  background: var(--surface-2);
}
.reader-muted-actors-avatar-fallback { background: var(--border-strong); }
.reader-muted-actors-meta {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.reader-muted-actors-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-muted-actors-handle {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-muted-actors-unmute {
  flex: 0 0 auto;
  padding: 6px 12px;
  font-size: 13px;
}
.reader-muted-actors-dialog .modal-actions {
  margin-top: 16px;
}

.reader-muted-words-add-btn {
  flex: 0 0 auto;
  padding: 8px 16px;
  border-radius: 8px;
  font-size: 14px;
  white-space: nowrap;
}
.reader-muted-words-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
}
.reader-muted-words-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 133, 255, 0.15);
}
.reader-drafts-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 12px;
}
.reader-drafts-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.reader-drafts-row.empty { background: var(--surface-2); }
.reader-drafts-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.reader-drafts-preview {
  font-size: 14px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-drafts-row.empty .reader-drafts-preview { color: var(--text-faint); }
.reader-drafts-time {
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.reader-drafts-actions {
  display: flex;
  gap: 6px;
  flex: 0 0 auto;
}
.reader-drafts-actions .btn {
  padding: 4px 10px;
  font-size: 12px;
}
/* The Composer's `.composer-top` slot has a margin-bottom even when
 * `extraTop` is unused. Reader doesn't use that slot, so collapse it
 * so the textarea sits as high as the body label removal allows. */
.reader-compose-dialog .composer-top:empty { display: none; }
/* Media-slot card: rendered above the body when the user pastes a
 * URL that produced either a link card (cardyb OG metadata) or a
 * feed card (bsky feed generator). Single slot — link/feed mutually
 * exclusive. Layout: thumb on left, title + description + host on
 * right, × remove button on the top-right (hidden when the host
 * pinned the card via opts.fixedFeedCard). */
.composer-media-slot {
  margin-bottom: 12px;
}
.composer-media-slot[hidden] { display: none; }
.composer-media-card {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 10px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  overflow: hidden;
}
.composer-media-thumb {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--surface-hover);
}
.composer-media-thumb-feed { border-radius: 10px; }
/* Quote-card uses the author's avatar — circular like everywhere else. */
.composer-media-thumb-quote { border-radius: 50%; }
.composer-media-thumb-empty { display: inline-block; }
.composer-media-card.is-loading {
  /* placeholder while link/feed/quote metadata is fetching — same chassis
     as the real card so it doesn't shift position when the data arrives */
  opacity: 0.7;
}
.composer-media-card.is-loading .composer-media-thumb {
  background: var(--border, #e5e7eb);
  animation: composer-media-pulse 1.2s infinite ease-in-out;
}
.composer-media-loading {
  font-size: 13px;
  color: var(--text-muted);
  font-weight: 500;
}
@keyframes composer-media-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}
.composer-media-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-right: 22px;  /* leave room for the × button */
}
.composer-media-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.composer-media-desc {
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.composer-media-host {
  font-size: 11px;
  color: var(--text-faint);
  margin-top: 2px;
}
.composer-media-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--surface-hover);
  color: var(--text-muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.composer-media-remove:hover {
  background: var(--border);
  color: var(--text);
}

/* Reply-mode preview: a full post-card sits above the composer with
 * a ⋮ visualising the thread chain (parent → my reply). The
 * post-card is non-interactive in this context (action buttons +
 * links are still tab targets but don't navigate anywhere useful in
 * the modal). */
.reader-compose-reply-preview {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  margin-bottom: 0;
}
.reader-compose-reply-card {
  display: block;
}
.reader-compose-reply-card .reader-card {
  margin: 0;
  cursor: default;
}
/* Vertical-ellipsis chain glyph between the preview card and the
 * composer body. Centered + tight top padding only — the bottom gap
 * comes from suppressed composer-top margin below. */
.reader-compose-chain-dots {
  display: block;
  text-align: center;
  padding: 4px 0 0;
  font-size: 22px;
  line-height: 1;
  color: var(--text-muted);
  letter-spacing: 0;
}
/* When composer-top carries the reply preview, collapse its 16px
 * trailing margin so the ⋮ glyph sits flush against the body input
 * underneath. Without this, the chain reads as a too-wide gap that
 * breaks the "post → my reply" visual continuity. */
.reader-compose-dialog .composer-top:has(> .reader-compose-reply-preview) {
  margin-bottom: 0;
}
/* Reader-only: paint link text in the accent colour. The textarea
 * itself can't colour per-range, so we make its text transparent
 * (caret + selection still visible) and let the mirror behind it
 * render the visible characters — the mirror's link spans get the
 * accent colour while everything else stays in --text. The link
 * background + underline (.mirror-link) are unchanged. */
.reader-compose-dialog .composer-textarea {
  color: transparent;
  -webkit-text-fill-color: transparent;
  caret-color: var(--text);
}
.reader-compose-dialog .composer-textarea::placeholder {
  color: var(--text-muted);
  -webkit-text-fill-color: var(--text-muted);
}
.reader-compose-dialog .composer-textarea::selection {
  /* Semi-transparent so the mirror text shows through during a
   * highlight; otherwise the opaque default selection bg would
   * cover the coloured characters underneath. */
  background: rgba(0, 133, 255, 0.30);
  color: transparent;
  -webkit-text-fill-color: transparent;
}
.reader-compose-dialog .composer-textarea-mirror { color: var(--text); }
/* 컴포저 미러 정렬 fix (2026-05-31 공개) — 모바일 브라우저의
 * text-size-adjust:auto 가 <textarea> 글자만 부풀리고 <div> 미러는 안
 * 부풀려, 보이는 글자(미러)와 실제 선택/캐럿(textarea)이 어긋난다.  두
 * 레이어를 100% 로 고정해 인플레이션을 끄면 정렬 복구.  검증:
 * iPhone13/Pixel7 에뮬 ta vs mir scrollHeight 254/244 → 167=167. */
.reader-compose-dialog .composer-textarea,
.reader-compose-dialog .composer-textarea-mirror {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}
/* Mask controls in the reader composer.
 *
 * The mask-add button (.reader-compose-mask-add) is grafted onto the
 * composer's .composer-link-toolbar at mount, so it inherits the
 * same selection-gated visibility as the link button — both live in
 * one row and vanish together when there's no selection.
 *
 * The mode picker + preview button (.reader-compose-mask-row) lives
 * below the textarea in extraSection. The row is hidden until at
 * least one mask is set; clearing all masks (via edits or 비우기)
 * hides it again. */
.reader-compose-mask-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.reader-compose-mask-row[hidden] { display: none; }
.reader-compose-mask-row-top,
.reader-compose-mask-row-bottom {
  display: flex;
  align-items: center;
  gap: 8px;
}
.reader-compose-mask-row-bottom { flex-wrap: wrap; }
/* Collapse the entire extra-section slot when the masking row inside
 * it is hidden, so the textarea ↔ images gap doesn't keep the empty
 * padding + 16px section margin between them. Reader-only — keeps
 * the standalone #/masking tool untouched. */
.reader-compose-dialog .composer-extra-section:has(> .reader-compose-mask-row[hidden]) {
  display: none;
}
/* When the masking row IS visible, tighten the gap on both sides:
 * drop the slot's own padding-top, shrink its bottom margin, and
 * shorten the body row's bottom margin sitting above it. */
.reader-compose-dialog .composer-extra-section:has(> .reader-compose-mask-row:not([hidden])) {
  padding-top: 0;
  margin-bottom: 6px;
}
.reader-compose-dialog .composer-section:has(+ .composer-extra-section > .reader-compose-mask-row:not([hidden])) {
  margin-bottom: 6px;
}
.reader-compose-mask-mode-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  cursor: pointer;
}
.reader-compose-mask-mode-label-text {
  font-size: 13px;
  opacity: 0.85;
}
.reader-compose-mask-mode-select {
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.reader-compose-mask-mode-select:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
/* Passphrase button — flips to an "active" look when a passphrase
 * is set so the user has a clear at-a-glance signal. */
.reader-compose-mask-passphrase.is-set {
  border-color: rgba(212, 24, 61, 0.45);
  color: var(--accent, #4a90e2);
}
[data-theme="dark"] .reader-compose-mask-passphrase.is-set {
  border-color: rgba(248, 113, 133, 0.55);
}
/* Restrict-viewers button — same active styling as passphrase so a
 * masked post with any side-channel rule (audience / passphrase) has
 * the same visual weight in the composer toolbar. */
.reader-compose-mask-restrict.is-set {
  border-color: rgba(212, 24, 61, 0.45);
  color: var(--accent, #4a90e2);
}
[data-theme="dark"] .reader-compose-mask-restrict.is-set {
  border-color: rgba(248, 113, 133, 0.55);
}
/* Restrict-viewers modal — toggle rows with pill-style choices. */
.reader-mask-restrict-modal .modal-body { min-width: min(420px, 92vw); }
.reader-mask-restrict-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 14px 0 0;
}
.reader-mask-restrict-row[hidden] { display: none; }
.reader-mask-restrict-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
}
.reader-mask-restrict-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.reader-mask-restrict-pill {
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  font-size: 13px;
}
.reader-mask-restrict-pill:hover { background: var(--surface-hover); }
.reader-mask-restrict-pill.is-active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}
.reader-mask-restrict-list-select {
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
}
.reader-mask-restrict-list-hint { margin: 4px 0 0; }
/* Passphrase setter modal — separate from the reveal-time prompt
 * because this one shows on the publish side (composer) and carries
 * the rules hint. */
.reader-mask-pp-setter-modal { max-width: 480px; }
.reader-mask-pp-setter-modal .modal-body { min-width: min(420px, 92vw); }
.reader-mask-pp-setter-title { margin: 0 0 6px; }
.reader-mask-pp-setter-hint { margin: 0 0 10px; }
.reader-mask-pp-setter-input {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  margin-bottom: 12px;
}
.reader-mask-pp-setter-input:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
.reader-compose-mask-custom-input {
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  font-size: 13px;
  /* Sits to the right of the mode picker and stretches to the row's
   * right edge so its right side lines up with the composer
   * textarea above. */
  flex: 1 1 auto;
  min-width: 0;
}
.reader-compose-mask-custom-input[hidden] { display: none; }
.reader-compose-mask-custom-input:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
/* Mask preview modal — read-only rendering of what the published
 * body will look like after the masks are applied. URL substrings
 * are tinted with the link colour so the preview matches the
 * post-card rendering. */
.reader-mask-preview-modal { max-width: 520px; }
.reader-mask-preview-modal .modal-body { min-width: min(480px, 92vw); }
.reader-mask-preview-title { margin: 0 0 6px; }
.reader-mask-preview-text {
  margin: 8px 0 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  white-space: pre-wrap;
  word-break: break-word;
  font-family: inherit;
  line-height: 1.5;
}
.reader-mask-preview-link {
  color: var(--accent, #4a90e2);
  text-decoration: underline;
}
.reader-compose-dialog .mirror-link {
  color: var(--accent);
  /* Drop the 10% blue tint background reader-side — the text colour
   * + underline (inherited box-shadow from the global rule) is enough
   * to show what's linked. */
  background: transparent;
}
/* Settings title row — "설정" on the left, export + import buttons
 * on the right. Both buttons are compact (secondary style) and sized
 * to their content, so the title keeps left alignment. */
.reader-settings-title-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
}
.reader-settings-title-row h3 { margin: 0; flex: 1; min-width: 0; }
/* Cache-bust marker. Sits next to the settings title — small +
 * muted so it reads as metadata, not a heading. */
.reader-settings-version {
  font-size: 11px;
  font-weight: 400;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  margin-left: 4px;
}
/* PDS-sync row hosts the toggle + a status line beneath. Outer
 * row stays flex (toggle on left, ⓘ on right); the inner div
 * stacks the toggle label and the status line vertically. */
.reader-pds-sync-row-inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
/* Parent-scoped so the (0,2,0) specificity beats .modal-body p's
 * default 0 0 16px bottom margin — without this the device-local
 * footnote line drops a 16px gap inside its capsule and the card
 * looks padded-out below the message. */
.reader-pds-sync-row .reader-pds-sync-status {
  margin: 0;
  font-size: 10px;
  font-weight: 400;
  line-height: 1.25;
  color: var(--text-faint);
  /* "이 설정은 기기마다 따로 저장돼요." sub-line under each
   * device-local toggle. Aggressively muted (10 px / 0.55) so it
   * reads as a barely-there footnote rather than a second label
   * competing for attention with the toggle's main copy. */
  opacity: 0.55;
  letter-spacing: 0;
}
.reader-pds-sync-row .reader-pds-sync-status:empty { display: none; }
.reader-pds-sync-status-error { color: var(--danger); }
/* PDS 동기화 충돌 모달 — 동기화 ON 시 로컬 ≠ 원격 일 때 "어느 쪽으로
   맞출지" 묻는 다이얼로그.  설명 줄 + 3 버튼(취소 / 동기화된 설정 /
   지금 설정).  버튼 이 좁은 화면 에서 넘치 지 않 게 wrap. */
.reader-pds-conflict-modal .modal-body { min-width: min(440px, 92vw); max-width: 520px; }
.reader-pds-conflict-line { margin: 8px 0 0; color: var(--text-muted); line-height: 1.5; }
.reader-pds-conflict-actions { margin-top: 16px; flex-wrap: wrap; }
/* Transfer modal — two sections (file save / cross-account move)
 * stacked vertically. Account rows reuse the session-menu-account
 * flex layout so avatars + handles align identically to the global
 * account switcher. */
.reader-transfer-modal .modal-body { min-width: 320px; max-width: 480px; }
/* 예전 글 보기 결과 리스트 — 카드 사이 간격 확보. */
.reader-time-travel-list { display: flex; flex-direction: column; gap: 10px; }

.reader-mine-search-modal .modal-body { min-width: min(420px, 88vw); max-width: 640px; }
.reader-mine-search-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; font-size: 13px; }
.reader-mine-search-field > input { width: 100%; box-sizing: border-box; }
/* 날짜 두 칸을 한 줄에 나란히 두되 input 이 label 폭을 넘어 겹치지
   않도록 flex:1 + min-width:0 으로 두 컬럼이 동등하게 가용 폭을
   나눠 갖게 함.  input 자체는 width:100% box-sizing:border-box
   (위 .reader-mine-search-field > input 규칙) 라 컬럼 안에 정확히
   들어맞음.

   다만 iOS Safari 는 <input type='date'> 에 width / min-width 를
   무시하고 native picker chrome 의 min-content 사이즈를 강요해서
   flex 컬럼을 overflow → 옆 input 과 시각적 겹침이 생김.  cleaner
   가 같은 문제를 line ~10696 의 .filter-section input[type='date']
   { appearance: none; -webkit-appearance: none; width: ... } 로 풀어
   둔 패턴을 차용 — appearance: none 으로 native picker UI 를 강제로
   걷어내면 width 가 제대로 먹음.  picker 자체는 input 을 탭했을 때
   여전히 native dialog 가 뜸 (chrome 만 없을 뿐). */
.reader-mine-search-daterow { display: flex; gap: 12px; }
.reader-mine-search-daterow .reader-mine-search-field {
  flex: 1 1 0;
  min-width: 0;
  margin-bottom: 0;
}
.reader-mine-search-daterow input[type='date'] {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  /* 값이 있을 때 (= 텍스트 그림) 와 없을 때 (= 빈 박스) input native
   * 가 다른 높이를 그리는 것을 방지 — 같은 줄 두 input 의 height 가
   * 어긋나 보이는 문제 픽스.  min-height 로 큰 쪽 높이에 맞춰 잠금. */
  min-height: 38px;
  line-height: 1.3;
}
/* 종류 세그먼트 토글 — 3 버튼을 한 pill 안에 붙여 표시. */
.reader-mine-search-typetoggle {
  display: flex;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.reader-mine-search-typetoggle .btn {
  flex: 1;
  border: none;
  border-radius: 0;
  padding: 8px 12px;
  font-size: 13px;
  background: transparent;
  color: var(--text);
}
.reader-mine-search-typetoggle .btn + .btn {
  border-left: 1px solid var(--border);
}
.reader-mine-search-typetoggle .btn.primary {
  background: var(--accent);
  color: #fff;
}
.reader-mine-search-status { margin: 10px 0; }
.reader-mine-search-results > * { margin-bottom: 8px; }
.reader-profile-search-btn { margin-right: 6px; }

.server-tool-section-head {
  margin: 18px 0 6px;
  padding: 0 4px;
}
.server-tool-section-head h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 700;
}
.server-tool-section-head .hint {
  margin: 4px 0 0;
  font-size: 12px;
  line-height: 1.4;
}
.server-tool-section-head:first-child { margin-top: 4px; }

.reader-server-status-modal .modal-body { min-width: 320px; }
.reader-server-status-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 14px 0 18px;
}
.reader-server-status-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--card-bg, transparent);
}
.reader-server-status-dot {
  flex: 0 0 14px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #9aa0a6;
  box-shadow: 0 0 0 3px rgba(154, 160, 166, 0.18);
}
.reader-server-status-dot.is-checking {
  background: #9aa0a6;
  animation: reader-server-status-pulse 1s infinite ease-in-out;
}
.reader-server-status-dot.is-ok {
  background: #22c55e;
  box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
}
.reader-server-status-dot.is-slow {
  background: #f59e0b;
  box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2);
}
.reader-server-status-dot.is-down {
  background: #ef4444;
  box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
}
@keyframes reader-server-status-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}
.reader-server-status-meta { flex: 1 1 auto; min-width: 0; }
.reader-server-status-label { font-size: 14px; font-weight: 600; }
.reader-server-status-host {
  font-size: 11px;
  color: var(--text-muted);
  margin-top: 1px;
  word-break: break-all;
}
.reader-server-status-state {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.reader-server-status-refresh { margin-bottom: 12px; }
.reader-server-status-note { font-size: 12px; line-height: 1.4; }

/* 나빌레라 이용자 통계 모달 — 폭은 dialog 에(표준 규칙). */
dialog.modal.reader-nabusers-modal { max-width: 640px; }
.reader-nabusers-modal .modal-body { min-width: min(560px, 92vw); }
.reader-nabusers-status { margin: 0 0 12px; }
.reader-nabusers-content { display: flex; flex-direction: column; }
.reader-nabusers-h4 { font-size: 14px; font-weight: 700; margin: 20px 0 10px; }
.reader-nabusers-range { margin: 8px 0 0; font-size: 12px; }
/* 요약 카드 */
.reader-nabusers-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.reader-nabusers-card {
  background: var(--surface-2, var(--surface)); border: 1px solid var(--border);
  border-radius: 12px; padding: 12px 10px; text-align: center;
}
.reader-nabusers-card-v { font-size: 18px; font-weight: 800; color: var(--accent); line-height: 1.2; }
.reader-nabusers-card-k { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
/* 가로 막대 (이용자별) */
.reader-nabusers-hbars { display: flex; flex-direction: column; gap: 7px; }
.reader-nabusers-hbar-row { display: flex; align-items: center; gap: 8px; }
.reader-nabusers-hbar-label {
  flex: 0 0 96px; width: 96px; font-size: 12px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.reader-nabusers-hbar-track { flex: 1 1 auto; height: 14px; background: var(--surface-2, rgba(127,127,127,0.15)); border-radius: 7px; overflow: hidden; }
.reader-nabusers-hbar-fill { display: block; height: 100%; border-radius: 7px; min-width: 2px; }
.reader-nabusers-hbar-val { flex: 0 0 auto; font-size: 12px; font-weight: 700; min-width: 22px; text-align: right; }
.reader-nabusers-more { margin: 8px 0 0; font-size: 12px; }
/* 주별 스택 막대 + 요일 막대 공통 컬럼 */
.reader-nabusers-weekchart, .reader-nabusers-dowchart {
  display: flex; align-items: flex-end; gap: 4px; overflow-x: auto;
  padding-bottom: 4px; min-height: 60px;
}
.reader-nabusers-dowchart { gap: 8px; justify-content: space-between; }
.reader-nabusers-week-col { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1 0 auto; }
.reader-nabusers-week-bar { width: 18px; display: flex; flex-direction: column-reverse; border-radius: 3px 3px 0 0; overflow: hidden; background: rgba(127,127,127,0.12); }
.reader-nabusers-week-seg { width: 100%; }
.reader-nabusers-week-lbl { font-size: 9px; color: var(--text-muted); white-space: nowrap; }
.reader-nabusers-dow-bar { width: 26px; background: var(--accent); border-radius: 4px 4px 0 0; }
.reader-nabusers-dow-val { font-size: 11px; font-weight: 700; color: var(--text-muted); }
.reader-nabusers-legend { display: flex; flex-wrap: wrap; gap: 8px 12px; margin-top: 10px; }
.reader-nabusers-leg { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; }
.reader-nabusers-leg-dot { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }

.reader-transfer-section + .reader-transfer-section { margin-top: 18px; }
.reader-transfer-section-title {
  margin: 0 0 8px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.reader-transfer-file-actions { display: flex; gap: 8px; flex-wrap: wrap; }
/* 전체 백업 / 복원 (v1.6.0 공개) */
.reader-backup-scope { display: flex; gap: 6px; margin: 10px 0; }
.reader-backup-scope-btn {
  flex: 1 1 0;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  cursor: pointer;
}
.reader-backup-scope-btn.is-active {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
  font-weight: 600;
}
.reader-backup-apikey {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
}
.reader-backup-apikey input { flex: none; }
.reader-copy-account-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 8px;
}
.reader-copy-account-row {
  width: 100%;
}
.reader-copy-account-row.disabled,
.reader-copy-account-row[disabled] {
  cursor: not-allowed;
  opacity: 0.6;
}
.reader-copy-account-row.disabled:hover,
.reader-copy-account-row[disabled]:hover {
  background: transparent;
}
.reader-copy-account-empty {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-faint);
}
/* × close pinned at the far top-right of the settings dialog and
 * its descendant modals (font / advanced / sidebar order). Plain
 * × glyph — no circle background, just a muted character that
 * deepens on hover. Sized to match the title row's vertical
 * rhythm. */
.reader-settings-close {
  flex: 0 0 auto;
  padding: 0 4px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.reader-settings-close:hover { color: var(--text); }

/* PWA hard-refresh 버튼 — 설정 모달 의 × 왼쪽 에 위치, standalone PWA
 * 일 때 만 보임 (JS 가 mount 결정).  같은 muted → hover color 패턴
 * 으로 × 와 한 줄 에 정렬.  is-spinning 동안 잠깐 회전 (사용자 가
 * 클릭 했음 을 시각 적 으 로 인 식). */
.reader-settings-header-actions {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
  margin-left: auto;
}
.reader-settings-refresh {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  transition: color 120ms;
}
.reader-settings-refresh:hover:not(:disabled) {
  color: var(--text);
  background: color-mix(in srgb, var(--text) 8%, transparent);
}
.reader-settings-refresh:disabled { opacity: 0.5; cursor: progress; }
.reader-settings-refresh.is-spinning svg {
  animation: reader-settings-refresh-spin 700ms linear infinite;
}
@keyframes reader-settings-refresh-spin {
  to { transform: rotate(360deg); }
}
/* Two-column basic-settings grid. Each column is its own flex
 * stack so an unbalanced row count (left has time / timezone /
 * align; right has font / theme / advanced) doesn't leave an
 * awkward empty cell. A 1px vertical divider sits between them
 * via the middle track — uses a thin gap + a border on the
 * first column rather than a separate element. */
.reader-settings-grid {
  display: grid;
  grid-template-columns: 1fr 1px 1fr;
  gap: 14px;
  margin-top: 8px;
}
.reader-settings-grid::before {
  content: '';
  grid-column: 2;
  grid-row: 1 / -1;
  background: var(--border);
  width: 1px;
  justify-self: center;
  align-self: stretch;
}
.reader-settings-col {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}
.reader-settings-grid > .reader-settings-col:first-of-type { grid-column: 1; }
.reader-settings-grid > .reader-settings-col:last-of-type { grid-column: 3; }
/* Two-column grid. Each column is its own flex stack so an unbalanced
 * count of rows on left vs right doesn't leave an awkward empty cell.
 * Left: font + align stacked. Right: realtime toggle. */
/* Base flex layout — used by every settings row regardless of
 * which section it lives in. Section-specific looks (pill capsule
 * for the main stack, flat rows for the advanced panel) layer on
 * top via more specific selectors below. */
.reader-settings-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.reader-settings-row[hidden] { display: none; }
/* 유리 굴절 토글 행 — 위 옵션(블러 슬라이더)과 같은 간격(12px). */
.reader-glass-refract-row { margin-top: 12px; }

/* Single-column stack of pill rows for the MAIN settings (글자 /
 * 정렬 / 시간 / 시간대). Each row sits in its own surface-2
 * capsule with the label on the left and the control on the
 * right, so label + control read as one grouped unit despite
 * hugging opposite ends. */
.reader-settings-stack {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 12px 0;
}
.reader-settings-stack > .reader-settings-row {
  justify-content: space-between;
  gap: 12px;
  padding: 8px 12px;
  background: var(--surface-2);
  border-radius: var(--radius);
}
/* Top-level settings dialog (post-reorg) — 5 category buttons each
 * sit on their own row, with the row constrained to half the modal
 * width and centered.  No surface-2 capsule on the row; the button
 * itself supplies the visual presence. */
.reader-settings-stack-top > .reader-settings-row {
  background: transparent;
  padding: 0;
  margin: 0 auto;
  width: min(280px, 70%);
}
.reader-settings-stack-top > .reader-settings-row > .btn,
.reader-settings-stack-top > .reader-settings-row > .reader-settings-action {
  width: 100%;
}
/* Soft-card category button — A안.  Applied to every "drill-in"
 * button across the 5 settings modals: 메인 설정 (stack-top),
 * 블루스카이 설정 (.reader-bluesky-settings-stack), 그리고 UI /
 * 부가기능 / 고급 (stack-flat).  Bigger padding, surface-2 fill,
 * left-aligned label, chevron ›  via ::after, hover 시 accent 색
 * 테두리. 버튼 DOM 은 단일 텍스트 노드 그대로 두고 chevron 만
 * pseudo 로 붙임. */
.reader-settings-stack-top > .reader-settings-row > .btn.reader-settings-action,
.reader-settings-stack-flat > .reader-settings-row > .btn.reader-settings-action,
.reader-bluesky-settings-stack > .btn {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  justify-content: space-between;
  text-align: left;
  padding: 14px 16px;
  font-size: 14px;
  font-weight: 500;
  border-radius: 12px;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}
.reader-settings-stack-top > .reader-settings-row > .btn.reader-settings-action:hover,
.reader-settings-stack-flat > .reader-settings-row > .btn.reader-settings-action:hover,
.reader-bluesky-settings-stack > .btn:hover {
  background: var(--surface-hover);
  border-color: var(--accent);
}
.reader-settings-stack-top > .reader-settings-row > .btn.reader-settings-action:active,
.reader-settings-stack-flat > .reader-settings-row > .btn.reader-settings-action:active,
.reader-bluesky-settings-stack > .btn:active {
  transform: scale(0.99);
}
.reader-settings-stack-top > .reader-settings-row > .btn.reader-settings-action::after,
.reader-settings-stack-flat > .reader-settings-row > .btn.reader-settings-action::after,
.reader-bluesky-settings-stack > .btn::after {
  content: '›';
  color: var(--text-muted);
  font-size: 16px;
  line-height: 1;
  flex: 0 0 auto;
  margin-left: 8px;
}
/* Per-category submodals (UI 설정 / 부가기능 설정).  Same half-width
 * centered layout, but rows may contain either a button or a label+
 * toggle pair.  We hide the .row-leader (dashed filler) so label and
 * toggle hug each other tightly per user request, and let the row
 * shrink to its content width (then centered via margin auto). */
.reader-settings-stack-flat > .reader-settings-row {
  background: transparent;
  padding: 0;
  margin: 0 auto;   /* 세로 0 — 행 간격 = stack 의 gap(6px)만 (메인 설정/블루
                       스카이 의 좁은 간격 과 일치).  auto 는 가로 가운데정렬 유지. */
  width: min(280px, 70%);
  justify-content: space-between;
  gap: 8px;
}
.reader-settings-stack-flat > .reader-settings-row > .row-leader {
  display: none;
}
/* Re-show the dashed —— filler for rows that opt in via
 * .reader-settings-row-leader (time / timezone / align rows in
 * UI 설정).  Row width matches the buttons below, and the gap on
 * either side of the leader is narrowed so the dashed line hugs
 * the label and control. */
.reader-settings-stack-flat > .reader-settings-row-leader {
  gap: 4px;
}
/* Override the global `.reader-settings-row > :first-child { flex: 1 }`
 * for leader rows: the label should hug its text so the dashed leader
 * begins right after the text (with only the 4 px flex gap between).
 * Otherwise the label box inflates to take half the row width and
 * pushes the dashes far away from "시간" / "정렬" / "타임존" text. */
.reader-settings-stack-flat > .reader-settings-row-leader > :first-child {
  flex: 0 0 auto;
}
.reader-settings-stack-flat > .reader-settings-row-leader > .row-leader {
  display: block;
  flex: 1 1 auto;
  min-width: 16px;
}
.reader-settings-stack-flat > .reader-settings-row > .btn,
.reader-settings-stack-flat > .reader-settings-row > .reader-settings-action {
  width: 100%;
}
/* "멀티 컬럼 폭" 숫자 인풋 — 우측 정렬 (leader 행 의 맨 오른쪽 control). */
.reader-multi-col-width-input {
  flex: 0 0 auto;
  width: 88px;
  text-align: right;
}
/* (Bluesky-stack centering override lives at the existing
 * .reader-bluesky-settings-stack rule further down so we don't
 * duplicate flex/gap definitions.) */
.reader-settings-stack > .reader-settings-row:has(> .inline-row:only-child) {
  justify-content: flex-start;
}

/* Advanced section keeps the older flat rows — checkbox + info
 * button stacked vertically with simple vertical margins, no
 * surface-2 background. */
.reader-settings-advanced-content .reader-settings-row {
  margin: 10px 0;
}
.reader-settings-advanced-content .reader-settings-row:first-child {
  margin-top: 4px;
}
.reader-settings-advanced-content .reader-settings-row:last-child {
  margin-bottom: 4px;
}
/* Advanced settings — single full-width section under the two-
 * column grid. The toggle is a borderless inline button with a
 * rotating caret; clicking expands the content underneath. */
.reader-settings-advanced { margin-top: 16px; }
.reader-settings-advanced-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  opacity: 0.85;
}
.reader-settings-advanced-toggle:hover {
  opacity: 1;
  background: var(--surface-hover);
  border-radius: 6px;
}
/* Suppress the browser's mouse-click focus ring — it lingers as a
 * 2px outline around '고급 설정' after the user clicks. Keyboard
 * tab still draws a focus-visible ring so a11y stays intact. */
.reader-settings-advanced-toggle:focus:not(:focus-visible) { outline: none; }
.reader-settings-advanced-toggle-caret {
  display: inline-block;
  transition: transform 0.15s ease;
}
.reader-settings-advanced-toggle.is-open .reader-settings-advanced-toggle-caret {
  transform: rotate(180deg);
}
.reader-settings-advanced-content {
  margin-top: 6px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
}
.reader-settings-advanced-content[hidden] { display: none; }
.reader-timezone-select {
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  /* Keep the dropdown from stretching wider than its longest option
   * — without this the row's first-child flex: 1 hands it the whole
   * column and the "시간대" label gets squeezed onto vertical
   * single-glyph stacks on narrow viewports. */
  max-width: 180px;
}
.reader-timezone-select:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
/* The setting-row label sits on the left flex: 1 slot, but a longer
 * control (the timezone select) can still squeeze it down. Keep the
 * label from line-wrapping onto vertical glyph stacks. */
.reader-settings-row > span:first-child { white-space: nowrap; }
/* First child of each row takes the available width so the control
 * (toggle / picker / button / info-btn) parks on the right. */
.reader-settings-row > :first-child {
  flex: 1;
}
.reader-settings-row > span:first-child {
  color: var(--text);
}
/* Segmented toggles in the settings dialog. Shared styling between
 * font-size and text-align. Active state uses accent-soft like the
 * feed tabs. */
.reader-fontsize-toggle,
.reader-textalign-toggle,
.reader-timedisplay-toggle {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.reader-fontsize-toggle button,
.reader-textalign-toggle button,
.reader-timedisplay-toggle button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  padding: 4px 10px;
  font: inherit;
  font-size: 13px;
  line-height: 1.2;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.reader-fontsize-toggle button + button,
.reader-textalign-toggle button + button,
.reader-timedisplay-toggle button + button {
  border-left: 1px solid var(--border);
}
/* Font-size buttons scale their own label so the tap target hints at
 * the resulting size. */
.reader-fontsize-toggle button[data-size="small"]  { font-size: 11px; }
.reader-fontsize-toggle button[data-size="medium"] { font-size: 13px; }
.reader-fontsize-toggle button[data-size="large"]  { font-size: 15px; }
/* Text-align toggle now ships SVG glyphs in place of "양쪽/왼쪽/오른쪽"
 * labels — center the icon in the tap target and trim horizontal
 * padding so three buttons fit comfortably next to the row label. */
.reader-textalign-toggle button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px 8px;
}
.reader-textalign-toggle button svg {
  display: block;
}
.reader-fontsize-toggle button.is-active,
.reader-textalign-toggle button.is-active,
.reader-timedisplay-toggle button.is-active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}
.reader-fontsize-toggle button:hover:not(.is-active),
.reader-textalign-toggle button:hover:not(.is-active) {
  background: var(--surface-hover);
  color: var(--text);
}

/* 컨텐츠 표시 sub-modal — adult content master toggle + per-label
 * visibility choices.  The choice pill (show / warn / hide) reuses
 * the same segmented-control look as .reader-textalign-toggle so it
 * sits visually with the rest of the settings inputs. */
.reader-content-vis-modal { max-width: 480px; }
.reader-content-vis-adult-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 8px;
  padding: 6px 0 8px;
}
.reader-content-vis-adult-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
}
.reader-content-vis-adult-label input[type="checkbox"]:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}
.reader-content-vis-adult-hint {
  width: 100%;
  margin: 0;
  font-size: 12px;
}
/* 단방향 sync 안내문 — 카테고리 행 들 밑에 살짝 떨어진 상태로
 * 띄워서 "이 설정 은 로컬" 메시지가 시선에 들어오게. */
.reader-content-vis-local-only-notice {
  margin: 14px 0 0;
  padding: 10px 12px;
  background: var(--surface-2);
  border-left: 3px solid var(--accent);
  border-radius: 4px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
}
.reader-content-vis-adult-hint a { color: var(--accent); }

/* ── Labelers (third-party subscription) modal ────────────────────
 * 한 모달에 두 가지 영역: 위쪽 = 현재 구독중인 라벨러 리스트 (행
 * 마다 avatar + name + handle + description + × 제거 버튼), 아래쪽
 * = 핸들/DID 입력 + "추가" 버튼.  기본 라벨러 (Bluesky moderation)
 * 는 첫 행 으로 항상 노출되고 × 대신 "기본 (제거 불가)" 태그. */
.reader-labelers-modal { max-width: 520px; }
.reader-labelers-modal .modal-body { gap: 8px; }
.reader-labelers-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 8px 0 4px;
  max-height: 50vh;
  overflow-y: auto;
}
.reader-labelers-empty { text-align: center; margin: 16px 0; }
.reader-labelers-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.reader-labelers-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--surface-hover);
}
.reader-labelers-avatar-fallback { background: var(--surface-hover); }
.reader-labelers-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.reader-labelers-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reader-labelers-handle {
  font-size: 12px;
  color: var(--text-faint);
}
.reader-labelers-desc {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.4;
  margin-top: 4px;
}
.reader-labelers-remove {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  border-radius: 50%;
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
}
.reader-labelers-remove:hover {
  background: var(--surface-hover);
  color: var(--danger);
}
.reader-labelers-default-tag {
  flex: 0 0 auto;
  padding: 4px 8px;
  font-size: 10px;
  color: var(--text-faint);
  background: var(--surface-2);
  border-radius: 999px;
  align-self: center;
}
.reader-labelers-add {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* ── Labeler profile modal — labels tab + feeds tab ────────────── */
.reader-labeler-label-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 6px;
}
.reader-labeler-label-info {
  flex: 1;
  min-width: 0;
}
.reader-labeler-label-titlerow {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}
.reader-labeler-label-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.reader-labeler-label-id {
  font-size: 11px;
  color: var(--text-faint);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.reader-labeler-label-pill { flex: 0 0 auto; }
.reader-labeler-labels-empty,
.reader-labeler-feeds-empty {
  margin: 24px 0;
  text-align: center;
}

.reader-labeler-feed-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 8px;
  cursor: pointer;
}
.reader-labeler-feed-row:hover { background: var(--surface-hover); }
.reader-labeler-feed-avatar {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--surface-hover);
}
.reader-labeler-feed-avatar-fallback { background: var(--surface-hover); }
.reader-labeler-feed-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.reader-labeler-feed-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.reader-labeler-feed-desc {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.4;
}
.reader-content-vis-labels {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
}
.reader-content-vis-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
}
.reader-content-vis-row-label {
  flex: 1 1 auto;
  min-width: 0;
}
.reader-content-vis-choice {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
  flex: 0 0 auto;
}
.reader-content-vis-choice-btn {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  padding: 4px 10px;
  font: inherit;
  font-size: 13px;
  line-height: 1.2;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.reader-content-vis-choice-btn + .reader-content-vis-choice-btn {
  border-left: 1px solid var(--border);
}
.reader-content-vis-choice-btn.is-active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}
.reader-content-vis-choice-btn:hover:not(.is-active) {
  background: var(--surface-hover);
  color: var(--text);
}

/* ── 내 글 상호작용 (threadgate / postgate) — 설정 + per-post ───
 * 답글 / 인용 제한 모달.  같은 buildInteractionEditor() factory 가
 * 설정-default 와 composer per-post 양쪽에서 재사용되므로 양쪽 모달
 * 모두 동일 스타일.  레이아웃: 라디오 6 + 리스트 picker (라디오와
 * 같은 행) + 인용 체크박스. */
.reader-interaction-modal { max-width: 480px; }
.reader-interaction-editor {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 4px 0;
}
.reader-interaction-section-title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.reader-interaction-radio-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.reader-interaction-radio-row {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 6px;
}
.reader-interaction-radio-row:hover {
  background: var(--surface-hover);
}
.reader-interaction-radio-row input[type="radio"]:disabled + .reader-interaction-radio-label {
  opacity: 0.5;
}
.reader-interaction-radio-label {
  flex: 0 0 auto;
}
/* Sub-group: 4 checkboxes shown when "일부만" radio is on.  Indented
 * under the radio so the visual hierarchy reads as "↳ these are
 * children of 일부만".  The list checkbox expands further into a
 * per-list multi-select. */
.reader-interaction-sub-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 2px 0 4px 24px;
  padding: 6px 0;
  border-left: 2px solid var(--border);
  padding-left: 12px;
}
.reader-interaction-sub-group[hidden] { display: none; }
.reader-interaction-sub-row {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 6px;
}
.reader-interaction-sub-row:hover {
  background: var(--surface-hover);
}
.reader-interaction-sub-row input[type="checkbox"]:disabled + .reader-interaction-sub-label {
  opacity: 0.5;
}
.reader-interaction-list-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 2px 0 0 22px;
  padding-left: 8px;
  border-left: 1px dashed var(--border);
}
.reader-interaction-list-group[hidden] { display: none; }
.reader-interaction-list-row {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 3px 6px;
  border-radius: 4px;
  font-size: 13px;
}
.reader-interaction-list-row:hover {
  background: var(--surface-hover);
}
.reader-interaction-list-empty {
  padding: 4px 6px;
  color: var(--text-faint);
  font-size: 12px;
  font-style: italic;
}
.reader-interaction-quote-row {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 6px 6px;
  border-top: 1px solid var(--border);
  margin-top: 6px;
  padding-top: 12px;
}
.reader-interaction-quote-label { font-weight: 500; }
.reader-interaction-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 12px;
}
.reader-interaction-status {
  margin: 4px 0 0;
  min-height: 1em;
}
.reader-interaction-status.ok { color: var(--success, #2ea043); }
.reader-interaction-status.err { color: var(--danger, #f85149); }
.reader-interaction-needs-session {
  margin: 16px 0;
  color: var(--text-faint);
  font-style: italic;
}

/* ── 라벨 가림 — warn (옵션 B, image-only blur) ────────────────
 * 카드 위에 라벨 strip 을 얹고 본문 텍스트는 그대로 보이되 카드
 * 안의 이미지 / 비디오 / 외부 링크 카드 / 인용 / feed embed 만
 * blur 처리.  공식 Bluesky 앱 동작과 동일.  사용자가 우측 "표시"
 * 버튼을 누르면 .is-revealed 가 붙어 blur 가 풀리고 strip 의
 * 버튼은 "가리기" 로 토글 — 라벨 strip 자체는 reveal 후에도 그
 * 대로 표시되어 사용자가 어떤 라벨이 붙은 카드인지 계속 알 수
 * 있다.  reveal 상태는 모듈 scope 의 revealedLabelUris Set 에
 * 저장되므로 카드 재페인트 (sibling 이동 / 모달 / 스크롤) 시
 * 그대로 유지된다. */
.reader-label-warn { position: relative; overflow: hidden; }
.reader-label-warn .reader-label-warn-strip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: color-mix(in srgb, var(--danger, #f85149) 14%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--danger, #f85149) 35%, transparent);
  margin: -14px -16px 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--danger, #f85149);
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.reader-label-warn .reader-label-warn-strip-label {
  /* Sized to its own text so the reveal-toggle button sits
   * immediately to its right.  Shrink (flex-shrink: 1) is allowed
   * for very narrow viewports so the label ellipsises instead of
   * pushing the button off-row.  flex-grow stays 0 so the label
   * never stretches and creates a gap between itself and the button. */
  flex: 0 1 auto;
  min-width: 0;
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}
.reader-label-warn .reader-label-warn-show {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--accent);
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  padding: 0 2px;
  flex: 0 0 auto;
}
/* Content wrap inherits the original .reader-card flex column +
 * gap so head / body / actions stay spaced apart by 10px just like
 * an un-labeled card.  Without this, wrapping every child under a
 * plain <div> collapsed those rows together (visible regression:
 * label warn cards looked squished compared to plain cards). */
.reader-label-warn .reader-label-warn-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* Image visibility — text content stays readable; images / video /
 * link-cards / quoted embeds / feed embeds are display:none'd
 * outright (per user request: not just blurred, gone) until reveal.
 * When .is-revealed flips on, the selector stops matching and the
 * children paint normally. */
.reader-label-warn:not(.is-revealed) .reader-label-warn-content :is(
  .reader-images,
  .reader-image,
  .reader-image-placeholder,
  .reader-video,
  .reader-video-stage,
  .reader-link-card,
  .reader-feed-embed-card
) {
  display: none !important;
}
/* Revealed state — strip stays (so the label remains visible), but
 * dialed back to a calmer muted tone since the content is now
 * opted-in. */
.reader-label-warn.is-revealed .reader-label-warn-strip {
  background: color-mix(in srgb, var(--text-muted) 12%, transparent);
  border-bottom-color: var(--border);
  color: var(--text-muted);
}

/* ── severity variants ───────────────────────────────────────────────
 * labelValueDefinitions.severity from the publishing labeler.  The
 * default (no class) is the alert tone we shipped originally.  Inform
 * dials back to a muted grey strip — the label is information, not
 * a warning.  None means the labeler intentionally wants no chrome
 * weight; we drop the strip background entirely (the text still
 * names the label so it isn't invisible). */
.reader-label-warn.reader-label-severity-inform .reader-label-warn-strip {
  background: color-mix(in srgb, var(--text-muted) 14%, transparent);
  border-bottom-color: color-mix(in srgb, var(--text-muted) 35%, transparent);
  color: var(--text-muted);
}
.reader-label-warn.reader-label-severity-none .reader-label-warn-strip {
  background: transparent;
  border-bottom-color: var(--border);
  color: var(--text-muted);
}

/* ── blurs variants ──────────────────────────────────────────────────
 * Default (no class) is the existing media-hide behaviour: only
 * .reader-image / .reader-video / .reader-link-card / .reader-feed-
 * embed-card descendants are display:none-d until reveal.
 * content variant: ALSO blurs the surrounding text so the strip is
 *   the only readable thing.  We use filter:blur on the content
 *   wrap and re-enable the media descendants (otherwise display:
 *   none takes them out of layout, and the blurred backdrop has
 *   nothing to convey).
 * none variant: no hiding at all — labeler wants the strip as a
 *   badge only.  Override the default media-hide back to revert. */
.reader-label-warn.reader-label-blur-content:not(.is-revealed) .reader-label-warn-content {
  filter: blur(8px);
  pointer-events: none;
  user-select: none;
}
.reader-label-warn.reader-label-blur-content:not(.is-revealed) .reader-label-warn-content :is(
  .reader-images,
  .reader-image,
  .reader-image-placeholder,
  .reader-video,
  .reader-video-stage,
  .reader-link-card,
  .reader-feed-embed-card
) {
  display: revert !important;
}
.reader-label-warn.reader-label-blur-none:not(.is-revealed) .reader-label-warn-content :is(
  .reader-images,
  .reader-image,
  .reader-image-placeholder,
  .reader-video,
  .reader-video-stage,
  .reader-link-card,
  .reader-feed-embed-card
) {
  display: revert !important;
}
/* Watermark (↪ for self-replies, ✓ for verified-quote, etc.) is
 * normally anchored to the card's top-right via .reader-card-
 * watermark { top: 14px; right: 16px; }.  When a label strip sits
 * along the top of the card, the watermark would otherwise paint
 * INSIDE that strip — visually colliding with the "표시" button
 * and the label text.  Push it down by the strip's visual height
 * (≈36px) so it lands in the card's body area instead, lined up
 * with where the watermark sits on a non-labeled card. */
.reader-card.reader-label-warn.has-watermark .reader-card-watermark {
  top: 50px;
}
/* Quick-actions row: language picker + theme toggle + wide brand
 * link, all side-by-side on a single line. The brand link uses
 * flex: 1 so it absorbs the leftover width. */
.reader-settings-quickrow {
  display: flex;
  align-items: stretch;
  gap: 8px;
  margin: 12px 0;
}
.reader-settings-quickrow > .lang-picker,
.reader-settings-quickrow > .theme-btn,
.reader-settings-quickrow > .reader-bug-report-btn {
  flex: 0 0 auto;
}
/* Brand link — tap to go back to the main tools landing. Sized to
 * fill the row, surface-2 background, hover lift. Replaces the
 * global header's brand + tool-picker on reader. */
.reader-settings-brand {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 0 12px;
  min-height: 32px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text);
  font-weight: 600;
  text-decoration: none;
  transition: background 0.15s, border-color 0.15s;
  white-space: nowrap;
}
.reader-settings-brand:hover {
  background: var(--surface-hover);
  border-color: var(--border-strong);
  text-decoration: none;
}
.reader-settings-brand .brand-emoji {
  font-size: 16px;
  line-height: 1;
}
/* Random hint line shown below the quickrow. Tiny, muted, centred —
 * a passing suggestion rather than a permanent label. Inline chips
 * keep their RP-badge colouring (light=pink, dark=maroon), but the
 * pointer cursor is neutralised since the chip here isn't clickable. */
.reader-settings-hint {
  margin: 14px 0 0;
  font-size: 11px;
  color: var(--text-muted);
  text-align: center;
  line-height: 1.5;
}
.reader-settings-hint .reader-rp-prefix.is-active {
  cursor: default;
  font-size: 11px;
  padding: 0 4px;
}
/* ⓘ next to specific hints — opens a per-hint detail modal. Sits
 * inline with the hint text, picks up the accent color, no button
 * chrome so it reads as a soft inline glyph rather than a CTA. */
.reader-hint-info {
  display: inline-block;
  margin-left: 2px;
  padding: 0 2px;
  border: 0;
  background: none;
  color: var(--accent);
  cursor: pointer;
  font: inherit;
  line-height: 1;
  vertical-align: baseline;
  transition: opacity 0.15s ease;
}
.reader-hint-info:hover { opacity: 0.7; }
.reader-hint-info:focus,
.reader-hint-info:focus-visible { outline: none; opacity: 0.7; }
/* In the all-hints modal the items run at 12px, so let the ⓘ
 * inherit that size (already does via `font: inherit`) and add a
 * little breathing room since hints there sit on their own lines. */
.reader-all-hints-list .reader-hint-info {
  margin-left: 4px;
}
/* The 💡 in the settings hint is the trigger for the all-hints
 * easter-egg modal — press and hold for 3 s. cursor: pointer + a
 * subtle hover bump invite exploration; the .is-pressing animation
 * gives clear visual feedback that the hold is registering. The
 * callout / selection suppressions are mobile-side: long-press on
 * iOS would otherwise pop the copy / share sheet right over the
 * bulb. */
.reader-hint-bulb {
  display: inline-block;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  transform-origin: center center;
  transition: transform 0.18s ease;
  vertical-align: baseline;
}
.reader-hint-bulb:hover { transform: scale(1.12); }
.reader-hint-bulb:focus,
.reader-hint-bulb:focus-visible { outline: none; }
.reader-hint-bulb.is-pressing {
  animation: hint-bulb-charge 3s ease-in forwards;
}
@keyframes hint-bulb-charge {
  0%   { transform: scale(1) rotate(0deg); filter: none; }
  20%  { transform: scale(1.1) rotate(-6deg); }
  40%  { transform: scale(1.18) rotate(6deg);
         filter: drop-shadow(0 0 4px rgba(255, 196, 0, 0.45)); }
  60%  { transform: scale(1.25) rotate(-4deg);
         filter: drop-shadow(0 0 8px rgba(255, 196, 0, 0.65)); }
  80%  { transform: scale(1.32) rotate(4deg);
         filter: drop-shadow(0 0 12px rgba(255, 196, 0, 0.8)); }
  100% { transform: scale(1.45) rotate(0deg);
         filter: drop-shadow(0 0 16px rgba(255, 196, 0, 0.95)); }
}
@media (prefers-reduced-motion: reduce) {
  .reader-hint-bulb:hover { transform: none; }
  .reader-hint-bulb.is-pressing { animation: none; }
}
/* Tagline — 조지훈 「승무」 마지막 행 차용. 브랜드(나빌레라)의 어원이라
 * 설정창 마지막에 시 한 줄처럼 떨어뜨려 둔다. Diphylleia 한글 세리프 +
 * 가운데 정렬, opacity로 차분히 가라앉힘. 위쪽 옅은 구분선이 hint과
 * 시각적으로 분리해 주고, 바로 아래 credits는 자체 구분선을 해제해서
 * tagline과 한 묶음으로 흐름. */
.reader-settings-tagline {
  margin: 18px 0 0;
  padding: 14px 12px 0;
  border-top: 1px solid var(--border);
  font-family: 'Diphylleia', 'Noto Serif KR', serif;
  font-size: 1.05rem;
  text-align: center;
  line-height: 1.6;
  letter-spacing: 0.02em;
  color: var(--text);
  opacity: 0.88;
  /* EN's tagline is a couplet with an embedded \n; pre-line preserves
   * that line break while still collapsing soft wraps. KO / JA stay
   * single-line so this is a no-op for them. */
  white-space: pre-line;
}
/* KO 한정 강조 — "나빌레라" 단어가 액센트 컬러로 살짝 굵게 + 천천히
 * 위아래로 펄럭이며 미세하게 회전. 브랜드 어원이 "나비처럼 펄럭이는구나"
 * 라서 단어 의미를 그대로 시각화. 진폭은 3px 내외로 작아서 읽기를 방해
 * 하지 않고, 4초 주기 ease-in-out로 한 사이클 안에 정점 + 양쪽 기울임을
 * 다 잡는다. reduced-motion 환경에선 정지(컬러 + 굵기만 유지). */
.reader-settings-tagline-brand {
  display: inline-block;
  color: var(--accent);
  font-weight: 500;
  animation: tagline-flutter 4s ease-in-out infinite;
  transform-origin: center bottom;
  will-change: transform;
  cursor: pointer;
  user-select: none;
}
@keyframes tagline-flutter {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  25% { transform: translateY(-2px) rotate(-0.8deg); }
  50% { transform: translateY(-3px) rotate(0deg); }
  75% { transform: translateY(-2px) rotate(0.8deg); }
}
/* Easter-egg click animations. Each is 5s total: ~2s out, 1s
 * invisible hold, ~2s back in. JS picks one of four classes at
 * random per click (fade / fly / scatter / wings); in-flight clicks
 * are ignored. Wings only fires when there are ≥2 graphemes (1-char
 * brands fall back to fade).
 *
 * The brand span is split into per-grapheme child spans
 * (.tagline-brand-char) so scatter and wings can move each glyph
 * independently. Fade + fly act on the parent and let the children
 * inherit. */
.tagline-brand-char { display: inline-block; }
.wing-group { display: inline-block; }
.reader-settings-tagline-brand.is-flying-fade {
  animation: tagline-brand-fade 5s ease-in-out forwards;
}
.reader-settings-tagline-brand.is-flying-fly {
  animation: tagline-brand-fly 5s ease-in-out forwards;
}
.reader-settings-tagline-brand.is-flying-scatter .tagline-brand-char {
  animation: tagline-brand-scatter 5s ease-in-out forwards;
}
.reader-settings-tagline-brand.is-flying-wings { perspective: 240px; }
.reader-settings-tagline-brand.is-flying-wings .wing-group.wing-left {
  transform-origin: right center;
  animation: tagline-brand-wing-left 2.5s ease-in-out 2;
}
.reader-settings-tagline-brand.is-flying-wings .wing-group.wing-right {
  transform-origin: left center;
  animation: tagline-brand-wing-right 2.5s ease-in-out 2;
}
@keyframes tagline-brand-fade {
  0%   { opacity: 1; }
  40%  { opacity: 0; }
  60%  { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes tagline-brand-fly {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  40%  { transform: translate(220px, -140px) rotate(25deg); opacity: 0; }
  60%  { transform: translate(220px, -140px) rotate(25deg); opacity: 0; }
  100% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
}
@keyframes tagline-brand-scatter {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  40%  {
    transform: translate(var(--scatter-x, 0), var(--scatter-y, 0)) rotate(var(--scatter-rot, 0deg));
    opacity: 0;
  }
  60%  {
    transform: translate(var(--scatter-x, 0), var(--scatter-y, 0)) rotate(var(--scatter-rot, 0deg));
    opacity: 0;
  }
  100% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
}
@keyframes tagline-brand-wing-left {
  0%, 100% { transform: rotateY(0deg); }
  50% { transform: rotateY(-70deg); }
}
@keyframes tagline-brand-wing-right {
  0%, 100% { transform: rotateY(0deg); }
  50% { transform: rotateY(70deg); }
}
@media (prefers-reduced-motion: reduce) {
  .reader-settings-tagline-brand { animation: none; }
  /* All random picks collapse to the gentle fade — no translate /
   * rotate / spin / 3D flap for users who opted out of motion. */
  .reader-settings-tagline-brand.is-flying-fade,
  .reader-settings-tagline-brand.is-flying-fly,
  .reader-settings-tagline-brand.is-flying-scatter,
  .reader-settings-tagline-brand.is-flying-wings {
    animation: tagline-brand-fade 5s ease-in-out forwards;
  }
  .reader-settings-tagline-brand.is-flying-scatter .tagline-brand-char,
  .reader-settings-tagline-brand.is-flying-wings .wing-group.wing-left,
  .reader-settings-tagline-brand.is-flying-wings .wing-group.wing-right {
    animation: none;
  }
}
/* Credits — same lines as the global footer (hidden on reader),
 * re-hosted inside the settings dialog so the privacy / attribution
 * info is still reachable. Small + muted so they sit under the
 * primary action without competing. */
.reader-settings-credits {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  font-size: 11px;
  color: var(--text-faint);
  text-align: center;
}
/* When the tagline sits directly above credits, drop credits' own
 * top border so the two read as a single closing block. */
.reader-settings-tagline + .reader-settings-credits {
  margin-top: 10px;
  padding-top: 0;
  border-top: none;
}
.reader-settings-credits .foot-line { margin: 0; }
.reader-settings-credits .foot-line + .foot-line { margin-top: 4px; }
.reader-settings-credits a { color: var(--text-muted); }
.reader-settings-credits a:hover { color: var(--accent); }
/* 블스리더 라우트에 있을 때는 전역 푸터를 숨겨 사이드바 하단 버튼이
 * 푸터에 가리지 않도록. 토글은 app.js의 render()에서. */
.reader-active .site-footer { display: none; }
/* The global header is also hidden on reader — its language / theme /
 * account / brand widgets are re-hosted inside the reader's own
 * settings dialog and sidebar bottom rail. */
.reader-active .site-header { display: none; }

/* iOS keyboard handling. The viewport meta's
 * `interactive-widget=resizes-content` (index.html) makes the layout
 * viewport SHRINK when the soft keyboard opens (iOS 17.4+, Chrome
 * 108+). Combined with 100dvh on body and overflow:hidden on html, the
 * page tracks the visible area natively — no need to pin body with
 * position:fixed (that pin used to conflict with iOS's dialog
 * scroll-into-view, leaving the compose modal scrolled past the
 * visible top with a stray empty area at the bottom). overscroll-
 * behavior:none kills the rubber-band that could trigger document-
 * level scroll. */
html.reader-active,
html.reader-active body {
  overscroll-behavior: none;
}
html.reader-active body {
  height: 100vh;   /* fallback */
  height: 100dvh;
}

/* Brief 250 ms opacity fade for every <img> the reader paints —
 * avatars, attached post media, link-card thumbs, video stills,
 * lightbox image, all of it. fadeInImg() in reader.js adds
 * .reader-fade-in at construction and .is-loaded once the load
 * event fires (or, for cached imgs, on the next frame so the
 * animation has a chance to play). Reduced-motion users skip
 * the animation entirely and see the image at opacity 1
 * immediately. */
img.reader-fade-in {
  opacity: 0;
  /* Transition rather than CSS animation so that re-parenting the
   * img (the thread modal does this when moving a card between
   * slots) does NOT replay the fade — Safari re-runs CSS animations
   * on DOM-move, which the user perceived as an avatar / image
   * blink right after every swipe.  Transition only fires when the
   * opacity VALUE actually changes, so a move keeps the img stable
   * at opacity 1. */
  transition: opacity 250ms ease-out;
}
img.reader-fade-in.is-loaded { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  img.reader-fade-in,
  img.reader-fade-in.is-loaded {
    opacity: 1;
    transition: none;
  }
}

/* Initial-load splash for the reader. Full-viewport overlay painted
 * in a HARDCODED background + foreground palette so the brand
 * presentation stays identical across every theme variant — light /
 * dark switch, custom slots, base overrides, all bypassed. Per the
 * user's standing rule: the splash's colours, text, font, and
 * position are fixed; nothing in the settings UI should ever move
 * the splash. JS removes the element shortly after the animation
 * finishes. Reduced-motion users skip the whole thing — the JS
 * bails before injecting the splash. */
.reader-splash {
  position: fixed;
  inset: 0;
  z-index: 9999;
  /* HARDCODED — must NOT use var(--bg) (custom slots override it).
   * Light/dark variants are split with a [data-theme="dark"] override
   * below so the splash still respects the user's base theme choice
   * but ignores any custom-slot colour overrides. */
  background: #f8fafc;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
  animation: reader-splash-vanish 500ms ease-in 1500ms forwards;
  /* Splash text is a brand surface — long-pressing it on iOS would
   * normally turn the "나빌레라" / version chip into a text-selection
   * highlight (blue underline + magnifier callout) which sticks
   * around for the rest of the splash animation.  Belt-and-braces:
   * disable selection AND the iOS context-menu callout on the
   * splash and its inner pieces. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
[data-theme="dark"] .reader-splash { background: #0b1220; }
.reader-splash-version {
  position: absolute;
  /* Position history:
   *   v1: bottom: 14px (hard-against-bottom).
   *   v2: bottom: 24% (overshot — too close to the brand row).
   *   v3: midpoint(14px, 24%) = calc(12% + 7px).
   *   v4 (this rule): midpoint(v3, v1) = calc(6% + 10.5px).
   * Each iteration moves the chip ~halfway back toward the bottom
   * edge per the user's tuning. */
  bottom: calc(6% + 10.5px);
  left: 50%;
  transform: translateX(-50%);
  font-size: 10px;
  /* HARDCODED — must NOT use var(--text-faint). Dark variant below. */
  color: #94a3b8;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  opacity: 0.7;
  letter-spacing: 0;
  pointer-events: none;
  /* Fade together with the brand text + subtitle so the version
   * chip doesn't linger after the words it sits below have already
   * dissolved. Same keyframes / timing as .reader-splash-text. */
  animation: reader-splash-text-vanish 400ms ease-out 1100ms forwards;
}
/* Tool-hub variant pins its brand text at full opacity (the whole
 * overlay just fades). Match that: leave the version chip steady
 * too — the overlay's `reader-splash-vanish` carries it out. */
.reader-splash-single .reader-splash-version {
  animation: none;
}
/* Tool-hub variant — single-step fade. Brand text holds full
 * opacity all the way through; the overlay starts dissolving at
 * the moment the two-step reader splash would have started its
 * inner text fade (~1100ms in), so the timing parity feels
 * consistent with the reader splash even though only one fade
 * fires. Tool-hub splash runs at 2× speed: half the hold +
 * half the fade so it gets out of the way faster on a route the
 * user is just passing through. */
.reader-splash-single {
  animation: reader-splash-vanish 250ms ease-in 550ms forwards;
}
.reader-splash-single .reader-splash-text {
  animation: none;
}
.reader-splash-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
/* Frozen variant — used when an anon visitor lands directly on
 * #/nabilera.  Disables the vanish animation so the brand surface
 * persists as a backdrop while the inner login form takes the
 * stage.  Switches the splash itself to a flex-column layout so
 * the brand text + login form can stack vertically (the default
 * splash uses centered row layout for the bare brand stack).
 * Per the splash rules (CLAUDE.md): colour / font / size hardcodes
 * unchanged — only structural rules added here. */
.reader-splash.reader-splash-frozen {
  animation: none;
  flex-direction: column;
  gap: 24px;
  /* The login form is below the brand text, not centered with it,
   * so pull the whole stack up a notch so the form doesn't get
   * cropped on short viewports. */
  padding-top: 12vh;
  padding-bottom: 12vh;
  /* Long content (login form + error banners) on a small phone
   * could overflow the viewport — let the page scroll inside the
   * splash rather than clip the form. */
  overflow-y: auto;
  align-items: center;
  justify-content: flex-start;
  /* Sits ABOVE the standard client splash (z:9999) during the
   * cross-fade so .is-fading reveals the client splash underneath
   * rather than just disappearing into the reader. */
  z-index: 10000;
  /* Cross-fade out on login: opacity-driven transition complements
   * the client splash that gets layered behind us. */
  transition: opacity 400ms ease-out;
}
/* Children of the frozen splash inherit the 1100ms text-vanish
 * animation by default — that would fade the brand + subtitle +
 * version chip ~1.5s into the page even though there's no user
 * interaction yet. Disable per-child animations on the frozen
 * variant so the brand surface stays steady until login fires
 * the cross-fade. */
.reader-splash.reader-splash-frozen .reader-splash-text,
.reader-splash.reader-splash-frozen .reader-splash-sub,
.reader-splash.reader-splash-frozen .reader-splash-version {
  animation: none;
}
.reader-splash.reader-splash-frozen.is-fading {
  opacity: 0;
  pointer-events: none;
}
.reader-splash-form {
  width: min(360px, calc(100% - 32px));
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* Inputs inside the login form should be touch-friendly + read
 * cleanly on both light and dark splash backgrounds.  The base
 * .input class already handles colours via var(--surface) etc.,
 * which the splash respects (only brand-specific hardcodes are
 * locked).  No overrides needed beyond the layout cap above. */
.reader-splash-text {
  font-family: 'Diphylleia', 'Noto Serif KR', serif;
  /* HARDCODED — must NOT use clamp() / vw units. The splash is a
   * brand surface that stays IDENTICAL across every device and every
   * trigger (cold launch / account switch / refresh). Earlier this
   * was clamp(36px, 8vw, 64px) which varied by viewport AND between
   * paints — account switch could trip a different measurement
   * window and the title would grow past the viewport edge. Locking
   * to a single fixed px size kills both variances. */
  font-size: 48px;
  font-weight: 500;
  max-width: 90vw;
  white-space: nowrap;
  /* line-height: 1.2 ≈ browser default. Earlier tried 1.0 to land
   * the gradient's 100% stop at the glyph bottom — but Korean
   * glyphs with bottom-jamo descenders (e.g. ㄹ in "빌") extend
   * BELOW the em-box, so a 1.0 line-box clipped them away to
   * transparent (background-clip: text only paints inside the
   * box).  Using 1.2 gives descenders room; the gradient's
   * deepest-stop is pulled in to ~88% (≈ glyph descender bottom)
   * so the visible darkest / lightest tone still lands ON the
   * glyph and not below it. */
  line-height: 1.2;
  /* HARDCODED — must NOT use var(--accent). Dark variant below.
   * `color` is a fallback for the rare browser without background-
   * clip: text support; the visible paint comes from the gradient
   * + -webkit-text-fill-color: transparent.
   *
   * Light theme: gradient runs solid brand blue down to the 50%
   * midline, then BRIGHTENS to a pale sky blue toward the bottom
   * — the text "fades toward" the light background.
   * Dark theme (below): runs solid bright blue down to the 50%
   * midline, then DARKENS to a deep navy toward the bottom — the
   * text "fades toward" the dark background.
   * The two variants are mirrored so each theme fades into its
   * own bg in the same direction (top contrasting, bottom muted).
   *
   * Opacity-based vanish animation still works since opacity is
   * a separate channel from the painted gradient. */
  color: #0085ff;
  background: linear-gradient(
    to bottom,
    #0085ff 0%,
    #0085ff 50%,
    #99d5ff 88%,
    #99d5ff 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: reader-splash-text-vanish 400ms ease-out 1100ms forwards;
}
[data-theme="dark"] .reader-splash-text {
  color: #4aa8ff;
  background: linear-gradient(
    to bottom,
    #4aa8ff 0%,
    #4aa8ff 50%,
    #1a4d8c 88%,
    #1a4d8c 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
[data-theme="dark"] .reader-splash-version { color: #64748b; }
/* Subtitle "Bluesky Web Client" — same font family + same fade as
 * the brand row. Starts at an arbitrary base size; JS rewrites the
 * font-size after mount so the rendered width matches the brand.
 * white-space: nowrap guarantees the line never breaks mid-phrase
 * even if the web font is still loading when JS measures (which
 * can flip the natural width and push the upscaled glyph past
 * the brand row). */
.reader-splash-sub {
  font-family: 'Diphylleia', 'Noto Serif KR', serif;
  font-weight: 500;
  /* HARDCODED — must NOT use var(--text). Dark variant below. */
  color: #0f172a;
  font-size: 16px;
  line-height: 1;
  white-space: nowrap;
  animation: reader-splash-text-vanish 400ms ease-out 1100ms forwards;
}
[data-theme="dark"] .reader-splash-sub { color: #e2e8f0; }
@keyframes reader-splash-text-vanish {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes reader-splash-vanish {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* Reader: tighter top/bottom padding so the timeline sits close under
 * the global header, and a smaller right padding so the card's right
 * edge sits the same ~11px from the viewport as the sidebar buttons
 * sit from the left (the sidebar buttons overflow the 24px sidebar
 * by 5px, so the left visual gap is container_padding_left − 5 = 11).
 * Both visual gaps now match the sidebar-to-card gap (shell gap 16 −
 * button overflow 5 = 11). */
.reader-active .container {
  padding: 12px 11px 12px 16px;
}
/* ── Multi-feed shell (sidebar + main) ──────────────────────────── */
.reader-shell {
  display: flex;
  align-items: flex-start;
  gap: 16px;
}
/* 사용자 보고 2026-06-03 : 데스크톱 단일 컬럼 에서 사이드바 버튼(.reader-
 * sidebar-btn = 34px, 24px spine 에 margin-left:-5 로 센터)이 shell gap(16px)
 * 안으로 오른쪽 5px 침범 → 피드와의 실제 간격이 11px 로 좁게 보였음.  그
 * overhang(5px)만큼 gap 을 키워 버튼 ↔ 피드 간격을 의도한 16px 로 복원.
 * ※ 다른 환경은 안 건드림 : 모바일(≤1024)·멀티컬럼(reader-has-passive-cols)·
 * 월루(data-woolu, 더 높은 specificity 로 자체 override) 제외, 데스크톱
 * 단일 컬럼(min-width:1025 + passive 없음)에만 적용. */
@media (min-width: 1025px) {
  body:not(.reader-has-passive-cols) .reader-shell { gap: 21px; }
}
.reader-main {
  flex: 1;
  min-width: 0;
  /* 멀티컬럼 액션 버튼(위로 가기) per-column 절대 위치 기준. */
  position: relative;
}

/* ── 멀티 컬럼 (v1.3.0 ready 승격 — 모든 로그인 유저) ──────────────
 * 액티브 (.reader-main) + 패시브 (.reader-column) 컬럼들을 좌→우로 나열.
 * 패시브가 0 개 일 때 (1.2.x 이전 동작) 는 시각적으로 변화 없음.
 *
 * 데스크탑 너비 분배 (사용자 spec 2026-05-27 갱신):
 *   • data-cols="1" — 액티브만, 기본 너비 (변화 없음).
 *   • data-cols="2" — 액티브 + 1 패시브 = 균등 2 등 분 (flex:1).
 *   • data-cols >= 3 — 모바일 너비 360 px 고정 + 가로 스크롤.  컬럼 수
 *     × 360 이 wrap 너비 보다 커지면 스크롤이 발동.
 *
 * 모바일 (<900px) — 각 컬럼 100% viewport + scroll-snap 좌우 스와이프.
 *
 * 스크롤 격리 (사용자 보고 2026-05-27) — 액티브 컬럼 스크롤 시 패시브 컬
 * 럼 도 같이 스크롤 되는 버그 방지 :
 *   • .reader-columns.has-passive-columns 는 자체 max-height + overflow:
 *     auto (가로) 만, 세로 는 hidden.
 *   • 모든 자식 컬럼 (.reader-main + .reader-column) 이 자체 세로 scroll
 *     container — column 내부 에서 만 세로 스크롤, body / 다른 컬럼 영향
 *     없음. */
.reader-columns {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.reader-columns.has-passive-columns {
  flex-direction: row;
  align-items: stretch;
  gap: 12px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  /* 사용자 보고 2026-05-27 : column gap (12px) 영역 잡 고 상하 swipe 시
   * page 가 배너 높이 만큼 왔다 갔다.  원인 : wrap height calc(100vh -
   * 32px) 이 <main> element 의 실제 height (header / footer 제외) 보다
   * 큼 → <main> 자체 의 overflow-y auto 가 scroll 발동.
   * Fix : wrap height = 100% (= parent shell = container = main inner
   * content height).  main 자체 scroll 안 발동. */
  height: 100%;
  max-height: 100%;
}
/* multi-column 모드 활성 시 container 와 그 부모 chain 의 height 도 정의
 * — wrap 의 height: 100% 가 viewport 까지 합 산 가능 하도록.
 * 사용자 spec 2026-05-27 (7 차) : body.reader-has-passive-cols 도 같은
 * height chain — 1 패시브 컬럼 만 있 어 도 column-content sticky 작동.
 * reader-multi-active 는 옛 3+ cols 의 dynamic container max-width 전용 으
 * 로 유지.  has-passive-cols 가 부분 집합 (1+ 패시브 모두) 이므 로 multi-
 * active 케이스 도 동시 매칭. */
body.reader-multi-active #view.container,
body.reader-multi-active .reader-shell,
body.reader-has-passive-cols #view.container,
body.reader-has-passive-cols .reader-shell {
  height: 100%;
}
/* 사용자 보고 2026-05-28 : 멀티컬럼 (액티브 포함 2 cols 이상) 에 서 피드
 * 의 세로 가시 영역 이 container 의 상하 padding (.reader-active 의 12px)
 * 만큼 줄 어, 상단 12px / 하단 12px (= 사이드바 맨 위/맨 아래 버튼 사이)
 * 안 으 로 박스 처럼 갇 히 던 버그.  액티브 단독 모드 는 main(viewport)
 * 자체 가 스크롤 → 피드 가 viewport 전체 높이 를 사용 하 지 만, 멀티컬럼
 * 은 각 컬럼 이 height:100% (= container content box = viewport - 상하
 * padding) 로 고정 + 내부 스크롤 이 라 그 12px dead-zone 이 영구적.
 * fix : 멀티컬럼 일 때 container 상하 padding 을 0 으 로 → 컬럼 이
 * viewport 전체 높이 (0→100%) 차지.  좌우 padding 은 사이드바 정렬 위 해
 * 유지 (3+ cols 의 7144 rule + 기본 11/16 그 대 로).  데스크톱/모바일
 * 공통 — 두 케이스 다 같 은 height chain 을 타 므 로 동일 하 게 적용. */
body.reader-multi-active #view.container,
body.reader-has-passive-cols #view.container {
  padding-top: 0;
  padding-bottom: 0;
}
/* 컬럼 너비 (사용자 spec 2026-05-27 재 정리):
 *   • data-cols="1" — as-is.  .container max-width 960, .reader-main flex:1
 *     가득 (1.2.x 와 동일).
 *   • data-cols="2" — .container max-width 960 유지.  active + passive 가
 *     flex:1 1 0 으로 균등 2 등 분.
 *   • data-cols >= 3 — 모든 컬럼 360 px 고정.  .container max-width 를 동
 *     적 으로 set (= sidebar + 컬럼들 + paddings 의 합), viewport 보다 크
 *     면 100vw 로 cap.  컬럼 추가 시 container 가 점진 확장, viewport 도
 *     달 시 wrap 의 overflow-x: auto 가 좌우 스크롤 발동.  --reader-required
 *     -w 는 JS (updateColumnsLayoutMeta) 가 inline style 로 set. */
:root {
  --reader-passive-col-w: 360px;
}
/* 3+ cols 모드 — body.reader-multi-active 가 .container max-width 를
 * 동적 으로 확장.  --reader-required-w 는 JS 가 컬럼 개수에 따라 set. */
body.reader-multi-active #view.container {
  max-width: min(var(--reader-required-w, 100vw), 100vw);
  padding-left: 16px;
  padding-right: 16px;
}
/* 사이드바 영역 전체 의 opaque background — 사용자 spec 2026-05-27 : 컬
 * 럼 을 움직 일 때 사이드바 의 button 만 가리는 게 아니 라 그 영역 전
 * 체 가 column 카드 / fab 보 다 위.  z-index 12 — column (0) + fab (10)
 * + sidebar buttons (11) 모두 보다 위.  sidebar buttons 가 z-index 12
 * (rail bg) 보 다 위 paint 되 도록 buttons 의 z-index 도 13 으로 올림. */
.reader-sidebar-rail-bg {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  /* 사용자 보고 2026-05-27 : 영역 좁음 — fab 가 sidebar 영역 의 일부
   * (shell gap) 까지 visible.  width 를 column wrap 의 left edge 까지
   * 늘림.  옛 width = sidebar.left + 24 (button width).  새 width =
   * sidebar.left + 24 + 16 (shell gap) = column wrap 의 left edge. */
  width: calc(max(16px, calc((100vw - min(var(--reader-container-w, 928px), 100vw)) / 2)) + 40px);
  background: var(--bg);
  z-index: 50;
  pointer-events: none;
  /* overflow: hidden 으 로 ::before 의 viewport-attachment 가 rail-bg 의
   * clipped 영역 만 보 이 도 록.  ::before 는 absolute + inset:0 으 로 rail
   * box 를 가득 채 우 지 만 background-attachment:fixed 가 viewport-relative. */
  overflow: hidden;
}
/* 사용자 보고 2026-05-27 (4 차 라 운 드) : custom wallpaper 가 사이드바
 * 영역 에 도 보 여 야 함 — 옛 solid var(--bg) 가 wallpaper 를 가 림 (데
 * 스 크 탑 / 모바일 둘 다).  body::before 의 wallpaper 와 시 각 적 으 로
 * 연 속 되 도 록 같 은 image + 같 은 opacity 를 rail-bg 의 ::before pseudo
 * 로 layer.  background-attachment: fixed → 이 미 지 가 viewport 전 체 에
 * 깔 리 고 rail-bg 의 clipped 영역 만 visible.  body::before 와 픽 셀
 * 단 위 alignment 일 치. */
body[data-reader-bg="1"] .reader-sidebar-rail-bg::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--reader-bg-url) center / cover no-repeat fixed;
  opacity: var(--reader-bg-opacity, 0.4);
  pointer-events: none;
}
/* data-cols="2" : 균등 2 등 분 (container max-width 960 안). */
.reader-columns.has-passive-columns[data-cols="2"] > .reader-main,
.reader-columns.has-passive-columns[data-cols="2"] > .reader-column {
  flex: 1 1 0;
  min-width: 0;
  max-width: none;
  scroll-snap-align: start;
}
/* data-cols >= 3 : 모든 컬럼 360 고정. */
.reader-columns.has-passive-columns:not([data-cols="2"]) > .reader-main,
.reader-columns.has-passive-columns:not([data-cols="2"]) > .reader-column {
  flex: 0 0 var(--reader-passive-col-w);
  min-width: var(--reader-passive-col-w);
  max-width: var(--reader-passive-col-w);
  scroll-snap-align: start;
}
/* 패시브 활성 시 .reader-main 도 column shell 처럼 자체 세로 scroll.
 * overscroll-behavior: contain 으로 scroll chain 차단 — top scrolled 상
 * 태 에서 아래 로 스 와이프 시 page (= 다른 컬럼) 같이 움직 임 방지. */
.reader-columns.has-passive-columns > .reader-main {
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  height: 100%;
  max-height: 100%;
  /* Y축만 contain — 세로 scroll chain 만 차단(page bounce/pull 전파 방지).
   * 옛 `overscroll-behavior: contain` 은 shorthand 라 X축도 contain 돼,
   * Android Chrome 에서 컬럼 위 가로 스와이프가 부모 .reader-columns 의
   * 가로 스크롤로 전파 못 하고 갇혔다(사용자 보고 2026-06-04). */
  overscroll-behavior-y: contain;
  /* 사용자 보고 2026-05-28 : 멀티컬럼 일 때 액티브 컬럼 도 자체 scroll
   * container 라, top-scrolled 시 첫 콘텐츠 가 사이드바 top 과 맞 도 록
   * 상단 12px 여백.  scroll 안 쪽 padding → 스크롤 하면 사라 져 dead-zone
   * 아 님.  옛 container 상단 padding (액티브 단독 모드 의 정렬) 의 멀티
   * 컬럼 대체. */
  padding-top: 12px;
}
.reader-column {
  display: flex;
  flex-direction: column;
  min-width: 0;
  height: 100%;
  max-height: 100%;
  /* 멀티컬럼 액션 버튼(위로 가기) per-column 절대 위치 기준. */
  position: relative;
  /* 사용자 spec 2026-05-27 : 컬럼 은 모달 아닌 홈 피드 같은 모양.
   * background / border / border-radius 제거 (banner 와 카드 만 시각).
   * overflow hidden 은 자체 scroll container 동작 유지 위해 보존. */
  overflow: hidden;
}
/* 배너 — 모든 4 모서리 round (사용자 spec 2026-05-27).  column 의 overflow
 * hidden 이 top-corners 를 자동으로 cut 하므로 bottom radius 도 명시 해
 * 야 시각 적 으로 4 모서리 둥근 pill.  banner 와 content 사이 12 px
 * 간격 도 같이. */
.reader-column-banner {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  font-weight: 600;
  font-size: 13px;
  flex: 0 0 auto;
  border-radius: var(--radius);
  margin: 0 0 10px;
  /* 사용자 spec 2026-05-27 (8 차) : 배너 sticky 제거 — 모바일 / 데스크탑
   * 모두 스크롤 시 같이 올라가서 사라지는 평범한 flow.  옛 sticky 의 의
   * 도 (스와이프 gesture 가 banner 영역 에서 도 작동) 는 column-content
   * 의 자체 scroll + overscroll-behavior 가 이미 cover. */
}
/* 프로필 컬럼 배너 — 리포스트 의 lime 보다 파랑빛 (사용자 spec 2026-05-27).
 * 별도 var (--column-profile-banner-bg) 라서 커스텀 테마 슬롯 에서 도 색
 * 조정 가능. */
.reader-column-profile .reader-column-banner {
  background: var(--column-profile-banner-bg);
  color: var(--text);
}
/* 검색 컬럼 배너 — 다크 모드 기준 남색 (사용자 spec 2026-05-27). */
.reader-column-search .reader-column-banner {
  background: var(--column-search-banner-bg);
  color: var(--text);
}
/* 알림 컬럼 배너 — 라이트 모드 기준 연한 핑크 (사용자 spec 2026-05-27). */
.reader-column-notifications .reader-column-banner {
  background: var(--column-notif-banner-bg);
  color: var(--text);
}
/* 좋아요 / 북마크 컬럼 배너 — 알림 과 동일 색 (사용자 spec 2026-05-27 5 차). */
.reader-column-likes .reader-column-banner {
  background: var(--column-likes-banner-bg);
  color: var(--text);
}
.reader-column-bookmarks .reader-column-banner {
  background: var(--column-bookmarks-banner-bg);
  color: var(--text);
}
/* 커스텀 피드 컬럼 배너 — 라이트 기준 연한 주황 (사용자 spec 2026-05-27 5 차). */
.reader-column-feed .reader-column-banner {
  background: var(--column-feed-banner-bg);
  color: var(--text);
}
/* 커스텀 리스트 컬럼 배너 — 라이트 기준 연한 보라 (사용자 spec 2026-05-27 9 차). */
.reader-column-list .reader-column-banner {
  background: var(--column-list-banner-bg);
  color: var(--text);
}
/* 사용자 spec 2026-05-27 (7 차) : custom wallpaper 가 켜 진 경우 배너 도
 * 포스트 카드 와 동일 한 불투명도 (--reader-card-alpha) 로 wallpaper 가
 * 비치 도록.  포스트 카드 의 color-mix(surface, transparent) 패턴 그 대로
 * 배너 의 자체 색 var(--column-*-banner-bg) 에 적용. */
body[data-reader-bg="1"] .reader-column-profile .reader-column-banner {
  background: color-mix(in srgb,
    var(--column-profile-banner-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent);
}
body[data-reader-bg="1"] .reader-column-search .reader-column-banner {
  background: color-mix(in srgb,
    var(--column-search-banner-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent);
}
body[data-reader-bg="1"] .reader-column-notifications .reader-column-banner {
  background: color-mix(in srgb,
    var(--column-notif-banner-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent);
}
body[data-reader-bg="1"] .reader-column-likes .reader-column-banner {
  background: color-mix(in srgb,
    var(--column-likes-banner-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent);
}
body[data-reader-bg="1"] .reader-column-bookmarks .reader-column-banner {
  background: color-mix(in srgb,
    var(--column-bookmarks-banner-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent);
}
body[data-reader-bg="1"] .reader-column-feed .reader-column-banner {
  background: color-mix(in srgb,
    var(--column-feed-banner-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent);
}
body[data-reader-bg="1"] .reader-column-list .reader-column-banner {
  background: color-mix(in srgb,
    var(--column-list-banner-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent);
}
/* 검색 결과 줄 의 "+ 새 컬럼에 추가" 버튼 — 오른쪽 정렬 (advanced toggle
 * 과 같은 row 안에서 space-between). */
.reader-search-advanced-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.reader-search-add-column-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 8px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.reader-search-add-column-btn:hover { background: var(--surface-hover); }
.reader-search-add-column-btn[hidden] { display: none; }
.reader-column-banner-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 클릭 가능 한 타이틀 (프로필 컬럼) — hover/active 시각 피드백. */
.reader-column-banner-title.is-clickable {
  cursor: pointer;
  user-select: none;
}
.reader-column-banner-title.is-clickable:hover {
  text-decoration: underline;
}
.reader-column-banner-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 4px;
}
.reader-column-move-btn,
.reader-column-close-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 0;
  background: transparent;
  color: var(--text);
  border-radius: 6px;
  cursor: pointer;
  padding: 0;
}
.reader-column-move-btn:hover:not(:disabled),
.reader-column-close-btn:hover {
  background: rgba(0, 0, 0, 0.08);
}
[data-theme="dark"] .reader-column-move-btn:hover:not(:disabled),
[data-theme="dark"] .reader-column-close-btn:hover {
  background: rgba(255, 255, 255, 0.12);
}
.reader-column-move-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.reader-column-close-btn {
  font-size: 18px;
  line-height: 1;
}
.reader-column-content {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  /* 사용자 spec 2026-05-27 : 카드 너비 가 active 컬럼 의 카드 와 같 게
   * (column 너비 가득) — content 좌우 padding 제거.  bottom 12 만 마지
   * 막 카드 와 column 끝 사이 여백. */
  /* 사용자 보고 2026-05-28 : top-scrolled 일 때 첫 콘텐츠(배너) 가 사이드
   * 바 top 버튼 (top:12px) 과 맞 도 록 상단 12px 여백.  scroll container
   * 의 안 쪽 padding 이 라 스크롤 하면 같이 올라가 사라 짐 — 옛 container
   * 의 상단 padding (dead-zone) 과 달 리 피드 가 viewport 전체 를 계속
   * 사용.  bottom 12 는 마 지 막 카드 와 column 끝 사이 여백. */
  padding: 12px 0;
  /* 사용자 보고 2026-05-27 : 컬럼 의 top scrolled 상태 에서 아래 로 스 와
   * 이프 하면 page 전체 (그래서 모든 컬럼) 가 같이 움직 임.  Y축 만 contain
   * 으로 세로 scroll chain 만 차단 — 컬럼 가 자체 의 overscroll 처리
   * (pull-to-refresh 등) 만 발 동, page 로 전 파 안 함.  X축 은 auto 로
   * 둬 가로 스와이프가 부모 .reader-columns 의 가로 스크롤로 전파 되도록
   * (Android Chrome 가로 스와이프 막힘 — 사용자 보고 2026-06-04). */
  overscroll-behavior-y: contain;
}
/* .reader-list 클래스 도 같이 — 액티브 카드 사이 gap 12px 등 그대로
 * 상속.  .reader-column-list 는 추가 hook 만 (현재는 없음). */
.reader-column-list { /* hook */ }
.reader-column-loading,
.reader-column-empty {
  padding: 24px 12px;
  text-align: center;
  color: var(--text-faint);
  font-size: 14px;
}
.reader-column-sentinel { height: 1px; }
/* 모바일 (≤1024px) — 각 컬럼 100% viewport (사이드바 + 컬럼 1 개 만, 좌
 * 우 스크롤 로 전환).  사용자 spec 2026-05-27.  data-cols specificity
 * (0,3,1) 가 media-query 의 0,2,1 보 다 높 아 override 못 했던 버그 — 같
 * 은 specificity 의 selector chain 으 로 명시. */
@media (max-width: 1024px) {
  .reader-columns.has-passive-columns[data-cols="2"] > .reader-main,
  .reader-columns.has-passive-columns[data-cols="2"] > .reader-column,
  .reader-columns.has-passive-columns:not([data-cols="2"]) > .reader-main,
  .reader-columns.has-passive-columns:not([data-cols="2"]) > .reader-column {
    flex: 0 0 100%;
    min-width: 100%;
    max-width: 100%;
  }
  /* 모바일 multi-column home margin-top hack 제거 (사용자 보고 2026-05-27
   * v2 : 이번엔 home feed 가 너무 아래 — 위 47px 빈 공간 부조화).  margin
   * 0 = main top y 그대로 (다른 mode 와 동일). */
  /* 사용자 spec 2026-05-27 : 모바일 은 무조건 컬럼 1 개만.  scroll-snap
   * proximity 가 사용자 가 중간 위치 정지 가능 → mandatory 로 강제 snap. */
  .reader-columns.has-passive-columns {
    scroll-snap-type: x mandatory;
  }
  /* 가로 스와이프 fix (Android Chrome) : 액티브 컬럼도 스냅 대상(.reader-
   * main)과 세로 스크롤러(.reader-main-scroll)를 분리 — 패시브 .reader-
   * column / .reader-column-content 와 동일 패턴.  같은 요소가 스냅 대상이자
   * 세로 스크롤러면 Android 가 세로에 락이 걸려 부모 가로 스크롤로 전환을
   * 못 함.  데스크톱/단일컬럼은 이 규칙이 없어 .reader-main-scroll 이
   * passthrough(overflow visible) → 동작 불변. */
  .reader-columns.has-passive-columns > .reader-main {
    overflow: hidden !important;
    padding-top: 0 !important;
  }
  .reader-columns.has-passive-columns > .reader-main > .reader-main-scroll {
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    /* Y축만 contain — X축 contain(shorthand)이면 Android Chrome 에서 가로
     * 스와이프가 부모 가로 스크롤로 전파 못 해 막힘(사용자 보고 2026-06-04). */
    overscroll-behavior-y: contain;
    /* 옛 .reader-main 의 상단 12px(사이드바 top 정렬) — 스크롤러 안쪽으로
     * 옮겨 스크롤 시 사라지게. */
    padding-top: 12px;
    /* 스크롤바를 단일 컬럼처럼 뷰포트 오른쪽 끝에 두기 — 컨테이너 우측
     * 인셋(11px)을 0으로 풀어(아래) 컬럼이 뷰포트 끝까지 닿게 하고, 그
     * 11px를 여기 내부 우측 패딩으로 옮겨 카드는 기존 폭(풀폭, 419) 유지.
     * 스크롤바가 그 거터(419~430)에 떠 카드와 안 겹침(사용자 보고 2026-06-04). */
    padding-right: 11px;
  }
  /* 멀티컬럼일 때 컨테이너 우측 인셋 제거 → 컬럼이 뷰포트 오른쪽 끝까지.
   * (좌측 인셋·세로 패딩은 그대로.  스크롤바 거터는 위 스크롤러 내부 패딩.) */
  body.reader-has-passive-cols #view.container { padding-right: 0; }
  /* 패시브 컬럼도 동일 — 스크롤러 우측 거터로 스크롤바가 카드와 안 겹침. */
  .reader-columns.has-passive-columns > .reader-column > .reader-column-content {
    padding-right: 11px;
  }
}
/* ── 모바일 컬럼 인디케이터 (점) — 사용자 spec 2026-05-28 ─────────────
 * 모바일 멀티컬럼 에서 컬럼 을 좌우 스와이프 로 전환 하 면 하단 중앙 에
 * 점 으 로 현재 위치 표시.  .is-shown 추가 = 빠른 페이드인, 제거 = 느린
 * 페이드아웃.  기본 (no .is-shown) 의 transition 을 느린 fade-out, .is-
 * shown 의 transition 을 빠른 fade-in 으 로 분리.  JS 가 모바일 + 2 cols
 * 이상 일 때만 element 를 생성/유지 하 므 로 평소 엔 DOM 에 없 음. */
.reader-column-indicator {
  position: fixed;
  left: 50%;
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 7px 11px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--bg, #000) 62%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28);
  pointer-events: none;
  z-index: 14;
  opacity: 0;
  transition: opacity 900ms ease-in;   /* 느린 fade-out (기본 상태) */
}
.reader-column-indicator.is-shown {
  opacity: 1;
  transition: opacity 130ms ease-out;  /* 빠른 fade-in */
}
.reader-column-indicator-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text-faint, #94a3b8);
  opacity: 0.5;
  transition: background 160ms ease-out, opacity 160ms ease-out, transform 160ms ease-out;
}
.reader-column-indicator-dot.is-active {
  background: var(--accent, #0085ff);
  opacity: 1;
  transform: scale(1.35);
}
/* desktop 안전망 — JS 가 element 를 제거 하 지 만, resize race 등 으 로
 * 잠 깐 남 더 라 도 데스크톱 에 서 는 보 이 지 않 도록. */
@media (min-width: 1025px) {
  .reader-column-indicator { display: none; }
}
/* Search view — swapped in for the timeline when the search tab is
 * active. Header stays at the top (input + options); the list scrolls
 * with the page like the timeline does. */
.reader-search-view {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: 8px;
}
.reader-search-view[hidden] { display: none; }

/* ── "내가 본 글들" 히스토리 surface (실험중) ─────────────────────────
 * 액티브 surface : 상단 [비우기][새 컬럼으로] 버튼 행 + 카드 리스트.
 * 패시브 컬럼 : .reader-history-list 만 (헤더 버튼 없 음).  각 카드 는
 * 일반 .reader-card 위 에 우상단 X (개별 삭제) 가 덧붙음. */
/* 버튼 행 의 상하 여백 을 좋아요 피드 "새 컬럼으로" 버튼 과 동일 하 게
 * (위 = padding-top 1 + 버튼 margin-top 2 = 3px, 아래 = 버튼 margin-bottom
 * 4 + gap 2 = 6px).  사용자 spec 2026-05-29. */
.reader-history-view { display: flex; flex-direction: column; gap: 2px; padding-top: 1px; }
.reader-history-view[hidden] { display: none; }
.reader-history-header {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  padding: 0 4px;
  /* 사용자 spec 2026-05-29 : 두 버튼 중앙정렬. */
  justify-content: center;
}
/* 히스토리 헤더 의 [비우기][새 컬럼으로] 는 좋아요 피드 위 의 "새 컬럼
 * 으로" 버튼(.reader-feed-add-column-btn) 과 같 은 스타일/크기.  feed 쪽
 * 규칙 은 .reader-feed-header > 로 스코프 돼 있 어 여기 서 동일 props 를
 * 재선언 (margin 은 flex 중앙정렬 이 라 auto 대신 0). */
.reader-history-header > .reader-feed-add-column-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 8px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  margin: 2px 0 4px;
}
.reader-history-header > .reader-feed-add-column-btn:hover {
  background: var(--surface-hover);
}
.reader-history-card { position: relative; }
/* 우상단 X — 그 글 만 히스토리 에 서 삭제.  카드 의 우상단 워터마크 /
 * 시각 과 겹치 지 않 게 살짝 안 쪽, 작은 원형 버튼. */
.reader-history-remove {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 3;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-muted);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  opacity: 0.85;
}
.reader-history-remove:hover {
  color: var(--text);
  border-color: var(--border-strong);
  opacity: 1;
}
.reader-search-header {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 0 4px;
}
.reader-search-input-row {
  display: flex;
  gap: 8px;
}
/* Wrap so the clear-× can absolute-position against the input's
 * right edge without disrupting the row's flex layout. */
.reader-search-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
}
.reader-search-input {
  flex: 1;
  min-width: 0;
  padding: 8px 32px 8px 12px;  /* extra right-pad reserves room for × */
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: inherit;
  font: inherit;
}
.reader-search-input:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
/* Suppress the browser default ::-webkit-search-cancel-button —
 * we render our own × so the look is consistent across engines. */
.reader-search-input::-webkit-search-cancel-button,
.reader-search-input::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}
.reader-search-input-clear {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--surface-hover);
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.reader-search-input-clear:hover {
  background: var(--border);
  color: var(--text);
}
.reader-search-input-clear[hidden] { display: none; }
.reader-search-submit {
  padding: 8px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.reader-search-submit:hover { background: var(--surface-hover); }
.reader-search-options {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  font-size: 13px;
}
.reader-search-options[hidden] { display: none; }
/* Advanced-options toggle — small left-aligned text button with a
 * rotating caret. When open, the caret flips and the panel below
 * un-collapses. */
.reader-search-advanced-toggle {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  opacity: 0.85;
}
.reader-search-advanced-toggle:hover {
  opacity: 1;
  background: var(--surface-hover);
  border-radius: 6px;
}
.reader-search-advanced-toggle[hidden] { display: none; }
.reader-search-advanced-toggle-caret {
  display: inline-block;
  transition: transform 0.15s ease;
}
.reader-search-advanced-toggle.is-open .reader-search-advanced-toggle-caret {
  transform: rotate(180deg);
}
.reader-search-advanced-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
}
.reader-search-advanced-panel[hidden] { display: none; }
/* Reset button row at the bottom of the advanced panel. */
.reader-search-advanced-reset-row {
  display: flex;
  justify-content: flex-end;
  margin-top: 4px;
}
.reader-search-advanced-reset {
  font-size: 12px;
  padding: 4px 10px;
}
/* Advanced-panel label — sits above each filter row. */
.reader-search-advanced-label {
  font-size: 12px;
  font-weight: 600;
  opacity: 0.7;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* Author filter row — mode toggle, chip (when set), and the
 * input + paste + typeahead group (when no chip). */
.reader-search-author-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  position: relative;
}
.reader-search-author-mode {
  display: inline-flex;
  align-self: flex-start;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.reader-search-author-mode[hidden] { display: none; }
.reader-search-author-mode-btn {
  padding: 4px 10px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.reader-search-author-mode-btn + .reader-search-author-mode-btn {
  border-left: 1px solid var(--border);
}
.reader-search-author-mode-btn:hover { background: var(--surface-hover); }
.reader-search-author-mode-btn.is-active {
  background: var(--surface-hover);
  font-weight: 600;
}
.reader-search-author-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  align-self: flex-start;
  padding: 4px 4px 4px 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  max-width: 100%;
}
.reader-search-author-chip[hidden] { display: none; }
.reader-search-author-chip-avatar {
  width: 22px; height: 22px; border-radius: 50%;
  object-fit: cover; flex: 0 0 auto;
  background: var(--surface-hover);
}
.reader-search-author-chip-avatar-fallback { background: var(--border-strong); color: var(--text-muted); }
.reader-search-author-chip-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 200px;
}
.reader-search-author-chip-handle {
  font-size: 13px;
  opacity: 0.75;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 200px;
}
.reader-search-author-chip-remove {
  width: 22px; height: 22px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
}
.reader-search-author-chip-remove:hover {
  background: var(--surface-hover);
  opacity: 1;
}
.reader-search-author-input-wrap {
  display: flex;
  gap: 6px;
}
.reader-search-author-input-wrap[hidden] { display: none; }
.reader-search-author-input {
  flex: 1;
  min-width: 0;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  font: inherit;
}
.reader-search-author-input:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
.reader-search-author-input.is-invalid {
  border-color: #d33;
}
.reader-search-author-paste {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  cursor: pointer;
}
.reader-search-author-paste:hover { background: var(--surface-hover); }
.reader-search-author-paste svg { display: block; }
/* Typeahead dropdown — popover-style under the input. */
.reader-search-author-dropdown {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  max-height: 260px;
  overflow-y: auto;
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.reader-search-author-dropdown[hidden] { display: none; }
.reader-search-author-dropdown-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.reader-search-author-dropdown-row:hover {
  background: var(--surface-hover);
}
.reader-search-author-dropdown-avatar {
  width: 28px; height: 28px; border-radius: 50%;
  object-fit: cover; flex: 0 0 auto;
  background: var(--surface-hover);
}
.reader-search-author-dropdown-avatar-fallback { background: var(--border-strong); color: var(--text-muted); }
.reader-search-author-dropdown-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.reader-search-author-dropdown-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-search-author-dropdown-handle {
  font-size: 12px;
  opacity: 0.75;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Lang / URL rows — single-field rows under the author block. */
.reader-search-lang-row,
.reader-search-url-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.reader-search-lang-select {
  align-self: flex-start;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  min-width: 160px;
}
.reader-search-lang-select:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
.reader-search-url-input {
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  font: inherit;
}
.reader-search-url-input:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
.reader-search-exclude-mine[disabled],
.reader-search-mine[disabled] {
  cursor: not-allowed;
}
.reader-search-mine-label:has(input:disabled) {
  opacity: 0.55;
  cursor: not-allowed;
}
/* Date range — two native date inputs with a separator between. */
.reader-search-date-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.reader-search-date-inputs {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.reader-search-date-input {
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  font: inherit;
}
.reader-search-date-input:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
.reader-search-date-sep { opacity: 0.6; }
/* "한글 확장 검색" expand button + rate-limit hint — shown in place
 * of the end-marker when the current query qualifies for Korean
 * particle fan-out. Wrapped together so a single [hidden] flag on
 * the parent toggles both. */
.reader-search-hangul-expand-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  margin: 8px 0;
}
.reader-search-hangul-expand-wrap[hidden] { display: none; }
.reader-search-hangul-expand { display: block; }
.reader-search-hangul-expand[disabled] {
  cursor: progress;
  opacity: 0.7;
}
.reader-search-hangul-expand-hint {
  margin: 0;
  font-size: 12px;
  text-align: center;
  opacity: 0.7;
}
.reader-search-sort-group {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.reader-search-sort-group[hidden] { display: none; }
.reader-search-sort-btn {
  padding: 4px 10px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.reader-search-sort-btn + .reader-search-sort-btn {
  border-left: 1px solid var(--border);
}
.reader-search-sort-btn:hover { background: var(--surface-hover); }
.reader-search-sort-btn.is-active {
  background: var(--surface-hover);
  font-weight: 600;
}
.reader-search-mine-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
}
.reader-search-status[hidden] { display: none; }
.reader-search-list {
  display: flex;
  flex-direction: column;
  /* Match .reader-list's card spacing so search results read like the
   * timeline. */
  gap: 12px;
  margin: 0 0 12px;
}
.reader-search-list[hidden],
.reader-search-user-list[hidden],
.reader-search-feed-list[hidden] { display: none; }
.reader-search-feed-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0 8px;
}
/* "내가 본 글"(history) 검색 결과 카드 리스트 — .reader-search-list 와
 * 동일한 카드 간 간격(12px).  searchHistoryListEl 은 .reader-list 를 안
 * 달아 부모 gap 이 카드 사이엔 안 먹어 카드가 붙어 보이던 버그.  history
 * 리스트는 .hidden 으로 토글되므로 author display:flex 가 UA [hidden] 을
 * 이기지 못하게 [hidden] 규칙을 명시(명시도 함정 방지). */
.reader-search-history-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.reader-search-history-list[hidden] { display: none; }
/* Mode toggle (포스트 / 유저 / 피드) — segmented pill at the top of
 * the search header. Centered along the flex-column header so the
 * three-button capsule reads as a clear top-of-page chooser
 * rather than a left-aligned sub-tab. */
.reader-search-mode-row {
  display: inline-flex;
  align-self: center;
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.reader-search-mode-btn {
  padding: 6px 14px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.reader-search-mode-btn + .reader-search-mode-btn {
  border-left: 1px solid var(--border);
}
.reader-search-mode-btn:hover { background: var(--surface-hover); }
.reader-search-mode-btn.is-active {
  background: var(--surface-hover);
  font-weight: 600;
}
/* Notif panel reuses the same segmented chassis (.reader-search-mode-
 * row).  사용자 spec 2026-05-27 (updated) : 토글 이 "+ 새 컬럼에 추가"
 * 버튼 제외 한 남은 공간 의 중간 위치.  3 column grid (1fr auto 1fr) +
 * toggle center / button right.  좌측 1fr 의 spacer 가 우측 button 의
 * 대칭 — toggle 가 row 의 정확 한 가운데 (= row width - button - left
 * spacer 의 center). */
.reader-notif-top-row {
  /* 사용자 spec 2026-05-27 (updated again) : "버튼 을 제외한 남은 공간
   * 의 중간".  즉 toggle center = (row width - button) / 2 from left.
   * grid 4 col [1fr | toggle | 1fr | button] : 두 1fr same width (=
   * (W - toggle - B) / 2 each), toggle col2 center, button col4 right.
   * 결과 : toggle center = 1fr + toggle_w/2 = (W - B) / 2. 사용자 spec
   * 정 답.
   *
   * margin-top 0 — 옛 8 일 때 active notif 일 때 만 main 의 첫 visible
   * top 이 home 보다 8px 아래 였음 (사용자 보고 "액티브 알림창 일 때 모
   * 든 컬럼 top 살짝 올라감" 의 원인 — visually top-row 만 아래 → 사용
   * 자 가 column 들 이 같이 변경 됐다고 인식).  margin-top 0 으로 home
   * 과 동일 baseline. */
  display: grid;
  grid-template-columns: 1fr auto 1fr auto;
  align-items: center;
  gap: 12px;
  margin: 0 0 14px;
}
.reader-notif-top-row .reader-search-mode-row {
  grid-column: 2;
  justify-self: center;
}
.reader-notif-top-row .reader-notif-add-column-btn {
  grid-column: 4;
  justify-self: end;
}
.reader-notif-top-row .reader-search-mode-row {
  margin: 0;
}
.reader-notif-view > .reader-search-mode-row {
  display: flex;
  width: fit-content;
  margin: 8px auto 14px;
}
.reader-notif-add-column-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 8px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.reader-notif-add-column-btn:hover { background: var(--surface-hover); }

/* 사용자 spec 2026-05-27 (5 차) : 좋아요 / 북마크 active view 의 맨 위
 * 가운데 정렬 "새 컬럼에 추가" 버튼.  feedHeader 자리에 직접 painted —
 * paintFeedHeader 의 FEED_LIKES / FEED_BOOKMARKS 분기 가 single button
 * 만 렌더.  .reader-notif-add-column-btn 와 동일 스타일 + 가운데 정렬. */
.reader-feed-header > .reader-feed-add-column-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 8px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  /* 사용자 spec 2026-05-27 (8 차) : 위 아 래 여 백 또 절반 — 3/7 → 2/4. */
  margin: 2px auto 4px;
}
.reader-feed-header:has(> .reader-feed-add-column-btn) {
  /* feedHeader 의 default block layout 을 가운데 정렬 flex 로 — single
   * button 만 있는 경우 의 layout.  has() 가 미지원 인 옛 브라우저 는
   * 위 의 inline-flex + margin auto 로 충분.
   * 사용자 spec 2026-05-27 (8 차) : padding 또 절반 — 2/4 → 1/2. */
  display: flex;
  justify-content: center;
  padding: 1px 0 2px;
  background: transparent;
  border: 0;
  min-height: 0;
}
.reader-feed-header > .reader-feed-add-column-btn:hover {
  background: var(--surface-hover);
}
/* Profile feed tabs (홈 / 답글 / 이미지 / 비디오) — segmented pill
 * that sits between the profile header's bottom border and the
 * first post.  Hosted inside .reader-profile-feed-tabs-host so a
 * single hidden toggle controls visibility without depending on
 * the inner button list being present.  Centered horizontally,
 * vertical breathing room above and below to read as a clear
 * "section header" between the bio and the timeline. */
.reader-profile-feed-tabs-host {
  display: flex;
  justify-content: center;
  padding: 10px 8px 12px;
}
.reader-profile-feed-tabs-host[hidden] { display: none; }
.reader-profile-feed-tabs { display: inline-flex; }
/* User search rows — avatar + name/handle on the left, copy button on
 * the right. The avatar/name area is a real <a> to the bsky.app
 * profile; the copy button stops propagation so it doesn't follow. */
.reader-search-user-list {
  display: flex;
  flex-direction: column;
}
.reader-search-user-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}
.reader-search-user-link {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}
.reader-search-user-link:hover,
.reader-search-user-link:focus,
.reader-search-user-link:active {
  text-decoration: none;
}
.reader-search-user-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  flex: 0 0 auto;
  object-fit: cover;
  background: var(--surface-hover);
}
.reader-search-user-avatar-fallback { background: var(--border-strong); color: var(--text-muted); }
.reader-search-user-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
/* Compact counts row under @handle in user-search results +
 * follower/follow modal rows. Same pill chassis as the profile
 * header pills but tighter (smaller icons, no left margin). */
/* Per-reason push-notification filter pills — six toggleable
 * chips that surface below the main push toggle when push is
 * enabled. Each pill flips on click; the all-off case routes to
 * a confirm modal that drops the master toggle. */
.reader-push-types-row[hidden] { display: none; }
.reader-push-types-row {
  justify-content: center;
}
.reader-push-types {
  display: flex;
  flex-direction: row;
  gap: 2px;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: flex-start;
  width: 100%;
}
/* Each pill is a vertical stack: white-icon disc on top, small label
 * underneath. The disc is the visible toggle target; the label echoes
 * what used to be inside the disc as text. Disc bg flips between
 * accent (on) and a muted shade (off) for an obvious state cue, and
 * the icon is uniformly white regardless of state so all eight read
 * as one consistent visual family. flex:1 + min-width:0 lets all
 * eight share the available width evenly and shrink as the modal
 * narrows, instead of overflowing past the right edge. */
.reader-push-type-pill {
  appearance: none;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  flex: 1 1 0;
  min-width: 0;
  max-width: 56px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.reader-push-type-pill-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  transition: background 0.15s, opacity 0.15s;
}
.reader-push-type-pill:not(.is-on) .reader-push-type-pill-icon {
  background: var(--text-faint);
  opacity: 0.75;
}
.reader-push-type-pill:hover .reader-push-type-pill-icon {
  filter: brightness(0.92);
}
.reader-push-type-pill-label {
  font-size: 10px;
  line-height: 1.1;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.reader-search-user-name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-search-user-handle {
  font-size: 13px;
  opacity: 0.75;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 검색 결과 user row 의 핸들 옆 labeler 태그 — actor.associated.labeler
 * 가 true 면 inline 으로 표시.  부드러운 회색 chip + 작은 폰트. */
.reader-labeler-tag {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  font-size: 11px;
  font-weight: 500;
  color: var(--text-muted);
  background: var(--surface-2);
  border-radius: 4px;
  vertical-align: middle;
}
.reader-search-user-copy {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: inherit;
  cursor: pointer;
}
.reader-search-user-copy:hover { background: var(--surface-hover); }
.reader-search-user-copy svg { display: block; }
/* Search history — horizontal scrollable chip row under the input.
 * Scrollbar is hidden (scrollbar-width: none + WebKit pseudo) so the
 * row stays clean while still allowing wheel / touch scroll. */
.reader-search-history {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  overflow-y: hidden;
  padding-bottom: 2px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.reader-search-history::-webkit-scrollbar { display: none; }
.reader-search-history[hidden] { display: none; }
.reader-search-history-chip {
  display: inline-flex;
  align-items: stretch;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  overflow: hidden;
  max-width: 240px;
}
.reader-search-history-chip-body {
  padding: 4px 10px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  max-width: 200px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-search-history-chip-body:hover { background: var(--surface-hover); }
/* Account-history chip: small avatar + @handle. Used in user-search
 * mode where the chips remember recently-clicked accounts. */
.reader-search-history-chip-account .reader-search-history-chip-body {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px 3px 4px;
}
.reader-search-history-chip-avatar {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--surface-2);
  flex: 0 0 auto;
  object-fit: cover;
}
.reader-search-history-chip-avatar-fallback {
  background: linear-gradient(135deg, var(--accent-soft), var(--surface-2));
}
.reader-search-history-chip-handle {
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-search-history-chip-remove {
  padding: 0 8px;
  border: 0;
  border-left: 1px solid var(--border);
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
}
.reader-search-history-chip-remove:hover {
  background: var(--surface-hover);
  opacity: 1;
}
/* The sidebar element is just a 24px flex spacer — every visible
 * column inside it is position: fixed so they pin to the viewport. */
.reader-sidebar {
  width: 24px;
  flex: 0 0 auto;
}
/* Shared left calc — the sidebar columns all align with the
 * container's content edge: container max-width 960 + 16px padding →
 * content starts at max(16, (vw-928)/2).
 *
 * multi-column 모드 (사용자 spec 2026-05-27) 에서는 .container 의 max-width
 * 가 동적 으로 확장 되므로 sidebar left 도 같이 갱신.  --reader-container-w
 * 는 JS (updateColumnsLayoutMeta) 가 :root 에 set 한 container 의 inner
 * 너비 (= max-width - padding 32px).  default 928 = 960 - 32. */
.reader-sidebar-fixed-top,
.reader-sidebar-feeds,
.reader-sidebar-bottom {
  position: fixed;
  left: max(16px, calc((100vw - min(var(--reader-container-w, 928px), 100vw)) / 2));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  /* 사용자 spec 2026-05-27 : sidebar 영역 의 rail-bg (z-index 50) 위 에
   * sidebar buttons paint — 51. */
  z-index: 51;
}
/* Fixed top rail — home + my-timeline. Pinned at viewport top so it
 * never scrolls out of reach. With `interactive-widget=resizes-content`
 * (viewport meta), the layout viewport shrinks when the iOS soft
 * keyboard opens, so the top:12px reference tracks the visible area
 * natively — no JS compensation needed. */
.reader-sidebar-fixed-top {
  top: 12px;
  width: 24px;
}
/* Scrollable middle — saved custom feeds. Anchored both top and
 * bottom so it fills the gap between the two fixed rails and
 * overflows internally when the list is too long. Custom feed
 * buttons are 24px (avatar only, no outline) so the column stays at
 * 24px and `overflow-y: auto` doesn't trigger horizontal clipping. */
.reader-sidebar-feeds {
  /* Below the fixed top: 12 (top inset) + 6×34 (six built-in tabs:
   * home / search / likes / bookmarks / notifications / mine) +
   * 5×14 (gaps between them) + 14 (gap to scrolled feeds) = 300. */
  top: 300px;
  /* Above the fixed bottom: 12 (bottom) + 4×34 + 3×14 (four buttons +
   * three gaps) + 14 (gap to scrolled feeds) = 204. */
  bottom: 204px;
  /* 34px buttons need the wrapper to be 34 wide too — otherwise
   * overflow-y: auto's implicit horizontal clipping chops the
   * buttons' edges. margin-left: -5 re-centres on the 24px spine. */
  width: 34px;
  margin-left: -5px;
  overflow-y: auto;
  /* Keep touch scroll contained to this rail — swipe on it shouldn't
   * bubble up to the main pane or the document. */
  overscroll-behavior: none;
  scrollbar-width: none;
  /* 사용자 spec 2026-05-27 : sidebar 영역 의 rail-bg (z-index 50) 위 에
   * custom feed / list 버튼 도 paint.  옛 z-index 10 (shared selector 의
   * 51 을 override 했 음) → 51 로 맞춤. */
  z-index: 51;
}
.reader-sidebar-feeds::-webkit-scrollbar { display: none; }
/* Custom feed tab — same chassis (34×34 border + radius) as the
 * built-in sidebar buttons, but the inside is filled with the feed's
 * avatar (rounded-square, matching Bluesky's feed icon shape). Active
 * state tints the avatar with a translucent accent overlay so it
 * "darkens with blue" like the home/mine accent-soft selection. */
.reader-sidebar-btn { position: relative; }
.reader-feed-tab-custom { overflow: hidden; }
.reader-feed-tab-custom .reader-feed-tab-avatar,
.reader-feed-tab-custom .reader-feed-tab-letter {
  width: 32px;
  height: 32px;
  border-radius: 7px;
  display: flex;
  align-items: center;
  justify-content: center;
  object-fit: cover;
  background: var(--surface-hover);
}
.reader-feed-tab-custom.is-active {
  border-color: var(--accent);
}
.reader-feed-tab-custom.is-active::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--accent);
  opacity: 0.25;
  pointer-events: none;
  border-radius: inherit;
}
/* Saved-feed tabs: avatar (24×24 round) inside the 34×34 sidebar
 * button, with a first-letter fallback when the feed has no avatar. */
.reader-feed-tab-avatar {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: block;
  object-fit: cover;
  background: var(--surface-hover);
}
.reader-feed-tab-letter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--surface-hover);
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
}
/* Curate-list sidebar button (실험중 'reader:sidebar-lists').  Shape /
 * size identical to the feed tab — `.reader-feed-tab-custom` 의 모든
 * 규칙 그대로 상속 (avatar 32×32, border-radius 7px).  사이드바 의
 * 통일 모양 디자인 원칙 유지 — 피드 와 의 구분 은 위 의 separator hr
 * 가 담당 (사용자 spec 2026-05-26).
 *
 * Separator — 피드 와 리스트 사이 의 가로 선 1 줄.  --border-strong 은
 * 라이트 (#cbd5e1) / 다크 (#3b4a6b) 둘 다 배경 대비 잘 보 임.  flex
 * column 의 14 px gap 위·아래 양쪽 으 로 자연 spacing. */
.reader-sidebar-feed-list-separator {
  flex: 0 0 auto;
  display: block;
  width: 22px;
  height: 1px;
  margin: 0;
  border: 0;
  background: var(--border-strong);
}
/* In-page list pane — replaces listEl when 큐레이션 리스트 탭 active.
 * Same vertical-stack layout as searchView (header + list scrolls with
 * page).  header 의 avatar 가 큰 사이즈 (48×48) 라 list 모달 의 visual
 * presence 와 비슷 함. */
.reader-list-pane {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: 8px;
}
.reader-list-pane[hidden] { display: none; }
.reader-list-pane-header {
  padding: 4px 4px 12px;
  border-bottom: 1px solid var(--border);
}
.reader-list-pane-header-inner {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.reader-list-pane-header-avatar {
  width: 48px;
  height: 48px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  object-fit: cover;
  background: var(--surface-hover);
  flex: 0 0 auto;
}
.reader-list-pane-header-avatar-fallback {
  color: var(--text-muted);
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
}
.reader-list-pane-header-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.reader-list-pane-header-title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
}
.reader-list-pane-header-meta {
  color: var(--text-muted);
  font-size: 13px;
}
.reader-list-pane-header-desc {
  margin: 4px 0 0;
  color: var(--text);
  font-size: 14px;
  line-height: 1.5;
  white-space: pre-wrap;
}
.reader-list-pane-list {
  display: flex;
  flex-direction: column;
}
.reader-list-pane-status[hidden],
.reader-list-pane-end[hidden] { display: none; }
/* Bottom rail (👤 account + ⚙ settings + ↑ scroll-top) — pinned at
 * the viewport bottom regardless of scroll. Width / left / display
 * inherited from the shared `.reader-sidebar-fixed-top, ..., -bottom`
 * rule above. */
.reader-sidebar-bottom {
  bottom: 12px;
  width: 24px;
}
/* Installed-PWA bottom-edge guard. When running standalone (Add to
 * Home Screen / Install App), iOS's home-indicator strip and
 * Android's gesture inset clip the lower rail — the ↑ scroll-top
 * button reads as half-cut. Add safe-area inset plus a small extra
 * buffer; in browser tabs there's no safe-area inset so this is a
 * no-op for non-PWA users. The feeds rail above shifts up by the
 * same delta so the two rails don't collide. */
@media (display-mode: standalone) {
  .reader-sidebar-bottom {
    bottom: calc(12px + env(safe-area-inset-bottom, 0px) + 12px);
  }
  .reader-sidebar-feeds {
    bottom: calc(204px + env(safe-area-inset-bottom, 0px) + 12px);
  }
}
/* All sidebar buttons share the same look — borrowed from .theme-btn
 * in the global header so the line icons read as one family across
 * the app. Buttons are wider than the 24px rail so they extend a few
 * px to either side, giving each tab a slight "tab-on-spine" silhouette. */
.reader-sidebar-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  /* Pin the size so the scrollable feed list doesn't flex-shrink
   * buttons vertically when the list is taller than max-height. */
  flex: 0 0 34px;
  padding: 0;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s, opacity 0.25s ease;
}
.reader-sidebar-btn svg { width: 20px; height: 20px; }
.reader-sidebar-btn:hover {
  background: var(--surface-hover);
  color: var(--text);
}
.reader-sidebar-btn svg { display: block; }
.reader-sidebar-btn.faded {
  opacity: 0;
  pointer-events: none;
}
/* DM unread badge — same shape + position + palette as the
 * scroll-top button's new-post count badge so the two read as
 * a matched pair on the sidebar. Reuses the .reader-scroll-top-
 * badge rule directly; this selector only exists to be
 * targetable from JS / aria. */
.reader-dm-unread-badge {
  position: absolute;
  right: -5px;
  bottom: -5px;
  min-width: 14px;
  height: 14px;
  padding: 1px 4px 0;
  border-radius: 999px;
  background: #b91c1c;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 0 1.5px var(--bg);
  pointer-events: none;
}
.reader-dm-unread-badge[hidden] { display: none; }
/* Account button — keeps the same 34×34 rounded-square chassis as
 * the other sidebar buttons, but the inside is filled with the
 * user's own avatar so it visually differs from the 내 타임라인
 * person SVG icon above. overflow: hidden clips the image's corners
 * to match the button's border-radius. */
/* Account button chassis stays at 34×34 with the standard sidebar
 * border + radius. The avatar img inside fills the inside (32×32 =
 * 34 minus 1px border each side) and uses border-radius 7 to match
 * the button's 8px corner minus the 1px border, so the avatar reads
 * as "filling" the button without a corner gap. */
.reader-account-avatar {
  width: 32px;
  height: 32px;
  display: block;
  object-fit: cover;
  border-radius: 7px;
}
/* iOS-style notification badge — small dark-red circle pinned to the
 * scroll-top button's bottom-right corner, overlapping by roughly a
 * quarter of its diameter. Shows the count of pending new posts that
 * arrived while the user is mid-scrolled. The scroll-top button is
 * `position: relative` (default isn't, but we promote it here) so the
 * absolute badge anchors to it. */
.reader-scroll-top { position: relative; }
.reader-scroll-top-badge {
  position: absolute;
  /* Negative offsets pull the badge so ~25% of it sits inside the
   * button — matches the iOS folder badge look. */
  right: -5px;
  bottom: -5px;
  min-width: 14px;
  height: 14px;
  /* Digits in most fonts sit slightly above the geometric centre of
   * the line-box because the ascender is taller than the descender.
   * 1px of padding-top counter-balances that so single-digit counts
   * read as truly centred. */
  padding: 1px 4px 0;
  border-radius: 999px;
  background: #b91c1c;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Thin matching-bg ring so the badge reads as a separate chip
   * against the sidebar / button surface. */
  box-shadow: 0 0 0 1.5px var(--bg);
  pointer-events: none;
}
.reader-scroll-top-badge[hidden] { display: none; }

/* Unread-notifications badge on the bell sidebar button — same
 * chip shape and palette as the scroll-top badge so the two
 * counters read as a consistent visual idiom. Hidden when count
 * is 0; updated by the 30s visibility-aware poll loop. */
.reader-notif-tab { position: relative; }
.reader-notif-badge {
  position: absolute;
  right: -5px;
  bottom: -5px;
  min-width: 14px;
  height: 14px;
  padding: 1px 4px 0;
  border-radius: 999px;
  background: #b91c1c;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 0 1.5px var(--bg);
  pointer-events: none;
}
.reader-notif-badge[hidden] { display: none; }

/* "숫자는 표시 안 함" (dot 모드) — 숫자 없는 작은 빨간 점.  notif +
 * scroll-top 두 배지 가 같은 .is-dot 모디파이어 공유.  width/padding/
 * border-radius/font-size 등 숫자 chip 의 modifier 만 덮어쓰고 background
 * (#b91c1c) 와 box-shadow (--bg ring) 는 base 규칙 그대로 상속. */
.reader-scroll-top-badge.is-dot,
.reader-notif-badge.is-dot {
  min-width: 0;
  width: 10px;
  height: 10px;
  padding: 0;
  font-size: 0;
  line-height: 0;
}

/* UI 설정 의 "숫자 배지 {badge} 표시" 라벨 안에 inline 으로 렌더 되는
 * 실제 빨간 pill (숫자 1) preview — 사용자 spec : 괄호 친 부분 은
 * 텍스트 가 아니라 실제 모습 을 그려 줘야 함.  base notif badge 와 같은
 * background (#b91c1c) + box-shadow ring + size 14px height 를 유지
 * 하되, position: absolute 가 아닌 inline-flex 로 텍스트 흐름 안 에
 * 자연 스럽게 흐름. */
.reader-number-badge-preview {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 14px;
  height: 14px;
  padding: 1px 4px 0;
  border-radius: 999px;
  background: #b91c1c;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  vertical-align: middle;
  margin: 0 2px;
  box-shadow: 0 0 0 1.5px var(--bg);
}

/* Notifications feed list — separate from .reader-list because
 * each row mixes compact (like / repost / follow) with full post
 * cards (mention / reply / quote). The compact rows sit in their
 * own pill so they read as "alert chips" against the timeline
 * cards beneath. */
.reader-notif-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.reader-notif-empty {
  padding: 30px 16px;
  text-align: center;
  color: var(--text-muted);
  font-size: 13px;
}
/* Compact alert row used for like / repost / follow. Two stacked
 * rows: a top "who" row (icon + 1 avatar + actor label + time)
 * and an optional subject snippet beneath that spans the full
 * card width with no left indent. */
.reader-notif-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px;
  background: var(--surface);
  border: 2px solid var(--border);
  border-radius: var(--radius);
}
.reader-notif-row.is-unread {
  background: var(--accent-soft);
  border-color: var(--accent-soft);
}
/* Smooth the transition out of the unread tint so when the user
 * clicks a notif card and we drop .is-unread, the colour fades
 * back to the default surface instead of snapping. */
.reader-notif-row,
.reader-notif-card-wrap {
  transition: background-color 0.6s ease-out, border-color 0.6s ease-out;
}
/* Wallpaper card-alpha applies to notification cards too — both
 * the regular --surface fill and the unread --accent-soft tint
 * pick up the user's card-opacity setting so the bell feed reads
 * as a uniform "see wallpaper through cards" surface. */
body[data-reader-bg="1"] .reader-notif-row {
  background: color-mix(
    in srgb, var(--surface) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
body[data-reader-bg="1"] .reader-notif-row.is-unread {
  background: color-mix(
    in srgb, var(--accent-soft) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
/* Per-reason border tinting. Sits AFTER .is-unread so the reason
 * color still reads even when the row is unread (background is the
 * unread cue, border is the reason cue). */
.reader-notif-row.reader-notif-row-like { border-color: var(--notif-like-border); }
.reader-notif-row.reader-notif-row-repost { border-color: var(--notif-repost-border); }
.reader-notif-row.reader-notif-row-follow { border-color: var(--notif-follow-border); }
.reader-notif-row.reader-notif-row-starterpack-joined { border-color: var(--notif-starterpack-border); }
/* Verified / unverified rows paint a full rainbow gradient over the
 * border via the dual-background trick: surface fill clipped to the
 * padding-box sits on top of a saturated rainbow clipped to the
 * border-box, so the 2px border reveals the gradient while the
 * inner card keeps its solid background. is-unread variant swaps
 * the inner fill to the accent-soft tint that other unread rows
 * use, without disturbing the gradient. */
.reader-notif-row.reader-notif-row-verified,
.reader-notif-row.reader-notif-row-unverified {
  border-color: transparent;
  background:
    linear-gradient(var(--surface), var(--surface)) padding-box,
    linear-gradient(90deg, #f87171, #fb923c, #fbbf24, #4ade80, #60a5fa, #a78bfa, #f472b6) border-box;
}
.reader-notif-row.reader-notif-row-verified.is-unread,
.reader-notif-row.reader-notif-row-unverified.is-unread {
  background:
    linear-gradient(var(--accent-soft), var(--accent-soft)) padding-box,
    linear-gradient(90deg, #f87171, #fb923c, #fbbf24, #4ade80, #60a5fa, #a78bfa, #f472b6) border-box;
}
.reader-notif-row-top {
  display: flex;
  /* center-align so the reason glyph + avatar stack stay at the
   * avatar's vertical center even when the actor + verb text wraps
   * to a second line. */
  align-items: center;
  gap: 8px;
  font-size: 13px;
}
/* The time pill is small and reads best when pinned to the top of
 * the row so it doesn't drift down with the centered text block. */
.reader-notif-row-top > .reader-notif-row-time {
  align-self: flex-start;
  margin-top: 2px;
}
.reader-notif-row-icon {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  color: var(--text-muted);
}
/* Row icon currentColor tracks the reason border palette so the
 * inline SVG glyph matches the card's tint without needing a per-
 * reason stroke override. */
.reader-notif-row-icon-like { color: var(--notif-like-border); }
.reader-notif-row-icon-repost { color: var(--notif-repost-border); }
.reader-notif-row-icon-follow { color: var(--notif-follow-border); }
.reader-notif-row-icon-starterpack-joined { color: var(--notif-starterpack-border); }
.reader-notif-row-icon-verified,
.reader-notif-row-icon-unverified { color: var(--notif-verified-border); }
/* Mention / reply / quote also render on .reader-notif-card-wrap;
 * for those, the watermark on the post card carries the visual
 * cue, but when the post hydration failed and we fall back to a
 * compact row, color the row icon with the accent. */
.reader-notif-row-icon-mention,
.reader-notif-row-icon-reply,
.reader-notif-row-icon-quote { color: var(--accent); }
.reader-notif-row-avatar {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
}
.reader-notif-row-avatar-fallback {
  background: var(--border-strong);
  color: var(--text-muted);
}

/* Avatar stack for grouped like / repost rows. Each successive
 * circle pulls left by 10 px on top of the previous one's right
 * edge, giving the "(((( O" silhouette the user described — the
 * rightmost author (the representative, last in source order) sits
 * fully visible on top of the stack, the rest peek out as
 * partially-occluded left edges behind it. The surface-colored
 * ring around every circle keeps the overlap readable against
 * neighbours. */
.reader-notif-avatars {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
}
.reader-notif-avatars > .reader-notif-row-avatar {
  box-shadow: 0 0 0 2px var(--surface);
}
.reader-notif-row.is-unread .reader-notif-avatars > .reader-notif-row-avatar {
  box-shadow: 0 0 0 2px var(--accent-soft);
}
.reader-notif-avatars > .reader-notif-row-avatar + .reader-notif-row-avatar {
  margin-left: -10px;
}
/* Z-index ladder so the representative (last in source order =
 * rightmost) always paints on top of the others, even when it
 * happens to be the fallback placeholder. position:relative is
 * required for z-index to kick in. */
.reader-notif-avatars > .reader-notif-row-avatar { position: relative; }
.reader-notif-avatars > .reader-notif-row-avatar:nth-last-child(1) { z-index: 4; }
.reader-notif-avatars > .reader-notif-row-avatar:nth-last-child(2) { z-index: 3; }
.reader-notif-avatars > .reader-notif-row-avatar:nth-last-child(3) { z-index: 2; }
.reader-notif-avatars > .reader-notif-row-avatar:nth-last-child(4) { z-index: 1; }

/* ── Avatar fallback person silhouette ────────────────────────────
 * Any element with a class containing "avatar-fallback" gets a
 * person silhouette painted by a ::before pseudo-element using
 * mask-image. Lets us keep all the per-context fallback classes
 * (.reader-avatar-fallback, .masking-post-avatar-fallback, etc.)
 * sized + positioned by their own rules while sharing one
 * silhouette glyph — currentColor on the mask makes it theme-
 * aware. */
[class*="avatar-fallback"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
[class*="avatar-fallback"]::before {
  content: '';
  display: block;
  width: 64%;
  height: 64%;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='8' r='4'/%3E%3Cpath d='M4 21v-1a8 8 0 0 1 16 0v1'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='8' r='4'/%3E%3Cpath d='M4 21v-1a8 8 0 0 1 16 0v1'/%3E%3C/svg%3E") center/contain no-repeat;
}
.reader-notif-row-actor {
  flex: 1;
  min-width: 0;
  color: var(--text);
  line-height: 1.4;
  /* Allow the actor + verb phrase to wrap to a second line when
   * it doesn't fit. word-break: keep-all keeps Korean word units
   * intact (breaks only at spaces) so "Alice님이 내 포스트를" stays
   * legible across the wrap point. overflow-wrap: anywhere is a
   * safety net for unbroken display names. */
  white-space: normal;
  word-break: keep-all;
  overflow-wrap: anywhere;
}
/* Wrap around (avatars + actor label) — clickable. Single author →
 * profile modal; 2+ grouped → titleless authors-list modal. */
.reader-notif-row-actor-target {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  cursor: pointer;
  border-radius: 6px;
  padding: 2px 4px;
  margin: -2px -4px;
}
.reader-notif-row-actor-target:hover {
  background: var(--surface-hover);
}
.reader-notif-row-actor-target:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.reader-notif-row-actor-target .reader-notif-row-actor {
  flex: 1;
  min-width: 0;
}
.reader-notif-row-subject {
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-muted);
  /* No left-indent — spans the row's full content width. Clamp at
   * three lines so a giant subject post doesn't dominate the
   * column. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.reader-notif-row-time {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: 11px;
  color: var(--text-faint);
  white-space: nowrap;
}

/* mention / reply / quote wrap a full renderPostCard. mention and
 * quote drop the explicit "X mentioned/quoted you" header and
 * paint their type via the watermark stamped on the post card
 * itself (see .reader-card-watermark above + attachCardWatermark
 * in reader.js). Reply keeps its inline header. */
.reader-notif-card-wrap { position: relative; }
/* mention / reply / quote 알림 은 틴트 가 wrap 이 아 니 라 내부 .reader-card
 * 에 칠 해 진 다 (위 .reader-notif-card-wrap.is-unread .reader-card).  wrap 의
 * transition 은 카드 배경 에 닿 지 않 으 므 로, unseen→seen 토글 시 색 이 부드럽
 * 게 fade 하 도록 내부 카드 에 도 같 은 0.6s background transition 을 둔 다.
 * 알림 card-wrap 안 의 카드 로 스코프 — 일반 피드 카드 엔 영향 없 음. */
.reader-notif-card-wrap .reader-card {
  transition: background-color 0.6s ease-out;
}
.reader-notif-card-wrap.is-unread .reader-card {
  background: var(--accent-soft);
}
/* Wallpaper alpha for unread mention / reply / quote notification
 * cards. The (0,3,0)-specificity `.reader-notif-card-wrap.is-unread
 * .reader-card` rule above out-specs the generic
 * `body[data-reader-bg="1"] .reader-card` color-mix rule (0,2,1), so
 * the unread variant was painting a fully-opaque accent-soft tint
 * over the wallpaper. Match the same color-mix treatment used by
 * the regular unread row variants. */
body[data-reader-bg="1"] .reader-notif-card-wrap.is-unread .reader-card {
  background: color-mix(
    in srgb, var(--accent-soft) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
.reader-feed-tab.is-active {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent-soft);
}
.reader-feed-tab.is-active:hover {
  background: var(--accent-soft);
  color: var(--accent);
}
/* Feed-info block above the timeline — visible only when a custom
 * feed is selected (toggled via the `hidden` attribute). Avatar on
 * the left, displayName + description stacked on the right. */
.reader-feed-header {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px 0 14px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.reader-feed-header[hidden] { display: none; }
/* Header avatar: rounded-square (8px) by default for custom feed
 * avatars (Bluesky styles feed icons that way), and circular for
 * actual profiles via the `is-profile` modifier. */
.reader-feed-header-avatar {
  width: 44px;
  height: 44px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--surface-hover);
  flex: 0 0 44px;
}
.reader-feed-header-avatar.is-profile { border-radius: 50%; }
.reader-feed-header-avatar-fallback {
  background: var(--border-strong);
  color: var(--text-muted);
}
.reader-feed-header-text {
  flex: 1;
  min-width: 0;
}
.reader-feed-header-title {
  font-size: 16px;
  font-weight: 700;
  margin: 0;
  color: var(--text);
}
.reader-feed-header-handle {
  font-size: 12px;
  color: var(--text-muted);
  margin: 2px 0 4px;
}
.reader-feed-header-desc {
  font-size: 13px;
  color: var(--text-muted);
  margin: 0;
  line-height: 1.45;
  white-space: pre-wrap;
  /* Bios occasionally hold unbreakable strings (long percent-encoded
   * URLs, monster handles, etc.). Without an explicit wrap rule those
   * push the parent flex column wider than the modal viewport. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* Custom-feed action row — sits under the description in both the
 * inline feed header and the feed modal. Left cluster (pin / like /
 * share / more) is always shown; right cluster (reorder ↑↓) only
 * mounts when the feed is subscribed. Buttons inherit currentColor
 * so the toggleable .is-on state can swap the icon's fill via the
 * SVG's fill="currentColor" attribute (set in pinIcon/heartIconToggle
 * when filled=true). */
.reader-feed-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.reader-feed-actions-left,
.reader-feed-actions-right {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.reader-feed-action {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 6px 8px;
  color: var(--text-muted);
  cursor: pointer;
  font: inherit;
  line-height: 1;
  transition: background-color 120ms, color 120ms, border-color 120ms;
}
.reader-feed-action:hover:not(:disabled) {
  background: var(--surface-hover, color-mix(in srgb, var(--text) 8%, transparent));
  color: var(--text);
}
.reader-feed-action:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.reader-feed-action.is-on { color: var(--accent, #3b82f6); }
.reader-feed-action.reader-feed-action-like.is-on { color: #ef4444; }
.reader-feed-action-count {
  font-size: 12px;
  line-height: 1;
  min-width: 1ch;
}
/* More-menu dropdown — appended to <body> so it floats above any
 * modal. Positioned via inline style (top/left set in JS off the
 * trigger's bounding rect). width:max-content makes the menu hug
 * the longest row, so single-line labels never wrap and the menu
 * doesn't sit at an arbitrary fixed width that's mostly empty. */
.reader-feed-action-menu {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
  padding: 4px;
  width: max-content;
  max-width: 320px;
  /* 사용자 보고 2026-05-27 : 데스크탑 타인 프로필 모달 의 + 드롭다운 안
   * 보임.  z-index 100 → 1000 — sidebar rail-bg (50) + sidebar buttons
   * (51) + 다른 stacking context 보다 위. */
  z-index: 1000;
}
/* 사용자 보고 2026-05-27 (12 차) : 옛 .reader-feed-action-menu-dialog
 * wrapper (10 차) 제거.  dialog.modal 의 transform 제거 로 containing
 * block reroot 자체 가 사라져 menu 가 simple fixed-positioned div 로 정
 * 상 동작.  외 부 click 은 document mousedown listener 가 처리. */
.reader-feed-action-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  border-radius: 6px;
  padding: 8px 10px;
  font: inherit;
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
  white-space: nowrap;
}
.reader-feed-action-menu-item:hover {
  background: color-mix(in srgb, var(--text) 8%, transparent);
}
.reader-feed-action-menu-item svg {
  color: var(--text-muted);
  flex: 0 0 auto;
}
.reader-feed-action-menu-label { flex: 1 1 auto; }
/* ── Feed modal ──────────────────────────────────────────────────
 * Single-instance dialog that mirrors the inline custom-feed pane
 * (header + action row + paginated post list). Triggered from
 * embedded-feed clicks in posts and from feed-search results. */
.reader-feed-modal,
.reader-profile-modal {
  width: min(560px, 92vw);
  max-height: 86vh;
  padding: 0;
  /* Lift the modal off the page background. Using --surface (post-
   * card colour) instead of --bg gives a visible step in both
   * themes — light mode goes #f8fafc → #ffffff, dark mode
   * #0b1220 → #15213a. The border uses --border-strong (a step
   * lighter than --border) so the edge reads against --surface
   * even with the chunky shadow underneath. */
  border: 1.5px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
  color: var(--text);
  overflow: hidden;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
}
.reader-feed-modal::backdrop,
.reader-profile-modal::backdrop { background: rgba(0, 0, 0, 0.6); }
.reader-profile-modal-body {
  position: relative;
  max-height: 86vh;
  overflow-y: auto;
  /* Defensive — even with overflow-wrap on the bio, embedded post
   * cards inside the profile modal can carry their own unbreakable
   * URLs. Clip horizontally at the modal edge instead of letting
   * the dialog widen past the viewport. */
  overflow-x: hidden;
  padding: 0;
  /* `contain` — navigator-level pull-to-refresh (페이지 reload) 는 차
   * 단 (코멘트 의 원래 의도) 하 면 서, 모달 안 의 over-scroll bounce
   * 자체 는 시각 적 으 로 살림.  사용자 spec (2026-05-26) : "피드 모달
   * 처럼 아래 로 같이 드래그 도 되 게".  `none` 이 던 시절 은 사용자
   * 가 pull-down 해도 visual 변화 없 어 "스와이프 인식 안 됨" 으 로
   * 느낌. */
  overscroll-behavior: contain;
}
/* Inside the profile modal, the .reader-feed-header carries the
 * full bio + counts + action row — no border-bottom override
 * needed (it's a self-contained block). The post-list spacing
 * matches the feed modal. */
.reader-profile-modal-header { display: block; }
.reader-profile-modal-header .reader-feed-header { border-bottom: 1px solid var(--border); }
/* Edge-to-edge banner: drop the inline-context vertical padding so
 * the banner background paints flush against the modal top/sides,
 * and move the content padding onto .reader-feed-header-inner so the
 * avatar + name don't touch the very edge. */
.reader-profile-modal-header .reader-feed-header {
  padding: 0;
  margin-bottom: 0;
}
.reader-profile-modal-header .reader-feed-header-inner {
  padding: 6px 8px 4px;
}
/* Wider post cards: zero the inset on the modal's posts list so each
 * post-card spans the full modal width. Small top-padding so the
 * first card doesn't visually merge into the header's bottom
 * border — without this the 1px split sat under the card's own
 * border-top and read as one thick line. */
.reader-profile-modal-body .reader-feed-modal-list {
  padding: 6px 0 0;
}
.reader-profile-modal-body .reader-end {
  padding: 12px 14px;
}
/* Gentle fade-in for each post card as it lands in the profile
 * modal feed. Scoped to .reader-profile-modal-body so the main
 * timeline doesn't pay the animation cost on every card. */
.reader-profile-modal-body .reader-card {
  animation: reader-profile-card-fade-in 0.55s ease-out;
}
@keyframes reader-profile-card-fade-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Hide masked posts during their async restrict check so content
 * the viewer isn't allowed to see doesn't flash for a frame
 * before the check resolves + removes the card. Removing the
 * class restores normal display: block for allowed cards. */
.reader-card.is-masked-pending { display: none !important; }
/* Floating scroll-to-top inside the profile modal. Sticky inside
 * the scrollable body so it pins to the bottom-right of the
 * visible area as the user scrolls — and the modal close box
 * already takes the top-right. */
.reader-profile-scrolltop {
  position: sticky;
  bottom: 16px;
  margin-left: auto;
  margin-right: 12px;
  margin-top: -56px; /* tuck back so it doesn't reserve a row */
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  color: var(--text);
  cursor: pointer;
  font-size: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  transition: opacity 0.2s ease;
}
.reader-profile-scrolltop[hidden] { display: none; }
.reader-profile-scrolltop:hover {
  background: color-mix(in srgb, var(--surface-2) 70%, var(--text));
}
/* Action row spacing inside the profile-modal header. Tightened
 * up against the handle/pills above and the inner padding below
 * so the row doesn't drift to the bottom of the header. */
.reader-profile-modal-header .reader-profile-actions {
  margin-top: 4px;
}
/* Tighten the vertical rhythm of the header text column when it's
 * inside the profile modal — the @handle / pills / bio / actions
 * block was leaving the action row noticeably far from both the
 * @handle above and the bottom of the header. Right-panel
 * defaults (no .reader-profile-modal-header scope) are unchanged. */
.reader-profile-modal-header .reader-feed-header-handle { margin: 1px 0 2px; }
.reader-profile-modal-header .reader-profile-pills { margin: 4px 0; }
.reader-profile-modal-header .reader-feed-header-desc { margin: 0 0 2px; }
/* Avatar + name in post cards become click targets (open the
 * profile modal). cursor:pointer signals the interaction. */
.reader-card .reader-avatar.reader-profile-target { cursor: pointer; }
.reader-card .reader-profile-target { cursor: pointer; }
.reader-post-target[data-post-uri] {
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  text-underline-offset: 2px;
}
.reader-post-target[data-post-uri]:hover {
  text-decoration-color: currentColor;
}
/* Actor feeds / lists submodals — same chassis as the feed modal,
 * just a different title + list shape. */
.reader-actor-list-title {
  margin: 0;
  padding: 6px 8px 4px;
  font-size: 16px;
  border-bottom: 1px solid var(--border);
}
.reader-actor-feeds-list,
.reader-actor-lists-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 6px;
}
/* List card — squarish avatar + name + purpose label + count + bio. */
.reader-list-card {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}
.reader-list-card.is-interactive { cursor: pointer; transition: background-color 120ms; }
.reader-list-card.is-interactive:hover { background: color-mix(in srgb, var(--surface-2) 70%, var(--text)); }
.reader-list-card.is-interactive:focus-visible {
  outline: 2px solid var(--accent, #3b82f6);
  outline-offset: 2px;
}
.reader-list-card-avatar {
  width: 36px;
  height: 36px;
  border-radius: 8px;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--surface);
}
.reader-list-card-avatar-fallback {
  background: linear-gradient(135deg, var(--accent, #3b82f6),
    color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--text)));
}
.reader-list-card-text { flex: 1 1 auto; min-width: 0; }
.reader-list-card-title { font-weight: 700; font-size: 14px; }
.reader-list-card-meta {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 2px;
}
.reader-list-card-desc {
  font-size: 13px;
  color: var(--text-muted);
  margin: 6px 0 0;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* List-as-feed modal header — square avatar + name + meta + bio. */
.reader-feed-header.is-list-header {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px 16px 12px;
  border-bottom: 1px solid var(--border);
}
.reader-feed-header.is-list-header .reader-feed-header-avatar {
  width: 48px;
  height: 48px;
  border-radius: 10px;
}
.reader-feed-header.is-list-header .reader-feed-header-title {
  font-size: 17px;
  margin: 0;
}
/* "내 리스트 관리" — × delete button DOCKED INSIDE the card on the
 * right.  Same pattern as the subscribed-feeds modal's × — keeps the
 * card the only clickable surface so tap → detail modal still works,
 * but the × can stopPropagation to avoid accidentally opening detail
 * when the user wanted to delete. */
.reader-my-list-card {
  position: relative;
  padding-right: 44px;  /* leave room for the × dock */
}
.reader-my-list-delete-btn {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 120ms, color 120ms, border-color 120ms;
}
.reader-my-list-delete-btn:hover {
  background: color-mix(in srgb, #ef4444 12%, transparent);
  color: #ef4444;
  border-color: color-mix(in srgb, #ef4444 35%, var(--border));
}
.reader-my-list-delete-btn:disabled { opacity: 0.5; cursor: progress; }

/* Sticky-ish bottom action bar inside the my-lists modal. */
.reader-my-lists-bottom {
  position: sticky;
  bottom: 0;
  padding: 12px 0 4px;
  background: var(--surface);
  display: flex;
  justify-content: center;
  border-top: 1px solid var(--border);
  margin-top: 12px;
}
.reader-my-lists-create-btn {
  min-width: 160px;
}

/* List CRUD form modal — name input, desc textarea, purpose radio. */
.reader-list-form-modal .modal-body { min-width: min(420px, 92vw); max-width: 520px; }
/* 검색 컬럼 검색어 재설정 모달 : .modal-actions 는 기본 margin-top 이
   0 이 라 입력창 바로 아래 에 버튼 이 붙는다.  입력창 과 취소/변경
   버튼 사이 에 세로 여백 (사용자 보고 2026-05-30). */
.reader-search-edit-modal .modal-actions { margin-top: 14px; }
.reader-list-form-label {
  display: block;
  margin-top: 12px;
  font-size: 13px;
  color: var(--text-muted);
}
.reader-list-form-label > span {
  display: block;
  margin-bottom: 4px;
}
.reader-list-form-input,
.reader-list-form-textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  font-size: 14px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: inherit;
}
.reader-list-form-textarea {
  resize: vertical;
  min-height: 64px;
  line-height: 1.4;
}
.reader-list-form-purpose {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 4px;
}
.reader-list-form-purpose-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: 13px;
  cursor: pointer;
}
.reader-list-form-purpose-row input { margin-top: 3px; }
.reader-list-form-status {
  margin-top: 8px;
  text-align: center;
}

/* Edit button on a list header — sits below the description, docked
 * to the right edge of the header text block so it doesn't crowd the
 * title.  Same treatment for the feed-header edit button. */
.reader-list-edit-btn,
.reader-feed-edit-btn,
.reader-list-share-btn,
.reader-feed-share-btn {
  font-size: 12px;
  padding: 4px 10px;
}
/* Header action row — share + edit (when own).  Right-docks itself
 * inside the header text block so the buttons stack next to each
 * other on the bottom-right of the header, mirroring the list
 * detail header pattern. */
.reader-list-header-actions,
.reader-feed-header-actions {
  display: flex;
  gap: 6px;
  align-self: flex-end;
  margin-top: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* ── 스타터팩 폼 ─────────────────────────────────────────────── */
.reader-starterpack-form-modal .modal-body { min-width: min(440px, 92vw); max-width: 560px; }
.reader-starterpack-members-hint {
  font-size: 12px;
  color: var(--text-muted);
  margin: 0;
}
.reader-starterpack-member-count {
  font-size: 12px;
  color: var(--text-muted);
  margin: 4px 0 6px;
}
.reader-starterpack-member-count.is-ok {
  color: #16a34a;
  font-weight: 600;
}
.reader-starterpack-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 4px 0 8px;
}
.reader-starterpack-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px 4px 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 12px;
  max-width: 220px;
}
.reader-starterpack-chip img {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  flex: 0 0 auto;
  object-fit: cover;
}
.reader-starterpack-chip-handle {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reader-starterpack-chip-remove {
  flex: 0 0 auto;
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}
.reader-starterpack-chip-remove:hover { color: #ef4444; }
.reader-starterpack-search {
  margin-top: 4px;
}
.reader-starterpack-results {
  display: flex;
  flex-direction: column;
  margin-top: 6px;
  max-height: 220px;
  overflow-y: auto;
}
.reader-starterpack-result-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  text-align: left;
  width: 100%;
}
.reader-starterpack-result-row:hover {
  background: color-mix(in srgb, var(--surface-2) 80%, var(--text));
}
.reader-starterpack-result-row.is-added {
  opacity: 0.45;
  cursor: not-allowed;
}
.reader-starterpack-result-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--surface-2);
  flex: 0 0 auto;
  object-fit: cover;
}
.reader-starterpack-result-avatar-fallback {
  display: inline-block;
}
.reader-starterpack-result-text { flex: 1 1 auto; min-width: 0; }
.reader-starterpack-result-name {
  font-weight: 600;
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-starterpack-result-handle {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Member row with × remove button (own list only). */
.reader-list-modal-row-wrap {
  display: flex;
  align-items: stretch;
  gap: 6px;
}
.reader-list-modal-row-wrap > .reader-list-modal-row {
  flex: 1 1 auto;
  min-width: 0;
}
.reader-list-modal-row-remove {
  flex: 0 0 auto;
  width: 32px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 120ms, color 120ms, border-color 120ms;
}
.reader-list-modal-row-remove:hover {
  background: color-mix(in srgb, #ef4444 12%, transparent);
  color: #ef4444;
  border-color: color-mix(in srgb, #ef4444 35%, var(--border));
}
.reader-list-modal-row-remove:disabled { opacity: 0.5; cursor: progress; }

/* "내 피드 관리" — list of subscribed custom feeds with reorder +
 * unsubscribe affordances.  Two modes:
 *   view    — drag handle is invisible (display:none), × shows
 *   reorder — drag handle shows, × is suppressed, row gains drag
 *             cursor.  is-dragging row is dimmed + pointer-events
 *             none so the drag's hit-test skips itself. */
.reader-my-feeds-modal .modal-body { min-width: min(420px, 92vw); max-width: 520px; }
.reader-my-feeds-hint {
  font-size: 12px;
  margin: 4px 0 0;
  color: var(--text-muted);
}
.reader-my-feeds-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 8px;
}
.reader-my-feeds-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  background: color-mix(in srgb, var(--surface-2) 60%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.reader-my-feeds-drag-handle {
  display: none;
  flex: 0 0 auto;
  font-size: 16px;
  color: var(--text-muted);
  cursor: grab;
  user-select: none;
  padding: 2px 4px;
  letter-spacing: -2px;
  /* Block the browser's native touch panning on this element so a
   * touch-drag fires our pointermove instead of scrolling the modal
   * body.  Without this, dragging in a tall (scrollable) modal would
   * scroll instead of reorder — the bug the user reported. */
  touch-action: none;
}
.reader-my-feeds-list.is-reorder-mode .reader-my-feeds-drag-handle {
  display: inline-flex;
  align-items: center;
}
.reader-my-feeds-list.is-reorder-mode .reader-my-feeds-unsub-btn {
  display: none;
}
.reader-my-feeds-row.is-dragging {
  opacity: 0.55;
  pointer-events: none;
  cursor: grabbing;
}
.reader-my-feeds-avatar {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--surface-2);
}
.reader-my-feeds-avatar-fallback {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  color: var(--text-muted);
  font-size: 16px;
  text-transform: uppercase;
}
.reader-my-feeds-text { flex: 1 1 auto; min-width: 0; }
.reader-my-feeds-row-title {
  font-weight: 700;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-my-feeds-desc {
  font-size: 12px;
  color: var(--text-muted);
  margin: 2px 0 0;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.reader-my-feeds-unsub-btn {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 120ms, color 120ms, border-color 120ms;
}
.reader-my-feeds-unsub-btn:hover {
  background: color-mix(in srgb, #ef4444 12%, transparent);
  color: #ef4444;
  border-color: color-mix(in srgb, #ef4444 35%, var(--border));
}
.reader-my-feeds-unsub-btn:disabled { opacity: 0.5; cursor: progress; }
/* Owned-feeds rows are clickable — navigate to that feed's reader
 * view.  Subscribed-feeds rows are not (their click target is the
 * × / drag handle / etc.). */
.reader-my-feeds-row.is-clickable {
  cursor: pointer;
  transition: background-color 120ms;
}
.reader-my-feeds-row.is-clickable:hover {
  background: color-mix(in srgb, var(--surface-2) 80%, var(--text));
}
/* The trash variant uses the same chassis as the × unsub button but
 * with a slightly larger inner padding so the SVG icon centers. */
.reader-my-feeds-trash-btn {
  font-size: 0;  /* hides the text fallback so only the SVG paints */
}
.reader-my-feeds-trash-btn svg {
  width: 16px;
  height: 16px;
}

/* 피드 편집 form — avatar row sits to the left of the picker buttons. */
.reader-feed-form-avatar-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 4px;
  flex-wrap: wrap;
}
.reader-feed-form-avatar-preview {
  width: 48px;
  height: 48px;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  flex: 0 0 auto;
}
.reader-feed-form-avatar-status {
  flex: 1 1 100%;
  font-size: 12px;
  margin: 4px 0 0;
}
.reader-my-feeds-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* Add-to-list picker — each card has the same chassis as the actor
 * lists view, but adds a "추가" button on the right. */
.reader-add-to-list-modal .reader-list-card {
  align-items: center;
}
.reader-add-to-list-btn {
  flex-shrink: 0;
  padding: 6px 12px;
  font-size: 12px;
}
/* Report modal — radio list + comment textarea. */
.reader-report-modal { width: min(440px, 92vw); }
.reader-report-options {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 8px 0 14px;
}
.reader-report-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 8px;
  cursor: pointer;
}
.reader-report-option:hover {
  background: color-mix(in srgb, var(--text) 6%, transparent);
}
.reader-report-comment {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  resize: vertical;
  box-sizing: border-box;
  margin-top: 4px;
  margin-bottom: 12px;
}
.reader-feed-modal-body {
  position: relative;
  max-height: 86vh;
  overflow-y: auto;
  padding: 0;
}
.reader-feed-modal-close {
  position: absolute;
  top: 8px;
  right: 10px;
  z-index: 2;
  width: 32px;
  height: 32px;
  border: 0;
  background: color-mix(in srgb, var(--bg) 80%, var(--text));
  color: var(--text);
  border-radius: 50%;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.reader-feed-modal-close:hover { background: color-mix(in srgb, var(--bg) 60%, var(--text)); }
.reader-feed-modal-header {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 6px 8px 6px;
  border-bottom: 1px solid var(--border);
}
.reader-feed-modal-header .reader-feed-header-avatar { width: 48px; height: 48px; border-radius: 10px; }
.reader-feed-modal-header .reader-feed-header-title { font-size: 17px; margin: 0; }
.reader-feed-modal-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 4px 4px 0;
}
.reader-feed-modal-list .reader-card { margin: 0; }
.reader-feed-modal-status {
  padding: 12px 16px;
  font-size: 13px;
  color: var(--text-muted);
  text-align: center;
}
.reader-feed-modal .reader-end { padding: 12px 16px; }
/* Profile header — own-profile variant gets a faint banner backdrop.
 * The image sits behind everything; an absolutely-positioned overlay
 * mutes it so the text + pills stay readable. .reader-feed-header
 * already has padding + a border-bottom, both preserved. */
.reader-feed-header.is-profile-header {
  position: relative;
  cursor: pointer;
}
.reader-feed-header.has-banner {
  background-size: cover;
  background-position: center;
}
/* When the user has a custom wallpaper enabled, suppress the
 * BANNER PAINT (just the background-image + the dim overlay)
 * on the page-level feed header — that's where the user's own
 * banner shows under FEED_MINE. The wallpaper is a deliberate
 * choice; layering a banner image on top of it fights both
 * visually and tonally. The avatar, name, bio, and action row
 * stay visible because they live as separate DOM children. We
 * scope to `.reader-main` (descendant) so profile MODALS (which sit
 * over the wallpaper inside a dialog) keep their banner intact —
 * the modal's <dialog> is appended to <body>, not inside the
 * reader-main column.  (descendant, not `>`, because the timeline
 * feed-header now lives inside a .reader-main-scroll wrapper.) */
body[data-reader-bg="1"] .reader-main .reader-feed-header.is-profile-header.has-banner {
  background-image: none !important;
}
body[data-reader-bg="1"] .reader-main .reader-feed-header.is-profile-header.has-banner::before {
  display: none;
}
/* Post-card alpha — when wallpaper is enabled, fade the post cards
 * by --reader-card-alpha (set in reader.js's applyBackground from
 * prefs.bgCardOpacity). 1 = fully opaque (matches the pre-toggle
 * default); lower lets the wallpaper bleed through.
 *
 * We can't fade the WHOLE card with `opacity` because that bleeds
 * over text + images too. Instead, paint a fixed --surface
 * background and dial that one paint's alpha down via
 * color-mix() — keeps text crisp at any setting. */
body[data-reader-bg="1"] .reader-card {
  background: color-mix(
    in srgb,
    var(--surface) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
/* 유리(glass) 효과(실험 nabilera:glass) — 벽지 ON + data-reader-glass.
 * 포스트 카드 뿐 아니라 포스트카드와 "같은 반투명도(--reader-card-alpha)"를
 * 받는 top-level 표면(컬럼 배너 / 트렌드 패널 / 알림 행)에도 동일 적용 —
 * 이미 반투명한 표면 뒤 벽지를 backdrop blur + saturate 로 frosted-glass 화 +
 * 가장자리 하이라이트.  벽지가 있어야 의미 있으므로 [data-reader-bg="1"] 와
 * 함께 스코프.  (인용 미니카드 / 검색 칩 등 카드 내부·소형 요소는 중첩
 * backdrop 방지 위해 제외.)  splash 는 무관(스플래시 불변 규칙 유지). */
body[data-reader-bg="1"][data-reader-glass="1"] .reader-card,
body[data-reader-bg="1"][data-reader-glass="1"] .reader-column-banner,
body[data-reader-bg="1"][data-reader-glass="1"] .reader-trend-panel,
body[data-reader-bg="1"][data-reader-glass="1"] .reader-notif-row {
  -webkit-backdrop-filter: blur(var(--reader-glass-blur, 8.4px)) saturate(1.6);
  backdrop-filter: blur(var(--reader-glass-blur, 8.4px)) saturate(1.6);
  border-color: color-mix(in srgb, var(--border) 45%, rgba(255, 255, 255, 0.65));
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.45),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 6px 22px rgba(0, 0, 0, 0.14);
}
[data-theme="dark"] body[data-reader-bg="1"][data-reader-glass="1"] .reader-card,
[data-theme="dark"] body[data-reader-bg="1"][data-reader-glass="1"] .reader-column-banner,
[data-theme="dark"] body[data-reader-bg="1"][data-reader-glass="1"] .reader-trend-panel,
[data-theme="dark"] body[data-reader-bg="1"][data-reader-glass="1"] .reader-notif-row {
  border-color: color-mix(in srgb, var(--border) 55%, rgba(255, 255, 255, 0.28));
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.16),
    0 6px 22px rgba(0, 0, 0, 0.45);
}
/* 유리 굴절(실험 안의 실험) — data-reader-glass-refract 시 backdrop 에 SVG
 * feDisplacementMap(#reader-glass-refract) 을 더해 표면이 굴절되는 liquid-glass
 * 느낌.  표준 backdrop-filter 에만 url() 을 부여 — Chromium 계열만 url()
 * backdrop 지원.  -webkit-backdrop-filter(위 규칙의 블러)는 그대로라 Safari/
 * iOS 는 굴절 없이 블러만(자연 폴백). */
body[data-reader-bg="1"][data-reader-glass="1"][data-reader-glass-refract="1"] .reader-card,
body[data-reader-bg="1"][data-reader-glass="1"][data-reader-glass-refract="1"] .reader-column-banner,
body[data-reader-bg="1"][data-reader-glass="1"][data-reader-glass-refract="1"] .reader-trend-panel,
body[data-reader-bg="1"][data-reader-glass="1"][data-reader-glass-refract="1"] .reader-notif-row {
  backdrop-filter: url(#reader-glass-refract) blur(var(--reader-glass-blur, 8.4px)) saturate(1.6);
}
/* 미리보기 카드 굴절. */
.reader-custom-bg-preview-card.is-glass.is-refract {
  backdrop-filter: url(#reader-glass-refract) blur(var(--preview-glass-blur, 8.4px)) saturate(1.6);
}
/* Tinted card variants (repost / masked / repost+masked) also pick
 * up the wallpaper alpha so the green and pink backdrops dial down
 * uniformly with the rest of the feed instead of staying loud
 * against a faded surface. Each rule mirrors the variant's base
 * color but threads --reader-card-alpha into a color-mix with
 * transparent. */
/* Pinned post — buildPinnedCard adds .is-pinned + an
 * attachCardWatermark('pinned') corner glyph, same family as the
 * mention / quote / reply / repost marks. The post card body itself
 * is otherwise unstyled (no extra highlight) so the watermark alone
 * signals the state. */
.reader-card.is-pinned { position: relative; }
/* Pin watermark reads slightly more present than the interaction
 * marks (0.18 default) because it's a STATE the user set on
 * purpose, not a passive interaction tag. Still subtle enough to
 * stay out of the way of the body. */
.reader-card-watermark.reader-card-watermark-pinned { opacity: 0.32; }
/* 차단 / 뮤트 — STATE 라 interaction off 토글과 무관, 항상 노출.
 * block 은 dim 한 red 톤, mute 는 회색 톤으로 서로 구별 가능. */
.reader-card-watermark.reader-card-watermark-blocked {
  opacity: 0.42;
  color: #b91c1c;
}
.reader-card-watermark.reader-card-watermark-muted {
  opacity: 0.42;
  color: var(--text-muted);
}
body[data-interaction-watermarks="off"] .reader-card-watermark-blocked,
body[data-interaction-watermarks="off"] .reader-card-watermark-muted {
  display: block !important;
}

/* Actor row chip — appendActorBadges 가 actor.viewer 따라 인라인
 * chip 으로 자동 노출.  검색 / 팔로워 / 팔로잉 / 알림 등 모든
 * 곳에서 같은 모양. */
.reader-actor-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-left: 6px;
  padding: 1px 6px 1px 4px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
  line-height: 14px;
  vertical-align: middle;
}
.reader-actor-chip svg { width: 12px; height: 12px; }
.reader-actor-chip-blocked {
  background: #fee2e2;
  color: #b91c1c;
}
[data-theme="dark"] .reader-actor-chip-blocked {
  background: #4c1d1d;
  color: #fca5a5;
}
.reader-actor-chip-muted {
  background: var(--surface-2);
  color: var(--text-muted);
}
/* Profile-level label chip (impersonation / scam / etc.) — small
 * pill next to the author's display name in addition to whatever
 * gate the warn / hide strip surfaces.  Always visible so the user
 * sees which labels an account carries; the warn / hide decision
 * is independent (evaluateLabels picks one strongest gate). */
.reader-actor-chip-label {
  background: color-mix(in srgb, var(--danger, #f85149) 14%, transparent);
  color: var(--danger, #f85149);
}
/* Pin is a state, not an interaction. The data-interaction-watermarks
 * "off" toggle should NOT hide it — even when interaction marks are
 * disabled, the user pinning a post means they want it identified. */
body[data-interaction-watermarks="off"] .reader-card-watermark-pinned {
  display: block;
}
body[data-interaction-watermarks="off"] .reader-card.is-pinned.has-watermark .reader-card-head {
  padding-right: 44px;
}
.reader-action-pin { color: var(--text-muted); }
.reader-action-pin:hover { color: var(--text); background: var(--accent-soft); }
/* Solid variant: applied when this post is currently the user's
 * pinned slot. Color stays in the text palette (NOT the accent blue
 * the feed-subscribe pin uses) so the two pin surfaces — feed
 * subscribe vs pin-my-post — read as different actions even though
 * they share the same Lucide pushpin glyph. paintPinBtn keeps the
 * .is-pinned class in sync with _pinnedPostUri. */
.reader-action-pin.is-pinned { color: var(--text); }
.reader-action-pin.is-pinned .reader-action-glyph svg {
  fill: currentColor;
}

/* Search surfaces — when wallpaper is on, the search input, the
 * SELECTED mode-toggle segment (포스트 / 유저 / 피드), the
 * SELECTED sort segment (최신순 / 인기순), the history chips, and
 * the advanced-options panel all take the same --reader-card-alpha
 * as post cards so the search header doesn't look like an opaque
 * island floating over a semi-transparent feed below. The
 * INACTIVE mode / sort segments already have
 * `background: transparent`, so they bleed naturally. */
body[data-reader-bg="1"] .reader-search-input {
  background: color-mix(
    in srgb, var(--surface) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
body[data-reader-bg="1"] .reader-search-mode-btn.is-active,
body[data-reader-bg="1"] .reader-search-sort-btn.is-active {
  background: color-mix(
    in srgb, var(--surface-hover) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
body[data-reader-bg="1"] .reader-search-history-chip,
body[data-reader-bg="1"] .reader-search-advanced-panel {
  background: color-mix(
    in srgb, var(--surface) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
/* Feed-embed cards (used both as in-post quote embeds AND as feed-
 * search result rows) also pick up the wallpaper alpha so the
 * feed-search list reads as one continuous semi-transparent stack
 * with the post timeline below it. */
body[data-reader-bg="1"] .reader-feed-embed-card {
  background: color-mix(
    in srgb, var(--surface-2) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
/* Hover variant for the interactive feed-embed card — same pattern
 * the .reader-card[data-post-uri] hover uses: gated to mouse-only
 * (@media hover: hover) so touch doesn't get the sticky-opaque
 * paint after a tap, AND nested color-mix so the hover tint is
 * blended into --reader-card-alpha instead of stomping it. */
@media (hover: hover) {
  body[data-reader-bg="1"] .reader-feed-embed-card.is-interactive:hover {
    background: color-mix(
      in srgb,
      color-mix(in srgb, var(--surface-2) 70%, var(--text))
        calc(var(--reader-card-alpha, 1) * 100%),
      transparent
    );
  }
}

body[data-reader-bg="1"] .reader-card.is-repost {
  background: color-mix(
    in srgb, var(--card-repost-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
[data-theme="dark"] body[data-reader-bg="1"] .reader-card.is-repost {
  background: color-mix(
    in srgb, var(--card-repost-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
/* Masked-card wallpaper variants — thread --card-masked-bg
 * (precomputed solid wash) through --reader-card-alpha so the
 * wallpaper bleeds through at the user's chosen opacity. Solid
 * bg matches the non-wallpaper case (no alpha-stacking glitch). */
body[data-reader-bg="1"] .reader-card.is-masked {
  background: color-mix(
    in srgb, var(--card-masked-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
[data-theme="dark"] body[data-reader-bg="1"] .reader-card.is-masked {
  background: color-mix(
    in srgb, var(--card-masked-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
body[data-reader-bg="1"] .reader-card.is-repost.is-masked {
  background: color-mix(
    in srgb, var(--card-repost-masked-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
[data-theme="dark"] body[data-reader-bg="1"] .reader-card.is-repost.is-masked {
  background: color-mix(
    in srgb, var(--card-repost-masked-bg) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
/* Wallpaper option-modal sliders pick up the same accent the view
 * button uses, so the controls + the activation surface read as a
 * matched palette instead of a default browser-blue range thumb
 * sitting next to a custom button. */
.reader-custom-bg-opacity { accent-color: var(--accent); }
/* Wallpaper view-button — small floating action in the bottom-right
 * that hides every overlay so the user can see the wallpaper as a
 * standalone image. Opt-in via prefs.bgViewButton; only mounted
 * when a wallpaper is set + the option is on (reader.js handles
 * mount/unmount). */
.reader-bg-view-btn {
  position: fixed;
  right: 18px;
  bottom: 18px;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,0.25);
  z-index: 1000;
  transition: transform 0.15s ease, opacity 0.25s ease;
}
.reader-bg-view-btn:hover { transform: scale(1.06); }
.reader-bg-view-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* 액션 버튼 레이아웃 엔진 이 hidden 속성 으로 숨김 — display:none 으로
 * 확실 히 (clazz+attr 명시도 로 base display 를 이김, tts-fab 와 동일 패턴). */
.reader-bg-view-btn.nabilera-action-fab[hidden] { display: none; }
/* View mode — hide every overlay so the user sees just the
 * wallpaper. The button itself stays visible so they can tap it
 * again to restore the UI; everything else (header, sidebar, feed,
 * modals, etc.) fades out. */
body.reader-bg-view-mode .site-header,
body.reader-bg-view-mode .site-footer,
body.reader-bg-view-mode main,
body.reader-bg-view-mode dialog.modal,
body.reader-bg-view-mode .nabilera-tts-fab {
  visibility: hidden;
}
body.reader-bg-view-mode .reader-bg-view-btn { opacity: 0.5; }
body.reader-bg-view-mode .reader-bg-view-btn:hover { opacity: 1; }
/* Wallpaper itself stays visible at full setting opacity while in
 * view mode — we want the user to see what they're really looking
 * at, not the dimmed-by---reader-bg-opacity version. */
body[data-reader-bg="1"].reader-bg-view-mode::before {
  opacity: 1 !important;
}
/* 사용자 보고 2026-05-28 : 배경 감상 모드 에 서 좌측 사이드바 strip 만
 * 원본(전체 opacity wallpaper) 과 다 르 게 어둡 게 보 임.  rail-bg 는
 * document.body 직속 (main 밖) 이 라 `main { visibility: hidden }` 에 안
 * 잡히 고, 자체 의 불투명 var(--bg) + ::before wallpaper(opacity 0.4) 가
 * 좌측 을 muted 시 킴.  fix : 감상 모드 에 서 는 rail-bg (+ 같 은 body
 * 직속 overlay 인 컬럼 인디케이터) 도 숨 겨 body::before (opacity 1) 가
 * viewport 전체 에 균일 하 게 보 이 도 록. */
body.reader-bg-view-mode .reader-sidebar-rail-bg,
body.reader-bg-view-mode .reader-column-indicator {
  visibility: hidden;
}
.reader-feed-header.has-banner::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--bg);
  opacity: 0.78;
  pointer-events: none;
  z-index: 0;
}
.reader-feed-header-inner {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
}
.reader-feed-header-inner .reader-feed-header-text {
  flex: 1;
  min-width: 0;
}
/* Pills (followers / following / posts) — single horizontal row that
 * sits between @handle and the bio. Clickable pills get a hover lift;
 * the static "posts" pill stays muted. */
.reader-profile-pills {
  display: flex;
  flex-direction: row;
  gap: 6px;
  /* Symmetric margins so the gap above (between @handle and pills)
   * matches the gap below (between pills and bio). */
  margin: 6px 0;
  flex-wrap: wrap;
}
.reader-profile-pill {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.reader-profile-pill:hover:not(.is-static) {
  background: var(--surface-hover);
  border-color: var(--border-strong);
  color: var(--text);
}
.reader-profile-pill.is-static { cursor: default; }
.reader-profile-pill svg { display: block; }
.reader-profile-pill-count { font-variant-numeric: tabular-nums; }
/* Followers / following list modal — scrollable column of profile
 * rows. Each row opens that profile externally for now (TODO: in-tool
 * profile modal). */
.reader-list-dialog .modal-body {
  max-height: 80dvh;
  overflow-y: auto;
  /* Narrow the gap between the list rows and the modal edges —
   * the default 22px global modal-body padding leaves the inside
   * feeling cavernous for a tight scroll list. */
  padding: 8px 8px 6px;
}
/* Header row of the followers / following modal — title on the left,
 * exact count on the right. Pills round (1.5K / 23M) but the modal
 * gets the full figure (e.g. "1,523" or "23,401,234"). */
.reader-list-dialog-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 12px;
}
.reader-list-dialog-head h3 { margin: 0; }
.reader-list-dialog-count {
  color: var(--text-muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
/* Engagements 모달 의 close 버튼 — 헤더 우측 의 ×.  cleaner-modal /
 * compose-modal close 버튼 과 같은 sizing. */
.reader-list-dialog-close {
  appearance: none;
  background: transparent;
  border: 0;
  width: 32px;
  height: 32px;
  font-size: 20px;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.reader-list-dialog-close:hover { background: var(--surface-hover); color: var(--text); }
/* Engagements 의 두 탭 (리포스트 / 좋아요) — segmented tab bar */
.reader-engagements-tabs {
  display: flex;
  gap: 4px;
  margin: 0 0 12px;
  border-bottom: 1px solid var(--border);
}
.reader-engagements-tab {
  appearance: none;
  background: transparent;
  border: 0;
  padding: 8px 14px;
  font: inherit;
  font-size: 14px;
  color: var(--text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.reader-engagements-tab:hover { color: var(--text); }
.reader-engagements-tab.is-active {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}
.reader-engagements-status { margin: 8px 0 0; text-align: center; }
.reader-list-modal-handle { color: var(--text-muted); font-size: 13px; }
.reader-list-modal-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0 -6px;
  padding: 4px 6px;
}
.reader-list-modal-row {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border: 0;
  background: transparent;
  border-radius: 8px;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  min-width: 0;
}
.reader-list-modal-row:hover { background: var(--surface-hover); }
.reader-list-modal-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-hover);
  flex: 0 0 36px;
}
.reader-list-modal-avatar-fallback { background: var(--border-strong); color: var(--text-muted); }
.reader-list-modal-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.reader-list-modal-text strong {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-list-modal-rel-slot {
  margin-left: 6px;
}
.reader-list-modal-rel-slot:empty { display: none; }
.reader-list-modal-handle {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Per-actor counts row under the @handle — populated
 * asynchronously after a getProfiles batch resolves. Empty
 * container has zero height (no min-height reserve), so rows
 * with no detailed view yet sit at their natural height; once
 * pills land they slot in below. Shared between the follower/
 * follow modal and the user-search typeahead results. */
.reader-list-modal-pills,
.reader-search-user-pills {
  display: flex;
  flex-direction: row;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 4px;
}
.reader-list-modal-pills:empty,
.reader-search-user-pills:empty { margin-top: 0; }
.reader-list-modal-pills .reader-profile-pill,
.reader-search-user-pills .reader-profile-pill {
  padding: 2px 6px;
  font-size: 11px;
}
/* Profile edit modal — minimal form (displayName + bio). Avatar /
 * banner upload is a TODO. */
.reader-edit-dialog .modal-body { min-width: min(420px, 92vw); }
.reader-edit-dialog .field-label {
  display: block;
  font-size: 12px;
  color: var(--text-muted);
  margin: 14px 0 6px;
}
.reader-edit-dialog .input { width: 100%; }
.reader-edit-dialog textarea.input {
  resize: vertical;
  font: inherit;
}
.reader-edit-dialog .hint.err { color: var(--danger); margin: 8px 0 0; }
/* ── 트렌드 토픽 패널 (2026-06-02 공개) — 피드 상단 접이식 ──────────────── */
.reader-trend-panel {
  margin: 0 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2, var(--surface));
  padding: 8px 10px;
}
/* 커스텀 배경(wallpaper) 시 트렌드 패널도 포스트카드와 동일한 알파
 * (--reader-card-alpha = prefs.bgCardOpacity)로 페이드 — 카드만 비치고
 * 트렌드만 불투명하던 불일치 수정(사용자 spec 2026-06-03).  포스트카드
 * (10094)와 같은 color-mix 패턴, 패널 고유색(--surface-2)에 알파만 적용. */
body[data-reader-bg="1"] .reader-trend-panel {
  background: color-mix(
    in srgb,
    var(--surface-2, var(--surface)) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
/* 헤더 줄 전체 가 토글(가운데 정렬) — "🔥 트렌드 ▾".  누르 면 접기/펼치기. */
.reader-trend-header {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; border: 0; background: transparent; cursor: pointer;
  padding: 2px 0; color: var(--text-muted); font: inherit;
}
.reader-trend-header:hover { color: var(--text); }
.reader-trend-title { font-size: 13px; font-weight: 700; color: inherit; }
/* 역삼각형(▾) — 열림 시 180° 회전 으 로 위 를 향하 게(애니메이션). */
.reader-trend-caret {
  font-size: 9px; line-height: 1; transform: rotate(0deg);
  transition: transform .25s ease;
}
.reader-trend-panel:not(.is-collapsed) .reader-trend-caret { transform: rotate(180deg); }
/* 칩 영역 — 접힘 시 max-height/opacity 로 부드럽게 열고 닫음. */
.reader-trend-chips {
  display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
  overflow: hidden; max-height: 300px; opacity: 1;
  transition: max-height .3s ease, opacity .25s ease, margin-top .3s ease;
}
.reader-trend-panel.is-collapsed .reader-trend-chips {
  max-height: 0; opacity: 0; margin-top: 0;
}
@media (prefers-reduced-motion: reduce) {
  .reader-trend-caret, .reader-trend-chips { transition: none; }
}
.reader-trend-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border);
  font: inherit; font-size: 12px; color: var(--text); white-space: nowrap;
  max-width: 100%; cursor: pointer;
}
.reader-trend-chip:hover { background: var(--surface-hover, var(--surface-2)); border-color: var(--accent); }
/* 해시태그 칩 강조(.is-tag accent) 제거 — 일반 단어와 똑같이 표시(사용자 요청 2026-06-03). */
.reader-trend-word { overflow: hidden; text-overflow: ellipsis; }
/* 단어당 숫자(.reader-trend-count)는 표시하지 않음(사용자 요청 2026-06-02). */
/* 트렌드 단어 모달 — 그 단어 가 뽑힌 글 카드 목록. */
/* 전역 dialog.modal 의 max-width:420 가 modal-body 의 min-width:520 을 잘라
 * 내용(제목/카드)이 좌측으로 넘쳐 잘리던 문제 → 트렌드 모달은 560 까지 허용. */
dialog.modal.reader-trend-modal { max-width: 560px; }
.reader-trend-modal .modal-body { min-width: min(520px, 92vw); max-width: 560px; }
.reader-trend-modal-list { display: flex; flex-direction: column; gap: 12px; margin-top: 4px; }
.reader-trend-item { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.reader-trend-item-meta {
  font-size: 12px; font-weight: 600; color: var(--accent);
  padding-left: 2px; letter-spacing: -0.01em;
}

.reader-list {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* No top margin — the first card aligns with the home tab in the
   * sidebar (both at ~viewport top + container padding). */
  margin: 0 0 12px;
}
/* Author display:flex above wins over the UA stylesheet's
 * [hidden] { display: none }, so a "hidden" but flex-sized empty
 * .reader-list still contributed its 12px bottom margin and pushed
 * the notif / search panels 12px lower than the home button.
 * Re-assert display:none for the hidden state so the margin is
 * stripped along with the layout. */
.reader-list[hidden] { display: none; }
/* (v1.6.0 공개) 북마크 폴더 탭 바 — 전체 + 폴더들 + (+).  가로 스크롤. */
.reader-bookmark-tabs-host { margin: 0 0 8px; }
.reader-bookmark-tabs-host[hidden] { display: none; }
/* 패시브 컬럼 안의 탭바 — 컬럼 콘텐츠 좌우 여백에 맞춤. */
.reader-bookmark-tabs-host-col { padding: 4px 8px 0; margin: 0; }
.reader-bookmark-tabs {
  display: flex; align-items: center; gap: 6px;
  overflow-x: auto; scrollbar-width: none; padding: 2px 0;
}
.reader-bookmark-tabs::-webkit-scrollbar { display: none; }
.reader-bookmark-tab {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 10px; border-radius: 999px; white-space: nowrap;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-muted);
  font: inherit; font-size: 13px; cursor: pointer; flex: 0 0 auto;
}
.reader-bookmark-tab:hover { color: var(--text); border-color: var(--accent); }
.reader-bookmark-tab.is-active {
  background: var(--accent-soft, color-mix(in srgb, var(--accent) 16%, transparent));
  border-color: var(--accent); color: var(--accent); font-weight: 700;
}
.reader-bookmark-tab-x {
  border: 0; background: transparent; color: inherit; cursor: pointer;
  font-size: 13px; line-height: 1; padding: 0 0 0 2px; opacity: 0.6;
}
.reader-bookmark-tab-x:hover { opacity: 1; color: var(--danger); }
.reader-bookmark-tab-add {
  flex: 0 0 auto; width: 28px; height: 28px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px dashed var(--border-strong); background: transparent; color: var(--text-muted);
  line-height: 1; cursor: pointer; padding: 0; position: relative;
  /* '+' 텍스트 글리프는 폰트 메트릭상 line-box 안에서 위로 치우쳐(math axis)
   * flex 센터로도 원 정중앙이 안 됐음(사용자 보고 2026-06-04).  텍스트를
   * 숨기고(font-size:0) 십자(+)를 pseudo-element 두 막대로 그려 폰트와
   * 무관하게 픽셀 단위 정중앙에 배치. */
  font-size: 0;
}
.reader-bookmark-tab-add::before,
.reader-bookmark-tab-add::after {
  content: ''; position: absolute; top: 50%; left: 50%;
  background: currentColor; border-radius: 1px;
}
.reader-bookmark-tab-add::before { width: 11px; height: 1.6px; transform: translate(-50%, -50%); }
.reader-bookmark-tab-add::after  { width: 1.6px; height: 11px; transform: translate(-50%, -50%); }
.reader-bookmark-tab-add:hover { color: var(--accent); border-color: var(--accent); }
/* 폭은 dialog 에 걸어야 오른쪽 공백이 안 생긴다 (base dialog.modal 은
 * 420px — body 에만 max-width 를 주면 dialog 가 420 인 채 body 만 좁아져
 * 오른쪽 죽은 공간.  게임선택/트렌드 모달과 같은 부류의 실수). */
dialog.modal.reader-bm-folder-modal,
dialog.modal.reader-bm-picker-modal,
dialog.modal.reader-bm-remove-modal { max-width: 360px; }
.reader-bm-folder-modal .modal-body,
.reader-bm-picker-modal .modal-body,
.reader-bm-remove-modal .modal-body { min-width: 0; max-width: none; }
.reader-bm-folder-input {
  width: 100%; box-sizing: border-box; margin: 6px 0 14px;
  padding: 9px 11px; border-radius: 10px; border: 1px solid var(--border-strong);
  background: var(--bg); color: var(--text); font: inherit;
}
.reader-bm-picker-list { display: flex; flex-direction: column; gap: 2px; margin: 8px 0 14px; }
.reader-bm-picker-row { padding: 6px 4px; border-radius: 8px; cursor: pointer; }
.reader-bm-picker-row:hover { background: var(--surface-hover, var(--surface-2)); }
/* (v1.6.0 공개) 북마크된 글의 북마크 버튼 → 해제/폴더추가 선택 모달. */
.reader-bm-remove-radios { display: flex; flex-direction: column; gap: 2px; margin: 4px 0 8px; }
.reader-bm-remove-radio { display: flex; align-items: center; gap: 8px; padding: 8px 6px; border-radius: 8px; cursor: pointer; }
.reader-bm-remove-radio:hover { background: var(--surface-hover, var(--surface-2)); }
.reader-bm-remove-folders {
  margin: 0 0 12px 26px;
  padding-left: 10px;
  border-left: 2px solid var(--border);
  display: flex; flex-direction: column; gap: 2px;
}
.reader-bm-remove-folders[hidden] { display: none; }
/* (v1.6.0 공개) "여기까지 읽음" 구분선 — 새 글과 지난 글 경계.  "—— 여기까지 읽음 ——". */
.reader-read-line {
  display: flex; align-items: center; gap: 10px;
  margin: 0 2px; user-select: none;
}
.reader-read-line::before, .reader-read-line::after {
  content: ''; flex: 1; height: 1px;
  background: color-mix(in srgb, var(--accent) 45%, transparent);
}
.reader-read-line-label {
  color: var(--accent); font-size: 12px; font-weight: 700; white-space: nowrap;
  letter-spacing: -0.01em;
}
/* 1.6 업데이트 안내 모달 — 짧은 텍스트 + '공식 계정' 링크 + 확인 버튼.
 * ⚠ 폭은 dialog 에 건다(base dialog.modal 은 width:calc(100%-32px) +
 * max-width:420 라 박스가 420 까지 채워짐 — body 에만 max-width 를 걸면
 * dialog 는 420 그대로라 오른쪽에 빈 공간이 생긴다.  line 10740 참고). */
dialog.modal.reader-update-notice-modal { max-width: 360px; }
.reader-update-notice-body {
  display: flex; flex-direction: column; gap: 16px;
  padding: 22px 22px 20px;
}
.reader-update-notice-text {
  margin: 0; font-size: 15px; line-height: 1.6; color: var(--text);
  word-break: keep-all;
}
.reader-update-notice-link {
  appearance: none; border: 0; background: none; padding: 0; margin: 0;
  font: inherit; color: var(--accent); font-weight: 700; cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.reader-update-notice-link:hover { opacity: 0.82; }
.reader-update-notice-ok {
  align-self: flex-end; appearance: none; cursor: pointer;
  padding: 8px 20px; border-radius: 999px; border: 0;
  background: var(--accent); color: #fff; font: inherit; font-weight: 700;
}
.reader-update-notice-ok:hover { opacity: 0.9; }

/* ── 계정 메모 (1.7 실험) ─────────────────────────────────────────────
 * 디스플레이 네임 옆 "(메모)" 작고 옅은 태그(카드/알림/검색/인용/프로필) +
 * 프로필 모달의 메모 편집 버튼 + 편집 다이얼로그. */
.reader-memo-tag {
  font-size: 0.82em; font-weight: 400;
  color: var(--text-muted); opacity: 0.8;
  margin-left: 4px; white-space: nowrap;
}
.reader-memo-tag[hidden] { display: none; }
.reader-memo-profile-control {
  display: inline-flex; align-items: center; gap: 2px;
  margin-left: 6px; vertical-align: middle;
}
/* 프로필 헤더 안 메모 태그는 h2(타이틀) 폰트가 커서 살짝 더 작게. */
.reader-memo-profile-control .reader-memo-tag { font-size: 0.6em; margin-left: 0; }
.reader-memo-edit-btn {
  appearance: none; border: 0; background: transparent; padding: 3px; cursor: pointer;
  color: var(--text-muted); display: inline-flex; align-items: center;
  border-radius: 6px; line-height: 0;
}
.reader-memo-edit-btn:hover { color: var(--accent); background: var(--surface-hover); }
.reader-memo-edit-btn svg { width: 14px; height: 14px; display: block; }
/* ⚠ 폭은 dialog 에 건다(.modal-body 아님 — 오른쪽 빈 공간 방지, CLAUDE.md 규칙). */
dialog.modal.reader-memo-modal { max-width: 340px; }
.reader-memo-body { display: flex; flex-direction: column; gap: 12px; padding: 20px; }
.reader-memo-modal-title { margin: 0; font-size: 15px; font-weight: 700; color: var(--text); }
.reader-memo-input {
  width: 100%; box-sizing: border-box; padding: 9px 11px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  font: inherit; font-size: 14px;
}
.reader-memo-input:focus { outline: none; border-color: var(--accent); }
.reader-memo-actions { display: flex; justify-content: flex-end; gap: 8px; }
.reader-memo-save, .reader-memo-cancel {
  appearance: none; cursor: pointer; padding: 7px 16px; border-radius: 999px;
  font: inherit; font-weight: 700;
}
.reader-memo-save { border: 0; background: var(--accent); color: #fff; }
.reader-memo-cancel { border: 1px solid var(--border); background: transparent; color: var(--text-muted); }
.reader-memo-save:hover { opacity: 0.9; }
.reader-memo-cancel:hover { color: var(--text); }

/* ── 예전 핸들 보기 (1.7 실험) ────────────────────────────────────────
 * ⚠ 폭은 dialog 에 (CLAUDE.md 규칙). */
dialog.modal.reader-handle-history-modal { max-width: 380px; }
.reader-handle-history-body { padding: 20px; }
.reader-handle-history-title { margin: 0 0 14px; font-size: 15px; font-weight: 700; color: var(--text); padding-right: 24px; }
.reader-handle-history-list { display: flex; flex-direction: column; gap: 8px; }
.reader-handle-history-status { margin: 6px 0; color: var(--text-muted); font-size: 14px; }
.reader-handle-history-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 8px 11px; border-radius: 8px; background: var(--surface-2);
}
.reader-handle-history-row.is-current {
  background: var(--accent-soft, color-mix(in srgb, var(--accent) 14%, transparent));
}
.reader-handle-history-hint { margin: -6px 0 4px; font-size: 12px; color: var(--text-muted); }
.reader-handle-history-handle { font-weight: 600; color: var(--text); word-break: break-all; }
.reader-handle-history-row.is-current .reader-handle-history-handle { color: var(--accent); }
.reader-handle-history-current-badge {
  margin-left: 6px; font-size: 11px; font-weight: 700; color: var(--accent);
  background: var(--accent-soft, color-mix(in srgb, var(--accent) 16%, transparent));
  padding: 1px 7px; border-radius: 999px; vertical-align: middle;
}
.reader-handle-history-date { flex: 0 0 auto; font-size: 12px; color: var(--text-muted); white-space: nowrap; }

/* ── 민감 정보 암호화 저장 (1.7 실험) — 행 구조는 PDS 동기화 행과 공유
 *    (reader-pds-sync-row / -inner / .reader-pds-sync-status) 로 스타일 일치.
 *    미지원 안내만 danger 색(specificity 위해 행 클래스로 한정). */
.reader-tokenenc-row .reader-tokenenc-unsupported { color: var(--danger, #c0392b); }

.reader-card {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* Bottom padding is intentionally smaller (≈ half of the others) so
   * the action row sits closer to the card's bottom edge. */
  padding: 14px 16px 7px;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* Parent / child "peek" — a shadow-card shape sitting beside the
 * main card, with only an 8px strip protruding past the top
 * (parent / .has-parent-peek) or bottom (child / .has-child-peek).
 * Geometrically: same height as the card, 20px narrower (10px
 * inset on each side of the card OUTER edge), shifted ±8px on
 * the major axis, with the same border-radius as the card so the
 * rounded corners on the protruding side match.
 *
 * The hidden portion is NOT just covered by the card — it's
 * clip-path'd away entirely.  That matters because the main card
 * is often semi-transparent (custom-bg + card-tint user prefs),
 * which would otherwise let the covered slab show through the
 * card body as a ghost strip.
 *
 * Note on numbers: containing block is the card's padding box,
 * which under border-box sits 1px inside the outer edge (the card
 * has a 1px border).  So to get an 8px protrusion past the OUTER
 * edge we use 9px (= 8 + 1 border) on top/bottom; same logic for
 * the 9px L/R inset (= 10 outer inset − 1 border).
 *
 * z-index: -1 puts each peek below the card's painted background
 * (the card has no stacking context, so the peek falls back to the
 * root layer of .reader-list). */
.reader-card.has-parent-peek::before,
.reader-card.has-child-peek::after {
  content: '';
  position: absolute;
  left: 9px;
  right: 9px;
  height: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  z-index: -1;
  pointer-events: none;
}
.reader-card.has-parent-peek::before {
  top: -9px;
  /* keep only the top 8 px — everything covered by the main card
   * is clipped away so a semi-transparent card body has nothing
   * behind it to bleed through. */
  clip-path: inset(0 0 calc(100% - 8px) 0);
}
.reader-card.has-child-peek::after {
  bottom: -9px;
  clip-path: inset(calc(100% - 8px) 0 0 0);
}
/* The 8 px peeks live OUTSIDE the card's border box, so they eat
 * into the .reader-list gap (12 px) and visually pinch adjacent
 * cards together.  Add a matching 8 px margin on each peek side
 * so the empty space between two cards (peek-to-peek) stays at
 * the original 12 px regardless of whether either side has a
 * peek. */
.reader-card.has-parent-peek { margin-top: 8px; }
.reader-card.has-child-peek  { margin-bottom: 8px; }
/* Top-right watermark stamped on a post card when it relates to one
 * of MY posts — reply / quote / repost — or, on the notifications
 * surface, when the card itself is a mention / quote of me. Drawn
 * via inline SVG (attachCardWatermark in reader.js) so the glyph
 * renders consistently across browsers and locales without leaning
 * on a particular system font. Anchored to the avatar row by
 * matching the card's 14 / 16 padding. */
.reader-card-watermark {
  position: absolute;
  top: 14px;
  right: 16px;
  width: 36px;
  height: 36px;
  color: var(--text);
  opacity: 0.18;
  pointer-events: none;
  z-index: 2;
}
.reader-card-watermark svg {
  width: 100%;
  height: 100%;
  display: block;
}
/* Reserve space on the right edge of the card head so the avatar
 * row's content doesn't run under the watermark glyph. */
.reader-card.has-watermark .reader-card-head { padding-right: 44px; }
/* Interaction-watermark live toggle. The watermark DOM is always
 * built at render time so flipping the pref off can hide every
 * mark + reclaim the head's reserved right padding instantly,
 * without a re-render. When the body attribute is "off", every
 * .reader-card-watermark is hidden and the head padding drops
 * back to the default 16px. */
body[data-interaction-watermarks="off"] .reader-card-watermark { display: none; }
body[data-interaction-watermarks="off"] .reader-card.has-watermark .reader-card-head { padding-right: 16px; }
/* Masked post tint — pale pink in light mode, deeper pink in dark.
 * Matches the .mirror-mask palette so a masked post on the timeline
 * reads visually as the same family as a masked span in the composer.
 * The action row's "view masked content" button sits at the card's
 * bottom edge; the body text still shows the censored tokens
 * (⬛ / █ / [검열] / etc) the publisher chose. */
.reader-card.is-masked {
  background: var(--card-masked-bg);
  border-color: var(--card-masked-border);
}
[data-theme="dark"] .reader-card.is-masked {
  background: var(--card-masked-bg);
  border-color: var(--card-masked-border);
}
/* Reposted + masked — solid orange so the combined state reads as
 * its own third tint, separate from the repost-only lime and the
 * masked-only pink. Background + border both themable via vars. */
.reader-card.is-repost.is-masked {
  background: var(--card-repost-masked-bg);
  border-color: var(--card-repost-masked-border);
}
[data-theme="dark"] .reader-card.is-repost.is-masked {
  background: var(--card-repost-masked-bg);
  border-color: var(--card-repost-masked-border);
}
/* Card-tint kill-switch — body-level data attribute set from prefs.
 * Specificity-wise body+class+class beats class+class so the
 * is-repost / is-masked / is-repost.is-masked tints all collapse
 * back to the plain surface color. Dark-theme override is included
 * so the [data-theme="dark"] specificity doesn't win. */
body[data-card-tint="off"] .reader-card.is-repost,
body[data-card-tint="off"] .reader-card.is-masked,
body[data-card-tint="off"] .reader-card.is-repost.is-masked {
  background: var(--surface);
  border-color: var(--border);
}
[data-theme="dark"] body[data-card-tint="off"] .reader-card.is-repost,
[data-theme="dark"] body[data-card-tint="off"] .reader-card.is-masked,
[data-theme="dark"] body[data-card-tint="off"] .reader-card.is-repost.is-masked {
  background: var(--surface);
  border-color: var(--border);
}
.reader-mask-reveal {
  display: block;
  margin: 4px auto 0;
}

/* ── Shared-theme card ─────────────────────────────────────────
 * When a post's link card resolves to a nabilera.xyz/?theme=
 * share URL, the regular external embed is suppressed and the
 * post card itself paints a rainbow gradient so the share stands
 * out at a glance. An "apply" button sits where the link card
 * would have been.
 *
 * Like .is-repost / .is-masked, the rainbow respects the same
 * card-alpha + card-tint=off toggles. Wallpaper-alpha threads
 * each gradient stop through color-mix; tint-off collapses to
 * --surface. */
.reader-card.is-theme-share {
  /* 2026-05-25 사용자 spec : 사용자 가 커스텀 테마 (예 : 핑크) 활성
   * 화 한 경우 에 도 공유 카드 는 항상 무지개 로.  !important 로
   * 사용자 inline var override 와 다른 cascade override 모두 차단. */
  background: linear-gradient(135deg,
    #ff6b9d 0%,
    #fbb13c 17%,
    #f6e76c 33%,
    #6cd97e 50%,
    #4cc9f0 67%,
    #8d6bff 83%,
    #ff6b9d 100%) !important;
  border-color: #d8779b !important;
}
[data-theme="dark"] .reader-card.is-theme-share {
  /* Same hue ramp, slightly desaturated for dark mode legibility. */
  background: linear-gradient(135deg,
    #c84a78 0%,
    #c97a2e 17%,
    #b9a83f 33%,
    #4f9d5e 50%,
    #3a8db0 67%,
    #6a4fc0 83%,
    #c84a78 100%) !important;
  border-color: #984063 !important;
}
body[data-reader-bg="1"] .reader-card.is-theme-share {
  /* Wallpaper variant — thread each gradient stop through the
   * card alpha so the wallpaper bleeds through evenly across the
   * rainbow. Same pattern other card-tint variants use. */
  background: linear-gradient(135deg,
    color-mix(in srgb, #ff6b9d calc(var(--reader-card-alpha, 1) * 100%), transparent) 0%,
    color-mix(in srgb, #fbb13c calc(var(--reader-card-alpha, 1) * 100%), transparent) 17%,
    color-mix(in srgb, #f6e76c calc(var(--reader-card-alpha, 1) * 100%), transparent) 33%,
    color-mix(in srgb, #6cd97e calc(var(--reader-card-alpha, 1) * 100%), transparent) 50%,
    color-mix(in srgb, #4cc9f0 calc(var(--reader-card-alpha, 1) * 100%), transparent) 67%,
    color-mix(in srgb, #8d6bff calc(var(--reader-card-alpha, 1) * 100%), transparent) 83%,
    color-mix(in srgb, #ff6b9d calc(var(--reader-card-alpha, 1) * 100%), transparent) 100%);
}
[data-theme="dark"] body[data-reader-bg="1"] .reader-card.is-theme-share {
  background: linear-gradient(135deg,
    color-mix(in srgb, #c84a78 calc(var(--reader-card-alpha, 1) * 100%), transparent) 0%,
    color-mix(in srgb, #c97a2e calc(var(--reader-card-alpha, 1) * 100%), transparent) 17%,
    color-mix(in srgb, #b9a83f calc(var(--reader-card-alpha, 1) * 100%), transparent) 33%,
    color-mix(in srgb, #4f9d5e calc(var(--reader-card-alpha, 1) * 100%), transparent) 50%,
    color-mix(in srgb, #3a8db0 calc(var(--reader-card-alpha, 1) * 100%), transparent) 67%,
    color-mix(in srgb, #6a4fc0 calc(var(--reader-card-alpha, 1) * 100%), transparent) 83%,
    color-mix(in srgb, #c84a78 calc(var(--reader-card-alpha, 1) * 100%), transparent) 100%);
}
/* Tint-off override for the shared-theme card. Must sit AFTER the
 * wallpaper-variant rule above — both selectors evaluate to the
 * same (0,3,1) / (0,4,1) specificity, so source order is the
 * tiebreaker. Without this, "카드 색 강조 끄기" worked for the
 * repost / masked / repost+masked variants but the rainbow stayed
 * lit whenever wallpaper mode was also on. */
body[data-card-tint="off"] .reader-card.is-theme-share {
  background: var(--surface);
  border-color: var(--border);
}
[data-theme="dark"] body[data-card-tint="off"] .reader-card.is-theme-share {
  background: var(--surface);
  border-color: var(--border);
}

/* 공유 테마 카드 가독성 (사용자 보고 2026-05-29) — 무지개 배경 위 에 서
 * 텍스트(디스플레이 네임 / 핸들 / 본문 / 작성시각 / 액션 카운트) 와 액션
 * 아이콘 을 흰 글자 + 자막 스타일 어두 운 외곽선 으 로 통일.  밝 은 stop
 * (노랑/하늘) 위 든 어두 운 stop(보라) 위 든 항 상 읽 히 게.  apply /
 * translate 버튼 은 자체 스타일 유지 (선택자 제외).
 * 무지개 배경 은 base 규칙 의 !important 라 card-tint 끔 / 테마 무관 하 게
 * 항 상 켜 짐 → 흰 글자 override 도 게이트 없 이 항 상 적용. */
.reader-card.is-theme-share .reader-card-head,
.reader-card.is-theme-share .reader-who,
.reader-card.is-theme-share .reader-handle,
.reader-card.is-theme-share .reader-handle .handle-bsky-suffix,
.reader-card.is-theme-share .reader-body,
.reader-card.is-theme-share .reader-time,
.reader-card.is-theme-share .reader-action,
.reader-card.is-theme-share .reader-action-count {
  color: #fff !important;
  text-shadow:
    -1px -1px 0 rgba(0, 0, 0, 0.55),
    1px -1px 0 rgba(0, 0, 0, 0.55),
    -1px 1px 0 rgba(0, 0, 0, 0.55),
    1px 1px 0 rgba(0, 0, 0, 0.55),
    0 1px 3px rgba(0, 0, 0, 0.78);
}
.reader-card.is-theme-share .reader-action-glyph {
  color: #fff !important;
  filter:
    drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.8))
    drop-shadow(0 0 1px rgba(0, 0, 0, 0.9));
}

/* The big "apply this shared theme" button sits where the link
 * card would have been. Center-aligned, glassy white pill so it
 * reads against the rainbow regardless of which stop sits behind
 * it. */
.reader-theme-apply-btn {
  display: block;
  margin: 12px auto 4px;
  padding: 10px 22px;
  background: rgba(255, 255, 255, 0.92);
  color: #1f2937;
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 999px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
  transition: transform 0.08s ease, background 0.12s ease;
}
.reader-theme-apply-btn:hover {
  background: #ffffff;
  transform: translateY(-1px);
}
[data-theme="dark"] .reader-theme-apply-btn {
  background: rgba(15, 23, 42, 0.85);
  color: #f1f5f9;
  border-color: rgba(255, 255, 255, 0.18);
}
[data-theme="dark"] .reader-theme-apply-btn:hover {
  background: rgba(15, 23, 42, 0.95);
}

/* ── Apply-shared-theme modal ─────────────────────────────────── */
.theme-apply-dialog .theme-apply-body {
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* dialog.modal 의 padding 은 0 이 라 본문 이 박스 가장자리 에 붙음 —
   * 다른 모달 (.modal-body 22px) 처럼 상하좌우 여백 을 준 다. */
  padding: 22px;
}
.theme-apply-head { display: flex; flex-direction: column; gap: 6px; }
.theme-apply-title { margin: 0; font-size: 17px; font-weight: 600; color: var(--text); }
.theme-apply-sub { margin: 0; font-size: 13px; color: var(--text-muted); }
.theme-apply-summary {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.theme-apply-summary-line { font-size: 13px; color: var(--text); }
.theme-apply-swatches { display: flex; gap: 4px; flex-wrap: wrap; }
.theme-apply-swatch {
  display: inline-block;
  width: 18px;
  height: 18px;
  border-radius: 4px;
  border: 1px solid var(--border);
}
.theme-apply-slots { display: flex; flex-direction: column; gap: 6px; }
.theme-apply-slot-row {
  display: grid;
  grid-template-columns: 28px 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--text);
}
.theme-apply-slot-row:hover {
  background: var(--surface-hover);
  border-color: var(--accent);
}
/* 슬롯 복사 모달 의 "현재 슬롯" — 선택 불가 시각 표시. */
.theme-apply-slot-row.is-disabled,
.theme-apply-slot-row[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}
.theme-apply-slot-idx {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
}
.theme-apply-slot-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.theme-apply-slot-name {
  font-size: 14px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.theme-apply-slot-base { font-size: 11px; color: var(--text-muted); }
.theme-apply-slot-cta { font-size: 12px; color: var(--accent); white-space: nowrap; }
.theme-apply-cancel { align-self: center; }
/* "추천 테마" 행 — 슬롯-인덱스 칩 없 이 메타 + 스와치 만 (2열). */
.recommended-theme-row { grid-template-columns: minmax(0, 1fr) auto; }
.recommended-theme-row .theme-apply-swatches { flex-wrap: nowrap; }
/* 1.4 "미리 체험" — 슬롯-저장 모달 에 들어오 는 테마 를 입힐 때 박스
 * 색 전환 이 부 드 럽 도록.  실제 색 은 JS 가 inline 으 로 깐 다. */
.theme-apply-dialog.is-theme-preview {
  transition: background 180ms ease-out, color 180ms ease-out;
}
/* Masked-post reveal modal — passphrase prompt + decoded post card. */
/* Override dialog.modal's 420px cap (selector: tag + class = higher
 * specificity) so the reveal modal has room for the input + Unlock
 * + close row. */
dialog.modal.reader-mask-reveal-modal { max-width: 640px; }
.reader-mask-reveal-modal .modal-body { min-width: min(600px, 92vw); }
/* "Every hint" easter-egg modal — opened when the user selects the
 * brand word (long-press on mobile, double-click / drag on desktop).
 * Slightly wider than the default 420px cap so multi-line hints
 * don't wrap awkwardly, and the inner list scrolls when the pool
 * outgrows the viewport. */
dialog.modal.reader-all-hints-modal { max-width: 520px; }
.reader-all-hints-list {
  max-height: 60vh;
  overflow-y: auto;
  margin: 0 0 16px;
  padding-right: 4px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.reader-all-hints-list .reader-all-hints-item {
  margin: 0;
  font-size: 12px;
  line-height: 1.55;
  color: var(--text-muted);
  text-align: left;
}
.reader-all-hints-list .reader-all-hints-item .reader-rp-prefix.is-active {
  cursor: default;
  font-size: 12px;
  padding: 0 4px;
}
.reader-mask-reveal-head { margin-bottom: 12px; }
.reader-mask-reveal-title { margin: 0; }
.reader-mask-reveal-body { margin: 8px 0 12px; }
.reader-mask-passphrase-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.reader-mask-passphrase-input-row {
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  gap: 8px;
}
.reader-mask-passphrase-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: inherit;
  font: inherit;
}
.reader-mask-passphrase-input:focus {
  outline: none;
  border-color: var(--accent, #4a90e2);
}
.reader-mask-passphrase-err { margin: 0; }
.reader-mask-passphrase-err:empty { display: none; }
/* Decoded post card — reuses .reader-card layout but tighter padding
 * since it lives inside a modal. */
.reader-mask-revealed-card { margin: 0; }
.reader-mask-revealed-body { white-space: pre-wrap; word-break: break-word; }
/* Repost card tint — pale 연두색 (light yellow-green) in light mode;
 * in dark mode, the default surface (rgb 21, 33, 58) with the G
 * channel nudged up so the card reads as faintly green-tinted
 * without changing R or B. */
.reader-card.is-repost {
  background: var(--card-repost-bg);
  border-color: var(--card-repost-border);
}
[data-theme="dark"] .reader-card.is-repost {
  background: var(--card-repost-bg);
  border-color: var(--card-repost-border);
}
.reader-repost-banner,
.reader-reply-banner {
  font-size: 12px;
  color: var(--text-muted);
  margin: -4px 0 -2px;
}
/* SVG glyph at the start of the repost banner. Inline-flex so it
 * sits centered with the surrounding text; size matches the muted
 * banner text (12px ≈ 13px SVG). */
.reader-repost-banner-glyph {
  display: inline-flex;
  align-items: center;
  vertical-align: -2px;
  margin-right: 2px;
}
.reader-repost-banner-glyph svg { width: 13px; height: 13px; }
.reader-card-head {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  flex-wrap: nowrap;
}
.reader-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 36px;
}
.reader-avatar-fallback { background: var(--border-strong); color: var(--text-muted); }
/* Author block — name on top, @handle below as a second line. */
.reader-who { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.reader-who strong { font-size: 15px; }
.reader-handle { color: var(--text-muted); font-size: 13px; }
/* When a relationship chip lives next to the @handle, wrap both in
 * a horizontal row so they share the same baseline. min-width:0 so
 * the handle text can still ellipsis if needed. */
.reader-who-handle-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
  min-width: 0;
}
.reader-who-handle-row .reader-handle {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Relationship chip ("맞팔 / 팔로워 / 팔로잉") — inline next to the
 * @handle. Small, tight, doesn't grow the row's height. */
.reader-relationship-tag {
  flex: 0 0 auto;
  display: inline-block;
  padding: 0 6px;
  font-size: 10px;
  line-height: 1.4;
  font-weight: 500;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  white-space: nowrap;
}
/* In the profile-modal header, the @handle div hosts the chip
 * inline. Make the div flex so they share the baseline. */
.reader-feed-header-handle {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}
/* Home timeline: every author there is already followed by the
 * session user by definition, so the "팔로잉" tag is redundant
 * noise. Hide just that variant (mutual / follower tags still
 * carry useful info — "they follow me back" / "this is one of my
 * followers reposted into my feed"). */
.reader-list[data-feed="home"] .reader-relationship-tag-following { display: none; }
/* Global toggle: prefs.relationshipTags = false flips the body
 * attribute, which hides every chip without a re-render. */
body[data-relationship-tags="off"] .reader-relationship-tag { display: none; }
/* "모든 답글 표시" toggle.  Cards on the home feed get stamped with
 * `data-foreign-reply="true"` at render time when the viewer doesn't
 * follow BOTH the reply author and the parent post author.  When the
 * toggle is OFF (default) we hide them via this single body-level
 * rule — so flipping the toggle takes effect instantly for every
 * card already in the DOM, no re-render required.  Cards on other
 * feeds never get the attribute, so the rule is feed-scoped by
 * construction. */
body[data-show-all-replies="off"] .reader-card[data-foreign-reply="true"] { display: none; }
/* The trailing ".bsky.social" on default-handle accounts is rendered
 * inside a .handle-bsky-suffix span so we can paint it 50% transparent —
 * cuts the "all my friends are foo.bsky.social" visual noise without
 * hiding the domain entirely. Anywhere a handle is rendered via
 * UI.makeHandleText picks this up; custom-domain handles render fully
 * opaque since the suffix span isn't generated for them. */
.handle-bsky-suffix { opacity: 0.55; }
body[data-bsky-suffix-dim="off"] .handle-bsky-suffix { opacity: 1; }
/* Via-Nabilera 🦋 — sits on the same line as the handle. The emoji
 * itself carries the blue/purple butterfly hue across platforms, so
 * we just need to nudge spacing + size so it sits next to '@handle'
 * without dominating it. */
.reader-via-nabilera {
  display: inline-block;
  margin-left: 4px;
  font-size: 12px;
  line-height: 1;
  vertical-align: middle;
}
/* Verified-author badge — blue filled circle + white check.
 * Sits next to the display name (or handle if no displayName)
 * to mirror Bluesky's official-app affordance. The inline SVG
 * already contains the white check; this rule just colors the
 * surrounding circle through currentColor. */
.reader-verified {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  vertical-align: middle;
  color: #1d9bf0;
}
.reader-verified svg { display: block; }
/* "인증 배지 숨기기" 토글이 켜졌을 때 모든 verified 글리프를 한 번에
 * 가린다.  body[data-hide-verification="true"] 는 applyHide
 * VerificationBadges() 가 모달 토글 변경 / 마운트 hydrate 시 토글. */
body[data-hide-verification="true"] .reader-verified { display: none !important; }
/* Automated-account badge — neutral-gray plate + white robot face.
 * Painted via appendActorBadges() BEFORE the verified badge so the
 * inline rhythm reads `displayName 🤖 ✓`.  Color uses --text-muted
 * so it adapts to light / dark themes and doesn't compete with the
 * verified badge's bright blue. */
.reader-automated {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  vertical-align: middle;
  color: var(--text-muted);
}
.reader-automated svg { display: block; }
/* Time sits at the far right of the action bar, just past the
 * share button. Faded so it doesn't compete with the action icons. */
/* Time + share live in the action row. Time gets margin-left: auto
 * so it (and the share button that follows it) get pushed to the
 * right edge — visual: [💬 ↻ ♥ " ─── time ↗]. */
.reader-time {
  color: var(--text-faint);
  font-size: 12px;
  align-self: center;
  margin-left: auto;
  /* Don't let flex shrink the time chip — that'd let the date
   * string wrap inside the span before triggering the action-row
   * wrap. Keeping flex-shrink: 0 means the chip stays one piece
   * and the action row wraps it to its own line instead. */
  flex-shrink: 0;
}

/* 모든 [data-iso] 시각 노드 는 클릭 으로 상대 ↔ 절대 토글 가능 —
 * 포인터 커서 로 affordance 표시, user-select: none 으로 더블 클릭 시
 * 텍스트 selection 이 한 글자 만 잡혀 어색 한 UX 방지.  outline 은
 * tabindex=0 기본 포커스 링 그대로 사용 (a11y). */
[data-iso] {
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}

/* 라벨 chip / warn-strip label : 클릭 으로 라벨 정보 팝오버 (라벨러
 * + 설명문) 열림.  포인터 커서 + 호버 시 살짝 강조 색.  warn-strip
 * 의 .is-clickable 한정 — 기존 strip 의 기본 painting 은 유지. */
.reader-actor-chip-label,
.reader-label-warn-strip-label.is-clickable {
  cursor: pointer;
}
.reader-actor-chip-label:hover,
.reader-actor-chip-label:focus-visible,
.reader-label-warn-strip-label.is-clickable:hover,
.reader-label-warn-strip-label.is-clickable:focus-visible {
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
}

/* 라벨 정보 팝오버 — 드롭다 운 카드.  fixed positioning + popover API.
 * 미지원 브라우저 는 일반 fixed (top-layer 없 음) 로 fallback — dialog
 * 내부 에서는 transform containing block 영향 으 로 위치 가 살짝 어긋
 * 날 수 있 으 나 보이지 않 는 것보다 나음. */
.reader-label-popover {
  position: fixed;
  z-index: 10000;
  /* popover API 는 promoted element 에 inset: 0 + margin: auto 를 자동
   * 적용 — 우리 의 inline top/left 와 결합 해 도 bottom/right 가 남 아
   * 가운데 로 밀 림.  explicit reset 필수. */
  inset: auto;
  margin: 0;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  width: min(300px, calc(100vw - 16px));
  font-size: 13px;
  line-height: 1.5;
  padding: 0;
  overflow: hidden;
}
.reader-label-popover-banner {
  background: var(--accent);
  color: #fff;
  padding: 8px 12px;
  font-weight: 600;
  font-size: 14px;
}
.reader-label-popover-body {
  padding: 10px 12px;
  color: var(--text);
}
.reader-label-popover-footer {
  padding: 8px 12px;
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: 12px;
}
.reader-label-popover-labeler {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
}
.reader-label-popover-labeler:hover,
.reader-label-popover-labeler:focus-visible {
  text-decoration-style: solid;
}
.reader-tag {
  font-size: 11px;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 500;
}
.reader-tag-mask-restrict {
  align-self: flex-start;
}
.reader-mask-restrict-tag-row {
  display: flex;
  justify-content: flex-start;
  margin-top: 4px;
}
.reader-body {
  font-size: 15px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
  /* Justify is the default (last line of each paragraph naturally
   * falls back to left-aligned thanks to pre-wrap). The settings
   * dialog can switch this to plain left via the
   * .reader-align-left override below. */
  text-align: justify;
  text-justify: inter-character;
}
.reader-align-left .reader-body { text-align: left; }
.reader-align-right .reader-body { text-align: right; }
/* Font-size preference — toggled via the settings dialog. Scales only
 * the post body so UI chrome (header / counts / time) stays fixed. */
.reader-font-small .reader-body { font-size: 13px; }
.reader-font-large .reader-body { font-size: 17px; }
/* 인용 카드 본문은 메인 본문보다 한 단계 작은 sizing 을 유지하면서
 * 같은 사이즈 토글에 함께 반응 — 그러지 않으면 메인 본문만 커지고
 * 인용은 그대로라 사용자가 "글꼴 변경이 안 먹는 것 같다" 로 인지함. */
.reader-font-small .reader-quote-body  { font-size: 12px; }
.reader-font-large .reader-quote-body  { font-size: 16px; }
/* 글꼴 변경 다이얼로그의 미리보기 pangram 라인도 사이즈 토글에 같이
 * 반응 — 본문과 동일 스케일 (13/15/17 px).  다이얼로그가 body 직속에
 * 떠 있어서 #view 자손 셀렉터로 도달 못 하지만, openFontChangeModal
 * 이 dialog 자체에도 .reader-font-* 를 toggle 하므로 이 규칙이 적용됨.
 * .reader-font-preview 까지 끼워 specificity 를 .reader-font-preview
 * .reader-font-preview-line { font-size: 15px } (기본 15px 고정 규칙)
 * 보다 한 단계 위로 올려서 우선 적용되게 함. */
.reader-font-small .reader-font-preview .reader-font-preview-line { font-size: 13px; }
.reader-font-large .reader-font-preview .reader-font-preview-line { font-size: 17px; }
/* Font-family override via --reader-body-font CSS variable so every
 * post-body-like surface (.reader-body, .reader-quote-body,
 * .reader-font-preview-line) follows the user's pick in one place.
 * "system" is the default with no override (falls back to inherit
 * → page chrome font). Built-in font names line up with the
 * @font-face declarations loaded from Google Fonts in index.html;
 * custom uploads emit their own rule from applyCustomFontStyles. */
body[data-font="notoSans"]        { --reader-body-font: 'Noto Sans KR', system-ui, sans-serif; }
body[data-font="notoSerif"]       { --reader-body-font: 'Noto Serif KR', 'Noto Sans KR', serif; }
body[data-font="ibmPlex"]         { --reader-body-font: 'IBM Plex Sans KR', 'Noto Sans KR', sans-serif; }
body[data-font="diphylleia"]      { --reader-body-font: Diphylleia, 'Noto Serif KR', serif; }
/* External-CDN-hosted fonts (Pretendard / Spoqa / D2Coding). */
body[data-font="pretendard"]      { --reader-body-font: 'Pretendard Variable', Pretendard, system-ui, sans-serif; }
body[data-font="spoqaHanSansNeo"] { --reader-body-font: 'Spoqa Han Sans Neo', system-ui, sans-serif; }
body[data-font="d2coding"]        { --reader-body-font: 'D2Coding', ui-monospace, 'Courier New', monospace; }
/* Added v0.9.759 — Google Fonts -hosted (verified working). */
body[data-font="gothicA1"]        { --reader-body-font: 'Gothic A1', system-ui, sans-serif; }
body[data-font="nanumGothic"]     { --reader-body-font: 'Nanum Gothic', 'Noto Sans KR', sans-serif; }
body[data-font="nanumMyeongjo"]   { --reader-body-font: 'Nanum Myeongjo', 'Noto Serif KR', serif; }
body[data-font="hahmlet"]         { --reader-body-font: Hahmlet, 'Noto Serif KR', serif; }
.reader-body,
.reader-quote-body,
.reader-font-preview-line,
/* DM chat bubbles are "body" text too — they carry the
 * user's prose, so they pick up the reader-body font in the
 * "본문만" mode just like post bodies. */
.dm-message-bubble,
/* Profile bio paragraph — user-written prose, treated as body
 * text so the chosen reader font lands even in the default
 * "본문만" scope (not just meta / all). Covers own-profile
 * + other-profile + profile-modal headers since they share
 * the same `.reader-feed-header-desc` class. */
.reader-feed-header-desc {
  font-family: var(--reader-body-font, inherit);
}
/* Custom background — opt-in via the reader's "커스텀 배경" modal.
 * body[data-reader-bg="1"]::before paints a fixed full-viewport
 * image layer behind everything, with user-adjustable opacity.
 * z-index: -1 + pointer-events: none keep it strictly decorative. */
body[data-reader-bg="1"]::before {
  content: '';
  position: fixed;
  inset: 0;
  background: var(--reader-bg-url) center / cover no-repeat;
  opacity: var(--reader-bg-opacity, 0.4);
  z-index: -1;
  pointer-events: none;
}
/* Custom background modal layout. */
.reader-custom-bg-modal .modal-body { min-width: 320px; max-width: 480px; }
.reader-custom-bg-preview {
  margin: 12px 0;
  min-height: 120px;
  border: 1px dashed var(--border);
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--surface-2);
}
/* Stage: a box that simulates the live page. The bg layer paints
 * with center-crop `cover` sizing so the user sees what part of the
 * picture the wallpaper will actually show, and the dummy card sits
 * fully visible on top so the card-alpha mix is rendered against
 * the chosen wallpaper in real time. */
.reader-custom-bg-preview-stage {
  position: relative;
  width: 100%;
  overflow: hidden;
  --preview-bg-opacity: 0.6;
  --preview-card-alpha: 0.6;
  background: var(--bg);
  padding: 18px;
  box-sizing: border-box;
  border-radius: 8px;
}
.reader-custom-bg-preview-bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  opacity: var(--preview-bg-opacity);
}
.reader-custom-bg-preview-card {
  position: relative;
  /* Full card centered in the stage so the wallpaper still shows
   * around it. Width clamps to readable widths on wide modals. */
  width: 100%;
  max-width: 360px;
  margin: 0 auto;
  background: color-mix(in srgb, var(--surface) calc(var(--preview-card-alpha) * 100%), transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 12px 14px;
  font-size: 12px;
  color: var(--text);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* 유리(glass) 미리보기(실험 nabilera:glass) — 본 피드 카드와 동일한 frosted-
 * glass 를 미리보기 카드에도.  --preview-glass-blur 로 슬라이더 강도 반영. */
.reader-custom-bg-preview-card.is-glass {
  -webkit-backdrop-filter: blur(var(--preview-glass-blur, 8.4px)) saturate(1.6);
  backdrop-filter: blur(var(--preview-glass-blur, 8.4px)) saturate(1.6);
  border-color: color-mix(in srgb, var(--border) 45%, rgba(255, 255, 255, 0.65));
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.45),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 6px 22px rgba(0, 0, 0, 0.14);
}
[data-theme="dark"] .reader-custom-bg-preview-card.is-glass {
  border-color: color-mix(in srgb, var(--border) 55%, rgba(255, 255, 255, 0.28));
  box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.16), 0 6px 22px rgba(0, 0, 0, 0.45);
}
.reader-custom-bg-preview-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.reader-custom-bg-preview-card-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  flex-shrink: 0;
}
.reader-custom-bg-preview-card-who {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
  min-width: 0;
}
.reader-custom-bg-preview-card-who strong {
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-custom-bg-preview-card-handle {
  font-size: 12px;
  color: var(--text-muted);
}
.reader-custom-bg-preview-card-body {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
  word-break: break-word;
}
/* Static dummy action row inside the custom-bg preview card. Reuses
 * the .reader-actions / .reader-action / .reader-time chrome the
 * real cards use so token / spacing tweaks land here automatically,
 * but the items are rendered as <span> (not <button>) and we kill
 * cursor/hover styling so the user doesn't read it as interactive. */
.reader-custom-bg-preview-actions { pointer-events: none; }
.reader-custom-bg-preview-actions .reader-action {
  cursor: default;
  opacity: 1;
}
.reader-custom-bg-preview-actions .reader-action:hover {
  background: transparent;
  color: var(--text-faint);
}
.reader-custom-bg-empty {
  margin: 0;
  padding: 16px;
  color: var(--text-muted);
  text-align: center;
}
/* Upload row sits at the top of the modal — one centered button so
 * it reads as the primary action when no image is set yet. */
.reader-custom-bg-upload-row {
  display: flex;
  justify-content: center;
  margin: 8px 0 12px;
}
/* Toggle row sits below the preview — "사용함 / 사용 안 함" toggle
 * on the left, red 초기화 (reset) button on the right.  The toggle's
 * label-wrap (label element wrapping checkbox + text span) hugs its
 * content; margin-left:auto on the reset button pushes it to the
 * far right within the same flex row. */
.reader-custom-bg-toggle-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 8px 0 12px;
}
/* Segmented pill (사용함 / 사용 안 함) — same chassis as the search
 * + notif mode toggles.  When no blob is saved the pill grays out
 * and ignores pointer events so the user can't flip a setting that
 * has nothing to act on. */
.reader-custom-bg-toggle-pill.is-disabled {
  opacity: 0.45;
  pointer-events: none;
}
.reader-custom-bg-reset {
  margin-left: auto;
}
.reader-custom-bg-opacity-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
}
.reader-custom-bg-opacity-label {
  font-size: 13px;
  /* Match the view-button option's label color (white in dark
   * mode, plain text in light) so the wallpaper modal's controls
   * read as one palette. The previous --text-muted was washed
   * out against the modal surface. */
  color: var(--text);
  flex: 0 0 auto;
}
/* View-button option row — label sits to the RIGHT of the info-
 * button so the row layout matches the other wallpaper rows.
 * Same --text color as the opacity labels for tonal match. */
.reader-custom-bg-option-label {
  color: var(--text);
}
.reader-custom-bg-option-row {
  align-items: center;
}
.reader-custom-bg-opacity {
  flex: 1;
  min-width: 0;
}
.reader-custom-bg-opacity-value {
  font-size: 12px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
  min-width: 40px;
  text-align: right;
}
/* Stage 2 — body + author meta (display name + handle). EVERY
 * display name + handle pair across the client adopts the chosen
 * body font in this mode, not just the reader timeline. New
 * surfaces (DM sidebar, DM chat header, search dropdowns,
 * follower list rows, quoted posts, account switcher) all
 * register their classes here. */
body[data-font-scope="meta"] .reader-feed-header-title,
body[data-font-scope="meta"] .reader-feed-header-handle,
body[data-font-scope="meta"] .reader-displayname,
body[data-font-scope="meta"] .reader-handle,
body[data-font-scope="meta"] .reader-who strong,
body[data-font-scope="meta"] .reader-who .reader-handle,
body[data-font-scope="meta"] .reader-list-modal-text strong,
body[data-font-scope="meta"] .reader-list-modal-handle,
body[data-font-scope="meta"] .reader-quote-name,
body[data-font-scope="meta"] .reader-quote-handle,
body[data-font-scope="meta"] .reader-search-user-name,
body[data-font-scope="meta"] .reader-search-user-handle,
body[data-font-scope="meta"] .reader-search-author-chip-name,
body[data-font-scope="meta"] .reader-search-author-chip-handle,
body[data-font-scope="meta"] .reader-search-author-dropdown-name,
body[data-font-scope="meta"] .reader-search-author-dropdown-handle,
body[data-font-scope="meta"] .reader-search-history-chip-handle,
body[data-font-scope="meta"] .session-menu-account-handle,
body[data-font-scope="meta"] .dm-convo-display,
body[data-font-scope="meta"] .dm-convo-handle,
body[data-font-scope="meta"] .dm-chat-display,
body[data-font-scope="meta"] .dm-chat-handle,
body[data-font-scope="all"]  .reader-feed-header-title,
body[data-font-scope="all"]  .reader-feed-header-handle,
body[data-font-scope="all"]  .reader-displayname,
body[data-font-scope="all"]  .reader-handle,
body[data-font-scope="all"]  .reader-who strong,
body[data-font-scope="all"]  .reader-who .reader-handle,
body[data-font-scope="all"]  .reader-list-modal-text strong,
body[data-font-scope="all"]  .reader-list-modal-handle,
body[data-font-scope="all"]  .reader-quote-name,
body[data-font-scope="all"]  .reader-quote-handle,
body[data-font-scope="all"]  .reader-search-user-name,
body[data-font-scope="all"]  .reader-search-user-handle,
body[data-font-scope="all"]  .reader-search-author-chip-name,
body[data-font-scope="all"]  .reader-search-author-chip-handle,
body[data-font-scope="all"]  .reader-search-author-dropdown-name,
body[data-font-scope="all"]  .reader-search-author-dropdown-handle,
body[data-font-scope="all"]  .reader-search-history-chip-handle,
body[data-font-scope="all"]  .session-menu-account-handle,
body[data-font-scope="all"]  .dm-convo-display,
body[data-font-scope="all"]  .dm-convo-handle,
body[data-font-scope="all"]  .dm-chat-display,
body[data-font-scope="all"]  .dm-chat-handle {
  font-family: var(--reader-body-font, inherit);
}
/* Stage 3 — entire reader. Excludes monospace inputs / mono spans
 * (login key fields etc.) by carve-outs in the selector list so
 * those keep system-ui mono. Also leaves <code>, <pre> alone. */
html.reader-active body[data-font-scope="all"],
html.reader-active body[data-font-scope="all"] :not(.mono):not(code):not(pre):not(.reader-body):not(.reader-quote-body):not(.reader-font-preview-line):not(.reader-settings-tagline):not(.reader-settings-tagline-brand):not(.tagline-brand-char) {
  font-family: var(--reader-body-font, inherit);
}
/* The 「나빌레라」 poem tagline in the settings dialog keeps its
 * Diphylleia / Noto Serif KR look even under font-scope=all — the
 * font is part of the brand voice, not user-changeable copy. */
html.reader-active body[data-font-scope="all"] .reader-settings-tagline,
html.reader-active body[data-font-scope="all"] .reader-settings-tagline * {
  font-family: 'Diphylleia', 'Noto Serif KR', serif !important;
}
/* Splash screens (both the reader splash with the "Bluesky Web
 * Client" subtitle and the tool-hub brand-only splash) are part of
 * the brand frame, not user-changeable copy — so they keep their
 * fixed type stack regardless of the font-scope setting. The
 * `body[data-font-scope="all"]` wildcard above otherwise sweeps the
 * splash text into the chosen reader font. */
.reader-splash .reader-splash-text,
.reader-splash .reader-splash-sub {
  font-family: 'Diphylleia', 'Noto Serif KR', serif !important;
}
.reader-splash .reader-splash-version {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace !important;
}
.reader-facet-link { color: var(--accent); text-decoration: none; }
.reader-facet-link:hover { text-decoration: underline; }
.reader-facet-mention { color: var(--accent); cursor: pointer; }
.reader-facet-tag { color: var(--accent); }
/* 해시태그 검색(nabilera:hashtag 실험) — super 세션이면 클릭 가능 표시. */
html[data-hashtag-search] .reader-facet-tag { cursor: pointer; }
html[data-hashtag-search] .reader-facet-tag:hover { text-decoration: underline; }
/* 해시태그 검색 결과 모달.  폭은 dialog 에 (CLAUDE.md: body 에 걸면 오른쪽 공백). */
dialog.modal.reader-hashtag-modal { max-width: 560px; }
.reader-hashtag-sort { margin: 4px 0 10px; }
.reader-hashtag-list { display: flex; flex-direction: column; gap: 8px; }
.reader-hashtag-status { text-align: center; margin: 16px 0; color: var(--text-muted); }
.reader-hashtag-more { display: block; margin: 12px auto 0; }
/* "RP)" prefix — plain text until the author's prior activity is
 * confirmed as a repost. Once confirmed, .is-active turns it into a
 * coloured badge that opens the "probably this post" modal. Light
 * mode uses pink, dark mode keeps the maroon tone so it reads as
 * "warm accent" against either surface. */
.reader-rp-prefix.is-active {
  display: inline-block;
  background: #db2777;
  color: #fff;
  padding: 0 6px;
  margin-right: 4px;
  border-radius: 4px;
  font-weight: 600;
  cursor: pointer;
  line-height: 1.4;
  /* No focus ring or text-selection highlight on tap — the badge is
   * effectively a button and the existing background + hover state
   * is enough affordance. */
  outline: none;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}
.reader-rp-prefix.is-active:hover { background: #be185d; }
[data-theme="dark"] .reader-rp-prefix.is-active { background: #7f1d1d; }
[data-theme="dark"] .reader-rp-prefix.is-active:hover { background: #991b1b; }
.reader-images {
  display: grid;
  gap: 4px;
  border-radius: var(--radius);
  overflow: hidden;
}
/* 단일 이미지 : 래퍼 는 카드 전체 폭 을 유지 한 다 (width:auto = block).
 * 그 래 야 셀 의 inline aspect-ratio 가 로 드 전 에 슬롯 높이 를 미 리 확보
 * 해 — 아래 below-the-fold 이미지 가 나중 에 도착 해 도 타임라인 이 점프
 * 하 지 않 음.  (래퍼 를 fit-content 로 두 면 로 드 전 콘텐츠 폭 이 0 이 라
 * 슬롯 이 0×0 으 로 수축 → 자리 확보 가 사라짐, 사용자 보고 2026-05-29.)
 *
 * 코너 라운딩 은 래퍼 가 아 니 라 실제 이미지 셀(.reader-image) 에 건 다 :
 * 세로 이미지 는 셀 이 aspect-ratio × max-height(480) 로 카드 폭 보 다
 * 좁 아 좌측 정렬 되 는데, 래퍼(전체 폭) 에 만 라운딩 을 주 면 우측 둥근
 * 모서리 가 이미지 에 서 멀 어 이미지 우측 코너 가 직각 으 로 보였 음
 * (사용자 보고 2026-05-29).  셀 에 직접 border-radius + (셀 의 overflow
 * :hidden) 를 주 면 이미지 의 실제 모서리 가 네 코너 모두 둥글 게 클립. */
.reader-images-1 {
  grid-template-columns: 1fr;
}
.reader-images-1 .reader-image {
  border-radius: var(--radius);
}
.reader-images-2 { grid-template-columns: 1fr 1fr; }
/* 3-image layout: tall left image spanning both rows + two stacked
 * squares on the right. The whole grid is square (aspect-ratio: 1),
 * with two equal-height rows, so the spanning left cell ends up 1:2
 * (portrait) and each right cell ends up 1:1 — matching the standard
 * Bluesky / Twitter look. */
.reader-images-3 {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  aspect-ratio: 1 / 1;
}
.reader-images-3 .reader-image:first-child { grid-row: 1 / span 2; }
.reader-images-4 { grid-template-columns: 1fr 1fr; }
.reader-image {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--surface-hover);
  max-height: 480px;
}
/* Multi-image grids tile in fixed-aspect cells so the layout doesn't
 * jitter as each image loads. Single-image embeds get their natural
 * aspect from img.aspectRatio (set inline). */
.reader-images-2 .reader-image,
.reader-images-4 .reader-image { aspect-ratio: 1 / 1; }
/* 5장 이상(갤러리) — 공식 앱 v1.123 처럼 필름스트립 캐러셀 : 고정 높이 한
 * 줄에 각 사진이 제 비율대로 폭을 가져 한 화면에 여러 장이 거의 붙어 보이고
 * 자유 스크롤(마그네틱 스냅 없음).  4장 이하는 위 그리드 그대로(사용자 보고
 * 2026-06-09). */
.reader-carousel { position: relative; margin-top: 8px; }
.reader-carousel-track {
  display: flex;
  gap: 3px;
  overflow-x: auto;
  overflow-y: hidden;
  height: 300px;
  max-height: 70vh;
  border-radius: var(--radius);
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  scroll-snap-type: none;
  overscroll-behavior-x: contain;
}
.reader-carousel-track::-webkit-scrollbar { display: none; }
/* 슬라이드 = 고정 높이(트랙 100%) × 각 사진의 종횡비(JS 가 inline 으로 설정,
 * 가로 극단 3:2 / 세로 극단 2:3 클램프).  사진은 그 슬라이드에 cover(중앙
 * 크롭) → 극단 비율도 일정 범위로 잘려 보임(공식 앱 방식). */
.reader-carousel-slide {
  flex: 0 0 auto;
  height: 100%;
  position: relative;
}
.reader-carousel-slide .reader-image {
  width: 100%;
  height: 100%;
  max-height: none;
  margin: 0;
  border-radius: 0;
}
.reader-carousel-slide .reader-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* 슬라이드마다 "n/총" 번호 배지(공식 앱처럼 항상 표시). */
.reader-carousel-index {
  position: absolute;
  top: 8px;
  right: 8px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  padding: 2px 9px;
  border-radius: 11px;
  pointer-events: none;
  z-index: 2;
}
/* 3-image case: cells inherit their size from the grid's row tracks
 * (the spanning first cell becomes 1:2 portrait, the other two stay
 * 1:1). Forcing aspect-ratio: 1 on every cell here would fight the
 * grid and warp the spanning image. */
.reader-image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.reader-image img.is-loaded { opacity: 1; }
.reader-image-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: var(--text-faint);
  font-size: 13px;
  pointer-events: none;
}
/* image load 가 2 회 (retry 포함) 실패 시 broken-image icon 대신 표시
 * 되 는 fallback 박스 — alt 텍스트 가 있 으 면 그것, 없 으 면 generic
 * '이미지를 불러오지 못했어요' 메시지.  카드 의 reserved aspect-ratio
 * 슬롯 안 에 채워 짐. */
.reader-image-failed {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 16px;
  text-align: center;
  background: var(--surface-2);
  color: var(--text-faint);
  font-size: 13px;
  line-height: 1.4;
}
.reader-image-failed-icon {
  font-size: 24px;
  color: var(--text-muted);
}
.reader-image-failed-msg {
  max-width: 80%;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
}
.reader-linkcard {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  text-decoration: none;
  color: var(--text);
  transition: border-color 0.15s;
}
.reader-linkcard:hover { border-color: var(--border-strong); text-decoration: none; }
/* Repost / masking card backgrounds (lime / pink tints) sit close in
 * lightness to --border, so the linkcard's 1px edge bleeds into the
 * card surface. Bump to --border-strong inside tinted variants so
 * the linkcard reads as a separate sub-card in all three tints
 * (is-repost, is-masked, is-repost.is-masked). */
.reader-card.is-repost .reader-linkcard,
.reader-card.is-masked .reader-linkcard {
  border-color: var(--border-strong);
}
.reader-linkcard-thumb {
  /* Square + center-cropped — link-card thumbnails arrive in
   * arbitrary aspect ratios; without a fixed square the column
   * wound up as tall as the body text and stretched the image. */
  width: 120px;
  height: 120px;
  flex: 0 0 120px;
  object-fit: cover;
  object-position: center;
  align-self: center;
}
.reader-linkcard-body { padding: 10px 12px; min-width: 0; flex: 1; align-self: center; }
/* Playable variant — YouTube / Vimeo etc. The thumb takes the full
 * width with a 16:9 stage and a center play overlay; the meta drops
 * below as a footer. */
.reader-media-stage {
  /* Standalone media embed — no surrounding card or title/description
   * footer, just the player. Default is 16:9 for video kinds (YouTube
   * / Vimeo / Twitch / Loom / Streamable); audio kinds switch to a
   * fixed slab height via .is-audio / .is-audio-list below. */
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  overflow: hidden;
  border-radius: var(--radius);
}
/* Audio embeds — short slab for single-track players (SoundCloud,
 * Mixcloud, Spotify track / episode). Height matches the widgets'
 * own minimum. */
.reader-media-stage.is-audio {
  aspect-ratio: auto;
  height: 166px;
}
/* Audio-list embeds — taller frame for player widgets that show a
 * track list (Spotify album / playlist / show, Apple Music album /
 * playlist). */
.reader-media-stage.is-audio-list {
  aspect-ratio: auto;
  height: 352px;
}
/* Title + channel overlay along the bottom of the playable stage.
 * Gradient scrim so the white text reads against any thumbnail
 * brightness. Pointer-events:none so the central ▶ button still
 * catches the tap. */
.reader-media-meta {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 28px 14px 12px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0) 100%);
  color: #fff;
  pointer-events: none;
  z-index: 1;
}
.reader-media-meta-title {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.reader-media-meta-sub {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.85);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.reader-media-thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.reader-media-play {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 64px; height: 64px;
  border-radius: 50%;
  border: 0;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  font-size: 28px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  padding: 0 0 0 4px; /* nudge the ▶ glyph optically centered */
  transition: background 0.15s, transform 0.15s;
}
.reader-media-play:hover { background: rgba(0, 0, 0, 0.85); transform: translate(-50%, -50%) scale(1.05); }
.reader-media-iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
/* Inline GIF (Tenor / Giphy) stage — 자연 비율 보존 + 자동 재생.
 * 16:9 video stage 와 달리 aspect-ratio 를 dictate 하 지 않 고 컨텐츠
 * 가 자체 비율 잡 게 둠.  GIF 의 background 는 흰 색 (대부분 흰 / 투명
 * 배경 의 sticker / meme 가 흔하 므 로).  최대 높이 600 px cap 으로
 * 너무 긴 GIF (세로 형 long-form) 가 timeline 을 잡 아 먹지 않 게. */
.reader-media-stage.is-gif {
  aspect-ratio: auto;
  background: var(--bg-soft, #f3f3f3);
}
.reader-gif-direct {
  display: block;
  width: 100%;
  height: auto;
  max-height: 600px;
  object-fit: contain;
}
.reader-gif-embed {
  display: block;
  width: 100%;
  /* Tenor / Giphy embed iframe 은 GIF 자연 비율 을 자체 적 으로 맞 추지
   * 만, iframe 의 default 높이 가 0 이라 명시 한 값 이 필요.  4:3 이
   * 가장 흔 한 GIF 비율 (정사각형 보 다 wider, 16:9 보 다 taller). */
  aspect-ratio: 4 / 3;
  border: 0;
}
.reader-media-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  padding: 3px 7px;
  border-radius: 4px;
  pointer-events: none;
  z-index: 1;
}
/* GIF stage 의 클릭 일시정지 / 재개 overlay.  pause 상태 에서만 visible
 * — 살짝 어두운 layer + 중앙 ▶ 버튼.  pointer-events:none 으로 전체
 * stage 의 click 토 글 영역 을 막 지 않음. */
.reader-media-stage.is-gif-toggleable,
.reader-video-stage.is-gif-presentation.is-gif-toggleable {
  cursor: pointer;
}
.reader-gif-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.35);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease;
  z-index: 2;
}
.reader-media-stage.is-gif-paused .reader-gif-overlay,
.reader-video-stage.is-gif-paused .reader-gif-overlay {
  opacity: 1;
}
.reader-gif-overlay-btn {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  padding-left: 4px; /* 삼각형 시각 중심 보정 */
  pointer-events: none;
}
/* Lightbox image viewer.
 *
 * The dialog itself spans the full viewport (transparent background;
 * the dim layer is just ::backdrop). Image is centred via flex and
 * sized with object-fit: contain so it always fits at 1× zoom. Zoom
 * transforms then grow the image freely inside the viewport — the
 * dialog no longer clips at the image's natural box. The UA defaults
 * for <dialog>:modal include max-width/max-height shrinking calcs
 * that get in our way, so they're !important'd. */
.reader-lightbox[open] {
  position: fixed;
  inset: 0;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: #fff;
  width: 100vw;
  height: 100vh;   /* fallback */
  height: 100dvh;
  max-width: 100vw !important;
  max-height: 100vh !important;   /* fallback */
  max-height: 100dvh !important;
  overflow: hidden;
  overscroll-behavior: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.reader-lightbox::backdrop { background: rgba(0, 0, 0, 0.92); }
.reader-lightbox:focus, .reader-lightbox:focus-visible { outline: none; }
.reader-lightbox-frame {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Frame hugs the image; the dialog (full viewport) centres it. */
  width: auto;
  height: auto;
  max-width: 100vw;
  max-height: 100vh;   /* fallback */
  max-height: 100dvh;
  /* Animated when a fast swipe gestures the lightbox closed —
   * fling away in the direction of the swipe, then close. The JS
   * setTimeout in flingClose() matches this duration. */
  transition: transform 0.4s ease-out, opacity 0.4s ease-out;
}
.reader-lightbox-frame.flying-down  { transform: translateY( 120vh); opacity: 0; }
.reader-lightbox-frame.flying-up    { transform: translateY(-120vh); opacity: 0; }
.reader-lightbox-frame.flying-right { transform: translateX( 120vw); opacity: 0; }
.reader-lightbox-frame.flying-left  { transform: translateX(-120vw); opacity: 0; }
.reader-lightbox-img {
  display: block;
  max-width: 100vw;
  max-height: 100vh;   /* fallback */
  max-height: 100dvh;
  width: auto;
  height: auto;
  object-fit: contain;
  /* Kill every native touch gesture on the image — we drive
   * pinch-zoom and pan with JS so the rest of the app doesn't
   * scale (viewport-level pinch would zoom everything). */
  touch-action: none;
  transform-origin: center center;
  will-change: transform;
}
.reader-lightbox-topbar {
  /* Anchored to the frame (which hugs the image), so the buttons sit
   * at the image's top-right corner — not the viewport's. The frame
   * is the topbar's offset parent and also carries the fling-close
   * transform, so the buttons fly away with the image on close. */
  position: absolute;
  top: 12px;
  right: 12px;
  display: flex;
  gap: 8px;
  transition: opacity 0.4s ease;
  z-index: 1;
}
/* Hide the action buttons while the image is zoomed — they'd just
 * float over the picture and get in the way. Add is instant (no
 * fade-out — we don't want a half-visible topbar lingering over the
 * zoom gesture); remove falls back to the parent's 0.4s transition
 * so the buttons fade back in once the image snaps to 1×. */
.reader-lightbox-topbar.zoomed {
  opacity: 0;
  pointer-events: none;
  transition: none;
}
.reader-lightbox-topbar.faded {
  opacity: 0;
  pointer-events: none;
}
/* While the lightbox's <img> is still fetching / decoding the
 * current source, the ↗ / 💾 / ✕ topbar stays hidden. Otherwise the
 * three action buttons land first and invite the user to "save" a
 * blank frame before the bitmap arrives. The JS clears the
 * `.is-loading` class once img.decode() resolves (with a 4 s safety
 * cap so a broken image doesn't strand the close button forever). */
.reader-lightbox.is-loading .reader-lightbox-topbar {
  opacity: 0;
  pointer-events: none;
}
/* 라이트박스 상단 중앙 페이지 점 — 모바일 컬럼 인디케이터 점 스타일 차용,
 * topbar 3버튼과 동일한 페이드 규칙(.faded / .is-loading).  프레임(이미지)
 * 상단 중앙 에 앵커 (사용자 보고 2026-06-09). */
.reader-lightbox-dots {
  position: absolute;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 7px 11px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.5);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  pointer-events: none;
  z-index: 1;
  transition: opacity 0.4s ease;
}
.reader-lightbox-dots[hidden] { display: none; }
.reader-lightbox-dots.faded { opacity: 0; }
.reader-lightbox.is-loading .reader-lightbox-dots { opacity: 0; }
.reader-lightbox-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.45);
  transition: background 160ms ease-out, transform 160ms ease-out;
}
.reader-lightbox-dot.is-active {
  background: #fff;
  transform: scale(1.35);
}
/* 하단 대체텍스트 캡션 — 이미지를 열면 alt 표시(없으면 숨김), 동일 페이드. */
.reader-lightbox-alt {
  position: absolute;
  left: 50%;
  bottom: 48px;
  transform: translateX(-50%);
  max-width: min(92%, 640px);
  max-height: 32%;
  overflow-y: auto;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
  font-size: 13px;
  line-height: 1.5;
  white-space: pre-wrap;
  padding: 8px 12px;
  border-radius: 10px;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  z-index: 1;
  transition: opacity 0.4s ease;
}
.reader-lightbox-alt[hidden] { display: none; }
.reader-lightbox-alt.faded { opacity: 0; pointer-events: none; }
.reader-lightbox.is-loading .reader-lightbox-alt { opacity: 0; }
.reader-lightbox-iconbtn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  border: 0;
  font-size: 18px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.reader-lightbox-iconbtn:hover { background: rgba(0, 0, 0, 0.85); }
.reader-lightbox-iconbtn:disabled { opacity: 0.5; cursor: not-allowed; }
/* PC(마우스) 전용 좌/우 이미지 네비 삼각형 — 다중 이미지 일 때, 마우스
 * 움직이 면 보이 고 (JS .faded 토글) 클릭 하 면 이전/다음.  표시 여부 자체
 * 는 JS (fine-pointer + 다중 이미지 + 경계) 가 display 로 제어.  터치 는
 * 스와이프 가 있 어 숨김. */
.reader-lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  border: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  z-index: 2;
  opacity: 1;
  transition: opacity 0.25s ease-out, background 0.12s ease;
}
.reader-lightbox-nav:hover { background: rgba(0, 0, 0, 0.82); }
.reader-lightbox-nav-prev { left: 10px; }
.reader-lightbox-nav-next { right: 10px; }
.reader-lightbox-nav.faded { opacity: 0; pointer-events: none; }
/* "Probably this post" modal — shown when the user clicks the RP)
 * badge. Frames a single post card with a "아마도 이 포스트인 것 같아요"
 * header and a × close button. Click outside (on the backdrop) closes. */
.reader-rp-modal[open] {
  position: fixed;
  inset: 0;
  margin: auto;
  padding: 16px;
  /* Matches the profile / feed modal border treatment so the
   * "raised layer" cue reads consistently across data viewers. */
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--bg);
  color: var(--text);
  width: fit-content;
  height: fit-content;
  max-width: min(560px, 95vw) !important;
  max-height: 90dvh !important;
  overflow-y: auto;
  overscroll-behavior: none;
  box-shadow: var(--shadow-md);
}
.reader-rp-modal::backdrop { background: rgba(0, 0, 0, 0.55); }
.reader-rp-modal-close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  border: 0;
  font-size: 22px;
  cursor: pointer;
  line-height: 1;
}
.reader-rp-modal-close:hover { background: var(--surface-hover); }
.reader-rp-modal-header {
  font-size: 14px;
  color: var(--text-muted);
  margin: 0 28px 12px 0;
}
.reader-rp-modal-body { width: 100%; }
.reader-linkcard-title {
  font-weight: 600;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.reader-linkcard-desc {
  font-size: 13px;
  color: var(--text-muted);
  margin: 4px 0;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.reader-linkcard-host { font-size: 12px; color: var(--text-faint); }

/* ── 넷플릭스 링크 카드 (실험중 'reader:netflix-card') ────────────
 * 썸 네일 fill + 좌상단 NETFLIX 로고 + 제목 + 좌하단 설명.  카드 자체
 * 가 flex-row 의 thumb+body 가 아니 라 absolute-positioned overlay 들
 * 로 구성 — 일반 .reader-linkcard 의 flex 레이아웃 override. */
.reader-linkcard-netflix {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius);
  background: #000;
  color: #fff;
  text-decoration: none;
  border: 1px solid var(--border);
}
.reader-linkcard-netflix:hover { border-color: var(--border-strong); }
.reader-linkcard-netflix-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Darken gradient — 상/하 텍스트 자리 만 진 하 게 해 가독 성 확보. */
.reader-linkcard-netflix-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(0,0,0,0.75) 0%,
      rgba(0,0,0,0.10) 28%,
      rgba(0,0,0,0.10) 64%,
      rgba(0,0,0,0.90) 100%);
}
/* 좌상단 anchor — 사용자 spec 2026-05-26 : 로고 의 시각 적 top/left 여
 * 백 이 desc 의 bottom/left (12/14) 와 같아 지 게.  netflix.png 가 uniform
 * 10.3% transparent 패딩 (height 27 px 일 때 ~7 css px) 을 가 지 고 있
 * 으 므 로 top/left 를 7 씩 줄 여 보 정 — 결과 적 으 로 시각 적 inset
 * (visible 빨강 픽셀 의 top/left edge) 이 desc 와 같이 12/14 .
 *
 * gap : flex container 의 logo↔title 간격.  사용자 spec 2026-05-27 :
 * '로고 의 왼쪽 visible 여백 (card-edge → 빨강 픽셀, ~14 px) 과 로고 의
 * 오른쪽 visible 여백 (빨강 끝 → 제목 시작) 이 비슷 하 게'.  옛 gap
 * 10 에서 title 이 너무 멀어 visible right gap ≈ 16 px → 비대칭.  PNG
 * 의 right phantom 패딩 ~7 px 빼 면 title-card 와 의 실효 gap = flex.gap.
 * flex.gap = 7 로 줄이 면 visible right gap ≈ 14 ≈ left visible 여백. */
.reader-linkcard-netflix-top {
  position: absolute;
  top: 5px;
  left: 7px;
  right: 14px;
  display: flex;
  align-items: center;
  gap: 7px;
  z-index: 1;
}
/* 넷플릭스 워드마크 — assets/netflix.png (1800×756, RGBA).  높이 27 px
 * (사용자 spec 2026-05-26 : 옛 18 의 1.5 배) + aspect 자동 (== width ~64
 * px).  drop-shadow 로 어두운 썸 위 의 가독 성 (Netflix red 는 어두운
 * 배경 에서 도 잘 보 임 — 그림자 만 가벼 운 정도). */
.reader-linkcard-netflix-logo {
  flex: 0 0 auto;
  display: block;
  height: 27px;
  width: auto;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.55));
}
.reader-linkcard-netflix-title {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.75);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-linkcard-netflix-desc {
  position: absolute;
  bottom: 12px;
  left: 14px;
  right: 14px;
  color: #fff;
  font-size: 13px;
  line-height: 1.45;
  text-shadow: 0 1px 3px rgba(0,0,0,0.85);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  z-index: 1;
}

/* ── Apple TV+ 링크 카드 (실험중 'reader:appletv-card') ────────────
 * Netflix 와 같 은 layout (썸 fill + 좌상단 로고/제목 + 좌하단 설명).
 * 로고 차이 : assets/appletv.png 가 검정 wordmark 라 CSS filter 로 흰
 * 색 화 (brightness(0) invert(1) — 어 떤 색 의 opaque 픽셀 이 든 흰색
 * 으 로 강제 + 알파 유지).  drop-shadow 는 filter chain 의 마지막 에
 * 와 야 흰색 화 된 결과 에 그림자 가 적 용 됨. */
.reader-linkcard-appletv {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius);
  background: #000;
  color: #fff;
  text-decoration: none;
  border: 1px solid var(--border);
}
.reader-linkcard-appletv:hover { border-color: var(--border-strong); }
.reader-linkcard-appletv-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.reader-linkcard-appletv-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(0,0,0,0.75) 0%,
      rgba(0,0,0,0.10) 28%,
      rgba(0,0,0,0.10) 64%,
      rgba(0,0,0,0.90) 100%);
}
.reader-linkcard-appletv-top {
  position: absolute;
  top: 12px;
  left: 14px;
  right: 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  z-index: 1;
}
.reader-linkcard-appletv-logo {
  flex: 0 0 auto;
  display: block;
  /* 사용자 spec 2026-05-27 : 로고 의 visible bottom 이 title 의 bottom
   * 과 비슷 해 지 도록 size 축소.  title 의 line-box ≈ 15px font × 1.25
   * line-height = ~19 px.  로고 height 20 px 로 맞 추 면 align-items:
   * center 의 row height 가 max(20, 19) = 20 으 로, 둘 다 bottom 이 ~20
   * 에서 정렬.  top 은 row 상단 으 로 부 터 거의 같 으 므 로 위 / 왼쪽
   * 경계 와 의 간격 변화 없 음. */
  height: 20px;
  width: auto;
  /* 검정 wordmark → 흰색.  brightness(0) 가 모든 opaque 픽셀 을 검정 으
   * 로 만든 뒤 invert(1) 로 흰색.  알파 채널 그대로. */
  filter: brightness(0) invert(1) drop-shadow(0 1px 3px rgba(0,0,0,0.55));
}
.reader-linkcard-appletv-title {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.75);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-linkcard-appletv-desc {
  position: absolute;
  bottom: 12px;
  left: 14px;
  right: 14px;
  color: #fff;
  font-size: 13px;
  line-height: 1.45;
  text-shadow: 0 1px 3px rgba(0,0,0,0.85);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  z-index: 1;
}

/* ── 티빙 링크 카드 (실험중 'reader:tving-card') ───────────────────
 * 다른 OTT 카드 와 같 은 layout (썸 fill + 좌상단 로고/제목 + 좌하단
 * 설명).  로고 차이 : TVING 은 square red-box icon (1:1, 색 있 어 filter
 * 없 음).  PNG 가 모서리 까 지 색 으 로 채워져 있 어 visible top/left
 * 가 box top/left 와 같 음 (transparent padding 0) — 보 정 안 필요.
 * size 는 title line-box (~19 px) 와 비슷 한 22 px square + border-
 * radius 4px 로 app-icon 느낌.  align-items: center 의 row 가 max(22,
 * 19) = 22 — bottom 도 자연 스레 정렬. */
.reader-linkcard-tving {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius);
  background: #000;
  color: #fff;
  text-decoration: none;
  border: 1px solid var(--border);
}
.reader-linkcard-tving:hover { border-color: var(--border-strong); }
.reader-linkcard-tving-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.reader-linkcard-tving-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(0,0,0,0.75) 0%,
      rgba(0,0,0,0.10) 28%,
      rgba(0,0,0,0.10) 64%,
      rgba(0,0,0,0.90) 100%);
}
.reader-linkcard-tving-top {
  position: absolute;
  top: 12px;
  left: 14px;
  right: 14px;
  display: flex;
  align-items: center;
  gap: 8px;
  z-index: 1;
}
.reader-linkcard-tving-logo {
  flex: 0 0 auto;
  display: block;
  width: 22px;
  height: 22px;
  border-radius: 4px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.55);
}
.reader-linkcard-tving-title {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.75);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-linkcard-tving-desc {
  position: absolute;
  bottom: 12px;
  left: 14px;
  right: 14px;
  color: #fff;
  font-size: 13px;
  line-height: 1.45;
  text-shadow: 0 1px 3px rgba(0,0,0,0.85);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  z-index: 1;
}

/* ── 디즈니 플러스 링크 카드 (실험중 'reader:disneyplus-card') ─────
 * Netflix / Apple TV+ / TVING 와 같 은 layout.  로고 = assets/disney.png
 * (공식 wordmark + arc, 1280×696 RGBA — 모서리 까 지 가시 픽셀 이라
 * 보 정 안 필요).  height 28 px (Netflix 27 과 비슷 한 시각 무 게). */
.reader-linkcard-disneyplus {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius);
  background: #000;
  color: #fff;
  text-decoration: none;
  border: 1px solid var(--border);
}
.reader-linkcard-disneyplus:hover { border-color: var(--border-strong); }
.reader-linkcard-disneyplus-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.reader-linkcard-disneyplus-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(0,0,0,0.75) 0%,
      rgba(0,0,0,0.10) 28%,
      rgba(0,0,0,0.10) 64%,
      rgba(0,0,0,0.90) 100%);
}
.reader-linkcard-disneyplus-top {
  position: absolute;
  top: 12px;
  left: 14px;
  right: 14px;
  display: flex;
  align-items: center;
  gap: 8px;
  z-index: 1;
}
.reader-linkcard-disneyplus-logo {
  flex: 0 0 auto;
  display: block;
  height: 28px;
  width: auto;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.55));
}
.reader-linkcard-disneyplus-title {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.75);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-linkcard-disneyplus-desc {
  position: absolute;
  bottom: 12px;
  left: 14px;
  right: 14px;
  color: #fff;
  font-size: 13px;
  line-height: 1.45;
  text-shadow: 0 1px 3px rgba(0,0,0,0.85);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  z-index: 1;
}

/* Prime Video 링크 카드 (실험중 'reader:primevideo-card', SUPER_DIDS) —
 * 같은 OTT layout.  로고 (assets/primevideo.png) 는 흰 wordmark + Amazon
 * arrow 라 filter invert/brightness 없이 그대로 paint.  wordmark 가 가
 * 로 로 길 어 (3.1:1 aspect) height 22px 로 설정. */
.reader-linkcard-primevideo {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius);
  background: #000;
  color: #fff;
  text-decoration: none;
  border: 1px solid var(--border);
}
.reader-linkcard-primevideo:hover { border-color: var(--border-strong); }
.reader-linkcard-primevideo-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.reader-linkcard-primevideo-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(0,0,0,0.75) 0%,
      rgba(0,0,0,0.10) 28%,
      rgba(0,0,0,0.10) 64%,
      rgba(0,0,0,0.90) 100%);
}
.reader-linkcard-primevideo-top {
  position: absolute;
  top: 12px;
  left: 14px;
  right: 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  z-index: 1;
}
.reader-linkcard-primevideo-logo {
  flex: 0 0 auto;
  display: block;
  height: 22px;
  width: auto;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.55));
}
.reader-linkcard-primevideo-title {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.75);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-linkcard-primevideo-desc {
  position: absolute;
  bottom: 12px;
  left: 14px;
  right: 14px;
  color: #fff;
  font-size: 13px;
  line-height: 1.45;
  text-shadow: 0 1px 3px rgba(0,0,0,0.85);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  z-index: 1;
}

/* Wavve 링크 카드 — 같은 OTT
 * layout.  로고 (assets/wavve.png) 는 푸른 배경 + 흰 W 정사각형이라
 * filter invert/brightness 없이 그대로 paint.  TVING 처럼 22 px square
 * (정사각 logo). */
.reader-linkcard-wavve {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius);
  background: #000;
  color: #fff;
  text-decoration: none;
  border: 1px solid var(--border);
}
.reader-linkcard-wavve:hover { border-color: var(--border-strong); }
.reader-linkcard-wavve-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.reader-linkcard-wavve-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(0,0,0,0.75) 0%,
      rgba(0,0,0,0.10) 28%,
      rgba(0,0,0,0.10) 64%,
      rgba(0,0,0,0.90) 100%);
}
.reader-linkcard-wavve-top {
  position: absolute;
  top: 12px;
  left: 14px;
  right: 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  z-index: 1;
}
.reader-linkcard-wavve-logo {
  flex: 0 0 auto;
  display: block;
  width: 22px;
  height: 22px;
  border-radius: 4px;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.55));
}
.reader-linkcard-wavve-title {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.25;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.75);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-linkcard-wavve-desc {
  position: absolute;
  bottom: 12px;
  left: 14px;
  right: 14px;
  color: #fff;
  font-size: 13px;
  line-height: 1.45;
  text-shadow: 0 1px 3px rgba(0,0,0,0.85);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  z-index: 1;
}

/* composer 의 링크 카드 미리 보기 도 같은 시각 (사용자 spec 2026-05-26).
 * .composer-media-card 의 flex / padding / surface-2 배경 을 모두 override
 * 해 reader 카드 그대로 paint.  × 제거 버튼 은 카드 위 layer 에 두고 (top
 * row 의 text 와 겹 치 지 않 도록 우 측 공간 확보), 어 두 운 썸 위 의 가
 * 독 성 위해 배경/색 도 조정. */
.composer-media-card.composer-media-card-netflix,
.composer-media-card.composer-media-card-appletv,
.composer-media-card.composer-media-card-tving,
.composer-media-card.composer-media-card-disneyplus,
.composer-media-card.composer-media-card-primevideo,
.composer-media-card.composer-media-card-wavve,
.composer-media-card.composer-media-card-inline-player {
  display: block;
  padding: 0;
  background: transparent;
  /* aspect-ratio + border-radius 는 .reader-linkcard-{...} / .reader-media-stage 가 set */
}
/* inline-player 컴포저 카드의 × 버튼 — 어두운 stage 위에서 가독성. */
.composer-media-card-inline-player .composer-media-remove {
  z-index: 5;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
}
.composer-media-card-inline-player .composer-media-remove:hover {
  background: rgba(0, 0, 0, 0.85);
}
.composer-media-card-netflix .reader-linkcard-netflix-top,
.composer-media-card-appletv .reader-linkcard-appletv-top,
.composer-media-card-tving .reader-linkcard-tving-top,
.composer-media-card-disneyplus .reader-linkcard-disneyplus-top,
.composer-media-card-primevideo .reader-linkcard-primevideo-top,
.composer-media-card-wavve .reader-linkcard-wavve-top {
  /* × 버튼 (top: 4px right: 4px, 24x24) 의 자리 확보 — 옛 14px 의 right
   * 자리 면 title ellipsis 가 × 와 겹 침. */
  right: 38px;
}
.composer-media-card-netflix .composer-media-remove,
.composer-media-card-appletv .composer-media-remove,
.composer-media-card-tving .composer-media-remove,
.composer-media-card-disneyplus .composer-media-remove,
.composer-media-card-primevideo .composer-media-remove,
.composer-media-card-wavve .composer-media-remove {
  z-index: 2;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
}
.composer-media-card-netflix .composer-media-remove:hover,
.composer-media-card-appletv .composer-media-remove:hover,
.composer-media-card-tving .composer-media-remove:hover,
.composer-media-card-disneyplus .composer-media-remove:hover,
.composer-media-card-primevideo .composer-media-remove:hover,
.composer-media-card-wavve .composer-media-remove:hover {
  background: rgba(0, 0, 0, 0.85);
  color: #fff;
}

/* OTT 카 드 의 thumb upload 가 진 행 중 임 을 알 리 는 반 투 명 spinner
 * overlay (사 용 자 spec 2026-05-27).  cardyb fetch + Bluesky uploadBlob
 * 두 step 이 끝 나 기 전 에 사 용 자 가 작 성 버 튼 을 누 르 지 않 도 록
 * "이 미 지 받 는 중" 을 시 각 적 으 로 알 림.  uploadLinkThumb 의 .then
 * 에 서 refreshMediaEmbed 가 다 시 paint 되 면 자 동 으 로 사 라 짐. */
.composer-media-thumb-loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.35);
  pointer-events: none;
  z-index: 3;
  border-radius: var(--radius);
}
.composer-media-thumb-spinner {
  width: 32px;
  height: 32px;
  border: 3px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: composer-media-thumb-spin 0.8s linear infinite;
}
@keyframes composer-media-thumb-spin {
  to { transform: rotate(360deg); }
}

.reader-quote {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  background: var(--surface-2);
}
/* Quote mini-card inside a post threads --reader-card-alpha so the
 * wallpaper bleeds through it at the same rate as the outer card
 * instead of staying opaque "card inside card". */
body[data-reader-bg="1"] .reader-quote {
  background: color-mix(
    in srgb, var(--surface-2) calc(var(--reader-card-alpha, 1) * 100%),
    transparent
  );
}
.reader-quote-stub {
  color: var(--text-faint);
  font-style: italic;
  text-align: center;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.reader-quote-stub-glyph { font-size: 20px; font-style: normal; }
.reader-quote-stub-label { font-size: 12px; line-height: 1.4; }
/* 친절화된 stub — 차단은 더 진한 톤, notfound / detached 는 옅게 */
.reader-quote-stub-blocked { color: var(--text-muted); }
/* 차단/뮤트된 인용 '가리기' 스텁 — stub 와 동일 외형(사용자 spec 2026-06-08). */
.reader-quote-cover { color: var(--text-muted); }

/* 차단/뮤트된 인용 설정 모달 — 3 케이스 × 3 컨텍스트 그리드.
 * 폭 상한은 dialog 에 건다(.modal-body 에 걸면 오른쪽 빈 띠 — 표준 규칙). */
dialog.modal.reader-blocked-quote-modal { max-width: 460px; }
/* "나빌레라로 공유" 안내 모달 — 폭은 dialog 에(표준 규칙). */
dialog.modal.reader-share-guide-modal { max-width: 480px; }
.reader-share-guide-title { margin: 0 0 6px; font-size: 17px; }
.reader-share-guide-desc { margin: 0 0 10px; font-size: 13px; line-height: 1.55; color: var(--text-muted); }
/* 아코디언 — 환경별 접이식 섹션(누르면 그 아래 펼침). */
.reader-share-guide-acc { border-top: 1px solid var(--border); }
.reader-share-guide-acc:last-of-type { border-bottom: 1px solid var(--border); }
.reader-share-guide-acc-head {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 13px 2px; background: none; border: 0; cursor: pointer;
  font-size: 15px; font-weight: 600; color: var(--text); text-align: left;
}
.reader-share-guide-acc-caret { transition: transform 0.18s ease; color: var(--text-muted); }
.reader-share-guide-acc-head.is-open .reader-share-guide-acc-caret { transform: rotate(180deg); }
.reader-share-guide-acc-body { padding: 0 2px 12px; }
.reader-share-guide-acc-body[hidden] { display: none; }
.reader-share-guide-acc-body ol { margin: 4px 0 8px; padding-left: 20px; }
.reader-share-guide-acc-body li { font-size: 13px; line-height: 1.55; margin-bottom: 6px; }
.reader-share-guide-section { margin: 0 0 14px; }
.reader-share-guide-section h3 { margin: 0 0 6px; font-size: 14px; }
.reader-share-guide-section ol { margin: 0; padding-left: 20px; }
.reader-share-guide-section li { font-size: 13px; line-height: 1.55; margin-bottom: 4px; }
.reader-share-guide-bm {
  margin-top: 6px; padding: 12px; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
}
.reader-share-guide-bm h3 { margin: 0 0 4px; font-size: 14px; }
.reader-share-guide-bm-hint { margin: 0 0 8px; font-size: 12px; line-height: 1.5; color: var(--text-muted); }
.reader-share-guide-bm-code {
  display: block; font-family: monospace; font-size: 11px; line-height: 1.4;
  background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px; margin-bottom: 8px; word-break: break-all; white-space: pre-wrap;
  max-height: 96px; overflow-y: auto;
}
.reader-share-guide-install { display: block; width: fit-content; margin: 6px auto 8px; text-decoration: none; }
.reader-share-guide-subhead { margin: 12px 0 4px; font-size: 13px; color: var(--text-muted); font-weight: 600; }
.reader-share-guide-actions { margin-top: 16px; display: flex; justify-content: flex-end; }

/* ── 읽씹 (detective) 게임 ─────────────────────────────────────────────── */
.detective-root { max-width: 520px; margin: 0 auto; padding: 8px 0; }
/* 게임은 뷰포트 높이에 맞춰 표시 — 전역 .container 의 상하 패딩(32+32px)이
   고정 높이 폰 위에 더해져 페이지(main) 스크롤바를 만들었음.  detective 페이지
   에서만 그 패딩을 제거해 홈·게임·도움말이 스크롤 없이 들어가게 한다.
   (스테이지 선택의 긴 그리드는 그대로 main 스크롤로 둔다.) */
main:has(.detective-root) > .container { padding-top: 0; padding-bottom: 0; }
/* ── 메뉴(홈/도움말/스테이지 선택)는 전부 스마트폰 목업 안에서 전환 ──── */
.detective-menu-phone { background: #1a1a1f; }
.detective-screen {
  background: var(--bg); color: var(--text);
  /* 폰 안쪽 폭 324px × 표지 이미지 비율(594:899) ≈ 490px → 표지가 잘리지 않음. */
  height: 490px; overflow-y: auto; padding: 22px 16px;
  display: flex; flex-direction: column;
}
/* 홈 화면 = 표지 이미지 한 장으로 폰 스크린을 가득 채움(여백·텍스트 없음). */
.detective-home-screen { padding: 0; gap: 0; align-items: stretch; justify-content: flex-start; overflow: hidden; }
.detective-home-cover { width: 100%; height: 100%; object-fit: cover; display: block; user-select: none; }
.detective-help-screen { gap: 12px; }
.detective-help-title { font-size: 19px; font-weight: 800; margin: 0 0 2px; text-align: center; }
.detective-help-line { font-size: 13px; line-height: 1.65; color: var(--text); margin: 0; }
/* 플레이 방법 밑 테마 강조 한 줄. */
.detective-help-theme {
  margin: 6px 0 0; padding: 12px 14px; border-radius: 12px; text-align: center;
  /* i18n 문자열의 \n 에서 줄바꿈 — "테마로 한 추리 퍼즐!"이 둘째 줄로. */
  white-space: pre-line;
  font-size: 13px; font-weight: 700; line-height: 1.5; color: var(--accent);
  background: linear-gradient(135deg, rgba(0,133,255,0.12), rgba(92,208,255,0.12));
  border: 1px solid rgba(0,133,255,0.25);
}
.detective-stage-screen { gap: 12px; }
.detective-stage-progress { text-align: center; font-size: 13px; font-weight: 600; color: var(--text-muted); margin: 0; flex: 0 0 auto; }
/* 하단 바 — 홈은 양끝(? / 플레이), 도움말·스테이지는 가운데(메인으로). */
.detective-phone-bar {
  background: #1a1a1f; display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 12px 16px; flex: 0 0 auto;
}
.detective-phone-bar-single { justify-content: center; }
.detective-help-btn {
  width: 44px; height: 44px; border-radius: 50%; border: 0; background: #2e2e36; color: #fff;
  font-size: 20px; font-weight: 800; cursor: pointer; flex: 0 0 auto;
}
.detective-help-btn:hover { background: #3c3c46; }
.detective-play-main {
  border: 0; border-radius: 22px; background: var(--accent); color: #fff;
  font-size: 15px; font-weight: 700; padding: 12px 30px; cursor: pointer;
}
.detective-play-main:hover { filter: brightness(1.07); }
.detective-home-btn {
  border: 0; border-radius: 22px; background: #2e2e36; color: #fff;
  font-size: 14px; font-weight: 600; padding: 11px 30px; cursor: pointer;
}
.detective-home-btn:hover { background: #3c3c46; }
.detective-stage-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 12px; }
/* 폰 안 스테이지 그리드 — 칸이 작아 4열 고정 + 폰트 축소. */
.detective-stage-screen .detective-stage-grid { grid-template-columns: repeat(4, 1fr); gap: 8px; }
.detective-stage-screen .detective-stage-btn { font-size: 22px; border-radius: 12px; }
.detective-stage-screen .detective-stage-badge { font-size: 12px; top: 4px; right: 5px; }
.detective-stage-btn {
  position: relative; aspect-ratio: 1 / 1; border: 1px solid var(--border);
  border-radius: 16px; background: var(--surface); color: var(--text);
  font-size: 30px; font-weight: 700; cursor: pointer; display: flex;
  align-items: center; justify-content: center;
}
.detective-stage-btn:not(.is-locked):hover { background: var(--surface-hover); }
.detective-stage-btn.is-locked { opacity: 0.5; cursor: default; }
.detective-stage-btn.is-done { border-color: var(--accent); }
.detective-stage-num { line-height: 1; }
.detective-stage-badge { position: absolute; top: 6px; right: 8px; font-size: 14px; }
/* 유형 색띠 — 추리(파랑) / 모순(보라) 위쪽 테두리. */
.detective-stage-btn.is-deduce { box-shadow: inset 0 3px 0 0 rgba(0, 133, 255, 0.55); }
.detective-stage-btn.is-glitch { box-shadow: inset 0 3px 0 0 rgba(180, 107, 255, 0.6); }
.detective-stage-btn.is-locked { box-shadow: none; }

/* ── 게임 클리어 화면 ───────────────────────────────────────────── */
.detective-clear-card {
  max-width: 440px; margin: 24px auto 0; padding: 28px 22px; text-align: center;
  border-radius: 20px; border: 1px solid var(--border);
  background: linear-gradient(135deg, rgba(0,133,255,0.10), rgba(92,208,255,0.10));
}
.detective-clear-emoji { font-size: 54px; line-height: 1; }
.detective-clear-title { font-size: 30px; font-weight: 800; margin: 8px 0 4px; color: var(--accent); }
.detective-clear-sub { font-size: 14px; color: var(--text-muted); margin: 0 0 14px; }
.detective-clear-wrong { font-size: 18px; font-weight: 700; margin: 0; }
.detective-clear-brand { font-size: 12px; color: var(--text-muted); margin: 14px 0 0; }
.detective-clear-actions {
  display: flex; gap: 10px; max-width: 440px; margin: 16px auto 0;
}
.detective-clear-actions .btn { flex: 1 1 0; min-width: 0; }
/* 인증 컴포저 모달에서는 링크 facet 편집 UI 를 숨김(미리 박아둔 링크만 사용). */
.detective-clear-share .composer-link-section { display: none; }
/* 모순 캐치 판정 버튼 — 폰 아래 왼쪽(이상해!) / 오른쪽(정상). */
/* 모순 판정 버튼 — 컨트롤 줄 왼쪽에 컴팩트하게(어두운 바 위라 밝은 글자). */
.detective-judge-group { display: flex; gap: 6px; align-items: center; }
.detective-judge-group .btn { padding: 8px 11px; font-size: 13px; font-weight: 700; border-radius: 16px; }
.detective-judge-weird { border: 1px solid #e0245e; color: #ff5277; background: transparent; }
.detective-judge-normal { border: 1px solid #17bf63; color: #3ddc84; background: transparent; }
.detective-judge-group .is-wrong-flash { background: #e0245e !important; color: #fff !important; }
.detective-judge-group .is-dim { opacity: 0.4; cursor: default; }
/* 기명 투표 박스 — 어두운 채팅 배경 안 밝은 카드. */
.detective-vote { margin-top: 6px; background: rgba(255,255,255,0.96); border-radius: 10px; padding: 8px 10px; font-size: 12px; color: #1b2b52; max-width: 240px; }
.detective-vote-title { font-weight: 700; margin-bottom: 2px; }
.detective-vote-names { color: #445; line-height: 1.5; word-break: keep-all; }
/* 디버그 모드(SUPER_DID) — 화면 왼쪽 고정 버튼 + 펼치면 리셋. */
/* 디버그(SUPER_DID 전용) — 스마트폰 목업 아래 중앙(폰 폭에 맞춤). */
.detective-debug { margin: 16px auto 0; max-width: 340px; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.detective-debug-btn, .detective-debug-reset, .detective-debug-unlock {
  font-size: 12px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); color: var(--text); cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.15);
}
.detective-debug-reset { color: #e0245e; border-color: #e0245e; }
.detective-debug-reset[hidden] { display: none; }
.detective-debug-unlock { color: #1d9bf0; border-color: #1d9bf0; }
.detective-debug-unlock[hidden] { display: none; }
/* 스마트폰 목업 — 화면 중앙. */
.detective-phone {
  max-width: 340px; margin: 0 auto; border: 8px solid #1a1a1f; border-radius: 32px;
  background: #16234a; overflow: hidden; box-shadow: 0 8px 28px rgba(0,0,0,0.3);
  display: flex; flex-direction: column;
}
.detective-chat-header {
  background: #11193a; color: #fff; padding: 10px 12px; font-size: 15px; font-weight: 700;
  display: flex; align-items: center; justify-content: space-between; gap: 8px; flex: 0 0 auto;
}
.detective-chat-header .detective-room-wrap { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.detective-chat-header .detective-room { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.detective-chat-header .detective-count { font-size: 13px; font-weight: 500; opacity: 0.7; flex: 0 0 auto; }
/* 헤더 우측 [스테이지 선택] — 어두운 헤더 위 옅은 버튼. */
.detective-stage-exit {
  flex: 0 0 auto; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.08);
  color: #fff; font-size: 12px; font-weight: 600; padding: 5px 10px; border-radius: 14px;
  cursor: pointer; white-space: nowrap;
}
.detective-stage-exit:hover { background: rgba(255,255,255,0.18); }
/* 채팅 영역 — 메신저 목록 + 그 위로 뜨는 퀴즈/결과 오버레이(폰 크기 고정). */
.detective-chat-area { position: relative; flex: 0 0 auto; }
.detective-chat-list {
  /* 게임 폰은 크기 고정(표지/메뉴 폰과 동일감).  짧은 뷰포트에선 줄되
     220px 바닥.  큰 화면 상한 452px → 폰 총높이가 표지 폰과 맞음. */
  height: clamp(220px, calc(100vh - 284px), 452px);
  overflow-y: auto; padding: 14px 12px; display: flex;
  flex-direction: column; gap: 14px; background: #1b2b52;
}
.detective-msg { display: flex; flex-direction: column; gap: 4px; max-width: 78%; }
.detective-msg-sender { font-size: 12px; color: rgba(255,255,255,0.72); margin-left: 2px; }
.detective-bubble-wrap { display: flex; align-items: flex-end; gap: 5px; }
.detective-bubble {
  background: #fff; color: #1a1a1f; padding: 9px 12px; border-radius: 4px 14px 14px 14px;
  font-size: 14px; line-height: 1.45; box-shadow: 0 1px 1px rgba(0,0,0,0.08);
}
/* 안 읽은 사람 수 — 짙은 파랑 배경에 잘 보이는 밝은 노랑. */
.detective-unread { color: #ffd400; font-size: 12px; font-weight: 700; flex: 0 0 auto; align-self: flex-end; min-width: 10px; text-align: right; }
/* 컨트롤 줄 — 왼쪽 판정 버튼(모순 모드) + 오른쪽 턴라벨·역재생·재생. */
.detective-controls { background: #1a1a1f; display: flex; align-items: center; gap: 8px; padding: 8px 12px; flex: 0 0 auto; }
.detective-nav-group { margin-left: auto; display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.detective-ctrl {
  width: 40px; height: 40px; border-radius: 50%; border: 0; background: #2e2e36; color: #fff;
  font-size: 15px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex: 0 0 auto;
}
.detective-ctrl:not(.is-dim):hover { background: #3c3c46; }
.detective-ctrl.is-dim { opacity: 0.3; cursor: default; }
.detective-turn-label { color: rgba(255,255,255,0.65); font-size: 12px; font-weight: 600; margin: 0 2px 0 0; white-space: nowrap; }

/* 퀴즈/결과 오버레이 — 폰 채팅 영역 위로 뜨는 팝업 창. */
.detective-overlay {
  position: absolute; inset: 0; z-index: 3; display: flex; align-items: center; justify-content: center;
  padding: 14px; background: rgba(10,15,30,0.55); overflow-y: auto;
}
.detective-overlay[hidden] { display: none; }
.detective-overlay-card {
  width: 100%; background: var(--surface); border: 1px solid var(--border); border-radius: 16px;
  padding: 16px 14px; box-shadow: 0 12px 32px rgba(0,0,0,0.4); max-height: 100%; overflow-y: auto;
}
.detective-q { font-size: 15px; font-weight: 600; margin: 0 0 12px; text-align: center; }
.detective-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.detective-choice {
  position: relative; padding: 14px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface); color: var(--text); font-size: 15px; font-weight: 600; cursor: pointer;
}
.detective-choice:not(.is-wrong):hover { background: var(--surface-hover); }
.detective-choice.is-wrong { border-color: #e0245e; color: #e0245e; opacity: 0.7; cursor: default; }
.detective-x { position: absolute; top: 4px; right: 8px; color: #e0245e; font-weight: 800; }
.detective-correct { font-size: 16px; font-weight: 700; color: var(--accent); text-align: center; margin: 0 0 8px; }
.detective-explain { font-size: 13px; line-height: 1.6; color: var(--text-muted); margin: 0 0 14px; }
/* 결과 액션 한 줄 — [다음 스테이지로] [블스에 공유하기]. */
.detective-result-actions { display: flex; gap: 10px; width: 100%; max-width: 340px; margin: 4px auto 0; }
.detective-result-actions > .btn { flex: 1 1 0; min-width: 0; }
.detective-next-btn.is-dim { opacity: 0.5; cursor: default; }
/* 공유 모달 — 컴포저 호스트.  폭은 dialog 에(표준 규칙). */
dialog.modal.detective-share-modal { max-width: 480px; }
.detective-share-body { position: relative; }
.detective-share-close {
  position: absolute; top: 6px; right: 8px; z-index: 2; width: 32px; height: 32px;
  border: 0; background: none; color: var(--text-muted); font-size: 22px; cursor: pointer; line-height: 1;
}
.reader-blocked-quote-title { margin: 0 0 6px; font-size: 17px; }
.reader-blocked-quote-desc {
  margin: 0 0 14px; font-size: 12.5px; line-height: 1.5; color: var(--text-muted);
}
.reader-blocked-quote-grid {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  gap: 8px 10px;
  align-items: center;
}
.reader-bq-colhead {
  font-size: 12px; font-weight: 600; color: var(--text-muted);
  text-align: center; padding: 0 2px;
}
.reader-bq-rowlabel { font-size: 13px; line-height: 1.35; padding-right: 4px; }
.reader-bq-cell { display: flex; justify-content: center; }
/* 셀의 나빌레라 picker(답글 순서 등과 동일 .reader-reply-order-* 패턴) — 셀
 * 폭에 맞춰 가운데, 메뉴는 트리거 기준 절대 위치(전역 picker 스타일 상속). */
.reader-bq-picker { margin: 0; }
.reader-bq-actions { margin-top: 16px; display: flex; justify-content: flex-end; }

/* 차단 / 뮤트 stub 카드 — author.viewer 가 block / mute 관계면
 * renderPostCard 가 일반 카드 대신 이 stub.  우상단에는 attach
 * CardWatermark('blocked'|'muted') 로 같은 워터마크 family.
 * block 시리즈는 dim 한 빨강 dashed, mute 시리즈는 회색 dashed
 * 으로 시각 구별. */
.reader-card-blocked-stub {
  position: relative;
  border: 1px dashed var(--border);
  background: var(--surface-2);
  /* 일반 카드와 같은 워터마크 위치(top 14 right 16) + 사이즈(36) 를
   * 그대로 쓰려고 우측 패딩 60 + 위/아래 패딩 22 로 자리 확보. */
  padding: 22px 60px 22px 16px;
  min-height: 64px;
  border-radius: var(--radius);
  font-size: 13px;
  color: var(--text-muted);
  font-style: italic;
  text-align: center;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
}
.reader-card-blocked-stub-block {
  border-color: rgba(185, 28, 28, 0.45);
  background: rgba(254, 226, 226, 0.45);
}
[data-theme="dark"] .reader-card-blocked-stub-block {
  border-color: rgba(252, 165, 165, 0.4);
  background: rgba(76, 29, 29, 0.45);
}
.reader-card-blocked-stub-text { margin: 0; }

/* 프로필 모달의 blockedBy 안내 */
.reader-profile-blocked-by-notice {
  margin: 16px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  text-align: center;
  font-size: 13px;
}

/* "블루스카이 설정" 모달 — wide button stack */
.reader-bluesky-settings-modal .modal-body { min-width: min(360px, 92vw); max-width: 480px; }
.reader-bluesky-settings-stack {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 8px;
  align-items: center;
}
/* Half-width centered buttons (post-reorg).  Overrides the existing
 * .reader-settings-action-wide (width: 100%) for this stack only. */
.reader-bluesky-settings-stack > .btn,
.reader-bluesky-settings-stack > .reader-settings-action-wide {
  width: min(280px, 70%);
}
/* 자동화 라벨 토글 row — sits under the 5 buttons in the Bluesky
 * settings modal.  Centered to match the buttons above (same width
 * as the buttons, no top divider). */
.reader-bluesky-settings-extra {
  margin-top: 10px;
}
.reader-bluesky-settings-extra > .reader-settings-row {
  margin: 0 auto;
  width: min(280px, 70%);
  justify-content: space-between;
  gap: 8px;
}
.reader-bluesky-settings-extra .row-leader { display: none; }
.reader-blocked-actors-dialog .modal-body { min-width: min(420px, 92vw); max-width: 560px; }

/* 모더레이션 리스트 액션 — header 설명문 아래.  두 버튼 (전부
 * 뮤트 / 전부 차단) 가 나란히. */
.reader-list-subscribe-row {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.reader-list-action-btn { font-size: 13px; padding: 6px 14px; }
.reader-list-subscribe-btn { font-size: 13px; padding: 6px 14px; }

/* 모더레이션 리스트 관리 모달 — 뮤트 / 차단 두 섹션 */
.reader-mod-lists-dialog .modal-body { min-width: min(440px, 92vw); max-width: 600px; }
.reader-mod-lists-section { margin-top: 14px; }
.reader-mod-lists-section + .reader-mod-lists-section { margin-top: 18px; }
.reader-mod-lists-section-title {
  margin: 0 0 8px;
  font-size: 13px;
  color: var(--text-muted);
  text-transform: none;
}
.reader-quote-head {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: 13px;
  margin-bottom: 6px;
}
.reader-quote-avatar { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }
.reader-quote-name { font-weight: 600; }
.reader-quote-handle { color: var(--text-muted); }
.reader-quote-time { color: var(--text-faint); font-size: 12px; }
.reader-quote-body { font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
.reader-quote-feed strong, .reader-quote-list strong { font-size: 14px; }
.reader-quote-feed p, .reader-quote-list p {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--text-muted);
}
/* Custom-feed embed card — shared between the in-post quote
 * embed (renderQuoteEmbed's generatorView branch), the share
 * composer preview, and future search-result lists. Mirrors
 * the Bluesky official-app feed card: 36px square avatar on
 * the left, name + byline + (optional) description + likeCount
 * stacked on the right. */
.reader-feed-embed-card {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  /* Asymmetric bottom padding — keeps the stats row from feeling
   * cramped against the card edge while still tighter than the
   * top so the descriptionic feels weighted upward. */
  padding: 12px 12px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}
/* Interactive cards (in-post embed, future search results) hint
 * at clickability and surface a soft hover state. Non-interactive
 * cards (share-composer preview) skip both. */
.reader-feed-embed-card.is-interactive { cursor: pointer; transition: background-color 120ms; }
.reader-feed-embed-card.is-interactive:hover { background: color-mix(in srgb, var(--surface-2) 70%, var(--text)); }
.reader-feed-embed-card.is-interactive:focus-visible {
  outline: 2px solid var(--accent, #3b82f6);
  outline-offset: 2px;
}
.reader-feed-embed-avatar {
  width: 36px;
  height: 36px;
  border-radius: 8px;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--surface);
}
.reader-feed-embed-avatar-fallback {
  background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--text)));
}
.reader-feed-embed-text { flex: 1 1 auto; min-width: 0; }
.reader-feed-embed-title {
  font-weight: 700;
  font-size: 14px;
  line-height: 1.3;
  word-break: break-word;
}
.reader-feed-embed-byline {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 2px;
}
.reader-feed-embed-desc {
  font-size: 13px;
  color: var(--text-muted);
  margin: 6px 0 0;
  line-height: 1.4;
  white-space: pre-wrap;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* line-clamp clips visible lines but pre-wrap'd trailing
   * newlines past the clamp still reserve box height, leaving
   * a phantom blank line above the ♥ row. Pin the box height
   * to exactly 2× line-height so the bottom of the desc sits
   * tight against whatever follows. */
  max-height: 2.8em;
}
/* Stats row at the bottom of the embed card — pin (subscribed?) +
 * heart (liked?) + likeCount. Both icons are presentational; the
 * actual toggle lives in the feed modal / inline header. Coloured
 * with .is-on accents when the user has the action set, matching
 * the action-row colour vocabulary. */
.reader-feed-embed-stats {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-faint);
}
/* Pin + heart wraps in the stats row carry color via .is-on so the
 * SVG's currentColor fill / stroke picks up the accent. Pin: faint
 * monochrome when not subscribed, accent blue when subscribed
 * (matches the feed modal's action-row pin). Heart: faint when
 * not liked, red when liked. */
.reader-feed-embed-pin,
.reader-feed-embed-likes {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--text-faint);
}
.reader-feed-embed-pin.is-on { color: var(--accent, #3b82f6); }
.reader-feed-embed-likes.is-on { color: #ef4444; }
.reader-feed-embed-likes-count { line-height: 1; }
/* Composer-extraTop slot containing the share preview card —
 * labeled with a small caption so the user understands this is
 * the embedded card they're about to ship, not part of the body. */
.reader-compose-preview-box {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 10px;
}
.reader-compose-preview-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text-faint);
}
.reader-embed-stack { display: flex; flex-direction: column; gap: 8px; }
.reader-video-stage {
  width: 100%;
  background: #000;
  border-radius: var(--radius);
  overflow: hidden;
  display: block;
}
/* video embed with presentation:"gif" — atproto lex hint 가 "GIF 처럼
 * 렌더" 라는 신호.  reader-media-badge ("GIF") 가 우상단 에 떠 외부
 * Tenor / Giphy 카드 와 동일 한 시각 언어 로 통일. */
.reader-video-stage.is-gif-presentation {
  position: relative;
  background: var(--bg-soft, #f3f3f3);
}
.reader-video {
  width: 100%;
  height: 100%;
  display: block;
}
/* Old placeholder for the "video unsupported" message — kept as a
 * fallback in case a future code path needs to surface a hint. */
.reader-video-placeholder {
  padding: 32px 16px;
  text-align: center;
  background: var(--surface-2);
  border-radius: var(--radius);
  color: var(--text-muted);
}
.reader-counts {
  display: flex;
  gap: 18px;
  color: var(--text-faint);
  font-size: 13px;
  padding-top: 4px;
}
/* Interactive action row — reply / repost / like / quote / share.
 * Buttons sit in a flat row with hover affordances; the "on" state
 * for like/repost colours the glyph to signal the user's own action. */
.reader-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  /* Halve the visual spacing above the action row. card.gap is 10px;
   * margin-top: -2 brings the effective gap to 8 (≈ half of the
   * previous 16: card.gap + padding-top:6). */
  padding-top: 0;
  margin-top: -2px;
  margin-left: -6px;
  /* Universal time-wrap: when the icons + time chip don't fit on
   * one line, time wraps to its own row below the icons.
   * `flex-wrap: wrap` + `flex-shrink: 0` on .reader-time means
   * time keeps its full width AND wraps cleanly when it can't fit
   * — no need to detect "is this absolute? is this modal? is this
   * own-post?" anymore. `margin-left: auto` on .reader-time still
   * pushes it to the right edge on whichever row it lands. */
  flex-wrap: wrap;
  row-gap: 4px;
}
.reader-action {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 4px 6px;
  border: 0;
  background: transparent;
  border-radius: var(--radius);
  font-size: 13px;
  color: var(--text-faint);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, transform 0.1s;
}
.reader-action:hover:not(:disabled) { background: var(--surface-hover); color: var(--text-muted); }
.reader-action:active:not(:disabled) { transform: scale(0.96); }
.reader-action:disabled { opacity: 0.6; cursor: wait; }
/* Read-only mode (블스투데이 historical view): the action row icons
 * are visible as a snapshot but the wait cursor + 0.6 opacity from
 * :disabled would look like "loading" instead of "view-only".
 * .is-readonly restores the default cursor and softens the opacity
 * a touch so the row reads as a static summary. */
.reader-action.is-readonly {
  cursor: default;
  opacity: 0.85;
}
.reader-action.is-readonly:hover {
  background: transparent;
  color: var(--text-faint);
}
/* Gated mode: post author's threadgate / postgate blocks the current
 * viewer from this interaction (reply / quote).  AppView surfaces
 * viewer.replyDisabled / viewer.embeddingDisabled and we mirror that
 * here.  Cursor not-allowed + heavier opacity than is-readonly so the
 * "you can't do this" signal is stronger than "this is a snapshot". */
.reader-action.is-gated {
  cursor: not-allowed;
  opacity: 0.4;
}
.reader-action.is-gated:hover {
  background: transparent;
  color: var(--text-faint);
}
.reader-action-glyph { font-size: 15px; line-height: 1; }
/* Reply chat-bubble SVG sits higher than the like/repost text glyphs
 * because its visual mass is in the upper portion of the viewBox
 * (the tail extends to y=21 but the bubble body is centered around
 * y=11). Nudge down a hair so the bubble's optical centre lines up
 * with the count digits and the other glyphs. */
.reader-action-reply .reader-action-glyph svg {
  transform: translateY(2px);
}
.reader-action-count { font-variant-numeric: tabular-nums; }
/* "+" extras button — only rendered on own posts. Sits between the
 * share button and the time stamp. Wrapped in a relative div so the
 * dropdown menu can anchor to its bottom-right corner. */
.reader-action-bonus-wrap {
  position: relative;
  display: inline-flex;
}
.reader-action.reader-action-bonus .reader-action-glyph {
  font-size: 17px;
  font-weight: 600;
  line-height: 1;
}
.reader-bonus-menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 20;
  padding: 6px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Size to the widest item — no min-width so a single-entry menu
   * doesn't pad blank space to the right of its label. */
  width: max-content;
  max-width: 90vw;
  /* Slide-in animation: starts slightly raised + transparent, eases
   * to its actual position when .is-visible lands (JS adds the class
   * on requestAnimationFrame after un-hiding so the transition has
   * a clean from-state).  Flip-up variant inverts the translate so
   * the menu appears to drop UP from the trigger. */
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 140ms ease-out, transform 140ms ease-out;
}
.reader-bonus-menu.reader-bonus-menu-flipped {
  transform: translateY(4px);
}
.reader-bonus-menu.is-visible {
  opacity: 1;
  transform: translateY(0);
}
.reader-bonus-menu[hidden] { display: none; }
.reader-bonus-menu-item {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  background: transparent;
  text-align: left;
  font: inherit;
  font-size: 13px;
  color: var(--text);
  border-radius: 6px;
  cursor: pointer;
}
.reader-bonus-menu-item:hover { background: var(--surface-hover); }
.reader-bonus-menu-item:focus,
.reader-bonus-menu-item:focus-visible { outline: none; background: var(--surface-hover); }

/* Per-post "+" → 🎲 추첨 modal — wraps the standalone Drawing tool
 * inside a wider dialog so the reactor lists + cutoff picker don't
 * cramp. The custom header replaces the dialog's default modal-body
 * h3 with a row that pairs the title and a ✕ close affordance. */
dialog.modal.reader-bonus-drawing-modal {
  max-width: min(720px, 96vw);
  width: calc(100% - 24px);
}
.reader-bonus-drawing-modal .modal-body { padding: 18px; }
.reader-bonus-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.reader-bonus-modal-head h3 { margin: 0; font-size: 17px; }
.reader-bonus-modal-close {
  width: 32px;
  height: 32px;
  border: 0;
  background: transparent;
  font-size: 18px;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 50%;
}
.reader-bonus-modal-close:hover { background: var(--surface-hover); color: var(--text); }
.reader-bonus-drawing-slot { display: block; }
/* Reaction-view modal — same chassis as the drawing modal. */
dialog.modal.reader-bonus-reaction-modal {
  max-width: min(720px, 96vw);
  width: calc(100% - 24px);
}
.reader-bonus-reaction-modal .modal-body { padding: 18px; }
.reader-bonus-reaction-slot { display: block; }
/* Client-side reactions list — each reaction is a regular post
 * card via renderPostCard, stacked under the subject post. The
 * subject card is dimmed slightly so the user knows it's the
 * source, not one of the reactions. */
.reader-bonus-reaction-subject {
  margin-bottom: 14px;
  padding-bottom: 14px;
  border-bottom: 1px dashed var(--border);
  opacity: 0.85;
}
.reader-bonus-reaction-status {
  margin: 8px 0;
  color: var(--text-faint);
  font-size: 13px;
}
.reader-bonus-reaction-list {
  display: flex; flex-direction: column; gap: 10px;
}
.reader-bonus-reaction-card-wrap { display: block; }

/* Own-profile "+" button overlay — circle-outlined + glyph that
 * floats at the top-right of the profile feed-header banner. Sits
 * above the banner's dim overlay (::before z-index 0) and the
 * inner content (z-index 1) so the button stays clickable
 * regardless of where the user clicks elsewhere on the header
 * (which still opens the edit-profile modal). */
.reader-profile-bonus-wrap {
  position: absolute;
  top: 10px;
  right: 10px;
  /* Was z-index: 2 — same level as .reader-card-watermark, which
   * combined with `position: absolute` here created a stacking
   * context that TRAPPED the inner .reader-profile-bonus-menu at
   * z-index 2 in the root.  The pinned-post card (later in DOM)
   * then painted its pin watermark over the menu.  Bumping to 10
   * keeps us below the sidebar (z-index: 11) but above every
   * .reader-card-watermark on the page. */
  z-index: 10;
}
/* TTS FAB (.nabilera-tts-fab, 44 px right:16 top:16 fixed) 가 visible
 * 일 때 — 프로필 헤더 의 검색 / 피드 / 부가도구 버튼 wrap 이 FAB 와
 * 겹치지 않도록 왼쪽 으로 FAB 의 지름 (44 px) 만큼 shift.  사용자
 * spec.  body.has-tts-fab class 는 ttsRefreshChrome 가 토글. */
body.has-tts-fab .reader-profile-bonus-wrap {
  right: calc(10px + 44px);
}
.reader-profile-bonus-btn {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1.5px solid var(--text-muted);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  color: var(--text);
  font-size: 20px;
  font-weight: 500;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  transition: background 0.15s, transform 0.1s, border-color 0.15s;
}
.reader-profile-bonus-btn:hover {
  background: var(--surface);
  border-color: var(--text);
}
.reader-profile-bonus-btn:active { transform: scale(0.94); }
.reader-profile-bonus-btn:focus,
.reader-profile-bonus-btn:focus-visible { outline: none; }
.reader-profile-bonus-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Hug the widest label — see .reader-bonus-menu for the same
   * reasoning. max-width clamps so the menu can't overflow off
   * screen on narrow viewports. */
  width: max-content;
  max-width: 90vw;
  /* Stack above the .reader-card-watermark (z-index: 2) — the
   * pinned-post pin glyph was bleeding through the dropdown
   * because the menu's default z-index: auto lost to the
   * watermark layer in the same stacking context. */
  z-index: 100;
}
.reader-profile-bonus-menu[hidden] { display: none; }

/* Profile-scoped "+ → 도구" modal — wider than the default 420px so
 * embedded Cloud / Rhythm / Cleaner panels have room to breathe.
 * Each tool brings its own toolbar header, so the modal itself
 * doesn't render an h3 title — just a floating ✕ in the top-right
 * corner that sits above whatever the tool paints. */
dialog.modal.reader-profile-bonus-modal {
  max-width: min(1000px, 96vw);
  width: calc(100% - 24px);
}
.reader-profile-bonus-modal .modal-body {
  position: relative;
  padding: 16px;
}
.reader-profile-bonus-close {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 5;
  width: 32px;
  height: 32px;
  border: 0;
  background: var(--surface);
  font-size: 18px;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}
.reader-profile-bonus-close:hover {
  background: var(--surface-hover);
  color: var(--text);
}
.reader-profile-bonus-slot { display: block; padding-top: 6px; }

/* Custom theme modal — slot tabs at top, slot editor (name + base
 * toggle + grouped token rows + actions) below. Wide so the 22
 * token rows have room to breathe and the alpha slider still
 * lines up. */
dialog.modal.reader-custom-theme-modal {
  max-width: min(720px, 96vw);
  width: calc(100% - 24px);
}
.reader-custom-theme-modal .modal-body { padding: 18px; }
/* Font-change modal. Houses the font-family dropdown + the
 * font-size segmented control + a live preview area (pangrams +
 * a miniature post card). */
dialog.modal.reader-font-change-modal {
  max-width: min(480px, 96vw);
}
.reader-font-change-modal .modal-body { padding: 14px 16px 18px; }
/* Custom font-family picker — replaces the native <select> so
 * each option row can be rendered in its own font for live
 * preview (browsers don't reliably style <option> elements). */
/* "나빌레라식" custom dropdown — used in the radio settings modal
 * (and any future modal that wants a popover-style picker instead
 * of a native <select>).  Shares the look-and-feel of .reader-
 * font-picker but a separate class-namespace so future variants
 * can diverge if needed.  All radio-modal dropdowns (rate / speed
 * mode / number mode / base language / voice) route through this. */
.reader-radio-picker {
  position: relative;
  display: inline-block;
  flex: 0 1 auto;
  max-width: 260px;
}
.reader-radio-picker-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 8px 5px 10px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  text-align: left;
}
.reader-radio-picker-trigger:hover { background: var(--surface-hover); }
.reader-radio-picker-trigger[aria-expanded="true"] { border-color: var(--accent); }
.reader-radio-picker-current {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.reader-radio-picker-caret {
  flex: 0 0 auto;
  color: var(--text-faint);
  font-size: 11px;
  line-height: 1;
}
.reader-radio-picker-menu {
  /* Default-position fallback for the brief gap between trigger
   * insert and the first open (positionMenu writes inline left /
   * top / max-height once openMenu portals the node into <body>). */
  position: fixed;
  top: 0;
  left: 0;
  min-width: 100%;
  max-height: 320px;
  overflow-y: auto;
  overscroll-behavior: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
  padding: 4px;
  /* z-index has to beat the radio settings dialog (which sits at
   * ~100 via the native top-layer + a stacking-context shadow).
   * 10000 puts the portal-menu visibly above everything. */
  z-index: 10000;
}
.reader-radio-picker-option {
  display: block;
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
  color: var(--text);
  font-size: 14px;
  text-align: left;
  white-space: nowrap;
}
.reader-radio-picker-option:hover { background: var(--surface-hover); }
.reader-radio-picker-option.is-selected {
  background: var(--accent-soft);
  color: var(--accent);
}

.reader-font-picker {
  position: relative;
  display: inline-block;
  flex: 0 1 auto;
  max-width: 200px;
}
.reader-font-picker-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 8px 5px 10px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  text-align: left;
}
.reader-font-picker-trigger:hover { background: var(--surface-hover); }
.reader-font-picker-trigger[aria-expanded="true"] {
  border-color: var(--accent);
}
.reader-font-picker-current {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* font-family set inline per current selection */
}
.reader-font-picker-caret {
  flex: 0 0 auto;
  color: var(--text-faint);
  font-size: 11px;
  line-height: 1;
}
.reader-font-picker-menu {
  position: absolute;
  top: calc(100% + 4px);
  /* Anchor right-edge of menu to right-edge of trigger so the
   * menu opens LEFTWARD (the trigger sits on the right side of
   * the settings row, near the upload button — opening rightward
   * pushed the menu past the modal edge). */
  right: 0;
  min-width: 100%;
  /* Cap to about 8 rows (~40px each) so the bottom row is
   * partially clipped, signaling there's more to scroll —
   * matches the .lang-menu pattern. With 11 built-in fonts +
   * user uploads the list can grow well past 10 entries. */
  max-height: 320px;
  overflow-y: auto;
  overscroll-behavior: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
  padding: 4px;
  z-index: 20;
}
.reader-font-picker-option {
  display: block;
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
  color: var(--text);
  font-size: 14px;
  text-align: left;
  white-space: nowrap;
  /* font-family set inline per row for the preview effect */
}
.reader-font-picker-option:hover { background: var(--surface-hover); }
.reader-font-picker-option.is-selected {
  background: var(--accent-soft);
  color: var(--accent);
}
.reader-font-picker-separator {
  margin: 4px 4px;
  border-top: 1px dashed var(--border);
  text-align: center;
  color: var(--text-faint);
  font-size: 10px;
  padding-top: 4px;
}
.reader-font-upload-btn {
  flex: 0 0 auto;
  padding: 4px 10px;
  font-size: 12px;
}
.reader-font-upload-error {
  margin: 4px 0 8px;
  color: var(--danger);
  font-size: 12px;
}
/* Font-modal rows: label on the left, control(s) on the right,
 * with a dashed leader filling the gap between them — visually
 * separates the label from its control without forcing each row
 * into its own boxy section. */
.reader-font-change-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
}
.reader-font-change-row-label {
  flex: 0 0 auto;
  font-size: 13px;
  color: var(--text);
}
/* Shared dashed-leader span. Drop one between any "label" and
 * "controls" pair in a flex row and it fills the gap with a 1px
 * dashed line, giving the "라벨 ——————— [컨트롤]" leader look. */
.row-leader {
  flex: 1 1 auto;
  height: 1px;
  border-bottom: 1px dashed var(--border);
  align-self: center;
  min-width: 12px;
}
.reader-font-change-row-controls {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}
.reader-font-preview {
  margin-top: 12px;
  padding: 12px;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--surface-2);
}
/* Parent-scoped so the (0,2,0) specificity beats the
 * .modal-body p { margin: 0 0 16px } rule that lives lower in this
 * file and would otherwise leave a 16px gap between the pangram
 * lines. */
.reader-font-preview .reader-font-preview-line {
  margin: 0;
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
}
.reader-font-preview .reader-font-preview-line + .reader-font-preview-line { margin-top: 2px; }
.reader-font-preview .reader-font-preview-line + .reader-font-preview-card { margin-top: 12px; }
.reader-font-preview-card {
  margin: 0;
  border: 1px solid var(--border);
  background: var(--surface);
}

/* Advanced settings modal — opens off the basic settings dialog
 * via the "고급 설정" button. Holds every advanced row in a
 * single .reader-settings-stack (rel-tags / card-tint / push
 * notif / sidebar order, etc.). */
dialog.modal.reader-advanced-settings-modal {
  max-width: min(560px, 96vw);
}
.reader-advanced-settings-modal .modal-body { padding: 14px 16px 18px; }

/* Sidebar reorder modal. Left column hosts the two reorderable
 * lists (top + bottom sections), right column shows a miniature
 * sidebar previewing the current arrangement live. Items use
 * pointer-events drag (HTML5 dnd fires unreliably inside the
 * native <dialog> top-layer). is-dragging makes the source pass
 * through the hit-test so elementFromPoint finds the row below,
 * is-drag-over draws an accent border on the target. */
dialog.modal.reader-sidebar-order-modal {
  max-width: min(560px, 96vw);
}
.reader-sidebar-order-modal .modal-body { padding: 14px 16px 18px; }
.reader-sidebar-order-grid {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 14px;
  align-items: start;
  margin: 8px 0 16px;
}
.reader-sidebar-order-left {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.reader-sidebar-order-section {
  margin: 4px 0 6px;
  font-size: 13px;
  color: var(--text-muted);
  font-weight: 500;
  /* 각 라벨(보관함/상단/하단/미리보기) 을 바로 아래 박스 폭 에 맞 춰
   * 중앙정렬 — 라벨 은 각 컬럼 폭 을 꽉 채 우 므 로 text-align 으 로 충분.
   * 사용자 spec 2026-05-29. */
  text-align: center;
}
.reader-sidebar-order-section:first-child { margin-top: 0; }
/* 글쓰기 도구 설정 모달 — 선택한 도구들 / 보관함 두 박스 + 드래그. */
dialog.modal.reader-ctools-modal { max-width: 420px; }
.reader-ctools-section { margin: 12px 0 4px; font-size: 13px; color: var(--text-muted); }
.reader-ctools-list {
  list-style: none; margin: 0; padding: 6px;
  min-height: 48px; display: flex; flex-direction: column; gap: 6px;
  border: 1px dashed var(--border); border-radius: 10px; background: var(--surface-2);
}
.reader-ctools-list.is-empty::after {
  content: attr(data-empty); display: block; text-align: center;
  color: var(--text-faint); font-size: 12px; padding: 8px 0;
}
.reader-ctools-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: 8px;
  background: var(--surface); border: 1px solid var(--border);
  cursor: grab; touch-action: none; user-select: none;
}
.reader-ctools-item.is-dragging { opacity: 0.6; cursor: grabbing; pointer-events: none; }
.reader-ctools-grip { flex: 0 0 auto; color: var(--text-faint); font-size: 14px; }
.reader-ctools-glyph { flex: 0 0 auto; width: 22px; text-align: center; font-size: 16px; }
.reader-ctools-label { flex: 1 1 auto; min-width: 0; font-size: 14px; }
/* 보관함 박스와 '기본값으로/닫기' 줄 사이 간격. */
dialog.modal.reader-ctools-modal .modal-actions { margin-top: 16px; }

/* 컴포저 번역 모달 — 결과 미리보기 + 경고. */
dialog.modal.composer-translate-modal { max-width: 460px; }
.composer-translate-warn { color: var(--warn, #b26a00); }
.composer-translate-preview {
  margin: 8px 0; padding: 10px 12px; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
  white-space: pre-wrap; word-break: break-word; max-height: 40vh; overflow-y: auto;
  font-size: 14px; line-height: 1.5;
}

/* ── LLM 보조 모달 (문장 다듬기 · 이어쓰기) — 번역 모달과 동일한 룩 ── */
dialog.modal.composer-llm-modal { max-width: 460px; }
.composer-llm-warn { color: var(--warn, #b26a00); }
.composer-llm-runrow { display: flex; align-items: center; gap: 10px; margin: 6px 0; }
.composer-llm-runrow .hint { margin: 0; }
.composer-llm-preview {
  margin: 8px 0; padding: 10px 12px; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
  white-space: pre-wrap; word-break: break-word; max-height: 40vh; overflow-y: auto;
  font-size: 14px; line-height: 1.5;
}

/* ── AI 도움말(실험) 모달 ── */
dialog.modal.reader-aihelp-modal { max-width: 520px; }
.reader-aihelp-input { width: 100%; box-sizing: border-box; margin: 8px 0 16px; }
.reader-aihelp-results { display: flex; flex-direction: column; gap: 4px; }
.reader-aihelp-item {
  border: 1px solid var(--border); border-radius: 10px;
  padding: 8px 12px; background: var(--surface-2);
}
.reader-aihelp-item > summary { cursor: pointer; font-weight: 600; }
.reader-aihelp-item > p { margin: 8px 0 0; font-size: 14px; line-height: 1.55; color: var(--text); }
.reader-aihelp-airow { display: flex; align-items: center; gap: 12px; margin-top: 18px; }
.reader-aihelp-airow .hint { margin: 0; }
.reader-aihelp-answer {
  margin: 10px 0 0; padding: 10px 12px; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
  white-space: pre-wrap; word-break: break-word; max-height: 40vh; overflow-y: auto;
  font-size: 14px; line-height: 1.6;
}

/* ── AI 도우미 모달 — 다듬기/이어쓰기/번역 모드 탭 (밑줄형 탭 바) ── */
.composer-assist-tabs {
  display: flex;
  gap: 4px;
  margin: 10px 0 12px;
  border-bottom: 1px solid var(--border);
}
.composer-assist-tab {
  flex: 1 1 0;
  padding: 9px 8px;
  font-size: 14px;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px; /* overlap the container's bottom border */
  color: var(--text-faint);
  cursor: pointer;
}
.composer-assist-tab:hover { color: var(--text); }
.composer-assist-tab.is-on {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}
/* short per-mode description directly under the tab bar */
.composer-assist-desc { margin: 0 0 10px; }
/* 번역 모드: 언어 드롭다운을 번역하기 버튼 옆에 */
.composer-llm-runrow { flex-wrap: wrap; }
.composer-assist-lang { display: inline-flex; align-items: center; }

/* ── 음성 받아쓰기(STT) 모달 ── */
dialog.modal.composer-stt-modal { max-width: 460px; }
.composer-stt-recrow { display: flex; align-items: center; gap: 12px; margin: 8px 0; }
.composer-stt-recrow .hint { margin: 0; }
.composer-stt-rec.is-recording { background: #ef4444; border-color: #ef4444; }
.composer-stt-preview {
  margin: 8px 0; padding: 10px 12px; min-height: 64px; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
  white-space: pre-wrap; word-break: break-word; max-height: 40vh; overflow-y: auto;
  font-size: 15px; line-height: 1.6;
}
.composer-stt-interim { color: var(--text-faint); }
.composer-stt-ph { color: var(--text-faint); }

/* ── 유니코드 꾸밈 글씨 모달 — 스타일별 미리보기 버튼 목록 ── */
dialog.modal.composer-fancy-modal { max-width: 420px; }
.composer-fancy-style {
  display: flex; align-items: baseline; gap: 10px; width: 100%;
  margin: 6px 0; text-align: left; justify-content: flex-start;
}
.composer-fancy-style-name { flex: 0 0 auto; font-size: 12px; color: var(--text-faint); min-width: 76px; }
.composer-fancy-style-prev { flex: 1 1 auto; font-size: 16px; word-break: break-word; }

/* ── 그림 그리기 모달 ── */
dialog.modal.composer-drawing-modal { max-width: 700px; }
.composer-drawing-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 8px 0; }
.composer-drawing-swatches { display: flex; gap: 6px; }
.composer-drawing-swatch {
  width: 26px; height: 26px; border-radius: 50%; padding: 0;
  border: 2px solid var(--border); cursor: pointer;
}
.composer-drawing-swatch.is-on { border-color: var(--accent, #58a6ff); box-shadow: 0 0 0 2px var(--accent, #58a6ff); }
.composer-drawing-size { flex: 1 1 120px; min-width: 80px; }
.composer-drawing-eraser.is-active { background: color-mix(in srgb, var(--accent) 18%, var(--surface)); color: var(--accent); border-color: var(--accent); }
.composer-drawing-canvas-wrap { width: 100%; }
.composer-drawing-canvas {
  display: block; width: 100%; height: auto; touch-action: none;
  background: #fff; border: 1px solid var(--border); border-radius: 10px; cursor: crosshair;
}

/* ── DrawPad — 고도화 그림 편집기 ─────────────────────────────────── */
dialog.modal.drawpad-modal { max-width: 780px; }
.drawpad-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 8px 0; }
.drawpad-iconbtn {
  min-width: 40px; height: 38px; padding: 0 8px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 18px; line-height: 1;
}
.drawpad-iconbtn.is-on { background: color-mix(in srgb, var(--accent) 18%, var(--surface)); color: var(--accent); border-color: var(--accent); }
.drawpad-iconbtn:disabled { opacity: 0.4; cursor: default; }
.drawpad-swatch {
  width: 38px; height: 38px; border-radius: 8px; padding: 0;
  border: 2px solid var(--border); cursor: pointer;
}
.drawpad-size { flex: 1 1 120px; min-width: 90px; }
.drawpad-brushes { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; }
.drawpad-brush { padding: 4px 10px; font-size: 13px; }
.drawpad-brush.is-on { background: color-mix(in srgb, var(--accent) 18%, var(--surface)); color: var(--accent); border-color: var(--accent); }

/* stage — canvas + selection overlay stacked */
.drawpad-stage { position: relative; width: 100%; line-height: 0; }
.drawpad-canvas, .drawpad-overlay {
  display: block; width: 100%; height: auto; touch-action: none;
  border-radius: 10px;
}
.drawpad-canvas { background: #fff; border: 1px solid var(--border); }
.drawpad-overlay { position: absolute; inset: 0; pointer-events: none; }

/* color popover (HSV wheel) */
.drawpad-colorpop {
  margin: 8px 0; padding: 10px; border: 1px solid var(--border);
  border-radius: 10px; background: var(--surface-2); width: max-content;
}
.drawpad-colorpop[hidden] { display: none; }
.drawpad-wheel { display: block; touch-action: none; cursor: crosshair; }

/* layer panel — sits between the canvas and the action buttons; the
 * generous top/bottom margins give the requested breathing room between
 * the canvas and the 첨부/취소 row. */
.drawpad-layers { margin: 18px 0 20px; border: 1px solid var(--border); border-radius: 10px; padding: 8px; }
.drawpad-layers-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-size: 13px; color: var(--text-faint); }
.drawpad-layer {
  display: flex; align-items: center; gap: 6px; padding: 5px 6px;
  border-radius: 8px; border: 1px solid transparent;
}
.drawpad-layer.is-active { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
.drawpad-layer-vis, .drawpad-layer-act {
  border: none; background: transparent; cursor: pointer; font-size: 15px; padding: 2px 4px; border-radius: 6px;
}
.drawpad-layer-vis:hover, .drawpad-layer-act:hover { background: var(--surface-hover); }
.drawpad-layer-act.is-on { color: var(--accent); }
.drawpad-layer-name { flex: 1 1 auto; text-align: left; border: none; background: transparent; color: var(--text); cursor: pointer; font-size: 13px; min-width: 40px; }
.drawpad-blend { flex: 0 0 auto; font-size: 12px; max-width: 96px; }
.drawpad-layer-op { flex: 0 1 80px; min-width: 56px; }
.drawpad-actions { margin-top: 4px; }

.reader-sidebar-order-list {
  list-style: none;
  margin: 0 0 8px;
  padding: 4px;
  min-height: 32px;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--surface-2);
}
.reader-sidebar-order-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  margin: 4px 0;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  cursor: grab;
  user-select: none;
  touch-action: none;
}
/* hover 강조 는 실제 hover 가능 한 기기(마우스) 에서 만.  터치 기기 는
 * :hover 가 마지막 터치 행 에 들 러 붙 어(sticky hover), 드래그 후 DM /
 * 설정 같 은 행 이 선택된 것 처 럼 남 는 버그 (사용자 보고 2026-05-29). */
@media (hover: hover) {
  .reader-sidebar-order-item:hover { background: var(--surface-hover); }
}
.reader-sidebar-order-item.is-dragging {
  /* Source button lifts during drag so the other rows can slide
   * underneath. pointer-events:none takes the source out of the
   * hit-test so elementFromPoint finds the row below it. The
   * displaced rows animate via inline transform set by flipAnimate
   * — we explicitly leave .is-dragging transform-free so the FLIP
   * code doesn't fight a scale rule. */
  background: var(--surface-hover);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
  pointer-events: none;
  cursor: grabbing;
}
.reader-sidebar-order-item.is-drag-over {
  border-color: var(--accent);
  box-shadow: 0 -2px 0 var(--accent) inset;
}
.reader-sidebar-order-grip {
  color: var(--text-faint);
  font-size: 14px;
  line-height: 1;
  cursor: grab;
}
.reader-sidebar-order-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--text-muted);
}
.reader-sidebar-order-label { flex: 1; min-width: 0; }
/* Mini-sidebar preview — same chassis as the real sidebar (column
 * of 34px tiles), only static. The "feeds" gap stands in for the
 * dynamic custom-feed rail and stretches via flex:1 so the top
 * section sticks to the top of the preview and the bottom section
 * sticks to the bottom, matching how the real sidebar is laid out
 * with sidebarTopFixed at top:12 and sidebarBottom at bottom:12. */
/* 오른쪽 컬럼 = "미리보기" 라벨 + 프리뷰 박스.  grid cell 로서 row 높이
 * (= 좌측 리스트 높이) 에 맞 춰 stretch, 라벨 아래 프리뷰 가 남 은 높이 를
 * 채 우 도록 flex column. */
.reader-sidebar-order-right {
  display: flex;
  flex-direction: column;
  align-self: stretch;
}
.reader-sidebar-order-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 8px 6px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  min-height: 360px;
  flex: 1 1 auto;
}
.reader-sidebar-order-preview-top,
.reader-sidebar-order-preview-bottom {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: center;
  flex: 0 0 auto;
}
.reader-sidebar-order-preview-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-muted);
  border: 1px solid var(--border);
}
.reader-sidebar-order-preview-feeds {
  font-size: 11px;
  line-height: 1.15;
  color: var(--text-faint);
  text-align: center;
  width: 36px;
  padding: 6px 0;
  margin: 4px 0;
  border-top: 1px dashed var(--border);
  border-bottom: 1px dashed var(--border);
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Vertical stack — characters paint top-to-bottom, kept upright
   * for CJK + Latin scripts alike. The narrow 36px rail isn't wide
   * enough to render horizontally, so vertical writing-mode fits
   * the label without truncation regardless of locale. */
  writing-mode: vertical-rl;
  text-orientation: upright;
  letter-spacing: 0.1em;
}
/* ── 보관함 (1.4 실험 "사이드바 편집") ──────────────────────────────
 * 안 쓰는 버튼 을 끌어다 두 면 사이드바 에 서 숨 김.  그리드 는 보관함
 * 추가 시 [보관함][리스트][미리보기] 3열.  카드 는 미리보기 와 같 은
 * 박스 스타일/크기. */
.reader-sidebar-order-grid.has-archive {
  /* 보관함·미리보기 는 같 은 고정 너비 (52px = 미리보기 의 자연 너비).
   * auto 로 두 면 보관함 의 빈 안내 문구(긴 한 줄) 가 컬럼 을 335px 까지
   * 넓 혀 리스트 가 찌그러 짐 — 고정 으 로 막 고 안내 문구 는 카드 안 에 서
   * 줄바꿈 (사용자 보고 2026-05-29). */
  grid-template-columns: 52px minmax(0, 1fr) 52px;
}
.reader-sidebar-order-archive-col {
  display: flex;
  flex-direction: column;
  align-self: stretch;
}
.reader-sidebar-order-archive {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px 6px;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  min-height: 360px;
  flex: 1 1 auto;
}
.reader-sidebar-order-archive.is-drop-target {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-soft);
}
/* 보관함 아이콘 버튼 — 사이드바 버튼 칩 모양 (드래그 가능). */
.reader-sidebar-order-archive-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  cursor: grab;
  flex: 0 0 auto;
  touch-action: none;
  user-select: none;
}
.reader-sidebar-order-archive-btn.is-dragging { opacity: 0.5; cursor: grabbing; }
/* 보관함 아이콘 을 리스트 로 끌 때 그 리스트 하이라이트. */
.reader-sidebar-order-list.is-drop-target {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-soft);
}
/* Action row needs breathing room from the bottom list so the
 * Close / Reset buttons don't visually merge into the drop zone
 * above. */
.reader-sidebar-order-actions {
  margin-top: 14px;
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

/* ── 액션 버튼 관리 모달 (1.7 실험) ──────────────────────────────────
 * 사이드바 편집 모달 의 그리드/리스트/보관함 스타일 을 그대로 재사용
 * (.reader-sidebar-order-*).  여기 는 차이 나는 부분 만 :
 *   · 미리보기 = 사이드바 미리보기 와 같은 단일 세로 레일.  상단 스택 은
 *     위, 하단 스택 은 아래(space-between), 버튼 은 좌우 중앙.  (좁은 52px
 *     레일 안 에 서 "우상단/우하단" 구분 은 리스트 라벨 이 이미 전달 —
 *     미리보기 는 세로 쌓임 순서 만 보여 줌.  중첩 점선 박스 / right 정렬
 *     로 어긋나 보이 던 것 제거, 2026-06-05.)
 *   · 보관함 의 잠금(기능 off) 아이콘 = 옅게·드래그 불가. */
.reader-action-modal .reader-action-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 6px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  min-height: 360px;
  flex: 1 1 auto;
}
.reader-action-preview-corner {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
  flex: 0 0 auto;
}
/* 리스트 순서 = 화면 순서(위→아래) : 하단 그룹 도 첫 항목 이 위, 마지막
 * 항목 이 화면 가장 바닥 — 일반 column(역순 아님).  편집 리스트 와 일치. */
.reader-action-preview-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-muted);
  border: 1px solid var(--border);
}
/* 보관함 의 잠금(기능 OFF) 아이콘 — 옅게 + 드래그/grab 불가, 클릭 시 토스트. */
.reader-sidebar-order-archive-btn.reader-action-archive-locked {
  opacity: 0.4;
  cursor: not-allowed;
}
.reader-sidebar-order-archive-btn.reader-action-archive-locked:hover {
  background: var(--surface);
}
/* 모달 안 아이콘 글리프(이모지/SVG) 정렬 — 리스트(22)·보관함(34)·미리보기(28)
 * 슬롯 어디 든 가운데. */
.reader-action-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  line-height: 1;
}

/* ── 안전 캡쳐 (1.7 실험) ─────────────────────────────────────────────
 * 하단 액션 버튼(.reader-safecap-btn) — 다른 FAB 와 같은 외형.  위치 는
 * layoutActionButtons 가 inline 으로 설정. */
.reader-safecap-btn {
  position: fixed;
  right: 18px;
  bottom: 18px;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,0.25);
  z-index: 10;
  transition: transform 0.15s ease;
}
.reader-safecap-btn:hover { transform: scale(1.06); }
.reader-safecap-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.reader-safecap-btn.nabilera-action-fab[hidden] { display: none; }
/* 위로 가기 — 단일/모바일 에선 일반 하단 FAB(스택), 데스크탑 멀티컬럼 에선
 * per-column(.reader-col-action-btn) 로 렌더. */
.reader-coltop-btn {
  position: fixed;
  right: 18px;
  bottom: 18px;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,0.25);
  z-index: 10;
  transition: transform 0.15s ease;
}
.reader-coltop-btn:hover { transform: scale(1.06); }
.reader-coltop-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.reader-coltop-btn.nabilera-action-fab[hidden] { display: none; }

/* 멀티컬럼 액션 버튼 (컬럼 마다 우하단) — 컬럼 wrapper 기준 절대 위치.
 * 컬럼 의 positioning context 는 .reader-main / .reader-column 블록 에
 * 직접 position:relative 로 부여(아래 별도 rule 로 빼면 부수 효과 테스트
 * 정규식 에 걸림). */
.reader-col-action-btn {
  position: absolute;
  right: 18px;
  bottom: 18px;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,0.25);
  z-index: 6;
  transition: transform 0.15s ease;
}
.reader-col-action-btn:hover { transform: scale(1.06); }
.reader-col-action-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* 액티브 컬럼(.reader-main) 은 데스크탑 멀티컬럼 에서 자기 자신 이 스크롤러
 * 라, absolute 자식 버튼 은 콘텐츠 와 함께 스크롤 됨(사용자 보고 2026-06-07).
 * 뷰포트 fixed 로 바꿔 스크롤 에 안 따라가게 — right/bottom 은 JS 가 컬럼
 * 우하단 에 맞춰 inline 으로 설정(layoutMultiColButtons).  fixed 는 overflow
 * 조상 에 클리핑 안 됨. */
.reader-col-action-btn.reader-col-action-btn-fixed {
  position: fixed;
}
/* position:fixed FAB 들을 컴포지터(자체 레이어)로 승격 — Windows Chrome 의
 * 분수 디스플레이 배율(125 / 150 %)에서 스크롤 중 position:fixed 요소가
 * device-pixel 에 다시 스냅 되 며 re-rasterize → 1 device-pixel 씩 "툭툭"
 * 떠 보이 는 컴포지팅 지터(사용자 보고 2026-06-08, Windows Chrome 1920px
 * 멀티컬럼 홈피드 스크롤).  getBoundingClientRect 는 CSS 픽셀 이라 좌표 상
 * 으 론 멀쩡 해 헤드리스(배율 100%)로는 안 잡힘 — 컴포지터 레이어 로 올리면
 * 스크롤 프레임 마다 재래스터 안 하 고 합성 만 → 지터 제거.  will-change 는
 * 요소 자신 에 거 는 hint 라 fixed 위치/containing block 안 바뀌고,
 * :hover { transform: scale(1.06) } 도 그대로(여 기 서 transform 미설정). */
.nabilera-action-fab,
.reader-col-action-btn.reader-col-action-btn-fixed {
  will-change: transform;
}
/* 안전 캡쳐 스크린샷 모드 — per-column 버튼 도 함께 숨겨 깨끗하게 찍힘. */
body.safecap-shot .reader-col-action-btn { display: none !important; }

/* 액션 버튼 편집 — 고정(pinned) 항목(맨 위 스크롤) : 드래그 불가, 자물쇠 +
 * '고정' 배지, 옅은 배경 으로 구분. */
.reader-action-item-pinned {
  cursor: default;
  background: var(--surface-2);
  opacity: 0.85;
}
@media (hover: hover) {
  .reader-action-item-pinned:hover { background: var(--surface-2); }
}
.reader-action-pin-lock { cursor: default; font-size: 12px; }
.reader-action-pin-badge {
  flex: 0 0 auto;
  font-size: 11px;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px 6px;
  background: var(--surface);
}

/* 안전 모드 효과 — 토글 시 실제 화면(body) 에 바로 적용(= 미리보기). */
body.safecap-blur-posts .reader-card { filter: blur(10px); }
/* 프로필 가리기 — 프로필 헤더(배너/아바타/이름) + 각 포스트카드 의 작성자
 * 헤더(아바타/디스플레이네임/핸들).  본문 은 그대로(블러 대상 아님). */
body.safecap-blur-profile .reader-feed-header.is-profile-header,
body.safecap-blur-profile .reader-card-head { filter: blur(12px); }
body.safecap-blur-customfeed .reader-sidebar-feeds { filter: blur(10px); }
body.safecap-blur-sidebar .reader-sidebar { filter: blur(12px); }
body.safecap-blur-multicol .reader-column-banner { filter: blur(10px); }
/* 미디어 가리기 — 첨부 이미지/비디오/GIF/링크카드(외부 임베드). */
body.safecap-blur-media .reader-images,
body.safecap-blur-media .reader-media-stage,
body.safecap-blur-media .reader-video-stage,
body.safecap-blur-media .reader-media-iframe { filter: blur(16px); }
/* (배경 가리기 는 JS 가 data-reader-bg 속성 을 잠시 떼 어 처리 — CSS 불필요.) */

/* 스크린샷 모드 — 시트/ FAB 숨기고 '완료' 버튼만 남겨 깨끗하게 캡쳐. */
body.safecap-shot .nabilera-action-fab { display: none !important; }
.reader-safecap-exit {
  position: fixed;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: 2147483600;
  padding: 8px 16px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,0.3);
}
.reader-safecap-exit:hover { background: var(--surface-hover); }

/* 안전 캡쳐 시트 — 하단 비-모달 패널(위쪽 실제 화면 이 라이브 미리보기). */
.reader-safecap-sheet {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  width: min(440px, 100%);
  z-index: 2147483600;
  box-sizing: border-box;
  padding: 14px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  background: var(--surface);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -8px 28px rgba(0,0,0,0.28);
}
.reader-safecap-title { margin: 0 0 4px; font-size: 16px; }
.reader-safecap-hint { margin: 0 0 10px; }
.reader-safecap-opts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 12px;
  margin-bottom: 12px;
}
.reader-safecap-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  cursor: pointer;
  user-select: none;
}
.reader-safecap-opt input { margin: 0; flex: 0 0 auto; }
.reader-safecap-opt-label { min-width: 0; font-size: 13px; }
.reader-safecap-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}
@media (max-width: 420px) {
  .reader-safecap-opts { grid-template-columns: 1fr; }
}
.custom-theme-tabs {
  display: flex;
  gap: 6px;
  flex-wrap: nowrap;
  overflow-x: auto;
  margin-bottom: 14px;
  /* No scrollbar — the row scrolls horizontally if it overflows
   * but doesn't show a track. Touch swipe + wheel scrolling still
   * work since overflow-x is auto. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.custom-theme-tabs::-webkit-scrollbar { display: none; }
/* 슬롯 탭 아래 "추천 테마" / "테마 검색" 줄 (1.4 실험). */
.custom-theme-discover {
  display: flex;
  gap: 8px;
  margin-bottom: 14px;
}
.custom-theme-discover-btn {
  flex: 1 1 0;
  white-space: nowrap;
}
/* "테마 검색" 모달 — 검색 결과 (공유 테마 포스트) 스크롤 리스트. */
dialog.modal.reader-theme-search-modal { max-width: min(560px, 96vw); }
.reader-theme-search-modal .modal-body { min-width: min(520px, 92vw); }
.reader-theme-search-sub {
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--text-muted);
}
.reader-theme-search-results {
  max-height: 64vh;
  overflow-y: auto;
}
.reader-theme-search-status,
.reader-theme-search-empty {
  padding: 28px 8px;
  text-align: center;
  font-size: 13px;
  color: var(--text-muted);
}
.reader-theme-search-list { display: flex; flex-direction: column; gap: 12px; }
.reader-theme-search-sentinel { height: 1px; }
.custom-theme-tab { flex: 0 0 auto; }
.custom-theme-tab {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  padding: 6px 10px;
  font-size: 12px;
  border-radius: 999px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.custom-theme-tab:hover { background: var(--surface-hover); }
.custom-theme-tab.is-editing {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--text);
}
.custom-theme-tab.is-active { font-weight: 600; }
.custom-theme-tab-dot {
  color: var(--accent);
  font-size: 8px;
  line-height: 1;
}
.custom-theme-editor {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-height: 70vh;
  overflow-y: auto;
  padding-right: 4px;
}
.custom-theme-active-hint {
  margin: 0;
  padding: 8px 12px;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  font-size: 12px;
  color: var(--text);
}
.custom-theme-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.custom-theme-row .field-label { flex: 0 0 70px; }
.custom-theme-name-input { flex: 1; min-width: 0; }
/* 슬롯 이름 입력 옆 의 "슬롯 복사" 버튼 — flex 0 으로 input 의 flex 영역
 * 침범 안 함, padding 도 짧 게. */
.custom-theme-copy-btn { flex: 0 0 auto; padding: 6px 12px; font-size: 13px; }
.custom-theme-base-toggle {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.custom-theme-base-btn {
  border: 0;
  background: transparent;
  color: var(--text-muted);
  padding: 6px 14px;
  font-size: 12px;
  cursor: pointer;
}
.custom-theme-base-btn.is-active {
  background: var(--accent);
  color: #fff;
}
.custom-theme-groups {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.custom-theme-group-title {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.custom-theme-token-row {
  /* Flex (not grid) so each row sits on its own — label hugs its
   * natural text width, dashed leader fills whatever gap is left,
   * controls cluster on the right.  Matches the .reader-settings-
   * row family used everywhere else in the app, where the leader
   * starts RIGHT after the label text (rather than after a fixed-
   * width label column that left awkward whitespace between text
   * end and dash start). */
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
}
/* Label: hugs its text, may wrap to a second line for very long
 * locale translations (no ellipsis truncation that would collapse
 * "리포스트 카드 배경" / "리포스트 카드 테두리" into the same
 * displayed string).  flex: 0 0 auto pins the label to its content
 * width so the leader immediately picks up at the text's right edge. */
.custom-theme-token-label {
  flex: 0 0 auto;
  font-size: 13px;
  color: var(--text);
  line-height: 1.25;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* Leader fills the remaining horizontal space between the label and
 * the controls (.row-leader's base rule already has flex: 1 + dashed
 * bottom-border). Tighten the row gap on either side so the dashes
 * sit close to BOTH the label and the swatch — the leader's job is
 * to connect them, not to leave breathing room. */
.custom-theme-token-row > .row-leader {
  height: 1px;
}
/* Controls cluster on the right.  Each non-leader / non-label
 * sibling (swatch / color / alpha / reset) hugs its own intrinsic
 * size so the slider stays a consistent 120px end-to-end. */
.custom-theme-token-row > .custom-theme-swatch,
.custom-theme-token-row > .custom-theme-color,
.custom-theme-token-row > .custom-theme-reset-token,
.custom-theme-token-row > .custom-theme-alpha {
  flex: 0 0 auto;
}
.custom-theme-token-row > .custom-theme-alpha { width: 120px; }
.custom-theme-swatch {
  display: inline-block;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid var(--border);
}
.custom-theme-color {
  width: 36px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: transparent;
  cursor: pointer;
}
.custom-theme-color::-webkit-color-swatch-wrapper { padding: 0; }
.custom-theme-color::-webkit-color-swatch { border: 0; border-radius: 3px; }
.custom-theme-alpha {
  width: 100%; /* fills the fixed 120 px grid cell */
  accent-color: var(--accent);
}
.custom-theme-reset-token {
  width: 28px;
  height: 28px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-size: 14px;
  cursor: pointer;
  border-radius: 50%;
}
.custom-theme-reset-token:hover { background: var(--surface-hover); color: var(--text); }
.custom-theme-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
  justify-content: center;
}
.custom-theme-io {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  justify-content: center;
}
@media (max-width: 540px) {
  .custom-theme-token-row { gap: 6px; }
  /* Narrow viewport: shrink the alpha slider so the controls cluster
   * still fits next to a long wrapped label.  Flex pushes everything
   * naturally; nothing else needs to change. */
  .custom-theme-token-row > .custom-theme-alpha { width: 90px; }
}

/* Per-category preview cards inside the custom-theme modal. Each
 * sits between the group title and that group's token rows, and
 * paints a representative chunk of UI using the slot's tokens. The
 * outer container picks up --bg from the preview zone so the whole
 * card reads as a "miniature surface" against whatever background
 * the slot is going for. */
.cthm-preview {
  background: var(--bg);
  padding: 10px;
  border-radius: 8px;
  border: 1px dashed var(--border);
  font-size: 12px;
}
.cthm-preview-surface { display: flex; flex-direction: column; gap: 6px; }
.cthm-preview-surface .cthm-card {
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
}
.cthm-preview-surface .cthm-card-2 { background: var(--surface-2); border-color: transparent; }
.cthm-preview-surface .cthm-card-hover { background: var(--surface-hover); border-color: transparent; }

.cthm-preview-border { display: flex; gap: 8px; flex-wrap: wrap; background: var(--surface); border-style: solid; }
.cthm-preview-border .cthm-border,
.cthm-preview-border .cthm-border-strong {
  padding: 6px 10px;
  border-radius: 6px;
  color: var(--text-muted);
  background: var(--surface);
}
.cthm-preview-border .cthm-border { border: 1px solid var(--border); }
.cthm-preview-border .cthm-border-strong { border: 1px solid var(--border-strong); }

.cthm-preview-text { background: var(--surface); border-style: solid; }
.cthm-preview-text p { margin: 0; line-height: 1.5; }
.cthm-preview-text .cthm-text-body { color: var(--text); }
.cthm-preview-text .cthm-text-muted { color: var(--text-muted); }
.cthm-preview-text .cthm-text-faint { color: var(--text-faint); }

.cthm-preview-accent { background: var(--surface); border-style: solid; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.cthm-btn-primary, .cthm-btn-primary-hover {
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 12px;
  color: #fff;
  cursor: default;
}
.cthm-btn-primary { background: var(--accent); }
.cthm-btn-primary-hover { background: var(--accent-hover); }
.cthm-chip-soft {
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 11px;
  background: var(--accent-soft);
  color: var(--accent);
}

.cthm-preview-danger { background: var(--surface); border-style: solid; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.cthm-btn-danger, .cthm-btn-danger-hover {
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 12px;
  color: #fff;
  cursor: default;
}
.cthm-btn-danger { background: var(--danger); }
.cthm-btn-danger-hover { background: var(--danger-hover); }
.cthm-banner-danger {
  padding: 6px 10px;
  border-radius: 6px;
  background: var(--danger-soft);
  border: 1px solid var(--danger-border);
  color: var(--danger-text);
  font-size: 11px;
}

.cthm-banner-warning {
  padding: 8px 12px;
  background: var(--warning-soft);
  border: 1px solid var(--warning-border);
  color: var(--warning-text);
  border-radius: 6px;
  font-size: 12px;
}

.cthm-preview-toast { background: var(--surface); border-style: solid; }
.cthm-toast {
  display: inline-block;
  padding: 8px 14px;
  background: var(--toast-bg);
  color: var(--toast-fg);
  border-radius: 999px;
  font-size: 12px;
}

/* Realistic preview — mock of the actual reader UI (sidebar +
 * post card + button row + banner) so the user sees how a slot's
 * token edits read in context, not just as per-token swatches.
 * Inherits .cthm-preview's background (= --bg) so the surrounding
 * "page" is also the slot's bg.  Tokens inside resolve against
 * the slot's inline custom properties via CSS variable
 * inheritance (refreshPreview writes them onto the root). */
.cthm-realistic-preview {
  padding: 12px;
  border-style: solid;
}
.cthm-mock-shell {
  display: flex;
  gap: 10px;
  align-items: stretch;
}
.cthm-mock-sidebar {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex-shrink: 0;
  width: 70px;
}
.cthm-mock-tab {
  padding: 6px 8px;
  border-radius: 6px;
  font-size: 11px;
  color: var(--text-muted);
  text-align: center;
  cursor: default;
}
.cthm-mock-tab.is-active {
  background: var(--surface-hover);
  color: var(--accent);
  font-weight: 600;
}
.cthm-mock-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.cthm-mock-post {
  display: flex;
  gap: 10px;
  padding: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.cthm-mock-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--accent-soft);
  flex-shrink: 0;
}
.cthm-mock-postbody {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.cthm-mock-namerow {
  display: flex;
  gap: 6px;
  align-items: baseline;
  flex-wrap: wrap;
}
.cthm-mock-name {
  font-weight: 600;
  font-size: 12px;
  color: var(--text);
}
.cthm-mock-handle {
  font-size: 11px;
  color: var(--text-faint);
}
.cthm-mock-body {
  margin: 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text);
}
.cthm-mock-actions {
  display: flex;
  gap: 14px;
  margin-top: 2px;
}
.cthm-mock-action {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  color: var(--text-faint);
}
.cthm-mock-action svg { display: block; }
.cthm-mock-btnrow {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
}
.cthm-mock-btn {
  padding: 5px 12px;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 500;
  cursor: default;
}
.cthm-mock-btn.primary {
  background: var(--accent);
  color: #fff;
}
.cthm-mock-btn.secondary {
  background: var(--surface-hover);
  color: var(--text);
  border: 1px solid var(--border);
}
.cthm-mock-banner {
  padding: 6px 10px;
  background: var(--warning-soft);
  border: 1px solid var(--warning-border);
  color: var(--warning-text);
  border-radius: 6px;
  font-size: 11px;
}

/* Auto-publish warning that lives right above the "추첨 시작" button.
 * Bordered + colored to read as a stop-and-think label without
 * looking like an error. Same banner shows in the standalone tool
 * AND the per-post modal. Uses the amber --warning-* tokens that
 * the rest of the site already shares for "heads up" surfaces. */
.drawing-auto-publish-warning {
  margin: 12px 0 8px;
  padding: 10px 14px;
  border: 1px solid var(--warning-border);
  border-radius: var(--radius-md, 8px);
  background: var(--warning-soft);
  font-size: 13px;
  line-height: 1.55;
  color: var(--warning-text);
}

/* Inline label-on-left + control-on-right row for the drawing tool's
 * engagement / count sections. Used both in the standalone 블스추첨기
 * page and in the per-post "+" embedded modal. The cutoff section
 * uses a checkbox in the label slot and a separate sub-row for the
 * date / time / tz pickers when the checkbox is on. */
.drawing-row-layout {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.drawing-row-layout .section-head {
  /* Some drawing-row-layout usages don't sit under .composer-section,
   * so the .field-label inside isn't already in a flex context.
   * Make section-head itself a flex row so label + ⓘ sit inline. */
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 0;
  flex: 0 0 auto;
}
.drawing-row-layout .scope-toggle {
  flex: 0 0 auto;
  margin: 0;
}
.drawing-row-layout input.input[type="number"] {
  flex: 0 0 120px;
  width: 120px;
  text-align: right;
}
.drawing-cutoff-checkbox {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  cursor: pointer;
}
.drawing-cutoff-sub {
  margin-top: 10px;
}
.drawing-cutoff-sub[hidden] { display: none; }
/* Share sits next to the four action buttons (no count beside it);
 * time has margin-left: auto so it pushes itself alone to the far
 * right. Row order: […  ↻  ♥  "  ↗  ────  time]. */
/* "On" state — when the user has liked / reposted / bookmarked this
 * post. Bookmark fills the line-icon's silhouette (the path's
 * inline fill="none" attribute is overridden by `fill: currentColor`
 * since CSS beats SVG presentation attributes). */
.reader-action-like.is-on { color: #e84a6c; }
.reader-action-like.is-on .reader-action-glyph { color: #e84a6c; }
/* Outline by default → fill flips on when the user has liked the
 * post. Same fill-toggle pattern bookmark + pin use; the SVG itself
 * (likeActionGlyph) ships with fill:none so the empty heart is the
 * unliked state. */
.reader-action-like .reader-action-glyph svg { display: block; }
.reader-action-like.is-on .reader-action-glyph svg { fill: currentColor; }
.reader-action-repost.is-on { color: #2e9648; }
.reader-action-repost.is-on .reader-action-glyph { color: #2e9648; }
.reader-action-bookmark .reader-action-glyph svg { display: block; }
.reader-action-bookmark.is-on { color: var(--accent); }
.reader-action-bookmark.is-on .reader-action-glyph { color: var(--accent); }
.reader-action-bookmark.is-on svg path { fill: currentColor; }
/* Engagements 액션 — bookmark 다음, 사람 둘 silhouette.  toggle 상태
 * 가 없 으므로 색 은 다른 아이콘 과 같은 outline / hover 만. */
.reader-action-engagements .reader-action-glyph svg { display: block; }
[data-theme="dark"] .reader-action-like.is-on { color: #ff6b8b; }
[data-theme="dark"] .reader-action-like.is-on .reader-action-glyph { color: #ff6b8b; }
[data-theme="dark"] .reader-action-repost.is-on { color: #4fc46d; }
[data-theme="dark"] .reader-action-repost.is-on .reader-action-glyph { color: #4fc46d; }
.reader-action-share.flashed { background: var(--accent-soft); color: var(--accent); }
/* Delete action — own-post only, leftmost in the action row.
 * Muted red so it's distinguishable from the other actions but
 * not so loud that the user keeps glancing at it. */
.reader-action-delete { color: var(--text-faint); }
.reader-action-delete:hover { color: #c43a3a; background: rgba(196, 58, 58, 0.08); }
[data-theme="dark"] .reader-action-delete:hover { color: #ff7a7a; background: rgba(255, 122, 122, 0.1); }
/* The "더 보기" button used to live below every feed (timeline,
 * search, notifications). Each feed auto-prefetches when the user
 * scrolls within reach of the end — IntersectionObserver for
 * timeline / search, a scrollHeight check for notifications — so
 * the button has been redundant noise. Keep the DOM element + its
 * "hidden" toggles around in JS so the existing pagination
 * bookkeeping isn't disturbed, but hide it permanently here. */
.reader-load-more { display: none; }
.reader-end { text-align: center; color: var(--text-faint); margin: 16px 0; }

/* 블스타래 */
.thread-options { padding: 16px; margin-bottom: 16px; }
.thread-opt-stack {
  /* Force the label row and the toggle onto separate lines — both
   * children are inline-flex (.inline-row + .scope-toggle) so without
   * this they'd otherwise sit side by side on a wide viewport. */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}
.thread-template-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  max-width: 360px;
  margin-top: 4px;
}
.thread-template-row[hidden] { display: none; }
.thread-template-label { margin: 0; }
.thread-template-input { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.thread-delay-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.thread-delay-input { max-width: 100px; }
.thread-list { display: flex; flex-direction: column; gap: 16px; margin-bottom: 16px; }
.thread-item {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 12px;
  background: var(--surface);
}
.thread-item-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}
.thread-item-label { font-weight: 600; color: var(--text-muted); }
.thread-item-move {
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: 14px;
  padding: 2px 8px;
  cursor: pointer;
  line-height: 1;
}
.thread-item-move:first-of-type { margin-left: auto; }
.thread-item-move:hover:not(:disabled) { color: var(--text); border-color: var(--border-strong); }
.thread-item-move:disabled { opacity: 0.3; cursor: not-allowed; }
.thread-item-move[hidden] { display: none; }
.thread-item-status { font-size: 13px; font-weight: 500; }
.thread-item-status.pending { color: var(--text-muted); }
.thread-item-status.ok { color: #16a34a; }
[data-theme="dark"] .thread-item-status.ok { color: #4ade80; }
.thread-item-status.err { color: #dc2626; }
[data-theme="dark"] .thread-item-status.err { color: #f87171; }
.thread-item-status.skip { color: var(--text-faint); }
.thread-item-remove {
  margin-left: auto;
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 13px;
  padding: 2px 6px;
  cursor: pointer;
}
.thread-item-remove:hover { color: var(--text); }
.thread-item-remove[hidden] { display: none; }
.thread-add-btn { display: block; margin: 0 auto 16px; }
.thread-add-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.thread-publish-row { display: flex; justify-content: center; margin-top: 8px; }
.thread-publish { min-width: 200px; }
.thread-status { text-align: center; margin-top: 8px; }
.thread-status.ok { color: #16a34a; }
[data-theme="dark"] .thread-status.ok { color: #4ade80; }
.thread-status.err { color: #dc2626; }
[data-theme="dark"] .thread-status.err { color: #f87171; }

.tool-section-title {
  margin: 24px 0 12px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.tool-section-title:first-of-type { margin-top: 8px; }
/* Surfaced only for sessions on a planned-tool allowlist. Sits
 * between the hero and the first category title — soft accent
 * border + muted body so it reads as a heads-up, not a warning. */
.home-tester-notice {
  margin: 4px 0 0;
  padding: 10px 14px;
  /* Uniform 1px accent-soft border on all four sides — the
   * earlier 3px accent left edge made the box look like a
   * different element on that side. */
  border: 1px solid var(--accent-soft, var(--border));
  border-radius: var(--radius-md, 8px);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.home-tester-notice p {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}
.tool-grid {
  display: grid;
  gap: 16px;
  grid-template-columns: 1fr;
  margin-bottom: 8px;
}
@media (min-width: 640px) {
  .tool-grid { grid-template-columns: 1fr 1fr; }
}
.tool {
  display: block;
  color: var(--text);
}
.tool .card { height: 100%; transition: border-color 0.15s, box-shadow 0.15s; }
.tool[href]:hover { text-decoration: none; }
.tool[href]:hover .card {
  border-color: #bae6fd;
  box-shadow: var(--shadow-md);
}
.tool .head {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 8px;
}
.tool .icon {
  width: 32px; height: 32px;
  background: var(--accent-soft);
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
}
.tool h2 { margin: 0; font-size: 16px; }
.tool .tag {
  margin-left: auto;
  font-size: 11px;
  background: var(--surface-hover);
  color: var(--text-muted);
  padding: 2px 8px;
  border-radius: 999px;
}
.tool.disabled .card { opacity: 0.55; }
.tool p { margin: 0; color: var(--text-muted); font-size: 13px; }

/* Login form */
.login {
  max-width: 420px;
  margin: 24px auto;
}
.login h2 {
  margin: 0 0 8px;
  font-size: 18px;
}
.login p { margin: 0 0 16px; color: var(--text-muted); }

.auth-section {
  display: block;
  margin-bottom: 28px;
}
.auth-section .login { margin-top: 0; margin-bottom: 0; }

/* Tool pages: shown when no session exists. */
.login-prompt {
  max-width: 460px;
  margin: 24px auto;
  text-align: center;
}
.login-prompt h2 { margin: 0 0 8px; font-size: 18px; }
.login-prompt p { margin: 0 0 16px; color: var(--text-muted); }
.login-prompt .btn { display: inline-flex; }

/* Cleaner */
.cleaner-toolbar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: 12px;
  margin-bottom: 20px;
}
.cleaner-toolbar h1 { margin: 0 0 4px; font-size: 28px; letter-spacing: -0.01em; }
.cleaner-toolbar p { margin: 0; color: var(--text-muted); font-size: 13px; }
.cleaner-toolbar .actions { display: flex; gap: 8px; }
.tool-title-row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.tool-title-row h1 { margin: 0; }

/* 블스북리뷰 — 서재 grid + inline expanded post card. */
.review-shelf {
  margin-top: 32px;
  padding-top: 24px;
  border-top: 1px solid var(--surface-stroke, var(--text-faint));
}
.review-shelf-head { margin-bottom: 12px; }
.review-shelf-head h2 {
  margin: 0 0 4px;
  font-size: 20px;
  letter-spacing: -0.01em;
}
.review-shelf-head p { margin: 0; color: var(--text-muted); font-size: 13px; }
.review-shelf-status { margin: 0 0 12px; color: var(--text-muted); }
.review-shelf-status[hidden] { display: none; }
.review-shelf-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
  gap: 10px;
}
.review-shelf-tile {
  position: relative;
  display: block;
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--surface-stroke, var(--text-faint));
  border-radius: 6px;
  overflow: hidden;
  cursor: pointer;
  aspect-ratio: 2 / 3;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.review-shelf-tile:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.review-shelf-tile:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.review-shelf-cover {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.review-shelf-expanded {
  margin-top: 20px;
}
.review-shelf-expanded:empty { display: none; }

/* Vertical stack — filter card on top, results below. Matches the
 * drawing tool's single-column layout so the filter doesn't sit as
 * a narrow strip on the left with vast empty space on the right
 * before the user has triggered a preview. */
.cleaner-grid {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.scope-toggle {
  display: inline-flex;
  background: var(--surface-hover);
  border-radius: 8px;
  padding: 2px;
  margin-bottom: 10px;
  font-size: 12px;
}
.scope-with-inputs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.scope-with-inputs .scope-toggle { margin-bottom: 0; }
.date-range {
  display: flex;
  align-items: center;
  gap: 6px;
}
.date-range .input { margin: 0; }
.date-sep {
  color: var(--text-faint);
  font-weight: 500;
}
.date-input-wrap {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 1 1 auto;
  min-width: 0;
}
.date-input-wrap .input {
  flex: 1 1 auto;
  min-width: 0;
}
.date-clear-btn {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--surface-hover);
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.date-clear-btn:hover {
  background: var(--border);
  color: var(--text);
}
.scope-toggle .scope-btn {
  border: 0;
  background: transparent;
  color: var(--text-muted);
  padding: 4px 12px;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 500;
}
.scope-toggle .scope-btn:hover { color: var(--text); }
.scope-toggle .scope-btn.active {
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
}

.chip-all { font-weight: 700; }
.chip-all.active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-hover);
}

.preview-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 24px;
}
.preview-action .preview-btn {
  min-width: 280px;
  padding: 10px 18px;
  font-size: 14px;
}
.preview-hint {
  font-size: 12px;
  color: var(--text-muted);
  margin: 8px 0 0;
  text-align: center;
}

.mode-toggle {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface-2);
}
.mode-section .mode-toggle { margin-top: 2px; }
.mode-toggle .mode {
  border: 0;
  background: transparent;
  color: var(--text-muted);
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
}
.mode-toggle .mode + .mode { border-left: 1px solid var(--border); }
.mode-toggle .mode:hover { color: var(--text); }
.mode-toggle .mode.active {
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
}

/* fieldset reset — the wrapper around all filter sections gains
 * `disabled` when mode === 'all', natively disabling every input/button
 * inside without per-element wiring. */
.filter-body {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}
.filter-body:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
.filter-body:disabled .chip { cursor: not-allowed; }

.filter-section { margin-bottom: 18px; }
.filter-section:last-child { margin-bottom: 0; }
.filter-section h3 {
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.filter-section .section-head {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 10px;
}
.filter-section .section-head h3 { margin: 0; }
/* When a scope-toggle / mode-toggle is parked inside the section
 * head (mode / date / text sections), clear the default bottom
 * margin those toggles ship with so the head row stays a single
 * horizontal band. A .row-leader between info-btn and the toggle
 * pushes the toggle to the right edge with a dashed line in
 * between. */
.filter-section .section-head .scope-toggle,
.filter-section .section-head .mode-toggle {
  margin-bottom: 0;
}

.info-btn {
  width: 16px;
  height: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--text-faint);
  background: transparent;
  border-radius: 50%;
  font-size: 11px;
  font-style: italic;
  font-weight: 700;
  color: var(--text-faint);
  cursor: pointer;
  line-height: 1;
  font-family: Georgia, 'Times New Roman', serif;
  flex: 0 0 auto;
}
.info-btn:hover {
  color: var(--text);
  border-color: var(--text);
}
/* Option-level info marks (next to a filter/section heading) are smaller
 * than the title-level mark next to a tool's H1. */
.section-head .info-btn {
  width: 13px;
  height: 13px;
  font-size: 9px;
}
.filter-section .row { display: flex; gap: 8px; align-items: center; }
/* Two date inputs side-by-side. iOS Safari ignores width/min-width on
 * <input type='date'> while keeping its picker chrome — strip the
 * native chrome with -webkit-appearance: none and pin a hard 100px
 * width so two fit on a row even on the narrowest phones. */
.filter-section .grid-2 {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.filter-section .grid-2 > * { flex: 0 0 auto; }
.filter-section .grid-3 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 8px;
}
.filter-section input[type='date'] {
  appearance: none;
  -webkit-appearance: none;
  width: 100px;
  min-width: 0;
  max-width: 100px;
  box-sizing: border-box;
  /* 값이 있으면 native 가 텍스트 (큰 폰트) 를 그려 input 이 커지고,
   * 비어있으면 placeholder 자리만큼 작아짐 — 두 input 의 높이가
   * 어긋남.  명시적 min-height 로 양쪽 높이를 동일하게 잠금. */
  min-height: 34px;
  line-height: 1.2;
}
.radio-row { display: flex; gap: 12px; font-size: 12px; color: var(--text-muted); }
.radio-row label { display: inline-flex; align-items: center; gap: 4px; }

.option-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
  user-select: none;
}
.option-row + .option-row { margin-top: 6px; }
.option-row input[type='checkbox'] { flex: 0 0 auto; cursor: pointer; }

/* Announcement option, rendered as a section-style header row. The
 * heading + info button + checkbox cluster together on the left so the
 * checkbox sits right next to the (i) button. */
.announce-section {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}
.announce-section .section-head { margin-bottom: 0; }
.announce-section .section-head h3 { margin: 0; }
.announce-checkbox { flex: 0 0 auto; cursor: pointer; }

.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  padding: 4px 10px;
  font-size: 12px;
  border-radius: 999px;
  cursor: pointer;
}
.chip:hover { border-color: var(--border-strong); }
.chip.active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-hover);
}

.result-bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
  font-size: 13px;
  color: var(--text-muted);
}
.result-bar strong { color: var(--text); }
.result-actions { display: flex; gap: 6px; flex-wrap: wrap; }

/* Indeterminate "streaming" progress bar — a thin sliding ticker since
 * we don't know the total count until the stream completes. */
.progress.indeterminate { position: relative; overflow: hidden; }
.progress.indeterminate > .bar {
  position: absolute;
  width: 30%;
  height: 100%;
  background: var(--accent);
  animation: progress-indet 1.2s linear infinite;
}
@keyframes progress-indet {
  from { left: -30%; }
  to   { left: 100%; }
}

.posts {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.posts-header {
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  padding: 8px 14px;
  font-size: 13px;
  color: var(--text-muted);
}
.posts-header label { display: inline-flex; align-items: center; gap: 8px; }
.posts-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 60vh;
  overflow-y: auto;
}
.post {
  display: flex;
  gap: 12px;
  padding: 12px 14px;
  border-top: 1px solid var(--border);
}
.post:first-child { border-top: 0; }
.post-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: var(--text-muted);
  margin-bottom: 4px;
}
.post-meta .badge {
  background: var(--surface-hover);
  padding: 1px 6px;
  border-radius: 4px;
}
.post-text {
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
}
.post-text.empty { color: var(--text-faint); font-style: italic; }
.post-stats {
  margin-top: 6px;
  display: flex;
  gap: 14px;
  font-size: 11px;
  color: var(--text-muted);
}
.post-stats span::before { content: ""; margin-right: 4px; }
.post-stats .like::before { content: "♡"; }
.post-stats .rt::before { content: "↻"; }
.post-stats .reply::before { content: "💬"; }

.empty-state {
  text-align: center;
  color: var(--text-muted);
  padding: 24px;
}

/* Modal */
dialog.modal {
  /* Layout pinned explicitly so the dialog box doesn't reshuffle
   * when [open] is dropped during the close transition.  Without
   * inset:0 + margin:auto, removing [open] (and the UA's top-layer
   * centering with it) briefly lets the dialog reflow to its raw
   * position — visible as a "modal fills the screen" flash right
   * before unmount per user report. */
  position: fixed;
  inset: 0;
  margin: auto;
  border: 0;
  border-radius: var(--radius-lg);
  padding: 0;
  box-shadow: var(--shadow-md);
  max-width: 420px;
  width: calc(100% - 32px);
  /* Auto height so a small modal doesn't stretch to viewport, while
   * still capping at the dynamic viewport so iOS keyboard opening
   * shrinks the dialog along with the visible area. */
  height: max-content;
  max-height: calc(100vh - 16px);
  max-height: calc(100dvh - 16px);
  /* 사용자 보고 2026-05-27 (12 차) : 옛 `transform: translateY(0) scale(1)`
   * (identity) 가 CSS spec 상 fixed-positioned 자손 의 containing block
   * 을 viewport 가 아 닌 dialog box 로 만 듦.  이로 인 해 dialog 안 의
   * dropdown menu (position:fixed) 가 viewport 좌 표 가 아 닌 dialog 좌
   * 표 로 해 석 되 어 위치 / hit-test / overflow clipping 모 두 어 긋 남.
   * fix — transform 제거 (open + close).  open / close animation 은 opacity
   * 만.  slight rise 효과 손 실 은 minor — fade 가 메인 효과 이 고 dialog
   * 안 의 menu 가 정 상 동작 하 는 게 더 중요. */
  opacity: 1;
  transition:
    opacity 180ms ease-out,
    overlay 180ms ease-out allow-discrete,
    display 180ms ease-out allow-discrete;
}
/* When the dialog is not [open] it's display:none, so we set the
 * "end-of-close" target state here.  allow-discrete on overlay +
 * display in the dialog rule above is what makes the close animation
 * actually run before the dialog disappears. */
dialog.modal:not([open]) {
  opacity: 0;
}
/* JS-driven close fade (reader.js patches HTMLDialogElement.proto
 * type.close to add .is-closing for 200ms before the actual close).
 * Same end-state as :not([open]) but applied while the dialog is
 * STILL [open] — so the regular opacity transition has frames to
 * animate.  Works on iOS Safari where the allow-discrete + @starting-
 * style path was unreliable. */
dialog.modal.is-closing {
  opacity: 0;
}
/* Open-time starting state — @starting-style applies only on the
 * frame the element enters [open], so the transition can run from
 * here to the [open] state.  Without this the dialog snaps in
 * because the browser sees the same opacity 1 at both ends. */
@starting-style {
  dialog.modal[open] {
    opacity: 0;
    transform: translateY(4px) scale(0.985);
  }
}
/* tabindex="-1" on these dialogs lets them absorb focus (so a child
 * button doesn't get auto-focused with a stray blue ring) — but the
 * dialog itself then receives the browser's focus outline. Suppress
 * that too; the backdrop already makes the modal-vs-rest distinction
 * obvious. */
dialog.modal:focus,
dialog.modal:focus-visible { outline: none; }
.reader-rp-modal:focus,
.reader-rp-modal:focus-visible { outline: none; }
dialog.modal::backdrop {
  /* Bumped from 0.4 → 0.65 so the page behind doesn't bleed through
   * the gap that sometimes appears when iOS animates the soft
   * keyboard open and the dialog hasn't finished re-fitting the
   * shrunken visual viewport. The deeper backdrop reads the gap as
   * "modal context" rather than "scrolled page". */
  background: rgba(15, 23, 42, 0.65);
  /* Backdrop fade — same allow-discrete pattern as the dialog itself
   * so the dim layer eases in / out instead of snapping. */
  transition:
    background 180ms ease-out,
    overlay 180ms ease-out allow-discrete,
    display 180ms ease-out allow-discrete;
}
@starting-style {
  dialog.modal[open]::backdrop {
    background: rgba(15, 23, 42, 0);
  }
}
dialog.modal:not([open])::backdrop {
  background: rgba(15, 23, 42, 0);
}
/* Companion to .is-closing on the dialog body — fades the backdrop
 * dim layer to transparent during the 200ms JS-driven close window.
 * Without this, iOS Safari's ::backdrop snaps to transparent the
 * moment [open] is dropped, which user described as the "갑자기
 * 밝아짐" flash. */
dialog.modal.is-closing::backdrop {
  background: rgba(15, 23, 42, 0);
}

/* ── New-notification toast banner ─────────────────────────────────
 * Slides down from the top of the viewport when pollUnreadCount
 * sees the unread count increase.  Pill-shape, centered, accent
 * background.  Auto-hides after ~3s; tap → notifications tab.
 * Live in document.body (NOT inside the reader-shell) so it floats
 * above any modal / dialog without z-index gymnastics.  Pre-mounted
 * once at reader.mount() and reused for every toast event. */
.reader-notif-toast {
  position: fixed;
  top: 12px;
  left: 50%;
  z-index: 10000;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 18px;
  background: var(--accent);
  color: #fff;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
  /* Hidden initial state: lifted above the viewport + transparent.
   * .is-visible removes both → slides down + fades in.  pointer-
   * events off when hidden so the invisible pill doesn't intercept
   * clicks on the feed below. */
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, calc(-100% - 24px));
  transition:
    transform 260ms cubic-bezier(0.2, 0.8, 0.2, 1),
    opacity 220ms ease-out;
}
.reader-notif-toast.is-visible {
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, 0);
}
.reader-notif-toast:hover { background: var(--accent-hover, var(--accent)); }
.reader-notif-toast-icon { font-size: 15px; line-height: 1; }
.reader-notif-toast-text { white-space: nowrap; }
.modal-body { padding: 22px; }
.modal-body h3 { margin: 0 0 8px; font-size: 17px; }
.modal-body h3.warn { color: var(--danger); }
.modal-body p { margin: 0 0 16px; color: var(--text-muted); }
/* Multi-section info modal (push-notification setup guide).
 * Each section gets a bolded heading + a small-gap body, with
 * the tip paragraph styled faint so it reads as supplementary.
 * white-space:pre-line lets the i18n body strings put one step
 * per \n so the user reads them as a vertical checklist. */
.reader-info-section { margin: 0 0 14px; }
.reader-info-section strong {
  display: block;
  margin-bottom: 4px;
  font-size: 14px;
  color: var(--text);
}
.reader-info-section p {
  margin: 0;
  line-height: 1.55;
  white-space: pre-line;
}
.reader-info-tip {
  font-size: 13px;
  color: var(--text-faint);
  font-style: italic;
  border-top: 1px solid var(--border);
  padding-top: 10px;
  margin-top: 2px !important;
}
/* Inline action — a button slotted into prose that looks like
 * a link. Used in the iOS section's step 2 to trigger
 * navigator.share so the user doesn't have to find Safari's
 * share icon themselves. */
.reader-info-inline-action {
  display: inline;
  background: transparent;
  border: 0;
  padding: 0 2px;
  margin: 0;
  font: inherit;
  color: var(--accent, #3b82f6);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.reader-info-inline-action:hover {
  text-decoration-thickness: 2px;
}
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
.modal-actions[hidden] { display: none; }

.sample-block {
  background: var(--danger-soft);
  border: 1px solid var(--danger-border);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin: 0 0 16px;
}
.sample-label {
  margin: 0 0 6px !important;
  font-size: 11px;
  font-weight: 600;
  color: var(--warning-text);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.sample-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 12px;
  color: var(--text);
}
.sample-list li {
  padding: 2px 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sample-list li.more { color: var(--text-muted); font-style: italic; }

.stat-row {
  display: flex;
  gap: 10px;
  margin: 0 0 12px;
}
.stat {
  flex: 1;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  text-align: center;
  min-width: 0;
}
.stat-label {
  display: block;
  font-size: 10px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.stat-value {
  display: block;
  font-size: 18px;
  font-weight: 600;
  color: var(--text);
  margin-top: 2px;
  font-variant-numeric: tabular-nums;
}

.live-lines { margin: 12px 0 0; }
.live-section { font-size: 12px; line-height: 1.5; }
.live-section + .live-section { margin-top: 10px; }
.live-section .live-lbl {
  font-weight: 600;
  margin-bottom: 3px;
}
.live-section.deleted .live-lbl { color: var(--danger); }
.live-section.remaining .live-lbl { color: var(--text-muted); }
.live-section ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.live-section li {
  padding: 1px 0 1px 14px;
  position: relative;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}
.live-section li::before {
  content: '·';
  position: absolute;
  left: 4px;
  color: var(--text-faint);
}

.progress {
  height: 8px;
  background: var(--surface-hover);
  border-radius: 999px;
  overflow: hidden;
}
.progress > .bar {
  height: 100%;
  background: var(--danger);
  transition: width 0.2s ease;
}

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

/* ── Debug mode (hidden — 7-tap to enable) ─────────────────────────
 * Floating bug button (bottom-right) opens the snapshot dialog.
 * Live overlay (top-right) prints checked items in real time so the
 * user can screenshot UI state mid-swipe / mid-modal without the
 * snapshot dialog itself adding to the modal stack. */
.nabilera-debug-fab {
  position: fixed;
  right: 16px;
  bottom: 16px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 22px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  /* Below the splash (z-index 9999) so the brand presentation stays
   * clean during the 2s splash window.  Modals live in the browser
   * top-layer regardless of z-index, so being "low" here doesn't
   * stop modal interactions. */
  z-index: 9000;
  box-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
/* TTS toggle fab — mirror of the Debug bug (bottom-right) at the
 * TOP-right.  Same circular chassis so the two FABs read as a pair.
 * .is-active flips colors to the accent so the user can see at a
 * glance that narration is on. */
.nabilera-tts-fab {
  position: fixed;
  /* 사용자 spec 2026-05-27 (updated) : 프로필 모달 의 세 버튼 (search /
   * list / +) 과 겹 치 지 않 도 록 viewport 우측 끝 가 까 이 (8 px) 으로
   * 이동.  이전 active 컬럼 기준 spec 무효. */
  right: 8px;
  top: 16px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border);
  /* 모바일 long-press 시 텍스트 선택 / 컨텍스트 메뉴 가 뜨 던 사용자 보고
   * (2026-05-26) 의 대응 — FAB 안 의 SVG 도 같이 정지 (children 의
   * inheritance).  user-select 없 어 도 클릭 / 키보드 동작 영향 없 음. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  background: var(--surface);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  /* 사용자 spec 2026-05-27 : sidebar (z-index 11) 보 다 아래.  fab 가
   * sidebar 의 아이콘 들 위 로 가는 시각 부조화 fix.  9000 → 10 (sidebar
   * 11 미만).  splash (z-index 9999) 시 fab 가 splash 위 로 안 나오는 점
   * 도 같이 — sidebar / splash 둘 다 fab 보 다 위. */
  z-index: 10;
  box-shadow: 0 2px 8px rgba(0,0,0,0.25);
  transition: background 160ms ease-out, color 160ms ease-out,
              border-color 160ms ease-out;
}
/* hidden 속성 존중 — 위 .nabilera-tts-fab { display: inline-flex } (author,
 * 0,1,0) 가 UA 의 [hidden]{display:none} (0,1,0) 을 동일 명시도 로 이 겨
 * hidden 을 줘도 안 사라지 던 버그 (사용자 보고 2026-05-29).  [hidden] 을
 * 클래스 와 함께 써 (0,2,0) 명시도 를 높 여 확실 히 숨 김. */
.nabilera-tts-fab[hidden] { display: none; }
.nabilera-tts-fab:hover { background: var(--surface-hover); }
.nabilera-tts-fab.is-active {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
.nabilera-tts-fab.is-active:hover {
  background: var(--accent-hover, var(--accent));
}
/* Welcome-pulse — first ~5 s after nabilera mounts the FAB pulses a
 * soft halo to draw the eye to the radio feature.  Two concentric
 * box-shadow rings ride a 1.4 s ease-out loop; the class is removed
 * after ~5 s by setTimeout so the page is calm afterwards.  Skip
 * the pulse entirely when the user has reduced-motion turned on. */
@keyframes nabilera-tts-pulse {
  0%   { box-shadow: 0 2px 8px rgba(0,0,0,0.25),
                     0 0 0 0    color-mix(in srgb, var(--accent) 65%, transparent); }
  70%  { box-shadow: 0 2px 8px rgba(0,0,0,0.25),
                     0 0 0 18px color-mix(in srgb, var(--accent) 0%,  transparent); }
  100% { box-shadow: 0 2px 8px rgba(0,0,0,0.25),
                     0 0 0 0    color-mix(in srgb, var(--accent) 0%,  transparent); }
}
.nabilera-tts-fab.is-welcome-pulse {
  animation: nabilera-tts-pulse 1.4s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .nabilera-tts-fab.is-welcome-pulse { animation: none; }
}

/* ── 다마고치 펫 (1.5 실험 모드) — 임시 위젯 ──────────────────────
 * 병아리 FAB : 라디오 FAB(top:16, h:44) 바로 아래.  라디오 와 동일
 * chassis, z-index 도 sidebar 미만(10). */
.nabilera-pet-fab {
  position: fixed;
  right: 8px;
  top: 68px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  z-index: 10;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
.nabilera-pet-fab[hidden] { display: none; }
.nabilera-pet-fab:hover { background: var(--surface-hover); }
.reader-pet-modal .modal-body { min-width: 240px; }
.reader-pet-stage {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 10px 0 14px;
}
.reader-pet-sprite { width: 76px; height: 76px; flex: 0 0 76px; }
.reader-pet-sprite svg { display: block; width: 100%; height: 100%; }
.reader-pet-stage-name { font-weight: 600; }
.reader-pet-stat {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 6px 0;
}
.reader-pet-stat-icon { font-size: 18px; }
.reader-pet-stat-label { min-width: 3.5em; color: var(--text-faint); }
.reader-pet-actions {
  display: flex;
  gap: 8px;
  margin-top: 14px;
}
/* 먹이/놀이/상점 한 줄 — 먹이·놀이는 인벤(n/3)을 아이콘 3개로(왼쪽 보유분
 * 정상, 빈 슬롯 옅게).  정상 아이콘 클릭 = 1개 소비. */
.reader-pet-care-row { align-items: center; flex-wrap: wrap; justify-content: center; }
.reader-pet-care-group { display: inline-flex; gap: 3px; align-items: center; }
.reader-pet-care-icon {
  border: 0; background: transparent; padding: 2px; cursor: pointer;
  font-size: 22px; line-height: 1;
  transition: transform .12s ease, opacity .12s ease;
}
.reader-pet-care-icon:hover:not(.is-empty) { transform: scale(1.18); }
.reader-pet-care-icon:active:not(.is-empty) { transform: scale(0.95); }
.reader-pet-care-icon.is-empty { opacity: 0.28; cursor: default; filter: grayscale(0.7); }
/* 디버그(super did 전용) — 토글 + 수치/시각 조작 버튼들. */
.reader-pet-debug-toggle {
  margin-top: 16px;
  font-size: 12px;
  opacity: 0.75;
}
.reader-pet-debug {
  margin-top: 8px;
  padding: 8px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}
.reader-pet-debug-stats {
  font-variant-numeric: tabular-nums;
  margin-bottom: 8px;
  word-break: keep-all;
}
.reader-pet-debug-btns {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.reader-pet-debug-btns .btn { font-size: 12px; padding: 4px 8px; }
/* 형질 편집기(디버그) — 이진 토글 + RGB 슬라이더. */
.reader-pet-trait-title { margin: 10px 0 4px; font-weight: 600; }
.reader-pet-trait-rgb { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
.reader-pet-trait-rgb > span { width: 1.4em; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.reader-pet-trait-slider { flex: 1 1 auto; min-width: 0; }
/* 상점 — 악세서리 구매/장착 목록. */
.reader-pet-shop {
  margin-top: 14px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}
.reader-pet-shop-title { margin-bottom: 8px; }
.reader-pet-shop-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 4px 0;
}
.reader-pet-shop-name { font-size: 14px; }
.reader-pet-shop-row .btn { font-size: 12px; padding: 4px 10px; }
/* 교배 요청 특수 카드 (피드 내 링크카드 스왑). */
.reader-pet-breed-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  margin-top: 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2, var(--surface));
}
.reader-pet-breed-sprite { width: 56px; height: 56px; flex: 0 0 56px; }
.reader-pet-breed-sprite svg { width: 100%; height: 100%; display: block; }
.reader-pet-breed-info { flex: 1 1 auto; min-width: 0; }
.reader-pet-breed-title { font-weight: 600; }
.reader-pet-breed-btn { flex: 0 0 auto; }
/* 교배 요청 버튼 — 먹이/놀이/상점 줄 아래, 너비 꽉 채움(2026-06-02). */
.reader-pet-breed-request { margin-top: 8px; width: 100%; }
/* 교배 불가 안내("불량 펫은…"/"보관 중인 알이…") — 중앙 정렬(2026-06-02). */
.reader-pet-breed-note { text-align: center; }
/* 디버그 펫 기록(앨범) 목록. */
.reader-pet-album-list { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; max-height: 60vh; overflow-y: auto; }
.reader-pet-album-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.reader-pet-album-swatch { width: 16px; height: 16px; border-radius: 50%; border: 1px solid var(--border); flex: 0 0 auto; }
/* 놀아주기 → 블스369 게임 모달(2026-06-02).  top-layer dialog 대신 일반
 * fixed 오버레이 — 게임 웨이브 splash(z 100001, body 부착)가 위에 뜨도록
 * 오버레이 z 를 그보다 낮게.  게임 root 가 height:100% 라 패널에 고정 높이. */
/* 놀아주기 → 게임 선택 모달 (사용자 spec 2026-06-03).
 * ※ 폭은 .modal-body 가 아니라 dialog 에 건다 — body 만 좁히면 dialog
 * (base calc(100%-32px))가 그대로 넓어 body 오른쪽에 빈 공간이 남는다
 * (트렌드 모달과 같은 부류의 실수, 2026-06-03 사용자 지적).  .reader-pet-modal
 * 패턴대로 dialog 를 cap + body 는 채우게. */
dialog.modal.reader-pet-gamepick-modal { max-width: 340px; }
.reader-pet-gamepick-modal .modal-body { min-width: 0; max-width: none; }
.reader-pet-gamepick-hint { text-align: center; margin: 6px 2px 14px; }
.reader-pet-gamepick-list { display: flex; flex-direction: column; gap: 8px; }
.reader-pet-gamepick-btn { width: 100%; }
.reader-pet-game-overlay {
  position: fixed; inset: 0; z-index: 9000;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.5);
}
.reader-pet-game-panel {
  position: relative;
  width: min(560px, 95vw); height: min(82vh, 780px);
  max-width: 95vw; max-height: 92vh;
  background: var(--surface); border-radius: var(--radius);
  overflow: hidden; display: flex; flex-direction: column;
}
.reader-pet-game-close {
  position: absolute; top: 8px; right: 10px; z-index: 50;
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  border-radius: 50%;
}
.reader-pet-game-root { flex: 1 1 auto; min-height: 0; display: flex; }
.reader-pet-game-root .bsky369-root { width: 100%; height: 100%; }

/* ── 펫 위젯 재설계 (2026-06-02, 사용자 spec) ───────────────────────────
 * 상단 상태줄(상태/세대/코인 칩) + 로밍 놀이판 + 상점 버튼.  포만/흥미
 * 수치 비노출 — 흥미=표정(눈), 포만=행동(점프/공복 말풍선). */
/* 다이얼로그 폭 도 같이 좁힘 — dialog.modal 기본 max-width 420 인데 modal-body
 * 만 360 으로 두면 좌측 정렬 되 어 X 버튼 우측 에 빈 공간(사용자 보고).
 * 다이얼로그 = modal-body 폭 으로 맞춰 좌우 패딩 대칭. */
dialog.modal.reader-pet-modal { max-width: 360px; }
dialog.modal.reader-pet-shop-modal { max-width: 340px; }
.reader-pet-modal .modal-body { min-width: 0; max-width: none; }
.reader-pet-status {
  display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 10px;
}
.reader-pet-status-chip {
  font-size: 12px; padding: 3px 10px; border-radius: 999px;
  background: var(--surface-2, var(--surface)); border: 1px solid var(--border);
  color: var(--text-muted); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* 놀이판 — 펫이 돌아다니는 바운디드 박스.  하단 22% 가 바닥.
 * 색 은 테마 변수 가 아니라 HARDCODED 중간 밝기 파스텔(하늘→잔디) — 다크/
 * 라이트 어느 테마 에서도 동일·적절 한 밝기(사용자 spec : 다크 너무 어둡고
 * 라이트 너무 밝던 것 통일).  splash 처럼 var(--*) 안 씀. */
.reader-pet-arena {
  position: relative; width: 100%; height: 170px; margin: 4px 0 12px;
  border: 1px solid rgba(120, 140, 155, 0.45);
  border-radius: var(--radius); overflow: hidden;
  background:
    linear-gradient(to bottom,
      #bcd8ee 0%, #cfe2ee 54%, #bdd6c0 76%, #b3cfb6 100%);
}
.reader-pet-arena.is-static { display: flex; align-items: center; justify-content: center; }
/* 세대별 배경 씬(방/숲) — 스프라이트/말풍선 뒤(2026-06-02). */
.reader-pet-scene { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
/* 디버그 '펫 가림' — 배경만 보이게 스프라이트/말풍선/마크 숨김. */
.reader-pet-arena.is-pet-hidden .reader-pet-roam-sprite,
.reader-pet-arena.is-pet-hidden .reader-pet-thought,
.reader-pet-arena.is-pet-hidden .reader-pet-angry { display: none !important; }
.reader-pet-scene svg { display: block; width: 100%; height: 100%; }
.reader-pet-roam-sprite, .reader-pet-thought, .reader-pet-angry { z-index: 1; }
.reader-pet-arena.is-static .reader-pet-arena-sprite { position: relative; z-index: 1; }
.reader-pet-arena-sprite { width: 84px; height: 84px; }
.reader-pet-arena-sprite svg { display: block; width: 100%; height: 100%; }
/* 로밍 스프라이트 — JS 가 매 프레임 transform(이동/눌림/회전/점프) 갱신.
 * 발이 바닥에 닿도록 bottom 기준 + transform-origin 중앙하단. */
.reader-pet-roam-sprite {
  position: absolute; left: 0; bottom: 8px; width: 76px; height: 76px;
  transform-origin: center bottom; will-change: transform;
  /* 클릭 가능 — 탭 시 검은 사각형(웹킷 tap-highlight)/포커스 아웃라인 제거. */
  -webkit-tap-highlight-color: transparent;
  outline: none; -webkit-user-select: none; user-select: none;
}
.reader-pet-roam-sprite svg { display: block; width: 100%; height: 100%; }
/* 공복 말풍선 — 머리 위, 먹이 심볼.  JS 가 x 를 따라 translateX. */
.reader-pet-thought[hidden] { display: none !important; }
.reader-pet-thought {
  position: absolute; left: 0; top: 0; width: 32px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  background: #fff; border: 1.5px solid #cfd6df; border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0,0,0,.15);
  will-change: transform;
}
.reader-pet-thought::before,
.reader-pet-thought::after {
  content: ""; position: absolute; left: 4px; background: #fff;
  border: 1.5px solid #cfd6df; border-radius: 50%;
}
.reader-pet-thought::before { width: 8px; height: 8px; bottom: -7px; }
.reader-pet-thought::after { width: 4px; height: 4px; bottom: -13px; left: 2px; }
.reader-pet-thought-food { font-size: 16px; line-height: 1; }
/* 지루 클릭 반응 — 화남 마크(💢) 얼굴 우상단. */
.reader-pet-angry[hidden] { display: none !important; }
.reader-pet-angry {
  position: absolute; left: 0; top: 0; font-size: 20px; line-height: 1;
  will-change: transform; pointer-events: none;
}
/* 알 — 부화 전 살짝 흔들림(꿈틀). */
.reader-pet-egg-bob { animation: reader-pet-egg-bob 2.4s ease-in-out infinite; transform-origin: center bottom; }
@keyframes reader-pet-egg-bob {
  0%, 60%, 100% { transform: rotate(0deg); }
  68% { transform: rotate(-5deg); }
  76% { transform: rotate(5deg); }
  84% { transform: rotate(-3deg); }
  92% { transform: rotate(2deg); }
}
/* 힌트창 — 알/공복/지루/비뚤어짐/교배안내 등 상태별 안내 한 줄. */
.reader-pet-hint { text-align: center; margin: 8px 0 4px; font-weight: 600; }
/* 교배 요청 모달 : 유저 검색(typeahead) + 오른쪽 정렬 작성 버튼. */
.reader-pet-breed-search { width: 100%; }
.reader-pet-breed-results { display: flex; flex-direction: column; margin-top: 6px; max-height: 240px; overflow-y: auto; }
.reader-pet-breed-result-text { display: flex; flex-direction: column; min-width: 0; }
.reader-pet-breed-result-text strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reader-pet-breed-actions { display: flex; justify-content: flex-end; margin-top: 16px; }
.reader-pet-breed-send { width: auto; }
/* 상점 버튼은 먹이/놀이와 한 줄(care row) — 전체 폭 금지(2026-06-02). */
.reader-pet-shop-btn { flex: 0 0 auto; white-space: nowrap; }
.reader-pet-shop-coins { margin: 6px 0 10px; font-variant-numeric: tabular-nums; }
.reader-pet-shop-modal .modal-body { min-width: 260px; }
@media (prefers-reduced-motion: reduce) {
  .reader-pet-egg-bob { animation: none; }
}

/* 자동 대체 텍스트(BYOK) 설정 모달 + 컴포저 생성 버튼. */
.reader-alttext-modal .modal-body { min-width: 280px; max-width: 460px; }
/* LLM 설정 토글 행(자동 대체텍스트 / AI 번역 / 자동 번역) 사이 세로 간격 —
 * .reader-settings-row 는 gap(가로)만 있고 행간 margin 이 없어 체크박스들이
 * 붙어 보이던 문제.  이 모달에만 스코프. */
.reader-alttext-modal .reader-settings-row { margin-top: 12px; }
.reader-alttext-providers { margin-top: 10px; }
.reader-alttext-providers.is-disabled { opacity: 0.45; pointer-events: none; }
/* AI 번역(실험) — 자동 번역 행: AI 번역 OFF 면 옅게/비활성. */
.reader-aitrans-auto-row.is-disabled { opacity: 0.45; pointer-events: none; }
.reader-aitrans-auto-hint { margin-top: 2px; font-size: 12px; }
.reader-alttext-provider-row {
  padding: 8px 0;
  border-top: 1px solid var(--border);
}
.reader-alttext-provider-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.reader-alttext-key { width: 100%; margin-top: 8px; }
.reader-alttext-model-label {
  display: block;
  margin-top: 8px;
  font-size: 13px;
  color: var(--text-faint);
}
.reader-alttext-model { width: 100%; margin-top: 4px; }
.reader-alttext-custom-model { width: 100%; margin-top: 6px; }
/* 무료 모델 선택 시 모델 드롭다운 아래에 뜨는 요청 제한 안내 —
   배경 박스 없이 일반 안내 텍스트(hint) 스타일.
   안내문은 .reader-alttext-provider-row(padding: 8px 0)의 마지막
   자식이라 아래쪽에 row 의 padding-bottom 8px 가 항상 더 붙는다.
   드롭다운 박스 하단↔안내 첫 줄 = 위 간격, 안내 끝 줄↔구분선 = 아래
   간격.  chromium + 실제 Noto Serif KR 실측(fresh reload): margin 0 이면
   위 2px / 아래 10px(아래에 row padding-bottom 8px 포함)로 아래가 더
   큼.  위 = 2 + margin-top, 아래 = 10 + margin-bottom, margin-top 은
   아래 간격에 영향 없음 → 균등 조건 margin-top − margin-bottom = 8.
   ⚠ 반드시 .reader-alttext-modal 로 스코프(specificity 0,2,1)해야
   .modal-body p { margin: 0 0 16px } (0,1,1) 를 이긴다.  안 그러면
   .modal-body p 가 이겨서 margin 0 0 16px 가 적용 → 아래만 벌어짐
   (이번 세션에서 단순 .reader-alttext-free-note 로 줬다가 계속 안
   먹었던 원인, 실제 앱 boot 실측으로 확인 2026-05-31).
   내 규칙이 이기면 margin 단축형이 4변 모두 set → bottom=0.  박스모델:
   위 간격 = margin-top + leading, 아래 간격 = margin-bottom + row
   padding-bottom 8 + leading.  균등 조건 margin-top = margin-bottom + 8.
   margin-top 8 / margin-bottom 0 → 위 10 / 아래 10 균등(실제 앱 실측). */
.reader-alttext-modal .reader-alttext-free-note {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-faint);
  margin: 8px 0 0;
}
.composer-alt-generate {
  display: block;
  margin: 10px auto 0;  /* 중앙 정렬 */
  font-size: 12px;
}
/* 대체 텍스트 — 썸네일 아래 칩(워터마크 chip 스타일 공유 = composer-alt-chip)
 * + 입력 모달. */
.composer-alt-modal .modal-body { min-width: 280px; max-width: 480px; }
.composer-alt-preview {
  text-align: center;
  margin: 8px 0;
}
.composer-alt-preview-img {
  max-width: 100%;
  max-height: 240px;
  border-radius: var(--radius);
  object-fit: contain;
}
.composer-alt-textarea {
  width: 100%;
  min-height: 96px;
  resize: vertical;
  font-family: inherit;
  line-height: 1.4;
  box-sizing: border-box;
}
.composer-alt-counter-row {
  text-align: right;
  margin: 8px 0 2px;  /* textarea 위쪽 — 미리보기와 입력창 사이 */
}
.composer-alt-counter {
  font-size: 12px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.composer-alt-counter.is-over {
  color: #e0245e;
  font-weight: 600;
}
.composer-alt-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 10px;
}

/* ── 라디오 채널 드롭다운 ─────────────────────────────────────────
 * TTS FAB 1 초 long-press 시 위쪽 에 뜨 는 menu.  position: fixed —
 * radioOpenDropdown 가 ttsFab rect 으 로 top/right 계산 후 inline
 * style 로 위치 정.  width 240 px 고정 (긴 displayName 도 ellipsis). */
.nabilera-radio-dropdown {
  width: 260px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.18);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.nabilera-radio-dropdown-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  background: transparent;
  border: 0;
  border-radius: 8px;
  text-align: left;
  font: inherit;
  color: var(--text);
  cursor: pointer;
}
.nabilera-radio-dropdown-row:hover { background: var(--surface-hover); }
.nabilera-radio-dropdown-row.is-active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}
.nabilera-radio-dropdown-avatar {
  width: 24px; height: 24px; border-radius: 6px; object-fit: cover; flex: 0 0 auto;
  background: var(--surface-hover);
}
.nabilera-radio-dropdown-icon {
  width: 24px; height: 24px; border-radius: 6px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-hover); color: var(--text-muted); font-weight: 700;
}
.nabilera-radio-dropdown-label {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.nabilera-radio-dropdown-remove {
  flex: 0 0 auto;
  width: 20px; height: 20px;
  border: 0; background: transparent; color: var(--text-muted);
  border-radius: 50%; cursor: pointer; font-size: 14px; line-height: 1;
}
.nabilera-radio-dropdown-remove:hover { background: var(--surface-hover); color: var(--text); }
.nabilera-radio-dropdown-add { color: var(--accent); font-weight: 600; }
.nabilera-radio-dropdown-sep {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 4px 0;
}
.nabilera-radio-dropdown-off { color: var(--text-muted); }
.nabilera-radio-dropdown-off:hover { color: #b91c1c; }

/* ── 기념일 효과 ──────────────────────────────────────────────────
 * 종이 폭죽 + 토스트.  full-viewport overlay (pointer-events: none 로
 * 클릭 차단 안 함), confetti 가 위 에서 떨어 지 면서 회전 + 좌우 sway,
 * toast 는 가운데 위쪽 fade-in/up → 자동 fade-out. */
.nabilera-anniversary {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 10000;
  overflow: hidden;
  transition: opacity 0.6s ease;
}
.nabilera-anniversary.is-leaving { opacity: 0; }
.nabilera-anniversary-confetti {
  position: absolute;
  inset: 0;
}
.nabilera-anniversary-piece {
  position: absolute;
  top: -20px;
  width: 8px;
  height: 14px;
  border-radius: 2px;
  transform-origin: center;
  animation-name: nabilera-anniversary-fall;
  animation-timing-function: cubic-bezier(0.32, 0.58, 0.65, 1);
  animation-fill-mode: forwards;
  will-change: transform, opacity;
}
@keyframes nabilera-anniversary-fall {
  0%   {
    transform: translate3d(0, 0, 0) rotate(var(--rot-start, 0deg));
    opacity: 0;
  }
  10%  { opacity: 1; }
  100% {
    transform: translate3d(var(--sway, 0px), 110vh, 0) rotate(var(--rot-end, 720deg));
    opacity: 0.85;
  }
}
.nabilera-anniversary-toast {
  position: absolute;
  top: 22%;
  left: 50%;
  transform: translateX(-50%) translateY(-10px);
  padding: 14px 22px;
  background: rgba(15, 23, 42, 0.92);
  color: #fff;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 600;
  box-shadow: 0 8px 24px rgba(0,0,0,0.25);
  opacity: 0;
  animation: nabilera-anniversary-toast-in 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) 0.15s forwards,
             nabilera-anniversary-toast-out 0.6s ease 4s forwards;
  white-space: nowrap;
  max-width: 90vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
@keyframes nabilera-anniversary-toast-in {
  0%   { opacity: 0; transform: translateX(-50%) translateY(-10px); }
  100% { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes nabilera-anniversary-toast-out {
  0%   { opacity: 1; transform: translateX(-50%) translateY(0); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-6px); }
}
@media (prefers-reduced-motion: reduce) {
  .nabilera-anniversary-piece { animation: none; opacity: 0; }
  .nabilera-anniversary-toast { animation: none; opacity: 1; }
}

/* ── + 채널 추가 모달 ──────────────────────────────────────────── */
.reader-add-channel-modal .modal-body {
  min-width: min(440px, 92vw);
  max-width: 560px;
  display: flex; flex-direction: column; gap: 12px;
}
.reader-add-channel-title { margin: 0; font-size: 18px; }
.reader-add-channel-tabs {
  display: flex; gap: 4px;
  border-bottom: 1px solid var(--border);
}
.reader-add-channel-tab {
  background: transparent; border: 0; padding: 8px 14px;
  font: inherit; color: var(--text-muted); cursor: pointer;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.reader-add-channel-tab.is-active {
  color: var(--text); border-bottom-color: var(--accent); font-weight: 600;
}
.reader-add-channel-pane {
  display: flex; flex-direction: column; gap: 8px;
  min-height: 280px; max-height: 50vh;
}
/* `display: flex` 가 [hidden] 의 default user-agent display: none 을 override
 * 하 던 버그 fix (사용자 보고 2026-05-26 : 모달 에 3 탭 의 검색 / 결과 가
 * 다 보 임).  [hidden] 명시 적 으 로 display: none 으 로. */
.reader-add-channel-pane[hidden] { display: none; }
.reader-add-channel-search {
  width: 100%; box-sizing: border-box;
  padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px;
  font: inherit; background: var(--bg); color: var(--text);
}
.reader-add-channel-results {
  flex: 1; overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
}
.reader-add-channel-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px; border-radius: 8px;
}
.reader-add-channel-row:hover { background: var(--surface-hover); }
.reader-add-channel-row-avatar {
  width: 36px; height: 36px; border-radius: 8px; object-fit: cover;
  background: var(--surface-hover); flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
}
.reader-add-channel-row-avatar-fallback {
  color: var(--text-muted); font-weight: 600;
}
.reader-add-channel-row-text {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column;
}
.reader-add-channel-row-name {
  font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.reader-add-channel-row-handle {
  font-size: 13px; color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.reader-add-channel-add-btn {
  flex: 0 0 auto;
  width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--bg);
  color: var(--accent); font-size: 18px; font-weight: 700; line-height: 1;
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.reader-add-channel-add-btn:hover {
  background: var(--accent); color: #fff; border-color: var(--accent);
}

/* ── 라디오 설정 sub-modal ──────────────────────────────────────────
 * Compact list of toggles + a speed select + a "try sample" button.
 * Layout mirrors the interaction-editor modal so the user has one
 * mental model across all sub-settings.  */
.reader-radio-settings-modal { max-width: 420px; }
/* Master "라디오 사용" row — slightly emphasised so it reads as the
 * top-level switch above the rest of the settings.  Tiny bottom
 * divider so the disabled-stack visually separates from it. */
.reader-radio-settings-master {
  font-weight: 600;
  padding-bottom: 8px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
/* Disabled stack — when the master switch is OFF, fade + de-
 * interact every control below.  pointer-events:none also blocks
 * the custom .reader-radio-picker triggers from popping their
 * menus, and disables checkbox / range / button focus. */
.reader-radio-settings-rest.is-disabled {
  opacity: 0.45;
  pointer-events: none;
  /* Subtle inset so the user can tell the section is "off" even
   * before they try to interact. */
  filter: saturate(0.6);
}
.reader-radio-settings-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 6px;
  border-radius: 6px;
}
/* cursor: pointer + hover-tint only on <label> rows (checkboxes) —
 * the whole row IS the click target there.  Slider / dropdown rows
 * are <div> and let their inner control own its own cursor /
 * focus ring; tinting the row on hover would imply clickability
 * that isn't there, and on mobile Safari the label semantics
 * would also swallow the dropdown's own tap. */
label.reader-radio-settings-row { cursor: pointer; }
label.reader-radio-settings-row:hover { background: var(--surface-hover); }
.reader-radio-settings-row input[type="checkbox"] { flex: 0 0 auto; }
.reader-radio-rate-select {
  margin-left: auto;
  padding: 4px 8px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
}
/* Volume slider — range input + a small percent readout to its
 * right.  Wrap pushes both to the right edge to mirror the speed
 * select's layout. */
.reader-radio-volume-wrap {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Track width — bumped from 150px to 225px (1.5x) so the slider
   * has more room to drag finely.  The wrap is the slider's track-
   * box; .reader-radio-volume-slider inside uses flex:1 to fill
   * whatever width the wrap gets. */
  min-width: 225px;
}
.reader-radio-volume-slider {
  flex: 1;
  accent-color: var(--accent);
  cursor: pointer;
}
.reader-radio-volume-readout {
  font-size: 12px;
  color: var(--text-faint);
  min-width: 36px;
  text-align: right;
}
.reader-radio-settings-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 12px;
}
.nabilera-debug-overlay {
  position: fixed;
  top: 80px;
  right: 8px;
  max-width: 320px;
  z-index: 999998;
  padding: 8px 10px;
  border-radius: 6px;
  background: rgba(13, 17, 23, 0.85);
  color: #c9d1d9;
  font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre-wrap;
  word-break: break-all;
  pointer-events: none;
  box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
dialog.modal.nabilera-debug-dialog { max-width: 720px; }
.nabilera-debug-dialog .nabilera-debug-section {
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.nabilera-debug-dialog .nabilera-debug-section h4 {
  margin: 0 0 6px;
  font-size: 13px;
}
.nabilera-debug-dialog textarea {
  width: 100%;
  min-height: 140px;
  font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  padding: 6px 8px;
  box-sizing: border-box;
  resize: vertical;
}
.nabilera-debug-section-actions {
  margin-top: 6px;
  display: flex;
  gap: 8px;
}
.nabilera-debug-overlay-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 8px;
  padding-left: 4px;
}
.nabilera-debug-overlay-row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
}
.nabilera-debug-foot {
  margin-top: 18px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: flex-end;
}

/* ── Composer self-label trigger + dropdown ─────────────────────────
 * ⚠ SVG icon button next to the @ mention trigger.  Sizing /
 * spacing mirror .composer-mention-trigger so the row of buttons
 * (media-add → @ → ⚠) lines up at the same height with the same
 * 6px gap between them.  Clicking opens a dropdown above the
 * button with 4 checkboxes for the adult-category labels. */
.composer-selflabel-wrap {
  /* position:relative anchors the dropdown menu; layout (height/gap)
   * is owned by .composer-tools-row + .composer-tool-btn now. */
  position: relative;
  display: inline-flex;
  align-items: center;
}
.composer-selflabel-trigger {
  /* gap separates the ⚠ icon from the count badge; size comes from
   * .composer-tool-btn. */
  gap: 4px;
}
/* 셀프 라벨 선택 시 — 숫자 배지 대신 버튼을 빨갛게(폭은 그대로 유지). */
.composer-selflabel-trigger.is-active {
  background: color-mix(in srgb, #ef4444 18%, var(--surface));
  color: #ef4444;
  border-color: #ef4444;
}
.composer-selflabel-icon {
  display: block;
  width: 16px;
  height: 16px;
}
.composer-selflabel-count {
  font-size: 11px;
  font-weight: 600;
  min-width: 16px;
  height: 16px;
  border-radius: 8px;
  background: var(--accent, #58a6ff);
  color: #fff;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Class selectors and [hidden] have identical specificity, so the
 * default `[hidden] { display: none }` is overridden in source
 * order by our display rules above.  Explicit re-declaration
 * keeps the dropdown closed and the count badge invisible until
 * we want them. */
.composer-selflabel-count[hidden] { display: none !important; }
.composer-selflabel-dropdown {
  position: absolute;
  z-index: 50;
  bottom: calc(100% + 6px);
  /* Right-anchor so the dropdown extends LEFTWARD from the trigger.
   * The trigger (⚠ icon) sits at the right end of the composer's
   * media-buttons row, so opening rightward (left:0 in the previous
   * iteration) pushed the menu past the composer's right edge on
   * narrow viewports.  Anchoring right keeps the menu inside the
   * card on every layout, since the trigger is always far enough
   * from the LEFT edge that 200px of menu fits without clipping. */
  right: 0;
  min-width: 200px;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Slide-up animation — drops UP from the trigger (since the
   * dropdown opens above the button, anchored to bottom).  JS adds
   * .is-visible on requestAnimationFrame after un-hiding so the
   * browser commits the hidden state first and the transition
   * actually interpolates. */
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 140ms ease-out, transform 140ms ease-out;
}
.composer-selflabel-dropdown.is-visible {
  opacity: 1;
  transform: translateY(0);
}
.composer-selflabel-dropdown[hidden] { display: none !important; }
.composer-selflabel-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  font-size: 13px;
  cursor: pointer;
  border-radius: 4px;
  user-select: none;
}
.composer-selflabel-row:hover { background: var(--surface-hover); }

/* ── Composer interaction-settings trigger ──────────────────────────
 * ⚙ gear-icon button between @ and ⚠.  Per-post threadgate / postgate
 * editor.  Same sizing chassis as .composer-selflabel-trigger so the
 * row of icon buttons (media-add → @ → ⚙ → ⚠) shares one height +
 * margin grid.  is-active = user picked a non-default override.
 *
 * The wrap mirrors .composer-selflabel-wrap (inline-flex, vertical-
 * align middle) so the icon-button is in the same layout context as
 * the ⚠ next door — without it, the trigger gets baseline-aligned
 * directly and ends up vertically offset / visually taller. */
.composer-interaction-wrap {
  display: inline-flex;
  align-items: center;
}
.composer-interaction-trigger {
  gap: 4px;
}
.composer-interaction-trigger[hidden] { display: none; }
.composer-interaction-trigger.is-active {
  background: color-mix(in srgb, var(--accent) 18%, var(--surface));
  color: var(--accent);
  border-color: var(--accent);
}
.composer-interaction-icon {
  display: block;
  width: 16px;
  height: 16px;
}

/* ── Composer favorite-emoji trigger ────────────────────────────────
 * 텍스트(이모지) 한 글자가 곧 버튼 라벨.  @ / ⚙ / ⚠ 와 같은 grid
 * 에 정렬되도록 wrap + line-height / min-width 모두 동일. */
.composer-fav-emoji-wrap {
  position: relative; /* anchors the emoji dropdown */
  display: inline-flex;
  align-items: center;
}
/* size/padding/font come from .composer-tool-btn */
.composer-fav-emoji-trigger[hidden] { display: none; }
/* Dropdown of registered emoji (composertools experiment) — mirrors the
 * self-label dropdown: opens UP from the button, slide-in, right-anchored
 * so it stays inside the card on narrow viewports. */
.composer-fav-emoji-dropdown {
  position: absolute;
  z-index: 50;
  bottom: calc(100% + 6px);
  right: 0;
  min-width: 0;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
  display: flex;
  gap: 4px;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 140ms ease-out, transform 140ms ease-out;
}
.composer-fav-emoji-dropdown.is-visible { opacity: 1; transform: translateY(0); }
.composer-fav-emoji-dropdown[hidden] { display: none !important; }
.composer-fav-emoji-option {
  font-size: 22px;
  line-height: 1;
  min-width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  cursor: pointer;
}
.composer-fav-emoji-option:hover { background: var(--surface-hover); border-color: var(--border); }

/* ── Favorite-emoji settings modal ──────────────────────────────────
 * 큰 preview + text input + 8 열 grid. */
.reader-fav-emoji-modal { max-width: 420px; }
.reader-fav-emoji-preview {
  font-size: 56px;
  text-align: center;
  line-height: 1.1;
  padding: 8px 0 12px;
  color: var(--text);
}
.reader-fav-emoji-input {
  width: 100%;
  padding: 10px 14px;
  font-size: 18px;
  text-align: center;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-sizing: border-box;
}
.reader-fav-emoji-input:focus {
  outline: none;
  border-color: var(--accent);
}
.reader-fav-emoji-divider {
  text-align: center;
  margin: 16px 0 8px;
  color: var(--text-faint);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.reader-fav-emoji-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 4px;
}
.reader-fav-emoji-cell {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 6px 0;
  font-size: 22px;
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.reader-fav-emoji-cell:hover {
  background: var(--surface-hover);
  border-color: var(--border);
}
/* Up-to-3 selection chips (composertools experiment). */
.reader-fav-emoji-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  min-height: 48px;
  padding: 8px 0 4px;
}
.reader-fav-emoji-empty { color: var(--text-faint); font-size: 13px; }
.reader-fav-emoji-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
}
.reader-fav-emoji-chip-glyph { font-size: 24px; line-height: 1; }
.reader-fav-emoji-chip-x {
  border: none;
  background: transparent;
  color: var(--text-faint);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}
.reader-fav-emoji-chip-x:hover { color: var(--text); }
.reader-fav-emoji-state { margin: 0 0 8px; }
.reader-fav-emoji-addrow { display: flex; gap: 8px; align-items: stretch; }
.reader-fav-emoji-addrow .reader-fav-emoji-input { flex: 1 1 auto; }
.reader-fav-emoji-add { flex: 0 0 auto; }

/* ── 블스369 — 1인용 369 게임 ─────────────────────────────────────── */
.bsky369-root {
  display: flex;
  flex-direction: column;
  /* main / container 가 bsky369 active 시 padding 제거 + overflow
   * hidden (아래 :has() selector) — 그러면 root 가 main 의 100% fit.
   * 자체 height 100% 로 main 의 가용 영역 정확 차지 → scroll 안
   * 생김. */
  height: 100%;
  max-height: 100%;
  padding: 12px;
  gap: 8px;
  align-items: stretch;
  overflow: hidden;
  /* game over overlay (.bsky369-gameover) 의 absolute positioning anchor. */
  position: relative;
}
/* bsky369 가 mount 됐을 때 만 main / container 의 chrome 제거 —
 * :has() 가 자동 으로 active state 따라가서 unmount 후 정상 복귀.
 * 사용자 보고 "페이지 에 자꾸 스크롤 이 작게 생기는" 해결 — 기존
 * container padding 64px 가 root 의 height calculation 보다 약간
 * 더 작게 만들어 약간 의 overflow 발생.  bsky369 의 root 가 main 의
 * 100% 차지 + main overflow:hidden 으로 차단. */
main:has(.bsky369-root) {
  overflow: hidden;
}
main:has(.bsky369-root) > .container {
  padding: 0;
  max-width: none;
  height: 100%;
}
.bsky369-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
  margin: 60px auto;
  max-width: 480px;
}
.bsky369-title { font-size: 24px; font-weight: 700; margin: 0; }
.bsky369-rules { color: var(--text-muted); line-height: 1.5; margin: 0; }
.bsky369-start { padding: 10px 24px; font-size: 16px; }
.bsky369-topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 4px 8px;
}
.bsky369-stats {
  display: flex;
  gap: 12px;
  align-items: baseline;
  font-size: 13px;
  flex-wrap: wrap;
}
.bsky369-stats-row {
  display: contents; /* 더 이상 separate row 가 아님 — 직접 stats 의 child */
}
.bsky369-stat { color: var(--text-muted); }
/* 신기록 badge 의 popover 앵커 — score-num-wrap 의 50% center.  scoreSpan
 * 자체 는 updateScoreDisplay 가 textContent 으로 child 를 wipe 하므로
 * 별도 wrap 으로 묶 음.  badge 는 absolute 이라 wrap 의 inline width 에
 * 기여 X → wrap 의 center = 점수 숫자 center. */
.bsky369-score-num-wrap { position: relative; display: inline-block; }
.bsky369-stat-wave .bsky369-wave-num {
  color: var(--accent, #0085ff); font-weight: 700; margin-left: 2px;
}
.bsky369-stat-score .bsky369-score-num {
  color: var(--text); font-weight: 700; margin-left: 2px;
}
.bsky369-stat-high .bsky369-high-num {
  color: var(--text); font-weight: 700; margin-left: 2px;
}
.bsky369-stage {
  position: relative;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 320px;
}
.bsky369-circle {
  position: relative;
  /* radius 110px = seat 의 translateY(-110px) 와 일치 → 각 아바타 의
   * 중심 이 정확 히 dashed 원 위 에 올라 옴.  사용자 spec : "캐릭터
   * 아바타 원 의 중심 은 메인 dashed 원 위 에 놓여야 함".  width /
   * height 는 디버그 / 변경 시 한 줄 만 손 보면 되도록 110*2 = 220. */
  width: 220px;
  height: 220px;
  border: 2px dashed var(--border);
  border-radius: 50%;
}
.bsky369-seat {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  transition: transform 200ms ease;
  /* dirarrow (z-index 1) 보다 위 — bubble 이 가운데 화살표 에 가려지지
   * 않도록.  seat 은 transform 으로 이미 stacking context, 자식 인
   * bubble 의 z-index 는 이 context 안 에서 만 작동 하므로 seat 자체 의
   * z-index 가 회전 화살표 보다 커야 함. */
  z-index: 5;
}
.bsky369-seat .bsky369-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
}
/* 사용자 의 self-identification 은 .is-user 의 파란 border (평소).
 * 매 turn 의 active highlight (사용자 보고 #3) + setup 의 시작자
 * highlight 는 .is-active / .is-starter 의 빨간 원.  selector
 * specificity 가 같으므로 — turn 표시 가 self-id 보다 우선 하게
 * .is-active / .is-starter 규칙 을 .is-user 보다 나중 (= 아래 의
 * 별도 selector) 에 둠.  Pulse animation 으로 빨간 원 이 부드럽게
 * 깜빡 — 매 turn 마다 다음 seat 로 옮겨감. */
/* 사용자 spec : 빨간 원 → 노란 spotlight glow.  border 도 노랑 + 다층
 * box-shadow 의 부드러운 halo + pulse animation. */
@keyframes bsky369-active-pulse {
  from {
    box-shadow:
      0 0 16px rgba(253, 224, 71, 0.85),
      0 0 32px rgba(253, 224, 71, 0.55),
      0 0 56px rgba(253, 224, 71, 0.30);
  }
  to {
    box-shadow:
      0 0 28px rgba(253, 224, 71, 1.0),
      0 0 56px rgba(253, 224, 71, 0.75),
      0 0 92px rgba(253, 224, 71, 0.45);
  }
}
.bsky369-seat.is-user .bsky369-avatar { border-color: color-mix(in srgb, var(--accent, #0085ff) 70%, transparent); }
/* .is-active / .is-starter 가 .is-user 보다 뒤 에 정의 — turn 표시
 * 의 노란 spotlight 이 self-id 의 파란 border 를 이김 (same specificity,
 * source-order tiebreak). */
.bsky369-seat.is-active .bsky369-avatar,
.bsky369-seat.is-starter .bsky369-avatar {
  border-color: #fde047;
  animation: bsky369-active-pulse 1100ms ease-in-out infinite alternate;
}
.bsky369-name { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
.bsky369-bubble {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 4px 10px;
  font-weight: 700;
  font-size: 16px;
  white-space: nowrap;
  max-width: 120px;
  /* overflow: hidden + ellipsis 제거 — cartoon polish 의 ::after / ::before
   * 꼬리 (bottom: -8px) 가 잘려 안 보이는 root cause.  text 는 짧음. */
  position: absolute;
  top: -36px;
  box-shadow: var(--shadow-md);
}
.bsky369-bubble[hidden] { display: none; }
.bsky369-center {
  position: absolute;
  font-size: 80px;
  font-weight: 700;
  color: var(--accent, #0085ff);
  pointer-events: none;
  /* 사용자 spec : 카운트다운 표시 가 아바타 들 (.bsky369-seat z-index 5)
   * 위 로 보여야 함.  seat 보다 큰 z-index 로 — countdown 의 cinematic
   * scale 1.4 transform 시 avatar 와 겹치는 영역 의 가독성 확보. */
  z-index: 10;
  text-shadow: 0 0 18px var(--surface), 0 0 6px var(--surface);
}
.bsky369-infochip {
  position: absolute;
  bottom: -28px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
}
.bsky369-input {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding-bottom: 16px;
}
.bsky369-numchoices {
  display: flex;
  gap: 12px;
  /* fixed height — visible / hidden 둘 다 같은 layout 공간.  사용자
   * 보고 : 숫자 카드 나타날 때 캐릭터 들 의 원 이 위로 밀려 올라가는
   * 현상 방지.  56px box-sizing + 2px border 더한 button 실제 height
   * 와 일치 (아래 .bsky369-numchoice 의 height: 56px). */
  height: 56px;
  align-items: center;
}
/* hidden 시 layout 공간 유지 (display:none 대신 visibility). */
.bsky369-numchoices[hidden] {
  display: flex;
  visibility: hidden;
  pointer-events: none;
}
.bsky369-numchoice {
  min-width: 72px;
  height: 56px;
  padding: 0 18px;
  box-sizing: border-box;
  font-size: 22px;
  font-weight: 700;
  background: var(--surface);
  border: 2px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
  color: inherit;
}
.bsky369-numchoice:hover {
  border-color: var(--accent, #0085ff);
  background: color-mix(in srgb, var(--accent, #0085ff) 10%, transparent);
}
.bsky369-claparea {
  width: 120px;
  height: 88px;
  border-radius: 18px;
  border: 2px solid var(--border);
  background: var(--surface);
  font-size: 44px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 80ms ease, transform 80ms ease, border-color 80ms ease;
}
.bsky369-claparea:hover { border-color: var(--accent, #0085ff); }
.bsky369-claparea.is-flash {
  background: color-mix(in srgb, var(--accent, #0085ff) 25%, transparent);
  transform: scale(0.96);
}
.bsky369-gameover {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  margin: 80px auto;
  text-align: center;
}
/* 게임 오버 의 새 layout (사용자 spec : "게임 화면 살짝 반투명, 그 위 로
 * GAME OVER" — blur 는 쓰지 않 음).  page 에 is-gameover class 가 있을
 * 때 직전 의 stage children 은 opacity 만, .bsky369-gameover 만 overlay
 * 로 위 painting. */
.bsky369-root.is-gameover > *:not(.bsky369-gameover) {
  opacity: 0.45;
  pointer-events: none;
  transition: opacity 200ms ease;
}
.bsky369-root.is-gameover > .bsky369-gameover {
  position: absolute;
  inset: 0; /* top:0 right:0 bottom:0 left:0 */
  margin: 0;
  z-index: 50;
  justify-content: center;
  /* backdrop / blur 없음 — 사용자 spec "블러 처리 는 하지 말고 그냥
   * 살짝 반투명".  GAME OVER 의 빨강 + SCORE 의 색깔 의 contrast 는
   * stage 의 opacity 0.45 만 으로 도 충분. */
  color: #f1f5f9;
  animation: bsky369-go-fade-in 300ms ease-out;
}
@keyframes bsky369-go-fade-in {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

/* PAUSE overlay — gameover overlay 와 같은 stage-dim 패턴.  사용자 spec :
 * "GAME OVER 때 처럼 게임 화면 반투명 + PAUSE 표시.  재개 = 다시 동작". */
.bsky369-root.is-paused > *:not(.bsky369-pause) {
  opacity: 0.45;
  pointer-events: none;
  transition: opacity 200ms ease;
}
.bsky369-root.is-paused > .bsky369-pause {
  position: absolute;
  inset: 0;
  margin: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 16px;
  color: #f1f5f9;
  animation: bsky369-go-fade-in 300ms ease-out;
}
.bsky369-pause-title {
  font-size: 56px;
  font-weight: 900;
  letter-spacing: 0.08em;
  color: #f59e0b;
  margin: 0;
}

/* INTRO overlay — 사용자 spec : 처음 화면 의 stage 위 dim overlay +
 * 369 logo + 게임 방법 / 게임 시작 버튼.  pause / gameover 와 같은
 * dim 패턴. */
.bsky369-root.is-intro > *:not(.bsky369-intro) {
  opacity: 0.45;
  pointer-events: none;
  transition: opacity 200ms ease;
}
.bsky369-root.is-intro > .bsky369-intro {
  position: absolute;
  inset: 0;
  margin: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 28px;
  color: #f1f5f9;
  animation: bsky369-go-fade-in 300ms ease-out;
}
.bsky369-intro-logo {
  /* 사용자 spec : 369 / GAME 두 줄 의 logo.  큰 369 위 + 작은 GAME 밑. */
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0;
  line-height: 1;
}
.bsky369-intro-logo-369,
.bsky369-intro-logo-game {
  font-weight: 900;
  letter-spacing: 0.05em;
  background: linear-gradient(135deg, #fbbf24 0%, #ef4444 50%, #f59e0b 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  text-shadow: 0 4px 24px rgba(245, 158, 11, 0.4);
}
.bsky369-intro-logo-369 { font-size: 112px; line-height: 0.95; }
.bsky369-intro-logo-game { font-size: 48px; letter-spacing: 0.2em; }
.bsky369-intro-actions {
  display: flex;
  gap: 12px;
}

/* HIGH SCORE 옆 의 작은 ↺ reset 버튼 — 사용자 spec. */
.bsky369-high-reset {
  background: transparent;
  border: 0;
  padding: 0 4px;
  margin-left: 2px;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 14px;
  vertical-align: middle;
}
.bsky369-high-reset:hover { color: var(--accent); }
body.bsky369-stage-mode .bsky369-high-reset { color: rgba(255, 255, 255, 0.55); }
body.bsky369-stage-mode .bsky369-high-reset:hover { color: white; }

/* PAUSE + 종료 그룹 — 사용자 spec : 일시정지 가 종료 왼쪽 에 붙어
 * 있도록.  topbar 의 space-between 에서 두 버튼 이 따로 떨어지지 않게
 * 한 wrapper 안 으로 묶 음. */
.bsky369-topbar-actions {
  display: flex;
  gap: 6px;
  align-items: center;
}
.bsky369-pause-btn {
  /* 종료 와 같은 디자인 / 사이즈 (.btn.secondary inherited). */
}

/* 아이템 — wave 3+ 의 5% spawn.  page (.bsky369-root) 안 의 absolute
 * positioned 으로 spawn, 3 초 후 자동 fade.  클릭 collect → 5초 속도
 * -20% 효과 + body.bsky369-item-active 로 무대 배경 dark green tint. */
/* 사용자 spec : 초록색 알약 (pill) 모양 + pulse + 빛나는 효과.
 * 가로 긴 캡슐 (radius 50% 의 양 끝).  중심 의 밝은 highlight + 다층
 * box-shadow 의 glow + 무한 pulse animation 으로 visibility 강조. */
.bsky369-item {
  position: absolute;
  width: 56px;
  height: 28px;
  border-radius: 999px;
  background:
    radial-gradient(ellipse at 30% 30%, rgba(255, 255, 255, 0.55) 0%, transparent 60%),
    linear-gradient(180deg, #4ade80 0%, #16a34a 100%);
  border: 1px solid #166534;
  cursor: pointer;
  z-index: 30;
  /* pop (등장) + glow-pulse (continuous) 의 두 애니메이션 동시 작동. */
  animation:
    bsky369-item-pop 320ms cubic-bezier(0.34, 1.56, 0.64, 1),
    bsky369-item-glow 1100ms ease-in-out infinite alternate 320ms;
  transition: transform 150ms ease;
}
.bsky369-item:hover {
  transform: scale(1.18);
}
.bsky369-item.is-collected {
  animation: bsky369-item-collect 250ms ease-out forwards;
  pointer-events: none;
}
.bsky369-item.is-fading {
  animation: bsky369-item-fade 250ms ease-out forwards;
  pointer-events: none;
}
@keyframes bsky369-item-pop {
  0%   { transform: scale(0); opacity: 0; }
  60%  { transform: scale(1.18); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes bsky369-item-glow {
  0% {
    box-shadow:
      0 0 12px rgba(74, 222, 128, 0.7),
      0 0 24px rgba(74, 222, 128, 0.45);
    transform: scale(1);
  }
  100% {
    box-shadow:
      0 0 24px rgba(134, 239, 172, 1.0),
      0 0 48px rgba(74, 222, 128, 0.8),
      0 0 80px rgba(74, 222, 128, 0.45);
    transform: scale(1.08);
  }
}
@keyframes bsky369-item-collect {
  0%   { transform: scale(1); opacity: 1; }
  100% { transform: scale(2); opacity: 0; }
}
@keyframes bsky369-item-fade {
  0%   { transform: scale(1); opacity: 1; }
  100% { transform: scale(0.7); opacity: 0; }
}

/* (시작자 의 빨간 원 + active turn 의 빨간 원 은 위 의 통합 selector
 *  에서 같이 styled — 별도 .is-starter 규칙 / starter-pulse keyframe
 *  은 사용자 보고 #3 으로 active 와 통일 되어 제거 됨.) */
/* 회전 방향 화살표 — 큰 빨간색 glyph, circle 중앙 위.  카운트다운
 * 중 만 DOM 에 존재 (clearSetupGraphics 가 제거). */
.bsky369-dirarrow {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 140px;
  color: #ef4444;
  pointer-events: none;
  opacity: 0.28;
  font-weight: 700;
  line-height: 1;
  z-index: 1;
}
/* ↻ / ↺ glyph 자체 가 이미 시계 / 반시계 방향 인 unicode — 별도
 * transform 변형 없이 그대로 표시. */

/* 게임 오버 화면 — 크게 GAME OVER + SCORE + HIGH SCORE + 버튼들. */
.bsky369-go-title {
  font-size: 56px;
  font-weight: 900;
  letter-spacing: 0.04em;
  color: #ef4444;
  margin: 24px 0 8px;
}
/* GAME OVER 아래 의 안내 메시지 — 왜 틀렸는지 한 줄.  살짝 부드러운
 * 톤 으로 (italic + muted) — 사용자 spec 의 "…" 줄임표 한국어 정서
 * 와 맞춤. */
.bsky369-go-reason {
  font-size: 16px;
  font-style: italic;
  color: var(--text-muted);
  margin: 0 16px 16px;
  max-width: 360px;
  line-height: 1.4;
}
.bsky369-go-score {
  font-size: 28px;
  color: var(--text);
  margin: 6px 0;
}
.bsky369-go-score strong { color: var(--accent, #0085ff); margin-left: 6px; }
.bsky369-go-high {
  font-size: 18px;
  color: var(--text-muted);
}
.bsky369-go-high strong { color: var(--text); margin-left: 6px; }
.bsky369-go-high.is-new strong { color: #ef4444; }
.bsky369-go-new { color: #ef4444; font-weight: 700; margin-left: 4px; }
.bsky369-go-buttons {
  display: flex;
  gap: 12px;
  margin-top: 24px;
}
.bsky369-share-dialog { max-width: 560px !important; width: calc(100% - 32px); }
.bsky369-share-dialog .modal-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  max-height: 80vh;
  max-height: 80dvh;
  overflow: auto;
}
.bsky369-share-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.bsky369-share-close {
  background: transparent;
  border: 0;
  cursor: pointer;
  font-size: 24px;
  line-height: 1;
  color: var(--text-muted);
  padding: 0 8px;
}
.bsky369-share-composer-root { display: flex; flex-direction: column; }
/* 게임 결과 공유 modal — 첨부 된 결과 카드 (1200x630 비율) 의
 * preview 를 더 크게 (composer 의 default thumbnail 보다).  alt /
 * watermark 칩 도 hidden 이라서 image cell 이 빈 공간 없이 큼.
 * Composer 의 image grid 의 한 칸 만 차지 — 다른 image 없 으므로
 * grid layout 도 1-up 으로 stretch. */
.bsky369-share-composer-root .composer-images {
  /* 결과 카드 한 장 만 — 1-up layout */
  grid-template-columns: 1fr !important;
  display: grid;
}
.bsky369-share-composer-root .image-cell {
  max-width: 100%;
  min-height: 180px;
}
.bsky369-share-composer-root .image-thumb {
  width: 100%;
  height: auto;
  max-height: 360px;
  object-fit: contain;
  background: var(--surface-2);
  border-radius: 8px;
  cursor: zoom-in;
}
/* (old direct-uploadBlob preview / textarea / hint styles removed —
 * Composer 가 자체 의 textarea / image attach UI 제공.) */
.bsky369-share-dialog-deprecated-placeholder {
  display: none;
}

/* Bonus menu 의 wrapping dialog — top-layer escape 의 chromium-safe
 * path.  Dialog 의 default ::backdrop / inset:0 / margin:auto / max-
 * width 등 을 모두 reset 해서 자식 menu 가 viewport 기준 fixed
 * positioning 으로 자유롭게 그려지도록.  modal 위 stack + hit-target
 * 정상 + modal box 밖 으로 나옴 동시 만족. */
.reader-bonus-menu-dialog {
  /* viewport 전체 차지 — menu 의 fixed positioning containing block
   * 을 viewport 로 유지 + backdrop click 영역 도 viewport 전체 라
   * outside click 의 ev.target === menuDialog 가 정확히 fire. */
  position: fixed;
  inset: 0;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  max-width: none;
  max-height: none;
  width: auto;
  height: auto;
  overflow: visible;
  z-index: 100000;
  box-shadow: none;
}
.reader-bonus-menu-dialog::backdrop {
  background: transparent;
}

/* ── 블스369 — visual polish (v1.0.29) ──────────────────────────── */

/* 현재 turn 의 active player 의 avatar 살짝 zoom-up — 빨간 ring +
 * pulse 외 의 추가 cue (사용자 spec #3). */
.bsky369-seat .bsky369-avatar {
  transition: transform 220ms cubic-bezier(0.4, 0.0, 0.2, 1);
}
.bsky369-seat.is-active .bsky369-avatar,
.bsky369-seat.is-starter .bsky369-avatar {
  transform: scale(1.25);
}

/* 정답 / 오답 flash — input element (numchoices 또는 claparea) 의
 * 짧은 green pulse / red shake.  사용자 spec #2. */
@keyframes bsky369-flash-correct {
  0%   { background-color: transparent; }
  30%  { background-color: color-mix(in srgb, #10b981 30%, transparent); }
  100% { background-color: transparent; }
}
@keyframes bsky369-flash-wrong {
  0%, 100% { transform: translateX(0); }
  20%  { transform: translateX(-8px); background-color: color-mix(in srgb, #ef4444 30%, transparent); }
  40%  { transform: translateX(8px); }
  60%  { transform: translateX(-6px); }
  80%  { transform: translateX(6px); }
}
.bsky369-numchoices.is-flash-correct,
.bsky369-claparea.is-flash-correct {
  animation: bsky369-flash-correct 300ms ease-out;
  border-radius: 12px;
}
.bsky369-numchoices.is-flash-wrong,
.bsky369-claparea.is-flash-wrong {
  animation: bsky369-flash-wrong 300ms ease-out;
  border-radius: 12px;
}

/* Wave splash — "WAVE N" 큰 글자 가 화면 위 에서 내려옴 (사용자 spec
 * #4 — 3 phase motion).  JS 가 transform translateY 를 매 frame 직접
 * 계산 — 여기 는 typography + 색 + 가로 가운데 정렬 만. */
.bsky369-wave-splash {
  position: fixed;
  left: 50%;
  top: 0;
  /* 시작 시 화면 위 밖 (transform 의 translateY 가 음수) — JS 가
   * step 마다 transform 설정 함.  여기 의 default 도 translate-50%
   * 가로 가운데 + 위 밖 으로 시작. */
  transform: translate(-50%, -100px);
  z-index: 100001;
  font-size: 72px;
  font-weight: 900;
  letter-spacing: 0.08em;
  color: #ef4444;
  pointer-events: none;
  white-space: nowrap;
}

/* ── 2D / 3D mode toggle (사용자 spec) ─────────────────────────── */
.bsky369-mode-toggle {
  position: fixed;
  left: 12px;
  /* footer 위 로 — JS 가 측정 한 footer 높이 + 12px gap.  default 60
   * 은 footer 미 mount 시 fallback. */
  bottom: calc(var(--bsky369-footer-h, 60px) + 12px);
  z-index: 100002;
  display: flex;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px;
  box-shadow: var(--shadow-md);
  font-size: 11px;
  font-weight: 700;
}
.bsky369-mode-btn {
  background: transparent;
  border: 0;
  padding: 6px 12px;
  border-radius: 999px;
  cursor: pointer;
  color: var(--text-muted);
  font: inherit;
  letter-spacing: 0.04em;
  transition: background 120ms ease, color 120ms ease;
}
.bsky369-mode-btn.is-active {
  background: var(--accent, #0085ff);
  color: white;
}
.bsky369-mode-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
/* DEBUG 버튼 — mode-toggle 의 마지막 자리.  on 일 때 빨강 강조 + 옆
 * 의 wave 선택 패널 visible. */
.bsky369-debug-btn {
  margin-left: 2px;
  border-left: 1px solid var(--border);
  padding-left: 12px;
  letter-spacing: 0.08em;
}
.bsky369-debug-btn.is-on,
.bsky369-debug-long-btn.is-on,
.bsky369-debug-inv-btn.is-on {
  background: #dc2626;
  color: white;
}
.bsky369-debug-long-btn[hidden],
.bsky369-debug-inv-btn[hidden] { display: none; }
/* Wave 선택 native <select> — DEBUG on 일 때 만 visible.  toggle 의
 * 마지막 child 로 capsule 안 에 자연 스럽게 흡수.  native dropdown
 * 이라 layout 을 위/아래 로 차지 하지 않 음 — 박수 영역 가리는
 * 이전 panel 의 문제 해결. */
.bsky369-debug-wave-select {
  background: transparent;
  border: 0;
  border-left: 1px solid var(--border);
  border-radius: 0 999px 999px 0;
  padding: 0 8px 0 10px;
  margin-left: 2px;
  font: inherit;
  color: var(--text);
  cursor: pointer;
  /* 기본 native select 의 화살표 + 여백 유지 — appearance 강제 변경 X. */
}
.bsky369-debug-wave-select[hidden] { display: none; }
.bsky369-debug-wave-select:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
body.bsky369-stage-mode .bsky369-debug-wave-select {
  color: white;
  border-left-color: rgba(255, 255, 255, 0.2);
}
body.bsky369-stage-mode .bsky369-debug-wave-select option {
  background: #1f2030; /* dark dropdown 옵션 — 무대 mode 에서 잘 보이게. */
  color: white;
}
body.bsky369-stage-mode .bsky369-debug-btn {
  border-left-color: rgba(255, 255, 255, 0.2);
}

/* 3D avatar canvas — bsky369-avatar div 안 의 canvas.  기존 avatar
 * 의 width/height (44px) 와 같은 box 안 에 fill. */
/* Avatar style — 3D 동물 의 머리 만 둥근 frame 안 에 (사용자 spec
 * "각 캐릭터 마다 원 안 의 동물 얼굴 만").  avatar div 를 둥글게
 * (border-radius 50%) + overflow hidden 으로 canvas 의 둥근 mask
 * 효과.  background 가 가운데 부드러운 spotlight (radial gradient)
 * — 어두운 무대 위 의 캐릭터 highlight. */
.bsky369-avatar-canvas {
  width: 100%;
  height: 100%;
  display: block;
}
.bsky369-avatar:has(canvas.bsky369-avatar-canvas) {
  /* 사용자 spec : 아바타 원 1.5배 (72→108px).  모델 의 visible 크기 는
   * 그대로 — JS 의 fit 가 1/1.5 로 보정 → world space scale 줄어듦. */
  width: 108px;
  height: 108px;
  border-radius: 50%;
  overflow: hidden;
  /* background-color : 사용자 spec — dashed circle 이 avatar 뒤 로 가려
   * 지도록 opaque base 부여.  이전 의 radial-gradient 는 alpha 가 낮 아
   * dashed border (parent 의 border) 가 avatar 통해 비쳤음.  base 색 +
   * gradient overlay 두 층 으로 spotlight 효과 유지 + 불 투명. */
  background-color: #1f2030;
  background-image: radial-gradient(circle at 50% 45%, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.04) 60%, rgba(0, 0, 0, 0.25) 100%);
  border: 2px solid var(--border);
}
body.bsky369-stage-mode .bsky369-avatar:has(canvas.bsky369-avatar-canvas) {
  border-color: rgba(255, 255, 255, 0.2);
}

/* ── 블스369 — v1.0.33 polish : 신기록 / 무대 theme / 시네마틱 카운트
 *               다운 / 말풍선 cartoon ────────────────────────────── */

/* (1) 신기록 — score flash + "신기록!" badge */
@keyframes bsky369-score-flash {
  0%   { transform: scale(1); color: var(--text); }
  30%  { transform: scale(1.4); color: #f59e0b; text-shadow: 0 0 12px rgba(245, 158, 11, 0.6); }
  100% { transform: scale(1); color: #f59e0b; text-shadow: none; }
}
.bsky369-score-num.is-flash-new-high {
  display: inline-block; /* transform 적용 위해 */
  animation: bsky369-score-flash 420ms ease-out;
  color: #f59e0b;
  font-weight: 800;
}
/* 사용자 spec : 신기록 badge 는 점수 아래 의 popover (absolute) — 두 줄
 * 안 되도록.  .bsky369-stat-score (relative) 의 자식 이라 그 기준 으로
 * 점수 의 아래 에 떠 있음. */
.bsky369-new-high-badge {
  position: absolute;
  top: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  padding: 3px 10px;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.06em;
  background: #f59e0b;
  color: white;
  border-radius: 999px;
  pointer-events: none;
  z-index: 10;
  animation: bsky369-new-high-pulse 1200ms ease-in-out infinite;
}
.bsky369-new-high-badge[hidden] { display: none; }
@keyframes bsky369-new-high-pulse {
  /* badge 는 popover 라 translateX(-50%) 로 center 정렬 됨 — keyframe
   * 의 transform 이 그것 을 override 하지 않도록 함께 유지. */
  0%, 100% { transform: translateX(-50%) scale(1); box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.5); }
  50%      { transform: translateX(-50%) scale(1.08); box-shadow: 0 0 0 6px rgba(245, 158, 11, 0); }
}

/* (2) 무대 theme — 어두운 background + spotlight radial 의 무대 분위기.
 * body 의 class 로 활성화 — game setup / play / gameover 동안.  토글
 * (.bsky369-mode-toggle) 도 dark 환경 에 맞춰 contrast 조정. */
body.bsky369-stage-mode {
  background: radial-gradient(circle at 50% 50%, #1f2030 0%, #0a0a14 70%) fixed;
  color: #f1f5f9;
  /* 아이템 효과 의 부드러운 tint transition 위 한 stacking context
   * 형성 — ::before 의 z-index -1 이 body content (z auto) 보다 아래,
   * body bg 보다 위 painted 되도록. */
  position: relative;
  z-index: 0;
}
/* 아이템 효과 활성 중 어두운 연두색 overlay — opacity transition 으로
 * 부드러운 dissolve.  사용자 spec "어두운 연두색, 부드러운 애니메이션". */
body.bsky369-stage-mode::before {
  content: '';
  position: fixed;
  inset: 0;
  background: #1a4222;
  opacity: 0;
  pointer-events: none;
  z-index: -1;
  transition: opacity 600ms ease;
}
body.bsky369-stage-mode.bsky369-item-active::before {
  opacity: 0.6;
}
body.bsky369-stage-mode .site-header,
body.bsky369-stage-mode .site-footer {
  background: transparent;
  border-color: rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.65);
}
body.bsky369-stage-mode .bsky369-circle {
  border-color: rgba(255, 255, 255, 0.15);
}
body.bsky369-stage-mode .bsky369-stat { color: rgba(255, 255, 255, 0.65); }
body.bsky369-stage-mode .bsky369-stat-score .bsky369-score-num,
body.bsky369-stage-mode .bsky369-stat-high .bsky369-high-num {
  color: #f1f5f9;
}
body.bsky369-stage-mode .bsky369-name {
  color: rgba(255, 255, 255, 0.7);
}
body.bsky369-stage-mode .bsky369-quit {
  background: rgba(255, 255, 255, 0.1);
  color: white;
  border-color: rgba(255, 255, 255, 0.2);
}
body.bsky369-stage-mode .bsky369-numchoice {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(255, 255, 255, 0.2);
  color: white;
}
body.bsky369-stage-mode .bsky369-numchoice:hover {
  background: color-mix(in srgb, var(--accent, #0085ff) 24%, transparent);
}
body.bsky369-stage-mode .bsky369-claparea {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(255, 255, 255, 0.2);
}
body.bsky369-stage-mode .bsky369-go-score,
body.bsky369-stage-mode .bsky369-go-reason {
  color: rgba(255, 255, 255, 0.8);
}
body.bsky369-stage-mode .bsky369-go-high { color: rgba(255, 255, 255, 0.55); }
body.bsky369-stage-mode .bsky369-go-high strong { color: white; }
body.bsky369-stage-mode .bsky369-mode-toggle {
  background: rgba(20, 20, 30, 0.85);
  border-color: rgba(255, 255, 255, 0.2);
}
body.bsky369-stage-mode .bsky369-mode-btn { color: rgba(255, 255, 255, 0.6); }

/* (3) 카운트다운 시네마틱 — 3 / 2 / 1 의 zoom-in + fade, Go! 의 더
 * dramatic 한 splash + shake. */
@keyframes bsky369-cd-zoom {
  0%   { transform: scale(0.4); opacity: 0; }
  18%  { transform: scale(1.4); opacity: 1; }
  60%  { transform: scale(1.0); opacity: 1; }
  100% { transform: scale(0.8); opacity: 0; }
}
@keyframes bsky369-cd-go {
  0%   { transform: scale(0.2); opacity: 0; }
  12%  { transform: scale(2.0); opacity: 1; }
  18%  { transform: scale(1.6) translateX(-10px); }
  24%  { transform: scale(1.6) translateX(10px); }
  30%  { transform: scale(1.6) translateX(-6px); }
  36%  { transform: scale(1.6) translateX(0); }
  100% { transform: scale(1.2); opacity: 0; }
}
/* fill-mode forwards + duration matched to runCountdown 의 setTimeout
 * (1000ms) — animation 이 끝난 뒤 element 가 default state (opacity 1,
 * transform none) 로 돌아오면 textContent 가 바뀌기 전까지 이전 글자
 * 가 잠깐 보이는 50ms gap 이 생기는 문제 (3 직후 2 보이기 전 3 잠깐
 * 보임, Go! 끝나고 cleared 직전 plain "Go!" 보임) 를 막는다. */
.bsky369-center-cinematic {
  animation: bsky369-cd-zoom 1000ms ease-out forwards;
}
.bsky369-center-cinematic-go {
  animation: bsky369-cd-go 1000ms ease-out forwards;
  color: #ef4444;
}

/* (4) 말풍선 cartoon polish — tail (꼬리) + drop-shadow + speak / clap
 * 색 분기. */
.bsky369-bubble {
  position: absolute;
  top: -42px;
  background: #ffffff;
  color: #111827;
  border: 2px solid #1f2937;
  border-radius: 14px;
  padding: 5px 12px;
  font-weight: 800;
  font-size: 16px;
  white-space: nowrap;
  max-width: 120px;
  /* overflow: hidden + text-overflow: ellipsis 를 제거.  ::before /
   * ::after 의 꼬리 (bottom: -8px) 가 잘려 안 보이던 문제.  text 는
   * 숫자 / 박수 emoji 만 이라 max-width + nowrap 으로 충분. */
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.25));
  z-index: 2;
}
.bsky369-bubble[hidden] { display: none; }
.bsky369-bubble::after {
  content: '';
  position: absolute;
  bottom: -8px;
  left: 50%;
  transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-top: 8px solid #1f2937;
}
.bsky369-bubble::before {
  content: '';
  position: absolute;
  bottom: -5px;
  left: 50%;
  transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 6px solid #ffffff;
  z-index: 1;
}
.bsky369-bubble.is-clap {
  background: #fef08a;
  border-color: #ca8a04;
}
.bsky369-bubble.is-clap::after { border-top-color: #ca8a04; }
.bsky369-bubble.is-clap::before { border-top-color: #fef08a; }
/* 거짓말 / 거짓박수 — wave 8+ 에서 CPU 가 false action 을 하는 케이스.
 * 진실 turn 에서는 (wave 5+) 말풍선이 숨겨지지만, 거짓말 turn 에서는
 * 빨간 말풍선이 보이면서 user 가 '저놈이 거짓말한다' 를 알 수 있도록
 * 한다. */
.bsky369-bubble.is-lie {
  background: #fecaca;
  border-color: #dc2626;
  color: #7f1d1d;
}
.bsky369-bubble.is-lie::after { border-top-color: #dc2626; }
.bsky369-bubble.is-lie::before { border-top-color: #fecaca; }

/* ── 블스RP스타 — 내 timeline 의 most-reposted 글 top 20 ──────────── */
/* .container (외부 wrapper) 가 이미 padding: 32px 16px + max-width:
 * 960px 을 제공 하므로, tool 내부 wrapper 는 layout-neutral.  cleaner
 * / drawing 과 같은 spacing 유지 하려면 여기 서 extra padding /
 * max-width 를 넣지 않아야 함 (이전 의 padding: 16px + max-width: 720px
 * 가 사방 으로 한 겹 더 들어가 사용자 가 "상하좌우 여백이 다르다" 라고
 * 보고). */
.repostrank-root {
  /* 의도적 으로 비워 둠 — 외부 .container 의 spacing 그대로 사용. */
}
.repostrank-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 32px 16px;
  text-align: center;
}
.repostrank-title { margin: 0; font-size: 24px; font-weight: 800; }
.repostrank-rules { margin: 0; color: var(--text-muted); font-size: 14px; }
.repostrank-start { padding: 10px 24px; font-size: 16px; }
.repostrank-progress {
  font-size: 14px;
  color: var(--text-muted);
  padding: 16px;
}
.repostrank-error { color: #dc2626; margin: 8px 0; }
.repostrank-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}
.repostrank-meta {
  font-size: 12px;
  color: var(--text-muted);
}
.repostrank-filters {
  display: flex;
  gap: 12px;
  margin-left: auto;
}
.repostrank-filter {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
  user-select: none;
}
.repostrank-refresh { padding: 4px 10px; font-size: 12px; }
.repostrank-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.repostrank-empty {
  color: var(--text-muted);
  text-align: center;
  padding: 32px;
}
.repostrank-card {
  display: flex;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px;
}
.repostrank-rank {
  flex: 0 0 auto;
  font-size: 18px;
  font-weight: 800;
  color: var(--accent, #0085ff);
  min-width: 36px;
  text-align: center;
}
.repostrank-card-right {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.repostrank-card-meta {
  display: flex;
  align-items: center;
  gap: 8px;
}
.repostrank-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--border);
  object-fit: cover;
}
.repostrank-who { display: flex; flex-direction: column; min-width: 0; }
.repostrank-name {
  font-weight: 700;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.repostrank-handle {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.repostrank-body {
  font-size: 14px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
.repostrank-stats {
  display: flex;
  gap: 12px;
  font-size: 13px;
  color: var(--text-muted);
}
.repostrank-rp { font-weight: 700; color: var(--accent, #0085ff); }
.repostrank-open {
  font-size: 12px;
  color: var(--text-muted);
  text-decoration: none;
}
.repostrank-open:hover { color: var(--accent); text-decoration: underline; }

/* 사용자 spec : reader 의 advanced settings 의 답글 순서 dropdown 을
 * native <select> 대신 Nabilera picker 로.  option row 의 padding 12px
 * 으로 native 보다 시각 적 으로 더 큰 height. */
/* settings modal 의 picker (글을 눌렀을 때 / 답글 순서 / 리포스트 표시
 * / 숫자 배지) — 라디오 의 reader-radio-picker 와 동일 사이즈 로 통일.
 * width 는 JS sizePickerToWidestOption() 가 옵션 라벨 의 최대 너비 측정
 * 후 inline style 로 적용.  picker 의 max-width 는 안전 cap. */
.reader-reply-order-picker {
  position: relative;
  display: inline-block;
  flex: 0 1 auto;
  max-width: 260px;
}
.reader-reply-order-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 8px 5px 10px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  text-align: left;
}
.reader-reply-order-trigger:hover { background: var(--surface-hover); }
.reader-reply-order-trigger[aria-expanded="true"] { border-color: var(--accent); }
.reader-reply-order-current {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: left;
}
.reader-reply-order-caret {
  flex: 0 0 auto;
  color: var(--text-faint);
  font-size: 11px;
  line-height: 1;
}
.reader-reply-order-menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
  padding: 4px;
  min-width: 100%;
  z-index: 100;
  max-height: 70vh;
  overflow-y: auto;
}
.reader-reply-order-menu[hidden] { display: none; }
.reader-reply-order-option {
  display: block;
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
  color: var(--text);
  font-size: 14px;
  text-align: left;
  white-space: nowrap;
  font: inherit;
}
.reader-reply-order-option:hover { background: var(--surface-hover); }
.reader-reply-order-option.is-selected {
  background: var(--accent-soft);
  color: var(--accent-hover);
  font-weight: 600;
}

/* 블스RP스타 — 추가 elements (idle 화면 의 기간 라디오, 결과 의 nabilera
 * 링크 등).  공통 base styles 는 직전 commit 의 .repostrank-* rules. */
.repostrank-range-wrap {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 20px 0 16px;
}
.repostrank-range-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
}
.repostrank-range-radios {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
.repostrank-range-radio {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 14px;
  color: var(--text);
  cursor: pointer;
  user-select: none;
}
.repostrank-start-wrap {
  display: flex;
  justify-content: flex-start;
  margin-bottom: 12px;
}
.repostrank-links {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}
.repostrank-open-nabilera { color: var(--accent); }

/* ── 🔮 블스타로 ─────────────────────────────────────────────────── */
.tarot { display: flex; flex-direction: column; gap: 16px; }
.tarot-hero h1 { margin: 0 0 4px; }
.tarot-hero .hint { margin: 0; }
.tarot-keycard .tarot-key-input,
.tarot-question,
.tarot-post-text { width: 100%; box-sizing: border-box; }
.tarot-key-link { display: inline-block; margin: 8px 0; }
.tarot-actions { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
.tarot-spread-row { display: flex; gap: 8px; margin: 12px 0 4px; flex-wrap: wrap; }
.tarot-spread-btn.is-active { background: var(--accent); color: #fff; border-color: var(--accent); }
/* 컨트롤 행 : 스프레드 토글(좌) + 뽑기 버튼(우) */
.tarot-controls {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-top: 14px;
}
.tarot-spread-toggle {
  display: inline-flex; border: 1px solid var(--border); border-radius: 999px; overflow: hidden;
}
.tarot-spread-seg {
  padding: 8px 18px; border: 0; background: transparent; color: var(--text-muted);
  cursor: pointer; font: inherit; transition: background .15s, color .15s;
}
.tarot-spread-seg + .tarot-spread-seg { border-left: 1px solid var(--border); }
.tarot-spread-seg.is-active { background: var(--accent); color: #fff; }
.tarot-image-preview { display: flex; justify-content: center; margin: 14px 0; min-height: 60px; }
.tarot-image-loading { padding: 40px 0; text-align: center; }
.tarot-image-canvas {
  max-width: 100%; height: auto; border-radius: 14px;
  box-shadow: 0 6px 28px rgba(0, 0, 0, 0.45);
}
.tarot-keyhint { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.btn.ghost { background: transparent; border: 1px solid var(--border); color: var(--text-muted); }
.tarot-results { display: flex; flex-direction: column; gap: 14px; }
.tarot-drawn-q { font-style: italic; color: var(--text-muted); margin-bottom: 10px; }
.tarot-disclaimer { margin: 12px 0 0; text-align: center; font-size: 12px; color: var(--text-faint); }
.tarot-cards { display: flex; gap: 10px; flex-wrap: wrap; }
.tarot-card {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  min-width: 96px; padding: 12px 10px; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--surface-2); text-align: center;
}
.tarot-card.is-reversed { border-style: dashed; }
.tarot-card.is-reversed .tarot-card-glyph { transform: rotate(180deg); }
.tarot-card-glyph { font-size: 26px; line-height: 1; }
.tarot-card-name { font-size: 13px; font-weight: 600; }
.tarot-card-dir { font-size: 11px; color: var(--text-faint); }
.tarot-tellers { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.tarot-teller { margin: 0; }
.tarot-teller.is-away { opacity: 0.7; }
.tarot-teller-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.tarot-teller-emoji { font-size: 20px; }
.tarot-reading { margin: 0; line-height: 1.6; white-space: pre-wrap; }
.tarot-away { margin: 0; color: var(--text-faint); font-style: italic; }
.tarot-spinner { color: var(--text-faint); }
.tarot-choose-row { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
.tarot-choose.is-active { background: var(--accent); color: #fff; border-color: var(--accent); }
.tarot-post-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 8px; }
.tarot-counter { font-size: 12px; color: var(--text-faint); }
.tarot-counter.over { color: #e5484d; font-weight: 700; }

/* 인터랙티브 뽑기 — 78장 회전 휠 */
.tarot-pick-info { text-align: center; color: var(--text-muted); margin: 4px 0 8px; min-height: 20px; }
.tarot-ring {
  position: relative; width: 360px; height: 360px;
  max-width: 90vw; max-height: 90vw; margin: 4px auto; --R: 158px;
}
@media (max-width: 520px) { .tarot-ring { --R: 41vw; } }
/* 카드 뽑기 여부와 무관하게 휠은 계속 회전(사용자 요청).  hover-pause 없음. */
.tarot-ring-spin {
  position: absolute; inset: 0;
  animation: tarot-spin 60s linear infinite;
}
@keyframes tarot-spin { to { transform: rotate(360deg); } }
/* 78장이 원주에 촘촘히 — 카드는 가늘게(14px) + 호 방향으로 세워(--ang 회전)
   부채살처럼 배열, 겹침 최소화.  translateY 로 반지름만큼 바깥. */
.tarot-pick-card {
  position: absolute; left: 50%; top: 50%; width: 14px; height: 30px;
  margin: -15px 0 0 -7px; padding: 0; border: 0; background: none; cursor: pointer;
  transform: rotate(var(--ang)) translateY(calc(-1 * var(--R)));
  transition: filter .15s ease, opacity .45s ease, transform .45s ease;
}
.tarot-pick-card:hover {
  filter: drop-shadow(0 0 6px var(--accent)); z-index: 3;
  transform: rotate(var(--ang)) translateY(calc(-1 * var(--R) - 6px)) scale(1.18);
}
.tarot-pick-card.is-taken {
  opacity: 0; pointer-events: none;
  transform: rotate(var(--ang)) translateY(calc(-1 * var(--R))) scale(.35);
}
.tarot-pick-back {
  display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;
  border-radius: 3px; border: 1px solid #6b53b8; color: #d9c8ff; font-size: 7px;
  background: linear-gradient(145deg, #2a2160, #4a2f8f); box-shadow: 0 1px 2px rgba(0,0,0,.4);
  pointer-events: none; /* 클릭은 버튼이 직접 받도록 (겹친 카드 사이 정확한 타깃) */
}
.tarot-picked-row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 12px; }
.tarot-card-pos { font-size: 10px; color: var(--accent); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }

/* 고른 카드가 중앙으로 슈욱 다가오며 플립 */
.tarot-fly {
  position: fixed; left: 50%; top: 42%; width: 124px; height: 196px; margin: -98px 0 0 -62px;
  z-index: 10000; perspective: 900px; pointer-events: none;
  opacity: 0; transform: translateY(64px) scale(.28);
  transition: opacity .45s ease, transform .9s cubic-bezier(.18,.85,.25,1);
}
.tarot-fly.is-revealed { opacity: 1; transform: translateY(0) scale(1); }
.tarot-fly-inner {
  position: relative; width: 100%; height: 100%; transform-style: preserve-3d;
  transition: transform .9s cubic-bezier(.3,.7,.3,1);
}
.tarot-fly.is-revealed .tarot-fly-inner { transform: rotateY(180deg); }
.tarot-fly-face {
  position: absolute; inset: 0; backface-visibility: hidden; border-radius: 12px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
}
.tarot-fly-back {
  background: linear-gradient(145deg, #2a2160, #4a2f8f); color: #d9c8ff;
  border: 1px solid #6b53b8; font-size: 46px;
}
.tarot-fly-front {
  background: var(--surface); border: 1px solid var(--border-strong);
  transform: rotateY(180deg); padding: 12px; text-align: center;
}
.tarot-fly-front .tarot-card-glyph { font-size: 44px; }
.tarot-fly-front.is-reversed .tarot-card-glyph { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  /* 휠 회전은 사용자 요청으로 계속 유지(은은한 상시 회전).  급격한 슈욱
     플라이/플립만 끈다. */
  .tarot-ring-spin { animation-duration: 120s; }
  .tarot-fly { transition: none; }
  .tarot-fly-inner { transition: none; }
}



/* ── 🗂 월루(월급루팡) 모드 ──────────────────────────────────────────────
 * boss-key 위장.  엑셀 스킨은 데스크톱(>=900px) 에서만 — reader.js 의
 * wooluExcelOnDom()/wooluExcelOn() 도 같은 브레이크포인트로 게이트돼 모바
 * 일에선 평소 카드.  설정은 기기 로컬(PDS 비동기화).  스플래시/브랜드 미
 * 변경(CLAUDE.md FIXED). */

/* 월루모드 모달 — 옵션마다 한 줄(라디오 + 라벨), 모든 화면폭에서 동일. */
.reader-woolu-note { margin: 0 0 10px; }
.reader-woolu-options { display: flex; flex-direction: column; gap: 8px; }
.reader-woolu-option {
  display: flex; align-items: center; gap: 8px; width: 100%;
  cursor: pointer; padding: 6px 4px;
}
.reader-woolu-option input { flex: 0 0 auto; margin: 0; }
.reader-woolu-opt-label { flex: 1 1 auto; }

/* ── 엑셀 스킨 (데스크톱 전용) ─────────────────────────────────────────── */
@media (min-width: 900px) {
  body[data-woolu="excel"] {
    --woolu-side-w: 132px;                 /* A열(사이드바) 너비 */
    --woolu-cols: 150px minmax(0,1fr) 52px 60px 52px 60px 60px;  /* B~H */
    --woolu-grid: #c8ccd2;
    --woolu-head: #e9edf1;
  }

  /* 엑셀 모드는 다크/커스텀 테마를 잠시 무력화 → 라이트 강제 (사용자 spec).
   * 핵심 색 토큰을 :root(라이트) 값으로 !important 재정의하면 시트뿐 아니라
   * body 직속 모달(<dialog>)까지 상속받아 흰 배경 + 검은 글자가 된다.
   * data-theme="dark" 블록(stylesheet) 과 커스텀 테마 인라인 변수(non-
   * important) 둘 다 이 !important 가 이긴다.
   *
   * ⚠ 반드시 html.reader-active 로 게이트 — 이 라이트 강제는 '리더(클라이언트)
   * 의 엑셀 모드 화면' 안에서만.  다른 나빌레라 도구로 가면 reader-active 가
   * 빠지므로(app.js route 토글) 다크였던 사용자는 다시 다크로 복원된다.
   * (data-woolu attr 가 body 에 남아 있어도 reader-active 없으면 무효.) */
  html.reader-active body[data-woolu="excel"] {
    --bg: #f8fafc !important;
    --surface: #ffffff !important;
    --surface-2: #f8fafc !important;
    --surface-hover: #f1f5f9 !important;
    --border: #e2e8f0 !important;
    --border-strong: #cbd5e1 !important;
    --text: #0f172a !important;
    --text-muted: #475569 !important;
    --text-faint: #94a3b8 !important;
    --accent: #0085ff !important;
    --accent-hover: #0066cc !important;
    --accent-soft: #eff6ff !important;
    --danger: #e11d48 !important;
    --danger-hover: #be123c !important;
    --danger-soft: #fff1f2 !important;
    --danger-border: #fecdd3 !important;
    --danger-text: #9f1239 !important;
    --warning-soft: #fffbeb !important;
    --warning-border: #fde68a !important;
    --warning-text: #92400e !important;
    /* 카드 틴트(리포스트/마스킹) — 다크 테마 값이 남으면 리포스트 카드의
     * 글자가 안 보였음(사용자 보고).  라이트 값으로 강제. */
    --card-repost-bg: #ecfccb !important;
    --card-repost-border: #e2e8f0 !important;
    --card-masked-bg: #fbedef !important;
    --card-masked-border: #f2bac5 !important;
    --card-repost-masked-bg: #fed7aa !important;
    --card-repost-masked-border: #d97f43 !important;
    --notif-like-border: #f9a8d4 !important;
    --notif-repost-border: #86efac !important;
    --notif-follow-border: #93c5fd !important;
    --notif-starterpack-border: #fdba74 !important;
    --notif-verified-border: #8b5cf6 !important;
    --column-profile-banner-bg: #cfe5fb !important;
    --column-search-banner-bg: #dde0fa !important;
    --column-notif-banner-bg: #fce7f3 !important;
    --column-likes-banner-bg: #fce7f3 !important;
    --column-bookmarks-banner-bg: #fce7f3 !important;
    --column-feed-banner-bg: #fde4c6 !important;
    --column-list-banner-bg: #ede1fc !important;
    color-scheme: light !important;
  }

  /* 좌상단 flush — 컨테이너 패딩/최대폭 제거, shell 이 viewport 를 꽉 채움. */
  html.reader-active body[data-woolu="excel"] #view.container,
  body[data-woolu="excel"] #view.container {
    max-width: none !important; margin: 0 !important; padding: 0 !important;
    height: 100dvh !important;
  }
  body[data-woolu="excel"] main { overflow: hidden !important; }

  /* 사이드바 레일 배경(fixed, body 직속) — 평소엔 FAB 를 가리려 좌측 strip
   * 을 칠하지만 월루 모드에선 그게 'A' 헤더/사이드바 위를 덮어 빈 영역으로
   * 보였음(사용자 보고).  월루 모드 사이드바는 자체 grid 셀 + 배경이 있어
   * 레일이 불필요 → 숨김. */
  body[data-woolu="excel"] .reader-sidebar-rail-bg { display: none !important; }

  /* shell = 시트 그리드 : 1행 헤더(A~H) · 2행 [사이드바 | 피드]. */
  body[data-woolu="excel"] .reader-shell {
    display: grid !important;
    grid-template-columns: var(--woolu-side-w) 1fr !important;
    grid-template-rows: auto minmax(0, 1fr) !important;
    gap: 0 !important; align-items: stretch !important;
    height: 100dvh !important; width: 100% !important; margin: 0 !important;
    background: #fff !important;
  }

  /* 흰 시트 배경 + 검정 시스템폰트. */
  body[data-woolu="excel"] .reader-shell,
  body[data-woolu="excel"] .reader-sidebar,
  body[data-woolu="excel"] .reader-main,
  body[data-woolu="excel"] .reader-columns,
  body[data-woolu="excel"] .reader-column,
  body[data-woolu="excel"] .reader-column-content,
  body[data-woolu="excel"] .reader-list,
  body[data-woolu="excel"] .reader-column-list { background: #fff !important; }
  /* 글자색은 강제하지 않는다 — 라이트 테마 강제(위 토큰 override)가 모든
   * 표면을 흰 배경 + 어두운 --text 로 만들어 가독성을 보장하므로, 예전의
   * blanket color:#000 은 불필요(오히려 다크 틴트 카드에서 검정-위-검정으로
   * 안 보이게 만들던 원인).  폰트만 시스템 폰트로 통일. */
  body[data-woolu="excel"] .reader-main *,
  body[data-woolu="excel"] .reader-column *,
  body[data-woolu="excel"] .reader-sidebar * {
    font-family: -apple-system, "Segoe UI", "Malgun Gothic", system-ui, sans-serif !important;
  }

  /* ── A열 = 사이드바 : fixed 그룹들을 static 으로 풀어 grid 셀에 정렬. ── */
  body[data-woolu="excel"] .reader-sidebar {
    grid-row: 2 !important; grid-column: 1 !important;
    width: var(--woolu-side-w) !important; min-width: 0 !important;
    display: flex !important; flex-direction: column !important;
    background: #f7f8fa !important;
    border-right: 1px solid var(--woolu-grid) !important;
    position: static !important; overflow-y: auto !important;
    padding: 0 !important; margin: 0 !important; gap: 0 !important;
  }
  body[data-woolu="excel"] .reader-sidebar-fixed-top,
  body[data-woolu="excel"] .reader-sidebar-feeds,
  body[data-woolu="excel"] .reader-sidebar-bottom {
    position: static !important;
    left: auto !important; top: auto !important; bottom: auto !important;
    /* 평소 24px 스파인 재중심용 margin-left:-5px(특히 .reader-sidebar-feeds)
     * 가 남으면 레일이 왼쪽으로 밀려 커스텀 피드 행의 가로 구분선이 우측
     * 5px 못 미쳐 끊겼음(사용자 보고).  월루에선 margin 0 으로 풀 너비. */
    margin: 0 !important;
    width: 100% !important;
    display: flex !important; flex-direction: column !important;
    align-items: stretch !important; gap: 0 !important;
  }
  body[data-woolu="excel"] .reader-sidebar-btn {
    width: 100% !important; height: auto !important; min-height: 0 !important;
    background: transparent !important; border: 0 !important;
    border-bottom: 1px solid var(--woolu-grid) !important; border-radius: 0 !important;
    box-shadow: none !important; margin: 0 !important; padding: 0 !important;
    justify-content: flex-start !important;
  }
  body[data-woolu="excel"] .reader-sidebar-btn svg,
  body[data-woolu="excel"] .reader-sidebar-btn img,
  body[data-woolu="excel"] .reader-account-avatar,
  body[data-woolu="excel"] .reader-sidebar-btn .reader-avatar,
  /* 저장된 피드 버튼의 아바타 이미지 / 이니셜 박스(.reader-feed-tab-letter)
   * 도 숨김 — 안 그러면 검은 이니셜 사각형이 A열에 남는다(사용자 보고). */
  body[data-woolu="excel"] .reader-feed-tab-avatar,
  body[data-woolu="excel"] .reader-feed-tab-letter { display: none !important; }
  body[data-woolu="excel"] .reader-sidebar-btn::after {
    content: attr(aria-label); display: block; font-size: 12px; line-height: 2.6;
    padding: 0 8px; width: 100%; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; text-align: left;
  }
  body[data-woolu="excel"] .reader-sidebar-btn.is-active::after {
    font-weight: 700 !important; background: #dbe7f3;
  }
  body[data-woolu="excel"] .reader-notif-badge {
    position: static !important; margin-left: 6px !important;
    background: transparent !important; font-size: 11px !important;
  }

  /* ── B~H열 = 피드 영역 : 단일 시트(패시브 숨김), 좌상단 flush. ── */
  body[data-woolu="excel"] .reader-columns {
    grid-row: 2 !important; grid-column: 2 !important;
    display: block !important; gap: 0 !important;
    overflow-x: hidden !important; overflow-y: auto !important;
    max-height: none !important; height: auto !important; min-width: 0 !important;
  }
  body[data-woolu="excel"] .reader-columns > .reader-column { display: none !important; }
  body[data-woolu="excel"] .reader-main {
    flex: none !important; width: 100% !important; max-width: none !important;
    min-width: 0 !important; overflow: visible !important;
    /* 멀티컬럼(has-passive-columns)일 때 .reader-main 에 붙는 padding-top:12px
     * 가 헤더와 첫 행 사이에 빈 틈을 만들어 맨 위에서 세로 격자선이 끊겼음
     * (사용자 보고).  월루 모드에선 패딩 0 — 첫 행이 헤더에 딱 붙어 세로선
     * 연속. */
    padding: 0 !important;
  }
  /* 시트 외 chrome (피드 헤더/탭/상태/히스토리·알림 상단 버튼) 숨김 —
   * ⚠ 반드시 .reader-main 안의 것만.  프로필 모달의 헤더도 renderProfileHeader
   * 가 .reader-feed-header(.is-profile-header) 로 만들고 탭도 .reader-profile-
   * feed-tabs-host 라, 스코프 없이 숨기면 모달의 프로필 헤더/탭까지 사라져
   * "글 위에 프로필이 안 뜸"(사용자 보고).  모달(<dialog>)은 .reader-main
   * 밖이라 영향 없음 → 평소 라이트 프로필 모달 그대로. */
  body[data-woolu="excel"] .reader-main .reader-feed-header,
  body[data-woolu="excel"] .reader-main .reader-profile-feed-tabs-host,
  body[data-woolu="excel"] .reader-main .reader-status,
  body[data-woolu="excel"] .reader-main .reader-history-header,
  body[data-woolu="excel"] .reader-main .reader-notif-add-column-btn,
  /* 떠 있는 FAB / 배경 감상 버튼은 body 직속 — 그대로 숨김(boss-key 위장). */
  body[data-woolu="excel"] .reader-radio-picker,
  body[data-woolu="excel"] .nabilera-tts-fab,
  body[data-woolu="excel"] .nabilera-pet-fab,
  body[data-woolu="excel"] .nabilera-debug-fab,
  body[data-woolu="excel"] .reader-bg-view-btn,
  body[data-woolu="excel"] .reader-pet-fab { display: none !important; }

  /* ── (헤더) A~H 컬럼문자 행 — shell 1행 전체폭. ── */
  body[data-woolu="excel"] .reader-woolu-colhead {
    grid-row: 1 !important; grid-column: 1 / -1 !important;
    display: grid !important;
    grid-template-columns: var(--woolu-side-w) var(--woolu-cols) !important;
    background: var(--woolu-head) !important;
    border-bottom: 1px solid var(--woolu-grid);
    font: 11px ui-monospace, monospace;
  }
  body[data-woolu="excel"] .reader-woolu-colhead-cell {
    text-align: center; padding: 3px 4px; border-right: 1px solid var(--woolu-grid);
    font-weight: 700; color: #3a3f47 !important;
  }
  body[data-woolu="excel"] .reader-woolu-colhead-cell:first-child { background: #dfe3e8; }
  body[data-woolu="excel"] .reader-woolu-colhead-cell:last-child { border-right: 0; }

  /* ── 데이터 행 : 행 사이 여백 0 → 세로 격자선 연속. ── */
  body[data-woolu="excel"] .reader-list,
  body[data-woolu="excel"] .reader-column-list,
  body[data-woolu="excel"] .reader-notif-list,
  body[data-woolu="excel"] .reader-search-list,
  body[data-woolu="excel"] .reader-mine-search-results {
    display: block !important; gap: 0 !important;
    margin: 0 !important; padding: 0 !important;
  }
  /* 알림 mention/reply/quote 행 래퍼 도 여백 0 — 안 그러면 행마다 세로선
   * 끊김.  unread 틴트(.reader-card 배경 accent-soft)도 흰 시트 로 통일. */
  body[data-woolu="excel"] .reader-notif-card-wrap {
    margin: 0 !important; padding: 0 !important; border: 0 !important;
  }
  body[data-woolu="excel"] .reader-notif-card-wrap.is-unread .reader-card {
    background: #fff !important;
  }
  body[data-woolu="excel"] .reader-woolu-row {
    display: grid !important;
    grid-template-columns: var(--woolu-cols);   /* B~H */
    background: #fff !important; border: 0 !important;
    border-bottom: 1px solid var(--woolu-grid) !important; border-radius: 0 !important;
    margin: 0 !important; padding: 0 !important; gap: 0 !important;
    font-size: 12px; line-height: 1.45; align-items: stretch;
  }
  body[data-woolu="excel"] .reader-woolu-cell {
    padding: 5px 8px; border-right: 1px solid var(--woolu-grid); min-width: 0; overflow: hidden;
  }
  body[data-woolu="excel"] .reader-woolu-dname { font-weight: 600; font-size: 12px; }
  body[data-woolu="excel"] .reader-woolu-handle { color: #5a6069 !important; font-size: 11px; }
  body[data-woolu="excel"] .reader-woolu-rel { color: #6a7079 !important; font-size: 10px; }
  body[data-woolu="excel"] .reader-woolu-content { white-space: pre-wrap; word-break: break-word; }
  body[data-woolu="excel"] .reader-woolu-text {
    display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; max-height: 6em; overflow: hidden;
  }
  body[data-woolu="excel"] .reader-woolu-link {
    display: inline-block; margin-top: 3px;
    font: 12px ui-monospace, monospace; color: #1a56db !important;
    text-decoration: underline; cursor: pointer; word-break: break-all;
  }
  body[data-woolu="excel"] .reader-woolu-num { text-align: right; font: 12px ui-monospace, monospace; }
  body[data-woolu="excel"] .reader-woolu-bookmarks { border-right: 0; }
  /* 클릭 가능 셀 — B(사용자→프로필), D~H(수치→인게이지먼트 모달).  엑셀
   * 셀처럼 보이되 호버 시 옅은 파랑 틴트로 클릭 가능함을 알린다. */
  body[data-woolu="excel"] .reader-woolu-user.reader-profile-target,
  body[data-woolu="excel"] .reader-woolu-clickable { cursor: pointer; }
  body[data-woolu="excel"] .reader-woolu-user.reader-profile-target:hover,
  body[data-woolu="excel"] .reader-woolu-clickable:hover { background: #eef3fb !important; }
  /* C열 본문 끝의 작성시각 (1분 전) + 알림 대상 글 스니펫 — 옅은 회색. */
  body[data-woolu="excel"] .reader-woolu-time {
    display: inline-block; margin-top: 3px; color: #6a7079 !important;
    font: 11px ui-monospace, monospace;
  }
  body[data-woolu="excel"] .reader-woolu-subject,
  body[data-woolu="excel"] .reader-woolu-quote {
    margin-top: 3px; color: #5a6069 !important; font-size: 11px;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    max-height: 3em; overflow: hidden;
  }
  /* 리포스트 / 답글 표시 줄 — 본문 위, 옅은 회색. */
  body[data-woolu="excel"] .reader-woolu-repost,
  body[data-woolu="excel"] .reader-woolu-reply {
    color: #6a7079 !important; font-size: 11px; margin-bottom: 2px;
  }
}

/* ── CLI 스킨 (데스크톱 전용) — 터미널/ASCII 위장 ─────────────────────────
 * 검은 화면 + 흰/녹색 모노스페이스 글자.  카드는 ASCII 박스, 사이드바는
 * 텍스트 메뉴(프롬프트 마커 "> "), 사이드바↔패널 경계는 한 줄 선.  엑셀
 * 스킨과 대칭 구조 — 단, 라이트가 아니라 "다크 강제".
 *
 * ⚠ 엑셀과 동일하게 html.reader-active 로 게이트 — 이 다크 강제는 '리더의
 * CLI 모드 화면' 안에서만.  월급루팡 모드를 끄거나 다른 나빌레라 도구로
 * 가면 reader-active 가 빠져(app.js route 토글) 원래 테마/모드로 복원된다.
 * data-woolu attr 가 body 에 남아 있어도 reader-active 없으면 무효.  prefs/
 * Theme 는 절대 안 바뀌므로(override 일 뿐) 복원은 attr/class 제거만으로 됨. */
@media (min-width: 900px) {
  html.reader-active body[data-woolu="cli"] {
    --bg: #000 !important;
    --surface: #000 !important;
    --surface-2: #0a0a0a !important;
    --surface-hover: #111418 !important;
    --border: #1c2128 !important;
    --border-strong: #2d333b !important;
    --text: #d6dee6 !important;
    --text-muted: #8b949e !important;
    --text-faint: #565d66 !important;
    --accent: #3fb950 !important;          /* 터미널 그린 */
    --accent-hover: #56d364 !important;
    --accent-soft: #0d1117 !important;
    --danger: #f85149 !important;
    --danger-hover: #ff7b72 !important;
    --danger-soft: #0d1117 !important;
    --danger-border: #2d333b !important;
    --danger-text: #ff7b72 !important;
    --warning-soft: #0d1117 !important;
    --warning-border: #2d333b !important;
    --warning-text: #d29922 !important;
    /* 카드 틴트(리포스트/마스킹/알림 테두리) — 검정/중립으로 눌러
     * 검은 화면 위에서 색 카드가 튀지 않게(터미널 일관성). */
    --card-repost-bg: #000 !important;     --card-repost-border: #1c2128 !important;
    --card-masked-bg: #000 !important;     --card-masked-border: #1c2128 !important;
    --card-repost-masked-bg: #000 !important; --card-repost-masked-border: #1c2128 !important;
    --notif-like-border: #2d333b !important;
    --notif-repost-border: #2d333b !important;
    --notif-follow-border: #2d333b !important;
    --notif-starterpack-border: #2d333b !important;
    --notif-verified-border: #2d333b !important;
    --column-profile-banner-bg: #0d1117 !important;
    --column-search-banner-bg: #0d1117 !important;
    --column-notif-banner-bg: #0d1117 !important;
    --column-likes-banner-bg: #0d1117 !important;
    --column-bookmarks-banner-bg: #0d1117 !important;
    --column-feed-banner-bg: #0d1117 !important;
    --column-list-banner-bg: #0d1117 !important;
    color-scheme: dark !important;
  }

  /* 좌상단 flush — 컨테이너 패딩/최대폭 제거, shell 이 viewport 를 꽉 채움.
   * ⚠ persistent 요소(#view·main·body)는 반드시 html.reader-active 로 게이트
   * — data-woolu attr 은 리더 이탈 후에도 body 에 남으므로, 게이트 없으면
   * 다른 나빌레라 도구로 가도 검은 화면/풀높이가 새어나온다(복원 깨짐).
   * body 배경은 var(--bg)(위 토큰 override, 이미 reader-active 게이트)가
   * 다스리므로 별도 background 규칙 불필요 — 이탈 시 자동 복원. */
  html.reader-active body[data-woolu="cli"] #view.container {
    max-width: none !important; margin: 0 !important; padding: 0 !important;
    height: 100dvh !important;
  }
  html.reader-active body[data-woolu="cli"] main { overflow: hidden !important; }
  body[data-woolu="cli"] .reader-sidebar-rail-bg { display: none !important; }

  /* 전부 모노스페이스 + 검정 배경. */
  body[data-woolu="cli"] .reader-shell,
  body[data-woolu="cli"] .reader-sidebar,
  body[data-woolu="cli"] .reader-main,
  body[data-woolu="cli"] .reader-columns,
  body[data-woolu="cli"] .reader-column,
  body[data-woolu="cli"] .reader-column-content,
  body[data-woolu="cli"] .reader-list,
  body[data-woolu="cli"] .reader-column-list { background: #000 !important; }
  body[data-woolu="cli"] .reader-main *,
  body[data-woolu="cli"] .reader-column *,
  body[data-woolu="cli"] .reader-sidebar * {
    font-family: ui-monospace, "SF Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace !important;
    letter-spacing: 0 !important;
    border-radius: 0 !important;
  }

  /* shell = [사이드바 | 패널] grid.  경계선은 사이드바 우측 한 줄. */
  body[data-woolu="cli"] .reader-shell {
    display: grid !important;
    grid-template-columns: 210px 1fr !important;
    gap: 0 !important; align-items: stretch !important;
    height: 100dvh !important; width: 100% !important; margin: 0 !important;
    background: #000 !important;
  }

  /* ── 사이드바 = 터미널 메뉴 (아이콘 숨기고 텍스트 라벨 + 프롬프트 마커) ── */
  body[data-woolu="cli"] .reader-sidebar {
    grid-column: 1 !important; width: 210px !important; min-width: 0 !important;
    display: flex !important; flex-direction: column !important;
    background: #000 !important;
    border-right: 1px solid #2d333b !important;     /* ← 패널 경계 */
    position: static !important; overflow-y: auto !important;
    padding: 10px 0 !important; margin: 0 !important; gap: 0 !important;
  }
  body[data-woolu="cli"] .reader-sidebar-fixed-top,
  body[data-woolu="cli"] .reader-sidebar-feeds,
  body[data-woolu="cli"] .reader-sidebar-bottom {
    position: static !important; left: auto !important; top: auto !important;
    bottom: auto !important; width: 100% !important;
    /* 셋 다 동일 들여쓰기로 정렬 — 특히 .reader-sidebar-feeds 의 base
     * margin-left:-5px(24px 스파인 재정렬용)를 0 으로 리셋하지 않으면
     * 커스텀 피드 제목이 다른 메뉴보다 왼쪽으로 밀린다(사용자 보고). */
    margin: 0 !important; padding: 0 !important;
    display: flex !important; flex-direction: column !important; gap: 0 !important;
  }
  /* 아이콘(svg) + 내부 라벨 span 숨기고, aria-label 을 ::after 텍스트로. */
  body[data-woolu="cli"] .reader-sidebar-btn {
    display: block !important; width: 100% !important; height: auto !important;
    min-height: 0 !important; padding: 0 !important; margin: 0 !important;
    background: transparent !important; border: 0 !important;
    text-align: left !important;
  }
  /* 아이콘(svg)·아바타(img, 커스텀 피드 탭)·폴백 letter·배지 숨기고,
   * aria-label 을 ::after 텍스트로 — 커스텀 피드도 이름만 텍스트로 보이게. */
  body[data-woolu="cli"] .reader-sidebar-btn > svg,
  body[data-woolu="cli"] .reader-sidebar-btn > span,
  body[data-woolu="cli"] .reader-sidebar-btn > img,
  body[data-woolu="cli"] .reader-sidebar-btn .reader-feed-tab-avatar,
  body[data-woolu="cli"] .reader-sidebar-btn .reader-feed-tab-letter,
  body[data-woolu="cli"] .reader-sidebar-btn .reader-notif-badge,
  body[data-woolu="cli"] .reader-sidebar-btn .reader-sidebar-badge { display: none !important; }
  body[data-woolu="cli"] .reader-sidebar-btn::after {
    content: "  " attr(aria-label);
    display: block; font-size: 13px; line-height: 2.1;
    padding: 0 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    color: #8b949e;
  }
  body[data-woolu="cli"] .reader-sidebar-btn:hover::after { color: #d6dee6; background: #0d1117; }
  body[data-woolu="cli"] .reader-sidebar-btn.is-active::after {
    content: "> " attr(aria-label);
    color: #3fb950 !important; font-weight: 700 !important; background: #0d1117;
  }
  /* 사이드바 상단에 가짜 프롬프트 헤더 한 줄. */
  body[data-woolu="cli"] .reader-sidebar-fixed-top::before {
    content: "user@nabilera:~$";
    display: block; padding: 0 12px 8px; margin-bottom: 4px;
    font-size: 12px; color: #3fb950; border-bottom: 1px solid #1c2128;
  }

  /* ── 카드 = 터미널 박스 (검정 + 한 줄 테두리, 모서리 각짐) ── */
  body[data-woolu="cli"] .reader-card {
    background: #000 !important;
    border: 1px solid #2d333b !important;
    border-radius: 0 !important; box-shadow: none !important;
    margin: 0 0 -1px 0 !important;                  /* 테두리 겹쳐 연속 로그처럼 */
    padding: 8px 12px !important;
  }
  body[data-woolu="cli"] .reader-card:hover { background: #0d1117 !important; }
  /* 아바타 이미지 숨김 — 터미널엔 그림 없음. */
  body[data-woolu="cli"] .reader-avatar,
  body[data-woolu="cli"] .reader-card-head img { display: none !important; }
  body[data-woolu="cli"] .reader-card-head {
    display: flex !important; align-items: baseline !important; gap: 6px !important;
    margin-bottom: 2px !important;
  }
  /* 작성자 = 프롬프트 풍 녹색, 핸들/시간 = 흐린 회색. */
  body[data-woolu="cli"] .reader-who { display: flex !important; align-items: baseline; gap: 6px; flex-wrap: wrap; }
  body[data-woolu="cli"] .reader-who strong::before { content: "@"; color: #3fb950; }
  body[data-woolu="cli"] .reader-who strong {
    color: #3fb950 !important; font-weight: 700 !important; font-size: 13px !important;
  }
  body[data-woolu="cli"] .reader-handle,
  body[data-woolu="cli"] .handle-bsky-suffix { color: #565d66 !important; font-size: 12px !important; }
  body[data-woolu="cli"] .reader-time { color: #565d66 !important; font-size: 12px !important; }
  /* 본문 = 흰 글자, 줄바꿈 보존(터미널 출력처럼). */
  body[data-woolu="cli"] .reader-body {
    color: #d6dee6 !important; font-size: 13px !important; line-height: 1.55 !important;
    white-space: pre-wrap !important; word-break: break-word !important;
  }
  /* 번역 버튼·플로팅 위젯 등 illusion 깨는 요소 숨김 (TTS 로봇 FAB·다마고치
   * 오리 등 — 실제 클래스는 nabilera-*-fab). */
  body[data-woolu="cli"] .reader-translate-wrap,
  body[data-woolu="cli"] .reader-card-watermark,
  body[data-woolu="cli"] .nabilera-tts-fab,
  body[data-woolu="cli"] .nabilera-pet-fab,
  body[data-woolu="cli"] [class*="nabilera-"][class*="-fab"],
  body[data-woolu="cli"] .reader-scrolltop,
  body[data-woolu="cli"] .reader-fab { display: none !important; }

  /* ── 액션 행 = 모노스페이스 카운트.  아이콘(svg)을 ASCII 토큰으로. ── */
  body[data-woolu="cli"] .reader-actions {
    display: flex !important; gap: 14px !important; margin-top: 4px !important;
    padding-top: 2px !important;
  }
  body[data-woolu="cli"] .reader-action {
    background: transparent !important; border: 0 !important; padding: 0 !important;
    color: #565d66 !important; font-size: 12px !important;
    display: inline-flex !important; align-items: baseline !important; gap: 3px !important;
  }
  body[data-woolu="cli"] .reader-action:hover { color: #d6dee6 !important; }
  body[data-woolu="cli"] .reader-action-glyph svg { display: none !important; }
  body[data-woolu="cli"] .reader-action-glyph {
    width: auto !important; height: auto !important; color: inherit !important;
  }
  body[data-woolu="cli"] .reader-action-reply    .reader-action-glyph::before { content: "re"; }
  body[data-woolu="cli"] .reader-action-repost   .reader-action-glyph::before { content: "rt"; }
  body[data-woolu="cli"] .reader-action-quote    .reader-action-glyph::before { content: "q"; }
  body[data-woolu="cli"] .reader-action-like     .reader-action-glyph::before { content: "<3"; }
  body[data-woolu="cli"] .reader-action-bookmark .reader-action-glyph::before { content: "*"; }
  body[data-woolu="cli"] .reader-action-engagements,
  body[data-woolu="cli"] .reader-action-bonus,
  body[data-woolu="cli"] .reader-action-pin { display: none !important; }
  body[data-woolu="cli"] .reader-action-glyph::before { color: #565d66; }

  /* 컬럼 배너(헤더) = 터미널 섹션 헤더 "== … ==" 풍. */
  body[data-woolu="cli"] .reader-column-banner,
  body[data-woolu="cli"] .reader-feed-banner {
    background: #0d1117 !important; color: #3fb950 !important;
    border-bottom: 1px solid #2d333b !important; font-size: 12px !important;
  }
}
