/**
 * RU: Kartivi — единый слой дизайн-токенов для всех пяти поверхностей продукта
 *     (публичная карта, кабинет бизнеса, кабинет жителя, админка, хаб городов).
 *     До этого файла у каждой поверхности была своя палитра из пяти-десяти
 *     переменных: `--ink` в app.css отличался от `--ink` в biz.css, а тёмная тема
 *     существовала как три переменные `--k-*` плюс инверсия картинок фильтром.
 *     Здесь один источник правды: палитра, шкала отступов, радиусы, тени,
 *     уровни z-index, типографическая шкала — в светлой, тёмной и контрастной темах.
 *
 * EN: Kartivi — one design-token layer for all five product surfaces (public map,
 *     business cabinet, resident cabinet, admin, city hub). Before this file each
 *     surface carried its own five-to-ten variable palette: `--ink` in app.css was
 *     a different colour from `--ink` in biz.css, and dark mode existed as three
 *     `--k-*` variables plus a CSS filter that inverted images. This is the single
 *     source of truth: palette, spacing scale, radii, shadows, z-index ladder and
 *     type scale, in light, dark and high-contrast themes.
 *
 * RU: Подключается ПЕРВЫМ, до всех остальных таблиц стилей — они переопределяют
 *     свои исторические имена (`--ink`, `--paper`, `--line`) через эти токены,
 *     поэтому существующая вёрстка продолжает работать без правок.
 * EN: Loaded FIRST, before every other stylesheet — those alias their historical
 *     names (`--ink`, `--paper`, `--line`) onto these tokens, so existing markup
 *     keeps working untouched.
 *
 * RU: Правило темы. Тема выбирается тремя способами и в таком приоритете:
 *     1) явный выбор пользователя — атрибут `data-theme="light|dark|contrast"`
 *        на <html>, его ставит public/js/theme.js;
 *     2) системная настройка — `prefers-color-scheme: dark`, но только когда
 *        явного выбора нет (селектор `:root:not([data-theme])`);
 *     3) светлая тема по умолчанию.
 *     Ни один цвет не объявляется ТОЛЬКО внутри media-запроса: базовые значения
 *     всегда есть в `:root`, иначе при отсутствии атрибута страница осталась бы
 *     без половины палитры.
 * EN: Theme rule. The theme is chosen three ways, in this order of precedence:
 *     1) the user's explicit choice — a `data-theme="light|dark|contrast"`
 *        attribute on <html>, set by public/js/theme.js;
 *     2) the system setting — `prefers-color-scheme: dark`, but only when no
 *        explicit choice exists (the `:root:not([data-theme])` selector);
 *     3) light by default.
 *     No colour is declared ONLY inside a media query: base values always live in
 *     `:root`, otherwise a document without the attribute would render with half
 *     the palette missing.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   RU: 1. Неизменяемые токены — не зависят от темы.
   EN: 1. Theme-independent tokens.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* RU: Шрифты. Fraunces — заголовки и имя города, DM Sans — интерфейс,
         DM Mono — всё, что выстраивается в колонку: телефоны, счётчики, координаты.
     EN: Typefaces. Fraunces for headings and the city name, DM Sans for the UI,
         DM Mono for anything that lines up in a column: phones, counters, coordinates. */
  --k-font-ui: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --k-font-brand: "Fraunces", Georgia, "Times New Roman", serif;
  --k-font-mono: "DM Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* RU: Шкала отступов, кратная 4 px. Раньше отступы задавались в rem «на глаз»
         (0.65rem, 0.85rem, 1.25rem), из-за чего одинаковые по смыслу зазоры
         отличались на пару пикселей от блока к блоку.
     EN: A 4 px spacing scale. Spacing used to be eyeballed in rem (0.65rem,
         0.85rem, 1.25rem), so gaps meant to match differed by a pixel or two
         from block to block. */
  --k-s-1: 4px;
  --k-s-2: 8px;
  --k-s-3: 12px;
  --k-s-4: 16px;
  --k-s-5: 20px;
  --k-s-6: 24px;
  --k-s-7: 32px;
  --k-s-8: 40px;
  --k-s-9: 56px;

  /* RU: Радиусы. Четыре ступени: контрол, карточка, панель, «таблетка».
     EN: Radii. Four steps: control, card, panel, pill. */
  --k-r-1: 6px;
  --k-r-2: 10px;
  --k-r-3: 14px;
  --k-r-4: 20px;
  --k-r-pill: 999px;

  /* RU: Типографическая шкала. Нижняя ступень — 12 px, а не 11.5: текст мельче
         12 px в статус-строке не проходил проверку контраста AA.
     EN: Type scale. The bottom step is 12 px rather than 11.5: text smaller than
         12 px in the status bar failed the AA contrast check. */
  --k-fs-1: 12px;
  --k-fs-2: 13px;
  --k-fs-3: 14px;
  --k-fs-4: 15px;
  --k-fs-5: 17px;
  --k-fs-6: 20px;
  --k-fs-7: 26px;
  --k-fs-8: 34px;
  --k-lh-tight: 1.2;
  --k-lh-normal: 1.5;
  --k-lh-loose: 1.65;

  /* RU: Уровни наложения. Раньше z-index ставился случайными числами (2, 5, 10,
         1000), и всплывашка над шторкой оказывалась под ней при смене порядка.
         Здесь лестница именованная, и новый слой встаёт между ступенями.
     EN: Stacking levels. z-index used to be arbitrary numbers (2, 5, 10, 1000),
         so a popover above the sheet could end up beneath it when order changed.
         This is a named ladder; a new layer slots between two steps. */
  --k-z-map: 0;
  --k-z-map-overlay: 10;
  --k-z-panel: 20;
  --k-z-sheet: 30;
  --k-z-popover: 40;
  --k-z-toast: 50;
  --k-z-modal: 60;

  /* RU: Минимальная тач-цель. 44 px — порог, ниже которого промахи по кнопке
         на телефоне становятся системными; кнопки инструментов карты сейчас 26–32 px.
     EN: Minimum tap target. 44 px is the threshold below which mis-taps on a phone
         become systematic; the map tool buttons are currently 26–32 px. */
  --k-tap-min: 44px;

  /* RU: Длительности. Одна короткая для откликов на нажатие, одна средняя для
         появления панелей. Больше двух значений в продукте не нужно.
     EN: Durations. One short for press feedback, one medium for panels appearing.
         The product needs no more than two values. */
  --k-dur-fast: 120ms;
  --k-dur-slow: 240ms;
}

