/* ===========================================================================
   «пусси дашборд.» — дизайн-система платформы.

   Язык не привязан к конкретному стилю — он смещается со временем, и это
   нормально. Текущая редакция: приглушённая тёплая палитра (пигменты, а не
   неон), мягкая рассеянная тень вместо жёсткого смещённого контура, тонкая
   обводка вместо толстой. Три правила ниже — это архитектура, а не описание
   конкретной картинки, и переживают любую смену языка:

   1. ЦВЕТ ЗАДАЁТ ПЛАТФОРМА, А НЕ МОДУЛЬ. Модуль пишет `<div class="card">` и
      получает карточку; какого она будет цвета — решает ядро, раздавая тон
      (`--tone`) по slug модуля. Поэтому дашборд из десяти чужих друг другу
      модулей выглядит одним альбомом, а не свалкой вкусов.
   2. ПОВЕРХНОСТЬ ЦВЕТНАЯ, КОНТУР И ТЕКСТ — ЧЕРНИЛА. Одна пара `--tone` и
      `--ink` объясняет любой блок: цветная плашка, тёмный контур, тёмный
      текст. В тёмной теме плоскость гаснет до глубокого оттенка того же
      цвета, чернила становятся кремовыми — роли не меняются.
   3. НИЧЕГО НЕ ВИСИТ В ВОЗДУХЕ. У каждого блока есть тень, которая кладёт
      его на бумагу, а не растворяет в ней — форма тени (жёсткая, мягкая)
      может меняться, сам факт тени под каждым блоком — нет.

   Модулю доступны только семантические классы (см. §10 module-spec).
   Своих цветов он не заводит — иначе ломается и тёмная тема, и целостность.
   =========================================================================== */

