/* ============================================================
   reader.css — слово / IPA-сверху / перевод-чипом-снизу
                + 7 цветовых режимов раскраски + 4 модификатора

   Зависит от: tokens.css, themes.css, base.css

   Разметка одного слова:
     <span class="w w--show-ipa w--show-tr"
           data-pos="noun" data-tense="past" data-role="subject">
       <span class="w__ipa">ˈmɔːnɪŋ</span>
       <span class="w__text">morning</span>
       <span class="w__tr">утро</span>
     </span>

   Режимы (data-mode на .reader):
     off | pos | pos-underline |
     tense | tense-underline | tense-frame | role
   (pos-frame удалён 2026-05-12 — рамки вокруг каждого слова навязчивы)

   Модификаторы (классы на .reader):
     reader--plain-tr   (перевод без чипа)
     reader--only-tr    (только переводы, IPA скрыто)
     reader--only-ipa   (только IPA, переводы скрыты)
     reader--compact    (плотный line-height, для only-*)
     reader--tr-stack   (3-этажный стек переводов)
     reader--show-all   (отладка: показать всё на всех словах)
   ============================================================ */

/* ===== Базовый ридер ===== */
.reader {
  font-family: var(--font-body);
  font-size: var(--fz-word);
  color: var(--ink);
  line-height: var(--line-h);
  letter-spacing: -0.005em;
  /* WCAG ≥ 0.16em — улучшает скан-рейт у дислексии и слабого зрения */
  word-spacing: 0.04em;
}

/* ============================================================
   Слово — три слоя: IPA / текст / перевод
   .w — inline-block чтобы абсолютные слои привязывались к слову,
   но между словами оставался естественный пробел текстового потока.
   ============================================================ */
.w {
  display: inline-block;
  position: relative;
  cursor: pointer;
  line-height: 1.1;  /* локальный для слова, не общий line-h. 2026-05-15: 1.2→1.1 (user «сужай») */
}
.w--punct {
  cursor: default;
  color: var(--ink-soft);
  margin-left: -0.18em;
}

/* ===== IPA (сверху) ===== */
.w__ipa {
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  margin-bottom: var(--ipa-gap);
  font-family: var(--font-ipa);
  font-size: var(--fz-ipa);
  line-height: 1;
  color: var(--ink-faint);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease;
}
.w--show-ipa .w__ipa { opacity: 0.7; }

/* ===== Слово (текст) — все декорации висят на .w__text =====
   padding всегда есть — при hover меняется только background, чтобы
   строка не дрожала. */
.w__text {
  display: inline-block;
  background: transparent;
  padding: 0 3px;
  margin: 0 -3px;
  border-radius: var(--radius-sm);
  color: var(--ink);
  transition: background 160ms ease, color 240ms ease;
}

/* Полупрозрачная заливка при hover/open */
.w:hover .w__text,
.w.is-open .w__text {
  background: color-mix(in oklab, var(--hl) 55%, transparent);
}

/* ===== Перевод (снизу, чип) ===== */
.w {
  user-select: none;                  /* не выделяем текст при click/dblclick */
  -webkit-user-select: none;
}

.w__tr {
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translateX(-50%);
  margin-top: var(--tr-gap);
  font-family: var(--font-body);
  font-size: var(--fz-tr);
  line-height: 1;
  color: var(--translation);
  white-space: nowrap;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease;
}
/* Полупрозрачный чип перевода (дефолт).
   2026-05-15 v12: padding 1/6 → 0/5px — chip height 15→13, освобождает 2px
   на каждой TR-bearing строке + позволяет уменьшить --tr-chip-ext (см. ниже). */
.w--show-tr .w__tr {
  background: color-mix(in oklab, var(--surface-2) 70%, transparent);
  border-color: color-mix(in oklab, var(--border-soft) 60%, transparent);
  padding: 0 5px;
  opacity: 1;
}

/* «перевод без чипа» — просто текст под словом, без фона/рамки */
.reader--plain-tr .w--show-tr .w__tr {
  background: transparent;
  border-color: transparent;
  padding: 0;
  opacity: 1;
}

/* ============================================================
   Группировка придаточного предложения
   Соседние слова с одинаковым clause оборачиваются в общий
   <span class="clause clause--subordinate"> — даёт цельную рамку
   без разрывов между inline-block словами.
   ============================================================ */
.clause {
  display: inline;
}

/* ============================================================
   РЕЖИМ 1-2: pos — части речи (цвет / подчёркивание)
   ============================================================
   DATA-DRIVEN раскраска (эталон паттерна — см. css-v2/STANDARDS.md):
     1. МАППИНГ  — data-pos → --pos-c (по одной строке на часть речи)
     2. ПРИМЕНЕНИЕ — режим решает КАК использовать --pos-c (color / box-shadow)
   Добавить часть речи = одна строка маппинга, оба режима подхватят сами.
   Добавить режим = одна строка применения, все части речи подхватятся.
   Раньше было: режимы × части_речи = N*M почти одинаковых правил.
   Специфичность применения (0,5,0) сохранена 1:1 со старыми правилами.
   ============================================================ */

/* 1. МАППИНГ: data-pos → --pos-c (core EN + CJK/Hindi extensions) */
.w[data-pos="noun"]  { --pos-c: var(--pos-noun); }
.w[data-pos="verb"]  { --pos-c: var(--pos-verb); }
.w[data-pos="adj"]   { --pos-c: var(--pos-adj); }
.w[data-pos="adv"]   { --pos-c: var(--pos-adv); }
.w[data-pos="prep"]  { --pos-c: var(--pos-prep); }
.w[data-pos="pron"]  { --pos-c: var(--pos-pron); }
.w[data-pos="det"]   { --pos-c: var(--pos-det); }
.w[data-pos="conj"]  { --pos-c: var(--pos-conj); }
.w[data-pos="aux"]   { --pos-c: var(--pos-aux); }
.w[data-pos="part"]  { --pos-c: var(--pos-part); }    /* CJK/Hindi частица */
.w[data-pos="class"] { --pos-c: var(--pos-class); }   /* CJK классификатор */
.w[data-pos="num"]   { --pos-c: var(--pos-num); }     /* числительное */

/* 2. ПРИМЕНЕНИЕ: режим → способ */
.reader[data-mode="pos"]           .w[data-pos] .w__text { color: var(--pos-c); }
.reader[data-mode="pos-underline"] .w[data-pos] .w__text { box-shadow: inset 0 -2px 0 var(--pos-c); }

/* РЕЖИМ 3: pos-frame удалён 2026-05-12 — рамки вокруг каждого слова
   создают визуальную сетку, которая мешает скан-рейту. Если решим вернуть:
   git log -- web/css-v2/reader.css | grep -B1 "pos-frame removed". */

/* ============================================================
   РЕЖИМ 4: tense — времена · цвет (только глаголы)
   font-weight: 500 усиливает контраст глаголов на нейтральном фоне
   ============================================================ */
.reader[data-mode="tense"] .w[data-tense="past-perfect"]    .w__text { color: var(--tense-past-perfect); font-weight: 500; }
.reader[data-mode="tense"] .w[data-tense="past"]            .w__text { color: var(--tense-past); font-weight: 500; }
.reader[data-mode="tense"] .w[data-tense="past-cont"]       .w__text { color: var(--tense-past-cont); font-weight: 500; }
.reader[data-mode="tense"] .w[data-tense="present"]         .w__text { color: var(--tense-present); font-weight: 500; }
.reader[data-mode="tense"] .w[data-tense="present-cont"]    .w__text { color: var(--tense-present-cont); font-weight: 500; }
.reader[data-mode="tense"] .w[data-tense="present-perfect"] .w__text { color: var(--tense-present-perfect); font-weight: 500; }
.reader[data-mode="tense"] .w[data-tense="future"]          .w__text { color: var(--tense-future); font-weight: 500; }
.reader[data-mode="tense"] .w[data-tense="cond"]            .w__text { color: var(--tense-cond); font-weight: 500; }

/* ============================================================
   РЕЖИМ 5: tense-underline — времена · подчёркивание
   ============================================================ */
.reader[data-mode="tense-underline"] .w[data-tense="past-perfect"]    .w__text { box-shadow: inset 0 -2px 0 var(--tense-past-perfect); }
.reader[data-mode="tense-underline"] .w[data-tense="past"]            .w__text { box-shadow: inset 0 -2px 0 var(--tense-past); }
.reader[data-mode="tense-underline"] .w[data-tense="past-cont"]       .w__text { box-shadow: inset 0 -2px 0 var(--tense-past-cont); }
.reader[data-mode="tense-underline"] .w[data-tense="present"]         .w__text { box-shadow: inset 0 -2px 0 var(--tense-present); }
.reader[data-mode="tense-underline"] .w[data-tense="present-cont"]    .w__text { box-shadow: inset 0 -2px 0 var(--tense-present-cont); }
.reader[data-mode="tense-underline"] .w[data-tense="present-perfect"] .w__text { box-shadow: inset 0 -2px 0 var(--tense-present-perfect); }
.reader[data-mode="tense-underline"] .w[data-tense="future"]          .w__text { box-shadow: inset 0 -2px 0 var(--tense-future); }
.reader[data-mode="tense-underline"] .w[data-tense="cond"]            .w__text { box-shadow: inset 0 -2px 0 var(--tense-cond); }

/* ============================================================
   РЕЖИМ 6: tense-frame — времена · рамка вокруг глаголов
   ============================================================ */
.reader[data-mode="tense-frame"] .w[data-tense] .w__text {
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 0 4px;
  margin: 0 -4px;
}
.reader[data-mode="tense-frame"] .w[data-tense="past-perfect"]    .w__text { border-color: var(--tense-past-perfect); }
.reader[data-mode="tense-frame"] .w[data-tense="past"]            .w__text { border-color: var(--tense-past); }
.reader[data-mode="tense-frame"] .w[data-tense="past-cont"]       .w__text { border-color: var(--tense-past-cont); }
.reader[data-mode="tense-frame"] .w[data-tense="present"]         .w__text { border-color: var(--tense-present); }
.reader[data-mode="tense-frame"] .w[data-tense="present-cont"]    .w__text { border-color: var(--tense-present-cont); }
.reader[data-mode="tense-frame"] .w[data-tense="present-perfect"] .w__text { border-color: var(--tense-present-perfect); }
.reader[data-mode="tense-frame"] .w[data-tense="future"]          .w__text { border-color: var(--tense-future); }
.reader[data-mode="tense-frame"] .w[data-tense="cond"]            .w__text { border-color: var(--tense-cond); }

/* ============================================================
   РЕЖИМ 7: role — синтаксическая роль
   Цвет на словах + рамка-обводка вокруг придаточного целиком.
   font-weight: 500 — консистентно с режимом tense (эмфазис ключевых слов).
   2026-05-15: единственное определение role — дубль из multilang-секции удалён.
   ============================================================ */
.reader[data-mode="role"] .w[data-role="subject"]  .w__text { color: var(--role-subject); font-weight: 500; }
.reader[data-mode="role"] .w[data-role="verb"]     .w__text { color: var(--role-verb); font-weight: 500; }
.reader[data-mode="role"] .w[data-role="object"]   .w__text { color: var(--role-object); font-weight: 500; }
.reader[data-mode="role"] .w[data-role="modifier"] .w__text { color: var(--role-modifier); }

.reader[data-mode="role"] .clause--subordinate {
  background: color-mix(in oklab, var(--role-clause) 14%, transparent);
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--role-clause) 35%, transparent),
    inset 0 -1px 0 color-mix(in oklab, var(--role-clause) 20%, transparent);
  border-radius: 4px;
  padding: 1px 4px;
  /* Компенсация padding — чтобы группа не «толкала» соседей */
  margin: 0 -2px;
  /* Поддержка переноса строки внутри обёртки */
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* ============================================================
   Per-mode корректировки --ipa-gap / --tr-gap
   В режимах с подчёркиванием/рамкой слово занимает больше
   вертикали — отступы пере-определяются.
   ============================================================ */

/* Подчёркивания: перевод снизу — близко к подчёркиванию */
.reader[data-mode="pos-underline"]   { --tr-gap: 2px; }
.reader[data-mode="tense-underline"] { --tr-gap: 2px; }

/* Рамки: IPA остаётся внутри/у самой рамки слова, перевод чуть под */
.reader[data-mode="tense-frame"] {
  --ipa-gap: -4px;
  --tr-gap: 1px;
}

/* role: рамка вокруг clause не двигает текст по вертикали,
   но требует отступа снизу для перевода */
.reader[data-mode="role"] {
  --ipa-gap: -4px;
  --tr-gap: 4px;
}

/* ============================================================
   Dot markers (Phase C, 2026-05-14, SPEC-reader-interactions.md)
   ● paragraph dot — в начале каждого <p.para>, click = TTS параграф.
   • sentence dot — между предложениями, click = TTS одного предложения.
   Visibility per show-mode: basic — fadein on hover (не отвлекают),
   intermediate/pro — always visible (учим читать кусками).
   ============================================================ */
.dot {
  display: inline-block;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  color: var(--ink-faint);
  vertical-align: middle;
  /* 2026-05-14 Phase C fix (design-review caught): opacity NOT в transition —
     иначе при preset switch (массовый attribute change на reader content)
     transition stuck'ался в running state и opacity застревала на 0. Color
     и transform — transitionable, opacity — hard-switch. Pulsing animation
     уже отдельно через @keyframes, не conflict'ит. */
  transition: color 200ms ease, transform 200ms ease;
  line-height: 1;
}
.dot--paragraph {
  font-size: 10px;
  margin-left: -18px;
  margin-right: 8px;
  opacity: 0.5;
}
.dot--sentence {
  font-size: 6px;
  margin-right: 3px;
  opacity: 0.3;
}
.dot:hover {
  opacity: 1;
  color: var(--accent);
  transform: scale(1.4);
}

/* Pulsing animation когда TTS воспроизводит этот chunk */
.dot--playing {
  opacity: 1 !important;
  color: var(--accent) !important;
  animation: dot-pulse 800ms ease-in-out infinite;
}
@keyframes dot-pulse {
  0%, 100% { transform: scale(1.0); opacity: 0.6; }
  50%      { transform: scale(1.4); opacity: 1.0; }
}

/* Visibility per show-mode (SPEC):
   Basic: paragraph dot fade-in на hover параграфа, sentence dots — fully hidden до hover.
   Intermediate: оба видны всегда (sentence-by-sentence чтение для learners).
   Pro: paragraph dots всегда, sentence dots — на hover. */
.reader[data-show-mode="basic"] .dot--paragraph {
  opacity: 0;
}
.reader[data-show-mode="basic"] p.para:hover .dot--paragraph {
  opacity: 0.5;
}
.reader[data-show-mode="basic"] .dot--sentence {
  opacity: 0;
}
.reader[data-show-mode="basic"] p.para:hover .dot--sentence {
  opacity: 0.3;
}
/* Advanced/Pro — paragraph dot visible всегда (explicit чтобы перебить transition
   stuck-state); sentence dot — on paragraph hover. */
.reader[data-show-mode="advanced"] .dot--paragraph {
  opacity: 0.5;
}
.reader[data-show-mode="advanced"] .dot--sentence {
  opacity: 0;
}
.reader[data-show-mode="advanced"] p.para:hover .dot--sentence {
  opacity: 0.3;
}

/* Intermediate (Продолжающий) — preset combinator, не отдельный data-show-mode value.
   В Intermediate активен basic showMode + `.reader--only-tr` layer + `.reader--compact` density.
   Таргетим через эту comination для отличия от pure-basic: оба dots видны всегда. */
.reader[data-show-mode="basic"].reader--only-tr.reader--compact .dot--paragraph {
  opacity: 0.5;
}
.reader[data-show-mode="basic"].reader--only-tr.reader--compact .dot--sentence {
  opacity: 0.3;
}

/* ============================================================
   Saved-word ★ marker (Phase B, 2026-05-14, SPEC-reader-interactions.md)
   Visual marker для слов в словаре. Subtle, золотистый, маленький.
   Position: absolute (не влияет на line layout), слева сверху от слова.
   Status-aware: ярче для status="learn"/"new", тусклее для "known".
   ============================================================ */
.w.is-saved::before {
  content: "★";
  position: absolute;
  left: -2px;
  top: -4px;
  font-size: 9px;
  line-height: 1;
  color: var(--accent);
  opacity: 0.85;
  pointer-events: none;
  transition: opacity 200ms ease, color 200ms ease;
}
/* Known = выучила = тусклее (слово в архиве словаря, не активно учится) */
.w.is-saved[data-status="known"]:not(.is-due)::before {
  opacity: 0.4;
  color: var(--ink-faint);
}
/* Due (пришло время повторить) = более яркий, accent */
.w.is-saved.is-due::before {
  opacity: 1;
  color: var(--accent);
}
/* Hover на saved слове — звёздочка чуть ярче для visibility */
.w.is-saved:hover::before {
  opacity: 1;
}

