/* ===================== ВЕЩЕСТВО ===================== */
/* Бор — зелёное пламя (BurningHouse/Design/services.md, таблица свободных).
   Выбран из трёх свободных (бор/кальций/барий) как самый непохожий на уже
   занятые: у остальных шести сервисов пламя тёплое (янтарь/золото/багрянец/
   сирень/малиновый) или холодное сине-зелёное (бирюза у поездок) — уверенный
   жёлто-зелёный не путается ни с одним из них даже на маленьком фавиконе.
   Совпадение кстати: тот же зелёный уже нужен был интерфейсу как цвет
   «совпадения» при сборке — деталь вспыхивает этим оттенком, когда стыкуется
   с соседом (см. .piece.just-snapped ниже), так что вещество и обратная
   связь сборки говорят одним цветом, а не двумя разными зелёными вперемешку. */
:root{
  --flame-base:#3f6212;
  --flame-tip:#7ddf3c;
  --night-1:#0a1408;
  --night-2:#16260d;
  --dawn-1:#f6fbea;
  --dawn-2:#dcf2ad;
}

/* ===================== BASE ===================== */
:root{color-scheme:light dark}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
/* Фон отдаём наружу под .bh-backdrop--page (assets/brand.css): сам он
   position:fixed с z-index:-1 и красится поверх html, но ПОД любым
   непрозрачным блоком страницы — если html/body остаются непрозрачными,
   фон окажется не виден вовсе. */
html{background:var(--dawn-1)}
@media (prefers-color-scheme: dark){ html:not([data-theme="light"]){background:var(--night-1)} }
html[data-theme="dark"]{background:var(--night-1)}
body{background:transparent;font-family:var(--md-font);color:var(--md-sys-color-on-surface);overflow-x:hidden}
body.modal-open{overflow:hidden}

button{font:inherit}
img{display:block;max-width:100%}
a{color:inherit}

.icon{width:24px;height:24px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex:none}
.muted{color:var(--md-sys-color-on-surface-variant);font-size:.85rem}

/* ===================== APPBAR ===================== */
/* height/z-index — как в остальных сервисах BurningHouse (см. Movies) —
   .table-screen ниже вычитает высоту шапки через calc(), проверено, что
   64px укладывается в уже заложенный запас (4.6rem). */
.appbar{position:sticky;top:0;z-index:20;height:64px;backdrop-filter:blur(10px);
  background:color-mix(in srgb, var(--md-sys-color-surface) 78%, transparent);
  border-bottom:1px solid var(--md-sys-color-outline-variant)}
.appbar-inner{max-width:76rem;height:100%;margin:0 auto;padding:0 1.1rem;display:flex;align-items:center;gap:1rem}
.spacer{flex:1}
/* Кнопка публикации в шапке (см. index.html) — текстовая, а не иконка:
   единственное действие в шапке, которое иначе терялось бы среди одинаковых
   значков. На узких экранах подпись прячем и оставляем кружок с иконкой:
   рядом лежит лого с названием сервиса, и кнопка с текстом выдавливала бы
   остальную навигацию за край. */
/* Цифра непрочитанных на кнопке аккаунта. Сидит ВНУТРИ границ кнопки, а не
   свешивается за угол, как это обычно делают: у .icon-btn стоит
   overflow:hidden (ради собственной круглой подсветки), и вылезающий бейдж
   она бы просто обрезала. pointer-events:none — чтобы цифра не перехватывала
   клик по самой кнопке. Красный — тот же, что у бейджа в модалке
   (.account-notif-badge), это один и тот же счётчик в двух местах. */
.appbar-notif-badge{
  position:absolute;top:.3rem;right:.3rem;min-width:1.05rem;height:1.05rem;padding:0 .25rem;
  border-radius:var(--md-sys-shape-corner-full);
  background:var(--md-sys-color-error);color:var(--md-sys-color-on-error);
  font-size:.66rem;font-weight:800;line-height:1.05rem;text-align:center;pointer-events:none;
}
.appbar-notif-badge[hidden]{display:none}
.appbar-publish{flex:none;white-space:nowrap}
.appbar-publish .icon{width:1.1rem;height:1.1rem}
@media(max-width:34rem){
  .appbar-publish{padding:.5em;border-radius:50%}
  .appbar-publish #appbarPublishText{display:none}
  /* Даже свёрнутая в кружок кнопка шире обычной .icon-btn (у неё своя
     заливка и паддинги) — на 375px шапка из-за неё переставала помещаться и
     срезала иконку аккаунта у правого края. Поджимаем шаг и поля самой
     шапки: это дешевле, чем прятать что-то из навигации. */
  .appbar-inner{gap:.45rem;padding:0 .7rem}
}
.auth-area{display:flex;align-items:center;gap:.6rem}
.auth-user{display:flex;align-items:center;gap:.5rem;font-size:.92rem}
.auth-user-name{font-weight:600}

/* Модалка аккаунта — как в остальных сервисах BurningHouse (Movies и
   т.д.), дословно оттуда же (иконка-кружок, центральная модалка вместо
   дропдауна). Тема — теперь текстовой кнопкой внутри этой модалки, не
   отдельным значком в шапке (см. правку «Тема и обучение — в окно
   "Аккаунт"»), своего .icon-btn у неё больше нет. */
.icon-btn{
  position:relative;width:44px;height:44px;flex:none;padding:0;border-radius:var(--md-sys-shape-corner-full);
  border:0;background:transparent;color:var(--md-sys-color-on-surface-variant);
  cursor:pointer;display:grid;place-items:center;overflow:hidden;
}
/* display:grid выше бьёт спецификой стандартный [hidden]{display:none} из
   UA-стилей — без этого правила скрытая через атрибут hidden кнопка (см.
   #installBtn) всё равно остаётся видимой и кликабельной. */
.icon-btn[hidden]{display:none}
.icon-btn::after{content:"";position:absolute;inset:0;background:currentColor;opacity:0;transition:opacity .2s}
.icon-btn:hover::after{opacity:.08}
.icon-btn:active::after{opacity:.12}
.icon-btn.tonal{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container)}
/* #langBtn — текстом (RU/EN), не иконкой (см. план «Английский язык в
   интерфейсе», index.html) — тому же .icon-btn нужен свой размер/вес
   шрифта, у остальных кнопок этого ряда контент — <svg>, тут plain text. */
#langBtn{font-size:.78rem;font-weight:700;letter-spacing:.02em}
/* Знак вопроса обучения стола (#tableHelpBtn — единственный, что остался,
   #libraryHelpBtn переехал в модалку «Аккаунт» текстовой кнопкой, см. выше)
   — тот же plain-text приём, что у #langBtn выше, тому же .icon-btn нужен
   свой вес — голый "?" в обычном тексте на кружке выглядел тонковато и
   терялся. */
#tableHelpBtn{font-size:1.15rem;font-weight:800}
/* Компактный размер для инлайновых точечных действий (крестик "Удалить" в
   карточке пазла/строке истории/карточке активной сессии) — полноразмерный
   44px там слишком тяжёл рядом с мелким текстом строки. */
.icon-btn.xs{width:1.7rem;height:1.7rem}
.icon-btn.xs .icon{width:16px;height:16px}

.menu-wrap{position:relative;flex:none}
.menu-user{
  padding:.5em .8em .6em;border-bottom:1px solid var(--md-sys-color-outline-variant);
  margin-bottom:.3rem;display:flex;flex-direction:column;gap:.1rem;
}
.menu-user b{font-weight:650}
#accountModalBackdrop .menu-user{padding:0 0 .2rem;margin-bottom:0;border-bottom:0}
#accountModalBackdrop .btn{margin-top:.5rem}
/* Ведущая иконка в кнопках модалки (см. правку «Переделать окно Аккаунт») —
   дефолтные 24px (.icon) рядом с текстом кнопки выглядят слишком крупно,
   чуть уменьшаем именно тут, не трогая .icon глобально (используется и как
   самостоятельная иконка покрупнее в других местах). */
#accountModalBackdrop .btn .icon{width:20px;height:20px}
/* Секции модалки «Аккаунт» (см. правку «Переделать окно Аккаунт») — тот же
   приём, что у Movies/assets/styles.css (#accountModalBackdrop .modal-
   section-label) — маленькая подпись с верхней чертой вместо одного общего
   .modal-sep на всю модалку: раньше уведомления/тема/обучение/управление
   шли плоским стопкom кнопок почти без визуальной иерархии. */
#accountModalBackdrop .modal-section-label{
  font-size:12px;color:var(--md-sys-color-on-surface-variant);
  margin-top:1.2rem;padding-top:.9rem;border-top:1px solid var(--md-sys-color-outline-variant);
}
/* Первая секция сразу после карточки аккаунта — своей верхней черты не
   нужно, .menu-user уже визуально закрывает эту роль (см. правило выше). */
#accountModalUser .modal-section-label:first-of-type{margin-top:.3rem;padding-top:0;border-top:0}
/* Уведомления (см. план «Системные уведомления Auth → Puzzle», правку
   «Уведомления за колокольчик, Выйти вниз») — список свёрнут за кнопкой-
   колокольчиком, не развёрнут всегда, как было раньше. Кнопка — не .btn
   (тем нужен justify-content:center, тут — колокольчик+текст слева,
   бейдж+шеврон справа, .space-between). */
.account-notif-toggle{
  width:100%;margin-top:.3rem;display:flex;align-items:center;justify-content:space-between;gap:.6rem;
  padding:.6em 1.1em;border-radius:var(--md-sys-shape-corner-full);
  border:1px solid var(--md-sys-color-outline-variant);background:transparent;color:inherit;
  font:inherit;font-weight:600;cursor:pointer;
}
.account-notif-toggle:hover{background:color-mix(in srgb, var(--md-sys-color-on-surface) 6%, transparent)}
.account-notif-toggle-label,.account-notif-toggle-right{display:flex;align-items:center;gap:.5rem}
.account-notif-toggle .icon{width:20px;height:20px;flex:none}
.account-notif-toggle .chevron{transition:transform .2s var(--ease-std)}
.account-notif-toggle[aria-expanded="true"] .chevron{transform:rotate(180deg)}
/* Бейдж непрочитанных (см. loadAccountNotifications в app.js) — та же
   красная таблетка-счётчик, что везде в интерфейсах для этого паттерна,
   .btn.danger тут не подходит — это не кнопка с действием, просто цифра. */
.account-notif-badge{
  min-width:1.3em;height:1.3em;padding:0 .35em;border-radius:999px;
  background:var(--md-sys-color-error);color:var(--md-sys-color-on-error);
  font-size:.72rem;font-weight:700;line-height:1.3em;text-align:center;
}
#accountNotificationsList{display:flex;flex-direction:column;gap:.4rem;max-height:14rem;overflow:auto;margin-top:.5rem}
#accountNotificationsList[hidden]{display:none}
.notification-item{
  display:block;text-decoration:none;color:inherit;padding:.5em .7em;
  border-radius:var(--md-sys-shape-corner-m);background:var(--md-sys-color-surface-container-low);
  transition:background .15s var(--ease-std);
}
a.notification-item{cursor:pointer}
a.notification-item:hover{background:var(--md-sys-color-surface-container-high)}
.notification-item.unread{background:var(--md-sys-color-tertiary-container)}
.notification-title{font-size:.86rem;font-weight:600}
.notification-item:not(.unread) .notification-title{font-weight:500}
.notification-body{font-size:.8rem;color:var(--md-sys-color-on-surface-variant);margin-top:.15rem}

/* ===================== МОДАЛКИ ===================== */
.modal-backdrop{position:fixed;inset:0;z-index:30;background:rgba(0,0,0,.5);
  display:flex;align-items:center;justify-content:center;padding:1.2rem}
.modal-backdrop.hidden{display:none}
.modal{
  background:var(--md-sys-color-surface-container-low);
  border-radius:var(--md-sys-shape-corner-xl);
  width:100%;max-width:30rem;max-height:85vh;overflow:auto;
  padding:1.1rem 1.3rem 1.3rem;box-shadow:var(--e2);
  scrollbar-width:thin;scrollbar-color:var(--md-sys-color-outline-variant) transparent;
}
.modal-head{display:flex;justify-content:space-between;align-items:center;gap:.8rem}
.modal-head h2{margin:0}
/* Модалка «Добавить пазл» в комнате (renderRoom) — шире обычной, чтобы
   сетка карточек библиотеки (см. .add-puzzle-grid ниже) не сжималась в
   одну узкую колонку. */
.modal.wide{max-width:46rem}
/* Сегментированный переключатель («Из библиотеки» / «Загрузить своё
   фото», см. renderRoom) — не две отдельные кнопки с зазором (читалось как
   случайная пара, не как вкладки), а один цельный трек с бегающей заливкой
   активного сегмента — узнаваемый паттерн табов, без лишней рамки. */
.modal-tabs{
  display:flex;gap:.2rem;margin:.9rem 0 1.1rem;padding:.25rem;
  background:var(--md-sys-color-surface-container);border-radius:var(--md-sys-shape-corner-full);
}
.modal-tabs .btn{
  flex:1;border:0;background:transparent;color:var(--md-sys-color-on-surface-variant);
  transition:background .15s var(--ease-std),color .15s var(--ease-std);
}
.modal-tabs .btn.active{background:var(--md-sys-color-surface-container-lowest);color:var(--md-sys-color-on-surface);box-shadow:var(--e1)}
/* Модалка обратной связи (см. правку «Форма обратной связи») — короткое
   пояснение над формой, тем же приглушённым тоном, что и остальные
   вспомогательные подписи в модалках. */
.modal-hint{color:var(--md-sys-color-on-surface-variant);font-size:.88rem;margin:.6rem 0 1rem}
.feedback-form{display:flex;flex-direction:column;gap:.7rem}
.feedback-form textarea.text-input{width:100%;box-sizing:border-box;resize:vertical;min-height:6rem;font-family:inherit}
.feedback-form .state-note{margin:0}
.add-puzzle-category-row{display:flex;align-items:center;gap:.6rem;margin-bottom:.9rem}
.add-puzzle-category-row select{min-width:12rem}
.add-puzzle-grid{display:grid;gap:.8rem;grid-template-columns:repeat(auto-fill,minmax(11rem,1fr))}
.add-puzzle-card{
  display:flex;flex-direction:column;gap:.4rem;border:1px solid var(--md-sys-color-outline-variant);
  border-radius:var(--md-sys-shape-corner-l);padding:.6rem;background:var(--md-sys-color-surface);
}
.add-puzzle-card img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--md-sys-shape-corner-m)}
.add-puzzle-card .title{font-size:.85rem;line-height:1.25}
.add-puzzle-card .btn{align-self:stretch}
.bar{display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;margin-top:1rem}
.bar .text-input{flex:1;width:auto;min-width:0}
/* «За стол» в модалке истории (см. app.js, openHistoryModal) — margin-left:
   auto, не просто расчёт на justify-content:space-between выше: «Страница
   пазла» рядом иногда скрыта (чужое/неопубликованное фото, см. правку про
   404 не автору) — с одним-единственным видимым элементом space-between
   прижимал бы его к ЛЕВОМУ краю (см. правку «Кнопка "за стол" направо»).
   auto-маргин у flex-элемента съедает свободное место ДО justify-content,
   поэтому кнопка липнет к правому краю что с соседом, что без него. */
#historyModalPlayBtn{margin-left:auto}
.checkbox-row{
  display:flex;align-items:center;gap:.6rem;margin-top:.9rem;font-size:.88rem;
  color:var(--md-sys-color-on-surface-variant);cursor:pointer;
}
.checkbox-row input{width:1.1rem;height:1.1rem;accent-color:var(--flame-tip);cursor:pointer}

