Единый источник правды о размерах и стиле текста в интерфейсе. Кегль в UI не выбирается «на глаз в каждом экране» — он выбирается по ЯРУСУ, к которому принадлежит элемент. Здесь — шкала, ярусы, правила и примеры.

Связано: Journal - The World Replicates, The View Does Not, токены — Assets/_Project/UI/Theme/tokens.primitives.uss, применение — components.uss.

Правило номер один: font-size задаётся ТОЛЬКО токеном --gm-font-* и только в USS. Инлайн-размеры в C# и голые пиксели в классах запрещены — иначе «единый источник» превращается в тридцать источников, и ни один экран не согласуется с соседним.


1. Шкала

Все значения — под нативную канву 1920×1080 (PanelSettings.scale = 1, ScaleWithScreenSize).

ТокенpxГолосГде стоит по факту
--gm-font-xxs13Служебная подпись: то, что читают глазами по диагонали.gm-stat__label, .gm-arcana-card__title, .gm-chip--sm, .gm-tooltip__line-label
--gm-font-xs17Плотный рабочий текст: значения, нарратив, поля ввода.gm-stat__value, .gm-loadout__narrative-text, .gm-loadout__search, .gm-tooltip__desc
--gm-font-sm22Заголовок блока внутри плотного экрана; вторичные подписи.gm-loadout__section-label, .gm-card__name, .gm-tooltip__title, .gm-text-muted
--gm-font-body26Основной текст полноэкранного/диалогового слоя.gm-button, .gm-tab, .gm-event-body, .gm-slider-row__label
--gm-font-title34Заголовок панели-диалога.gm-panel__title, .gm-shop__gold
--gm-font-lg48Одна вещь на экран: титул игры, глиф, тост.gm-mainmenu__title, .gm-titlecard__title, .gm-chest__glyph

2. Три яруса — как выбрать кегль

Кегль определяется НЕ важностью текста, а плотностью экрана, на котором он живёт.

ЯрусЧто этоЗаголовокТекстПодпись
Диалоговыйpause, настройки, ивент, награда, главное меню — окно на пустом фонеtitle (34)body (26)sm (22)
Рабочий (плотный)инвентарь, магазин, лоадаут — много блоков одновременноsm (22)xs (17)xxs (13)
Вторичныйтултип, тост, служебный оверлей — комментарий к тому, что уже на экранеsm (22)xs (17)xxs (13)

Вторичный слой никогда не крупнее того, что объясняет. Это самая частая ошибка: подсказка, набранная кеглем диалога, читается как отдельный экран и выглядит громоздкой, даже если текста в ней две строки.

Пример-урок (2026-07-26). Тултип был набран body (26) заголовок и sm (22) текст — крупнее панели деталей инвентаря, к которой он относился (xxs/xs), при max-width: 220px. Итог: узкая колонка крупных букв, фраза рвётся на шесть строк, окно занимает четверть экрана. Починка — не «уменьшить окно», а вернуть его на свой ярус: sm/xs/xxs плюс ширина 220 → 320. Высокое узкое окно выглядит массивнее широкого и низкого при том же объёме текста.

3. Ширина строки

Комфортная строка — 45–75 знаков. При 17 px это примерно 300–380 px, поэтому:

Классmax-widthПочему
.gm-tooltip320текстовая подсказка в 2–3 строки
.gm-tooltip--wide380разбор со стат-строками: подпись и значение в одну строку

Не сужать ради «компактности»: сужение не уменьшает окно, оно делает его выше.

4. Гарнитуры

РольШрифтГде
Тело интерфейсаFira Sans (TextCore SDF)всё, кроме перечисленного ниже
Заголовки, нарративCormorant Garamond Medium.gm-panel__title, .gm-loadout__narrative-text (курсив)

Гарнитура прибивается в components.uss (секция «ЯРУС ШРИФТОВ»), а не в UXML: тема Unity вешает шрифт прямо на .unity-text-element, и прямое правило обязано бить унаследованное.

4.1 Шрифтовые ассеты — только Static-атлас