:root {
  color-scheme: light;

  /* ======================================================================
     ВРЕМЯ СУТОК

     Палитра объявляет ДВА набора цветов — дневной (`--day-*`) и ночной
     (`--night-*`), — а рабочие токены (`--bg`, `--paper`, `--ink`, тона)
     собираются из них смесью по трём числам, которые ставит браузерный код
     (web/seasons.js): `--night`, `--dusk`, `--dawn`, каждое 0…1.

     Почему так, а не «тёмная тема» отдельным файлом: тем здесь восемь, и
     восемь вторых копий — это восемь мест, где цвета разъедутся. И почему не
     полупрозрачная вуаль поверх страницы, как было сначала: вуаль гасит
     ВСЁ разом, включая цвет, — интерфейс становится плоской серой плашкой,
     а не вечерним. Здесь же ночью меняются сами цвета: зимняя ночь синяя,
     осенняя — тёмно-янтарная, а закат подмешивает поверх любой из них
     тёплый свет.
     ====================================================================== */
  --night: 0;   /* 0 — день, 1 — глубокая ночь */
  --dusk: 0;    /* 1 — солнце у горизонта вечером */
  --dawn: 0;    /* то же утром: свет холоднее и розовее */
  --tint-dusk: #ff8a3a;
  --tint-dawn: #ff9db8;

  /* --- дневная палитра платформы --------------------------------------- */
  /* Девять тонов для раздачи модулям — приглушённые пигменты (как природный
     краситель на ткани), а не яркий «стикер-пак» и не неон. Число тонов не
     меняется (см. TONES в sdk.js) — меняется только их насыщенность. */
  --day-c-sky:       #6f95ab;
  --day-c-lemon:     #c99a44;
  --day-c-bubble:    #c98a7c;
  --day-c-mint:      #7d9c85;
  --day-c-grape:     #8d7aa6;
  --day-c-coral:     #b5563a;
  --day-c-teal:      #5f8a86;
  --day-c-tangerine: #c1793c;
  --day-c-lime:      #a3ad6e;

  /* Чернила: контур, тень и текст поверх цветной плоскости — один цвет.
     Днём тёплый почти-чёрный под тёплую бумагу, ночью — наоборот, светлый
     по тёмной. Роль не меняется, меняется полюс. */
  --day-ink: #2a1f18;

  --day-bg: #ece0c8;             /* бумага под всем — тёплый пергамент */
  --day-paper: #f8f1e2;          /* поверхность нейтрального блока */
  --day-surface-2: #ecdfc4;      /* вложенная поверхность: строка, ячейка */
  --day-accent: #a8542f;         /* ссылки и текстовые акценты */

  /* Шторка (боковая навигация) — тёмная и днём, и ночью: единственная тёмная
     поверхность на светлой странице днём и самая глубокая ночью. Оттенки
     наведения и активной строки не авторятся — они выводятся из самого фона
     шторки ниже, иначе их пришлось бы держать в двух экземплярах на каждую
     из восьми палитр. */
  --day-sidebar-bg: #232f1f;
  --day-sidebar-ink: #dbe1c9;

  /* --- ночная палитра (Herat — она же палитра по умолчанию) ------------- */
  --night-bg: #15190f;
  --night-paper: #1e2418;
  --night-surface-2: #273020;
  --night-sidebar-bg: #0d1108;
  --night-ink: #ece6d2;
  --night-accent: #e08a54;
  /* Куда тянуть тона ночью: пигмент по тёмной бумаге вязнет, и его чуть
     подсветляют — в тёплую сторону у тёплых палитр, в холодную у холодных. */
  --tone-lift: #e6e0cc;

  --success: #4c7d54;
  --warning: #a8792e;
  --error: #a8492f;

  /* Цвета редкости ачивок. Те же, что в `core/badges.RARITIES`: карточку
     рисует сервер, а фольгу и счётчики поверх неё — браузер, и разъезжаться
     этим двум наборам нельзя. От времени суток они не зависят: редкость на
     светлом и на тёмном обязана быть одного цвета, иначе не узнаётся. */
  --rarity-common: #c3cbd6;
  --rarity-uncommon: #3fce85;
  --rarity-rare: #3d8df5;
  --rarity-epic: #9b5cf6;
  --rarity-legendary: #ffb020;

  /* Типографическая шкала: шаг 1.2, всё остальное — производные от неё. */
  --fs-xs: 12px;
  --fs-sm: 13px;
  --fs-md: 15px;
  --fs-lg: 18px;
  --fs-xl: 23px;
  --fs-2xl: 30px;
  --fs-display: clamp(30px, 5vw, 52px);
  /* Шаг отступов кратен четырём. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 28px;

  /* Скругление: три ступени — крупный блок, элемент управления, мелкий чип
     внутри карточки — чтобы скругление читалось системой, а не случайными
     числами по файлу. Функционально круглые элементы (точка на колокольчике,
     кружки редкости, кольцо доли) этот токен не берут — у них
     border-radius:50% прямо в своём правиле. */
  --radius: 24px;
  --radius-sm: 14px;
  --radius-xs: 10px;
  --radius-pill: 999px;
  --radius-frame: 32px; /* скругление стыка сайдбар/панель контента */
  /* Обводки нет: блок держит не контур, а тень (правило 3 выше) — вес языка
     целиком на ней. Токен остаётся объявленным (не 0 напрямую в каждом
     правиле), потому что `input`/`select`/`textarea` ниже держат свою
     собственную тонкую обводку ради читаемости поля. */
  --bw: 0;
  --cell: 84px;
  --gap: 20px;

  /* Ретро-окна: двухцветный градиент по тону и глянцевая полоса сверху.
     Платформенный chrome (шапки, кнопки, карточки, пилюли) не берёт этот
     токен по умолчанию — референс плоский, без глянца. Токен остаётся
     объявленным на случай, если его явно захочет модуль в своей собственной
     разметке (уровень модуля свободный), но платформа сама его не подставляет. */
  --chrome-gloss: linear-gradient(180deg,
      color-mix(in srgb, #fff 38%, transparent) 0%,
      color-mix(in srgb, #fff 6%, transparent) 45%,
      transparent 55%);
}

/* ==========================================================================
   Токены объявлены через @property — ради двух вещей.

   Первая: незарегистрированное свойство меняется рывком, а зарегистрированное
   БРАУЗЕР УМЕЕТ АНИМИРОВАТЬ. Поэтому вечерний переход на ночной набор, сам по
   себе почти мгновенный (полоса `--ts` узкая нарочно — см. выше), на экране
   выглядит как плавное угасание за секунду. Заодно плавно меняются и палитры.

   Вторая: у зарегистрированного свойства вычисленное значение — настоящий
   цвет, а не строка `color-mix(...)`. Всё, что дальше выводится из этих
   токенов (приглушённый текст, оттенки шторки, тени), пересчитывается на
   каждом кадре анимации само.

   initial-value обязателен по спецификации; сюда вписан дневной Herat —
   палитра по умолчанию, то есть ровно то, что и должно быть до применения
   чего-либо ещё. */
@property --bg          { syntax: "<color>"; inherits: true; initial-value: #ece0c8; }
@property --paper       { syntax: "<color>"; inherits: true; initial-value: #f8f1e2; }
@property --surface-2   { syntax: "<color>"; inherits: true; initial-value: #ecdfc4; }
@property --ink         { syntax: "<color>"; inherits: true; initial-value: #2a1f18; }
@property --accent      { syntax: "<color>"; inherits: true; initial-value: #a8542f; }
@property --sidebar-bg  { syntax: "<color>"; inherits: true; initial-value: #232f1f; }
@property --sidebar-ink { syntax: "<color>"; inherits: true; initial-value: #dbe1c9; }
@property --c-sky       { syntax: "<color>"; inherits: true; initial-value: #6f95ab; }
@property --c-lemon     { syntax: "<color>"; inherits: true; initial-value: #c99a44; }
@property --c-bubble    { syntax: "<color>"; inherits: true; initial-value: #c98a7c; }
@property --c-mint      { syntax: "<color>"; inherits: true; initial-value: #7d9c85; }
@property --c-grape     { syntax: "<color>"; inherits: true; initial-value: #8d7aa6; }
@property --c-coral     { syntax: "<color>"; inherits: true; initial-value: #b5563a; }
@property --c-teal      { syntax: "<color>"; inherits: true; initial-value: #5f8a86; }
@property --c-tangerine { syntax: "<color>"; inherits: true; initial-value: #c1793c; }
@property --c-lime      { syntax: "<color>"; inherits: true; initial-value: #a3ad6e; }

:root {
  transition:
    --bg .9s ease, --paper .9s ease, --surface-2 .9s ease, --ink .9s ease,
    --accent .9s ease, --sidebar-bg .9s ease, --sidebar-ink .9s ease,
    --c-sky .9s ease, --c-lemon .9s ease, --c-bubble .9s ease, --c-mint .9s ease,
    --c-grape .9s ease, --c-coral .9s ease, --c-teal .9s ease,
    --c-tangerine .9s ease, --c-lime .9s ease;
}
@media (prefers-reduced-motion: reduce) { :root { transition: none; } }

/* Порог темноты. Ставится браузерным кодом (web/seasons.js) ровно в середине
   перехода бумаги, и делает две вещи разом: сообщает браузеру, что он на
   тёмном (от этого зависят системные поля ввода, полосы прокрутки и
   автозаполнение — наши токены их не красят), и переворачивает чернила.
   Порог, а не плавность: чернила, растворяющиеся между тёмными и светлыми,
   в середине совпадают с бумагой — текста нет вообще. */
:root[data-dark] {
  color-scheme: dark;
  --ink: var(--night-ink);
}

/* --tone-fill вычисляется НА КАЖДОМ элементе, а не один раз в `:root`.
   Это не стилистическая мелочь, а условие, без которого тон вообще не
   работает. Custom property наследуется вычисленным значением: пока
   `--tone-fill: var(--tone)` стоял в `:root`, он резолвился там же — на
   корне, где тон всегда дефолтный, — и весь интерфейс наследовал готовый
   жёлтый. Ядро исправно ставило `--tone` на <main> и на каждый виджет, но
   пересчитать уже унаследованный `--tone-fill` это не могло, и дашборд из
   шести разноцветных модулей выходил одноцветным. */
* { --tone-fill: var(--tone); }

/* ==========================================================================
   ВОСЕМЬ ПАЛИТР — четыре именные и четыре сезонные.

   Каждая объявляет свой день и свою ночь. Ни одна не «инвертирует» страницу
   произвольно: днём бумага светлая, ночью тёмная, порядок глубин один и тот
   же всегда — шторка темнее фона, фон темнее карточки. Меняются акцент,
   бумага и — самое важное — набор из девяти тонов, которые ядро раздаёт
   модулям по хешу slug (см. sdk.js): поэтому смена палитры не просто
   перекрашивает хром, а выдаёт каждому модулю новую тройку родственных
   оттенков.
   ========================================================================== */
:root[data-palette="tabriz"] {
  --day-sidebar-bg: #452a20; --day-sidebar-ink: #ecdac9;
  --day-bg: #e6e0c6; --day-paper: #f7eee0; --day-surface-2: #ecdfc1;
  --day-accent: #5f7a4a;
  --day-c-sky: #7a99a3; --day-c-lemon: #c2a044; --day-c-bubble: #c98f6e;
  --day-c-mint: #8aa06a; --day-c-grape: #96789a; --day-c-coral: #a85a3a;
  --day-c-teal: #6a8f78; --day-c-tangerine: #c17a3c; --day-c-lime: #b0a85e;

  --night-bg: #1a120d; --night-paper: #241a13; --night-surface-2: #2e2219;
  --night-sidebar-bg: #100b07; --night-ink: #f0e2d2; --night-accent: #97b673;
  --tone-lift: #eee2cf;
}
:root[data-palette="isfahan"] {
  --day-sidebar-bg: #212b3c; --day-sidebar-ink: #d7dfe9;
  --day-bg: #e3e0d2; --day-paper: #f3ece0; --day-surface-2: #e3d8c3;
  --day-accent: #c99a44;
  --day-c-sky: #6f88a6; --day-c-lemon: #c9a25a; --day-c-bubble: #a67c96;
  --day-c-mint: #6e97a0; --day-c-grape: #7a6fa0; --day-c-coral: #a65c52;
  --day-c-teal: #557f96; --day-c-tangerine: #b8824f; --day-c-lime: #8fa06e;

  --night-bg: #0f1420; --night-paper: #171e2c; --night-surface-2: #202939;
  --night-sidebar-bg: #090d16; --night-ink: #e3e9f2; --night-accent: #e5b45f;
  --tone-lift: #dde6f2;
}
:root[data-palette="bukhara"] {
  --day-sidebar-bg: #3b1620; --day-sidebar-ink: #edd7d3;
  --day-bg: #ecdfc9; --day-paper: #f6ede0; --day-surface-2: #e8d8c0;
  --day-accent: #b8863f;
  --day-c-sky: #7a8fa0; --day-c-lemon: #c99a4f; --day-c-bubble: #c17e8a;
  --day-c-mint: #7d9670; --day-c-grape: #9a6f8f; --day-c-coral: #a8493f;
  --day-c-teal: #61857e; --day-c-tangerine: #b8863f; --day-c-lime: #a3a066;

  --night-bg: #1a0e12; --night-paper: #24141a; --night-surface-2: #2f1b23;
  --night-sidebar-bg: #110709; --night-ink: #f2dfd8; --night-accent: #d9a05a;
  --tone-lift: #f0dcd6;
}

/* Сезонные палитры — тот же контракт, но с настроением времени года. К
   каждой прилагается своя погода (web/seasons.js): лепестки, дождь, листья
   или снег. Погода — не часть палитры, а её спутник: цвета остаются, даже
   когда эффект выключен. */
:root[data-palette="spring"] {
  --day-sidebar-bg: #3b2a3a; --day-sidebar-ink: #f1dfe6;
  --day-bg: #eadfdf; --day-paper: #faf1ef; --day-surface-2: #e9d9d9;
  --day-accent: #a84f72;
  --day-c-sky: #7fa0c2; --day-c-lemon: #cfab4e; --day-c-bubble: #d1899b;
  --day-c-mint: #86b184; --day-c-grape: #a07fbb; --day-c-coral: #c2665f;
  --day-c-teal: #6aa89c; --day-c-tangerine: #cc8a55; --day-c-lime: #a8bd66;

  --night-bg: #170f18; --night-paper: #211823; --night-surface-2: #2c2030;
  --night-sidebar-bg: #100911; --night-ink: #f2e2ea; --night-accent: #dd87a6;
  --tone-lift: #f6e2ec;
}
:root[data-palette="summer"] {
  --day-sidebar-bg: #17323c; --day-sidebar-ink: #d9e9e6;
  --day-bg: #dfeae6; --day-paper: #f2f8f4; --day-surface-2: #d9e8e2;
  --day-accent: #1f7d8c;
  --day-c-sky: #4f93b8; --day-c-lemon: #cfa63f; --day-c-bubble: #cf8378;
  --day-c-mint: #66ad7c; --day-c-grape: #7d7cb5; --day-c-coral: #c05540;
  --day-c-teal: #3f948f; --day-c-tangerine: #c7822f; --day-c-lime: #94b852;

  --night-bg: #0b1820; --night-paper: #12222b; --night-surface-2: #192e39;
  --night-sidebar-bg: #061017; --night-ink: #dfeef0; --night-accent: #56b9c9;
  --tone-lift: #d9edf0;
}
:root[data-palette="autumn"] {
  --day-sidebar-bg: #3a2215; --day-sidebar-ink: #f0dcc4;
  --day-bg: #ecd9bc; --day-paper: #f8ecd7; --day-surface-2: #e7d3b0;
  --day-accent: #a8541f;
  --day-c-sky: #7d8d9a; --day-c-lemon: #cd9a35; --day-c-bubble: #c47b60;
  --day-c-mint: #8b9a60; --day-c-grape: #8e6d84; --day-c-coral: #b04a2c;
  --day-c-teal: #6a8a70; --day-c-tangerine: #c4702a; --day-c-lime: #a99a4b;

  --night-bg: #180f0a; --night-paper: #231710; --night-surface-2: #2e2017;
  --night-sidebar-bg: #0f0805; --night-ink: #f3e2cc; --night-accent: #d98a45;
  --tone-lift: #f4e3cb;
}
:root[data-palette="winter"] {
  --day-sidebar-bg: #1b2635; --day-sidebar-ink: #dde6f1;
  --day-bg: #e2e8f0; --day-paper: #f5f8fc; --day-surface-2: #dde5ef;
  --day-accent: #3d6fa0;
  --day-c-sky: #5f87ad; --day-c-lemon: #b9a05e; --day-c-bubble: #a98d9e;
  --day-c-mint: #7f9f9a; --day-c-grape: #7f7ca6; --day-c-coral: #a6605c;
  --day-c-teal: #5a8898; --day-c-tangerine: #ad8360; --day-c-lime: #90a08a;

  --night-bg: #0c111b; --night-paper: #141b27; --night-surface-2: #1d2534;
  --night-sidebar-bg: #070b12; --night-ink: #e2ebf7; --night-accent: #74a8db;
  --tone-lift: #dce8f7;
}

/* ==========================================================================
   СБОРКА РАБОЧИХ ТОКЕНОВ

   Ниже — единственное место, где день превращается в вечер и ночь. Правила
   общие для всех восьми палитр: каждая уже дала свои два полюса, остаётся
   смешать. Сначала смесь дня и ночи по `--night`, потом поверх неё — тёплый
   налёт заката (`--dusk`) или холодный рассветный (`--dawn`).

   Смешиваем в oklab, а не в sRGB: линейная смесь тёплого крема с тёмно-синим
   проходит через грязно-серый, и весь вечер выглядит выцветшим. В oklab
   переход держит цвет.
   ========================================================================== */
:root {
  /* Две разные кривые, и это главное место файла.
     ── `--dim` — вечернее угасание: бумага темнеет и теплеет, НО остаётся
        светлой, а чернила на ней остаются тёмными. Это золотой час.
     ── `--ts` — переключение на ночной набор, узкой полосой около
        `--night` ≈ 0.6…0.74 (это примерно −3°…−6° высоты солнца, минут
        десять реального времени).
     Почему не одна плавная смесь на всё: если чернила и бумага ползут
     навстречу друг другу, ровно посередине они встречаются — и текст
     пропадает. Замеры показывали 2.2:1 на закате при 13:1 днём. Поэтому
     бумага переходит плавно, а чернила — один раз, ровно в середине этого
     перехода (см. `[data-dark]` ниже): худший замер стал 4:1 на пару минут
     в сутки вместо провала на весь вечер. */
  --dim: clamp(0, calc(var(--night) / 0.6), 1);
  --ts: clamp(0, calc((var(--night) - 0.6) / 0.04), 1);

  --mix-bg:        color-mix(in oklab, color-mix(in oklab, var(--day-bg),         var(--day-sidebar-bg) calc(var(--dim) * 20%)), var(--night-bg)         calc(var(--ts) * 100%));
  --mix-paper:     color-mix(in oklab, color-mix(in oklab, var(--day-paper),      var(--day-sidebar-bg) calc(var(--dim) * 18%)), var(--night-paper)      calc(var(--ts) * 100%));
  --mix-surface-2: color-mix(in oklab, color-mix(in oklab, var(--day-surface-2),  var(--day-sidebar-bg) calc(var(--dim) * 18%)), var(--night-surface-2)  calc(var(--ts) * 100%));
  --mix-sidebar:   color-mix(in oklab, var(--day-sidebar-bg), var(--night-sidebar-bg) calc(var(--ts) * 100%));
  --mix-accent:    color-mix(in oklab, var(--day-accent),     var(--night-accent)     calc(var(--ts) * 100%));

  --ink:   var(--day-ink);
  --text:  var(--ink);
  --border: var(--ink);

  --bg:        color-mix(in oklab, color-mix(in oklab, var(--mix-bg),        var(--tint-dusk) calc(var(--dusk) * 14%)), var(--tint-dawn) calc(var(--dawn) * 11%));
  --paper:     color-mix(in oklab, color-mix(in oklab, var(--mix-paper),     var(--tint-dusk) calc(var(--dusk) * 11%)), var(--tint-dawn) calc(var(--dawn) * 9%));
  --surface-2: color-mix(in oklab, color-mix(in oklab, var(--mix-surface-2), var(--tint-dusk) calc(var(--dusk) * 11%)), var(--tint-dawn) calc(var(--dawn) * 9%));
  --accent:    color-mix(in oklab, color-mix(in oklab, var(--mix-accent),    var(--tint-dusk) calc(var(--dusk) * 12%)), var(--tint-dawn) calc(var(--dawn) * 10%));
  --surface: var(--paper);

  /* Приглушённый текст — не отдельный цвет, а чернила, притопленные в
     бумагу: тогда он приглушён одинаково и на кремовой, и на ночной. */
  --text-muted: color-mix(in oklab, var(--ink), var(--paper) 44%);
  --rule: color-mix(in srgb, var(--ink) 7%, transparent);

  /* Шторка: свой фон, а наведение и активная строка — от него же. */
  --sidebar-bg: color-mix(in oklab, color-mix(in oklab, var(--mix-sidebar), var(--tint-dusk) calc(var(--dusk) * 9%)), var(--tint-dawn) calc(var(--dawn) * 7%));
  --sidebar-ink: color-mix(in oklab, var(--day-sidebar-ink), var(--night-ink) calc(var(--ts) * 65%));
  --sidebar-ink-dim: color-mix(in oklab, var(--sidebar-ink), var(--sidebar-bg) 45%);
  --sidebar-hover: color-mix(in oklab, var(--sidebar-bg), var(--sidebar-ink) 13%);
  --sidebar-active: color-mix(in oklab, var(--sidebar-bg), #000 42%);

  /* Тени сделаны из отдельного цвета, а не из чернил: ночью чернила светлеют,
     и тень из них превратилась бы в свечение. Ночью тень к тому же плотнее —
     на тёмной бумаге мягкая полупрозрачная просто не видна, а без неё
     нарушается третье правило языка: ничего не висит в воздухе. */
  --shade: #07060a;
  --shadow:
    0 1px 2px color-mix(in srgb, var(--shade) calc(18% + var(--ts) * 20%), transparent),
    0 14px 28px -12px color-mix(in srgb, var(--shade) calc(40% + var(--ts) * 28%), transparent);
  --shadow-lg:
    0 2px 3px color-mix(in srgb, var(--shade) calc(20% + var(--ts) * 22%), transparent),
    0 28px 54px -16px color-mix(in srgb, var(--shade) calc(48% + var(--ts) * 30%), transparent);

  /* Тона модулей: ночью тянутся к светлому оттенку своей палитры, иначе
     пигмент вязнет в тёмной бумаге и девять разных модулей сливаются в один. */
  --c-sky:       color-mix(in oklab, var(--day-c-sky),       var(--tone-lift) calc(var(--ts) * 26%));
  --c-lemon:     color-mix(in oklab, var(--day-c-lemon),     var(--tone-lift) calc(var(--ts) * 26%));
  --c-bubble:    color-mix(in oklab, var(--day-c-bubble),    var(--tone-lift) calc(var(--ts) * 26%));
  --c-mint:      color-mix(in oklab, var(--day-c-mint),      var(--tone-lift) calc(var(--ts) * 26%));
  --c-grape:     color-mix(in oklab, var(--day-c-grape),     var(--tone-lift) calc(var(--ts) * 26%));
  --c-coral:     color-mix(in oklab, var(--day-c-coral),     var(--tone-lift) calc(var(--ts) * 26%));
  --c-teal:      color-mix(in oklab, var(--day-c-teal),      var(--tone-lift) calc(var(--ts) * 26%));
  --c-tangerine: color-mix(in oklab, var(--day-c-tangerine), var(--tone-lift) calc(var(--ts) * 26%));
  --c-lime:      color-mix(in oklab, var(--day-c-lime),      var(--tone-lift) calc(var(--ts) * 26%));

  /* Тон по умолчанию. Настоящий приходит инлайном от ядра: `--tone` ставится
     на карточке, виджете или на всей странице модуля. */
  --tone: var(--c-lemon);
  /* По цветной плоскости пишем тёмным в любое время суток: сама плоскость
     остаётся светлее текста и днём, и ночью, а `--ink` ночью уходит в крем. */
  --tone-text: #1b1712;
}

* { box-sizing: border-box; }

@font-face {
  font-family: Rubik; font-style: normal; font-weight: 300 900;
  font-display: swap; src: url("/Rubik.ttf") format("truetype-variations");
}

body {
  margin: 0;
  color: var(--text);
  font: 15px/1.5 Rubik, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  background-color: var(--sidebar-bg);
  position: relative;
}

a { color: var(--accent); text-decoration: none; font-weight: 600; }
h1, h2, h3, h4 { margin: 0 0 .5em; font-weight: 800; letter-spacing: -.03em; }
h1 { font-size: var(--fs-2xl); font-weight: 900; }
h2 { font-size: var(--fs-xl); font-weight: 900; }
/* Заголовок третьего уровня — ярлык раздела: короткий, заглавными, с цветной
   меткой слева. Просто серый текст в этой графике теряется. */
h3 {
  font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .08em;
  color: var(--text); display: flex; align-items: center; gap: var(--sp-2);
}
h3::before {
  content: ""; width: 12px; height: 12px; flex: none;
  background: var(--tone-fill); border: var(--bw) solid var(--ink);
  border-radius: 4px; rotate: 12deg;
}
h4 { font-size: var(--fs-md); }
.small { font-size: var(--fs-sm); }
.muted { color: var(--text-muted); }
.mono { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 12px; }

.page-title {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  margin: 0; padding: 0;
  font-size: clamp(22px, 3vw, 30px); font-weight: 800; letter-spacing: -.02em;
  color: var(--text);
}
.page-title .i { width: .85em; height: .85em; color: var(--tone-fill); }
.card-header .page-title { flex: none; }

/* --- каркас: боковая шторка ---------------------------------------------- */
/* Навигация ушла с верхней полосы в левую шторку целиком — модули, задания,
   каталог, колокольчик, тема, профиль. Отдельной верхней панели больше нет:
   на телефоне саму шторку сворачивает не она, а тонкий рельс слева (см. ниже),
   так что верхний бар «только ради гамбургера» не нужен. */
/* Фон .shell — тот же цвет, что у сайдбара: панель контента накрывает его
   скруглённым левым краем (см. `.content-frame`), и в изгибе скругления
   сверху и снизу проглядывает сайдбар — шов читается как «панель наложена
   на сайдбар», а не как два блока встык. */
.shell { display: flex; min-height: 100vh; background: var(--sidebar-bg); }
.sidebar {
  position: sticky; top: 0; align-self: flex-start;
  flex: none; width: 248px; height: 100vh;
  display: flex; flex-direction: column;
  /* Шторка тёмная во всех палитрах — единственная тёмная поверхность на
     светлой странице. Переопределяя `--ink`/`--text`/`--text-muted`/`--paper`/
     `--surface-2` прямо здесь, а не переписывая каждое правило `.sidebar-*`
     заново, получаем тёмную версию бесплатно: все обычные правила ниже по
     файлу читают контур/текст/тень/hover-подложку из тех же токенов, что и
     светлые поверхности — здесь они просто указывают на другие значения. */
  --ink: var(--sidebar-ink);
  --text: var(--sidebar-ink);
  --text-muted: var(--sidebar-ink-dim);
  --paper: var(--sidebar-bg);
  --surface-2: var(--sidebar-hover);
  background: var(--sidebar-bg); color: var(--sidebar-ink);
  box-shadow: var(--shadow-lg);
  transition: width .16s ease, background-color .2s ease;
  z-index: 40;
}
.sidebar.collapsed { width: 68px; }
.sidebar.collapsed .label { display: none; }

.sidebar-head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3); border-bottom: var(--bw) solid var(--ink); flex: none;
}
.brand {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-lg); font-weight: 900; letter-spacing: -.04em;
  color: var(--text); white-space: nowrap; overflow: hidden; min-width: 0;
}
/* Морда, а не лежащий кот: рядом с коротким словом широкий силуэт читался как
   клякса и спорил с кеглем. Высота логотипа привязана к строке. */
/* Кот сидит на собственной плашке: цветная подложка снизу, картинка чуть уже
   и придвинута к нижнему краю, поэтому читается «стоит на полу», а не
   «наклеен посередине». */
.brand .logo {
  width: 2.1em; height: 2.1em; flex: none; border-radius: var(--radius-xs);
  background-color: var(--accent);
  background-image: url("/logo.png");
  background-position: center 86%;
  background-size: 76% auto;
  background-repeat: no-repeat;
}
/* Розовый на тёмной шторке сам по себе темнее фона недостаточно, чтобы
   потеряться, но чуть подсветлить и поднасытить всё равно стоит — иначе
   кот в шторке выглядит тусклее своей же версии на светлой табличке. */
.sidebar .brand .logo { filter: brightness(1.15) saturate(1.1); }
.brand b { font-weight: 900; }
.sidebar-collapse-btn { margin-left: auto; flex: none; }
.sidebar.collapsed .sidebar-collapse-btn .i { rotate: 180deg; }

/* Пункт навигации — не строка в списке, а собственный bordered-прямоугольник.
   Активный — залит тоном модуля (тем же, что у его виджетов на дашборде — по
   цвету модуль и опознаётся), неактивный — цвет бумаги с тёмным контуром. */
.sidebar-nav {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--sp-3); display: flex; flex-direction: column; gap: 8px;
}
.sidebar-link {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 11px; border: var(--bw) solid transparent;
  background: transparent; color: var(--text-muted); font: inherit; cursor: pointer;
  font-weight: 800; font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .03em;
  white-space: nowrap; overflow: hidden; border-radius: var(--radius-sm); box-shadow: none;
  transition: background .14s ease, color .14s ease;
}
.sidebar-link:hover { background: var(--surface-2); color: var(--text); }
.sidebar-link.active {
  background: var(--tone-fill); color: var(--tone-text); border-color: var(--tone-fill);
}
.sidebar-link .label { overflow: hidden; text-overflow: ellipsis; }
.sidebar-link[data-slug] { cursor: grab; touch-action: none; }
.sidebar-link[data-slug].dragging { opacity: .6; cursor: grabbing; }
.sidebar.collapsed .sidebar-link { justify-content: center; padding: 9px; }

.sidebar-foot {
  flex: none; border-top: var(--bw) solid var(--ink);
  padding: var(--sp-3); display: flex; flex-direction: column; gap: 8px;
}

/* Свотчи палитры — по одному кружку на палитру: все восемь видны сразу, а не
   по одной за нажатие кнопки-цикла. Два ряда по четыре: сверху именные, снизу
   сезонные — со значком времени года прямо в кружке, потому что «весна» от
   «лета» цветом кружка не отличается так же ясно, как Herat от Tabriz. Цвета
   в кружке литеральные (не токены): кружок обязан показывать ЧУЖУЮ палитру,
   пока активна текущая, а токен всегда отдаёт только то, что выбрано сейчас. */
.palette-picker { display: flex; flex-direction: column; gap: 7px; padding: 2px 11px 6px; }
.palette-row { display: flex; gap: 8px; }
.palette-swatch {
  width: 22px; height: 22px; border-radius: 50%; padding: 0; flex: none;
  border: 2px solid transparent; cursor: pointer;
  transition: transform .12s ease, border-color .12s ease;
}
.palette-swatch:hover { transform: translateY(-1px); }
.palette-swatch[aria-pressed="true"] { border-color: var(--sidebar-ink); }
.palette-swatch.palette-herat   { background: conic-gradient(#232f1f 0 50%, #a8542f 0 100%); }
.palette-swatch.palette-tabriz  { background: conic-gradient(#452a20 0 50%, #5f7a4a 0 100%); }
.palette-swatch.palette-isfahan { background: conic-gradient(#212b3c 0 50%, #c99a44 0 100%); }
.palette-swatch.palette-bukhara { background: conic-gradient(#3b1620 0 50%, #b8863f 0 100%); }
.palette-swatch.palette-spring  { background: conic-gradient(#3b2a3a 0 50%, #d1899b 0 100%); }
.palette-swatch.palette-summer  { background: conic-gradient(#17323c 0 50%, #4f93b8 0 100%); }
.palette-swatch.palette-autumn  { background: conic-gradient(#3a2215 0 50%, #c4702a 0 100%); }
.palette-swatch.palette-winter  { background: conic-gradient(#1b2635 0 50%, #9fc0e0 0 100%); }
/* Значок сезона поверх кружка: сам кружок остаётся цветной подложкой. */
.palette-swatch.season {
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; line-height: 1;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .45);
}
/* Среда — под створкой. Палитру выбирают мимоходом и часто, а плотность
   метели и время суток трогают раз и надолго: держать четыре ряда управления
   развёрнутыми ради этого — значит занимать ими низ шторки всё остальное
   время. Свёрнутая строка при этом не пустая: она показывает, что за окном,
   и открывать створку ради «посмотреть» не нужно. */
.ambient { margin-top: 1px; }
.ambient-head {
  display: flex; align-items: center; gap: 6px;
  padding: 3px 6px; border-radius: var(--radius-xs);
  font-size: var(--fs-xs); font-weight: 700; color: var(--sidebar-ink-dim);
  cursor: pointer; user-select: none;
  list-style: none;                    /* треугольник у <summary> рисуем свой */
}
.ambient-head::-webkit-details-marker { display: none; }
.ambient-head:hover { background: var(--sidebar-hover); color: var(--sidebar-ink); }
.ambient[open] .ambient-head { color: var(--sidebar-ink); }
.ambient-state { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ambient-chevron { flex: none; font-size: 13px; line-height: 1; transition: rotate .15s ease; }
.ambient[open] .ambient-chevron { rotate: 90deg; }
.ambient-body { display: flex; flex-direction: column; gap: 5px; padding-top: 5px; }

/* Плотность погоды — не выключатель, а четыре деления: без погоды, тихо,
   обычно, буря. Столбики растут слева направо, поэтому выбранный уровень
   виден силуэтом, без чтения подписи. */
.fx-row { display: flex; align-items: center; gap: 7px; padding: 1px 6px; }
.fx-name {
  flex: 1; min-width: 0; font-size: var(--fs-xs); font-weight: 700;
  color: var(--sidebar-ink-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fx-steps { display: flex; align-items: flex-end; gap: 3px; flex: none; height: 15px; }
.fx-step {
  width: 6px; padding: 0; border: 0; cursor: pointer; border-radius: 2px;
  background: color-mix(in srgb, var(--sidebar-ink) 24%, transparent);
  transition: background-color .12s ease;
}
.fx-step:nth-child(2) { height: 6px; }
.fx-step:nth-child(3) { height: 10px; }
.fx-step:nth-child(4) { height: 15px; }
.fx-step.fx-none { width: 5px; height: 5px; border-radius: 50%; align-self: center; }
.fx-step:hover { background: color-mix(in srgb, var(--sidebar-ink) 60%, transparent); }
.fx-step.on, .fx-step[aria-pressed="true"] { background: var(--sidebar-ink); }

/* Время суток. Ползунок — не только выбор момента: его полоса и есть световая
   дуга сегодняшнего дня (градиент собирается в daylightTrack, web/app.js), так
   что видно, когда темнеет, ещё до того, как что-то трогаешь. */
.night-row { display: flex; align-items: center; gap: 6px; padding: 1px 6px; }
.night-toggle, .night-reset {
  flex: none; padding: 0; border: 0; background: none; cursor: pointer;
  font-size: 13px; line-height: 1; color: var(--sidebar-ink-dim);
}
.night-toggle:hover, .night-reset:hover:not(:disabled) { filter: brightness(1.3); }
.night-reset { font-size: 12px; }
.night-reset:disabled { opacity: .25; cursor: default; }
.night-slider {
  flex: 1; min-width: 0; height: 15px; margin: 0; padding: 0;
  -webkit-appearance: none; appearance: none; background: none; cursor: pointer;
}
.night-slider::-webkit-slider-runnable-track {
  height: 8px; border-radius: var(--radius-pill);
  background: var(--track, var(--sidebar-hover));
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / .3);
}
.night-slider::-moz-range-track {
  height: 8px; border-radius: var(--radius-pill);
  background: var(--track, var(--sidebar-hover));
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / .3);
}
.night-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 9px; height: 15px; margin-top: -3.5px; border-radius: 3px;
  background: var(--sidebar-ink); box-shadow: 0 1px 3px rgb(0 0 0 / .55);
}
.night-slider::-moz-range-thumb {
  width: 9px; height: 15px; border: 0; border-radius: 3px;
  background: var(--sidebar-ink); box-shadow: 0 1px 3px rgb(0 0 0 / .55);
}
.night-caption {
  padding: 0 6px; font-size: 11px; font-weight: 700; letter-spacing: .01em;
  color: var(--sidebar-ink-dim);
}
/* Время остановлено вручную — подпись становится яркой, иначе непонятно,
   почему за окном полночь, а часы показывают полдень. */
.night-caption.frozen { color: var(--sidebar-ink); }
.night-off {
  display: flex; align-items: center; gap: 6px; padding: 3px 6px;
  background: none; border: 0; border-radius: var(--radius-xs);
  font: inherit; font-size: var(--fs-xs); font-weight: 700;
  color: var(--sidebar-ink-dim); text-align: left; cursor: pointer;
}
.night-off:hover { background: var(--sidebar-hover); color: var(--sidebar-ink); }
.sidebar.collapsed .palette-picker { display: none; }

/* Погода и звёзды: холст поверх всей страницы — снег падает НА предметы, а не
   под ними. Ночь до него не дотягивается (она живёт в токенах, а не слоем
   поверх), поэтому снежинка ночью остаётся белой, а звезда — звездой.
   Рисуется в web/seasons.js. */
.season-fx {
  position: fixed; inset: 0; z-index: 43;
  width: 100%; height: 100%;
  pointer-events: none; opacity: .8;
}
@media (prefers-reduced-motion: reduce) { .season-fx { display: none; } }

.sidebar-backdrop {
  display: none; position: fixed; inset: 0; z-index: 65;
  background: color-mix(in srgb, #2a1f18 55%, transparent);
}

/* Плавающая кнопка-триггер: единственный способ открыть шторку на телефоне,
   раз сама она там не показывается вовсе. На десктопе/планшете не нужна —
   там шторка всегда на месте (открытая или в виде рельса). */
.sidebar-fab {
  display: none; position: fixed; left: 12px; z-index: 71;
  width: 44px; height: 44px; padding: 0; justify-content: center; align-items: center;
  border: var(--bw) solid var(--ink); background: var(--paper); color: var(--ink);
  border-radius: var(--radius-sm); box-shadow: var(--shadow);
}
/* Открыта — прячем кнопку: у открытой шторки уже есть чем закрыться
   (подложка, переход по ссылке), вторая кнопка в том же углу — лишнее. */
.sidebar-fab[hidden] { display: none !important; }

.content-frame {
  flex: 1; min-width: 0; min-height: 100vh;
  background: var(--bg);
  border-radius: var(--radius-frame) 0 0 var(--radius-frame);
}
main {
  /* Правое поле шире левого нарочно: резервирует место под плавающий
     колокольчик (`.bell-corner`) — на широких экранах он и так уходит за
     эту колонку, а вот на среднем окне без запаса шапка страницы упиралась
     бы прямо в него. */
  padding: var(--sp-5) 78px var(--sp-5) var(--sp-5);
  max-width: 1400px; margin: 0 auto; position: relative;
}
/* Ритм между блоками страницы — тот же приём, что внутри карточки, и по той же
   причине. Отступ был только между соседними `.card`; ряд плиток `tiles` рядом
   с карточкой, строка кнопок над плитками и любой блок, который не карточка,
   отступа не получали и слипались с соседом. Правило спускается в безымянный
   контейнер верхнего уровня: страницы модулей почти всегда заводят
   `<div id="…">` как место, куда перерисовывать содержимое. */
main > * + *,
#module-root > * + *,
#module-root > div:not([class]) > * + * { margin-top: var(--sp-6); }
/* У шапки страницы свой отступ снизу — иначе он сложится с этим в двойной. */
main > .card-header + *, #module-root > .card-header + * { margin-top: 0; }
/* Экран входа рисуется мимо <main>, а слой наклеек ему нужен так же: без
   позиционированного предка он растянулся бы по вьюпорту и уехал за экран. */
#app { position: relative; min-height: 100vh; }
.center-screen { position: relative; z-index: 1; }

/* --- блоки -------------------------------------------------------------- */
.card {
  position: relative;
  background: var(--paper); border: var(--bw) solid var(--ink);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: var(--sp-5);
}
/* Цветной блок среди бумажных. Цвет приходит из `--tone`, который ставит
   ядро: модуль просит «цветную карточку», а не конкретный цвет. */
.card.tint { background: var(--tone-fill); color: var(--tone-text); }
.card.tint .muted, .card.tint .list-item .s { color: color-mix(in srgb, var(--tone-text) 62%, transparent); }
.card + .card { margin-top: var(--sp-6); }
/* Ритм внутри карточки. Раньше отступ между блоками ставил каждый модуль сам,
   инлайновым `style="margin-top:…"`, и стоило забыть — заголовок, число и
   строка кнопок слипались в кашу. Правило спускается и в безымянные
   контейнеры: модули часто заводят <div id="…"> просто как место, куда
   перерисовывать кусок страницы, и содержимое такого места — те же блоки
   карточки. */
.card > * + *,
.card > div:not([class]) > * + * { margin-top: var(--sp-4); }
/* Исключения: у шапки уже есть отступ снизу, строки списка разделяет линейка,
   а заголовок стоит ближе к тому, что подписывает, чем к предыдущему блоку. */
.card > .card-header + * { margin-top: 0; }
.card .list-item + .list-item { margin-top: 0; }
.card h3 + * { margin-top: var(--sp-3); }
/* Отступ выше нужен только в вертикальном потоке. В сетке карточки разводит
   gap, а margin-top сдвигал бы вниз всё, кроме первой — ряд разъезжался. */
:is(.tiles, .grid) > .card { margin-top: 0; }
.card-header {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: var(--sp-3); margin-bottom: var(--sp-4);
}
.row { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap; }
.col { display: flex; flex-direction: column; gap: var(--sp-3); }
.spacer { flex: 1; }
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--gap); }

/* --- кнопки ------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 18px; border-radius: var(--radius-pill);
  border: var(--bw) solid var(--ink);
  background: var(--paper); color: var(--text);
  font: inherit; font-size: 14px; font-weight: 700; cursor: pointer;
  box-shadow: var(--shadow);
  transition: transform .12s ease, box-shadow .15s ease, background .14s ease, filter .15s ease;
}
/* Лёгкий подъём на ховере, лёгкое приседание на клике — тень мягкая, а не
   штрих в один пиксель, поэтому физика читается объёмом, а не смещением. */
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-lg); filter: brightness(1.04); }
.btn:active { transform: translateY(0) scale(.98); box-shadow: 0 4px 10px -8px color-mix(in srgb, var(--shade) 50%, transparent); filter: none; }
/* Главная кнопка красится в тон своего раздела — тот же цвет, что у шапки
   виджета или плашки заголовка рядом с ней. */
.btn-primary { background: var(--tone-fill); color: var(--tone-text); }
.btn-ghost {
  background: transparent; border-color: transparent;
  box-shadow: none; color: var(--text-muted);
}
.btn-ghost:hover { background: var(--surface-2); color: var(--text); transform: none; box-shadow: none; }
.btn-ghost:active { transform: none; }
.btn-danger { background: var(--c-coral); color: var(--tone-text); }
/* Опасное действие, которое не должно перекрикивать соседнее опасное. В ряду
   каталога рядом стоят «Снять у себя» (обратимо, касается только тебя) и
   «Удалить с платформы» (необратимо, касается всех): две одинаково красные
   заливки читались бы как две одинаковые кнопки. Эта — контур цвета тревоги:
   тише заливки, но безобиднее обычной кнопки не выглядит. */
.btn-danger-soft {
  background: transparent; box-shadow: none;
  color: color-mix(in srgb, var(--c-coral) 78%, var(--ink));
}
.btn-danger-soft:hover {
  background: color-mix(in srgb, var(--c-coral) 15%, var(--paper));
  box-shadow: var(--shadow);
}
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; box-shadow: var(--shadow); }
.btn.small { padding: 5px 14px; font-size: 13px; border-radius: var(--radius-pill); box-shadow: var(--shadow); }
.btn.small:hover { box-shadow: var(--shadow-lg); }
.btn.big { padding: 13px 28px; font-size: var(--fs-lg); box-shadow: var(--shadow-lg); }

/* Фокус с клавиатуры — отдельное толстое кольцо поверх тени, а не браузерный
   outline: на скруглённых бордерах штатный прямоугольный outline срезает
   углы и выглядит багом, а не подсказкой. Мышиный клик кольца не получает —
   :focus-visible достаточно умён, чтобы не мешать mouse-first сценарию. */
.btn:focus-visible, .sidebar-link:focus-visible, .sidebar-fab:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible,
.spark-point:focus-visible, a:focus-visible {
  outline: 2.5px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-xs);
}

/* --- поля --------------------------------------------------------------- */
input, select, textarea {
  padding: 9px 12px; border-radius: var(--radius-xs);
  /* Поле — единственное место, где обводка всё ещё нужна: без неё границы
     поля ввода не видно на собственной поверхности, и по странице нельзя
     понять, куда можно кликнуть. Токен свой, не --bw, ­— значение фиксировано
     и не гаснет вместе с общей обводкой платформы. */
  border: 1.5px solid var(--surface-2);
  background: var(--paper); color: var(--text); font: inherit; font-size: 14px;
  width: 100%;
}
input:focus, select:focus, textarea:focus { outline: none; box-shadow: 3px 3px 0 var(--tone-fill); }
input[type="checkbox"] { accent-color: var(--accent); }
label { display: block; font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 5px; }
.field { margin-bottom: var(--sp-3); }

/* Свитч — стилизованный чекбокс для булевых настроек (шаблон shadcn Switch,
   реализация ванильным CSS: :has() красит трек по состоянию вложенного
   input, псевдоэлемент — бегунок). Настоящий `<input type="checkbox">`
   внутри остаётся: это чекбокс для форм и для скринридера, свитч — только
   его кожа. */
label.switch {
  --sw: 44px; --sh: 26px;
  width: var(--sw); height: var(--sh); margin: 0;
  border: var(--bw) solid var(--ink); border-radius: var(--radius-pill);
  background: var(--surface-2); position: relative; display: inline-block;
  flex: none; cursor: pointer; transition: background .14s ease; vertical-align: middle;
}
.switch input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; width: auto; }
.switch::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: 50%; background: var(--paper); box-shadow: 0 1px 3px rgba(0,0,0,.28);
  transition: translate .16s cubic-bezier(.3, 1.4, .5, 1);
}
.switch:has(input:checked) { background: var(--tone-fill); }
.switch:has(input:checked)::after { translate: 18px 0; }
.switch:has(input:focus-visible) { outline: 2.5px solid var(--accent); outline-offset: 2px; }
.switch:has(input:disabled) { opacity: .5; cursor: not-allowed; }

/* --- таблица ------------------------------------------------------------ */
table.table { width: 100%; border-collapse: collapse; }
table.table th {
  text-align: left; font-size: 11px; text-transform: uppercase;
  letter-spacing: .07em; font-weight: 800; color: var(--text);
  padding: var(--sp-2); border-bottom: 1.5px solid var(--surface-2);
}
table.table td { padding: var(--sp-3) var(--sp-2); border-top: 2px solid var(--surface-2); }
table.table tr:hover td { background: var(--surface-2); }
/* Поля внутри таблицы не должны растягиваться на всю колонку. */
table.table select, table.table input { width: auto; min-width: 118px; padding: 5px 8px; font-size: 13px; }

/* --- мелочи ------------------------------------------------------------- */
.badge {
  display: inline-block; padding: 3px 11px; border-radius: var(--radius-pill);
  background: var(--surface-2);
  font-size: 12px; font-weight: 700; color: var(--text);
}
.badge-success { background: color-mix(in srgb, var(--success) 18%, var(--paper)); color: var(--success); }
.badge-warning { background: color-mix(in srgb, var(--warning) 20%, var(--paper)); color: var(--warning); }
.badge-error   { background: color-mix(in srgb, var(--error) 16%, var(--paper)); color: var(--error); }
.badge-accent  { background: color-mix(in srgb, var(--c-grape) 20%, var(--paper)); color: color-mix(in srgb, var(--c-grape) 70%, var(--ink)); }

/* Чек-борд — мотив «пустого фото»: клетчатый квадрат под ещё не пришедшей
   картинкой. Используется как утилита (`.checker`) и сразу вшит в пустые
   состояния — так дашборд без данных выглядит рамкой в ожидании контента,
   а не дырой. */
.checker,
.empty-state {
  background-color: var(--paper);
  background-image:
    linear-gradient(45deg, var(--surface-2) 25%, transparent 25%, transparent 75%, var(--surface-2) 75%),
    linear-gradient(45deg, var(--surface-2) 25%, transparent 25%, transparent 75%, var(--surface-2) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 8px 8px;
}
.empty-state {
  text-align: center; color: var(--text-muted); padding: var(--sp-6) var(--sp-3); font-size: 14px; font-weight: 600;
  border: 2px dashed color-mix(in srgb, var(--ink) 32%, transparent);
  border-radius: var(--radius-sm);
}
.empty-state.with-cat::before {
  content: ""; display: block; width: 132px; height: 66px; margin: 0 auto 10px;
  background: url("/cat.png") center / contain no-repeat; opacity: .9;
}

.progress {
  height: 18px; border-radius: var(--radius-pill); background: var(--surface-2);
  overflow: hidden;
}
.progress > i {
  display: block; height: 100%; background: var(--tone-fill);
  transition: width .3s cubic-bezier(.3, 1.4, .5, 1);
}

/* Скелетон — заглушка на время загрузки (шаблон shadcn Skeleton) вместо
   текста «Загрузка…»: форма самого блока видна сразу, шиммер только
   подсказывает «сейчас появится», а не «читай эту фразу». */
.skeleton {
  position: relative; overflow: hidden;
  background: var(--surface-2); border: var(--bw) solid var(--ink); border-radius: var(--radius-sm);
}
.skeleton::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--paper) 75%, transparent), transparent);
  animation: skeleton-sheen 1.3s ease-in-out infinite;
}
@keyframes skeleton-sheen { from { translate: -120% 0; } to { translate: 120% 0; } }
@media (prefers-reduced-motion: reduce) { .skeleton::after { animation: none; } }

/* --- полосы прокрутки --------------------------------------------------- */
/* Системная полоса рядом с толстыми контурами выглядит деталью чужого
   интерфейса — единственное место, куда не дотянулась тема. Красим её тем же
   языком: ползунок в тоне раздела, с контуром и скруглением. Firefox умеет
   только пару цветов, поэтому там ползунок чернильный — рисовать контур
   нечем. */
* { scrollbar-width: thin; scrollbar-color: var(--ink) transparent; }
::-webkit-scrollbar { width: 13px; height: 13px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--tone-fill); border: 2px solid var(--ink);
  border-radius: var(--radius-pill);
  /* Отступ от края даёт «дорожку» без отдельного элемента: фон трека
     просвечивает вокруг ползунка. */
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink); }
::-webkit-scrollbar-corner { background: transparent; }
/* Внутри виджета полоса тоньше: там её задача — не мешать. */
.widget-body::-webkit-scrollbar { width: 9px; height: 9px; }

/* --- иконки ------------------------------------------------------------- */
/* Lucide одним спрайтом: цвет наследуется из currentColor, размер — из шрифта. */
.i { width: 1em; height: 1em; flex: none; vertical-align: -.14em; }
.i-lg { width: 1.25em; height: 1.25em; }
.btn .i { margin-right: -2px; }

/* --- наклейки и ленты --------------------------------------------------- */
/* Декор, который платформа разбрасывает сама: звёздочки, сердца, радуги по
   краям страниц и пустых состояний. Живёт отдельным слоем и НИКОГДА не ловит
   клики — это графика, а не интерфейс. Модуль наклейки не ставит: их место и
   плотность — вопрос композиции всей страницы, а не отдельного блока. */
/* Высота слоя — экран, а НЕ высота страницы. Наклейки расставлены в
   процентах от слоя, поэтому, пока он тянулся по содержимому, любое изменение
   высоты страницы их двигало: перетаскиваешь виджет на строку ниже — сетка
   становится выше, и весь фон уезжает вслед за ней. */
.stickers {
  position: absolute; inset: 0 0 auto 0; height: 100vh;
  overflow: hidden; pointer-events: none; z-index: 0;
}
.sticker {
  position: absolute;
  left: var(--x, 50%); top: var(--y, 50%);
  width: var(--s, 48px); height: var(--s, 48px);
  rotate: var(--r, 0deg);
  color: var(--sticker-color, var(--c-grape));
  /* Ночью декор уходит в тень первым: днём наклейка — украшение страницы,
     ночью тот же цветной завиток в полную силу читается как грязь на экране. */
  opacity: calc(.85 - var(--night) * .5);
}
/* Содержимое всегда выше слоя наклеек. */
main > *:not(.stickers) { position: relative; z-index: 1; }

/* Лента — шов между разделами. Статичная полоса, не бегущая строка. */
.ribbon {
  overflow: hidden; margin: var(--sp-5) 0;
  background: var(--tone-fill); color: var(--tone-text);
  border-block: var(--bw) solid var(--ink);
  padding: 7px 0;
}
.ribbon-track {
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding-inline: var(--sp-4);
  font-weight: 900; text-transform: uppercase; letter-spacing: .1em;
  font-size: var(--fs-sm);
}

/* --- ачивки: карточка ----------------------------------------------------
   Сервер отдаёт только персонажа (прозрачный PNG, см. core/badges.py) —
   рамку, текст и эффекты (наклон, блик, искры, свечение) строит браузер,
   разметку собирает web/badge-card.js. Цвет корпуса — случайный из
   фиксированной палитры, редкость показывают штрихи и сила свечения, а не
   цвет. */
.badge-card { position: relative; }

.bc-stage { position: relative; perspective: 900px; }
.bc-tr { position: absolute; width: 33.34%; height: 33.34%; z-index: 4; }
.bc-tr-1 { top: 0; left: 0; } .bc-tr-2 { top: 0; left: 33.33%; } .bc-tr-3 { top: 0; left: 66.66%; }
.bc-tr-4 { top: 33.33%; left: 0; } .bc-tr-5 { top: 33.33%; left: 33.33%; } .bc-tr-6 { top: 33.33%; left: 66.66%; }
.bc-tr-7 { top: 66.66%; left: 0; } .bc-tr-8 { top: 66.66%; left: 33.33%; } .bc-tr-9 { top: 66.66%; left: 66.66%; }

.bc-frame {
  position: relative; transform-style: preserve-3d;
  transition: transform .45s cubic-bezier(.25, 1, .5, 1);
}
.bc-tr-1:hover ~ .bc-frame { transform: rotateX(7deg) rotateY(-7deg) scale(1.045); }
.bc-tr-2:hover ~ .bc-frame { transform: rotateX(7deg) rotateY(0deg) scale(1.045); }
.bc-tr-3:hover ~ .bc-frame { transform: rotateX(7deg) rotateY(7deg) scale(1.045); }
.bc-tr-4:hover ~ .bc-frame { transform: rotateX(0deg) rotateY(-7deg) scale(1.045); }
.bc-tr-5:hover ~ .bc-frame { transform: rotateX(0deg) rotateY(0deg) scale(1.05); }
.bc-tr-6:hover ~ .bc-frame { transform: rotateX(0deg) rotateY(7deg) scale(1.045); }
.bc-tr-7:hover ~ .bc-frame { transform: rotateX(-7deg) rotateY(-7deg) scale(1.045); }
.bc-tr-8:hover ~ .bc-frame { transform: rotateX(-7deg) rotateY(0deg) scale(1.045); }
.bc-tr-9:hover ~ .bc-frame { transform: rotateX(-7deg) rotateY(7deg) scale(1.045); }
.badge-card.locked .bc-tr { pointer-events: none; }

.bc-face {
  position: relative; z-index: 1; overflow: hidden;
  aspect-ratio: 3.2 / 4; padding: 14px;
  background: linear-gradient(165deg, color-mix(in srgb, var(--card-bg) 96%, #fff 4%),
                                       color-mix(in srgb, var(--card-bg) 88%, #000 12%));
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column; color: #14141a;
}
.badge-card.locked .bc-face { background: linear-gradient(165deg, #d3d6dc, #aeb2bb); }

.bc-tag { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.bc-ticks { display: flex; gap: 2px; }
.bc-ticks i {
  display: block; width: 6px; height: 13px; transform: skewX(-18deg);
  background: currentColor; opacity: .25;
}
.bc-ticks i.filled { opacity: .85; }
.bc-rarity { font: 8px "Press Start 2P", monospace; text-transform: uppercase; letter-spacing: .03em; opacity: .8; }

.bc-art {
  position: relative; flex: 1; min-height: 0;
  margin-bottom: 8px; display: flex; align-items: center; justify-content: center;
  background: linear-gradient(155deg, rgba(255,255,255,.55), rgba(255,255,255,.22));
  border-radius: 10px; overflow: hidden;
}
.bc-emblem {
  display: grid; place-items: center; width: 56%; aspect-ratio: 1 / 1;
  color: color-mix(in srgb, var(--card-bg) 30%, #14141a 70%);
  filter: drop-shadow(0 5px 7px rgba(0,0,0,.25));
}
.bc-emblem .i { width: 100%; height: 100%; }
.badge-card.locked .bc-emblem { color: #6b6f7a; opacity: .75; }
.bc-emblem[hidden] { display: none; }
.bc-lock-badge {
  position: absolute; right: 6px; bottom: 6px; z-index: 2;
  width: 22px; height: 22px; display: grid; place-items: center;
  background: rgba(20,20,26,.6); color: #fff; border-radius: 50%;
}
.bc-lock-badge .i { width: 60%; height: 60%; }
.bc-pic {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; padding: 4%; box-sizing: border-box;
  image-rendering: pixelated;
}

.bc-meta {
  display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
  font: 7px "Press Start 2P", monospace; letter-spacing: .02em; opacity: .75;
}
.bc-rule {
  flex: 1; height: 2px;
  background-image: repeating-linear-gradient(90deg, currentColor 0 5px, transparent 5px 9px);
  opacity: .35;
}

.bc-title {
  font: 10px "Press Start 2P", monospace; letter-spacing: .01em;
  text-transform: uppercase; margin-bottom: 6px; line-height: 1.5; text-wrap: balance;
}

.bc-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: auto; }
.bc-issued { font-size: 10px; font-weight: 700; line-height: 1.25; opacity: .8; }
.bc-mod {
  flex: none; width: 22px; height: 22px; border-radius: 7px;
  background: color-mix(in srgb, #000 12%, transparent); color: #14141a;
  display: grid; place-items: center;
}
.bc-mod .i { width: 13px; height: 13px; }

/* зерно и блик — тонкая фактура и статичный "лоск" поверх карточки */
.bc-grain {
  position: absolute; inset: 0; z-index: 2; pointer-events: none; opacity: .05; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.bc-glare {
  position: absolute; inset: -20%; z-index: 3; pointer-events: none;
  background: radial-gradient(circle at center, rgba(255,255,255,.4), transparent 55%);
  opacity: 0; transition: opacity .4s ease, transform .4s ease;
}
.bc-tr-1:hover ~ .bc-frame .bc-glare { opacity: 1; transform: translate(16%, 16%); }
.bc-tr-3:hover ~ .bc-frame .bc-glare { opacity: 1; transform: translate(-16%, 16%); }
.bc-tr-7:hover ~ .bc-frame .bc-glare { opacity: 1; transform: translate(16%, -16%); }
.bc-tr-9:hover ~ .bc-frame .bc-glare { opacity: 1; transform: translate(-16%, -16%); }
.bc-tr-2:hover ~ .bc-frame .bc-glare, .bc-tr-4:hover ~ .bc-frame .bc-glare,
.bc-tr-6:hover ~ .bc-frame .bc-glare, .bc-tr-8:hover ~ .bc-frame .bc-glare { opacity: .65; }
.bc-tr-5:hover ~ .bc-frame .bc-glare { opacity: .4; }

/* свечение по редкости — статичная/пульсирующая аура позади карточки,
   сила растёт с числом штрихов (tier = rarity_pips, 1..5) */
.bc-halo {
  position: absolute; inset: -8px; z-index: 0; opacity: 0; pointer-events: none;
  background: linear-gradient(315deg, var(--glow), var(--glow2));
  filter: blur(16px); transition: opacity .4s ease, filter .4s ease;
}
@keyframes bc-pulse { 0%, 100% { opacity: .3; } 50% { opacity: .55; } }
.badge-card.tier-1 .bc-halo { display: none; }
.badge-card.tier-2:hover .bc-halo { opacity: .4; }
.badge-card.tier-3 .bc-halo { opacity: .22; animation: bc-pulse 3s ease-in-out infinite; }
.badge-card.tier-3:hover .bc-halo { opacity: .55; filter: blur(18px); }
.badge-card.tier-4 .bc-halo { opacity: .32; animation: bc-pulse 2.6s ease-in-out infinite; }
.badge-card.tier-4:hover .bc-halo { opacity: .65; filter: blur(20px); }
.badge-card.tier-5 .bc-halo { opacity: .42; filter: blur(20px); animation: bc-pulse 2.4s ease-in-out infinite; }
.badge-card.tier-5:hover .bc-halo { opacity: .8; filter: blur(22px); }
.badge-card.locked .bc-halo { display: none !important; }

.bc-sparkle {
  position: absolute; width: 4px; height: 4px; border-radius: 50%;
  background: #fff; box-shadow: 0 0 6px 2px #fff; z-index: 4; opacity: 0;
  animation: bc-sparkle 2.4s ease-in-out infinite; pointer-events: none;
}
@keyframes bc-sparkle { 0%, 100% { opacity: 0; transform: scale(.5); } 50% { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) {
  .bc-halo, .bc-sparkle { animation: none !important; }
}

/* Счётчик по редкостям под полосой прогресса в окне ачивок: сколько собрано из
   скольких в каждой ступени. Полностью закрытая ступень подсвечивается —
   это уже достижение. */
.ach-tally { gap: var(--sp-2); margin-bottom: var(--sp-4); }
.ach-tally-item {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-xs); color: var(--text-muted); font-weight: 700;
}
.ach-tally-item i {
  width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid var(--ink); background: var(--rarity-common);
}
.ach-tally-item[data-rarity="uncommon"] i { background: var(--rarity-uncommon); }
.ach-tally-item[data-rarity="rare"] i { background: var(--rarity-rare); }
.ach-tally-item[data-rarity="epic"] i { background: var(--rarity-epic); }
.ach-tally-item[data-rarity="legendary"] i { background: var(--rarity-legendary); }
.ach-tally-item.full { color: var(--text); }

.ach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--sp-4); }

/* --- дашборд ------------------------------------------------------------ */
.grid {
  display: grid; grid-template-columns: repeat(12, 1fr);
  grid-auto-rows: var(--cell); gap: var(--gap);
}

/* Виджет — карточка с заголовком: иконка модуля в его тоне + название, и
   цветная полоска слева — тот же тон, опознавательный знак модуля виден
   раньше, чем прочитано название. */
.widget {
  position: relative; overflow: hidden;
  background: var(--paper); border-left: 5px solid var(--tone-fill);
  border-radius: var(--radius); box-shadow: var(--shadow);
  display: flex; flex-direction: column;
}
.widget-head {
  display: flex; align-items: center; gap: 10px;
  padding: var(--sp-3) var(--sp-3) 0;
  cursor: grab; user-select: none;
}
.widget-head .w-icon {
  width: 30px; height: 30px; border-radius: var(--radius-xs); flex: none;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--tone-fill) 16%, var(--paper));
  color: color-mix(in srgb, var(--tone-fill) 75%, var(--ink));
}
.widget-head .w-icon .i { width: 16px; height: 16px; }
.widget-head .name {
  font-weight: 800; font-size: var(--fs-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.widget-body { padding: var(--sp-3) var(--sp-3) var(--sp-4); flex: 1; min-height: 0; overflow: auto; position: relative; }
/* Кнопки в шапке виджета — квадратики ровно по её высоте. Обычная `.btn small`
   с её отступами, контуром и тенью здесь выше самой шапки и вылезает за
   кромку: шапка тут не панель инструментов, а полоска в 30 px. */
.widget-tools { position: absolute; top: 5px; right: 7px; display: none; gap: 4px; }
.widget:hover .widget-tools { display: flex; }
.widget-tools .btn {
  width: 22px; height: 22px; padding: 0; flex: none;
  justify-content: center; font-size: 12px; line-height: 1;
  border: none; border-radius: var(--radius-xs); box-shadow: none;
  background: var(--paper); color: var(--text-muted);
}
.widget-tools .btn:hover { color: var(--ink); }
.widget-tools .btn:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow); }
.widget-tools .btn:active { transform: translate(1px, 1px); box-shadow: none; }
.widget-resize {
  position: absolute; right: 3px; bottom: 3px; width: 16px; height: 16px;
  cursor: nwse-resize; opacity: 0;
  background: linear-gradient(135deg, transparent 45%, var(--ink) 45%, var(--ink) 56%,
              transparent 56%, transparent 72%, var(--ink) 72%, var(--ink) 83%, transparent 83%);
}
.widget:hover .widget-resize { opacity: .5; }
.widget.dragging { opacity: .85; z-index: 30; box-shadow: var(--shadow-lg); rotate: -1.2deg; }
.grid.editing .widget { outline: 2px dashed var(--accent); outline-offset: 3px; }

/* --- содержимое виджетов ------------------------------------------------ */
/* Всё ниже — конструкторы из sdk.js. Модуль возвращает данные, разметку
   собирает платформа, поэтому один и тот же «показатель» у всех модулей
   выглядит одинаково — и одинаково хорошо. */

/* Обёртка показателя: число слева, наклейка справа, каждому своё место. */
.stat-wrap { display: flex; align-items: flex-start; gap: var(--sp-3); }
.stat-wrap .stat { flex: 1; min-width: 0; }
.stat { display: flex; flex-direction: column; gap: 2px; }
.stat .value {
  font-size: clamp(26px, 5.5vw, 38px); font-weight: 900; letter-spacing: -.05em;
  line-height: 1; display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
}
.stat .label { color: var(--text-muted); font-size: var(--fs-sm); font-weight: 600; }
/* Клэмп — только у строки-описания под числом (прямой потомок `.stat`).
   У `.value .label` (единица прямо у числа, «кг») он ломает промежуток от
   `gap` в `.value`: `display:-webkit-box` там сбивает флекс-разбор соседних
   элементов, и число с единицей склеиваются в одно слово без пробела. */
.stat > .label {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
.stat .value .label { font-size: var(--fs-md); letter-spacing: normal; }
.stat .trend {
  font-size: var(--fs-xs); font-weight: 800; align-self: flex-start;
  padding: 3px 10px; border-radius: var(--radius-pill); margin-top: 6px;
}
.trend.up { background: color-mix(in srgb, var(--success) 18%, var(--paper)); color: var(--success); }
.trend.down { background: color-mix(in srgb, var(--error) 16%, var(--paper)); color: var(--error); }
/* Крупная эмодзи-наклейка в углу: показатель перестаёт быть строкой отчёта и
   становится картинкой, которую видно с другого конца комнаты. */
.stat-face {
  flex: none; font-size: 42px; line-height: 1;
  rotate: -8deg; pointer-events: none; user-select: none;
  filter: drop-shadow(0 5px 8px color-mix(in srgb, var(--shade) 45%, transparent));
}

.list-item {
  display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) 0;
  border-top: 2px solid var(--surface-2); font-size: 14px;
}
.list-item:first-child { border-top: none; }
.list-item .t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.list-item .s { color: var(--text-muted); font-size: 12px; font-weight: 600; }
/* Метка строки: цветной квадратик с эмодзи или номером. Список из десяти
   одинаковых строк без неё читается как простыня. */
.list-item .bullet {
  width: 27px; height: 27px; flex: none; display: grid; place-items: center;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--tone-fill) 16%, var(--paper));
  color: color-mix(in srgb, var(--tone-fill) 75%, var(--ink));
  font-size: 13px; font-weight: 800;
}

/* Кольцо: доля, которую видно одним взглядом. conic-gradient, а не svg, —
   меньше разметки и анимируется одним свойством. */
.ring-wrap { display: flex; align-items: center; gap: var(--sp-4); }
.ring {
  --p: 0;
  width: 92px; height: 92px; border-radius: 50%; flex: none;
  background: conic-gradient(var(--tone-fill) calc(var(--p) * 1%), var(--surface-2) 0);
  border: var(--bw) solid var(--ink);
  display: grid; place-items: center; position: relative;
  box-shadow: var(--shadow);
}
.ring::after {
  content: ""; position: absolute; inset: 12px; border-radius: 50%;
  background: var(--paper);
}
.ring b { position: relative; z-index: 1; font-size: 19px; font-weight: 900; letter-spacing: -.04em; }
.ring-wrap .label { color: var(--text-muted); font-size: var(--fs-sm); font-weight: 600; }
.ring-wrap .big { display: block; font-size: var(--fs-xl); font-weight: 900; letter-spacing: -.03em; }

/* Столбики: с контуром и плоской заливкой, как всё остальное. Наибольший
   выделен цветом — глазу не приходится сравнивать высоты на глаз. */
.bars { display: flex; gap: 5px; align-items: flex-end; cursor: default; }
.bars > div { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 4px; min-width: 0; max-width: 100%; }
/* Рядовые столбики — приглушённый тон, наибольший — тон в полную силу.
   Контраст держится на насыщенности одного и того же цвета. */
.bars i {
  display: block; width: 100%;
  background: color-mix(in srgb, var(--tone-fill) 38%, var(--paper));
  border-radius: 6px 6px 0 0;
  transition: height .3s cubic-bezier(.3, 1.4, .5, 1);
}
.bars small {
  display: block; max-width: 100%; font-size: 10px; color: var(--text-muted); font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bars > div.peak i { background: var(--tone-fill); }
.bars > div.peak small { color: var(--text); font-weight: 800; }

/* Ломаная: тот же ряд чисел, но когда важна форма, а не сравнение. */
.spark-wrap { position: relative; height: 74px; }
.spark { display: block; width: 100%; height: 100%; overflow: visible; }
.spark .line { fill: none; stroke: var(--ink); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.spark .fill { fill: var(--tone-fill); stroke: none; opacity: .5; }
/* Точки поверх svg — не его элементы, потому что svg растянут по ширине и
   любую фигуру внутри растягивает вместе с собой (круг превращался бы в
   блин). Заодно это hover/tap-цели для подсказки со значением. */
.spark-point {
  position: absolute; width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px;
  border-radius: 50%; background: var(--tone-fill); box-shadow: 0 0 0 2px var(--paper);
  cursor: pointer; opacity: 0; transition: opacity .15s, scale .15s;
}
.spark-wrap:hover .spark-point { opacity: .6; }
.spark-point:hover, .spark-point:focus-visible { opacity: 1; scale: 1.4; }
@media (hover: none) { .spark-point { opacity: .6; } }

/* Пилюли-теги: набор коротких значений, у которых нет порядка. */
.pills { display: flex; flex-wrap: wrap; gap: 6px; }
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 11px; border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--tone-fill) 20%, var(--paper));
  color: color-mix(in srgb, var(--tone-fill) 70%, var(--ink));
  font-size: var(--fs-xs); font-weight: 800;
}
.pill.off { background: var(--surface-2); color: var(--text-muted); }

/* Сетка активности: каждый квадрат — день, насыщенность — сколько сделано. */
.heat { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 1fr); gap: 3px; }
.heat i {
  width: 13px; height: 13px; border-radius: 4px;
  background: color-mix(in srgb, var(--tone-fill) calc(var(--v, 0) * 1%), var(--surface-2));
}

/* Наклейка-состояние: когда у виджета нет числа, а есть настроение. */
.sticker-card {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 5px; height: 100%; text-align: center;
}
.sticker-card .face {
  font-size: 52px; line-height: 1;
  filter: drop-shadow(0 6px 10px color-mix(in srgb, var(--ink) 35%, transparent));
  animation: sticker-bob 3s ease-in-out infinite;
}
.sticker-card .t { font-weight: 900; font-size: var(--fs-lg); letter-spacing: -.03em; }
.sticker-card .s { color: var(--text-muted); font-size: var(--fs-xs); font-weight: 600; }
@keyframes sticker-bob { 50% { transform: translateY(-5px) rotate(-4deg); } }
@media (prefers-reduced-motion: reduce) { .sticker-card .face { animation: none; } }

/* --- уведомления и модалки ---------------------------------------------- */
/* Колокольчик — не строка в шторке, а отдельная плавающая кнопка в углу
   экрана: доступен с любой страницы и не зависит от того, свёрнута ли
   шторка или открыта ли она вообще на телефоне. */
.bell.bell-corner {
  position: fixed; top: var(--sp-4); right: var(--sp-4); z-index: 41;
  width: 44px; height: 44px; padding: 0; justify-content: center;
  border-radius: 50%; background: var(--paper); color: var(--ink);
}
@media (max-width: 720px) {
  .bell.bell-corner { top: max(var(--sp-3), env(safe-area-inset-top)); right: var(--sp-3); }
}
.bell { position: relative; }
.bell .count {
  position: absolute; top: -5px; right: -5px; min-width: 19px; height: 19px;
  padding: 0 4px; border-radius: 50%; /* индикатор-точка, не тег: круглым и остаётся */
  background: var(--c-coral); color: #fff;
  font-size: 11px; font-weight: 900; display: grid; place-items: center;
}
/* Шторка уведомлений: не всплывающий дропдаун у колокольчика, а полноразмерная
   по высоте панель у правого края — открывается и прячется целиком, а не
   перекрывает пару виджетов рядом с курсором. */
.panel {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(380px, 92vw);
  overflow: auto; z-index: 60; padding: var(--sp-5);
  background: var(--paper); border-left: var(--bw) solid var(--ink);
  border-radius: 0; box-shadow: var(--shadow-lg);
  animation: panel-in .2s ease-out;
}
@keyframes panel-in { from { transform: translateX(24px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .panel { animation: none; } }
.notif { padding: var(--sp-3) 0; border-top: 2px solid var(--surface-2); }
.notif:first-of-type { border-top: none; }
.notif .title { font-weight: 800; margin: 3px 0; }
.notif.unread { position: relative; padding-left: 13px; }
.notif.unread::before {
  content: ""; position: absolute; left: 0; top: 15px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--c-coral);
}

.modal-back {
  position: fixed; inset: 0; background: color-mix(in srgb, #2a1f18 58%, transparent);
  z-index: 80; display: flex; align-items: center; justify-content: center;
  padding: var(--sp-5);
}
.modal {
  background: var(--paper); border-radius: var(--radius);
  border: var(--bw) solid var(--ink); box-shadow: var(--shadow-lg);
  padding: var(--sp-5); width: min(560px, 100%); max-height: 85vh; overflow: auto;
  animation: pop .16s ease-out;
}
/* Селектор из двух классов — иначе правило .modal ниже по файлу перебивает ширину. */
.modal.modal-wide { width: min(980px, 100%); }
@keyframes pop { from { transform: translateY(8px) scale(.98); opacity: 0; } }

/* Тосты — стопка независимых карточек (шаблон shadcn Sonner), а не одна
   нода на весь экран: несколько уведомлений подряд не наезжают друг на
   друга, у каждого есть кнопка закрытия, а по цвету левой полосы видно
   успех/ошибку без чтения текста. Контейнер лежит внизу справа на десктопе
   и во всю ширину снизу на телефоне (см. медиа-блок ниже). */
.toast-stack {
  position: fixed; z-index: 90; right: var(--sp-4); bottom: var(--sp-4);
  display: flex; flex-direction: column-reverse; gap: 10px;
  align-items: flex-end; pointer-events: none;
}
.toast {
  pointer-events: auto; display: flex; align-items: flex-start; gap: 9px;
  width: min(340px, 82vw); padding: 11px 12px;
  background: var(--paper); color: var(--text);
  border: var(--bw) solid var(--ink); border-left-width: 7px;
  border-radius: var(--radius-sm); font-size: 13px; font-weight: 700; line-height: 1.35;
  box-shadow: var(--shadow); animation: pop .16s ease-out;
}
.toast .i { flex: none; margin-top: 1px; }
.toast .text { flex: 1; min-width: 0; }
.toast .close {
  flex: none; width: 20px; height: 20px; padding: 0; border: none; background: none;
  color: var(--text-muted); cursor: pointer; display: grid; place-items: center; border-radius: 50%;
}
.toast .close:hover { background: var(--surface-2); color: var(--text); }
.toast-success { border-left-color: var(--success); }
.toast-success .i { color: var(--success); }
.toast-error { border-left-color: var(--error); }
.toast-error .i { color: var(--error); }

.center-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: var(--sp-5); }
.auth-box { width: min(390px, 100%); position: relative; overflow: hidden; }
/* Титульная полоса окна входа — то же окно, что и везде на платформе: три
   кнопки управления слева, подпись справа, а не голая радужная лента. Первое,
   что видно до входа, называет язык, на котором говорит всё остальное. */
.win-bar {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: calc(-1 * var(--sp-5)) calc(-1 * var(--sp-5)) var(--sp-4);
  padding: 8px var(--sp-4);
  background: linear-gradient(90deg,
    var(--c-coral) 0 11.11%, var(--c-tangerine) 0 22.22%, var(--c-lemon) 0 33.33%,
    var(--c-lime) 0 44.44%, var(--c-mint) 0 55.55%, var(--c-teal) 0 66.66%,
    var(--c-sky) 0 77.77%, var(--c-grape) 0 88.88%, var(--c-bubble) 0);
  border-bottom: var(--bw) solid var(--ink);
}
.win-bar .dot { width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--ink); flex: none; }
.win-bar .dot.r { background: #ff5f57; }
.win-bar .dot.y { background: #ffc93c; }
.win-bar .dot.g { background: #33cf6e; }
.win-bar-title {
  margin-left: auto; font-size: 11px; font-weight: 900; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink); text-shadow: 0 1px 0 rgba(255, 255, 255, .35);
}
.auth-cat { display: block; width: 220px; max-width: 85%; height: auto; margin: 16px auto; }

.module-tile { display: flex; gap: var(--sp-3); align-items: flex-start; }
.module-icon {
  width: 48px; height: 48px; border-radius: var(--radius-xs); flex: none;
  display: grid; place-items: center; font-size: 23px;
  background: var(--tone-fill); border: var(--bw) solid var(--ink);
  box-shadow: var(--shadow);
}

/* --- подсказки ---------------------------------------------------------- */
.hint { position: relative; }
.hint:last-of-type > .hint-body { left: auto; right: 0; translate: 0 0; }
/* Вниз, а не вверх: у элементов шапки подсказка сверху упиралась в край окна. */
.hint > .hint-body {
  position: absolute; top: calc(100% + 8px); left: 50%; translate: -50% 0;
  width: max-content; max-width: 240px; padding: 9px 11px; z-index: 50;
  background: var(--paper); color: var(--text);
  border: var(--bw) solid var(--ink); box-shadow: var(--shadow);
  border-radius: var(--radius-sm); font-size: 12px; line-height: 1.35; text-align: left;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity .14s ease, transform .14s ease, visibility .14s;
  pointer-events: none;
}
.hint:hover > .hint-body, .hint:focus-visible > .hint-body {
  opacity: 1; visibility: visible; transform: translateY(0);
}
.hint-body b { display: block; margin-bottom: 2px; }

/* Подсказка поверх страницы (см. initTooltips в sdk.js) — для мест вроде
   витрины, где обычная `.hint` внутри скроллящейся ленты обрезалась бы или
   заводила лишнюю прокрутку прямо на наведении. */
.tip {
  position: fixed; z-index: 200; width: max-content; max-width: 240px;
  padding: 9px 11px; background: var(--paper); color: var(--text);
  border: var(--bw) solid var(--ink); box-shadow: var(--shadow);
  border-radius: var(--radius-sm); font-size: 12px; line-height: 1.35; text-align: left;
  pointer-events: none;
}
.tip b { display: block; margin-bottom: 2px; }
.tip[hidden] { display: none; }

/* --- разметка картинок ачивок ------------------------------------------- */
.modal.spmap { width: min(1180px, 100%); }
.spmap-help { margin-bottom: var(--sp-3); }
.spmap-body {
  display: grid; grid-template-columns: minmax(0, 1fr) 330px;
  gap: var(--sp-4); align-items: start;
}
/* Подложка тёмная, и это не выбор темы: фигуры на листе белые с тёмным
   контуром — ровно как на карточке, где они лежат на тёмной панели. */
.spmap-canvas {
  position: relative; overflow: hidden; cursor: crosshair;
  border: var(--bw) solid var(--ink); border-radius: var(--radius-sm);
  background-color: #23262f;
  background-image:
    linear-gradient(45deg, rgba(255, 255, 255, .05) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, .05) 75%),
    linear-gradient(45deg, rgba(255, 255, 255, .05) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, .05) 75%);
  background-size: 18px 18px;
  background-position: 0 0, 9px 9px;
  touch-action: none;               /* палец обводит рамку, а не листает страницу */
}
.spmap-canvas img {
  display: block; width: 100%; height: auto; user-select: none; -webkit-user-drag: none;
  image-rendering: pixelated; /* лист — пиксель-арт, разметка должна показывать его как есть, не мылить */
}
.spmap-layer { position: absolute; inset: 0; }
/* Найденное автопоиском — пунктиром: подсказка, а не решение. */
.spmap-figure { position: absolute; border: 2px dashed rgba(255, 255, 255, .45); border-radius: 6px; }
.spmap-figure span { position: absolute; top: 2px; left: 3px; font-size: 10px; font-weight: 700; color: rgba(255, 255, 255, .6); }
.spmap-box {
  position: absolute; border: var(--bw) solid var(--c-grape); border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--c-grape) 14%, transparent); cursor: pointer;
}
.spmap-box.active { border-color: var(--c-bubble); box-shadow: 0 0 0 2px var(--ink); }
.spmap-box.ghost { border-style: dashed; pointer-events: none; }
.spmap-box span {
  position: absolute; top: 0; left: 0; max-width: 100%;
  padding: 1px 5px; border-radius: 5px 0 5px 0;
  background: var(--c-grape); color: var(--ink);
  font-size: 10px; font-weight: 800; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.spmap-box.active span { background: var(--c-bubble); }
.spmap-list { display: flex; flex-direction: column; gap: var(--sp-2); max-height: 62vh; overflow: auto; padding-right: 2px; }
.spmap-row {
  display: flex; gap: var(--sp-3); align-items: center; cursor: pointer;
  padding: var(--sp-2); border: var(--bw) solid var(--ink);
  border-radius: var(--radius-sm); background: var(--surface-2);
  transition: transform .1s, box-shadow .1s;
}
.spmap-row:hover { transform: translate(-1px, -1px); box-shadow: var(--shadow); }
.spmap-row.active { background: var(--tone-fill); color: var(--tone-text); box-shadow: var(--shadow); }
.spmap-thumb {
  width: 52px; height: 52px; flex: none; border-radius: var(--radius-xs);
  border: var(--bw) solid var(--ink); background-color: #23262f;
  background-repeat: no-repeat; display: grid; place-items: center;
  color: rgba(255, 255, 255, .45);
  image-rendering: pixelated;
}
.spmap-text { flex: 1; min-width: 0; }
.spmap-text .t { font-weight: 800; font-size: var(--fs-sm); }
.spmap-text .s { font-size: var(--fs-xs); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.spmap-row.active .s { color: inherit; opacity: .75; }
.spmap-art { font-style: italic; }
.spmap-row-tools { display: flex; align-items: center; gap: var(--sp-1); flex: none; }
.spmap-foot { margin-top: var(--sp-4); flex-wrap: wrap; }

/* --- телефон ------------------------------------------------------------ */
@media (max-width: 720px) {
  /* На телефоне шторка прячется целиком — узкий экран ей ничего не должен.
     Открывается отдельной плавающей кнопкой (.sidebar-fab, иконка menu) и
     закрывается подложкой или переходом по ссылке внутри. */
  .shell { display: block; }
  /* Шторка на телефоне — оверлей поверх страницы, а не соседняя колонка,
     поэтому у панели контента больше нет соседа, со скруглением которого
     нужно состыковаться: во всю ширину и без скругления, иначе по бокам
     проглядывал бы фон шторки там, где самой шторки уже нет. */
  .content-frame { border-radius: 0; min-height: 100dvh; }
  .sidebar {
    position: fixed; left: 0; top: 0; height: 100dvh; width: min(84vw, 300px);
    padding-top: env(safe-area-inset-top); z-index: 70;
    transform: translateX(-100%); transition: transform .18s ease;
  }
  /* На десктопе/планшете .collapsed заужает шторку до 68px рельса — этот флаг
     живёт в localStorage и переживает переход на телефон. Специфичность
     `.sidebar.collapsed` (два класса) выше, чем у `.sidebar` в этом же блоке
     (один класс), и без явного сброса здесь она бы победила media-запрос по
     порядку каскада — шторка на телефоне открывалась бы тем же узким рельсом,
     а не во всю ширину. */
  .sidebar.collapsed { width: min(84vw, 300px); }
  .sidebar.collapsed .label { display: block; }
  .sidebar.collapsed .sidebar-link { justify-content: flex-start; padding: 9px 11px; }
  /* Флаг "свёрнута" живёт в localStorage и переживает переход на телефон
     (см. комментарий выше), но там от него нужна только ширина — свотчи
     палитры на всю ширину помещаются свободно. */
  .sidebar.collapsed .palette-picker { display: flex; flex-direction: column; }
  .sidebar-collapse-btn { display: none; }   /* сворачивать нечего — либо открыта, либо скрыта целиком */
  .sidebar.open { transform: translateX(0); box-shadow: var(--shadow-lg); }
  .sidebar-backdrop.show { display: block; }
  .sidebar-fab {
    display: flex; top: max(12px, env(safe-area-inset-top));
  }
  main {
    padding: 12px; padding-bottom: max(12px, env(safe-area-inset-bottom));
  }
  h1 { font-size: 22px; }
  .card { padding: var(--sp-4); }
  /* Полоса окна входа отступает от края ровно на подложенный `.card` паддинг —
     на телефоне он меньше, чем на десктопе, и величину бегства надо повторить. */
  .win-bar { margin: calc(-1 * var(--sp-4)) calc(-1 * var(--sp-4)) var(--sp-4); }
  .card-header { flex-wrap: wrap; }
  .card-header h1, .card-header h2 { flex: 1 1 100%; }
  /* Широкая таблица скроллится в своих границах, а не раздвигает страницу. */
  .card:has(table.table) { overflow-x: auto; }
  table.table { min-width: 480px; }

  /* Одна колонка — но виджеты несут ещё и `grid-row` из десктопной раскладки
     (её ставит инлайновым стилем dashboard.js под 12-колоночную сетку). Без
     сброса ряда два виджета, стоявшие рядом по горизонтали, здесь остаются в
     тех же строках — то есть в одной колонке — и ложатся друг на друга. */
  .grid { grid-template-columns: 1fr; grid-auto-flow: row; }
  /* span сохраняем (--h ставит place() в dashboard.js), сбрасываем только
     абсолютный номер строки — на мобильном виджеты идут потоком друг под
     другом, а не по десктопным координатам. Без сохранения span виджет с
     h:2 (кольцо, график) сжимался бы до одной ячейки 84px и обрезался. */
  .grid .widget { grid-column: 1 / -1 !important; grid-row: span var(--h, 1) !important; }
  /* Перетаскивание на телефоне отключено (см. dashboard.js) — уголок только мешает. */
  .widget-head { cursor: default; }
  .widget-resize { display: none; }

  /* Панель уведомлений: во всю высоту в телефон не помещается, поэтому лист
     снизу — та же шторка, только едет вверх, а не слева направо. */
  .panel {
    top: auto; left: 8px; right: 8px; bottom: max(8px, env(safe-area-inset-bottom));
    width: auto; max-height: 72vh;
    border: var(--bw) solid var(--ink); border-radius: var(--radius);
    animation: panel-in-up .2s ease-out;
  }
  @keyframes panel-in-up { from { transform: translateY(16px); opacity: 0; } }
  .modal-back { padding: 12px; align-items: flex-end; }
  .modal { padding: 16px; max-height: 88vh; }
  /* Стопка тостов на телефоне — во всю ширину снизу, не в углу. */
  .toast-stack { left: 8px; right: 8px; bottom: max(8px, env(safe-area-inset-bottom)); align-items: stretch; }
  .toast { width: 100%; }
  /* Разметка в один столбец: 330 px под список рядом с листом не остаётся. */
  .spmap-body { grid-template-columns: 1fr; }
  .spmap-list { max-height: 40vh; }
  /* Наклейки на узком экране лезут под текст — на телефоне остаётся клетка. */
  .stickers { display: none; }
}

/* Наведения нет — иначе кнопки виджета недостижимы: снять виджет было нечем. */
@media (hover: none) {
  .widget-tools { display: flex; }
}
