/* ───────────────────────────────────────────────
   Spark Design System — base.css
   ─────────────────────────────────────────────── */

:root {
    color-scheme: light;

    /* ── Surfaces ───────────────────────────── */
    --bg:           #F4F5F7;
    --surface:      #FFFFFF;
    --surface-2:    #F1F3F8;

    /* ── Ink & Text ─────────────────────────── */
    --ink:          #0E1116;
    --text:         #1A1F29;
    /* Было #6B7280 — на --bg 4.43, на --surface-2 4.35 при норме 4.5 (аудит 11.7); это — 5.14 / 5.05. */
    --muted:        #616875;
    --dim:          #666E80;
    /* Серая заливка под белую цифру: старая попытка в календаре, «Не оценено»,
       балл соседа. Отдельно от --dim, потому что --dim — цвет подписей и в
       тёмной теме светлый (белое на нём 3.43). Strong — наведение и выбор. */
    --dim-fill:        var(--dim);
    --dim-fill-strong: color-mix(in srgb, var(--dim) 75%, var(--ink));
    --line:         #E6E8EE;

    /* ── Brand Blues (значения фиолетовые с 28.08.2026, см. DESIGN.md) ── */
    --blue:         #A855F7;
    --blue-deep:    #9333EA;
    --blue-mid:     #C084FC;
    --blue-ink:     #581C87;
    --blue-on-soft: #6B21A8;
    /* Фиолетовый текстом (ссылки, активные пункты, акцентные подписи): сам --blue на белом
       3.96, на --surface-2 3.56 — ниже 4.5 (аудит 10.4). Тот же тон глубже — смесь двух
       ступеней шкалы: 6.49 на белом, 4.77 на --blue-tint. Заливки кнопок остаются на --blue. */
    --blue-text:    color-mix(in srgb, var(--blue-deep) 60%, var(--blue-on-soft));
    /* Заливка под белым текстом и значками: белое на --blue — 3.96 (аудит 11.2), на этой — 5.38.
       Strong — наведение, выбор и тёмный край градиентных шапок (белое 8.0). От темы не зависят:
       --blue-deep в тёмном блоке не переопределён, а --blue-on-soft там светлый. */
    --blue-fill:        var(--blue-deep);
    --blue-fill-strong: color-mix(in srgb, var(--blue-deep) 72%, black);
    --blue-soft:    #F3E8FF;
    --blue-tint:    #E9D5FF;

    /* ── Status ─────────────────────────────── */
    --success:      #15803D;
    /* Было #DC2626 — на --bg 4.43, на розовых плашках 4.0–4.3 (аудит 11.7); это — 5.26, плашки от 4.7. */
    --error:        #C81E1E;
    --warning:      #B45309;
    --warning-deep: #92400E;
    --warning-ink:  #78350F;
    --warning-text: #92400E;
    --warning-soft: #FEF3C7;
    /* Заливка под белым текстом (счётчики непроверенного, «Продолжить»): сам --warning
       в тёмной теме светлый — белое на нём 1.67 (аудит 11.4б). */
    --warning-fill: var(--warning);
    /* Заливка под белым текстом (нажатая .btn-danger, крестики удаления): сам --error
       в тёмной теме светлый — белое на нём 2.77 (аудит 11.7). */
    --error-fill:   var(--error);
    --warning-tint: #FDE68A;
    --ok:           #047857;
    --ok-strong:    #047857;
    --success-text: #166534;
    --gold:         #FCD34D;
    /* 4-тиерный градиент балла (Пробники): red/orange/lime/green. Тон
       theme-invariant, как и у существующего .cal-hero-score. */
    --score-orange: #F97316;
    --score-lime:   #84CC16;
    /* Заливки под белой цифрой балла: сами тона выше дают белому 1.98–4.83, а
       прозрачная плашка поверх фото — до 1.92 на светлой работе (аудит 11.3).
       Тот же тон, затемнённый до белого ≥ 5; плашка сплошная, фото не просвечивает.
       Красный и зелёный идут от --error/--success, которые в тёмной теме светлые, —
       там они переопределены с другой долей. Без балла — --dim-fill. */
    --score-red-fill:    color-mix(in srgb, var(--error) 90%, black);
    --score-orange-fill: color-mix(in srgb, var(--score-orange) 70%, black);
    --score-lime-fill:   color-mix(in srgb, var(--score-lime) 60%, black);
    --score-green-fill:  color-mix(in srgb, var(--success) 90%, black);
    --on-color:     #fff;

    /* ── Тип блока конструктора (TaskBlock.block_type) ──
       Категориальная метка, не статус: сознательно не переиспользует
       --success/--error/--warning/--blue, чтобы цвет блока не читался как
       «верно/неверно/предупреждение/основное действие». Единственный
       источник для обеих сторон — редактора (program_ios.css) и панели
       ученика (tracker.css), см. DESIGN.md «Новый цвет — только под новый
       устойчивый смысл». Текстовая подпись типа остаётся первичным
       носителем — цвет здесь вторичный ускоритель, не единственный. */
    --blk-text:     var(--dim);
    --blk-photo:    #0D9488;
    --blk-video:    #4F46E5;
    --blk-link:     #0EA5E9;
    --blk-question: #DB2777;
    /* Пять типов, добавленных после 31.08.2026, цвета не получили и висели
       на дефолтном сером `--blk-text` — в списке из десяти блоков половина
       выглядела одинаково (владелец 08.09.2026: «блоки при добавлении в
       задании должны визуально выделяться друг от друга»). Продолжение той
       же категориальной шкалы, не новая система. */
    --blk-portfolio: #D97706;
    --blk-scale:     #7C3AED;
    --blk-timed:     #65A30D;
    --blk-upload:    #059669;
    /* Бордовый, а не второй серый: `--blk-text` уже серый, и на экране две
       серые полосы читались как один и тот же тип. */
    --blk-rules:     #9F1239;
    /* Фуксия — комбинированный блок «Фото + сдача работы» (владелец
       12.09.2026), ни с одним из десяти прежних цветов не пересекается. */
    --blk-photo-upload: #C026D3;
    /* Оранжевый — «Голосовое / кружок» (владелец 25.09.2026): тёплый тон
       живого голоса, не совпадает с янтарным портфолио по светлоте. */
    --blk-media:        #EA580C;
    /* Жёлтое золото — «Сравнение работ» (Лиза 27.09.2026): выбор победителя.
       Светлее янтарного портфолио и заметно желтее оранжевого голосового. */
    --blk-compare:      #CA8A04;

    /* ── Палитра типов событий дайджеста (EVENT_PALETTE в models/tracker.py) ──
       Владелец 04.10.2026: тип события = цвет, цвет выбирает Главный
       преподаватель из этой палитры. Тройка на ключ: сам цвет (контур периода,
       кружок-контур), мягкая заливка (таблетка периода, кружок дня) и чернила
       поверх заливки — тёмные, AA на заливке. Стартовые оттенки — пастель из
       макета «Путь к сотке»; когда Лиза пришлёт свои, меняются только значения
       здесь, ключи и данные в базе остаются. */
    --evt-sky: #3A9FDD; --evt-sky-soft: #BEE3FA; --evt-sky-ink: #0B4A6F;
    --evt-lavender: #8B7CF0; --evt-lavender-soft: #DDD5FC; --evt-lavender-ink: #35268A;
    --evt-pink: #E274AB; --evt-pink-soft: #FAD3E7; --evt-pink-ink: #7A1F4E;
    --evt-violet: #8A5CE6; --evt-violet-soft: #CBB8F8; --evt-violet-ink: #2F1670;
    --evt-orange: #EE8434; --evt-orange-soft: #FFDABA; --evt-orange-ink: #7A3407;
    --evt-yellow: #D4A30C; --evt-yellow-soft: #FBE8A6; --evt-yellow-ink: #634800;
    --evt-mint: #34A872; --evt-mint-soft: #C2ECD5; --evt-mint-ink: #0E5333;
    --evt-teal: #1B9A9A; --evt-teal-soft: #B8E7E7; --evt-teal-ink: #0A4D4D;
    --evt-coral: #E8634E; --evt-coral-soft: #FCCDC4; --evt-coral-ink: #7A2112;
    --evt-gray: #858E9E; --evt-gray-soft: #DEE1E7; --evt-gray-ink: #2E3440;

    /* ── iOS re-skin palette (Apple HIG) — 28.08.2026: раньше повторялась
       одинаковым значением в каждом из 24 *_ios.css/login.html вместо
       общего токена, теперь единственный источник — здесь, локально
       файлы её больше не переопределяют. ── */
    --ios-blue:   #AF52DE;
    --ios-green:  #34C759;
    --ios-red:    #FF3B30;
    --ios-orange: #FF9500;
    --ios-purple: #AF52DE;

    /* Текст этими цветами — только через токены текста (аудит «цвета iOS», 30.09.2026): сами iOS-цвета
       на белом 2.2 (оранжевый, зелёный), 3.55 (красный), 4.13 (фиолетовый). Фиолетовый текстом — --blue-text,
       красный — --error. У оранжевого и зелёного своего текстового токена с тёмной темой нет:
       --warning-text/--success-text в тёмной не переопределены, а --warning/--success на своих плашках 4.4. */
    --ios-orange-text: var(--warning-text);
    --ios-green-text:  var(--success-text);
    /* Буквы на сплошном --ios-green («СДЕЛАНО», «ЗАДАНИЕ ВЫПОЛНЕНО»): белое на нём 2.2 / 2.0, тёмно-зелёное —
       6.1 в светлой и 6.7 в тёмной, поэтому в тёмной не переопределяется. Владелец 01.10.2026 выбрал вариант
       «яркий зелёный, тёмные буквы» из трёх образцов. */
    --on-ios-green:    #06361A;

    /* ── iOS re-skin: шрифт, кривая анимации и нейтральная палитра
       (label/separator/подложка) — 03.09.2026, первый шаг перед тёмной
       темой. Тот же самый блок (`--ios-font` … `--ios-card-bg`) был
       буквально скопирован в 23 файла (`.ios-prg-day`, `.ios-tracker`,
       `.ios-staff-dashboard` и т.д.), каждый — свой `:root`-подобный
       скоуп: переключатель темы упёрся бы в то, что переопределение
       выше по каскаду (здесь) ничего не меняет, файлы снова
       перезаписывают его локально. Кастомное свойство наследуется по
       DOM вниз без переобъявления — локальные копии убраны, скоупы
       читают эти значения прямо отсюда. ── */
    --ios-font:       -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
    --ios-ease:       cubic-bezier(0.16, 1, 0.3, 1);
    --ios-label:      #1C1C1E;
    /* Было rgba(60,60,67,.60) — 3.3 на --bg, ≈ 240 надписей (аудит «цвета iOS»); это — 5.2–6.0 на любом светлом фоне.
       --ios-label-3 (30 %) текстом не ставится: 1.7 в обеих темах, он для стрелок, пунктира и точек. */
    --ios-label-2:    #636368;
    --ios-label-3:    rgba(60,60,67,0.30);
    --ios-sep:        rgba(60,60,67,0.29);
    --ios-grouped-bg: #F2F2F7;
    --ios-card-bg:    #FFFFFF;

    /* ── Цвета тарифов — 13.09.2026, решение владельца: «у каждого тарифа
       свой цвет», плашка тарифа белая, надпись в цвет тарифа. Значения
       подобраны под белый фон плашки: надпись мелкая (12px), поэтому каждый
       цвет проверен на контраст с белым и держит WCAG AA (>= 4.5:1) —
       ярко-розовый #FF2D78 и системный синий #007AFF в эту планку не прошли
       (3.6 и 4.0) и здесь не стоят. Группу цвета шаблон получает фильтром
       `tariff_slug` (app/tmpl.py), hex знает только CSS. ── */
    --tariff-confident-max: #D81B60;   /* УВЕРЕННЫЙ МАКСИМУМ — розовый, 4.95:1 */
    --tariff-with-you:      #8E24AA;   /* Я С ВАМИ — фиолетовый, 7.04:1 */
    --tariff-self:          #1565C0;   /* Я САМ — синий, 5.75:1 */
    --tariff-legacy:        #4A4A55;   /* отработавшие тарифы и пустое значение, 8.74:1 */
    /* Фон плашки — белый на обеих темах: она лежит на цветной шапке, и в
       тёмной теме шапка остаётся цветной. Поэтому не `--ios-card-bg`, тот в
       тёмной теме чернеет, и надпись пропала бы. */
    --tariff-pill-bg:       #FFFFFF;

    /* ── Shadows ────────────────────────────── */
    --sh-sm:   0 1px 2px rgba(15,23,42,.04), 0 1px 1px rgba(15,23,42,.03);
    --sh-md:   0 8px 24px rgba(20,30,80,.08);
    --sh-blue: 0 10px 28px rgba(168,85,247,.28);

    /* ── Motion ─────────────────────────────── */
    --ease-out:      cubic-bezier(0.16, 1, 0.3, 1);
    --duration-fast: 120ms;
    --duration-base: 180ms;

    /* ── Radii ──────────────────────────────── */
    --r-sm:   10px;
    --r-md:   14px;
    --r-lg:   20px;
    --r-xl:   28px;
    --r-pill: 999px;

    /* ── Spacing scale (4px base) ───────────── */
    --s-1:  4px;
    --s-2:  8px;
    --s-3:  12px;
    --s-4:  16px;
    --s-5:  20px;
    --s-6:  24px;
    --s-7:  32px;
    --s-8:  40px;

    /* ── Typography ─────────────────────────── */
    --font-display: 'Manrope', system-ui, -apple-system, sans-serif;
    --font-body:    'Figtree', system-ui, -apple-system, sans-serif;
    --font-mono:    ui-monospace, SFMono-Regular, Menlo, monospace;

    --fs-display: 64px;
    --fs-h1:      40px;
    --fs-h2:      28px;
    --fs-h3:      20px;
    --fs-body:    15px;
    --fs-small:   13px;
    --fs-caption: 11px;

    /* ── Legacy aliases (backward compat) ───── */
    --bg2:          var(--surface-2);
    --nav-bg:       var(--surface);
    --card:         var(--surface);
    --card-dark:    var(--blue-deep);
    --card-dark2:   var(--blue-mid);
    --accent:       var(--blue);
    --accent-mid:   var(--blue-mid);
    --accent-dark:  var(--blue-deep);
    --accent-soft:  var(--blue-soft);
    --accent-tag:   var(--blue-tint);
    --border:       var(--line);
    --border-hover: rgba(168,85,247,0.30);
    --hover:        var(--surface-2);
    --shadow:       var(--sh-sm);
    --shadow-lg:    var(--sh-md);
    --shadow-blue:  var(--sh-blue);
}

