:root {
  --bg:        #f3ead6;
  --ink:       #2b2723;
  --ink-light: #7a6f65;
  --accent:    #4a7c59;
  --radius:    6px;
  /* ТЗ №13, фаза 2 → ТЗ №14, фаза 1: узор на фоне — тон чернил темы поверх
     бумаги. 5% (ТЗ №13) основатель счёл слишком слабым («точки появились,
     нравится, но слишком слабо заметны») — проявленность даёт
     непрозрачность, а не радиус/шаг сетки (те не менялись). Поднято до 9%
     (средний из показанных 7/9/11%, см. docs/reports/2026-08-22_pattern_v2.md).
     Формула фиксированная (--pattern-image ниже, задаётся один раз), цвет
     пересчитан вручную под ink каждой темы (RGB чернил × 0.09 + RGB бумаги
     × 0.91), чтобы не тянуть color-mix() ради поддержки старых webview. */
  --pattern-dot: rgba(43, 39, 35, 0.09);
}

/* Задача E: косметика — другой цвет чернил, фон "тёплой бумаги" не трогаем.
   Класс вешается на <html> (main.js applyCosmetic), а не #app, — переопределяет
   :root для ВСЕХ экранов, включая уже отрисованные до применения. */
html.theme-ink-blue {
  --ink:       #1f2d4a;
  --ink-light: #55637f;
  --accent:    #3d6b96;
  --pattern-dot: rgba(31, 45, 74, 0.09);
}
/* Задача I: ещё две гаммы «тёплой бумаги» — та же схема переменных. */
html.theme-sepia {
  --bg:        #ece0c8;
  --ink:       #4a3728;
  --ink-light: #8a6f56;
  --accent:    #a3703f;
  --pattern-dot: rgba(74, 55, 40, 0.09);
}
html.theme-graphite {
  --bg:        #e9e7e2;
  --ink:       #2e2e30;
  --ink-light: #6c6c70;
  --accent:    #55606b;
  --pattern-dot: rgba(46, 46, 48, 0.09);
}
/* ТЗ №01: награда за 3-й день серии входов — своя гамма, ОБЯЗАНА
   отличаться от трёх платных выше (бесплатная выдача не дублирует
   ассортимент магазина, см. retention.js/main.js grantStyle).
   ТЗ №03, Фаза 2: --ink затемнён (было #7a3b2e, контраст 157.4 — ниже
   всех трёх платных, минимум Sepia 164.6). Тот же тон (H≈10°, тёплый
   красно-коричневый), просто темнее — HSL(10°,45%,25%) вместо
   HSL(10°,45%,33%), не сдвинут в сторону Sepia (H≈26°) или Graphite
   (насыщенность ≈0%). Перезамер тем же скриптом — см. отчёт
   docs/reports/2026-08-20_retention_prerelease.md. */
html.theme-streak-rust {
  --bg:        #f3ead6;
  --ink:       #5d2d23;
  --ink-light: #8a5a4c;
  --accent:    #c1533c;
  --pattern-dot: rgba(93, 45, 35, 0.09);
}
/* ТЗ №49: награда за 7-й день лестницы — единственная ТЁМНАЯ гамма студии
   (--bg тёмный, не «тёплая бумага»), поэтому вместо пересчёта
   --pattern-dot по формуле «чернила×0.09+бумага×0.91» (та формула
   предполагает тёмные чернила на светлой бумаге — здесь наоборот) взят
   белый узор на тёмном фоне той же прозрачности 0.09, тот же приём глазом
   читается одинаково независимо от того, кто темнее — бумага или чернила. */
html.theme-night {
  --bg:        #1c1f26;
  --ink:       #e8e6df;
  --ink-light: #a9aab3;
  --accent:    #8fb0ff;
  --pattern-dot: rgba(232, 230, 223, 0.08);
}

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

html, body {
  width: 100%; height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  /* ТЗ №13, фаза 2: узор — чистый CSS (repeating-radial-gradient), без
     файлов-ассетов и внешних запросов. Живёт на body (не на .screen —
     у экранов нет своей заливки, они прозрачны до этого правила), поэтому
     сам по себе виден на всех экранах разом; #game ниже перекрывает его
     собственной сплошной заливкой — на игровом поле узора нет. Вариант —
     самый сдержанный из 3, показанных основателю на скриншотах
     (docs/reports/2026-08-22_polish_debts.md); точки радиусом 1px на сетке
     28px были при рендере визуально неотличимы от чистого фона (1px
     растеризуется в почти ничего) — это не решило бы жалобу основателя на
     плоский фон, поэтому первая итерация (радиус 1px/28px) отклонена ДО
     показа, крупнее круг (1.4px) и чаще сетка (22px) при тех же 5%
     непрозрачности чернил темы (--pattern-dot) уже устойчиво видны. */
  background-color: var(--bg);
  background-image: radial-gradient(circle at center, var(--pattern-dot) 1.4px, transparent 1.8px);
  background-size: 22px 22px;
  color: var(--ink);
  font-family: Georgia, 'Times New Roman', serif;
}

