  /* 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)}}
  /* УЗКО — ТОЛЬКО СТРЕЛКА (blox60 #5, решение владельца 18.09.2026). Левая дорожка имеет минимум
     НОЛЬ (см. grid-template-columns выше), правая — min-content: значит при тесноте схлопывается
     именно «назад», и схлопывается ДО КОНЦА. 17.09 колокольчик открыли ученикам (blox60 #3) — в
     правый кластер ребёнка добавилось 38px, и слот «назад» ужался до 6.2px: подпись «Homework»
     получила НУЛЕВУЮ ширину, а кнопка вылезла за свой слот и наехала на логотип (на 375px — на
     11.8px, на 320px — на 18px). Выход из чата группы у ребёнка стал невидимым — кнопка жила и
     нажималась, но увидеть её было нельзя.
     Лечим не колокольчик, а правило: ниже 620px подпись прячется целиком, остаётся стрелка в
     честном квадрате 44×44 (цель касания, lingo-ui §3), а дорожка получает пол min-content и
     больше не схлопывается. Пусто в слоте — min-content равен нулю, дорожка снова чистая 1fr, и
     логотип стоит по центру ровно как раньше: экраны БЕЗ кнопки «назад» не меняются ни на пиксель.
     ПОЧЕМУ 620, а не 375. Порог выбран замером, а не на глаз, и он про САМУЮ ДЛИННУЮ подпись:
     «Homework» у ребёнка режется многоточием вплоть до 560px (у ребёнка правый кластер шире —
     в нём блок аккаунта) и становится целым только на 620. У Лу «Кабинет» помещается уже с 414px,
     так что на планшете она слово теряет — сознательный размен: лучше честная стрелка везде, чем
     смазанный огрызок на части ширин. Появится подпись ДЛИННЕЕ «Homework» — порог надо пересчитать
     тем же замером (ширина .mlabel: scrollWidth против clientWidth). За нулевой шириной и наездом
     на логотип следит геометрия ночного обходчика (G2/G6, tools/crawl.js) — она этот дефект и нашла. */
  @media (max-width:620px){
    header{grid-template-columns:minmax(min-content,1fr) auto minmax(min-content,1fr)}
    .hdr-back .mback .mlabel{display:none}
    .hdr-back .mback{width:44px;height:44px;justify-content:center;padding:0;gap:0}
  }
  .hdr-right{justify-self:end;display:flex;align-items:center;gap:10px;min-width:0}
  /* ЗНАК ШКОЛЫ — картинка (см. index.html). Прежде здесь было слово «Lingo» текстом плюс
     точка на ::before; и точка, и подбор шрифта ушли вместе с текстом: у рукописной надписи
     есть своя россыпь крапинок, и правильная точка рядом с ней читалась бы вторым знаком.
     Токен --brand-dot остался — его берёт та же точка на экране входа. */
  header .logo{
    font-weight:800;font-size:var(--fs-2xl);letter-spacing:var(--tracking-tight);
    display:flex;align-items:center;justify-self:center;white-space:nowrap;min-width:0;
  }
  /* Высоту задаём картинке, ширину она добирает сама — пропорции знака не искажаются никогда
     (это же проверяет G12 в ночном обходе). max-width страхует узкую дорожку сетки: знак
     сожмётся, но не столкнётся с правым кластером. */
  header .logo img{height:34px;width:auto;max-width:100%;object-fit:contain;display:block}
  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 .logo img{height:26px}   /* знак уступает место правому кластеру, а не наоборот */
    header .who select{max-width:72px}   /* dev-элемент; ужат, чтобы не наезжать на центр-логотип */
    header .who span{display:none}   /* «Кто я:» — dev-подпись, на телефоне прячем ради ширины */
  }
  /* КОЛЕСИКО ЗАГРУЗКИ (blox72 #11) — кольцо вокруг колокольчика; включает core.js (netPaint), когда
     передний запрос ждёт ответа дольше секунды. ПОЧЕМУ КОЛЬЦО ВОКРУГ, А НЕ ЗНАЧОК РЯДОМ: на
     телефоне в шапке нет свободного места — замер у всех ролей на 320–414px показал между знаком
     школы и правым кластером ровно зазор сетки (8px). Значок в потоке растолкал бы кластер и на
     узком экране ребёнка выдавил бы шапку за край. Кольцо висит поверх слота (position:absolute,
     вне потока): шапка не сдвигается ни на пиксель ни при появлении, ни при исчезновении.
     Диаметр 50 — чтобы кольцо обошло скруглённые углы кнопки 38×38 (угол на 22.7px от центра),
     не задев их. Клики проходят насквозь: кольцо ничего не блокирует. Цвет — тот же
     полупрозрачно-белый язык, что у контура колокольчика и шестерёнки на тёмной шапке. */
  .notif-slot{position:relative;display:flex;align-items:center}
  .netwait{position:absolute;left:50%;top:50%;width:0;height:0;pointer-events:none;z-index:1}
  .netwait[hidden]{display:none}
  .netwait-ring{position:absolute;left:-25px;top:-25px;width:50px;height:50px;box-sizing:border-box;
    border-radius:50%;border:2px solid rgba(255,255,255,.22);border-top-color:var(--on-dark);
    border-right-color:var(--on-dark);animation:btn-spin .8s linear infinite}
  @media (prefers-reduced-motion:reduce){.netwait-ring{animation-duration:2s}}
  /* Колокольчика нет (роль без ленты, экран входа) — кольцо обходит шестерёнку: слот пуст и
     шириной ноль, следующий в ряду — .settings через зазор .hdr-right (10px; 8px на телефоне). */
  .notif-slot:has(> .notif:empty) .netwait{left:calc(100% + 10px + 19px)}
  @media(max-width:430px){.notif-slot:has(> .notif:empty) .netwait{left:calc(100% + 8px + 19px)}}
  /* 15 секунд без ответа — строка под шапкой у правого края, прямо под колесиком. Не в ряду шапки
     по той же причине, что и кольцо (места нет); висит под ней, ничего не блокирует
     (pointer-events:none) и уходит сама, как только ответ пришёл. */
  .netwait-msg{position:absolute;top:calc(100% + 6px);right:max(10px,env(safe-area-inset-right));
    max-width:calc(100vw - 20px);box-sizing:border-box;padding:8px 12px;border-radius:var(--r-sm);
    background:var(--card);color:var(--ink);box-shadow:var(--sheet-pop);
    font-size:var(--fs-sm);font-weight:700;line-height:var(--lh-tight);text-wrap:balance;pointer-events:none}
  .netwait-msg[hidden]{display:none}
  /* строка выбора шрифта — используется в попапе настроек (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}
  /* blox55 #22 — ДВЕ КОЛОНКИ вместо столбика (почему — комментарий в settings.js над mountSettings).
     Сетка объявлена ЗДЕСЬ, а не у каждой секции: и шрифты, и схемы — один и тот же список выбора
     из четырёх, и раскладка у них обязана быть одна. Ширину меню на широких экранах поднимаем
     до 380px: в 300px ячейка получается 139px, и подпись схемы («Графитовая» + «графит и Maya
     Blue») ломается на три строки — выигрыш от двух колонок съедается обратно. На телефоне
     ширину не трогаем: там меню и так во весь экран минус поля. */
  .set-grid{display:grid;grid-template-columns:1fr 1fr;gap:2px}
  @media(min-width:761px){ .set-menu{max-width:min(380px,calc(100vw - 24px))} }
  /* blox69 #7 расширял меню до 560px и ставил шрифты в три колонки — их было шесть. С blox72 #7
     шрифтов два (остальные под заглушкой) — обычные две колонки, одна строка, ширина прежняя. */
  /* В ячейке вдвое меньше ширины, поэтому в сетке всё поджато: точки мельче, зазор до текста
     меньше, поля кнопки уже. Сами точки остаются главным превью палитры — ассерт «цветовая схема»
     сверяет их цвет с токенами включённой схемы, и уменьшение размера ему безразлично. */
  .set-grid .set-scheme{gap:2px 8px;padding:9px 8px}
  .set-grid .set-dots i{width:11px;height:11px}
  .set-grid .fsw-item{padding:9px 9px}
  /* ПОДПИСЬ СХЕМЫ В СЕТКЕ НЕ ПОКАЗЫВАЕМ. В половинной ячейке «ночная, виноградный якорь» ломается
     на три строки, и секция из двух колонок выходит почти такой же высокой, как столбик, — то есть
     правка не срабатывает. Потеря тут небольшая: подпись пересказывает словами то, что рядом уже
     нарисовано точками (первая = шапка схемы), а имя схемы остаётся. У ШРИФТОВ подпись остаётся —
     она не дублирует ничего: «мягкий, для младших» и «шрифт устройства» — это совет, как выбрать. */
  .set-grid .set-snote{display:none}
  /* А на ТЕЛЕФОНЕ схемы остаются столбиком: меню там 300px, половинная ячейка — 145px, и в неё
     не помещается «точки + Sneakers + галочка» (подпись вылезала за край меню). Шрифтов это не
     касается — их ячейка укладывается. Столбик схем на телефоне ничего не ломает: без подписей
     секция всё равно вдвое ниже прежней, и меню целиком влезает в экран. */
  @media(max-width:760px){ .set-grid-sch{grid-template-columns:1fr} }
  /* ГРАВИРОВАННАЯ ЛИНЕЙКА (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)}}
  /* 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)}}
  /* Подсказка «как установить» в меню настроек (blox55 #18). НАРОЧНО не кнопка и не похожа на
     неё — ни рамки, ни фона, ни наведения: нажимать в ней не на что (жест ручной, его делает сам
     человек через меню браузера), а контрол, который ничего не делает, — это мёртвый клик.
     Ширина задана здесь, а не у меню: меню тянется по содержимому, и без ограничения одна длинная
     строка растянула бы его до предела на всех экранах. */
  .set-hint{padding:0 11px 8px;color:var(--muted);font-size:var(--fs-xs);line-height:1.45;max-width:230px}
  /* на телефоне запас по ширине минимальный — режем селект сильнее, чтобы шапка держала одну строку.
     Стоит ПОСЛЕ базового 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}
  /* Подсказка «а если аккаунт верный» — вторым планом: она нужна меньшинству, а первую строку
     перебивать не должна. */
  .login-err-hint{margin-top:8px;font-size:var(--fs-sm);line-height:1.4;color:var(--muted-strong);
    padding-top:8px;border-top:1px solid var(--alert-line)}
  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}
  /* padding-right резервирует правую колонку под «сделал» (.rcheck, абсолютная во всю высоту), чтобы
     серый столбец тянулся до низа карточки при любой её высоте (blox18 #7). Плеера в карточке больше
     нет — он открывается шторкой (.hwpsheet, blox68 #6). */
  .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: иконка типа стояла отдельной КОЛОНКОЙ у левого края таска.
     blox63 #8 — колонки больше нет: иконка уехала внутрь строки с названием типа (.rtico), потому
     что в колонке она вставала на уровень первой строки ЗАДАНИЯ и подписывала собой чужой текст. */
  .rect .rtico{font-size:var(--ic-md);line-height:1;flex:0 0 auto}
  .rect .rbody{flex:1;min-width:0;padding:14px 14px 14px 14px;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: абзацу нужно ровное «дно», а не ровные
     строки. */
  /* blox63 #9 — РАЗМЕР тут больше, чем у заголовка-ярлыка, и это намеренное расхождение с
     остальными тремя карточками: у учителя лист — столбик, который он просматривает, а у ребёнка
     это текст, который он ЧИТАЕТ и по которому делает задание. 15px хватало ярлыку, читаемому
     абзацу — нет. */
  .rect .rtitle-body{white-space:pre-wrap;font-weight:var(--fw-read);text-wrap:pretty;
        font-size:var(--fs-lg);line-height:1.4}
  /* тип таска подписью — появляется, когда заголовок занят самим заданием */
  .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)}
  /* blox70 #2 — «🤔» учителя на несделанном задании: знак тем же видом, что реакция на сдачу
     (.subreact-ro), и подпись в строку — одна тихая строка, не тревога */
  .rect .rwait,.tsc .rwait{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:6px}
  .rect .rwait .subreact,.tsc .rwait .subreact{margin-top:0}
  .rect .rwait-lbl,.tsc .rwait-lbl{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,.tsc .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,.tsc .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,.tsc .rp-count{font-weight:800;color:var(--gold-ink);display:flex;align-items:center;gap:6px}
  .rect .rp-did,.tsc .rp-did{color:var(--ok-ink);overflow-wrap:anywhere}
  .rect .rp-did b,.tsc .rp-did b{color:var(--gold-ink)}
  .rect .rp-not,.tsc .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}
  /* ОБЛАЧКО УЧИТЕЛЯ (blox71 #1/#3) — одно на карточке листа и на экране таска (.tsc-ret). Сверху
     вердикт (если он нужен), под ним РЕПЛИКА одной строкой с левого края: «Mrs. Lou: текст 👍».
     Принято — белая бумага с зелёной кромкой поверх зелёной сделанной карточки; вернули — акцентная
     подложка (не тревога: красный — только настоящая беда). */
  .tplate{display:flex;flex-direction:column;gap:6px;padding:9px 12px;border-radius:var(--r-sm);
        background:var(--surface-2);border:1px solid var(--line-soft);color:var(--ink)}
  .tplate.tp-ok{background:var(--card);border-color:var(--ok-line)}
  .tplate.tp-ret{background:var(--accent-tint);border-color:var(--accent-line)}
  .tp-verdict{display:flex;align-items:center;gap:5px;font-size:var(--fs-sm);font-weight:700}
  .tp-ok .tp-verdict{color:var(--ok-ink)}
  .tp-msg{font-size:var(--fs-sm);line-height:1.45;text-align:left;overflow-wrap:anywhere}
  .tp-who{font-weight:700;color:var(--muted)}
  /* КОММЕНТАРИЙ УЧИТЕЛЯ — читаемый текст (blox52 #6): тот же вес, что у самого задания рядом, иначе
     в одном предмете два веса у двух одинаковых по роли текстов. Переносов нет: учитель пишет
     комментарий по-английски не реже, чем по-русски. Переводы строк учителя сохраняем. */
  .tplate .tp-note{font-weight:var(--fw-read);white-space:pre-wrap;text-wrap:pretty}
  /* знак — в строку, сразу после текста: inline-flex перебивает flex общего .subreact */
  .tp-msg .subreact-ro{display:inline-flex;vertical-align:middle;margin:-4px 0 -2px 2px;font-size:var(--fs-xl)}
  /* ПРИНЯТОЕ СВЁРНУТО (blox71 #1–#3): [✓ задание / тип] и под ними облачко учителя. Облачко
     начинается на вертикали ✓ (оба стоят у левого края .rbody), а кончается там же, где облачко
     «вернули»: правая колонка 66px (резерв .rect) остаётся, в ней стрелка «›» вместо галочки —
     того же зелёного цвета, что колонка «сделал» у сделанной карточки. */
  .rect .rfold{gap:8px}
  .rf-top{display:flex;align-items:flex-start;gap:10px}
  .rf-tick{flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:var(--ok-ink);color:var(--on-dark);
        display:flex;align-items:center;justify-content:center;margin-top:-1px}
  .rf-tick .tick{width:15px;height:15px;display:block;fill:none;stroke:currentColor;
        stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
  .rf-main{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:5px}
  .rf-title{font-size:var(--fs-md);font-weight:600;line-height:1.3;color:var(--muted);overflow-wrap:anywhere;
        display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}
  .rf-type{display:flex;align-items:center;gap:6px;font-size:var(--fs-sm);font-weight:700;color:var(--muted)}
  .rf-go{position:absolute;top:0;right:0;bottom:0;width:66px;display:flex;align-items:center;justify-content:center;
        background:var(--ok-fill);border-left:1px solid var(--ok-line);color:var(--ok-ink);font-size:var(--ic-md)}
  /* blox22 #3: свёрнутый «Мой ответ» — ученик пересматривает свою сдачу */
  /* blox27 #4 — «Мой ответ» показан сразу сущностью файла (не тоггл): подпись + кликабельная карточка */
  .rect .myans,.tsc .myans{margin-top:6px}
  .rect .myans-lbl,.tsc .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)}

  /* blox64 #6 — РЕАКЦИЯ УЧИТЕЛЯ НА СДАЧУ. Ряд из пяти знаков под сущностью файла и под плеером в
     оверлее. Разметку рисует submission-view.js, набор знаков приходит из public/reactions.js.
     Это ЕДИНСТВЕННОЕ место интерфейса, где эмодзи стоят намеренно (сторож в tools/check.js знает
     про исключение): знак здесь и есть содержание — ребёнок узнаёт «огонь», а не контурную фигуру.
     У ребёнка кнопок нет вовсе, стоит только .subreact-ro с поставленным знаком (флаг от экрана). */
  .subreact{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:8px}
  .subreact-b{flex:0 0 auto;width:44px;height:44px;display:flex;align-items:center;justify-content:center;
        font-size:var(--fs-xl);line-height:1;border:1px solid var(--line);border-radius:var(--r-sm);
        background:var(--surface);cursor:pointer;padding:0}
  .subreact-b:active{transform:scale(.94)}
  @media (hover:hover){.subreact-b:hover{border-color:var(--accent-line)}}
  /* ПОСТАВЛЕННАЯ — ЗЕЛЁНАЯ (решение владельца 22.09.2026). До этого дня здесь стоял оттенок схемы
     с оговоркой «зелёный значит принято/сделано, а реакция — не приёмка». Владелец решил иначе:
     поставленный знак читается как отметка, и зелёный тут уместен.
     Заливка СПЛОШНАЯ (--ok-solid), и это осознанно: мягкий --ok-fill на карточке принятой работы
     (она сама зеленоватая) читался бледно — сравнили два снимка. Значит смысл зелёного расширен:
     не только «сделано/принято», но и «учитель отметил». Поэтому .subreact-b.on ВПИСАНА в реестр
     GREEN_OK (tools/check.js) — сторож остаётся закрытым списком, а не обходится.
     Эмодзи — собственного цвета, белого текста тут нет, поэтому контраст к AA не применим. */
  .subreact-b.on{background:var(--ok-solid);border-color:var(--ok-solid);box-shadow:var(--sh-1)}
  /* показ ребёнку: знак без рамки-кнопки — нажимать нечего. С 22.09.2026 перед знаком стоит имя
     того, кто его поставил («Mrs. Lou: 👍»): у ребёнка это единственное место, где похвала
     ИМЕННАЯ. Имя — обычными подписными чернилами и обычным кеглем, знак крупный: читается «кто»
     и сразу «что», а не строка одинакового размера. У старых знаков имени нет — тогда это просто
     знак, как раньше, и выравнивание не съезжает (gap между отсутствующими узлами не рисуется). */
  .subreact-ro{display:inline-flex;align-items:center;gap:4px;font-size:var(--fs-2xl);line-height:1;
        margin-top:8px}
  .subreact-who{font-size:var(--fs-sm);line-height:1;color:var(--muted);font-weight:600}
  .subreact-sign{line-height:1}

  /* ОТВЕТ НА НАЖАТИЕ (22.09.2026). Раньше ряд отвечал только сменой заливки — на телефоне её
     закрывает палец, и методист, не увидев результата, второй раз СНЯЛА свою же реакцию.
     «Пшик» стоит на обоих узлах: у кнопки (учитель нажал) и у знака ребёнка (знак появился с
     салютом, и он же должен дрогнуть). Анимация трансформом — она не влияет на поток, поэтому
     правило «экран не прыгает» здесь не может быть нарушено в принципе. */
  @keyframes subreact-pop{0%{transform:scale(1)}32%{transform:scale(1.3)}62%{transform:scale(.9)}100%{transform:scale(1)}}
  .subreact-b.pop,.subreact-ro.pop{animation:subreact-pop .34s ease-out}

  /* САЛЮТ. Слой общий и один на приложение (public/salute.js его создаёт при первой надобности):
     position:fixed поверх всего, pointer-events:none — сквозь него нажимают, он ничего не ловит и
     ничего не сдвигает. z-index 130 — выше шторок (50), тостов (70) и «нет связи» (120): салют
     стреляет и из оверлея со сдачей, значит обязан лежать над ним. */
  .salute-layer{position:fixed;inset:0;z-index:130;pointer-events:none;overflow:hidden}
  /* --dx/--dy/--rot ставит на каждую частицу salute.js (свой угол и своя дальность). Объявлены
     ЗДЕСЬ со значениями по умолчанию: без объявления правило читается как сломанное — и сторож
     «переменная используется, но нигде не задана» справедливо ругался. Ноли значат «никуда не
     полетела»: частица без скрипта просто гаснет на месте, а не прыгает в угол экрана. */
  .salute-p{--dx:0px;--dy:0px;--rot:0deg;
        position:absolute;display:block;width:7px;height:9px;border-radius:var(--r-2xs);
        margin:-4px 0 0 -3px;opacity:0;animation:salute-fly .78s ease-out forwards}
  /* до 70% — разлёт, дальше частица ПАДАЕТ и гаснет: без этого хвоста салют читается как
     расползающееся пятно, а не как хлопушка */
  @keyframes salute-fly{
    0%{opacity:1;transform:translate(0,0) rotate(0) scale(1)}
    70%{opacity:1;transform:translate(var(--dx),var(--dy)) rotate(var(--rot)) scale(1)}
    100%{opacity:0;transform:translate(calc(var(--dx) * 1.1),calc(var(--dy) + 26px)) rotate(var(--rot)) scale(.85)}
  }
  /* человек попросил покой — гасим ОБА знака внимания: «пшик» замирает, частицы не рождаются
     вовсе (это делает сам модуль, здесь — страховка на случай уже созданного слоя) */
  @media (prefers-reduced-motion:reduce){
    .subreact-b.pop,.subreact-ro.pop{animation:none}
    .salute-layer{display:none}
  }

  /* медиа-оверлей сдачи: фото вписано (не вылезает за экран), видео/аудио — через наш плеер */
  .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}
  /* колонка, а не строка: под плеером встаёт ряд реакций (blox64 #6), и в строке он оказался бы
     сбоку от плеера, а не под ним */
  .mediaov .mediabody{padding:8px 0 4px;display:flex;flex-direction:column;align-items: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)}}
  /* СДЕЛАННЫЙ ТАСК ГАСИТ СВОЮ ГРОМКУЮ КНОПКУ. Заливка акцентом — знак «вот ради чего карточка
     существует»; когда задание сдано и плашка позеленела, звать больше некуда, а кнопка всё так же
     кричит с зелёного листа и перетягивает взгляд с тех тасков, что ещё ждут. Поэтому в .rect.done
     (и в золотом .rect.chal.done — он тоже .done) «Слушать» / «Открыть задание» возвращаются к
     ТИХОЙ контурной базе: фон прозрачный, чтобы сквозь кнопку читался тинт самой карточки, и она
     сидела В листе, а не поверх него. Кнопка остаётся ЖИВОЙ — переслушать сданное законно, — и
     этим отличается от «Открыто ✓» (.opened), которая disabled. Волна возвращается к режиму схемы
     (--ripple-blend:inherit): normal нужен только тёмной заливке, а её тут больше нет.
     Специфичность равна громкому правилу — побеждает порядком, поэтому стоит ПОСЛЕ него. */
  .rect.done .ract.open:not(.opened),.rect.done .ract.play{
        background:transparent;border-color:var(--line-mid);color:var(--muted-strong);
        box-shadow:none;--ripple-blend:inherit}
  @media (hover:hover){.rect.done .ract.open:not(.opened):hover,.rect.done .ract.play:hover{
        background:var(--card);border-color:var(--line-strong);box-shadow:none}}
  /* «Открыто ✓» (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)}
  /* у контрольного файла нет колонки «сделал» (.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) и «На телевизор».
     Обе — ОДИН и тот же предмет: таблетка в шапке пула, поэтому и правило одно на двоих
     (blox62 #1). Второй набор свойств рядом — это ровно та копипаста, из которой вырастает
     «почему эти две кнопки разные». */
  .poolhead{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
  .poolhead h2{margin:0}
  .pool-dlall,.pool-tv{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,.pool-tv:hover{border-color:var(--primary);box-shadow:var(--sh-2)}}
  .pool-dlall .ic,.pool-tv .ic{width:16px;height:16px}

  /* ШТОРКА «Смотреть на телевизоре» (blox62 #1, public/tv-pair.js). Своей геометрии почти нет:
     .sheet уже держит ширину, прокрутку и потолок 90vh. Здесь только то, чего в проекте ещё не
     было, — КРУПНЫЕ ЦИФРЫ КОДА: их переписывают с телефона на пульт, глядя то на экран, то на
     телевизор, поэтому цифра стоит в своей клетке и не сливается с соседней. */
  .tvsheet .tv-step{margin:10px 0 6px;color:var(--muted);font-size:var(--fs-sm)}
  .tvsheet .tv-step b{color:var(--ink)}
  .tvsheet .tv-addr{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
  .tvsheet .tv-addr code{flex:1 1 auto;font-size:var(--fs-md);font-weight:700;color:var(--ink);
              background:var(--bg);border:1px solid var(--line);border-radius:var(--r-md);
              padding:10px 12px;overflow-wrap:anywhere}
  .tvsheet .tv-addr .btn-ghost{width:auto;flex:0 0 auto;margin-top:0}
  .tvsheet .tv-digits{display:flex;gap:6px}
  .tvsheet .tv-digits span{flex:1 1 0;text-align:center;font-weight:800;font-size:var(--fs-xl);
              letter-spacing:.04em;background:var(--bg);border:1px solid var(--line);
              border-radius:var(--r-md);padding:10px 0}
  .tvsheet .tv-left{margin-top:6px;color:var(--muted);font-size:var(--fs-sm)}
  .tvsheet .tv-dhead{margin:16px 0 6px;font-weight:700}
  .tvsheet .tv-dev{display:flex;align-items:center;justify-content:space-between;gap:10px;
              padding:8px 0;border-top:1px solid var(--line)}
  .tvsheet .tv-dev i{display:block;font-style:normal;color:var(--muted);font-size:var(--fs-sm)}
  .tvsheet .tv-dev .btn-ghost{width:auto;flex:0 0 auto;margin-top:0;min-height:44px}

  /* teacher tabs */
  /* segmented control, iOS-style: a soft track with a filled selected pill.
     ОДИН класс на все переключатели «одно из трёх»: вкладки Chats и панель Аудио/Видео/Микс на
     экране плейлиста (blox55 #14 — она была собрана заново инлайном, с цветами числом, и потому
     не слушалась цветовой схемы). Дорожка во ВСЮ ширину колонки, кнопки делят её поровну
     (flex:1) — так панель совпадает по краям с карточками под ней. */
  .tabs{display:flex;gap:4px;margin-bottom:18px;background:var(--fill-soft);
    border-radius:var(--r-md);padding:4px}
  .tabs button{
    flex:1;min-height:44px;                       /* палец: WCAG 2.5.5 */
    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)}}
  /* выбранная вкладка = ЯКОРЬ своей схемы (blox55 #14): не «сделано» и не тревога, а выбор —
     значит акцент схемы, а не зелёный/красный (§11). Радужная кнопка детского чата перекрывает
     это правило ниже (.tab-rainbow) и остаётся радужной в любой схеме. */
  .tabs button.on{background:var(--anchor);color:var(--on-dark);box-shadow:var(--sh-1)}
  /* у панели плейлиста свой ритм: она стоит между подписью и карточкой плеера */
  .pool-tabs{margin:12px 0 6px}

  /* 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)}
  /* «Запись» — дверь в инструмент записи задания (24.09): красный кружок, как у кнопки записи, но
     неподвижный — это вход, а не «идёт запись» */
  .src-tabs .src-dot{display:inline-block;width:12px;height:12px;border-radius:50%;background:currentColor;color:var(--danger);vertical-align:-1px}
  /* 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}
  /* КРАЙ ГОВОРИТ, ЧТО ТАМ ЕЩЁ ЕСТЬ. Полоса прокрутки спрятана, и без намёка спрятанные таблетки
     не существуют для учителя. Рецепт самонастраивающийся, без единой строчки JS: две «шторки»
     цвета листа приклеены к СОДЕРЖИМОМУ (background-attachment:local) и уезжают вместе с ним,
     а две тени приклеены к РАМКЕ (scroll) и стоят на месте — поэтому тень видна ровно с той
     стороны, где что-то спрятано, и гаснет, когда доехали до края. Тон тени — общий --sh-rgb,
     значит намёк следует за цветовой схемой сам. */
  /* ЦВЕТ ШТОРКИ ЗАВИСИТ ОТ ТОГО, НА ЧЁМ ЛЕЖИТ РЯД, и это не мелочь: за лентой групп на мониторе
     стоит ФОН СТРАНИЦЫ (замер 15.09.2026: body.teacher = #d6e2ea), а за полосой уроков — белый
     лист окна. Зашей я сюда --card, монитор получил бы два белых мазка по краям — тот самый
     близнецовый баг, когда правку видят на одном экране и не видят на другом. Поэтому контекст
     САМ объявляет, что у него за спиной; появится третье место — объявит своё. */
  .gstrip{--strip-bg:var(--bg)}
  .emodal .gstrip{--strip-bg:var(--card)}
  .gstrip{
    background:
      linear-gradient(90deg, var(--strip-bg) 40%, transparent) left center,
      linear-gradient(90deg, transparent, var(--strip-bg) 60%) right center,
      radial-gradient(farthest-side at 0 50%, rgba(var(--sh-rgb),.20), transparent) left center,
      radial-gradient(farthest-side at 100% 50%, rgba(var(--sh-rgb),.20), transparent) right center;
    background-repeat:no-repeat;
    background-size:28px 100%, 28px 100%, 12px 100%, 12px 100%;
    background-attachment:local, local, scroll, scroll}
  .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)}
  /* ОТМЕТКА «всё сделано» (blox59 #5). Была точка 8px цвета --checked на два смысла: зелёная —
     «порядок», красная — «ДЗ не выдано». Красную сняли — о тревоге и так кричит рамка чипа, а
     точка с рамкой повторяли друг друга. Оставшийся смысл один и положительный, поэтому и знак
     стал прямым: ГАЛОЧКА. Цвет — --ok-ink, а не заливочный --ok: у тонкого штриха контраст
     считается по линии, и целадон-500 на бумаге её бы утопил. Размер задан в em (иконка и так
     1.15em от текста), чтобы отметка ехала со шкалой шрифта чипа.
     blox59 #6: перо ЖИРНЕЕ (--icw 2.6 против 1.75 по умолчанию — толщину пускает внутрь чужого
     документа только эта переменная, снаружи стилем до штриха не дотянуться, см. icons.svg) и
     сама галочка ПИСАНАЯ, с изгибом обоих плеч (i-check-soft). */
  .gchip .gtick{width:1.15em;height:1.15em;color:var(--ok-ink);flex:0 0 auto;--icw:2.6}
  /* ТРЕВОГА ЧИПА — снова КРАСНАЯ, но на узком факте (blox59 #4, решение владельца 16.09.2026).
     История в двух шагах. Сначала (blox51 #3) чип красился ЦЕЛИКОМ по флагу r.alert — «нет ДЗ
     ИЛИ черновик ИЛИ есть непроверенные сдачи»; в живой школе это верно почти всегда, и лента
     краснела вся, поэтому цвет сняли. Потом (blox52 #11) остаток тревоги перевели в акцент
     схемы. Итог: экран перестал отличать «работа не сделана» от «работа в очереди».
     Возврат сделан не откатом, а СУЖЕНИЕМ предиката: класс .alert теперь ставится по r.nohw —
     ближайший урок не выдан (пусто или черновик), — а непроверенные сдачи красного больше не
     дают, у них свой счётчик в акценте. Таких чипов на ленте один-два, красный снова редок.
     Заливка чипа НЕ красится (это и была «красная стена»): тревогу несут точка и рамка.
     Правило стоит ПОСЛЕ :hover намеренно — под курсором тревога обязана остаться (тот же
     приём, что у .lday:hover перед состояниями дня).
     blox59 #5: тревога носит ТОЛЬКО плоскость и рамку. Красная точка ушла — она говорила ровно
     то же, что рамка, и была второй пометкой об одном факте; к тому же точку забирал себе
     счётчик «к проверке», и группа «ДЗ не выдано + 3 к проверке» теряла бы красный совсем.
     blox59 #6 (решение владельца): ЗАЛИВКА вернулась — как было до blox51 #3. Тогда её сняли
     из-за широкого флага r.alert, поднятого почти всегда; теперь чип красит r.alarm, который
     молчит и при непроверенных сдачах, и у групп с плейлистом, так что красных чипов на ленте
     единицы и стены из них не выходит.
     blox59 #7 (решение владельца): ИМЯ ГРУППЫ НЕ КРАСИМ. Красные чернила были третьей пометкой об
     одном факте (заливка · рамка · текст), и они перекрашивали единственное, что на чипе несёт
     СОДЕРЖАНИЕ, а не состояние: имя группы читают, чтобы найти нужную, а не чтобы узнать про ДЗ.
     Цвет текста остаётся общим — --muted у невыбранного чипа, --ink у выбранного. */
  .gchip.alert{border-color:var(--alert-line);background:var(--alert-tint)}
  /* НАВЕДЕНИЕ НА ТРЕВОЖНЫЙ ЧИП (blox59 #10). У обычного чипа наведение есть (правило выше), а у
     покрасневшего его не было ВООБЩЕ: .gchip.alert объявлен ПОЗЖЕ .gchip:hover и при большей
     специфичности перекрывал и рамку, и подложку — под курсором тревожный чип не шевелился, и
     человек переставал понимать, что он нажимается. Ответ тот же по СМЫСЛУ, но в своей семье
     цветов: подложка на ступень гуще (--alert-fill, 22% пигмента против 12% у тинта — это и есть
     «затемнение»), рамка глубже (сплошной --alert вместо бледного --alert-line), тень на ступень
     выше — ровно как обычный чип отвечает --accent-deep вместо --line.
     Стоит ПЕРЕД .gchip.on.alert намеренно: у ВЫБРАННОГО тревожного чипа подложка обязана остаться
     своей, он и так помечен полоской, — тот же закон, по которому обычный .gchip.on не берёт
     заливку наведения. */
  @media (hover:hover){.gchip.alert:hover{border-color:var(--alert);
    background:var(--alert-fill);box-shadow:var(--sh-2)}}
  /* 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)}
  /* ВЫБРАННЫЙ И ТРЕВОЖНЫЙ. .gchip.on объявлен ПОСЛЕ .gchip.alert и при равной специфичности
     перекрасил бы тревожный чип акцентом, стоит его выбрать (та же ловушка порядка, что в
     blox52 #9 с рамкой панели). Лечится не !important, а третьим классом в селекторе: тревога
     держит свою заливку, а «выбран» читается полоской .gind и рамкой погуще. */
  .gchip.on.alert{border-color:var(--alert);background:var(--alert-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}
  /* ПК (blox69 #1): гамбургер раскрыт — его пункты стоят обычными кнопками у левого края строки,
     Post+Chat остаются правой кучкой. Ключ — мышь (hover+fine), а не одна ширина: планшет в
     альбомной ориентации широк, но палец там тот же, и гамбургер ему удобнее. От 900px: уже
     шести кнопкам в ряд тесно. Меню не
     пересобирается и обработчики не меняются — только раскладка тех же узлов, поэтому `hidden`
     перебивается здесь (display в CSS сильнее атрибута — правило 7 skill). */
  @media (min-width:900px) and (hover:hover) and (pointer:fine){
    .gkebab{display:none}
    .gkebab-menu,.gkebab-menu[hidden]{display:flex;position:static;flex-direction:row;align-items:center;
      gap:8px;min-width:0;padding:0;background:none;box-shadow:none;border-radius:0}
    .gkebab-menu .btn-ghost,.gkebab-menu .btn{width:auto;white-space:nowrap}
  }
  /* blox69 #3 — «Другие группы» у методиста. Нажатая кнопка ВДАВЛЕНА (владелец: «чтобы кнопка
     как бы зажималась»): внутренняя тень нажатия и тон колодца держатся, пока ряд учителей открыт.
     Сам ряд — те же вкладки .sb-tab, что у переключателя учителей в расписании Лу. Прячется
     атрибутом hidden, а у .sb-tabs свой display:flex, который атрибут перебил бы (правило 7). */
  .ghead-tools .gt-others.on{box-shadow:var(--sh-press);background:var(--surface);
    border-color:var(--accent-deep);color:var(--accent-ink)}
  .gteach{margin:-4px 0 12px}
  .gteach[hidden]{display:none}
  /* blox72 #1 — РЕЖИМ ПРОСМОТРА чужой группы (body.ro-view ставит монитор, core.js setRoView).
     Всё, что меняет данные, экраны помечают data-w — здесь оно пропадает целиком. !important
     нарочно: у кнопок свои display (flex/inline-flex), и тихо проигрывать им нельзя — оставшаяся
     кнопка записи и есть та дыра, которую режим закрывает. Сервер всё равно ответит 403. */
  body.ro-view [data-w]{display:none !important}
  /* плашка «Только просмотр» над лентой групп: говорит, ПОЧЕМУ нет кнопок правки */
  .ro-note{display:none}
  body.ro-view .ro-note{display:flex;align-items:center;gap:6px;margin:-4px 0 10px;padding:6px 10px;
    border-radius:var(--r-sm);background:var(--surface);color:var(--muted);font-size:.9em}
  /* телефон (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;
      min-width:32px;min-height:32px}   /* цель касания ≥24px даже в тесной шапке (axe target-size) */
  }

  /* 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(--alert)}
  /* ПАНЕЛЬ-КРАЙ — пустая линованная страница журнала (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(--alert);
    animation:nagpulse 2.6s ease-in-out infinite}
  /* ПУЛЬСАЦИЯ ближайшего занятия БЕЗ ЛИСТА (вернулась в blox59 #4). Старая версия (shimmerGold)
     мигала в ЗАШИТЫЙ #fff — ярче нашей бумаги в любой схеме и просто вспышка белым ночью;
     поэтому её и сняли вместе с цветом. Здесь обе опоры схемные: 0/100% — прозрачность (у
     .opane фон прозрачный, так что это и есть «как у стола»), 50% — --alert-tint своей схемы.
     Пульсирует ТОЛЬКО пустой лист: у черновика панель обычно уже открыта в конструкторе, и
     дышащий фон под тасками — шум; ему достаточно красной рамки .opane.next.nag.
     Движение гасится по prefers-reduced-motion, но факт остаётся: вместо анимации — статичный
     тинт, иначе выключенное движение отбирало бы у листа половину сигнала. */
  @keyframes nagpulse{0%,100%{background-color:transparent}50%{background-color:var(--alert-tint)}}
  @media (prefers-reduced-motion:reduce){
    .opane.empty.urgent{animation:none;background-color:var(--alert-tint)} }
  .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-тени нет;
     граница «жива/мертва» честная. */
  /* blox59 #13 — зажатие плашки держит подсветку, и долгое нажатие на телефоне не должно
     параллельно запускать родное «выделить слово» (Android Chrome и iOS Safari одинаково) и
     меню «Копировать». user-select в двух формах — Safari без префикса понимает его лишь с 16.4;
     touch-callout есть только у Safari, остальные его пропускают. Свойства наследуются: ссылка
     в заголовке тоже не выделяется, но кликается как прежде. */
  .oent-live{cursor:pointer;transition:box-shadow var(--t) var(--ease);
    -webkit-user-select:none;user-select:none;-webkit-touch-callout:none}
  @media (hover:hover){.oent-live:hover{box-shadow:var(--sh-2)}}
  /* blox70 #2 — тап по ЛЮБОЙ живой плашке открывает «сдачи по таску» (task-review.js), так что
     нажимаемой она выглядит и без списка «не сделали». Выделение текста гасим тем же рецептом. */
  .oent-open{cursor:pointer;transition:box-shadow var(--t) var(--ease);
    -webkit-user-select:none;user-select:none;-webkit-touch-callout:none}
  @media (hover:hover){.oent-open:hover{box-shadow:var(--sh-2)}}
  /* аудит blox70 #2 (#12): плашка встаёт в Tab-цепочку, Enter/пробел открывают экран таска */
  .oent-open:focus-visible{outline:none;box-shadow:var(--ring)}
  .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)}
  /* blox53 #16 — контрольных файлов на уроке бывает несколько, и каждый идёт своей плашкой под
     наслушкой плейлиста. Панели .plonly, для которой писалось прежнее правило, больше нет
     (blox56 #2): плашки стоят внутри .plstats, а колонку им задаёт она. */
  .plstats{display:flex;flex-direction:column;align-items:stretch;gap:9px}
  /* blox56 #5 — СПИСОК «Из плейлиста» в окне таска: плоские строки-кнопки, одна на файл. Нарочно
     не плитки проводника: тут не ходят по папкам, тут один тап по имени — и файл выбран. */
  .pl-poolpick{display:flex;flex-direction:column;gap:4px;margin-top:6px;max-height:240px;overflow:auto}
  .pl-poolrow{display:flex;align-items:center;gap:8px;width:100%;text-align:left;
    padding:8px 10px;background:var(--card);border:1px solid var(--line-soft);
    border-radius:var(--r-xs);cursor:pointer;font-size:var(--fs-sm);font-weight:700;color:var(--ink)}
  @media (hover:hover){.pl-poolrow:hover{border-color:var(--accent-deep)}}
  .pl-poolrow .pl-nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .pl-poolrow svg{width:16px;height:16px}
  .plctrl-none{padding:2px 4px 4px}
  .oent.plbig{gap:6px}
  /* blox56 #2 — те же строки живут теперь ВНУТРИ обычной плашки плейлиста (развёрнутый вид), а не
     только в большой плашке бывшей playlist-группы: предок .plbig из селекторов убран. */
  .plb-sec{margin-top:9px;font-size:var(--fs-xs);font-weight:800;color:var(--muted);
    display:flex;align-items:center;gap:6px}
  .plb-sec svg{width:14px;height:14px;opacity:.75}
  .plb-sec.plb-ctrl{color:var(--accent-dark)}
  .plb-list{display:flex;flex-direction:column;gap:4px;margin-top:5px}
  /* blox56 #8 — ИМЯ И ЦИФРА ВСТРЕЧАЮТСЯ У СЕРЕДИНЫ. Было justify-content:space-between: имя
     прижато к левому краю, число — к правому, и на широкой панели между «Liza» и «7» лежала
     ладонь пустоты; глаз терял строку и читал чужое число. Делим плашку пополам (по сетке, не
     линией): имена прижаты к ПРАВОМУ краю левой половины, числа — к ЛЕВОМУ краю правой. Пара
     стоит вплотную к невидимой середине, и длина имени больше не растаскивает их. min-width:0 —
     чтобы длинное имя резалось многоточием, а не распирало свою колонку. */
  .plb-row{display:grid;grid-template-columns:1fr 1fr;align-items:center;gap:0 10px;
    padding:5px 10px;background:var(--card);border:1px solid var(--line-soft);border-radius:var(--r-xs)}
  .plb-name{min-width:0;justify-self:end;text-align:right;
    font-weight:700;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .plb-n{min-width:0;justify-self:start;text-align:left;
    font-weight:800;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .plb-row.zero .plb-n{color:var(--muted);font-weight:600}
  .plb-none{padding:6px 2px}
  /* blox56 #2 — ШЕВРОН ВИДА в углу плашки плейлиста. Не второе равное действие рядом с дверью, а
     настройка самой плашки, поэтому он мелкий, без подписи и лежит в углу; 28px — минимум, ниже
     которого палец мажет (полный 24px-квадрат внутри + воздух). Развёрнутый вид поворачивает тот
     же шеврон, отдельной иконки «вверх» для этого не нужно. */
  .oent.pool{position:relative}
  /* дверь плашки — кнопка, но выглядит текстом плашки (см. poolPlateHTML) */
  .pool-door{all:unset;display:inline-flex;align-items:center;gap:inherit;min-height:24px;cursor:pointer;font:inherit;color:inherit}   /* ≥24px — цель касания (axe target-size) */
  .pool-door:focus-visible{outline:2px solid var(--ring-c);outline-offset:2px;border-radius:var(--r-xs)}
  /* blox72 #4 — ЗОНА НАЖАТИЯ ≠ ЗНАЧОК. Было 28×28 в углу, а вся остальная плашка — дверь в «Плейлист»:
     учитель целился в стрелку, промахивался на палец и улетал с монитора. Теперь кнопка — полоса во
     всю высоту свёрнутой плашки у правого края (56×64: палец не промахнётся), а видимый значок —
     кружок 32px в её середине, чтобы в покое было видно, что это отдельная кнопка (lingo-ui §12).
     Высота фиксирована, а не до низа: в развёрнутом виде ниже лежит наслушка, её строки полоса
     перекрывать не должна. Поворачивается только значок — коробка прозрачная. */
  .pool-vw{position:absolute;top:0;right:0;width:56px;height:64px;display:grid;place-items:center;
    padding:0;border:0;border-radius:0 var(--r-sm) 0 0;background:transparent;color:var(--muted);cursor:pointer}
  .pool-vw svg{width:20px;height:20px;padding:6px;box-sizing:content-box;border-radius:50%;
    background:var(--fill-soft);transition:transform var(--t) var(--ease),color var(--t) var(--ease),background var(--t) var(--ease)}
  @media (hover:hover){.pool-vw:hover svg{background:var(--line-soft);color:var(--ink)}}
  .pool-vw:focus-visible{outline:none}
  .pool-vw:focus-visible svg{box-shadow:var(--ring)}
  .pool-vw.on{color:var(--ink)}
  .pool-vw.on svg{transform:rotate(180deg)}
  @media (prefers-reduced-motion:reduce){.pool-vw svg{transition:none}}
  .oent.pool .plstats{margin-top:8px}
  /* имя прикреплённого файла — в плашке Листа ДЗ и в карточке разбора ученика */
  .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}
  /* blox59 #14 — имя файла таска у учителя (монитор, личка) — кнопка, файл открывается нашим
     плеером / листалкой PDF, как у ученика. Сброс кнопки как у .pool-door; выглядит ссылкой. В
     строке .ofile она flex-ребёнок и сама режет длинное имя многоточием (иначе обрезал бы
     контейнер, а подчёркивание уезжало бы за край); в заголовке .oent-t.big/.srtitle — inline,
     наследует перенос заголовка. */
  .ofile-a{all:unset;display:inline;cursor:pointer;font:inherit;color:var(--accent-dark);
    text-decoration:underline;text-underline-offset:2px}
  .ofile-a:focus-visible{outline:2px solid var(--ring-c);outline-offset:2px;border-radius:var(--r-xs)}
  .ofile .ofile-a{display:block;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);white-space:pre-wrap}
  .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}
  /* pre-line, а не normal: тот же сборный таск стоит заголовком и здесь (лесенка taskHeadline),
     и строка на упражнение обязана дожить до монитора. Обрезка в две строки остаётся. */
  .oent-t.big{font-size:var(--fs-md);font-weight:800;line-height:1.3;white-space:pre-line;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}
    /* НА ТЕЛЕФОНЕ ЧИПЫ УЧЕНИКОВ СТОЯТ НА ТОЙ ЖЕ ЛЕВОЙ ОСИ, ЧТО ПЛАШКИ ТАСКОВ (владелец,
       17.09.2026). В столбик (десктоп) ростер — соседняя колонка, и своя вертикаль ему законна;
       на телефоне колонка разворачивается в строку ПОД плашками, и разница в 10px читалась как
       съехавший блок. Отступы те же, что у .oent.loose (11/10) — одна и та же ось у плашки вне
       рамки, у плашки в рамке (.oframe padding 10px) и теперь у чипов.
       Отступ 10px, а не 11: одиннадцать у .oent.loose компенсируют ЕЩЁ И рамку темы, внутри
       которой стоят плашки её тасков, — у чипов такой рамки нет (замер: плашка в рамке 39px
       от края экрана, чипы с 11px давали 40). */
    .oroster{flex:1 1 auto;flex-direction:row;flex-wrap:wrap;margin:0 10px}
    .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}
  /* blox63 #2 — та же правка, что у ребёнка (#8): иконка типа живёт В СТРОКЕ типа (.bm), а не
     отдельной колонкой слева. Размер по строке меты, не по заголовку. */
  .bitem .bico{font-size:var(--ic-md)}
  .bitem .binfo{flex:1 1 100%;min-width:0;overflow-wrap:anywhere}
  /* кнопки действий уходят на свою строку под текстом, вся тройка прижата вправо */
  .bitem .act:first-of-type{margin-left:auto}
  /* blox58 #6: СБОРНЫЙ таск («выбрать несколько» в учебнике) приходит строкой на упражнение —
     переводы строк в нём несущие, а HTML их схлопывает, и шесть упражнений читались сплошняком.
     pre-line (а не pre-wrap): переносы сохраняем, но отступы разметки шаблона не печатаем —
     рядом с текстом в .bt стоят чипы «для:» и «челлендж». */
  .bitem .bt{font-weight:700;font-size:var(--fs-md);white-space:pre-line}   /* тот же размер, что у трёх других карточек */
  .bitem .bm{font-size:var(--fs-xs);color:var(--muted);margin-top:2px;
    display:flex;align-items:center;gap:6px;flex-wrap:wrap}
  .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-tint; ниже — почему
     он стал залитой кнопкой. Цвет по-прежнему берётся токенами роли --ok-* (не сырой лестницей). */
  /* ОБЪЁМНЫЙ, А НЕ ПУНКТИРНЫЙ (владелец, 16.09.2026) — и с 17.09 он ЗЕЛЁНАЯ ПЛАШКА, а не
     кнопка-полоса (blox59 #17, выбран из шести видов на живом листе). Промахов было два, оба
     владелец назвал сам: бледный пунктир читался «блёклым месивом», а залитая полоса во всю
     ширину (копия .btn-pub) легла на экран тяжело — слот стоит внизу КАЖДОЙ темы плюс один вне
     тем, и на листе из трёх тем это четыре громких прямоугольника. Поэтому:
       · РАЗМЕР кнопки, а не полосы: width:fit-content по центру — площадь втрое меньше;
       · МАТЕРИАЛ плашки таска: тот же лист (--sheet-1: обрезной край + тиснение + тень) и
         тот же ВДАВЛЕННЫЙ РЕЛЬС сбоку (--emb-rail/--emb-groove, рецепт .rect/.bitem/.oent/
         .srcard выше), только --rail БЕЛЫЙ: у настоящих плашек цвет рельса говорит про вид
         задания, у пустого слота говорить нечего — рельс бесцветный;
       · СКРУГЛЕНИЕ --r-md, а не --r-lg: на коробке в 53px радиус 18px съедал по половине
         высоты с каждого угла, и прямоугольник читался пилюлей;
       · ЗАЛИВКА РАЗБАВЛЕНА НА 15% — прозрачна именно она, а не весь предмет: opacity погасил
         бы и подпись, и рельс, да ещё поставил бы вид кнопки в зависимость от подложки (тот же
         урок, что у гашения .lcar-arr:disabled выше). Белые чернила и рельс остаются в силу.
     ЦВЕТ — АКЦЕНТ СВОЕЙ СХЕМЫ (владелец, 17.09.2026, вторая правка того же дня): слот носит тот же
     --grad-primary, что кнопка-решение .btn-pub, а зелёное ушло туда, где ему и место по закону
     цвета, — на «✓ Готово» (см. .btn-pub.btn-done ниже). Заодно снялось исключение .eslot из
     реестра GREEN_OK: сплошного зелёного у слота больше нет, спорить со словом «принято» нечем.
     ЗАЛИВКА РАЗБАВЛЕНА НА 20% (владелец, 17.09.2026) — и прозрачна именно она, а не весь предмет:
     opacity погасил бы подпись с рельсом и поставил бы вид кнопки в зависимость от подложки.
     Собрана НЕ из --grad-primary (тот объявлен сырыми hex в каждой схеме, разбавить его на месте
     нечем), а из --anchor — якоря схемы, который и есть нижний стоп этого градиента во всех
     четырёх палитрах; верхний стоп получается тем же подмесом белого, что у --grad-accept. */
  /* ВЫСОТА У ОБЕИХ КНОПОК КОНСТРУКТОРА ОДНА (владелец, 17.09.2026): слот и «+ theme» стоят на
     листе друг под другом, и разница в 5px читалась как небрежность. Число живёт ОДИН раз, в
     --slot-h у общего селектора ниже, и обе кнопки берут его min-height'ом — иначе высоты
     разъехались бы при первой же правке отступов у одной из них. */
  .eslot{display:flex;align-items:center;justify-content:flex-start;gap:10px;
    /* по ЛЕВОМУ краю (владелец, 17.09.2026): слот встаёт на ту же вертикаль, что плашки тасков
       над ним, и его белый рельс оказывается на одной линии с их цветными — ряд читается колонкой,
       а не тремя предметами вразнобой. margin-right:auto держит ширину по содержимому. */
    width:fit-content;margin:0 auto 0 0;min-height:var(--slot-h);
    border:1px solid transparent;border-radius:var(--r-md);padding:16px 22px 16px 28px;
    /* РЕГИСТР ПОДПИСИ ЖИВЁТ В СТРОКЕ, А НЕ В CSS (владелец, 17.09.2026): нужно «+ Theme task» —
       с большой только первое слово, — а это не выражается ни `uppercase`, ни `capitalize`
       (он поднимает КАЖДОЕ слово). Поэтому подписи написаны как надо в builder.js, а
       text-transform тут больше нет. До этого за полчаса перепробованы капс и capitalize. */
    color:var(--on-dark);font-weight:700;font-size:var(--fs-sm);cursor:pointer;
    background:linear-gradient(180deg,
      color-mix(in srgb, color-mix(in srgb, var(--anchor) 88%, var(--on-dark)) 80%, transparent),
      color-mix(in srgb, var(--anchor) 80%, transparent));
    box-shadow:var(--sheet-1);
    transition:box-shadow var(--t) var(--ease);
    /* рельс — тот же псевдоэлемент и те же ручки, что у четырёх тиснёных плашек */
    --rail:var(--on-dark);--rail-eff:var(--rail);--rail-x:6px;--rail-y:7px;--rail-w:7px;
    --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%);
    /* слот вошёл в SURFACES (public/ripple.js): position:relative — якорь коробки волны,
       а --ripple-blend:normal потому, что на залитой роли волна белая (см. .btn--accept) */
    position:relative;-webkit-tap-highlight-color:transparent;--ripple-blend:normal}
  .eslot::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)}
  @media (hover:hover){.eslot:hover{box-shadow:var(--sheet-2)}}
  .eslot .dots{font-size:var(--ic-lg);letter-spacing:3px;line-height:1}
  /* blox45 #13 — «+ тема» рисуется как ПУСТАЯ тема: та же скобка-рамка и та же ширина, что
     появится после нажатия, только ПУНКТИРНАЯ. Рамка — тот же псевдоэлемент, что у .theme
     (см. .tframe,.oframe,.theme выше) — значит и ориентация у неё та же.
     До blox45 это была узкая белая кнопка на левой линии «+ таск вне темы» (blox13 #4). */
  /* УЖАТА ДО ПОДПИСИ И ПРИЖАТА ВПРАВО (владелец, 17.09.2026 — blox59 #17). Пустая тема во всю
     ширину занимала на листе столько же места, сколько настоящая, и спорила с ним: лист ДЗ читался
     как «тема, тема, ещё одна тема». Теперь это узкая скобка по ширине подписи у ПРАВОГО края —
     там же, где закрытый край рамки темы, так что кнопка стоит на её линии и читается её началом.
     Штрих вдвое толще (3px): на коробке в 50px прежний волосок 1.5px рядом с зелёной плашкой
     пропадал. Это исключение в BW_OK (tools/check.js) — скобка тут не граница содержимого,
     а САМ ПРЕДМЕТ, нарисованная пустая тема.
     --fpad/--frail обнулены В ПИКСЕЛЯХ, и это не придирка: у полосы во всю ширину они подтягивали
     усы внутрь, к краю карточек, а у узкой кнопки из-за них подпись оказывалась СНАРУЖИ скобки.
     Написать `0` без единицы нельзя — `calc(0 + 0)` невалиден, правило отбрасывается целиком, и
     рамка молча исчезает (проверено). */
  /* --slot-h — одна высота на слот и на «+ theme» (см. комментарий у .eslot выше) */
  .eslot,.newtheme{--slot-h:56px}
  /* УСЫ ДОТЯНУТЫ ДО СЕРЕДИНЫ ЛИСТА (владелец, 17.09.2026): ширина кнопки — ровно 50% листа ДЗ,
     подпись прижата к её правому краю. Тянуть сам псевдоэлемент влево было бы нечестно — его
     проценты считаются от КНОПКИ, а не от листа, и «половина» уехала бы при любой правке подписи;
     ширину же 50% лист задаёт сам. Скобка при этом по-прежнему рисует ровно коробку кнопки. */
  .newtheme{position:relative;display:flex;align-items:center;justify-content:flex-end;
    width:50%;min-height:var(--slot-h);box-sizing:border-box;
    --fpad:0px;--frail:0px;background:transparent;border:none;
    padding:0 22px;margin:0 0 18px auto;cursor:pointer;text-align:center;
    font-family:inherit;font-size:var(--fs-input);font-weight:800;color:var(--accent-dark)}
  /* скругление --r-md, а не --r-xl: на коробке в 56px радиус 24px закруглял правый край почти
     в полукруг, и скобка читалась пилюлей — та же правка, что у слота (blox59 #17) */
  .newtheme::before{content:"";position:absolute;top:0;bottom:0;
    left:calc(var(--fpad) + var(--frail));right:0;
    border:3px dashed var(--frame-line);border-left:none;
    border-radius:0 var(--r-md) var(--r-md) 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 — тёплый в базовой, свой в каждой схеме. */
  /* overscroll-behavior:contain — жест ОСТАЁТСЯ в окне (айфон-PWA). Без него, когда содержимое
     окна прокручено до края (а чаще — когда прокручивать в нём нечего), палец продолжает тащить
     страницу ПОД окном: владелец видит, как за открытым «Новым таском» уезжает монитор. Тот же
     приём уже стоит на длинных телах внутри окна и в проводнике. */
  /* Верхний отступ считается от БЕЗОПАСНОЙ ЗОНЫ. В установленном приложении на айфоне строка
     состояния прозрачная и лежит ПОВЕРХ страницы: с жёсткими 36px окно (а с ним и крестик в его
     углу) оказывалось под часами. max(...) страхует обратную сторону — в браузере env равен
     нулю, и отступ остаётся ровно прежним, 36px. */
  .emodal{position:fixed;inset:0;background:var(--scrim);display:flex;align-items:flex-start;
    justify-content:center;z-index:60;overflow:auto;overscroll-behavior:contain;
    padding:max(36px, calc(env(safe-area-inset-top, 0px) + 14px)) 14px 36px}
  /* СТРАНИЦА ПОД ОТКРЫТЫМ ОКНОМ НЕ ЕДЕТ (core.js: lockPageScroll). Рецепт держится на
     position:fixed у самого body: только он на iOS убирает страницу из потока насмерть —
     одного overflow:hidden телефону мало, а overscroll-behavior выше работает лишь там, где
     внутри окна есть что прокручивать. Отметку (top:-Npx) ставит и снимает JS, он же
     возвращает прокрутку на место при закрытии. */
  body.scroll-locked{position:fixed;left:0;right:0;width:100%;overflow:hidden}
  /* 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)}}
  /* «✓ ГОТОВО» — ЗЕЛЁНАЯ (владелец, 17.09.2026). Единственная кнопка .btn-pub, которой зелёный
     положен по закону цвета: она не решение и не выдача, а «я закончил править» — то самое
     «сделано», за которым зелёная заливка и закреплена. Класс отдельный намеренно: на черновике
     ту же .btn-pub носит «Выдать ученикам», и она обязана остаться акцентом схемы — покрась мы
     весь класс, зелёный снова значил бы два разных дела. Записана в GREEN_OK (tools/check.js). */
  .btn-pub.btn-done{background:var(--grad-accept)}
  @media (hover:hover){.btn-pub.btn-done:hover{box-shadow:var(--bevel),0 6px 18px color-mix(in srgb,var(--ok-solid) 40%,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}
  /* Ряд УРОКОВ внутри юнита (Focus 3). Переиспользует .gstrip/.gchip с экрана учителя — здесь
     только двухстрочная начинка таблетки: номер урока и его раздел. Полоса прокручивается вбок,
     как ряд групп, поэтому девять уроков помещаются и на 375px. */
  .lstrip{margin-bottom:12px}
  .lchip{flex-direction:column;align-items:flex-start;gap:1px;padding:7px 13px;line-height:1.2}
  .lchip .lno{font-weight:800;font-size:var(--fs-sm)}
  .lchip .lsec{font-weight:600;font-size:var(--fs-2xs);color:var(--faint)}
  /* ВЫБРАННЫЙ обязан читаться сильнее НАВЕДЁННОГО. У .gchip «выбран» — только подложка, а наведение
     рисует ещё и акцентную рамку: на снимке 1280px таблетка под курсором выглядела выбранной, а
     настоящая выбранная — нет. Поэтому выбранному даём рамку акцентными ЧЕРНИЛАМИ (гуще, чем
     --accent-deep у наведения) — правило идёт после, поэтому побеждает при равной специфичности. */
  .lchip.on{border-color:var(--accent-ink)}
  .lchip.on .lsec{color:var(--accent-dark)}
  /* ВИДЕО урока: принадлежит уроку целиком, не строке таблицы, поэтому стоит НАД сеткой
     упражнений отдельным рядом. Пустой урок — пустой .vid-wrap нулевой высоты: блок существует
     всегда, чтобы переключение урока не меняло высоту окна (правило «высота инвариантна»). */
  .vid-wrap:empty{display:none}
  .vid-row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
  .vid-tile{display:flex;align-items:center;gap:7px;cursor:pointer;min-height:44px;
    padding:8px 13px;border-radius:var(--r-md);border:1px solid var(--accent-line);
    background:var(--accent-tint);color:var(--accent-dark);font-weight:700;font-size:var(--fs-sm)}
  @media (hover:hover){.vid-tile:hover{border-color:var(--accent-deep);box-shadow:var(--sh-2)}}
  .vid-tile:active{transform:scale(.97)}
  .vid-lbl{overflow-wrap:anywhere}
  .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)}}
  /* blox58 #6 — режим «выбрать несколько»: выбранная плитка/хотспот, кнопка режима, строка «Выбрано N» */
  .ex-tile.sel{border-color:var(--accent-deep);background:var(--accent-tint);box-shadow:inset 0 0 0 2px var(--accent-deep)}
  .pg-hot.sel{border-style:solid;border-color:var(--accent-deep);background:color-mix(in srgb, var(--accent-deep) 28%, transparent)}
  .pk-multi[aria-pressed="true"]{background:var(--accent-tint);border-color:var(--accent-deep);color:var(--accent-dark)}
  .pk-selbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 10px;padding:8px 12px;
    border-radius:var(--r-md);background:var(--accent-tint);font-size:var(--fs-sm)}
  .pk-selbar .pk-cnt{font-weight:700}
  .pk-selbar .pk-warn{color:var(--alert-ink)}
  .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)}
  /* плитка правила (type 'info'): приглушённая, с подписью «правило» рядом с номером */
  .ex-tile.ex-info{background:var(--surface-2)}
  .ex-kind{margin-left:8px;font-size:var(--fs-xs);font-weight:600;color:var(--muted)}
  /* ── ВИД «СТРАНИЦА» окна «из учебника» (blox57 #2). Переключатель список/страница + сама
     страница учебника с прозрачными кнопками поверх упражнений. */
  /* ШАПКА ОКНА: название слева, тумблер вида справа — одной строкой (владелец, 15.09.2026:
     «переместить рядом с названием юнита»). Три вещи, которые эта строка обязана держать:
     · тумблер НЕ жмётся и НЕ переносится (flex:0 0 auto) — иначе длинное имя юнита сдвинет его
       вниз и вернёт ту самую строку, ради экономии которой всё и затевалось;
     · длинное имя РЕЖЕТСЯ троеточием, а не упирается в тумблер (min-width:0 обязателен: без него
       flex-ребёнок не ужимается ниже своего содержимого и overflow не наступает вовсе);
     · справа зарезервировано место под крестик .eclose — он лежит абсолютом в углу панели. */
  .pk-head{display:flex;align-items:center;gap:12px;margin-bottom:14px;padding-right:30px}
  .pk-head h3{flex:1 1 auto;min-width:0;margin:0}
  /* Троеточие — ТОЛЬКО когда рядом стоит тумблер. На телефоне его нет вовсе, и там длинное имя
     по-прежнему переносится на вторую строку: резать его там не за что — соперника по строке нет,
     а обрезанное имя юнита это потерянная подсказка, где учитель находится. */
  .pk-head:has(.pgsw) h3{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .pk-head .pgsw-wrap{flex:0 0 auto}
  /* сам тумблер — общий .onotes (вид описан выше, у монитора); здесь только его поля в шапке */
  .pgsw{margin:-10px -8px -10px 0}
  /* РАСТУЩИЙ РЕБЁНОК ОБЯЗАН БЫТЬ ОГРАНИЧЕН (lingo-ui §3, самая переподаваемая жалоба проекта).
     Страница A4 при ширине окна 840px — это ~1190px высоты, разворот вдвое больше: без потолка
     низ окна уехал бы за край экрана. Поэтому страницы листают ВНУТРИ своей области, и высота
     окна не зависит от того, одна их тут или пять. */
  .pg-view{overflow:auto;overscroll-behavior:contain;margin-bottom:14px;
    border:1px solid var(--line);border-radius:var(--r-md);background:var(--surface);padding:8px}
  /* ОКНО СО СТРАНИЦЕЙ ОБЯЗАНО ВЛЕЗАТЬ В ЭКРАН ПО ВЫСОТЕ. Первая редакция дала области потолок
     62vh — и этого НЕ ХВАТИЛО: замер показал, что на экране 800px низ окна («или ввести вручную»)
     уходит за край на 58px, на 700px — на 96px. Виноват не потолок, а то, что над областью стоят
     шапка, ряд уроков, ряд видео и переключатель, и их высоту 62vh не знает.
     Механизм тот же, что у окна таска (blox30 #1), и магических констант в нём нет: панель
     ограничена вьюпортом, все её дети не жмутся, а ужимается РОВНО ОДИН — область со сканами.
     Сколько заняли шапка и ряды, столько ей и осталось; низ окна при этом прибит к экрану.
     Сначала правило включалось только в виде «страница» (:has(.pg-view)), а список «вёл себя как
     прежде» — и прежде значило: у Focus 3 в юните до 99 упражнений, панель вырастала за экран, и во
     ВПИСАННОМ окне (body.taskdock — панель position:fixed с потолком, .emodal не прокручивается) низ
     списка и кнопка «или ввести вручную» просто отрезались (владелец, 15.09.2026: «кнопки „или
     вручную“ даже не видно»). Поэтому :has(.ex-body) — оба вида; в списке прокручивается сам
     .ex-body (у страницы свой скролл — .pg-view, его следящий wireSpy слушает именно его). */
  .emodal .panel:has(.ex-body){display:flex;flex-direction:column;max-height:calc(100dvh - 2 * var(--modal-gap))}
  .emodal .panel:has(.ex-body) > *{flex-shrink:0}
  .emodal .panel:has(.ex-body) > .ex-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
  .ex-body:has(> .ex-grid){overflow:auto;overscroll-behavior:contain}
  .ex-body > .pg-view{flex:1 1 auto;min-height:140px}
  .pg-sheet{position:relative;width:100%}
  .pg-sheet + .pg-sheet{margin-top:8px}
  .pg-sheet img{display:block;width:100%;height:auto;border-radius:var(--r-sm)}
  /* Хотспот виден и В ПОКОЕ — пунктирный контур: кликабельное обязано выглядеть кликабельным до
     нажатия (lingo-ui §12). Наведение заливает его полупрозрачным акцентом, и текст книги под
     заливкой остаётся читаемым — это не украшение: НИЗ бокса ВЫВЕДЕН (до следующего номера в
     колонке), а не прочитан из книги, и учитель обязан видеть границы того, что выберет, ДО
     клика. Честность вместо мнимой точности. */
  .pg-hot{position:absolute;border:1px dashed var(--accent-line);background:transparent;
    border-radius:var(--r-sm);cursor:pointer;padding:0}
  /* Своего номера хотспот НЕ несёт: книга печатает его сама, ровно в левом верхнем углу рамки —
     вторая цифра поверх первой была бы лишними чернилами. Номер для слепого чтения живёт в
     aria-label, подсказка с формулировкой — в title. */
  @media (hover:hover){
    .pg-hot:hover{border-style:solid;border-color:var(--accent-deep);
      background:color-mix(in srgb, var(--accent-deep) 20%, transparent)}
  }
  /* КНОПКА НА ПЕЧАТНОЙ ССЫЛКЕ «For the worksheet, go to page 116.» (blox57 #2). Та же механика, что
     у хотспота — проценты от страницы, — но роль другая: не «выбрать упражнение», а «перейти по
     полотну». Поэтому и вид другой: точечный контур вместо пунктира, и стоит ПОВЕРХ хотспота
     (z-index): на стр. 10 строка ссылки напечатана внутри рамки упражнения 8, и без этого клик
     по ней ставил бы упражнение. Ссылка — это ДАННЫЕ книги: где строки нет, нет и кнопки. */
  .pg-link{position:absolute;z-index:1;border:1px dotted var(--accent-deep);background:transparent;
    border-radius:var(--r-sm);cursor:pointer;padding:0;min-height:22px}
  @media (hover:hover){
    .pg-link:hover{border-style:solid;background:color-mix(in srgb, var(--accent-deep) 20%, transparent)}
  }
  .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 — телефон: кнопки типов идут ПАРАМИ, пара делит строку пополам. Было 2+2+3 (семь
     типов, последняя тройка в строку); с восьмым типом Home writing (обращение №103) — четыре
     ровные пары: аудио-пара · Sing + Watch · Read + Copybook · Do + Home writing. Порядок кнопок =
     порядок TASK_TYPES в core.js. */
  .recpick{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin-top:8px}
  /* 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}
  /* blox61 #6 — ПРИЧИНА серых кнопок, строкой под пикером. Подсказки по наведению тут не работают
     (у .recb.off pointer-events:none), а на телефоне наведения нет вовсе — поэтому причина написана
     обычным текстом: её видно сразу, без тапа и без мыши. */
  .recbox{position:relative}
  .rechint{position:absolute;top:100%;left:0;right:0;margin-top:1px;
    font-size:var(--fs-2xs);color:var(--faint);line-height:1.2;pointer-events:none;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .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(8,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), который сам добирает/отдаёт разницу. Стало выше поле «Текст» — середина ужалась ровно на
     столько же; сумма (а значит и низ окна) неизменна САМА, без правки резерва. Резерв — ОДНО число на
     ширину (телефон/ПК), заданное под самый высокий вид «Файл»; всё прочее выводится из него. */
  /* ЗАЗОР ОКНА над и под панелью — одно число, --modal-gap (:root). На ПК он 10px вместо 22:
     окну таска и окну учебника на ноутбуке не хватало высоты, а 24px воздуха ничего не решали
     (владелец, 15.09.2026: «прибавку на ПК делаем»). Телефон НЕ трогаем — там 22px остаются.
     Честная сноска: правило .emodal выше с max(36px, safe-area) ЭТОЙ строкой перекрыто давно, на
     телефоне зазор тоже 22px (замер 375px: paddingTop 22px) — если ✕ окажется под часами в
     установленном приложении, чинить надо здесь, а не там. Потолки панелей ниже считают от того же
     токена, чтобы менять зазор в одном месте. */
  .emodal{padding-top:var(--modal-gap);padding-bottom:var(--modal-gap)}
  .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 - 2 * var(--modal-gap))}
  .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}

  /* ── ОКНО ТАСКА ВПИСАНО В ЛИСТ ДЗ (blox55 #23) ───────────────────────────────────────────────
     Жалоба владельца: «окно постановки таска должно вписываться в размер текущего листа дз, чтобы
     учитель мог одновременно видеть левую половину монитора (прошлое дз или notes)». Замер на
     1366×768: листы по 650px (22–672 и 695–1344), окно — 263–1103 шириной 840, то есть от левого
     листа оставалась полоска в 240px. Теперь окно встаёт РОВНО в полосу своего листа: тот, на
     котором открыт конструктор (.opane.build) — он не всегда правый, учитель может строить ДЗ и
     на левом. Координаты приезжают из builder.js двумя переменными (лист меряется по месту, а не
     вычисляется из ширины экрана: между листами стоит ось со стрелками, и делить экран пополам —
     врать на 20–40px). Значения ниже — запасные, на случай если разметка встретится без JS.
     По вертикали НЕ вписываем: лист длиннее экрана, вписывать не во что.
     ВКЛЮЧАЕТСЯ ЭТО НЕ ВЕЗДЕ: узкий лист (<440px) и совсем низкое окно (<420px) окна не кормят, а
     между 420 и 600 вписывание даётся только тонкому указателю — мыши. Пороги и почему их два,
     а не один (ноутбук владельца, 13.09.2026) — в builder.js рядом с DOCK_MIN_H; здесь важно
     одно: признак ставит JS, и если он его не поставил, окно ведёт себя как до blox55 #23. */
  :root{--dock-l:0px;--dock-w:100%;--dock-top:96px;--modal-gap:22px}
  @media(min-width:860px){ :root{--modal-gap:10px} }
  body.taskdock .emodal{background:transparent;display:block;overflow:visible;padding:0}
  /* ВУАЛЬ ТОЛЬКО НАД СВОИМ ЛИСТОМ. Затемнять весь экран здесь бессмысленно: ради того, чтобы
     соседний лист было ВИДНО, всё и затевалось. Полоса шире листа на 10px с каждой стороны —
     иначе её край совпал бы с краем окна и читался вторым контуром. */
  body.taskdock .emodal::before{content:"";position:fixed;top:0;bottom:0;
    left:calc(var(--dock-l) - 10px);width:calc(var(--dock-w) + 20px);background:var(--scrim)}
  body.taskdock .emodal .panel{position:fixed;top:var(--modal-gap);left:var(--dock-l);width:var(--dock-w);
    max-width:none;max-height:calc(100dvh - 2 * var(--modal-gap));box-shadow:var(--sheet-4);
    /* окно становится системой отсчёта для своей начинки: раскладка пикера типов ниже спрашивает
       ширину ОКНА, а не экрана */
    container-type:inline-size}
  /* РАСКЛАДКА ПО ШИРИНЕ ОКНА, А НЕ ЭКРАНА. Восемь кнопок типа в одну строку (правило
     @media(min-width:860px) выше) считали ширину ЭКРАНА — а окно теперь шириной с лист: 650px на
     ноутбуке, 479 на айпаде. Восемь колонок по 55px нечитаемы. Здесь тот же телефонный расклад
     парами, но включает его ширина самого окна. Стоит ПОСЛЕ @media(min-width:860px) намеренно:
     специфичность у правил одинаковая, спор решает порядок. */
  @container (max-width:819px){
    .emodal .recpick{grid-template-columns:repeat(2,1fr);gap:6px}
    .emodal .recb{padding:6px 8px}
    .emodal .typepick button{flex-direction:column;padding:8px 6px;gap:4px}
  }
  /* СОСЕДНИЙ ЛИСТ ОСТАЁТСЯ ЖИВЫМ, НО ТОЛЬКО НА ЧТЕНИЕ (решение владельца: «клик за пределами окна
     кроме кнопки notes/tasks не делает ничего, но прокрутка содержания левого листа доступна»).
     z-index поднимает лист НАД окном-заслонкой (.emodal, z:60) — иначе заслонка съедала бы и
     прокрутку, и тумблер. Ниже все органы управления листа обесточиваются, кроме самого тумблера:
     редактировать чужой день, пока строишь этот, всё равно нельзя. */
  .otwo .opane.dockside{position:relative;z-index:61;display:flex;flex-direction:column;
    max-height:calc(100vh - var(--dock-top) - 12px)}
  .otwo .opane.dockside > .opane-h{flex:0 0 auto}      /* дата + тумблер всегда на виду */
  .otwo .opane.dockside > .opane-body{flex:1 1 auto;min-height:0;overflow-y:auto;
    overscroll-behavior:contain}
  /* opacity — не украшение, а ЧЕСТНОСТЬ (lingo-ui #12: контрол обязан выглядеть контролом).
     Обесточенная кнопка, которая выглядит живой, — мёртвый клик: человек жмёт «удалить» на
     соседнем листе и не понимает, почему ничего не произошло. Приглушаем ТОЛЬКО органы
     управления; текст, карточки и сводка досье остаются в полную силу — их пришли читать. */
  .otwo .opane.dockside button:not(.onotes):not(.onw),
  .otwo .opane.dockside a, .otwo .opane.dockside input, .otwo .opane.dockside select,
  .otwo .opane.dockside textarea{pointer-events:none;opacity:.55}
  /* ПРОВОДНИК Я.ДИСКА ИЗ ОКНА ТАСКА — В ТУ ЖЕ ПОЛОСУ (blox55 #23, п.4). Шторка проводника общая:
     её же открывают плейлист и проверка сдачи — там она обязана остаться во весь экран. Поэтому
     правило висит на body.taskdock: признак стоит ТОЛЬКО пока открыто вписанное окно таска. */
  body.taskdock .overlay.exp-shutter{align-items:flex-start;justify-content:flex-start}
  body.taskdock .overlay.exp-shutter .exp-sheet{max-width:none;width:var(--dock-w);
    margin-left:var(--dock-l);margin-top:var(--modal-gap);height:calc(100dvh - 2 * var(--modal-gap));max-height:calc(100dvh - 2 * var(--modal-gap))}
  /* #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)}

  /* blox59 #2 — шторка «Отправить лист ДЗ в группу»: СТРОКА НА ГРУППУ. Лентой (.gstrip) чипы тут
     были до 16.09.2026, и дате рядом с ними места не было: лента едет вбок и обрезает всё, что не
     влезло. Столбец даёт каждой группе свою строку «чип + дата ближайшего урока» и растёт вниз,
     внутри собственной прокрутки шторки. */
  .sg-list{display:flex;flex-direction:column;gap:8px;margin:0 0 14px;max-height:52vh;overflow:auto}
  .sg-row{display:flex;align-items:center;gap:8px}
  /* чип занимает остаток строки и обрезает длинное имя многоточием: дата справа не должна
     уезжать за край шторки из-за названия группы */
  .sg-chip{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:7px;text-align:left}
  .sg-chip .sgname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .sg-chip .sgmark{display:flex;color:var(--line)}          /* «не отмечено» — кружок есть, он погашен */
  .sg-chip.on{border-color:var(--accent-deep)}
  .sg-chip.on .sgmark{color:var(--accent-ink)}
  .sg-chip[disabled]{opacity:.55;cursor:default}
  .sg-date{flex:0 0 auto;display:flex;align-items:center;gap:6px;min-height:44px;padding:9px 12px;
    border:1px solid var(--line-soft);border-radius:var(--r-md);background:var(--surface);
    font:inherit;color:var(--ink);cursor:pointer;white-space:nowrap}
  .sg-date.open{border-color:var(--accent-deep);background:var(--accent-tint)}
  .sg-nodate,.sg-done{flex:0 0 auto;color:var(--muted);font-size:var(--fs-sm);white-space:nowrap}
  .sg-done{color:var(--ok);display:flex;align-items:center;gap:5px}
  .sg-row.is-sent{opacity:.6}
  .sg-err{color:var(--danger);font-size:var(--fs-sm);margin:-4px 0 0 2px}
  .sg-lessons{display:flex;flex-direction:column;gap:6px;margin:-2px 0 4px 14px}
  .sg-lesson{display:flex;align-items:center;gap:8px;text-align:left;padding:10px 12px;border:1px solid var(--line-soft);
    border-radius:var(--r-md);background:var(--surface);font:inherit;cursor:pointer;min-height:44px}
  .sg-lesson.on{border-color:var(--accent-deep);background:var(--accent-tint)}

  /* blox69 #9 — шторка «Отсутствующие» (roll.js): имя в строку, тап = «нет на уроке». Отсутствие —
     это СОСТОЯНИЕ, а не тревога (правило красной заливки), поэтому тинт + рамка, без сплошного
     красного. Список прокручивается сам, «Готово» всегда на виду, как в шторке «Duplicate». */
  .rl-hint{color:var(--muted);font-size:var(--fs-sm);margin:-4px 0 10px}
  .rl-list{display:flex;flex-direction:column;gap:6px;margin:0 0 14px;max-height:56vh;overflow:auto}
  .rl-row{border:1px solid var(--line-soft);border-radius:var(--r-md);background:var(--surface)}
  .rl-row.off{border-color:var(--alert);background:var(--alert-tint)}
  .rl-name{display:flex;align-items:center;gap:8px;width:100%;min-height:44px;padding:9px 12px;
    border:0;background:none;font:inherit;color:var(--ink);text-align:left;cursor:pointer}
  .rl-nm{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
  .rl-st{flex:0 0 auto;color:var(--muted);font-size:var(--fs-sm)}
  .rl-row.off .rl-st{color:var(--alert-ink);font-weight:600}
  .rl-why{display:flex;flex-wrap:wrap;gap:6px;padding:0 10px 10px}
  .rl-rsn{min-height:44px;padding:8px 12px;border:1px solid var(--line-soft);border-radius:var(--r-md);
    background:var(--card);font:inherit;color:var(--ink);cursor:pointer}
  .rl-rsn.on{border-color:var(--accent-deep);background:var(--accent-tint);font-weight:600}
  /* 16px — иначе Safari увеличит страницу при фокусе и не вернёт (правило 3 lingo-ui) */
  .rl-note{flex:1 1 100%;min-height:44px;padding:8px 10px;border:1px solid var(--line-soft);
    border-radius:var(--r-md);background:var(--card);font:inherit;font-size:var(--fs-input);color:var(--ink)}
  .rl-sheet #rl-go{width:100%}
  /* кнопка переклички в шапке урока на мониторе: после «Готово» показывает итог */
  .oroll.done{color:var(--muted)}
  .oroll.done.some{color:var(--alert-ink)}
  /* список пропусков под плитками отчёта родителя и Stats учителя (student.js absListHTML) */
  .pabs{margin:12px 0 0;display:flex;flex-direction:column;gap:4px}
  .pabs-h{font-weight:700;margin-bottom:2px}
  .pabs-row{font-size:var(--fs-sm);overflow-wrap:anywhere}
  .pabs-note{color:var(--muted)}
  @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)}
  /* blox68 #6 — ШТОРКА ПЛЕЕРА ЗАДАНИЯ (student.js hwSheet): плеер/диктофон с «Мои фразы».
     Потолок — видимый экран (dvh: у айфона vh больше видимого на панель Safari), прокручивается
     ТЕЛО, а шапка с ✕ стоит на месте: полоса фраз растёт с каждой записью. Свою рамку плеера
     (.lp — карточка с тенью) внутри шторки снимаем: карточка в карточке. */
  /* потолок — ниже часов и выреза (safe-area): иначе на маленьком айфоне ✕ шапки вставал под строку
     состояния, где касания забирает сам айфон; низ — выше полоски «домой» */
  .hwpsheet{display:flex;flex-direction:column;overflow:hidden;
    max-height:calc(100dvh - max(24px, env(safe-area-inset-top) + 8px));
    padding-bottom:max(18px, env(safe-area-inset-bottom))}
  .hwpsheet .sheethead{flex:0 0 auto;gap:10px}
  .hwpsheet .sheethead b{display:flex;align-items:center;gap:6px;min-width:0}
  .hwpsheet .sheethead .x{flex:0 0 auto;width:44px;height:44px;border-radius:50%}   /* палец (WCAG 2.5.5) */
  .hwpbody{flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain}
  .hwp-what{color:var(--muted-strong);font-size:var(--fs-sm);line-height:1.45;margin:-4px 0 10px;
    white-space:pre-line;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:3;
    -webkit-box-orient:vertical;overflow:hidden}
  .hwpsheet .lp{box-shadow:none;padding:0;max-width:none;background:none}
  /* 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)}
  /* blox70 #8 — «Мои обращения». Вкладки — общий .tabs; список скроллится внутри .sheet (90vh), так
     что длинная переписка не выталкивает шторку за экран ни на телефоне, ни на ПК. */
  .sup-tabs{margin:0 0 12px}
  .sup-cnt{color:var(--accent-deep)}
  .sup-empty{color:var(--muted);font-size:var(--fs-md);text-align:center;padding:22px 6px}
  .sup-t{border:1px solid var(--line);border-radius:var(--r-md);background:var(--card);margin-bottom:8px}
  .sup-t--new{border-color:var(--accent-line)}
  .sup-th{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;width:100%;min-height:44px;
    padding:10px 12px;background:none;border:none;text-align:left;cursor:pointer;color:var(--ink)}
  .sup-st{font-size:var(--fs-xs);font-weight:700;border-radius:var(--r-sm);padding:1px 7px;
    background:var(--accent-tint);color:var(--accent-ink)}
  .sup-st--closed{background:var(--fill-soft);color:var(--muted-strong)}
  .sup-td{font-size:var(--fs-xs);color:var(--muted)}
  .sup-nb{font-size:var(--fs-xs);font-weight:700;color:var(--accent-deep)}
  .sup-tt{flex:0 0 100%;font-size:var(--fs-md);overflow-wrap:anywhere;display:-webkit-box;
    -webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .sup-th[aria-expanded="true"] .sup-tt{display:none}
  .sup-tb{padding:0 12px 12px}
  .sup-thread{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
  .sup-m{max-width:88%;align-self:flex-start;background:var(--surface-2);border-radius:var(--r-md);padding:7px 10px}
  .sup-m--staff{align-self:flex-end;background:var(--accent-tint)}
  .sup-m--new{box-shadow:0 0 0 2px var(--accent-line)}
  .sup-mw{display:block;font-size:var(--fs-2xs);color:var(--muted-strong);margin-bottom:2px}
  .sup-mw b{color:var(--accent-deep)}
  .sup-mb{font-size:var(--fs-md);white-space:pre-wrap;overflow-wrap:anywhere}
  .sup-addgo{margin-top:8px}
  /* PDF-просмотрщик (blox9 #5): почти во весь экран, свой ✕ и запасная ссылка «в браузере» */
  .overlay.pdfov{align-items:stretch;justify-content:stretch}
  /* отступы — с зоной часов и выреза (safe-area): приложение рисуется во весь экран (viewport-fit=cover),
     и при 12px сверху «Сохранить ↓» и ✕ вставали ПОД строку состояния — айфон забирает касания
     там себе, кнопки были недостижимы (владелец, 24.09.2026) */
  .pdfbox{position:relative;background:var(--card);display:flex;flex-direction:column;width:100%;height:100%;
    padding:max(12px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) 0 max(12px,env(safe-area-inset-left))}
  .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; тянется за верхнюю ручку. */
  /* max-height — плашка с записью фраз (blox68 #5) бывает выше учебника: потолок держит её в рамке,
     тело .pdf-fp-body прокручивается */
  .pdf-fplayer{position:absolute;z-index:5;width:min(340px,calc(100% - 12px));max-height: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}
  /* blox68 #5 — на плашке идёт запись (фразы или диктофон, recorder.js ставит data-rec): рамка и
     ручка краснеют, чтобы ребёнок, глядя в текст учебника, краем глаза видел «меня пишут» */
  .pdf-fplayer[data-rec="on"]{border:2px solid var(--danger)}
  .pdf-fplayer[data-rec="on"] .pdf-fp-grip{background:var(--alert-tint);color:var(--alert-ink)}
  .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;overflow-y:auto;min-height:0}
  .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 — иначе он не переиспользуем. blox57 #2 поставил такой же
     в шапку окна «из учебника» («Список / Страница»), и запертый в мониторе вид пришлось бы
     копировать вторым экземпляром — ровно то, из-за чего платформа и расползается (правило «один
     элемент — один класс»). Панельное осталось панельным: место (.opane > .onotes), метка
     загрузки (.busy) и резерв строки у панели-края — ниже. */
  .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}   /* досье — колонкой, а не рядом «таски+ростер» */
  .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}
  /* blox58 #3 — блок «Stats» в личке: те же плитки .pstat, что у родителя. Кнопка-раскрывашка
     живёт в .toolbar рядом с Feedback, тело — здесь, МЕЖДУ панелью и сводкой занятия: раскрытые
     плитки стоят вплотную к своей кнопке и сдвигают сводку вниз. Отступы висят на .srpar-body, а
     не на обёртке: у скрытого тела (display:none) их нет вовсе, и свёрнутый блок не оставляет
     между панелью и сводкой лишнего зазора. */
  .srpar{margin:0}
  .srpar-body{margin:0 0 12px}
  .srpar-note{font-size:var(--fs-xs);font-weight:700;color:var(--muted);margin-bottom:8px;max-width:60ch}
  .srpar-chev{transition:transform var(--t) var(--ease)}
  .srpar-btn[aria-expanded="true"] .srpar-chev{transform:rotate(180deg)}
  /* ЛИЧКА НА ТЕЛЕФОНЕ: «Досье» · «Stats» · «Feedback» — один ряд равными долями (flex:1 1 0 даёт
     одинаковую ширину независимо от длины подписи), отступы сжаты ручкой движка --btn-px, как у
     ряда чипов ученика (.toolbar.stbar). «Принять всё» — главное действие, ему своя строка во всю
     ширину: четвёртой кнопкой в ряду оно бы раздавило остальные три. */
  @media (max-width:430px){
    .toolbar.srbar{gap:6px;width:100%}
    .toolbar.srbar>.btn-ghost,.toolbar.srbar>.btn-fb{--btn-px:8px;flex:1 1 0;min-width:0;white-space:nowrap}
    .toolbar.srbar>.btn{flex:1 1 100%}
  }
  .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)}
  /* «сдал — на проверке» — та же ОЧЕРЕДЬ РАБОТЫ, что и бейдж .rrev.pend («N to check») в обзоре
     группы (blox52 #11): ребёнок сделал своё, действовать учителю. Значит и красится так же —
     якорь схемы под белым текстом, а не тревожный красный тинт. Один смысл — один цвет. */
  .srind.spend{background:var(--anchor);color:var(--on-dark);border-color:var(--anchor)}
  .srind.sret{background:var(--draft-tint);color:var(--draft-ink);border-color:var(--draft-line)}
  /* Два «не»-состояния — красноватые, но приглушённые: это не очередь работы (та ушла в якорь
     схемы у «на проверке»), а отсутствие события. Поэтому обе плашки полупрозрачные, а
     «не открывал» (ребёнок даже не заглянул) насыщеннее «не сдавал» (заглянул, но не прислал)
     — фоном на ступень темнее и меньшей прозрачностью. */
  .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}
  /* «⧗ N на проверку» — та же ОЧЕРЕДЬ РАБОТЫ, что и плашка .srind.spend на карточке ниже и бейдж
     .rrev.pend в обзоре группы (blox52 #11, blox55 #11): якорь схемы, не тревожный красный.
     «N возвращено» (иконка undo, blox71 #4) рядом ждёт РЕБЁНКА, а не учителя, — это факт, и он носит краску возврата
     (--draft-*), ту же, что чип .srind.sret на карточке. Раньше стоял инлайном в teacher.js. */
  .summary .pend{color:var(--anchor-ink);font-weight:700}
  .summary .pend.ret{background:var(--draft-tint);color:var(--draft-ink)}
  /* 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-sw — это РАСКЛАДКА (blox56 #11): тумблер слева на две
     строки, справа жирный заголовок и приглушённое описание. Раскладку носят ОБА тумблера
     ростера — «Плейлист» и «English only». А .mg-play — МОДИФИКАТОР только плейлистного:
     он несёт радугу «у группы есть плейлист» (ниже). Разнесены нарочно: пока раскладка и радуга
     жили в одном классе, второй тумблер загорался бы плейлистной радугой и врал. */
  .mg-sw{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}
  /* ТУМБЛЕР — КОМПОНЕНТ, а не украшение одной галочки (blox56 #10). Пишется на самом
     input[type=checkbox] (appearance:none), без обёрток и без второго элемента: контрол остаётся
     НАСТОЯЩЕЙ галочкой — её ловит клавиатура, читает скринридер, и все, кто уже на неё ссылается
     (обработчик, `:has(input:checked)` у радуги, ассерт), продолжают работать. role="switch"
     ставится в разметке — он говорит про ВИД контрола, а не про его устройство.
     Зелёного во включённом положении НЕТ, хотя на айфоне он зелёный: в этом приложении зелёная
     заливка означает «сделано/принято» и только это (гейт «зелёная заливка» в npm run check).
     Включённый тумблер берёт якорь схемы — тот же цвет, что и прочие «включено» состояния.
     46×28 — палец кладётся на весь тумблер, а не в 24px-квадрат, как раньше. */
  /* min-width/min-height обязательны, а не для красоты: в гриде колонка `auto` считает минимум
     пустого контрола по СОДЕРЖИМОМУ (то есть ноль) и на тесной строке ужимает трек — это было
     видно буквально: трек сжался до 24px, а ручка выехала на соседний текст. */
  .swi{appearance:none;-webkit-appearance:none;flex:none;position:relative;margin:0;
    width:46px;height:28px;min-width:46px;min-height:28px;
    border:0;border-radius:var(--r-pill);background:var(--line-mid);
    cursor:pointer;transition:background var(--t) var(--ease)}
  .swi::after{content:'';position:absolute;top:2px;left:2px;width:24px;height:24px;
    border-radius:var(--r-pill);background:var(--card);box-shadow:var(--sh-1);
    transition:transform var(--t) var(--ease)}
  .swi:checked{background:var(--anchor-ink)}
  .swi:checked::after{transform:translateX(18px)}
  .swi:focus-visible{outline:none;box-shadow:var(--ring)}
  @media (prefers-reduced-motion:reduce){ .swi,.swi::after{transition:none} }
  .mg-sw .swi{grid-row:1 / span 2}
  .mg-sw-t{font-weight:700;color:var(--ink)}
  .mg-sw-d{grid-column:2;font-size:var(--fs-xs);font-weight:600;color:var(--muted);line-height:1.35}
  /* РАДУГА = «У ГРУППЫ ЕСТЬ ПЛЕЙЛИСТ» (blox40 #3 → blox56 #9/#10). Заливка --grad-playlist — общая
     метка плейлиста во всём приложении (плашка «Постоянный плейлист» у ребёнка, рельс плашки
     Playlist у учителя), поэтому в ростере она обязана гореть, пока плейлист У ГРУППЫ ЕСТЬ.
     Условие этой метки переворачивали ДВАЖДЫ вместе со смыслом контрола, и каждый раз она
     оставалась привязана к «плейлист есть»: blox40 #3 — галочка ВКЛЮЧАЛА режим прослушки;
     blox56 #2 — галочка стала ОТНИМАТЬ плейлист (подсветку тогда сняли зря); blox56 #10 — контрол
     стал тумблером ПЛЕЙЛИСТА, и включённое положение снова значит «есть». Отсюда и селектор
     вернулся к :has(input:checked) — чистое CSS-условие, поэтому состояние верное уже на ПЕРВОЙ
     отрисовке, а не только после клика. */
  .mg-play:has(input:checked){background:var(--grad-playlist);border-color:transparent;
    box-shadow:var(--sh-2)}
  .mg-play:has(input:checked) .mg-sw-t{color:var(--ink-strong)}
  .mg-play:has(input:checked) .mg-sw-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 .rtico{color:var(--rail)}                 /* task icon = its type's rail colour */
  .rect.done .rtico{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)}