/* ═══════════════════════════════════════════════════════════════════════════
   RU: 2. Светлая тема — базовые значения. Объявлены в голом :root, поэтому
       действуют всегда, даже если атрибут темы не проставлен.
   EN: 2. Light theme — the base values. Declared on a bare :root so they always
       apply, even when no theme attribute is stamped.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* RU: Поверхности. Нейтральные с лёгким уклоном в зелёный — тот же уклон,
         что у акцента #1f6b4a. Чистый серый рядом с зелёным читается грязным.
     EN: Surfaces. Neutrals biased slightly green — the same bias as the #1f6b4a
         accent. A pure grey next to that green reads as dirty. */
  --k-bg: #f1f5ef;
  --k-surface: #ffffff;
  --k-surface-2: #e9efe7;
  --k-surface-3: #f6f9f5;
  --k-card-bg: #ffffff;

  /* RU: Текст. Три ступени: основной, приглушённый, служебный.
     EN: Text. Three steps: primary, muted, subtle. */
  --k-text: #0f1a14;
  --k-text-muted: #46574c;
  --k-text-subtle: #6d7f74;
  --k-text-on-accent: #ffffff;

  /* RU: Линии. Мягкая — для разделителей внутри карточки, обычная — для границ.
     EN: Lines. Soft for dividers inside a card, regular for borders. */
  --k-border: #d2ded5;
  --k-border-soft: #e3ebe3;
  --k-border-strong: #7d9187;

  /* RU: Акцент — зелёный Kartivi. `-soft` для подложек, `-ink` для текста
         акцентного цвета на светлом фоне (сам акцент на белом даёт 5.2:1,
         этого хватает для крупного текста, но не для мелкого).
     EN: Accent — the Kartivi green. `-soft` for fills, `-ink` for accent-coloured
         text on a light ground (the accent itself gives 5.2:1 on white, enough
         for large text but not for small). */
  --k-brand: #1f6b4a;
  --k-brand-hover: #17573b;
  --k-brand-soft: #d8eee3;
  --k-brand-ink: #14513a;

  /* RU: Состояния. Это отдельная от акцента шкала: «хорошо» не обязано быть
         брендовым зелёным, иначе успех и бренд читаются как одно и то же.
     EN: Status. A scale separate from the accent: "good" need not be the brand
         green, otherwise success and brand read as the same thing. */
  --k-ok: #1b6d40;
  --k-ok-soft: #daeee2;
  --k-warn: #7a5709;
  --k-warn-soft: #f5ead1;
  --k-danger: #a33526;
  --k-danger-soft: #f6dcd9;
  --k-info: #2f6d92;
  --k-info-soft: #dbe9f2;

  /* RU: Цвет оценки (звёзды и среднее). Отдельная шкала, а не состояние: пять
         звёзд — не «успех», одна — не «ошибка», и переиспользовать здесь --k-warn
         значило бы сказать неправду о смысле цвета.

         В светлой теме золото пришлось увести в тёмную сторону. Прежний #d4a017
         давал по белому 2.15:1 при норме 4.5:1 для текста рядом со звёздами и 3:1
         для самих звёзд — то есть не проходил ни по одному порогу. Взято #88660f:
         5.31:1 по белому и 4.82:1 по --k-bg, тон сохранён.
     EN: The rating colour (stars and the average). A scale of its own rather than a status:
         five stars are not "success" and one is not an "error", and reusing --k-warn here
         would misstate what the colour means.

         In the light theme the gold had to move darker. The previous #d4a017 gave 2.15:1
         against white, against a 4.5:1 requirement for the text beside the stars and 3:1
         for the stars themselves — failing both thresholds. #88660f was chosen: 5.31:1
         against white and 4.82:1 against --k-bg, with the hue preserved. */
  --k-rating: #88660f;

  /* RU: Карта. `map-void` — цвет холста до загрузки тайлов; должен совпадать
         с фоном стиля, иначе при старте видно вспышку чужого цвета.
     EN: Map. `map-void` is the canvas colour before tiles load; it must match the
         style's background, otherwise startup shows a flash of a foreign colour. */
  --k-map-void: #dce6df;

  /* RU: Тени. Три ступени подъёма. Все на цвете чернил, а не на чистом чёрном —
         чёрная тень на зеленоватом фоне выглядит грязным пятном.
     EN: Shadows. Three elevation steps, all built on the ink colour rather than
         pure black — a black shadow on a greenish ground looks like a smudge. */
  --k-shadow-1: 0 1px 2px rgba(15, 26, 20, 0.06);
  --k-shadow-2: 0 6px 16px -8px rgba(15, 26, 20, 0.24);
  --k-shadow-3: 0 18px 44px -22px rgba(15, 26, 20, 0.42);
  --k-card-shadow: rgba(15, 26, 20, 0.08);

  /* RU: Фокус. Видимое кольцо обязательно на всём кликаемом, включая пины карты.
     EN: Focus. A visible ring is mandatory on everything clickable, map pins included. */
  --k-focus: 0 0 0 2px var(--k-bg), 0 0 0 4px var(--k-brand);

  /* RU: Атмосфера страницы — фоновые градиенты хаба, кабинета и витрины бизнеса.
         Отдельные токены нужны потому, что градиент состоит из нескольких остановок:
         подставить токен только в одну — значит получить в тёмной теме переход
         из тёмного в светлое посреди экрана.
     EN: Page atmosphere — the background gradients of the hub, the cabinet and the
         business storefront. They need their own tokens because a gradient has
         several stops: substituting a token into just one would fade from dark to
         light mid-screen in the dark theme. */
  --k-atmo-glow: #d4e8da;
  --k-atmo-glow-warm: #f0e4d8;
  --k-atmo-1: #eef5f0;
  --k-atmo-2: #e2ebe4;
  --k-atmo-3: #d9e6de;
}