#app {
  width: 100%; height: 100%;
  touch-action: none;
  position: relative;
  /* ПК-модерация (п.1.6.2.7): не даём браузеру выделять текст игровых
     экранов ПКМ-протяжкой/тройным кликом — это отдельная защита от
     дефолтного контекстного меню (contextmenu гасится в main.js). */
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.screen {
  position: absolute;
  top: 0; left: 0;
  /* ТЗ №10, фаза 1/2 (шаг B) — резерв места под ВК-баннер вместо наезда
     поверх контента. Переменные ставит/снимает adapters/vk_bridge.js
     (applyBannerReserve) по факту колбэков проверки/обновления/закрытия
     баннера площадки (см. VK-адаптер — имена методов не дублируем здесь
     буквально: build.py check_no_leak сканирует style.css как COMMON_FILE
     и раньше на этом же тексте падал бы в яндекс-сборке, просто это
     обнаружилось только сейчас, ТЗ №13, п.3.3 — build.py yandex до
     pre-commit-хука на этой ветке ни разу не запускался);
     в источнике и в яндекс-сборке переменных нет —
     0px, .screen как раньше растянут на весь #app, поведение не меняется.
     Явные right/bottom вместо inset:0 — сокращают собственный бокс
     .screen (не padding предка: тот НЕ сжимает containing block
     абсолютно спозиционированных потомков, проверено живым прогоном),
     поэтому flex-центрирование (align-items/justify-content ниже) само
     центрирует портретную колонку в ОСТАВШЕЙСЯ области — без правок
     разметки экранов. safe-area (фаза 2, низ на мобиле) добавляется, а
     не заменяет резерв. */
  right: var(--vk-banner-reserve-right, 0px);
  bottom: calc(var(--vk-banner-reserve-bottom, 0px) + env(safe-area-inset-bottom, 0px));
  transition: right 0.2s ease, bottom 0.2s ease;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.screen.is-active { display: flex; }

/* Загрузка */
.loader {
  width: 40px; height: 40px;
  border: 3px solid var(--ink-light);
  border-top-color: var(--ink);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { margin-top: 16px; color: var(--ink-light); font-size: 0.9rem; }

/* RV-загрузка (2026-09-06, баг-репорт основателя): пока идёт реальный
   показ Platform.showRewarded (до 40с, REWARD_AD_TIMEOUT_MS/
   AD_HANG_TIMEOUT_MS — оба адаптера), экран раньше молча "замирал" без
   единого признака жизни — неотличимо от зависшей игры. Явный оверлей
   поверх ВСЕГО (z-index выше .retention-toast, единственного, что уже
   использовало 50) — видимая причина паузы, не имитация поломки. .loader/
   .loading-text переиспользованы как есть (экран загрузки выше) — не
   плодим второй спиннер под другим именем. Кнопку/экран под оверлеем НЕ
   прячем и НЕ дизейблим (п.190/шрам Color Sort про #retention-rewarded-btn
   остаётся в силе) — просто временно перекрываем явным, понятным слоем;
   main.js: showAdLoadingOverlay()/hideAdLoadingOverlay(), парой вокруг
   каждого вызова Platform.showRewarded (onHintClick, onRewardedButtonClick). */
.ad-loading-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.35);
}
.ad-loading-overlay[hidden] { display: none; }
/* ТЗ №49: раньше border-color/color брались из var(--bg), полагаясь на то,
   что --bg ВСЕГДА светлая «бумага» (читаемо на тёмном rgba-скриме ниже).
   Гамма «Ночь» (theme-night) впервые сделала --bg тёмным — тот же приём
   слил бы спиннер/текст с чёрным скримом до нечитаемости. Скрим здесь
   фиксированный rgba(0,0,0,…), НЕ завязан на активную тему вообще —
   поэтому и его содержимое берёт фиксированный светлый цвет, а не var(--bg). */
.ad-loading-overlay .loader { border-color: rgba(255, 255, 255, 0.85); border-top-color: transparent; }
.ad-loading-text { margin-top: 16px; color: #f3ead6; font-size: 0.9rem; text-align: center; padding: 0 24px; }

/* Debug-оверлей (2026-09-06, ?debug=1) — z-index выше .ad-loading-overlay
   (60), чтобы строки лога были видны, даже пока идёт ожидание рекламы.
   Тёмный монохромный стиль намеренно НЕ похож на бумажную тему игры —
   сразу видно, что это служебная панель, не часть UI.
   БАГ (2026-09-07, живой скриншот основателя): панель до 42vh высотой
   перекрывала боевые кнопки внизу экрана (#btn-hint, #retention-rewarded-btn)
   — инструмент диагностики ломал ровно тот сценарий, который должен
   измерять. pointer-events:none на контейнере — тапы проходят СКВОЗЬ
   панель к настоящим кнопкам под ней, независимо от того, насколько
   панель разрослась; auto возвращён только кнопке очистки. */
.debug-overlay {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  max-height: 42vh;
  z-index: 70;
  display: flex;
  flex-direction: column;
  background: rgba(0, 0, 0, 0.85);
  font-family: monospace;
  pointer-events: none;
}
.debug-overlay[hidden] { display: none; }
.debug-overlay-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 10px;
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  font-size: 11px;
  font-weight: bold;
  letter-spacing: 0.05em;
  flex-shrink: 0;
}
.debug-clear-btn {
  background: none;
  border: none;
  color: #fff;
  font-size: 16px;
  line-height: 1;
  padding: 0 6px;
  cursor: pointer;
  pointer-events: auto;
}
.debug-log {
  overflow-y: auto;
  padding: 4px 10px 8px;
  color: #7cfc7c;
  font-size: 10.5px;
  white-space: pre-wrap;
  word-break: break-word;
}
.debug-log > div { border-bottom: 1px solid rgba(255, 255, 255, 0.08); padding: 2px 0; }
/* Финальный исход showRewarded (2026-09-07) — крупнее и заметнее строк
   ожидания, чтобы не потеряться среди одинаковых "жду ответа моста: …". */
