/* ─── Table Cell ──────────────────────────────────────────────────────────────
   Values: site/tokens.css (--ds-tcell-*)
   Primary consumers: site/preview/table-cell.html + src/components/TableCell/TableCell.tsx
   States: data-attributes only
   Figma: файл Components, страница ↳ Table Cell, 12 наборов; коллекция ❖ Table Cell
          (22 числовые переменные × моды SM/MD/LG). Спека — 03_Components/Table Cell/
   ─────────────────────────────────────────────────────────────────────────────

   Что компонент НЕ делает. Ячейка не знает ни про колонки, ни про сортировку, ни про
   виртуализацию, ни про роль таблицы — это пишет приложение (.design-sync/conventions.md).
   Ячейка владеет собой: высотой строки, инсетами, границами, типографикой и слотами.
   Перемещение стрелками, roving-tabindex и порядок обхода принадлежат таблице.

   Почему два уровня разметки. Корень — настоящий <th>/<td>, и он обязан оставаться
   table-cell, иначе рассыпается раскладка колонок. Но table-cell нельзя сделать
   flex-контейнером, а вся внутренняя раскладка макета — auto-layout. Поэтому падинги,
   минимальная высота и флекс-ряд живут на обёртке .ds-tcell-inner, а на корне остаются
   только фон, границы и типографика.

   ЧЕГО ЭТО РАЗДЕЛЕНИЕ НЕ ДАЁТ. Здесь стояло «так компонент работает и внутри <table>, и
   отдельным <div> в сторибуке» — неправда, замерено 2026-09-21. Верно только про САМ ФАЙЛ:
   селекторов по элементам таблицы в нём нет ни одного (из 99 селекторов — 0; слова
   table|tr|td|th встречаются только в комментариях), поэтому рисунок перенёсся бы на любой
   носитель. Но носителя другого нет: TableCell.tsx выдаёт Tag = header ? 'th' : 'td'
   безусловно, в витрине все 101 корень — 79 <td> и 22 <th>, и каждая история сторибука
   рисует ячейку внутри <table><tbody><tr>: Playground — декоратором, остальные — своим
   render через обёртку Table. Ни одного div-рендера ячейки нет ни в витрине, ни в сторибуке.

   ЧТО ЭТО РАЗДЕЛЕНИЕ ДАЁТ — замерено в тот же день. Неизменённый <td>/<th> в CSS-сетке
   (контейнер role="table" + grid-template-columns, строки role="row" с display:contents,
   между строкой и ячейкой обёртка display:contents — так её ставит рантайм внешнего
   потребителя) браузер переводит из table-cell в block, и рисунок совпадает с настоящей
   таблицей: 644 сверки из 644 — на тему 10 строк (шапка и 9 состояний) × 2 ячейки ×
   8 свойств, включая все четыре псевдоэлемента линий и кольцо фокуса, плюс высота всей
   таблицы = 161, × 4 темы. Без сетки — 248 расхождений:
   ячейки слипаются в строку, так что сетка обязательна. Инсеты и min-height на
   .ds-tcell-inner, а не на корне, — именно то, что делает ячейку годной и для такого
   носителя. Реестр: TABLECELL-UNUSABLE-IN-CONSUMER-TEMPLATE.

   Почему min-height, а не height. В макете корень набора — HORIZONTAL auto-layout с
   FILL/HUG и привязкой minHeight к Column/Min height. При содержимом в две строки
   (16 + 16) и паддинге 6+6 выходит 44, а рендерится 48 — высоту держит именно минимум,
   контент центрируется. Формула: content_area = min-height − border − padding_y×2;
   при MD это 48 − 1 − 12 = 35 против содержимого 32 — минимум выигрывает, сходится с
   замеренными 48. При SM и LG двухстрочное содержимое перерастает минимум и ячейка
   тянется (SM: 32+8+1 = 41 при минимуме 32) — это ровно то же, что делает HUG в макете,
   а не дефект: фиксированная height обрезала бы вторую строку. */

/* ── Root ───────────────────────────────────────────────────────────────────── */