/* ===================== BUTTONS ===================== */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:.4em;
  border:0;border-radius:var(--md-sys-shape-corner-full);
  padding:.6em 1.3em;font-weight:600;font-size:.92rem;cursor:pointer;
  transition:transform .15s var(--ease-std),box-shadow .15s var(--ease-std),background-color .15s var(--ease-std);
  white-space:nowrap;
  /* .btn вешается и на <a> (кнопка публикации в шапке, «Опубликовать пазл»
     на профиле, «Страница пазла» в истории) — браузер подчёркивает такие
     ссылки по умолчанию, и кнопка выглядела кнопкой с подчёркнутой
     надписью. Отдельные компоненты гасили это у себя поштучно; правило
     принадлежит самой .btn — подчёркнутых кнопок не бывает. */
  text-decoration:none;
}
/* .btn{display:inline-flex} у автора-стиля перебивает встроенное правило
   браузера [hidden]{display:none} (то же имя специфичности — выигрывает тот,
   кто загружен позже, а не UA-стиль) — без этого правила hidden="" на любой
   .btn-кнопке визуально ничего не даёт (был баг именно на этом: гостевая
   кнопка входа в шапке скрывалась атрибутом, но .btn{display:inline-flex}
   всё равно её показывал). Тот же приём уже точечно применён у
   .presence-count[hidden] ниже. */
.btn[hidden]{display:none}
.btn:active{transform:scale(.97)}
.btn.filled{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);box-shadow:var(--e1)}
.btn.filled:hover{box-shadow:var(--e2)}
.btn.tonal{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container)}
.btn.outlined{background:transparent;color:var(--md-sys-color-primary);border:1px solid var(--md-sys-color-outline)}
.btn.text{background:transparent;color:var(--md-sys-color-primary)}
.btn.sm{padding:.42em 1em;font-size:.84rem}
.btn.icon{padding:.5em;border-radius:50%}
.btn.full{width:100%}
.btn.danger{color:var(--md-sys-color-error)}
.btn.outlined.danger{border-color:color-mix(in srgb, var(--md-sys-color-error) 45%, var(--md-sys-color-outline))}
.btn:disabled{opacity:.5;cursor:default;pointer-events:none}

/* ===================== CONTENT ===================== */
.content{max-width:76rem;margin:0 auto;padding:1.4rem 1.1rem 4rem}

/* ===================== ФУТЕР ===================== */
/* Вне #app (см. index.html) — не перерисовывается роутером, живёт на
   странице постоянно. surface-container-low — тем же тоном, что карточки
   FAQ внутри него (.faq details ниже), лёгкое обособление от фона без
   отдельной ещё одной поверхности. */
.bh-footer{margin-top:3rem;border-top:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-low)}
.bh-footer-inner{max-width:76rem;margin:0 auto;padding:2.4rem 1.1rem 2rem}
.bh-footer-top{display:flex;flex-wrap:wrap;gap:2.4rem 3rem;margin-bottom:2rem}
.bh-footer-brand{flex:2 1 16rem;min-width:14rem}
.bh-footer-brand .bh-lockup{margin-bottom:.6rem}
.bh-footer-brand p{color:var(--md-sys-color-on-surface-variant);font-size:.9rem;max-width:22rem}
.bh-footer-links{flex:1 1 11rem;min-width:9rem;display:flex;flex-direction:column;gap:.55rem}
.bh-footer-links h3{font-size:.78rem;font-weight:650;text-transform:uppercase;letter-spacing:.04em;
  color:var(--md-sys-color-on-surface-variant);margin-bottom:.2rem}
.bh-footer-links a{color:var(--md-sys-color-on-surface);text-decoration:none;font-size:.92rem;width:fit-content}
.bh-footer-links a:hover{color:var(--md-sys-color-primary);text-decoration:underline}
/* Кнопка «Обратная связь» (см. правку «Форма обратной связи») сидит в том
   же списке, что и обычные ссылки-<a> выше — открывает модалку, а не
   ведёт на URL, поэтому это <button>, но должна выглядеть неотличимо:
   сброс браузерных стилей кнопки + повтор правил .bh-footer-links a. */
.bh-footer-links button{
  background:none;border:0;padding:0;font:inherit;cursor:pointer;text-align:left;
  color:var(--md-sys-color-on-surface);width:fit-content;
}
.bh-footer-links button:hover{color:var(--md-sys-color-primary);text-decoration:underline}
.bh-footer .faq{margin-top:0;padding-top:1.8rem;border-top:1px solid var(--md-sys-color-outline-variant)}
/* Переключатель языка переехал сюда из шапки (см. план «Выдвигающиеся
   кнопки») — не первоочередная настройка, рядом с копирайтом ему самое
   место, не в основной панели навигации. */
.bh-footer-bottom{display:flex;align-items:center;justify-content:space-between;gap:.8rem;margin-top:1.8rem}
.bh-footer-copy{color:var(--md-sys-color-on-surface-variant);font-size:.82rem}

/* ===================== ЗАГОЛОВОК БИБЛИОТЕКИ ===================== */
/* .6rem, не больше — «Продолжить сборку» сразу под ней несёт СВОЙ
   margin-top:1.2rem (.room-section-title), два отступа стакались в
   заметно пустой промежуток между текстом и заголовком секции. */
.library-head{margin-bottom:.6rem}
.library-head h1{font-size:1.6rem;font-weight:650;letter-spacing:-.01em}
.library-head p{color:var(--md-sys-color-on-surface-variant);margin-top:.3rem;
  text-align:justify;-webkit-hyphens:auto;hyphens:auto}

/* «Продолжить сборку» над библиотекой (см. план, renderInProgressList в
   app.js) — :not(:empty), потому что renderInProgressList ничего не рисует
   в #inProgressWrap, если незавершённых сборок нет, и пустой div без этого
   правила не добавлял бы отступ, но и не мешал бы — а вот когда список
   ЕСТЬ, снизу до карусели категорий не хватало воздуха (см. план). */
#inProgressWrap:not(:empty){margin-bottom:2rem}

/* Комнаты на главной (см. правку «Комнаты на главной») — [hidden] сам даёт
   display:none, когда комнат нет (см. mountRoomsSection/visibilityEl в
   app.js), поэтому :not(:empty) тут не нужен, в отличие от #inProgressWrap
   выше — margin-bottom просто не рендерится вместе с самим блоком. Без
   этого отступа карточки комнат утыкались прямо в чипы категорий следом. */
#roomsSectionOuter{margin-bottom:2rem}

/* Карусель категорий над сеткой библиотеки (см. план «Категории пазлов») —
   тот же вид плашек, что у .member-chip, но крупнее и кликабельно: это
   переключатель фильтра, а не статус. */
.category-carousel{display:flex;flex-wrap:wrap;gap:.6rem;margin-bottom:1.2rem}
/* Сортировка — своя строка НАД сеткой (см. renderLibrary, план «Рейтинг на
   карточке» → сортировка), отдельно от карусели категорий: это разные оси
   (что показывать vs в каком порядке), сливать их в один ряд компонентов
   было бы путаницей. select — .text-input, тот же язык, что у остальных
   select-ов в проекте (см. публикацию/категории), ширина по контенту (нет
   width:100%), тут это к месту — строка и так короткая. */
/* Та же ловушка, что у .puzzle-page-author-badge ниже: строка по умолчанию
   помечена hidden и раскрывается, только если хоть у одного пазла есть
   оценка (см. renderLibrary), но display:flex перебивал бы атрибут — и
   пустая сортировка висела бы на странице всегда. */
.library-sort-row[hidden]{display:none}
.library-sort-row{display:flex;align-items:center;gap:.6rem;margin-bottom:1.2rem;font-size:.9rem;color:var(--md-sys-color-on-surface-variant)}
.library-sort-row select{font-size:.88rem;padding:.4em .7em}
.category-chip{
  flex:none;display:inline-flex;align-items:center;
  background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);
  font-size:.88rem;font-weight:600;padding:.5em 1em;border-radius:var(--md-sys-shape-corner-full);
  border:0;cursor:pointer;white-space:nowrap;transition:background .15s,color .15s;
}
.category-chip.is-active{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary)}
/* На телефонах — не сразу все строки чипов, только 2, остальное за кнопкой
   «Показать ещё» (см. план «Категории на телефонах», updateToggleVisibility
   в app.js — та же цифра 6.4rem, что и тут, иначе JS считал бы порог не
   от той высоты, что реально обрезает CSS). .expanded снимает потолок —
   ставит сама кнопка по клику. */
@media (max-width: 30rem){
  .category-carousel:not(.expanded){max-height:6.4rem;overflow:hidden}
}
.category-carousel-toggle{
  display:none;width:100%;justify-content:center;margin:-.6rem 0 1.2rem;
  background:none;border:0;color:var(--md-sys-color-primary);font-size:.85rem;font-weight:650;
  padding:.4rem;cursor:pointer;
}
.category-carousel-toggle.is-visible{display:flex}

/* Блоки на отдельной странице /categories (см. план «Прямые ссылки +
   страница категорий») — крупнее и весомее .category-chip: там это был
   узкий переключатель фильтра рядом с сеткой, здесь — единственное
   содержимое страницы, сама навигация к каждой категории. Обложка (см.
   план «Обложка категории») — тот же приём превью, что у .puzzle-card-thumb
   (aspect-ratio + overflow:hidden), просто с текстовой панелью под ней
   вместо кнопки «За стол». */
.category-block-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:1rem}
.category-block{
  display:flex;flex-direction:column;overflow:hidden;border-radius:var(--md-sys-shape-corner-l);
  background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);
  text-decoration:none;box-shadow:var(--e1);transition:transform .15s,box-shadow .15s;
}
.category-block:hover{transform:translateY(-.15rem);box-shadow:var(--e2)}
.category-block-thumb{aspect-ratio:4/3;background:var(--md-sys-color-surface-container-high);overflow:hidden}
.category-block-thumb img{width:100%;height:100%;object-fit:cover}
.category-block-body{display:flex;flex-direction:column;gap:.3rem;padding:.85rem 1.1rem}
.category-block-name{font-size:1.1rem;font-weight:650}
.category-block-count{color:var(--md-sys-color-on-surface-variant);font-size:.88rem}

/* Серверная заглушка /categories до отработки JS (см. server.js,
   categoriesListHtml) — обложка тут только чтобы не остаться голым битым
   img до замены на .category-block выше, самого списка пользователь
   почти никогда не видит. */
.category-server-list img{width:2.2rem;height:2.2rem;object-fit:cover;border-radius:.4rem;vertical-align:middle;margin-right:.5rem}

/* ===================== КАРТОЧКИ ПАЗЛОВ ===================== */
.puzzle-grid{display:grid;gap:1.1rem;grid-template-columns:repeat(auto-fill,minmax(15.5rem,1fr))}
/* position:relative + БЕЗ overflow:hidden на самой карточке — якорь для
   .card-menu-wrap (кнопка «…» поверх превью, см. buildCard) и его
   выпадающего .menu, который иначе обрезался бы (тот же баг и то же
   решение, что в Movies — см. комментарий у .puzzle-card-thumb ниже,
   зеркалит .movie-tile{overflow:visible} + .movie-tile-poster-wrap). */
.puzzle-card{
  position:relative;background:var(--md-sys-color-surface-container-low);
  border-radius:var(--md-sys-shape-corner-l);box-shadow:var(--e1);display:flex;flex-direction:column;
  cursor:pointer; /* вся карточка кликабельна — открывает превью, см. buildCard */
  transition:transform .18s var(--ease-emph),box-shadow .18s;
}
/* Наведение/клавиатурный фокус — тот же приём, что .movie-tile в Movies
   (см. buildCard, role=button/tabIndex/keydown): карточка САМА не <button>,
   поэтому фокусируемость и подсветку даёт role=button + свой :focus-visible,
   не браузерный дефолт кнопки. */
.puzzle-card:hover{transform:translateY(-2px);box-shadow:var(--e2)}
.puzzle-card:focus-visible{outline:2px solid var(--md-sys-color-primary);outline-offset:2px}
/* Карточка без открытия (см. buildCard, openable:false — блок «На
   модерации» в профиле): показывает, но никуда не ведёт — курсор и подъём
   при наведении обещали бы клик, которого нет. Меню «…» внутри работает
   как обычно, поэтому карточку не гасим целиком. */
.puzzle-card.is-inert{cursor:default}
.puzzle-card.is-inert:hover{transform:none;box-shadow:var(--e1)}
/* Свои заявки на модерации над сеткой одобренного (см. renderProfile) —
   визуально отдельный блок, а не ещё один ряд той же сетки: иначе
   непроверенное читалось бы как уже опубликованное. */
.profile-pending{
  margin:0 0 1.6rem;padding:1rem;border-radius:var(--md-sys-shape-corner-l);
  background:var(--md-sys-color-surface-container-low);
  border:1px dashed var(--md-sys-color-outline-variant);
}
.profile-pending h2{margin:0 0 .2rem;font-size:1.05rem}
.profile-pending .state-note{margin:0 0 .9rem;padding:0;text-align:left}
/* Явно, на будущее: стоит кому-нибудь дописать сюда display — и встроенное
   [hidden]{display:none} браузера молча проиграет (тот же баг-класс, что у
   .btn[hidden]/.library-sort-row[hidden] выше, ловленный тут уже не раз). */
.profile-pending[hidden]{display:none}
/* Скругление верхних углов — теперь СВОЁ (border-radius + overflow:hidden
   именно на превью), не унаследованное от overflow:hidden всей карточки:
   картинка edge-to-edge всё ещё должна обрезаться по углам, но выпадающее
   меню (см. .card-menu-wrap) — не должно, а раньше оба клипались одним и
   тем же overflow:hidden на .puzzle-card. */
.puzzle-card-thumb{
  position:relative;aspect-ratio:4/3;background:var(--md-sys-color-surface-container-high);
  overflow:hidden;border-radius:var(--md-sys-shape-corner-l) var(--md-sys-shape-corner-l) 0 0;
}
.puzzle-card-thumb img{width:100%;height:100%;object-fit:cover}
/* «Не для детей» (см. правку «Возрастное подтверждение») — размывается ТОЛЬКО
   сама картинка: класс висит на .puzzle-card-thumb, но фильтр применяется к
   вложенной <img>, иначе замылилась бы и плашка поверх неё. Заметный радиус
   (18px) — чтобы сюжет действительно не читался, а не «почти видно».
   transition — чтобы после подтверждения возраста картинка проявлялась, а не
   дёргалась (класс снимается на месте, без перерисовки сетки). */
.puzzle-card-thumb.is-blurred img{filter:blur(18px);transform:scale(1.06);transition:filter .3s var(--ease-std)}
.puzzle-card-agegate{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  padding:.35em .9em;border-radius:var(--md-sys-shape-corner-full);
  background:color-mix(in srgb, var(--md-sys-color-inverse-surface) 86%, transparent);
  color:var(--md-sys-color-inverse-on-surface);
  font-size:.78rem;font-weight:700;white-space:nowrap;pointer-events:none;
}
.puzzle-page-image.is-blurred{filter:blur(24px);transition:filter .3s var(--ease-std)}
/* Модалка подтверждения возраста. Всё по центру одной колонкой: тут короткий
   вопрос и два равнозначных ответа, а не форма — левый край и разведённые по
   углам кнопки читались бы как «служебное окно». Значок 18+ вместо иконки:
   он и есть само сообщение, объяснять картинкой нечего. Тёплый акцент
   (--flame-tip) вместо красного: это не ошибка и не запрет, а развилка. */
.age-modal{
  position:relative;max-width:22rem;
  display:flex;flex-direction:column;align-items:center;text-align:center;gap:.7rem;
  padding:2.2rem 1.6rem 1.6rem;
}
.age-modal h2{margin:0;font-size:1.15rem}
.age-modal p{margin:0;color:var(--md-sys-color-on-surface-variant)}
.age-modal .bar{justify-content:center;gap:.6rem;margin-top:.4rem;width:100%}
.age-modal-badge{
  display:grid;place-items:center;width:3.4rem;height:3.4rem;border-radius:50%;
  background:color-mix(in srgb, var(--flame-tip) 16%, transparent);
  border:2px solid color-mix(in srgb, var(--flame-tip) 55%, transparent);
  color:var(--flame-tip);font-size:1.05rem;font-weight:800;letter-spacing:-.02em;
}
.age-modal-close{position:absolute;top:.4rem;right:.4rem}
/* top-LEFT, не right — правый верхний угол занят кнопкой «…» (.card-menu-wrap,
   см. ниже), которая теперь есть почти на каждой карточке библиотеки. */