/* ── Тёмная тема — переключатель, решение владельца 03.09.2026 ──
   Явный opt-in через `[data-theme="dark"]` на `<html>` (кнопка в базовом
   шаблоне, состояние — localStorage), НЕ `prefers-color-scheme`:
   системная тёмная тема захватила бы всех учеников/кураторов с тёмным
   режимом на телефоне сразу при деплое, включая экраны, до которых эта
   правка ещё не дошла. Системный авто-режим можно завести отдельным
   шагом, когда переменных вниз по каскаду будет обкатано больше.

   Переопределяются только структурные токены (поверхности, текст,
   статусы, мягкий акцент, iOS re-skin палитра) — сами значения
   `--blue`/`--blue-deep`/`--blue-mid` не трогаем, вибрирующий фиолет
   одинаково хорошо читается на светлом и тёмном фоне.

   Известная граница покрытия (не переделано этим шагом, см. отчёт в
   сессии 03.09.2026): 34 шаблона хранят собственный инлайновый
   <style>-блок (около 5800 строк, из них ~150 строк — захардкоженный
   hex мимо токенов) — шаблоны course_calculator, 3dlab, cabinet_student
   (и cabinet_students), cabinet_admin_mock_check,
   cabinet_admin_retake_check, login и другие (имена без расширения —
   слово-точка-html в комментарии рядом с правилом читается регэкспом
   храповика переиспользования как имя CSS-класса). Большая часть (950 из ~1100 цветовых
   объявлений) уже использует токены и подхватит тёмную тему сама, но
   точечные захардкоженные пары («бледная плашка + тёмные чернила»,
   рассчитанные только на белый холст) там не проверялись и могут
   читаться хуже на тёмном фоне — не сломаны, но не вычищены. */
:root[data-theme="dark"] {
    color-scheme: dark;

    --bg:           #0D0F14;
    --surface:      #171A22;
    --surface-2:    #1F2330;
    --ink:          #F7F8FA;
    --text:         #E5E7EB;
    --muted:        #97A0B0;
    /* Прежнее значение на --surface-2 давало 4.15 при норме 4.5 (дни календарей
       пробников, аудит 29.09.2026); это — 4.56, всё ещё тусклее --muted. */
    --dim:          #828B9B;
    /* Белое на заливке 4.83, от --surface-2 она отличается на 3.24 (аудит 10.1). */
    --dim-fill:        color-mix(in srgb, var(--dim) 80%, var(--bg));
    --dim-fill-strong: color-mix(in srgb, var(--dim) 65%, var(--bg));
    --line:         #2A3140;

    --success:      #34D399;
    --error:        #F87171;
    --warning:      #FBBF24;
    /* Белое 5.55; светлая тема — 5.02 на самом --warning. */
    --warning-fill: color-mix(in srgb, var(--warning-deep) 85%, var(--warning));
    /* Зелёные чернила плашек: светлый вариант на тёмном холсте давал контраст 2.8.
       Сильный зелёный не трогаем — это заливка под белым текстом. */
    --ok:           var(--success);
    /* Белое 5.89; светлая тема — 5.74 на самом --error. */
    --error-fill:   color-mix(in srgb, var(--error) 65%, black);
    /* Белое на этих заливках 5.89 и 5.71 (светлые --error/--success — 2.77 и 1.92). */
    --score-red-fill:   color-mix(in srgb, var(--error) 65%, black);
    --score-green-fill: color-mix(in srgb, var(--success) 55%, black);

    /* Мягкий акцент (плашки, активный пункт меню) — на тёмном холсте
       бледно-лавандовый фон читается пятном, тёмные чернила поверх него
       становятся нечитаемыми. Пара откалибрована заново, сам `--blue` —
       нет. */
    --blue-soft:    rgba(168,85,247,0.16);
    --blue-tint:    rgba(168,85,247,0.28);
    --blue-ink:     #E9D5FF;
    --blue-on-soft: #D8B4FE;
    /* На тёмных фонах --blue 3.96–4.84; этот — от 4.64 (на --blue-tint) до 7.25. */
    --blue-text:    var(--blue-mid);

    /* Палитра типов событий на тёмном холсте: пастельная заливка светилась бы
       пятном, поэтому она — цвет, утопленный в поверхность, а чернила светлые. */
    --evt-sky-soft: color-mix(in srgb, var(--evt-sky) 38%, var(--surface)); --evt-sky-ink: color-mix(in srgb, var(--evt-sky) 22%, white);
    --evt-lavender-soft: color-mix(in srgb, var(--evt-lavender) 38%, var(--surface)); --evt-lavender-ink: color-mix(in srgb, var(--evt-lavender) 22%, white);
    --evt-pink-soft: color-mix(in srgb, var(--evt-pink) 38%, var(--surface)); --evt-pink-ink: color-mix(in srgb, var(--evt-pink) 22%, white);
    --evt-violet-soft: color-mix(in srgb, var(--evt-violet) 38%, var(--surface)); --evt-violet-ink: color-mix(in srgb, var(--evt-violet) 22%, white);
    --evt-orange-soft: color-mix(in srgb, var(--evt-orange) 38%, var(--surface)); --evt-orange-ink: color-mix(in srgb, var(--evt-orange) 22%, white);
    --evt-yellow-soft: color-mix(in srgb, var(--evt-yellow) 38%, var(--surface)); --evt-yellow-ink: color-mix(in srgb, var(--evt-yellow) 22%, white);
    --evt-mint-soft: color-mix(in srgb, var(--evt-mint) 38%, var(--surface)); --evt-mint-ink: color-mix(in srgb, var(--evt-mint) 22%, white);
    --evt-teal-soft: color-mix(in srgb, var(--evt-teal) 38%, var(--surface)); --evt-teal-ink: color-mix(in srgb, var(--evt-teal) 22%, white);
    --evt-coral-soft: color-mix(in srgb, var(--evt-coral) 38%, var(--surface)); --evt-coral-ink: color-mix(in srgb, var(--evt-coral) 22%, white);
    --evt-gray-soft: color-mix(in srgb, var(--evt-gray) 38%, var(--surface)); --evt-gray-ink: color-mix(in srgb, var(--evt-gray) 22%, white);

    /* iOS re-skin — Apple HIG dynamic colors (dark), тот же источник
       принципа, что и у светлых значений выше. */
    --ios-label:      #FFFFFF;
    --ios-label-2:    rgba(235,235,245,0.60);
    --ios-label-3:    rgba(235,235,245,0.30);
    --ios-sep:        rgba(84,84,88,0.65);
    --ios-grouped-bg: #000000;
    --ios-card-bg:    #1C1C1E;
    --ios-blue:       #BF5AF2;
    --ios-green:      #30D158;
    --ios-red:        #FF453A;
    --ios-orange:     #FF9F0A;
    --ios-purple:     #BF5AF2;
    --ios-orange-text: var(--ios-orange);
    --ios-green-text:  var(--ios-green);
}
/* Плашки `.tag-pill--*`/`.student-info-pill--*`: бледный полупрозрачный
   фон + тёмные чернила, откалиброванные на белый холст (сами цвета —
   ниже в файле, `.tag-pill--case`/`--publish`/`--curator`/`--orange`/
   `--pink`/`--purple`, `.student-info-pill--pink`/`--purple`). На тёмном
   фоне тёмные чернила почти сливаются с плашкой — меняем только текст,
   фон остаётся тем же полупрозрачным пятном (он сам по себе адаптируется
   под подложку). `--blue`-варианты (`.tag-pill--blue`,
   `.student-info-pill--blue`) уже используют var(--blue) и ничего не
   требуют. */