/* ── blox68 #1 — «Почта родителя» (public/parent-emails.js) и «Мои дети» (public/family.js) ── */
.pmail{margin:4px 0 0}
.pmail-h{font-weight:600;margin-bottom:2px}
.pmail-sub{color:var(--muted);font-size:var(--fs-sm);margin-bottom:8px}
.pmail-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 0;border-bottom:1px solid var(--line-soft)}
.pmail-mail{font-weight:500;overflow-wrap:anywhere;min-width:0}
.pmail-who{color:var(--muted);font-size:var(--fs-sm);flex:1 1 auto}
.pmail-del{margin-left:auto}
.pmail-none{color:var(--muted);font-size:var(--fs-sm);padding:4px 0}
.pmail-add{display:flex;gap:8px;margin-top:10px}
.pmail-in{flex:1 1 auto;min-width:0;font-size:var(--fs-input)}
.fam-kid{display:flex;align-items:center;gap:12px;width:100%;text-align:left;padding:14px 16px;margin:0 0 10px;
  background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--r-md);cursor:pointer;
  font:inherit;color:var(--ink);min-height:44px}
.fam-kid:hover{border-color:var(--primary)}
.fam-nick{font-weight:600;font-size:var(--fs-lg)}
.fam-grp{color:var(--muted);font-size:var(--fs-sm);margin-left:auto}