/* ═══════════════════════════════════════════════════════════════════════════
   RU: 3. Тёмная тема. Не инверсия светлой: вода, парк и дорога ночью меняют
       роль, а не только яркость. Акцент осветляется — тёмно-зелёный #1f6b4a
       на тёмном фоне даёт контраст 1.6:1 и нечитаем.
   EN: 3. Dark theme. Not an inversion of light: water, park and road change role
       at night rather than merely brightness. The accent is lightened — the dark
       #1f6b4a green on a dark ground gives 1.6:1 contrast and is unreadable.
   ═══════════════════════════════════════════════════════════════════════════ */

/* RU: Общий набор значений вынесен в блок, который подключается двумя
       селекторами: системной настройкой (когда явного выбора нет) и явным
       атрибутом. Дублирование здесь намеренное — CSS не умеет «применить набор
       по двум условиям» без препроцессора, а вводить его ради этого не стоит.
   EN: The shared values are repeated under two selectors: the system setting (when
       no explicit choice exists) and the explicit attribute. The duplication is
       deliberate — plain CSS cannot apply one block under two conditions without a
       preprocessor, and adding one just for this is not worth it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --k-bg: #0d1512;
    --k-surface: #131e19;
    --k-surface-2: #1a2721;
    --k-surface-3: #101a15;
    --k-card-bg: #131e19;

    --k-text: #e7efe9;
    --k-text-muted: #a6b8ad;
    --k-text-subtle: #7c8f84;
    --k-text-on-accent: #04150d;

    --k-border: #26362e;
    --k-border-soft: #1d2b24;
    --k-border-strong: #5c7568;

    --k-brand: #58c491;
    --k-brand-hover: #74d6a7;
    --k-brand-soft: #163529;
    --k-brand-ink: #8ddcb4;

    --k-ok: #45b877;
    --k-ok-soft: #14301f;
    --k-warn: #d3a53c;
    --k-warn-soft: #2d2411;
    --k-danger: #e0685a;
    --k-danger-soft: #331815;
    --k-info: #6fb4dc;
    --k-info-soft: #12303e;

    /* RU: Ф6.7: значение было только в явной ночной теме ([data-theme="dark"]). В режиме «как в системе» ночью оставалось дневное
           #88660f — тёмное золото по тёмному фону, и звёзды с рейтингом почти пропадали. Значение то же, что ниже: 7.80:1 к --k-bg.
       EN: Ф6.7: the value existed only in the explicit night theme ([data-theme="dark"]). In "follow the system" mode at night the day
           #88660f stayed — dark gold on a dark ground, and stars and ratings nearly vanished. The same value as below: 7.80:1 against --k-bg. */
    --k-rating: #d4a017;

    --k-map-void: #0f1815;

    --k-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
    --k-shadow-2: 0 6px 16px -8px rgba(0, 0, 0, 0.6);
    --k-shadow-3: 0 18px 44px -22px rgba(0, 0, 0, 0.75);
    --k-card-shadow: rgba(0, 0, 0, 0.4);

    /* RU: Ночная атмосфера. Свечения приглушены до едва заметных: на тёмном фоне
           они читаются как подсветка, а не как пятна.
       EN: Night atmosphere. The glows are dimmed to barely visible: on a dark ground
           they read as illumination rather than as blotches. */
    --k-atmo-glow: #16352a;
    --k-atmo-glow-warm: #2a2318;
    --k-atmo-1: #101a15;
    --k-atmo-2: #0c1512;
    --k-atmo-3: #0a1210;
  }
}