:root[data-theme="dark"] .tag-pill--case                          { color: #FDE68A; }
:root[data-theme="dark"] .tag-pill--publish                       { color: #C4B5FD; }
:root[data-theme="dark"] .tag-pill--curator,
:root[data-theme="dark"] .tag-pill--curator-tag,
:root[data-theme="dark"] .tag-pill--orange                        { color: #FDBA74; }
:root[data-theme="dark"] .tag-pill--pink,
:root[data-theme="dark"] .student-info-pill--pink                 { color: #F9A8D4; }
:root[data-theme="dark"] .tag-pill--purple,
:root[data-theme="dark"] .student-info-pill--purple                { color: #D8B4FE; }

/* ── Reset ── */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
/* Атрибут hidden прячет всегда. Браузерное `[hidden] { display: none }` слабее любого
   авторского `display`: до 30.09.2026 `.btn-outline { display: inline-flex }` оживлял
   скрытую «Назад» на первом вопросе опроса, нажатие стирало опрос до перезагрузки
   (аудит АОП, находка 1). Частных заплаток `.x[hidden]` к тому дню было больше двадцати. */
[hidden] { display: none !important; }

/* ── Base ── */
/* Фон ровный, как в кабинете ученика. До 30.09.2026 в светлой теме здесь была фотография
   бренда: текст, лежавший прямо на ней, не дотягивал до контраста 4.5 (аудит 11.12). */
body {
    font-family: var(--font-body);
    background-color: var(--bg);
    color: var(--text);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, .display {
    font-family: var(--font-display);
    color: var(--ink);
    letter-spacing: -0.025em;
}

a { color: var(--blue-text); text-decoration: none; transition: color var(--duration-fast); }
a:hover { color: var(--blue-deep); }

/* ── Skip link (accessibility) ── */
.skip-link {
    position: absolute; top: -100%; left: 8px;
    background: var(--blue-fill); color: #fff;
    padding: 8px 16px; border-radius: 0 0 8px 8px;
    font-size: 14px; font-weight: 700; z-index: 10000;
    transition: top var(--duration-fast);
}
.skip-link:focus { top: 0; color: #fff; opacity: 1; }

/* ── Nav loader ── */
#nav-loader {
    position: fixed; top: 0; left: 0; right: 0; z-index: 9999;
    height: 3px; background: var(--blue);
    transform: scaleX(0); transform-origin: left;
    pointer-events: none;
}
#nav-loader.active {
    transform: scaleX(0.85);
    transition: transform 0.4s cubic-bezier(0.1, 0.6, 0.3, 1);
}

/* ── Переключатель темы (владелец 03.09.2026, место у ученика на мобилке
   пересмотрено 03.09.2026) ──
   Не плавающая кнопка — контрол лежит там, где у роли уже есть "рядом с
   Выйти": desktop/планшет — .desk-nav-footer-row в шаблонах сайдбара
   персонала и ученика (там же у ученика ещё шестерёнка уведомлений);
   мобилка персонала — рядом с .mobile-dashboard-logout в шапке дашборда; у
   ученика на мобилке — предпоследний лепесток радиального веера-меню
   (тот же приём для колокольчика уведомлений, файл веера, не здесь).

   Иконки солнце/луна общие для всех кнопок — переключаются по
   `[data-theme]` на `<html>`, а не по классу конкретной кнопки. */
.theme-toggle-icon--moon { display: none; }
:root[data-theme="dark"] .theme-toggle-icon--sun { display: none; }
:root[data-theme="dark"] .theme-toggle-icon--moon { display: block; }

/* Кнопка в подвале десктопного сайдбара (персонал и ученик) — тот же
   приём круглой иконки, что у `.notif-gear-btn` в шестерёнке ученика
   по соседству. */
.sidebar-theme-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0;
    border: 1px solid var(--line); background: var(--surface);
    color: var(--muted); cursor: pointer; padding: 0;
    transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);
}
.sidebar-theme-btn:hover { background: var(--surface-2); color: var(--ink); opacity: 1; }

/* Кнопка в мобильной шапке дашборда персонала (superadmin/главный
   преподаватель и куратор) — сидит рядом с .mobile-dashboard-logout__button
   на цветной hero-карточке, поэтому
   переиспользует именно её раскладку (белая иконка на полупрозрачной
   подложке), а не .sidebar-theme-btn. Составной селектор — специфичность
   выше, чем у самой .mobile-dashboard-logout__button, иначе она глушит
   display:none этого класса вне мобильной ширины. */
.mobile-dashboard-theme-btn.mobile-dashboard-logout__button { display: none; }
@media (max-width: 768px) {
    .mobile-dashboard-theme-btn.mobile-dashboard-logout__button { display: inline-flex; }
}

/* ── Floating widgets ── */
.floating-widget-tray {
    position: fixed;
    right: 18px;
    bottom: calc(72px + env(safe-area-inset-bottom, 0px) + 12px);
    z-index: 250;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    pointer-events: none;
}
/* Полноэкранное видео (плеер урока, z-index 10000) рисуется в body, вне
   сайдбара/боттом-нава — колокольчик уведомлений (z-index:250) сидит там же,
   на любой ширине экрана, и вылезал поверх видео, накрывая кнопку выхода
   снизу справа (владелец 31.08.2026, живая проверка на планшете). Прячем на
   время полноэкранного режима — тем же классом-флагом на html/body, что уже
   блокирует скролл (заведён в шаблонах видео, не здесь). */
html.has-video-pseudo-fullscreen .floating-widget-tray,
body.has-video-pseudo-fullscreen .floating-widget-tray,
html.has-video-pseudo-fullscreen .notif-pop,
body.has-video-pseudo-fullscreen .notif-pop { display: none; }
.notif-bell {
    position: relative;
    flex: 0 0 auto;
    width: 48px; height: 48px;
    border-radius: 50%;
    background: var(--surface);
    border: 1.5px solid var(--line);
    box-shadow: var(--sh-md);
    display: flex; align-items: center; justify-content: center;
    font-size: 22px;
    text-decoration: none;
    transition: box-shadow var(--duration-base), transform var(--duration-fast);
    color: var(--blue-text);
    pointer-events: auto;
}
.notif-bell:hover { box-shadow: 0 8px 28px rgba(20,30,80,.14); transform: translateY(-2px); opacity: 1; }
.notif-bell-badge {
    position: absolute; top: -2px; right: -2px;
    min-width: 18px; height: 18px; border-radius: 9px;
    background: #DC2626; color: #fff;
    font-size: 10px; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
    padding: 0 4px; line-height: 1;
    border: 2px solid var(--surface);
}

/* ── Notifications popup ── */
.notif-pop {
    position: fixed;
    z-index: 260;
    width: 340px; max-width: calc(100vw - 28px);
    max-height: min(70vh, 480px);
    right: 18px;
    bottom: calc(72px + env(safe-area-inset-bottom, 0px) + 72px);
    background: var(--surface);
    border: 1.5px solid var(--line);
    border-radius: 16px;
    box-shadow: 0 16px 48px rgba(20,30,80,.18);
    display: flex; flex-direction: column;
    overflow: hidden;
}
.notif-pop-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: 14px 16px; border-bottom: 1px solid var(--line);
    flex-shrink: 0;
}
.notif-pop-title { font-size: 15px; font-weight: 800; color: var(--ink); }
.notif-pop-close {
    background: none; border: none; cursor: pointer;
    font-size: 24px; line-height: 1; color: var(--muted);
    padding: 0 2px; font-family: inherit;
}
.notif-pop-close:hover { color: var(--ink); }
.notif-pop-body {
    overflow-y: auto; padding: 8px;
    display: flex; flex-direction: column; gap: 6px;
}
.notif-pop-footer {
    flex-shrink: 0; text-align: center;
    padding: 10px 16px; border-top: 1px solid var(--line);
}
.notif-pop-footer a {
    font-size: 13px; font-weight: 600; color: var(--blue-text); text-decoration: none;
}
.notif-pop-footer a:hover { text-decoration: underline; }
.notif-pop-empty {
    text-align: center; padding: 36px 16px;
    font-size: 13px; color: var(--dim);
}
.notif-pop-item {
    display: block; text-decoration: none; color: inherit;
    padding: 12px 12px; border-radius: 12px;
    border: 1px solid var(--line); background: var(--surface);
    transition: border-color var(--duration-fast), background var(--duration-fast);
}
a.notif-pop-item:hover { border-color: var(--blue); opacity: 1; }
.notif-pop-item.unread {
    background: rgba(168,85,247,0.06);
    border-color: rgba(168,85,247,0.2);
}
.notif-pop-item-title {
    font-size: 13px; font-weight: 600; color: var(--ink); line-height: 1.4;
}
.notif-pop-item.unread .notif-pop-item-title { font-weight: 700; }
.notif-pop-item-text {
    font-size: 12px; color: var(--muted); line-height: 1.55;
    white-space: pre-wrap; margin-top: 6px;
}
.notif-pop-item-meta { font-size: 11px; color: var(--dim); margin-top: 6px; }

@media (max-width: 767px) {
    .notif-pop {
        right: 10px; left: 10px; width: auto; max-width: none;
        top: calc(64px + env(safe-area-inset-top, 0px));
        bottom: auto;
    }
}

/* ── Main ── */
.main {
    flex: 1; max-width: 1000px; width: 100%; margin: 0 auto;
    padding: 24px 24px calc(80px + env(safe-area-inset-bottom, 0px));
}

/* ═══════════════════════════════════════
   PAGE STRUCTURE
   ═══════════════════════════════════════ */
.page-header {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--s-4); margin-bottom: var(--s-6);
}
.page-header--stacked { display: block; }
.page-title {
    font-family: var(--font-display);
    font-size: 24px; font-weight: 900; line-height: 1.25;
    letter-spacing: -0.025em; color: var(--text);
}
.page-title--standalone { margin-bottom: var(--s-6); }
.page-subtitle {
    margin-top: var(--s-1);
    font-size: 14px; color: var(--dim);
}
.section-label {
    margin-bottom: 10px;
    font-size: var(--fs-caption); font-weight: 700;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--dim);
}