.ds-tcell {
  position:       relative;      /* якорь для боковых линий и кольца фокуса */
  box-sizing:     border-box;
  padding:        0;             /* инсеты живут на .ds-tcell-inner */
  vertical-align: middle;
  /* Заливки в покое у ячейки НЕТ — сквозь неё виден контейнер таблицы, а он белый или
     чёрный (Static/Background-Blue/0 - Base: #ffffff / #131316 / #020304 / #ffffff).
     В макете вариант .BG Cell / Default привязан к Surface/Neutral-Blue/High Contrast,
     и это, по слову владельца 2026-09-07, «просто тот же цвет, что у поверхности под
     ней» — то есть способ Figma показать «как под ней», а не собственная заливка.
     Перенести этот бинд в CSS буквально было ошибкой и стоило ложной находки: HC в
     тёмных модах давал тогда #222629 и #1c2023 — ровно цвет зебры, и чередование «пропадало».
     Пропадало оно против МОЕЙ ЖЕ заливки, которой в продукте нет. */
  color:          var(--ds-text-primary);
  font-family:    inherit;
  font-weight:    400;
  border:         none;
  transition:     background-color var(--ds-dur-1) var(--ds-ease);
}

.ds-tcell-inner {
  position:    relative;         /* якорь боковых линий — см. секцию «Границы» */
  display:     flex;
  align-items: center;           /* до 3 строк разницы — по центру (дефолт компонента) */
  box-sizing:  border-box;
  width:       100%;
}

/* ── Размеры ────────────────────────────────────────────────────────────────── */

.ds-tcell[data-size='sm'] { min-width: var(--ds-tcell-sm-minw); font-size: var(--ds-tcell-sm-fs); line-height: var(--ds-tcell-sm-lh); }
.ds-tcell[data-size='md'] { min-width: var(--ds-tcell-md-minw); font-size: var(--ds-tcell-md-fs); line-height: var(--ds-tcell-md-lh); }
.ds-tcell[data-size='lg'] { min-width: var(--ds-tcell-lg-minw); font-size: var(--ds-tcell-lg-fs); line-height: var(--ds-tcell-lg-lh); }

.ds-tcell[data-size='sm'] > .ds-tcell-inner {
  min-height: var(--ds-tcell-sm-h);
  padding:    var(--ds-tcell-sm-py) var(--ds-tcell-sm-px);
  gap:        var(--ds-tcell-sm-gap);
}
.ds-tcell[data-size='md'] > .ds-tcell-inner {
  min-height: var(--ds-tcell-md-h);
  padding:    var(--ds-tcell-md-py) var(--ds-tcell-md-px);
  gap:        var(--ds-tcell-md-gap);
}
.ds-tcell[data-size='lg'] > .ds-tcell-inner {
  min-height: var(--ds-tcell-lg-h);
  padding:    var(--ds-tcell-lg-py) var(--ds-tcell-lg-px);
  gap:        var(--ds-tcell-lg-gap);
}

/* Шапка. Три отличия от содержимого, и все три замерены: свой padding-y (4 на всех
   размерах против 4/6/16), своя типографика (Label 12/12/14 Medium против Body 14/14/16
   Regular) и свой цвет (Tertiary против Primary). Плюс своя разрядка до слота иконок. */
.ds-tcell[data-header] { color: var(--ds-text-tertiary); font-weight: 500; }

.ds-tcell[data-header][data-size='sm'] { font-size: var(--ds-tcell-hdr-sm-fs); line-height: var(--ds-tcell-hdr-sm-lh); }
.ds-tcell[data-header][data-size='md'] { font-size: var(--ds-tcell-hdr-md-fs); line-height: var(--ds-tcell-hdr-md-lh); }
.ds-tcell[data-header][data-size='lg'] { font-size: var(--ds-tcell-hdr-lg-fs); line-height: var(--ds-tcell-hdr-lg-lh); }

.ds-tcell[data-header][data-size='sm'] > .ds-tcell-inner { padding-block: var(--ds-tcell-hdr-py); gap: var(--ds-tcell-hdr-sm-icon-gap); }
.ds-tcell[data-header][data-size='md'] > .ds-tcell-inner { padding-block: var(--ds-tcell-hdr-py); gap: var(--ds-tcell-hdr-md-icon-gap); }
.ds-tcell[data-header][data-size='lg'] > .ds-tcell-inner { padding-block: var(--ds-tcell-hdr-py); gap: var(--ds-tcell-hdr-lg-icon-gap); }