.debug-log-final {
  font-size: 14px;
  font-weight: bold;
  color: #fff;
  background: rgba(255, 255, 255, 0.14);
  padding: 4px 4px !important;
}

/* Меню */
.menu-head { text-align: center; margin-bottom: 48px; }
.game-title { font-size: 2.6rem; font-weight: normal; margin: 8px 0 6px; }
.game-sub { color: var(--ink-light); font-size: 0.95rem; }

.menu-actions { display: flex; flex-direction: column; gap: 12px; width: 200px; }

.btn {
  padding: 14px 24px;
  border: 2px solid var(--ink);
  border-radius: var(--radius);
  font-family: inherit;
  font-size: 1rem;
  cursor: pointer;
  background: transparent;
  color: var(--ink);
  transition: background 0.15s, color 0.15s;
}
.btn-primary { background: var(--ink); color: var(--bg); }
.btn-primary:active { opacity: 0.8; }
.btn-secondary:active { background: var(--ink); color: var(--bg); }
#btn-daily.is-done { opacity: 0.5; }

/* Стрик + кнопка календаря */
.daily-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 28px;
  margin-top: 4px;
}
.daily-streak {
  font-size: 0.85rem;
  color: var(--accent);
  letter-spacing: 0.02em;
}
.cal-icon-btn {
  font-size: 1rem;
  color: var(--ink-light);
  padding: 2px 6px;
}

/* ---- ТЗ №01: модуль удержания (только ВК-сборка, узлы вставляет
   build.py transform_index_html_vk — в общем index.html их нет) ---- */

/* Постоянная строка — не модалка, не перекрывает кнопки (п. приёмки:
   видна целиком на 320px, в т.ч. горизонтально). */
.retention-line {
  font-size: 0.8rem;
  color: var(--ink-light);
  text-align: center;
  max-width: 280px;
  line-height: 1.3;
}
#retention-streak-line { margin-top: 6px; }
#retention-drip-line   { margin-bottom: 8px; }

/* ТЗ №09, фаза 3 — rewarded «Открыть ещё +N», рядом со строкой раздатчика.
   Меньше и тише основной кнопки «Играть» — вторична по иерархии. */
.retention-rewarded-btn {
  display: block;
  margin: 0 auto 16px;
  font-size: 0.85rem;
  padding: 8px 16px;
  min-height: 34px;
}
/* ТЗ №12: без этого правила атрибут [hidden] (renderRewardedButton(),
   конец кампании — открывать больше нечего) проигрывает по порядку
   каскада безусловному display:block выше — на экране оставалась пустая
   нефункциональная кнопка вместо её исчезновения (тот же класс дефекта,
   что у .daily-meta, см. build.py transform_index_html_vk). */
.retention-rewarded-btn[hidden] { display: none; }

/* ТЗ (RV-значок, Яндекс 4.5.1, усиление 2026-09-06): значок «экран +
   треугольник» на обеих RV-точках (эта кнопка + #btn-hint, см.
   .hint-icon-wrap ниже) — рядом с текстом, тем же var(--accent), что и
   остальной акцент студии, чтобы не потеряться ни в одной из 4
   косметических тем магазина (Задача I). Инлайн-flex обёртка вместо flex
   на самой кнопке — сохраняет её auto-ширину/центрирование (margin:0 auto
   выше рассчитан на shrink-to-fit кнопки, не на flex-контейнер). */