/* ═══════════════════════════════════════
   CARDS
   ═══════════════════════════════════════ */
.card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-sm);
    padding: 22px;
    transition: box-shadow var(--duration-base), transform var(--duration-base), border-color var(--duration-base);
}
.card:hover { box-shadow: var(--sh-md); transform: translateY(-2px); border-color: rgba(168,85,247,.18); }

/* Full-screen status card (blocked/denied): own padding scale so it still shrinks under the 480px card override */
.status-card { max-width: 420px; padding: 48px 32px; }
@media (max-width: 480px) {
    .status-card { padding: 32px 20px; }
}

.card-dark {
    background: var(--blue-deep);
    border: 1px solid rgba(255,255,255,0.06);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-sm);
    padding: 22px;
    color: #fff;
    transition: box-shadow var(--duration-base), transform var(--duration-base);
}
.card-dark:hover { box-shadow: 0 8px 32px rgba(20,30,80,.22); transform: translateY(-2px); }

.card-blue {
    background: linear-gradient(165deg, var(--blue-fill-strong) 0%, var(--blue-fill) 100%);
    border: none;
    border-radius: var(--r-lg);
    box-shadow: var(--sh-blue);
    padding: 22px;
    color: #fff;
    transition: box-shadow var(--duration-base), transform var(--duration-base);
}
.card-blue:hover { box-shadow: 0 12px 36px rgba(168,85,247,.38); transform: translateY(-2px); }