/* ============================================================
   SHOW-MODE — видимость IPA/перевода per слово
   2026-05-12: мигрировано из words.css (там был duplicate без data-show-mode правил)

   basic    — каждый "interesting" токен показывает IPA+перевод по дефолту;
              user марк known → silence
   advanced — чистый текст, только saved words (status=new|learn) показывают
              слои; known слова silent пока не due (SR re-surfacing)

   Когда status=known && !is-due — скрыто в обоих режимах.
   ============================================================ */

/* basic: respect w--show-ipa / w--show-tr, но скрыть на known-not-due */
.reader[data-show-mode="basic"] .w.is-saved[data-status="known"]:not(.is-due) .w__ipa,
.reader[data-show-mode="basic"] .w.is-saved[data-status="known"]:not(.is-due) .w__tr {
  opacity: 0;
}

/* advanced: всё скрыто по дефолту, reveal только .is-saved+(not-known OR is-due) */
.reader[data-show-mode="advanced"] .w--show-ipa .w__ipa,
.reader[data-show-mode="advanced"] .w--show-tr .w__tr {
  opacity: 0;
}
.reader[data-show-mode="advanced"] .w.is-saved:not([data-status="known"]) .w__ipa,
.reader[data-show-mode="advanced"] .w.is-saved.is-due .w__ipa,
.reader[data-show-mode="advanced"] .w.is-saved:not([data-status="known"]) .w__tr,
.reader[data-show-mode="advanced"] .w.is-saved.is-due .w__tr {
  opacity: 1;
}
.reader[data-show-mode="advanced"] .w.is-saved:not([data-status="known"]) .w__ipa,
.reader[data-show-mode="advanced"] .w.is-saved.is-due .w__ipa {
  opacity: 0.7;          /* IPA чуть тише чем перевод даже когда оба видны */
}

/* "Due for review" — маленькая точка под словом (in either mode) */
.w.is-due .w__text::after {
  content: "";
  display: inline-block;
  width: 4px;
  height: 4px;
  background: var(--accent);
  border-radius: 50%;
  vertical-align: super;
  margin-left: 2px;
  opacity: 0.7;
}

/* ============================================================
   Reader mode toolbar — pills row, same look as .theme-switch
   2026-05-12: мигрировано из words.css duplicate.
   ============================================================ */
.reader-modes {
  display: flex;
  flex-wrap: wrap;
  /* v29 (2026-05-17): compact spacing — gap 8→6, padding 14/6→6/4.
     Settings panel теперь 2-col grid (style.css:.settings-panel__inner),
     меньше vertical thrash на каждой группе = ~40% компактнее vertically.
     align-items: center выравнивает kicker label с chips on baseline. */
  gap: 6px;
  padding: 6px 0 4px;
  align-items: center;
  font-family: var(--font-ui);
  font-size: 12px;
}
.reader-modes button {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  /* v29: chip padding 6/14 → 3/11 = ~half vertical, slightly tighter horizontal.
     При font 12px chip остаётся легко-clickable (24px height tap-target). */
  padding: 3px 11px;
  color: var(--ink-soft);
  cursor: pointer;
  font-family: inherit;
  font-size: inherit;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.reader-modes button:hover {
  color: var(--ink);
  border-color: var(--accent-soft);
}
.reader-modes button.active {
  background: var(--hl);
  color: var(--ink);
  border-color: var(--hl-strong);
}
.reader-modes button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  border-style: dashed;
}
.reader-modes .kicker {
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-muted);
  align-self: center;
  /* v29: min-width даёт vertical alignment — все labels занимают ровно 72px,
     chips начинаются на одной X-кромке в каждой колонке 2-col grid. */
  min-width: 72px;
  margin-right: 0;
}

/* ============================================================
   Модификатор: reader--plain-tr (перевод без чипа)
   Без фона визуальный вес перевода меньше — можно держать плотнее
   ============================================================ */
.reader--plain-tr {
  --tr-gap: -3px;
  line-height: 2.0;
}
.reader--plain-tr[data-mode="pos-underline"]   { --tr-gap: -1px; }
.reader--plain-tr[data-mode="tense-underline"] { --tr-gap: -1px; }
.reader--plain-tr[data-mode="tense-frame"]     { --tr-gap: -2px; }
.reader--plain-tr[data-mode="role"]            { --tr-gap: 1px; }

/* ============================================================
   Модификаторы: only-tr / only-ipa (один слой убран)
   ============================================================ */
.reader--only-tr {
  line-height: 1.85;
}
.reader--only-tr .w__ipa { display: none; }

.reader--only-ipa {
  line-height: 1.85;
}
.reader--only-ipa .w__tr { display: none; }

/* Ультра-компактный — для самых продвинутых, ещё плотнее.
   Перебивает .reader--tr-stack по специфичности. */
.reader--only-tr.reader--compact.reader--tr-stack,
.reader--only-tr.reader--compact { line-height: 1.55; }
.reader--only-ipa.reader--compact.reader--tr-stack,
.reader--only-ipa.reader--compact { line-height: 1.55; }

/* ============================================================
   Модификатор: reader--tr-stack — 3-этажный стек переводов
   data-tr-level="0|1|2|3" ставит JS на словах с visible translation
   ============================================================
   2026-05-14: per-line adaptive line-height.
   Раньше: paragraph-level lineHeight (2.6 + maxLevel*0.5) — все строки
   параграфа растягивались по максимальному стеку. Простые строки страдали
   из-за одной сложной (user feedback: «когда чтение комфортнее без
   переводов — текст должен стремиться к нормальному интервалу»).
   Теперь: baseline 1.6 (плотный, без tr), а .w[data-tr-level]
   получает padding-bottom — browser автоматически берёт max paddingа
   в каждой visual line. Простые строки остаются плотными,
   collision-строки растягиваются. */
.reader--tr-stack {
  --tr-row-h: 16px;
  line-height: 1.3;     /* baseline для строк БЕЗ visible TR/IPA. История: 1.6 → 1.4 → 1.3.
                           21px font × 1.3 = 27.3px (Bringhurst-рекомендация для serif body).
                           Lines с TR/IPA не затронуты — их высота = max(line-height, IPA-mt + font + TR-mb)
                           = max(27.3, 7 + 21 + 13) = 41 (без изменений vs 1.4). */
}
/* 2026-05-15 v7: УНИФИЦИРОВАН transform — чистый шаг сетки.
   Раньше: ad-hoc -4px (norm) / -7px (compact) «корректировки» из старой
   reserve-модели → level-1 chip садился на 4-7px выше своей зоны →
   наезжал на level-0 переводы соседних слов. User: «у тебя расстояние
   где в два уровня перевода».
   Теперь: translateY = --tr-row-h × N ровно. Каждый уровень встаёт на
   чистую сетку с шагом --tr-row-h (16px). Compact-варианты убраны —
   они и были источником рассинхрона. Reserve (--tr-chip-h + --tr-row-h × N)
   точно покрывает каждый уровень. */
.reader--tr-stack .w[data-tr-level="1"] .w__tr {
  transform: translateX(-50%) translateY(var(--tr-row-h));
}
.reader--tr-stack .w[data-tr-level="2"] .w__tr {
  transform: translateX(-50%) translateY(calc(var(--tr-row-h) * 2));
}
.reader--tr-stack .w[data-tr-level="3"] .w__tr {
  transform: translateX(-50%) translateY(calc(var(--tr-row-h) * 3));
}

/* Per-word vertical reservation — заставляет line-box расширяться адаптивно.
   .w__tr и .w__ipa — оба position: absolute (не в flow), anchored к
   `.w` padding box через `bottom: 100%` / `top: 100%`.

   ⚠️ 2026-05-15 v4 FIX (regression caught by user screenshot):
   Раньше использовался `padding-top` / `padding-bottom` на `.w` для reserve.
   Но `padding` ВХОДИТ в padding box → `.w__ipa { bottom: 100% }` начинал
   считать «100%» от полной высоты `.w` ВКЛЮЧАЯ padding → IPA взлетал на
   ~66px вверх, перевод улетал на ~66px вниз. Слои отрывались от слов.
   РЕШЕНИЕ: `margin-top` / `margin-bottom` вместо padding. Vertical margin
   на inline-block ТАКЖЕ расширяет line-box (reserve сохраняется), НО
   margin НЕ входит в padding box → `bottom: 100%` / `top: 100%` снова
   anchored к content height. Якорь absolute-слоёв стабилен.

   Browser line-box height = max(margin-top) + font + max(margin-bottom)
   среди inline-block .w в строке → автоматический per-line адаптив.
   Если в строке нет ни одного visible IPA/TR — line shrinks к baseline. */

/* ============================================================
   PER-LINE RESERVE — измеренные extent'ы + ОДНА константа зазора
   ============================================================
   2026-05-15 v8: переписано под ментальную модель user'а:
   «расстояние между строками = от последнего элемента строки N до
    первого элемента строки N+1, и это ОДНА константа — не зависит,
    перевод в один уровень, в два или в три».

   Три ИЗМЕРЕННЫХ факта (preview_eval, реальные пиксели):
     --tr-chip-ext   TR chip выступает ВНИЗ на 13px от word baseline (level-0)
     --ipa-chip-ext  IPA chip выступает ВВЕРХ на ~7px от word top
     --tr-row-h      шаг стекинга collision: каждый уровень = +16px

   ОДНА «магическая штука»:
     --line-gap      постоянный зазор между строками. Живёт ВЕСЬ в
                     margin-bottom. margin-top = ровно ipa-extent (без
                     зазора — иначе двойной счёт).

   Гарантия постоянного зазора. Для любых двух соседних строк с переводами:
     margin-bottom(N) = tr-extent + N×row-h + line-gap
     margin-top(N+1)  = ipa-extent
   Browser стыкует margin-box'ы вплотную →
     gap(TR-bottom строки N → IPA-top строки N+1) = line-gap. ВСЕГДА.

   Cascade без войны специфичности: collision-rules таргетят .w[data-tr-level],
   а hide-rules — .w--show-tr:not([data-tr-level]). JS присваивает
   data-tr-level ТОЛЬКО видимым словам (удаляет у невидимых) → селекторы
   ВЗАИМОИСКЛЮЧАЮЩИЕ, конфликт невозможен by construction. */
.reader--tr-stack {
  --tr-row-h:     13px;   /* шаг стекинга collision-уровней. История: 16 → 14 → 13.
                             2026-05-15 v15: после fz-tr 11→10 → chip 12px, шаг 13. */
  --tr-chip-ext:   8px;   /* TR chip выступ вниз от word baseline.
                             История: 13 → 11 → 10 → 8. v20: tr-gap -2→-4 (extra overlap). */
  --ipa-chip-ext:  4px;   /* IPA chip выступ вверх. История: 7 → 6 → 4.
                             v20: ipa-gap -4→-6 (extra overlap). */
  --line-gap:      0px;   /* ★ ЕДИНСТВЕННАЯ константа — запас СВЕРХ chip-extent.
                             История: 6 → 3 → 0 (2026-05-15 v8.1 → v9 → v10).
                             User: «строки можно ещё поджать. без переводов и транскрипций
                             расстояние = обычный комфортный reading. каждая строка
                             расширяется ТОЛЬКО на величину элемента». baseline line-height
                             1.4 уже даёт breathing — extra gap'а не нужно. Margin
                             перекрывает chip-extent ровно. */
}
.reader--tr-stack.reader--compact {
  --line-gap: 0px;        /* compact = тот же 0 — chip-extent уже минимально достаточен */
}

/* --- TR reserve (margin-bottom) — baseline: работает СРАЗУ, до JS assignTrLevels --- */
.reader--tr-stack .w--show-tr {
  margin-bottom: calc(var(--tr-chip-ext) + var(--line-gap));
}

/* --- IPA reserve (margin-top) — РОВНО extent IPA, без зазора --- */
.reader--tr-stack .w--show-ipa {
  margin-top: var(--ipa-chip-ext);
}

/* --- Visibility-aware hide — invisible IPA/TR → margin 0 (нет пустого reserve) ---
   basic:    saved+known+!due → оба слоя скрыты (opacity rules :277-296).
             Specificity (0,4,0) перебивает baseline.
   advanced: всё скрыто КРОМЕ saved+(not-known | due). Таргет :not([data-tr-level])
             делает rule взаимоисключающим с collision rules ниже. */
.reader[data-show-mode="basic"].reader--tr-stack .w.is-saved[data-status="known"]:not(.is-due) {
  margin-bottom: 0;
  margin-top: 0;
}
.reader[data-show-mode="advanced"].reader--tr-stack .w--show-tr:not([data-tr-level]) {
  margin-bottom: 0;
}
.reader[data-show-mode="advanced"].reader--tr-stack .w--show-ipa {
  margin-top: 0;
}
.reader[data-show-mode="advanced"].reader--tr-stack .w--show-ipa.is-saved:not([data-status="known"]),
.reader[data-show-mode="advanced"].reader--tr-stack .w--show-ipa.is-saved.is-due {
  margin-top: var(--ipa-chip-ext);
}

/* --- Global-hide пары: only-tr/only-ipa скрывают слой через display:none ---
   2026-05-15 v9 FIX (regression caught by user screenshot): когда слой скрыт
   глобально (`.reader--only-tr .w__ipa { display:none }`, lines 503/508),
   per-word reserve `margin-top: var(--ipa-chip-ext)` (или margin-bottom для TR)
   всё ещё применялся к `.w--show-ipa` / `.w--show-tr` / `.w[data-tr-level]` —
   получался пустой ~7px reserve между строками без видимого слоя.
   Симптом: «нет транскрипции, но строки не сжались».
   Fix: обнулить margin когда соответствующий слой скрыт глобально.
   Specificity (0,3,0) перебивает baseline (0,2,0) и collision-rules (0,3,0)
   = равная — побеждает порядок (этот блок идёт ПОСЛЕ всех reserve-правил). */
.reader--only-tr.reader--tr-stack .w--show-ipa {
  margin-top: 0;
}
.reader--only-ipa.reader--tr-stack .w--show-tr,
.reader--only-ipa.reader--tr-stack .w[data-tr-level="0"],
.reader--only-ipa.reader--tr-stack .w[data-tr-level="1"],
.reader--only-ipa.reader--tr-stack .w[data-tr-level="2"],
.reader--only-ipa.reader--tr-stack .w[data-tr-level="3"] {
  margin-bottom: 0;
}

/* --- Collision overrides — JS rectangle-pack ставит data-tr-level=0/1/2/3 ---
   на КАЖДОЕ visible-tr слово (0 для не-overlap'ящихся, 1/2/3 для опущенных).
   reserve = chip-extent + (level × шаг стекинга) + line-gap.
   level 0: 13+0+gap   ·   level 1: 13+16+gap   ·   level 2: 13+32+gap   ·   level 3: 13+48+gap
   → TR-chip любого уровня покрыт ровно, зазор всегда = --line-gap. */
.reader--tr-stack .w[data-tr-level="0"] {
  margin-bottom: calc(var(--tr-chip-ext) + var(--line-gap));
}
.reader--tr-stack .w[data-tr-level="1"] {
  margin-bottom: calc(var(--tr-chip-ext) + var(--tr-row-h) + var(--line-gap));
}
.reader--tr-stack .w[data-tr-level="2"] {
  margin-bottom: calc(var(--tr-chip-ext) + var(--tr-row-h) * 2 + var(--line-gap));
}
.reader--tr-stack .w[data-tr-level="3"] {
  margin-bottom: calc(var(--tr-chip-ext) + var(--tr-row-h) * 3 + var(--line-gap));
}

/* Тонкая «леска» от слова к опущенному переводу */
.reader--tr-stack .w[data-tr-level]:not([data-tr-level="0"]) .w__tr::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 1px;
  height: calc(var(--tr-row-h) * var(--tr-level, 1) - 2px);
  background: color-mix(in oklab, var(--ink-faint) 40%, transparent);
  transform: translateX(-0.5px);
  pointer-events: none;
}
.reader--tr-stack .w[data-tr-level="1"] { --tr-level: 1; }
.reader--tr-stack .w[data-tr-level="2"] { --tr-level: 2; }
.reader--tr-stack .w[data-tr-level="3"] { --tr-level: 3; }

/* ============================================================
   Отладочный режим: показать переводы и IPA на ВСЕХ словах
   ============================================================ */
.reader--show-all .w:not(.w--punct) .w__ipa { opacity: 0.7; }
.reader--show-all .w:not(.w--punct) .w__tr  { opacity: 1; }

/* Скрываем пустые переводы (для артиклей и т.п.) */
.reader--show-all .w__tr[data-empty] { opacity: 0 !important; }

/* В «без чипа» — оставляем без фона/рамки */
.reader--plain-tr.reader--show-all .w:not(.w--punct) .w__tr {
  background: transparent;
  border-color: transparent;
  padding: 0;
}