.puzzle-card-badge{
  position:absolute;top:.6rem;left:.6rem;background:color-mix(in srgb, var(--md-sys-color-inverse-surface) 82%, transparent);
  color:var(--md-sys-color-inverse-on-surface);font-size:.76rem;font-weight:700;padding:.28em .7em;border-radius:var(--md-sys-shape-corner-full);
}
.puzzle-card-badge.done{background:var(--md-ok);color:#fff}
.puzzle-card-body{padding:.9rem 1rem 1.1rem;display:flex;flex-direction:column;gap:.5rem;flex:1}
/* «За стол» — справа от названия, в одну строку (см. план): заголовок
   обрезается многоточием, если не помещается, кнопка — фиксированного
   размера, не сжимается. */
.puzzle-card-title-row{display:flex;align-items:center;justify-content:space-between;gap:.7rem}
.puzzle-card-title{font-size:1.05rem;font-weight:650;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.puzzle-card-meta{font-size:.85rem;color:var(--md-sys-color-on-surface-variant);flex:1}
/* Звезда — тем же акцентом, что .star.filled на странице пазла (see выше),
   число — обычным текстом на surface, тот же язык, что .puzzle-page-
   rating-summary там же, просто компактнее под карточку. flex:none —
   не сжимается, как и .puzzle-card-play рядом (title-row делит место
   через flex:1 у самого заголовка, см. выше). */
.puzzle-card-rating{flex:none;font-size:.85rem;font-weight:600;color:var(--flame-tip);white-space:nowrap}
.puzzle-card-rating b{color:var(--md-sys-color-on-surface);font-weight:700}
.puzzle-card-play{flex:none}
/* Статус модерации своего фото (см. план «Модерация загруженных фото») —
   текстовая строка в теле карточки, не путать с .puzzle-card-badge (та —
   про прогресс сборки, поверх превью). */
.puzzle-card-moderation{font-size:.8rem;font-weight:600}
.puzzle-card-moderation.pending{color:var(--md-warn)}
.puzzle-card-moderation.approved{color:var(--md-ok)}
.puzzle-card-moderation.rejected{color:var(--md-sys-color-error)}

/* Атрибуция (см. план «Категории many-to-many, автор карточки, профиль»)
   — мелкая приглушённая ссылка, отдельной строкой под .puzzle-card-meta.
   Показывается только у ОДОБРЕННЫХ публикаций (см. buildCard) — на своём
   же приватном/ждущем модерации фото она была бы преждевременной: это
   элемент публичной библиотеки, а не «черновика». */
.puzzle-card-author{font-size:.8rem;color:var(--md-sys-color-on-surface-variant);text-decoration:underline;align-self:flex-start}
.puzzle-card-author:hover{color:var(--md-sys-color-primary)}

/* Меню «…» карточки — реверанс Movies (renderCardMenu/.menu-wrap/.menu/
   .menu-item): второстепенные действия (Удалить/Скрыть, Опубликовать,
   + В комнату), которые раньше были отдельными кнопками и не влезали в
   узкую карточку, теперь собраны в один выпадающий список. .menu-wrap —
   тот же класс, что и у #accountMenuWrap в шапке (position:relative;
   flex:none по умолчанию), тут переопределяем позицию поверх правого
   верхнего угла превью тем же приёмом, что у Movies
   (.movie-tile-poster-wrap .menu-wrap). Кнопка видна только при
   наведении/фокусе (кроме тач-устройств, там hover не работает). */
.puzzle-card>.menu-wrap{position:absolute;top:.5rem;right:.5rem;z-index:2}
.puzzle-card>.menu-wrap>.icon-btn{
  background:color-mix(in srgb, var(--md-sys-color-inverse-surface) 82%, transparent);
  color:var(--md-sys-color-inverse-on-surface);opacity:0;transition:opacity .15s var(--ease-std);
}
.puzzle-card:hover>.menu-wrap>.icon-btn,
.puzzle-card:focus-within>.menu-wrap>.icon-btn,
.puzzle-card>.menu-wrap:has(.menu:not([hidden]))>.icon-btn{opacity:1}
@media (hover:none){
  .puzzle-card>.menu-wrap>.icon-btn{opacity:1}
}
/* Удаление прогресса на карточке «Продолжить сборку» (см. план,
   buildInProgressCard в app.js) — тот же приём наведения/фокуса, что у
   .menu-wrap выше, но без выпадающего меню: тут только одно действие,
   отдельная кнопка вместо .icon-btn внутри .menu-wrap. */
.puzzle-card>.progress-card-delete{
  position:absolute;top:.5rem;right:.5rem;z-index:2;
  background:color-mix(in srgb, var(--md-sys-color-inverse-surface) 82%, transparent);
  color:var(--md-sys-color-inverse-on-surface);opacity:0;transition:opacity .15s var(--ease-std);
}
.puzzle-card:hover>.progress-card-delete,
.puzzle-card:focus-within>.progress-card-delete{opacity:1}
@media (hover:none){
  .puzzle-card>.progress-card-delete{opacity:1}
}
.menu{
  position:absolute;right:0;top:calc(100% + 6px);min-width:11.5rem;z-index:10;
  background:var(--md-sys-color-surface-container-high);border:1px solid var(--md-sys-color-outline-variant);
  border-radius:var(--md-sys-shape-corner-m);box-shadow:var(--e2);overflow:hidden;padding:6px;
}
.menu-item{
  display:block;width:100%;text-align:left;background:transparent;border:0;color:inherit;
  font:inherit;font-weight:500;font-size:.85rem;padding:.6em .8em;border-radius:var(--md-sys-shape-corner-s);
  cursor:pointer;text-decoration:none;
}
.menu-item:hover{background:var(--md-sys-color-surface-container)}
.menu-item.danger{color:var(--md-sys-color-error)}

/* ===================== FAQ (см. план «SEO») ===================== */
/* Нативные <details>/<summary> — раскрывается без единой строчки JS,
   индексируется поисковиками и без свёрнутого-по-умолчанию содержимого. */
.faq{margin-top:2.4rem;padding-top:1.6rem;border-top:1px solid var(--md-sys-color-outline-variant)}
.faq h2{font-size:1.25rem;font-weight:650;margin-bottom:.9rem}
.faq details{
  background:var(--md-sys-color-surface-container-low);border-radius:var(--md-sys-shape-corner-m);
  padding:.2rem .9rem;margin-bottom:.6rem;
}
.faq summary{
  padding:.8rem 0;font-weight:600;cursor:pointer;list-style:none;
  display:flex;align-items:center;justify-content:space-between;gap:.8rem;
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:"";flex:none;width:.6rem;height:.6rem;border-right:2px solid currentColor;border-bottom:2px solid currentColor;
  transform:rotate(45deg);transition:transform .15s var(--ease-std);
}
.faq details[open] summary::after{transform:rotate(-135deg)}
.faq details p{padding:0 0 .9rem;color:var(--md-sys-color-on-surface-variant);font-size:.92rem}

/* ===================== ЗАГРУЗКА СВОЕГО ФОТО =====================
   Раньше — отдельная карточка со своим фоном/тенью (форма стояла прямо на
   странице комнаты); теперь mountUploadForm вызывается только внутри
   модалки «Добавить пазл» (см. renderRoom) — своя карточка внутри уже
   оформленной модалки того же цвета (surface-container-low у обеих)
   только добавляла пустое обрамление без визуальной разницы, убрана. */
.upload-form{display:flex;flex-direction:column;gap:.8rem}
.upload-form .btn{align-self:flex-start}
/* Нативная кнопка «Выберите файл» (input[type=file]) в браузерном виде по
   умолчанию почти теряется рядом с обычными .btn — приводим её к тому же
   языку через ::file-selector-button (стандартный псевдоэлемент), не через
   свою обёртку label+скрытый input: тут это единственное такое поле,
   заводить отдельный компонент не окупается. Залитая, тон в тон с
   .btn.filled — это первый обязательный шаг формы (без файла нечего
   собирать), должна цеплять взгляд не меньше, чем сам сабмит внизу; была
   пробована приглушённая .outlined-версия — по отзыву всё равно терялась. */
#uploadFile{font:inherit;font-size:.85rem;color:var(--md-sys-color-on-surface-variant)}
#uploadFile::file-selector-button{
  display:inline-flex;align-items:center;justify-content:center;gap:.4em;
  border:0;border-radius:var(--md-sys-shape-corner-full);
  padding:.6em 1.3em;font-weight:600;font-size:.92rem;cursor:pointer;
  background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);
  box-shadow:var(--e1);margin-right:.7rem;
  transition:box-shadow .15s var(--ease-std);
}
#uploadFile::file-selector-button:hover{box-shadow:var(--e2)}
/* Согласие + запрещённые категории (см. план «Модерация загруженных фото») —
   внутри .upload-form, перед кнопкой сабмита. */
.upload-consent{
  background:var(--md-sys-color-surface-container);border-radius:var(--md-sys-shape-corner-m);
  padding:.8rem .9rem;display:flex;flex-direction:column;gap:.5rem;
}
.upload-consent-title{font-size:.85rem;font-weight:600}
.upload-consent-list{margin:0;padding-left:1.2rem;font-size:.82rem;color:var(--md-sys-color-on-surface-variant);display:flex;flex-direction:column;gap:.15rem}
/* Сам чекбокс — заметнее (см. правку «Галочка согласия заметнее»): это
   единственное место, где человек формально берёт на себя ответственность
   за контент, при этом раньше выглядел как обычный мелкий нативный
   чекбокс среди прочего мелкого текста, легко проскочить не глядя.
   Крупнее + акцентный цвет вместо цвета браузера по умолчанию + текст
   полужирный, тем же приёмом, что и .upload-consent-title выше. */
.upload-consent-check{display:flex;align-items:flex-start;gap:.6rem;font-size:.85rem;font-weight:600;cursor:pointer}
.upload-consent-check input{margin-top:.1rem;flex:none;width:1.3rem;height:1.3rem;accent-color:var(--flame-tip);cursor:pointer}

/* ===================== ГОСТЕВАЯ ПОДСКАЗКА ===================== */
.guest-note{
  margin-bottom:1.2rem;padding:.8rem 1rem;border-radius:var(--md-sys-shape-corner-m);
  background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);
  font-size:.9rem;display:flex;align-items:center;gap:.7rem;flex-wrap:wrap;
}

/* ===================== СТОЛ ===================== */
/* Стол растягивается на всю ширину окна, а не только до max-width родителя
   (.content: 76rem) — стандартный выход из центрированного контейнера через
   100vw + компенсирующие отрицательные margin. body{overflow-x:hidden} уже
   стоит в базовых стилях выше, так что не страшно, если 100vw чуть шире
   реальной области просмотра из-за полосы прокрутки — обрежет, а не породит
   горизонтальный скролл страницы. */
/* Подвал сайта на столе не показываем вовсе (класс ставит route() в app.js)
   — стол и так во весь экран, а подвал под ним делал страницу длиннее
   экрана: доска уезжала от случайного свайпа. display:none, а не удаление
   из разметки — ссылки остаются в HTML для краулера; сам /table/:id в
   sitemap и не ходит (туда идёт /puzzle/:id, у которого подвал на месте). */
body.on-table .bh-footer{display:none}
.table-screen{
  display:flex;flex-direction:column;gap:.8rem;height:calc(100dvh - 4.6rem);
  /* Отрицательные margin'ы съедают padding самой main.content (22.4px
     сверху и 64px снизу) — иначе стол высотой во весь экран плюс эти
     отступы делают страницу длиннее экрана, и доска уезжает от случайного
     свайпа. К подвалу отношения не имеют (он скрыт правилом выше) —
     проверено замером: без margin-bottom страница оставалась на 54px
     длиннее экрана ровно на величину padding-bottom main.content. */
  margin-top:-1.4rem;margin-bottom:-4rem;
  width:100vw;margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw);
}
/* .table-toolbar (полоса с названием, счётчиком и «?» над доской) убрана
   совсем — см. правку «Стол без шапки». Счётчик переехал в .zoom-controls
   (нижний правый угол), «?» туда же, название пазла осталось только в
   <title> вкладки и на странице пазла: на столе оно ничего не решало, а
   полоса через весь экран отъедала высоту доски. */
.table-progress{
  font-size:.82rem;font-weight:700;line-height:1;text-align:center;
  color:var(--md-sys-color-on-surface);
  padding:.3rem .1rem .35rem;font-variant-numeric:tabular-nums;
}

.table-stage{
  position:relative;flex:1;overflow:hidden;touch-action:none;cursor:grab;
  background:
    radial-gradient(circle at 1px 1px, var(--md-sys-color-outline-variant) 1px, transparent 0) 0 0/22px 22px,
    var(--md-sys-color-surface-container-lowest);
  border-top:1px solid var(--md-sys-color-outline-variant);
  /* position:relative без z-index стека НЕ создаёт (только с z-index≠auto) —
     непрозрачный фон стола держится поверх фонового bh-backdrop (fixed,
     z-index:-1, см. базовые стили) чисто порядком отрисовки, без явной
     стековой границы. .table-world (панорама/зум) и десятки .piece с
     постоянным filter:drop-shadow каждая — это отдельные composited-слои
     GPU поверх/внутри стола; на слабом мобильном GPU при большом числе
     деталей (до 480 на "Легенде") и активном перемещении эти слои иногда
     ре-компонуются в неверном порядке на кадр-другой — фон стола "теряется",
     и сквозь него на миг видно fixed-подложку под всем сайтом (репорт:
     мерцание на Pixel 9, на десктопе не воспроизводится). isolation:isolate
     даёт столу явную собственную стековую границу — GPU больше не может
     перепутать порядок компоновки этого поддерева с fixed-подложкой снаружи. */
  isolation:isolate;
}
.table-stage.panning{cursor:grabbing}
.table-world{position:absolute;left:0;top:0;transform-origin:0 0;will-change:transform}

/* Массовое выделение рамкой (Shift+тяни по пустому месту доски, см.
   pointerdown ниже) — экранные координаты, а не мировые: рисуется прямо в
   .table-stage, а не в #world, поэтому не нужно пересчитывать под zoom/pan
   (родитель не трогает этот элемент своим transform). Сама логика
   пересечения с деталями — в JS (updateMarqueeSelection), тут только
   визуал. */
.marquee-select{
  position:absolute;border:1.5px dashed var(--flame-tip);border-radius:4px;
  background:color-mix(in srgb, var(--flame-tip) 14%, transparent);
  pointer-events:none;z-index:5;
}
.marquee-select[hidden]{display:none}

.board-outline{
  position:absolute;left:0;top:0;border:2px dashed var(--md-sys-color-outline-variant);
  border-radius:6px;pointer-events:none;
}

.piece{
  position:absolute;left:0;top:0;touch-action:none;cursor:grab;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.25));
  -webkit-user-drag:none;user-select:none;
}
.piece svg{display:block;width:100%;height:100%}
.piece.dragging{cursor:grabbing;filter:drop-shadow(0 6px 14px rgba(0,0,0,.35));z-index:1000}
/* Механика «связей»: у детали больше нет фиксированного места, поэтому
   ничем не блокируем драг — даже полностью собранную картинку можно взять
   за любую деталь и подвинуть целиком (класс .piece.placed сюда не пишется
   больше нигде в app.js — состояние "собрано" целиком производное от
   buildClusters, не хранится на детали). */
/* Деталь встала на место (образовалось новое ребро кластера) — вспышка в
   цвет пламени сервиса, а не абстрактный
   "успех"-зелёный: связывает подтверждение сборки с веществом на фоне. */