:root[data-theme="dark"] {
  --k-bg: #0d1512;
  --k-surface: #131e19;
  --k-surface-2: #1a2721;
  --k-surface-3: #101a15;
  --k-card-bg: #131e19;

  --k-text: #e7efe9;
  --k-text-muted: #a6b8ad;
  --k-text-subtle: #7c8f84;
  --k-text-on-accent: #04150d;

  --k-border: #26362e;
  --k-border-soft: #1d2b24;
  --k-border-strong: #5c7568;

  --k-brand: #58c491;
  --k-brand-hover: #74d6a7;
  --k-brand-soft: #163529;
  --k-brand-ink: #8ddcb4;

  --k-ok: #45b877;
  --k-ok-soft: #14301f;
  --k-warn: #d3a53c;
  --k-warn-soft: #2d2411;
  --k-danger: #e0685a;
  --k-danger-soft: #331815;
  --k-info: #6fb4dc;
  --k-info-soft: #12303e;

  /* RU: В тёмной теме исходное золото проходит с запасом (7.80:1 к --k-bg) — менять
         нечего, и не нужно: узнаваемость цвета оценки важнее единообразия правки.
     EN: In the dark theme the original gold passes comfortably (7.80:1 against --k-bg) —
         there is nothing to change, and no need: the rating colour's recognisability
         matters more than making the same edit everywhere. */
  --k-rating: #d4a017;

  --k-map-void: #0f1815;

  --k-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
  --k-shadow-2: 0 6px 16px -8px rgba(0, 0, 0, 0.6);
  --k-shadow-3: 0 18px 44px -22px rgba(0, 0, 0, 0.75);
  --k-card-shadow: rgba(0, 0, 0, 0.4);

  /* RU: Ночная атмосфера. Свечения приглушены до едва заметных: на тёмном фоне
         они читаются как подсветка, а не как пятна.
     EN: Night atmosphere. The glows are dimmed to barely visible: on a dark ground
         they read as illumination rather than as blotches. */
  --k-atmo-glow: #16352a;
  --k-atmo-glow-warm: #2a2318;
  --k-atmo-1: #101a15;
  --k-atmo-2: #0c1512;
  --k-atmo-3: #0a1210;
}