.card-icon { font-size: 28px; margin-bottom: 14px; }
.card-title { font-family: var(--font-display); font-size: 17px; font-weight: 700; margin-bottom: 6px; letter-spacing: -0.015em; }
.card-desc { font-size: 13px; line-height: 1.55; }
.card .card-title { color: var(--ink); }
.card .card-desc { color: var(--muted); }
.card-dark .card-title, .card-blue .card-title { color: #fff; }
.card-dark .card-desc, .card-blue .card-desc { color: rgba(255,255,255,.72); }
.card-blue .card-desc { color: rgba(255,255,255,.93); }

/* Card icon variants */
.card-icon-wrap {
    width: 40px; height: 40px;
    border-radius: var(--r-md);
    background: var(--blue-soft);
    color: var(--blue-text);
    display: inline-flex; align-items: center; justify-content: center;
    margin-bottom: 14px;
}
.card-dark .card-icon-wrap,
.card-blue .card-icon-wrap { background: rgba(255,255,255,.16); color: #fff; }

/* ═══════════════════════════════════════
   BUTTONS
   ═══════════════════════════════════════ */

/* Blue brand button */
.btn-blue {
    display: inline-flex; align-items: center; gap: 8px;
    background: var(--blue-fill); color: #fff;
    font-size: 14px; font-weight: 600;
    padding: 13px 22px; border-radius: var(--r-pill);
    border: none; cursor: pointer; font-family: inherit;
    box-shadow: var(--sh-blue);
    text-decoration: none;
    transition: filter var(--duration-fast), transform var(--duration-fast);
}
.btn-blue:hover { filter: brightness(1.05); transform: translateY(-1px); color: #fff; opacity: 1; }
.btn-blue:disabled { opacity: 0.38; cursor: not-allowed; filter: none; transform: none; }
.btn-blue:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

/* Outline button */
.btn-outline {
    display: inline-flex; align-items: center; gap: 8px;
    background: transparent; color: var(--ink);
    font-size: 14px; font-weight: 600;
    padding: 12px 22px; border-radius: var(--r-pill);
    border: 1.5px solid var(--line);
    cursor: pointer; font-family: inherit;
    text-decoration: none;
    transition: border-color var(--duration-fast), color var(--duration-fast);
}
.btn-outline:hover { border-color: var(--blue); color: var(--blue-text); opacity: 1; }

/* Full-width primary modifier */
.btn-full { width: 100%; justify-content: center; }

/* Компактный save-модификатор (эталон «Сохранить балл» staff-проверки,
   REDESIGN-PLAN-2 Фаза 2): используем .btn-blue.btn-save во всех формах
   проверки/инлайн-редактирования вместо локальных save-классов */
.btn-save {
    min-height: 44px;
    padding: 8px 14px;
    border-radius: var(--r-sm);
    font-size: 13px;
    justify-content: center;
}

/* Destructive action — конвенция №3 (REDESIGN-PLAN-2): контурная, ВСЕГДА
   видимо-красная в покое (не только на hover — иначе на touch неотличима
   от нейтральной). Образец — .dlg-delete-btn из cabinet_feedback_detail. */
.btn-danger {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    background: transparent; color: var(--error);
    font-size: 14px; font-weight: 600;
    padding: 12px 22px; border-radius: var(--r-pill);
    border: 1.5px solid var(--error);
    cursor: pointer; font-family: inherit;
    text-decoration: none;
    transition: background var(--duration-fast), color var(--duration-fast);
}
.btn-danger:hover { background: var(--error-fill); color: #fff; opacity: 1; }
.btn-danger:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-danger:focus-visible { outline: 2px solid var(--error); outline-offset: 2px; }

/* Warning-действие (вернуть на правку и т.п.) — тот же контурный паттерн */
.btn-warn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    background: transparent; color: var(--warning-deep);
    font-size: 14px; font-weight: 600;
    padding: 12px 22px; border-radius: var(--r-pill);
    border: 1.5px solid var(--warning-deep);
    cursor: pointer; font-family: inherit;
    text-decoration: none;
    transition: background var(--duration-fast), color var(--duration-fast);
}
.btn-warn:hover { background: var(--warning-deep); color: #fff; opacity: 1; }
.btn-warn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-warn:focus-visible { outline: 2px solid var(--warning-deep); outline-offset: 2px; }

/* ═══════════════════════════════════════
   ACTION LAYOUT (REDESIGN-PLAN-2, Фаза 0)
   Конвенция №2: в DOM Cancel/назад идёт ПЕРВЫМ, Confirm/primary — ПОСЛЕДНИМ;
   flex-end прижимает группу вправо (primary оказывается справа). На узких
   экранах .form-actions складывается колонкой primary-сверху.
   ═══════════════════════════════════════ */
.form-actions {
    display: flex; align-items: center; justify-content: flex-end;
    flex-wrap: wrap; gap: 10px;
    margin-top: 18px;
}
/* `flex-wrap` и складывание колонкой — те же, что у `.form-actions` выше.
   До 28.09.2026 их тут не было: пара кнопок в модалке на узком экране не
   переносилась и сжималась по ширине. Правило одно на оба ряда, чтобы они не
   разъезжались снова. */
.modal-actions {
    display: flex; align-items: center; justify-content: flex-end;
    flex-wrap: wrap; gap: 10px;
    margin-top: 20px;
}
@media (max-width: 480px) {
    .form-actions, .modal-actions { flex-direction: column-reverse; align-items: stretch; }
    .form-actions > *, .modal-actions > * { width: 100%; justify-content: center; }
}

/* «← Назад» — единый класс (конвенция №4), визуальный язык существующих
   ~15 локальных копий (.dlg-back/.page-back/.su-back и т.д.) */
.back-link {
    display: inline-flex; align-items: center; gap: 6px;
    min-height: 44px;
    font-size: 13px; font-weight: 700;
    color: var(--muted); text-decoration: none;
    margin-bottom: 10px;
}
.back-link:hover { color: var(--blue-text); opacity: 1; }

/* Возврат в АОП на мобилке (шаблон partials/back_to_learning, владелец
   15.09.2026) — фиксированная кнопка в верхнем левом углу каждого экрана
   ученика. До неё выход в «Обучение» был только через бургер внизу: два тапа
   вместо одного, а на под-экранах (сдача работы, видео, загрузка фото) ещё и
   неочевидно, что бургер вообще откроется на нужный пункт.

   Отличается от .back-link по задаче, а не по виду: .back-link стоит в потоке
   контента и ведёт на предыдущий экран цепочки, эта кнопка всегда висит
   поверх страницы и всегда ведёт в одно место — домой.

   Место под неё резервируем padding'ом .main, а не отступом в каждом шаблоне.
   Зацепка — класс `has-learning-back`, который партиал вешает на body сам:
   там, где он себя не вывел (сам АОП, истёкший доступ, не-ученик), класса
   нет и вёрстка остаётся прежней. Не `body:has(...)` — :has() не понимают
   старые мобильные браузеры, а это правило несёт вёрстку, и его отказ клал
   бы кнопку на первый заголовок экрана.

   z-index 95 — ниже бургера (100) и лайтбокса (280): у кнопки свой угол экрана,
   перекрывать просмотр фото или открытое меню ей незачем. */
.learning-back {
    position: fixed;
    top: calc(12px + env(safe-area-inset-top, 0px));
    left: 16px;
    z-index: 95;
    display: none;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    padding: 0 16px 0 12px;
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    background: var(--surface);
    box-shadow: var(--sh-sm);
    color: var(--text);
    font-size: 13px; font-weight: 700;
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
    transition: background var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.learning-back:hover { background: var(--surface-2); }
.learning-back:active { transform: scale(.97); }
.learning-back:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.learning-back svg { display: block; flex-shrink: 0; color: var(--blue-text); }

/* Граница 1023px, а не 768px: бургер ученика живёт до той же ширины
   (bottom_nav_burger.css), с 1024px включается десктопный сайдбар, где пункт
   «Обучение» и так на виду. */
@media (max-width: 1023px) {
    .learning-back { display: inline-flex; }
    body.has-learning-back .main { padding-top: 68px; }
    /* Часть экранов держит собственную ссылку «← Кабинет» в начале контента,
       и ведёт она туда же: /cabinet у ученика редиректом уходит на
       /cabinet/learning (api/cabinet.py, ROLE_CABINET_MAP). Рядом с кнопкой
       это две стрелки назад в одно место — вторую на мобилке убираем.
       Правилом по href, а не правкой шаблонов: под него попадает и
       динамический back_url, когда он указывает на АОП. Десктоп не трогаем —
       там кнопки нет, ссылка остаётся единственным выходом. */
    body.has-learning-back .back-link[href="/cabinet/learning"],
    body.has-learning-back .back-link[href="/cabinet"] { display: none; }
}

/* Узкий экран: .main сужает боковой отступ до 12px — кнопка идёт следом,
   иначе её левый край не совпадает с краем карточек под ней. */
@media (max-width: 480px) {
    .learning-back { left: 12px; }
}

/* Native trigger for image lightboxes. Keeps gallery thumbnails keyboard-accessible. */
.photo-zoom-button {
    display: block; max-width: 100%; padding: 0; border: 0;
    background: transparent; color: inherit; font: inherit; line-height: 0;
    cursor: zoom-in;
}
.photo-zoom-button > img { display: block; max-width: 100%; }
.photo-zoom-button:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

/* Голосовые вложения в чатах (диалог обратной связи и подобные) — без этого
   правила <audio controls> растягивается на всю ширину контейнера в некоторых
   браузерах. Тег, не класс: любой чат может использовать без нового CSS. */
audio { max-width: 100%; }

/* Sticky mobile CTA (конвенция №5): обёртка вокруг primary-кнопки внизу
   главной формы экрана. position:sticky остаётся в потоке (не перекрывает
   контент), липнет над bottom-nav (высота ~72px, z-index 100) до своей
   естественной позиции. На десктопе (≥1024px, bottom-nav скрыт) — обычный блок. */
.cta-sticky {
    position: sticky;
    bottom: calc(72px + env(safe-area-inset-bottom, 0px) + 10px);
    z-index: 90;
}
/* Вариант для экранов без bottom-nav (онбординг-профиль и т.п.) */
.cta-sticky--bare { bottom: calc(env(safe-area-inset-bottom, 0px) + 10px); }
@media (min-width: 1024px) {
    .cta-sticky { position: static; }
}

/* ═══════════════════════════════════════
   PILL NAVIGATION
   ═══════════════════════════════════════ */
.nav-pill {
    display: inline-flex; align-items: center; gap: 4px;
    background: var(--surface);
    border: 1px solid var(--line);
    padding: 6px;
    border-radius: var(--r-pill);
}
.nav-pill-item {
    font-size: 14px; font-weight: 500;
    color: var(--muted);
    text-decoration: none;
    padding: 8px 16px; border-radius: var(--r-pill);
    transition: color var(--duration-fast), background var(--duration-fast);
    border: none; cursor: pointer; font-family: inherit; background: transparent;
}
.nav-pill-item:hover { color: var(--ink); opacity: 1; }
.nav-pill-item.active,
.nav-pill-item--active {
    background: var(--blue-fill);
    color: #fff;
}
/* Полоса, которая не помещается в 375px, прокручивается вбок, а не ломает
   страницу. Тот же приём, что `.act-tabs` на «Статистике активности». */
.nav-pill--scroll { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
.nav-pill--scroll .nav-pill-item { flex: 0 0 auto; white-space: nowrap; }

/* ═══════════════════════════════════════
   CHIPS / TAGS
   ═══════════════════════════════════════ */
.chip {
    display: inline-flex; align-items: center; gap: 6px;
    background: var(--blue-soft);
    color: var(--blue-ink);
    border: 1px solid var(--blue-tint);
    font-size: 12px; font-weight: 500;
    padding: 5px 12px;
    border-radius: var(--r-pill);
}
.chip.dark {
    background: rgba(255,255,255,.16);
    color: #fff;
    border-color: rgba(255,255,255,.28);
}
.chip-x { opacity: 0.55; font-weight: 400; }

/* Compact categorical label. Semantic color variants are defined below. */
.tag-pill {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 3px 10px; border-radius: var(--r-pill);
    font-size: 11px; font-weight: 800; line-height: 1.4;
    letter-spacing: 0.02em; white-space: nowrap;
    background: var(--blue-soft); color: var(--blue-on-soft);
}
.tag-pill-muted {
    background: var(--bg); color: var(--muted);
    border: 1px solid var(--line);
}

/* ═══════════════════════════════════════
   STAT TILE / METRIC
   ═══════════════════════════════════════ */
.stat-tile {
    background: var(--surface-2);
    border-radius: var(--r-lg);
    padding: 18px 20px;
    display: flex; flex-direction: column;
    justify-content: space-between;
    min-width: 0;
}
.stat-tile-head {
    display: flex; align-items: center; gap: 8px;
    color: var(--dim);
    font-size: 13px; font-weight: 500;
}
.stat-tile-head .ico {
    width: 28px; height: 28px;
    background: var(--blue-soft);
    color: var(--blue-text);
    border-radius: 8px;
    display: inline-flex; align-items: center; justify-content: center;
}
.stat-tile-value {
    font-family: var(--font-display);
    font-size: 30px; font-weight: 700;
    letter-spacing: -0.025em;
    color: var(--ink);
    margin-top: 18px;
}

/* ═══════════════════════════════════════
   SEGMENTED CONTROL
   ═══════════════════════════════════════ */
.seg {
    display: inline-flex;
    background: var(--surface-2);
    border-radius: var(--r-pill);
    padding: 4px;
}
.seg-btn {
    background: transparent;
    border: none; cursor: pointer; font-family: inherit;
    color: var(--muted);
    font-size: 13px; font-weight: 500;
    padding: 8px 18px;
    border-radius: var(--r-pill);
    transition: background var(--duration-fast), color var(--duration-fast);
}
.seg-btn.active {
    background: var(--blue-fill);
    color: #fff;
}

/* ═══════════════════════════════════════
   FORMS
   ═══════════════════════════════════════ */
.form-label,
.field-label {
    display: block;
    font-size: 12px; font-weight: 600;
    color: var(--ink);
    margin-bottom: 8px;
}

.form-select, .form-input,
.field-input, .field-select {
    width: 100%;
    background: var(--surface-2);
    border: 1px solid transparent;
    border-radius: var(--r-md);
    padding: 14px 18px;
    font-size: 14px;
    font-family: inherit;
    color: var(--ink);
    outline: none;
    transition: border-color var(--duration-fast), background var(--duration-fast), box-shadow var(--duration-fast);
}
.form-select::placeholder, .form-input::placeholder,
.field-input::placeholder { color: var(--dim); }
.form-select:focus, .form-input:focus,
.field-input:focus, .field-select:focus {
    background: var(--surface);
    border-color: var(--blue);
    box-shadow: 0 0 0 3px rgba(168,85,247,.15);
}
.form-select option, .field-select option { background: var(--surface); }

/* Select with custom arrow */
.field-select, select.form-select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%236B7280' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 16px center;
    padding-right: 40px;
    cursor: pointer;
}

/* ── Radio buttons ── */
.radio-group { display: flex; gap: 10px; margin-bottom: var(--s-3); }
.radio-btn {
    flex: 1; padding: 11px; border: 1.5px solid var(--line); border-radius: var(--r-sm);
    text-align: center; cursor: pointer; transition: border-color var(--duration-base), color var(--duration-base), background var(--duration-base);
    font-size: 14px; font-weight: 500; color: var(--muted);
}
.radio-btn:hover { border-color: rgba(168,85,247,.30); color: var(--blue-text); }
/* Кнопка спрятана визуально, а не `display: none`: так выбор доступен с
   клавиатуры и экранному диктору (первое место — оценка ОС учеником,
   партиал `partials/feedback_rating`, 01.10.2026). Селектор с `[type="radio"]`:
   общее правило радиокнопок ниже иначе вернуло бы ей 20×20 и сдвинуло цифру. */
.radio-btn input[type="radio"] {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.radio-btn:has(input:focus-visible) { outline: 2px solid var(--blue); outline-offset: 2px; }
.radio-btn.active, .radio-btn:has(input:checked) {
    border-color: var(--blue); color: var(--blue-text);
    background: var(--blue-soft);
}

/* ── Флажок и переключатель — единственное описание на весь кабинет ──────────
   До 28.09.2026 чекбокса в дизайн-системе не было вовсе, и каждый скин описал
   свой: три файла (`program_ios.css`, `digest_goal_ios.css`,
   `videos_admin_ios.css`) несли это правило слово в слово, а superadmin-экраны
   ставили 18×18, 15×15 и системный 13×13. Замер дал пять размеров одного
   контрола на одном продукте.

   Тот же приём, которым сюда съехала палитра `--ios-*` (см. комментарий
   у `:root`): общее объявление живёт здесь, скоупы его наследуют.

   `flex-shrink: 0` обязателен: флажок почти всегда стоит в flex-строке рядом
   с подписью и без него сплющивается на узком экране. */
input[type="checkbox"],
input[type="radio"] {
    appearance: none; -webkit-appearance: none; margin: 0; flex-shrink: 0;
    position: relative; width: 20px; height: 20px;
    border: 1.5px solid var(--ios-label-3); background: var(--ios-card-bg); cursor: pointer;
}
input[type="checkbox"] { border-radius: 6px; }
input[type="radio"] { border-radius: 50%; }
input[type="checkbox"]:checked,
input[type="radio"]:checked { background: var(--ios-blue); border-color: var(--ios-blue); }
input[type="checkbox"]:disabled,
input[type="radio"]:disabled { opacity: .5; cursor: not-allowed; }
input[type="checkbox"]:checked::after {
    content: ''; position: absolute; top: 44%; left: 50%;
    width: 10px; height: 5px;
    border-left: 2px solid var(--on-color); border-bottom: 2px solid var(--on-color);
    transform: translate(-50%, -50%) rotate(-45deg);
}
input[type="radio"]:checked::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    width: 7px; height: 7px; border-radius: 50%; background: var(--on-color);
    transform: translate(-50%, -50%);
}
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible { outline: 2px solid var(--ios-blue); outline-offset: 2px; }

/* ── Выбор файла ── */
/* Системное поле говорит на языке телефона («Choose Files / No file chosen»).
   Поле остаётся настоящим, но скрыто визуально, а не `display: none`: так оно
   держит фокус с клавиатуры и читается экранным диктором. Нажимают на
   кнопку-`label` (`.btn-outline.file-pick-btn`), имена выбранных файлов —
   строкой `.file-pick-names`. Первое место — блоки «Домашнее задание» в ленте
   ученика (`task-blocks-render.js`, `uploadForm`; аудит АОП 30.09.2026).
   Заменил неиспользуемый `.file-input-wrap`. */
.file-pick { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-bottom: var(--s-3); }
.file-pick-input {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.file-pick-input:focus-visible + .file-pick-btn { outline: 2px solid var(--blue); outline-offset: 2px; }
.file-pick-btn { min-height: 44px; }
.file-pick-names { margin: 0; font-size: 13px; line-height: 1.4; color: var(--muted); overflow-wrap: anywhere; }

/* ═══════════════════════════════════════
   ALERTS
   ═══════════════════════════════════════ */
.alert { padding: 14px 18px; border-radius: var(--r-md); font-size: 14px; line-height: 1.5; }
.alert-success { background: rgba(22,163,74,0.08); border: 1px solid rgba(22,163,74,0.18); color: var(--success); }
.alert-error   { background: rgba(220,38,38,0.07); border: 1px solid rgba(220,38,38,0.18); color: var(--error); }

/* ═══════════════════════════════════════
   EMPTY STATES
   ═══════════════════════════════════════ */
.empty-state {
    padding: 48px 20px;
    text-align: center;
    color: var(--dim);
}
.empty-icon { margin-bottom: var(--s-3); font-size: 40px; }
.empty-text { font-size: 14px; }
.empty-mini {
    padding: var(--s-6) 0;
    text-align: center;
    color: var(--dim);
    font-size: var(--fs-small);
}

/* ═══════════════════════════════════════
   GRIDS & LAYOUT UTILITIES
   ═══════════════════════════════════════ */
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ═══════════════════════════════════════
   TEXT UTILITIES
   ═══════════════════════════════════════ */
.text-accent { font-weight: 800; color: var(--blue-text); }
.text-muted { color: var(--muted); }
.footer { text-align: center; padding: 20px; font-size: 12px; color: var(--dim); }

/* ── Badges ── */
.badge { display: inline-block; padding: 4px 10px; border-radius: var(--r-pill); font-size: 12px; font-weight: 700; }
.badge-before { background: rgba(220,38,38,0.08); color: var(--error); }
.badge-after  { background: rgba(22,163,74,0.10); color: var(--success); }

/* ── Caption ── */
.caption {
    font-size: var(--fs-caption); font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.12em;
    color: var(--dim);
}

/* ═══════════════════════════════════════
   PHOTO GRID
   ═══════════════════════════════════════ */
.photo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; }
.photo-grid img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--r-sm); border: 1px solid var(--line); }
/* Превью выбранного фото до отправки (`static/js/photo-prep.js`, 04.10.2026):
   крестик убирает фото, ⟳ поворачивает его на 90° — пиксели крутятся в
   браузере перед отправкой, на экране превью крутится через transform. */
.photo-grid[hidden] { display: none; }
/* Превью под кнопкой «Выбрать фото» в блоке задания — до подписи «Описание работы». */
.file-pick + .photo-grid { margin-bottom: 12px; }
.preview-cell { position: relative; overflow: hidden; border-radius: var(--r-sm); }
.preview-cell img { display: block; transition: transform var(--duration-fast); }
.preview-remove,
.preview-rotate {
    position: absolute; width: 32px; height: 32px; border-radius: 50%;
    background: rgba(28,28,30,0.6); color: var(--on-color);
    border: none; cursor: pointer; font-size: 13px;
    display: flex; align-items: center; justify-content: center;
    z-index: 2; line-height: 1;
}
.preview-remove { top: 4px; right: 4px; }
.preview-rotate { bottom: 4px; left: 4px; font-size: 18px; touch-action: manipulation; }
/* Видно 32 px, а палец ловит 44 (06.10.2026): в ячейке ~117 px на телефоне
   кнопка крупнее закрывала бы рисунок, а 32 px — мимо, по фото. */
.preview-remove::before,
.preview-rotate::before { content: ""; position: absolute; inset: -6px; }
.preview-rotate[hidden] { display: none; }
.preview-remove:hover { background: var(--error-fill); }
.preview-rotate:hover { background: rgba(28,28,30,0.8); }
@media (prefers-reduced-motion: reduce) {
    .preview-cell img { transition: none; }
}

/* Ход выбора в блоке «Сравнение работ» на экране проверки (владелец
   28.09.2026): пара за парой, выбранная работа обведена. */
.cmp-steps { margin-top: var(--s-2); }
.cmp-steps > summary { cursor: pointer; font-weight: 600; }
.cmp-steps-list { list-style: none; margin: var(--s-2) 0 0; padding: 0; display: grid; gap: 12px; }
.cmp-step { display: grid; grid-template-columns: repeat(2, minmax(0, 160px)); gap: var(--s-2); }
.cmp-step-num { grid-column: 1 / -1; font-size: 13px; color: var(--dim); }
.cmp-step-work { margin: 0; min-width: 0; display: grid; gap: 4px; font-size: 13px; }
.cmp-step-work img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--r-sm); border: 1px solid var(--line); }
.cmp-step-work.is-winner img { border: 3px solid var(--success); }
.cmp-step-work .chip { justify-self: start; }

/* ═══════════════════════════════════════
   LIGHTBOX
   ═══════════════════════════════════════ */
/* z-index 280 (владелец 29.09.2026): на 100 поверх затемнения рисовались
   нижнее меню (100, позже в разметке), меню сотрудника (200) и колокольчик
   (250), а колокольчик накрывал «×». Выше — всплывашки уведомлений и
   подсказок (260) и калькулятор (261–262): их открывают не из просмотра фото.
   Ниже — только то, что обязано перекрыть всё: окно «сначала сдай работы»
   и тост (300), онбординг (1200), полноэкранное видео (10000). */
.lightbox {
    position: fixed; inset: 0; background: rgba(10,10,30,0.9);
    z-index: 280; display: none; align-items: center; justify-content: center; padding: 20px;
}
.lightbox.open { display: flex; }
.lightbox-stage {
    position: relative; display: flex; flex-direction: column; align-items: center;
    gap: 12px; width: min(100%, 1100px); max-height: 100%;
}
/* Рамка обрезает приближенное фото по своему краю (07.10.2026): иначе
   увеличенный рисунок ложился бы на стрелки и кнопки. `touch-action: none`
   отдаёт щипок и перетаскивание `lightbox.js`, а не масштабу страницы. */
.lightbox-frame {
    position: relative; overflow: hidden; max-width: 100%; line-height: 0;
    border-radius: var(--r-md); box-shadow: 0 24px 80px rgba(0,0,0,.5);
    touch-action: none; user-select: none; -webkit-user-select: none;
    cursor: zoom-in;
    /* Не сжиматься (11.10.2026): с `overflow: hidden` флекс разрешает рамке
       ужаться ниже фото, и когда фото + кнопки + миниатюры не влезали по
       высоте, рамка молча срезала низ рисунка — ученица решила, что так и
       задумано, и спросила, считать ли пропорции по обрезанному мольберту. */
    flex-shrink: 0;
}
.lightbox.is-zoomed .lightbox-frame { cursor: grab; }
.lightbox.is-zoomed .lightbox-frame.is-gesturing { cursor: grabbing; }
/* Высота фото — не больше того, что остаётся от видимого экрана после
   кнопок, счётчика и миниатюр (`--lightbox-chrome`, замерено на стенде).
   `dvh`, а не `vh`: на айфоне `vh` считается по экрану без панелей браузера
   и Telegram, и фото выходило выше видимой части. Браузер без `dvh` берёт
   прежнюю строку с `vh`. */
.lightbox { --lightbox-chrome: 220px; }
.lightbox.is-single { --lightbox-chrome: 110px; }
#lightbox-img {
    max-width: 100%; max-height: 75vh; object-fit: contain; background: #000;
    max-height: min(75vh, calc(100dvh - var(--lightbox-chrome)));
    transform-origin: center; transition: transform var(--duration-fast);
    -webkit-user-drag: none;
}
/* Пока палец или мышь ведут фото, без анимации: она отставала бы от пальца. */
.lightbox-frame.is-gesturing #lightbox-img { transition: none; }
.lightbox-close {
    position: absolute; top: calc(16px + env(safe-area-inset-top, 0px)); right: 16px;
    background: rgba(20,20,30,.62); border: 1px solid rgba(255,255,255,.22); color: #fff;
    width: 44px; height: 44px; border-radius: 50%; font-size: 20px; cursor: pointer;
    transition: background var(--duration-base); z-index: 2;
}
.lightbox-close:hover { background: rgba(20,20,30,.85); }
.lightbox-nav {
    position: absolute; top: 50%; transform: translateY(-50%);
    /* Тёмная подложка, а не белая: стрелки лежат поверх фото, и на рисунке
       на белой бумаге белые пропадали (06.10.2026). */
    background: rgba(20,20,30,.62); border: 1px solid rgba(255,255,255,.22); color: #fff;
    width: 44px; height: 44px; border-radius: 50%; font-size: 28px; line-height: 1; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: background var(--duration-base);
    /* Над рамкой фото: она позиционирована и иначе накрыла бы «‹» на телефоне. */
    z-index: 1;
}
.lightbox-nav:hover { background: rgba(20,20,30,.85); }
.lightbox-prev { left: -8px; }
.lightbox-next { right: -8px; }
.lightbox.is-single .lightbox-nav,
.lightbox.is-single .lightbox-thumbs,
.lightbox.is-single .lightbox-counter { display: none; }
.lightbox-counter { color: rgba(255,255,255,.8); font-size: 13px; font-weight: 600; letter-spacing: 0.5px; }
.lightbox-thumbs {
    display: flex; gap: 8px; overflow-x: auto; max-width: 100%;
    padding: 4px 2px; scrollbar-width: thin;
}
.lightbox-thumbs::-webkit-scrollbar { height: 6px; }
.lightbox-thumbs::-webkit-scrollbar-thumb { background: rgba(255,255,255,.25); border-radius: 3px; }
.lightbox-thumb {
    flex: 0 0 auto; padding: 0; background: none; cursor: pointer;
    border: 2px solid transparent; border-radius: 8px; overflow: hidden;
    transition: border-color var(--duration-fast), transform var(--duration-fast);
}
.lightbox-thumb img { width: 72px; height: 54px; object-fit: cover; display: block; opacity: 0.7; transition: opacity var(--duration-fast); }
.lightbox-thumb:hover { border-color: rgba(255,255,255,.4); }
.lightbox-thumb:hover img { opacity: 0.9; }
.lightbox-thumb.is-current { border-color: var(--blue); }
.lightbox-thumb.is-current img { opacity: 1; }
/* Поворот фото (с 04.10.2026 — у всех ролей, каждой в своей зоне). Кнопки
   рендерятся скрытыми, `lightbox.js` открывает их только у слайда, который
   сервер разрешил крутить (`/cabinet/rotate-photo/allowed`).
   Ряд под фото, значки как в «Фото» на iPhone (владелец 06.10.2026): поверх
   фото белые кнопки сливались с рисунком на белой бумаге. */