@keyframes piece-snap{
  0%  { filter:drop-shadow(0 1px 1px rgba(0,0,0,.18)) drop-shadow(0 0 0 transparent) }
  35% { filter:drop-shadow(0 1px 1px rgba(0,0,0,.18)) drop-shadow(0 0 10px var(--flame-tip)) }
  100%{ filter:drop-shadow(0 1px 1px rgba(0,0,0,.18)) drop-shadow(0 0 0 transparent) }
}
.piece.just-snapped{animation:piece-snap .55s var(--ease-emph-dec)}
/* Раньше тут была обводка через drop-shadow прямо на .piece — не видна
   почти совсем (см. правку «Выделение пазла: видимая обводка», жалоба с
   ПК): wrap (= .piece) несёт clip-path:path(d) для хит-теста (см.
   createPieceEl в app.js), а clip-path обрезает ЛЮБОЙ вышедший НАРУЖУ blur
   ровно по контуру детали — та же причина, по которой три первых захода
   на обводку подсказки ниже тоже были невидимы, просто тогда это заметили
   и переделали, а .selected — нет. Теперь — тот же outlineEl-приём, что и
   у подсказки (см. .piece-outline-halo/accent ниже): свой SVG-элемент,
   СОСЕДНИЙ с wrap, а не внутри него, clip-path его не касается. Тень
   "приподнятости" остаётся тут же, отдельным простым drop-shadow. */
.piece.selected{
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.25));
}
/* Подсказка (см. план «Повороты деталей + звук + подсказка», pickHintPair
   в app.js) — прошла три захода: drop-shadow-обводка как у .selected выше
   (невидима — wrap несёт clip-path:path(...) для хит-теста, см.
   createPieceEl в app.js, и он обрезает ЛЮБОЕ размытие ровно по контуру,
   живой тест подтвердил — drop-shadow(0 0 40px red) не проявлялся вообще);
   затем заливка через ::after (видна, но терялась на пазлах, где и так
   много зелёного — заливка меняет тон всей детали, а не создаёт границу);
   потом обводка-<path> ВНУТРИ svg детали (видна и контрастна, но прямо по
   контуру — тот же clip-path у wrap обрезает внешнюю половину stroke,
   отступа наружу добиться нельзя, пока путь рисуется потомком wrap).
   Текущая версия (см. правку «Отступ от контура») — outlineEl,
   ОТДЕЛЬНЫЙ от wrap svg-элемент (см. createPieceEl, applyPieceTransform:
   держится в шаге с деталью тем же transform, но clip-path wrap на него
   вообще не действует, раз он не потомок) — это и даёт свободу увеличить
   контур (transform:scale ниже) на равный отступ по всему периметру, а не
   упираться в границу clip-path. Два слоя стиля прежние: тёмный
   полупрозрачный "нимб" пошире снизу — контраст на ЛЮБОМ фоне, включая
   зелёный пазл (тёмный цвет не зависит от тона под ним), яркий акцентный
   цвет пламени поверх — тоньше, для узнаваемости. */
@keyframes piece-hint-outline{
  0%, 100% { opacity:.55 }
  50%      { opacity:1 }
}
/* Та же схема позиционирования, что у .piece (position:absolute;left:0;
   top:0 + свой transform) — outlineEl держится вровень с деталью только
   потому, что несёт тот же transform (см. applyPieceTransform), сама по
   себе никак не привязана к разметке детали. */
/* display:none по умолчанию — это САМАЯ дорогая вещь на большом столе, и не
   из-за рисования: у .piece-outline-halo/-accent ниже stroke-width и
   stroke-dasharray заданы как calc(N / var(--cam-scale)), а --cam-scale
   переписывается на .table-world при КАЖДОМ движении камеры (см.
   applyWorldTransform в app.js). Пока обводки висели в DOM с opacity:0, любая
   панорама заставляла браузер пересчитывать и перерисовывать 2×N путей — на
   999 деталях кадр панорамы стоил ~53 мс (≈19 fps) даже на десктопе; с
   display:none — 5.9 мс. Отсюда и лаги на бюджетных телефонах, про которые
   пришла жалоба. Показываем ровно в двух состояниях, где обводка и нужна
   (см. селекторы .hint-glow/.selected ниже) — opacity у самих путей остаётся
   как было и продолжает отвечать за плавное появление. */
.piece-outline-overlay{position:absolute;left:0;top:0;display:none}
.piece.hint-glow + .piece-outline-overlay,
.piece.selected + .piece-outline-overlay{display:block}
.piece-outline-halo,.piece-outline-accent{
  fill:none;opacity:0;pointer-events:none;vector-effect:non-scaling-stroke;
  /* fill-box — растёт от центра СОБСТВЕННОГО контура детали, включая
     выступы/пазы, а не от угла viewBox — иначе отступ был бы неравномерным
     по сторонам. */
  transform-box:fill-box;transform-origin:center;transform:scale(1.07);
}
/* Ширина ниже — в локальных SVG-юнитах детали, non-scaling-stroke спасает
   только от transform:scale(1.07) на самой этой paths (см. выше), а не от
   зума камеры: .table-world выше по DOM в app.js, её CSS transform:scale()
   снаружи этой svg — non-scaling-stroke его не видит и не компенсирует, и
   ЛЮБОЕ фиксированное число тут при zoom≈0.12 (fitView на большом пазле,
   ZOOM_MIN) превращалось на экране в еле заметную нитку, сколько её ни
   поднимай (живая проверка на 999 деталях подтвердила — просто «жирнее в
   локальных юнитах» не помогает, если камера потом делит на восемь). Вместо
   этого — applyWorldTransform в app.js пишет --cam-scale на .table-world (=
   текущий zoom), а тут делим на неё: итоговая толщина на ЭКРАНЕ остаётся
   постоянной независимо от отдаления камеры (см. правку «Подсказка про
   пазл: жирнее, теперь по-настоящему»). Фолбэк /1 — на случай, если
   applyWorldTransform ещё не успел выставить переменную. */
.piece-outline-halo{stroke:#000;stroke-opacity:.6;stroke-width:calc(11 / var(--cam-scale, 1))}
/* Пунктир + бегущий stroke-dashoffset — «муравьиная дорожка» по контуру
   (см. правку «Ещё анимация»): подсказка ведь как раз про «обойди рамку
   по кругу», бегущая линия читается как намёк именно на это, не просто
   украшение. Дэши тоже делятся на --cam-scale — иначе при сильном
   отдалении толстая линия (см. выше) осталась бы почти сплошной, дробные
   9/7 потерялись бы на фоне выросшей толщины. 9+7=16 — сдвиг offset ровно
   на один период (см. keyframes ниже) даёт идеально бесшовный цикл,
   дробный сдвиг оставил бы видимый "скачок" на стыке итераций. */
.piece-outline-accent{
  stroke:var(--flame-tip);stroke-width:calc(4.6 / var(--cam-scale, 1));
  stroke-dasharray:calc(9 / var(--cam-scale, 1)) calc(7 / var(--cam-scale, 1));
}
@keyframes piece-hint-march{ to { stroke-dashoffset:calc(-16 / var(--cam-scale, 1)) } }
/* outlineEl — сосед wrap в DOM (см. world.appendChild порядок в app.js),
   не потомок, поэтому класс .hint-glow (висит на wrap/.piece) достаётся
   через соседний селектор, а не обычную вложенность. */
.piece.hint-glow + .piece-outline-overlay .piece-outline-halo{
  animation:piece-hint-outline .9s ease-in-out infinite;
}
.piece.hint-glow + .piece-outline-overlay .piece-outline-accent{
  animation:piece-hint-outline .9s ease-in-out infinite, piece-hint-march 1s linear infinite;
}
/* Выделение (клик/shift-клик/рамка выделения, см. setSelected в app.js) —
   тот же outlineEl, что у подсказки выше, тем же приёмом (соседний
   SVG, clip-path wrap его не обрезает), но БЕЗ анимации: подсказка —
   разовый «посмотри сюда» на пару секунд, а выделение — устойчивое
   состояние, ему нужна ровная сплошная обводка, а не мигание. Сплошная —
   поэтому stroke-dasharray:none переопределяет пунктир базового правила
   .piece-outline-accent выше. Раньше во время самого драга обводку
   намеренно гасили (читалась как шум, тени было достаточно) — по факту
   оказалось наоборот не видно вообще ни выделения, ни тем более группы
   во время переноса (см. ту же правку) — тут это не gate'ится по
   .dragging нарочно, обводка теперь держится и во время переноса тоже. */
.piece.selected + .piece-outline-overlay .piece-outline-halo,
.piece.selected + .piece-outline-overlay .piece-outline-accent{
  opacity:1;
}
/* Чуть тоньше, чем у подсказки выше (см. правку «Тоньше при таскании») —
   выделение держится постоянно, пока деталь выбрана/тащится, подсказка —
   пару секунд и специально бросается в глаза; одинаковая толщина у обоих
   не давала визуально отличить «я сам это выбрал» от «мне намекают». */
.piece.selected + .piece-outline-overlay .piece-outline-halo{
  stroke-width:calc(7 / var(--cam-scale, 1));
}
.piece.selected + .piece-outline-overlay .piece-outline-accent{
  stroke-width:calc(3 / var(--cam-scale, 1));
  stroke-dasharray:none;
}
/* Тень "приподнятости" во время переноса — независимо от кольца выше,
   отдельный слой (filter на самом .piece, а не outlineEl). */
.piece.selected.dragging{
  filter:drop-shadow(0 6px 14px rgba(0,0,0,.35));
}

.zoom-controls{
  position:absolute;right:.9rem;bottom:.9rem;display:flex;flex-direction:column;gap:.4rem;
  background:color-mix(in srgb, var(--md-sys-color-surface) 88%, transparent);
  border-radius:var(--md-sys-shape-corner-l);padding:.4rem;box-shadow:var(--e1);
}
.zoom-controls .btn{width:2.4rem;height:2.4rem;padding:0}

/* Перемешать/Показать картинку/Светлый фон — раньше текстовые кнопки в
   .table-toolbar, схлопывались в иконки только на узком экране (см. былой
   .toolbar-btn/@media ниже). Теперь всегда иконки, в собственной плашке в
   том же визуальном языке, что .zoom-controls, но в другом углу (нижний
   левый — .zoom-controls внизу справа, .preview-thumb вверху слева (под
   .board-back, см. ниже), кнопка присутствия вверху справа, см.
   .presence-widget), чтобы ничего не перекрывалось. */
.board-tools{
  position:absolute;left:.9rem;bottom:.9rem;display:flex;flex-direction:row;align-items:center;gap:.4rem;
  background:color-mix(in srgb, var(--md-sys-color-surface) 88%, transparent);
  border-radius:var(--md-sys-shape-corner-l);padding:.4rem;box-shadow:var(--e1);
  transition:gap .25s var(--ease-emph-dec);
}
.board-tools .btn{width:2.4rem;height:2.4rem;padding:0}
/* #selectModeBtn, пока включён (aria-pressed="true") — иначе снаружи не
   видно, что один палец по пустому месту сейчас рисует рамку, а не таскает
   доску (на десктопе то же самое видно и так, по Shift). */
.board-tools .btn[aria-pressed="true"]{
  background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);border-color:transparent;
}
/* Свёртывание в тоггл (см. план «Выдвигающиеся кнопки», bindCollapsibleCluster
   в app.js) — .tools-row «выезжает»/«заезжает» по max-width+opacity, не
   display: так есть анимация, а не мгновенное исчезновение. gap у самого
   .board-tools/.table-widgets тоже схлопывается (см. .collapsed ниже) —
   иначе между тогглом и невидимой (но всё ещё max-width:0-присутствующей)
   строкой оставался бы пустой зазор. */
.tools-row{
  display:flex;align-items:center;gap:.4rem;overflow:hidden;
  max-width:22rem;opacity:1;
  transition:max-width .28s var(--ease-emph-dec),opacity .18s var(--ease-std);
}
.board-tools.collapsed, .table-widgets.collapsed, .hint-widgets.collapsed{gap:0}
.tools-row.collapsed{max-width:0;opacity:0;pointer-events:none}
.tools-toggle svg{transition:transform .2s var(--ease-std)}
.tools-toggle[aria-expanded="false"] svg{transform:rotate(180deg)}
/* Подсказки — ОТДЕЛЬНЫЙ сворачиваемый кластер под звуком/чатом/
   участниками, не внутри #widgetsRow и не внутри .table-widgets (см.
   правку «Подсказки — своя независимая
   группа» — раньше жили внутри общей плашки .table-widgets и делили с ней
   один тоггл: визуально ОДНА выдвигающаяся плашка, хотя кнопки внутри —
   два разных по смыслу набора действий; до этого — вообще в одном
   горизонтальном ряду со звуком). .widgets-stack просто раскладывает
   .table-widgets/.hint-widgets — два НЕЗАВИСИМЫХ «острова», каждый со
   своим тогглом и своим bindCollapsibleCluster (см. app.js) — колонкой.
   .hint-widgets — сам «остров» (фон/тень, тот же визуальный язык, что у
   .table-widgets), тоггл в нём ПЕРВЫЙ ребёнок (якорь сверху),
   .hint-btn-group раскрывается вниз — тот же паттерн, что у .table-widgets
   с .tools-row, только по вертикали и с descendant-селектором ниже (тут
   тоггл и раскрывающийся блок — родитель/потомок одного острова, не
   соседи по .widgets-stack). align-items:center у группы — не stretch
   (умолчание для flex-column) — круглые .btn.icon растянулись бы в
   овалы. */
/* align-items:flex-end — НЕ умолчание (stretch): при stretch оба острова
   тянутся на ширину самого широкого из них, и когда .table-widgets
   схлопывается (уже, чем .hint-widgets), общая ширина .widgets-stack
   пересчитывается — .hint-widgets, растянутый на неё, сдвигается по
   горизонтали вслед за ЧУЖИМ схлопыванием, хотя сам не менялся (баг,
   пойманный на глаз: «двигается группа подсказок»). flex-end — у каждого
   острова своя, независимая от соседа ширина по контенту, оба всегда
   прижаты правым краем к .9rem — сосед может как угодно схлопываться,
   не задевая позицию другого. */
.widgets-stack{position:absolute;right:.9rem;top:.9rem;display:flex;flex-direction:column;align-items:flex-end;gap:.5rem}
.hint-widgets{
  display:flex;flex-direction:column;align-items:center;gap:.35rem;
  background:color-mix(in srgb, var(--md-sys-color-surface) 88%, transparent);
  border-radius:var(--md-sys-shape-corner-l);padding:.4rem;box-shadow:var(--e1);
  transition:gap .25s var(--ease-emph-dec);
}
.hint-btn-group{
  display:flex;flex-direction:column;align-items:center;gap:.35rem;overflow:hidden;
  /* Ровно 2 кнопки по 2.4rem + 1 gap — раньше тут стояли «на глаз» 6rem
     (с запасом под старую, ошибочно сжатую до 24px высоту кнопок, см.
     .hint-widgets .btn выше) — теперь, когда кнопки нормального размера,
     запас просто оставлял пустой хвост в конце схлопывающей анимации. */
  max-height:5.15rem;opacity:1;
  transition:max-height .28s var(--ease-emph-dec),opacity .18s var(--ease-std);
}
.hint-widgets.collapsed .hint-btn-group{max-height:0;opacity:0;pointer-events:none}
/* Внимание к подсказкам при бездействии (см. app.js, bindHintIdleReminder/
   setHintAttention, правка «Анимация подсказок при бездействии») — та же
   пара кнопок, что и в .hint-btn-group выше, просто на время «залипает»
   пульсом акцентного цвета, чтобы взгляд сам зацепился, а не только текст
   в пузыре-напоминании рядом. Снимается сразу по активности (см. ту же
   функцию) — не бесконечная анимация, а именно привлечение внимания. */
@keyframes hint-attn-pulse{
  0%, 100% { transform:scale(1); box-shadow:0 0 0 0 color-mix(in srgb, var(--flame-tip) 45%, transparent); }
  50%      { transform:scale(1.08); box-shadow:0 0 0 7px color-mix(in srgb, var(--flame-tip) 0%, transparent); }
}
.hint-attn{
  animation:hint-attn-pulse 1.6s ease-in-out infinite;
  border-color:var(--flame-tip);color:var(--flame-tip);
}