/* ── blox70 #2 — «СДАЧИ ПО ТАСКУ» (public/task-review.js) ─────────────────────────────────────
   Один таск глазами всей группы: шапка (сам таск + счётчики + «Принять всё» · Notes), окно заметок
   по группе, ниже — корзины учеников (на проверку · не сдали · возвращено · принято), в каждой
   карточки общего рисовальщика review-card.js (.srcard). На ПК экран стоит ВНУТРИ панели листа
   (.opane.tview), на телефоне — отдельным экраном (.tk-screen). */
.tk-view{display:flex;flex-direction:column;gap:12px;min-width:0}
.tk-head{display:flex;flex-direction:column;gap:6px;min-width:0}
.tk-head .tk-back{align-self:flex-start;width:auto;margin:0}
.tk-kicker{font-size:var(--fs-xs);font-weight:700;color:var(--muted)}
.tk-title{margin:0;font-size:var(--fs-xl);line-height:1.25;overflow-wrap:anywhere;white-space:pre-line;
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;cursor:pointer}
/* аудит blox70 #2 (#19): длинный текст задания — не больше трёх строк, тап раскрывает целиком */
.tk-title.open{display:block;-webkit-line-clamp:unset;overflow:visible}
.tk-title a{color:var(--accent-dark);overflow-wrap:anywhere}
/* ссылка задания — своей строкой и нажимается, как на плашке монитора (.oent-url) */
.tk-url{display:block;color:var(--accent-dark);font-weight:700;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;min-width:0}
/* предложение аудита №7 — «🤔 ждём: N» в сводке: факт без срочности, приглушённым */
.tk-sum .tk-waitn{color:var(--muted);font-weight:700}
.tk-head .srsub{display:flex;align-items:center;flex-wrap:wrap;gap:6px;font-size:var(--fs-xs);
  font-weight:700;color:var(--muted)}