.lightbox-tools { display: flex; justify-content: center; gap: 16px; }
.lightbox-tools[hidden] { display: none; }
.lightbox-tool {
    background: rgba(20,20,30,.62); border: 1px solid rgba(255,255,255,.22); color: var(--on-color);
    width: 44px; height: 44px; border-radius: 50%; padding: 0;
    cursor: pointer; transition: background 0.2s; touch-action: manipulation;
    display: flex; align-items: center; justify-content: center;
}
.lightbox-tool:hover { background: rgba(20,20,30,.85); }
.lightbox-tool:disabled { opacity: .4; cursor: default; }
.lightbox-tool-icon {
    width: 24px; height: 24px; fill: none; stroke: currentColor;
    stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
.lightbox-tool-icon.is-mirrored { transform: scaleX(-1); }
.lightbox-tools.is-busy { pointer-events: none; }
.lightbox-tools.is-busy .lightbox-tool:not(.is-spinning) { opacity: .45; }
.lightbox-tool.is-spinning { animation: spin 0.9s linear infinite; }

/* ═══════════════════════════════════════
   GATE MODAL — портфолио «До» не загружено (14.09.2026)
   Блокирующий поп-ап поверх экрана: пока ученик не сдал работы «До», клик
   по любому разделу меню, кроме «Актуального образовательного пространства»,
   должен не открывать раздел, а показывать это окно. Оверлей и карточка
   переиспользуют существующий язык — .lightbox для fixed+backdrop,
   .card/.status-card для самой карточки, .modal-actions/.btn-blue для кнопки.
   ═══════════════════════════════════════ */
.gate-modal {
    position: fixed; inset: 0; z-index: 300;
    background: rgba(10,10,30,0.55);
    display: none; align-items: center; justify-content: center; padding: 20px;
}
.gate-modal.open { display: flex; }
.gate-modal-card { position: relative; text-align: center; }
.gate-modal-close {
    position: absolute; top: 10px; right: 10px;
    width: 32px; height: 32px; border-radius: 50%;
    border: none; background: var(--surface-2); color: var(--dim);
    font-size: 18px; line-height: 1; cursor: pointer;
}
.gate-modal-close:hover { background: var(--line); color: var(--ink); }

/* ── Spinner ── */
.spinner {
    width: 16px; height: 16px;
    border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
    border-radius: 50%; animation: spin 0.7s linear infinite;
    display: inline-block; vertical-align: middle; margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Inline SVG icons ── */
.svg-icon {
    display: inline-block;
    width: 1em; height: 1em;
    vertical-align: -0.125em;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    flex-shrink: 0;
}
.svg-icon-fill { fill: currentColor; stroke: none; }

/* ═══════════════════════════════════════
   DESKTOP SIDEBAR (≥1024px)
   ═══════════════════════════════════════ */
.app-sidebar, .staff-aside { display: none; }

@media (min-width: 1024px) {
    /* Sidebars are position:fixed (out of flow) — .main is normally body's only
       in-flow flex child, so flex-direction has no visible effect on it. But the
       impersonation banner (position:sticky, stays in-flow) becomes a 2nd flex
       item when present; row + align-items:stretch stretched it to full viewport
       height. Column direction keeps both stacking correctly in both cases. */
    body { flex-direction: column; }

    .app-sidebar, .staff-aside {
        display: flex;
        flex-direction: column;
        width: 220px;
        flex-shrink: 0;
        position: fixed;
        top: 0; left: 0;
        height: 100vh;
        background: var(--surface);
        border-right: 1px solid var(--line);
        z-index: 50;
        overflow-y: auto;
        padding: 0;
    }

    /* Impersonation banner is in-flow (position:sticky) above the fixed sidebar;
       push the sidebar down by the banner's single-line height so its logo isn't
       covered. Approximate — if the banner wraps to 2 lines (long name), a small
       overlap can reappear, but the sidebar itself stays fully usable either way. */
    body.is-impersonating .app-sidebar,
    body.is-impersonating .staff-aside {
        top: 50px;
        height: calc(100vh - 50px);
    }

    .main {
        margin-left: 220px;
        max-width: none;
        padding: 32px 40px 48px;
        width: calc(100% - 220px);
    }

    .bottom-nav { display: none !important; }

    .floating-widget-tray {
        top: auto;
        right: 24px;
        bottom: 24px;
        z-index: 250;
    }
}

@media (max-width: 767px) {
    .floating-widget-tray {
        top: calc(10px + env(safe-area-inset-top, 0px));
        right: 10px;
        bottom: auto;
        gap: 8px;
    }
    .notif-bell {
        width: 44px;
        height: 44px;
    }
}

@media (min-width: 1400px) {
    .main { padding: 40px 64px 48px; }
}

/* ── Desktop sidebar inner styles ── */
.desk-brand {
    display: flex; align-items: center; gap: 10px;
    padding: 24px 20px 20px;
    border-bottom: 1px solid var(--line);
}
.desk-brand-name { font-size: 15px; font-weight: 800; color: var(--ink); letter-spacing: -0.3px; }

.desk-nav {
    flex: 1;
    display: flex; flex-direction: column;
    padding: 12px 10px;
    gap: 2px;
}
.desk-nav-item {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 12px; border-radius: var(--r-sm);
    font-size: 14px; font-weight: 600;
    color: var(--muted); text-decoration: none;
    transition: background var(--duration-fast), color var(--duration-fast);
}
.desk-nav-item:hover { background: var(--surface-2); color: var(--ink); opacity: 1; }
.desk-nav-item:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; border-radius: var(--r-sm); }
.desk-nav-item.desk-nav-active { background: var(--blue-soft); color: var(--blue-text); }
.desk-nav-icon { font-size: 18px; width: 24px; text-align: center; flex-shrink: 0; }
.desk-nav-divider { height: 1px; background: var(--line); margin: 8px 10px; }
.desk-nav-footer {
    padding: 12px 10px 20px;
    border-top: 1px solid var(--line);
    display: flex; flex-direction: column; gap: 2px;
}
.desk-nav-footer-row { display: flex; align-items: center; gap: 6px; }
.desk-nav-footer-row .desk-nav-logout-form { flex: 1; margin: 0; }
.desk-nav-logout { color: var(--dim) !important; }
.desk-nav-logout:hover { color: var(--error) !important; background: rgba(220,38,38,.06) !important; }
/* Логаут — POST-форма с кнопкой, стилизованной под пункт меню. */
.desk-nav-logout-form { margin: 0; }
button.desk-nav-item {
    width: 100%;
    border: none;
    background: none;
    cursor: pointer;
    font: inherit;
    text-align: left;
}

/* Мобильный выход рядом с именем в синей hero-карточке дашборда. */
.dashboard-name-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 6px;
}
.mobile-dashboard-logout {
    display: none;
    margin: 0;
    flex-shrink: 0;
}
.mobile-dashboard-logout__button {
    width: 36px; height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid rgba(255,255,255,.35);
    border-radius: 50%;
    background: rgba(255,255,255,.14);
    color: #fff;
    font: inherit;
    line-height: 1;
    cursor: pointer;
    transition: background var(--duration-fast), transform var(--duration-fast);
}
.mobile-dashboard-logout__button:hover {
    background: rgba(255,255,255,.24);
}

