:root {
    --border: #dcdfe4;
    --bg: #f7f8fa;
    --accent: #1a6b3c;
    --accent-dark: #114b29;
    --text: #1f2328;
    --muted: #6b7280;
    --own-product: #ffedd5;
    --own-product-strong: #f59e0b;
    /* "Свой товар вне рейтинга" — deliberately distinct from both
       --own-product (plain orange, normally ranked) and .row-mark-red's
       #fde2e1/#b3261e (the "🔴 опасный" competitor marker) — per the
       user's explicit "более красноватый оранжевый, но не красный". */
    --own-unranked: #ffd9c2;
    --own-unranked-strong: #ea580c;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    font-family: "Segoe UI", Arial, sans-serif;
    color: var(--text);
    background: var(--bg);
}

/* Full-page "loading" overlay — see base.html for when it shows/hides.
   Visible by default (no .hidden class in the initial HTML) so it covers
   the page from the very first paint, before any JS has even run. */
.page-loading-overlay {
    position: fixed; inset: 0; z-index: 9999;
    background: rgba(247, 248, 250, 0.88);
    display: flex; align-items: center; justify-content: center;
    opacity: 1; transition: opacity 0.15s ease;
}
.page-loading-overlay.hidden { opacity: 0; pointer-events: none; }
.page-loading-box {
    display: flex; flex-direction: column; align-items: center; gap: 12px;
    font-size: 0.95rem; color: var(--text); font-weight: 600;
}
.page-loading-spinner {
    width: 34px; height: 34px; border-radius: 50%;
    border: 3px solid var(--border); border-top-color: var(--accent);
    animation: page-loading-spin 0.8s linear infinite;
}
@keyframes page-loading-spin { to { transform: rotate(360deg); } }
.page-loading-gif { width: 220px; height: auto; image-rendering: pixelated; }

.topbar {
    display: flex;
    align-items: center;
    gap: 24px;
    padding: 12px 24px;
    background: var(--accent-dark);
    color: white;
}

.topbar .brand {
    font-weight: 600;
    color: white;
    text-decoration: none;
}

.topbar nav a {
    color: #d7f0e0;
    text-decoration: none;
    margin-right: 16px;
}

.topbar nav a:hover { color: white; }