.tk-head .tk-sum{margin:4px 0 0}
.tk-bar{display:flex;flex-wrap:wrap;gap:8px;margin:0}
.tk-bar>.btn,.tk-bar>.btn-ghost{width:auto;margin:0}
/* окно заметок: своя прокрутка и потолок высоты — длинный журнал не отталкивает список учеников
   вниз за экран (lingo-ui #3, «растущий ребёнок ограничен») */
.tk-notes{background:var(--card);border-radius:var(--r-md);box-shadow:var(--sheet-1);padding:10px 14px}
.tk-notes[hidden]{display:none}
.tk-notes-h{font-weight:800;font-size:var(--fs-sm);color:var(--ink);display:flex;align-items:center;gap:6px;
  margin-bottom:6px}
.tk-notes-body{min-width:0}
/* аудит #11: потолок стоял на ВСЁМ окне (320px) и срезал кнопку «Добавить запись» — выглядело
   поломкой. Теперь форма журнала видна целиком, а прокручивается только лента записей. */
.tk-notes .djournal{max-height:220px;overflow-y:auto;overscroll-behavior:contain}
/* аудит #9: пока левая панель монитора — то же досье группы (режим Notes), второе окно заметок и
   его кнопка на экране таска не показываются */
.tk-nonotes .tk-notes,.tk-nonotes .tk-notes-btn{display:none}
.tk-list{display:flex;flex-direction:column;gap:16px}
.tk-sec{display:flex;flex-direction:column;gap:10px}
.tk-sec-h{font-weight:800;font-size:var(--fs-md);color:var(--ink);display:flex;align-items:center;gap:8px}
.tk-sec-h .tk-n{font-size:var(--fs-xs);font-weight:800;color:var(--muted);background:var(--surface);
  border:1px solid var(--line);border-radius:var(--r-lg);padding:1px 8px}