/* Шестерёнка настроек уведомлений — рядом с "Выйти" в кабинете ученика. */
.notif-gear-wrap { position: relative; flex-shrink: 0; }
.notif-gear-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; border-radius: 50%;
    border: 1px solid var(--line); background: var(--surface);
    color: var(--muted); cursor: pointer; padding: 0; flex-shrink: 0;
    transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);
}
.notif-gear-btn:hover { background: var(--surface-2); color: var(--ink); opacity: 1; }
.notif-gear-btn[aria-expanded="true"] { background: var(--blue-soft); color: var(--blue-text); border-color: var(--blue-tint); }
.profile-hero .notif-gear-wrap { display: none; }
.profile-hero .notif-gear-btn {
    background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.35); color: #fff;
}
.profile-hero .notif-gear-btn:hover { background: rgba(255,255,255,.22); color: #fff; }
.profile-hero .notif-gear-btn[aria-expanded="true"] { background: rgba(255,255,255,.28); border-color: rgba(255,255,255,.5); }

/* position:fixed + координаты через JS (getBoundingClientRect кнопки) —
   .profile-hero держит overflow:hidden ради декоративных кругов, absolute
   внутри него обрезался бы по границе карточки. */
.notif-gear-pop {
    position: fixed; z-index: 260;
    width: 280px; max-width: calc(100vw - 32px);
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--r-lg); box-shadow: 0 16px 48px rgba(20,30,80,.18);
    padding: 14px;
}
.notif-gear-pop-title { font-size: 13px; font-weight: 800; color: var(--ink); margin-bottom: 8px; }
.notif-gear-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
.notif-gear-row + .notif-gear-row { border-top: 1px solid var(--line); }
.notif-gear-row-body { flex: 1; min-width: 0; }
.notif-gear-row-title { font-size: 13px; font-weight: 700; color: var(--ink); }
.notif-gear-row-note { font-size: 11px; color: var(--dim); line-height: 1.4; margin-top: 2px; }
.notif-gear-switch { position: relative; flex-shrink: 0; width: 40px; height: 24px; }
.notif-gear-switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.notif-gear-track {
    position: absolute; inset: 0; background: var(--line);
    border-radius: var(--r-pill); transition: background 0.15s;
}
.notif-gear-track::before {
    content: ''; position: absolute; top: 2px; left: 2px;
    width: 20px; height: 20px; border-radius: 50%;
    background: #fff; box-shadow: var(--sh-md); transition: transform 0.15s;
}
.notif-gear-switch input:checked + .notif-gear-track { background: var(--blue); }
.notif-gear-switch input:checked + .notif-gear-track::before { transform: translateX(16px); }
.notif-gear-switch input:disabled + .notif-gear-track { opacity: 0.5; cursor: not-allowed; }

/* Ник Telegram с копированием по нажатию (макрос tg_copy в components/,
   поведение — copy.js в static/js/). Экраны «Статистика активности» и
   статистика цикла (должники). До 02.10.2026 жил как .ios-ss .ss-copy. */
.tg-copy {
    display: inline-flex; align-items: center; gap: 4px; min-height: 24px; padding: 0;
    border: none; background: none; font: inherit; color: var(--blue-text); cursor: pointer;
}
.tg-copy::after { content: '⧉'; font-size: .95em; opacity: .6; }
.tg-copy.is-copied::after { content: '✓ скопировано'; color: var(--success-text); opacity: 1; }
.tg-copy:hover { opacity: .75; }
.tg-copy:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; border-radius: 4px; }

/* Подсказки-«всплывашки» по тапу — общий компонент кабинета ученика.
   position:fixed, координаты через JS, та же причина, что у .notif-gear-pop:
   карточки вроде .profile-hero держат overflow:hidden ради декоративных
   кругов, absolute внутри обрезался бы по границе карточки. В отличие от
   .notif-gear-wrap (один экземпляр на страницу), триггеров подсказки на
   странице много — app/static/js/hint.js управляет каждым независимо. */