/* В обычном (с чипом) — даём чип */
.reader:not(.reader--plain-tr).reader--show-all .w:not(.w--punct) .w__tr {
  background: color-mix(in oklab, var(--surface-2) 70%, transparent);
  border-color: color-mix(in oklab, var(--border-soft) 60%, transparent);
  padding: 1px 6px;
}

/* Не на пустом — он не должен показывать чип */
.reader:not(.reader--plain-tr).reader--show-all .w__tr[data-empty] {
  background: transparent !important;
  border-color: transparent !important;
  padding: 0 !important;
}

/* ============================================================
   Попап (увеличенная подсказка по клику)
   ============================================================ */
.w__popover {
  /* 2026-05-11: убран display:none toggle + animation:pop-in transform.
     Раньше display:none → display:flex toggle вызывал layout thrash —
     popover мелькал «сбоку» пока браузер пересчитывал absolute position.
     Теперь display:flex всегда, видимость через opacity+visibility (compositor-only). */
  position: absolute;
  left: 50%;
  bottom: calc(100% + 4px);
  transform: translateX(-50%);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 7px 11px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  align-items: center;
  white-space: nowrap;
  z-index: 5;
  box-shadow: var(--shadow-pop);
  /* Hidden state: pure compositor properties, no layout changes */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 140ms ease-out, visibility 0s linear 140ms;
  font-family: var(--font-body);
}

/* 2026-05-11: было `rgba(0,0,0,0.5)` hardcoded. Вынесено в --shadow-pop-dark
   (см. themes.css). Default --shadow-pop ~ 0.08 для light themes;
   --shadow-pop-dark ~ 0.5 для контрастности на тёмном bg.
   2026-05-15: добавлены .theme-mist/.theme-sumi — reader.html использует
   class-селектор тем, без них тень не применялась. */
[data-theme="mist"] .w__popover,
[data-theme="sumi"] .w__popover,
.theme-mist .w__popover,
.theme-sumi .w__popover {
  box-shadow: var(--shadow-pop-dark, 0 6px 16px rgba(0, 0, 0, 0.5));
}

.w.is-open .w__popover {
  /* Visible state: opacity + visibility fade-in, no transform morphing.
     `transition: visibility 0s linear 0s` чтобы visibility включалась мгновенно при open
     (delay только при close, см. default state). */
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 140ms ease-out, visibility 0s linear 0s;
}

/* ============================================================
   Margin annotations — 4 variants (none / star / plus / categories).
   Reference: Design книги/02-margin-elements/{bare-glyph-5-variants,
   plus-on-margin-5-variants, quote-star-archive}.html.

   Каждый <p.para> получает <button.margin-mark> absolute-positioned слева.
   Saved заметка → .is-marked на параграфе + <aside.margin-note> inline ниже.
   Storage: localStorage `vls.notes.{bookId}.{chapter}.{para}`.
   Per-variant glyphs + color via category accent var (--mark-c).
   ============================================================ */

/* Категорийные accent-цвета заметок = bookmark-палитра (--bm-* из tokens.css).
   var(--bm-N) сам резолвится в активную тему → отдельный dark-override НЕ нужен.
   2026-05-15: были захардкоженные hex (дубль --bm-* под другими именами). */
.reader {
  --mark-quote: var(--bm-5);   /* aubergine — quotes */
  --mark-star:  var(--bm-1);   /* rust — favorites */
  --mark-arch:  var(--bm-3);   /* olive — archive */
}

/* Paragraph relative positioning — нужно для absolute margin-mark.
   v28 (2026-05-16): density toggle теперь даёт ВИДИМУЮ difference на default
   layer (`both` = IPA+перевод). Раньше `.reader--compact` имел effect только
   в комбинации с `.reader--only-tr/only-ipa` → на default layer обе button'ы
   density (стандарт/компактно) визуально identical (verified diff_paraMB
   SAME, diff_lineGap SAME). User: «стандарт надо добавить по паре пикселей
   между блоками». Fix: `--density-para-gap` extra на paragraph margin —
   нормальный layout = +4px breathing, compact = 0 (current). Влияет на ВСЕ
   layer combinations (both / only-tr / only-ipa). Line-gap (внутри tr-stack)
   НЕ трогаем — user раньше explicit «строки ещё поджать» = 0. */
.reader {
  --density-para-gap: 4px;        /* normal density: +4px between paragraphs */
}
.reader--compact {
  --density-para-gap: 0px;        /* compact: keep tight (browser 1em default) */
}
.reader p.para {
  position: relative;
  margin-bottom: calc(1em + var(--density-para-gap, 0px));
}

/* The margin button itself — absolute-positioned слева от текста параграфа */
.margin-mark {
  position: absolute;
  left: -34px;
  top: 0;
  width: 24px;
  height: 1.6em;
  background: transparent;
  border: 0;
  padding: 0;
  font-family: var(--font-display, 'Noto Serif', Georgia, serif);
  font-size: 18px;
  line-height: 1;
  color: var(--ink-faint);
  cursor: pointer;
  opacity: 0;
  transition: opacity 180ms, color 180ms, transform 180ms;
  user-select: none;
  text-align: center;
}
.reader p.para:hover .margin-mark,
.reader p.para.is-marked .margin-mark {
  opacity: 1;
}
.margin-mark:hover {
  color: var(--ink-soft);
  transform: translateY(-1px);
}

/* === Variant: none (hide all marks) === */
.reader[data-mark-style="none"] .margin-mark { display: none; }
.reader[data-mark-style="none"] .margin-note { display: none; }

/* === Variant: star (✦ always — bare glyph F1) === */
.reader[data-mark-style="star"] .margin-mark::before {
  content: "✦";
}
.reader[data-mark-style="star"] p.para.is-marked .margin-mark {
  color: var(--mark-star);
}

/* === Variant: plus (+ idle → ✦ marked, F3 plus-on-margin) === */
.reader[data-mark-style="plus"] .margin-mark::before {
  content: "+";
  font-size: 16px;
  font-weight: 400;
}
.reader[data-mark-style="plus"] p.para.is-marked .margin-mark::before {
  content: "✦";
  font-size: 18px;
}
.reader[data-mark-style="plus"] p.para.is-marked .margin-mark {
  color: var(--mark-star);
}

/* === Variant: categories (❝ quote / ★ star / ▣ archive — quote-star-archive) ===
   Glyph выбирается по data-mark-type (quote/star/archive) saved при создании. */
.reader[data-mark-style="categories"] .margin-mark::before {
  content: "+";                       /* idle = просто плюс, выбор категории по click */
  font-size: 14px;
  color: var(--ink-faint);
}
.reader[data-mark-style="categories"] .margin-mark[data-mark-type="quote"]::before {
  content: "\201D";                   /* ❞ */
  font-size: 22px;
}
.reader[data-mark-style="categories"] .margin-mark[data-mark-type="star"]::before {
  content: "\2605";                   /* ★ */
  font-size: 16px;
}
.reader[data-mark-style="categories"] .margin-mark[data-mark-type="archive"]::before {
  content: "\25A3";                   /* ▣ */
  font-size: 16px;
}
.reader[data-mark-style="categories"] .margin-mark[data-mark-type="quote"]   { color: var(--mark-quote); }
.reader[data-mark-style="categories"] .margin-mark[data-mark-type="star"]    { color: var(--mark-star); }
.reader[data-mark-style="categories"] .margin-mark[data-mark-type="archive"] { color: var(--mark-arch); }

/* === Inline note slot — appears under saved paragraph === */
.margin-note {
  position: relative;
  display: block;
  margin: 10px 0 14px;
  padding: 10px 14px 12px;
  background: color-mix(in srgb, var(--mark-c, var(--mark-star)) 6%, var(--bg));
  border-left: 2px solid var(--mark-c, var(--mark-star));
  font-family: var(--font-ui, 'Inter', system-ui, sans-serif);
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-soft);
}
.margin-note[data-mark-type="quote"]   { --mark-c: var(--mark-quote); }
.margin-note[data-mark-type="star"]    { --mark-c: var(--mark-star); }
.margin-note[data-mark-type="archive"] { --mark-c: var(--mark-arch); }
.margin-note__caret {
  position: absolute;
  left: -8px;
  top: 12px;
  width: 0; height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-right: 6px solid var(--mark-c, var(--mark-star));
}
.margin-note__meta {
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mark-c, var(--mark-star));
  margin-bottom: 4px;
}
.margin-note__body {
  font-family: var(--font-body, 'Source Serif 4', Georgia, serif);
  font-style: italic;
  font-size: 13px;
  color: var(--ink);
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  .margin-mark { transition: none; }
  .margin-mark:hover { transform: none; }
}

/* ============================================================
   Margin annotations v2 — mouse-tracked left margin interaction zone
   + inline comment input + Bookmark panel (Бусины Горизонт A2)

   Reference: docs/SPEC-margin-annotations-v2.md
              Design книги/02-margin-elements/bare-glyph-5-variants.html (F1 ✦)
              Design книги/03-bookmarks/notches-quotes-minimap-detail.html (A2 hearts)

   Архитектура:
   - .margin-zone — overlay absolute-positioned слева от .reader-content,
     ловит mousemove/mousedown/mouseup для star + stripe interactions
   - .margin-star — голый глиф ✦ который следует за курсором по Y
   - .margin-stripe — color tinted bg + accent edge для параграфа под мышью
     (или для всех параграфов в drag range)
   - .margin-note-input — inline form ниже параграфа после release / click
   - .bookmarks-panel — horizontal bar над reader-content, dots = saved notes
   ============================================================ */

/* ===== Margin interaction zone (overlay) ===== */
.margin-zone {
  position: absolute;
  top: 0;
  left: 0;
  width: 50px;
  height: 100%;
  pointer-events: auto;
  z-index: 5;
  /* visual cue: faint vertical rail на hover, чтобы user понял что зона активна */
}
/* 2026-05-11 v7: right margin zone — mirror left, position: fixed на правом краю */
.margin-zone--right .margin-star {
  left: auto;
  right: 14px;
}
.margin-zone--right::before {
  left: auto;
  right: 8px;
}
.margin-zone::before {
  content: "";
  position: absolute;
  top: 6px;
  bottom: 6px;
  right: 8px;
  width: 1px;
  background: var(--border-soft);
  opacity: 0;
  transition: opacity 200ms ease;
  pointer-events: none;
}
.margin-zone.is-hot::before,
.margin-zone.is-dragging::before {
  opacity: 0.7;
}

/* --glyph-char (heart ♥ / star ✦) для cursor trail + bookmarks:
   дефолт — в tokens.css :root, переключение [data-glyph] — в themes.css. */

/* Star ✦ — follows mouse Y while hovering (not dragging) */
.margin-star {
  position: absolute;
  left: 14px;
  width: 22px;
  height: 22px;
  line-height: 22px;
  text-align: center;
  font-family: var(--font-display, 'Noto Serif', Georgia, serif);
  font-size: 16px;
  color: var(--bm-1);
  pointer-events: none;
  opacity: 0;
  transform: translateY(-50%);
  transition: opacity 140ms ease, color 200ms ease;
  user-select: none;
  will-change: top, opacity;
  /* Текст глифа из CSS var — heart ♥ или star ✦ */
  font-size: 18px;
}
.margin-star::before { content: var(--glyph-char, "♥"); }
.margin-zone.is-hot:not(.is-dragging) .margin-star {
  opacity: 0.9;
}
.margin-zone.is-dragging .margin-star {
  opacity: 0;   /* hidden во время drag, only stripe shown */
}

/* 2026-05-11 v3: sparkle trail behind cursor — each ✦ fades out + drifts down.
   Spawned by JS at clientX/clientY (position: fixed → viewport coords).
   Auto-removed после animation. Z-index 100 чтобы быть поверх content but ниже modals. */
.margin-sparkle {
  position: fixed;
  width: 16px;
  height: 20px;
  line-height: 20px;
  text-align: center;
  font-family: var(--font-display, 'Noto Serif', Georgia, serif);
  font-size: 14px;
  color: var(--bm-1);
  pointer-events: none;
  user-select: none;
  z-index: 100;
  animation: sparkle-fade 800ms ease-out forwards;
  will-change: opacity, transform;
}
.margin-sparkle::before { content: var(--glyph-char, "♥"); }
@keyframes sparkle-fade {
  0%   { opacity: 0.85; transform: scale(1) translateY(0) rotate(0deg); }
  100% { opacity: 0;    transform: scale(0.5) translateY(14px) rotate(20deg); }
}
@media (prefers-reduced-motion: reduce) {
  .margin-sparkle { animation: none; opacity: 0; }
}

/* .is-marking / .has-note — JS-only state classes (drag-range / saved-note).
   НАМЕРЕННО без визуальных стилей: единственный visual cue = .line-marker overlays
   (line-precision). Раньше здесь был paragraph background — убран 2026-05-12 v4 по
   user feedback. Классы НЕ удалять — JS logic от них зависит. CSS-правил тут быть НЕ должно. */

/* ============================================================
   Line-level hint strip (2026-05-12 v3 — final per user feedback).
   "просто полосочка с той стороны где мышка, намекает что клик к этой строке"
   ОДНА тонкая вертикальная полосочка (3px) В МАРЖИНЕ возле строки.
   НЕ overlay over text — это было visual noise.
   Side: left (мышь в левой зоне) или right (правой).
   ============================================================ */
/* Default (pastel) = highlighter feel — bg на всю ширину строки в pastel оттенке.
   JS ставит width = line text width в pastel mode. */
.line-marker {
  position: absolute;
  pointer-events: none;
  z-index: 1;       /* за текстом — highlighter feel (color-mix transparent → текст просвечивает) */
  background: color-mix(in srgb, var(--bm-cur, var(--bm-1)) 22%, transparent);
  border-radius: 3px;
  transition: opacity 80ms ease;
}
.line-marker--start {
  /* первая строка диапазона — чуть ярче как anchor */
  background: color-mix(in srgb, var(--bm-cur, var(--bm-1)) 28%, transparent);
}

/* Mono override — 3px тонкая полоска в маржине (JS позиционирует через side='left'/'right'). */
[data-marker-style="mono"] .line-marker {
  background: var(--bm-cur, var(--bm-1));
  opacity: 0.7;
  border-radius: 2px;
  z-index: 5;
}
[data-marker-style="mono"] .line-marker.line-marker--start {
  background: var(--bm-cur, var(--bm-1));
  opacity: 1;
}

/* 2026-05-15 v12: read-icon — ПЕРСИСТЕНТНЫЙ элемент-sibling .line-marker в .reader-main
   (НЕ дочерний пересоздаваемого маркера). reader.js создаёт его ОДИН раз и лишь
   репозиционирует: top = вертикальный центр активной стартовой строки, left = 26px
   влево от левого края текста. Старый баг «невозможно нажать»: иконка была child'ом
   .line-marker, который clearLineMarkers() уничтожал на КАЖДЫЙ mousemove → DOM-узел
   кнопки пересоздавался под курсором, click не успевал сработать. Теперь узел стабилен.
   Геометрия: position:absolute + JS top/left, transform:translateY(-50%) центрирует
   иконку ровно НАПРОТИВ строки (user: «иконка напротив строки должна быть»).

   2026-05-15 v13: z-index ПОДНЯТ 60 → 250. Корневой баг «невозможно нажать» (3 провала
   подряд): форма заметки .margin-note-input--at-cursor (z-index:200) появляется в
   маржине РОВНО поверх иконки и при z-60 перекрывала её — настоящий клик попадал в
   форму, не в кнопку. 250 > 200 (форма) > 110 (saved bubble hover) > 90 (words-panel).
   Сам клик при этом обрабатывается ГЕОМЕТРИЧЕСКИ в reader.js (lineReadIconHitAt) и не
   зависит от z-index — но визуально иконка обязана быть сверху, иначе её не видно. */
.line-marker__read {
  position: absolute;
  top: 0;                          /* JS перезаписывает на центр строки */
  left: 0;                         /* JS перезаписывает */
  transform: translateY(-50%);     /* центрируем иконку по вертикали строки */
  width: 18px;
  height: 18px;
  padding: 0;
  margin: 0;
  border: 1.5px solid var(--bg);
  border-radius: 50%;
  background: var(--bm-cur, var(--bm-1));
  color: #fff;
  font-size: 7px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  pointer-events: auto;
  z-index: 250;                    /* выше формы заметки (z-200), saved bubble (z-110), words-panel (z-90) */
  box-shadow: 0 1px 4px rgba(0, 0, 0, .28);
  transition: transform 120ms ease, opacity 120ms ease;
}
.line-marker__read[hidden] { display: none; }
.line-marker__read::before { content: "▶"; }
/* Комфортная click-зона ВОКРУГ иконки (user: «надёжная зона где курсор ведёт себя
   как кнопка»). Симметричная +11px со всех сторон → итоговая зона ~40×40px.
   2026-05-15 v13: inset держать синхронно с LINE_READ_HIT_PAD в reader.js — там
   геометрический hit-test использует ту же величину. ::after — только визуальная/
   pointer-events подложка; фактическое решение «клик попал в иконку» reader.js
   принимает по rect самой кнопки + LINE_READ_HIT_PAD, не по elementsFromPoint. */