/* очередь работы учителя — той же краской, что «N to check» (якорь схемы), без тревоги */
.tk-sec.tk-pend>.tk-sec-h .tk-n{background:var(--anchor);color:var(--on-dark);border-color:var(--anchor)}
.srcard.tk-card .tk-name{font-size:var(--fs-lg)}
/* ряд «🤔 жду»: кнопка того же вида, что реакции на сдачу, и подпись, что это увидит ребёнок */
.tk-wait{margin-top:8px}
.tk-wait-lbl{font-size:var(--fs-xs);font-weight:700;color:var(--muted)}
.opane.tview .otaskhost{padding:4px 2px 8px;min-width:0}
.tk-screen{padding-bottom:24px}
/* blox70 #3 — стрелки по плашкам листа: выход слева, «‹ N из M ›» справа, в одну строку */
.tk-top{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}
.tk-top .tk-back{width:auto;margin:0}
.tk-nav{display:flex;align-items:center;gap:4px;flex:0 0 auto}
.tk-nav .btn-ghost{width:44px;height:44px;margin:0;padding:0;font-size:var(--fs-xl)}   /* палец (WCAG 2.5.5) */
.tk-pos{font-size:var(--fs-sm);font-weight:700;color:var(--muted);min-width:4.6em;text-align:center;white-space:nowrap}
/* blox70 #3 — «Слушать подряд»: оболочка — медиа-оверлей сдачи (.mediaov .mediabox, 92vh с прокруткой
   внутри), здесь только своё: имя крупно, плеер во всю ширину, ряд решений переносится целиком */
.mediaov .lq-box{max-width:min(560px,94vw)}   /* сильнее .mediaov .mediabox (900px): одна колонка плеера, без пустого поля справа */
.lq-h{display:flex;align-items:center;gap:6px;min-width:0}
.lq-pos{color:var(--muted);font-weight:700}
.lq-task{font-size:var(--fs-xs);font-weight:700;color:var(--muted);margin:-6px 0 8px;overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow-wrap:anywhere}
.lq-body{display:flex;flex-direction:column;gap:10px;min-width:0}
.lq-name{font-size:var(--fs-xl);font-weight:800;color:var(--ink);overflow-wrap:anywhere}
.lq-player{width:100%;min-width:0}
.lq-act{display:flex;flex-wrap:wrap;gap:8px}
.lq-act>.btn,.lq-act>.btn-ghost{width:auto;margin:0}
.lq-cwrap{display:flex;flex-direction:column;gap:6px;margin:0}
.lq-cwrap[hidden]{display:none}
.lq-cwrap>.btn-ghost{width:auto;align-self:flex-start;margin:0}
.lq-line{font-size:var(--fs-md);color:var(--ink)}
.lq-n{font-weight:700}
.lq-sum{display:flex;flex-direction:column;gap:10px}