/* «Назад» — была текстовой ссылкой «← Комната»/«← Библиотека» в
   .table-toolbar, теперь иконка в стиле кнопок масштаба, в ЛЕВОМ ВЕРХНЕМ
   углу доски (не рядом с .board-tools внизу — выход со стола логически не
   относится к инструментам сборки). Тот же угол, что у .preview-panel —
   допустимо: кнопка маленькая (2.4rem), панель смещена ниже неё по
   умолчанию (см. .preview-panel), а начальное перекрытие неважно — панель
   можно оттащить. */
.board-back{
  position:absolute;left:.9rem;top:.9rem;display:flex;
  background:color-mix(in srgb, var(--md-sys-color-surface) 88%, transparent);
  border-radius:var(--md-sys-shape-corner-l);padding:.4rem;box-shadow:var(--e1);
}
.board-back .btn{width:2.4rem;height:2.4rem;padding:0}

/* Справочная картинка «как должно получиться» — плавающая панель поверх
   стола: не просто статичная подсказка (раньше pointer-events:none,
   drag/клик проходили сквозь неё к деталям под ней), теперь можно оттащить
   с дороги за саму картинку и подрастянуть за кружок в углу — логика в
   bindPreviewThumb (assets/app.js), общая для соло и комнаты. Позиция/
   размер — инлайн left/top/width, которые ставит JS поверх этих
   дефолтов при первом перетаскивании/резайзе. Живёт в .table-stage, а не
   в #world — панорама/зум доски на неё не действует. Исключена из
   панорамы стола отдельно через .preview-panel (см. pointerdown ниже),
   как и .zoom-controls/.board-tools — иначе стол начинал бы панориться
   вместо перетаскивания панели. */
.preview-panel{
  position:absolute;left:.9rem;top:3.7rem;width:14rem;max-width:90%;
  touch-action:none;
}
.preview-panel[hidden]{display:none}
.preview-thumb{
  display:block;width:100%;height:auto;border-radius:var(--md-sys-shape-corner-l);
  background:var(--md-sys-color-surface-container-lowest);
  border:2px solid var(--md-sys-color-surface);box-shadow:var(--e2);
  cursor:grab;
  /* draggable="false" на самом <img> (см. app.js) уже отбивает нативный
     drag в Chrome/Firefox — тут дублируем на уровне CSS для Safari, где
     -webkit-user-drag надёжнее атрибута: без обоих браузер запускал
     нативный drag-n-drop картинки вместо pointermove-перетаскивания
     панели (см. bindPreviewThumb) — курсор менялся на «нельзя», сама
     панель не двигалась. user-select — чтобы быстрый драг не выделял
     соседний текст на странице как побочный эффект. */
  -webkit-user-drag:none;user-select:none;
}
.preview-thumb:active{cursor:grabbing}
/* Кружок в правом нижнем углу панели — тянешь по диагонали, ширина панели
   меняется, высота следует автоматически (height:auto на .preview-thumb
   сохраняет пропорции картинки без object-fit). */
.preview-resize-handle{
  position:absolute;right:-.35rem;bottom:-.35rem;width:1.1rem;height:1.1rem;
  border-radius:50%;background:var(--md-sys-color-primary);
  border:2px solid var(--md-sys-color-surface);box-shadow:var(--e1);
  cursor:nwse-resize;touch-action:none;
  /* Полупрозрачность в покое (см. правку «Крестик и уголок — полупрозрачные»):
     панель — это подсказка «как должно получиться», и постоянно яркие
     служебные кружки на ней отвлекают от самой картинки. Возвращаем полную
     непрозрачность, когда с панелью работают: наведение на панель, фокус с
     клавиатуры и сам захват (:active) — на тач-устройствах наведения нет,
     но кнопка и в приглушённом виде прекрасно нажимается. */
  opacity:.45;transition:opacity .18s var(--ease-std);
}
.preview-resize-handle::before{content:"";position:absolute;inset:-.7rem}
.preview-panel:hover .preview-resize-handle,
.preview-resize-handle:active,
.preview-resize-handle:focus-visible{opacity:1}
/* Крестик — поверх правого верхнего угла картинки, с тёмной подложкой (тот
   же приём, что у .win-image-actions на окне победы: без неё белый крестик
   теряется на светлом фото). Мельче обычного .icon-btn 44×44 — панель сама
   бывает шириной от 96px, кнопка во весь стандартный размер закрыла бы
   собой половину подсказки. */
.preview-close{
  position:absolute;top:.3rem;right:.3rem;width:1.6rem;height:1.6rem;
  background:rgba(0,0,0,.45);color:#fff;
  opacity:.45;transition:opacity .18s var(--ease-std);
  /* overflow у .icon-btn стоит hidden, чтобы квадратная рябь (::after)
     не торчала из круга, — но он же обрезал бы и запас под палец ниже.
     Возвращаем видимость, а рябь делаем круглой напрямую. */
  overflow:visible;
}
.preview-close::after{border-radius:inherit}
/* Видимый кружок 1.6rem (≈26px) — заметно меньше пальца: на телефоне в
   крестик надо было бы целиться. Прозрачный запас вокруг доводит область
   нажатия до ≈42px, не трогая саму картинку визуально. То же у уголка
   масштабирования ниже. */
.preview-close::before{content:"";position:absolute;inset:-.5rem}
.preview-close .icon{width:1rem;height:1rem}
.preview-panel:hover .preview-close,
.preview-close:hover,
.preview-close:focus-visible{opacity:1}

/* overflow:auto + margin:auto на карточке вместо align-items/justify-content
   центрирования: на низком экране (телевизор, телефон в альбомной) карточка
   выше оверлея, и при центрировании по флексу верх карточки уезжал ЗА
   границу контейнера — до него было не докрутить (классическая ловушка
   flex + overflow). margin:auto центрирует так же, пока место есть, но
   схлопывается в ноль, когда его нет, и прокрутка работает по обеим осям. */
.win-overlay{
  position:absolute;inset:0;display:flex;overflow:auto;padding:1.2rem;
  background:color-mix(in srgb, var(--md-sys-color-inverse-surface) 55%, transparent);
  animation:puzzle-win-fade .25s var(--ease-emph-dec) both;
}
@keyframes puzzle-win-fade{from{opacity:0}to{opacity:1}}
.confetti-canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.win-card{
  background:var(--md-sys-color-surface-container-lowest);border-radius:var(--md-sys-shape-corner-xl);
  padding:2rem 2.2rem;text-align:center;box-shadow:var(--e2);
  /* Было 22rem без width — flex-item без явного width считает себя по
     содержимому (самая длинная строка текста, ~300px), на широком столе
     терялась мелкой посреди пустоты. width:100% + max-width:30rem — та же
     формула, что у .modal (тоже 30rem): width:100% просит всю ширину
     .win-overlay, max-width её же и обрезает потолком — на узком экране
     сама сожмётся вместе с контейнером, на широком — реально дорастёт до
     потолка, а не только до текста внутри. */
  width:100%;max-width:30rem;margin:auto;
}
/* Картинка на всю ширину карточки, вровень с краями (см. правку «Окно
   победы — превью и результат») — тот же приём, что у .puzzle-preview-image
   (превью пазла/выбор сложности): отрицательный margin компенсирует padding
   .win-card, скругление верхних углов теперь на самой картинке (не через
   overflow:hidden на карточке — так безопаснее, не рискует незаметно
   обрезать что-то ещё внутри .win-card). Раньше картинка была врезана
   внутрь паддинга (max-height:14rem, object-fit:contain) — с портретным
   фото получались пустые поля по бокам, теперь cover — без полей, как
   раньше было в модалке превью. */
.win-image-wrap{position:relative;margin:-2rem -2.2rem 0}
.win-image{
  width:100%;aspect-ratio:4/3;object-fit:cover;display:block;
  border-radius:var(--md-sys-shape-corner-xl) var(--md-sys-shape-corner-xl) 0 0;
  background:var(--md-sys-color-surface-container-high);
}
/* Кнопка «поделиться результатом» плавает поверх картинки, тем же приёмом,
   что и шапка #puzzlePreviewModalBackdrop .modal-head — полупрозрачная
   тёмная подложка, иначе теряется на светлом фото. */
.win-image-actions{position:absolute;top:.6rem;right:.6rem}
.win-image-actions .icon-btn{background:rgba(0,0,0,.45);color:#fff}
.win-card h2{font-size:1.35rem;margin:1.2rem 0 .5rem}
/* Плашка автора (см. app.js, showWin в renderRoomTable, правка «Автор на
   окне победы») — тот же .puzzle-page-author-badge, что на странице пазла,
   но margin-bottom тут свой: там элемент в flex-колонке с общим gap, тут —
   обычный блок, без него следующий .win-stats прижался бы вплотную. */
.win-card .puzzle-page-author-badge{margin-bottom:1.2rem}
.win-card p{color:var(--md-sys-color-on-surface-variant);margin-bottom:1.2rem}
/* Статистика сборки (см. план «Статистика сборки») — на окне победы
   (buildStatsBlock в app.js, между описанием и рейтингом) и на странице
   пазла (тот же класс, см. .puzzle-page-mystats ниже) — пара «время
   сборки / деталей» рядом, тем же приёмом, что .win-rating-label/значение. */
.win-stats{display:flex;justify-content:center;gap:1.6rem;margin-bottom:1.2rem}
.win-stat{display:flex;flex-direction:column;align-items:center;gap:.15rem}
.win-stat-label{font-size:.76rem;color:var(--md-sys-color-on-surface-variant)}
.win-stat-value{font-size:1.05rem;font-weight:650}
/* Было три кнопки вперемешку — маленькая круглая «скопировать ссылку» и
   два разноразмерных пилла (см. правку «Окно победы — превью и
   результат») — ссылка-иконка переехала на картинку выше (её сменила
   «поделиться результатом»), тут остались только два одинаковых полных
   пилла, выровненных поровну. */
.win-actions{display:flex;gap:.6rem}
.win-actions .btn{flex:1}
/* Призыв опубликовать своё фото прямо на окне победы (см. app.js, showWin
   в renderRoomTable, правка «Публикация с окна победы») — та же формула
   акцентной рамки, что у .category-suggest, только компактнее: тут не
   отдельная страница, а один блок внутри уже тесной .win-card. */
.win-publish-cta{
  /* margin-top — в окне победы съедается уже собственным margin-bottom
     .win-stats/рейтинга (сиблинги, коллапс берёт максимум, не сумму), а в
     модалках предпросмотра/истории у мета-строки над блоком СВОЕГО
     отступа нет вовсе (.puzzle-page-rating-summary) — без него блок сидел
     впритык (см. правку «Отступы у CTA публикации» — жалоба на нехватку
     воздуха именно там). */
  margin:1rem 0 1.5rem;padding:.9rem 1.1rem;text-align:center;
  border-radius:var(--md-sys-shape-corner-l);
  background:color-mix(in srgb, var(--flame-tip) 8%, var(--md-sys-color-surface-container-low));
  border:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
}
.win-publish-cta p{margin:0 0 .7rem;color:var(--md-sys-color-on-surface);font-size:.9rem}
/* Превью пазла (см. план «Карточка — по образцу Movies») — картинка на всю
   ширину модалки, вровень с краями (отрицательные margin компенсируют
   padding .modal:1.1rem 1.3rem 1.3rem — см. .movie-info-poster в Movies,
   тот же рецепт день в день), скругление верхних углов даёт сама модалка
   (overflow:auto + border-radius), отдельно тут клипать не нужно.
   aspect-ratio:4/3 — тот же, что у .puzzle-card-thumb на самой карточке
   (obj-fit:cover уже смирился с обрезкой краёв фото и там). */
.puzzle-preview-image{
  /* max-width:none — глобальный img{max-width:100%} (см. начало файла)
     иначе перебивает width ниже своим "потолком": он резолвится в 100% от
     content-box МОДАЛКИ (без учёта padding), тогда как width должен дойти
     до padding-box (см. margin ниже) — без сброса картинка упиралась в
     content-box и оставляла пустую тёмную полосу справа (со стороны, где
     она не докатывалась до края модалки). */
  width:calc(100% + 2.6rem);max-width:none;margin:-1.1rem -1.3rem 0;aspect-ratio:4/3;
  object-fit:cover;display:block;background:var(--md-sys-color-surface-container-high);
}
/* Название+рейтинг под картинкой, в одну строку (тот же приём, что
   .movie-info-caption: название слева, чип рейтинга справа). */
.puzzle-preview-caption{display:flex;justify-content:space-between;align-items:flex-start;gap:.8rem;margin-top:.8rem}
.puzzle-preview-caption-title{font-weight:650;font-size:1rem;min-width:0}
.puzzle-preview-rating{
  flex:none;white-space:nowrap;font-size:.78rem;font-weight:600;padding:.28em .7em;
  border-radius:var(--md-sys-shape-corner-full);
  background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);
}
#puzzlePreviewAuthor{margin-top:.3rem}
/* Шапка поверх картинки — тот же приём, что #movieInfoModalBackdrop в
   Movies: h2 остаётся в DOM только ради aria-labelledby (визуально спрятан
   sr-only, не display:none — тот убрал бы его и из accessibility-дерева),
   видимое название переехало в .puzzle-preview-caption выше. .modal-head
   абсолютным позиционированием ложится поверх картинки (та теперь первая
   в потоке и идёт вровень с верхним краем модалки), justify-content:
   flex-end — иконки жмутся к правому краю, раз текстового заголовка в
   строке больше нет. Полупрозрачная тёмная подложка на кнопках — иначе
   «поделиться»/крестик потерялись бы на светлом фото. */
#puzzlePreviewTitle{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
}
#puzzlePreviewModalBackdrop .modal{position:relative}
#puzzlePreviewModalBackdrop .modal-head{position:absolute;top:.6rem;left:.6rem;right:.6rem;z-index:2;justify-content:flex-end}
#puzzlePreviewModalBackdrop .modal-head-actions .icon-btn{background:rgba(0,0,0,.45);color:#fff}
.modal-head-actions{display:flex;align-items:center;gap:.1rem}
/* Модалка выбора сложности (см. план «Стилистически интереснее») — тот же
   приём «картинка на всю ширину + шапка плавает поверх», что у превью
   пазла выше (#puzzlePreviewModalBackdrop) — .puzzle-preview-image
   переиспользуется как есть, свой класс под неё тут не заводим. */
#difficultyModalTitle{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
}
#difficultyModalBackdrop .modal{position:relative}
#difficultyModalBackdrop .modal-head{position:absolute;top:.6rem;left:.6rem;right:.6rem;z-index:2;justify-content:flex-end}
#difficultyModalBackdrop .modal-head .icon-btn{background:rgba(0,0,0,.45);color:#fff}
/* Модалка истории сборки (см. openHistoryModal в app.js) — тот же приём,
   что у превью пазла/выбора сложности выше: h2 не был скрыт, поэтому
   рендерился поверх картинки прямо в потоке, наполовину закрытый ею (см.
   правку «Дублирующийся заголовок в модалке истории») — название и так
   есть ниже, в .puzzle-preview-caption-title, тут только для aria-labelledby. */
#historyModalTitle{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
}
#historyModalBackdrop .modal{position:relative}
#historyModalBackdrop .modal-head{position:absolute;top:.6rem;left:.6rem;right:.6rem;z-index:2;justify-content:flex-end}
#historyModalBackdrop .modal-head .icon-btn{background:rgba(0,0,0,.45);color:#fff}
.difficulty-caption{font-weight:650;font-size:1rem;margin-top:.8rem}
/* Плитки уровней сложности вместо прежнего <select> (см. renderDifficultyGrid
   в app.js) — крупнее и заметнее ряда в выпадающем списке, выбранная
   подсвечена акцентным цветом вместо синей рамки нативного select:checked. */