.line-marker__read::after {
  content: "";
  position: absolute;
  inset: -11px;
}
.line-marker__read:hover { transform: translateY(-50%) scale(1.18); }
.line-marker__read:active { transform: translateY(-50%) scale(0.95); }
body.is-reading-line .line-marker__read { display: none; }

/* ===== Inline note input — появляется ниже параграфа(ов) после click или release ===== */
.margin-note-input {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 8px 0 14px;
  padding: 10px 12px 10px 14px;
  background: color-mix(in srgb, var(--bm-cur, var(--bm-1)) 5%, var(--bg));
  border-left: 2px solid var(--bm-cur, var(--bm-1));
  border-radius: 0 4px 4px 0;
  font-family: var(--font-ui, 'Inter', system-ui, sans-serif);
  animation: noteFadeIn 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes noteFadeIn {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.margin-note-input__meta {
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 9px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bm-cur, var(--bm-1));
}
.margin-note-input__textarea {
  font-family: var(--font-body, 'Lora', Georgia, serif);
  font-style: italic;
  font-size: 14px;
  line-height: 1.45;
  color: var(--ink);
  background: transparent;
  border: 0;
  outline: 0;
  resize: vertical;
  min-height: 38px;
  padding: 2px 0;
  width: 100%;
  font-variant-numeric: oldstyle-nums;
}
.margin-note-input__textarea::placeholder {
  color: var(--ink-faint);
  font-style: italic;
}
.margin-note-input__hint {
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 9px;
  color: var(--ink-faint);
  letter-spacing: 0.06em;
  text-align: right;
  /* 2026-05-12: flex child default min-width:auto = natural content width (~215px),
     заставляет parent .margin-note-input быть ≥215px иначе hint оверфлоу за viewport.
     min-width:0 + flex-wrap позволяют hint wrap'ать на 2 строки при узких bubbles. */
  min-width: 0;
  white-space: normal;
  word-spacing: -0.04em;   /* плотнее = реже wrap */
}
.margin-note-input__hint kbd {
  font-family: inherit;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: 3px;
  padding: 0 4px;
  margin: 0 1px;
}

/* 2026-05-11 v6: floating mode — position absolute relative to .reader-main, в left margin.
   left + width — устанавливает JS (placeFloatingBubble) на основе actual viewport space.
   CSS лишь задаёт box decoration. */
.margin-note-input--floating {
  position: absolute;
  margin: 0;
  z-index: 50;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--bm-cur, var(--bm-1));
  border-radius: 6px;
  box-shadow: 0 6px 24px color-mix(in srgb, var(--ink) 12%, transparent);
  padding: 8px 10px;
}
.margin-note-input--floating .margin-note-input__textarea {
  min-height: 22px;
  max-height: 200px;
  resize: none;             /* auto-grow via JS */
  overflow: hidden;
}
.margin-note-input--floating .margin-note-input__hint {
  font-size: 8px;
  line-height: 1.5;        /* запас под wrap на 2 строки */
}
/* Narrow viewport — bubble overlays content area, добавляем translucent background */
.margin-note-input--floating.is-narrow {
  background: color-mix(in srgb, var(--surface) 96%, var(--ink) 4%);
  backdrop-filter: blur(2px);
}

/* 2026-05-15 v6: input-форма ПОД КУРСОРОМ (user «должно появляться под мышкой»).
   Может перекрывать текст пока вводишь — z-index 200 (выше всего content).
   Полупрозрачный фон + blur чтобы текст под формой просвечивал слегка,
   не теряя контекст. Лёгкая анимация появления (scale-in от точки клика). */
.margin-note-input--at-cursor {
  z-index: 200;
  background: color-mix(in srgb, var(--surface) 94%, var(--ink) 6%);
  backdrop-filter: blur(3px);
  box-shadow: 0 8px 28px color-mix(in srgb, var(--ink) 22%, transparent);
  transform-origin: top left;
  animation: note-input-popin 140ms cubic-bezier(0.16, 1, 0.3, 1);
  width: 240px;
  max-width: calc(100vw - 24px);
}
@keyframes note-input-popin {
  from { opacity: 0; transform: scale(0.92); }
  to   { opacity: 1; transform: scale(1); }
}

/* 2026-05-13 v13: strip mode для form — mirror'ит expanded state saved-bubble
   (translateX(-232px) + width 240). НО без resting-collapsed state — form всегда
   полноразмерная (нельзя печатать в 8px полоску). User: "при создании комментария
   должно быть сразу два места — для сжатого состояния экрана и для где есть поля".
   left = mainRect.width - 6 (anchor у правого края, как saved bubble strip),
   transform смещает контент leftward в content area. */
.margin-note-input--floating.is-strip {
  width: 240px;
  min-height: 60px;
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 4px solid var(--bm-cur, var(--bm-1));
  border-radius: 6px 2px 2px 6px;
  transform: translateX(-232px);
  z-index: 60;
}

/* Persistent saved-note display — appears ниже параграфа для existing notes.
   Reference: F1 bare-glyph .note slot (italic body + meta line). */
.margin-note-saved {
  position: relative;
  margin: 8px 0 14px;
  padding: 8px 12px 10px 14px;
  background: color-mix(in srgb, var(--bm-cur, var(--bm-1)) 4%, var(--bg));
  border-left: 2px solid var(--bm-cur, var(--bm-1));
  border-radius: 0 4px 4px 0;
  font-family: var(--font-ui, 'Inter', system-ui, sans-serif);
  cursor: pointer;
  transition: background 160ms ease;
}
.margin-note-saved:hover {
  background: color-mix(in srgb, var(--bm-cur, var(--bm-1)) 8%, var(--bg));
}

/* 2026-05-11 v6: floating saved-bubble — anchored к paragraph в left margin.
   left + width — устанавливает JS (placeFloatingBubble). */
.margin-note-saved--floating {
  /* 2026-05-13 v8: merged from second .margin-note-saved--floating duplicate +
     align visual styling с .margin-note-input--floating: same padding 8px 10px,
     colored border-left 3px, flex column for delete button at bottom-right. */
  position: absolute;
  margin: 0;
  z-index: 95;
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--bm-cur, var(--bm-1));
  border-radius: 6px;
  display: flex;
  flex-direction: column;
}
.margin-note-saved--floating .margin-note-saved__body {
  font-family: var(--font-body, 'Lora', Georgia, serif);
  font-style: italic;
  font-size: 12.5px;
  line-height: 1.4;
}
.margin-note-saved--floating.is-narrow {
  background: color-mix(in srgb, var(--surface) 94%, var(--ink) 6%);
}

/* 2026-05-13 v20: width через CSS var `--bubble-w` (set by JS placeFloatingBubble).
   Compact = HALF от вычисленной margin-width, hover = FULL. Left-side bubble компенсирует
   translateX(+halfW) чтобы compact визуально жался к тексту, а hover разворачивался
   В МАРЖИН (а не на текст). Right-side bubble естественно растёт вправо без transform.
   Form (margin-note-input--floating) всегда full width — нельзя печатать в half-width.
   User: "по ширине по умолчанию в половину уже сделай. при наведении раскрывается полный
   текст заметки и снизу перевод. это для режима где есть поля". */
.margin-note-input--floating {
  width: var(--bubble-w, 240px);
}
.margin-note-saved--floating:not(.is-strip) {
  width: calc(var(--bubble-w, 240px) / 2);
  transition: width 200ms cubic-bezier(.4,0,.2,1),
              transform 200ms cubic-bezier(.4,0,.2,1),
              padding 180ms ease;
}
/* Left-side compact: shift right by halfW чтобы bubble жался к text edge.
   На hover transform=0 → bubble растёт ВЛЕВО (в маржин), не на текст. */
.margin-note-saved--floating:not(.is-strip):not(.is-right) {
  transform: translateX(calc(var(--bubble-w, 240px) / 2));
}
.margin-note-saved--floating:not(.is-strip):hover,
.margin-note-saved--floating:not(.is-strip):focus-within {
  width: var(--bubble-w, 240px);
}
.margin-note-saved--floating:not(.is-strip):not(.is-right):hover,
.margin-note-saved--floating:not(.is-strip):not(.is-right):focus-within {
  transform: translateX(0);
}

/* 2026-05-13 v15: COMPACT-BY-DEFAULT для saved bubbles вне strip-режима.
   User: "слишком много информации. тут должна быть только заметка на русском
   и то частично чтобы места не занимали много. при наведении уже английская
   часть разворачивается и полный текст. при клики редактирование включается".
   Default: только note.text truncated 1 line. Hover/focus-within: full content
   (meta + полный body + translation + delete). Strip mode исключён через :not —
   там свой собственный cascade (8px resting → 240px expand). */
.margin-note-saved--floating:not(.is-strip) {
  padding: 4px 8px 5px;
  /* 2026-05-15 v9: более квадратный дефолт (user: «по умолчанию заметки более квадратными»).
     min-height даёт боксу высоту даже для коротких заметок → не плоская широкая полоска.
     Ширина createX-баблов = 150px (placeFloatingBubble squareW) → итог ≈ квадратный. */
  min-height: 60px;
}
.margin-note-saved--floating:not(.is-strip) .margin-note-saved__meta {
  display: none;
}
.margin-note-saved--floating:not(.is-strip) .margin-note-saved__translation {
  display: none;
}
.margin-note-saved--floating:not(.is-strip) .margin-note-saved__body {
  display: -webkit-box;
  -webkit-line-clamp: 3;       /* 2026-05-15 v9: 1→3 строки — бабл выше = более квадратный */
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
  line-height: 1.3;
}
/* Hover/focus-within → разворачиваем: показываем всё, восстанавливаем padding,
   bumping z-index чтобы overlay'ить соседние bubbles при stacking offset 76px. */
.margin-note-saved--floating:not(.is-strip):hover,
.margin-note-saved--floating:not(.is-strip):focus-within {
  padding: 8px 10px;
  z-index: 110;
}
.margin-note-saved--floating:not(.is-strip):hover .margin-note-saved__meta,
.margin-note-saved--floating:not(.is-strip):focus-within .margin-note-saved__meta {
  display: block;
}
.margin-note-saved--floating:not(.is-strip):hover .margin-note-saved__body,
.margin-note-saved--floating:not(.is-strip):focus-within .margin-note-saved__body {
  display: block;
  -webkit-line-clamp: unset;
  overflow: visible;
  text-overflow: unset;
  font-size: 12.5px;
  line-height: 1.4;
}
.margin-note-saved--floating:not(.is-strip):hover .margin-note-saved__translation,
.margin-note-saved--floating:not(.is-strip):focus-within .margin-note-saved__translation {
  display: flex;
}

/* ============================================================
   BUBBLE REVEAL ANIMATION — 3 режима (источник: web/animations-demo.html, блок .summary)
   Setting: html[data-bubble-anim]. Default = scale-corner (B2, ★ DEFAULT в demo).
   Переопределяет transform/transition у .margin-note-saved--floating:not(.is-strip)
   (выше специфичностью: html[attr] .class:not()). Compact-content раскрытие
   (meta/body/translation на hover) — общее, его НЕ трогаем.
   2026-05-15 v9: реализованы B2/B10/B11 по прямой инструкции user
   («баблы всё ещё не такой анимации как просила, сделай как в инструкциях»).
   ============================================================ */

/* --- B2 «Scale from corner» (★ DEFAULT) — бабл вырастает из угла с лёгкой пружиной --- */
html[data-bubble-anim="scale-corner"] .margin-note-saved--floating:not(.is-strip) {
  width: var(--bubble-w, 240px);
  transform: scale(0.5);
  transform-origin: right center;
  transition: transform 280ms cubic-bezier(.34, 1.2, .64, 1), padding 180ms ease;
}
html[data-bubble-anim="scale-corner"] .margin-note-saved--floating:not(.is-strip):not(.is-right) {
  transform-origin: left center;
}
html[data-bubble-anim="scale-corner"] .margin-note-saved--floating:not(.is-strip):hover,
html[data-bubble-anim="scale-corner"] .margin-note-saved--floating:not(.is-strip):focus-within {
  transform: scale(1);
}

/* --- B10 «Slide-in from edge» + B11 «Slide → drawer down» — УДАЛЕНЫ 2026-05-15 v10 ---
   ПРИЧИНА: `transform: translateX()` в RESTING-состоянии двигает визуальный бокс (=
   зону hover) ПРОЧЬ от курсора → hover теряется → бокс возвращается → hover снова →
   бесконечное ДРОЖАНИЕ. User (дважды): «они начинают дрожать под мышкой когда наводишь
   с краю, фокус теряется потом возвращается — некрасивое дрожание».
   Плюс B11: user — «эффект "влево выехало и вниз развернулось" ТОЛЬКО для самого сжатого
   варианта (strip, приклеенного к боку когда нет полей)». Это и есть `.is-strip:hover`
   (ниже, отдельный intrinsic cascade) — НЕ общая bubble-anim опция.
   Flicker-free режимы: scale-corner (B2, transform-origin фиксирован) + iris (mask,
   не двигает бокс). translateX-режимы как пользовательский выбор — нельзя. */

/* --- Iris «звезда/сердечко» — контент проявляется через расширяющуюся ФИГУРУ ---
   2026-05-15 v10: режим из demo B7 («Iris»), но фигура НЕ круг (user: «форма должна быть
   как звезда сердечко или звездочка»). Реализация — CSS mask + SVG-фигура; mask-size
   плавно анимируется small→huge. Фигура берётся из настройки «глиф»: star (default) или
   heart (html[data-glyph]). FLICKER-FREE: mask НЕ блокирует pointer-events → зона hover =
   всегда полный rect бабла, не двигается → дрожания нет. */
html[data-bubble-anim="iris"] .margin-note-saved--floating:not(.is-strip) {
  width: var(--bubble-w, 240px);
  transform: none;
  -webkit-mask-repeat: no-repeat;          mask-repeat: no-repeat;
  -webkit-mask-position: 18px center;      mask-position: 18px center;
  -webkit-mask-size: 30px;                 mask-size: 30px;
  /* default-фигура — звезда */
  -webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2l2.4 7.4h7.8l-6.3 4.6 2.4 7.4-6.3-4.6-6.3 4.6 2.4-7.4-6.3-4.6h7.8z" fill="black"/></svg>');
          mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2l2.4 7.4h7.8l-6.3 4.6 2.4 7.4-6.3-4.6-6.3 4.6 2.4-7.4-6.3-4.6h7.8z" fill="black"/></svg>');
  transition: -webkit-mask-size 320ms cubic-bezier(.4, 0, .2, 1),
              mask-size 320ms cubic-bezier(.4, 0, .2, 1),
              padding 180ms ease;
}
/* фигура = сердечко, если глиф настроен на heart */
html[data-bubble-anim="iris"][data-glyph="heart"] .margin-note-saved--floating:not(.is-strip) {
  -webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="black"/></svg>');
          mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="black"/></svg>');
}
html[data-bubble-anim="iris"] .margin-note-saved--floating:not(.is-strip):hover,
html[data-bubble-anim="iris"] .margin-note-saved--floating:not(.is-strip):focus-within {
  transform: none;
  -webkit-mask-size: 640px;                mask-size: 640px;
}

/* 2026-05-13 v2: strip mode — тонкая вертикальная цветная полоска при узком viewport.
   User: "когда мало места плашки должны стать полосочками разноцветными тоненькими".
   Resting: 8px wide × 28px tall. Hover/focus → expand 240px in width LEFTWARD в content area. */