/* ═══════════════════════════════════════════════════════════════════════════
   RU: 4. Контрастная тема. Не «светлая, но жирнее»: границы становятся тёмными
       и толстыми, приглушённый текст подтягивается почти до основного, акцент
       темнеет до 8:1 на белом. Нужна не только слабовидящим — ещё и на солнце.
   EN: 4. High-contrast theme. Not "light but bolder": borders become dark and
       thick, muted text rises almost to primary, the accent darkens to 8:1 on
       white. Needed not only for low vision — also in direct sunlight.
   ═══════════════════════════════════════════════════════════════════════════ */
:root[data-theme="contrast"] {
  --k-bg: #ffffff;
  --k-surface: #ffffff;
  --k-surface-2: #eef2ef;
  --k-surface-3: #f8faf8;
  --k-card-bg: #ffffff;

  --k-text: #05100a;
  --k-text-muted: #1f2f26;
  --k-text-subtle: #35473c;
  --k-text-on-accent: #ffffff;

  --k-border: #35473c;
  --k-border-soft: #7d8f85;
  --k-border-strong: #05100a;

  --k-brand: #0c4a30;
  --k-brand-hover: #06331f;
  --k-brand-soft: #cfe6da;
  --k-brand-ink: #06331f;

  --k-ok: #12603a;
  --k-ok-soft: #cfe6da;
  --k-warn: #5c4208;
  --k-warn-soft: #f2e6c8;
  --k-danger: #7a2016;
  --k-danger-soft: #f2d3ce;
  --k-info: #0f4763;
  --k-info-soft: #cfe0ec;

  /* RU: В контрастной теме золото уходит ещё темнее: тема существует ради людей,
         которым обычного AA мало, и 5.31:1 здесь не запас, а минимум.
     EN: In the contrast theme the gold goes darker still: the theme exists for people for
         whom ordinary AA is not enough, and 5.31:1 here is the floor, not the margin. */
  --k-rating: #5c4208;

  --k-map-void: #e7ece8;

  --k-shadow-1: 0 0 0 1px #35473c;
  --k-shadow-2: 0 2px 0 0 #05100a;
  --k-shadow-3: 0 3px 0 0 #05100a;
  --k-card-shadow: rgba(5, 16, 10, 0.28);

  /* RU: В контрастной теме атмосферы нет вовсе: градиент снижает контраст текста,
         а это ровно то, ради чего тему включают.
     EN: The contrast theme has no atmosphere at all: a gradient lowers text contrast,
         which is precisely what the theme exists to avoid. */
  --k-atmo-glow: transparent;
  --k-atmo-glow-warm: transparent;
  --k-atmo-1: #ffffff;
  --k-atmo-2: #ffffff;
  --k-atmo-3: #ffffff;
}

/* ═══════════════════════════════════════════════════════════════════════════
   RU: 5. Базовые правила, общие для всех поверхностей.
   EN: 5. Base rules shared by every surface.
   ═══════════════════════════════════════════════════════════════════════════ */

/* RU: Сообщаем браузеру фактическую светлоту темы — от этого зависят цвета
       системных элементов: полос прокрутки, полей ввода, чекбоксов. Без этого
       на тёмной странице остаются белые скроллбары.
   EN: Tell the browser the theme's actual lightness — system chrome depends on it:
       scrollbars, form fields, checkboxes. Without it a dark page keeps white
       scrollbars. */
:root {
  color-scheme: light;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    color-scheme: dark;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
}
:root[data-theme="light"],
:root[data-theme="contrast"] {
  color-scheme: light;
}