.difficulty-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-top:.9rem}
.difficulty-pill{
  display:flex;flex-direction:column;align-items:center;gap:.15rem;
  padding:.65rem .4rem;border-radius:var(--md-sys-shape-corner-l);
  border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-low);
  color:inherit;cursor:pointer;transition:transform .15s var(--ease-std),border-color .15s var(--ease-std),background .15s var(--ease-std);
}
.difficulty-pill:hover{transform:translateY(-1px);border-color:var(--md-sys-color-primary)}
.difficulty-pill.is-selected{background:var(--md-sys-color-primary);border-color:var(--md-sys-color-primary)}
.difficulty-pill.is-selected .difficulty-pill-level,
.difficulty-pill.is-selected .difficulty-pill-count{color:var(--md-sys-color-on-primary)}
.difficulty-pill-level{font-weight:650;font-size:.92rem}
.difficulty-pill-count{font-size:.76rem;color:var(--md-sys-color-on-surface-variant)}
#difficultyPlayBtn{width:100%;margin-top:1.1rem}
/* ===================== СТРАНИЦА ПАЗЛА (/puzzle/:id) =====================
   Настоящая страница в потоке контента (см. правку «Нормальная вёрстка
   страницы пазла») — раньше это была буквально модалка превью, просто
   вставленная в #app вместо оверлея (узкая карточка 34rem, плавающая шапка
   поверх картинки, sr-only h1 — всё родом из .modal). Теперь — двухколоночный
   приём: картинка слева, инфо-панель справа (крошки/h1/автор/рейтинг/
   сложность), ниже — похожие пазлы той же категории. На узких экранах
   колонки схлопываются в одну, картинка сверху. */
.puzzle-page-crumbs{
  display:flex;align-items:center;gap:.4rem;flex-wrap:wrap;margin-bottom:1rem;
  font-size:.86rem;color:var(--md-sys-color-on-surface-variant);
}
.puzzle-page-crumbs a{color:inherit;text-decoration:underline}
.puzzle-page-crumbs a:hover{color:var(--md-sys-color-primary)}
.puzzle-page{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr);gap:2rem;align-items:start}
@media (max-width:46rem){ .puzzle-page{grid-template-columns:1fr} }
/* Левая колонка — картинка + «своя статистика» под ней (см. правку
   «SEO-аудит»/расположение блока статистики): раньше mystats жил в правой
   .puzzle-page-info вместе со сложностью и растягивал только правую
   колонку — под картинкой при этом просто пустовало место, а справа
   получался длинный столбец. Теперь занимает как раз тот пустой остаток
   под картинкой, а не удлиняет и без того более длинную колонку. */
.puzzle-page-media{display:flex;flex-direction:column;gap:1rem}
.puzzle-page-image{
  width:100%;aspect-ratio:4/3;object-fit:cover;display:block;
  border-radius:var(--md-sys-shape-corner-xl);box-shadow:var(--e1);
  background:var(--md-sys-color-surface-container-high);
}
.puzzle-page-info{display:flex;flex-direction:column;gap:.9rem}
.puzzle-page-title-row{display:flex;justify-content:space-between;align-items:flex-start;gap:.8rem}
.puzzle-page-title-row h1{font-size:1.55rem;font-weight:650;letter-spacing:-.01em;text-wrap:balance}
/* Тег категории — та же форма таблетки, что у .puzzle-preview-rating ниже
   (rating), но secondary-container — чтобы визуально не путались: один
   ведёт на /category/:slug, другой ничего не открывает. */
.puzzle-page-tag{
  align-self:flex-start;font-size:.78rem;font-weight:600;padding:.28em .7em;
  border-radius:var(--md-sys-shape-corner-full);text-decoration:none;
  background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);
}
.puzzle-page-tag:hover{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary)}
/* Плашка автора (см. правку «Плашка автора на странице пазла») — та же
   форма таблетки, что у .puzzle-page-tag выше (secondary-container,
   тот же hover), но с person-icon вместо голого текста — на самой
   странице пазла места достаточно для полноценной плашки, в отличие от
   карточки/превью-модалки (там остался прежний .puzzle-card-author,
   тонкая подпись-ссылка). */
/* [hidden] обязан побеждать display:inline-flex — иначе у пазла БЕЗ автора
   (встроенный/добавленный через Admin, uploaderUsername пустой) бейдж
   остаётся на странице пустой плашкой: сам элемент всегда в разметке, его
   прячет атрибут (см. renderPuzzlePage). Те же грабли уже ловили на
   .btn[hidden] и на состояниях зоны загрузки. */
.puzzle-page-author-badge[hidden]{display:none}
.puzzle-page-author-badge{
  display:inline-flex;align-items:center;gap:.4em;align-self:flex-start;
  padding:.32em .8em .32em .55em;border-radius:var(--md-sys-shape-corner-full);
  background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);
  font-size:.82rem;font-weight:600;text-decoration:none;
}
.puzzle-page-author-badge:hover{background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary)}
.puzzle-page-author-badge .icon{width:1.1rem;height:1.1rem;stroke-width:2.2}
.puzzle-page-rating-summary{font-size:.9rem;color:var(--md-sys-color-on-surface-variant)}
.puzzle-page-rating-summary b{color:var(--md-sys-color-on-surface);font-weight:650}
/* buildRatingWidget (app.js) — тот же виджет, что на окне победы, звёзды
   там center'ированы под узкий модальный оверлей; тут — обычная левая
   колонка, по левому краю. */
.puzzle-page-rate .win-rating{margin-bottom:0}
.puzzle-page-rate .win-rating-stars{justify-content:flex-start}
.puzzle-page-sep{height:1px;background:var(--md-sys-color-outline-variant);margin:.2rem 0}
.puzzle-page-subhead{font-size:1rem;font-weight:650;margin-bottom:-.3rem}
/* Свёрнутый по умолчанию выбор сложности (см. правку «Картинка и «За стол»
   на одном уровне») — тоггл показывает только выбранный уровень, полная
   сетка difficulty-grid и чекбоксы — в раскрывающейся панели ниже. Раньше
   вся сетка (11 плиток) была видна всегда — это и было главной причиной,
   почему правая колонка почти всегда выходила заметно длиннее картинки
   слева. */
.puzzle-page-diffbox{
  border:1px solid var(--md-sys-color-outline-variant);border-radius:var(--md-sys-shape-corner-l);
  overflow:hidden;
}
.puzzle-page-diffbox-toggle{
  width:100%;display:flex;align-items:center;justify-content:space-between;gap:.8rem;
  padding:.65em .9em;background:var(--md-sys-color-surface-container-low);border:0;cursor:pointer;
  text-align:left;color:inherit;font:inherit;
}
.puzzle-page-diffbox-toggle:hover{background:var(--md-sys-color-surface-container)}
.puzzle-page-diffbox-label{display:block;font-size:.76rem;color:var(--md-sys-color-on-surface-variant)}
.puzzle-page-diffbox-toggle b{font-size:.96rem;font-weight:650}
.puzzle-page-diffbox-toggle .icon{width:20px;height:20px;flex:none;transition:transform .2s var(--ease-std)}
.puzzle-page-diffbox-toggle[aria-expanded="true"] .icon{transform:rotate(180deg)}
.puzzle-page-diffbox-panel{
  padding:.9rem;border-top:1px solid var(--md-sys-color-outline-variant);
  display:flex;flex-direction:column;gap:.8rem;
}
/* display:flex у автора-стиля перебивает встроенное [hidden]{display:none}
   (та же специфичность — выигрывает тот, кто загружен позже, см. уже
   существующий .btn[hidden] выше) — без этой строки панель оставалась
   видна, даже когда JS ставил hidden. */
.puzzle-page-diffbox-panel[hidden]{display:none}
/* .difficulty-grid несёt свой margin-top (см. правило выше) — рассчитан на
   контекст модалки (отступ от подписи над сеткой), тут сетка первым
   ребёнком в панели с собственным padding/gap — свой отступ сверху лишний. */
.puzzle-page-diffbox-panel .difficulty-grid{margin-top:0}
/* «Своя статистика» (см. план «Статистика сборки») — выделена лёгкой
   плашкой под картинкой (см. .puzzle-page-media выше), не просто ещё одна
   строка в правой колонке: это не общая информация о пазле, а обращение
   конкретно к этому зрителю («вы уже…»), должно считываться сразу.
   .win-stats/.win-stat* переиспользуются как есть (тот же блок, что и на
   окне победы) — только выравнивание влево, там та же логика «модалка по
   центру / колонка по левому краю», что и у .puzzle-page-rate. */
.puzzle-page-mystats{
  background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);
  border-radius:var(--md-sys-shape-corner-l);padding:.9rem 1rem;
  display:flex;flex-direction:column;gap:.5rem;
}
/* Тот же [hidden]-перебой, что у .puzzle-page-diffbox-panel/.btn выше — это
   и была настоящая причина «пустого фиолетового блока» в баг-репорте: JS
   честно ставил hidden, когда статистики нет, но display:flex всё равно
   рисовал пустую плашку с одним заголовком без текста. */
.puzzle-page-mystats[hidden]{display:none}
.puzzle-page-mystats .puzzle-page-subhead{margin-bottom:0}
.puzzle-page-mystats .win-stats{justify-content:flex-start;margin-bottom:0}
.puzzle-page-mystats-date{font-size:.86rem;opacity:.85}
#puzzlePagePlayBtn{margin-top:.3rem}
.puzzle-page-related{margin-top:2.6rem}
.puzzle-page-related h2{font-size:1.2rem;font-weight:650;margin-bottom:1rem}
/* Оценка на окне победы (см. план «Оценка пазла на окне победы») —
   5 звёзд, контурные по умолчанию, заливаются акцентным цветом до выбранной
   (или наведённой) включительно. */
.win-rating{margin-bottom:1.2rem}
.win-rating-label{color:var(--md-sys-color-on-surface-variant);margin-bottom:.4rem;font-size:.88rem}
.win-rating-stars{display:flex;gap:.2rem;justify-content:center}
.star{
  background:transparent;border:0;padding:.1em;font-size:1.7rem;line-height:1;cursor:pointer;
  color:var(--md-sys-color-outline);transition:color .12s var(--ease-std),transform .12s var(--ease-std);
}
.star:hover{transform:scale(1.12)}
.star.filled{color:var(--flame-tip)}
.win-rating-note{color:var(--md-sys-color-on-surface-variant);font-size:.82rem;margin-top:.3rem}

/* Фон стола — свободный выбор цвета (см. план «RGB-фон стола», bindBoardBackground
   в app.js) — не пресет светлый/тёмный, как раньше (см. историю, .light-board):
   деталь на фоне того же тона почти не видна по краям, пока не собрана,
   поэтому даём выбрать ЛЮБОЙ цвет, а не два готовых. --table-bg/--table-bg-dot
   — инлайновые custom properties (JS, dotColorFor выбирает светлые или
   тёмные точки по яркости выбранного фона) — .custom-bg тот же двухслойный
   background, что у стола по умолчанию (точки + заливка) чуть выше, просто
   на выбранных цветах вместо токенов темы: без точек на сплошной заливке
   пропадал ориентир на пустом столе (см. план «точки не видно»). */
.table-stage.custom-bg{
  background:
    radial-gradient(circle at 1px 1px, var(--table-bg-dot) 1px, transparent 0) 0 0/22px 22px,
    var(--table-bg);
}
/* Нативный <input type="color"> — визуально спрятан (не display:none, чтобы
   клик по <label>, который его оборачивает, надёжно открывал системный
   пикер во всех браузерах), сама кнопка — обычная .btn.outlined.icon
   с иконкой палитры поверх. */
#boardBgBtn{position:relative}
#boardBgBtn input[type="color"]{position:absolute;width:0;height:0;opacity:0;pointer-events:none}

/* ===================== КОМНАТЫ: СПИСОК ===================== */
.text-input{
  border:1px solid var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-m);
  padding:.6em .9em;font:inherit;background:var(--md-sys-color-surface-container-lowest);
  color:var(--md-sys-color-on-surface);
}

/* Категории при публикации (см. openPublishModal) — <select> категорий +
   поле новой категории. Без явного width:100% оба остаются на своей
   intrinsic ширине — у select это ширина самого длинного <option> (см.
   правку «Ширина модалки публикации»: "Без категории (по умолчанию —
   «Пользовательские»)" шире самой модалки на узких экранах, .modal{
   overflow:auto} в ответ заводил горизонтальный скролл ВНУТРИ модалки —
   тот самый маленький баг с шириной). box-sizing:border-box — без него
   100%+паддинг/border вылезали бы за ту же ширину ещё раз. margin-top у
   #publishConfirmBtn ниже отделяет блок от кнопки подтверждения.
   Селектор по .text-input, а не по конкретным id (#publishNewCategoryName/
   #publishCategorySelect, как было) — тот же блок теперь есть и на странице
   прямой публикации (см. renderPublishPage, правка «Публикация в обход
   комнаты») со своими id, и привязка к id молча оставила бы её без этого
   width:100% — ровно с тем же вылезающим select'ом. */
.publish-categories{display:flex;flex-direction:column;gap:.5rem;margin:.9rem 0}
.publish-categories .text-input{width:100%;box-sizing:border-box}
#publishConfirmBtn{margin-top:.9rem}

/* ===================== СТРАНИЦА ПУБЛИКАЦИИ =====================
   См. renderPublishPage (правка «Дизайн страницы публикации»). До этого
   страница была голой формой на пустом фоне: системный «Файл не выбран»
   как главное действие и простыня из десяти запретов первым, что видит
   глаз. Язык берём уже существующий — градиентный круг с иконкой и
   акцентная рамка, как у .category-suggest, — ничего нового не изобретаем,
   просто собираем страницу как обещание результата, а не как анкету. */
.publish-hero{
  display:flex;flex-direction:column;align-items:center;text-align:center;gap:.7rem;
  padding:2.2rem 1.2rem 1.6rem;
}
.publish-hero h1{margin:0;font-size:1.9rem}
.publish-hero p{margin:0;max-width:34rem;color:var(--md-sys-color-on-surface-variant)}
.publish-hero-icon{
  display:grid;place-items:center;width:4rem;height:4rem;border-radius:50%;
  background:linear-gradient(140deg, var(--flame-tip), color-mix(in srgb, var(--flame-tip) 45%, var(--md-sys-color-primary)));
  color:#0a1408;
}
.publish-hero-icon .icon{width:2rem;height:2rem}

/* Три шага — «что произойдёт после кнопки». Сетка auto-fit: на узком
   экране просто становятся друг под другом, без отдельного media-запроса. */
.publish-steps{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));gap:.9rem;
  list-style:none;margin:0 0 1.6rem;padding:0;
}
.publish-steps li{
  display:flex;flex-direction:column;gap:.3rem;
  padding:1rem 1.1rem;border-radius:var(--md-sys-shape-corner-l);
  background:var(--md-sys-color-surface-container-low);
  border:1px solid var(--md-sys-color-outline-variant);
}
.publish-steps b{font-size:.95rem}
.publish-steps span:not(.publish-step-num){font-size:.84rem;color:var(--md-sys-color-on-surface-variant)}
.publish-step-num{
  display:grid;place-items:center;width:1.6rem;height:1.6rem;margin-bottom:.2rem;
  border-radius:50%;background:color-mix(in srgb, var(--flame-tip) 18%, transparent);
  color:var(--flame-tip);font-size:.8rem;font-weight:800;
}

/* Плашка «нужен вход» вместо формы (см. renderPublishPage) — тот же язык,
   что у .publish-card ниже, но с приглушённой иконкой замка: это не ошибка
   и не запрет, а просто ещё не выполненное условие. */