Шрифты в Assets/_Project/UI/Fonts/*SDF*.assetвсегда Static atlas population с заранее запечённым набором EN+RU. Dynamic запрещён.

Почему. Dynamic дорисовывает глифы прямо в редакторе — при наборе текста, при входе в Play — и пересохраняет весь .asset. Файл это гигантский бинароподобный блоб (FiraSans-Regular раздувался до 26 МБ), и git складывает новую копию при каждой регенерации: сотни тысяч строк diff и мерж-конфликты на общей ветке. Static замораживает атлас — ноль регенераций, фиксированный размер, детерминизм между машинами, быстрее рантайм.

Набор символов (213 глифов): ASCII 0x20–0x7E + кириллица 0x0400–0x045F + экстра 0x00A0 00AB 00BB 00A9 00AE 00B0 00B7 00D7 2013 2014 2018 2019 201C 201D 2022 2026 2116 20BD 20AC 2212 2192 2190. Появилась локаль с новыми глифами (укр. ҐґІі, дополнительная пунктуация) — расширяем набор и делаем ре-бейк: один осознанный коммит вместо пассивного churn.

Рецепт бейка (через execute_code, на каждый шрифт):

  1. SerializedObjectm_AtlasWidth/m_AtlasHeight = 2048, m_IsMultiAtlasTexturesEnabled = false, Apply().
  2. atlasPopulationMode = Dynamic, затем ClearFontAssetData(false).
  3. TryAddCharacters(charset, out missing) — проверить, что missing.Length == 0.
  4. atlasPopulationMode = Static.
  5. SetDirty на сам ассет и на текстуры атласа, SaveAssets().

Point size / padding / render mode (90 / 9 / SDFAA) не трогать — это визуал, его принимает Макс.

Готчи.

  • Это UnityEngine.TextCore.Text.FontAsset (UI Toolkit / TextCore), не TMPro.TMP_FontAsset: грузить LoadAssetAtPath<UnityEngine.TextCore.Text.FontAsset>(...), TMP-тип вернёт null. Enum тоже свой — UnityEngine.TextCore.Text.AtlasPopulationMode.
  • В .gitattributes стоит *SDF*.asset -diff -merge — но это страховочная сеть, а не решение. Решение — Static.
  • Старые блобы в истории убираются только её переписыванием (BFG / filter-repo). Не затеваем: цена координации выше выгоды, цель — остановить рост, а не отыграть прошлое.
  • Настоящий динамический текст (будущие имена игроков из Steam, чат) — отдельный контейнерный fallback-шрифт. Решать, когда дойдёт; в churn-путь не пускать.

5. Цвет текста — тоже часть стиля

ТокенСмысл
--gm-color-textосновной текст, значения
--gm-color-text-mutedподписи, описания, всё вторичное
--gm-color-disabled-textвыключенный элемент (гасим ЦВЕТОМ, не прозрачностью — сквозь неё виден фон)
--gm-color-accentакцент: теги, выделенное состояние
--gm-color-positive / --gm-color-negativeприрост / падение величины

6. Единый каркас подсказки

У ЛЮБОЙ подсказки — мементо, характеристики, пояснения к пустому слоту — есть шапка: заголовок слева, мета справа, под ними тонкий разделитель. Дальше идёт тело: теги, описание, строки «подпись — значение», определения терминов.

Правило держится API, а не дисциплиной: TooltipRequest.Plain(title, text) требует заголовок и без него просто не создаёт запрос. Подсказка без заголовка читается как обрывок чужого текста — игрок не понимает, к чему она относится, стоит увести взгляд от курсора.

Внизу окна живёт полоса ожидания (3 px) — у любой подсказки, без исключений. Она занимает своё место в раскладке, а не лежит поверх текста: иначе последняя строка читалась бы сквозь неё.

Ещё не сделано (ждёт контента): цвет, полужирность и подсветка внутри текста. UI Toolkit поддерживает в рантайме <b>, <i>, <color>, <mark>, <size>, <sprite> — но rich text не читает USS-переменные, поэтому цвета придётся отдавать из одного места как hex, иначе палитра разъедется по текстам.

7. Термины в тексте

Ключевое слово глоссария пишется в квадратных скобках и с заглавной буквы: [Скрытность], [Горение], [Физический урон]. Разметку расставляет KeywordMarkup из авторской записи [kw:stealth:acc] — руками rich text в текстах не пишем.

Почему скобки, а не подчёркивание: скобки читаются как термин везде — в игре, в таблице локализации, в логе, в поиске по строкам. Подчёркивание жило только на экране и терялось всюду, где rich text не рисуется. (Решение Макса, 2026-07-26.)

8. Как менять

  1. Меняется вся игра → правь значение токена в tokens.primitives.uss.
  2. Меняется один компонент → правь его класс в components.uss, но выбирай токен из шкалы, а не пиши пиксели.
  3. Нужен новый кегль → сначала докажи, что шести не хватает. Седьмой размер в шкале — это седьмой способ выглядеть по-разному на соседних экранах.