  /* Design tokens live in theme.css (loaded first). Legacy aliases kept so the
     older component rules below keep resolving while they migrate. */
  :root{
    /* высота sticky-шапки; настоящее значение подставляет core.js (trackHeaderHeight),
       здесь — запасное на случай, если модуль ещё не выполнился */
    --hdr-h:66px;
    --accent-dark:var(--accent-ink);
    --checked:var(--ok);
    --red:var(--danger);
    --green:var(--ok);
  }

  /* ---- app chrome ---------------------------------------------------------- */
  /* blox31 #1: три зоны — «назад» слева, логотип строго по центру, правый кластер (dev-список,
     ник, шестерёнка) справа. Боковые колонки равной ширины (minmax(0,1fr)) держат логотип по
     центру; длинное «назад» режется многоточием — но только потому, что слот .hdr-back растянут
     по своей дорожке (см. его правило ниже, blox52 #16 п.1). */
  /* env(safe-area-inset-*) через max() — фон шапки тянется до физических краёв (viewport-fit=cover),
     а содержимое (логотип, ник, шестерёнка) отодвинуто от выреза/скруглённого угла в горизонтали.
     Без выреза env()=0 → max() берёт базовые 14/18px, раскладка не меняется. */
  header{
    position:sticky;top:0;z-index:40;
    /* ДВА СЛОЯ ФОНА: сверху свет в углу (--hdr-sheen), под ним сама заливка схемы.
       Здесь не переопределяется НИЧЕГО: и заливка, и форма света — токены в theme.css,
       а силу света каждая схема объявляет у себя (у «Lingo» она вполсилы, по замеру
       контраста белого текста). */
    background:var(--hdr-sheen),var(--grad-header);color:var(--on-dark);
    padding:max(14px,env(safe-area-inset-top)) max(18px,env(safe-area-inset-right))
            14px max(18px,env(safe-area-inset-left));
    /* Три колонки: назад · логотип · правый кластер. У ПРАВОЙ минимум = min-content, а не 0
       (blox37 #7). С minmax(0,1fr) её дорожка сжималась уже 375px, но содержимое (выпадашка
       ребёнка + шестерёнка) не сжималось вместе с ней и вылезало ВЛЕВО — прямо на слово «Lingo»
       (перекрытие 30px). Теперь дорожка не бывает уже своего содержимого: тесно — логотип
       уезжает от центра влево, но столкновения не будет никогда. Логотип — украшение,
       переключатель ребёнка и шестерёнка — органы управления; уступать обязано украшение. */
    display:grid;grid-template-columns:minmax(0,1fr) auto minmax(min-content,1fr);
    align-items:center;gap:14px;
    /* ПЛАНКА, А НЕ КАРТОЧКА (blox52 #5). Была --sh-2 — ровно та же мягкая тень, что у
       любого листа, и шапка читалась большой карточкой. Планка собрана своим рецептом:
       волосок по нижней кромке + чёткая линия контакта + короткая тень (theme.css, «ПЛАНКА
       ШАПКИ»).
       ЭТА СТРОКА — ЗАПАСНОЙ ВАРИАНТ, и она нарочно лежит СНАРУЖИ @supports ниже. Браузер,
       который не умеет привязывать анимацию к прокрутке, видит только её и получает
       постоянную тень — сегодняшнее поведение, просто новой формы. Перенести её внутрь
       @supports значит молча оставить такие браузеры вообще без тени; за этим следит
       секция «планка шапки» в npm run check. */
    box-shadow:var(--plank);
  }
  /* ТЕНЬ ПОЯВЛЯЕТСЯ ОТ ПРОКРУТКИ — и ни строчки на JS. Пока страница в нуле, бумага начинается
     вплотную под шапкой: тени взяться неоткуда, и планка плоская. Тень набирает полную силу за
     первые 40px прокрутки — ровно на том пути, где под шапку заезжает содержимое.
     Почему CSS, а не обработчик scroll: обработчик считался бы на каждом кадре прокрутки (на
     телефоне это самый дорогой кадр из всех) и требовал бы записи в DOM; здесь же браузер сам
     двигает анимацию по позиции прокрутки, вне основного потока вычислений. Ничего не
     перерисовывается и не переставляется — значит и «прыжка экрана» здесь взяться неоткуда.
     @supports обязателен: привязка к прокрутке есть не везде. Где её нет — правило целиком
     не применяется, и остаётся постоянная тень из header выше. Деградация незаметная.
     Число слоёв в --plank-flat и --plank совпадает намеренно — иначе браузеру нечего
     интерполировать и тень включалась бы рывком. */
  @supports (animation-timeline: scroll()){
    @keyframes hdr-plank{
      from{box-shadow:var(--plank-flat)}
      to{box-shadow:var(--plank)}
    }
    header{
      animation-name:hdr-plank;
      animation-timing-function:linear;
      animation-fill-mode:both;
      /* длительность НЕ пишем: при живой шкале прокрутки её значение по умолчанию (auto)
         и означает «растянуть на весь диапазон ниже». Записать сюда секунды — сломать связь. */
      animation-timeline:scroll(root block);
      animation-range:0 40px;
    }
  }
  /* justify-self:stretch, НЕ start (blox52 #16 п.1, нашли оба судьи): start-выравненный
     грид-элемент ширится ПО СОДЕРЖИМОМУ и переливается через свою дорожку — на 375 кнопка
     «‹ Кабинет» (113px при дорожке 84px) въезжала прямо в точку у слова Lingo. Stretch
     прижимает слот к ширине дорожки, и многоточие .mlabel наконец получает право резать
     подпись; кнопка внутри и так стоит слева (flex). Знак не столкнётся ни в каком шрифте:
     ограничение геометрическое, а не подобранное под Onest. */
  .hdr-back{justify-self:stretch;display:flex;align-items:center;min-width:0;overflow:hidden}
  /* в шапке сбрасываем margin-bottom тела-версии .mback — иначе он занимает место снизу и при
     align-items:center кнопка визуально уезжает вверх (blox31 #… — «съехала чуть вверх») */
  /* та же контурная кнопка B, но на тёмной шапке: полупрозрачно-белый контур (язык .set-btn),
     margin сброшен — иначе margin-bottom тела-версии уводит кнопку вверх при align центре. */
  .hdr-back .mback{color:var(--on-dark);border-color:rgba(255,255,255,.35);max-width:100%;margin:0}
  @media (hover:hover){.hdr-back .mback:hover{border-color:rgba(255,255,255,.55);background:rgba(255,255,255,.14);color:var(--on-dark)}}
  .hdr-right{justify-self:end;display:flex;align-items:center;gap:10px;min-width:0}
  header .logo{
    font-weight:800;font-size:var(--fs-2xl);letter-spacing:var(--tracking-tight);
    display:flex;align-items:center;gap:9px;justify-self:center;white-space:nowrap;
  }
  header .logo::before{
    content:"";width:11px;height:11px;border-radius:50%;
    /* точка — часть ЗНАКА, поэтому она на --brand-dot, а не на акценте: базовая схема увела
       акцент в чёрный (заход 3.2). Ореол — color-mix от неё же: зашитая небесная rgba
       оставалась синей даже на Maya-точке синих схем. */
    background:var(--brand-dot);box-shadow:0 0 0 4px color-mix(in srgb, var(--brand-dot) 22%, transparent);
  }
  header .who{display:flex;align-items:center;gap:8px;min-width:0}
  header .auth{display:flex;align-items:center;gap:10px}
  /* на телефоне режем отступы/зазоры шапки — иначе правый кластер не влезает в одну строку */
  @media(max-width:430px){
    header{padding:max(12px,env(safe-area-inset-top)) max(10px,env(safe-area-inset-right))
                   12px max(10px,env(safe-area-inset-left));gap:8px}
    header .who{gap:6px}
    header .auth{gap:8px}
    .hdr-right{gap:8px}
    header .who select{max-width:72px}   /* dev-элемент; ужат, чтобы не наезжать на центр-логотип */
    header .who span{display:none}   /* «Кто я:» — dev-подпись, на телефоне прячем ради ширины */
  }
  /* строка выбора шрифта — используется в попапе настроек (settings.js, blox18 #9) */
  .fsw-item{display:grid;grid-template-columns:1fr auto;align-items:center;gap:2px 10px;
    background:none;border:none;border-radius:var(--r-sm);padding:9px 11px;cursor:pointer;text-align:left;
    color:var(--ink);font-size:var(--fs-md);font-weight:700}
  @media (hover:hover){.fsw-item:hover{background:var(--accent-tint)}}
  .fsw-item.on{background:var(--accent-tint)}
  .fsw-item .fsw-note{grid-column:1;font-size:var(--fs-xs);font-weight:600;color:var(--muted)}
  .fsw-item .fsw-tick{grid-column:2;grid-row:1 / span 2;color:var(--accent-dark);font-weight:800;
    opacity:0}
  .fsw-item.on .fsw-tick{opacity:1}

  /* единый попап настроек (шестерёнка) — шрифт + цветовая схема (blox18 #9).
     Пункты шрифта переиспользуют .fsw-item выше. */
  .settings{position:relative;display:flex;align-items:center}
  .set-btn{width:38px;height:38px;border-radius:var(--r-sm);border:1px solid rgba(255,255,255,.35);
    background:rgba(255,255,255,.12);color:var(--on-dark);cursor:pointer;
    display:flex;align-items:center;justify-content:center}
  @media (hover:hover){.set-btn:hover{background:rgba(255,255,255,.22)}}
  .set-menu{display:none;position:absolute;top:calc(100% + 8px);right:0;z-index:50;
    width:max-content;max-width:min(300px,calc(100vw - 24px));
    max-height:calc(100vh - 84px);overflow-y:auto;   /* не обрезаем «Выйти» на низкой ПК-высоте (blox31 #1) */
    /* РАМКИ НЕТ (blox52 #4, п.1): край листа задан обрезным краем внутри --sheet-pop и
       контрастом к тому, на чём меню висит. Рамка поверх — второе объявление того же факта.
       Бортик здесь ослаблен (--sheet-pop): меню висит вплотную под ТЁМНОЙ шапкой, и
       95%-белая грань блестела бы над ним полосой. */
    background:var(--card);border-radius:var(--r-lg);box-shadow:var(--sheet-pop);
    padding:8px;flex-direction:column;gap:4px}
  .set-menu.open{display:flex}
  .set-sec{display:flex;flex-direction:column;gap:2px}
  /* ГРАВИРОВАННАЯ ЛИНЕЙКА (blox52 #4, п.3): волосок + светлый блик ПОД ним — то же
     тиснение, что на карточке, только в одну строку. Так разделитель перестаёт быть
     просто серой чертой поверх бумаги и читается оттиском В бумаге. */
  .set-sec + .set-sec{margin-top:6px;padding-top:8px;border-top:1px solid var(--line-soft);
    box-shadow:var(--rule-t)}
  .set-h{font-size:var(--fs-2xs);font-weight:800;letter-spacing:.06em;text-transform:uppercase;
    color:var(--muted);padding:2px 11px 4px}
  .set-scheme{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:2px 11px;
    background:none;border:none;border-radius:var(--r-sm);padding:9px 11px;cursor:pointer;text-align:left;
    color:var(--ink);width:100%}
  @media (hover:hover){.set-scheme:hover{background:var(--accent-tint)}}
  .set-scheme.on{background:var(--accent-tint)}
  .set-dots{grid-row:1 / span 2;display:flex;gap:3px}
  .set-dots i{width:13px;height:13px;border-radius:50%;border:1px solid rgba(0,0,0,.08)}
  .set-sbody{grid-column:2;display:flex;flex-direction:column}
  .set-sname{font-size:var(--fs-md);font-weight:700}
  .set-snote{font-size:var(--fs-xs);font-weight:600;color:var(--muted)}
  .set-scheme .fsw-tick{grid-column:3;grid-row:1 / span 2;color:var(--accent-dark);font-weight:800;opacity:0}
  .set-scheme.on .fsw-tick{opacity:1}
  .auth .auth-me{font-weight:700;font-size:var(--fs-sm);opacity:.95}
  /* переключатель детей в шапке ребёнка: без ограничения ширины нативный select раздувается
     до самого длинного пункта («➕ Добавить») и вся шапка ломается на две строки на телефоне.
     Ограничиваем ширину и обрезаем длинное имя многоточием — как у учителя, одна строка. */
  .auth .kid-switch{max-width:150px;text-overflow:ellipsis;padding:9px 10px}
  /* «Выйти» переехал из шапки вниз всплывашки настроек (blox31 #1). Отдельная секция снизу,
     отбита линией; мягкий красноватый акцент как у деструктивного действия, но негромкий. */
  .set-sec-logout{margin-top:6px;padding-top:8px;border-top:1px solid var(--line-soft);
    box-shadow:var(--rule-t)}
  .set-logout{width:100%;background:none;border:none;border-radius:var(--r-sm);padding:10px 11px;
    cursor:pointer;text-align:left;color:var(--alert-ink);font-weight:700;font-size:var(--fs-md)}
  @media (hover:hover){.set-logout:hover{background:var(--alert-tint)}}
  /* вход в код-вику из приложения: без него владелец, войдя, оказывался внутри линго без
     дороги обратно и набирал адрес руками. Видно только персоналу — body.teacher ставится
     в render() для всех ролей кроме ученика, так что отдельной проверки роли не нужно. */
  .set-sec-wiki{display:none}
  body.teacher .set-sec-wiki{display:block;margin-top:6px;padding-top:8px;
    border-top:1px solid var(--line-soft);box-shadow:var(--rule-t)}
  .set-wiki{display:block;width:100%;border-radius:var(--r-sm);padding:10px 11px;
    text-decoration:none;text-align:left;color:var(--ink);font-weight:700;font-size:var(--fs-md)}
  @media (hover:hover){.set-wiki:hover{background:var(--surface-2)}}
  /* blox37 #1 — «Сообщить о проблеме». Тот же пункт-строка, что «Код-вики» и «Выйти», но виден
     ВСЕМ ролям: застревают чаще родители и дети. min-height 44px — палец (WCAG 2.5.5). */
  .set-sec-support{margin-top:6px;padding-top:8px;border-top:1px solid var(--line-soft);
    box-shadow:var(--rule-t)}
  .set-support{display:block;width:100%;min-height:44px;background:none;border:none;border-radius:var(--r-sm);
    padding:10px 11px;text-align:left;color:var(--ink);font-weight:700;font-size:var(--fs-md);cursor:pointer}
  @media (hover:hover){.set-support:hover{background:var(--surface-2)}}
  /* на телефоне запас по ширине минимальный — режем селект сильнее, чтобы шапка держала одну строку.
     Стоит ПОСЛЕ базового max-width:150px, иначе при равной специфичности победит базовое правило. */
  @media(max-width:430px){ .auth .kid-switch{max-width:120px} }
  .ya-btn{display:inline-flex;align-items:center;gap:7px;background:#fff;color:#000;
    text-decoration:none;font-weight:700;font-size:var(--fs-sm);padding:8px 14px;border-radius:var(--r-sm);
    border:none;cursor:pointer}
  /* «Я» — фирменный знак Яндекса, а не иконка интерфейса: и кегль, и скругление привязаны
     к СВОЕЙ плашке (20px и 26px), а не к нашим лестницам --ic-* и --r-*. Поэтому здесь и в
     -lg числа оставлены числами: сдвинь мы ступень — знак перестанет попадать в свой
     квадрат, а его угол разойдётся с углом квадрата. */
  .ya-btn::before{content:"Я";display:inline-flex;align-items:center;justify-content:center;
    width:20px;height:20px;border-radius:5px;background:var(--ya-red);color:#fff;font-weight:800;font-size:14px}
  .ya-btn-lg{font-size:var(--fs-lg);padding:14px 22px;border-radius:var(--r-md);box-shadow:var(--sh-2)}
  .ya-btn-lg::before{width:26px;height:26px;font-size:18px;border-radius:7px}
  .landing{min-height:70vh;display:flex;align-items:center;justify-content:center;padding:24px}
  .landing-card{max-width:380px;text-align:center;background:var(--card);
    border-radius:var(--r-2xl);padding:38px 30px;box-shadow:var(--sheet-3)}
  .landing-logo{font-weight:800;font-size:var(--fs-4xl);letter-spacing:var(--tracking-tight);margin-bottom:10px;
    background:var(--grad-wordmark);
    -webkit-background-clip:text;background-clip:text;color:transparent}
  .landing-sub{color:var(--muted-strong);font-size:var(--fs-md);line-height:1.5;margin:0 0 22px}
  /* Две двери входа — «Ученик» и «Учитель» — стоят столбиком и НАМЕРЕННО одинаковы: роль
     решает строка в users, а не кнопка. Подпись БЕЗ «Я»: значок Яндекса рисуется через
     .ya-btn::before (красный квадрат с буквой «Я»), и «Я ученик» читалось как «Я Я ученик».
     Выделять одну из дверей нечем. Растягиваем по ширине карточки, иначе inline-flex сажает
     их в одну строку. */
  .landing-doors{display:flex;flex-direction:column;gap:10px}
  .landing-doors .ya-btn{width:100%;justify-content:center;box-sizing:border-box}
  /* 44px высоты — это ссылка-кнопка на экране входа, палец промахивается по 18-пиксельной строке */
  .switch-link{display:block;margin:8px auto 0;padding:12px 4px;min-height:44px;box-sizing:border-box;
    color:var(--accent-ink);font-size:var(--fs-sm);text-decoration:underline}
  /* Ошибка входа. Живёт НА экране входа, а не в alert: почти всегда причина в том, что браузер уже
     залогинен не в тот Яндекс-аккаунт, и рядом с текстом обязана быть кнопка «выбрать другой». */
  .login-err{text-align:left;border:1px solid var(--alert-line);border-radius:var(--r-lg);
    background:var(--alert-tint);padding:12px 14px;margin:0 0 18px}
  .login-err-msg{display:flex;gap:8px;font-size:var(--fs-md);line-height:1.45;font-weight:600;color:var(--ink)}
  .login-err-msg svg{width:18px;height:18px;flex:0 0 18px;margin-top:2px;color:var(--alert-ink)}
  .login-err-em{margin-top:8px;font-size:var(--fs-sm);color:var(--muted-strong);overflow-wrap:anywhere}
  select{
    border:1px solid var(--line);border-radius:var(--r-sm);padding:9px 12px;font-size:var(--fs-md);
    background:var(--card);color:var(--ink);font-weight:600;
  }
  header select{border-color:transparent}
  /* боковые/нижний отступы контента учитывают вырез (viewport-fit=cover): в горизонтали карточки
     не заезжают под вырез слева/справа, а низ страницы не прячется под home-индикатор. env()=0 без
     выреза → max()/сумма дают базовые значения, обычный экран не меняется. */
  main{max-width:900px;margin:0 auto;
    padding:var(--sp-5) max(var(--sp-4),env(safe-area-inset-right))
            calc(var(--sp-8) + env(safe-area-inset-bottom)) max(var(--sp-4),env(safe-area-inset-left))}
  /* blox53 #29 — КОЛОНТИТУЛ ШКОЛЫ у нижнего края. Лежит на СТОЛЕ, а не на бумаге, поэтому цвет
     --on-bg (у --faint на столе 1.9:1 — подпись исчезла бы). Тише контента и по центру: это
     подпись документа, а не сообщение — она не должна спорить с последней карточкой листа.
     Отступ снизу учитывает вырез телефона тем же способом, что и main. */
  .pfoot{max-width:900px;margin:0 auto;
    padding:0 max(var(--sp-4),env(safe-area-inset-right))
            calc(var(--sp-6) + env(safe-area-inset-bottom)) max(var(--sp-4),env(safe-area-inset-left));
    text-align:center;color:var(--on-bg);font-size:var(--fs-xs);line-height:1.5}
  .pfoot-b{font-weight:700;white-space:nowrap}
  .sub{color:var(--muted);margin:4px 0 18px;font-size:var(--fs-md)}
  h2{margin:0 0 2px;font-size:var(--fs-2xl);font-weight:800;color:var(--ink-strong)}

  .badge{font-size:var(--ic-lg)}

  /* ===== STUDENT HOMEWORK — the flagship list =================================
     A task used to be a saturated colour block with white text. It is now a white
     card carrying a colour RAIL on its left edge (--rail, set by JS from the task
     type: grape = nothing to hand in, sky = a turn-in is expected). Same colour
     code, far better legibility, and it finally rhymes with the teacher's cards
     (.ecard/.srcard), which were already white-with-a-stripe. */
  /* ===== ТИСНЁНАЯ ПЛАШКА — ОДИН материал четырёх карточек (blox51 #3, заход 2) =====
     .rect — таск у ученика · .bitem — таск в черновике ДЗ · .oent — таск на мониторе
     · .srcard — таск в личке ученика. Все четыре говорили об одном и том же предмете,
     но были собраны по-разному: своя тень, своя граница, свои цифры. Теперь это один
     ЛИСТ БУМАГИ, в который ВДАВЛЕН рельс, а геометрия (padding, радиус, плотность)
     остаётся у каждой своя — ниже в её собственном блоке.
     Цвет рельса везде приходит одним путём: --rail (JS ставит его из itemColor).
     Материал целиком собран из токенов --emb* — см. theme.css, «ТИСНЕНИЕ».
     Рельс живёт в ::before, а НЕ в border-left: границу нельзя ни скруглить внутри
     плашки, ни утопить — а именно вдавленность и читается как «дорого». */
  /* --rail ставит JS ИНЛАЙНОМ (цвет по типу таска), а инлайн не перебить классом. Поэтому
     светит не он, а --rail-eff: состояния карточки (.done, .chal.done) меняют именно его.
     Раньше ту же роль играл border-left-color, который JS не трогал. */
  .rect,.bitem,.oent,.srcard{--rail:var(--task-plain);--rail-eff:var(--rail);position:relative;
    background:var(--card);box-shadow:var(--sheet-1);
    --emb-rail:linear-gradient(180deg,
      color-mix(in srgb, var(--rail-eff) 86%, var(--emb-deep)) 0%,
      var(--rail-eff) 44%,
      color-mix(in srgb, var(--rail-eff) 90%, var(--emb-lift)) 100%)}
  .rect::before,.bitem::before,.oent::before,.srcard::before{content:"";position:absolute;
    left:var(--rail-x);top:var(--rail-y);bottom:var(--rail-y);width:var(--rail-w);
    border-radius:calc(var(--rail-w) / 2);
    background:var(--emb-rail);box-shadow:var(--emb-groove)}
  /* значения по умолчанию — крупная плашка; плотные (монитор) сужают их у себя */
  .rect,.bitem,.oent,.srcard{--rail-x:5px;--rail-y:9px;--rail-w:7px}

  .hwlist{display:flex;flex-direction:column;gap:10px}
  /* flex-wrap: плеер (.rplayer) переносится на СВОЮ строку во всю ширину плашки — в узкой колонке
     .rbody он не помещался и на телефоне обрезался по краю карточки (blox10 #10) */
  /* padding-right резервирует правую колонку под «сделал» (.rcheck, абсолютная во всю высоту), чтобы
     серый столбец тянулся до низа карточки даже когда плеер удлиняет её (blox18 #7) */
  .rect{display:flex;flex-wrap:wrap;align-items:stretch;
        padding-right:66px;padding-left:15px;
        border:1px solid var(--line-soft);
        border-radius:var(--r-lg);overflow:hidden;color:var(--ink);
        transition:opacity var(--t-slow) var(--ease),box-shadow var(--t) var(--ease),
                   transform var(--t-fast) var(--ease),background var(--t-slow) var(--ease)}
  @media (hover:hover){.rect:hover{box-shadow:var(--sheet-2)}}
  /* done = the card quietly recedes: celadon wash + faded rail + no shadow carry the state.
     No blanket opacity — it dragged the title/links below 4.5:1 (axe color-contrast); the green
     wash alone already reads as «done». */
  /* «сделано» — карточка тихо отступает: тень снимается, но ОБРЕЗНОЙ КРАЙ остаётся,
     иначе выполненный таск перестаёт быть листом и становится пятном заливки. */
  .rect.done{background:var(--ok-tint);border-color:var(--ok-line);--rail-eff:var(--ok-line);
        box-shadow:var(--sheet-edge)}
  .rect.done .rtitle{color:var(--muted)}
  /* blox6: task-type icon on the LEFT edge of the task — now a tinted round chip */
  .rect .ricon{flex:0 0 auto;display:flex;align-items:flex-start;justify-content:center;
        padding:14px 2px 0 3px;font-size:var(--ic-lg);line-height:1}
  .rect .rbody{flex:1;min-width:0;padding:14px 14px 14px 10px;display:flex;flex-direction:column;gap:9px}
  /* заглавная строка карточки: один размер на всех экранах (--fs-md, 15px), по образцу
     конструктора (blox46 #2). У РЕБЁНКА в ней до первого открытия стоит тип таска — см. blox46 #3 */
  .rect .rtitle{font-weight:700;font-size:var(--fs-md);line-height:1.3;color:var(--ink);
        letter-spacing:var(--tracking-tight);overflow-wrap:anywhere;text-wrap:balance}
  /* ...а после открытия — САМО задание. Не режем его ничем: шторку убрали (blox46 #8), дочитать
     длинный текст больше негде, карточка — единственное место, где он живёт. Переводы строк
     учительского текста сохраняем, как их сохраняла шторка */
  /* И ВЕС ТУТ ДРУГОЙ (blox52 #6). Пока в строке стоит ТИП таска («Read by yourself») — это
     ярлык, 700 на месте. Как только на её месте оказывается само задание, это уже абзац,
     который ребёнок читает, и 700 превращали короткий текст в вывеску: ученик видел афишу
     вместо задания. Переносов здесь нет намеренно — задание учитель чаще всего пишет
     по-английски, а словарь переносов взялся бы русский (язык страницы). text-wrap:pretty
     перебивает balance, унаследованный от .rtitle: абзацу нужно ровное «дно», а не ровные
     строки. */
  .rect .rtitle-body{white-space:pre-wrap;font-weight:var(--fw-read);text-wrap:pretty}
  /* тип таска подписью — появляется, когда заголовок занят самим заданием */
  .rect .rsub{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:-5px;
        font-size:var(--fs-sm);font-weight:700;color:var(--muted)}
  /* имя прикреплённого файла — под типом таска, тише заголовка, но читаемо */
  .rect .rfile{display:flex;align-items:center;gap:6px;font-size:var(--fs-sm);font-weight:600;
        color:var(--muted);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:-3px}
  .rect .rstatus{font-size:var(--fs-sm);font-weight:600;color:var(--muted)}

  /* ---- ЧЕЛЛЕНДЖ (blox15 #6) — та же плашка .rect, тот же рельс, только он ЗОЛОТОЙ.
     blox23 #1: финишный флаг переехал из низа иконочной колонки к верхней строке счётчиков-витрины
     (см. .rp-count ниже). Выполненный челлендж НЕ гаснет как обычный таск: витрина должна быть видна. */
  .rect.chal{border-color:var(--gold-line);background:var(--gold-tint)}
  .rect.chal.done{background:var(--gold-tint);border-color:var(--gold-line);
        --rail-eff:var(--gold);opacity:1}
  .rect.chal.done .rtitle{color:var(--ink)}
  .rect .rflag{color:var(--gold-ink);font-size:var(--ic-md);display:inline-flex;align-items:center;flex:0 0 auto}
  .rect .rdue{font-size:var(--fs-sm);font-weight:700;color:var(--gold-ink);
        background:var(--card);border:1px solid var(--gold-line);border-radius:var(--r-pill);padding:1px 8px;
        white-space:nowrap}
  /* витрина: кто сделал, кто нет (у слушательного челленджа — с числом наслушек) */
  .rect .rpeers{display:flex;flex-direction:column;gap:3px;font-size:var(--fs-sm);
        border-top:1px dashed var(--gold-line);padding-top:8px}
  .rect .rp-count{font-weight:800;color:var(--gold-ink);display:flex;align-items:center;gap:6px}
  .rect .rp-did{color:var(--ok-ink);overflow-wrap:anywhere}
  .rect .rp-did b{color:var(--gold-ink)}
  .rect .rp-not{color:var(--muted);overflow-wrap:anywhere}
  /* padding + отрицательный margin: зона нажатия ≥24px (G5), а видимая надпись стоит где стояла */
  /* «переотправить» живёт ВНУТРИ зелёной сделанной карточки (.rect.done, --ok-tint): тревожные
     чернила в состоянии успеха читались «что-то не так» (blox52 #11). Акцентные чернила схемы —
     это «действие текстом», и на ok-tint они читаются во всех схемах (замер 06.09.2026: 14.1 ·
     5.4 · 5.4 · 11.0). */
  .rect .resend{background:none;border:none;cursor:pointer;font-family:inherit;font-size:var(--fs-sm);
    font-weight:700;color:var(--accent-ink);text-decoration:underline;
    padding:8px 6px;margin:-8px -6px -8px 2px}
  /* КОММЕНТАРИЙ УЧИТЕЛЯ К ТАСКУ — читаемый текст (blox52 #6), и лежит он в ОДНОЙ карточке с
     самим заданием. Оставить его на 600 рядом с заданием на 450 значило бы, что внутри одного
     предмета два веса у двух одинаковых по роли текстов — ровно та разноголосица, которую эта
     пачка убирает. Переносов нет: учитель пишет комментарий по-английски не реже, чем по-русски. */
  .rect .rcomment{font-size:var(--fs-xs);font-weight:var(--fw-read);color:var(--ink);background:var(--surface-2);
        border:1px solid var(--line-soft);border-radius:var(--r-xs);padding:7px 10px;text-wrap:pretty}
  /* blox22 #3: свёрнутый «Мой ответ» — ученик пересматривает свою сдачу */
  /* blox27 #4 — «Мой ответ» показан сразу сущностью файла (не тоггл): подпись + кликабельная карточка */
  .rect .myans{margin-top:6px}
  .rect .myans-lbl{display:flex;align-items:center;gap:5px;font-size:var(--fs-sm);font-weight:700;
        color:var(--accent-ink);margin-bottom:6px}

  /* blox26 #3 — ЕДИНАЯ «сущность приложенного файла»: скруглённая карточка [иконка типа · имя · глаз].
     Одна на экране ученика («Мой ответ») и в проверке учителя. Медиа НЕ разворачивается в потоке —
     открывается по клику во встроенном оверлее .mediaov (фото вписано; видео/аудио через наш плеер).
     Это и убрало переполнение на широком экране, и наложение двух плееров. */
  .fileent{display:flex;align-items:center;gap:10px;max-width:100%;padding:9px 11px;border-radius:var(--r-sm);
        background:var(--surface);border:1px solid var(--line);cursor:pointer;color:var(--ink);
        text-decoration:none;box-shadow:var(--sh-1)}
  @media (hover:hover){.fileent:hover{border-color:var(--accent-line)}}
  .fileent-ico{flex:0 0 auto;font-size:var(--ic-md);line-height:1;display:flex;color:var(--accent-ink)}
  .fileent-nm{flex:1;min-width:0;font-weight:650;font-size:var(--fs-sm);line-height:1.3;overflow-wrap:anywhere}
  .fileent-view{flex:0 0 auto;width:34px;height:34px;border:none;border-radius:var(--r-sm);cursor:pointer;
        background:var(--accent-tint);color:var(--accent-ink);display:flex;align-items:center;justify-content:center}
  .fileent-view:active{transform:scale(.94)}
  .fileent-go{flex:0 0 auto;font-weight:800;color:var(--accent-ink)}

  /* медиа-оверлей сдачи: фото вписано (не вылезает за экран), видео/аудио — через наш плеер */
  .mediaov .mediabox{background:var(--card);border-radius:var(--r-lg);box-shadow:var(--sheet-4);
        width:100%;max-width:min(900px,94vw);max-height:92vh;overflow:auto;padding:12px 14px}
  .mediaov .mediabody{padding:8px 0 4px;display:flex;justify-content:center}
  .mediaimg{max-width:100%;max-height:78vh;object-fit:contain;border-radius:var(--r-xs);display:block}
  /* blox33 #2 — сдача архивирована по сроку хранения: спокойная заглушка вместо плеера */
  .sub-archived{display:flex;align-items:center;gap:10px;color:var(--muted);font-size:var(--fs-sm);
        padding:22px 16px;text-align:center;line-height:1.4}
  .sub-archived svg{width:22px;height:22px;flex:0 0 auto;opacity:.75}
  /* flickering (edited): the teacher changed the task after the pupil did it */
  .edited-flag{font-size:var(--fs-xs);font-weight:800;color:var(--on-dark);background:var(--alert-solid);
        border-radius:var(--r-xs);padding:2px 8px;white-space:nowrap;animation:editblink 1.4s var(--ease) infinite}
  @keyframes editblink{0%,100%{opacity:1}50%{opacity:.35}}
  .rect .ractions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
  /* position:relative — якорь коробки волны .ripple-box (blox52 #13: .ract вошёл в SURFACES);
     без него волна вписалась бы в ближайшего позиционированного предка — во всю карточку .rect */
  .rect .ract{border:1px solid var(--line);border-radius:var(--r-sm);padding:10px 15px;
        font-size:var(--fs-sm);font-weight:700;background:var(--card);color:var(--ink);cursor:pointer;
        text-decoration:none;display:inline-block;text-align:center;position:relative;
        transition:border-color var(--t) var(--ease),background var(--t) var(--ease),transform var(--t-fast) var(--ease)}
  @media (hover:hover){.rect .ract:hover{border-color:var(--accent-deep);background:var(--accent-tint)}}
  .rect .ract:active{transform:scale(.96)}
  /* ГЛАВНОЕ ДЕЙСТВИЕ КАРТОЧКИ ЗАЛИТО акцентом схемы (blox52 #13). «Открыть задание» /
     «▶ Слушать» — то, ради чего карточка существует, а была самым тихим её элементом
     (белая контурная). Рецепт — тот же, что у .btn--primary (движок кнопок): --grad-primary
     под белым текстом (AA замерен в blox52 #10, включая верхний стоп), блик+фаска --bevel,
     hover — свечение своим цветом. Волна на тёмной заливке — normal (умножение белым —
     тождество, blox52 #8). Вторая, тихая кнопка «Страница» (.page) остаётся контурной базой
     выше — у карточки одно громкое действие. */
  .rect .ract.open:not(.opened),.rect .ract.play{border-color:transparent;background:var(--grad-primary);
        color:var(--on-dark);box-shadow:var(--bevel),var(--sh-1);--ripple-blend:normal;
        display:inline-flex;align-items:center;justify-content:center;gap:6px}
  @media (hover:hover){.rect .ract.open:not(.opened):hover,.rect .ract.play:hover{border-color:transparent;
        background:var(--grad-primary);box-shadow:var(--bevel),0 6px 18px color-mix(in srgb,var(--anchor) 42%,transparent)}}
  /* «Открыто ✓» (blox52 #13) — кнопка «Открыть задание» после тапа НЕ исчезает (удаление
     меняло высоту карточки под пальцем), а становится контурной disabled-меткой ТЕХ ЖЕ
     размеров (min-width/height фиксирует JS). Нарочно НЕ зелёная: зелёный занят смыслом
     «сделано/принято» (§11), а «открыто» — тихий факт. Стоит ПОСЛЕ правила заливки:
     специфичность равная, побеждает порядок. */
  .rect .ract.opened{background:var(--card);border-color:var(--line-mid);color:var(--muted);
        cursor:default;box-shadow:none}
  .rect .ract.opened:active{transform:none}
  /* blox24 #9: файл-таск без файла — нечего слушать/открывать; тихая пометка вместо кнопки */
  .rect .ract-empty{display:inline-flex;align-items:center;gap:6px;font-size:var(--fs-sm);
        font-weight:600;color:var(--muted)}
  .rect audio,.rect video{width:100%;margin-top:2px;border-radius:var(--r-sm)}
  /* строка плеера: во всю ширину карточки, поверх обеих колонок */
  .rect .rplayer{flex:0 0 100%;min-width:0;padding:0 12px 12px}
  .rect .rplayer .lp{margin:0 auto}   /* на десктопе плеер не растягивается, а стоит по центру */
  /* у контрольного файла нет колонки «сделал» (.rcheck), поэтому 66px-резерв справа — мёртвый:
     он сдвигал плеер влево от центра плашки и оставлял пустой столбец. Снимаем — плеер встаёт
     ровно по центру таска, во всю ширину. Под .rcheck заезжать нечему (её тут нет). */
  .rect.ctrltask{padding-right:0}
  /* the check: a tall tap target on the right, ringed circle -> filled celadon tick */
  /* ВОЛНА ОТ КАСАНИЯ (blox52 #9). Это ГЛАВНАЯ кнопка ребёнка — десятки нажатий в неделю, и
     именно на телефоне обычное «нажатое» рисуется непредсказуемо, ради чего волна и заведена
     (см. public/ripple.js, список SURFACES). Поверхность бумажная, поэтому чернила берём
     зелёные — цвет самой отметки: волна показывает, ЧЕМ станет кружок. Режим (умножение днём,
     обычный ночью) приезжает от схемы сам, как у плитки кабинета. Колонка уже позиционирована
     (position:absolute), так что коробке волны есть во что вписаться. */
  .rect .rcheck{position:absolute;top:0;right:0;bottom:0;width:66px;border:none;border-left:1px solid var(--line-soft);
        background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;
        -webkit-tap-highlight-color:transparent;--ripple-ink:var(--ok-ink);
        transition:background var(--t) var(--ease)}
  @media (hover:hover){.rect .rcheck:hover{background:var(--fill-soft)}}
  .rect .rcheck:active{transform:scale(.94)}
  /* КОЛЬЦО ДО НАЖАТИЯ — акцент схемы, а не серый волосок (blox52 #13). --line-mid в синих
     схемах давал ~1.7:1 к белой карточке: главный «щёлк» дня выглядел заглушкой. Берём
     --anchor-ink (якорь ЧЕРНИЛАМИ): в светлых схемах это сам якорь, ночью — светлый
     виноград, иначе тёмный якорь на тёмной бумаге сам дал бы 2.9:1 (замер 06.09.2026,
     цель ≥3:1 для нетекстового индикатора: 10.2 · 13.8 · 6.0 · 7.6). Толщина остаётся
     2px — ступень «сигнал» лестницы линий. Наведение (--ok) и анимации пачки 8 не тронуты. */
  .rect .rcheck .circ{width:32px;height:32px;border-radius:50%;border:2px solid var(--anchor-ink);
        background:var(--card);display:flex;align-items:center;justify-content:center;font-size:var(--ic-md);
        font-weight:800;color:transparent;
        transition:background var(--t) var(--ease),border-color var(--t) var(--ease),
                   color var(--t) var(--ease),transform var(--t) var(--ease)}
  @media (hover:hover){.rect .rcheck:hover .circ{border-color:var(--ok)}}
  .rect .rcheck.on{background:var(--ok-fill);border-left-color:var(--ok-line)}
  .rect .rcheck.on .circ{background:var(--ok-ink);border-color:var(--ok-ink);color:var(--on-dark);
        transform:scale(1.06)}
  /* галочка — ШТРИХ, а не буква: путь, который можно прочертить (см. TICK в student.js) */
  .rect .rcheck .circ .tick{width:19px;height:19px;display:block;fill:none;stroke:currentColor;
        stroke-width:3;stroke-linecap:round;stroke-linejoin:round}

  /* ═══════════════════════════════════════════════════════════════════════
     ОТКЛИК РЕБЁНКА НА СВОЁ ДЕЙСТВИЕ (blox52 #9, восьмая пачка судьи).
     Всё, что ниже, живёт на ОДНОМ факте: тик «сделал» перерисовывает лист целиком
     (toggleDone → renderStudent → view.innerHTML). Значит анимацию нельзя вешать на
     нажатую кнопку — её через миг не станет; карточка РОЖДАЕТСЯ с классом (.just-done /
     .just), и браузер играет анимацию на вставке нового узла.
     Имена анимаций начинаются с hw- сознательно: по этому признаку сторож npm run check
     находит их все и требует, чтобы каждая гасилась под «уменьшенным движением» — иначе
     новая анимация здесь появилась бы мимо чужой воли и никто бы не заметил.
     Ни одна из них НЕ меняет раскладку: только прозрачность, transform, цвет и штрих. */
  /* кружок СХЛОПЫВАЕТСЯ в отметку: начинает шире кольца, проваливается ниже цели и
     возвращается с перелётом (--ease-spring) — движение читается физическим, а не «включили
     картинку». Конец кадра равен статике .rcheck.on (scale 1.06), поэтому fill не нужен:
     анимация кончилась — состояние уже на месте, стыка не видно. */
  .rect .rcheck.just .circ{animation:hw-circ-pop .34s var(--ease-spring)}
  @keyframes hw-circ-pop{0%{transform:scale(1.3)}55%{transform:scale(.82)}100%{transform:scale(1.06)}}
  /* галочка ПРОРИСОВЫВАЕТСЯ: pathLength="1" делает длину пути единицей, поэтому dash-набор
     пишется числами, не зависящими от размера фигуры. Штрих живёт ТОЛЬКО внутри кадров —
     вне анимации у пути нет ни dasharray, ни offset, и галочка сплошная сама собой (в том
     числе когда анимация выключена по просьбе человека).
     КРИВАЯ — linear, а не общая --ease, по той же причине, по которой линейно затухает волна
     (см. ripple-fade): --ease описывает, как вещь ПРИЕЗЖАЕТ на место, и стартует резко —
     на замере она прочерчивала 80% галочки за первые 30 мс, то есть штриха никто бы не
     увидел. Прочерчивание и заливка — не приезд, у них своя ровная скорость. */
  .rect .rcheck.just .circ .tick{animation:hw-tick-draw .3s linear .05s both}
  @keyframes hw-tick-draw{from{stroke-dasharray:1;stroke-dashoffset:1}
                          to{stroke-dasharray:1;stroke-dashoffset:0}}
  /* ВОЛНА УСПЕХА: на полсекунды карточка становится заметнее — гуще заливка, рамка в полный
     --ok — и оседает в спокойный зелёный статики (.rect.done). Громко МГНОВЕНИЕ, а не всегда:
     галочка ставится десятки раз в неделю, и постоянный праздник обесценился бы за день.
     background-COLOR, а не background: короткая запись снесла бы фон-картинку, если она
     появится у карточки. */
  .rect.just-done{animation:hw-done-wash .5s linear}
  @keyframes hw-done-wash{
    0%{background-color:var(--card);border-color:var(--line-soft)}
    14%{background-color:var(--ok-fill);border-color:var(--ok)}
    45%{background-color:var(--ok-fill);border-color:var(--ok)}
    100%{background-color:var(--ok-tint);border-color:var(--ok-line)}}
  /* «ИДУ» — между нажатием и перерисовкой. На медленной связи там была пустота, и ребёнок
     жал второй раз. Тонкая полоса по ВЕРХНЕЙ кромке карточки: 2px, внутри overflow:hidden
     самой плашки, ничего не сдвигает и никуда не лезет. Снимается перерисовкой (нового узла
     класса нет) или руками, если запрос отказал — см. toggleDone. */
  .rect.busy::after{content:'';position:absolute;left:0;top:0;height:2px;width:38%;
    background:var(--ok-ink);border-radius:0 var(--r-2xs) var(--r-2xs) 0;
    animation:hw-busy-sweep 1.05s linear infinite}
  @keyframes hw-busy-sweep{from{transform:translateX(-100%)}to{transform:translateX(263%)}}
  /* РАСКЛАДКА ПРИ ПЕРВОМ ПОКАЗЕ: карточки въезжают по очереди. Класс .intro ставит JS ровно
     на первой отрисовке листа (см. hwIntroShown в student.js) — на обновлениях его нет, и
     список не шевелится вовсе. Только прозрачность и сдвиг 6px: раскладка не меняется, значит
     «въезд» физически не может ничего подвинуть (правило «экран не прыгает»). */
  .hwlist.intro .rect{--i:0;   /* ступень задержки; настоящий номер ставит JS инлайном */
    animation:hw-card-in .32s var(--ease) both;
    animation-delay:calc(var(--i) * 45ms)}
  @keyframes hw-card-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
  /* ЧУЖАЯ ВОЛЯ. Попросил человек систему не анимировать — движения не остаётся: отметка
     появляется готовой, лист показывается сразу, волна успеха не идёт. Полоса «иду» —
     единственная, что ОСТАЁТСЯ: это не украшение, а признак «запрос летит»; она просто
     перестаёт ездить и стоит во всю ширину. Тот же размен, что у волны от касания. */
  @media (prefers-reduced-motion:reduce){
    .rect .rcheck.just .circ,
    .rect .rcheck.just .circ .tick,
    .rect.just-done,
    .hwlist.intro .rect{animation:none}
    .rect.busy::after{animation:none;width:100%}
  }

  /* teacher grid */
  table{border-collapse:separate;border-spacing:8px;width:100%}
  th{font-size:var(--fs-sm);color:var(--muted);font-weight:700;text-align:center;padding:4px}
  th.name,td.name{text-align:left;font-weight:700;white-space:nowrap}
  .cell{
    width:64px;height:48px;border-radius:var(--r-md);color:#fff;margin:0 auto;
    display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--fs-sm);
  }
  .empty{color:var(--muted);padding:30px 0;text-align:center}

  /* ── ПУСТОЕ СОСТОЯНИЕ (blox51 #12) ──────────────────────────────────────────────
     Класс .empty носил ТРИ РАЗНЫЕ роли одной серой строкой, и это была не мелочь, а
     потеря смысла: «Загрузка…» (сейчас появится содержимое), «Групп пока нет» (содержимого
     нет и вот что с этим делать) и «Не удалось загрузить» (что-то сломалось). Человек
     видел одну и ту же извиняющуюся строчку и не мог отличить ожидание от тупика.
     Роли разведены:
       .empty  — ТОЛЬКО ожидание. Тихая, мелкая, живёт доли секунды и обязана оставаться
                 незаметной: сделать её кадром значило бы получить вспышку на пол-экрана
                 при каждой первой загрузке;
       .estate — НЕЧЕГО ПОКАЗАТЬ. Это не отсутствие экрана, а сам экран: знак, заголовок,
                 одна строка объяснения и — главное — ДЕЙСТВИЕ, которое отсюда выводит.
                 Раньше кнопка «＋ Новая группа» стояла ПОД строкой отдельным блоком, слева,
                 и читалась как не связанная с ней; теперь она часть композиции;
       .efail  — ОТКАЗ. Тот же кадр, но знаком тревоги и её чернилами: «не получилось» не
                 должно выглядеть как «пусто», иначе человек не поймёт, что стоит повторить.
     Кадр — ЛИСТ БУМАГИ (--card + тиснение), а не пунктирная коробка: пунктир в этом
     приложении уже занят и означает «черновик / предстоящий урок» (.opane.empty). */
  .estate{display:flex;flex-direction:column;align-items:center;text-align:center;
    gap:var(--sp-3);padding:var(--sp-8) var(--sp-5);margin:var(--sp-6) auto;max-width:520px;
    background:var(--card);border-radius:var(--r-xl);box-shadow:var(--sheet-1)}
  /* знак в лунке — тот же язык, что у плиток кабинета (.hub-ic), но крупнее и тише:
     это не кнопка, на него не нажимают, он только называет место */
  .estate-ic{display:flex;align-items:center;justify-content:center;width:64px;height:64px;
    border-radius:var(--r-xl);background:var(--surface);color:var(--faint);
    font-size:var(--fs-2xl);line-height:0}
  .estate-t{font-size:var(--fs-xl);font-weight:800;line-height:var(--lh-tight);
    letter-spacing:var(--tracking-tight);color:var(--ink);text-wrap:balance;margin:0}
  /* строка объяснения ограничена по МЕРЕ СТРОКИ, а не по ширине кадра: 42ch — это про
     чтение, а не про раскладку, поэтому на широком экране она не растягивается в ленту */
  .estate-n{font-size:var(--fs-md);color:var(--muted);line-height:var(--lh);max-width:42ch;margin:0}
  /* действие внутри кадра: .toolbar ставит свои отступы и выравнивание по левому краю —
     здесь оно центральное и без верхнего поля, иначе кнопка отваливается от композиции */
  .estate-a{display:flex;flex-wrap:wrap;gap:var(--sp-2);justify-content:center;margin:var(--sp-1) 0 0}
  .efail .estate-ic{background:var(--alert-tint);color:var(--alert)}
  .efail .estate-t{color:var(--alert-ink)}

  /* pool playlist panel */
  /* «Назад» внутри конструктора/расписания = вариант B (контур) на СВЕТЛОМ фоне */
  .back{display:inline-flex;align-items:center;gap:5px;box-sizing:border-box;
        background:none;border:1px solid var(--line);border-radius:var(--r-md);color:var(--muted-strong);
        font-weight:700;font-size:var(--fs-sm);cursor:pointer;padding:7px 13px;margin-bottom:10px;
        transition:border-color var(--t) var(--ease),background var(--t) var(--ease),color var(--t) var(--ease)}
  @media (hover:hover){.back:hover{border-color:var(--accent-deep);background:var(--accent-tint);color:var(--accent-ink)}}
  .now{background:var(--card);border-radius:var(--r-md);padding:16px;
       box-shadow:var(--sheet-2);margin-bottom:16px}
  .now .nt{font-weight:800;font-size:var(--fs-lg);margin-bottom:8px}
  /* blox41 — почему заиграл не тот файл. Спокойный тон: для ребёнка это не его ошибка, поэтому
     не alert-красный, а обычная подпись; строка держится до следующего осознанного тапа. */
  .now .pskip{font-size:var(--fs-sm);color:var(--muted);margin:-4px 0 10px}
  .now .pskip[hidden]{display:none}
  /* blox52 #17: аудио пула играет НАШ плеер (.lp) — лист в лист не кладём: внутри карточки .now
     у него нет своей подложки/тени/полей, он часть карточки (правило `.now audio` ушло вместе с
     системным <audio controls>; видео ставит ширину само, инлайном, как и раньше) */
  .now .lp{box-shadow:none;background:none;padding:0;max-width:none}
  .track{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--r-md);
         background:var(--card);box-shadow:var(--sheet-1);margin-bottom:8px;cursor:pointer}
  .track.playing{outline:2px solid var(--accent-deep)}
  /* битый трек остаётся в списке НАЗЛО соблазну его спрятать: спрятанный файл — это молча отнятый
     у группы файл, а вердикт по кодеку бывает неверным. Виден, помечен, но не преграждает дорогу. */
  .track.broken .tt{color:var(--muted)}
  .track.broken .meta{color:var(--alert)}
  .track.broken .dot{background:var(--muted)!important;border:none!important}
  .dot{width:14px;height:14px;border-radius:50%;flex:0 0 auto;box-sizing:border-box}
  /* лестница точки трека (blox52 #11): не слушал → ПУСТОЕ КОЛЬЦО (место под будущую отметку,
     а не красный крик), прослушано (90% хоть раз) → зелёный диск «сделано». Промежуточной
     ступени нет: частичный прогресс в данных не хранится (см. paint() в student.js). */
  .dot-zero{background:transparent;border:2px solid var(--line-mid)}
  .dot-done{background:var(--ok)}
  .track .tt{font-weight:700;flex:1}
  .track .meta{font-size:var(--fs-xs);color:var(--muted)}
  /* ↓ на каждом треке: 44×44 палец-цель (G5), не запускает плеер (stopPropagation) */
  .track .trackdl{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
                  width:44px;height:44px;border-radius:var(--r-md);color:var(--muted);text-decoration:none}
  @media (hover:hover){.track .trackdl:hover{color:var(--primary);background:var(--bg)}}
  .track .trackdl .ic{width:20px;height:20px}
  /* заголовок пула + кнопка «Скачать песни» (весь аудио-плейлист одним zip) */
  .poolhead{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
  .poolhead h2{margin:0}
  .pool-dlall{flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;
              font-weight:700;font-size:var(--fs-sm);color:var(--primary);--btn-ink:var(--primary);text-decoration:none;
              background:var(--card);border:1px solid var(--line);border-radius:var(--r-pill);
              padding:9px 15px;cursor:pointer;box-shadow:var(--sh-1);min-height:44px}
  @media (hover:hover){.pool-dlall:hover{border-color:var(--primary);box-shadow:var(--sh-2)}}
  .pool-dlall .ic{width:16px;height:16px}

  /* teacher tabs */
  /* segmented control, iOS-style: a soft track with a raised selected pill */
  .tabs{display:flex;gap:4px;margin-bottom:18px;background:var(--fill-soft);
    border-radius:var(--r-md);padding:4px;width:fit-content}
  .tabs button{
    border:none;border-radius:var(--r-sm);padding:9px 18px;font-size:var(--fs-sm);font-weight:700;
    /* --muted на дорожке --fill-soft даёт 4.34:1 — под AA для 14px (axe, 02.09.2026).
       --muted-strong заведён ровно для этого случая: «AA on --fill-soft» (theme.css). */
    background:transparent;color:var(--muted-strong);cursor:pointer;
    transition:background var(--t) var(--ease),color var(--t) var(--ease);
  }
  @media (hover:hover){.tabs button:hover{color:var(--ink)}}
  .tabs button.on{background:var(--card);color:var(--ink);box-shadow:var(--sh-1)}

  /* add-file panel */
  /* blox51 #3 (заход 8): БЕЗ РАМКИ. Край плоскости определяется ОДИН раз — либо контрастом к
     тому, что под ней, либо линией. Лист лежит на СТОЛЕ (заход 1), а это 1.32 по контрасту:
     край и так виден, и рамка поверх него — второе объявление того же факта. */
  .panel{background:var(--card);border-radius:var(--r-xl);
         padding:20px;box-shadow:var(--sheet-2);margin-bottom:18px}
  .panel h3{margin:0 0 14px;font-size:var(--fs-lg);font-weight:800;color:var(--ink-strong)}
  .field{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
  .field label{font-size:var(--fs-sm);color:var(--muted);font-weight:700}
  /* 16px, а не --fs-md (15px): ниже шестнадцати Safari на айфоне зумит страницу при фокусе и
     обратно не отматывает (шрам blox11 #3, lingo-ui §3). Правило было закрыто на экранах, но
     МОДАЛКИ его пережили — ночной обходчик впервые дошёл до окна нового таска только в
     blox50 #7 (навигация до него не доводила) и сразу нашёл 15px в «Текст». */
  .field input[type=text],.field input[type=url]{
    border:1px solid var(--line);border-radius:var(--r-sm);padding:11px 13px;font-size:var(--fs-input);
    width:100%;background:var(--card);color:var(--ink);transition:border-color var(--t) var(--ease),box-shadow var(--t) var(--ease);
  }
  .field input:focus,.field select:focus,.field textarea:focus{
    outline:none;border-color:var(--accent-deep);box-shadow:var(--ring)}
  .field.row{flex-direction:row;align-items:center;gap:8px}
  .field.row label{font-size:var(--fs-md);color:var(--ink)}
  .src-tabs{display:flex;gap:8px;margin-bottom:14px}
  .src-tabs button{flex:1;border:1px solid var(--line);background:var(--card);border-radius:var(--r-sm);
    padding:9px;font-weight:700;font-size:var(--fs-sm);color:var(--muted);cursor:pointer}
  .src-tabs button.on{border-color:var(--accent-deep);color:var(--accent-dark);background:var(--accent-tint)}
  /* PRIMARY = grape fill + white text (10.6:1). The sky is too light to carry white
     text, so it works as accent/selection instead — see theme.css. */
  /* .btn-primary → folded onto the engine's --primary role (see BUTTON ENGINE). */
  .hint{font-size:var(--fs-xs);color:var(--muted);margin-top:-4px}
  .hint.ok{color:var(--green);font-weight:700}    /* «Выбрано: файл» в листе сдачи */
  .msg{font-size:var(--fs-md);font-weight:600;margin-top:10px}
  .msg.ok{color:var(--green)} .msg.err{color:var(--red)}
  /* blox52 #18 — слот ошибки ЗАПОЛНЕНИЯ формы (core.js formError). Держит высоту строки
     ПУСТЫМ: текст появляется в уже занятом месте, и кнопка под ним не уезжает из-под пальца
     (то же «нет прыжка», что у .upbar выше). Мельче .msg — это подпись к полю, не итог формы.
     Резерв считается ОТ СОБСТВЕННОГО кегля (1.25em при line-height:1.25 = ровно один строчный
     бокс), а не круглым числом пикселей: круглое число не совпадает с настоящей высотой строки,
     и панель всё равно подрастает — пробник tools/probes/blox52-fslot-jump.js это и намерил. */
  .fslot{min-height:1.25em;line-height:1.25;margin-top:4px;font-size:var(--fs-sm)}
  /* blox51 #1 — полоса отдачи файла в шторке сдачи (живёт внутри .msg, той же высоты, что строка
     текста, — контейнер не прыгает при смене «полоса → текст ошибки») */
  .upbar{height:8px;border-radius:var(--r-2xs);background:var(--line);overflow:hidden;margin:5px 0}
  .upbar i{display:block;height:100%;border-radius:var(--r-2xs);background:var(--anchor);transition:width .2s}

  /* file library list */
  .file{display:flex;align-items:center;gap:12px;padding:12px;border-radius:var(--r-md);
        background:var(--card);box-shadow:var(--sheet-1);margin-bottom:8px}
  .file .ico{font-size:var(--ic-lg)}
  .file .info{flex:1;min-width:0}
  .file .info .ft{font-weight:700}
  .file .info .fm{font-size:var(--fs-xs);color:var(--muted)}
  .pill{font-size:var(--fs-2xs);font-weight:700;padding:3px 8px;border-radius:var(--r-lg);
        background:var(--accent-tint);color:var(--accent-dark)}
  .file .act{background:none;border:none;cursor:pointer;font-size:var(--fs-sm);font-weight:700;
        color:var(--accent-dark);padding:6px}
  .file .act.del{color:var(--red)}

  /* teacher home = the group screen. The old list-of-groups monitor is gone: every group of the
     teacher lives in ONE horizontal strip at the top, where the group's title used to be. A chip
     carries ONE indicator when the group needs something: the to-check counter, else a dot. */
  /* position:relative — якорь живого подчёркивания .gind (blox52 #12): индикатор лежит ВНУТРИ
     прокручиваемого ряда, поэтому горизонтальный скролл везёт его вместе с чипами без пересчёта.
     .gs-gswitch — близнец: тот же ряд .gchip на «Уроках» расписания (group-schedule.js), ему
     нужен только якорь — раскладка у него своя и тут не трогается. */
  .gstrip{position:relative;display:flex;gap:8px;overflow-x:auto;padding:2px 2px 8px;margin-bottom:2px;
    -webkit-overflow-scrolling:touch;scrollbar-width:none}
  .gs-gswitch{position:relative}
  .gstrip::-webkit-scrollbar{display:none}
  .gchip{position:relative;flex:0 0 auto;display:flex;align-items:center;gap:7px;cursor:pointer;
    padding:9px 14px;border-radius:var(--r-pill);border:1px solid var(--line);background:var(--card);
    font-weight:800;font-size:var(--fs-md);color:var(--muted);white-space:nowrap;box-shadow:var(--sh-1)}
  /* НАВЕДЕНИЕ (blox52 #3, п.6). У чипа группы были заведены только нажатие и «выбран», а состояния
     наведения не было ВООБЩЕ — при том что у соседних контролов (призрачная кнопка, кебаб,
     плитка хаба) оно есть. Тот же ключ, что у них: тёмный акцент линией (--accent-deep — акцент
     ЛИНИЕЙ, а не заливкой, см. theme.css), акцентная подложка и тень на ступень выше.
     Объявлено ДО .gchip.on намеренно: у выбранного чипа рамка обязана остаться акцентными
     чернилами, поэтому его правило идёт следом и побеждает при равной специфичности. */
  @media (hover:hover){.gchip:hover{border-color:var(--accent-deep);background:var(--accent-tint);
    box-shadow:var(--sh-2)}}
  .gchip:active{transform:scale(.97)}
  .gchip .gdot{width:8px;height:8px;border-radius:50%;background:var(--checked);flex:0 0 auto}
  /* blox51 #3 (заход 3), П-1: тревога группы больше НЕ красит чип целиком. Флаг alert
     включается, когда у ближайшего занятия нет ДЗ, ИЛИ лист в черновике, ИЛИ есть
     непроверенные сдачи (routes/teacher.js) — в живой школе это верно почти всегда, и
     лента краснела вся. Сигнал, который горит всегда, — это фон, а не сигнал, и он
     забирал единственный красный, который есть у продукта. Индикатор ОДИН (blox52 #11):
     счётчик непроверенного, а без него — точка; вместе они не рисуются (teacher.js).
     Точка тревоги — АКЦЕНТ СХЕМЫ, не красный: всё, о чём она говорит («выдай ДЗ»,
     «черновик не выдан»), — очередь работы учителя, а не провал ученика; настоящей
     просрочки в её семантике нет (см. r.alert в routes/teacher.js). */
  .gchip.alert .gdot{background:var(--anchor)}
  /* blox52 #12 — у активного чипа была ДВОЙНАЯ пометка: рамка 1.5px И полоска. Рамку сняли,
     осталась одна система: живая полоска .gind + лёгкая заливка чипа (замер контраста заливки
     не нужен — текст на ней те же чернила --ink). Полоска цвета --accent-ink — выбор blox52 #7:
     в синих схемах сам акцент (светлый Maya) давал на столе 1.53:1, чернила той же Maya — 4.55. */
  .gchip.on{color:var(--ink);background:var(--accent-tint)}
  /* индикатор выбранной группы (blox52 #12): БЫЛ ::after на каждом чипе — у псевдоэлемента нет
     пути ЕХАТЬ от чипа к чипу, подчёркивание перепрыгивало. Теперь это ОДИН общий узел .gind в
     ряду чипов (позицию ставит paintGind, core.js): transform/width едут ~200мс, и выбор ЕДЕТ
     к новому чипу. top/width — инлайном из JS (геометрия чипа), здесь только вид и движение. */
  .gind{position:absolute;left:0;top:0;height:3px;width:0;border-radius:var(--r-2xs);
    background:var(--accent-ink);pointer-events:none;
    transition:transform .2s var(--ease),width .2s var(--ease)}
  /* «выбрано» и «тревожно» — разные факты, и подчёркивание выбора говорило о втором.
     Выделение снова окрашено только акцентом, каким бы ни был флаг тревоги. */
  /* счётчик «к проверке» — очередь, не пожар (blox52 #11): акцент схемы, как у плашки
     «N to check» в ростере. Ховер — лёгкое высветление той же заливки, не смена цвета. */
  .gchip .gn{font-weight:800;font-size:var(--fs-2xs);color:var(--on-dark);background:var(--anchor);
    border-radius:var(--r-xs);padding:1px 6px;cursor:pointer}
  @media (hover:hover){.gchip .gn:hover{filter:brightness(1.15)}}
  /* СТРОКА ИНСТРУМЕНТОВ ГРУППЫ (blox31 #1 · переработана blox53 #1–#5): гамбургер · Post · Chat.
     Ровно три кнопки, ОДИН ряд на любой ширине. Ушли имя группы (оно написано на выбранном чипе
     ленты строкой ниже), «Notes» (досье открывает тумблер на левом листе) и «Playlist» (туда
     ведёт плашка плейлиста на панели) — на телефоне строка была двухэтажной именно из-за них. */
  .ghead-tools{display:flex;flex-wrap:nowrap;align-items:center;gap:8px;margin:4px 0 12px}
  .ghead-tools .btn-ghost{display:flex;align-items:center;justify-content:center;gap:6px;
    flex:0 1 auto;min-width:0;white-space:nowrap}
  .ghead-tools .gt-post{margin-left:auto}   /* гамбургер слева, Post+Chat правой кучкой */
  /* кнопка-гамбургер — прячет редкие действия (Ученики / Расписание / + Группа), blox31 #1.
     Меню всплывает под ней; [hidden] гасит его через display:none (правило 7 skill). */
  .gkebab-wrap{position:relative;display:inline-flex;align-items:center}
  .gkebab{width:38px;height:38px;border-radius:var(--r-sm);border:1px solid var(--line);background:var(--card);
    color:var(--muted);font-size:var(--ic-md);line-height:1;cursor:pointer;
    display:flex;align-items:center;justify-content:center}
  @media (hover:hover){.gkebab:hover{border-color:var(--accent-deep);background:var(--accent-tint);color:var(--accent-ink)}}
  /* меню якорится по ЛЕВОМУ краю кнопки на ЛЮБОЙ ширине. Центрированное (left:50% + сдвиг на
     половину) уезжало за край экрана, потому что гамбургер стоит первым в строке: меню шириной
     190px наполовину вылезало влево, и «Ученики/Расписание/Группа» читались как «ченики/асписание/
     уппа». На телефоне это чинили ещё в blox31 #1 отдельным правилом, а на ПК гамбургер тогда
     стоял НЕ с краю (перед ним были имя группы и другие кнопки) — левый край его прикрывал. В
     blox53 #1–#5 строку сократили до трёх кнопок, гамбургер выехал в начало и на ПК — и центровка
     сломалась там же. Поэтому правило одно на все ширины, отдельного телефонного больше нет. */
  .gkebab-menu{position:absolute;top:calc(100% + 6px);left:0;z-index:45;
    display:flex;flex-direction:column;gap:4px;min-width:190px;padding:8px;
    background:var(--card);border-radius:var(--r-lg);box-shadow:var(--sheet-pop)}
  .gkebab-menu[hidden]{display:none}
  .gkebab-menu .btn-ghost,.gkebab-menu .btn{width:100%;justify-content:flex-start;gap:8px}
  /* телефон (blox53 #4) — те же три кнопки, просто теснее: Post и Chat делят остаток строки
     поровну, гамбургер держит свои 38px. */
  @media(max-width:760px){
    .ghead-tools .gt-post,.ghead-tools .gt-chats{
      flex:1 1 0;font-size:var(--fs-sm);gap:4px;padding-left:4px;padding-right:4px}
  }

  /* pupil's "where I study" line: отделение · группа · учитель (blox10 #2) */
  /* строка «где я учусь» не должна липнуть к заголовку — своя воздушная подушка (blox13 #3) */
  .swhere{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:8px 0 16px;
    font-size:var(--fs-sm);color:var(--muted);font-weight:600}
  .swhere-grp{font-weight:800;color:var(--ink)}
  .swhere-sep{color:var(--faint)}

  /* pupil's lesson carousel — flip among days that have homework */
  .lcar{display:flex;align-items:center;justify-content:space-between;gap:10px;
    background:var(--card);border-radius:var(--r-lg);padding:8px 10px;
    margin:0 0 16px;box-shadow:var(--sheet-1)}
  .lcar-arr{width:44px;height:44px;flex:none;border:1px solid var(--line);background:var(--card);
    border-radius:var(--r-md);font-size:var(--ic-md);color:var(--ink);cursor:pointer;
    transition:background var(--t) var(--ease),border-color var(--t) var(--ease),transform var(--t-fast) var(--ease)}
  @media (hover:hover){.lcar-arr:hover:not(:disabled){background:var(--accent-tint);border-color:var(--accent-deep)}}
  .lcar-arr:active:not(:disabled){transform:scale(.93)}
  /* ВЫКЛЮЧЕНО ГОВОРЯТ РОЛИ ЦВЕТА, А НЕ ПРОЗРАЧНОСТЬ (blox52 #4, п.7). Кнопочный движок
     перевели на это в blox52 #3, а вне его «выключено» оставалось написанным шестью
     разными числами (.28 · .3 · .45 · .55 · .9 и отдельно обесцвечивание) — одна роль,
     шесть значений. Прозрачность вдобавок гасит предмет ЦЕЛИКОМ вместе с подписью и
     протаскивает сквозь него то, на чём он лежит, поэтому вид зависел от подложки.
     Рецепт один и тот же везде: заливка гаснет в нейтральную --fill-soft, текст — в
     --faint, рамка — в волосок. */
  .lcar-arr:disabled{background:var(--fill-soft);border-color:var(--line-soft);
    color:var(--faint);cursor:default}
  .lcar-mid{text-align:center;line-height:1.25}
  .lcar-date{font-weight:800;font-size:var(--fs-lg);color:var(--ink);letter-spacing:var(--tracking-tight)}
  .lcar-pos{font-size:var(--fs-xs);color:var(--faint);margin-top:2px}

  /* (blox52 #11) Блок .lh/.elist/.ecard старого монитора-списка удалён: ни один модуль эти
     классы больше не рисует (экран заменён .oent-плашками), а .ecard .enotdone был ещё одним
     красным текстом-сиротой. */
  /* тема = РАМКА, оборачивающая таски; скруглена с ЗАКРЫТОЙ стороны, ОТКРЫТА с другой.
     blox26 #5 — ЕДИНЫЙ визуал на ВСЕХ экранах: одна толщина (1.5px), цвет — токен схемы
     var(--frame-line) (перекрашивается вместе с палитрой), фон ПРОЗРАЧНЫЙ. Три класса
     остаются лишь потому, что различается ВНУТРЕННЯЯ раскладка/плотность:
       .tframe — Домашка ученика + проверка учителем
       .oframe — монитор учителя (плотнее)
       .theme  — постановка/редактирование ДЗ
     поэтому padding/margin и заголовки у каждого свои, а рамка — общая. */
  .tframe,.oframe,.theme{position:relative;background:transparent;--fpad:14px;--frail:6px}
  /* сама скобка — ПСЕВДОЭЛЕМЕНТ, а не border самого блока. Это позволяет «усам» (верхней
     и нижней линиям) КОНЧАТЬСЯ на открытом крае карточек, а не торчать до края блока
     (blox26 #5). --fpad = паддинг рамки с ОТКРЫТОЙ стороны = насколько подтянуть усы.
     Карточки при этом НЕ двигаются — выравнивание с тасками «вне темы» сохраняется.
     ОРИЕНТАЦИЯ (blox50 #5): закрытый скруглённый край — СПРАВА, открытая грань — СЛЕВА.
     Это единственный вид рамки; тумблер «Обычная/Зеркальная» (blox26 #5) убран, выбранный
     вид закреплён для всех. Открытая сторона совпадает со стороной цветного рельса таска
     (он всегда слева), поэтому усы подтянуты внутрь ещё на ШИРИНУ рельса (--frail): ус
     кончается там, где рельс ЗАКАНЧИВАЕТСЯ, и цветная окантовка целиком торчит наружу.
     (У монитора рельс тоньше — --frail:3px.) */
  .tframe::before,.oframe::before,.theme::before{content:"";position:absolute;
    top:0;bottom:0;left:calc(var(--fpad) + var(--frail));right:0;
    border:1.5px solid var(--frame-line);border-left:none;
    border-radius:0 var(--r-xl) var(--r-xl) 0;pointer-events:none}
  .tframe{padding:4px 14px 14px;margin:0 0 20px}
  .tframe>.tfh{text-align:center;font-weight:800;font-size:var(--fs-lg);color:var(--ink);
    letter-spacing:var(--tracking-tight);padding:11px 0 13px}
  .tframe>.tfcards{display:flex;flex-direction:column;gap:10px}
  /* таск ВНЕ темы — та же ширина, что и в рамке (сдвиг вправо на границу+паддинг рамки).
     Домашка ученика + экран проверки; в черновике ту же роль играет .standarea. */
  .hwlist>.sloose,.srlist>.sloose{margin-left:15px;margin-right:14px}
  .sortbar{display:flex;align-items:center;gap:10px;margin:18px 0 8px}
  .sortbar .sortlbl{font-weight:800;font-size:var(--fs-sm);color:var(--ink)}
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden}
  .seg button{border:0;background:var(--card);color:var(--muted);font-weight:700;font-size:var(--fs-xs);
    padding:7px 13px;cursor:pointer;transition:background var(--t-fast) var(--ease)}
  /* blox51 #3 (заход 3.2): выбранный сегмент — ЯКОРЬ схемы под белым текстом, как у всех
     остальных сегментных переключателей. Раньше здесь была светлая акцентная заливка с тёмным
     текстом; в базовой схеме акцент ушёл в чёрный, и пара «тёмное на тёмном» стала нечитаемой.
     Якорь работает в обеих сторонах: он тёмный в КАЖДОЙ схеме, и белый на нём проходит AA. */
  .seg button.on{background:var(--anchor);color:var(--on-dark)}
  .rlist{display:flex;flex-wrap:wrap;gap:8px}
  .rchip{display:flex;align-items:center;gap:10px;border:1px solid var(--line-soft);background:var(--card);
    border-radius:var(--r-sm);padding:8px 12px;cursor:pointer;font-weight:700;color:var(--ink);
    transition:border-color var(--t) var(--ease),background var(--t) var(--ease),
               box-shadow var(--t) var(--ease)}
  @media (hover:hover){.rchip:hover{border-color:var(--accent-deep)}}
  /* ПОДСВЕТКА ОТ ПЛАШКИ-ФИЛЬТРА (blox52 #12 · перекрашена и замедлена в blox55 #6): клик по
     .oent-live метит чипы учеников из её списка «не сделали». Только цвет и кольцо-тень —
     размеры не меняются, прокрутка на месте (кольцо box-shadow вне потока).
     ЦВЕТ — АЛЕРТНЫЙ, не якорь схемы: вспышка читается как «вот кого не хватает» и должна
     находиться глазом мгновенно; прежний якорь был почти неотличим от обычного hover чипа.
     ВРЕМЯ — 1.8с вместо 600мс (втрое), и обе половины вспышки ПЛАВНЫЕ: проявление (fade-in,
     0→22% ≈ 0.4с, ease-out — быстро набирает и мягко упирается), плато до 40%, затем долгое
     угасание (fade-out) до нуля. Ступеньки нет ни на входе, ни на выходе. Класс .hl снимается
     в teacher.js по тому же таймеру — уже после того, как цвет сошёл на нет. */
  /* края кадров НЕ перечисляют цвета нарочно: пустой 0%/100% браузер заполняет собственными
     значениями чипа, так что вспышка входит из его нормального вида и в него же возвращается —
     и рецепт листа (белая карточка обязана носить тень из семейства --sheet) не нарушается. */
  @keyframes rchip-hl{
    0%{box-shadow:0 0 0 0 transparent;animation-timing-function:cubic-bezier(.2,.7,.3,1)}
    22%,40%{border-color:var(--alert);background:var(--alert-tint);box-shadow:0 0 0 1.5px var(--alert)}
    100%{box-shadow:0 0 0 0 transparent}
  }
  .rchip.hl{animation:rchip-hl 1800ms var(--ease) both}
  /* без движения — та же пометка, просто без плавности: она обязана остаться видимой */
  @media (prefers-reduced-motion:reduce){
    .rchip.hl{animation:none;border-color:var(--alert);background:var(--alert-tint);
      box-shadow:0 0 0 1.5px var(--alert)}
  }
  /* review badge — единый английский словарь (blox52 #11): «N to check» (акцент схемы) /
     «N of N · Done!» (зел.) / «✓ N of N» (зел. тинт) / Nope (серый). nowrap — плашка в колонке
     148px (.oroster) не имеет права рваться на две строки: перенос вешал точку-разделитель
     «·» в конец строки (дефект Б1 второго прогона судей). */
  .rchip .rrev{font-size:var(--fs-xs);font-weight:800;padding:2px 8px;border-radius:var(--r-xs);
    white-space:nowrap}
  /* «сдано, ждёт проверки» — ОЧЕРЕДЬ РАБОТЫ, а не провал (blox52 #11): ребёнок сделал всё
     правильно, действовать надо учителю. Очередь говорит акцентом СВОЕЙ схемы — якорь под
     белым текстом (баклажан 10.8:1 · графит 13.8 · Maya 6.0 · виноград 5.3, замер 06.09.2026);
     красная заливка остаётся настоящим тревогам (реестр RED_OK в tools/check.js). */
  .rchip .rrev.pend{color:var(--on-dark);background:var(--anchor)}
  .rchip .rrev.done{color:var(--ok-ink);background:var(--ok-fill)}
  .rchip .rrev.checked{color:var(--ok);background:var(--ok-tint)}
  .rchip .rrev.nope{color:var(--muted-strong);background:var(--fill-soft)}   /* «ничего не сдал» за прошедший урок — факт, не тревога: серым, не цветом alert (blox22 #1). grape-600, а не --muted: на grape-100 у --muted было 4.34:1 */
  /* GROUP OVERVIEW — two-pane screen (docs/teacher-overview-screen.md) */
  .ostrip{margin:0 0 4px}
  /* ЗАНАВЕС ПАНЕЛЕЙ (blox52 #12). Тап по дню каретки: обе панели глушатся прозрачностью и
     перестают ловить нажатия, СТАРОЕ содержимое остаётся на месте (никаких «Загрузка…» —
     правило «экран не прыгает»); render() после ответа снимает класс. Мгновенный ответ из
     кэша занавес НЕ мигает: transition 160мс сам гасит короткую вспышку — класс, снятый
     через пару мс, успевает увести opacity лишь на доли процента. Поэтому transition тут
     НЕ снимается и под reduced-motion: это не движение, а противомигательный механизм
     (тот же довод, что у спиннера .is-loading). pointer-events выключается мгновенно —
     ему и надо: панель в занавесе не должна ловить второй тап ни единого кадра. */
  #gpanes{transition:opacity .16s var(--ease)}
  #gpanes.veil{opacity:.6;pointer-events:none}
  .otwo{display:flex;align-items:stretch;gap:0;margin-top:10px}
  .oarr{flex:0 0 auto;align-self:center;border:1px solid var(--line-soft);background:var(--card);border-radius:var(--r-sm);
    width:38px;height:54px;font-size:var(--ic-md);cursor:pointer;color:var(--ink)}
  .oarr:disabled{background:var(--fill-soft);border-color:var(--line-soft);
    color:var(--faint);cursor:default}
  /* blox51 #2 — панель занятия БОЛЬШЕ НЕ ЗАЛИВАЕТСЯ. Уровня ровно два: стол
     (--bg) и бумага (плашки внутри). Белая заливка делала третий, почти такой
     же светлый слой, и плашка на нём читалась как вмятина, а не как листок.
     Границу панели оставляем: на мониторе «прошло» и «предстоит» стоят рядом
     двумя колонками, и рамка тут несёт смысл — её растворять нельзя. */
  .opane{flex:1 1 0;min-width:0;border:1px solid var(--line);border-radius:var(--r-md);padding:12px;margin:0 6px;background:transparent}
  /* шапка панели отбита той же гравированной линейкой; линия здесь по НИЖНЕЙ грани,
     поэтому блик уходит наружу (--rule-b), а не внутрь */
  /* СЕТКА 1fr · auto · 1fr, а не flex (blox53 #6). Средняя колонка — тумблер Notes/Tasks: раньше
     он стоял абсолютом ПОВЕРХ шапки и своей непрозрачной подложкой закрывал то, на что наезжал.
     Подложки у него больше нет (два слова через слеш), и наложение стало видно: замер по всем
     урокам группы дал 19 наездов на 375px — на длинную дату («12 сентября») и на пару
     «выдано · сдач ещё нет» + Edit. В потоке наложение невозможно в принципе: колонки делят
     ширину, содержимое переносится. Боковые 1fr равны, поэтому тумблер стоит РОВНО по центру
     панели в обоих режимах и не ездит (assert «тумблер на месте ±2px»); align-items:start
     держит его Y на месте, даже если соседняя колонка выросла на строку. */
  .opane-h{display:grid;grid-template-columns:1fr auto 1fr;align-items:start;gap:6px;margin-bottom:8px;
    border-bottom:1px solid var(--line-soft);padding-bottom:6px;box-shadow:var(--rule-b)}
  .opane-d{font-weight:800;min-width:0;align-self:center}
  /* правая часть шапки: кнопка Edit (если есть) + статус «прошло/предстоит» на одной строке.
     Edit стоит ЛЕВЕЕ «предстоит»; отдельной строки .opane-tools больше нет (blox28 #1). */
  .opane-hr{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;
    min-width:0;grid-column:3;align-self:center}
  /* ТЕЛЕФОН: средней колонки на 343px не хватает — дата и «выдано · сдач ещё нет» ужимались
     в 99px и рвались на пять строк. Тут тумблер уходит на СВОЮ строку над шапкой (flex-обёртка:
     order:-1 + basis 100%), и дата с Edit получают всю ширину. Y тумблера от режима не зависит,
     так что «не ездит при переключении» держится и здесь. */
  @media(max-width:760px){
    .opane-h{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}
    .opane-h .onotes{order:-1;flex:1 0 100%;width:auto;max-width:none}
  }
  .opane-hr .oedit{padding:5px 12px;font-size:var(--fs-sm)}
  /* blox52 #14 — подпись состояния выданного листа («выдано · сдач ещё нет»): вторая мелкая
     строка под датой ВНУТРИ шапки. У шапки min-height:44px, а дата+подпись вместе ~34px —
     высота шапки не меняется и плашки под ней не едут. Тон --muted: факт без срочности. */
  .opubnote{display:block;font-size:var(--fs-xs);font-weight:600;color:var(--muted)}
  /* blox52 #14 — полоска-подтверждение успешной выдачи: акцент СВОЕЙ схемы по верхней кромке
     панели, absolute поверх кромки (содержимое не сдвигает), ~1.2с и гаснет. Базовое состояние
     ::before — opacity:0, анимация однократная: по её концу полоска исчезает сама, снимать
     класс не нужно (следующий рендер его и так не поставит — pubFlashLid одноразовый). */
  .opane.pubflash::before{content:"";position:absolute;left:-1px;right:-1px;top:-1px;height:4px;
    background:var(--grad-primary);border-radius:var(--r-md) var(--r-md) 0 0;
    opacity:0;animation:pubflash 1.2s ease-out;pointer-events:none}
  @keyframes pubflash{0%{opacity:0}12%{opacity:1}70%{opacity:1}100%{opacity:0}}
  /* попросили систему не анимировать — движения не остаётся: полоску не показываем вовсе
     (факт выдачи и так виден — панель сменилась на лист, в шапке подпись состояния) */
  @media (prefers-reduced-motion:reduce){ .opane.pubflash::before{animation:none} }
  /* blox51 #6: у выданного листа БОЛЬШЕ НЕТ салатовой рамки — он берёт рядовой волосок --line.
     Зелёный в проекте значит ровно одно: «сделано / проверено / делать нечего». Выданный лист
     этого не значит: рамка говорила «порядок» на панели, внутри которой «Не сделали: Emma,
     Fred, Lena, Rita, Sonya, Steve». Плюс «выдано» — состояние ПО УМОЛЧАНИЮ, у девяти панелей
     из десяти, и красить умолчание нечем: цвет остаётся там, где он чего-то требует —
     сиреневый черновик, alert «ДЗ не выдано», чернильный пунктир «ближайшее». */
  /* blox51 #4: тот же ПОДНОС, что и у .opane.build.draft ниже — это ОДНА панель в двух
     режимах (черновик закрыт / открыт в конструкторе). Пока заливки различались, панель
     меняла цвет от одного клика; теперь состояние держит только рамка. */
  .opane.draft{border-color:var(--draft-line);background:var(--surface)}
  .opane.empty{border-style:dashed}
  .opane.next{border-style:dashed;border-color:var(--ink)}       /* ближайшее предстоящее — чёрный пунктир, как act-right на каретке */
  /* ДЗ ещё не выдано — alert, как nag на каретке. ВТОРОЙ СЕЛЕКТОР — не украшение (blox52 #9):
     у панели с открытым конструктором черновик красит рамку правилом .opane.build.draft, и
     сила у него ровно та же (три класса), а записано оно НИЖЕ — красная рамка ближайшего
     занятия молча становилась сиреневой ровно в тот момент, когда учитель открывал черновик.
     Приоритет разведён явно, но БЕЗ !important: хватает лишнего класса в селекторе. У соседа
     по смыслу — плашки дня в ленте (.nday.nag) — та же пара разведена грубее, через
     !important; тут этого не понадобилось. */
  /* blox52 #11: «надо выдать ДЗ» — ОЧЕРЕДЬ РАБОТЫ, не пожар. Рамка ближайшего без ДЗ носит
     якорь схемы (тот же голос, что у счётчика «к проверке»); красный остаётся настоящим
     тревогам (провал/просрочка — реестр RED_OK в tools/check.js). Пара селекторов и их
     порядок — починка blox52 #9 (черновик не смеет перекрасить рамку), НЕ трогать. */
  .opane.next.nag,
  .opane.next.nag.build{border-color:var(--anchor)}
  /* ПАНЕЛЬ-КРАЙ — пустая линованная страница журнала (blox52 #15). Была коробкой во всю
     высоту соседа с одной строкой по центру — читалось «сломалось», а не «ещё не заполнено»
     (пустой понедельник — самое частое состояние учителя). Теперь: бледные горизонтальные
     линии (чернила 4%, шаг ровно 28px — целые px против муара на дробном dpr), подпись в
     верхнем левом углу тоном --faint, высота своя (~200px), а не растянутая по соседу.
     align-self:flex-start снимает stretch только на широкой раскладке (в телефонном столбике
     он вернул бы себе ширину — там медиа ниже ставит auto). Высота меняется ТОЛЬКО вместе с
     содержимым при перерисовке пары — «экран не прыгает» соблюдён. Класс только у ПУСТОЙ
     панели: у панелей с плашками линий нет и просвечивать им не под чем. */
  .opane.edge{color:var(--faint);background:var(--surface);align-self:flex-start;min-height:200px;
    background-image:repeating-linear-gradient(to bottom, transparent 0 27px, var(--page-rule) 27px 28px)}
  .opane.edge .omut{color:var(--faint)}
  .opane.past{background:var(--surface);border-color:var(--line-soft)}   /* прошло без ДЗ — приглушённая, но с шапкой сверху */
  .opane-mid{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;
    padding:26px 8px;text-align:center}
  .obig{font-size:var(--fs-md);padding:12px 18px}
  .odraft-t{font-weight:800;color:var(--draft-ink)}
  /* blox51 #3 (заход 3) снял пульсацию (три тревоги об одном факте); blox52 #11 забрал у
     этой панели и красный: «ближайшее занятие без ДЗ» — очередь учителя, а не провал, и
     говорит она якорем схемы, как рамка .nag выше. Заливки нет вовсе — рамка + большая
     кнопка «Установить ДЗ» (акцент схемы) и так самое громкое на панели, а красная
     плоскость была одной из «семи заливок пожара» из диагноза судей. */
  .opane.empty.urgent{border-color:var(--anchor)}
  .orow{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
  .ochip{padding:6px 9px}
  .oents{display:flex;flex-direction:column;gap:7px}
  /* blox53 #19 — text-align задан ЯВНО, и это не украшение. Плашка — карточка, а не надпись: её
     заголовок и подпись читаются от левого края, как у любого таска выданного листа. Но панель
     ПУСТОГО дня носит класс `.opane.empty`, а `empty` в проекте занят общим помощником «Загрузка…»
     (`.empty{text-align:center}`, строка выше) — имена совпали, и всё внутри такой панели молча
     центровалось. Пока внутри стоял только .opane-mid (центрированный сам по себе), этого не было
     видно; плашка встала туда — и текст поехал в середину. Свой text-align делает плашку
     независимой от того, в какую коробку её смонтировали. */
  .oent{--rail-x:5px;--rail-y:7px;--rail-w:6px;text-align:left;
    background:var(--task-plaque);border-radius:var(--r-sm);padding:8px 10px 9px 19px;font-size:var(--fs-sm)}
  /* ПЛАШКА-ФИЛЬТР (blox52 #12). Плашка задания выглядела карточкой, но была мертва — при
     живых чипах учеников рядом. Теперь у плашки СО списком «не сделали» (класс ставит
     oEntityCard) есть наведение (та же тень, что у живых карточек) и клик: подсветить чипы
     из своего списка (.rchip.hl выше). Плашка БЕЗ списка (все сделали / плейлист / снят)
     класса не получает — и не притворяется: cursor остаётся обычным, hover-тени нет;
     граница «жива/мертва» честная. */
  .oent-live{cursor:pointer;transition:box-shadow var(--t) var(--ease)}
  @media (hover:hover){.oent-live:hover{box-shadow:var(--sh-2)}}
  .oent.pool{color:var(--muted)}
  /* blox9 #7: у плашки плейлиста рельс радужный (как бар ученика), а не серый.
     blox53 #10 — тот же радужный рельс теперь и у БОЛЬШОЙ плашки playlist-группы: это одна и та же
     плашка в двух режимах группы, и «тиснение» у них обязано быть одно. */
  /* blox53 #16 — рельс радужный у ПЛЕЙЛИСТА, но не у контрольного файла: он вынесен в свою
     плашку (.plctrl) и обязан читаться отдельной сущностью, поэтому берёт обычный рельс. */
  .oent.pool::before,.oent.plbig:not(.plctrl)::before{background:var(--grad-playlist-v)}
  /* blox53 #7/#9 — плашка плейлиста кликабельна (дверь на экран «Плейлист») и должна выглядеть
     кликабельной В ПОКОЕ, а не только под курсором (правило 12 lingo-ui): курсор-рука + свой
     волосок по контуру; наведение поднимает тень, нажатие чуть вдавливает. */
  .oent.pool-open{cursor:pointer;border:1px solid var(--line-soft);
    transition:box-shadow var(--t) var(--ease),border-color var(--t) var(--ease)}
  @media (hover:hover){.oent.pool-open:hover{box-shadow:var(--sh-2);border-color:var(--accent-deep)}}
  .oent.pool-open:active{box-shadow:none}
  .oent.pool.ptop{margin-bottom:9px}   /* плашка стоит НАД листом, отдельно от его карточек */
  /* blox53 #13 — ПУСТОЙ плейлист: та же плашка, но последней строкой панели, под всеми тасками. */
  .oent.pool.pbot{margin-top:9px}
  /* blox53 #14/#17 — плашка держит ОБЕ колонки разом, поэтому края берёт у разных соседей:
     ЛЕВЫЙ — у карточек тасков (карточка вне рамки темы сдвинута .oent.loose на 11px, чтобы
     совпасть с карточками ВНУТРИ рамки; плашка — прямой ребёнок панели, и без этого сдвига её
     левый край торчал левее всего листа), ПРАВЫЙ — у чипов учеников, то есть у самого края
     панели: колонка учеников идёт до внутренней границы, и правый отступ у плашки нулевой. */
  .opane>.oent.pool{margin-left:11px;margin-right:0}
  .oweek{margin-top:4px;font-size:var(--fs-xs);font-weight:700;color:var(--accent-dark)}
  /* blox32 #7 rework — большой таск плейлиста на панели каретки playlist-группы */
  /* blox53 #16 — на панели playlist-группы теперь НЕСКОЛЬКО плашек (плейлист + по одной на каждый
     контрольный файл), и .opane-body из общего правила — флекс-СТРОКА: без этого они встали бы
     в ряд и сплющились. Ставим колонку со своим зазором. */
  .opane.plonly .opane-body{padding:2px 2px 4px;flex-direction:column;align-items:stretch;gap:9px}
  .plctrl-none{padding:2px 4px 4px}
  .oent.plbig{gap:6px}
  .plbig .plb-sec{margin-top:9px;font-size:var(--fs-xs);font-weight:800;color:var(--muted);
    display:flex;align-items:center;gap:6px}
  .plbig .plb-sec svg{width:14px;height:14px;opacity:.75}
  .plbig .plb-sec.plb-ctrl{color:var(--accent-dark)}
  .plbig .plb-list{display:flex;flex-direction:column;gap:4px;margin-top:5px}
  .plbig .plb-row{display:flex;align-items:center;justify-content:space-between;gap:10px;
    padding:5px 10px;background:var(--card);border:1px solid var(--line-soft);border-radius:var(--r-xs)}
  .plbig .plb-name{font-weight:700;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .plbig .plb-n{font-weight:800;color:var(--ink);white-space:nowrap}
  .plbig .plb-row.zero .plb-n{color:var(--muted);font-weight:600}
  .plbig .plb-none{padding:6px 2px}
  /* имя прикреплённого файла — в плашке Листа ДЗ и в карточке разбора ученика */
  .ofile{display:flex;align-items:center;gap:5px;margin-top:4px;font-size:var(--fs-xs);font-weight:600;
    color:var(--muted);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* сам текст задания / ссылка / комментарий — в плашке Листа ДЗ и в карточке разбора ученика.
     Тип таска говорит ЧТО делать, а это — что именно задано (blox14 #2). Перенос по словам:
     длинная ссылка не должна выпихивать плашку за правый край. */
  /* blox28 #16: текст задания от учителя (body → .otext) и комментарий (.ocomment) говорят на ОДНОМ
     визуальном языке — иконка-облачко слева + мягкая нейтральная подложка «это написал учитель».
     Раньше body шёл голым текстом без иконки, из-за чего у таска do текст не читался как «текст от
     учителя». Подложка (var(--fill-soft)) — мягкое выделение, а НЕ синий-подчёркнутый (тот читается
     как ссылка и кликается, а текст задания не кликабелен). Ссылка (.olink) — отдельно, она цветная
     и КЛИКАБЕЛЬНА по-настоящему. */
  .otext,.ocomment{display:flex;align-items:flex-start;gap:6px;margin-top:5px;
    padding:6px 9px;border-radius:var(--r-sm);background:var(--fill-soft);min-width:0;overflow-wrap:anywhere}
  .otext{font-size:var(--fs-sm);line-height:1.45;color:var(--ink);white-space:pre-wrap}
  .ocomment{font-size:var(--fs-xs);line-height:1.4;color:var(--muted)}
  .otext svg,.ocomment svg{flex:0 0 auto;margin-top:2px}
  .olink{display:flex;align-items:flex-start;gap:5px;margin-top:4px;font-size:var(--fs-xs);
    color:var(--muted);min-width:0;overflow-wrap:anywhere}
  .olink a{color:var(--accent-dark);font-weight:600}
  .oent-h{display:flex;align-items:center;gap:6px}
  .oent-t{font-weight:700;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* заголовок карточки выданного таска = САМО ДЗ: оно длиннее типа, поэтому переносится (до 3
     строк) вместо обрезки в одну; тип таска живёт ниже мелкой подписью (.oent-sub). */
  .oent-h.htop{align-items:flex-start}
  .oent-h.htop>.eico,.oent-h.htop>.omut{flex:0 0 auto;margin-top:1px}
  /* счётчик «6/10» в столбец (blox52 #15): фиксированный минимум ширины + выравнивание вправо —
     все «x/10» панели встают в одну вертикаль (цифры табличные глобально, theme.css). */
  .oent-h.htop>.omut{min-width:5.5ch;text-align:right}
  .oent-t.big{font-size:var(--fs-md);font-weight:800;line-height:1.3;white-space:normal;overflow-wrap:anywhere;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  /* таск-ссылка: URL — ПЕРВАЯ строка заголовка и кликается прямо с монитора (blox46 #6) */
  .oent-t.big .oent-url{display:block;color:var(--accent-dark);font-weight:700;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .oent-sub{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:4px}
  .oent-sub:empty{display:none}
  .oent-sub .otype{font-size:var(--fs-xs);font-weight:700;color:var(--muted)}
  .omut{font-size:var(--fs-xs);color:var(--muted)}
  .oarch{font-size:var(--fs-2xs);background:var(--alert-fill);color:var(--alert-ink);padding:1px 6px;border-radius:var(--r-xs)}
  /* Списки имён в плашке таска (blox52 #11): цвет ушёл ИЗ ТЕКСТА В МАРКЕР. Красные строки
     «Не открывали: …» на каждой плашке складывались в «экран пожара» (диагноз обоих судей),
     хотя «ещё не открыл» до урока — факт без срочности, а не провал. Теперь сам текст —
     чернила/серый, а состояние несёт маленький знак перед списком: ● зелёный «есть» /
     ○ пустое кольцо «нет» — тот же язык, что у точек треков в плейлисте ребёнка.
     Маркер строчный (::before), высоту строки не меняет — плашка не растёт. */
  .opos{font-size:var(--fs-xs);color:var(--ink);margin-top:3px}
  .opos::before{content:'●';color:var(--ok);font-size:var(--fs-2xs);margin-right:5px;vertical-align:1px}
  .opos.all{font-weight:700;color:var(--ok-ink)}
  .opos.all::before{content:none}
  .oneg{font-size:var(--fs-xs);color:var(--muted);margin-top:2px}
  .oneg::before{content:'○';color:var(--muted);font-size:var(--fs-2xs);margin-right:5px;vertical-align:1px}
  /* blox30 #2 — «открыл, но не дослушал»: промежуточное состояние между прослушал и не открывал */
  .omid{font-size:var(--fs-xs);color:var(--draft-ink);margin-top:2px}
  .omid svg,.opos svg{width:12px;height:12px;vertical-align:-1px}
  /* разделитель панелей «прошло/предстоит» — ПЕЧАТНАЯ ЛИНЕЙКА (blox52 #15): волосок 1px во всю
     высоту, без ореола и скругления — прежний 2px-рельс глаз читал как ползунок или ручку
     шторки, которой нет. Смысл .now («сейчас — между прошедшим и предстоящим», каретка стоит
     на дефолтной паре) НЕ потерян: линейка красится акцентом схемы (--anchor) вместо прежнего
     градиента accent→ok — тот же язык, что у точки lday-alert и рамки .nag. На телефоне линейки
     нет вовсе (медиа ниже): панели стоят столбиком и различаются шапками, «сейчас» там несёт
     кольцо «сегодня» и заливка .viewing на каретке. Мёртвые .oaxis-lbl/.oaxis-now зачищены —
     подпись из разметки ушла раньше, правила остались сиротами. */
  .oaxis{flex:0 0 auto;display:flex;flex-direction:column;padding:0 5px}
  .oaxis-line{width:1px;flex:1;background:var(--line-mid);min-height:60px}
  .oaxis.now .oaxis-line{background:var(--anchor)}
  /* published pane: Лист ДЗ on the left, roster column on the right */
  .opane-body{display:flex;gap:10px;align-items:flex-start}
  .opane-body>.oents{flex:1 1 0;min-width:0}
  .oroster{flex:0 0 148px;display:flex;flex-direction:column;gap:6px}
  .oroster .rchip{width:100%;justify-content:space-between;padding:6px 9px}
  /* theme frame inside a pane — title at the START of the row (left). Рамка — общая
     (см. .tframe,.oframe,.theme выше); тут только своя плотность. */
  .oframe{padding:8px 10px 10px;margin-bottom:9px;--fpad:10px;--frail:3px}
  .oframe>.ofh{font-weight:800;font-size:var(--fs-sm);color:var(--ink);padding:0 2px 8px 0;text-align:right}
  .oframe>.ofcards{display:flex;flex-direction:column;gap:7px}
  /* сущности «вне темы» — same width as framed ones, just shifted right to line up */
  .oent.loose{margin-left:11px;margin-right:10px}
  /* pane hosting the inline конструктор (постановка) */
  /* blox51 #4: конструктор — ПОДНОС, а не лист. Сперва лавандовая заливка черновика делала
     рабочую поверхность мутной: на ней же лежат белые плашки тасков, и они переставали
     читаться как бумага. Первая версия сделала панель самой бумагой (--card) — стало чисто,
     но плашки легли белым на белое и снова потеряли край. Правильный уровень — СРЕДНИЙ:
     --surface (`--paper-200`, «углубление в бумаге»), который по замеру делит зазор ровно
     пополам — 1.15 до бумаги вверх и 1.15 до стола вниз при 1.32 между ними. Плашка на нём
     снова читается листом, а сам поднос — углублением, а не третьим парящим слоем.
     Состояние никуда не делось — его держит РАМКА (у черновика сиреневая). */
  /* blox51 #6: конструктор на обычном уроке тоже потерял салатовую рамку — по той же причине,
     что и .opane.pub выше. А вот pubedit метку СОХРАНЯЕТ: «ты правишь лист, который дети уже
     видят» — единственное состояние здесь, о котором стоит предупредить. Метка переехала с
     зелёной на ЧЕРНИЛЬНУЮ: чернила в этой панели уже значат «смотри сюда» (.opane.next —
     чернильный пунктир «ближайшее»), и у них нет смысла-цвета, который можно прочитать
     неправильно. Зелёный читался «всё хорошо» ровно там, где нужна осторожность. */
  .opane.build{background:var(--surface)}
  .opane.build.draft{border-color:var(--draft-line);background:var(--surface)}
  .opane.build.pubedit{border-color:var(--ink);box-shadow:0 0 0 2px var(--ink) inset}
  /* ЧЕРНОВИК = «ЕЩЁ НЕ ОТПЕЧАТАНО» (blox52 #8). Состояние держал ОДИН признак — цвет
     рамки (сиреневый волосок), и на общем плане панель черновика была неотличима от
     выданной. Теперь черновик говорит МАТЕРИАЛОМ, тем же языком, что и остальной лист:
       · РАМКА ПУНКТИРОМ — та же ступень лестницы линий (волосок 1px, см. [[line-scale]]),
         меняется только стиль штриха, поэтому геометрия панели не сдвигается ни на пиксель
         (правило «экран не прыгает»). Пунктир в проекте уже значит «ещё не настоящее»:
         так нарисованы «+ тема», зона сброса и ближайшее занятие (.opane.next);
       · КОСАЯ ШТРИХОВКА по подложке — бумага, которую ещё не пропустили через печать.
         Сила ≤4% (--draft-hatch, theme.css): гуще — и панель прочиталась бы ОТКЛЮЧЁННОЙ,
         а черновик как раз рабочий. На карточки внутри штриховка не лезет: у .oent/.bitem
         своя непрозрачная бумага (--task-plaque), она ложится поверх.
     Правило объявлено ПОСЛЕ .opane.build.draft и перечисляет ОБА селектора: у панели с
     открытым конструктором заливка написана короткой записью background, а она обнуляет
     background-image — одной .opane.draft штриховка до неё бы не доехала. */
  .opane.draft,.opane.build.draft{border-style:dashed;
    background-image:repeating-linear-gradient(45deg,
      var(--draft-hatch) 0 1px, transparent 1px 7px)}
  .obuild-top{font-size:var(--fs-xs);font-weight:700;color:var(--muted-strong);margin:2px 0 8px}
  .obuildhost{min-height:40px}
  /* CARUSEL: a continuous weekday ribbon; lesson days carry a date + status dot; the two active
     lessons are boxed (left solid, right dashed). Draggable; «к сегодня» = pull-down / dbl-click. */
  .ncar-outer{position:relative;overflow:hidden;border:1px solid var(--line-soft);border-radius:var(--r-md);background:var(--surface-2)}
  /* лента непрозрачна и лежит НАД кольцом (z-index 1): при оттягивании вниз она открывает кольцо
     сверху. touch-action:pan-x — палец скроллит дни по горизонтали нативно, а вертикальный жест
     (pull-to-refresh) остаётся нам. .snap — плавный возврат ленты после отпускания. */
  .ncar{display:flex;gap:1px;overflow-x:auto;cursor:grab;user-select:none;padding:6px 8px;scrollbar-width:thin;
    position:relative;z-index:1;background:var(--surface-2);touch-action:pan-x;will-change:transform}
  .ncar.snap{transition:transform .26s cubic-bezier(.22,1,.36,1)}
  .ncar.grabbing{cursor:grabbing}
  /* ДОРОЖКА полосы прокрутки задана явно (blox51 #11): как только у ::-webkit-scrollbar
     появляется хоть одно своё правило, Chromium перестаёт рисовать дорожку сам и берёт
     светло-серую по умолчанию — в тёмной схеме под лентой уроков светилась белая полоса,
     самое яркое пятно экрана. Прозрачная дорожка верна во всех схемах: видно ползунок. */
  .ncar::-webkit-scrollbar{height:6px;background:transparent}
  .ncar::-webkit-scrollbar-track{background:transparent}
  .ncar::-webkit-scrollbar-thumb{background:var(--line);border-radius:var(--r-xs)}
  .nday{flex:0 0 auto;width:42px;display:flex;flex-direction:column;align-items:center;gap:2px;
    padding:5px 2px;border:1px solid transparent;border-radius:var(--r-sm);background:transparent;font:inherit;line-height:1.1}
  /* caption + date are real content, not hints → --muted (grape-400/300 gave ~1.4–3.2:1).
     noday (a filler day with no lesson) recedes via муteness alone; the lesson days stand out
     with bold --ink dates + the coloured underline, so no dimming opacity is needed. */
  .nday .nwd{font-size:var(--fs-2xs);color:var(--muted)}
  .nday .ndate{font-size:var(--fs-2xs);color:var(--muted)}
  /* тонкая недельная полоса на стыке Вс→Пн: рисуется на ячейке понедельника (.wkstart) как
     ::before в зазоре слева. Высота = высоте плашки (top/bottom:0 тянут её на весь рост ячейки,
     ячейки в .ncar растянуты по stretch). Не меняет ширину ячейки и не влияет на прокрутку
     (blox28 #2). ТОЛЩИНА 1.5px намеренная (blox52 #3: это знак, а не граница — вне лестницы
     линий законно), а вот ЦВЕТ сменён с --frame-line(=чернила) на --line-mid (blox52 #15):
     недельная линейка была самой тёмной линией экрана — темнее плашек с датами, то есть
     иерархия ежедневника стояла вверх ногами (сетка обязана быть тише записей). --line-mid
     перекрашивается схемой так же, как перекрашивался --frame-line. */
  .nday.wkstart{position:relative}
  .nday.wkstart::before{content:'';position:absolute;left:-1px;top:0;bottom:0;width:1.5px;
    border-radius:var(--r-2xs);background:var(--line-mid)}
  /* blox52 #12 — переход у наведения/нажатия дня: рамка и тень уже ехали (пачка 7), но заливка
     (.viewing ставится оптимистично по тапу) и вдавливание меняли вид скачком. background-color
     и transform добавлены в тот же transition; вдавливание — тот же приём, что у .gchip:active,
     только мягче (.96 на плашке 42px читается, .97 было заведено под чип покрупнее). */
  /* рамка плашки — --line-mid, а не --line-soft (blox52 #15): день с уроком обязан стоять
     В иерархии ВЫШЕ сетки (недельной линейки того же --line-mid, но без заливки и содержимого) —
     с невидимым --line-soft плашка держалась на одной заливке и проигрывала линейке. */
  .nday.lday{cursor:pointer;background:var(--card);border-color:var(--line-mid);position:relative;
    transition:border-color var(--t) var(--ease),box-shadow var(--t) var(--ease),
               background-color var(--t) var(--ease),transform var(--t-fast) var(--ease)}
  .nday.lday:active{transform:scale(.96)}
  /* «уменьшенное движение» гасит НОВОЕ движение пачки blox52 #12: подчёркивание группы
     прыгает к чипу без анимации, день не вдавливается и не везёт transform. Цветовые
     переходы (рамка/заливка/тень) остаются — это не движение. Занавес #gpanes тут не
     назван намеренно: его transition — противомигательный механизм, см. комментарий выше. */
  @media (prefers-reduced-motion:reduce){
    .gind{transition:none}
    .nday.lday{transition:border-color var(--t) var(--ease),box-shadow var(--t) var(--ease),
               background-color var(--t) var(--ease)}
    .nday.lday:active{transform:none}
  }
  /* НАВЕДЕНИЕ НА ПЛАШКУ ДНЯ (blox52 #8). Лента дней была последним рядом нажимаемых плиток
     без отклика на курсор — при том что у плитки кабинета, призрачной кнопки и чипа группы
     (blox52 #3) он есть. Тот же ключ, что у .hub-card: акцент ЛИНИЕЙ (--accent-deep) плюс
     тень на ступень выше; ни размер, ни отступы не трогаются — лента прокручивается по
     ширине плашки 42px, и подвинуть её значило бы сдвинуть весь расчёт каретки.
     Подложку намеренно НЕ красим: --accent-tint на этой ленте уже занят и значит «эту пару
     сейчас смотрим» (.nday.viewing) — наведение не имеет права говорить то же самое.
     Селектор написан ОДНИМ классом (.lday:hover, а не .nday.lday:hover) и объявлен ДО
     состояний ниже: у активной пары, у «сегодня» и у «ДЗ не выдано» рамка своя и обязана
     пережить курсор — при равной силе побеждает то правило, что записано позже. Тот же
     приём, что у .gchip:hover перед .gchip.on. */
  @media (hover:hover){.lday:hover{border-color:var(--accent-deep);box-shadow:var(--sh-1)}}
  /* blox35 #1 — тортик на плашке урока-праздника: в УГЛУ и выступает за плашку, как счётчик
     непрочитанных над кнопкой. Выход наружу безопасен: у .ncar padding 6px/8px, поэтому значок
     остаётся ВНУТРИ ленты и не обрезается её .ncar-outer{overflow:hidden}; вбок он заходит на
     пустое поле соседней ячейки (текст в ней центрирован и узкий), ширину плашки не меняет —
     на 42px держится расчёт прокрутки каретки. Геометрию стережёт DOM-ассерт #10. */
  .nday .ncake{position:absolute;top:-6px;right:-6px;line-height:0;pointer-events:none}
  .nday .ncake .ic{width:16px;height:16px}
  /* Число и месяц — ДВЕ строки (blox51 #9). Замер: внутри плашки 36px, «12 сент» просит 46 —
     в одну строку месяц не помещается ничем (трёхбуквенный просит ~50), а расширить плашку нельзя:
     от её ширины 42px считается прокрутка ленты. Высота ленты при этом постоянна — месяц стоит на
     КАЖДОЙ плашке урока, поэтому она не растёт и не садится от того, какие дни видны. nowrap на
     обеих строках: табличные цифры (заход 6) сделали единицу шириной с ноль, и перенос внутри
     строки снова поднял бы ячейку выше соседних. */
  .nday.lday .ndate{font-size:var(--fs-xs);font-weight:700;color:var(--ink);white-space:nowrap}
  .nday .nmon{font-size:var(--fs-2xs);font-weight:600;color:var(--muted);white-space:nowrap;line-height:1.05}
  .nday.lday::after{content:'';width:6px;height:6px;border-radius:50%;background:var(--line);margin-top:1px}
  .nday.lday-ok::after{background:var(--ok)}                            /* нечего делать: сдавать нечего / всё проверено */
  /* «учителю надо что-то сделать» = к проверке есть ИЛИ ДЗ не выдано (ldayCls, teacher.js) —
     обе причины ОЧЕРЕДЬ, просрочки в семантике точки нет → якорь схемы, не красный (blox52 #11) */
  .nday.lday-alert::after{background:var(--anchor)}
  .nday.act-left{border-color:var(--ink);border-style:solid;box-shadow:0 0 0 1px var(--ink) inset}
  .nday.act-right{border-color:var(--ink);border-style:dashed}
  .nday.nag{border-color:var(--anchor)!important;box-shadow:0 0 0 1px var(--anchor) inset}  /* нужно выдать ДЗ — очередь, якорь схемы (blox52 #11) */
  .nday.viewing{background:var(--accent-tint)}                                   /* какую пару сейчас смотрим */
  .nday.today{opacity:1}                                             /* сегодня всегда яркий, даже если не день занятия */
  .nday.today .ndate{display:flex;align-items:center;justify-content:center;width:22px;height:22px;
    border:1px solid var(--ink);border-radius:50%;color:var(--ink);font-weight:800;font-size:var(--fs-xs)}
  /* «Вернуться к сегодня» без кнопки: кольцо ПОД лентой. Оттягивание ленты вниз открывает его
     сверху, а дуга замыкается по мере оттягивания (--prog 0→1). За порогом .ready → зелёная,
     на отпускании .spin → крутится, пока грузится актуальный урок. blox31 #2. */
  .ncar-refresh{--prog:0;position:absolute;top:6px;left:50%;transform:translateX(-50%);
    width:28px;height:28px;opacity:0;z-index:0;pointer-events:none;transition:opacity .12s}
  .ncar-refresh.show{opacity:1}
  .ncr-svg{width:28px;height:28px;transform:rotate(-90deg)}
  .ncr-bg{fill:none;stroke:var(--line);stroke-width:3}
  .ncr-arc{fill:none;stroke:var(--ink);stroke-width:3;stroke-linecap:round;
    stroke-dasharray:94.2;stroke-dashoffset:calc(94.2 * (1 - var(--prog,0)))}
  .ncar-refresh.ready .ncr-arc{stroke:var(--ok-solid)}
  .ncar-refresh.spin .ncr-svg{animation:ncr-spin .7s linear infinite}
  .ncar-refresh.spin .ncr-arc{stroke:var(--ok-solid);stroke-dashoffset:calc(94.2 * .25)}
  @keyframes ncr-spin{to{transform:rotate(360deg)}}
  @media(max-width:760px){
    .otwo{flex-direction:column-reverse}   /* DOM: earlier, axis, later -> reversed = later on TOP */
    .opane{flex:1 1 100%;margin:6px 0}
    /* на телефоне между панелями — просто зазор (blox52 #15): горизонтальная полоса читалась
       ползунком; сами панели и так различимы шапками, «сейчас» несёт каретка */
    .oaxis{display:none}
    .opane.edge{align-self:auto}   /* столбик: cross-axis — ширина, flex-start её бы сузил */
    .oarr{width:100%;height:34px}
    .opane-body{flex-direction:column;align-items:stretch}
    .oroster{flex:1 1 auto;flex-direction:row;flex-wrap:wrap}
    .oroster .rchip{width:auto}
  }
  /* таск в черновике ДЗ = такая же плашка, как у ученика и на проверке: белый прямоугольник,
     тонкая рамка и цветной рельс слева (--rail: серый / синий «нужна сдача») — blox10 #8 */
  .bitem{display:flex;align-items:flex-start;flex-wrap:wrap;gap:8px 12px;padding:12px 12px 12px 21px;
    border:1px solid var(--line-soft);border-radius:var(--r-lg);margin-bottom:8px}
  .bitem .bico{font-size:var(--ic-lg)}
  .bitem .binfo{flex:1 1 calc(100% - 44px);min-width:0;overflow-wrap:anywhere}
  /* кнопки действий уходят на свою строку под текстом, вся тройка прижата вправо */
  .bitem .act:first-of-type{margin-left:auto}
  .bitem .bt{font-weight:700;font-size:var(--fs-md)}   /* тот же размер, что у трёх других карточек */
  .bitem .bm{font-size:var(--fs-xs);color:var(--muted);margin-top:2px}
  .bitem .act{background:none;border:none;cursor:pointer;font-size:var(--fs-sm);font-weight:700;
    color:var(--accent-dark);padding:4px}
  .bitem .act.del{color:var(--red)}
  /* ПОСТАНОВКА: тема = рамка. Рамка — общая (см. .tframe,.oframe,.theme выше), тут только
     свой padding/margin. */
  .theme{padding:4px 14px 14px;margin:0 0 18px}
  .theme .tin{background:transparent;border-radius:0;padding:0}
  .theme .thead{display:flex;align-items:center;gap:8px;margin-bottom:10px}
  .theme .tname{flex:1;min-width:0;width:100%;-webkit-appearance:none;appearance:none;
    text-align:center;border:none;border-bottom:1px dashed var(--line);
    font-weight:800;font-size:var(--fs-input);padding:4px 0 7px;background:transparent;color:var(--ink)}
  .theme .tname:focus{outline:none;border-bottom-color:var(--accent-deep)}
  /* 24px — минимум WCAG 2.2 target-size и порог G5. Было 23×22: крестик, удаляющий тему
     целиком, промахивался под пальцем. Размер добираем min-*, а не паддингом, чтобы глиф
     остался по центру и панель не поехала (выросло на 2px). */
  .theme .tdel{background:none;border:none;color:var(--red);cursor:pointer;font-size:var(--ic-md);
    line-height:1;padding:2px 4px;min-width:24px;min-height:24px;
    display:inline-flex;align-items:center;justify-content:center}
  /* привязка темы к учебнику: кнопка «Учебник» / название + карусель юнитов */
  .theme .tbook{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:-2px 0 12px;
    justify-content:center;position:relative}
  .tb-add{background:var(--accent-tint);border:1px solid var(--accent-line);color:var(--accent-dark);font-weight:700;
    border-radius:var(--r-sm);padding:5px 11px;cursor:pointer;font-size:var(--fs-sm)}
  @media (hover:hover){.tb-add:hover{background:var(--accent-fill)}}
  .tb-name{font-weight:800;font-size:var(--fs-sm);color:var(--accent-dark);background:var(--accent-tint);
    border-radius:var(--r-sm);padding:5px 10px}
  /* юниты (в книге их 10–15): системный <select> вместо карусели ◀ ▶ (blox50 #33). 16px — ниже
     iOS Safari зумит страницу на фокусе поля (правило 3 lingo-ui); min-height 32px — под палец
     (G5: прежние стрелки 23×18 были ниже порога, найдено обходчиком 03.09.2026). */
  .tb-usel{background:var(--card);border:1px solid var(--accent-line);border-radius:var(--r-sm);
    padding:5px 8px;font-weight:800;font-size:var(--fs-input);color:var(--ink);cursor:pointer;
    min-height:32px;max-width:190px}
  .tb-usel:disabled{color:var(--faint);cursor:default}
  /* «отвязать» — подчёркнутое ДЕЙСТВИЕ, его читают, а --faint (2.98:1 на --surface-2) заведён
     для декора и disabled. Тот же разбор, что у .stu-em и .sb-note в #20. */
  .tb-unbind{background:none;border:none;color:var(--muted);cursor:pointer;font-size:var(--fs-xs);
    text-decoration:underline;padding:4px}
  @media (hover:hover){.tb-unbind:hover{color:var(--red)}}
  .tb-menu{position:absolute;top:100%;left:50%;transform:translateX(-50%);z-index:30;background:var(--card);
    border:1px solid var(--accent-line);border-radius:var(--r-sm);box-shadow:var(--sheet-3);padding:6px;
    display:flex;flex-direction:column;gap:4px;min-width:190px;margin-top:4px}
  .tb-menu button{background:none;border:none;text-align:left;padding:8px 10px;border-radius:var(--r-xs);
    cursor:pointer;font-weight:700;font-size:var(--fs-sm);color:var(--ink)}
  @media (hover:hover){.tb-menu button:hover{background:var(--accent-tint)}}
  /* пустой шаблон сущности (три точки) — всегда внизу рамки и внизу области вне тем */
  /* зеленоватый (владелец, 23.08.2026): роль «сюда можно добавить» читается как приглашение, а не
     как ещё один нейтральный блок. Меняется ТОЛЬКО цвет — пунктир, скругление, отступы, вес и
     воздушность те же. Токены роли --ok-* (не сырая лестница) и они одинаковы во всех схемах. */
  .eslot{display:flex;align-items:center;justify-content:center;gap:10px;border:2px dashed var(--ok-line);
    border-radius:var(--r-md);padding:13px;color:var(--ok-ink);font-weight:700;cursor:pointer;background:var(--ok-tint)}
  @media (hover:hover){.eslot:hover{border-color:var(--ok);color:var(--ok-ink);background:var(--ok-fill)}}
  .eslot .dots{font-size:var(--ic-lg);letter-spacing:3px;line-height:1}
  /* blox45 #13 — «+ тема» рисуется как ПУСТАЯ тема: та же скобка-рамка и та же ширина, что
     появится после нажатия, только ПУНКТИРНАЯ. Рамка — тот же псевдоэлемент, что у .theme
     (см. .tframe,.oframe,.theme выше) — значит и ориентация у неё та же.
     До blox45 это была узкая белая кнопка на левой линии «+ таск вне темы» (blox13 #4). */
  .newtheme{position:relative;display:block;width:100%;box-sizing:border-box;
    --fpad:14px;--frail:6px;background:transparent;border:none;
    padding:22px 14px;margin:0 0 18px;cursor:pointer;text-align:center;
    font-family:inherit;font-size:var(--fs-input);font-weight:800;color:var(--accent-dark)}
  .newtheme::before{content:"";position:absolute;top:0;bottom:0;
    left:calc(var(--fpad) + var(--frail));right:0;
    border:1.5px dashed var(--frame-line);border-left:none;
    border-radius:0 var(--r-xl) var(--r-xl) 0;pointer-events:none}
  @media (hover:hover){.newtheme:hover{color:var(--accent)}
    .newtheme:hover::before{border-color:var(--accent-deep)}}
  /* «вне темы» стоит по той же вертикали, что таски внутри рамки (граница 1.25px + падинг 14px);
     сама скобка рамки закрыта справа, слева она открыта — тасков это не касается */
  .standarea{margin:0 14px 0 15px}
  /* окно заполнения сущности — модальное поверх экрана */
  /* вуаль — токен --scrim, не сырое число (blox52 #16 п.3): зашитый серо-лиловый
     rgba(37,26,46,.48) был холодным поверх тёплой бумаги базовой схемы и не темнел ночью.
     --scrim собран из --sh-rgb — тёплый в базовой, свой в каждой схеме. */
  .emodal{position:fixed;inset:0;background:var(--scrim);display:flex;align-items:flex-start;
    justify-content:center;padding:36px 14px;z-index:60;overflow:auto}
  /* position:relative — иначе крестик .eclose (position:absolute) цеплялся за .emodal (весь экран)
     и висел над тёмной шапкой на top:10px, а не в углу окна — почти невидимый (blox17 #4). */
  .emodal .panel{max-width:540px;width:100%;margin:0;position:relative}
  .emodal .eclose{position:absolute;top:10px;right:12px;background:none;border:none;font-size:var(--ic-lg);
    color:var(--muted);cursor:pointer;line-height:1;z-index:1}
  .pool-note{background:var(--accent-tint);border:1px solid var(--accent-line);border-radius:var(--r-md);padding:12px 14px;
    font-size:var(--fs-sm);color:var(--accent-dark);margin-bottom:8px;font-weight:600}
  /* student: the standing text note ("объявление") — a half-screen scrollable block */
  /* объявление — a standing note. Lavender, so it never reads as a "state" (alert/draft/done). */
  .annc{background:var(--note-bg);border:1px solid var(--note-line);border-radius:var(--r-lg);
    margin:8px 0 14px;overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--sheet-1)}
  .annc .ah{font-size:var(--fs-2xs);font-weight:800;letter-spacing:var(--tracking-caps);
    text-transform:uppercase;color:var(--note-ink);padding:12px 16px 6px}
  /* ТЕЛО ОБЪЯВЛЕНИЯ И ТЕЛО ЗОЛОТОГО ОТЗЫВА — читаемый текст (blox52 #6): книжный вес и
     pretty. Переносов нет: пишет учитель, чаще по-английски (см. .rtitle-body). Правило и у
     .fbnote .fbbody ниже — это близнецы одной формы, и правка одного без другого разъехалась
     бы молча (правило близнецов, CLAUDE.md). */
  .annc .abody{max-height:50vh;overflow-y:auto;-webkit-overflow-scrolling:touch;
    padding:0 16px 16px;font-size:var(--fs-sm);line-height:1.55;color:var(--ink);white-space:pre-wrap;
    word-break:break-word;overscroll-behavior:contain;font-weight:var(--fw-read);text-wrap:pretty}
  .annc .abody a{color:var(--accent-ink);text-decoration:underline;font-weight:600}
  /* blox27 #3 — учительский feedback за урок: ЗОЛОТОЙ блок в бывшем слоте объявления (само
     объявление переехало НАД карусель дней). Форма как у .annc, тема золотая. */
  .fbnote{background:var(--gold-tint);border:1px solid var(--gold-line);border-radius:var(--r-lg);
    margin:8px 0 14px;overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--sheet-1)}
  .fbnote .fbh{font-size:var(--fs-2xs);font-weight:800;letter-spacing:var(--tracking-caps);
    text-transform:uppercase;color:var(--gold-ink);padding:12px 16px 6px;display:flex;align-items:center;gap:6px}
  .fbnote .fbh .ic{color:var(--gold)}
  .fbnote .fbbody{max-height:50vh;overflow-y:auto;-webkit-overflow-scrolling:touch;
    padding:0 16px 16px;font-size:var(--fs-sm);line-height:1.55;color:var(--ink);white-space:pre-wrap;
    word-break:break-word;overscroll-behavior:contain;font-weight:var(--fw-read);text-wrap:pretty}
  .fbnote .fbbody a{color:var(--accent-ink);text-decoration:underline;font-weight:600}
  .toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}
  /* кнопки конструктора («Выдать ученикам» / «Подтянуть…») по той же левой линии, что «+ Новая
     тема» и «+ таск» (margin-left:15px) — blox16 #8 */
  .toolbar.bbar{margin-left:15px}
  /* blox52 #14 — строка подтверждения публикации ПУСТОГО листа: живёт ПОД кнопками конструктора
     (вставляется after .bbar), поэтому ничего над кнопками не сдвигает. Не modal и не alert —
     обычная строка панели, та же левая линия, что у .bbar. */
  .pubconfirm{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 14px 15px}
  .pubconfirm .pubq{font-size:var(--fs-sm);color:var(--ink)}
  /* пара «Выдать / Вернуться» ходит ВМЕСТЕ: перенос не имеет права разлучить кнопки решения
     (lingo-ui §3 — логически связанные контролы в одной неразрывной обёртке) */
  .pubconfirm .pubbtns{display:flex;align-items:center;gap:8px;flex-wrap:nowrap}
  /* .btn-ghost → folded onto the engine's --ghost role (the base look). Modifiers below. */
  /* blox24 #7 — «Очистить» весь лист ДЗ: разрушительное действие, поэтому тревожный акцент */
  /* --alert (gf-500) текстом на белом даёт 3.56:1. Разрушающее действие уже имеет свой ярлык —
     --danger-ink (7.1:1); в #20 его получили .btn--danger и .mgr-btn.warn, а .bdanger живёт
     отдельным правилом и тогда остался в стороне. */
  .btn-ghost.bdanger{color:var(--danger-ink)}
  @media (hover:hover){.btn-ghost.bdanger:hover{border-color:var(--alert);background:var(--alert-tint)}}
  /* счётчик долгов ученика (blox19 #2) — тревожный акцент, чтобы бросался в глаза */
  /* «Долги: N» (blox52 #11) — нейтральный чип с красной точкой-маркером. Красная заливка была
     ПЕРВЫМ, что видел ребёнок на своём экране, — удар до того, как он что-то сделал. Серьёзность
     остаётся: точка — настоящая тревога (просроченные сдачи), но говорит она маркером, не плоскостью. */
  .btn-ghost.sdebt{color:var(--ink);border-color:var(--line);background:var(--fill-soft);font-weight:800}
  .sdebt-dot{width:8px;height:8px;border-radius:50%;background:var(--alert);flex:0 0 auto}
  /* РЯД ЧИПОВ УЧЕНИКА НА ТЕЛЕФОНЕ (blox52 #13). На 375px три чипа («Чат группы» ·
     «Для родителя» · «Долги: N») не влезали в строку, и третий повисал один слева —
     рваный ряд прямо над заголовком. Лечим сжатием: узкие отступы (--btn-px — ручка
     движка кнопок) ставят все три в одну строку без обрезания текста; если чипов-текста
     всё же больше строки (длинный счётчик), flex:1 растягивает то, что перенеслось, во
     всю ширину — перенос перестаёт быть рваным. Текст внутри чипа не ломаем (nowrap:
     чип в полторы строки — хуже переноса). Ширины экрана ≥431px правило не трогает. */
  @media (max-width:430px){
    .toolbar.stbar{gap:6px}
    .toolbar.stbar .btn-ghost{--btn-px:11px;flex:1 1 auto;white-space:nowrap;min-width:0}
  }
  /* .btn-pub → --primary, .btn-fb → бумажная ghost (blox52 #10) — both in the BUTTON ENGINE
     block. Kept here: the «sent» (green ✓) state — «отправлено» = «сделано», зелёный ему по праву. */
  .btn-fb.sent{--btn-line:var(--ok-line);--btn-face:var(--ok-tint);--btn-ink:var(--ok-ink)}
  .btn-fb.sent .ic{color:var(--ok-solid)}

  /* ═══════════════════════════════════════════════════════════════════════
     BUTTON ENGINE (branch design-system) — ONE base + role variants.
     A text button = .btn + one .btn--<role> (+ optional .btn--sm).
     Geometry rides knob tokens (--btn-*) so a variant is a few lines and a
     later convergence is a one-value change, not a rewrite. Colour is ROLE-only
     (skill §11) so every button follows all three schemes automatically.
       Roles: --primary  gradient CTA (the one loud action on a screen)
              --accept   solid green confirm/publish («Выдать»/«Принять»/«Готово»)
              --ghost    outline secondary — the default look of the base
              --fill     solid accent (compact primary for dense cabinets)
              --danger   outline, alert-coloured (destructive)
     Phase 2 (branch design-system) = legacy names are FOLDED onto the engine
     here (aliased into the base + a role), and the geometry duplicated in their
     own rules below is stripped. HTML is untouched — so all their contextual
     overrides (.sheet .btn-ghost{width:100%}, .mgr-top3 .mgr-btn, .bdanger…)
     keep working — but every text button now shares ONE base: unified radius,
     padding, 700 weight, 44px touch target, and role-only colour. Map:
       .btn-primary/.btn-pub → --primary (blox52 #10: ОДИН цвет действия — кнопка-решение
       носит акцент СВОЕЙ схемы, зелёная заливка осталась только «принятию»: .btn--accept) ·
       .btn-ghost/.mgr-btn/.btn-fb → --ghost · .mgr-btn.primary → --fill ·
       .mgr-btn.warn → --danger. */
  .btn,
  .btn-primary,.btn-pub,.btn-ghost,.btn-fb,.mgr-btn{
    /* knobs — a variant / size overrides these */
    --btn-r:var(--r-md); --btn-px:18px; --btn-py:11px; --btn-fs:14px; --btn-fw:700;
    --btn-face:var(--card); --btn-ink:var(--ink); --btn-line:var(--line);
    display:inline-flex;align-items:center;justify-content:center;gap:6px;
    box-sizing:border-box;min-height:44px;                     /* G5 touch target */
    border:1px solid var(--btn-line);border-radius:var(--btn-r);
    padding:var(--btn-py) var(--btn-px);
    font-family:inherit;font-size:var(--btn-fs);font-weight:var(--btn-fw);line-height:1.2;
    color:var(--btn-ink);background:var(--btn-face);cursor:pointer;box-shadow:var(--sh-1); /* resting lift — even the outline roles float a touch */
    transition:border-color var(--t) var(--ease),background var(--t) var(--ease),
               box-shadow var(--t) var(--ease),transform var(--t-fast) var(--ease)}
  /* ОТКЛЮЧЁННАЯ КНОПКА — БЛЁКЛАЯ, А НЕ ВЫЦВЕТШАЯ (blox52 #3, п.1). opacity:.45 гасила предмет
     ЦЕЛИКОМ и вместе с ним подпись: у залитой кнопки («Перевести», «Выдать») получалось мутное
     сиреневое пятно, на котором белые буквы давали 1.9:1 — надпись отключённой кнопки просто
     не читалась, а именно она и объясняет, чего кнопка ждёт. Прозрачность вдобавок протаскивает
     сквозь кнопку всё, на чём она лежит, поэтому вид зависел от подложки.
     Теперь «отключено» говорят РОЛИ ЦВЕТА, а не прозрачность: текст уходит в --faint, рамка —
     в волосок. Курсор и вес 700 сохранены намеренно: предмет должен остаться КНОПКОЙ, просто
     сейчас недоступной, а не превратиться в подпись. */
  .btn:disabled,.btn-primary:disabled,.btn-pub:disabled,
  .btn-ghost:disabled,.btn-fb:disabled,.mgr-btn:disabled{
    --btn-ink:var(--faint);--btn-line:var(--line-soft);
    cursor:default;box-shadow:none;transform:none}
  /* ЗАЛИТЫЕ роли гасят и саму заливку — в нейтральную --fill-soft (--faint на ней 3.0-3.9:1 во
     всех четырёх схемах). Отдельным правилом, потому что background у них написан строкой
     (градиент), и ручкой --btn-face его не перебить. Контурные роли сюда НЕ входят: у них
     заливка и так бумага, гасить нечего — гаснут только текст и рамка. */
  .btn--primary:disabled,.btn-primary:disabled,.btn--fill:disabled,.mgr-btn.primary:disabled,
  .btn--accept:disabled,.btn-pub:disabled{
    --btn-face:var(--fill-soft);background:var(--fill-soft)}
  .btn:focus-visible,.btn-primary:focus-visible,.btn-pub:focus-visible,
  .btn-ghost:focus-visible,.btn-fb:focus-visible,.mgr-btn:focus-visible{outline:none;box-shadow:var(--ring)}
  .btn .ic{color:currentColor}
  /* size — compact (a fine-tuning tool; apply .btn--sm where density matters) */
  .btn--sm{--btn-px:14px;--btn-py:8px;--btn-fs:13px;--btn-r:var(--r-sm);min-height:38px}
  /* КАЖДОЕ правило с :hover в этом файле заперто в @media (hover:hover). Тач-браузер
     эмулирует наведение после тапа и НЕ снимает его: кнопка оставалась подсвеченной и
     приподнятой, пока не ткнёшь мимо — на планшете это читается как «интерфейс подвис».
     Медиазапрос отдаёт hover только настоящему курсору. Обёрнуто ПО МЕСТУ, а не одним
     блоком в конце: :active объявлен ПОСЛЕ :hover намеренно (на мыши истинны оба, и
     нажатие должно побеждать наведение) — перенос правил вниз сломал бы этот порядок.
     roles (legacy names aliased onto each). Filled roles carry --bevel (a lit top rim + a dark
     bottom edge) ON TOP of their drop shadow, and a gradient fill (--grad-*) instead of a flat colour. */
  @media (hover:hover){.btn--ghost:hover,.btn-ghost:hover,.mgr-btn:hover,.btn-fb:not(.sent):hover{border-color:var(--accent-deep);background:var(--accent-tint);box-shadow:var(--sh-2)}}
  /* filled hovers glow in the role's OWN colour (color-mix from the role token → follows
     every scheme on its own), so hover reads as the button lighting up, not a grey shadow. */
  /* blox51 #3 (заход 3.1): роли .btn--fill и .mgr-btn.primary СХЛОПНУТЫ сюда. Пока
     заливка была синей, а первичная — цветом схемы, это были две разные кнопки. Как
     только обе взяли якорь схемы, они стали одним цветом — держать два имени для
     одного вида значит ждать, пока они разъедутся. Имена оставлены как псевдонимы:
     их пять на весь проект, переименование в разметке ничего не даёт. */
  /* --ripple-blend:normal у ЗАЛИТЫХ ролей (blox52 #8): волна на них белая (currentColor
     = --on-dark), а умножение белым — тождество, то есть на тёмной заливке волны бы
     не стало вовсе. Бумажные роли (ghost/danger/fb) режим схемы не трогают. */
  /* blox52 #10 — ОДИН ЦВЕТ ДЕЙСТВИЯ. .btn-pub переехала из зелёной --accept сюда, в --primary:
     кнопка-РЕШЕНИЕ («Выдать ученикам», «Установить ДЗ», «Сохранить», «Добавить запись»…) носит
     акцент СВОЕЙ схемы (--grad-primary/--anchor), а зелёная заливка вернулась к единственному
     смыслу «сделано/принято» — её носит только .btn--accept («Принять» / «Принять всё»). */
  .btn--primary,.btn-primary,.btn-pub,.btn--fill,.mgr-btn.primary{--btn-line:transparent;--btn-ink:var(--on-dark);
    --ripple-blend:normal;
    background:var(--grad-primary);box-shadow:var(--bevel),var(--sh-1)}
  @media (hover:hover){.btn--primary:hover,.btn-primary:hover,.btn-pub:hover,.btn--fill:hover,.mgr-btn.primary:hover{box-shadow:var(--bevel),0 6px 18px color-mix(in srgb,var(--anchor) 42%,transparent)}}
  .btn--accept{--btn-line:transparent;--btn-ink:var(--on-dark);
    --ripple-blend:normal;
    background:var(--grad-accept);box-shadow:var(--bevel),var(--sh-1)}
  @media (hover:hover){.btn--accept:hover{box-shadow:var(--bevel),0 6px 18px color-mix(in srgb,var(--ok-solid) 40%,transparent)}}
  /* Текстом на белом --alert (gf-500) даёт 3.56:1 — под AA. Для разрушающего действия
     семантика уже есть: --danger-ink (gf-700), 7.1:1. Заливка/рамка hover'а остаются --alert. */
  .btn--danger,.mgr-btn.warn{--btn-ink:var(--danger-ink)}
  @media (hover:hover){.btn--danger:hover,.mgr-btn.warn:hover{border-color:var(--alert);background:var(--alert-tint);box-shadow:var(--sh-2)}}
  /* blox52 #10: .btn-fb СНЯЛА янтарь — это был пятый цвет «главной» кнопки и единственный
     янтарный элемент интерфейса, а золото в проекте занято ровно одним смыслом — «приз»
     (см. --gold в theme.css: «Nothing else may use it»). Теперь это бумажная ghost: базовый
     вид движка, hover — в общем ghost-списке выше. От blox27 #3 осталась половина по праву:
     зелёная ✓ после отправки (.btn-fb.sent) — «отправлено» и есть «сделано». */
  /* blox45 #16 — «Edit» в шапке ВЫДАННОГО листа тоже золотая: она единственная на панели зовёт
     тронуть уже живое ДЗ, и золото отличает её от нейтральных ghost-кнопок. Тот же приём, что у
     .btn-fb: knobs объявлены ПОСЛЕ базы, поэтому побеждают .btn-ghost. */
  /* blox51 #4: .oedit СНЯЛА золотые ручки. Золото в проекте занято ровно одним смыслом —
     «приз» (челлендж, см. --gold в theme.css: «Nothing else may use it»); кнопка правки
     одолжила его без права и рядом с соседкой Notes выглядела тревожной. Теперь это
     обычная призрачная кнопка базовой роли — ровно как Notes. Свой размер она держит
     в .opane-hr .oedit выше. */
  /* juicy MOTION — lift on hover, press on active. transform = no reflow (no CLS, G13).
     :active is declared AFTER :hover so a mouse-down (both true on a PC) reads as a press,
     not a stuck lift; :not(:disabled) keeps «Недоступно» from bobbing on hover. */
  @media (hover:hover){.btn:hover:not(:disabled),.btn-primary:hover:not(:disabled),.btn-pub:hover:not(:disabled),
  .btn-ghost:hover:not(:disabled),.btn-fb:hover:not(:disabled),.mgr-btn:hover:not(:disabled){transform:translateY(-1px)}}
  .btn:active,.btn-primary:active,.btn-pub:active,
  .btn-ghost:active,.btn-fb:active,.mgr-btn:active{transform:translateY(0) scale(.97);box-shadow:var(--sh-press)} /* press = pushed in */
  @media (prefers-reduced-motion:reduce){
    .btn,.btn-primary,.btn-pub,.btn-ghost,.btn-fb,.mgr-btn{transition:border-color var(--t) var(--ease),background var(--t) var(--ease),box-shadow var(--t) var(--ease)}
    .btn:hover,.btn-primary:hover,.btn-pub:hover,.btn-ghost:hover,.btn-fb:hover,.mgr-btn:hover,
    .btn:active,.btn-primary:active,.btn-pub:active,.btn-ghost:active,.btn-fb:active,.mgr-btn:active{transform:none}
  }

  /* STATE · loading — spinner replaces the label, clicks blocked (guards double-submit).
     The spinner borrows --btn-ink (still set even though `color` is transparent), so it's
     white on filled roles / ink on ghost automatically. reduced-motion slows it, never
     kills it — it's a functional indicator, not decoration (same call as .pl-spinner). */
  .btn.is-loading,.btn-primary.is-loading,.btn-pub.is-loading,.btn-ghost.is-loading,.mgr-btn.is-loading,.pool-dlall.is-loading{
    color:transparent;pointer-events:none;position:relative}
  .btn.is-loading::after,.btn-primary.is-loading::after,.btn-pub.is-loading::after,
  .btn-ghost.is-loading::after,.mgr-btn.is-loading::after,.pool-dlall.is-loading::after{
    content:"";position:absolute;top:calc(50% - 8px);left:calc(50% - 8px);width:16px;height:16px;
    border-radius:50%;border:2px solid var(--btn-ink);border-top-color:transparent;
    animation:btn-spin .7s linear infinite}
  @keyframes btn-spin{to{transform:rotate(360deg)}}
  @media (prefers-reduced-motion:reduce){
    .btn.is-loading::after,.btn-primary.is-loading::after,.btn-pub.is-loading::after,
    .btn-ghost.is-loading::after,.mgr-btn.is-loading::after{animation-duration:2s}}
  /* STATE · done — a brief green «✓ сработало» morph (generalises .btn-fb.sent). The caller
     swaps the label to a ✓ and drops the class after ~1s. Green tint overrides any role fill. */
  .btn.is-done,.btn-primary.is-done,.btn-pub.is-done,.btn-ghost.is-done,.mgr-btn.is-done{
    --btn-line:var(--ok-line);--btn-ink:var(--ok-ink);
    background:var(--ok-tint);filter:none;box-shadow:var(--sh-1)}

  /* ICON BUTTON — square/round, icon-only (✕ close · ← back · ⋯ kebab · remove).
     Collapses the ~15 one-off close/remove classes. Base = square; --circle for
     the close-in-a-ring; --sm for the tiny inline remove; --danger for delete. */
  .icon-btn{
    --ib-size:38px; --ib-r:var(--r-sm); --ib-ink:var(--muted); --ib-face:transparent;
    display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;
    box-sizing:border-box;width:var(--ib-size);height:var(--ib-size);
    border:1px solid var(--line);border-radius:var(--ib-r);
    background:var(--ib-face);color:var(--ib-ink);cursor:pointer;font-size:var(--ic-md);line-height:1;
    transition:border-color var(--t) var(--ease),background var(--t) var(--ease),
               color var(--t) var(--ease),transform var(--t-fast) var(--ease)}
  @media (hover:hover){.icon-btn:hover{border-color:var(--accent-deep);background:var(--accent-tint);color:var(--accent-ink)}}
  .icon-btn:active{transform:scale(.94)}
  .icon-btn:focus-visible{outline:none;box-shadow:var(--ring)}
  .icon-btn .ic{color:currentColor}
  .icon-btn--circle{--ib-r:var(--r-pill)}
  .icon-btn--sm{--ib-size:24px;font-size:var(--ic-sm)}
  @media (hover:hover){.icon-btn--danger:hover{border-color:var(--alert);background:var(--alert-tint);color:var(--alert-ink)}}

  /* ═══════════════════════════════════════════════════════════════════════
     ВОЛНА ОТ КАСАНИЯ — общий отклик всех нажимаемых поверхностей (public/ripple.js).
     Зачем поверх :active: в Safari на iOS :active рисуется непредсказуемо — браузер
     придерживает его, пока не решит, что палец не поедет в скролл. Тап мог не дать
     НИКАКОГО отклика, и ребёнок жал кнопку второй раз. Волна висит на pointerdown
     и отвечает всегда.

     ЦВЕТ — currentColor, то есть цвет текста самой кнопки. Это то, что делает волну
     универсальной: на тёмной заливке (--primary/--accept) текст белый → волна белая;
     на светлой контурной текст тёмный → волна тёмная. Контраст держится на любой
     заливке и во ВСЕХ трёх схемах, без единой строки на схему. Поверхность может
     попросить свой цвет через --ripple-ink. Плоская заливка не годилась: голубая
     волна на тёмно-виноградной кнопке читалась бы как грязное пятно.

     РАДИУС считает ripple.js и намеренно НЕ дотягивает до дальнего угла: полный
     радиус раздувает круг за один кадр, и волна читается как заливка слева направо —
     видно, что нажатие засчитано, но не видно ГДЕ коснулись. А точка касания и есть
     главное, что волна сообщает. Мягкий край (radial-gradient) — по той же причине.

     ОБРЕЗКА — своим слоем, а НЕ overflow:hidden на самой кнопке. Первая версия резала
     кнопку целиком, и вместе с волной срезались угловые индикаторы: кружок непрочитанных
     и тортик дня рождения сидят на `top:-7px;right:-7px`, то есть НАМЕРЕННО торчат за
     край (см. .chatdot). Волна живёт в собственной коробке `.ripple-box`, которая
     повторяет скругление кнопки и обрезает только себя. */
  /* .gchip/.rchip/.oent-live — blox52 #12: чипы групп и учеников (и ожившая плашка-фильтр)
     вошли в SURFACES (public/ripple.js) — решение blox45 #8 «волна на бумажных поверхностях»
     не отменялось, а их в списке не было. position:relative чипам нужен как якорь .ripple-box
     (у .gchip и .oent он уже был по другим причинам — тут для полноты списка). */
  .btn,.btn-primary,.btn-pub,.btn-ghost,.btn-fb,.mgr-btn,.icon-btn,.hub-card,.gchip,.rchip,.oent-live{
    position:relative;-webkit-tap-highlight-color:transparent}
  /* цветные плашки ВНУТРИ чипа — счётчик «к проверке» и бейдж статуса на якорной заливке —
     поднимаются НАД волной (тот же приём, что у .chatdot: z-index:1, .ripple-box лежит на 0):
     чернильная волна умножением делала бы тёмную заливку почти чёрной на время касания. */
  .gchip .gn,.rchip .rrev{position:relative;z-index:1}
  /* ЧЕРНИЛА, А НЕ ПЛЁНКА (blox52 #8). Волна лежала на бумаге ПОВЕРХ неё — обычным
     полупрозрачным пятном, и на светлых поверхностях (плитка кабинета просила
     --accent, а в синих схемах это светлая Maya) читалась как блик стекла, а не как
     след от пальца. Теперь на бумаге она УМНОЖАЕТ: краска уходит В лист, зерно и тон
     бумаги видны сквозь неё, и осветлить бумагу волна больше не может физически.
     Режим держит роль --ripple-blend (theme.css): днём multiply, НОЧЬЮ normal — ночная
     бумага тёмная, а чернила светлые, и умножение светлым по тёмному не даёт ничего.
     Залитые роли (--primary/--accept) гасят режим у себя: у них волна белая, а
     умножение белым — это тождество, то есть волна исчезла бы совсем.
     z-index у коробки СНЯТ намеренно: z-index:0 создавал контекст наложения, и
     смешивание замыкалось внутри пустой коробки — режим стоял, а эффекта не было.
     Порядок отрисовки не изменился: коробка позиционирована и идёт последним ребёнком,
     а угловые индикаторы держат свой z-index:1 (см. .chatdot). */
  .ripple-box{position:absolute;inset:0;overflow:hidden;border-radius:inherit;
    pointer-events:none}
  .ripple{position:absolute;border-radius:50%;pointer-events:none;transform:scale(0);
    mix-blend-mode:var(--ripple-blend,normal);
    background:radial-gradient(circle,var(--ripple-ink,currentColor) 0%,
                               var(--ripple-ink,currentColor) 40%,transparent 72%);
    animation:ripple-grow .45s cubic-bezier(.2,.5,.4,1) forwards,
              ripple-fade .45s linear forwards}
  /* Рост и затухание — ДВЕ анимации с разными кривыми, и это не педантизм. Сначала обе
     ехали на общем --ease: он резко стартует (.32,.72,0,1) и обнулял прозрачность за
     первые ~40 мс — волна честно расходилась, но увидеть её было нельзя. Интерфейсная
     кривая описывает, как вещь ПРИЕЗЖАЕТ на место; затуханием она не управляет. */
  @keyframes ripple-grow{to{transform:scale(1)}}
  @keyframes ripple-fade{0%{opacity:.3}60%{opacity:.24}100%{opacity:0}}
  /* Кому анимации мешают: рост снимаем, отклик НЕ убираем — на тач это единственный
     признак, что нажатие засчитано. Остаётся короткая ровная вспышка. */
  @media (prefers-reduced-motion:reduce){
    .ripple{animation:ripple-flash .2s linear forwards}}
  @keyframes ripple-flash{from{transform:scale(1);opacity:.22}to{transform:scale(1);opacity:0}}

  .tchip{display:inline-block;font-size:var(--fs-2xs);font-weight:700;padding:2px 8px;border-radius:var(--r-lg);
    background:var(--draft-tint);color:var(--alert-ink);margin-left:6px}
  /* ЧЕЛЛЕНДЖ (blox15 #6) — золото на учительских плашках: та же плашка, тот же рельс, другой цвет */
  .tchip.gold{background:var(--gold-tint);color:var(--gold-ink);border:1px solid var(--gold-line)}
  .bitem.chal{background:var(--gold-tint);border-color:var(--gold-line)}
  .oent.chal{background:var(--gold-tint)}
  .chal-lbl{color:var(--gold-ink)}
  .chal-lbl svg{vertical-align:-2px}
  .field select,.field textarea{border:1px solid var(--line);border-radius:var(--r-sm);padding:10px 12px;
    font-size:var(--fs-input);width:100%;font-family:inherit}   /* 16px — тот же зум-заслон, см. .field input выше */
  .field textarea{min-height:70px;resize:vertical}
  /* blox28 #9 (пережило blox42): поле «Текст» выше обычного, ПОКА к таску не приложен файл (.tall
     теперь вешается по содержимому, не по виду) — учитель пишет развёрнутое задание. С файлом
     поле обычное: комментарий файлового таска короткий, высоту забирает панель файла.
     blox43 #1 — 210px на ПК съедали весь резерв середины: зона «С устройства» открывалась за
     нижним краем прокрутки (виден был только пунктирный верх). 110px хватает на абзац, а панель
     файла с зоной видна СРАЗУ; надо больше — поле тянется за уголок (resize:vertical). */
  .field textarea.tall{min-height:110px}
  /* blox42 — на ТЕЛЕФОНЕ панель файла стоит в середине ВСЕГДА (плиток вида нет). Резерв середины
     на коротком экране (667px) ≈160px: высокое поле 210px съедало его целиком — строка «link: …»
     обрезалась краем прокрутки, а панель файла вовсе не была видна (снимок 22.08). Поэтому «втрое
     выше» на телефоне превращается в «вчетверо строк»: 84px хватает на задание в несколько строк,
     а подпись «Файл» с кнопками источника выглядывает и зовёт прокрутить. */
  @media(max-width:859px){ .emodal .taskmid textarea.tall{min-height:84px} }
  /* blox40 #6 — с приложенным файлом поле «Текст» на телефоне ужимается до одной строки: «Файл»
     держит в резерве больше всех (карточка файла, кнопки источника, зона «с устройства»), и
     раздутый «Текст» съедал ровно ту высоту, где должна быть видна иконка приложенного файла.
     Поле растягивается вручную (resize:vertical). */
  @media(max-width:859px){ .emodal .taskmid:has(.afld-file) textarea:not(.tall){min-height:48px} }
  .students-pick{display:flex;flex-wrap:wrap;gap:8px}
  .students-pick label{display:flex;align-items:center;gap:6px;font-size:var(--fs-sm);
    border:1px solid var(--line);border-radius:var(--r-lg);padding:6px 12px;cursor:pointer;font-weight:600}
  /* blox34 #9 — в окне таска состав учеников СКРОЛЛИТСЯ внутри себя: большой класс иначе растил бы
     окно вниз (кнопка «Сохранить» уходила бы за край — самая переподаваемая беда проекта, G1v). */
  .emodal .students-pick{max-height:132px;overflow-y:auto}
  /* blox34 #9 — строка двух флажков таска: ЧЕЛЛЕНДЖ и ЛИЧНОЕ. На ПК «Личное» прижато к правому
     краю, на телефоне падает под челлендж отдельной строкой (flex-basis:100%), а не дублируется
     второй разметкой под медиазапрос. Каждый флажок с подписью — один неразрывный узел .chkgrp,
     чтобы при переносе галочка не оторвалась от своей подписи (lingo-ui #3). */
  .chkrow{flex-wrap:wrap;row-gap:8px}
  .chkgrp{display:flex;align-items:center;gap:8px}
  .chkgrp input:disabled + label{color:var(--faint);cursor:not-allowed}
  .chkrow .indiv-grp{margin-left:auto}
  .excl-hint{margin:-4px 0 10px}
  @media(max-width:859px){ .chkrow .indiv-grp{margin-left:0;flex-basis:100%} }
  optgroup{font-weight:700}

  /* student: full-width standing-playlist bar — the app's one deliberately loud object.
     Was a grape body with a thin rainbow edge; now the palette gradient IS the bar (blox13 #5).
     Text goes dark grape, never white: white on a light sky/veil/grapefruit fill is unreadable. */
  .poolbar{position:relative;display:flex;align-items:center;gap:14px;border-radius:var(--r-xl);
    padding:18px 20px;color:var(--ink-strong);background:var(--grad-playlist);overflow:hidden;
    box-shadow:var(--sh-3);cursor:pointer;margin-bottom:18px;
    transition:transform var(--t-fast) var(--ease),box-shadow var(--t) var(--ease)}
  .poolbar::after{content:"";position:absolute;right:-40px;top:-60px;width:180px;height:180px;
    border-radius:50%;background:radial-gradient(circle,rgba(255,255,255,.45),transparent 65%);pointer-events:none}
  @media (hover:hover){.poolbar:hover{box-shadow:var(--sh-4)}}
  .poolbar .pbtext{flex:1;min-width:0;position:relative}
  .poolbar .pbt{font-weight:800;font-size:var(--fs-xl);letter-spacing:var(--tracking-tight)}
  .poolbar .pbm{font-size:var(--fs-sm);color:var(--ink);font-weight:700;margin-top:3px}
  /* the weekly stat is a full sentence, so it reads as a quiet footnote — not a pill */
  .poolbar .pbweek{font-size:var(--fs-xs);font-weight:600;line-height:1.45;margin-top:10px;
    padding-top:9px;border-top:1px solid rgba(var(--sh-rgb),.22);color:var(--ink);
    max-width:32ch}
  /* кружок «играть» — тёмный, чтобы на пастельной радуге он был якорем, а не растворялся */
  .poolbar .pbplay{position:relative;width:56px;height:56px;border-radius:50%;background:var(--primary);
    color:#fff;display:flex;align-items:center;justify-content:center;font-size:var(--ic-lg);
    flex:0 0 auto;box-shadow:0 6px 18px rgba(var(--sh-rgb),.30);
    transition:transform var(--t) var(--ease)}
  @media (hover:hover){.poolbar:hover .pbplay{transform:scale(1.06)}}
  .poolbar:active{transform:scale(.99)}
  /* link / file button (review panel); teacher comment bubble */
  .linkbtn{display:block;text-align:center;text-decoration:none;border-radius:var(--r-md);padding:9px;
    font-size:var(--fs-md);font-weight:700;background:rgba(255,255,255,.92);color:var(--ink)}
  .comment{font-size:var(--fs-xs);font-weight:600;opacity:.95;background:rgba(255,255,255,.18);
    border-radius:var(--r-xs);padding:5px 8px}
  /* add-square type tiles (the popup) */
  /* blox28 (owner) — зазор между плитками ВИДА и полем «Текст» убран (был 14+10px): освобождённую
     высоту тратим на то, чтобы окно таска на телефоне не уезжало вниз. Отступ снизу теперь даёт
     только сама .field (10px). */
  .typepick{display:flex;gap:8px;margin-bottom:0;flex-wrap:wrap}
  .typepick button{flex:1;min-width:68px;border:1px solid var(--line);background:var(--card);border-radius:var(--r-md);
    padding:12px 6px;display:flex;flex-direction:column;align-items:center;gap:6px;cursor:pointer;
    font-weight:700;font-size:var(--fs-sm);color:var(--muted)}
  .typepick button.on{border-color:var(--accent-deep);background:var(--accent-tint);color:var(--accent-dark)}
  /* blox25 #10 — вид нельзя сменить (есть сдачи / личное / челлендж): плитки видны, но не жмутся */
  .typepick button:disabled{background:var(--fill-soft);border-color:var(--line-soft);
    color:var(--faint);cursor:default}
  /* выбранный вид остаётся выбранным, даже когда переключать уже нельзя: правило .on идёт
     выше и слабее, поэтому акцент проговаривается тут ещё раз */
  .typepick button:disabled.on{background:var(--accent-tint);border-color:var(--accent-deep);
    color:var(--accent-dark)}
  .typepick .ti{font-size:var(--ic-lg)}
  /* textbook source inside the entity modal: book carousel + exercise grid */
  .bk-car{display:flex;align-items:center;justify-content:center;gap:10px;margin:0 0 14px}
  .bk-lbl{font-weight:800;font-size:var(--fs-sm);color:var(--accent-dark);min-width:150px;text-align:center}
  .bk-car button{background:var(--accent-tint);border:1px solid var(--accent-line);color:var(--accent-dark);
    border-radius:var(--r-sm);padding:4px 11px;cursor:pointer;font-size:var(--fs-md);line-height:1}
  .bk-car button:disabled{color:var(--faint);background:var(--surface);cursor:default}
  .ex-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-bottom:14px}
  .ex-tile{position:relative;text-align:left;border:1px solid var(--line);background:var(--card);border-radius:var(--r-md);
    padding:10px 30px 10px 12px;cursor:pointer;display:flex;flex-direction:column;gap:3px;min-height:54px}
  @media (hover:hover){.ex-tile:hover{border-color:var(--accent-deep);background:var(--accent-tint)}}
  .ex-no{font-weight:800;font-size:var(--fs-sm);color:var(--accent-dark)}
  .ex-tt{font-size:var(--fs-sm);color:var(--ink);line-height:1.25;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .ex-aud{position:absolute;top:8px;right:10px;font-size:var(--fs-sm)}
  .src-ref{font-weight:700;font-size:var(--fs-sm);color:var(--accent-dark);background:var(--accent-tint);
    border-radius:var(--r-sm);padding:8px 11px;margin-bottom:12px}
  .manual-sw{width:100%}
  /* blox3: record-mode quick-insert buttons under a "Текст" field */
  /* record-mode single-choice status picker (builder) */
  /* blox43 #5 — телефон: сетка на 6 долей. Первые ЧЕТЫРЕ кнопки (After the audio · In headphones,
     Sing along · Watch, listen, read) идут парами — каждая по 3 доли, пара делит строку пополам;
     последние три (Read by yourself · In the copybook · Do) по 2 доли — тройка в одну строку.
     Порядок кнопок = порядок TASK_TYPES в core.js: nth-child считает ИМЕННО его. */
  .recpick{display:grid;grid-template-columns:repeat(6,1fr);gap:6px;margin-top:8px}
  .recb:nth-child(-n+4){grid-column:span 3}
  .recb:nth-child(n+5){grid-column:span 2}
  /* blox43 #7 — иконки в кнопках типов жирнее текста: --icw уходит внутрь <use> (см. icons.svg).
     blox52 #3, п.5: значение стало ПИКСЕЛЯМИ ЭКРАНА (штрих больше не сжимается вместе с
     картинкой). 1.6px — ровно та толщина, что рисовалась прежними 2.5 при подписи в 13px,
     то есть кнопки типов выглядят как выглядели, только теперь их толщина написана прямо. */
  .recb .ic{--icw:1.6px}
  /* blox41 #1 + blox42 #7 — пикер рисует ВСЕ типы всегда; недопустимые текущему виду СЕРЕЮТ, но
     остаются видны (решение владельца 22.08: «не пропадают, а сереют» — так видно, ЧТО именно
     станет доступно, если поменять содержимое). Место кнопка держит по-прежнему — соседи не
     съезжают, высота пикера одна при любом виде (заменило «призрак» blox34 #10). Для пальца и
     скринридера погашенной кнопки нет: recPicker вешает ей атрибут disabled. */
  /* filter:grayscale(1) снят как мёртвая строка: знаки внутри — наши контурные .ic,
     они и так рисуются currentColor, обесцвечивать в них нечего. */
  .recb.off{background:var(--fill-soft);border-color:var(--line-soft);color:var(--faint);
    pointer-events:none}
  .recb{border:1px solid var(--line);background:var(--card);border-radius:var(--r-sm);padding:6px 8px;
    cursor:pointer;font-size:var(--fs-sm);line-height:1.2;display:flex;align-items:center;justify-content:center;
    text-align:center;white-space:normal;gap:4px;font-family:inherit}
  @media (hover:hover){.recb:hover{border-color:var(--accent-deep);background:var(--accent-tint)}}
  .recb.on{border-color:var(--accent-dark);background:var(--accent-fill);font-weight:700;
    box-shadow:inset 0 0 0 1px var(--accent-dark)}
  /* blox43 #3 — на ПК окно таска шире, и ВСЕ СЕМЬ типов стоят одной строкой: семь равных колонок,
     длинная подпись переносится ВНУТРИ кнопки, а не роняет кнопку на вторую строку (blox15 #4). */
  @media(min-width:860px){
    .emodal .panel{max-width:840px}
    .recpick{grid-template-columns:repeat(7,1fr);gap:8px}
    .recpick .recb{grid-column:auto;padding:8px 6px}
    /* blox28 #5 — на ПК плитки ВИДА (Файл/Текст/Ссылка/Квиз) кладём иконку и подпись В ОДНУ строку
       и жмём по вертикали: высвобождает место, чтобы окно таска влезало в экран целиком. На узких
       экранах остаётся вертикальная раскладка (иконка над подписью) — там ширины на строку нет. */
    .typepick button{flex-direction:row;padding:9px 8px;gap:8px}
    .typepick .ti{font-size:var(--ic-md)}
  }
  /* blox29 #1 — окно таска держит НИЗ на месте при смене ВИДа МЕХАНИЧЕСКИ, без ручной подгонки высот.
     Раньше это делали ПЯТЬ взаимозависимых констант (поле 70/210, резерв 325/340, компенсация 185/200,
     проводник 224/170): меняешь одну — пересчитывай остальные руками, иначе низ окна прыгает, и это
     видно только по скриншоту (история: blox28 #5/#9/#14 — всё ручные перетюнинги этих чисел). Теперь:
     обёртка .taskmid с ФИКСИРОВАННЫМ минимальным резервом, а #a-fields — единственный растущий ребёнок
     (flex:1), который сам добирает/отдаёт разницу. Стало выше поле «Текст» — середина ужалась ровно на
     столько же; сумма (а значит и низ окна) неизменна САМА, без правки резерва. Резерв — ОДНО число на
     ширину (телефон/ПК), заданное под самый высокий вид «Файл»; всё прочее выводится из него. */
  .emodal{padding-top:22px;padding-bottom:22px}
  .emodal .panel .field{margin-bottom:10px}
  /* blox30 #1 — окно таска ВЛЕЗАЕТ в экран целиком, даже на коротком ноутбуке. Раньше .taskmid был
     ЖЁСТКИЕ 395px, и всё окно (≈725px) не помещалось: на экране ниже ~725px кнопка «Добавить» уходила
     за нижний край (см. Task.jpg — виден только «Челлендж», кнопки нет). Теперь панель таск-модалки
     (и добавление, и редактирование — обе несут .taskmid) ограничена высотой вьюпорта, а ужимается
     ТОЛЬКО средний резерв .taskmid: он отдаёт высоту, проводник Я.Диска внутри скроллится (overflow
     уже включён), а низ — тип таска, челлендж, «Добавить» — прибит и всегда виден. На высоком экране
     резерв держит свои 395px (flex-grow:0), так что механизм blox29 #1 (низ не прыгает при смене вида)
     сохранён. Крайний случай сверхкороткого экрана деградирует мягко: .emodal сам скроллится (overflow
     :auto), как и раньше. */
  .emodal .panel:has(.taskmid){display:flex;flex-direction:column;max-height:calc(100dvh - 44px)}
  .emodal .panel:has(.taskmid) > *{flex-shrink:0}                       /* низ окна не жмётся */
  .emodal .panel:has(.taskmid) > .taskmid{flex:0 1 395px;min-height:0}  /* ужимается только резерв */
  @media(max-width:859px){ .emodal .panel:has(.taskmid) > .taskmid{flex-basis:410px} }
  /* display:flex column — внутренняя раскладка резерва: поле «Текст» + растущий #a-fields (flex:1),
     который сам добирает/отдаёт разницу при смене вида; overflow:auto проводника даёт ему явный потолок. */
  /* blox34 #10 — резерв ещё и СКРОЛЛИТСЯ внутри себя. Раньше он просто сжимался, и пока снизу
     ничего не прибавлялось, этого хватало; но галочка «Личное» открывает список учеников (~130px),
     резерв уходил ниже высоты своего содержимого — и панель файла НАЕЗЖАЛА на пикер типа (G9).
     Теперь лишнее прячется в скролл середины: низ окна остаётся на месте, ничего не перекрывается. */
  .taskmid{display:flex;flex-direction:column;overflow-y:auto}
  /* #a-fields / #e-fields — растущая середина: flex:1 «съедает» слабину, поэтому низ окна не двигается.
     Внутри — тоже flex-колонка, чтобы вид «Файл» отдал проводнику всю доступную высоту (см. .afld-file).
     (flex-контейнер не схлопывает margin'ы детей — прежний display:flow-root больше не нужен, blox25 #8
     решается сам.) */
  .afields{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
  .afields > .field{margin-bottom:0}
  /* blox42 — строка-индикатор распознанной ссылки под полем «Текст»: «link: …». Место занято
     ВСЕГДА (visibility, не display) — появление/пропажа не двигает середину (lingo-ui §1);
     не переносится и не растит окно вширь: nowrap + ellipsis (решение владельца). */
  .linkline{visibility:hidden;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
    min-height:17px;font-size:var(--fs-sm);color:var(--accent-dark);margin-top:3px}
  .linkline.on{visibility:visible}
  /* вид «Файл»: поле-обёртка и панель проводника растягиваются на всю середину, поэтому карточка
     выбранного файла / сообщение «Забираем файл…» ужимают САМ проводник (он flex внутри, см.
     playlist.css), а не роняют низ окна вниз — поведение blox28 #14/#5 теперь СЛЕДСТВИЕ того же
     механизма, без отдельной пары высот 224/170. */
  .afld-file{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
  .afld-file > #a-fb, .afld-file > #e-fb{flex:1 1 auto;min-height:0}
  /* blox25 (фидбек) — переключатель ЧЕЛЛЕНДЖА шёл вплотную под кнопками ТИПА (особенно в окне
     редактирования, где нет резерва середины) и «придавливался» к ним. Даём пикеру типа отступ
     снизу — челлендж отделён от кнопок типа. Отступ одинаков для всех видов, высоты не рассинхронит. */
  .recwrap{margin-top:2px;margin-bottom:14px}

  /* modal overlay + bottom sheet (turn-in / review) */
  /* ЗАТЕМНЕНИЕ ПОД МОДАЛКОЙ — тон тени СВОЕЙ схемы (blox52 #4, п.6). Было зашито
     виноградным числом: в холодных схемах накладка тянула тёплый фиолетовый, а в ночной
     оставалась светлее нужного. */
  .overlay{position:fixed;inset:0;background:var(--scrim);display:flex;align-items:flex-end;
    justify-content:center;z-index:50}
  .sheet{background:var(--card);width:100%;max-width:520px;border-radius:var(--r-lg) var(--r-lg) 0 0;padding:18px;
    max-height:90vh;overflow:auto;box-shadow:var(--sheet-rise)}
  @media(min-width:560px){ .overlay{align-items:center} .sheet{border-radius:var(--r-lg)} }
  .sheethead{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;font-size:var(--fs-lg)}
  /* blox37 #1 — шторка «Сообщить о проблеме». Своей геометрии почти нет: .sheet уже держит
     max-height:90vh + overflow:auto, поэтому окно не перерастёт экран ни на телефоне, ни на ПК. */
  .supsheet .sheethead .x{width:44px;height:44px;border-radius:50%}   /* палец (WCAG 2.5.5) */
  .sup-where{color:var(--muted);font-size:var(--fs-sm);margin-bottom:8px}
  .sup-where b{color:var(--ink)}
  /* 16px — не вкус: ниже этого Safari на айфоне зумит страницу при фокусе и обратно не отматывает */
  .sup-text{width:100%;box-sizing:border-box;font:16px/1.4 inherit;color:var(--ink);
    border:1px solid var(--line);border-radius:var(--r-md);padding:10px 12px;resize:vertical;
    background:var(--surface-2)}
  .sup-text:focus{outline:none;border-color:var(--accent-deep)}
  .sup-err{color:var(--danger-ink);font-size:var(--fs-md);font-weight:700;margin-top:8px}
  .sup-ok{text-align:center;padding:22px 6px;font-size:var(--fs-lg);font-weight:700;color:var(--ink)}
  .sup-ok span{display:inline-block;margin-top:6px;font-weight:400;font-size:var(--fs-md);color:var(--muted)}
  /* PDF-просмотрщик (blox9 #5): почти во весь экран, свой ✕ и запасная ссылка «в браузере» */
  .overlay.pdfov{align-items:stretch;justify-content:stretch}
  .pdfbox{position:relative;background:var(--card);display:flex;flex-direction:column;width:100%;height:100%;padding:12px 12px 0}
  .pdfbox .sheethead{margin-bottom:8px}
  .pdf-head-r{display:flex;align-items:center;gap:12px}
  /* blox18 #5: обе кнопки были еле заметны — «в браузере» это теперь явная кнопка-таблетка,
     а ✕ — заметный круг с фоном и рамкой (крупная область нажатия) */
  /* blox55 #1: в PWA тот же элемент рисуется <button> («Сохранить ↓») — поэтому гасим кнопочные
     умолчания браузера (серая заливка, свой шрифт) и просим палец */
  .pdf-newtab{font-size:var(--fs-sm);font-weight:700;color:var(--accent-dark);text-decoration:none;
    border:1px solid var(--accent);border-radius:var(--r-pill);padding:8px 16px;
    background:none;font-family:inherit;cursor:pointer;justify-content:center;line-height:1.35;
    display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
  .pdf-newtab:disabled{opacity:.6;cursor:default}
  @media (hover:hover){.pdf-newtab:hover{background:var(--accent-tint)}}
  .pdf-newtab:active{transform:scale(.96)}
  /* blox55 #4: страниц у книги нет (не нарезались / ещё режутся) — показываем сам файл, но
     говорим почему и даём выход. Полоса стоит НАД рамкой в потоке, ничего не перекрывает
     (правило «ничто не висит над содержимым») и не растёт: рамка ужимается сама, flex:1 */
  .pdf-note{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px;
    padding:10px 12px;border:1px solid var(--line);border-radius:var(--r-md);
    background:var(--surface-2);color:var(--ink);font-size:var(--fs-sm)}
  .pdf-note span{flex:1 1 200px}
  .pdfframe{flex:1;width:100%;border:none;border-radius:var(--r-md) var(--r-md) 0 0;background:var(--surface)}
  .sheethead .x{border:none;background:none;font-size:var(--ic-lg);cursor:pointer;color:var(--muted)}
  .pdfbox .sheethead .x{width:40px;height:40px;border-radius:50%;border:1px solid var(--line);
    background:var(--surface-2);color:var(--ink);font-size:var(--ic-lg);line-height:1;flex:0 0 auto;
    display:flex;align-items:center;justify-content:center}
  @media (hover:hover){.pdfbox .sheethead .x:hover{background:var(--fill-soft);border-color:var(--accent-deep);color:var(--accent-dark)}}
  .pdfbox .sheethead .x:active{transform:scale(.94)}
  /* blox24 #11: перетаскиваемый аудио-плеер поверх страницы учебника (наслушка + чтение вместе).
     Абсолютная плашка внутри .pdfbox; тянется за верхнюю ручку. */
  .pdf-fplayer{position:absolute;z-index:5;width:min(340px,calc(100% - 12px));
    background:var(--card);border:1px solid var(--line);border-radius:var(--r-md);
    box-shadow:var(--sheet-3);overflow:hidden;
    display:flex;flex-direction:column}
  .pdf-fplayer.dragging{box-shadow:var(--sheet-4);opacity:.96}
  .pdf-fp-grip{display:flex;align-items:center;justify-content:space-between;gap:8px;
    padding:8px 10px 8px 14px;min-height:44px;
    background:var(--surface);border-bottom:1px solid var(--line-soft);
    color:var(--muted);font-size:var(--ic-sm);font-weight:700;cursor:grab;
    touch-action:none;user-select:none;-webkit-user-select:none}
  .pdf-fp-grip-l{display:inline-flex;align-items:center;gap:8px;min-width:0}
  /* ✕ на дальнем от заголовка краю шапки — меньше прежнего плеерного, вписан в свою строку (blox24 #11) */
  .pdf-fp-x{flex:0 0 auto;width:30px;height:30px;border-radius:50%;border:1px solid var(--line);
    background:var(--card);color:var(--muted);font-size:var(--ic-sm);line-height:1;cursor:pointer;
    display:flex;align-items:center;justify-content:center;touch-action:manipulation}
  .pdf-fp-x:active{background:var(--surface)}
  .pdf-fplayer.dragging .pdf-fp-grip{cursor:grabbing}
  .pdf-fp-body{padding:10px 12px 12px}
  .pdf-fp-body .lp{gap:8px}
  .sheet .btn-ghost,.sheet .btn-primary{width:100%;margin-top:6px}
  .sheet .toolbar .btn-ghost,.sheet .toolbar .btn-pub{width:auto;flex:1}

  /* ---- ДОСЬЕ (blox23 #4): приватная шторка учителя — журнал по датам + цветные ярлыки.
     Вставляем/убираем один узел (никогда не перестраиваем список целиком) — прокрутка шторки
     не прыгает. Поля ≥16px, иначе Safari зумит страницу (lingo-ui rule 3). */
  .dsheet{max-width:560px}
  .dprivate{font-size:var(--fs-sm);color:var(--muted);margin:-4px 0 12px}
  /* авто-сводка группы: НЕ журнал (пересчитывается при каждом открытии) — отделена рамкой+фоном */
  .dsummary:empty{display:none}
  .dsummary{border:1px solid var(--line-soft);border-radius:var(--r-md);background:var(--surface-2);
    padding:11px 13px;margin-bottom:14px}
  .dsum-head{font-size:var(--fs-xs);font-weight:700;color:var(--muted);margin-bottom:7px;
    display:flex;align-items:center;gap:5px}
  .dsum-verdict{font-size:var(--fs-md);margin-bottom:8px}
  .dsum-verdict.v-good{color:var(--ok-ink)}
  .dsum-verdict.v-mid{color:var(--gold-ink)}
  .dsum-verdict.v-poor{color:var(--alert-ink)}
  .dsum-verdict.v-progress{color:var(--ink)}   /* предстоящий урок «в процессе» — нейтрально, не тревожно */
  .dsum-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
  .dsum-list li{font-size:var(--fs-sm);color:var(--ink);line-height:1.35;
    padding-left:14px;position:relative;overflow-wrap:anywhere}
  .dsum-list li::before{content:'•';position:absolute;left:2px;color:var(--muted)}
  .dsum-dim{color:var(--muted)}
  .dsum-empty{font-size:var(--fs-sm);color:var(--muted)}
  .dtags{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:10px}
  .dtags:empty{display:none}
  .dtag{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-sm);font-weight:700;
    padding:4px 6px 4px 12px;border-radius:var(--r-pill);border:1px solid;overflow-wrap:anywhere}
  .dtag .dtag-x{border:none;background:none;cursor:pointer;font-size:var(--ic-sm);line-height:1;
    color:inherit;opacity:.55;padding:0 2px}
  @media (hover:hover){.dtag .dtag-x:hover{opacity:1}}
  .dtag.c0{background:var(--fill-soft);border-color:var(--line-mid);color:var(--ink)}
  .dtag.c1{background:var(--ok-tint);border-color:var(--ok-line);color:var(--ok-ink)}
  .dtag.c2{background:var(--gold-tint);border-color:var(--gold-line);color:var(--gold-ink)}
  .dtag.c3{background:var(--accent-tint);border-color:var(--accent-deep);color:var(--accent-dark)}
  .dtag.c4{background:var(--alert-tint);border-color:var(--alert-line);color:var(--alert-ink)}
  .dtagadd{display:flex;gap:8px;margin-bottom:16px}
  .dtagadd input{flex:1;min-width:0;font-size:var(--fs-input);font-family:inherit;
    border:1px solid var(--line);border-radius:var(--r-sm);padding:9px 12px;color:var(--ink)}
  .dtagadd input:focus{outline:none;border-color:var(--accent-deep)}
  .dtagadd .btn-ghost{width:auto;flex:0 0 auto;margin-top:0}
  .dnew{margin-bottom:16px}
  .dnew textarea{width:100%;box-sizing:border-box;font-size:var(--fs-input);font-family:inherit;
    min-height:80px;resize:vertical;border:1px solid var(--line);border-radius:var(--r-sm);
    padding:10px 12px;color:var(--ink);margin-bottom:8px}
  .dnew textarea:focus{outline:none;border-color:var(--accent-deep)}
  .dnew .btn-pub{width:100%}
  .djournal{display:flex;flex-direction:column;gap:10px}
  .dentry{border:1px solid var(--line-soft);border-radius:var(--r-md);padding:10px 12px;background:var(--surface-2)}
  .dwhen{display:flex;align-items:center;justify-content:space-between;
    font-size:var(--fs-xs);font-weight:700;color:var(--muted);margin-bottom:5px}
  .dentry-x{border:none;background:none;cursor:pointer;color:var(--muted);font-size:var(--ic-sm);
    line-height:1;padding:0 2px;opacity:.6}
  @media (hover:hover){.dentry-x:hover{opacity:1;color:var(--alert-ink)}}
  /* ЗАПИСЬ ДОСЬЕ — тоже читаемый текст (blox52 #6): учитель пишет её предложениями и потом
     перечитывает. Судья её не называл, но оставить её жирной значило бы завести ровно ту
     разноголосицу, которую эта пачка и убирает. */
  .dbody{font-size:var(--fs-md);line-height:1.45;color:var(--ink);white-space:pre-wrap;overflow-wrap:anywhere;
    font-weight:var(--fw-read);text-wrap:pretty}
  /* blox43 #11 — личка на широком экране (ПК/планшет): таски 60% слева, ВСТРОЕННОЕ досье ученика
     справа. Колонка досье липнет к верху и прокручивается ВНУТРИ себя (длинный журнал не растит
     страницу — lingo-ui rule 3: растущий ребёнок ограничен своей зоной прокрутки).
     Липнет НЕ к нулю, а под шапку (--hdr-h): плашка sticky и лежит выше по z-index, поэтому при
     top:10px верх досье («Досье · Имя», «Личные заметки…») уезжал ЗА неё и было видно только низ
     (владелец). По той же причине из высоты вычитается и высота шапки — иначе низ колонки уходил
     бы за нижний край экрана ровно на эти 66px. */
  @media(min-width:761px){
    .sr-split{display:flex;gap:18px;align-items:flex-start}
    .sr-split .sr-main{flex:3;min-width:0}
    .sr-split .sr-side{flex:2;min-width:0;position:sticky;top:calc(var(--hdr-h) + 10px);
      max-height:calc(100vh - var(--hdr-h) - 20px);overflow:auto;
      background:var(--card);border:1px solid var(--line-soft);border-radius:var(--r-md);
      box-shadow:var(--sheet-1);padding:14px 16px;box-sizing:border-box}
    .sr-side-h{display:flex;align-items:center;gap:6px;font-weight:800;margin-bottom:10px}
  }
  /* blox43 #11 — монитор: тумблер Notes/Tasks по центру ВЕРХА левой панели. Абсолютная позиция —
     одна и та же точка в обоих режимах и на любом варианте панели (лист/edge/досье), тумблер не
     прыгает при переключении. Центруем НЕ через transform: hover/active ставят свой transform
     (лифт -1px, press scale) и затирали бы translateX(-50%) — на наведении прыгало вправо на
     пол-ширины (владелец, blox43 #11). left/right:0 + width:max-content + margin auto центруют
     без transform.
     blox53 #6 — ЭТО БОЛЬШЕ НЕ ПИЛЮЛЯ. Была кнопка-капсула с одним словом (и слово говорило, куда
     переключит, — по нему нельзя было прочесть, где ты сейчас). Стало: два слова через слеш,
     первое приподнято, второе приспущено, под словом ТЕКУЩЕГО режима — та же полоска, что под
     выбранным чипом группы (.gind: 3px, --accent-ink). Ни рамки, ни заливки, ни скруглений. */
  .opane{position:relative}
  /* абсолют остался ТОЛЬКО у панели-края: шапки у неё нет, вставать тумблеру некуда (см. ниже
     padding-top:56px). На всех прочих панелях он живёт средней колонкой шапки — в потоке.
     top:12px — это padding самой панели, то есть РОВНО тот же уровень, с которого начинается
     шапка у панели-листа. Иначе переключение Notes на панели-крае (край → досье, у досье шапка
     есть) двигало бы тумблер на 6px по вертикали — ассерт «тумблер на месте ±2px» это и поймал. */
  .opane > .onotes{position:absolute;top:12px;left:0;right:0;margin-left:auto;margin-right:auto;
    width:max-content;max-width:calc(100% - 24px);z-index:2}
  .opane .onotes{
    display:flex;align-items:center;justify-content:center;gap:5px;
    min-height:44px;box-sizing:border-box;   /* G5: палец попадает по тумблеру целиком */
    border:none;background:none;box-shadow:none;padding:0 8px;cursor:pointer;
    font-family:inherit;font-size:var(--fs-sm);line-height:1.1}
  .onotes .onw{position:relative;font-weight:800;color:var(--muted);padding:0 2px 6px;
    transition:color var(--t) var(--ease)}
  /* зона нажатия КАЖДОГО слова — во всю высоту тумблера (44px), хотя само слово ростом со строку:
     без неё палец попадал бы в 20px текста (G5). Невидимая, только ловит клик; слова разведены
     слешем, поэтому зоны не перекрываются. */
  .onotes .onw::before{content:"";position:absolute;inset:-14px -5px}
  .onotes .onw.up{transform:translateY(-4px)}   /* первое слово приподнято */
  .onotes .onw.dn{transform:translateY(4px)}    /* второе приспущено */
  .onotes .onsl{color:var(--faint);font-weight:700}
  .onotes .onw.on{color:var(--ink)}
  /* подчёркивание активного слова — тот же голос, что у выбранной группы (blox52 #12) */
  .onotes .onw.on::after{content:"";position:absolute;left:0;right:0;bottom:0;height:3px;
    border-radius:var(--r-2xs);background:var(--accent-ink)}
  @media (hover:hover){.onotes .onw:not(.on):hover{color:var(--ink)}}
  .opane .onotes.busy{opacity:.55}   /* досье грузится, панель ещё прежняя — клик не «провалился» */
  /* Панель-КРАЙ — единственная без шапки, поэтому тумблеру негде стоять: на телефоне такая панель
     схлопывается до высоты своей строки (43px), и тумблер ложится ровно на текст «раньше
     занятий не было» — он полностью закрыт (владелец, blox47 #3). Резервируем ему строку сверху,
     ту самую роль, которую у остальных панелей играет .opane-h{min-height:44px}. `:has` — чтобы
     у края БЕЗ тумблера (правый, «край расписания») отступ не появлялся. */
  .opane.edge:has(.onotes){padding-top:56px}
  /* тумблер ростом 44px (touch-target, G5) должен ВЛЕЗАТЬ в шапку, иначе свисает на контент (G9);
     запас одинаковый у обеих панелей — их шапки остаются одной высоты */
  .opane-h{min-height:44px}
  /* ...и вот тут запас переставал быть одинаковым. box-sizing глобально border-box (theme.css),
     то есть min-height считается ПО ВНЕШНЕЙ кромке: шапка БЕЗ тумблера садилась ровно в 44px
     (внутри остаётся 37px — даты и подписи хватало), а шапка С тумблером не могла: сам тумблер
     ростом 44px, и к нему прибавлялись padding-bottom:6px и линейка 1px — итого 51px. С blox53 #6
     тумблер стоит В ПОТОКЕ шапки (раньше лежал абсолютом и высоту не менял), поэтому эти 7px
     достались только ЛЕВОЙ панели: на мониторе панели стоят двумя колонками, и ВСЁ содержимое
     левой — плашка плейлиста, линейка шапки, лист — опускалось на 7px ниже своего близнеца
     справа (замер по всем группам: расходились 26 из 30; совпадали лишь те, где справа стояла
     двухстрочная дата с подписью «выдано · сдач ещё нет» и добирала те же 51px).
     Резервируем строку тумблера у ОБЕИХ шапок — 44px содержимого ПЛЮС своя отбивка.
     Только на мониторе: на телефоне панели идут одна под другой (.otwo column-reverse), ровнять
     нечего, а тумблер там и так уходит на свою строку и делает шапку выше. */
  @media(min-width:761px){ .opane-h{min-height:calc(44px + 6px + 1px)} }
  .opane.notes .opane-body{display:block}   /* досье — колонкой, а не рядом «таски+ростер» */
  .reczone{display:flex;flex-direction:column;align-items:center;gap:6px;padding:6px 0}
  .recbtn{border:none;border-radius:var(--r-md);padding:12px 20px;font-weight:700;font-size:var(--fs-md);cursor:pointer;
    background:var(--alert-tint);color:var(--alert-ink)}
  .recbtn.on{background:var(--alert-strong);color:var(--on-dark)}
  .reviewbody{margin-bottom:12px;display:flex;flex-direction:column;gap:10px}
  .cell.review{cursor:pointer;outline:2px solid rgba(73,54,87,.14)}
  /* teacher HOLLOW cell: white fill, TYPE-colour border, green corner badge (dot -> check) */
  .cell.hollow{background:var(--card);border:2px solid var(--line);position:relative;color:var(--ink)}
  .cell.hollow .ccount{opacity:.75;font-size:var(--fs-sm)}
  /* Угловой значок. Состояния сдачи/открытия он больше НЕ показывает (2026-08-17): в личке это
     дублировало чипы, которые говорят то же словами. Остался один носитель — «снят» (.ret). */
  .bdg{position:absolute;right:-5px;bottom:-5px;min-width:18px;height:18px;border-radius:var(--r-xs);
    background:var(--ok-tint);color:var(--green);font-size:var(--fs-xs);line-height:18px;text-align:center;
    font-weight:800;box-shadow:0 1px 3px rgba(var(--sh-rgb),.28);padding:0 2px}
  /* «снят» — ФАКТ, не тревога (blox52 #11): тот же тинтовый рецепт, что у близнеца .oarch
     на мониторе; сплошная красная заливка ушла к настоящим тревогам (реестр RED_OK). */
  .bdg.ret{background:var(--alert-fill);color:var(--alert-ink)}
  /* clickable student name -> opens that kid's individual homework screen */
  td.name.slink{cursor:pointer}
  @media (hover:hover){td.name.slink:hover{color:var(--accent-dark)}}
  td.name .schev{color:var(--muted);font-weight:700}

  /* teacher: one kid's homework — review cards (white, TYPE-colour left stripe) */
  .srlist{display:flex;flex-direction:column;gap:10px}
  .srcard{border-radius:var(--r-md);padding:14px 16px 14px 25px}
  /* ПРИНЯТО = ЗЕЛЁНОЕ, как у близнеца (blox52 #15). Стояло opacity:.62 — принятая работа
     выглядела ВЫКЛЮЧЕННОЙ, при том что тот же предмет у ученика (.rect.done) зелёный: успех
     не бывает полупрозрачным. Рецепт близнеца целиком: селадоновая заливка + зелёный рельс,
     тень оседает до обрезного края — карточка тихо отступает, но остаётся листом. Замер
     контраста на --ok-tint (06.09.2026): --ink 10.9/12.3/10.2 · --muted 6.1/5.6/4.9
     (база/синие/ночь) — AA держится; при opacity:.62 всё это глушилось разом.
     Реестр GREEN_OK (tools/check.js) стережёт СПЛОШНОЙ зелёный (--ok-solid/--grad-accept
     заливкой); тинт состояния --ok-tint — вне реестра сознательно, как и сам .rect.done. */
  .srcard.acc{background:var(--ok-tint);--rail-eff:var(--ok-line);box-shadow:var(--sheet-edge)}
  .srcard.acc .srtitle{color:var(--muted)}
  .srcard .srtitle{font-weight:800;font-size:var(--fs-md);padding-right:28px;overflow-wrap:anywhere}
  .srcard .srsub{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:4px;
    font-size:var(--fs-xs);font-weight:700;color:var(--muted)}
  .srcard .srstatus{font-size:var(--fs-sm);color:var(--muted);font-weight:600;margin-top:5px;
    display:flex;flex-wrap:wrap;gap:6px;align-items:center}
  /* blox30 #2 — индикаторы таска в личке: каждый факт своей плашкой, все видны одновременно
     (открыл · прослушал N · статус сдачи). Цвет = смысл (роль-токены, не сырые лестницы). */
  .srind{display:inline-flex;align-items:center;gap:4px;font-size:var(--fs-xs);font-weight:700;
    padding:2px 9px;border-radius:var(--r-lg);border:1px solid var(--line);
    background:var(--surface);color:var(--muted-strong);white-space:nowrap}
  .srind svg{width:13px;height:13px}
  .srind.op{background:var(--accent-tint);color:var(--accent-ink);border-color:var(--accent-line)}
  .srind.sok{background:var(--ok-tint);color:var(--ok-ink);border-color:var(--ok-line)}
  .srind.spend{background:var(--alert-tint);color:var(--alert-ink);border-color:var(--alert-line)}
  .srind.sret{background:var(--draft-tint);color:var(--draft-ink);border-color:var(--draft-line)}
  /* Два «не»-состояния — красноватые, но приглушённые: это не тревога, требующая действия
     (та живёт в --alert-tint у «на проверке»), а отсутствие события. Поэтому обе плашки
     полупрозрачные, а «не открывал» (ребёнок даже не заглянул) насыщеннее «не сдавал»
     (заглянул, но не прислал) — фоном на ступень темнее и меньшей прозрачностью. */
  .srind.no{background:var(--alert-fill);color:var(--alert-ink);border-color:var(--alert-line);opacity:.85}
  .srind.sno{background:var(--alert-tint);color:var(--alert-ink);border-color:var(--alert-line);opacity:.65}
  .srcard .srattach{margin:10px 0}
  /* blox26 #3 — инлайновые стили img/audio/video убраны: сдача теперь рендерится сущностью .fileent
     (см. выше), а не голым медиа в потоке. */
  .srcard .srnote{font-size:var(--fs-sm);background:var(--surface);border-radius:var(--r-xs);padding:6px 10px;margin-top:8px}
  /* prior (pre-edit) submission version, kept for old+new review */
  .srcard .srhist{margin-top:10px;border:1px dashed var(--line);border-radius:var(--r-sm);padding:8px 10px;background:var(--surface-2)}
  .srcard .srhist-h{font-size:var(--fs-xs);font-weight:700;color:var(--faint);margin-bottom:6px}
  .srcard .srhist-item{margin-bottom:6px}
  /* ПОДДЕЛЬНОГО КУРСИВА НЕТ (blox52 #3, п.3). Здесь стоял font-style:italic, но у Onest курсивного
     начертания не существует (см. fonts/onest.css — оба @font-face объявлены font-style:normal),
     поэтому браузер рисовал ОБЛИК: наклонял обычные буквы сдвигом. Это типографский брак —
     у настоящего курсива другие формы букв, а не наклон. Смысл «это не сдача, а самоотметка»
     несут те же два признака, что и раньше и без наклона: кегль на ступень мельче и приглушённый
     тон. */
  .srcard .srmuted{font-size:var(--fs-sm);color:var(--faint)}
  .srcard .srlink{display:inline-block;background:var(--accent-tint);border:1px solid var(--line);color:var(--accent-dark);
    text-decoration:none;border-radius:var(--r-sm);padding:8px 14px;font-weight:700;font-size:var(--fs-sm)}
  .srcard .sractions{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
  .srcard .sractions .btn,.srcard .sractions .btn-ghost{flex:0 0 auto}
  .srcard textarea{border:1px solid var(--line);border-radius:var(--r-sm);padding:9px 12px;font-size:var(--fs-input);
    width:100%;font-family:inherit;min-height:54px}
  .srcard .bdg{top:12px;right:12px;bottom:auto;left:auto}
  .locknote{background:var(--alert-tint);border:1px solid var(--alert-line);border-radius:var(--r-md);padding:11px 14px;
    font-size:var(--fs-sm);color:var(--alert-ink);margin:6px 0 12px;font-weight:600}

  /* group chat */
  .msgs{display:flex;flex-direction:column;gap:8px;min-height:240px;max-height:60vh;overflow:auto;
    background:var(--card);border-radius:var(--r-md);padding:12px;box-shadow:var(--sheet-1);margin-bottom:10px}
  /* blox52 #16 п.5 — пустой чат без бумажной коробки: скин снят, ГАБАРИТЫ прежние (min-height
     остаётся), чтобы строка ввода не сдвинулась при появлении первого сообщения. Ставится/
     снимается в paintFeed (chat-feed.js) — общий для всех трёх чатов. */
  .msgs.blank{background:none;box-shadow:none}
  /* записка пустого чата: тихая строка слева, НЕ .empty (та — роль «ожидание», по центру).
     Тон --on-bg, а не --faint: без коробки записка лежит на СТОЛЕ, где --faint даёт 1.9:1
     и исчезает (замер blox51 #2 — любой текст вне карточки берёт --on-bg). */
  .msgs .chat-note{color:var(--on-bg);font-size:var(--fs-md);padding:2px 2px;text-align:left}
  .msg-row{display:flex}
  .msg-row.mine{justify-content:flex-end}
  .bubble{max-width:80%;background:var(--surface);border-radius:var(--r-md);padding:8px 12px}
  .msg-row.mine .bubble{background:var(--accent-fill)}
  .bubble .who{font-size:var(--fs-2xs);font-weight:800;color:var(--accent-dark);margin-bottom:2px;word-break:break-word}
  /* СООБЩЕНИЕ В ЧАТЕ — книжный вес (blox52 #6). Правило одно на все три ленты (детский чат,
     родительский канал, учительская): пузырь рисует общий bubbleHTML из chat-feed.js.
     Ник .who рядом остаётся 800 — он подпись, а не речь. Переносов нет: в детском чате пишут
     по-английски (это правило группы), русский словарь резал бы чужие слова. */
  .bubble .txt{font-size:var(--fs-md);white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;
    font-weight:var(--fw-read);text-wrap:pretty}
  .bubble .time{font-size:var(--fs-2xs);color:var(--muted);margin-top:3px;text-align:right}
  /* blox40 #1 — «English only, my friend 😉»: ответ на заблокированную кириллицу. Место под строку
     держится ВСЕГДА (visibility), чтобы появление подсказки не сдвигало ленту и строку ввода. */
  .chat-eo{visibility:hidden;opacity:0;min-height:18px;margin:0 0 6px;padding:0 4px;
    font-size:var(--fs-sm);font-weight:700;color:var(--accent-dark);
    transition:opacity var(--t) var(--ease)}
  .chat-eo.on{visibility:visible;opacity:1}
  .chatbar{display:flex;gap:8px}
  /* страница Chats (blox32 #2): у встроенной панели родит.-канала первый .dprivate без верх. отступа */
  .chats-page>.dprivate:first-child{margin-top:0}
  /* min-width:0 обязателен: без него поле в flex-строке не сжимается уже своей «родной» ширины
     и распирает строку за правый край экрана (blox11 #3) */
  .chatbar input{flex:1;min-width:0;border:1px solid var(--line);border-radius:var(--r-md);padding:11px 14px;font-size:var(--fs-md)}
  /* width:auto перебивает «.sheet .btn-primary{width:100%}» (строка 724): в шторке родит.-чата
     эта кнопка «отправить» иначе растягивается на всю строку и давит поле ввода (blox23 #3 fix) */
  .chatbar .btn-primary{padding:11px 16px;flex:0 0 auto;width:auto}

  /* blox32 #2 — кружок-счётчик непрочитанных в углу кнопки/таба. ОДИН класс на все места (кнопка
     «Chats» учителя, «Чат группы» ученика, табы страницы Chats). pointer-events:none — клик проходит
     сквозь кружок на кнопку. Кольцо --card отделяет от фона кнопки.
     blox52 #11: заливка — якорь схемы, не красный. Непрочитанное сообщение — очередь («есть что
     прочитать»), а не провал; красный счётчик был одной из «семи заливок пожара» на мониторе. */
  /* z-index:1 — индикатор поверх волны касания (.ripple-box лежит на 0) */
  .chatdot{position:absolute;z-index:1;top:-7px;right:-7px;min-width:17px;height:17px;padding:0 4px;
    border-radius:var(--r-sm);background:var(--anchor);color:var(--on-dark);font-size:var(--fs-2xs);font-weight:800;
    line-height:17px;text-align:center;box-shadow:0 0 0 2px var(--card);pointer-events:none}
  #ochat,#schat,#oannc,.chats-tabs button{position:relative}
  /* blox35 #1 — тортик в том же углу, что и счётчик непрочитанных. Ни заливки, ни белого кольца:
     иконка цветная сама, а кружок-подложка (нужный цифре на красном) только сжимал бы рисунок —
     на 17px свечки в нём превращались в штрихи. Значок висит прямо на углу кнопки. */
  .chatdot--cake{background:none;box-shadow:none;min-width:0;width:auto;height:auto;padding:0;
    top:-9px;right:-7px;line-height:0}
  .chatdot--cake .ic{width:20px;height:20px}
  /* системный 🎂 (техника Apple) вместо нашей иконки: он ТЕКСТ, поэтому размер задаётся кеглем,
     а не width/height. Кегли подобраны так, чтобы оба варианта занимали одинаковое место. */
  .chatdot--cake .cakemoji{font-size:var(--ic-md);line-height:1;display:block}
  .nday .ncake .cakemoji{font-size:var(--ic-sm);line-height:1;display:block}
  .chats-h{margin-bottom:12px}
  /* детский чат-таб — радужный (как playlist), всегда, чтобы не промахнуться мимо взрослых тредов */
  .chats-tabs button.tab-rainbow,.chats-tabs button.tab-rainbow.on{
    background:var(--grad-playlist);color:var(--ink-strong)}

  /* ===== PARENT CHANNEL (blox23 #3) — poll cards + teacher's reschedule form ===== */
  .pcsheet{max-width:560px}
  .pc-tools{margin:-4px 0 10px}
  .pc-tools .btn-ghost{width:100%}
  .pc-pollform{margin-top:10px;padding:12px;background:var(--surface);border-radius:var(--r-md)}
  .pc-pollform .mgr-field{margin-bottom:8px}
  .pc-pollform select,.pc-pollform textarea{width:100%;font-size:var(--fs-input);border:1px solid var(--line);
    border-radius:var(--r-sm);padding:9px 11px;box-sizing:border-box}
  .pc-pollform textarea{min-height:60px;resize:vertical}
  .pc-lbl{display:block;font-size:var(--fs-sm);font-weight:700;color:var(--muted);margin:6px 0 4px}
  .pc-optrow{display:flex;gap:6px;align-items:center;margin-bottom:6px}
  .pc-optrow input{font-size:var(--fs-input);border:1px solid var(--line);border-radius:var(--r-sm);padding:8px 10px;min-width:0}
  .pc-optrow .pc-od{flex:1}
  .pc-oremove{border:none;background:none;color:var(--muted);font-size:var(--ic-md);cursor:pointer;flex:0 0 auto}
  #pc-addopt{margin:2px 0 10px}
  #pc-poll-send{width:100%}
  /* a poll rendered inside a message bubble */
  .pc-poll{margin-top:6px;display:flex;flex-direction:column;gap:6px}
  .pc-poll-h{font-size:var(--fs-sm);font-weight:800;color:var(--accent-dark)}
  .pc-opt{display:flex;align-items:center;gap:8px;text-align:left;width:100%;
    border:1px solid var(--line);background:var(--card);border-radius:var(--r-sm);padding:8px 10px;cursor:pointer}
  /* .9 была почти незаметной и ничего не сообщала: голос уже отдан, и об этом говорят
     .mine/.approved. Остался только курсор. */
  .pc-opt:disabled{cursor:default}
  .pc-opt.mine{border-color:var(--accent-deep);background:var(--accent-tint)}
  .pc-opt.approved{border-color:var(--ok-line);background:var(--ok-tint)}
  .pc-when{font-weight:700;font-size:var(--fs-sm);flex:0 0 auto}
  /* blox45 #17 — метка утверждённого варианта: точка вместо ✅. Цвет ей задаём тут, иначе она взяла
     бы цвет текста кнопки и «утверждено» перестало бы читаться с одного взгляда. */
  .pc-opt.approved .pc-when .ic{color:var(--ok-ink)}
  .pc-count{font-weight:800;color:var(--accent-dark);min-width:20px;text-align:center;flex:0 0 auto}
  .pc-voters{font-size:var(--fs-2xs);color:var(--muted);overflow-wrap:anywhere;flex:1}
  .pc-hint{font-size:var(--fs-2xs);color:var(--muted)}
  .pc-approved{font-size:var(--fs-xs);font-weight:700;color:var(--ok-ink)}
  /* PIN gate + methodologist «Доступы» */
  .pingate{max-width:360px}
  .pingate-in{width:100%;font-size:var(--fs-xl);letter-spacing:.3em;text-align:center;box-sizing:border-box;
    border:1px solid var(--line);border-radius:var(--r-md);padding:12px;margin:4px 0 8px}
  .pg-err{color:var(--alert-ink);font-size:var(--fs-sm);font-weight:600;margin-bottom:8px}
  .pingate .btn-pub{width:100%}
  .access-list{display:grid;grid-template-columns:1fr;gap:8px;margin-top:12px}
  @media(min-width:761px){ .access-list{grid-template-columns:1fr 1fr} }  /* группы в две колонки на ПК (blox34 #5) */
  .access-row{display:flex;align-items:center;justify-content:space-between;gap:12px;
    border:1px solid var(--line);border-radius:var(--r-md);padding:12px 14px;background:var(--card)}
  .access-row .ar-name{font-weight:700}
  .access-row .ar-par{font-size:var(--fs-sm);color:var(--muted)}
  .access-row .ar-pin{font-size:var(--fs-xl);font-weight:800;letter-spacing:.12em;color:var(--accent-dark)}
  /* blox42 #4: PIN + «Перевыпустить» едут вместе, не разрываясь при переносе строки (§3) */
  .access-row .ar-right{display:flex;align-items:center;gap:10px;flex-wrap:nowrap}

  /* ===== TEACHER PC LAYOUT ("big terminal") ===== */
  body.teacher main{max-width:1400px}
  /* group terminal: wide grid on the left, sticky review panel on the right */
  .term{display:flex;gap:18px;align-items:flex-start}
  .term-main{flex:1;min-width:0}
  .term-head{display:flex;align-items:flex-start;justify-content:space-between;
    gap:16px;flex-wrap:wrap;margin-bottom:12px}
  .term-head h2{font-size:var(--fs-2xl)}
  .term-head .toolbar{margin:0}
  .summary{background:var(--card);border-radius:var(--r-md);box-shadow:var(--sheet-1);
    padding:14px 16px;margin-bottom:16px}
  .summary .sline{display:flex;align-items:center;gap:14px;flex-wrap:wrap;font-size:var(--fs-md)}
  /* второй счётчик лички — просмотр тасков без сдачи. Свой слой информации, поэтому отдельная
     строка и приглушённый вес: работа учителя — верхняя строка (сдачи), эта только сообщает. */
  .summary .sline2{margin-top:6px;color:var(--muted);font-size:var(--fs-sm)}
  .summary .bar{flex:1;min-width:140px;height:10px;border-radius:var(--r-xs);background:var(--accent-tint);overflow:hidden}
  .summary .bar span{display:block;height:100%;background:var(--green);transition:width .3s}
  .summary .muted{color:var(--muted);font-weight:700}
  .summary .pend{color:var(--alert-ink);font-weight:700}
  /* bigger, more readable grid on a PC */
  body.teacher .cell{width:74px;height:54px;font-size:var(--fs-md)}
  .cell.sel{outline:2px solid var(--accent-dark)}
  /* review side panel */
  .term-side{flex:0 0 350px;width:350px;background:var(--card);border-radius:var(--r-md);
    box-shadow:var(--sheet-2);padding:18px;position:sticky;top:18px}
  .term-side h3{margin:0 0 4px;font-size:var(--fs-lg)}
  .term-side .rsub{color:var(--muted);font-size:var(--fs-sm);margin-bottom:12px}
  .rev-empty{color:var(--muted);font-size:var(--fs-md);text-align:center;padding:36px 6px;line-height:1.5}
  .rev-attach{display:flex;flex-direction:column;gap:10px;margin-bottom:12px}
  .rev-attach img{max-width:100%;border-radius:var(--r-sm)}
  .rev-attach audio{width:100%}
  .rev-next{font-size:var(--fs-sm);color:var(--accent-dark);font-weight:700;margin-top:10px;text-align:center}
  @media(max-width:900px){
    .term{flex-direction:column}
    .term-side{width:100%;flex:1 1 auto;position:static}
  }

  /* лист точного переноса урока (openMoveSheet): строка «в HH:MM · N мин» */
  .sched-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px}
  .sched-line label{display:flex;align-items:center;gap:6px;color:var(--muted-strong)}

  /* Лу — кабинет методиста: hub + группы/ученики (blox7) */
  /* «Назад» = вариант B (контур), edition для СВЕТЛОГО фона (чат, кабинет). На тёмной
     шапке .hdr-back .mback ниже перекрывает цвета для полупрозрачно-белого контура. */
  .mback{display:inline-flex;align-items:center;gap:5px;max-width:100%;box-sizing:border-box;
    background:none;border:1px solid var(--line);border-radius:var(--r-md);
    color:var(--muted-strong);font:inherit;font-weight:700;cursor:pointer;padding:6px 12px;margin-bottom:6px;
    transition:border-color var(--t) var(--ease),background var(--t) var(--ease),color var(--t) var(--ease)}
  @media (hover:hover){.mback:hover{border-color:var(--accent-deep);background:var(--accent-tint);color:var(--accent-ink)}}
  .mback .mlabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
  .chev{font-size:1.5em;line-height:1}   /* стрелка «‹» в 1,5 раза крупнее подписи */
  /* blox52 #16 п.2: на мониторе пять карточек тянулись во всю ширину — телефонный рулон,
     глаз проходил ~1000px пустой бумаги в каждой. Сетка: колонка не уже 360px, сколько
     влезет (на 1200 — две), общий предел 900px по центру. min(360px,100%) — чтобы на
     экранах уже 360px дорожка не распирала страницу. На 375 раскладка прежняя: одна
     колонка, вертикальный зазор те же 12px (14px — только МЕЖДУ колонками). */
  .hub-wrap{max-width:900px;margin:0 auto}
  .hub-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(360px,100%),1fr));
    gap:12px 14px;margin-top:16px}
  /* Отпускание — пружина с перелётом (--ease-spring), нажатие — сухое и быстрое
     (--t-press). Разные кривые на вдавливание и на возврат: это и даёт ощущение
     живой поверхности, а не переключателя. overflow:hidden — рамка для волны
     касания, иначе она вылезет за скруглённые углы. */
  .hub-card{display:grid;grid-template-columns:auto 1fr;column-gap:16px;align-items:start;
    /* РАМКА ПРОЗРАЧНАЯ, А НЕ СНЯТАЯ (blox52 #4, п.1): в покое край плитки задают обрезной
       край и контраст к столу, рамка тут была вторым объявлением того же. Но убрать её
       совсем нельзя — на наведении она красится акцентом и это главный признак ховера;
       прозрачная держит место, и плитка не дёргается на 1px под курсором. */
    width:100%;text-align:left;background:var(--card);border:1px solid transparent;
    border-radius:var(--r-xl);padding:18px 22px;cursor:pointer;box-shadow:var(--sheet-1);
    position:relative;overflow:hidden;-webkit-tap-highlight-color:transparent;
    /* blox52 #8: голубой ОСТАЛСЯ, но взят --accent-deep, а не --accent. В синих схемах
       --accent — светлая Maya под заливки: волна ею была светлее самой плитки, то есть
       ровно тем «светлым пятном поверх», на которое указал судья. --accent-deep — тот же
       фирменный цвет чернилами; вместе с multiply он читается впитавшимся в лист.
       В базовой и ночной схемах эти два токена равны — там вид не изменился. */
    --ripple-ink:var(--accent-deep);
    transition:border-color var(--t) var(--ease),box-shadow var(--t) var(--ease),
               transform var(--t-spring) var(--ease-spring)}
  .hub-card:active{transform:scale(.98);transition-duration:var(--t-press);transition-timing-function:ease-out}
  @media (hover:hover){.hub-card:hover{border-color:var(--accent-deep);box-shadow:var(--sheet-2)}}
  @media (prefers-reduced-motion:reduce){
    .hub-card{transition:border-color var(--t) var(--ease),box-shadow var(--t) var(--ease)}
    .hub-card:active{transform:none}
  }
  .hub-ic{grid-row:1/span 2;font-size:var(--fs-xl);color:var(--accent-deep);line-height:0;display:flex;
    align-items:center;justify-content:center;width:44px;height:44px;border-radius:var(--r-lg);
    background:var(--surface)}
  /* blox51 #4: заголовок плитки — ЯКОРЕМ схемы, а не чернилами. Шапка над хабом
     баклажановая, а пять заголовков под ней были плоско-чёрные: экран не выглядел
     собранным из одного материала. Якорь тёмный в КАЖДОЙ схеме (баклажан · графит ·
     синий), на бумаге даёт 10:1 — то есть это не «цветной» заголовок, а тот же
     тёмный вес, только в тон шапке. Описание .hub-d остаётся приглушённым. */
  .hub-t{font-weight:800;font-size:var(--fs-lg);letter-spacing:var(--tracking-tight);color:var(--anchor-ink);
    text-wrap:balance}
  /* ОПИСАНИЕ ПЛИТКИ — ЧИТАЮТ, А НЕ НАЖИМАЮТ (blox52 #6). Пять описаний хаба — русские фразы
     из manage.js, единственный текст этого экрана, который читают предложениями; поэтому им и
     книжный вес, и переносы (язык здесь заведомо русский). Заголовок плитки рядом остаётся 800:
     разница между «читают» и «нажимают» и есть весь смысл этой правки. */
  .hub-d{color:var(--muted);font-size:var(--fs-sm);margin-top:4px;line-height:1.5;
    font-weight:var(--fw-read);text-wrap:pretty;hyphens:auto}
  .mgr-toolbar{display:flex;flex-wrap:wrap;gap:10px;margin:12px 0 8px}
  /* .mgr-btn → --ghost, .mgr-btn.primary → --fill, .mgr-btn.warn → --danger (BUTTON ENGINE).
     Was radius 9 / pad 8·14 / font:inherit — now unified via the engine; .mgr-top3 below
     still lays the three top buttons out full-width in a 3-column grid. */
  /* верхние кнопки экрана «Группы и ученики» — одной ширины (blox16 #1). Кнопок стало четыре
     («Перевести группы на год вперёд»), поэтому колонки не фиксированные, а auto-fit: ПК — один
     ряд, планшет — 2×2, телефон — столбиком. Ширина в ряду остаётся равной, как и требовал #1. */
  .mgr-top3{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px}
  .mgr-top3 .mgr-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%}
  @media(max-width:760px){ .mgr-top3{grid-template-columns:1fr} }
  .dept-block{margin:16px 0}
  .dept-head{font-size:var(--fs-lg);font-weight:800;color:var(--accent-ink);border-bottom:2px solid var(--accent-line);
    padding-bottom:4px}
  .pot-row{margin:10px 0}
  .pot-label{font-size:var(--fs-sm);color:var(--muted);font-weight:600;margin-bottom:5px}
  .grp-chips{display:flex;flex-wrap:wrap;gap:8px}
  .grp-chip{text-align:left;background:var(--card);border:1px solid var(--line);border-radius:var(--r-sm);
    padding:9px 13px;cursor:pointer;min-width:120px}
  @media (hover:hover){.grp-chip:hover{border-color:var(--accent-deep)}}
  .grp-chip .gc-n{font-weight:700}
  .grp-chip .gc-c{font-size:var(--fs-xs);color:var(--muted);margin-top:2px}
  .dept-empty{color:var(--faint);font-size:var(--fs-sm);margin-top:6px}
  .mgr-panel{background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--r-md);padding:16px;margin:12px 0}
  .mgr-panel h3{margin:0 0 10px;font-size:var(--fs-md)}
  .mgr-field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px;max-width:340px}
  .mgr-field label{font-size:var(--fs-sm);color:var(--muted-strong)}
  .mgr-field input,.mgr-field select{padding:7px 9px;border:1px solid var(--line-mid);border-radius:var(--r-xs);font:inherit}
  /* blox52 #18 — виноватое поле. Строка ошибки говорит ЧТО, рамка показывает ГДЕ: на форме
     учителя (имя + ник + почта) одного текста мало. Толщина берётся из лестницы линий, цвет —
     роль --red (та же, что у текста .msg.err), гаснет вместе со строкой. */
  input.fbad,select.fbad,textarea.fbad{border-color:var(--red);box-shadow:0 0 0 2px var(--alert-tint)}
  /* заполненные данные ребёнка — компактная сводка вместо формы (blox22 #4) */
  .pf-read{display:flex;flex-direction:column;gap:6px;font-size:var(--fs-md);color:var(--ink);margin-bottom:4px}
  .pf-read .pf-lbl{display:inline-block;min-width:120px;color:var(--muted);font-size:var(--fs-sm)}
  /* ФИО + дата рождения: в один ряд на ПК, столбиком на телефоне (blox15 #1) */
  .fio-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:0 12px;max-width:760px}
  .fio-row .mgr-field{max-width:none}
  .mg-plonly{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;align-items:center;
    max-width:620px;margin:0 0 12px;padding:11px 13px;background:var(--card);
    border:1px solid var(--line-soft);border-radius:var(--r-sm);cursor:pointer}
  /* 24px — линия WCAG 2.5.8 для пальца, её же держит правило G5 гео-скана. Промах по этой
     галочке не безобиден: она переключает группе режим выдачи ДЗ (blox37 #8). */
  .mg-plonly input{grid-row:1 / span 2;width:24px;height:24px;accent-color:var(--primary);cursor:pointer}
  .mg-plonly-t{font-weight:700;color:var(--ink)}
  .mg-plonly-d{grid-column:2;font-size:var(--fs-xs);font-weight:600;color:var(--muted);line-height:1.35}
  /* blox40 #3 — включённый режим виден НЕ по одной галочке: плашка надевает ту же радугу
     (--grad-playlist), что и «Постоянный плейлист» у ребёнка и кнопка Playlist у учителя.
     Один взгляд на ростер — и понятно, что группа переведена в режим прослушки. */
  .mg-plonly:has(input:checked){background:var(--grad-playlist);border-color:transparent;
    box-shadow:var(--sh-2)}
  .mg-plonly:has(input:checked) .mg-plonly-t{color:var(--ink-strong)}
  .mg-plonly:has(input:checked) .mg-plonly-d{color:var(--ink)}
  .roster-hd{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin:6px 0}
  .roster-hd h2{margin:0}
  .roster-sub{color:var(--muted);font-size:var(--fs-sm)}
  .stu-list{display:flex;flex-direction:column;gap:6px;margin-top:10px;max-width:620px}
  .stu-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;background:var(--card);border:1px solid var(--line-soft);
    border-radius:var(--r-sm);padding:9px 12px}
  .stu-fio{font-weight:700;flex:1 1 120px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .stu-nick{color:var(--accent-ink);font-size:var(--fs-sm);flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .stu-bd{color:var(--draft-ink);font-size:var(--fs-xs);flex:0 0 auto;white-space:nowrap}
  /* --faint (3.16:1) — для декора и disabled; «почта —» это ИНФОРМАЦИЯ, её читают, поэтому
     --muted (5.33:1). Тот же разбор, что и у .sb-note (axe, 02.09.2026). */
  .stu-em{font-size:var(--fs-xs);flex:0 0 auto;white-space:nowrap;color:var(--muted)}
  .stu-row .stu-sp{margin-left:auto;display:flex;align-items:center;gap:8px}
  .stu-row select{padding:5px 7px;border:1px solid var(--line-mid);border-radius:var(--r-xs);font:inherit;font-size:var(--fs-sm)}
  .stu-x{border:none;background:none;color:var(--danger);font-size:var(--ic-md);cursor:pointer;padding:2px 6px}
  /* ПЕРЕВОД НА НОВЫЙ УЧЕБНЫЙ ГОД — экран предпросмотра плана.
     Строка плана — flex-wrap (не grid): на 375px «имя → куда» и счётчик детей переносятся сами,
     ничего не уезжает за край (G1). Слово-подтверждение — input 16px, иначе Safari зумит страницу
     и не возвращает обратно (правило 3). */
  .pm-sum{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);padding:14px 16px;
    margin:10px 0 18px;font-size:var(--fs-md);line-height:1.6}
  .pm-sec{display:flex;align-items:center;gap:10px;margin:18px 0 8px;font-size:var(--fs-xs);font-weight:800;
    letter-spacing:.06em;text-transform:uppercase;color:var(--muted-strong)}
  .pm-sec::after{content:'';flex:1 1 auto;height:1px;background:var(--line-soft)}
  .pm-sec .pm-cnt{order:3;font-weight:800;color:var(--muted)}
  .pm-sec-skip{color:var(--faint)}
  .pm-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;background:var(--card);
    border:1px solid var(--line-soft);border-radius:var(--r-sm);padding:9px 12px;margin-bottom:6px}
  .pm-name{font-weight:700;flex:1 1 110px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .pm-move{order:2;display:flex;align-items:center;gap:8px;min-width:0}
  .pm-from{color:var(--muted);font-size:var(--fs-sm);white-space:nowrap}
  .pm-arr{color:var(--faint)}
  .pm-to{font-weight:700;color:var(--accent-ink);white-space:nowrap}
  .pm-cross{color:var(--accent-deep)}
  .pm-kids{order:3;margin-left:auto;color:var(--muted);font-size:var(--fs-xs);white-space:nowrap}
  /* телефон: имя и счётчик в первой строке, переход целиком во второй — иначе строки рвутся
     каждая по-своему и список перестаёт читаться колонкой */
  @media(max-width:560px){
    .pm-row{display:grid;grid-template-columns:1fr auto;gap:2px 10px}
    .pm-name{grid-area:1/1} .pm-kids{grid-area:1/2;margin-left:0;text-align:right}
    .pm-move{grid-area:2/1/2/3}
  }
  .pm-row-grad{background:var(--alert-tint);border-color:var(--alert-line)}
  .pm-row-grad .pm-to{color:var(--alert-ink)}
  .pm-row-skip{background:transparent;border-style:dashed}
  .pm-row-skip .pm-to{color:var(--faint);font-weight:400;font-size:var(--fs-sm)}
  .pm-names{color:var(--muted-strong);font-size:var(--fs-sm);line-height:1.5;margin:-2px 0 10px;padding:0 12px}
  .pm-legend{color:var(--faint);font-size:var(--fs-xs);margin:6px 0 0}
  .pm-confirm{background:var(--alert-tint);border:1px solid var(--alert-line);border-radius:var(--r-md);
    padding:14px 16px;margin:22px 0 8px}
  .pm-warn{color:var(--alert-ink);font-size:var(--fs-md);line-height:1.5;margin-bottom:10px}
  .pm-go{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
  .pm-go input{font:inherit;font-size:var(--fs-input);padding:10px 12px;min-height:44px;
    border:1px solid var(--line-mid);border-radius:var(--r-sm);flex:0 1 180px;min-width:0}
  /* прозрачность снята (blox52 #3, п.1): её роль забрало общее правило .btn:disabled — блёклая
     заливка и приглушённый текст. Свой курсор остаётся: здесь «нельзя» объясняется словом,
     которое надо ввести рядом, и not-allowed — часть этого объяснения. */
  .pm-go .btn-pub:disabled{cursor:not-allowed}
  /* STUDENT MOVER — drag board */
  .sm-hint{color:var(--muted-strong);font-size:var(--fs-sm);line-height:1.5;margin:6px 0 12px;max-width:760px}
  .sm-warn{color:var(--danger);font-size:var(--fs-sm);margin:0 0 10px}
  .sm-grad{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;
    background:var(--alert-tint);border:2px dashed var(--alert-line);color:var(--alert-ink);border-radius:var(--r-md);
    padding:14px 18px;margin:0 0 14px;font-weight:700;font-size:var(--fs-md);text-align:center}
  .sm-gradsub{font-weight:400;color:var(--danger);font-size:var(--fs-xs)}
  .sm-grad.sm-over{background:var(--alert-fill);border-color:var(--danger);color:var(--alert-ink);box-shadow:0 0 0 2px var(--danger) inset}
  .sm-board{display:flex;gap:12px;align-items:flex-start}
  .sm-col{flex:1 1 0;min-width:0;background:var(--surface);border:1px solid var(--line-soft);
    border-radius:var(--r-md);padding:12px}
  .sm-col.sm-over{background:var(--ok-tint);border-color:var(--ok);box-shadow:0 0 0 2px var(--ok) inset}
  .sm-colhd{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:10px}
  .sm-colhd select{flex:1 1 auto;min-width:0;padding:7px 9px;border:1px solid var(--line-mid);
    border-radius:var(--r-xs);font:inherit}
  .sm-cnt{color:var(--muted);font-size:var(--fs-xs);white-space:nowrap}
  .sm-swap{align-self:center;cursor:pointer;font-size:var(--ic-lg);color:var(--accent-ink);user-select:none;
    padding:0 2px;flex:0 0 auto}
  .sm-drop{display:flex;flex-direction:column;gap:7px;min-height:60px}
  .sm-empty{color:var(--faint);font-size:var(--fs-sm);text-align:center;padding:14px 0}
  /* touch-action:pan-y — пальцем со списка можно скроллить; драг перехватывает жест сам
     (long-press → touchAction:none на плашке + preventDefault на touchmove) */
  .sm-chip{background:var(--card);border:1px solid var(--line);border-radius:var(--r-sm);padding:8px 11px;
    cursor:grab;touch-action:pan-y;user-select:none}
  .sm-chip:active{cursor:grabbing}
  .sm-chip.sm-lifted{opacity:.35}
  .sm-nick{font-weight:600;font-size:var(--fs-sm)}
  .sm-fio{color:var(--muted);font-size:var(--fs-xs);margin-top:1px}
  .sm-clone{position:fixed;z-index:60;pointer-events:none;background:var(--card);border:1px solid var(--accent-line);
    border-radius:var(--r-sm);padding:8px 11px;box-shadow:var(--sheet-4);transform:scale(1.03)}

  /* УЧИТЕЛЯ — статистика эффективности */
  .stat-note{color:var(--muted);font-size:var(--fs-sm);margin:6px 0 14px;max-width:720px}
  .tstat-card{background:var(--card);border-radius:var(--r-md);padding:16px 18px;
    margin-bottom:16px;box-shadow:var(--sheet-1)}
  .tstat-head{display:flex;align-items:center;gap:16px;cursor:pointer}
  .tsh-l{flex:1}
  /* карточка учителя складывается: на телефоне экран открывается списком, метрики — по тапу */
  .tsh-chev{display:none;color:var(--muted);font-size:var(--ic-lg);font-weight:700;
    transition:transform var(--t) var(--ease)}
  .tstat-card.closed .stat-grid{display:none}
  .tstat-card.closed .tsh-chev{transform:rotate(0deg)}
  .tstat-card .tsh-chev{transform:rotate(90deg)}
  @media(max-width:760px){ .tsh-chev{display:block} }
  .tsh-name{font-weight:800;font-size:var(--fs-lg)}
  .tsh-sub{color:var(--muted);font-size:var(--fs-sm);margin-top:2px}
  /* ✎ стоит вплотную к имени/нику учителя — правишь именно того, на кого смотришь */
  .tsh-name{display:flex;align-items:center;gap:8px}
  .tsh-edit{flex:0 0 auto;width:30px;height:30px;border:1px solid var(--line);background:var(--card);
    border-radius:var(--r-sm);color:var(--muted);cursor:pointer;display:flex;align-items:center;
    justify-content:center;transition:background var(--t) var(--ease),color var(--t) var(--ease)}
  @media (hover:hover){.tsh-edit:hover{background:var(--accent-tint);border-color:var(--accent-deep);color:var(--accent)}}
  .tsh-new{color:var(--muted)}
  /* журнал неудачных попыток входа с персональной двери */
  .att-box{border:1px solid var(--line);border-radius:var(--r-md);padding:10px 14px;margin:0 0 14px;
    background:var(--surface);max-width:720px}
  .att-box summary{cursor:pointer;font-weight:700;font-size:var(--fs-sm);color:var(--muted)}
  .att-row{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;padding:7px 0;
    border-top:1px solid var(--line-soft);font-size:var(--fs-sm);box-shadow:var(--rule-t)}
  .att-mail{font-weight:600}
  .att-why{color:var(--muted);flex:1}
  .att-when{color:var(--muted)}
  .tsh-index{border:2px solid;border-radius:var(--r-md);padding:8px 14px;text-align:center;min-width:78px}
  .tshi-n{font-weight:800;font-size:var(--fs-2xl);line-height:1}
  .tshi-c{font-size:var(--fs-2xs);color:var(--muted);margin-top:2px;letter-spacing:.5px}
  .stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:14px}
  .stat-tile{background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--r-sm);padding:11px 13px}
  .st-top{display:flex;align-items:center;justify-content:space-between}
  .st-ic{font-size:var(--fs-lg)}
  .st-score{font-weight:800;font-size:var(--fs-xl);line-height:1}
  .st-label{font-weight:600;font-size:var(--fs-sm);margin-top:6px}
  .st-detail{color:var(--faint);font-size:var(--fs-xs);margin-top:3px;line-height:1.35}

  /* ДЛЯ РОДИТЕЛЯ — отчёт по ребёнку */
  .p-sub{color:var(--muted);font-size:var(--fs-sm);margin:-4px 0 10px}
  .p-datebar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:14px}
  .p-datebar label{font-size:var(--fs-sm);color:var(--muted-strong)}
  .p-datebar input[type=date]{padding:6px 8px;border:1px solid var(--line-mid);border-radius:var(--r-xs);font:inherit}
  .pstat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px}
  .pstat{background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--r-md);padding:14px 14px 12px;text-align:center}
  .pstat.ok{background:var(--ok-tint);border-color:var(--ok-line)}
  .pstat.warn{background:var(--draft-tint);border-color:var(--draft-line)}
  .pstat.bad{background:var(--alert-tint);border-color:var(--alert-line)}
  .pst-val{font-weight:800;font-size:var(--fs-3xl);line-height:1;color:var(--ink)}
  .pst-of{font-size:var(--fs-lg);color:var(--faint);font-weight:700}
  .pst-lbl{font-size:var(--fs-xs);color:var(--muted);margin-top:7px;line-height:1.35}

  /* ---- icon tinting -----------------------------------------------------------
     An icon inherits the colour of its text, so most need nothing. These few sit on
     their own and would otherwise come out ink-black. */
  .rect .ricon{color:var(--rail)}                 /* task icon = its type's rail colour */
  .rect.done .ricon{color:var(--ok-ink)}
  .eico,.bico,.st-ic,.ex-aud{color:var(--muted)}
  .annc .ah .ic{color:var(--note-ink)}
  .btn-ghost .ic,.mgr-btn .ic,.btn-fb .ic{color:var(--muted)}
  @media (hover:hover){.btn-ghost:hover .ic{color:var(--accent-ink)}}
  .btn-primary .ic,.btn-pub .ic{color:currentColor}
  .srnote .ic{color:var(--muted)}
  .locknote .ic{color:var(--alert-ink)}
  .tb-add .ic,.tb-name .ic{color:var(--accent-ink)}