.rv-btn-inner {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.ad-icon {
  flex: none;
  color: var(--accent);
}
.ad-icon rect { fill: none; stroke: currentColor; stroke-width: 1.3; }
.ad-icon path { fill: currentColor; }

/* ТЗ №26, Р7 (26.08): на 568×320 (самая низкая рабочая ширина студии)
   строка раздатчика, строка пазла дня и кнопка «Магазин» уходили за
   нижний край — html/body не скроллятся (overflow:hidden выше), то есть
   были физически недостижимы, не просто «нужно прокрутить» (Этап 4,
   docs/reports/2026-08-26_ТЗ26_этап4_кадры.md). Решение основателя —
   вариант A: сжать вертикальные отступы МЕНЮ на низких viewport, ничего
   не прятать (вариант B — прятать строку раздатчика — отклонён как
   повторный откат к отверженному «невидимая механика не удерживает»).
   400px — по матрице ширин студии это ловит только 568×320 (высота 320);
   у остальных трёх ширин высота 600px, не задета. Общий style.css —
   правка задевает и ВК (тот же брейкпоинт, то же меню), это ожидаемо
   и сверено diff'ом с боевым v43 в отчёте Этапа 4б. */
@media (max-height: 400px) {
  .menu-head { margin-bottom: 8px; }
  .menu-actions { gap: 4px; }
  .menu-actions .btn { padding: 8px 24px; }
  #retention-streak-line { margin-top: 2px; }
  .daily-meta { margin-top: 2px; min-height: 22px; }
}

.chapter-card.is-locked {
  opacity: 0.55;
}
.chapter-card.is-locked .chapter-head-btn { cursor: pointer; }

/* Всплывающий отклик на выдачу награды (п.2.4) — короткий, ненавязчивый,
   сам прячется, не блокирует ввод (не overlay/модалка). */
.retention-toast {
  position: absolute;
  left: 50%; top: 14px;
  transform: translateX(-50%);
  max-width: 90%;
  padding: 8px 16px;
  background: var(--ink);
  color: var(--bg);
  border-radius: var(--radius);
  font-size: 0.85rem;
  text-align: center;
  pointer-events: none;
  z-index: 50;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.retention-toast.is-visible { opacity: 1; }

/* Кнопка звука */
#btn-sound-menu {
  position: absolute;
  top: 12px;
  right: 12px;
}
#btn-sound-game {
  margin-left: auto;   /* прижимает кнопку к правому краю шапки */
}
.sound-btn {
  position: relative;
  font-size: 1.3rem;
}
.sound-btn.is-muted::after {
  content: '';
  position: absolute;
  inset: 4px 8px;     /* совпадает с padding icon-btn, черта идёт сквозь символ */
  background: linear-gradient(
    to bottom right,
    transparent calc(50% - 1px),
    var(--ink)        calc(50% - 1px),
    var(--ink)        calc(50% + 1px),
    transparent       calc(50% + 1px)
  );
  pointer-events: none;
}

/* Dev-бейдж */
.dev-badge {
  position: absolute; bottom: 16px;
  font-size: 0.75rem;
  color: var(--ink-light);
}

/* Задача F: подпись версии/билда — не UI-элемент, только для основателя.
   Вне .screen (см. index.html), поэтому видна поверх любого активного
   экрана, включая оверлеи (z-index выше .overlay). */
.build-tag {
  position: absolute;
  left: max(8px, env(safe-area-inset-left));
  /* ТЗ №10: .build-tag живёт ВНЕ .screen (см. комментарий выше) — резерв
     .screen на bottom (мобильный баннер) его не задевает автоматически,
     как задевает всё остальное внутри .screen. Тот же --vk-banner-
     reserve-bottom нужен и здесь явно, иначе плашка окажется под
     баннером — ровно то, что это ТЗ и просило не допустить. */
  bottom: calc(max(8px, env(safe-area-inset-bottom)) + var(--vk-banner-reserve-bottom, 0px));
  transition: bottom 0.2s ease;
  font-size: 0.65rem;
  color: var(--ink-light);
  opacity: 0.35;
  pointer-events: none;
  user-select: none;
  z-index: 20;
}

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

/* ---- Экран глав («Альбом», ТЗ №50) ---- */

/* 13 карточек глав не помещаются без скролла на низких экранах (320×568 и
   меньше, тем более на высоких — с рядами миниатюр внутри каждой). Тот же
   приём, что уже был у экрана категорий (Задача K) и у #shop: список сам
   не по центру, а сверху, с прокруткой и отступом, чтобы не залезать под
   back-button. */
#chapters {
  justify-content: flex-start;
  overflow-y: auto;
  padding: 56px 0 24px;
}

.cat-back-btn {
  position: absolute;
  top: 12px;
  left: 12px;
}

.cat-screen-title {
  font-size: 1.4rem;
  font-weight: normal;
  margin-bottom: 12px;
  color: var(--ink);
}

.chapter-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: min(320px, 88vw);
}

.chapter-card {
  width: 100%;
  padding: 12px 16px 14px;
  border: 2px solid var(--ink);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  gap: 8px;
  position: relative;
}