.publish-gate{
  display:flex;flex-direction:column;align-items:center;text-align:center;gap:.8rem;
  padding:2.4rem 1.6rem;border-radius:var(--md-sys-shape-corner-xl);
  background:var(--md-sys-color-surface-container-low);
  border:1px solid var(--md-sys-color-outline-variant);box-shadow:var(--e1);
}
.publish-gate h2{margin:0;font-size:1.2rem}
.publish-gate p{margin:0;max-width:30rem;color:var(--md-sys-color-on-surface-variant)}
.publish-gate-icon{
  display:grid;place-items:center;width:3.2rem;height:3.2rem;border-radius:50%;
  background:color-mix(in srgb, var(--flame-tip) 14%, transparent);color:var(--flame-tip);
}
.publish-gate-icon .icon{width:1.6rem;height:1.6rem}

/* Сама форма — карточка-поверхность, а не элементы на голом фоне страницы. */
.publish-card{
  display:flex;flex-direction:column;gap:1.2rem;
  padding:1.6rem;border-radius:var(--md-sys-shape-corner-xl);
  background:var(--md-sys-color-surface-container-low);
  border:1px solid var(--md-sys-color-outline-variant);box-shadow:var(--e1);
}
.publish-field{display:flex;flex-direction:column;gap:.45rem}
.publish-field > label{font-size:.88rem;font-weight:650}
/* Звёздочка обязательного поля — акцентным цветом, чтобы «Название» и
   «Категория» (необязательная) отличались с одного взгляда, без подписи
   «обязательно» у каждого поля. */
.publish-required{color:var(--flame-tip)}
.publish-field .text-input{width:100%;box-sizing:border-box}

/* Зона выбора/перетаскивания файла. input внутри — visually-hidden, а НЕ
   display:none: спрятанный display:none input не получает фокус с
   клавиатуры, и выбрать файл без мыши стало бы невозможно. */
.publish-drop{
  position:relative;display:block;cursor:pointer;
  padding:1.6rem;border-radius:var(--md-sys-shape-corner-l);
  border:2px dashed color-mix(in srgb, var(--flame-tip) 35%, var(--md-sys-color-outline-variant));
  background:color-mix(in srgb, var(--flame-tip) 4%, transparent);
  transition:border-color .15s var(--ease-std),background .15s var(--ease-std);
}
.publish-drop:hover,.publish-drop.is-over{
  border-color:var(--flame-tip);
  background:color-mix(in srgb, var(--flame-tip) 10%, transparent);
}
.publish-drop input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.publish-drop input:focus-visible + .publish-drop-empty{outline:2px solid var(--md-sys-color-primary);outline-offset:6px;border-radius:var(--md-sys-shape-corner-m)}
/* [hidden] должен побеждать display:flex — см. комментарий у
   .publish-drop-preview[hidden] ниже, ловушка ровно та же. */
.publish-drop-empty[hidden]{display:none}
.publish-drop-empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;color:var(--md-sys-color-on-surface-variant)}
.publish-drop-empty .icon{width:2.2rem;height:2.2rem;color:var(--flame-tip)}
.publish-drop-empty b{color:var(--md-sys-color-on-surface);font-size:1rem}
.publish-drop-empty span{font-size:.84rem}
/* [hidden] + display:flex — классическая ловушка: свойство автора
   перебивает браузерное [hidden]{display:none}, и «превью» висело бы пустой
   рамкой поверх пустого состояния (в проекте на те же грабли уже наступали,
   см. .btn[hidden] выше). */
.publish-drop-preview[hidden]{display:none}
.publish-drop-preview{display:flex;align-items:center;gap:1rem}
.publish-drop-preview img{
  width:7rem;height:7rem;flex:none;object-fit:cover;
  border-radius:var(--md-sys-shape-corner-m);background:var(--md-sys-color-surface-container-high);
}
.publish-drop-meta{display:flex;flex-direction:column;gap:.2rem;min-width:0}
.publish-drop-meta b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.publish-drop-meta span{font-size:.84rem;color:var(--md-sys-color-on-surface-variant)}
.publish-drop-replace{color:var(--flame-tip);font-weight:600;text-decoration:underline}

/* Список запретов — в две колонки на широком экране: он обязан остаться
   видимым целиком (это осознанное согласие, прятать его под «показать
   ещё» нельзя), но простынёй в один столбец он забирал всю страницу.
   display возвращаем в block: базовый .upload-consent-list — flex-колонка
   (см. выше), а на flex-контейнере columns не работает вовсе; отступ между
   пунктами тогда даёт не gap, а margin у самих li. */
.publish-consent-list{display:block;columns:2;column-gap:1.6rem}
.publish-consent-list li{margin-bottom:.15rem;break-inside:avoid}
@media(max-width:36rem){.publish-consent-list{columns:1}}
/* Галочка про письмо (см. правку «Галочка про письмо») — намеренно НЕ в
   стиле .upload-consent-check: та крупная и акцентная, потому что ей человек
   берёт на себя ответственность за контент; эта же — обычная настройка
   доставки, и выглядеть как второе юридическое согласие ей не нужно. Есть и
   в модалке публикации, и на странице — стиль общий. */
.publish-notify-check{display:flex;align-items:flex-start;gap:.55rem;font-size:.86rem;cursor:pointer}
.publish-notify-check input{margin-top:.15rem;flex:none;width:1rem;height:1rem;accent-color:var(--flame-tip);cursor:pointer}
/* Приписка приглушённым — только там, где она объясняет ПОСЛЕДСТВИЕ выбора
   (галочка «не для детей»: картинка будет размыта). У галочки про письмо
   такую подпись убрали как лишнюю — см. правку. */
.publish-notify-hint{color:var(--md-sys-color-on-surface-variant)}
.publish-submit{align-self:flex-start;padding-inline:1.8rem}
.publish-error{padding:0;text-align:left;color:var(--md-sys-color-error)}
/* <select> под тем же классом (модалка выбора сложности, #difficultySelect)
   — .text-input выше уже задаёт border/padding/цвета, тут только то, что
   специфично для select. Нативную стрелку браузера убираем (appearance:none)
   и рисуем свою (--select-arrow, см. theme.css) — нативная в разных
   браузерах/ОС рендерилась по-разному и заметно смещённой вправо. */
select.text-input{
  cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none;
  padding-right:2.6em;background-image:var(--select-arrow);
  background-repeat:no-repeat;background-position:right .75em center;background-size:1.15em;
}

/* Создание/присоединение — раньше инлайн-форма, теперь две широкие кнопки
   под списком, каждая открывает свою модалку (см. #createRoomModalBackdrop/
   #joinRoomModalBackdrop в index.html и bindModal в app.js), дословно как
   .room-actions-row в Movies. */
.room-actions-row{display:flex;gap:.8rem;margin:1.2rem 0}
.room-actions-row .btn{flex:1}
@media(max-width:26rem){
  .room-actions-row{flex-direction:column}
  .room-actions-row .btn{flex:none;height:52px;font-size:15px}
}

/* Пейджер под списком комнат (Стр. N из M) — прячется целиком, если комнат
   меньше ROOMS_PAGE_SIZE (см. renderRoomsList в app.js). */
.pager{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:.8rem;margin-top:1rem}
/* Та же специфичность, что у браузерного [hidden]{display:none} (см. комментарий
   выше про .icon-btn[hidden]) — без этой строчки класс .pager побеждает и
   пейджер остаётся видимым (с disabled-стрелками) даже когда hidden выставлен. */
.pager[hidden]{display:none}

/* «Не нашли нужные пазлы?» — подвал библиотеки/категорий (см. план,
   renderCategorySuggestBox в app.js). Всё по центру одной колонкой —
   раньше иконка-бейдж стояла слева от текста (align-items:flex-start),
   и на переносе заголовка в две строки бейдж повисал только напротив
   первой строки, а не всего блока — читалось перекошенным. Центрированная
   колонка симметрична независимо от того, на сколько строк переносится
   текст на любой ширине экрана. */
.category-suggest{
  display:flex;flex-direction:column;align-items:center;text-align:center;
  margin-top:2.4rem;padding:2rem 1.6rem;
  border-radius:var(--md-sys-shape-corner-xl);
  background:linear-gradient(180deg, color-mix(in srgb, var(--flame-tip) 8%, var(--md-sys-color-surface-container-low)), var(--md-sys-color-surface-container-low) 55%);
  border:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
}
/* .own-photo-suggest сидит МЕЖДУ баннером гостя и сеткой/каруселью
   категорий (см. renderOwnPhotoCta), а не последним элементом страницы,
   как исходный .category-suggest («Не нашли нужные пазлы?») перед
   футером — там отступ снизу давал футер, здесь после блока не было
   вообще ничего до наступление вплотную (см. правку «Обозначить на
   главной, что для своих фото нужна комната»). */
.own-photo-suggest{margin-bottom:2.4rem}
.category-suggest-icon{
  width:3rem;height:3rem;border-radius:50%;display:flex;align-items:center;justify-content:center;
  background:linear-gradient(135deg, var(--flame-base), var(--flame-tip));color:#fff;
  box-shadow:var(--e1);margin-bottom:.9rem;
}
.category-suggest-icon .icon{width:1.5rem;height:1.5rem;stroke-width:2.2}
.category-suggest h2{font-size:1.2rem;font-weight:650;text-wrap:balance}
.category-suggest p{color:var(--md-sys-color-on-surface-variant);margin:.4rem 0 1.1rem;max-width:28rem}
.category-suggest-form{display:flex;gap:.6rem;flex-wrap:wrap;justify-content:center;width:100%;max-width:24rem}
.category-suggest-form .text-input{flex:1;min-width:12rem}
.category-suggest .state-note{margin-top:.7rem}

.room-list{display:grid;gap:1rem;grid-template-columns:repeat(auto-fill,minmax(15.5rem,1fr))}
.room-card{
  position:relative;overflow:hidden;
  background:var(--md-sys-color-surface-container-low);border-radius:var(--md-sys-shape-corner-l);
  padding:1rem 1.1rem;box-shadow:var(--e1);cursor:pointer;display:flex;flex-direction:column;gap:.35rem;
  transition:box-shadow .15s var(--ease-std),transform .15s var(--ease-std);
  animation:rise .4s var(--ease-emph-dec) both;
}
.room-card:hover{box-shadow:var(--e2);transform:translateY(-1px)}
/* Акцентная полоска слева в цвет пламени сервиса — тот же приём, что у
   карточек комнат в Movies (там пламя фиолетовое, тут — зелёное). */
.room-card::before{
  content:"";position:absolute;left:0;top:0;bottom:0;width:3px;
  background:linear-gradient(to top,var(--flame-base),var(--flame-tip));opacity:.75;
}
@keyframes rise{from{opacity:0;transform:translateY(10px)}}
.room-card-title{font-size:1.05rem;font-weight:650}
.room-card-meta{font-size:.85rem;color:var(--md-sys-color-on-surface-variant)}

/* ===================== КОМНАТЫ: ЭКРАН КОМНАТЫ ===================== */
/* Строка, не колонка — рядом с заголовком у владельца кнопка удаления
   комнаты целиком (см. #deleteRoomBtn); flex-wrap на случай узкого экрана,
   где заголовок длинный и не помещается в одну строку с кнопкой. */
.room-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.7rem;margin-bottom:1.2rem}
.room-head-title{font-size:1.4rem;font-weight:650}

/* Код приглашения — дословно паттерн Movies (.code-box/.code-box code, см.
   Movies/assets/app.js renderCodeArea и Movies/assets/styles.css) вместо
   readonly-поля со ссылкой: крупный кликабельный код + кнопка отдельно для
   полной ссылки. Без «Перевыпустить код» — этого эндпоинта у Puzzle нет. */
.code-box{
  display:flex;align-items:center;justify-content:center;gap:.6rem;flex-wrap:wrap;
  margin:.5rem auto 0;width:fit-content;max-width:100%;text-align:center;
  padding:.7em 1.2em;border-radius:var(--md-sys-shape-corner-m);
  background:var(--md-sys-color-surface-container-low);
  border:1px dashed color-mix(in srgb,var(--flame-tip) 45%,var(--md-sys-color-outline-variant));
}
.code-box code{
  font-size:1.7rem;font-weight:700;letter-spacing:.06em;cursor:pointer;
  font-family:ui-monospace,"Cascadia Mono",Consolas,monospace;
}
.code-box-hint{font-size:.82rem}
/* Подпись-напоминание под кодом (см. app.js, renderRoom) — .muted уже даёт
   цвет/размер, тут только раскладка: по центру, той же шириной, что и сам
   .code-box визуально ограничивает взгляд, не растягивается на всю строку. */
.room-invite-caption{margin:.6rem auto 1.2rem;max-width:26rem;text-align:center}

.room-members{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1.2rem}
.member-chip{
  display:inline-flex;align-items:center;gap:.3em;
  background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container);
  font-size:.82rem;font-weight:600;padding:.32em .5em .32em .8em;border-radius:var(--md-sys-shape-corner-full);
}
.member-chip.owner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}
/* Крестик "убрать участника" — виден только владельцу комнаты (см.
   canRemoveMembers в app.js), currentColor подхватывает цвет своей плашки
   (обычной/owner) без отдельной темизации. */
.member-chip-remove{
  display:grid;place-items:center;width:1.2em;height:1.2em;flex:none;padding:0;
  border:0;border-radius:var(--md-sys-shape-corner-full);background:transparent;color:inherit;
  cursor:pointer;position:relative;overflow:hidden;
}
.member-chip-remove .icon{width:.85em;height:.85em}
.member-chip-remove::after{content:"";position:absolute;inset:0;background:currentColor;opacity:0;transition:opacity .15s}
.member-chip-remove:hover::after{opacity:.18}
.member-chip-remove:disabled{opacity:.5;cursor:default}

.room-section-title{font-size:1.05rem;font-weight:650;margin:1.2rem 0 .7rem}
/* «Начать сборку» + кнопка «+» (открывает модалку загрузки своего фото) в
   одной строке — тот же приём justify-content:space-between, что и в шапке.
   margin-top СВОЙ, не .75rem от gap на .room-active (см. ниже) — тот
   рассчитан на паузу МЕЖДУ карточками одного списка, а тут смысловая
   граница: конец списка активных столов, начало другого раздела — те же
   ~2rem, что у #inProgressWrap/#roomsSectionOuter на главной перед
   карусель категорий (см. правку «Отступ после комнат/подсказки»). */
.room-section-head{display:flex;align-items:center;justify-content:space-between;gap:.8rem;margin-top:1.25rem}
.room-section-head .room-section-title{margin:0 0 .7rem}
/* Плашки активных досок (#roomActive, см. renderRoom в app.js) — без явного
   gap на контейнере несколько досок сразу (до MAX_ACTIVE_SESSIONS_PER_ROOM)
   рендерились впритык друг к другу, слипаясь в один блок без видимой
   границы между ними. gap на самом контейнере — не margin на карточках —
   не задваивается и одинаково работает что для одной плашки, что для пяти. */
.room-active{display:flex;flex-direction:column;gap:.75rem}
.room-active-card{
  background:var(--md-sys-color-surface-container-low);border-radius:var(--md-sys-shape-corner-l);
  padding:1rem 1.1rem;box-shadow:var(--e1);display:flex;align-items:center;gap:1rem;flex-wrap:wrap;
}
.room-active-card p{flex:1;min-width:12rem;color:var(--md-sys-color-on-surface-variant)}

/* «История сборок» — .puzzle-grid карточек .puzzle-card (см. правку
   «Картинка и время сборки в истории», buildHistoryCard в app.js), больше
   не свои .room-history/.history-row: раньше текстовые строки без превью,
   теперь тот же вид, что у «Продолжить сборку» на главной. Своих правил
   тут не нужно — .puzzle-grid/.puzzle-card уже определены выше. */

/* ===================== КОМНАТЫ: ВСТУПЛЕНИЕ ПО ССЫЛКЕ ===================== */
.room-join-screen{
  max-width:26rem;margin:3rem auto;text-align:center;display:flex;flex-direction:column;gap:1rem;
  background:var(--md-sys-color-surface-container-low);border-radius:var(--md-sys-shape-corner-xl);
  padding:2.2rem 2rem;box-shadow:var(--e1);
}
.room-join-screen h2{font-size:1.25rem}
.room-join-screen p{color:var(--md-sys-color-on-surface-variant)}
.room-join-screen .btn{align-self:center}