.margin-note-saved--floating.is-strip {
  /* v44 SINGLE SOURCE OF TRUTH (Agent A finding, conf 95%): магические числа
     v40-v42 (`width: 9px`, `transform: -231px`, `PROTRUSION_LEFT: 9` в JS) были
     3 независимыми literals для одной величины — каждое изменение width
     требовало пересчёта transform вручную, отсюда 13 итераций и репро-баги.
     Теперь width через CSS var `--strip-w`, hover transform — calc от него.
     JS читает `--strip-w` через getComputedStyle (не дублирует число). */
  --strip-w: 9px;          /* ← единственный источник width; change once, всё пересчитается */
  --strip-hover-w: 240px;  /* expanded bubble width */
  width: var(--strip-w);
  outline: none;
  max-height: 36px;
  min-height: 0;
  padding: 0;
  background: var(--bm-cur, var(--bm-1));
  border: 0;
  border-radius: 2px;
  box-shadow: -1px 0 3px color-mix(in oklab, var(--ink) 10%, transparent);
  overflow: hidden;
  cursor: pointer;
  z-index: 200;
  /* CLOSE-анимация (курсор уходит) — зеркало OPEN: сначала сворачивается ВВЕРХ
     (height/визуал, 0-240ms), ПОТОМ уезжает вправо к стенке (width/transform,
     200-460ms). OPEN-анимация (2 фазы) — на :hover ниже. */
  transition:
    max-height    240ms cubic-bezier(.4, 0, .2, 1),
    min-height    240ms cubic-bezier(.4, 0, .2, 1),
    padding       200ms ease,
    background    200ms ease,
    border-radius 200ms ease,
    border-color  200ms ease,
    width      260ms cubic-bezier(.4, 0, .2, 1) 200ms,
    transform  260ms cubic-bezier(.4, 0, .2, 1) 200ms;
}
.margin-note-saved--floating.is-strip > * {
  opacity: 0;
  pointer-events: none;
  transition: opacity 80ms ease;
}
.margin-note-saved--floating.is-strip:hover,
.margin-note-saved--floating.is-strip:focus-within {
  width: var(--strip-hover-w);
  /* 320px ≈ 16 строк — потолок max-height; типичный контент ~80-180px.
     overflow:hidden клампит экстремально длинные заметки (норма для маржиналии). */
  max-height: 320px;
  min-height: 60px;
  background: var(--surface);
  /* v43: чёрный outline убран — var(--border-soft) в dark themes
     выглядит как чёрная рамка. Оставляем только border-left colored accent. */
  border: 0;
  border-left: 4px solid var(--bm-cur, var(--bm-1));
  padding: 8px 10px;
  border-radius: 6px 2px 2px 6px;
  /* v44 SINGLE SOURCE OF TRUTH: transform = -(hover_w - rest_w).
     Anchor right edge на resting right (panel.left). При любом изменении
     --strip-w или --strip-hover-w transform пересчитывается АВТОМАТИЧЕСКИ. */
  transform: translateX(calc(var(--strip-w) - var(--strip-hover-w)));
  /* v35 hover z-index 205 — top-most над panel-expanded overlay (380px в strip-media). */
  z-index: 205;
  /* OPEN-анимация — 2 ФАЗЫ (user: «выезжать ВЛЕВО, ПОТОМ вниз раскрываться»):
     ФАЗА 1 (0-300ms): width+transform — полоска выезжает ВЛЕВО, ОСТАВАЯСЬ тонкой
       (max-height ещё 28px, height не трогается → НЕТ «скачка» в начале).
     ФАЗА 2 (260-560ms): max-height/min-height/padding/фон/рамка — раскрывается
       ВНИЗ, полоска превращается в бабл. transition-delay:260ms разводит фазы.
     Только для .is-strip (состояние «прижат к стенке») — :not(.is-strip) баблы
     имеют свои анимации (scale-corner / iris). */
  transition:
    width      300ms cubic-bezier(.4, 0, .2, 1),
    transform  300ms cubic-bezier(.4, 0, .2, 1),
    max-height    300ms cubic-bezier(.4, 0, .2, 1) 260ms,
    min-height    300ms cubic-bezier(.4, 0, .2, 1) 260ms,
    padding       240ms ease 260ms,
    background    240ms ease 260ms,
    border-radius 240ms ease 260ms,
    border-color  240ms ease 260ms;
}
/* 2026-05-17 v41 (user: «вижу чёрную обводку когда бабл разворачивается,
   убери её»): :focus / :focus-within / :hover на expanded bubble и на
   internal interactive children (delete button) — ВСЕ outline:none. */
.margin-note-saved--floating.is-strip:hover,
.margin-note-saved--floating.is-strip:focus-within,
.margin-note-saved--floating.is-strip:focus,
.margin-note-saved--floating.is-strip *:focus,
.margin-note-saved--floating.is-strip *:focus-visible {
  outline: none;
}
/* 2026-05-13 v8: strip mode после expand — delete button смещается слева
   (align-self default = flex-start) чтоб не уезжал к real-position right edge. */
.margin-note-saved--floating.is-strip:hover .margin-note-saved__delete,
.margin-note-saved--floating.is-strip:focus-within .margin-note-saved__delete {
  align-self: flex-start;
}
.margin-note-saved--floating.is-strip:hover > *,
.margin-note-saved--floating.is-strip:focus-within > * {
  opacity: 1;
  pointer-events: auto;
  /* контент появляется ВО ВРЕМЯ фазы 2 (раскрытие вниз), не раньше */
  transition: opacity 200ms ease 340ms;
}
.margin-note-saved--floating.is-strip .margin-note-saved__delete {
  opacity: 0;      /* hidden даже когда expand'нуто — появляется только на hover bubble */
}
.margin-note-saved--floating.is-strip:hover .margin-note-saved__delete {
  opacity: 1;
}
.margin-note-saved__meta {
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 9px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bm-cur, var(--bm-1));
  margin-bottom: 3px;
}
.margin-note-saved__body {
  font-family: var(--font-body, 'Lora', Georgia, serif);
  font-style: italic;
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--ink);
}

/* 2026-05-12 v4: bookmark variant — compact, без body. Just метка + меньше padding. */
.margin-note-saved--bookmark {
  padding: 4px 8px 5px 12px;
}
.margin-note-saved--bookmark .margin-note-saved__meta {
  margin-bottom: 0;
}

/* 2026-05-13 v6: Delete button now labeled "Удалить" text (was ×). Positioned bottom-right
   inside bubble (was top-right corner). User: "крестик удали — добавь кнопку удалить". */
.margin-note-saved__delete {
  align-self: flex-end;
  margin-top: 4px;
  background: transparent;
  border: 1px solid var(--border-soft);
  color: var(--ink-faint);
  font-family: var(--font-ui, 'Inter', sans-serif);
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 4px 9px;
  cursor: pointer;
  border-radius: 4px;
  opacity: 0;
  transition: opacity 120ms ease, color 100ms ease, background 100ms ease, border-color 100ms ease;
}
.margin-note-saved:hover .margin-note-saved__delete,
.margin-note-saved__delete:focus-visible {
  opacity: 1;
}
.margin-note-saved__delete:hover {
  color: var(--ink);
  background: color-mix(in oklab, var(--bm-cur, var(--bm-1)) 10%, transparent);
  border-color: var(--bm-cur, var(--bm-1));
}

/* Translation row — appears below body, italic gray placeholder text.
   Will populate via async stub VLS.translateUserText() when wired.
   2026-05-13 v8: flex-wrap + min-width:0 на text span чтобы narrow bubbles не overflow. */
.margin-note-saved__translation {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px dashed color-mix(in oklab, var(--bm-cur, var(--bm-1)) 30%, transparent);
  font-family: var(--font-body, 'Lora', Georgia, serif);
  font-style: italic;
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--ink-soft);
}
.margin-note-saved__translation-text {
  flex: 1;
  min-width: 0;
  word-break: break-word;
}
.margin-note-saved__translation-icon {
  color: color-mix(in oklab, var(--bm-cur, var(--bm-1)) 70%, transparent);
  font-style: normal;
  font-weight: 500;
  flex-shrink: 0;
}
.margin-note-saved__translation-text.is-placeholder {
  font-size: 10px;
  opacity: 0.6;
  font-family: var(--font-mono, monospace);
  font-style: normal;
}

/* 2026-05-13 v8: duplicate `.margin-note-saved--floating` ruleset merged ABOVE
   into primary block. Этот block deleted чтобы избежать confusion. */

/* ===== Bookmarks panel (Бусины Горизонт A2) =====
   Horizontal bar над reader-content. На bar — colored heart dots,
   расположенные по left% (paragraph_start / total_paras в главе).
   Hover на dot → bubble с note text + jump button.
   Current (.cur) — bigger + animated wobble. */
/* 2026-05-11 v5: panel positioned ABSOLUTELY на topbar bottom border (existing line).
   Hearts накладываются прямо на ту линию, не создаём новую. Panel = zero-height
   overlay в нижней части topbar (position: absolute; bottom: 0). */
.bookmarks-panel {
  position: absolute;            /* relative to .topbar (which is position: relative) */
  bottom: -1px;                  /* центр сердечка точно на borderline */
  left: 0;
  right: 0;
  width: 100%;
  height: 0;
  margin: 0;
  padding: 0 24px;
  pointer-events: none;          /* dots themselves restore pointer-events */
  z-index: 1;
}
.bookmarks-panel.is-empty {
  display: none;                 /* нет заметок → не показываем (но line topbar остаётся) */
}
.bookmarks-panel__bar {
  position: relative;
  height: 0;
  max-width: 760px;
  margin: 0 auto;
  pointer-events: none;
}
/* v72 (2026-05-17): invisible hit zone через bar::before — расширяет mousemove
   target до полной ширины бара ±12px по вертикали (heart area + buffer).
   JS handler tracks closest heart по horizontal distance и добавляет .is-near.
   pointer-events: auto перебивает inherited none с bar. z-index 0 под heart (1).
   Только для dock/fanout/static modes — sticky/click имеют свою логику. */
.bookmarks-panel__bar::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -12px;
  height: 24px;
  pointer-events: auto;
  z-index: 0;
}
/* 2026-05-16 v24: удалена мёртвая переменная --cluster-spread (нигде не
   читалась после v9-рефакторинга). Cluster expansion реализован через
   precomputed --ox/--oy в JS (renderBookmarksPanel), используется в
   mode-секции ниже (html[data-heart-anim]). */
/* 2026-05-11 v6: bookmark glyph через CSS var --glyph-char (heart / star).
   Was: CSS mask с heart-only SVG path. Now: ::before content → меняется по data-glyph. */
.bookmarks-panel__bar i {
  position: absolute;
  /* v56 (2026-05-17): top -11 → -8. User: «перемести их на строчку где
     я показала» — hearts должны сидеть **ровно на divider line** topbar,
     не выше. С heart 16×16 + bar.bottom=-1 (от topbar.bottom), top:-8 даёт
     heart-center = topbar.bottom - 1 - 8 + 8 = topbar.bottom - 1 = на divider. */
  top: -8px;
  width: 16px;
  height: 16px;
  line-height: 16px;
  text-align: center;
  pointer-events: auto;
  color: var(--c, var(--bm-1));
  background: transparent;
  font-size: 16px;
  /* v27: crisper glyph rendering — antialiased subpixel, kerning hints. */
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  cursor: pointer;
  /* v55: убраны transition transform + transform-origin — hearts static.
     Оставляем только filter transition для drop-shadow на focus (subtle keyboard
     hint). z-index baseline 1, hover поднимает до 3 для popover stacking. */
  transition: filter 160ms ease;
  z-index: 1;
}
.bookmarks-panel__bar i::before {
  content: var(--glyph-char, "♥");
}
/* v37 diagnostic confirmed:
   - Cache fresh (user saw lime green hearts → CSS landed)
   - Frames appear ONLY on hover state — это subpixel rendering между spread'нутыми hearts
   - В resting state hearts overlap в одной точке (v34) → нет рамок
   - На hover hearts разлетаются на --ox/--oy → cream topbar bg между ними looks like «frames»
   This is correct fan-out behavior, not bug. v37 reverted. */
/* 2026-05-16 v36 (FINAL — после v35 yellow/red/blue diagnostic):
   User's «рамки вокруг каждого heart'а» — это `::after` 32×28 box, который при
   close cluster overlap создавал visible cream «boxes» edges на topbar bg
   (даже с transparent bg — где-то sub-pixel rendering или Z-stacking).

   v31 ablation тест (`display: none`) был incorrect verdict — possibly user видел
   stale cache state. v35 diagnostic (yellow/red/blue !important) подтвердил что
   `::after` ЕСТЬ visible bounding box per heart.

   v36 решение: убрать ::after полностью. Sweep continuity less important чем
   clean visual state. Если sweep схлопывается — user может всё равно hover
   каждое heart индивидуально и видеть arc на нём. */
.bookmarks-panel__bar i::after {
  display: none;
}
.bookmarks-panel__bar i:hover {
  /* Базовый i:hover — только z-index подъём. Визуальный эффект задаёт mode-секция ниже. */
  z-index: 3;
}
/* 2026-05-17 v37: убрана rectangular рамка вокруг сердечка. User: «избавимся
   от рамочки вокруг сердечка». OLD (v24): outline 2px solid + offset 3px +
   border-radius:50% — на square 16x16 element outline-offset>0 рендерился как
   square halo (border-radius применяется к element box, но outline-offset
   shifts outline в outer rect, который НЕ ограничен radius элемента).
   NEW: `outline:none` для всех focus state + `filter: drop-shadow` на
   :focus-visible — drop-shadow следует ALPHA-каналу глифа ♥ (heart shape),
   а не box → круглый glow вокруг сердца без прямоугольника.
   Keyboard accessibility preserved через subtle color glow. */
.bookmarks-panel__bar i:focus,
.bookmarks-panel__bar i:focus-visible {
  outline: none;
}
.bookmarks-panel__bar i:focus-visible {
  filter: drop-shadow(0 0 3px var(--c, var(--bm-1)))
          drop-shadow(0 0 6px var(--c, var(--bm-1)));
  z-index: 4;
}

/* ============================================================
   v55 (2026-05-17): УБРАНЫ ВСЕ HOVER-АНИМАЦИИ HEARTS — user feedback
   «убери снизу все возможные стили анимаций и расположи где они должны идти
   по полоске разделения разделов, оставь только всплывающий бабл при наведении».

   Что было раньше (v9-v54):
   - 10 hover modes через html[data-heart-anim=*] (fanout-down/up/linear/dock/
     lift/spotlight/gap-scale/tooltip/stack-badge/wave) — 280+ строк CSS.
   - .is-clustered staircase indicator (v23-v34 evolving).
   - .cur wobble animation (1.6s pulse infinite).
   - reduced-motion override для всех 10 modes.

   Что осталось (минимум для bookmark navigation):
   - Hearts строго static на topbar divider line (top:-11, no transform).
   - i:hover — только z-index:3 для proper popover stacking (JS-rendered
     .bookmarks-panel__bubble показывается через mouseover event handler).
   - i:focus-visible — drop-shadow glow для keyboard navigation.
   - .cur — статичный slightly larger heart (без animation), цвет = note.

   data-heart-anim attribute keeps значение в localStorage для backward compat,
   но НЕ имеет CSS effect. applyHeartAnim() осталась no-op в reader.js.
   ============================================================ */
.bookmarks-panel__bar i.cur {
  width: 17px;
  height: 17px;
  top: -8px;
  font-size: 17px;
  line-height: 17px;
  z-index: 2;
  filter: drop-shadow(0 0 3px var(--c, var(--bm-1)));
}

/* All heart hover-animation modes (10 variants + .is-clustered + wobble + reduced-motion override) удалены в v55 — см. главный comment block выше. */

/* ============================================================
   v65 HEART ANIMATION MODES (user-selectable через settings):
   - dock — Mac magnify под cursor (peak + 2 falloff levels) [default]
   - fanout — symmetric arc вниз (peak + 2 sides)

   v65 architecture (after v60→v64 cluster-zone wrapping failed):
   Hearts остаются direct children of bar (no wrapper). Both modes
   use SIBLING SELECTORS (+ i, :has(+ i:hover)) для neighbor reactions.
   Universal pattern — works для любого heart density без cluster
   detection.

   Switched через html[data-heart-anim="..."]. applyHeartAnim() в reader.js
   persists в localStorage. UI buttons в reader.html settings-panel
   group--hearts.
   ============================================================ */
.bookmarks-panel__bar > i {
  transition: transform 200ms cubic-bezier(.4, 0, .2, 1),
              filter 160ms ease;
}

/* ============================================================
   MODE: dock (default) — Mac magnify под cursor.
   v68 STABLE FOCUS (user: «ускользает из под мышки всё равно»):
   peak heart NOT translateY → остаётся ровно под cursor, не уплывает.
   Только scale. Neighbors compensate visually через translateX spread.
   ============================================================ */
/* PEAK: hovered heart — magnify ONLY, NO translate → cursor focus STABLE
   v72: .is-near добавлен — JS tracks closest heart по mousemove через всю ширину
   бара (см. bar::before hit zone). Когда курсор между hearts — ближайший
   получает peak treatment, magnify «следует» за курсором. */
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:hover,
html[data-heart-anim="dock"] .bookmarks-panel__bar > i.is-near,
html:not([data-heart-anim]) .bookmarks-panel__bar > i:hover,
html:not([data-heart-anim]) .bookmarks-panel__bar > i.is-near {
  transform: scale(1.8);
  z-index: 6;
}
/* v70 (user: «не могу ниже переместить мышку на нижнее сердечко»):
   pseudo hit zone УМЕНЬШЕН (inset -12 → -4). Pseudo scales с parent
   scale(1.8) → 38*1.8 = 68px hit area был, блокировал neighbors.
   Теперь 22*1.8 = 40px — небольшой jitter buffer (~3px beyond visible
   при scale 1.8), не блокирует neighbors at +12px gap. */
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:hover::after,
html[data-heart-anim="dock"] .bookmarks-panel__bar > i.is-near::after,
html:not([data-heart-anim]) .bookmarks-panel__bar > i:hover::after,
html:not([data-heart-anim]) .bookmarks-panel__bar > i.is-near::after,
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:hover::after,
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i.is-near::after {
  content: '';
  display: block;
  position: absolute;
  inset: -4px;          /* small jitter buffer only */
  pointer-events: auto;
  background: transparent;
}
/* v70: neighbors NO translateY (user wants horizontal navigation to соседям —
   они должны быть aligned vertically с peak, не drops down to inaccessible
   positions). Only translateX spread + scale. */