.logout-form { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.nav-user { font-size: 0.82rem; color: #d7f0e0; white-space: nowrap; }
.logout-btn {
    background: transparent; border: 1px solid rgba(255, 255, 255, 0.4); color: white;
    border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 0.8rem;
}
.logout-btn:hover { background: rgba(255, 255, 255, 0.15); }

.auth-form-wrap {
    max-width: 360px; margin: 40px auto; background: white; border: 1px solid var(--border);
    border-radius: 8px; padding: 24px 28px;
}
.auth-form { display: flex; flex-direction: column; gap: 14px; margin-top: 16px; }
.auth-form label { display: flex; flex-direction: column; gap: 5px; font-size: 0.9rem; }
.auth-form input { padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 0.95rem; }
.auth-error { color: #b3261e; font-size: 0.9rem; }
.auth-links { margin-top: 14px; font-size: 0.85rem; }

/* Password field + "глазик" show/hide toggle (base.html's global click
   handler). .password-field wraps the input so the button can sit
   absolutely inside it without its own layout row. */
.password-field { position: relative; display: flex; }
.password-field .password-input { flex: 1; padding-right: 34px; }
.password-toggle {
    position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
    border: none; background: transparent; cursor: pointer; font-size: 1rem;
    line-height: 1; padding: 4px 6px; border-radius: 4px;
}
.password-toggle:hover { background: var(--bg); }

/* "Сохранить" safety-net button above the Список конкурентов table — see
   static/table-tools.js's setupSaveProgressButton for why it exists
   despite every cell already auto-saving on its own. */
.save-progress-bar { display: flex; align-items: center; gap: 12px; margin: 12px 0; }
.save-progress-status {
    font-size: 0.85rem; color: var(--accent); font-weight: 600;
    opacity: 0; transition: opacity 0.2s;
}
.save-progress-status.visible { opacity: 1; }
.save-progress-hint { margin: 0; }

.fill-in-instructions {
    background: #fff8e6; border: 1px solid #f0dca0; border-radius: 6px;
    padding: 8px 14px; margin: 10px 0;
}
.fill-in-instructions .hint { margin: 4px 0; color: var(--text); }

.settings-section {
    background: white; border: 1px solid var(--border); border-radius: 8px;
    padding: 16px 20px; margin: 20px 0;
}
.settings-table { border-collapse: collapse; margin: 12px 0; }
.settings-table th, .settings-table td { padding: 6px 14px 6px 0; text-align: left; border-bottom: 1px solid var(--border); }
.settings-add-form, .option-add-form { display: flex; align-items: end; gap: 12px; flex-wrap: wrap; margin-top: 10px; }
.settings-add-form label, .option-add-form label { display: flex; flex-direction: column; font-size: 0.82rem; gap: 4px; }
.settings-add-form input, .settings-add-form select,
.option-add-form select { padding: 5px 8px; border: 1px solid var(--border); border-radius: 5px; }

.settings-category-picker { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 16px; }
.settings-category-picker a {
    padding: 5px 12px; border: 1px solid var(--border); border-radius: 14px;
    text-decoration: none; color: var(--text); font-size: 0.85rem;
}
.settings-category-picker a.active { background: var(--accent); color: white; border-color: var(--accent); }

.option-columns { display: flex; flex-wrap: wrap; gap: 16px; }
.option-column-block { flex: 1 1 220px; min-width: 220px; border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
.option-column-block h3 { margin: 0 0 8px; font-size: 0.95rem; }
.option-value-list { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.option-value-list li {
    display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem;
    background: var(--bg); border-radius: 4px; padding: 3px 8px;
}
.option-delete-form button, .option-add-form button {
    border: none; background: transparent; color: var(--muted); cursor: pointer; font-size: 1rem; line-height: 1;
}
.option-delete-form button:hover { color: #b3261e; }
.option-add-form { margin-top: 0; }
.option-add-form input[type=text] { flex: 1; min-width: 0; padding: 5px 8px; border: 1px solid var(--border); border-radius: 5px; }
.option-add-form button { border: 1px solid var(--border); border-radius: 5px; padding: 4px 10px; }

/* Cluster/Доп характеристика/Комментарий — admin-only for a country-role
   session (see app.py's _MANAGER_LOCKED_KEYS): shown, never editable. */
.manager-locked-cell { display: flex; align-items: center; gap: 4px; min-height: 20px; opacity: 0.85; }
.manager-locked-text { color: var(--muted); font-size: 0.9rem; padding: 2px 4px; }
.manager-locked-empty { color: var(--muted); }

.content.full-width {
    max-width: none;
}

.content {
    padding: 24px;
    max-width: 1400px;
    margin: 0 auto;
}

h1 { margin-top: 0; }

.hint { color: var(--muted); }

.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 16px;
}

.card {
    background: white;
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 16px;
}

.card h2 { margin: 0 0 8px; font-size: 1.05rem; }
/* Per the user's explicit "локи фитнес категорий... подсвечены синим, а
   массажка - голубым, чтобы визуально понимать что где" — a country-role
   manager's category picker (country_categories.html) shows BOTH business
   lines mixed together (unlike an admin, who can be scoped to just one —
   see _visible_categories), so this color cue matters specifically there. */
.card-bl-fitness { border: 2px solid #2563eb; }
.card-bl-massage { border: 2px solid #22d3ee; }

.card-actions { display: flex; gap: 8px; margin-top: 12px; }
.card-completion { margin-top: 6px; font-size: 0.85rem; line-height: 1.5; }

.external-upload-form {
    display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin-top: 12px;
}
.external-upload-form input[type=file] { max-width: 100%; font-size: 0.85rem; }
.external-batches { margin-top: 12px; }
.external-batches summary { cursor: pointer; font-size: 0.85rem; color: var(--muted); }
.external-batch-list { list-style: none; margin: 8px 0; padding: 0; }
.external-batch-list li {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    padding: 4px 0; font-size: 0.82rem; border-bottom: 1px solid var(--border);
}
.external-batches > form { margin-top: 8px; }

.btn {
    display: inline-block;
    padding: 6px 12px;
    background: var(--accent);
    color: white;
    text-decoration: none;
    border-radius: 6px;
    font-size: 0.9rem;
}

.btn-secondary { background: white; color: var(--accent); border: 1px solid var(--accent); }

/* "ИИ анализ" button — always rendered now (not conditionally absent
   until the table's done), greyed out and click-intercepted instead
   (see table-tools.js's data-analysis-not-ready handler) while the
   table still has red cells/rows left. */
.btn-disabled { background: var(--muted); cursor: not-allowed; opacity: 0.75; }
.btn-disabled:hover { background: var(--muted); }

.table-wrap {
    overflow: auto;
    max-height: 70vh;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: white;
}

table { border-collapse: collapse; width: 100%; font-size: 0.95rem; }

th, td { padding: 8px 10px; border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; }

thead th { background: #eef1f4; position: sticky; top: 0; }

.empty { color: var(--muted); font-style: italic; }

.filter-bar { display: flex; align-items: end; gap: 16px; margin: 16px 0; }

.filter-bar label { display: flex; flex-direction: column; font-size: 0.85rem; gap: 4px; }

.add-links { margin: 16px 0; background: white; border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; }

.add-links textarea { width: 100%; margin: 8px 0; font-family: monospace; }

/* "Добавить конкурентов"/"Загрузить файл"/"Сброс данных" on the left,
   "Кластеры категории" on the right, side by side instead of one giant
   full-width block stacked below everything — per the user's explicit
   "огромное место занимают кластеры... компактнее всё укладываем". */
.panels-row { display: flex; gap: 16px; align-items: flex-start; }
.panels-left { flex: 1 1 55%; min-width: 0; }
.panels-right { flex: 1 1 38%; min-width: 260px; }
.panels-left .add-links:first-child, .panels-right .add-links { margin-top: 0; }

/* Compact list, not a full table — one line per cluster letter, scrolls
   internally past a modest height instead of growing the page with every
   cluster added. */
.cluster-panel { max-height: 380px; overflow-y: auto; }
/* Country-role session — no <details>, nothing to click, no descriptions
   ever rendered into the page at all (not just visually hidden). */
.cluster-panel-locked { max-height: none; }
.cluster-panel-locked-header { font-weight: 600; color: var(--muted); }
.cluster-list { display: flex; flex-direction: column; gap: 4px; }
.cluster-list-row { display: flex; align-items: flex-start; gap: 6px; }
.cluster-letter { flex-shrink: 0; margin-top: 1px; }
.cluster-list-row .cluster-desc-form { flex: 1; min-width: 0; }
.cluster-list-row textarea {
    width: 100%; box-sizing: border-box; font-size: 0.78rem; padding: 3px 6px;
    border: 1px solid var(--border); border-radius: 4px; resize: none;
    line-height: 1.3; max-height: 54px; overflow-y: auto;
}
.cluster-delete-form button {
    border: none; background: transparent; color: var(--muted);
    font-size: 1.1rem; line-height: 1; cursor: pointer; padding: 2px 4px;
}
.cluster-delete-form button:hover { color: #b3261e; }
.cluster-panel .cluster-add-form { margin-top: 10px; gap: 8px; }
.cluster-panel .cluster-add-form input[type=text] { font-size: 0.8rem; padding: 3px 5px; }
@media (max-width: 900px) {
    .panels-row { flex-direction: column; }
}

.cell-form input[type=text] { border: 1px solid transparent; background: transparent; width: 100%; min-width: 0; box-sizing: border-box; }
.cell-form input[type=text]:hover, .cell-form input[type=text]:focus { border-color: var(--border); background: white; }

/* Editable cell text — a <textarea>, not an <input>, specifically so long
   values WRAP onto new lines when a column is narrowed instead of being
   clipped at the cell's edge (an <input> can never wrap, no matter the
   CSS). Auto-grows its height via static/table-tools.js. */
.cell-form textarea.cell-value {
    display: block; border: 1px solid transparent; background: transparent;
    /* Per the user's real live report "при выборе фильтра... все сбивается"
       (columns collapsing to unreadable dashed slivers, this time for
       Фото/URL изображения/Доп характеристика — not just "Конкурент Genau",
       already fixed below via .match-select's own min-width) — a per-user
       SAVED column width percentage (table-tools.js's /api/column-widths)
       can convert to just a few px at a narrower window/table than
       whenever it was saved, on ANY column, not only the two that
       previously got their own min-width rule. Applied here on the base
       rule so every .cell-value column gets the same floor, not just the
       two that happened to get reported first. */
    width: 100%; min-width: 40px; box-sizing: border-box; resize: none;
    font-family: inherit; font-size: inherit; color: inherit;
    padding: 2px 4px; line-height: 1.35;
    /* Capped, not unbounded auto-grow (table-tools.js's autoGrow still
       sets an explicit taller `height`, but CSS max-height always wins
       over that) — a long value (a full CDN image URL wrapping across
       8-10 lines was the real case that prompted this) used to force the
       WHOLE row that tall, leaving a lot of empty space under the photo
       thumbnail next to it. Matches .thumb's own 180px cap (not a
       smaller number) so ordinary-length text never needs to scroll at
       all — confirmed live that a tighter cap (130px) made the native
       Windows scrollbar (big up/down arrow buttons) show up on cells
       that still had visible room below them, which read as broken/ugly
       rather than intentional. Scrollable only as a last resort for the
       rare genuinely-long value, and styled thin/borderless below
       instead of that native scrollbar. */
    max-height: 178px; overflow: hidden;
}
/* No scrollbar at all here — per the user's explicit "убери везде
   ползунки... если высота рамки больше высоты строки, то заканчивай
   рамку, а сам текст пусть уходит в неё": a scrollbar showed up on
   nearly every cell (confirmed live from her screenshots), not just the
   rare genuinely-long one, which read as clutter. A too-long value just
   clips at the cap now — she confirmed that's realistically only ever a
   URL, and the URL column already got a wider minimum earlier so most
   fit without clipping anyway. Only "Комментарий" keeps scrolling (she
   explicitly asked to keep that one) — see .comment-cell-value below. */
.cell-form textarea.cell-value.comment-cell-value {
    overflow-y: auto;
    scrollbar-width: thin; scrollbar-color: var(--muted) transparent;
}
.cell-form textarea.cell-value.comment-cell-value::-webkit-scrollbar { width: 6px; }
.cell-form textarea.cell-value.comment-cell-value::-webkit-scrollbar-track { background: transparent; }
.cell-form textarea.cell-value.comment-cell-value::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.cell-form textarea.cell-value.comment-cell-value::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.cell-form textarea.cell-value.comment-cell-value::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
.cell-form textarea.cell-value::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.cell-form textarea.cell-value::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.cell-form textarea.cell-value:hover, .cell-form textarea.cell-value:focus { border-color: var(--border); background: white; }
.cell-form textarea.cell-value.match-select,
.cell-form textarea.cell-value.option-select { cursor: pointer; caret-color: transparent; }
/* Per the user's real live report "когда сбрасываю или меняю фильтры
   страница выглядит так" (a screenshot showing "Конкурент Genau" as an
   unreadable sliver) — this column has a per-user SAVED percentage width
   (static/table-tools.js's /api/column-widths), and unlike .option-select
   just below (already given its own min-width for exactly this reason,
   see its own comment), .match-select never got the same floor. At a
   narrower window/table than whenever that width was saved, the computed
   pixel width can shrink to just a few px — text still there (nothing
   actually lost, matching her "при перезагрузке всё приходит в норму" —
   the data was never touched), just visually unreadable. Same floor as
   .option-select so this can't happen to this column either. */
.cell-form textarea.cell-value.match-select { min-width: 64px; }

/* A picker-backed cell (admin-defined dropdown list, e.g. "Нагрузка"/
   "Наклон") is just a plain wrapping textarea now (see table-tools.js's
   attachFixedOptionPicker — needed so it can wrap/shrink like any other
   characteristic cell, which a native <select> can't). Without any visual
   cue it read as "did my saved list disappear?" (confirmed live: it just
   looks like an empty box until clicked) — a small dropdown chevron +
   tinted background + a sensible minimum width make it unmistakably a
   picker again, not a regression in what's actually stored. */
.cell-form textarea.cell-value.option-select {
    min-width: 64px;
    background-color: #f7f8fa;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%236b7280' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 6px center;
    padding-right: 18px;
}
/* Not .cell-missing: that state must keep winning with its red border —
   see the shared rule near .cell-value.cell-missing above; this only
   dims the tint on hover/focus so the red stays the loudest signal. */
.cell-form textarea.cell-value.option-select:not(.cell-missing):hover,
.cell-form textarea.cell-value.option-select:not(.cell-missing):focus { background-color: white; }
.cell-form .cell-saving { opacity: 0.6; }

/* Custom "pick from existing values" popup (static/table-tools.js) — not a
   native <datalist>, since that only attaches to <input>, not <textarea>. */
.cell-picker {
    position: absolute; z-index: 50; background: white; border: 1px solid var(--border);
    border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 220px;
    overflow-y: auto; font-size: 0.85rem;
}
.cell-picker-item { padding: 5px 10px; cursor: pointer; white-space: normal; }
.cell-picker-item:hover { background: var(--bg); }

/* Excel-like table: wrapping text + resizable/sortable/filterable columns —
   see static/table-tools.js. Works on the read-only full-screen analysis
   table AND the editable Список конкурентов / Genau ассортимент tables. */
/* box-sizing: border-box is what makes table-tools.js's percentage column
   widths exact under table-layout:fixed — without it, a column's cell
   padding/border is added ON TOP of its declared width instead of being
   carved out of it, so the table ends up a few px wider than its
   container every time (confirmed live: a small but real horizontal
   scrollbar even with widths correctly summing to 100%) — the user's
   explicit "чтобы вся таблица в ширину умещалась всегда без ползунка". */
.excel-table th, .excel-table td { white-space: normal; word-break: break-word; vertical-align: top; box-sizing: border-box; }
/* Kept fully INSIDE the header cell's own box (right: 0, not a negative
   offset) — a handle that sticks out past its cell's right edge sits under
   the next header cell in paint order and becomes invisible/unclickable. */
.excel-table .col-resize-handle {
    position: absolute; top: 0; right: 0; width: 10px; height: 100%;
    cursor: col-resize; user-select: none; background: var(--border); opacity: 0.7;
    border-right: 2px solid var(--muted);
}
.excel-table .col-resize-handle:hover,
.excel-table .col-resize-handle.resizing { background: var(--accent); border-right-color: var(--accent); opacity: 1; }

.link-form { display: block; margin-top: 2px; }
.link-form input[type=text] { width: 100%; min-width: 0; box-sizing: border-box; font-size: 0.8rem; color: var(--muted); }

.thumb { max-height: 180px; max-width: 260px; object-fit: contain; display: block; }
/* Каталог конкурентов — per the user's explicit "фотка в каталоге может
   быть в два раза меньше в высоту": this is a browsing/reference table
   spanning many rows at once, not the working Список конкурентов table,
   so a full-size thumbnail wastes far more vertical space than it's
   worth there. */
.catalog-table img.thumb { max-height: 90px; }
/* Denser rows so more cards fit on screen at once — per the user's
   explicit "высота строк в каталоге должна быть меньше а сама таблица
   больше по высоте... чтобы как можно больше конкурентов помещалось".
   fitVisibleRows() in table-tools.js already grows the table to fill all
   available vertical screen space; shrinking each ROW's own height is
   what turns that same pixel budget into more visible rows. Matches
   .thumb's own smaller cap above (90px) instead of the working table's
   178px, so a wrapped characteristic/URL value never forces the row
   taller than the photo does. */
.catalog-table td { padding: 3px 6px; font-size: 0.85rem; }
.catalog-table textarea.cell-value { max-height: 90px; padding: 1px 3px; line-height: 1.25; }
/* The product name/link itself is plain text, not a .cell-value textarea
   (that cap above doesn't reach it) — a long name wrapping across many
   short lines inside the now-narrow Название column (see table-tools.js's
   isNarrowTextColumn) is the actual tallest thing in a row once that
   column got narrowed, overriding the .cell-value textareas' own 178px
   cap entirely. Confirmed live: this blows up row height on the WORKING
   Список конкурентов table too (1000px+ rows at a normal desktop width),
   not just the catalog — so the base rule matches .cell-value's existing
   178px cap everywhere, with the catalog's own tighter 90px (below, more
   specific selector) still overriding it there specifically. */
.name-cell-content { max-height: 178px; overflow: hidden; }
/* Long scraper errors are clamped to 3 lines (full text on hover via the
   title attribute) so they can't push the "Повторить сбор" button out. */
.collect-error-text {
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
    word-break: break-word;
}
.catalog-table .name-cell-content { max-height: 90px; }
/* Per the user's explicit "размер шрифта в беговых дорожках в части
   наименования нужно уменьшить для того чтобы все уместить" — long
   treadmill titles only, scoped by the table's data-category so other
   categories keep the normal size. */
.excel-table[data-category="bd"] .name-cell-content { font-size: 0.74rem; line-height: 1.25; }

.photo-cell { min-height: 24px; }
/* "x" next to the product name - to remove one competitor row from the
   list, admin-only (see app.py's delete_competitor_row/catalog_delete_card).
   Used to sit on top of the photo thumbnail, which read as "this deletes
   the photo" (per the user's explicit "перемести крестик с картинки на
   название") - now a plain inline element before the name text instead
   of an absolutely-positioned overlay, and red by default (not just on
   hover) so it's unmistakably a delete action. Confirmed via a plain JS
   confirm() dialog on submit (same pattern as the "Очистить всё" button
   below), and only ever deletes THIS row, unlike the bulk /reset. */
.row-delete-form { display: inline-block; vertical-align: middle; margin: 0 6px 2px 0; }
.row-delete-btn {
    width: 20px; height: 20px; padding: 0; line-height: 1;
    border: 1px solid #b3261e; border-radius: 50%; background: white;
    color: #b3261e; font-size: 0.95rem; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
}
.row-delete-btn:hover { background: #b3261e; border-color: #b3261e; color: white; }

.status-collected, .status-ok { color: var(--accent); }
.status-error, .status-not_configured { color: #b3261e; }
.refresh-data-form { display: inline-block; margin: 0 0 8px; }
.retry-collect-btn {
    font-size: 0.78rem; padding: 2px 8px; margin-top: 3px;
    border: 1px solid var(--border); border-radius: 4px; background: white; cursor: pointer;
}
.retry-collect-btn:hover { border-color: var(--accent); color: var(--accent); }
.assign-model-label {
    display: block; margin-top: 4px; font-size: 0.75rem; color: var(--muted);
}
.assign-model-select { font-size: 0.85rem; }

.badge-developing {
    display: inline-block; margin-left: 6px; padding: 1px 6px;
    font-size: 0.68rem; font-weight: 600; letter-spacing: 0.02em;
    color: #8a5a00; background: #fff3cd; border: 1px solid #f0c36d; border-radius: 4px;
    vertical-align: middle;
}
.row-actions { margin-top: 3px; display: flex; gap: 8px; }
/* Per-country visibility checkboxes under each assortment model's name —
   see app.py's toggle_assortment_product_country. Each is its own
   auto-submitting <form>, so display:inline-flex keeps them from each
   taking a full line. */
.country-visibility-row { margin-top: 3px; display: flex; flex-wrap: wrap; gap: 8px; }
.country-visibility-form { display: inline-flex; margin: 0; }
.country-visibility-label {
    display: inline-flex; align-items: center; gap: 3px;
    font-size: 0.75rem; color: var(--muted); cursor: pointer;
}
.country-visibility-label input[type="checkbox"] { margin: 0; cursor: pointer; }
.link-btn {
    font-size: 0.75rem; padding: 0; border: none; background: none;
    color: var(--accent); cursor: pointer; text-decoration: underline;
}
.link-btn:hover { opacity: 0.8; }
.link-btn.danger { color: #b3261e; }

.field-changes { margin: 0; padding-left: 18px; font-size: 0.82rem; }
.field-changes li { margin: 2px 0; }

/* "Кластер" column — two small letter pickers per row (competitors_category.html).
   flex:1/min-width:0 (not a fixed px width) so both selects shrink together
   with whatever share of the table's 100% this column ends up allocated —
   a fixed width here was exactly what forced the table wider than its
   container on a narrow "Кластер" column (a real overflow found live). */
.cluster-pickers { display: flex; gap: 4px; min-width: 0; }
.cluster-select { font-size: 0.85rem; padding: 2px; border: 1px solid var(--border); border-radius: 4px; flex: 1; min-width: 0; }

/* Cluster letter colors — background/text color set purely from the
   letter by static/table-tools.js (hash → hue), same everywhere a letter
   shows up: the "Кластеры категории" panel's badges, each row's cluster
   picker <select>, and the read-only analysis view's badges. Base layout
   only lives here; the actual color is always an inline style from JS. */
.cluster-badge {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 20px; height: 20px; padding: 0 5px; border-radius: 5px;
    font-weight: 700; font-size: 0.78rem; line-height: 1;
}
select.cluster-select {
    font-weight: 700; text-align: center; text-align-last: center;
    appearance: none; -webkit-appearance: none;
}

/* "Кластеры категории" panel (app.py's save/delete_category_cluster) */
.cluster-desc-form textarea { width: 100%; min-width: 240px; font-size: 0.85rem; }
.cluster-add-form { display: flex; gap: 12px; align-items: end; margin-top: 10px; flex-wrap: wrap; }
.cluster-add-form label { display: flex; flex-direction: column; font-size: 0.85rem; gap: 4px; }
.cluster-add-form input[type=text] { padding: 4px 6px; border: 1px solid var(--border); border-radius: 4px; }

/* As narrow as the collapsed <summary> line itself needs — per the
   user's explicit "белую разделительную полосу... максимально узкой" —
   this row only ever shows one line (the "Сырые данные" toggle) until
   opened, so the generic table td's 8px/10px padding was pure dead
   space between every pair of data rows. */
.raw-evidence td { white-space: normal; font-size: 0.85rem; color: var(--muted); padding: 1px 10px; }
.raw-evidence summary { cursor: pointer; line-height: 1.3; }
.raw-evidence details[open] summary { margin-bottom: 4px; }

.row-own-product td { background: var(--own-product); }
/* Own product present but not found in the loaded ranking (rank IS NULL) —
   appended at the bottom of the table, sorted among themselves by price;
   see app.py's _ensure_own_products_present/_load_competitors_view. */
.row-own-unranked td { background: var(--own-unranked); }
/* Specificity matches (and, being later, wins over) .cell-form
   input[type=text]'s transparent-border default at line ~261 — a plain
   .rank-input class alone loses that fight since it lacks the [type=text]
   attribute in its own selector. */
.cell-form input.rank-input {
    width: 90px; font-size: 0.82rem; padding: 2px 4px;
    border: 1px dashed var(--own-unranked-strong); border-radius: 4px;
    background: white; color: var(--own-unranked-strong); text-align: center;
}
.rank-input::placeholder { color: var(--own-unranked-strong); opacity: 0.8; }

/* Manual per-row color marker (app.py's update_competitor_highlight) — free
   choice, separate from the automatic orange own-product highlight above;
   a row never gets both (own-product always wins — see the templates). */
.row-mark-red td { background: #fde2e1; }
.row-mark-green td { background: #dcf5e3; }
.row-mark-blue td { background: #dce8fb; }
.row-mark-yellow td { background: #fff3b0; }
.highlight-select { font-size: 0.85rem; padding: 2px 4px; border: 1px solid var(--border); border-radius: 4px; }

/* Automatic "card not filled in yet" indicator (app.py's
   _incomplete_info_by_row) — a GENUINE COMPETITOR row (never GENAU's own —
   that's always orange instead) with an empty characteristic/price/photo-
   link/cluster field gets this gray background, per the user's explicit
   request, so at a glance the still-white rows are the complete ones. A
   manual highlight_color (red/green/blue) always wins over this — see the
   templates' <tr> class order. */
.row-incomplete td { background: #f0f1f3; }
/* Red border on the specific empty field — this one applies to EVERY row,
   including GENAU's own (own rows never get the gray row background, but
   still want to flag their own missing data). */
.cell-form textarea.cell-value.cell-missing, select.cluster-select.cell-missing { border: 2px solid #b3261e; }
/* Per the user's explicit "не видно не обязательных к заполнению ячеек...
   выделяй их но не красным, а серым, чтобы менеджеры понимали — тут можно
   заполнять но не обязательно" — a light grey outline (never red, never
   confused with .cell-missing above) on every editable field the
   completeness check doesn't require (Комментарий, Доп характеристика,
   Отзывы/Рейтинг when not file-imported, ...). */
.cell-form textarea.cell-value.cell-optional { border: 1px dashed var(--muted); }
.cell-form textarea.cell-value.cell-optional:hover,
.cell-form textarea.cell-value.cell-optional:focus { border-style: solid; }
/* A manager-locked read-only cell (assortment characteristics for a
   country-role session) has no textarea to put that border on — same
   red outline on the plain span instead, so the gap is still visible
   even though only the admin can actually fill it in. */
.manager-locked-text.cell-missing { display: inline-block; border: 2px solid #b3261e; border-radius: 4px; }
.own-badge {
    display: inline-block; background: var(--own-product-strong); color: white;
    font-size: 0.7rem; font-weight: 600; padding: 1px 6px; border-radius: 4px;
    margin-right: 6px; vertical-align: middle;
}
.own-product-swatch {
    display: inline-block; width: 12px; height: 12px; background: var(--own-product-strong);
    border-radius: 2px; vertical-align: middle;
}

/* Excel-like sort/filter — every table.excel-table (Genau ассортимент,
   Список конкурентов, and the full-screen analysis view) — see table-tools.js */
.sortable-th { cursor: pointer; user-select: none; white-space: nowrap; }
.sortable-th:hover { background: #e3e8ec; }
.sortable-th .sort-arrow { color: var(--muted); font-size: 0.75em; margin-left: 3px; }
.sortable-th.sort-asc .sort-arrow::after { content: "\2191"; }
.sortable-th.sort-desc .sort-arrow::after { content: "\2193"; }
/* `top` is set per-table in JS (table-tools.js), from the header row's own
   measured height, not a guessed constant here. */
.filter-row th { background: white; padding: 4px 6px; position: sticky; }
.filter-row input {
    width: 100%; box-sizing: border-box; font-size: 0.8rem; padding: 3px 4px;
    border: 1px solid var(--border); border-radius: 4px;
}
.filter-row .range-inputs { display: flex; gap: 3px; }
.filter-row .range-inputs input { width: 50%; }
.excel-table thead th { position: sticky; top: 0; z-index: 1; }
.excel-table tbody tr[hidden] { display: none; }
.reset-filters-btn { margin-bottom: 10px; }

/* Multi-select checkboxes for bulk actions (currently just delete) — per
   the user's explicit "нужна функция выбора нескольких карточек за раз
   (например для удаления)". */
.bulk-select-th, .bulk-select-td { width: 28px; text-align: center; }
.bulk-select-bar {
    display: inline-flex; align-items: center; gap: 10px; margin-left: 10px;
    padding: 4px 10px; background: #fdecea; border: 1px solid #f3c6c1; border-radius: 4px;
}

/* Multi-select "Кластер" column filter (table-tools.js) — a small popup
   of checkboxes instead of the plain text box every other column gets,
   per the user's explicit "фильтр... где можно выбрать один или
   несколько кластеров". */
.cluster-filter { position: relative; }
.cluster-filter-btn {
    width: 100%; box-sizing: border-box; font-size: 0.8rem; padding: 3px 6px;
    border: 1px solid var(--border); border-radius: 4px; background: white;
    text-align: left; cursor: pointer;
}
.cluster-filter-btn:hover { border-color: var(--accent); }
.cluster-filter-popup {
    position: absolute; top: 100%; left: 0; z-index: 60; margin-top: 2px;
    background: white; border: 1px solid var(--border); border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15); padding: 6px; min-width: 120px;
    max-height: 220px; overflow-y: auto;
}
.cluster-filter-item {
    display: flex; align-items: center; gap: 6px; padding: 3px 4px;
    font-size: 0.82rem; cursor: pointer; border-radius: 4px; font-weight: normal;
}
.cluster-filter-item:hover { background: var(--bg); }
.cluster-filter-item input[type=checkbox] { margin: 0; }
.cluster-filter-empty { font-size: 0.8rem; color: var(--muted); padding: 3px 4px; }

/* "Вид для анализа" toggle (admin-only) */
.btn-secondary.btn-active { background: var(--accent); color: white; }

/* ИИ анализ — saved results list (app.py's ai_analyses table). Redesigned
   per the user's explicit "сделай ui дизайн анализа. цвет шрифт фон — вот
   это всё, а не просто текст на белом фоне" — a colored left accent bar
   (по моделям vs по кластерам get their own hue, matching the badge
   below), a tinted card background instead of plain white, and the
   result text itself is now rendered Markdown->HTML (see
   ai_analysis.render_markdown_to_html), not raw pre-wrapped text. */
.ai-analysis-entry {
    background: #fbfdfb; border: 1px solid var(--border); border-left: 4px solid var(--accent);
    border-radius: 8px; padding: 14px 18px; margin: 12px 0;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.ai-analysis-entry-cluster { border-left-color: #6d5bd0; }
.ai-analysis-entry-independent { border-left-color: #d0821f; }
.ai-analysis-entry-brand { border-left-color: #b34f7a; }
.ai-analysis-entry-notes { border-left-color: #6b7a8c; }
.ai-analysis-entry summary { cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ai-analysis-mode-badge {
    display: inline-block; font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.02em; padding: 2px 8px; border-radius: 10px; color: white; background: var(--accent);
}
.ai-analysis-mode-cluster { background: #6d5bd0; }
.ai-analysis-mode-independent { background: #d0821f; }
.ai-analysis-mode-brand { background: #b34f7a; }
.ai-analysis-mode-notes { background: #6b7a8c; }
.ai-analysis-text {
    font-size: 0.94rem; line-height: 1.6; margin-top: 12px; color: var(--text);
    background: white; border: 1px solid var(--border); border-radius: 6px; padding: 14px 18px;
}
.ai-analysis-text h3, .ai-analysis-text h4, .ai-analysis-text h5 {
    color: var(--accent-dark); margin: 16px 0 6px;
}
.ai-analysis-text h3:first-child, .ai-analysis-text h4:first-child { margin-top: 0; }
.ai-analysis-text p { margin: 8px 0; }
.ai-analysis-text ul, .ai-analysis-text ol { margin: 6px 0; padding-left: 22px; }
.ai-analysis-text li { margin: 3px 0; }
.ai-analysis-text strong { color: var(--accent-dark); }
/* A paragraph that's just a bold "**Рекомендации:**"-style line (the
   subheading pattern build_analysis_prompt's formatting_note now asks
   for) gets extra breathing room above it so it reads as a subheading,
   not just another paragraph — per the user's explicit "фигово как то
   разделены абзацы... сделай более читаемо и разборчиво". */
.ai-analysis-text p:has(> strong:only-child) { margin-top: 18px; }

/* GENAU-vs-market comparison bars — opportunistically rendered from the
   trailing ```json block the AI is now asked for (see
   ai_analysis.split_result_text); silently absent for an analysis with no
   usable numeric comparison, never a broken/empty chart. */
.ai-chart-block {
    background: white; border: 1px solid var(--border); border-radius: 6px;
    padding: 12px 18px; margin-top: 12px; display: flex; flex-direction: column; gap: 14px;
}
.ai-chart-row + .ai-chart-row { border-top: 1px solid var(--bg); padding-top: 12px; }
.ai-chart-label { font-weight: 600; font-size: 0.88rem; margin-bottom: 6px; }
.ai-chart-label-meta { font-weight: 400; color: var(--muted); font-size: 0.82rem; }
.ai-chart-bars { display: flex; flex-direction: column; gap: 5px; }
.ai-chart-bar-line { display: grid; grid-template-columns: 52px 1fr auto; align-items: center; gap: 8px; }
.ai-chart-bar-tag { font-size: 0.76rem; color: var(--muted); }
.ai-chart-bar { height: 14px; border-radius: 3px; min-width: 2px; }
.ai-chart-bar-genau { background: var(--own-product-strong); }
.ai-chart-bar-market { background: #8892a0; }
.ai-chart-bar-value { font-size: 0.78rem; color: var(--muted); white-space: nowrap; }

/* Metric/chart-type toggle + explanatory caption — per the user's explicit
   "нужно иметь возможность переключать вид графика и то, что там
   отображается - оборот, штуки или цена... в целом везде подписывай и
   объясняй что показано на графике". Rendered entirely client-side (see
   ai_analysis.html's inline script) from the richer per-metric JSON the
   server now sends, so switching metric/chart type never re-runs the
   (paid) analysis. */
.ai-chart-controls { display: flex; flex-wrap: wrap; gap: 14px; font-size: 0.85rem; }
.ai-chart-controls select { margin-left: 4px; padding: 2px 4px; }
.ai-chart-caption { font-size: 0.8rem; color: var(--muted); font-style: italic; }
.ai-chart-render { display: flex; flex-direction: column; }
.ai-chart-row-pie { display: flex; flex-direction: column; gap: 6px; }
.ai-chart-pie-wrap { display: flex; align-items: center; gap: 16px; }
.ai-chart-pie { flex-shrink: 0; }
.ai-chart-pie-legend { display: flex; flex-direction: column; gap: 4px; font-size: 0.82rem; }
.ai-chart-pie-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; }
.ai-chart-pie-dot-genau { background: var(--own-product-strong); }
.ai-chart-pie-dot-market { background: #8892a0; }
/* Per the user's explicit "не хватает диаграммы кластерной - какой
   кластер и сколько занимает во всем рынке... в помодельном надо
   показывать доли на диаграмме только нашего оборудования" — a
   "kind":"breakdown" multi-slice pie (see ai_analysis.py) sits ahead of
   the regular per-model/cluster rows, visually set apart as a summary. */
.ai-chart-row-breakdown { background: var(--bg); border-radius: 6px; padding: 10px 12px; margin-bottom: 4px; }
.ai-chart-row-breakdown .ai-chart-pie-legend { flex-direction: row; flex-wrap: wrap; gap: 10px 16px; }

/* Token/cost summary — per the user's explicit "добавь везде, где
   тратятся токены окно подсчёта с количеством потраченных токенов и их
   стоимостью по итогу запроса". A small distinct box (not just a plain
   hint line) after every OpenAI call's result, wherever one happens
   (ИИ анализ history, cluster-dynamics explanation). */
.token-cost-box {
    display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
    margin-top: 10px; padding: 6px 12px; background: #f0f4f1; border: 1px solid var(--border);
    border-radius: 6px; font-size: 0.8rem; color: var(--text);
}

/* Compact competitor cards inside a saved analysis (ai_analysis.py's
   build_competitor_cards/render_markdown_to_html) — per the user's
   explicit "можешь в анализ рынка добавлять к просмотру карточки
   конкурентов о которых идет речь с изображениями и характеристиками...
   только характеристики и изображения... а то не понятно о ком идет
   речь". Deliberately no price/rank/turnover/rating (already in the text
   above) — just enough to recognize which physical product is meant. */
.ai-competitors-label {
    font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em;
    color: var(--muted); margin: 14px 0 6px;
}
.ai-competitor-strip { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 4px; }
.ai-competitor-card {
    flex: 0 0 auto; width: 148px; border: 1px solid var(--border); border-radius: 6px;
    overflow: hidden; background: var(--bg);
}
.ai-card-thumb { width: 100%; aspect-ratio: 1 / 1; background: white; display: flex; align-items: center; justify-content: center; }
.ai-card-thumb img { width: 100%; height: 100%; object-fit: cover; }
.ai-card-no-photo { font-size: 0.72rem; color: var(--muted); }
.ai-card-name { font-size: 0.78rem; font-weight: 700; padding: 6px 8px 2px; line-height: 1.3; }
.ai-card-specs { display: flex; flex-direction: column; gap: 2px; padding: 0 8px 8px; }
.ai-card-specs span { font-size: 0.7rem; color: var(--muted); line-height: 1.3; }
.ai-card-specs b { color: var(--text); font-weight: 600; }

.ai-analysis-delete-form { margin-top: 12px; }
.ai-analysis-delete-btn {
    font-size: 0.78rem; padding: 3px 10px; border: 1px solid var(--border); border-radius: 4px;
    background: white; color: var(--muted); cursor: pointer;
}
.ai-analysis-delete-btn:hover { border-color: #b3261e; color: #b3261e; }

/* "Движение по рейтингу" page (app.py's competitors_rating_history) —
   per the user's explicit "сохрани отдельную страницу — движение по
   рейтингу помодельно". */
.rank-history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
.rank-history-card {
    background: white; border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px;
}
.rank-history-card h3 { margin: 0 0 6px; font-size: 0.98rem; }
.rank-history-card-standalone { max-width: 600px; margin-top: 12px; }
.rank-sparkline { display: block; width: 100%; height: auto; max-width: 560px; }
.rank-history-url { font-size: 0.76rem; word-break: break-all; margin: 0 0 8px; }
.rank-history-search {
    padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; flex: 1; min-width: 220px;
}
.rank-history-empty-hint { margin-top: 8px; }

/* Toolbar above each product grid — search box + multi-select "combine"
   button, per the user's explicit "выбирать (отмечать галочками) несколько
   моделей... выводить их на один общий график". */
.rank-history-toolbar { display: flex; align-items: center; gap: 10px; margin: 8px 0 14px; flex-wrap: wrap; }
.rank-history-combine-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.rank-history-card-select {
    display: flex; align-items: flex-start; gap: 6px; font-weight: 600; font-size: 0.94rem;
    margin-bottom: 4px; cursor: pointer;
}
.rank-history-card-select input { margin-top: 3px; flex-shrink: 0; }
.rank-history-chart-wrap { margin-top: 4px; }
.rank-history-expand-btn {
    font-size: 0.76rem; padding: 2px 8px; margin-top: 4px; border: 1px solid var(--border);
    border-radius: 4px; background: white; color: var(--muted); cursor: pointer;
}
.rank-history-expand-btn:hover { border-color: var(--accent); color: var(--accent); }

/* Enlarged chart modal — per the user's explicit "графики можно было
   раскрывать по-больше в окнах поверх страницы (пусть будут в середине)".
   `display: flex` MUST live on the `:not([hidden])` variant, never on the
   bare `.chart-modal-overlay` selector — a plain class selector has the
   same specificity as the browser's own `[hidden] { display: none }` rule,
   but author CSS always wins over that UA-stylesheet rule regardless of
   specificity, so an unconditional `display: flex` here permanently
   defeated the `hidden` attribute: the JS "close" handlers correctly set
   `modal.hidden = true`, but the overlay stayed visually flexed/visible
   forever — confirmed live as the user's reported "зависшее окно — не
   убирается" (a stuck, blank overlay box that no close action removed). */
.chart-modal-overlay {
    position: fixed; inset: 0; background: rgba(15, 23, 20, 0.55); z-index: 300;
    padding: 24px;
}
.chart-modal-overlay:not([hidden]) { display: flex; align-items: center; justify-content: center; }
.chart-modal-box {
    background: white; border-radius: 10px; padding: 24px 28px; max-width: 860px; width: 100%;
    max-height: 88vh; overflow-y: auto; position: relative; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
}
.chart-modal-close {
    position: absolute; top: 10px; right: 14px; border: none; background: transparent;
    font-size: 1.6rem; line-height: 1; color: var(--muted); cursor: pointer; padding: 2px 6px;
}

/* "Все карточки заполнены" celebration (static/table-tools.js's
   showCelebration) — per the user's explicit "добавим на экран гифку со
   звуковым эффектом, когда менеджер или админ заполняет все карточки и
   все карточки становятся белыми". Same fixed full-screen overlay shape
   as .chart-modal-overlay above, own class so it can't be affected by any
   future change to that one (or vice versa) — this one is purely
   celebratory, never holds real content/data. */
.celebrate-overlay {
    position: fixed; inset: 0; background: rgba(17, 24, 21, 0.55); z-index: 999;
    display: none; align-items: center; justify-content: center;
}
.celebrate-overlay.celebrate-visible { display: flex; }
.celebrate-box { position: relative; }
.celebrate-box img {
    display: block; max-width: min(360px, 82vw); border-radius: 12px;
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.35);
}
.celebrate-close {
    position: absolute; top: -14px; right: -14px; width: 32px; height: 32px; border-radius: 50%;
    border: none; background: white; color: var(--text); font-size: 1.1rem; line-height: 1;
    cursor: pointer; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.celebrate-close:hover { background: var(--bg); }
.chart-modal-close:hover { color: var(--text); }
.chart-modal-box h3 { margin-top: 0; }
.rank-sparkline-modal { display: block; width: 100%; height: auto; max-width: 100%; }
.rank-history-legend { margin-top: 10px; }

/* Cluster dynamics — per the user's explicit "смотреть динамику
   кластеров... подходящие диаграммы". Reuses the ai-chart-* bar-comparison
   styles already built for the ИИ-анализ page (same visual language). */
.cluster-dynamics-card {
    background: #fbfdfb; border: 1px solid var(--border); border-left: 4px solid #6d5bd0;
    border-radius: 8px; padding: 14px 18px; margin: 12px 0;
}
.cluster-dynamics-card h3 { margin: 0 0 8px; font-size: 1rem; }
.cluster-explain-form { margin-top: 12px; }

/* Background ranking-import progress page (app.py's ranking_import_progress) */
.progress-bar {
    width: 100%; max-width: 500px; height: 18px; background: #e5e8eb;
    border-radius: 9px; overflow: hidden; margin: 10px 0;
}
.progress-bar-fill { height: 100%; background: var(--accent); transition: width 0.3s ease; }

/* Docked product-card preview panel (base.html) — a link with
   data-preview-url opens the card here instead of a new tab, so the
   manager can scroll it while filling in the table; it never closes on
   its own (no backdrop-click-to-dismiss), only the × button closes it. */
.product-preview-panel {
    position: fixed; top: 0; right: 0; height: 100vh;
    width: 42vw; max-width: 720px; min-width: 320px;
    background: white; border-left: 2px solid var(--border);
    box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
    z-index: 200; display: flex; flex-direction: column;
    transform: translateX(100%); transition: transform 0.2s ease;
}
.product-preview-panel.open { transform: translateX(0); }
.product-preview-panel .ppp-header {
    display: flex; align-items: center; gap: 10px; padding: 8px 12px;
    border-bottom: 1px solid var(--border); background: var(--bg); flex-shrink: 0;
}
.product-preview-panel .ppp-title {
    font-weight: 600; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.product-preview-panel .ppp-open-tab { font-size: 0.85rem; flex-shrink: 0; }
.product-preview-panel .ppp-close {
    border: none; background: transparent; font-size: 1.3rem; line-height: 1;
    cursor: pointer; color: var(--muted); padding: 0 4px; flex-shrink: 0;
}
.product-preview-panel .ppp-close:hover { color: var(--text); }
.product-preview-panel .ppp-frame { flex: 1; border: none; width: 100%; }

/* Shrinks the table area by the panel's own rendered width (set as a CSS
   variable in base.html's script, not a guessed constant) so the panel
   docks NEXT TO the table instead of covering its rightmost columns —
   the table's own horizontal scroll (see .table-wrap) still reaches every
   column in the space that's left. */
body.preview-open main.content.full-width { margin-right: var(--preview-panel-width, 0px); }

/* Rank-change column (app.py's _rank_change_by_row) — a competitor's move
   in the ranking between this and the previous period, matched by
   product_url. */
.rank-badge.rank-new {
    display: inline-block; background: var(--accent); color: white;
    font-size: 0.7rem; font-weight: 600; padding: 1px 6px; border-radius: 4px;
}
.rank-change { font-size: 0.85rem; white-space: nowrap; }
.rank-change.rank-up { color: var(--accent); }
.rank-change.rank-down { color: #b3261e; }
.rank-change.rank-same { color: var(--muted); }
.cell-with-change { display: flex; align-items: flex-start; gap: 3px; }
.cell-with-change .cell-form { flex: 1; min-width: 0; }
@media (max-width: 900px) {
    .product-preview-panel { width: 100vw; max-width: none; min-width: 0; }
    body.preview-open main.content.full-width { margin-right: 0; }
}

/* "ИИ анализы" overview page (templates/ai_analyses_overview.html) */
.ai-overview-table td, .ai-overview-table th { white-space: normal; vertical-align: top; }
.ai-overview-badge { display: inline-block; margin: 0 4px 4px 0; text-decoration: none; color: #fff; }
.ai-overview-date { opacity: 0.8; font-weight: normal; }
.ai-overview-open { font-size: 0.85rem; text-decoration: none; margin-left: 2px; }

/* Full-screen analysis table (templates/competitors_table.html) — per the
   user's explicit "таблица не растянута на всю высоту... изображения
   меньше в 1.5 раза... изображение и высота строки везде одинаковые":
   the box fills the window (table-tools.js sets its max-height), every row
   is exactly one fixed height (the photo, 180px / 1.5 = 120px), and
   anything taller than that inside a cell (long URL, long name) is
   clipped instead of stretching the row. */
.analysis-fullscreen { max-height: none; }
.analysis-fullscreen .thumb { max-height: 120px; max-width: 173px; }
.analysis-fullscreen td { padding: 4px 8px; vertical-align: top; }
.analysis-fullscreen .cell-clip {
    height: 120px; overflow: hidden; word-break: break-word; font-size: 0.85rem; line-height: 1.25;
}

/* Duplicate review page (templates/competitors_duplicates.html) */
.dup-table td, .dup-table th { white-space: normal; vertical-align: top; }
.dup-thumb { max-height: 70px; max-width: 100px; }
.dup-url { word-break: break-all; max-width: 380px; }

/* A product link that is really a search-results URL (".../search...") —
   user's explicit "подсвечивай красным шрифтом ссылки формата search". */
a.link-search, a.link-search:visited, .link-form input.link-search { color: #c0392b !important; }
.link-form input.link-search { font-weight: 600; }
.link-form input.cell-saved { outline: 2px solid #1a6b3c; }
.link-form input.cell-save-error { outline: 2px solid #c0392b; }

/* Analyst's notepad (static/notes-widget.js) — floats over the page's free
   space; nothing else on the page moves for it. */
#notes-widget { position: fixed; right: 16px; top: 150px; z-index: 55; font-size: 0.9rem; }
#notes-widget[data-page="table"] { top: 52px; }
#notes-widget .nw-tab {
    border: 1px solid var(--border); background: #fff; border-radius: 8px; padding: 5px 12px; cursor: pointer;
    box-shadow: 0 2px 8px rgba(0,0,0,.12); font-size: 0.85rem;
}
#notes-widget .nw-tab:hover { border-color: var(--accent); }
#notes-widget .nw-panel {
    width: 340px; background: #fff; border: 1px solid var(--border); border-radius: 10px;
    box-shadow: 0 6px 20px rgba(0,0,0,.18); display: flex; flex-direction: column; overflow: hidden;
}
#notes-widget .nw-panel[hidden] { display: none; }
#notes-widget .nw-head {
    display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; cursor: move;
    background: #eef1f4; font-weight: 600; user-select: none;
}
#notes-widget .nw-head button { border: 0; background: transparent; font-size: 1.1rem; cursor: pointer; }
#notes-widget textarea {
    width: 100%; box-sizing: border-box; height: 240px; min-height: 120px; resize: vertical; border: 0; outline: 0;
    padding: 8px 10px; font: inherit; line-height: 1.35;
}
#notes-widget .nw-foot { padding: 6px 10px 8px; border-top: 1px solid var(--border); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
#notes-widget .nw-foot .btn, #notes-widget .nw-foot button { font-size: 0.8rem; padding: 4px 9px; }
#notes-widget .nw-status { font-size: 0.75rem; color: var(--muted); margin-left: auto; }
#notes-widget .nw-msg { padding: 0 10px 8px; font-size: 0.8rem; }

/* A column hidden in Настройки: still editable here, greyed so it is clear it is not shown in the tables. */
.option-column-block.is-hidden { opacity: 0.55; }
.option-column-block.is-hidden:hover { opacity: 1; }

/* "Итого" row under the tables (static/table-tools.js updateSummary) — totals
   over the rows currently shown after filtering; sticks to the bottom edge of
   the scroll box. */
tfoot.summary-foot td {
    position: sticky; bottom: 0; z-index: 2; background: #fff8e1; border-top: 2px solid var(--border);
    font-size: 0.72rem; line-height: 1.3; white-space: normal; vertical-align: top; padding: 4px 6px;
}
tfoot.summary-foot .sum-line { overflow-wrap: anywhere; }

/* Editable cells on the full-screen analysis table look like PLAIN TEXT — no frame, no
   background, no dropdown arrow (user: "пусть это будет просто текст, но если на него
   нажать, он редактировался бы"); only a light outline appears while one is being edited.
   Every row keeps its one fixed height, so a long text scrolls inside its cell (scrollbar
   hidden until the cell is focused). Overrides the auto-grow height from table-tools.js and
   the required/optional/missing frames of the working table — this view only. */
.analysis-fullscreen textarea.cell-value,
.analysis-fullscreen textarea.cell-value.cell-required,
.analysis-fullscreen textarea.cell-value.cell-optional,
.analysis-fullscreen textarea.cell-value.cell-missing {
    height: 112px !important; width: 100%; box-sizing: border-box; resize: none; overflow-y: hidden;
    border: 1px solid transparent !important; background: transparent !important; background-image: none !important;
    box-shadow: none !important; padding: 0 2px; margin: 0; font: inherit; line-height: inherit; color: inherit;
    cursor: text; scrollbar-width: none;
}
.analysis-fullscreen textarea.cell-value::-webkit-scrollbar { display: none; }
.analysis-fullscreen textarea.cell-value.option-select { cursor: pointer; }
.analysis-fullscreen textarea.cell-value:focus {
    overflow-y: auto; scrollbar-width: thin; background: #fff !important; border-color: #9db8a8 !important;
    outline: none; border-radius: 3px;
}
.analysis-fullscreen textarea.cell-value:focus::-webkit-scrollbar { display: initial; }