/* RU: Видимый фокус на всём, что получает клавиатурный фокус. Правило намеренно
       глобальное: точечные `:focus-visible` в каждом файле забываются.
   EN: A visible focus ring on everything that takes keyboard focus. The rule is
       deliberately global: per-file `:focus-visible` declarations get forgotten. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--k-brand);
  outline-offset: 2px;
  border-radius: var(--k-r-1);
}

/* RU: Уважаем системную просьбу убрать движение. Проверка доступности в смоуке
       ищет это правило — без него анимации остаются у людей с вестибулярными
       нарушениями.
   EN: Honour the system request for reduced motion. The accessibility smoke check
       looks for this rule — without it animations stay on for people with
       vestibular disorders. */
@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;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   RU: 6. Скелетоны и пустые состояния. До этого между запросом и ответом экран
       оставался пустым, и это читалось как поломка, а не как ожидание.
   EN: 6. Skeletons and empty states. Until now the screen stayed blank between
       request and response, which read as a breakage rather than as waiting.
   ═══════════════════════════════════════════════════════════════════════════ */

/* RU: Полоса-заглушка. Ширину задаёт вызывающий код (inline width), высоту —
       модификаторы. Анимация — мягкое затухание, а не «бегущий блик»: блик
       на списке из десяти строк превращается в мигание.
   EN: A placeholder bar. The caller sets the width inline, modifiers set height.
       The animation is a soft pulse rather than a travelling shimmer: a shimmer
       across ten rows turns into flicker. */
.k-skeleton {
  display: block;
  background: var(--k-surface-2);
  border-radius: var(--k-r-1);
  animation: k-skeleton-pulse 1.4s ease-in-out infinite;
}
.k-skeleton--text {
  height: 12px;
  margin-bottom: var(--k-s-2);
}
.k-skeleton--title {
  height: 18px;
  margin-bottom: var(--k-s-3);
}
.k-skeleton--thumb {
  height: 48px;
  width: 48px;
  border-radius: var(--k-r-2);
}
.k-skeleton--card {
  height: 78px;
  margin-bottom: var(--k-s-2);
  border-radius: var(--k-r-2);
}

@keyframes k-skeleton-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.55;
  }
}

/* RU: Строка-скелетон под карточку результата: миниатюра слева, две строки текста.
       Форма повторяет настоящий результат, иначе при подмене список «прыгает».
   EN: A skeleton row shaped like a result card: thumbnail left, two text lines. The
       shape mirrors the real result, otherwise the list jumps when data arrives. */
.k-skeleton-row {
  display: grid;
  grid-template-columns: 48px 1fr;
  gap: var(--k-s-3);
  padding: var(--k-s-3);
  border: 1px solid var(--k-border-soft);
  border-radius: var(--k-r-2);
  background: var(--k-surface);
  margin-bottom: var(--k-s-2);
}

/* RU: Пустое состояние. Три части: что произошло, почему, что сделать.
       Третья часть обязательна — «ничего не найдено» без выхода это тупик.
   EN: An empty state. Three parts: what happened, why, what to do. The third is
       mandatory — "nothing found" with no way out is a dead end. */
.k-empty {
  display: grid;
  gap: var(--k-s-2);
  padding: var(--k-s-6) var(--k-s-4);
  text-align: center;
  color: var(--k-text-muted);
}
.k-empty-title {
  font-size: var(--k-fs-4);
  font-weight: 700;
  color: var(--k-text);
}
.k-empty-body {
  font-size: var(--k-fs-2);
  line-height: var(--k-lh-normal);
  max-width: 46ch;
  margin: 0 auto;
}
.k-empty-actions {
  display: flex;
  gap: var(--k-s-2);
  justify-content: center;
  flex-wrap: wrap;
  margin-top: var(--k-s-1);
}

/* RU: Состояние ошибки — тот же каркас, что у пустого, но с рамкой тревоги.
       Текст ошибки обязан говорить, что делать, а не отдавать код ответа.
   EN: The error state — the same frame as empty, with an alarm border. The error
       text must say what to do rather than surface a response code. */
.k-error {
  border: 1px solid var(--k-danger);
  background: var(--k-danger-soft);
  color: var(--k-text);
  border-radius: var(--k-r-2);
  padding: var(--k-s-3) var(--k-s-4);
  font-size: var(--k-fs-2);
  line-height: var(--k-lh-normal);
}