/* ±1 NEIGHBOR — magnify + push в стороны (no drop). v72: .is-near rules paired. */
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:hover + i,
html[data-heart-anim="dock"] .bookmarks-panel__bar > i.is-near + i,
html:not([data-heart-anim]) .bookmarks-panel__bar > i:hover + i,
html:not([data-heart-anim]) .bookmarks-panel__bar > i.is-near + i {
  transform: translateX(10px) scale(1.25);
  z-index: 5;
}
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:has(+ i:hover),
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:has(+ i.is-near),
html:not([data-heart-anim]) .bookmarks-panel__bar > i:has(+ i:hover),
html:not([data-heart-anim]) .bookmarks-panel__bar > i:has(+ i.is-near) {
  transform: translateX(-10px) scale(1.25);
  z-index: 5;
}
/* ±2 — smaller magnify + more push */
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:hover + i + i,
html[data-heart-anim="dock"] .bookmarks-panel__bar > i.is-near + i + i,
html:not([data-heart-anim]) .bookmarks-panel__bar > i:hover + i + i,
html:not([data-heart-anim]) .bookmarks-panel__bar > i.is-near + i + i {
  transform: translateX(16px) scale(1.1);
  z-index: 4;
}
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:has(+ i + i:hover),
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:has(+ i + i.is-near),
html:not([data-heart-anim]) .bookmarks-panel__bar > i:has(+ i + i:hover),
html:not([data-heart-anim]) .bookmarks-panel__bar > i:has(+ i + i.is-near) {
  transform: translateX(-16px) scale(1.1);
  z-index: 4;
}
/* ±3 — tiny fade */
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:hover + i + i + i,
html[data-heart-anim="dock"] .bookmarks-panel__bar > i.is-near + i + i + i,
html:not([data-heart-anim]) .bookmarks-panel__bar > i:hover + i + i + i,
html:not([data-heart-anim]) .bookmarks-panel__bar > i.is-near + i + i + i {
  transform: translateX(20px) scale(1.03);
  z-index: 3;
}
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:has(+ i + i + i:hover),
html[data-heart-anim="dock"] .bookmarks-panel__bar > i:has(+ i + i + i.is-near),
html:not([data-heart-anim]) .bookmarks-panel__bar > i:has(+ i + i + i:hover),
html:not([data-heart-anim]) .bookmarks-panel__bar > i:has(+ i + i + i.is-near) {
  transform: translateX(-20px) scale(1.03);
  z-index: 3;
}

/* ============================================================
   MODE: static — NO movement, только glow on hover.
   v71: maximum stability — heart НИКОГДА не двигается, только filter
   drop-shadow + brightness. Cursor никогда не теряет heart.
   ============================================================ */
html[data-heart-anim="static"] .bookmarks-panel__bar > i:hover,
html[data-heart-anim="static"] .bookmarks-panel__bar > i.is-near {
  filter: drop-shadow(0 0 6px var(--c, var(--bm-1))) brightness(1.2);
  z-index: 6;
  /* NO transform — heart stays exactly where cursor lands */
}
/* No neighbor reactions для maximum focus stability */

/* ============================================================
   MODE: sticky — animation preserved + JS locks heart на hover.
   v71: JS adds `.is-sticky-focus` class когда heart hovered.
   Siblings get pointer-events:none пока locked — cursor jitter не
   теряет focus. Visual = same as dock mode peak.
   ============================================================ */
html[data-heart-anim="sticky"] .bookmarks-panel__bar > i:hover,
html[data-heart-anim="sticky"] .bookmarks-panel__bar > i.is-sticky-focus {
  transform: scale(2);
  filter: drop-shadow(0 0 8px var(--c, var(--bm-1)));
  z-index: 6;
}
/* Neighbors push aside slightly (visual context) */
html[data-heart-anim="sticky"] .bookmarks-panel__bar > i.is-sticky-focus + i {
  transform: translateX(10px) scale(1.2);
  z-index: 5;
}
html[data-heart-anim="sticky"] .bookmarks-panel__bar > i:has(+ i.is-sticky-focus) {
  transform: translateX(-10px) scale(1.2);
  z-index: 5;
}

/* ============================================================
   MODE: click — bubble shows ТОЛЬКО при click, не hover.
   v71: hover = subtle highlight, click = bubble (sticky, не закрывается
   при mouseout). JS bubble handler switches behavior based на mode.
   ============================================================ */
html[data-heart-anim="click"] .bookmarks-panel__bar > i:hover {
  filter: brightness(1.15);
  z-index: 5;
}
html[data-heart-anim="click"] .bookmarks-panel__bar > i.is-clicked {
  transform: scale(1.6);
  filter: drop-shadow(0 0 6px var(--c, var(--bm-1)));
  z-index: 6;
}

/* ============================================================
   MODE: fanout — symmetric arc ВНИЗ via sibling selectors.
   v66: extended ±3 fade level для smooth edge transition.
   ============================================================ */
/* Peak (hovered heart). v72: .is-near paired со всеми ring'ами. */
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:hover,
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i.is-near {
  transform: translate(0, 22px) scale(1.2);
  z-index: 6;
}
/* ±1 — mid-arc */
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:hover + i,
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i.is-near + i {
  transform: translate(10px, 18px) scale(1.1);
  z-index: 5;
}
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:has(+ i:hover),
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:has(+ i.is-near) {
  transform: translate(-10px, 18px) scale(1.1);
  z-index: 5;
}
/* ±2 — outer arc */
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:hover + i + i,
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i.is-near + i + i {
  transform: translate(22px, 10px) scale(1.05);
  z-index: 4;
}
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:has(+ i + i:hover),
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:has(+ i + i.is-near) {
  transform: translate(-22px, 10px) scale(1.05);
  z-index: 4;
}
/* ±3 — fade edges (v66 smooth falloff) */
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:hover + i + i + i,
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i.is-near + i + i + i {
  transform: translate(32px, 4px) scale(1.02);
  z-index: 3;
}
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:has(+ i + i + i:hover),
html[data-heart-anim="fanout"] .bookmarks-panel__bar > i:has(+ i + i + i.is-near) {
  transform: translate(-32px, 4px) scale(1.02);
  z-index: 3;
}

/* Bubble — appears on hover, anchored BELOW the heart.
   v68 (after STABLE FOCUS fix — peak heart НЕ translates): peak heart
   bottom = bar.top + scale_extension только (14×1.8/2 = 12.6). Bubble
   может быть ближе: top 40 → 22 (peak bottom 12.6 + 10px margin). */
.bookmarks-panel__bubble {
  position: absolute;
  top: calc(100% + 30px);               /* v72.2: 27→30 (user: «ещё на 3px ниже») — итого +8px от v72 baseline 22px */
  transform: translateX(-50%);
  max-width: 260px;
  padding: 7px 11px;
  background: var(--surface);
  border: 1px solid var(--border-soft); /* softer than --border — fixes «рамочки» complaint */
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop);
  font-family: var(--font-body);
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--ink);
  z-index: 10;
  pointer-events: auto;
  /* Slide-down fade-in (200ms): bubble «оседает» снизу-вверх, не появляется резко */
  animation: bookmarkBubbleIn 200ms ease-out;
}
[data-theme="mist"] .bookmarks-panel__bubble,
[data-theme="sumi"] .bookmarks-panel__bubble,
.theme-mist .bookmarks-panel__bubble,
.theme-sumi .bookmarks-panel__bubble {
  box-shadow: var(--shadow-pop-dark, 0 6px 16px rgba(0,0,0,0.5));
}
@keyframes bookmarkBubbleIn {
  from { opacity: 0; transform: translateX(-50%) translateY(-4px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.bookmarks-panel__bubble-loc {
  /* 2026-05-15 v15: book-wide hearts — заголовок главы заметки. Маленький
     uppercase-tag в самом верху bubble (даёт контекст даже без hover'а на title).
     Variant `.is-other` (заметка из другой главы) — accent-цвет + ↗ маркер,
     намекает что click сделает chapter-jump. */
  display: block;
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-style: normal;
  font-size: 9.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 4px;
}
.bookmarks-panel__bubble-loc.is-other {
  color: var(--c, var(--bm-1));
  font-weight: 600;
}
.bookmarks-panel__bubble-body {
  display: block;
  margin-bottom: 6px;
  color: var(--ink);
}
.bookmarks-panel__bubble-jump {
  display: inline-block;
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c, var(--bm-1));
  text-decoration: none;
  cursor: pointer;
}
.bookmarks-panel__bubble-jump:hover {
  text-decoration: underline;
}

/* Empty state — short hint when no notes yet */
.bookmarks-panel.is-empty .bookmarks-panel__bar {
  background: var(--border-soft);
}
.bookmarks-panel__empty-hint {
  font-family: var(--font-body, 'Lora', Georgia, serif);
  font-style: italic;
  font-size: 11px;
  color: var(--ink-faint);
  letter-spacing: 0;
  text-transform: none;
  text-align: left;
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  .reader p.para.is-marking,
  .reader p.para.has-note { transition: none; }
  .margin-star { transition: none; }
  .margin-note-input { animation: none; }
  .bookmarks-panel__bubble { animation: none; }
  /* v65: vestibular accessibility — disable все heart transforms. */
  .bookmarks-panel__bar > i {
    transition: filter 160ms ease !important;
    transform: none !important;
  }
}

/* Disable legacy v1 margin-mark when v2 is active (data-mark-v2 on .reader).
   Kept v1 CSS above for backward compat; new model uses .margin-zone instead. */
.reader[data-mark-v2="on"] .margin-mark { display: none; }
.reader[data-mark-v2="on"] .margin-note { display: none; }

/* Reader-main needs position:relative для margin-zone overlay alignment */
.reader-main { position: relative; }

/* ============================================================
   Page-nav widget — 4 variants (B5-1 / B1 / C1 / C3).
   Reference: Design книги/04-page-nav/a5-b5-sizes.html + page-nav-3-variants.html.
   Widget живёт в words-panel над sidebar word grid. JS render'ит структуру
   соответствующего варианта; CSS определяет только styling.

   Heart glyph — CSS mask trick: background-color задаёт цвет, mask-image (SVG path)
   задаёт силуэт. Так heart filled/empty переключается одной строкой.
   ============================================================ */
.page-nav-widget {
  /* 2026-05-11 v3: дефолтные стили (если widget где-то ещё используется),
     для topbar — см. .page-nav-widget--topbar ниже (right-aligned, compact). */
  padding: 8px 8px 12px;
  margin-bottom: 8px;
  font-family: var(--font-ui);
  color: var(--ink);
  --pn-heart-size: 11px;
  display: flex;
  justify-content: flex-start;
}

/* ===== Topbar variant — widget переехал в top-right corner (2026-05-11 v3).
   Compact inline, right-aligned text, dropdown opens leftward. */
.page-nav-widget--topbar {
  padding: 0;
  margin: 0 14px 0 0;                 /* зазор от menu trigger справа */
  display: inline-flex;
  align-items: center;
  font-size: 11px;
}
.page-nav-widget--topbar .pn-b51 {
  align-items: center;
}
.page-nav-widget--topbar .pn-b51-meta {
  align-items: flex-end;              /* right-align flex children */
  text-align: right;
  gap: 1px;                           /* compactier vertical spacing */
}
.page-nav-widget--topbar .pn-b51-meta .k,
.page-nav-widget--topbar .pn-b51-meta .v,
.page-nav-widget--topbar .pn-b51-meta .pg {
  text-align: right;
  white-space: nowrap;
}
/* Dropdown — anchor RIGHT instead of LEFT (иначе вылезает за viewport edge) */
.page-nav-widget--topbar .pn-chapter-list {
  left: auto;
  right: 0;
  max-height: 320px;
}
/* C1 variant inside topbar — компактнее dial */
.page-nav-widget--topbar .pn-c1 {
  gap: 8px;
}
.page-nav-widget--topbar .pn-c1-meta {
  align-items: flex-end;
}
/* C3 variant — same logic */
.page-nav-widget--topbar .pn-c3 {
  gap: 8px;
}
.page-nav-widget--topbar .pn-c3-legend {
  align-items: flex-end;
}

/* 2026-05-16 v24: .pn-heart переделан с SVG-mask + background-color на text-glyph
   с ::before { content: var(--glyph-char) } — тот же паттерн что
   .bookmarks-panel__bar i. Зачем: чтобы те же hover-режимы html[data-heart-anim]
   применялись и к chapter sliding window (page-nav b1), а не только к закладкам.
   Также автоматически подхватывает glyph-переключатель ♥/✦. */
.page-nav-widget .pn-heart {
  display: inline-block;
  width: var(--pn-heart-size);
  height: var(--pn-heart-size);
  line-height: var(--pn-heart-size);
  font-size: var(--pn-heart-size);
  text-align: center;
  color: var(--ink-faint);
  background: transparent;
  cursor: pointer;
  user-select: none;
  position: relative;
  transform-origin: 50% 100%;
  transition: color 120ms, transform 220ms cubic-bezier(0, 0, .2, 1), filter 160ms ease;
}
.page-nav-widget .pn-heart::before {
  content: var(--glyph-char, "♥");
}
.page-nav-widget .pn-heart:hover { color: var(--ink-soft); z-index: 3; }
.page-nav-widget .pn-heart.cur { color: var(--ink); }
.page-nav-widget .pn-heart:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 50%;
  z-index: 4;
}

/* ============================================================
   page-nav heart-anim modes — подмножество (5 режимов), которое имеет смысл
   на flex-row без cluster math (--ox/--oy не выставляется в page-nav, hearts
   расположены через flex+gap). Режимы fanout/linear/stack-badge/tooltip
   требуют precomputed offsets и не имеют sensible mapping на 5-heart row,
   поэтому фоллбэчатся на default color-only behavior — это OK.
   ============================================================ */
/* dock (H3) — macOS dock magnify */
html[data-heart-anim="dock"] .page-nav-widget .pn-b1-row .pn-heart:hover {
  transform: scale(2.4) translateY(-6px);
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .18));
  z-index: 4;
}
html[data-heart-anim="dock"] .page-nav-widget .pn-b1-row .pn-heart:hover + .pn-heart,
html[data-heart-anim="dock"] .page-nav-widget .pn-b1-row .pn-heart:has(+ .pn-heart:hover) {
  transform: scale(1.5) translateY(-2px);
}

/* lift (H4) — всплытие + тень */
html[data-heart-anim="lift"] .page-nav-widget .pn-b1-row .pn-heart:hover {
  transform: translateY(-8px) scale(1.6);
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, .2));
  z-index: 4;
}

/* spotlight (H5) — соседи тускнеют */
html[data-heart-anim="spotlight"] .page-nav-widget .pn-b1-row:hover .pn-heart {
  opacity: 0.35;
  filter: grayscale(60%);
}
html[data-heart-anim="spotlight"] .page-nav-widget .pn-b1-row .pn-heart:hover {
  opacity: 1;
  filter: none;
  transform: scale(1.3);
  z-index: 4;
}

/* gap-scale (H7) — gap растёт + увеличение */
html[data-heart-anim="gap-scale"] .page-nav-widget .pn-b1-row {
  transition: gap 220ms cubic-bezier(.4, 0, .2, 1);
}
html[data-heart-anim="gap-scale"] .page-nav-widget .pn-b1-row:hover {
  gap: 18px;
}
html[data-heart-anim="gap-scale"] .page-nav-widget .pn-b1-row:hover .pn-heart {
  transform: scale(1.2);
}

/* wave (H10) — hovered поднимается, соседи через CSS sibling combinators */
html[data-heart-anim="wave"] .page-nav-widget .pn-b1-row .pn-heart:hover {
  transform: translateY(-10px) scale(1.6);
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, .2));
  z-index: 4;
}
html[data-heart-anim="wave"] .page-nav-widget .pn-b1-row .pn-heart:hover + .pn-heart,
html[data-heart-anim="wave"] .page-nav-widget .pn-b1-row .pn-heart:has(+ .pn-heart:hover) {
  transform: translateY(-5px) scale(1.2);
}
html[data-heart-anim="wave"] .page-nav-widget .pn-b1-row .pn-heart:hover + .pn-heart + .pn-heart,
html[data-heart-anim="wave"] .page-nav-widget .pn-b1-row .pn-heart:has(+ .pn-heart + .pn-heart:hover) {
  transform: translateY(-2px) scale(1.05);
}