.chapter-head-btn {
  width: 100%;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.chapter-name {
  font-size: 0.95rem;
  color: var(--ink);
  pointer-events: none;
}

.chapter-progress {
  font-size: 0.8rem;
  color: var(--ink-light);
  pointer-events: none;
  flex-shrink: 0;
}
.chapter-progress.is-done { color: var(--accent); }

.postcard-badge {
  font-size: 0.85rem;
  margin-left: 4px;
}

.chapter-locked-line {
  font-size: 0.78rem;
  color: var(--ink-light);
  line-height: 1.4;
}

.chapter-thumbs {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 4px;
}
/* 568×320 и уже — два ряда по 5 (ТЗ №50, п.4). */
@media (max-width: 400px) {
  .chapter-thumbs { grid-template-columns: repeat(5, 1fr); }
}

.thumb {
  aspect-ratio: 1;
  border: 1.5px solid var(--ink-light);
  border-radius: 3px;
  background: transparent;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1px;
  min-width: 0;
}
.thumb-canvas {
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  display: block;
}
.thumb-num {
  font-size: 0.72rem;
  color: var(--ink-light);
  pointer-events: none;
}
.thumb.is-open {
  border-color: var(--ink);
}
.thumb.is-open .thumb-num { color: var(--ink); }
.thumb.is-done {
  border-color: var(--accent);
}
.thumb.is-locked {
  border-style: dashed;
  cursor: pointer;
}
.thumb-dot { opacity: 0.5; }

.chapter-howto-btn {
  align-self: flex-start;
  margin-top: -2px;
  font-size: 0.78rem;
  color: var(--ink-light);
  text-decoration: underline;
  background: none;
  border: none;
  cursor: pointer;
  font-family: inherit;
  padding: 0;
  text-align: left;
}

/* ---- Экран игры ---- */

#game {
  align-items: stretch;
  justify-content: flex-start;
  /* ТЗ №14, фаза 2: раньше здесь была сплошная заливка var(--bg), которая
     гасила узор на ВСЁМ игровом экране (в т.ч. шапку и поля вокруг доски) —
     основатель указал, что это лишнее: узор нужен вокруг доски, как и на
     остальных экранах, узор body просвечивает здесь без изменений. Только
     сама доска непрозрачна — см. .puzzle ниже, её граница = граница
     заливки, никакой лишней "наклейки". */
}

.game-head {
  display: flex;
  align-items: center;
  height: 52px;
  padding: 0 12px;
  gap: 10px;
  flex-shrink: 0;
  border-bottom: 1px solid var(--ink-light);
}
.icon-btn {
  background: none;
  border: none;
  font-size: 1.5rem;
  color: var(--ink);
  cursor: pointer;
  padding: 4px 8px;
  line-height: 1;
}
.level-label { font-size: 0.9rem; color: var(--ink-light); }

.puzzle-container {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
  overflow: hidden;
}

/* ---- Головоломка (CSS Grid 2×2) ---- */

.puzzle {
  display: grid;
  grid-template-areas: "tl   cols"
                       "rows grid";
  grid-template-columns: var(--clue-col-w) auto;
  grid-template-rows: auto auto;
  touch-action: none;
  /* ТЗ №14, фаза 2: доска (числа-подсказки + сетка клеток) — непрозрачная
     подложка, узор на ней мешал бы читаемости сетки (регресс ТЗ №13
     сохраняется). Фон именно на .puzzle (не на .puzzle-container с его
     padding:12px вокруг) — грид занимает ровно свой контент без отступов,
     поэтому край заливки в точности совпадает с краем доски, без эффекта
     "наклейки". */
  background: var(--bg);
}

.puzzle-tl       { grid-area: tl; }
.puzzle-col-clues { grid-area: cols; display: flex; }
.puzzle-row-clues { grid-area: rows; display: flex; flex-direction: column; }
.puzzle-grid     { grid-area: grid; }

/* Подсказки столбцов: числа выровнены по нижнему краю */
.col-clue {
  width: var(--cell);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  padding-bottom: 6px;
}
.col-clue span {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
}
/* Задача G: строка/столбец закрыты — подсказка бледнеет (прогресс без цифр) */
.col-clue.is-closed span {
  opacity: 0.3;
  transition: opacity 0.2s ease-out;
}

/* Подсказки строк: числа выровнены по правому краю */
.row-clue {
  height: var(--cell);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 3px;
  padding-right: 6px;
}
.row-clue span {
  font-size: 13px;
  color: var(--ink);
}
.row-clue.is-closed span {
  opacity: 0.3;
  transition: opacity 0.2s ease-out;
}

/* Сетка */
.puzzle-grid {
  display: flex;
  flex-direction: column;
  border-top: 1.5px solid var(--ink);
  border-left: 1.5px solid var(--ink);
  touch-action: none;
}
.grid-row { display: flex; }
.grid-cell {
  width: var(--cell);
  height: var(--cell);
  border-right: 1.5px solid var(--ink);
  border-bottom: 1.5px solid var(--ink);
  background: var(--bg);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  user-select: none;
  cursor: pointer;
}

/* Состояния клетки */
.grid-cell.is-filled {
  background: var(--ink);
  animation: cell-stamp 90ms ease-out;
}
.grid-cell.is-cross::after {
  content: '×';
  font-size: calc(var(--cell) * 0.48);
  color: var(--ink-light);
  line-height: 1;
  pointer-events: none;
}
.grid-cell.is-cross {
  animation: cell-stamp 90ms ease-out;
}

/* Задача G: микро-штамп + "чернильная заливка" вместо мгновенной клетки */
@keyframes cell-stamp {
  from { transform: scale(0.85); opacity: 0.4; }
  to   { transform: scale(1);    opacity: 1; }
}