/* ═══════════════════════════════════════════════════════════════════════════
   RU: 7. Переключатель темы — общий для всех поверхностей.
   EN: 7. The theme switch — shared by every surface.
   ═══════════════════════════════════════════════════════════════════════════ */
.k-theme-switch {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--k-border);
  border-radius: var(--k-r-pill);
  background: var(--k-surface);
}
.k-theme-switch button {
  border: 0;
  background: transparent;
  color: var(--k-text-muted);
  font: 500 var(--k-fs-1) / 1 var(--k-font-ui);
  padding: 6px 10px;
  border-radius: var(--k-r-pill);
  cursor: pointer;
  min-height: 28px;
}
.k-theme-switch button[aria-pressed="true"] {
  background: var(--k-brand);
  color: var(--k-text-on-accent);
  font-weight: 700;
}

/* RU: Компактная кнопка-цикл для тесных мест (панель инструментов карты).
       Тач-цель доводится до минимума через min-width/min-height, а не паддингом:
       паддинг ломает сетку иконок.
   EN: A compact cycling button for tight places (the map toolbar). The tap target
       is raised to the minimum via min-width/min-height rather than padding, which
       would break the icon grid. */
.k-theme-cycle {
  min-width: var(--k-tap-min);
  min-height: var(--k-tap-min);
  display: grid;
  place-items: center;
  border: 1px solid var(--k-border);
  border-radius: var(--k-r-2);
  background: var(--k-surface);
  color: var(--k-text-muted);
  cursor: pointer;
}

/* RU: Полоса с переключателем оформления внизу страницы. Прижата к содержимому,
       а не зафиксирована в углу экрана: плавающий элемент поверх карточек мешает
       читать и перекрывает действия на узких экранах.
   EN: The appearance-switch bar at the bottom of the page. It sits with the content
       rather than floating in a corner: an element floating over cards gets in the
       way of reading and covers actions on narrow screens. */
.k-theme-bar {
  display: flex;
  align-items: center;
  gap: var(--k-s-3);
  flex-wrap: wrap;
  padding: var(--k-s-4) var(--k-s-5) var(--k-s-6);
  border-top: 1px solid var(--k-border-soft);
  margin-top: var(--k-s-6);
  color: var(--k-text-subtle);
}
.k-theme-bar-label {
  font: 500 var(--k-fs-1) / 1 var(--k-font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* ══════════════════════════════════════════════════════════════════════
   RU: Скрытый для глаз, но доступный скринридеру текст. Нужен ссылке «перейти к содержимому»
       и живым областям выдачи (#results-announce). Перенесено из слоя токенов ветки «оставшихся
       фаз»: сам слой не вливался (решение Р8), а эти два класса от темы не зависят.
   EN: Text hidden from the eye yet available to a screen reader. Needed by the "skip to content"
       link and the results live regions (#results-announce). Carried over from the
       remaining-phases branch's token layer: the layer itself was not merged (decision R8), while
       these two classes do not depend on the theme.
   ══════════════════════════════════════════════════════════════════════ */
/**
 * RU: Текст только для скринридера. Не `display:none` и не `visibility:hidden`
 *     — оба убирают узел из дерева доступности вместе с экраном. Приём с
 *     отсечкой в один пиксель оставляет узел озвучиваемым.
 * EN: Screen-reader-only text. Not `display:none` nor `visibility:hidden` —
 *     both remove the node from the accessibility tree along with the screen.
 *     The one-pixel clip keeps the node announceable.
 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/**
 * RU: Тот же приём, но узел проявляется при получении фокуса — для ссылки
 *     «перейти к содержимому», которую видит только клавиатурный пользователь.
 * EN: The same trick, but the node appears on focus — for a "skip to content"
 *     link that only a keyboard user ever sees.
 */
.sr-only-focusable:focus,
.sr-only-focusable:focus-visible {
  position: fixed;
  top: env(safe-area-inset-top, 0px);
  left: 0;
  z-index: 9999;
  width: auto;
  height: auto;
  margin: 0;
  padding: 0.6rem 1rem;
  overflow: visible;
  clip: auto;
  background: var(--k-surface);
  color: var(--k-text);
  border: 2px solid var(--k-focus);
}
