/* ============================================================ * KT AI Components v2.1 * Требует kt-ai-tokens.css. Только semantic/component токены, * никаких сырых hex. Имена классов v1 сохранены. * Слой @layer kt-components: продуктовый CSS объявляйте в слое * выше (@layer product) или вне слоёв – он выиграет каскад. * Канон: docs/DESIGN.md · Галерея: showcase.html * ============================================================ */ @layer kt-components { /* border-box для ВСЕХ компонентов ДС, где бы они ни жили. Компоненты вроде .kt-ai-input объявлены как width:100% + padding + border: в content-box они вылезают из контейнера (замерено: 326px в контейнере 300px). Раньше сброс жил только в HTML app-shell, поэтому баг проявлялся ровно там, где ДС вендорят в чужой проект — в основном сценарии передачи разработчикам. Селектор бьёт только по .kt-ai-*, чтобы не трогать вёрстку хост-приложения. */ [class*="kt-ai-"], [class*="kt-ai-"]::before, [class*="kt-ai-"]::after { box-sizing: border-box; } /* Сброс UA-хрома для интерактивных компонентов ДС. Браузер не наследует font-family для button/input/select/textarea и рисует кнопкам свою серую заливку. В HTML app-shell это гасил глобальный сброс, поэтому при вендоринге ДС в чужой проект компоненты приезжали сломанными: .kt-ai-tab — с серым UA-фоном, .kt-ai-filter-chip/.seg/.page-btn — в Arial. :where() даёт НУЛЕВУЮ специфичность — любое правило компонента ниже победит. */ :where(button[class*="kt-ai-"], input[class*="kt-ai-"], select[class*="kt-ai-"], textarea[class*="kt-ai-"]) { font-family: var(--kt-ai-font-sans); font-size: var(--kt-ai-text-sm); color: inherit; } :where(button[class*="kt-ai-"]) { -webkit-appearance: none; appearance: none; background: none; border: none; cursor: pointer; } /* ---------- База ---------- */ .kt-ai-app, .kt-ai-shell { background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); font-size: var(--kt-ai-text-sm); line-height: var(--kt-ai-leading-normal); -webkit-font-smoothing: antialiased; } .kt-ai-shell { min-height: 100dvh; display: grid; grid-template-columns: var(--kt-ai-sidebar-width) minmax(0, 1fr); } .kt-ai-sidebar { background: var(--kt-ai-bg-soft); border-right: 1px solid var(--kt-ai-divider); color: var(--kt-ai-fg-muted); position: sticky; top: 0; align-self: start; height: 100dvh; overflow-y: auto; } .kt-ai-main { min-width: 0; background: var(--kt-ai-bg); } .kt-ai-work-column { max-width: var(--kt-ai-content-max); margin: 0 auto; padding: var(--kt-ai-space-12) var(--kt-ai-space-8) var(--kt-ai-space-11); } .kt-ai-work-column-wide { max-width: var(--kt-ai-content-wide); margin: 0 auto; padding: var(--kt-ai-space-8); } .kt-ai-work-column-table { max-width: var(--kt-ai-content-table); margin: 0 auto; padding: var(--kt-ai-space-11) var(--kt-ai-space-8); } /* Скроллбары */ .kt-ai-app ::-webkit-scrollbar, .kt-ai-shell ::-webkit-scrollbar { width: 8px; height: 8px; } .kt-ai-app ::-webkit-scrollbar-thumb, .kt-ai-shell ::-webkit-scrollbar-thumb { background: var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-md); } .kt-ai-app ::-webkit-scrollbar-track, .kt-ai-shell ::-webkit-scrollbar-track { background: transparent; } /* Фокус: единый видимый стиль для всего интерактива */ .kt-ai-app :focus-visible, .kt-ai-shell :focus-visible { outline: 2px solid var(--kt-ai-focus-ring); outline-offset: 1px; } /* ---------- Типографика ---------- */ .kt-ai-h1 { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-bold); line-height: var(--kt-ai-leading-tight); letter-spacing: -.02em; } .kt-ai-h2 { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); line-height: var(--kt-ai-leading-tight); letter-spacing: -.01em; } .kt-ai-h3 { font-size: var(--kt-ai-text-md); font-weight: var(--kt-ai-weight-semibold); } /* Капс оставляем — узнаваемый eyebrow-паттерн. Но капс мелким кеглем это худший случай для чтения, и он не имеет права быть ещё и самым тусклым текстом системы: 11px + .8px разрядки + faint. Кегль на ступень выше, разрядка меньше (на 12px .8px избыточна), цвет — muted. */ .kt-ai-label { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-semibold); text-transform: uppercase; letter-spacing: .5px; color: var(--kt-ai-fg-muted); } .kt-ai-meta { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); } .kt-ai-num { font-family: var(--kt-ai-font-sans); font-variant-numeric: tabular-nums; } .kt-ai-link { color: var(--kt-ai-link); text-decoration: none; } .kt-ai-link:hover { text-decoration: underline; } /* ---------- Иконки ---------- */ .kt-icon { width: 16px; height: 16px; display: inline-block; flex: 0 0 16px; color: currentColor; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; vertical-align: -2px; } .kt-icon-sm { width: 14px; height: 14px; flex-basis: 14px; } .kt-icon-lg { width: 20px; height: 20px; flex-basis: 20px; } /* ---------- Кнопки ---------- */ .kt-ai-btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--kt-ai-space-3); height: var(--kt-ai-control-h-md); padding: 0 var(--kt-ai-space-5); border-radius: var(--kt-ai-radius-xl); border: 1px solid var(--kt-ai-border-strong); background: var(--kt-ai-bg-elevated); box-shadow: var(--kt-ai-shadow-sm); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); cursor: pointer; white-space: nowrap; transition: background var(--kt-ai-dur-fast) var(--kt-ai-ease-standard), border-color var(--kt-ai-dur-fast) var(--kt-ai-ease-standard), transform 60ms var(--kt-ai-ease-standard); } /* hover = тонирующий слой bg-hover поверх поверхности: в dark светлеет, в light темнеет */ .kt-ai-btn:hover { background-image: linear-gradient(var(--kt-ai-bg-hover), var(--kt-ai-bg-hover)); } .kt-ai-btn:active { transform: scale(.98); } .kt-ai-btn:disabled { opacity: .4; cursor: default; transform: none; background-image: none; } .kt-ai-btn[data-variant="primary"] { background: var(--kt-ai-primary); border-color: var(--kt-ai-primary); color: var(--kt-ai-fg-on-fill); font-weight: var(--kt-ai-weight-semibold); box-shadow: inset 0 1px 0 var(--kt-ai-inset-highlight); } .kt-ai-btn[data-variant="primary"]:hover { background: var(--kt-ai-primary-hover); border-color: var(--kt-ai-primary-hover); } .kt-ai-btn[data-variant="ghost"] { border-color: transparent; background: transparent; box-shadow: none; color: var(--kt-ai-fg-muted); } .kt-ai-btn[data-variant="ghost"]:hover { color: var(--kt-ai-fg); background: var(--kt-ai-bg-hover); } .kt-ai-btn[data-variant="danger"] { background: transparent; box-shadow: none; border-color: var(--kt-ai-danger); color: var(--kt-ai-danger); } .kt-ai-btn[data-variant="danger"]:hover { background: var(--kt-ai-danger); color: var(--kt-ai-fg-on-fill); } .kt-ai-btn[data-size="sm"] { height: var(--kt-ai-control-h-sm); padding: 0 var(--kt-ai-space-4); font-size: var(--kt-ai-text-xs); border-radius: var(--kt-ai-radius-lg); } .kt-ai-btn[data-size="lg"] { height: var(--kt-ai-control-h-lg); padding: 0 var(--kt-ai-space-7); font-size: var(--kt-ai-text-md); border-radius: var(--kt-ai-radius-xl); } .kt-ai-icon-button { display: inline-flex; align-items: center; justify-content: center; width: var(--kt-ai-control-h-md); height: var(--kt-ai-control-h-md); border-radius: var(--kt-ai-radius-lg); border: none; background: transparent; color: var(--kt-ai-fg-muted); cursor: pointer; transition: background var(--kt-ai-dur-fast) var(--kt-ai-ease-standard), color var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); } .kt-ai-icon-button:hover, .kt-ai-icon-button[data-active="true"] { background: var(--kt-ai-bg-hover); color: var(--kt-ai-fg); } /* ---------- Формы ---------- */ .kt-ai-field { display: flex; flex-direction: column; gap: var(--kt-ai-space-2); margin-bottom: var(--kt-ai-space-5); } /* Подпись поля. Две формы записи, и обе законны:
внутри распирают колонку, а вместе с ней и страницу. Сворот в одну колонку на ≤720px этого НЕ лечит — единственная колонка растягивается так же. Продукт видел это как горизонтальную прокрутку всей страницы на телефоне (390px экрана против 836px контента) и обходил своей утилитой. min-width:0 у прямых детей снимает минимум по содержимому: длинное теперь переносится или прокручивается ВНУТРИ своего блока, а не выталкивает страницу. Прокрутка широкого (таблицы) остаётся у .kt-ai-table-wrap — там ей и место. */ .kt-ai-cols > * { min-width: 0; } @media (max-width: 719.98px) { .kt-ai-cols { grid-template-columns: 1fr !important; } } /* v3.9: тактильность – нажатие чувствуется у всего кликабельного, не только у кнопок */ .kt-ai-suggestion:active, .kt-ai-filter-chip:active, .kt-ai-tab:active { transform: scale(.97); } .kt-ai-hover-lift:active { transform: translateY(0) scale(.99); } .kt-ai-nav-item:active { transform: scale(.985); } .kt-ai-kpi-strip[data-cards="true"] .kt-ai-kpi[data-tab="true"]:active { transform: scale(.985); } /* v3.5: hover-обратная связь (ElevenLabs) — ряды и кликабельные карточки отвечают на курсор */ .kt-ai-row-hover { border-radius: var(--kt-ai-radius-md); transition: background var(--kt-ai-dur-fast); } .kt-ai-row-hover:hover { background: var(--kt-ai-bg-hover); } .kt-ai-hover-lift { transition: border-color var(--kt-ai-dur-fast), box-shadow var(--kt-ai-dur-fast), transform var(--kt-ai-dur-fast); } .kt-ai-hover-lift:hover { border-color: var(--kt-ai-border-strong); box-shadow: 0 2px 8px var(--kt-black-a08); transform: translateY(-1px); } .kt-ai-chart-card svg circle { transition: opacity var(--kt-ai-dur-fast); } .kt-ai-chart-card svg circle:hover { opacity: .75; } svg circle[data-map-pt] { transition: r var(--kt-ai-dur-fast); cursor: pointer; } svg circle[data-map-pt]:hover { r: 2.3; } /* v2.5: вариант «стат-карточки» – сетка равных карточек вместо тихой полосы */ .kt-ai-kpi-strip[data-cards="true"] { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--kt-ai-stack-block); padding: 0; } .kt-ai-kpi-strip[data-cards="true"] .kt-ai-kpi { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-2xl); box-shadow: var(--kt-ai-card-shadow); padding: var(--kt-ai-space-6) var(--kt-ai-space-7); gap: var(--kt-ai-space-2); } .kt-ai-kpi-strip[data-cards="true"] .value { font-size: var(--kt-ai-text-xl); } .kt-ai-kpi-strip[data-cards="true"] .value[data-long="true"] { font-size: var(--kt-ai-text-lg); } /* KPI-карточка как таб (dashboard): выбранная ведёт чарт — специфичность выше card-правил */ .kt-ai-kpi-strip[data-cards="true"] .kt-ai-kpi[data-tab="true"] { cursor: pointer; transition: border-color .12s ease, box-shadow .12s ease; } .kt-ai-kpi-strip[data-cards="true"] .kt-ai-kpi[data-tab="true"]:hover { border-color: var(--kt-ai-border-strong); } .kt-ai-kpi-strip[data-cards="true"] .kt-ai-kpi[data-tab="true"][data-active="true"] { border-color: var(--kt-ai-fg); box-shadow: inset 0 0 0 1px var(--kt-ai-fg), var(--kt-ai-card-shadow); } /* ---------- Таблицы (frameless ElevenLabs: только горизонтальные разделители рядов, без вертикальных колонок-сепараторов) ---------- * Дефолт – full-bleed без внешней рамки. Контейнерный вариант: data-framed="true". */ .kt-ai-table-wrap { overflow: auto; background: transparent; } .kt-ai-table-wrap[data-framed="true"] { border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); background: var(--kt-ai-bg); } .kt-ai-table-toolbar { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) 0; flex-wrap: wrap; } .kt-ai-table-wrap[data-framed="true"] .kt-ai-table-toolbar { padding: var(--kt-ai-space-4) var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-border); } .kt-ai-table { width: 100%; border-collapse: collapse; font-size: var(--kt-ai-text-sm); } .kt-ai-table td { word-break: break-word; } /* На узких экранах (≤640px) строки таблицы превращаются в карточки: первая ячейка — заголовок карточки, * остальные — пары «label: value». Без горизонтального скролла (паттерн ElevenLabs/Linear). */ .kt-ai-table th { text-align: left; padding: var(--kt-ai-space-4) var(--kt-ai-space-5); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); /* обязательный контент: faint не проходит контраст */ border-bottom: 1px solid var(--kt-ai-divider); white-space: nowrap; position: sticky; top: 0; background: var(--kt-ai-bg); } .kt-ai-table td { padding: 0 var(--kt-ai-space-5); height: var(--kt-ai-table-row); border-bottom: 1px solid var(--kt-ai-divider); vertical-align: middle; } .kt-ai-table tr:last-child td { border-bottom: none; } /* Frameless (ElevenLabs): только горизонтальные разделители рядов, без вертикальных колонок-сепараторов (тихо, воздушно) */ .kt-ai-table tbody tr { transition: background var(--kt-ai-dur-fast); } /* ElevenLabs: чекбоксы появляются на hover/выборе, не загромождают */ .kt-ai-table tbody td:first-child .kt-ai-checkbox { opacity: 0; transition: opacity var(--kt-ai-dur-fast); } .kt-ai-table tbody tr:hover td:first-child .kt-ai-checkbox, .kt-ai-table tbody td:first-child .kt-ai-checkbox:has(input:checked) { opacity: 1; } .kt-ai-table tbody tr:hover { background: var(--kt-ai-bg-hover); } .kt-ai-table td.num { font-family: var(--kt-ai-font-sans); font-variant-numeric: tabular-nums; text-align: right; } .kt-ai-table[data-density="compact"] td { height: var(--kt-ai-table-row-compact); } .kt-ai-table[data-density="relaxed"] td { height: var(--kt-ai-table-row-relaxed); } /* ---------- Progress ---------- */ .kt-ai-progress { height: 5px; background: var(--kt-ai-bg-active); border-radius: var(--kt-ai-radius-full); overflow: hidden; } .kt-ai-progress > div { height: 100%; background: linear-gradient(90deg, var(--kt-ai-brand-blue), var(--kt-ai-primary)); border-radius: var(--kt-ai-radius-full); transition: width var(--kt-ai-dur-slower) var(--kt-ai-ease-standard); } /* ---------- Modal ---------- */ .kt-ai-modal-overlay { position: fixed; inset: 0; background: var(--kt-ai-overlay); display: flex; align-items: center; justify-content: center; z-index: var(--kt-ai-z-modal); animation: ktai-fade var(--kt-ai-dur-base) var(--kt-ai-ease-enter); } .kt-ai-modal { background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-3xl); box-shadow: var(--kt-ai-shadow-xl); width: 520px; max-width: 92vw; max-height: 86vh; overflow-y: auto; padding: var(--kt-ai-space-7); animation: ktai-rise var(--kt-ai-dur-slow) var(--kt-ai-ease-enter); } .kt-ai-modal .modal-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-5); } .kt-ai-modal .modal-actions { display: flex; justify-content: flex-end; gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-6); } /* ---------- Drawer ---------- */ .kt-ai-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: 420px; max-width: 92vw; background: var(--kt-ai-bg-elevated); border-left: 1px solid var(--kt-ai-border-strong); box-shadow: var(--kt-ai-shadow-lg); z-index: var(--kt-ai-z-drawer); padding: var(--kt-ai-space-7); overflow-y: auto; animation: ktai-slide-in var(--kt-ai-dur-slow) var(--kt-ai-ease-enter); } /* ---------- Документ-артефакт (что продукт производит: ТС, отчёт, СЗ…) ---------- */ .kt-ai-doc-list { display: flex; flex-direction: column; gap: var(--kt-ai-space-3); } .kt-ai-doc { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); } .kt-ai-doc .doc-ic { display: inline-grid; place-items: center; width: 36px; height: 36px; flex: 0 0 auto; border-radius: var(--kt-ai-radius-lg); background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); } .kt-ai-doc .doc-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .kt-ai-doc .doc-name { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); } .kt-ai-doc .doc-sub { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .kt-ai-doc .kt-ai-btn { flex: 0 0 auto; } .drawer-sec .kt-ai-doc { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-lg); } .drawer-sec .kt-ai-doc .doc-ic { width: 30px; height: 30px; } /* ---------- Activity timeline panel (правый слайд-овер: лента активности агента) ---------- */ .act-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--kt-ai-space-6); } .act-head strong { font-size: var(--kt-ai-text-md); font-weight: var(--kt-ai-weight-semibold); } .kt-ai-activity { display: flex; flex-direction: column; } .act-item { position: relative; padding-left: 22px; padding-bottom: var(--kt-ai-space-6); } .act-item::before { content: ""; position: absolute; left: 5px; top: 14px; bottom: 0; width: 1px; background: var(--kt-ai-divider); } .act-item:last-child { padding-bottom: 0; } .act-item:last-child::before { display: none; } .act-dot { position: absolute; left: 0; top: 3px; width: 11px; height: 11px; border-radius: 50%; box-shadow: 0 0 0 2px var(--kt-ai-bg-elevated); } .act-title { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold); line-height: var(--kt-ai-leading-tight); } .act-time { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-top: 2px; } .act-card { margin-top: var(--kt-ai-space-4); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); } .act-row { display: flex; gap: var(--kt-ai-space-4); font-size: var(--kt-ai-text-xs); margin-bottom: var(--kt-ai-space-2); } .act-row .k { color: var(--kt-ai-fg-muted); width: 56px; flex-shrink: 0; } .act-row .v { display: flex; flex-wrap: wrap; gap: 4px; } .act-summary { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg); margin: var(--kt-ai-space-3) 0; line-height: var(--kt-ai-leading-normal); } .act-card .kt-ai-btn { margin-top: var(--kt-ai-space-2); } /* ---------- Toast ---------- */ .kt-ai-toast-stack { position: fixed; bottom: max(var(--kt-ai-space-7), env(safe-area-inset-bottom)); right: max(var(--kt-ai-space-7), env(safe-area-inset-right)); display: flex; flex-direction: column; gap: var(--kt-ai-space-4); z-index: var(--kt-ai-z-toast); } .kt-ai-toast { display: flex; align-items: center; gap: var(--kt-ai-space-4); background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-lg); box-shadow: var(--kt-ai-shadow-md); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); font-size: var(--kt-ai-text-sm); min-width: 260px; max-width: 380px; animation: ktai-rise var(--kt-ai-dur-base) var(--kt-ai-ease-enter); } .kt-ai-toast::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--kt-ai-primary); flex-shrink: 0; } .kt-ai-toast[data-tone="ok"]::before { background: var(--kt-ai-status-ok-fg); } .kt-ai-toast[data-tone="warn"]::before { background: var(--kt-ai-status-warn-fg); } .kt-ai-toast[data-tone="risk"]::before { background: var(--kt-ai-status-risk-fg); } /* ---------- Tooltip ---------- */ /* ВАЖНО: data-tip рисуется через ::after, то есть это СГЕНЕРИРОВАННЫЙ контент. В дереве доступности его нет — диктор подсказку не прочитает. Подсказка объясняет иконку глазам и только им; имя кнопке даёт aria-label, и у icon-only кнопки обязаны быть ОБА (docs/DESIGN.md, CHECKLIST G6). Правило было записано и всё равно нарушено в пяти местах собственной витрины — то есть проза не работает; теперь это проверяет гейт 16. */ .kt-ai-tooltip { position: relative; } .kt-ai-tooltip::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--kt-ai-bg-elevated); color: var(--kt-ai-fg); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-md); box-shadow: var(--kt-ai-shadow-md); padding: var(--kt-ai-space-2) var(--kt-ai-space-4); font-size: var(--kt-ai-text-2xs); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); z-index: var(--kt-ai-z-tooltip); } .kt-ai-tooltip:hover::after, .kt-ai-tooltip:focus-visible::after, .kt-ai-tooltip:focus-within::after { opacity: 1; } /* ---------- Skeleton / Empty ---------- */ .kt-ai-skeleton { border-radius: var(--kt-ai-radius-md); background: linear-gradient(90deg, var(--kt-ai-bg-active) 25%, var(--kt-ai-bg-hover) 50%, var(--kt-ai-bg-active) 75%); background-size: 200% 100%; animation: ktai-shimmer 1.4s linear infinite; min-height: 12px; } .kt-ai-empty { display: flex; flex-direction: column; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-10) var(--kt-ai-space-8); text-align: center; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); border-radius: var(--kt-ai-radius-3xl); background-image: radial-gradient(color-mix(in srgb, currentColor 14%, transparent) 1px, transparent 1.4px); background-size: 14px 14px; } .kt-ai-empty .title { color: var(--kt-ai-fg); font-weight: var(--kt-ai-weight-semibold); } /* ---------- Командная палитра ---------- */ .kt-ai-command { overflow: hidden; animation: ktai-rise var(--kt-ai-dur-slow) var(--kt-ai-ease-enter); background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-3xl); box-shadow: var(--kt-ai-command-shadow); width: 560px; max-width: 92vw; } /* ---------- Анимации ---------- */ @keyframes ktai-fade { from { opacity: 0; } to { opacity: 1; } } @keyframes ktai-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } @keyframes ktai-slide-in { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } } @keyframes ktai-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } /* ============ v2.1 ДОПОЛНЕНИЯ (ревью 2026-06-09) ============ */ /* Оптика Inter и баланс заголовков */ .kt-ai-app, .kt-ai-shell { font-feature-settings: "cv05", "cv11", "ss01"; } .kt-ai-h1, .kt-ai-h2, .kt-ai-h3 { text-wrap: balance; } /* ---------- Loading-кнопка ---------- */ .kt-ai-btn[data-loading="true"] { pointer-events: none; position: relative; color: transparent !important; } .kt-ai-btn[data-loading="true"]::after { content: ""; position: absolute; left: calc(50% - 7px); top: calc(50% - 7px); width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--kt-ai-fg-muted); border-top-color: transparent; animation: ktai-spin .7s linear infinite; } .kt-ai-btn[data-variant="primary"][data-loading="true"]::after { border-color: var(--kt-ai-fg-on-fill); border-top-color: transparent; } /* ---------- Combobox / multi-select ---------- */ .kt-ai-combobox { position: relative; } .kt-ai-combobox .control { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-2); align-items: center; min-height: var(--kt-ai-control-h-md); padding: var(--kt-ai-space-2) var(--kt-ai-space-4); background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-lg); cursor: text; transition: border-color var(--kt-ai-dur-fast), box-shadow var(--kt-ai-dur-fast); } .kt-ai-combobox .control:focus-within { border-color: var(--kt-ai-primary); box-shadow: var(--kt-ai-focus-shadow); } .kt-ai-combobox .control input { flex: 1; min-width: 80px; border: none; background: transparent; color: var(--kt-ai-fg); font: inherit; outline: none; height: 22px; } .kt-ai-combobox .menu { position: absolute; top: calc(100% + 4px); left: 0; right: 0; background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-lg); box-shadow: var(--kt-ai-shadow-md); z-index: var(--kt-ai-z-dropdown); max-height: 240px; overflow-y: auto; padding: var(--kt-ai-space-2); display: none; } .kt-ai-combobox[data-open="true"] .menu { display: block; animation: ktai-rise var(--kt-ai-dur-base) var(--kt-ai-ease-enter); } .kt-ai-combobox .option { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-md); font-size: var(--kt-ai-text-sm); cursor: pointer; } .kt-ai-combobox .option:hover, .kt-ai-combobox .option[data-active="true"] { background: var(--kt-ai-bg-hover); } .kt-ai-combobox .option[data-selected="true"]::after { content: "✓"; margin-left: auto; color: var(--kt-ai-primary); font-weight: 700; } .kt-ai-chip[data-removable] { padding-right: var(--kt-ai-space-2); } .kt-ai-chip .remove { cursor: pointer; opacity: .6; padding: 0 3px; border-radius: 50%; } .kt-ai-chip .remove:hover { opacity: 1; background: var(--kt-ai-bg-active); } /* ---------- Bulk-select bar (возврат из v1) ---------- */ .kt-ai-table-bulkbar { display: none; align-items: center; gap: var(--kt-ai-space-5); padding: var(--kt-ai-space-3) var(--kt-ai-space-5); background: var(--kt-ai-primary-subtle); border-bottom: 1px solid var(--kt-ai-border); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg); } .kt-ai-table-bulkbar[data-open="true"] { display: flex; animation: ktai-rise var(--kt-ai-dur-base) var(--kt-ai-ease-enter); } .kt-ai-table-bulkbar .count { font-family: var(--kt-ai-font-sans); color: var(--kt-ai-primary); font-weight: 600; font-variant-numeric: tabular-nums;} /* ---------- File upload / dropzone ---------- */ .kt-ai-dropzone { display: flex; flex-direction: column; align-items: center; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-8); text-align: center; border: 1.5px dashed var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-xl); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); cursor: pointer; transition: border-color var(--kt-ai-dur-fast), background var(--kt-ai-dur-fast); } .kt-ai-dropzone:hover, .kt-ai-dropzone[data-drag="true"] { border-color: var(--kt-ai-primary); background: var(--kt-ai-primary-subtle); color: var(--kt-ai-fg); } .kt-ai-file-row { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-3) var(--kt-ai-space-5); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); font-size: var(--kt-ai-text-xs); } .kt-ai-file-row .size { margin-left: auto; font-family: var(--kt-ai-font-sans); color: var(--kt-ai-fg-faint); font-variant-numeric: tabular-nums;} /* ---------- Stepper ---------- */ .kt-ai-stepper { display: flex; align-items: center; gap: var(--kt-ai-space-3); flex-wrap: wrap; } .kt-ai-step-item { display: flex; align-items: center; gap: var(--kt-ai-space-3); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); } /* Кружок с номером. * * Иконкой это делать нельзя, и вот почему. Наши иконки — штриховые, 1.75 в * сетке 24, и используются на 14–15px. Цифра штрихом внутри штриховой * окружности на таком размере выходит 6–7px высотой при штрихе ~1px: просветы * у 6, 8 и 9 схлопываются, иконка перестаёт читаться. К тому же набор был бы * конечным (1–9), а номера бывают и двузначные. * * Поэтому номер рисуется ТЕКСТОМ в круге: любое число, чёткость на любом * размере, состояния и цвет от токенов. * * Раньше это правило жило внутри степпера как `.kt-ai-step-item .n` и было * недоступно снаружи — продукты рисовали свои кружки заново. * * ИМЯ. Не `.kt-ai-num` — оно уже занято утилитой табличных цифр (см. выше, * рядом с .kt-ai-meta). Второе определение того же класса перебивало первое, и * суммы вроде «562 572 660 ₸» рисовались кружком 22px поверх текста в 17 * местах продукта. Один класс — один смысл. */ .kt-ai-num-badge { width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-family: var(--kt-ai-font-mono); /* цифры одной ширины — колонка не пляшет */ font-size: var(--kt-ai-text-2xs); font-weight: var(--kt-ai-weight-bold); font-variant-numeric: tabular-nums; border: 1.5px solid var(--kt-ai-border-strong); color: var(--kt-ai-fg-faint); background: transparent; transition: background var(--kt-ai-dur-base) var(--kt-ai-ease-standard), border-color var(--kt-ai-dur-base) var(--kt-ai-ease-standard), color var(--kt-ai-dur-base) var(--kt-ai-ease-standard); } .kt-ai-num-badge[data-size="sm"] { width: 18px; height: 18px; font-size: 9px; border-width: 1.25px; } .kt-ai-num-badge[data-size="lg"] { width: 28px; height: 28px; font-size: var(--kt-ai-text-xs); } /* Текущий — заливка: он один на экране и обязан быть найден мгновенно. */ .kt-ai-num-badge[data-state="current"] { background: var(--kt-ai-primary); border-color: var(--kt-ai-primary); color: var(--kt-ai-fg-on-fill); } /* Пройденный — тише текущего, но не тусклее будущих: он уже что-то значит. */ .kt-ai-num-badge[data-state="done"] { border-color: var(--kt-ai-fg-muted); color: var(--kt-ai-fg-muted); } .kt-ai-num-badge[data-tone="ok"] { border-color: var(--kt-ai-status-ok-fg); color: var(--kt-ai-status-ok-fg); } .kt-ai-num-badge[data-tone="warn"] { border-color: var(--kt-ai-status-warn-fg); color: var(--kt-ai-status-warn-fg); } .kt-ai-num-badge[data-tone="risk"] { border-color: var(--kt-ai-status-risk-fg); color: var(--kt-ai-status-risk-fg); } /* Степпер пользуется тем же компонентом — определения одного и того же кружка в двух местах неизбежно разъезжаются (уже было с .kt-ai-label). */ .kt-ai-step-item .n { width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: var(--kt-ai-text-2xs); font-weight: 700; border: 1.5px solid var(--kt-ai-border-strong); color: var(--kt-ai-fg-faint); transition: all var(--kt-ai-dur-base); } .kt-ai-step-item[data-state="current"] { color: var(--kt-ai-fg); font-weight: 500; } .kt-ai-step-item[data-state="current"] .n { background: var(--kt-ai-primary); border-color: var(--kt-ai-primary); color: var(--kt-ai-fg-on-fill); } .kt-ai-step-item[data-state="done"] { color: var(--kt-ai-fg-muted); } .kt-ai-step-item[data-state="done"] .n { background: var(--kt-ai-status-ok-bg); border-color: transparent; color: var(--kt-ai-status-ok-fg); } .kt-ai-step-sep { width: 20px; height: 1px; background: var(--kt-ai-border-strong); flex-shrink: 0; } /* ---------- Breadcrumbs / Pagination / Сортировка ---------- */ .kt-ai-breadcrumbs { display: flex; align-items: center; gap: var(--kt-ai-space-3); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); } .kt-ai-breadcrumbs a { color: var(--kt-ai-fg-muted); text-decoration: none; } .kt-ai-breadcrumbs a:hover { color: var(--kt-ai-fg); } .kt-ai-breadcrumbs .sep { color: var(--kt-ai-fg-faint); } .kt-ai-breadcrumbs .current { color: var(--kt-ai-fg); } .kt-ai-pagination { display: flex; gap: 2px; align-items: center; } .kt-ai-pagination .page-btn { min-width: var(--kt-ai-control-h-sm); height: var(--kt-ai-control-h-sm); display: inline-flex; align-items: center; justify-content: center; padding: 0 var(--kt-ai-space-3); border: none; background: transparent; border-radius: var(--kt-ai-radius-md); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs); font-family: var(--kt-ai-font-sans); cursor: pointer; font-variant-numeric: tabular-nums;} .kt-ai-pagination .page-btn:hover { background: var(--kt-ai-bg-hover); color: var(--kt-ai-fg); } .kt-ai-pagination .page-btn[data-active="true"] { background: var(--kt-ai-bg-active); color: var(--kt-ai-fg); font-weight: 600; } .kt-ai-pagination .page-btn:disabled, .kt-ai-pagination .page-btn[disabled] { opacity: .4; cursor: default; } .table-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-5); } /* Сводный bar-chart над таблицей (data-drives-form: раскатка во времени вместо охоты по timestamp-колонке) */ .kt-ai-chart-card { border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); background: var(--kt-ai-bg-elevated); padding: var(--kt-ai-pad-card); margin-bottom: var(--kt-ai-stack-block); } .kt-ai-chart-card .cc-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-stack-block); } .kt-ai-chart-card .cc-title { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-bottom: 2px; } .kt-ai-chart-card .cc-total { display: flex; align-items: baseline; gap: var(--kt-ai-space-3); } .kt-ai-chart-card .cc-total .num { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); letter-spacing: -.01em; line-height: 1; } .kt-ai-chart-card .cc-total .unit { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); } .kt-ai-chart-card .cc-link { background: none; border: none; padding: 0; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs); cursor: pointer; display: inline-flex; align-items: center; gap: 4px; } .kt-ai-chart-card .cc-link:hover { color: var(--kt-ai-fg); } .kt-ai-chart-card .cc-bars { display: flex; align-items: flex-end; gap: 5px; height: 120px; } .kt-ai-chart-card .cc-bar { flex: 1 1 0; min-width: 0; height: 100%; display: flex; align-items: flex-end; } .kt-ai-chart-card .cc-bar > i { display: block; width: 100%; min-height: 2px; background: var(--cc-color, var(--kt-ai-chart-blue)); border-radius: var(--kt-ai-radius-sm) var(--kt-ai-radius-sm) 2px 2px; opacity: .85; transition: opacity var(--kt-ai-dur-fast); } .kt-ai-chart-card .cc-bar:hover > i { opacity: 1; } .kt-ai-chart-card .cc-axis { display: flex; justify-content: space-between; margin-top: var(--kt-ai-space-3); font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-faint); } .kt-ai-chart-card .cc-axis-cat span { flex: 1 1 0; min-width: 0; text-align: center; padding: 0 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* ---------- Подсказка значения на диаграмме ---------- Не `.kt-ai-tooltip`: та однострочная (`white-space: nowrap`), рисуется через ::after и у края карточки обрезается. Здесь нужен БЛОК — заголовок и до трёх пар «подпись — значение», — и он живёт в , поэтому его не режет overflow карточки. Позиционирует kt-ai-chart-tip.js: развернуть подсказку внутрь у края и удержать её у марки, а не у курсора, средствами CSS нельзя. Числа приходят готовыми от продукта: система отвечает за вид, не за формат. */ .kt-ai-chart-tip { position: absolute; z-index: var(--kt-ai-z-tooltip); min-width: 132px; max-width: 260px; padding: var(--kt-ai-space-4) var(--kt-ai-space-5); background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-lg); box-shadow: var(--kt-ai-shadow-md); color: var(--kt-ai-fg); font-size: var(--kt-ai-text-2xs); line-height: var(--kt-ai-leading-normal); pointer-events: none; animation: ktai-tip-in var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); } .kt-ai-chart-tip[hidden] { display: none; } .kt-ai-chart-tip .tip-title { font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg); } .kt-ai-chart-tip .tip-title + .tip-rows { margin-top: var(--kt-ai-space-3); } /* Пары «подпись — значение» сеткой: значения прижаты вправо и набраны табличными цифрами, иначе колонка чисел скачет от строки к строке. */ .kt-ai-chart-tip .tip-rows { display: grid; grid-template-columns: 1fr auto; gap: 2px var(--kt-ai-space-5); margin: 0; } .kt-ai-chart-tip .tip-rows dt { color: var(--kt-ai-fg-muted); min-width: 0; } .kt-ai-chart-tip .tip-rows dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; font-weight: var(--kt-ai-weight-medium); } @keyframes ktai-tip-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } } @media (prefers-reduced-motion: reduce) { .kt-ai-chart-tip { animation: none; } } /* Марка с подсказкой — цель наведения И фокуса: диаграмма обязана читаться не только мышью. Выделение и подсказка включаются вместе, а не по отдельности. */ [data-kt-tip] { cursor: default; } [data-kt-tip]:focus-visible { outline: 2px solid var(--kt-ai-focus-ring); outline-offset: 2px; border-radius: var(--kt-ai-radius-sm); } .kt-ai-chart-card .cc-bar[data-kt-tip]:hover > i, .kt-ai-chart-card .cc-bar[data-kt-tip][data-kt-tip-open="true"] > i { opacity: 1; filter: brightness(1.08); } .kt-ai-chart-card svg circle[data-kt-tip][data-kt-tip-open="true"] { opacity: .75; } /* Гашение терминальных/неактивных строк (status.dim) — активные выделяются */ .kt-ai-table tr[data-dim="true"] td { opacity: .5; } .kt-ai-table tr[data-dim="true"]:hover td { opacity: .72; } /* Кликабельные сортируемые заголовки (button + иконка + aria-sort) */ .th-sort { display: inline-flex; align-items: center; gap: 4px; background: none; border: none; font: inherit; color: inherit; cursor: pointer; padding: 0; } .th-sort:hover { color: var(--kt-ai-fg); } .th-sort .th-sort-ic { width: 13px; height: 13px; opacity: .4; transition: opacity var(--kt-ai-dur-fast); } .th-sort:hover .th-sort-ic { opacity: .7; } .kt-ai-table thead th[aria-sort] .th-sort { color: var(--kt-ai-primary); } .kt-ai-table thead th[aria-sort] .th-sort .th-sort-ic { opacity: 1; } .th-sort-ic[data-dir="asc"] { transform: rotate(180deg); } .kt-ai-table .kt-ai-sticky-first td:first-child, .kt-ai-table .kt-ai-sticky-first th:first-child { position: sticky; left: 0; background: var(--kt-ai-bg); z-index: 1; } /* ---------- Diff-view документов ---------- */ .kt-ai-diff { font-family: inherit; font-size: var(--kt-ai-text-sm); line-height: 1.55; border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); overflow: hidden; } .kt-ai-diff .line { padding: 2px var(--kt-ai-space-5); white-space: pre-wrap; } .kt-ai-diff .line[data-op="add"] { background: var(--kt-ai-status-ok-bg); color: var(--kt-ai-status-ok-fg); } .kt-ai-diff .line[data-op="del"] { background: var(--kt-ai-status-risk-bg); color: var(--kt-ai-status-risk-fg); text-decoration: line-through; } /* ---------- Notification list ---------- */ .kt-ai-notif-item { display: flex; gap: var(--kt-ai-space-4); align-items: flex-start; padding: var(--kt-ai-space-4) var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); } .kt-ai-notif-item:last-child { border-bottom: none; } .kt-ai-notif-item::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: transparent; margin-top: 7px; flex-shrink: 0; } .kt-ai-notif-item[data-unread="true"]::before { background: var(--kt-ai-primary); } .kt-ai-notif-item .time { margin-left: auto; font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-faint); white-space: nowrap; } /* ---------- Duotone empty-state ---------- */ .kt-ai-empty .icon-spot { width: 56px; height: 56px; border-radius: 50%; background: var(--kt-ai-primary-subtle); color: var(--kt-ai-primary); display: flex; align-items: center; justify-content: center; } /* ---------- Date input ---------- */ .kt-ai-input[type="date"], .kt-ai-input[type="datetime-local"] { font-family: var(--kt-ai-font-sans); font-size: var(--kt-ai-text-xs); font-variant-numeric: tabular-nums;} /* ---------- Card grid (адаптивная сетка карточек) ---------- */ .kt-ai-card-grid { display: grid; gap: var(--kt-ai-space-5); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); } /* ---------- Отзывчивость shell ---------- * ≥1024 (bp-lg): полный sidebar 232px * 832..1023: icon-rail 52px – подписи .nav-label скрываются * <832 (bp-md): sidebar = overlay-drawer, открывается data-open="true" * Брейкпоинты совпадают с --kt-ai-bp-* (в media var() недоступен) */ /* Диапазон рейла задан С ДВУХ сторон. Раньше стояло только `max-width: 1023.98`, и политика рейла доставала до выдвижной панели ниже 832px: структурное правило ниже скрывает всё без иконки, а восстановление `.kt-ai-sidebar .nav-label { display: initial }` в мобильном блоке его не перебивало — у структурного правила пять `:not()`, и оно просто специфичнее. В итоге панель во всю ширину экрана показывала колонку значков без единой подписи, хотя место под подписи там есть. Верхняя граница делает диапазон тем, чем он назван в комментарии к брейкпоинтам: 832..1023. */ @media (min-width: 832px) and (max-width: 1023.98px) { .kt-ai-shell { grid-template-columns: var(--kt-ai-sidebar-collapsed-width) minmax(0, 1fr); } /* ПОЛИТИКА РЕЙЛА. В 52px помещается только иконный ряд — и это правило, а не список исключений. Раньше здесь скрывались три известных класса (.nav-label, .count, .brand-label), поэтому любой блок, добавленный продуктом в сайдбар, оставался и раскладывался в колонку букв поверх навигации: замер на 924px давал элементы шириной 0px при высоте до 234px. Перечислять классы бессмысленно — список никогда не догонит разметку продуктов, поэтому правило структурное: показываем то, что несёт иконку. Элемент с иконкой (svg или img) внутри остаётся и становится иконной кнопкой. Элемент без иконки в рейле нечем показать — скрывается целиком. Опт-ин: data-rail="keep" оставляет как есть, и за 52px отвечает продукт. */ .kt-ai-sidebar :not(:has(svg, img)):not(svg):not(img):not([data-rail="keep"]):not(:has([data-rail="keep"])) { display: none; } .kt-ai-sidebar :is(.kt-ai-nav-item, :has(> svg), :has(> img)) { justify-content: center; padding: 0; } } /* Заголовок группы в полосе значков. Показывать его нечем — в 52px не влезает ни одно слово, а иконки у заголовка нет и быть не должно (иконка сделала бы его похожим на пункт). Поэтому в рейле он становится тем, чем является по сути: разделителем между группами. Опт-ин data-rail="keep" в разметке — иначе структурное правило выше сняло бы его целиком вместе с линией. Диапазон задан с двух сторон: ниже 832px сайдбар — выдвижная панель во всю ширину, там заголовок снова показывается словами. */ @media (min-width: 832px) and (max-width: 1023.98px) { .kt-ai-sidebar .kt-ai-nav-group { height: 0; overflow: hidden; padding: 0; margin: var(--kt-ai-space-4) var(--kt-ai-space-3); border-top: 1px solid var(--kt-ai-divider); } .kt-ai-sidebar .kt-ai-nav-group:first-child { margin-top: 0; border-top: 0; } } @media (max-width: 831.98px) { /* Жёсткие гарантии мобильной вёрстки: без горизонтального скролла страницы, * длинные строки переносятся, текстовые флекс-элементы умеют сжиматься */ .kt-ai-app, .kt-ai-shell { overflow-x: hidden; } .kt-ai-app .kt-ai-chip, .kt-ai-shell .kt-ai-chip { min-width: 0; } .kt-ai-card, .kt-ai-row, .kt-ai-banner { overflow-wrap: anywhere; } .kt-ai-shell { grid-template-columns: 1fr; } .kt-ai-sidebar { position: fixed; top: 0; left: 0; bottom: 0; width: var(--kt-ai-sidebar-width); transform: translateX(-100%); transition: transform var(--kt-ai-dur-slow) var(--kt-ai-ease-standard); z-index: var(--kt-ai-z-drawer); } .kt-ai-sidebar[data-open="true"] { transform: none; box-shadow: var(--kt-ai-shadow-lg); } .kt-ai-sidebar .nav-label, .kt-ai-sidebar .kt-ai-nav-item .count, .kt-ai-sidebar .brand-label { display: initial; } .kt-ai-sidebar .kt-ai-nav-item { justify-content: flex-start; padding: 0 var(--kt-ai-space-4); } .kt-ai-drawer { width: 100vw; max-width: 100vw; } .kt-ai-work-column, .kt-ai-work-column-wide, .kt-ai-work-column-table { padding-left: var(--kt-ai-space-5); padding-right: var(--kt-ai-space-5); } } @keyframes ktai-spin { to { transform: rotate(360deg); } } /* ---------- Theme toggle: иконка активной темы едет внутри бегунка ---------- */ .kt-ai-theme-toggle { position: relative; width: 52px; height: 28px; flex-shrink: 0; border-radius: var(--kt-ai-radius-full); border: 1px solid var(--kt-ai-border-strong); background: var(--kt-ai-bg-active); cursor: pointer; padding: 0; transition: background var(--kt-ai-dur-base), border-color var(--kt-ai-dur-base); } .kt-ai-theme-toggle:hover { border-color: var(--kt-ai-primary); } .kt-ai-theme-toggle[data-mode="dark"] { background: var(--kt-ai-bg-active); } .kt-ai-theme-toggle[data-mode="light"] { background: var(--kt-ai-primary-subtle); } .kt-ai-theme-toggle .thumb { position: absolute; top: 2px; left: 2px; width: 22px; height: 22px; border-radius: 50%; background: var(--kt-ai-bg-elevated); box-shadow: var(--kt-ai-shadow-sm); display: flex; align-items: center; justify-content: center; transition: transform var(--kt-ai-dur-slow) var(--kt-ai-ease-standard); } .kt-ai-theme-toggle[data-mode="dark"] .thumb { transform: translateX(24px); } .kt-ai-theme-toggle .ic { position: absolute; width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: opacity var(--kt-ai-dur-base), transform var(--kt-ai-dur-slow) var(--kt-ai-ease-standard); } .kt-ai-theme-toggle .ic-moon { color: var(--kt-ai-primary); } .kt-ai-theme-toggle .ic-sun { color: var(--kt-ai-status-warn-fg); } .kt-ai-theme-toggle[data-mode="dark"] .ic-sun { opacity: 0; transform: rotate(90deg) scale(.4); } .kt-ai-theme-toggle[data-mode="dark"] .ic-moon { opacity: 1; transform: none; } .kt-ai-theme-toggle[data-mode="light"] .ic-moon { opacity: 0; transform: rotate(-90deg) scale(.4); } .kt-ai-theme-toggle[data-mode="light"] .ic-sun { opacity: 1; transform: none; } .kt-ai-theme-toggle .tic { position: absolute; top: 50%; transform: translateY(-50%); width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; color: var(--kt-ai-fg-faint); transition: opacity .2s; } .kt-ai-theme-toggle .tic-sun { left: 7px; } .kt-ai-theme-toggle .tic-moon { right: 7px; } .kt-ai-theme-toggle[data-mode="dark"] .tic-moon { opacity: 0; } /* спрятана под бегунком */ .kt-ai-theme-toggle[data-mode="light"] .tic-sun { opacity: 0; } .kt-ai-theme-toggle .thumb { z-index: 1; } /* ============ v2.1.1 EDGE-КЕЙСЫ ============ */ /* Выделение текста */ .kt-ai-app ::selection, .kt-ai-shell ::selection { background: var(--kt-ai-selection); } /* Усечение текста (локализация: каз. строки длиннее) */ .kt-ai-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .kt-ai-truncate-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } /* Firefox-скроллбары */ .kt-ai-app, .kt-ai-shell { scrollbar-width: thin; scrollbar-color: var(--kt-ai-border-strong) transparent; } /* Tooltip: позиция и перенос длинного текста */ .kt-ai-tooltip[data-tip-pos="bottom"]::after { bottom: auto; top: calc(100% + 6px); } .kt-ai-tooltip[data-tip-pos="left"]::after { bottom: auto; left: auto; right: calc(100% + 6px); top: 50%; transform: translateY(-50%); } .kt-ai-tooltip[data-tip-pos="right"]::after { bottom: auto; left: calc(100% + 6px); top: 50%; transform: translateY(-50%); } .kt-ai-tooltip[data-tip-wrap]::after { white-space: normal; max-width: 240px; width: max-content; } /* Combobox внутри overflow-контейнера обрежется – раскрытие вверх */ .kt-ai-combobox[data-direction="up"] .menu { top: auto; bottom: calc(100% + 4px); } /* Windows High Contrast / forced-colors */ @media (forced-colors: active) { .kt-ai-btn, .kt-ai-input, .kt-ai-select, .kt-ai-textarea, .kt-ai-card, .kt-ai-modal { border: 1px solid ButtonText; } .kt-ai-status-pill, .kt-ai-chip { border: 1px solid ButtonText; } } /* Reduced motion: стоп бесконечным анимациям (длительности-токены уже обнулены) */ @media (prefers-reduced-motion: reduce) { .kt-ai-skeleton { animation: none; background: var(--kt-ai-bg-active); } .kt-ai-btn[data-loading="true"]::after { animation-duration: 1.5s; } /* спиннер оставляем – это статус, не декорация */ } /* Безопасные отступы iOS (notch) для фиксированных элементов */ /* ============ v2.2 ДОПОЛНЕНИЯ (ревью 2026-06-11) ============ */ /* ---------- Topbar: канонический glass-хром ---------- * Blur разрешён ТОЛЬКО на sticky-хроме и overlay, в контенте запрещён. */ .kt-ai-topbar { position: sticky; top: 0; z-index: var(--kt-ai-z-sticky); display: flex; align-items: center; gap: var(--kt-ai-space-5); height: 56px; padding: 0 var(--kt-ai-space-7); background: var(--kt-ai-bg-glass); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border-bottom: 1px solid var(--kt-ai-divider); } .kt-ai-topbar .spacer { flex: 1; } /* ---------- AI-диалог ---------- * Ассистент – полная ширина без пузыря; пользователь – компактный пузырь. * AI-контент всегда атрибутирован (.kt-ai-gen-label). */ .kt-ai-chat { display: flex; flex-direction: column; gap: var(--kt-ai-space-6); } .kt-ai-msg { display: flex; gap: var(--kt-ai-space-4); font-size: var(--kt-ai-text-md); line-height: var(--kt-ai-leading-normal); } /* v3.7: орб отправителя у КАЖДОГО сообщения ассистента (интро и ответы идентичны) */ .kt-ai-msg .msg-orb { width: 28px; height: 28px; border-radius: 50%; background: var(--kt-ai-ai-orb); flex-shrink: 0; margin-top: 1px; } /* v3.7: чат со своим скроллом — раскладка статична, растут только сообщения */ .kt-ai-chat[data-fill="true"] { flex: 1 1 auto; min-height: 0; overflow-y: auto; scroll-behavior: smooth; padding-right: var(--kt-ai-space-2); } /* v4.0 (Michelle): пустой диалог – приветствие в оптическом центре, а не сирота в углу */ .kt-ai-chat[data-fill="true"][data-empty="true"] { justify-content: center; } .kt-ai-chat[data-scroll="true"] { max-height: 420px; overflow-y: auto; scroll-behavior: smooth; padding-right: var(--kt-ai-space-2); } .kt-ai-msg .body { min-width: 0; max-width: 100%; } /* Ответ ассистента — полная ширина без пузыря. Рамка вокруг длинного ответа ничего не сообщает: роль уже закодирована выравниванием и орбом, а карточка внутри карточки диалога — свой же красный флаг. Пузырь остаётся только у пользователя, где реплики короткие и «свой/чужой» читается формой. */ .kt-ai-msg[data-role="assistant"] .body { max-width: 100%; padding-block: var(--kt-ai-space-3); } .kt-ai-msg[data-role="user"] { justify-content: flex-end; } .kt-ai-msg[data-role="user"] .body { max-width: 76%; background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); border-top-right-radius: var(--kt-ai-radius-sm); padding: var(--kt-ai-space-4) var(--kt-ai-space-6); } .kt-ai-msg[data-streaming="true"] .body::after { content: ""; display: inline-block; width: 8px; height: 14px; margin-left: var(--kt-ai-space-2); vertical-align: -2px; background: var(--kt-ai-ai-gradient); border-radius: var(--kt-ai-radius-xs); animation: ktai-caret 1s steps(2) infinite; } @keyframes ktai-caret { 50% { opacity: 0; } } /* Пилюли навыков агента над полем ввода. * * Идея из ai-agent-input (aicss.dev): выбранный навык — не текст, который можно * случайно надкусить бэкспейсом, а объект, удаляемый целиком. Там он вставлен * ВНУТРЬ строки, но это возможно только в contenteditable, а у нас textarea. * Переписывать композер на contenteditable ради этого нельзя: ломается undo, * ввод через IME, вставка из буфера, доступность и мобильные клавиатуры — * слишком дорого за одну возможность. Пилюли живут НАД полем, как вложения: * то же свойство «удаляется целиком», без единого из перечисленных рисков. */ .kt-ai-prompt-skills { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-3); width: 100%; padding: 0 var(--kt-ai-space-2) var(--kt-ai-space-3); } /* Не :empty — он не сработает, если в контейнере остались пробельные узлы от форматирования разметки, а это обычное дело. Проверяем ЭЛЕМЕНТЫ. */ .kt-ai-prompt-skills:not(:has(*)) { display: none; } .kt-ai-prompt-skill { display: inline-flex; align-items: center; gap: var(--kt-ai-space-2); height: 22px; padding: 0 var(--kt-ai-space-2) 0 var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-tag); background: var(--kt-ai-chip-purple-bg); color: var(--kt-ai-chip-purple-fg); font-size: var(--kt-ai-text-2xs); font-weight: var(--kt-ai-weight-medium); white-space: nowrap; } .kt-ai-prompt-skill button { display: grid; place-items: center; width: 16px; height: 16px; border-radius: var(--kt-ai-radius-full); color: inherit; opacity: .6; } .kt-ai-prompt-skill button:hover { opacity: 1; background: var(--kt-ai-bg-hover); } /* Меню slash-команд. Открывается НАД полем: композер обычно стоит внизу экрана, и список, растущий вниз, уехал бы за край. */ .kt-ai-slash-menu { position: absolute; bottom: calc(100% + var(--kt-ai-space-3)); left: 0; min-width: 260px; max-width: 360px; max-height: 240px; overflow-y: auto; padding: var(--kt-ai-space-3); background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); box-shadow: var(--kt-ai-shadow-lg); z-index: var(--kt-ai-z-dropdown); } .kt-ai-slash-menu[hidden] { display: none; } .kt-ai-slash-item { display: flex; flex-direction: column; gap: 1px; width: 100%; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-md); text-align: left; font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg); } .kt-ai-slash-item:hover, .kt-ai-slash-item[data-active="true"] { background: var(--kt-ai-bg-hover); } .kt-ai-prompt-bar { position: relative; display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-3) var(--kt-ai-space-4); background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-3xl); transition: border-color var(--kt-ai-dur-fast) var(--kt-ai-ease-standard), box-shadow var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); } /* AI-сигнатура: тихая градиентная кромка в фокусе (Mono: без тяжёлого ореола) */ .kt-ai-prompt-bar:focus-within { border-color: transparent; background: linear-gradient(var(--kt-ai-bg-soft), var(--kt-ai-bg-soft)) padding-box, var(--kt-ai-ai-gradient) border-box; box-shadow: 0 0 0 3px color-mix(in srgb, var(--kt-ai-ai-accent) 12%, transparent); } .kt-ai-prompt-bar textarea { flex: 1; min-width: 0; border: none; background: transparent; resize: none; outline: none; color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); font-size: var(--kt-ai-text-md); line-height: var(--kt-ai-leading-normal); padding: var(--kt-ai-space-3) var(--kt-ai-space-2); min-height: var(--kt-ai-control-h-md); max-height: 160px; } .kt-ai-prompt-bar textarea::placeholder { color: var(--kt-ai-fg-faint); } .kt-ai-prompt-bar[data-busy="true"] { opacity: .7; } .kt-ai-prompt-bar[data-busy="true"] textarea { pointer-events: none; } .kt-ai-prompt-send { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: var(--kt-ai-control-h-md); height: var(--kt-ai-control-h-md); border: none; border-radius: var(--kt-ai-radius-full); cursor: pointer; background: var(--kt-ai-ai-gradient); color: var(--kt-ai-fg-on-ai); box-shadow: inset 0 1px 0 var(--kt-ai-inset-highlight); transition: filter var(--kt-ai-dur-fast) var(--kt-ai-ease-standard), transform 60ms var(--kt-ai-ease-standard); } .kt-ai-prompt-send:hover { filter: saturate(1.15) brightness(1.06); } .kt-ai-prompt-send:active { transform: scale(.94); } .kt-ai-prompt-send:disabled { opacity: .4; cursor: default; } .kt-ai-suggestions { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-4); } .kt-ai-suggestion { display: inline-flex; align-items: center; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-3) var(--kt-ai-space-5); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-full); background: transparent; color: var(--kt-ai-fg-muted); font-family: var(--kt-ai-font-sans); font-size: var(--kt-ai-text-xs); cursor: pointer; transition: border-color var(--kt-ai-dur-fast), color var(--kt-ai-dur-fast), background var(--kt-ai-dur-fast); } .kt-ai-suggestion:hover { border-color: var(--kt-ai-primary); color: var(--kt-ai-fg); background: var(--kt-ai-primary-subtle); } .kt-ai-source-pill { display: inline-flex; align-items: center; gap: var(--kt-ai-space-2); padding: 1px var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-full); background: var(--kt-ai-primary-subtle); color: var(--kt-ai-link); font-size: var(--kt-ai-text-2xs); text-decoration: none; border: 1px solid transparent; white-space: nowrap; } .kt-ai-source-pill:hover { border-color: var(--kt-ai-primary); } .kt-ai-gen-label { display: inline-flex; align-items: center; gap: var(--kt-ai-space-2); font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-faint); } .kt-ai-gen-label::before { content: ""; width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; background: var(--kt-ai-ai-orb); } /* AI-вход: кнопка-сигнатура (точка входа Төре/чата) */ .kt-ai-btn[data-ai="true"] { background: var(--kt-ai-ai-gradient); border-color: transparent; color: var(--kt-ai-fg-on-ai); font-weight: var(--kt-ai-weight-semibold); box-shadow: var(--kt-ai-shadow-sm), inset 0 1px 0 var(--kt-ai-inset-highlight), 0 2px 10px color-mix(in srgb, var(--kt-ai-ai-accent) 16%, transparent); } .kt-ai-btn[data-ai="true"]:hover { background: var(--kt-ai-ai-gradient); filter: saturate(1.15) brightness(1.06); } /* ---------- Dropdown-меню действий ---------- */ .kt-ai-menu-anchor { position: relative; display: inline-block; } .kt-ai-menu { position: absolute; top: calc(100% + 4px); right: 0; min-width: 180px; background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-lg); box-shadow: var(--kt-ai-shadow-md); padding: var(--kt-ai-space-2); z-index: var(--kt-ai-z-dropdown); display: none; } .kt-ai-menu[data-open="true"] { display: block; animation: ktai-rise var(--kt-ai-dur-base) var(--kt-ai-ease-enter); } .kt-ai-menu .item { display: flex; align-items: center; gap: var(--kt-ai-space-4); width: 100%; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border: none; background: transparent; text-align: left; cursor: pointer; color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); font-size: var(--kt-ai-text-sm); border-radius: var(--kt-ai-radius-md); } .kt-ai-menu .item:hover, .kt-ai-menu .item[data-active="true"] { background: var(--kt-ai-bg-hover); } .kt-ai-menu .item[data-danger="true"] { color: var(--kt-ai-danger); } .kt-ai-menu .item .kt-ai-kbd { margin-left: auto; } .kt-ai-menu .sep { height: 1px; background: var(--kt-ai-divider); margin: var(--kt-ai-space-2) 0; } /* ---------- Command palette: внутренности (⌘K) ---------- */ .kt-ai-command-overlay { position: fixed; inset: 0; background: var(--kt-ai-overlay); display: flex; justify-content: center; align-items: flex-start; padding-top: 12vh; z-index: var(--kt-ai-z-modal); animation: ktai-fade var(--kt-ai-dur-base) var(--kt-ai-ease-enter); } .kt-ai-command .cmd-input { width: 100%; border: none; background: transparent; outline: none; padding: var(--kt-ai-space-5) var(--kt-ai-space-6); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); font-size: var(--kt-ai-text-md); border-bottom: 1px solid var(--kt-ai-divider); } .kt-ai-command .cmd-input::placeholder { color: var(--kt-ai-fg-faint); } .kt-ai-command .cmd-list { max-height: 320px; overflow-y: auto; padding: var(--kt-ai-space-2); } .kt-ai-command .cmd-group { padding: var(--kt-ai-space-3) var(--kt-ai-space-4) var(--kt-ai-space-1); font-size: var(--kt-ai-text-2xs); font-weight: var(--kt-ai-weight-semibold); text-transform: uppercase; letter-spacing: .8px; color: var(--kt-ai-fg-faint); } .kt-ai-command .cmd-item { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-md); cursor: pointer; font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg); } .kt-ai-command .cmd-item:hover, .kt-ai-command .cmd-item[data-active="true"] { background: var(--kt-ai-bg-hover); } .kt-ai-command .cmd-item .kt-ai-kbd { margin-left: auto; } .kt-ai-command .cmd-empty { padding: var(--kt-ai-space-8); text-align: center; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); } /* ---------- Filter-chips: применённые фильтры над таблицей ---------- */ .kt-ai-filterbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-3) 0; } /* v6.2 (DS-007): внутри стека вертикальный padding снимается — отбивку задаёт стек, как у .kt-ai-disclosure. Пока padding оставался, ступень ритма врала: `tight` (12) превращалась на экране в 12+6+6 = 24, то есть в блоковый зазор, и панель фильтров разъезжалась на два независимых блока. Продукт лечил это своим классом-обёрткой с gap: 0 — ровно тот обход ДС, который канон запрещает. Собственный padding остаётся для бара вне стека (например под тулбаром таблицы), где иначе он прилипнет к содержимому. */ .kt-ai-v > .kt-ai-filterbar { padding-block: 0; } /* Панель шага: строка фильтров + строка вкладок под ней — ОДНА вещь. Канонная обёртка, чтобы каждый продукт не изобретал свою: это уже случилось в консолидации (.app-toolbar) и было бы повторено следующим. */ .kt-ai-toolbar { display: flex; flex-direction: column; gap: var(--kt-ai-stack-tight); } .kt-ai-toolbar > .kt-ai-filterbar { padding-block: 0; } /* Селект или инпут внутри фильтр-бара — по содержимому, а не на всю строку: width:100% у форм рассчитан на .kt-ai-field, во flex-wrap он выталкивает каждый контрол на отдельную строку, и бар рассыпается в стопку. */ .kt-ai-filterbar :is(.kt-ai-select, .kt-ai-input) { width: auto; } .kt-ai-filter-chip { display: inline-flex; align-items: center; gap: var(--kt-ai-space-2); padding: 2px var(--kt-ai-space-4); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-tag); background: transparent; font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg); white-space: nowrap; } .kt-ai-filter-chip .k { color: var(--kt-ai-fg-muted); font-family: var(--kt-ai-font-sans); } .kt-ai-filter-chip .remove { cursor: pointer; opacity: .6; padding: 0 3px; border-radius: 50%; } .kt-ai-filter-chip .remove:hover { opacity: 1; background: var(--kt-ai-bg-active); } .kt-ai-filterbar .clear-all { border: none; background: transparent; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-2xs); cursor: pointer; padding: 2px var(--kt-ai-space-3); border-radius: var(--kt-ai-radius-sm); } .kt-ai-filterbar .clear-all:hover { color: var(--kt-ai-fg); background: var(--kt-ai-bg-hover); } /* Активная вкладка-фильтр: выбранная категория обязана быть видимой — иначе пользователь не знает, какой срез читает. */ .kt-ai-filter-chip[data-active="true"] { background: var(--kt-ai-primary-subtle); border-color: var(--kt-ai-primary); color: var(--kt-ai-primary); font-weight: var(--kt-ai-weight-semibold); } /* ---------- Radio ---------- */ .kt-ai-radio { display: inline-flex; align-items: center; gap: var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); cursor: pointer; } .kt-ai-radio input { width: 16px; height: 16px; accent-color: var(--kt-ai-primary); } .kt-ai-radio-group { display: flex; flex-direction: column; gap: var(--kt-ai-space-3); } /* ---------- Toast: действие (undo) и закрытие ---------- */ .kt-ai-toast .action { margin-left: auto; border: none; background: transparent; color: var(--kt-ai-link); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); cursor: pointer; padding: 2px var(--kt-ai-space-3); border-radius: var(--kt-ai-radius-sm); white-space: nowrap; } .kt-ai-toast .action:hover { background: var(--kt-ai-bg-hover); } .kt-ai-toast .close { border: none; background: transparent; color: var(--kt-ai-fg-faint); cursor: pointer; padding: 2px; border-radius: var(--kt-ai-radius-sm); line-height: 0; flex-shrink: 0; } .kt-ai-toast .close:hover { color: var(--kt-ai-fg); } .kt-ai-toast .action + .close { margin-left: 0; } .kt-ai-toast .close:only-of-type { margin-left: auto; } /* ---------- Popover: якорный интерактивный слой ---------- */ .kt-ai-popover-anchor { position: relative; display: inline-block; } .kt-ai-popover { position: absolute; top: calc(100% + 4px); left: 0; min-width: 220px; background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-lg); box-shadow: var(--kt-ai-shadow-md); padding: var(--kt-ai-space-5); z-index: var(--kt-ai-z-dropdown); display: none; } .kt-ai-popover[data-open="true"] { display: block; animation: ktai-rise var(--kt-ai-dur-base) var(--kt-ai-ease-enter); } .kt-ai-popover[data-align="right"] { left: auto; right: 0; } /* ---------- Плавающий bulk-bar ---------- */ .kt-ai-table-bulkbar[data-floating="true"] { position: fixed; left: 50%; transform: translateX(-50%); bottom: max(var(--kt-ai-space-8), env(safe-area-inset-bottom)); background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border-strong); border-radius: var(--kt-ai-radius-full); box-shadow: var(--kt-ai-shadow-lg); padding: var(--kt-ai-space-3) var(--kt-ai-space-6); z-index: var(--kt-ai-z-sticky); } /* ---------- Row actions: тихие действия по hover/focus ---------- */ .kt-ai-row .actions { margin-left: auto; display: flex; align-items: center; gap: var(--kt-ai-space-2); opacity: 0; transition: opacity var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); } .kt-ai-row:hover .actions, .kt-ai-row:focus-within .actions, .kt-ai-row[data-active="true"] .actions { opacity: 1; } .kt-ai-table tbody tr .actions { display: flex; align-items: center; gap: var(--kt-ai-space-2); opacity: 0; transition: opacity var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); } .kt-ai-table tbody tr:hover .actions, .kt-ai-table tbody tr:focus-within .actions { opacity: 1; } /* Мобильный card-режим ДОЛЖЕН идти после десктопных правил таблицы: специфичность у них одинаковая, медиазапрос её не повышает, поэтому решает порядок. Раньше блок стоял выше и десктопные height/padding протекали в карточки — ячейки распирало до 48px вместо ~20px. */ @media (max-width: 639.98px) { .kt-ai-table { min-width: 0; display: block; } .kt-ai-table thead, .kt-ai-table colgroup, .kt-ai-table .cell-select { display: none; } .kt-ai-table tbody { display: block; } .kt-ai-table tr { position: relative; display: block; border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); background: var(--kt-ai-bg-elevated); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); margin-bottom: var(--kt-ai-space-4); } .kt-ai-table tr:hover { background: var(--kt-ai-bg-elevated); } .kt-ai-table td { display: flex; align-items: baseline; justify-content: space-between; gap: var(--kt-ai-space-5); border: none; padding: var(--kt-ai-space-2) 0; text-align: right; height: auto; } .kt-ai-table td::before { content: attr(data-label); flex: 0 0 auto; text-align: left; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); } /* Без data-label карточный режим оставлял пустую колонку слева, а значение улетало вправо — обычная таблица выглядела сломанной, и подсказки не было. Теперь ячейка без подписи просто выравнивается по левому краю. */ .kt-ai-table td:not([data-label])::before { content: none; } .kt-ai-table td:not([data-label]) { justify-content: flex-start; text-align: left; } .kt-ai-table td:nth-child(2) { display: block; padding: 0 40px var(--kt-ai-space-3) 0; margin-bottom: var(--kt-ai-space-2); border-bottom: 1px solid var(--kt-ai-divider); text-align: left; font-weight: var(--kt-ai-weight-semibold); } .kt-ai-table td:nth-child(2)::before { display: none; } .kt-ai-table td:nth-child(2) .cell-sub { font-weight: var(--kt-ai-weight-regular); } .kt-ai-table .row-act { position: absolute; top: var(--kt-ai-space-3); right: var(--kt-ai-space-4); padding: 0; } .kt-ai-table .row-act::before { display: none; } .kt-ai-table .row-kebab { opacity: 1; } } @media (pointer: coarse) { .kt-ai-row .actions, .kt-ai-table tbody tr .actions { opacity: 1; } /* hover ненадёжен на touch */ } /* ============ v2.3 «Aurora × Attio» (2026-06-12) ============ */ /* Трекинг заголовков перенесён в их основные определения выше: держать его отдельным правилом значит определять класс дважды, и правка в основном определении молча не срабатывает. */ /* Mono-голос: технические подписи */ /* .kt-ai-label отсюда убран: он определялся дважды, и это правило молча перебивало гарнитуру, вес и разрядку из основного определения — правка там не давала эффекта. Всё про label теперь в одном месте, выше. */ .kt-ai-command .cmd-group { font-family: var(--kt-ai-font-mono); font-weight: var(--kt-ai-weight-medium); letter-spacing: .4px; } .kt-ai-notif-item .time { font-family: var(--kt-ai-font-sans); font-variant-numeric: tabular-nums;} /* Тихая ссылка «Подробнее →» */ .kt-ai-link[data-arrow] { color: var(--kt-ai-fg-muted); } .kt-ai-link[data-arrow]::after { content: " →"; display: inline-block; transition: transform var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); } .kt-ai-link[data-arrow]:hover { color: var(--kt-ai-fg); text-decoration: none; } .kt-ai-link[data-arrow]:hover::after { transform: translateX(2px); } /* Dot-grid текстура пустых состояний */ /* Property-row (Attio): детали объекта в drawer/карточке */ .kt-ai-prop { display: flex; align-items: center; gap: var(--kt-ai-space-4); min-height: 30px; font-size: var(--kt-ai-text-sm); } /* Ключ фиксировался на 148px жёстко: на панели 260px это 65% строки, и значению оставалось 70px. Ограничение по доле снимает случай узкой панели, не трогая обычную. */ .kt-ai-prop .k { display: flex; align-items: center; gap: var(--kt-ai-space-3); flex: 0 0 148px; max-width: 40%; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs); } .kt-ai-prop .v { min-width: 0; flex: 1 1 auto; overflow-wrap: anywhere; display: flex; align-items: center; gap: var(--kt-ai-space-3); flex-wrap: wrap; } .kt-ai-prop .v[data-empty="true"] { color: var(--kt-ai-fg-faint); } } /* @layer kt-components */ /* ── Сенсорная ширина: цель нажатия 44px (вне @layer — правило-порог) ─────── */ /* v5.8.0. Правило ДС: на сенсорной ширине (≤768px или указатель coarse) ни один интерактивный элемент не ниже 44px — минимальной цели, в которую попадают пальцем. Цена промаха измеряется не пикселями: рядом стоящие «Принять» и «Отклонить» в 24–32px дают чужое решение, отменять которое придётся отдельным действием. Токены это уже умели (@media pointer:coarse в kt-ai-tokens.css поднимает control-h), но покрывали только те компоненты, что берут высоту из токена. У пилюли-фильтра высоты нет вовсе — она складывается из padding и кегля 11px, и на телефоне оставалась 24px. Условие тоже было одно, `pointer: coarse`: на ноутбуке, суженном до 375px, правило молчало — а проверяют продукт именно так. Порог задаётся ОДНИМ токеном --kt-ai-control-h-touch, а не числом в каждом правиле: иначе «44» разъедется по файлу, как уже было с радиусами. Ширина остаётся по содержимому: цель — высота строки, растягивать пилюли по горизонтали значит ломать плотность списка фильтров. Для квадратных целей (иконка, чекбокс) ставится и min-width. */ @media (pointer: coarse), (max-width: 767.98px) { .kt-ai-filter-chip, .kt-ai-filterbar .clear-all, .kt-ai-tab, .kt-ai-segmented .seg, .kt-ai-suggestion, .kt-ai-switch-row { min-height: var(--kt-ai-control-h-touch); } /* Крестик внутри пилюли — самостоятельная цель: промах по нему снимает не тот фильтр, и специалист этого не замечает. Высоту он берёт РАСТЯЖЕНИЕМ по пилюле (align-self: stretch), а не своим min-height: иначе 44px крестика складывались бы с рамкой пилюли и та вырастала до 50px — цель выполнена, а список фильтров стал в два раза выше. */ .kt-ai-filter-chip .remove { min-width: var(--kt-ai-control-h-touch); align-self: stretch; display: inline-flex; align-items: center; justify-content: center; } .kt-ai-toast .close, .kt-ai-toast .action { min-width: var(--kt-ai-control-h-touch); min-height: var(--kt-ai-control-h-touch); display: inline-flex; align-items: center; justify-content: center; } .kt-ai-checkbox, .kt-ai-radio { min-height: var(--kt-ai-control-h-touch); min-width: var(--kt-ai-control-h-touch); justify-content: center; } /* Свитч: трек остаётся 34×20 — это индикатор состояния, раздувать его значит менять внешний вид ради цели нажатия. Растёт сама рамка контрола, трек центруется внутри. Через ::after поверх это не решается: невидимая область 44px торчала бы за пределы 34px-контрола и добавляла 5px к ширине страницы — правило про цель нажатия породило бы горизонтальную прокрутку, ровно ту, которую чинит T-176. Проверено замером: scrollWidth рос на 5px. */ .kt-ai-switch { min-width: var(--kt-ai-control-h-touch); min-height: var(--kt-ai-control-h-touch); } .kt-ai-switch .track { inset: auto; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 34px; height: 20px; } /* React-кит размечен утилитами Tailwind, а не классами .kt-ai-*: правила выше до него не достают, и порог пришлось бы дописывать к каждой из ~70 кнопок — то есть заводить 70 мест, где его можно забыть. Поэтому порог задаётся один раз на поддереве кита: [data-kt-ai-kit] ставят KTAIShell и KTScreen. min-height ПОБЕЖДАЕТ height по спецификации CSS — h-7 (28px) от Tailwind остаётся в разметке и просто перестаёт действовать на сенсорной ширине. Порог общий и для продуктового кода внутри оболочки: правило про цель нажатия не делит кнопки на свои и чужие. Осознанное исключение (плотный инлайн-контрол, который обязан быть меньше) помечается data-kt-touch="off" — явным отказом, а не молчанием. */ [data-kt-ai-kit] :is(button, a[href], [role="button"], [role="link"], [role="menuitem"], [role="tab"], summary):not([data-kt-touch="off"]) { min-height: var(--kt-ai-control-h-touch); } /* Подложка-ловушка клика (fixed inset-0) — не цель нажатия, а способ поймать промах мимо меню. Порог ей не нужен, а min-height на inset-0 безвреден, но врал бы в замерах: элемент не виден и целью не является. */ [data-kt-ai-kit] button[aria-hidden="true"], [data-kt-ai-kit] button[aria-hidden] { min-height: 0; } } /* ── Доступность (вне @layer, чтобы надёжно переопределять) ───────────────── */ /* Уважать «уменьшить движение»: глушим анимации/переходы, но не ломаем функциональность. */ @media (prefers-reduced-motion: reduce) { .kt-ai-app *, .kt-ai-app *::before, .kt-ai-app *::after, .kt-ai-toast, .kt-ai-drawer, .kt-ai-menu, .kt-ai-modal, .kt-ai-popover, .kt-ai-command { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } /* Повышенный контраст: толще фокус-кольцо, явные границы и подчёркнутые ссылки. */ @media (prefers-contrast: more) { .kt-ai-app :focus-visible, .kt-ai-shell :focus-visible { outline-width: 3px; outline-offset: 2px; } .kt-ai-app { --kt-ai-border: var(--kt-ai-border-strong); } .kt-ai-app .kt-ai-link { text-decoration: underline; } .kt-ai-status-pill { outline: 1px solid color-mix(in srgb, currentColor 40%, transparent); } } /* Меньше прозрачности: blur на sticky-хроме падает, фон становится плотным (легибельность важнее эффекта). */ @media (prefers-reduced-transparency: reduce) { .kt-ai-topbar { -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--kt-ai-bg-elevated); } } /* Утилита для скринридеров: видимо только для AT (для live-region'ов и подписей). */ /* Блокировка прокрутки фона, пока открыт модал/drawer. Вешать на (не на body) при открытии оверлея, снимать при закрытии: document.documentElement.classList.toggle('kt-ai-scroll-locked', open) Без этого страница продолжает скроллиться ПОД оверлеем — проверено поведением. Про рывок вбок: на Windows/Linux скроллбар занимает место, и когда он исчезает, контент прыгает. Лечится `scrollbar-gutter: stable` на прокручиваемом элементе — в kt-ai-page.css это уже сделано для страницы; в своём приложении поставь его на тот элемент, который реально скроллится. */ .kt-ai-scroll-locked { overflow: hidden; } .kt-ai-sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* ============================================================ ПРИМИТИВЫ РАСКЛАДКИ — чтобы ДС принимала интерфейсы, которых мы не предвидели. Компоненты покрывают известные случаи; примитивы дают собрать НОВЫЙ экран из шкал системы, а не из инлайн-стилей. Замерено на реальном экране: без них разработчик пишет ~40 инлайновых правил (12 из них — вертикальные отступы), и ритм расходится с системой уже на втором экране. Всё на токенах: правка шкалы в tokens.json меняет раскладку везде. ============================================================ */ /* Вертикальный стек: заменяет marginTop у каждого потомка. */ .kt-ai-v { display: flex; flex-direction: column; gap: var(--kt-ai-space-6); } .kt-ai-v[data-gap="xs"] { gap: var(--kt-ai-space-2); } .kt-ai-v[data-gap="sm"] { gap: var(--kt-ai-space-4); } .kt-ai-v[data-gap="lg"] { gap: var(--kt-ai-space-8); } .kt-ai-v[data-gap="xl"] { gap: var(--kt-ai-space-10); } /* v6.1: ступени ритма прямо на стеке. Без них экран группировался только своими классами продукта (`app-stack-*`), а чаще не группировался вовсе: корневой столбец получал один зазор, и панель фильтров, её вкладки и карточки списка расходились на одно и то же расстояние. Равномерность — не ритм: одинаковый зазор на всё говорит «здесь ничто ни с чем не связано». */ .kt-ai-v[data-gap="tight"] { gap: var(--kt-ai-stack-tight); } .kt-ai-v[data-gap="block"] { gap: var(--kt-ai-stack-block); } .kt-ai-v[data-gap="group"] { gap: var(--kt-ai-stack-group); } .kt-ai-v[data-gap="region"] { gap: var(--kt-ai-stack-region); } /* Горизонтальный ряд. Не путать с .kt-ai-row — та строка списка со своим паддингом. */ .kt-ai-h-stack { display: flex; align-items: center; gap: var(--kt-ai-space-4); flex-wrap: wrap; } .kt-ai-h-stack[data-gap="xs"] { gap: var(--kt-ai-space-2); } .kt-ai-h-stack[data-gap="lg"] { gap: var(--kt-ai-space-6); } .kt-ai-h-stack[data-gap="tight"] { gap: var(--kt-ai-stack-tight); } .kt-ai-h-stack[data-gap="block"] { gap: var(--kt-ai-stack-block); } .kt-ai-h-stack[data-align="start"] { align-items: flex-start; } .kt-ai-h-stack[data-align="baseline"] { align-items: baseline; } .kt-ai-h-stack[data-nowrap="true"] { flex-wrap: nowrap; } /* v5.8.1: ряд, которому запрещено переноситься, в узкой колонке выходит за родителя и толкает горизонтальную прокрутку ВСЕЙ страницы — тот же дефект, что чинил T-176, только источник другой. Замерено на 375px: страница расширялась до 700px. Само `nowrap` этого не лечит и не должно: значение `true` оставлено как есть, потому что прокручиваемый ряд — это НЕ бесплатная надстройка. `overflow-x: auto` по спецификации вынуждает `overflow-y` стать `auto` тоже, а значит ряд начинает обрезать всё, что вылезает вверх и вниз: меню, поповер, подсказку, кольцо фокуса. Ряд с кебаб-кнопкой такой правкой сломался бы молча. Поэтому выбор явный и по имени: `scroll` — ряд прокручивает себя сам, `true` — ряд остаётся под ответственностью автора. */ .kt-ai-h-stack[data-nowrap="scroll"] { flex-wrap: nowrap; min-width: 0; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; } /* Распорка: всё после неё уходит вправо. Заменяет marginLeft:auto. */ .kt-ai-spacer { margin-left: auto; } /* Текстовые утилиты — вместо инлайновых fontSize/fontWeight/color. */ .kt-ai-t-xs { font-size: var(--kt-ai-text-xs); } .kt-ai-t-sm { font-size: var(--kt-ai-text-sm); } .kt-ai-t-md { font-size: var(--kt-ai-text-md); } .kt-ai-t-lg { font-size: var(--kt-ai-text-lg); } .kt-ai-t-xl { font-size: var(--kt-ai-text-xl); } .kt-ai-t-medium { font-weight: var(--kt-ai-weight-medium); } .kt-ai-t-semibold { font-weight: var(--kt-ai-weight-semibold); } .kt-ai-t-muted { color: var(--kt-ai-fg-muted); } .kt-ai-t-faint { color: var(--kt-ai-fg-faint); } .kt-ai-t-nowrap { white-space: nowrap; } /* Ограничители ширины — колонка чтения и формы. */ .kt-ai-narrow { max-width: 640px; } .kt-ai-prose { max-width: 68ch; } /* ============================================================ ПОМОЩНИК — боковая панель, а не всплывающий попап. Раньше React-кит рисовал окошко 340px своей вёрсткой на Tailwind, хотя классы чата (.kt-ai-chat/.kt-ai-msg/.kt-ai-prompt-bar/.kt-ai-suggestions) в ДС уже были. Панель во всю высоту — паттерн ElevenLabs/Linear: диалог не перекрывает работу, его видно вместе с экраном, композер закреплён снизу, история скроллится сама. ============================================================ */ /* Кнопка вызова. Отдельный слой от панели: при открытии кнопка не сдвигается. */ .kt-ai-fab { position: fixed; right: var(--kt-ai-space-7); bottom: var(--kt-ai-space-7); /* Ниже drawer/модалки: на одном слое кнопка накрывала их подвал с действиями. */ z-index: var(--kt-ai-z-sticky); display: inline-flex; align-items: center; gap: var(--kt-ai-space-3); height: 40px; padding: 0 var(--kt-ai-space-6); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-full); background: var(--kt-ai-bg-elevated); color: var(--kt-ai-fg); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); box-shadow: var(--kt-ai-shadow-lg); cursor: pointer; transition: background var(--kt-ai-dur-fast) var(--kt-ai-ease-standard), transform var(--kt-ai-dur-fast); } .kt-ai-fab:hover { background: var(--kt-ai-bg-hover); } .kt-ai-fab:active { transform: scale(.97); } .kt-ai-fab .orb { width: 16px; height: 16px; border-radius: 50%; background: var(--kt-ai-ai-orb); flex-shrink: 0; } /* Орб агента (kt-ai-orb.js). Цвет НЕ задаётся: скрипт рисует точки currentColor, поэтому орб темизуется теми же токенами, что и текст рядом. */ .kt-ai-orb { display: block; flex-shrink: 0; color: var(--kt-ai-fg-muted); } .kt-ai-orb[data-state="thinking"] { color: var(--kt-ai-fg); } /* Кнопка агента: только орб, без подписи. Подпись «AI» ничего не добавляла — круглая кнопка в углу и так читается как вход в помощника, а текст делал её шире и заметнее, чем ей положено быть. */ .kt-ai-fab[data-orb-only="true"] { width: 44px; padding: 0; justify-content: center; gap: 0; height: 44px; } .kt-ai-fab[data-orb-only="true"] .kt-ai-orb { width: 22px; height: 22px; } .kt-ai-assistant { position: fixed; top: 0; right: 0; bottom: 0; z-index: var(--kt-ai-z-drawer); width: 400px; max-width: 100vw; display: flex; flex-direction: column; background: var(--kt-ai-bg-elevated); border-left: 1px solid var(--kt-ai-border-strong); box-shadow: var(--kt-ai-shadow-xl); animation: ktai-slide-in var(--kt-ai-dur-slow) var(--kt-ai-ease-enter); } @keyframes ktai-slide-in { from { transform: translateX(16px); opacity: 0; } to { transform: none; opacity: 1; } } @media (prefers-reduced-motion: reduce) { .kt-ai-assistant { animation: none; } } .kt-ai-assistant-head { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-border); flex-shrink: 0; } .kt-ai-assistant-head .orb { width: 26px; height: 26px; border-radius: 50%; background: var(--kt-ai-ai-orb); flex-shrink: 0; } .kt-ai-assistant .kt-ai-chat { padding: var(--kt-ai-space-5); } .kt-ai-assistant-composer { padding: var(--kt-ai-space-5); border-top: 1px solid var(--kt-ai-border); flex-shrink: 0; } /* На узком экране помощник занимает всю ширину — иначе работать в нём невозможно. */ @media (max-width: 639.98px) { .kt-ai-assistant { width: 100vw; } .kt-ai-fab { right: var(--kt-ai-space-5); bottom: var(--kt-ai-space-5); } } /* ============================================================ РАСКРЫВАЮЩИЙСЯ БЛОК (disclosure) — карточка, которая разворачивается на месте. Зачем: для очередей, где нужно СРАВНИВАТЬ объекты, drawer плох — он закрывает список, теряет контекст и заставляет открывать/закрывать по одному. Раскрытие на месте оставляет соседей видимыми: специалист листает и разбирает подряд. Паттерн Linear/GitHub/Notion. Разметка (кнопка — чтобы работали клавиатура и screen reader): … …шапка… …детали… ============================================================ */ .kt-ai-disclosure { border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-3xl); background: var(--kt-ai-card-bg); overflow: hidden; transition: border-color var(--kt-ai-dur-base) var(--kt-ai-ease-standard); } .kt-ai-disclosure:hover { border-color: var(--kt-ai-card-border-hover); } .kt-ai-disclosure[data-open="true"] { border-color: var(--kt-ai-border-strong); } .kt-ai-disclosure + .kt-ai-disclosure { margin-top: var(--kt-ai-space-4); } /* v6.1: внутри стека отбивку задаёт стек. Соседский margin складывался с gap родителя (12 + 8 = 20), и карточки списка расходились ДАЛЬШЕ, чем список от своей панели, — иерархия переворачивалась. Правило соседей оставлено для разметки без стека, где иначе карточки слипнутся. */ .kt-ai-v > .kt-ai-disclosure + .kt-ai-disclosure { margin-top: 0; } .kt-ai-disclosure-head { display: flex; align-items: flex-start; gap: var(--kt-ai-space-5); width: 100%; padding: var(--kt-ai-space-6); text-align: left; cursor: pointer; transition: background var(--kt-ai-dur-fast) var(--kt-ai-ease-standard); } .kt-ai-disclosure-head:hover { background: var(--kt-ai-bg-hover); } .kt-ai-disclosure-head:active { background: var(--kt-ai-bg-active); } /* Шеврон: единственная анимация — transform, как требует гейт G3. */ .kt-ai-disclosure-chevron { flex-shrink: 0; margin-top: 2px; color: var(--kt-ai-fg-faint); transition: transform var(--kt-ai-dur-base) var(--kt-ai-ease-standard); } .kt-ai-disclosure[data-open="true"] .kt-ai-disclosure-chevron { transform: rotate(90deg); } .kt-ai-disclosure-body { padding: var(--kt-ai-space-6); border-top: 1px solid var(--kt-ai-divider); animation: ktai-reveal var(--kt-ai-dur-base) var(--kt-ai-ease-enter); } @keyframes ktai-reveal { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } } @media (prefers-reduced-motion: reduce) { .kt-ai-disclosure-body { animation: none; } .kt-ai-disclosure-chevron { transition: none; } } /* ---------- Раскрытие на НАТИВНОМ ---------- .kt-ai-disclosure выше — на div-ах с data-open, и ему нужен JS. Серверному рантайму (Python/FastAPI в мониторинге качества) и любой странице без скриптов нужен тот же приём без JS, и оба продукта написали его сами: один завёл собственный .full-disclosure с гашением маркера, второй — правило `details > summary { cursor: pointer }`. Одинаковая правка в двух местах независимо друг от друга — это и есть заявка в канон. Браузер сам хранит состояние и сам его переключает; нам остаётся вид. */ .kt-ai-details { border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); background: var(--kt-ai-bg-soft); } .kt-ai-details > summary { display: flex; align-items: center; gap: var(--kt-ai-space-3); min-height: var(--kt-ai-control-h-lg); padding: 0 var(--kt-ai-space-5); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-semibold); cursor: pointer; list-style: none; } /* Треугольник ОС выглядит чужеродно рядом с волосяными линиями и разный в Safari/Chrome. Гасим его в обеих записях: ::marker — стандарт, ::-webkit-details-marker — Safari, который стандарт ещё не применяет. */ .kt-ai-details > summary::-webkit-details-marker { display: none; } .kt-ai-details > summary::marker { content: ""; } .kt-ai-details > summary:hover { color: var(--kt-ai-fg); } .kt-ai-details > summary:focus-visible { outline: 2px solid var(--kt-ai-focus-ring); outline-offset: -2px; border-radius: var(--kt-ai-radius-xl); } /* Свой шеврон — единственная анимация transform, как требует гейт G3. */ .kt-ai-details > summary .kt-ai-details-chevron { margin-left: auto; flex-shrink: 0; color: var(--kt-ai-fg-faint); transition: transform var(--kt-ai-dur-base) var(--kt-ai-ease-standard); } .kt-ai-details[open] > summary .kt-ai-details-chevron { transform: rotate(90deg); } .kt-ai-details > :not(summary) { padding: 0 var(--kt-ai-space-5) var(--kt-ai-space-5); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs); line-height: var(--kt-ai-leading-normal); } /* Одиночный вне .kt-ai-details тоже обязан выглядеть нажимаемым: курсор — единственный признак, по которому видно, что строка раскрывается. */ .kt-ai-app summary, .kt-ai-shell summary, [data-kt-ai-kit] summary { cursor: pointer; } @media (prefers-reduced-motion: reduce) { .kt-ai-details > summary .kt-ai-details-chevron { transition: none; } } /* ---------- Подвал действий боковой панели ---------- Панель разбора длинная, а решение принимают внизу: без липкого подвала «Принять» и «Отклонить» уезжают за экран, и специалист скроллит обратно каждый раз. Продукт мониторинга качества дописал это правило СНАРУЖИ, поверх класса ДС (`.kt-ai-drawer .drawer-actions`) — то есть держал чужой внутренний селектор у себя; следующая правка панели сломала бы его молча. Отрицательные поля вытягивают подвал на всю ширину панели, чтобы линия шла от края до края, а не обрывалась по паддингу. */ .kt-ai-drawer-actions { position: sticky; bottom: 0; z-index: 2; display: flex; align-items: center; gap: var(--kt-ai-space-4); margin: var(--kt-ai-space-6) calc(var(--kt-ai-space-7) * -1) calc(var(--kt-ai-space-7) * -1); padding: var(--kt-ai-space-5) var(--kt-ai-space-7); border-top: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg-elevated); flex-wrap: wrap; } @media (max-width: 639.98px) { .kt-ai-drawer-actions { margin-left: calc(var(--kt-ai-space-5) * -1); margin-right: calc(var(--kt-ai-space-5) * -1); padding-left: var(--kt-ai-space-5); padding-right: var(--kt-ai-space-5); } } /* Подвал действий внутри раскрытого блока. */ .kt-ai-disclosure-actions { display: flex; align-items: center; gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-6); padding-top: var(--kt-ai-space-5); border-top: 1px solid var(--kt-ai-divider); flex-wrap: wrap; } /* Подложка для боковой панели — НЕ модальная. Drawer открывают, чтобы разобрать объект, не теряя список: гасить экран на 32% чёрного здесь неверно, специалист перестаёт видеть соседние группы. Подложка нужна только чтобы поймать клик мимо панели. */ .kt-ai-scrim { position: fixed; inset: 0; z-index: var(--kt-ai-z-drawer); background: transparent; } /* Лёгкое затемнение только под самой панелью — отделяет её от контента, не пряча его. На узком экране панель во всю ширину, подложка не нужна. */ @media (min-width: 640px) { .kt-ai-scrim { background: color-mix(in srgb, var(--kt-ai-overlay) 28%, transparent); } }