/* Задача G: ошибка, вскрытая подсказкой — короткий шейк, без красного */
.grid-cell.is-shake {
  animation: cell-shake 120ms ease-in-out;
}
@keyframes cell-shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-6px); }
  75%      { transform: translateX(6px); }
}

@media (prefers-reduced-motion: reduce) {
  .grid-cell.is-filled,
  .grid-cell.is-cross,
  .grid-cell.is-shake {
    animation: none;
  }
  .row-clue.is-closed span,
  .col-clue.is-closed span {
    transition: none;
  }
}

/* ---- Подсказка на первом уровне ---- */

.level-hint {
  text-align: center;
  font-size: 0.82rem;
  color: var(--ink-light);
  padding: 0 16px 6px;
  flex-shrink: 0;
  animation: fade-in 0.6s ease both;
}
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .level-hint { animation: none; }
}

/* ---- Онбординг ---- */

.onboarding-card {
  background: var(--bg);
  border: 2px solid var(--ink);
  border-radius: var(--radius);
  padding: 28px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: 88vw;
  animation: card-pop 0.20s ease-out both;
}
.onboarding-title {
  font-size: 1.1rem;
  font-weight: normal;
  color: var(--ink);
}
.onboarding-text {
  font-size: 0.88rem;
  color: var(--ink-light);
  text-align: center;
  line-height: 1.5;
  max-width: 260px;
}

/* ---- Подтверждение (очистка поля и т.п.) ---- */
.confirm-actions {
  display: flex;
  gap: 12px;
}

/* ---- ТЗ №49: карточка лестницы 7 дней (#ladder-card, по образцу
   .onboarding-card) ---- */
.ladder-card-inner {
  max-width: 300px;
}
.ladder-card-day {
  font-size: 0.85rem;
  color: var(--ink-light);
  text-align: center;
  margin-top: -6px;
}
.ladder-strip {
  display: flex;
  gap: 4px;
  width: 100%;
}
.ladder-cell {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px 2px;
  border: 1.5px solid var(--ink-light);
  border-radius: 4px;
  color: var(--ink-light);
}
.ladder-cell-num { font-size: 0.66rem; line-height: 1; }
.ladder-cell-icon { font-size: 0.8rem; line-height: 1; }
.ladder-cell.is-done {
  border-color: var(--accent);
  color: var(--accent);
}
.ladder-cell.is-current {
  border-color: var(--ink);
  color: var(--ink);
  font-weight: bold;
}

/* ---- ТЗ №49: тизер завтрашней картинки дня (экран победы) ---- */
.win-tomorrow {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.win-tomorrow-canvas {
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  border: 1.5px solid var(--ink-light);
  border-radius: 3px;
  display: block;
}
.win-tomorrow-canvas.is-blurred {
  filter: blur(3px);
}
.win-tomorrow-label {
  font-size: 0.72rem;
  color: var(--ink-light);
  text-align: center;
}

/* ТЗ №49, Р7 (ТЗ №26): 568×320 — карточка лестницы и тизер обязаны влезать
   целиком, ничего не прятать (тот же приём, что уже применён к меню выше). */
@media (max-height: 400px) {
  .onboarding-card { padding: 14px 20px; gap: 8px; }
  .ladder-card-day { margin-top: -2px; }
  .win-card { padding: 14px 20px; gap: 8px; }
}

/* Наглядный пример «3 1» */
.how-example {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 4px 0;
}
.how-clue {
  font-size: 1rem;
  color: var(--ink);
  min-width: 28px;
  text-align: right;
}
.how-cells {
  display: flex;
  gap: 3px;
}
.how-cell {
  width: 30px;
  height: 30px;
  border: 1.5px solid var(--ink);
  border-radius: 2px;
  background: var(--bg);
}
.how-cell.filled {
  background: var(--ink);
}

/* ---- Победный оверлей ---- */

.overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(43, 39, 35, 0.65);
  z-index: 10;
}
.overlay[hidden] { display: none; }

.win-card {
  position: relative;
  z-index: 2;
  background: var(--bg);
  border: 2px solid var(--ink);
  border-radius: var(--radius);
  padding: 28px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  max-width: 88vw;
  animation: card-pop 0.20s ease-out both;
}
@keyframes card-pop {
  from { transform: scale(0.85); opacity: 0; }
  70%  { transform: scale(1.03); opacity: 1; }
  to   { transform: scale(1);    opacity: 1; }
}

.silhouette-mount {
  background: var(--bg);
  border: 1.5px solid var(--ink);
  border-radius: var(--radius);
  padding: 14px;
  box-shadow: 3px 3px 0 rgba(43, 39, 35, 0.18);
  line-height: 0;           /* убирает щель под canvas (inline gap) */
}
.win-canvas {
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  display: block;           /* border теперь на mount, не на canvas */
}
.win-theme {
  font-size: 1.15rem;
  color: var(--ink-light);
  letter-spacing: 0.06em;
}

/* ТЗ №50: «Картинка {k} из 10 · глава «{name}»» — под названием картинки. */
.win-chapter-line {
  font-size: 0.8rem;
  color: var(--ink-light);
  margin-top: -8px;
}