/* ── Содержимое ─────────────────────────────────────────────────────────────── */

.ds-tcell-content {
  display:         flex;
  flex-direction:  column;
  justify-content: center;
  flex:            1 1 auto;
  min-width:       0;            /* иначе длинный текст не даст ячейке сжаться */
}
.ds-tcell[data-size='sm'] .ds-tcell-content { gap: var(--ds-tcell-sm-core-gap); }
.ds-tcell[data-size='md'] .ds-tcell-content { gap: var(--ds-tcell-md-core-gap); }
.ds-tcell[data-size='lg'] .ds-tcell-content { gap: var(--ds-tcell-lg-core-gap); }

.ds-tcell-core,
.ds-tcell-caption-row {
  display:     flex;
  align-items: center;
  min-width:   0;
}
.ds-tcell[data-size='sm'] :is(.ds-tcell-core, .ds-tcell-caption-row) { gap: var(--ds-tcell-sm-gap); }
.ds-tcell[data-size='md'] :is(.ds-tcell-core, .ds-tcell-caption-row) { gap: var(--ds-tcell-md-gap); }
.ds-tcell[data-size='lg'] :is(.ds-tcell-core, .ds-tcell-caption-row) { gap: var(--ds-tcell-lg-gap); }

/* Обрезка в троеточие — повод показать Tooltip: подсказка появляется, когда текст
   действительно не поместился. Обнаруживает это приложение замером
   (scrollWidth > clientWidth), а не длиной строки. */
.ds-tcell-text,
.ds-tcell-value,
.ds-tcell-caption {
  min-width:     0;
  overflow:      hidden;
  text-overflow: ellipsis;
  white-space:   nowrap;
}

/* Подпись (анатомия 9, 13, 14) — текстовый стиль Caption/Caption, 12/16, и от размера ячейки
   не зависит: в макете она биндит ✦ Typography/Caption, а не ❖ Table Cell Cell/Text/*
   (Plugin API 2026-09-24, узлы 2343:11875 · 2345:16158 · 2357:16465). До этой правки
   подпись наследовала кегль ячейки и рисовалась 14/16 в SM и MD и 16/20 в LG. */
.ds-tcell-caption,
.ds-tcell-caption-row {
  color:       var(--ds-text-tertiary);
  font-size:   var(--ds-field-caption-fs);
  line-height: var(--ds-field-caption-lh);
}

/* Суффикс единиц измерения — приглушён сильнее подписи: в макете он биндит
   Text/Disable/Neutral (Blue), а не Static/Text/Tertiary. Это не «выключено»,
   а тише основного числа. */
.ds-tcell-suffix { color: var(--ds-disabled-text); flex: none; }

/* Числа: разряды строго друг под другом. Разделитель разрядов ставит приложение
   через Intl.NumberFormat локали интерфейса — своя сборка строки запрещена. */