/* Тост сетевого/серверного провала (blox48 #9, core.js apiToast). Полоска у нижнего края,
   поверх всего (z-index выше любого слоя — сообщение о провале обязано пробить и модалку);
   тап закрывает (правило «всегда есть выход»), сама гаснет через 6 с. Тёмная заливка под
   белым текстом — роль --alert-strong (§11 lingo-ui). */
.net-toast{position:fixed;left:12px;right:12px;bottom:12px;z-index:70;
  background:var(--alert-strong);color:#fff;padding:10px 14px;border-radius:var(--r-sm);
  font-size:var(--fs-md);line-height:1.35;box-shadow:0 4px 16px rgba(var(--sh-rgb),.25);cursor:pointer;
  overflow-wrap:anywhere}
.net-toast[hidden]{display:none}
/* зелёный собрат (blox52 #9): «получилось». Тот же узел, тот же выход по тапу — меняется
   только заливка. --ok-solid, а не --ok: он и заведён как СПЛОШНАЯ зелёная под белым текстом
   и одинаков во всех четырёх схемах, ровно как --alert-strong у красного. */
.net-toast.ok{background:var(--ok-solid)}
/* нейтральный собрат (blox52 #18): «прими к сведению» — совет, не провал и не победа.
   Якорь схемы (--anchor) — та же сплошная тёмная заливка под белым текстом, что у двух
   других, но своего цвета: красный по §11 обязан значить настоящую тревогу, и подсказка
   «видео удобнее скачать с компьютера» в красном читалась бы как поломка. */
.net-toast.info{background:var(--anchor)}

/* Карточка «сессия закончилась» (core.js sessionGone). Не тост: тост гаснет, а этот тупик —
   навсегда, до входа заново. Поэтому подложка на весь экран (кликать под ней нечего: сервер
   ответит 401 на что угодно) и z-index выше всех рабочих слоёв — модалок (60), шторок (95/99)
   и самого тоста (70). Инспектор кода (2147483000+) намеренно остаётся выше: он про починку
   платформы, а не про работу в ней. */
.slost{position:fixed;inset:0;z-index:120;display:flex;align-items:center;justify-content:center;
  padding:16px;background:var(--scrim)}
.slost-card{background:var(--card);border-radius:var(--r-lg);
  box-shadow:var(--sheet-4);padding:22px 20px;max-width:420px;width:100%;text-align:center}
.slost-card h2{margin:0 0 8px;font-size:var(--fs-lg);color:var(--ink-strong)}
.slost-card p{margin:0 0 18px;font-size:var(--fs-md);line-height:1.45;color:var(--muted)}