/* ===================== КОМНАТЫ: ПРИСУТСТВИЕ ЗА СТОЛОМ ===================== */
/* Раньше постоянная строка чипов в .table-toolbar — занимала место. Теперь
   кнопка-иконка (тот же язык, что .zoom-controls/.board-tools) с бейджем-
   числом, вверху справа стола (см. расстановку углов у .board-tools выше),
   раскрывающая компактный поповер со списком — не полноэкранная модалка,
   лёгкая position:absolute панель рядом с кнопкой. */
/* Верхний правый угол — подсказка/звук + присутствие/чат, один сворачиваемый
   кластер (см. план «Выдвигающиеся кнопки»): тот же визуальный язык и та же
   .tools-row-механика, что .board-tools внизу слева, только тоггл — ПОСЛЕДНИЙ
   ребёнок (якорь у самого правого края), а .tools-row — первый и раскрывается
   влево, к центру стола. Сами chat-widget/presence-widget внутри —
   position:relative только для якоря своих поповеров, собственный фон им
   больше не нужен — теперь на общей плашке. */
.table-widgets{
  display:flex;flex-direction:row;align-items:center;gap:.4rem;
  background:color-mix(in srgb, var(--md-sys-color-surface) 88%, transparent);
  border-radius:var(--md-sys-shape-corner-l);padding:.4rem;box-shadow:var(--e1);
  transition:gap .25s var(--ease-emph-dec);
}
.table-widgets .btn{width:2.4rem;height:2.4rem;padding:0}
/* Без этого переопределения кнопки .hint-widgets проваливались в общий
   .icon{width:24px;height:24px} (styles.css выше) — та же ловушка, что
   .table-widgets .btn выше уже решает для звука: сама кнопка тоже несёт
   класс "icon" (не только вложенный svg), и без более специфичного
   правила .icon{width:24px} перебивал .btn.icon{padding:.5em}, сжимая
   кнопку ровно до размера иконки (24×24) и обрезая её кружок-фон. */
.hint-widgets .btn{width:2.4rem;height:2.4rem;padding:0}
.presence-widget, .chat-widget{position:relative}
.btn.presence-btn, .btn.chat-btn{position:relative}
.presence-count{
  position:absolute;top:-.15rem;right:-.15rem;min-width:1.1rem;height:1.1rem;padding:0 .25rem;
  border-radius:var(--md-sys-shape-corner-full);background:var(--flame-tip);color:#0a1408;
  font-size:.68rem;font-weight:800;line-height:1.1rem;text-align:center;
}
.presence-count[hidden]{display:none}
.presence-popover, .chat-popover{
  position:absolute;top:calc(100% + .5rem);right:0;
  background:var(--md-sys-color-surface-container-low);border-radius:var(--md-sys-shape-corner-l);
  padding:.6rem .7rem;box-shadow:var(--e2);z-index:10;
}
.presence-popover{min-width:11rem;max-width:16rem;max-height:16rem;overflow:auto}
.presence-popover.hidden, .chat-popover.hidden{display:none}
.presence-popover-title{font-size:.78rem;font-weight:700;color:var(--md-sys-color-on-surface-variant);margin-bottom:.4rem}
.presence-popover-list{display:flex;flex-direction:column;gap:.35rem}
.presence-chip{
  background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);
  font-size:.78rem;font-weight:600;padding:.26em .7em;border-radius:var(--md-sys-shape-corner-full);
  align-self:flex-start;
}
/* Чат за столом (см. план «Чат на доску») — эфемерный, ничего не хранится:
   поповер шире presence-popover (нужно место под текст сообщений), с
   прокручиваемой лентой сверху и формой ввода снизу, зафиксированной
   (не уезжает при скролле ленты). Высота ленты — фиксированная (height, не
   max-height): панель не «прыгает» между пустым и заполненным состоянием,
   размер один и тот же с первого сообщения и до переполнения. */
.chat-popover{width:19rem;max-width:calc(100vw - 2rem);display:flex;flex-direction:column;transition:opacity .15s var(--ease-std)}
.chat-messages{display:flex;flex-direction:column;gap:.5rem;height:16rem;overflow:auto;margin-bottom:.6rem}
.chat-messages:has(> .state-note){justify-content:center}
/* Клик мимо чата, пока он открыт, не закрывает панель — только притушивает
   (см. renderRoomTable, обработчик клика вне .chat-widget) — насовсем
   убрать можно лишь повторным нажатием на кнопку чата. Клик/наведение на
   сам виджет — снова полная яркость. */
.chat-widget.chat-unfocused .chat-popover{opacity:.4}
.chat-widget.chat-unfocused .chat-popover:hover{opacity:1}
/* Пузыри в духе обычных мессенджеров — свои справа акцентным цветом, чужие
   слева нейтральным, подпись отправителя только у чужих (сторона и цвет
   и так узнают своё). */
.chat-message{display:flex;flex-direction:column;gap:.15rem;max-width:85%}
.chat-message.own{align-self:flex-end;align-items:flex-end}
.chat-message.other{align-self:flex-start;align-items:flex-start}
.chat-message .meta{display:flex;gap:.35em;font-size:.72rem;color:var(--md-sys-color-on-surface-variant)}
.chat-message .sender{font-weight:700}
.chat-message .bubble{
  padding:.4em .75em;border-radius:var(--md-sys-shape-corner-l);
  font-size:.85rem;line-height:1.35;overflow-wrap:anywhere;
}
.chat-message.own .bubble{
  background:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary);
  border-bottom-right-radius:var(--md-sys-shape-corner-xs);
}
.chat-message.other .bubble{
  background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface);
  border-bottom-left-radius:var(--md-sys-shape-corner-xs);
}
.chat-form{display:flex;gap:.4rem}
.chat-form .text-input{flex:1;min-width:0}
.chat-form .btn{flex:none;width:2.2rem;height:2.2rem}
/* Разрыв соединения — не ошибка, а временное состояние: сокет сам
   переподключается (см. connectRoomSocket), подсказка не должна выглядеть
   тревожно ярче, чем оно того стоит. */
.table-progress.offline{color:var(--md-sys-color-error)}
/* В углу рядом с кнопками масштаба на слово «переподключение…» нет ширины
   (раньше счётчик жил в широкой шапке) — значок вместо него, а сам смысл
   уходит в title (см. connectRoomSocket, onClose/onOpen в app.js). */
.table-progress.offline::before{content:"⟳ ";font-weight:600}
/* connectRoomSocket сдаётся после MAX_ATTEMPTS — сообщение поверх стола,
   а не в потоке (пазл-мир под ним — сплошь position:absolute). */
.table-give-up{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  background:var(--md-sys-color-surface-container-low);border-radius:var(--md-sys-shape-corner-l);
  box-shadow:var(--e2);z-index:2000;max-width:22rem;
}

/* ===================== ОБУЧЕНИЕ НА СТОЛЕ =====================
   Затемнение с «дырой» на подсвеченном элементе: один блок с box-shadow
   0 0 0 9999px — «дыра» получается сама собой, без клип-путей и без
   раскладки затемнения на четыре прямоугольника вокруг цели. Приём,
   разметка и имена классов — те же, что в «Поездках»/«Моих финансах» (см.
   assets/onboarding.js), чтобы обучение выглядело одинаково по всей
   BurningHouse. Слой выше модалок (30) — подсказка не должна оказаться
   под ними. */
.onb-scrim{position:fixed;inset:0;z-index:90;display:none}
.onb-scrim.show{display:block}
.onb-spotlight{
  position:fixed;border-radius:var(--md-sys-shape-corner-l);
  /* 2px раньше терялся на большом столе (много мелких деталей вокруг —
     тонкая рамка тонет в них) — см. правку «Подсказка про тур: жирнее». */
  box-shadow:0 0 0 9999px rgba(10,8,6,.62);pointer-events:none;
  outline:4px solid var(--flame-tip);outline-offset:3px;
  transition:top .25s var(--ease-emph-dec),left .25s var(--ease-emph-dec),
             width .25s var(--ease-emph-dec),height .25s var(--ease-emph-dec);
}
.onb-card{
  position:fixed;z-index:91;max-width:320px;padding:18px;
  background:var(--md-sys-color-surface-container-lowest);
  border:1px solid var(--md-sys-color-outline-variant);
  border-radius:var(--md-sys-shape-corner-l);box-shadow:var(--e2);
  animation:rise .25s var(--ease-emph-dec); /* уже есть в файле — своей у карточек тура не заводим */
}
.onb-card.center{
  top:50%;left:50%;max-width:360px;text-align:center;
  animation:popCenter .25s var(--ease-emph-dec) forwards;
}
@keyframes popCenter{from{opacity:0;transform:translate(-50%,-50%) scale(.92)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}
.onb-card h3{font-size:17px;margin-bottom:6px}
.onb-card p{font-size:13px;line-height:1.5;color:var(--md-sys-color-on-surface-variant);margin-bottom:14px}
.onb-actions{display:flex;align-items:center;gap:8px}
.onb-actions .spacer{flex:1}
.onb-dots{display:flex;gap:5px;justify-content:center;margin-bottom:10px}
.onb-dots i{width:6px;height:6px;border-radius:50%;background:var(--md-sys-color-outline-variant);display:block}
.onb-dots i.on{background:var(--flame-tip)}
/* Подсвеченная цель на время шага показывается целиком, даже если обычно
   проявляется только по наведению (например, меню «…» карточки). */
.onb-target{opacity:1 !important}
@media(max-width:520px){.onb-card{max-width:calc(100vw - 32px)}}

/* Разовая подсказка на «?» стола (см. assets/onboarding.js,
   maybeShowTableHint) — в отличие от .onb-* тура НЕ гасит экран (своего
   scrim тут нет вовсе): часть пришедших по рекламе доходят до стола, но не
   трогают детали (см. правку «Подсказка про тур», данные Метрики) — им
   нужен маленький намёк сбоку, а не блокирующий тур на весь экран. */
.table-hint{
  /* pointer-events:none — пузырь ЛЕЖИТ поверх панели виджетов (z-index:80,
     position:fixed), и на узком экране накрывает её целиком: на телефонах
     из-за этого не работали ни чат, ни список участников, ни звук — тап
     доставался пузырю, а он на клик не реагирует вовсе. Сквозной тап решает
     обе задачи разом: попадает в кнопку под пузырём И закрывает сам пузырь
     (см. onOutside в onboarding.js — он слушает pointerdown на document в
     фазе захвата, и теперь e.target уже не внутри пузыря). Крестику
     pointer-events возвращаем — он единственное, что тут кликают нарочно. */
  pointer-events:none;
  position:fixed;z-index:80;max-width:15rem;padding:.65rem 1.7rem .65rem .8rem;
  background:var(--md-sys-color-surface-container-lowest);
  border:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
  border-radius:var(--md-sys-shape-corner-l);box-shadow:var(--e2);
  animation:rise .25s var(--ease-emph-dec);
}
.table-hint p{font-size:.82rem;line-height:1.4;margin:0}
/* Треугольный «хвостик», указывающий вверх на кнопку — тот же приём, что у
   обычных чат-пузырей: квадрат с двумя видимыми гранями, повёрнутый на 45°. */
.table-hint::before{
  content:"";position:absolute;top:-6px;right:14px;width:11px;height:11px;
  background:var(--md-sys-color-surface-container-lowest);
  border-top:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
  border-left:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
  transform:rotate(45deg);
}
/* Пузырь слева от якоря (см. onboarding.js, positionTableHint, placement
   "left") — хвостик переезжает на правую грань и смотрит вправо: те же две
   видимые грани повёрнутого квадрата, только верхняя+правая вместо
   верхней+левой. */
/* Пузырь НАД якорем (см. positionTableHint, ветка «под кнопкой места нет»)
   — хвостик переезжает на нижнюю грань и смотрит вниз: те же две видимые
   грани повёрнутого квадрата, только нижняя+правая вместо верхней+левой. */
.table-hint.at-above::before{
  top:auto;bottom:-6px;
  border-top:0;border-left:0;
  border-bottom:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
  border-right:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
}
.table-hint.at-left::before{
  top:50%;right:-6px;margin-top:-5.5px;
  border-top:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
  border-left:0;
  border-right:1px solid color-mix(in srgb, var(--flame-tip) 28%, var(--md-sys-color-outline-variant));
}
.table-hint-close{pointer-events:auto;
  position:absolute;top:2px;right:2px;width:1.6rem;height:1.6rem;border:0;background:transparent;
  color:var(--md-sys-color-on-surface-variant);cursor:pointer;font-size:1rem;line-height:1;border-radius:50%;
}
.table-hint-close:hover{background:color-mix(in srgb, currentColor 10%, transparent)}
@media(max-width:420px){.table-hint{max-width:calc(100vw - 24px)}}

/* ===================== КОММЕНТАРИИ К ПАЗЛУ ===================== */
/* Лента под страницей пазла (см. mountPuzzleComments в app.js). Своей
   плашки-карточки у секции нет — она продолжение страницы, а не врезка;
   отделяется только заголовком и линией, как .puzzle-page-related ниже. */
.puzzle-comments{margin-top:2.4rem}
.puzzle-comments[hidden]{display:none}
.puzzle-comments h2{font-size:1.1rem;margin:0 0 1rem}
/* Каждая реплика — отдельная плашка, а не строка, отбитая линией: на
   тёмном фоне ряд разделителей читался как таблица, и было не видно, где
   кончается один комментарий и начинается другой. Теперь граница у каждого
   своя, со всех сторон. */
.comments-list{display:flex;flex-direction:column;gap:.6rem}
.comment{
  padding:.7rem .9rem;
  background:var(--md-sys-color-surface-container-low);
  border:1px solid var(--md-sys-color-outline-variant);
  border-radius:var(--md-sys-shape-corner-l);
}
.comment-head{display:flex;align-items:center;gap:.6rem;margin-bottom:.3rem}
.comment-head b{font-size:.92rem}
.comment-at{font-size:.8rem;color:var(--md-sys-color-on-surface-variant)}
/* Крестик удаления — у правого края строки с автором, приглушённый: он
   есть у каждой своей реплики и у ВСЕХ реплик при входе админом (см.
   canModerate), и в полную яркость превращал бы ленту в ряд кнопок. */
.comment-delete{margin-left:auto;width:1.8rem;height:1.8rem;opacity:.5}
.comment-delete .icon{width:1rem;height:1rem}
.comment-delete:hover,.comment-delete:focus-visible{opacity:1}
.comment-body{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}
.comment-form{display:flex;flex-direction:column;gap:.5rem;margin-top:1rem}
/* Поле и кнопка одной строкой. align-items:flex-end — кнопка держится у
   НИЖНЕГО края поля: textarea можно растянуть мышью (resize:vertical), и
   при центрировании кнопка уезжала бы на середину выросшего поля. */
.comment-form-row{display:flex;align-items:flex-end;gap:.6rem}
.comment-form textarea{
  flex:1;resize:vertical;font:inherit;
  /* Ниже прежнего (было 4.5rem под три строки): тут пишут пару фраз, а
     высокое пустое поле выглядело как заявка на сочинение. Растянуть под
     длинный текст по-прежнему можно вручную. */
  min-height:2.6rem;height:2.6rem;
}
/* Кнопка ровно в высоту поля, квадратная — .btn.icon даёт круг и padding в
   em, из-за которого она бы не совпала с полем по высоте. */
.comment-send{width:2.6rem;height:2.6rem;padding:0;flex:none}
.comment-error{margin:0;color:var(--md-sys-color-error);font-size:.85rem}
.comment-error[hidden]{display:none}

/* ===================== СОСТОЯНИЯ ЗАГРУЗКИ/ОШИБКИ ===================== */
.state-note{padding:2.4rem 1rem;text-align:center;color:var(--md-sys-color-on-surface-variant)}

/* Раньше тут .table-progress{display:none} — счётчик прятали на узком
   экране, потому что он не влезал в шапку. Шапки больше нет, в углу он
   помещается и на телефоне, поэтому правило убрано. */