.hint-wrap { position: relative; display: inline-flex; vertical-align: middle; }
.hint-trigger {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; border-radius: 50%;
    border: 1px solid var(--line); background: var(--surface-2);
    color: var(--muted); font-size: var(--fs-caption); font-weight: 700;
    cursor: pointer; padding: 0; flex-shrink: 0;
    transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);
}
/* Область касания 44×44 при кружке 20×20. Отступ считается от внутренней
   границы, без рамки: 18 + 2 × 13 = 44 (до аудита АОП 30.09.2026 стояло −12,
   то есть 42). Соседа ниже, который перекрыл бы область, держать под кнопкой
   по `z-index` — см. `.ios-learning .lrn-hero h1` в tracker.css. */
.hint-trigger::before { content: ''; position: absolute; inset: -13px; }
.hint-trigger:hover { background: var(--blue-soft); color: var(--blue-text); }
.hint-trigger[aria-expanded="true"] { background: var(--blue-soft); color: var(--blue-text); border-color: var(--blue-tint); }
.hint-trigger:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.hint-pop {
    position: fixed; z-index: 260;
    width: 260px; max-width: calc(100vw - 32px);
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--r-md); box-shadow: var(--sh-md);
    padding: 12px 30px 12px 14px;
}
.hint-pop-close {
    position: absolute; top: 6px; right: 6px;
    width: 28px; height: 28px; border-radius: 50%;
    border: none; background: transparent; color: var(--dim);
    cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
}
.hint-pop-close::before { content: ''; position: absolute; inset: -8px; }
.hint-pop-close:hover { background: var(--surface-2); color: var(--ink); }
.hint-pop-body { font-size: var(--fs-small); color: var(--ink); line-height: 1.45; }

/* ═══════════════════════════════════════
   RESPONSIVE
   ═══════════════════════════════════════ */
@media (max-width: 768px) {
    .nav { display: none; }
    .nav-mobile { display: none; }
    .grid-2 { grid-template-columns: 1fr; }
    .grid-3 { grid-template-columns: repeat(2, 1fr); }
    .main { padding: 16px 16px calc(80px + env(safe-area-inset-bottom, 0px)); }
    .dashboard-name-row { flex-wrap: wrap; }
    .mobile-dashboard-logout { display: inline-flex; }
    .profile-hero .notif-gear-wrap { display: inline-flex; }
}
@media (max-width: 640px) {
    .lightbox-prev { left: 4px; }
    .lightbox-next { right: 4px; }
    .lightbox-nav { width: 44px; height: 44px; font-size: 22px; }
    .lightbox-thumb img { width: 56px; height: 42px; }
    #lightbox-img { max-height: 65vh; max-height: min(65vh, calc(100dvh - var(--lightbox-chrome))); }
}
@media (max-width: 480px) {
    .page-header:not(.page-header--stacked) {
        flex-direction: column;
        align-items: stretch;
    }
    .page-header:not(.page-header--stacked) > .btn-blue,
    .page-header:not(.page-header--stacked) > .btn-outline {
        justify-content: center;
    }
    .grid-3 { grid-template-columns: 1fr; }
    .main { padding: 12px 12px calc(80px + env(safe-area-inset-bottom, 0px)); }
    .card, .card-dark, .card-blue { padding: 18px; border-radius: var(--r-md); }
    .card-title { font-size: 15px; }
}

/* ── Touch devices ── */
@media (hover: none), (pointer: coarse) {
    .btn-blue, .btn-outline, .btn-danger, .btn-warn { min-height: 44px; }
    /* Пилюли-переключатели — палец не попадал: кнопка была 36 px («На
       проверку / Все» в карточке ученика, проход 06.10.2026). Отступ
       обёртки 6 → 2, чтобы вся полоса осталась прежней высоты, 48 px. */
    .nav-pill { padding: 2px; }
    .nav-pill-item { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
    .btn-blue:hover { transform: none; box-shadow: none; filter: none; }
    .btn-danger:hover { background: transparent; color: var(--error); }
    .btn-warn:hover { background: transparent; color: var(--warning-deep); }
    .btn-danger:active { background: var(--error-fill); color: #fff; }
    .btn-warn:active { background: var(--warning-deep); color: #fff; }
    .card:hover, .card-dark:hover, .card-blue:hover { transform: none; box-shadow: var(--sh-sm); }
    .desk-nav-item:hover { background: none; color: var(--muted); }
    .btn-blue:active { filter: brightness(0.95); }
    .card:active { border-color: rgba(168,85,247,.18); }
    .mobile-dashboard-logout__button:hover { transform: none; }
    .mobile-dashboard-logout__button:active { background: rgba(255,255,255,.3); }
}

/* ── focus-visible ── */
:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }

/* ── Reduced motion (accessibility) ── */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
    /* Индикатор загрузки передаёт состояние, поэтому остаётся заметным. */
    .spinner {
        animation-duration: 0.9s !important;
        animation-iteration-count: infinite !important;
    }
}

/* ── Student tag pill variants (admin LK) ───────────────────────── */
.tag-pill--exam         { background: var(--blue-soft);            color: var(--blue-ink); }
.tag-pill--mode-offline { background: rgba(22,163,74,0.12);        color: var(--success-text); }
.tag-pill--mode-online  { background: rgba(168,85,247,0.12);        color: var(--blue-on-soft); }
.tag-pill--case         { background: rgba(252,211,77,0.22);       color: #92590A; }
.tag-pill--publish      { background: rgba(124,58,237,0.12);       color: #7C3AED; }
.tag-pill--curator,
.tag-pill--curator-tag,
.tag-pill--orange       { background: rgba(249,115,22,0.12);       color: #c2410c; }
.tag-pill--tariff       { background: rgba(100,116,139,0.10);      color: var(--dim); border: 1px solid var(--line); }

/* Плашка тарифа — общая для всех экранов, где она есть: шапка трекера и шапка
   дашборда ученика. Владелец 13.09.2026: «у каждого тарифа есть свой цвет,
   поэтому плашку лучше делать с белым фоном и надписью в цвет тарифа».
   До этого у каждого экрана была своя стеклянная копия с белой надписью и
   зелёной точкой — тариф не читался как отдельная сущность.

   Базовый цвет нейтральный, а не декоративный: без него тариф, для которого
   нет модификатора, унаследовал бы белый цвет шапки и пропал на белом фоне.
   Группу цвета шаблон получает фильтром tariff_slug, см. app/tmpl.py. */
.profile-tariff {
    display: inline-flex; align-items: center; gap: 6px;
    max-width: 100%;
    background: var(--tariff-pill-bg);
    border-radius: var(--r-pill);
    padding: 5px 13px;
    font-size: 12px; font-weight: 700;
    color: var(--tariff-legacy);
    letter-spacing: -0.01em;
    box-shadow: 0 2px 10px rgba(20,20,50,0.18);
}
.profile-tariff--confident-max { color: var(--tariff-confident-max); }
.profile-tariff--with-you      { color: var(--tariff-with-you); }
.profile-tariff--self          { color: var(--tariff-self); }
.profile-tariff--legacy        { color: var(--tariff-legacy); }
.profile-tariff-dot {
    width: 6px; height: 6px; flex-shrink: 0;
    border-radius: 50%; background: currentColor;
}
.tag-pill--pink         { background: rgba(236,72,153,0.12);       color: #be185d; }
.tag-pill--blue         { background: rgba(168,85,247,0.12);        color: var(--blue-text); }
.tag-pill--purple       { background: rgba(168,85,247,0.12);       color: #6b21a8; }

.student-info-pill--pink   { background: rgba(236,72,153,0.12); border-color: transparent; color: #be185d; }
.student-info-pill--blue   { background: rgba(168,85,247,0.12);  border-color: transparent; color: var(--blue-text); }
.student-info-pill--purple { background: rgba(168,85,247,0.12); border-color: transparent; color: #6b21a8; }

.student-hero-pill--pink   { background: rgba(236,72,153,0.35); border-color: rgba(236,72,153,0.5); color: #fff; }
.student-hero-pill--blue   { background: rgba(192,132,252,0.35); border-color: rgba(192,132,252,0.5); color: #fff; }
.student-hero-pill--purple { background: rgba(168,85,247,0.35); border-color: rgba(168,85,247,0.5); color: #fff; }

.student-tag-pills { display: inline-flex; flex-wrap: wrap; gap: 4px; }

/* Пробные экзамены: подборка соседей по баллу, владелец 11.09.2026.
   Модификаторы --red/--orange/--lime/--green — тот же 4-тиерный градиент,
   что и у бейджа в шапке работы, но под БЭМ-именем своего класса: голые
   имена вида «score-red» уже независимо переопределены в нескольких
   шаблонах — совпадение имени в base.css задело бы их как чужое
   переопределение общего класса, которого на деле нет. */
.cal-peers-title { margin-top: 14px; font-size: 12px; font-weight: 800; color: var(--dim); text-transform: uppercase; letter-spacing: 0.06em; }
.cal-peers { display: flex; gap: 8px; margin-top: 8px; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; padding-bottom: 4px; }
.cal-peer-card { flex: 0 0 92px; scroll-snap-align: center; position: relative; border-radius: 8px; overflow: hidden; border: 1px solid var(--line); }
.cal-peer-card img { width: 92px; height: 92px; object-fit: cover; display: block; cursor: zoom-in; }
.cal-peer-card.is-own { border: 2px solid var(--blue); box-shadow: var(--shadow-blue); }
.cal-peer-score { position: absolute; bottom: 4px; right: 4px; font-size: 11px; font-weight: 900; color: var(--on-color); padding: 2px 6px; border-radius: var(--r-pill); background: var(--dim-fill); }
.cal-peer-score--red { background: var(--score-red-fill); }
.cal-peer-score--orange { background: var(--score-orange-fill); }
.cal-peer-score--lime { background: var(--score-lime-fill); }
.cal-peer-score--green { background: var(--score-green-fill); }

/* Мелкая подсказка под полем формы. Общая: до неё каждый экран заводил свой
   класс на те же три свойства. */
.field-hint { font-size: 11px; color: var(--dim); margin-top: 4px; line-height: 1.4; }

/* Карта просмотра под плеером: какие куски ролика не засчитаны и кнопка
   перейти к первому (`video-player.js::renderWatchMap`, 09.10.2026). Общая
   для трёх разметок плеера — страница ролика, вкладка «Видео», видеоблок. */
.video-watch-map { display: grid; gap: 8px; margin-top: 8px; }
.video-watch-map[hidden] { display: none; }
.video-watch-map-bar {
    position: relative; height: 6px; overflow: hidden;
    border-radius: var(--r-pill); background: var(--success);
}
.video-watch-map-gap { position: absolute; top: 0; bottom: 0; background: var(--line); }
.video-watch-map-text { margin: 0; font-size: 13px; line-height: 1.45; color: var(--muted); }
.video-watch-map-jump { justify-self: start; padding: 8px 16px; font-size: 13px; }
.video-watch-map-jump[hidden] { display: none; }