/* Награда за главу — тем же каркасом, что #onboarding-overlay/.onboarding-card. */
.chapter-done-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.chapter-done-reward {
  font-size: 1rem;
  color: var(--accent);
  font-weight: bold;
}

/* Конфетти */
.confetti-container {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 1;
}
.confetti-piece {
  position: absolute;
  top: -12px;
  border-radius: 2px;
  animation: confetti-fall linear forwards;
}
@keyframes confetti-fall {
  to { transform: translateY(105vh) rotate(600deg); opacity: 0.2; }
}

@media (prefers-reduced-motion: reduce) {
  .win-card { animation: none; }
  .confetti-piece { display: none; }
}

/* Тумблер режима.
   Фикс 9B: раньше при нехватке места текст ОБРЕЗАЛСЯ (ellipsis) —
   недопустимо, весь текст должен читаться целиком даже на 320px. Вместо
   обрезки — шрифт/паддинги ужимаются (clamp, привязан к ширине вьюпорта),
   а flex-wrap — аварийный запасной вариант, если сжатие всё же не
   уместило три кнопки в одну строку. Левый/правый паддинг учитывает
   safe-area (вырез/жест-навигация в альбомной ориентации).
   Задача F: нижний край — единственный ряд кнопок, что делит угол
   с .build-tag. Базовый запас (24px) даёт подписи не наезжать на
   кнопку даже без safe-area (как в headless-проверке); + env() —
   на телефонах с домашним индикатором. */
.mode-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 12px max(16px, env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  justify-content: center;
  flex-shrink: 0;
  width: 100%;
}
.mode-btn, .hint-btn {
  flex: 1 1 auto;
  min-width: 0;
  border-radius: var(--radius);
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  padding: clamp(6px, 2vw, 10px) clamp(8px, 3vw, 16px);
}
.mode-btn {
  max-width: 160px;
  border: 2px solid var(--ink);
  background: transparent;
  color: var(--ink);
  font-size: clamp(0.66rem, 3.1vw, 0.9rem);
  transition: background 0.12s, color 0.12s;
}
.mode-btn.is-active {
  background: var(--ink);
  color: var(--bg);
}
.hint-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  max-width: 190px;
  border: 2px solid var(--ink-light);
  background: transparent;
  color: var(--ink-light);
  font-size: clamp(0.6rem, 2.8vw, 0.82rem);
}
.hint-btn:disabled {
  opacity: 0.35;
  cursor: default;
}
/* ТЗ (RV-значок, Яндекс 4.5.1, усиление 2026-09-06 №2, прямая просьба
   основателя): значок теперь ПОСТОЯННЫЙ элемент разметки (index.html, не
   переключается JS) — раньше появлялся только ПОСЛЕ исчерпания баланса,
   и сам переход ощущался как неожиданность. position:relative — якорь
   для .hint-badge (ниже), который теперь сидит на углу значка, а не на
   углу всей кнопки. */
.hint-icon-wrap {
  position: relative;
  display: inline-flex;
  flex: none;
  vertical-align: middle;
  margin-right: 8px;
}
/* ТЗ (RV-подпись, Яндекс 4.5.1, заход №3, 2026-09-06): "Подсказка" —
   ПОСТОЯННЫЙ, единственный главный текст кнопки (реагирует основатель:
   заход №2 при исчерпании баланса менял его целиком на «Смотреть
   рекламу» — кнопка переставала читаться как инструмент-подсказка среди
   «Закрасить»/«Крестик»). Факт рекламы — вторичная мелкая строка ниже,
   см. .hint-ad-caption. */
.hint-label {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.2;
  min-width: 0;
}
/* pointer-events наследуются нормально (не задаём none) — это часть текста
   кнопки, не декоративный оверлей. visibility, не hidden/display:none —
   строка всегда резервирует свою высоту, чтобы кнопка не меняла размер
   (и не тянула за собой всю .mode-bar, align-items:stretch по умолчанию)
   при переключении между «есть бесплатные» и «дальше реклама» — переключает
   .is-visible updateHintBadge() в main.js. */
.hint-ad-caption {
  font-size: 0.68em;
  font-weight: 400;
  visibility: hidden;
}
.hint-ad-caption.is-visible {
  visibility: visible;
}
/* ТЗ №03, Фаза 1: видимый баланс бонусных подсказок (награда 2-го дня
   серии входов, ТЗ №01 — до этого жила только 3с тоста и пропадала).
   pointer-events:none — бейдж никогда не перехватывает тап, весь клик
   уходит на саму кнопку. Якорь — .hint-icon-wrap (значок), не сама
   кнопка (см. правку 2026-09-06 №2 выше); смещение подобрано под 15px
   значок так, чтобы бейдж не уходил за правый край .mode-bar на
   320×568/568×320 (запас 16px, см. живой прогон — tools/test_hint_badge.js). */
.hint-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bg);
  font-size: 0.58rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  pointer-events: none;
}