/* Reduced motion для page-nav hearts — отключаем все 5 режимов */
@media (prefers-reduced-motion: reduce) {
  .page-nav-widget .pn-heart,
  html[data-heart-anim] .page-nav-widget .pn-b1-row .pn-heart:hover,
  html[data-heart-anim] .page-nav-widget .pn-b1-row .pn-heart:hover + .pn-heart,
  html[data-heart-anim] .page-nav-widget .pn-b1-row .pn-heart:has(+ .pn-heart:hover),
  html[data-heart-anim] .page-nav-widget .pn-b1-row .pn-heart:hover + .pn-heart + .pn-heart,
  html[data-heart-anim] .page-nav-widget .pn-b1-row .pn-heart:has(+ .pn-heart + .pn-heart:hover) {
    transform: none !important;
    transition: color 120ms, filter 160ms ease, opacity 160ms ease !important;
  }
}

/* Clickable chapter title и page number — обозначены underline-on-hover */
.page-nav-widget .pn-clickable {
  cursor: pointer;
  border-bottom: 1px dashed transparent;
  transition: border-color 120ms;
}
.page-nav-widget .pn-clickable:hover { border-bottom-color: var(--ink-muted); }

/* ===== B5-1 · meta block (2026-05-11 v2: hearts удалены, конструкция align-left) ===== */
.page-nav-widget[data-variant="b51"] .pn-b51 {
  display: flex; align-items: flex-start;
}
.page-nav-widget[data-variant="b51"] .pn-b51-meta {
  display: flex; flex-direction: column; gap: 3px;
  font-size: 11px;
  /* No border-left / padding-left — hearts больше не предшествуют meta */
}
.page-nav-widget[data-variant="b51"] .pn-b51-meta .k {
  font-family: var(--font-mono); font-size: 9px; color: var(--ink-muted);
  letter-spacing: 0.14em; text-transform: uppercase;
}
.page-nav-widget[data-variant="b51"] .pn-b51-meta .v {
  color: var(--ink); font-weight: 500;
}
.page-nav-widget[data-variant="b51"] .pn-b51-meta .pg {
  font-family: var(--font-mono); font-size: 10px; color: var(--ink-muted);
}

/* ===== B1 · vertical column hearts + page indicator ===== */
.page-nav-widget[data-variant="b1"] .pn-b1 {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.page-nav-widget[data-variant="b1"] .pn-b1-row {
  display: flex; align-items: center; gap: 10px;
}
.page-nav-widget[data-variant="b1"] .pn-b1-page {
  font-family: var(--font-mono); font-size: 10px; color: var(--ink-muted);
  letter-spacing: 0.06em;
}
.page-nav-widget[data-variant="b1"] .pn-b1-page .cur {
  color: var(--ink); font-weight: 500;
}

/* ===== C1 · ring with current chapter number inside + meta ===== */
.page-nav-widget[data-variant="c1"] .pn-c1 {
  display: flex; align-items: center; gap: 10px;
}
.page-nav-widget[data-variant="c1"] .pn-c1-dial {
  position: relative; width: 44px; height: 44px;
  flex-shrink: 0;
}
.page-nav-widget[data-variant="c1"] .pn-c1-dial .n {
  position: absolute; inset: 0; display: flex;
  align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  color: var(--ink);
}
.page-nav-widget[data-variant="c1"] .pn-c1-meta {
  display: flex; flex-direction: column; gap: 2px;
}
.page-nav-widget[data-variant="c1"] .pn-c1-meta .chap {
  font-size: 11px; color: var(--ink); font-weight: 500;
}
.page-nav-widget[data-variant="c1"] .pn-c1-meta .pg {
  font-family: var(--font-mono); font-size: 10px; color: var(--ink-muted);
}
.page-nav-widget[data-variant="c1"] .pn-arrow {
  background: transparent; border: 1px solid var(--border-soft);
  border-radius: 4px; padding: 2px 8px;
  font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted);
  cursor: pointer;
}
.page-nav-widget[data-variant="c1"] .pn-arrow:hover {
  color: var(--ink); border-color: var(--ink-muted);
}

/* ===== C3 · twin concentric rings (book + chapter progress) ===== */
.page-nav-widget[data-variant="c3"] .pn-c3 {
  display: flex; align-items: center; gap: 10px;
}
.page-nav-widget[data-variant="c3"] .pn-c3-dial {
  position: relative; width: 52px; height: 52px;
  flex-shrink: 0;
}
.page-nav-widget[data-variant="c3"] .pn-c3-legend {
  display: flex; flex-direction: column; gap: 4px;
  font-family: var(--font-mono); font-size: 9px; color: var(--ink-muted);
  letter-spacing: 0.08em; text-transform: uppercase;
}
.page-nav-widget[data-variant="c3"] .pn-c3-legend span {
  display: flex; align-items: center; gap: 5px;
}
.page-nav-widget[data-variant="c3"] .pn-c3-legend i {
  width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
}
.page-nav-widget[data-variant="c3"] .pn-c3-legend .a i { background: var(--ink); }
.page-nav-widget[data-variant="c3"] .pn-c3-legend .b i { background: var(--accent); }

/* ===== Inline chapter dropdown (using native <details>) =====
   Click on summary → expand list. Click on item → navigate + auto-close. */
.pn-chapter-dd {
  position: relative;
  display: inline-block;
}
.pn-chapter-dd > summary {
  cursor: pointer;
  list-style: none;
  outline: none;
  /* Inherits font-size/color from parent variant (.v / .chap / .a) */
}
.pn-chapter-dd > summary::-webkit-details-marker { display: none; }
.pn-chapter-dd > summary::after {
  content: " ▾";
  font-size: 8px;
  color: var(--ink-muted);
  vertical-align: middle;
}
.pn-chapter-dd[open] > summary::after { content: " ▴"; }
.pn-chapter-list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 0;
  margin: 0;
  list-style: none;
  z-index: 100;
  max-height: 280px;
  overflow-y: auto;
  min-width: 180px;
  box-shadow: var(--shadow-pop, 0 6px 16px rgba(0,0,0,0.08));
}
.pn-chapter-list li { margin: 0; }
.pn-chapter-list button {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  padding: 5px 12px;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--ink-soft);
  cursor: pointer;
  line-height: 1.3;
}
.pn-chapter-list button:hover { background: var(--hl); color: var(--ink); }
.pn-chapter-list button[aria-current="true"] {
  color: var(--ink);
  font-weight: 600;
  background: color-mix(in oklab, var(--accent) 12%, transparent);
}

/* ===== Inline page input — click label → swap to input → Enter to jump ===== */
.pn-page-form {
  display: inline-block;
  margin: 0;
}
.pn-page-input {
  width: 70px;
  font-family: var(--font-mono);
  font-size: 10px;
  padding: 2px 5px;
  border: 1px solid var(--border);
  border-radius: 3px;
  background: var(--bg);
  color: var(--ink);
  outline: none;
}
.pn-page-input:focus { border-color: var(--accent); }

/* ===== Popover style variants (2026-05-11) =====
   .reader--popover-condensed — только tr показан (минимальная подсказка)
   .reader--popover-full      — IPA + tr + POS chip (default, full info) */
.reader--popover-condensed .w__popover-ipa,
.reader--popover-condensed .w__popover-meta {
  display: none !important;       /* override base meta visibility rules в colored modes */
}
.reader--popover-full .w__popover-meta {
  /* Force-show meta даже когда mode не coloured. В full popover хотим видеть POS всегда. */
  display: block;
}

.w__popover-ipa {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-soft);
}
.w__popover-tr {
  font-style: italic;
  font-size: 12px;
  color: var(--ink);
}
.w__popover-meta {
  font-family: var(--font-ui);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
  margin-top: 4px;
  display: none;  /* скрыта по дефолту */
}

/* Мета показывается только в режимах с лингв. раскраской */
.reader[data-mode="pos"]             .w__popover-meta,
.reader[data-mode="pos-underline"]   .w__popover-meta,
.reader[data-mode="tense"]           .w__popover-meta,
.reader[data-mode="tense-underline"] .w__popover-meta,
.reader[data-mode="tense-frame"]     .w__popover-meta,
.reader[data-mode="role"]            .w__popover-meta { display: block; }

/* @keyframes pop-in убран 2026-05-11 — заменён pure opacity transition в .w__popover
   (см. выше). Раньше translateY+scale animation вызывал side-jump из-за layout thrash
   при display:none → display:flex toggle. */

/* ============================================================
   Mode-bar и легенда — UI-обвязка ридера
   ============================================================ */
.mode-bar {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin: 20px 0 32px;
  font-family: var(--font-ui);
  font-size: 12px;
}
.mode-bar button {
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 6px 12px;
  cursor: pointer;
  transition: all 160ms ease;
  font-family: inherit;
}
.mode-bar button:hover {
  color: var(--ink);
  border-color: var(--ink-muted);
}
.mode-bar button.active,
.mode-bar button.is-active {
  background: var(--surface-2);
  color: var(--ink);
  border-color: var(--ink-muted);
}

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  font-family: var(--font-ui);
  font-size: 11px;
  margin-bottom: 28px;
  color: var(--ink-soft);
  min-height: 18px;
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
}
.legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
}

/* ============================================================
   Chapter stack widget (2026-05-16 v26 — natural flex item в topbar).
   "Цифра · stacked (A5)" lift из vault/04-page-nav/a5-b5-sizes.html.

   v26 history (right-edge architecture fix):
   - Был position:fixed; top:14px; right:416px — hardcoded под words-panel
     width=400, после v20 (panel 520px) widget оказывался ВНУТРИ panel zone
     → visual overlap (см. PROBLEMS.md «right-edge v26»). Также @<1100px
     просто display:none → нет navigation в strip mode.
   - Теперь: natural flex item внутри <header class="topbar"> — никогда
     не overlap'ит panel (panel в своём grid column), всегда видим
     (graceful compression: title→скрыт@<700px, chevrons→скрыты@<500px).
   ============================================================ */