.ds-tcell[data-variant='amount'] :is(.ds-tcell-value, .ds-tcell-caption),
.ds-tcell[data-variant='edit-amount'] .ds-tcell-value {
  font-variant-numeric:  tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* ── Выравнивание ───────────────────────────────────────────────────────────── */

/* Ось Orientation макета — это ровно textAlignHorizontal лейбла (LEFT против RIGHT),
   замерено: у вариантов «→ Left» и «Right ←» больше не отличается ничего — ни позиции
   детей, ни ширины, ни сторона иконки. Поэтому здесь только выравнивание текста. */
.ds-tcell[data-align='start']  { text-align: left; }
.ds-tcell[data-align='end']    { text-align: right; }
.ds-tcell[data-align='center'] { text-align: center; }

.ds-tcell[data-align='start']  > .ds-tcell-inner { justify-content: flex-start; }
.ds-tcell[data-align='end']    > .ds-tcell-inner { justify-content: flex-end; }
.ds-tcell[data-align='center'] > .ds-tcell-inner { justify-content: center; }

.ds-tcell[data-align='end']    :is(.ds-tcell-content, .ds-tcell-core, .ds-tcell-caption-row) { align-items: flex-end; }
.ds-tcell[data-align='center'] :is(.ds-tcell-content, .ds-tcell-core, .ds-tcell-caption-row) { align-items: center; }
.ds-tcell[data-align='end']    :is(.ds-tcell-core, .ds-tcell-caption-row) { justify-content: flex-end; align-items: center; }

/* Больше 3 строк разницы между ячейками — по верхнему краю, чтобы вся строка
   начиналась с одного уровня. Настраивается вручную, дефолт остаётся по центру. */
.ds-tcell[data-valign='top']    > .ds-tcell-inner { align-items: flex-start; }
.ds-tcell[data-valign='bottom'] > .ds-tcell-inner { align-items: flex-end; }

/* Многострочный текст: обрезка в троеточие снимается, иначе перенос не наступит. */
.ds-tcell[data-wrap] :is(.ds-tcell-text, .ds-tcell-caption) {
  overflow:      visible;
  text-overflow: clip;
  white-space:   normal;
}

/* ── Иконки ─────────────────────────────────────────────────────────────────── */

/* Цвет слота — Static/Icon/Neutral-Blue/Tertiary (.Cell / Icons и .Header / Icons в мастерах
   1910:598 · 2345:16134 · 1909:2885). До 2026-09-23 правила цвета здесь не было, и иконка
   действия брала currentColor ячейки — Text/Primary: поэлементная сверка нашла это в 32 ячейках
   во всех четырёх темах. Кнопку в слоте это не перекрашивает — у неё свой цвет. */
.ds-tcell-actions {
  display:     inline-flex;
  align-items: center;
  flex:        none;
  color:       var(--ds-icon-tertiary);
}
.ds-tcell[data-size='sm'] .ds-tcell-actions { gap: var(--ds-tcell-sm-gap); }
.ds-tcell[data-size='md'] .ds-tcell-actions { gap: var(--ds-tcell-md-gap); }
.ds-tcell[data-size='lg'] .ds-tcell-actions { gap: var(--ds-tcell-lg-gap); }

.ds-tcell-icon {
  display:     inline-flex;
  align-items: center;
  flex:        none;
  line-height: 0;
  color:       var(--ds-icon-tertiary);
}

/* Слот держит размер и обводку сам: иконка, приносящая свои width/height/stroke-width,
   отменяет лестницу — этот класс ошибок ловили на LinkText. */
.ds-tcell[data-size='sm'] :is(.ds-tcell-icon, .ds-tcell-actions) svg {
  width: var(--ds-tcell-sm-icon); height: var(--ds-tcell-sm-icon); stroke-width: var(--ds-tcell-sm-icon-stroke);
}
.ds-tcell[data-size='md'] :is(.ds-tcell-icon, .ds-tcell-actions) svg {
  width: var(--ds-tcell-md-icon); height: var(--ds-tcell-md-icon); stroke-width: var(--ds-tcell-md-icon-stroke);
}
.ds-tcell[data-size='lg'] :is(.ds-tcell-icon, .ds-tcell-actions) svg {
  width: var(--ds-tcell-lg-icon); height: var(--ds-tcell-lg-icon); stroke-width: var(--ds-tcell-lg-icon-stroke);
}

/* Иконка шапки — своя лестница, короче: 16/16/20 против 20/20/24 у содержимого. */
.ds-tcell[data-header][data-size='sm'] :is(.ds-tcell-icon, .ds-tcell-actions) svg {
  width: var(--ds-tcell-hdr-sm-icon); height: var(--ds-tcell-hdr-sm-icon); stroke-width: var(--ds-tcell-hdr-sm-icon-stroke);
}
.ds-tcell[data-header][data-size='md'] :is(.ds-tcell-icon, .ds-tcell-actions) svg {
  width: var(--ds-tcell-hdr-md-icon); height: var(--ds-tcell-hdr-md-icon); stroke-width: var(--ds-tcell-hdr-md-icon-stroke);
}
.ds-tcell[data-header][data-size='lg'] :is(.ds-tcell-icon, .ds-tcell-actions) svg {
  width: var(--ds-tcell-hdr-lg-icon); height: var(--ds-tcell-hdr-lg-icon); stroke-width: var(--ds-tcell-hdr-lg-icon-stroke);
}

/* Иконка помощи В СТРОКЕ ТЕКСТА — и в шапке, и в содержимом идёт по лестнице иконки
   ШАПКИ (Column/*), а не слота действий. Привязка снята с макета 2026-09-24: узлы
   circle-question-mark в Header / Label (2343:11771) и в Cell / Text (2343:11804) держат
   ❖ Table Cell Column/Icon/Width — 16 на SM и MD, 20 на LG. Здесь стояло «16 везде, своей
   переменной нет»: замер был на MD, где число совпадает, а на LG код рисовал 16 при 20
   (строка реестра TABLECELL-HELP-ICON-LG-FIXED-16). Толщина — та же ступень шапки;
   обводку вектора справки Plugin API не отдал (у экземпляра пустой список детей), поэтому
   она взята по соседству с иконкой шапки, а не снята с узла. С лестницей слота действий
   эти правила не конкурируют: справка лежит в .ds-tcell-core, а лестница бьёт по
   :is(.ds-tcell-icon, .ds-tcell-actions) svg — селекторы не пересекаются, и порядок правил
   ни на что не влияет. */
.ds-tcell-help {
  display:     inline-flex;
  align-items: center;
  flex:        none;
  line-height: 0;
  background:  none;
  border:      none;
  padding:     0;
  cursor:      help;
  color:       var(--ds-icon-tertiary);
}
.ds-tcell .ds-tcell-help svg {
  width:        var(--ds-tcell-hdr-md-icon);
  height:       var(--ds-tcell-hdr-md-icon);
  stroke-width: var(--ds-tcell-hdr-md-icon-stroke);
}
.ds-tcell[data-size='sm'] .ds-tcell-help svg {
  width: var(--ds-tcell-hdr-sm-icon); height: var(--ds-tcell-hdr-sm-icon); stroke-width: var(--ds-tcell-hdr-sm-icon-stroke);
}
.ds-tcell[data-size='lg'] .ds-tcell-help svg {
  width: var(--ds-tcell-hdr-lg-icon); height: var(--ds-tcell-hdr-lg-icon); stroke-width: var(--ds-tcell-hdr-lg-icon-stroke);
}

/* ── Границы ────────────────────────────────────────────────────────────────── */

/* НИ ОДНА линия не рисуется настоящим border — все четыре псевдоэлементы. Причина
   замерена, а не выведена из вкуса: border-bottom на <td> добавляет свой пиксель К
   высоте, и MD-ячейка вместо фигмовских 48 отрисовалась 49, а при border-collapse на
   таблице ещё и распалась на 48.5 — половина пикселя ушла соседней строке. В макете
   Divider/Horizontal лежит ВНУТРИ бокса 48 (y=47 при высоте 48): линия занимает
   последний пиксель ячейки, а не прибавляет его. На таблице в 200 строк разница
   набежала бы в 200 лишних пикселей. Псевдоэлементы вне потока — высоты не добавляют.

   Боковые линии живут на .ds-tcell-inner: у корня оба псевдоэлемента заняты
   горизонтальными, а inner совпадает с ячейкой по боксу, так что геометрия та же.

   Высота боковой линии фиксирована (28) — в макете это инстанс Divider/Vertical без привязки к переменной
   и без FILL-сайзинга, то есть при 48 инсет выходит 10, а при 32 и 72 станет 2 и 22.
   Перенесено как есть; вопрос дизайнеру — реестр, TABLE-CELL-SIDE-DIVIDER-FIXED-HEIGHT. */
.ds-tcell[data-border-bottom]::after,
.ds-tcell[data-total]::before {
  content:        '';
  position:       absolute;
  left:           0;
  right:          0;
  height:         var(--ds-tcell-border-w);
  background:     var(--ds-stroke-low);
  pointer-events: none;
}
.ds-tcell[data-border-bottom]::after { bottom: 0; }

.ds-tcell[data-header][data-border-bottom]::after { background: var(--ds-stroke-high); }
/* «2nd level» — второй уровень заголовков: та же линия, но тише. Замерено:
   High (#222629) → Low (#C0C6D0). Единственное, что меняет этот переключатель. */
.ds-tcell[data-header][data-second-level][data-border-bottom]::after { background: var(--ds-stroke-low); }

.ds-tcell[data-border-left]  > .ds-tcell-inner::before,
.ds-tcell[data-border-right] > .ds-tcell-inner::after {
  content:        '';
  position:       absolute;
  top:            50%;
  transform:      translateY(-50%);
  width:          var(--ds-tcell-border-w);
  height:         var(--ds-tcell-side-h);
  background:     var(--ds-stroke-low);
  pointer-events: none;
}
.ds-tcell[data-border-left]  > .ds-tcell-inner::before { left:  0; }
.ds-tcell[data-border-right] > .ds-tcell-inner::after  { right: 0; }

/* Итоговая строка. Ось макета названа Total, но значения у неё перевёрнуты: Total=Off
   даёт ИТОГОВЫЙ вид (линия сверху, снизу пусто), Total=On — обычный (линия снизу).
   Проверено по absoluteBoundingBox: линия при Off стоит в y=0, при On — в y=47 при
   высоте 48. В коде пропс называется по смыслу: total=true → вид итога. Вопрос
   дизайнеру — реестр, TABLE-CELL-TOTAL-AXIS-INVERTED. */
.ds-tcell[data-total]::before { top: 0; background: var(--ds-stroke-high); }

/* ── Заливка строки ─────────────────────────────────────────────────────────── */

/* Шесть заливок вложенного набора .BG Cell, все шесть замерены на узлах.
   Зебра — это цвет СТРАНИЦЫ, проступающий сквозь карточку: страница --ds-page-bg
   (= nb-low), контейнер таблицы nb-base, полосатая строка возвращает шаг страницы.
   Поэтому она различима во всех четырёх темах, а не в двух:
   nb-low #f6f8fc · #222629 · #1c2023 · #dee4ea против nb-base #ffffff · #131316 ·
   #020304 · #ffffff. Контейнер обязан быть Base — на HC в значениях до 2026-09-21
   (#ffffff/#222629/#1c2023/#ffffff) зебра совпала бы с ним в двух тёмных темах; с 2026-09-21
   HC по значениям равна Base, но это другая переменная макета, и контейнер остаётся на Base. */
.ds-tcell[data-zebra] { background: var(--ds-surface-nb-low); }

/* Наведение и нажатие. Ghost/Hover — ровно 0.20 во ВСЕХ четырёх модах, поэтому
   dark-надбавки до nba-40 здесь нет и быть не должно: лесенка nba несёт α-сдвиг на
   ступенях 30/40/50, а на 20 нет. Тот же разбор уже записан у --ds-cal-day-bg-hover;
   надбавка на этой ступени была бы локальным решением поверх лесенки, а не P039.
   Заливка полупрозрачная и ложится поверх зебры — так же, как в макете. */
.ds-tcell[data-hovered]:not([data-selected]) { background: var(--ds-nba-20); }
.ds-tcell[data-pressed]:not([data-selected]) { background: var(--ds-nba-ghost-pressed); }

/* Выделенная строка. Адрес заливки — --ds-info-outline-hover / -active: у ролей
   Fill/Info/Ghost/* и Fill/Info/Outline/* в макете ТОЖДЕСТВЕННАЯ алиас-цепочка во всех
   четырёх модах (Main/Info/Light/info-200 → Light/Blueberry/200 и так далее), разными
   их делает обводка, а она живёт в своём токене. Дубля значения не заводим. */
.ds-tcell[data-selected][data-hovered] { background: var(--ds-info-outline-hover); }
.ds-tcell[data-selected][data-pressed] { background: var(--ds-info-outline-active); }

/* ── Фокус и ошибка ─────────────────────────────────────────────────────────── */

/* Кольцо ВНУТРЕННЕЕ, а не внешнее: в макете слой Focus — вектор во весь бокс ячейки
   с обводкой 2. Общий --ds-focus-ring рисует кольцо снаружи с зазором, и у соседних
   ячеек таблицы оно налезало бы друг на друга. Тень, а не смена border-width:
   толщина рамки неизменна, иначе content area сжимается и содержимое прыгает. */
/* Третья форма — :has() — не украшение, а единственный способ показать фокус у
   редактируемой ячейки. Табуляция приводит фокус во ВЛОЖЕННОЕ поле, а не в <td>:
   ячейка тогда не матчит :focus-visible, а у самого поля outline снят. Без этой
   строки остановка табуляции немая — ничего не меняется на экране вообще. Поймано
   не мной и не локальными воротами: audit-focus-coverage в статическом режиме
   проверяет ИМЯ атрибута в селекторе, а не то, что правило рисует, и сам об этом
   пишет границей оси. Нашёл боевой прогон в CI: «table-cell.html остановок 33 ·
   немых 1 · INPUT.ds-tcell-input». Макет требует того же — слой Focus у наборов
   Cell edit / Text и Cell edit / Amount рисуется на боксе ЯЧЕЙКИ, не на поле. */
.ds-tcell:focus-visible,
.ds-tcell[data-focus-visible],
.ds-tcell:has(.ds-tcell-input:focus-visible) {
  outline:    none;
  box-shadow: inset 0 0 0 var(--ds-focus-width) var(--ds-focus-ring-color);
}

/* Ошибка редактируемой ячейки — та же рамка, но негативная. Стоит ПОСЛЕ фокусной,
   чтобы ошибка была видна и в фокусе: в макете слой Focus у Error=On просто меняет
   цвет обводки, а не появляется поверх. */
/* Цветов ДВА, и это не косметика. Здесь стоял один --ds-neg-border-focus и на покой,
   и на фокус — тогда ячейка с ошибкой при табуляции не меняла ничего вообще, и остановка
   была немой. Симптом нашёл боевой прогон audit-focus-coverage в CI, причина оказалась
   в роли токена: ADR-011 держит REST на *-border-default, а validation-FOCUS на
   *-border-focus (Figma Border/<Intent>/Pressed). Ровно так это и исполнено у полей —
   input.css:529 покой, :533 фокус. Порядок правил сохранён: ошибка стоит ПОСЛЕ фокусной,
   поэтому в фокусе видна негативная рамка, а не нейтральное кольцо — в макете слой Focus
   при Error=On меняет цвет обводки, а не появляется поверх неё. */
.ds-tcell[data-negative] {
  box-shadow: inset 0 0 0 var(--ds-focus-width) var(--ds-neg-border-default);
}
.ds-tcell[data-negative]:focus-visible,
.ds-tcell[data-negative][data-focus-visible],
.ds-tcell[data-negative]:has(.ds-tcell-input:focus-visible) {
  box-shadow: inset 0 0 0 var(--ds-focus-width) var(--ds-neg-border-focus);
}

/* ── Редактируемая ячейка ───────────────────────────────────────────────────── */

/* Поле правки одного значения в потоке таблицы. Ни лейбла, ни подписи, ни валидации
   с текстом — для этого есть Input и SelectField (спека, «When NOT to use»). */
.ds-tcell-input {
  width:       100%;
  min-width:   0;
  border:      none;
  outline:     none;
  background:  none;
  padding:     0;
  margin:      0;
  color:       inherit;
  font:        inherit;
  text-align:  inherit;
}
.ds-tcell[data-variant='edit-amount'] .ds-tcell-input {
  font-variant-numeric:  tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* ── Swap slot ──────────────────────────────────────────────────────────────── */

/* Произвольный контент от приложения. Пунктирная рамка «Swap Container» из макета
   сюда не переносится — это макетный плейсхолдер, на его месте стоит настоящий контент. */
.ds-tcell-swap {
  display:     flex;
  align-items: center;
  flex:        1 1 auto;
  min-width:   0;
}

/* ── Motion ─────────────────────────────────────────────────────────────────── */

/* Анимируется только заливка. Ни transform, ни размеры, ни box-shadow: в таблице
   на 200+ строк любое из них вызывает пересчёт раскладки на каждой строке. */
@media (prefers-reduced-motion: reduce) {
  .ds-tcell { transition: none; }
}