/* ТЗ №51: кнопка «Проверить» (#btn-check) — тот же каркас .hint-btn/
   .hint-icon-wrap/.hint-label/.hint-ad-caption, что и у #btn-hint (см. выше).
   #check-icon-mark — статичная «✓», видна ВСЕГДА, кроме режима «за
   рекламу» (main.js::updateCheckButton переключает hidden на этом элементе
   и на #check-ad-icon зеркально — ровно один из двух виден). Размер 15px —
   тот же, что у .ad-icon (та же коробка hint-icon-wrap), чтобы кнопка не
   меняла ширину при переключении режима. */
.check-icon-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  font-weight: 700;
  line-height: 1;
}
/* ТЗ №51а: атрибут hidden на <svg> браузер не отрабатывает (UA-правило
   [hidden]{display:none} — только для HTML-элементов), поэтому #check-ad-icon
   (svg) оставался виден во всех режимах поверх «✓». #check-icon-mark — span,
   ему UA-правило и так помогает, но правило дублируем на оба явно, той же
   схемой, что .ad-loading-overlay[hidden]/.retention-rewarded-btn[hidden]
   (см. ТЗ №12). */
#check-ad-icon[hidden], #check-icon-mark[hidden] { display: none; }
/* Подпись-статус (бесплатно/за подсказку/за рекламу) — в отличие от
   #hint-ad-caption у #btn-hint (видна ТОЛЬКО в режиме рекламы), здесь
   видна ВСЕГДА — состояние меняется, а не появляется/исчезает целиком
   (п.2 ТЗ №51). is-visible проставлен статично в index.html. */
#check-caption { visibility: visible; }

/* ---- Экран календаря ---- */

.cal-back-btn {
  position: absolute;
  top: 12px;
  left: 12px;
}

.cal-wrap {
  width: min(300px, 90vw);
}

.cal-title {
  text-align: center;
  font-size: 1.1rem;
  font-weight: normal;
  margin-bottom: 16px;
  color: var(--ink);
}

.cal-weekdays,
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}

.cal-weekdays {
  margin-bottom: 4px;
}

.cal-weekday {
  text-align: center;
  font-size: 0.68rem;
  color: var(--ink-light);
  padding-bottom: 4px;
}

.cal-day {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.82rem;
  border-radius: var(--radius);
  border: 1.5px solid transparent;
  color: var(--ink);
}

.cal-day.empty { visibility: hidden; }

.cal-day.done {
  background: var(--accent);
  color: var(--bg);
}

.cal-day.today {
  border-color: var(--ink);
  font-weight: bold;
}

.cal-day.today.done {
  border-color: rgba(43, 39, 35, 0.35);
}

/* ---- Экран косметики (Задача E — каркас, Задача I — список гамм) ---- */

/* Список из N карточек может быть выше экрана (320×568 и меньше) —
   тот же приём, что у #chapters (свой скролл, а не по центру), с отступом
   сверху, чтобы не залезать под back-button (cat-back-btn:
   top 12px + ~36px высоты кнопки). */
#shop {
  justify-content: flex-start;
  overflow-y: auto;
  padding: 56px 0 24px;
}

.shop-status {
  font-size: 0.85rem;
  color: var(--ink-light);
  margin: 0 0 16px;
  text-align: center;
  width: min(280px, 85vw);
}

.shop-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: min(320px, 88vw);
}

.shop-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border: 2px solid var(--ink);
  border-radius: var(--radius);
}

.shop-swatch {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 3px;
  border: 1.5px solid var(--ink);
  position: relative;
}
.shop-swatch-ink {
  position: absolute;
  inset: 8px;
  border-radius: 2px;
}

.shop-item-body {
  flex: 1 1 auto;
  min-width: 0;
}
.shop-item-name {
  font-size: 0.95rem;
  color: var(--ink);
  /* Название гаммы не гарантированно содержит пробелы/дефисы (локали,
     будущие гаммы) — без этого длинное слово не переносится и вылезает
     поверх колонки кнопок (см. баг с «Чернильно-синий»). break-word (не
     anywhere) — переносит только когда реально не помещается, не режет
     слово раньше времени и не срезает intrinsic-ширину колонки в флексе. */
  overflow-wrap: break-word;
}
.shop-item-status {
  font-size: 0.78rem;
  color: var(--ink-light);
  min-height: 1.1em;
}

.shop-item-actions {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 0 0 auto;
  width: 104px; /* ТЗ №07, фаза 3.1: фикс. ширина колонки — иначе длинная
    подпись ("Откроется за серию входов") росла в nowrap-кнопке и наезжала
    на соседний ряд (баг «Терракоты»). Совместимо с overflow-wrap выше —
    ширина колонки задана явно, а не выведена из содержимого (ТЗ №18). */
}
.shop-item-actions .btn {
  min-height: 44px; /* тап-таргет пальцем — постановка задачи требует ≥44 CSS-px */
  padding: 6px 10px;
  font-size: 0.78rem;
  white-space: normal;
  text-align: center;
  line-height: 1.25;
}
.shop-currency-icon {
  height: 0.9em;
  width: auto;
  vertical-align: middle;
}