.chapter-stack {
  /* v28.1 (2026-05-16): фон + border ТОЛЬКО при hover. В resting карточка
     невидима — только number + title. Border 1px transparent резервирует место,
     чтобы layout не «дёргался» при появлении подсветки на hover.

     Natural flex item — no fixed positioning. Sits between menu-wrapper
     и bookmarks-panel в topbar's flex row. margin-left:auto push'ит widget
     к правому краю. */
  position: relative;
  margin-left: auto;
  user-select: none;
  font-family: var(--font-display, 'Fraunces', Georgia, serif);
  text-align: center;
  flex-shrink: 0;            /* не сжимать на узких viewport — лучше скрыть title */
  background: transparent;
  border: 1px solid transparent;
  border-radius: 12px;
  padding: 0;
  transition: background 140ms ease, border-color 140ms ease;
}
.chapter-stack:hover {
  background: var(--surface-2, color-mix(in oklab, var(--hl) 40%, var(--surface)));
  border-color: var(--border-soft, var(--border));
}
.chapter-stack__dd {
  position: relative;
}
.chapter-stack__dd > summary {
  list-style: none;
  cursor: pointer;
  /* v28: горизонтальный padding 36px (28px chev width + 8px breathing) чтобы
     number/title не наезжали на arrows внутри карточки. */
  padding: 8px 36px;
  border-radius: 11px;
  transition: background 140ms ease;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.chapter-stack__dd > summary::-webkit-details-marker { display: none; }
.chapter-stack__dd > summary:hover {
  background: color-mix(in oklab, var(--hl) 40%, transparent);
}
.chapter-stack__num {
  /* 2026-05-12 v9: <button> для click → swap to input. Reset button defaults. */
  appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
  font-size: 26px;
  font-weight: 500;
  color: var(--ink);
  line-height: 1;
  letter-spacing: -0.01em;
  position: relative;
}
.chapter-stack__num:hover {
  color: var(--accent, var(--ink));
}
.chapter-stack__num-input {
  font-family: var(--font-display, 'Fraunces', Georgia, serif);
  font-size: 26px;
  font-weight: 500;
  color: var(--ink);
  line-height: 1;
  width: 60px;
  text-align: center;
  background: transparent;
  border: 0;
  border-bottom: 2px solid var(--accent, var(--ink));
  outline: 0;
  padding: 0;
  -moz-appearance: textfield;
}
.chapter-stack__num-input::-webkit-inner-spin-button,
.chapter-stack__num-input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.chapter-stack__num::after {
  /* Ghost stack effect — A5 vibe */
  content: attr(data-num);
  position: absolute;
  inset: 4px 0 auto 0;
  color: var(--ink);
  opacity: 0.08;
  z-index: -1;
  pointer-events: none;
}
.chapter-stack__title {
  font-size: 12px;
  font-style: italic;
  color: var(--ink-soft);
  line-height: 1.25;
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chapter-stack__list {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  margin: 0;
  padding: 6px 0;
  list-style: none;
  min-width: 220px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  box-shadow: 0 8px 24px color-mix(in oklab, var(--ink) 15%, transparent);
  font-family: var(--font-ui, 'Inter', sans-serif);
  font-style: normal;
  font-size: 13px;
}
.chapter-stack__list li { margin: 0; }
.chapter-stack__list button {
  display: flex;
  align-items: baseline;
  gap: 10px;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  padding: 7px 14px;
  color: var(--ink-soft);
  cursor: pointer;
  font: inherit;
  transition: background 100ms, color 100ms;
}
.chapter-stack__list-num {
  flex-shrink: 0;
  min-width: 24px;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: color-mix(in oklab, var(--ink) 50%, transparent);
  font-size: 12px;
}
.chapter-stack__list-title {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chapter-stack__list button[aria-current="true"] .chapter-stack__list-num {
  color: var(--ink);
}
.chapter-stack__list button:hover { color: var(--ink); background: color-mix(in oklab, var(--hl) 30%, transparent); }
.chapter-stack__list button[aria-current="true"] {
  color: var(--ink);
  font-weight: 600;
  background: color-mix(in oklab, var(--hl) 40%, transparent);
}

/* v28 (2026-05-16): chevrons ВНУТРИ карточки. left/right: 4px вместо -28px.
   v28.1: fade-in только на hover карточки (как и background) — в resting
   видны только number + title, чисто и спокойно.
   v28.4 (2026-05-16): z-index 2 — поверх `.chapter-stack__dd` (которая
   position: relative + z auto). Без явного z-index chevron'ы и details
   на одном уровне, DOM-order tiebreaker: details идёт после prev в HTML →
   details перекрывает prev → клик на prev попадает в summary. Fix: chevron'ы
   z:2, details z auto (= 0) → chevron'ы reliably на top. */
.chapter-stack__chev {
  position: absolute;
  top: 50%;
  z-index: 2;
  transform: translateY(-50%);
  appearance: none;
  background: transparent;
  border: 0;
  padding: 8px 6px;
  font-size: 18px;
  color: color-mix(in oklab, var(--ink) 45%, transparent);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 140ms ease, color 120ms ease, transform 120ms ease;
}
.chapter-stack:hover .chapter-stack__chev {
  opacity: 1;
  pointer-events: auto;
}
.chapter-stack__chev:hover:not(:disabled) {
  color: var(--ink);
}
.chapter-stack__chev:disabled {
  opacity: 0.25 !important;
  cursor: not-allowed;
}
.chapter-stack__chev--prev {
  left: 4px;
}
.chapter-stack__chev--next {
  right: 4px;
}
.chapter-stack__chev--prev:hover:not(:disabled) { transform: translateY(-50%) translateX(-2px); }
.chapter-stack__chev--next:hover:not(:disabled) { transform: translateY(-50%) translateX(2px); }

/* 2026-05-16 v26: graceful compression вместо display:none@<1100px.
   Old behavior (v9): widget hidden совсем @<1100px → у user не было navigation
   в strip-mode (600-899px) и mobile (<600px). User explicit feedback: «где
   навигация наша?» — это и была root cause этой жалобы.
   Now: всегда видим, постепенно теряет элементы:
     ≥700px  — full widget (chevrons + number + title)
     <700px  — title скрыт (только number + chevrons + dropdown)
     <500px  — chevrons скрыты тоже (только number + dropdown через details)
   Number с dropdown — minimum-viable navigation на любом viewport. */
@media (max-width: 699px) {
  .chapter-stack__title { display: none; }
}
@media (max-width: 499px) {
  .chapter-stack__chev { display: none; }
}

/* ============================================================
   Bottom pager (2026-05-12 v3 — replaces 8 ribbons + "← Глава/Глава →").

   Структура:
     <nav.bottom-pager>
       <button.bottom-pager__btn id="prev-ch-2">‹</button>
       <span.bottom-pager__num data-num="N">N</span>
       <button.bottom-pager__btn id="next-ch-2">›</button>

   "Цифра · stacked (A5)" lift из Design книги/04-page-nav/a5-b5-sizes.html
   — крупная цифра с ghost-stack effect под ней (через ::after).
   Chevron buttons на каждом боку. Click → goRel(±1) на главу.
   ============================================================ */
.bottom-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 22px;
  margin: 32px auto 48px;
  padding: 0;
  font-family: var(--font-display, 'Fraunces', Georgia, serif);
  user-select: none;
}

.bottom-pager__btn {
  appearance: none;
  background: transparent;
  border: 0;
  color: color-mix(in oklab, var(--ink) 45%, transparent);
  font-family: inherit;
  font-size: 26px;
  font-weight: 400;
  line-height: 1;
  cursor: pointer;
  padding: 6px 12px;
  border-radius: 6px;
  transition: color 140ms ease, background 140ms ease, transform 140ms ease;
}
.bottom-pager__btn:hover:not(:disabled) {
  color: var(--ink);
  background: color-mix(in oklab, var(--hl) 35%, transparent);
}
.bottom-pager__btn:active:not(:disabled) {
  transform: translateX(0) scale(0.96);
}
.bottom-pager__btn:disabled {
  opacity: 0.25;
  cursor: not-allowed;
}
.bottom-pager__btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Цифра · stacked — крупно + ghost copy под ней */
.bottom-pager__num {
  position: relative;
  font-size: 32px;
  font-weight: 500;
  color: var(--ink);
  min-width: 56px;
  text-align: center;
  line-height: 1;
  letter-spacing: -0.01em;
}
.bottom-pager__num::after {
  /* Ghost stack — еле-видный duplicate под main number, A5 vibe */
  content: attr(data-num);
  position: absolute;
  left: 0;
  right: 0;
  top: 6px;
  font-size: inherit;
  font-weight: inherit;
  color: var(--ink);
  opacity: 0.08;
  pointer-events: none;
  z-index: -1;
}

@media (max-width: 600px) {
  .bottom-pager {
    gap: 16px;
    margin: 24px auto 36px;
  }
  .bottom-pager__num { font-size: 26px; min-width: 44px; }
  .bottom-pager__btn { font-size: 22px; padding: 5px 10px; }
}

/* ============================================================================
   MULTILANG SUPPORT (Wave 1a + 1b + 2 + 3 + 4) — 2026-05-14
   ----------------------------------------------------------------------------
   Blueprint: voice-learning-system/docs/multilang-css-blueprint.md
   Source: vault/01-reader/reader-multilang/reader-core.css
   Architecture: 3 script families (Default LTR / RTL semitic / CJK) + per-lang
   opt-ins (ru cases/aspect/morphology/stress, hi font, zh tones/HSK).
   Gender в shared core (5 языков), POS extensions (part/class/num) тоже shared.
   ============================================================================ */

/* ---- 1. Multi-script font fallback ------------------------------------------
   `--font-body` дефолт со встроенным Noto-стеком (CJK/Arabic/Hebrew/Devanagari)
   определён в tokens.css :root. Здесь дублировать НЕ нужно — per-script
   переопределения ниже (body.lang-zh и т.п.). */

/* ---- 2. LTR-isolation для IPA и перевода ----------------------------------
   Критично для RTL ридеров (ar/he): латинская IPA и русский перевод внутри
   арабского/иврита остаются LTR. Без этого ломается bidi и слой rendering. */
.w__ipa,
.w__tr {
  direction: ltr;
  unicode-bidi: isolate;
}

/* ---- 3. POS extensions для CJK + Hindi (part / class / num) ----------------
   Уже покрыты data-driven маппингом в секции «РЕЖИМ 1-2» (мапит все 12 частей
   речи включая part/class/num) + apply-правилами `.w[data-pos]`. Отдельные
   правила тут больше не нужны — удалены 2026-05-15. Токены — в tokens.css. */

/* ---- 4. POS-frame mode — УДАЛЁН (повторно) 2026-05-15 -----------------------
   Режим pos-frame убран 2026-05-12 (рамки вокруг каждого слова мешают скан-рейту):
   нет кнопки в reader.html, нет в VALID_MODES (reader.js). Был случайно повторно
   занесён при порте multilang — теперь удалён снова.
   ⚠️ НЕ возвращать без кнопки в HTML + записи в VALID_MODES — иначе мёртвый CSS. */

/* ---- 5. Tense extensions (subj / impf for fr/es) ---------------------------
   Токены --tense-subj/impf — в tokens.css (тёмные варианты — в themes.css). */
.reader[data-mode="tense"] .w[data-tense="subj"] .w__text { color: var(--tense-subj); font-weight: 500; }
.reader[data-mode="tense"] .w[data-tense="impf"] .w__text { color: var(--tense-impf); font-weight: 500; }
.reader[data-mode="tense-underline"] .w[data-tense="subj"] .w__text { box-shadow: inset 0 -2px 0 var(--tense-subj); }
.reader[data-mode="tense-underline"] .w[data-tense="impf"] .w__text { box-shadow: inset 0 -2px 0 var(--tense-impf); }

/* ---- 6. Tense-frame mode --------------------------------------------------- */
.reader[data-mode="tense-frame"] .w[data-tense] .w__text {
  padding: 0 4px;
  margin: 0 -4px;
  border-radius: 3px;
}
.reader[data-mode="tense-frame"] .w[data-tense="past-perfect"]    .w__text { border: 1px solid var(--tense-past-perfect); }
.reader[data-mode="tense-frame"] .w[data-tense="past"]            .w__text { border: 1px solid var(--tense-past); }
.reader[data-mode="tense-frame"] .w[data-tense="past-cont"]       .w__text { border: 1px solid var(--tense-past-cont); }
.reader[data-mode="tense-frame"] .w[data-tense="present"]         .w__text { border: 1px solid var(--tense-present); }
.reader[data-mode="tense-frame"] .w[data-tense="present-cont"]    .w__text { border: 1px solid var(--tense-present-cont); }
.reader[data-mode="tense-frame"] .w[data-tense="present-perfect"] .w__text { border: 1px solid var(--tense-present-perfect); }
.reader[data-mode="tense-frame"] .w[data-tense="future"]          .w__text { border: 1px solid var(--tense-future); }
.reader[data-mode="tense-frame"] .w[data-tense="cond"]            .w__text { border: 1px solid var(--tense-cond); }
.reader[data-mode="tense-frame"] .w[data-tense="subj"]            .w__text { border: 1px solid var(--tense-subj); }
.reader[data-mode="tense-frame"] .w[data-tense="impf"]            .w__text { border: 1px solid var(--tense-impf); }

/* ---- 7. Gender (shared — for 5 languages: fr/es/hi/he/ar) ------------------
   Токены --gender-m/f/n — в tokens.css (тёмные варианты — в themes.css). */
.reader[data-mode="gender"] .w[data-gender="m"] .w__text { color: var(--gender-m); }
.reader[data-mode="gender"] .w[data-gender="f"] .w__text { color: var(--gender-f); }
.reader[data-mode="gender"] .w[data-gender="n"] .w__text { color: var(--gender-n); }
.reader[data-mode="gender-underline"] .w[data-gender="m"] .w__text { box-shadow: inset 0 -2px 0 var(--gender-m); }
.reader[data-mode="gender-underline"] .w[data-gender="f"] .w__text { box-shadow: inset 0 -2px 0 var(--gender-f); }
.reader[data-mode="gender-underline"] .w[data-gender="n"] .w__text { box-shadow: inset 0 -2px 0 var(--gender-n); }

/* ---- 8. Syntactic roles — каноническое определение выше («РЕЖИМ 7: role»).
   Здесь был дубль (re-port из reader-core.css): свой :root --role-* (теперь
   в tokens.css) + повторные mode-rules + другой вид .clause--subordinate.
   Удалён 2026-05-15. Один источник: секция «РЕЖИМ 7» в начале файла. */

/* ---- 9. Font-preset body classes — body.font-noto-* УДАЛЕНЫ 2026-05-15:
   были мёртвыми (нет кнопок в HTML, reader.js их не выставляет). Per-script
   шрифт идёт через body.lang-* (см. base.css). */

/* ---- 10. RTL family hooks (ar / he) ----------------------------------------
   `dir="rtl"` ставится JS на reader element при detect'е lang-ar / lang-he. */
[dir="rtl"]            { word-spacing: 0.1em; }
[dir="rtl"] .w--punct  { margin-left: 0; margin-right: -0.18em; }

/* Harakat toggle (огласовки арабские) — body class hook. Real text-filter
   делается в JS (не CSS — diacritic marks встроены в текст). */
body.hide-harakat .lang-ar .w__text { font-feature-settings: "harf" 0; }

/* ---- 11. CJK family — lang-zh adjustments ---------------------------------- */
body.lang-zh {
  --fz-word: 28px;     /* CJK глиф визуально меньше латиницы */
  --fz-ipa:  12px;     /* пиньинь читаем */
  --line-h:  2.4;      /* воздух для пиньиня сверху */
}
body.lang-zh .w__ipa { letter-spacing: 0.02em; }

/* CJK tones — 5 уровней. Токены --tone-1..5 — в tokens.css (тёмные — в themes.css). */
.reader[data-mode="tone"] .w[data-tone="1"] .w__text { color: var(--tone-1); }
.reader[data-mode="tone"] .w[data-tone="2"] .w__text { color: var(--tone-2); }
.reader[data-mode="tone"] .w[data-tone="3"] .w__text { color: var(--tone-3); }
.reader[data-mode="tone"] .w[data-tone="4"] .w__text { color: var(--tone-4); }
.reader[data-mode="tone"] .w[data-tone="5"] .w__text { color: var(--tone-5); }

/* CJK HSK levels — gradient по сложности. Токены --hsk-1..6 — в tokens.css. */
.reader[data-mode="hsk"] .w[data-level="HSK1"] .w__text { color: var(--hsk-1); }
.reader[data-mode="hsk"] .w[data-level="HSK2"] .w__text { color: var(--hsk-2); }
.reader[data-mode="hsk"] .w[data-level="HSK3"] .w__text { color: var(--hsk-3); }
.reader[data-mode="hsk"] .w[data-level="HSK4"] .w__text { color: var(--hsk-4); }
.reader[data-mode="hsk"] .w[data-level="HSK5"] .w__text { color: var(--hsk-5); }
.reader[data-mode="hsk"] .w[data-level="HSK6"] .w__text { color: var(--hsk-6); }

/* CJK group-words (составные слова с общим переводом + пиньинем) */
.wg {
  display: inline-block;
  position: relative;
  margin: 1.1em 0.3em 1.7em;
  padding: 1em 0.4em 1em;
}
.wg__inner { display: inline-flex; align-items: baseline; }
.wg__sum {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  font-family: var(--font-body);
  font-size: var(--fz-tr);
  color: var(--translation);
  white-space: nowrap;
  padding: 1px 6px;
  background: color-mix(in oklab, var(--surface-2) 70%, transparent);
  border: 1px solid color-mix(in oklab, var(--border-soft) 60%, transparent);
  border-radius: 3px;
}
.wg__sum-ipa {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: var(--fz-ipa);
  color: var(--ink-faint);
  white-space: nowrap;
  opacity: 0.7;
}

/* ---- 12. lang-ru: cases (падежи) -------------------------------------------
   Токены --case-* — в tokens.css (тёмные варианты — в themes.css). */
.reader[data-mode="case"] .w[data-case="nom"] .w__text { color: var(--case-nom); }
.reader[data-mode="case"] .w[data-case="gen"] .w__text { color: var(--case-gen); }
.reader[data-mode="case"] .w[data-case="dat"] .w__text { color: var(--case-dat); }
.reader[data-mode="case"] .w[data-case="acc"] .w__text { color: var(--case-acc); }
.reader[data-mode="case"] .w[data-case="ins"] .w__text { color: var(--case-ins); }
.reader[data-mode="case"] .w[data-case="loc"] .w__text { color: var(--case-loc); }
.reader[data-mode="case"] .w[data-case="voc"] .w__text { color: var(--case-voc); }

/* ---- 13. lang-ru: aspect (вид глагола) -------------------------------------
   Токены --aspect-perf/impf — в tokens.css. */
.reader[data-mode="aspect"] .w[data-aspect="perf"] .w__part--prefix { color: var(--aspect-perf); font-weight: 600; }
.reader[data-mode="aspect"] .w[data-aspect="impf"] .w__part--prefix { color: var(--aspect-impf); font-weight: 600; }
.reader[data-mode="aspect"] .w[data-aspect="perf"]:not(:has(.w__part--prefix)) .w__text { color: var(--aspect-perf); font-weight: 500; }
.reader[data-mode="aspect"] .w[data-aspect="impf"]:not(:has(.w__part--prefix)) .w__text { color: var(--aspect-impf); font-weight: 500; }

/* ---- 14. lang-ru: morphology slots (.w__part--*) --------------------------- */
.w__part { display: inline; }
.reader--show-morph .w__part--ending {
  background: color-mix(in oklab, var(--case-acc) 16%, transparent);
  border-radius: 3px;
  padding: 0 1px;
}
.reader--show-morph .w__part--prefix {
  background: color-mix(in oklab, var(--aspect-perf) 16%, transparent);
  border-radius: 3px;
  padding: 0 1px;
}
.reader--show-morph .w__part--suffix {
  text-decoration: underline dotted color-mix(in oklab, var(--ink) 40%, transparent);
  text-underline-offset: 3px;
}

/* Case-coloured underline на ending (independent от data-mode) */
.reader--mark-endings .w[data-case="nom"] .w__part--ending { box-shadow: inset 0 -2px 0 var(--case-nom); }
.reader--mark-endings .w[data-case="gen"] .w__part--ending { box-shadow: inset 0 -2px 0 var(--case-gen); }
.reader--mark-endings .w[data-case="dat"] .w__part--ending { box-shadow: inset 0 -2px 0 var(--case-dat); }
.reader--mark-endings .w[data-case="acc"] .w__part--ending { box-shadow: inset 0 -2px 0 var(--case-acc); }
.reader--mark-endings .w[data-case="ins"] .w__part--ending { box-shadow: inset 0 -2px 0 var(--case-ins); }
.reader--mark-endings .w[data-case="loc"] .w__part--ending { box-shadow: inset 0 -2px 0 var(--case-loc); }
.reader--mark-endings .w[data-case="voc"] .w__part--ending { box-shadow: inset 0 -2px 0 var(--case-voc); }
/* Fallback на всё слово если морфологических слотов нет */
.reader--mark-endings .w[data-case="nom"]:not(:has(.w__part--ending)) .w__text { box-shadow: inset 0 -2px 0 var(--case-nom); }
.reader--mark-endings .w[data-case="gen"]:not(:has(.w__part--ending)) .w__text { box-shadow: inset 0 -2px 0 var(--case-gen); }
.reader--mark-endings .w[data-case="dat"]:not(:has(.w__part--ending)) .w__text { box-shadow: inset 0 -2px 0 var(--case-dat); }
.reader--mark-endings .w[data-case="acc"]:not(:has(.w__part--ending)) .w__text { box-shadow: inset 0 -2px 0 var(--case-acc); }
.reader--mark-endings .w[data-case="ins"]:not(:has(.w__part--ending)) .w__text { box-shadow: inset 0 -2px 0 var(--case-ins); }
.reader--mark-endings .w[data-case="loc"]:not(:has(.w__part--ending)) .w__text { box-shadow: inset 0 -2px 0 var(--case-loc); }
.reader--mark-endings .w[data-case="voc"]:not(:has(.w__part--ending)) .w__text { box-shadow: inset 0 -2px 0 var(--case-voc); }

/* ---- 15. lang-ru: stress mark --------------------------------------------- */
.w__stress-mark { position: relative; }
.reader--stress .w__stress-mark::after {
  content: attr(data-mark);
  position: absolute;
  left: 50%; top: -0.55em;
  transform: translateX(-50%);
  font-size: 0.85em;
  color: var(--ink);
  line-height: 1;
  pointer-events: none;
}

/* ---- 16. lang-hi: оптимизация для Devanagari ------------------------------ */
body.lang-hi { --line-h: 2.4; } /* воздух для матр сверху */

/* ---- 17. Modifiers ported from vault reader-core.css (option toggles) ----- */

/* .reader--alt-shades — zebra chip backgrounds via :nth-of-type (4n+1/2/3/0) */
.reader--alt-shades .w--show-tr:nth-of-type(4n+1) .w__tr {
  background: color-mix(in oklab, var(--surface-2) 60%, transparent);
}
.reader--alt-shades .w--show-tr:nth-of-type(4n+2) .w__tr {
  background: color-mix(in oklab, var(--surface-2) 75%, transparent);
}
.reader--alt-shades .w--show-tr:nth-of-type(4n+3) .w__tr {
  background: color-mix(in oklab, var(--surface-2) 50%, transparent);
}
.reader--alt-shades .w--show-tr:nth-of-type(4n) .w__tr {
  background: color-mix(in oklab, var(--surface-2) 85%, transparent);
}

/* .reader--word-zebra — alternating word underlines (helps CJK word boundary) */
.reader--word-zebra .w:nth-of-type(2n) .w__text {
  box-shadow: inset 0 -1px 0 color-mix(in oklab, var(--ink-faint) 30%, transparent);
}

/* .reader--show-all (debug-режим) — каноническое определение выше, секция
   «Отладочный режим». Дубликат, висевший здесь, удалён 2026-05-15. */

/* END MULTILANG SUPPORT block — 2026-05-14 (v2 fixes 2026-05-15) =============*/
