:root {
    --hub-primary: var(--accent);
    --hub-bg: var(--surface-sunken);
}

body {
    background: var(--hub-bg);
    font-family: var(--font-sans);
    color: var(--text-primary);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    letter-spacing: -0.004em;                    /* slight tightening at UI sizes */
}
::selection { background: var(--p-200); color: var(--p-900); }
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5 {
    font-family: var(--font-sans);
    letter-spacing: -0.018em;                    /* headings sit a touch tighter */
    color: var(--text-primary);
}

/* Reduced motion: keep state changes instant — no transitions, no spin/pop.
   A crossfade-free instant swap is the accessible fallback (WCAG 2.3.3). */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
    }
}

/* ---- Auth ---- */
.auth-body {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    /* soft brand wash at the top edge — first screen sets the language */
    background:
        radial-gradient(900px 460px at 50% -10%, rgba(45, 212, 191, .14) 0%, rgba(45, 212, 191, 0) 60%),
        var(--surface-sunken);
}
.auth-wrap { width: 100%; max-width: 400px; padding: 1.5rem; }
.auth-card {
    background: var(--n-0);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 2.25rem 2rem;
    box-shadow: var(--shadow-md);
}
.auth-brand {
    display: flex;
    align-items: center;
    gap: .55rem;
    margin-bottom: 1.5rem;
}
.auth-logo {
    width: 38px; height: 38px;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--hub-primary);
    color: var(--n-0);
    border-radius: var(--radius-md);
    font-size: var(--text-lg);
}
.auth-brand-name { font-size: var(--text-lg); color: var(--text-primary); }
.auth-title { font-size: 1.4rem; font-weight: 600; margin-bottom: .25rem; }
.auth-sub { color: var(--text-muted); font-size: var(--text-base); margin-bottom: 1.5rem; }
.auth-foot { text-align: center; color: var(--text-muted); font-size: var(--text-sm); margin-top: 1.25rem; }

/* ==== Button system ==========================================================
   Bootstrap buttons, tuned on our tokens + Emil-grade press feedback. Every
   pressable control gets: a consistent radius, an ease-out transition, a subtle
   scale-down on :active (the UI "hears" the press) and a token focus ring.
   Only transform/box-shadow/background animate (GPU-cheap). */
.btn {
    --bs-btn-border-radius: var(--radius-md);
    --bs-btn-font-weight: var(--font-weight-medium);
    --bs-btn-focus-box-shadow: var(--focus-ring);
    transition: transform var(--dur-press) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out),
                background-color var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out);
}
.btn:active { transform: scale(0.97); }
.btn:focus-visible { box-shadow: var(--focus-ring); outline: none; }

.btn-primary {
    --bs-btn-bg: var(--hub-primary);
    --bs-btn-border-color: var(--hub-primary);
    --bs-btn-hover-bg: var(--accent-hover);
    --bs-btn-hover-border-color: var(--accent-hover);
    --bs-btn-active-bg: var(--accent-hover);
    --bs-btn-active-border-color: var(--accent-hover);
    /* Bootstrap's disabled state ships literal #0d6efd — rebind or a disabled
       primary flashes blue (the "Merge selected (0)" bug). */
    --bs-btn-disabled-bg: var(--accent);
    --bs-btn-disabled-border-color: var(--accent);
    --bs-btn-disabled-color: var(--n-0);
    /* concept: inner top highlight + brand-tinted shadow (double-bezel hint) */
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 2px 5px -1px rgba(15, 118, 110, .22);
}
.btn-primary:hover { box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 4px 10px -2px rgba(15, 118, 110, .35); }
/* Bootstrap ships outline-primary / btn-link with a literal #0d6efd — rebind. */
.btn-outline-primary {
    --bs-btn-color: var(--accent);
    --bs-btn-border-color: var(--accent);
    --bs-btn-hover-bg: var(--accent);
    --bs-btn-hover-border-color: var(--accent);
    --bs-btn-hover-color: var(--n-0);
    --bs-btn-active-bg: var(--accent-hover);
    --bs-btn-active-border-color: var(--accent-hover);
    --bs-btn-active-color: var(--n-0);
    --bs-btn-disabled-color: var(--accent);
    --bs-btn-disabled-border-color: var(--accent);
}
/* Positive review-gate actions (mark processed, quick-save, inline-create):
   keep the success semantics but on OUR green, not Bootstrap's #198754. */
.btn-success {
    --bs-btn-bg: var(--success);
    --bs-btn-border-color: var(--success);
    --bs-btn-hover-bg: #15803d;
    --bs-btn-hover-border-color: #15803d;
    --bs-btn-active-bg: #15803d;
    --bs-btn-active-border-color: #15803d;
    --bs-btn-disabled-bg: var(--success);
    --bs-btn-disabled-border-color: var(--success);
}
.btn-link { --bs-btn-color: var(--accent); --bs-btn-hover-color: var(--accent-hover); }
.btn-outline-danger {
    --bs-btn-color: var(--danger);
    --bs-btn-border-color: color-mix(in srgb, var(--danger) 45%, var(--border-control));
    --bs-btn-hover-bg: var(--danger);
    --bs-btn-hover-border-color: var(--danger);
}
/* Inline-create "+" ships as btn-outline-success in 6 templates — a success hue
   on a neutral action (audit P2). Rebind the class to the quiet secondary look
   so every "add related" control reads neutral; accent appears on hover only. */
.btn-outline-success {
    --bs-btn-color: var(--text-secondary);
    --bs-btn-border-color: var(--border-control);
    --bs-btn-hover-bg: var(--n-50);
    --bs-btn-hover-border-color: var(--n-300);
    --bs-btn-hover-color: var(--accent);
    --bs-btn-active-bg: var(--n-100);
    --bs-btn-active-border-color: var(--n-300);
    --bs-btn-active-color: var(--accent);
}

/* Secondary / outline: quiet surface controls that lift a hair on hover. */
.btn-outline-secondary {
    --bs-btn-color: var(--text-secondary);
    --bs-btn-border-color: var(--border-control);
    --bs-btn-hover-bg: var(--surface-hover);
    --bs-btn-hover-border-color: var(--n-300);
    --bs-btn-hover-color: var(--text-primary);
    --bs-btn-active-bg: var(--n-100);
    --bs-btn-active-border-color: var(--n-300);
    --bs-btn-active-color: var(--text-primary);
}
.btn-light {
    --bs-btn-bg: var(--surface);
    --bs-btn-border-color: var(--border-control);
    --bs-btn-color: var(--text-secondary);
    --bs-btn-hover-bg: var(--surface-hover);
    --bs-btn-hover-border-color: var(--n-300);
    --bs-btn-hover-color: var(--text-primary);
}

/* ---- Icon button: a square, rounded action holding a single glyph. Used for
   logout, filter funnel, per-row kebab, sidebar collapse. 36px = AA target. */
.hub-icon-btn {
    --sz: 36px;
    flex: 0 0 auto;
    width: var(--sz); height: var(--sz);
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--text-secondary);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                transform var(--dur-press) var(--ease-out);
}
.hub-icon-btn:hover { background: var(--surface-hover); color: var(--text-primary); }
.hub-icon-btn:active { transform: scale(0.94); }
.hub-icon-btn:focus-visible { box-shadow: var(--focus-ring); outline: none; }
.hub-icon-btn.is-bordered { border-color: var(--border-control); background: var(--surface); }
.hub-icon-btn.is-danger:hover { background: color-mix(in srgb, var(--danger) 10%, var(--surface)); color: var(--danger); }

/* ---- Portal shell ---- */
.portal-shell { display: flex; min-height: 100vh; }

/* ---- Ink sidebar ---------------------------------------------------------
   Dark chrome separates navigation from the data surface (Stripe / Retool /
   Metabase pattern; Attio pairs the same slate-950 with a teal accent). Active
   item = tinted lift + white text + teal icon — no rails, no stripes. On ink,
   elevation is hairlines (rgba-white borders), never shadows. */
.portal-sidebar {
    width: 248px;
    flex-shrink: 0;
    background: var(--ink);
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100vh;
    transition: width .15s ease;
    overflow: hidden;
}
.sidebar-brand {
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: 1.05rem 1.1rem;
    border-bottom: 1px solid var(--ink-hairline);
    font-size: var(--text-lg);
    color: var(--ink-text-hi);
}
/* the brand mark needs a brighter teal to read on ink */
.portal-sidebar .auth-logo { background: var(--p-600); color: var(--n-0); }
.portal-sidebar .auth-brand-name { color: var(--ink-text-hi); }
.sidebar-nav {
    padding: .75rem .6rem;
    overflow-y: auto;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, .16) transparent;
}
.sidebar-nav::-webkit-scrollbar { width: 8px; }
.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .14); border-radius: var(--radius-xs); }
.sidebar-foot { margin-top: auto; padding-top: .5rem; border-top: 1px solid var(--ink-hairline); }
/* Approved 3-tier separation (concept v3): the tiers differ by STRUCTURE, not
   just gray value — role gets a hairline + air, group gets a disclosure
   chevron, items hang on a left thread (the VS Code guide-line pattern). */
/* Tier 1 — ROLE (Supplier / Consumer): hairline above + micro-caps, dimmest. */
.sidebar-section {
    text-transform: uppercase;
    font-size: 9.5px;
    letter-spacing: .11em;
    color: var(--ink-label);
    padding: .85rem .6rem .5rem;
    font-weight: 600;
}
.sidebar-role {
    border-top: 1px solid var(--ink-hairline);
    margin-top: 1rem;
}
/* Tier 2 — GROUP header: disclosure chevron + medium weight, lighter tone. */
.sidebar-subsection {
    display: flex;
    align-items: center;
    gap: 7px;
    text-transform: none;
    letter-spacing: 0;
    font-size: 11.5px;
    font-weight: 500;
    color: var(--ink-muted);
    padding: .1rem .6rem .4rem .7rem;
    border-top: 0;
    margin-top: 0;
}
.sidebar-subsection::before {
    content: "";
    width: 5px; height: 5px;
    border-right: 1.4px solid #5e6a7e;
    border-bottom: 1.4px solid #5e6a7e;
    transform: rotate(45deg);
    margin-top: -3px;
    flex: 0 0 auto;
}
/* Tier 3 — ITEM GROUP: left thread + tight rhythm inside, air after. */
.sidebar-grp {
    display: flex;
    flex-direction: column;
    gap: 2px;
    border-left: 1px solid var(--ink-thread);
    margin: 0 0 1rem .9rem;
    padding-left: 6px;
}
.sidebar-nav .nav-link {
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: .42rem .65rem;
    border-radius: var(--radius-md);
    color: var(--ink-text);
    font-size: var(--text-sm);
    white-space: nowrap;
    transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.sidebar-nav .nav-link:hover { background: var(--ink-hover); color: var(--ink-text-hi); }
.sidebar-nav .nav-link:hover i { color: var(--ink-text-hi); }
.sidebar-nav .nav-link.active { background: var(--ink-active); color: var(--ink-text-hi); font-weight: var(--font-weight-medium); }
.sidebar-nav .nav-link.active i { color: var(--p-400); }
.sidebar-nav .nav-link i { font-size: 1rem; width: 1.1rem; text-align: center; color: var(--ink-icon); transition: color var(--dur-fast) var(--ease-out); }
/* "no company" note carries a bootstrap text-muted utility — unreadable on ink */
.sidebar-empty { color: var(--ink-muted) !important; }

.portal-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.portal-topbar {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: .65rem 1.25rem;
    background: var(--n-0);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 10;
}
/* Collapse arrow lives inside the menu header; the topbar ☰ only appears once
   the menu is hidden, so there is always a way back. */
.sidebar-collapse-btn {
    border: 1px solid var(--ink-hairline);
    background: transparent;
    color: var(--ink-muted);
    padding: .2rem .45rem;
    line-height: 1;
    border-radius: var(--radius-md);
    transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.sidebar-collapse-btn i { font-size: 1rem; }
.sidebar-collapse-btn:hover { color: var(--ink-text-hi); background: var(--ink-hover); }

/* ---- Collapsed: slim icon rail (instead of hiding the menu outright) ---- */
.portal-shell.sidebar-collapsed .portal-sidebar { width: 60px; }
/* hide everything textual; keep only the icons + the toggle */
.portal-shell.sidebar-collapsed .auth-logo,
.portal-shell.sidebar-collapsed .auth-brand-name,
.portal-shell.sidebar-collapsed .sidebar-section { display: none; }
.portal-shell.sidebar-collapsed .sidebar-brand { justify-content: center; padding: 1.1rem 0; }
.portal-shell.sidebar-collapsed .sidebar-collapse-btn { margin: 0 !important; }
.portal-shell.sidebar-collapsed .sidebar-nav { padding: .75rem 0; align-items: center; }
.portal-shell.sidebar-collapsed .sidebar-foot { width: 100%; display: flex; justify-content: center; }
/* font-size:0 collapses the label text node; the icon keeps its own size */
.portal-shell.sidebar-collapsed .sidebar-nav .nav-link {
    font-size: 0;
    justify-content: center;
    width: 44px;
    padding: .55rem 0;
    gap: 0;
}
.portal-shell.sidebar-collapsed .sidebar-nav .nav-link i { font-size: var(--text-lg); width: auto; }
.portal-shell.sidebar-collapsed .sidebar-grp { border-left: 0; margin: 0; padding: 0; align-items: center; }
.portal-topbar { min-height: 56px; }
.topbar-title { font-size: 15px; font-weight: var(--font-weight-semibold); color: var(--text-primary); letter-spacing: -0.01em; }
.topbar-divider { width: 1px; align-self: stretch; margin: .55rem 0; background: var(--border); }

/* ---- Topbar ghost menus (company / storefront / language) -------------------
   Research: modern admin topbars keep switchers GHOST — text + chevron with a
   quiet hover fill; boxes are reserved for inputs. Icons stay gray, never
   brand-tinted: color is dosed, accent only marks actions/active state. */
.hub-top-menu {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .38rem .6rem;
    background: transparent;
    border: 0;
    border-radius: var(--radius-md);
    color: var(--n-700);
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    line-height: 1.2;
    transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.hub-top-menu:hover { background: var(--n-100); color: var(--text-primary); }
.hub-top-menu:focus-visible { box-shadow: var(--focus-ring); outline: none; }
.hub-top-menu > i:first-child { color: var(--n-400); font-size: .95rem; }
.hub-top-menu.dropdown-toggle::after { margin-left: .1rem; color: var(--n-400); }
.hub-top-menu.is-lang { font-variant-numeric: tabular-nums; }

/* signed-in user: ink initial disc + name. As the account-menu toggle the
   same chip also carries .hub-top-menu (padding, hover, focus ring); only
   the wider avatar gap below is its own. */
.topbar-account {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    font-size: var(--text-sm);
    color: var(--n-700);
    font-weight: var(--font-weight-medium);
}
.topbar-avatar {
    width: 26px; height: 26px;
    border-radius: var(--radius-round);
    background: var(--n-800);
    color: var(--n-0);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: var(--text-2xs);
    font-weight: var(--font-weight-semibold);
}

/* ---- ⌘K global search — fills the topbar's dead center ---- */
.hub-cmdk {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 0 1 340px;
    min-width: 180px;
    margin-left: auto;
    margin-right: auto;
    background: var(--n-50);
    border: 1px solid var(--n-150);
    border-radius: var(--radius-md);
    padding: 5px 6px 5px 10px;
    transition: border-color var(--dur-fast) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out),
                background-color var(--dur-fast) var(--ease-out);
}
.hub-cmdk:hover { border-color: var(--n-200); }
.hub-cmdk:focus-within { background: var(--n-0); border-color: var(--p-500); box-shadow: var(--focus-ring); }
.hub-cmdk .bi { color: var(--n-400); font-size: .85rem; }
.hub-cmdk-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0; outline: 0; background: transparent;
    font-size: var(--text-sm);
    color: var(--text-primary);
}
.hub-cmdk-input::placeholder { color: var(--n-400); }
.hub-cmdk kbd {
    background: var(--n-0);
    border: 1px solid var(--n-150);
    border-radius: 5px;
    padding: 1px 5px;
    font-family: var(--font-mono);
    font-size: 10.5px;
    color: var(--n-500);
    line-height: 1.4;
}
@media (max-width: 1150px) { .hub-cmdk { display: none; } }

/* ---- Dropdown menu: soft elevated panel, brand-tinted active (kills the harsh
   Bootstrap #0d6efd), origin-aware ease-out open. Applies to EVERY dropdown. */
.dropdown-menu {
    --bs-dropdown-border-color: var(--border);
    --bs-dropdown-border-radius: var(--radius-lg);
    --bs-dropdown-box-shadow: var(--shadow-pop);
    --bs-dropdown-link-active-bg: var(--accent-tint);
    --bs-dropdown-link-active-color: var(--accent-hover);
    --bs-dropdown-link-hover-bg: var(--surface-hover);
    --bs-dropdown-link-border-radius: var(--radius-sm);
    --bs-dropdown-item-padding-y: .45rem;
    --bs-dropdown-padding-x: .35rem;
    --bs-dropdown-padding-y: .35rem;
    box-shadow: var(--shadow-pop);
    padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);
}
.dropdown-menu .dropdown-item {
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    padding: var(--bs-dropdown-item-padding-y) .6rem;
    transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.dropdown-menu .dropdown-item.active,
.dropdown-menu .dropdown-item:active {
    background: var(--accent-tint);
    color: var(--accent-hover);
    font-weight: var(--font-weight-medium);
}
/* Open animation. Popper positions the menu a frame AFTER .show is added and
   writes an inline transform — so (a) never animate transform here (it would
   override Popper's positioning mid-flight), and (b) keep the menu invisible
   until Popper stamps data-popper-placement (static menus carry data-bs-popper
   instead). This kills the "opens on top of the trigger, then jumps down". */
.dropdown-menu.show { opacity: 0; }
.dropdown-menu.show[data-popper-placement],
.dropdown-menu.show[data-bs-popper] { opacity: 1; animation: hub-pop var(--dur-pop) var(--ease-out); }
@keyframes hub-pop {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* ---- Live jobs pill: an active import/parse run, clickable to its page. ---- */
.hub-jobs-pill {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .35rem .7rem;
    border-radius: var(--radius-pill);
    background: var(--accent-tint);
    border: 1px solid color-mix(in srgb, var(--accent) 25%, var(--border));
    color: var(--p-800);
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    text-decoration: none;
    line-height: 1.2;
    transition: background-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.hub-jobs-pill:hover { background: var(--p-100); box-shadow: var(--shadow-xs); color: var(--p-900); }
.hub-jobs-pill .bi { animation: hub-spin 1.1s linear infinite; font-size: .9rem; }
.hub-jobs-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 1.2em; padding: 0 .35em;
    border-radius: var(--radius-pill);
    background: var(--accent); color: var(--n-0);
    font-size: var(--text-2xs); font-weight: var(--font-weight-bold);
    font-variant-numeric: tabular-nums;
}
@keyframes hub-spin { to { transform: rotate(360deg); } }

.portal-content { padding: 1.5rem; }

/* ---- Narrow viewports: keep the layout inside the screen ----
   Below the lg breakpoint the fixed 248px sidebar would push page content past
   the right edge, leaving controls unreachable (there is no page-level
   horizontal scroll to chase them). Force the slim icon rail regardless of the
   saved collapse state, let the horizontal bars wrap instead of widening the
   page, and lean on .table-responsive so wide listings scroll inside their own
   box rather than overflowing the whole page. */
@media (max-width: 991.98px) {
    .portal-sidebar { width: 60px; }
    .portal-shell .auth-logo,
    .portal-shell .auth-brand-name,
    .portal-shell .sidebar-section { display: none; }
    .portal-shell .sidebar-brand { justify-content: center; padding: 1.1rem 0; }
    /* Nothing to collapse further — the rail is already forced here. */
    .portal-shell .sidebar-collapse-btn { display: none; }
    .portal-shell .sidebar-nav { padding: .75rem 0; align-items: center; }
    .portal-shell .sidebar-foot { width: 100%; display: flex; justify-content: center; }
    .portal-shell .sidebar-nav .nav-link {
        font-size: 0;
        justify-content: center;
        width: 44px;
        padding: .55rem 0;
        gap: 0;
    }
    .portal-shell .sidebar-nav .nav-link i { font-size: var(--text-lg); width: auto; }
    .portal-shell .sidebar-grp { border-left: 0; margin: 0; padding: 0; align-items: center; }

    /* Wrap the action bars so their buttons drop to a new line instead of
       forcing the page wider than the viewport. */
    .portal-topbar { flex-wrap: wrap; row-gap: .5rem; }
    .page-head { flex-wrap: wrap; }
    .hub-filterbar { flex-wrap: wrap; }

    .portal-content { padding: 1rem; }
}

/* ---- Tables ---- */
.hub-table thead th {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--text-muted);
    font-weight: var(--font-weight-medium);
    background: var(--n-25);
    border-bottom: 1px solid var(--n-150);
}
.hub-table td { border-color: var(--n-100); font-size: var(--text-base); }
/* Status badges inside tables → concept dot-pills: tinted pill + leading dot
   in the badge's own emphasis color (works with every -subtle/-emphasis pair). */
.hub-table .badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    border-radius: var(--radius-pill);
    font-size: 11.5px;
    font-weight: var(--font-weight-medium);
    padding: .3em .7em .3em .55em;
}
.hub-table .badge::before {
    content: "";
    width: 5px; height: 5px;
    border-radius: var(--radius-round);
    background: currentColor;
    flex: 0 0 auto;
}
.hub-table tbody tr:hover { background: var(--n-25); }

/* ---- Expandable run badges (prices held, hidden, restored…) ----
   Значок-раскрывашка ничем не отличался от обычного: клиент смотрел на строку
   загрузки, видел 15 одинаковых плашек и решил, что часть из них «не
   работает» (ecom-5133). Шеврон и курсор ставятся ОДНИМ правилом на любой
   <summary class="badge">, чтобы следующий такой значок получил их сам, а не
   ждал, пока про него вспомнят. */
details > summary.badge {
    cursor: pointer;
    list-style: none;
    user-select: none;
}
details > summary.badge::-webkit-details-marker { display: none; }
details > summary.badge::after {
    content: "\25BE";              /* ▾ — символом, а не иконочным шрифтом */
    margin-left: .3em;
    display: inline-block;
    line-height: 1;
    opacity: .7;
    transition: transform .15s var(--ease-out);
}
details[open] > summary.badge::after { transform: rotate(180deg); }
details > summary.badge:hover { filter: brightness(.95); }
details > summary.badge:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

.hub-thumb {
    width: 38px; height: 38px;
    border-radius: var(--radius-md);
    object-fit: cover;
    background: var(--n-50);
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--n-300);
}
.hub-thumb-empty { font-size: var(--text-lg); }

/* ---- Value chips (attribute values) ---- */
.hub-chip {
    display: inline-block;
    padding: .12rem .5rem;
    margin: .1rem .2rem .1rem 0;
    font-size: var(--text-xs);
    line-height: 1.4;
    color: var(--n-700);
    background: var(--n-50);
    border-radius: var(--radius-pill);
    white-space: nowrap;
}
.hub-chip-more { color: var(--text-muted); background: transparent; padding-left: .15rem; }
.hub-type-badge {
    font-size: var(--text-xs);
    font-weight: 600;
    padding: .18rem .5rem;
    border-radius: var(--radius-sm);
    background: var(--accent-tint);
    color: var(--accent-hover);
}

/* ---- Rendered docs (Markdown) ---- */
.hub-doc { font-size: .94rem; color: var(--n-800); line-height: 1.6; }
.hub-doc h1 { font-size: 1.5rem; font-weight: 600; margin: 0 0 1rem; }
.hub-doc h2 { font-size: var(--text-lg); font-weight: 600; margin: 1.6rem 0 .6rem; }
.hub-doc h3 { font-size: 1rem; font-weight: 600; margin: 1.2rem 0 .5rem; }
.hub-doc p, .hub-doc ul, .hub-doc ol { margin: 0 0 .8rem; }
.hub-doc blockquote {
    margin: 0 0 1rem; padding: .4rem .9rem;
    border-left: 3px solid var(--p-100); background: var(--n-25);
    color: var(--text-secondary); border-radius: var(--radius-xs);
}
.hub-doc code { background: var(--n-50); padding: .08rem .35rem; border-radius: var(--radius-xs); font-size: var(--text-sm); }
.hub-doc pre { background: var(--n-900); color: var(--n-150); padding: .9rem; border-radius: var(--radius-md); overflow-x: auto; }
.hub-doc pre code { background: transparent; color: inherit; padding: 0; }
.hub-doc table { width: 100%; border-collapse: collapse; margin: 0 0 1rem; font-size: var(--text-base); }
.hub-doc th, .hub-doc td { border: 1px solid var(--border); padding: .4rem .6rem; text-align: left; }
.hub-doc th { background: var(--n-25); font-weight: 600; }

/* ---- Detail page ---- */
.hub-tabs { border-bottom: 1px solid var(--n-150); }
.hub-tabs .nav-link {
    border: none;
    color: var(--text-muted);
    font-size: var(--text-base);
    padding: .6rem .9rem;
    border-bottom: 2px solid transparent;
}
.hub-tabs .nav-link:hover { color: var(--text-primary); }
.hub-tabs .nav-link.active {
    color: var(--hub-primary);
    background: transparent;
    border-bottom-color: var(--hub-primary);
    font-weight: 500;
}
.hub-lang-tabs .nav-link {
    padding: .2rem .7rem;
    font-size: var(--text-sm);
    color: var(--text-muted);
    background: var(--n-50);
    border-radius: var(--radius-sm);
}
.hub-lang-tabs .nav-link.active { background: var(--hub-primary); color: var(--n-0); }

.hub-dl { display: grid; grid-template-columns: 40% 60%; row-gap: .55rem; }
.hub-dl dt { color: var(--text-muted); font-weight: 500; font-size: var(--text-sm); }
.hub-dl dd { margin: 0; font-size: var(--text-base); }

.hub-media-cell {
    aspect-ratio: 1;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    overflow: hidden;
    display: flex; align-items: center; justify-content: center;
    background: var(--n-25);
}
.hub-media-cell img { width: 100%; height: 100%; object-fit: contain; }
.hub-media-empty { color: var(--n-300); font-size: var(--text-2xl); }

/* ---- Category tree: zebra whole subtrees by their top-level root, so each
   root group reads as a separate block ---- */
.hub-table tbody tr.cat-grp-a > td { background: var(--n-0); }
.hub-table tbody tr.cat-grp-b > td { background: var(--n-25); }
/* a small gap above each new root group (border colour = card background) */
.hub-table tbody tr.cat-grp-start > td { border-top: 7px solid var(--n-0); }
/* root rows: heavier, darker; child rows: lighter so depth reads at a glance */
.hub-table tbody tr.cat-grp-head > td:first-child { font-weight: 600; color: var(--text-primary); }
.hub-table tbody tr.cat-row:not(.cat-grp-head) td:first-child a { color: var(--text-muted); }
/* keep a visible hover even though the row background is set on the cells */
.hub-table tbody tr.cat-row:hover > td { background: var(--accent-tint); }
.cat-toggle { color: var(--n-400); line-height: 1; }
.cat-toggle:hover { color: var(--hub-primary); }
/* tree indent: one rule instead of 4235 inline styles (--d = depth level) */
.cat-indent { display: inline-block; width: calc(var(--d, 0) * 1.2rem); }
.cat-toggle-cell { display: inline-block; width: 1.4rem; text-align: center; }
.cat-icon {
    width: 22px; height: 22px;
    object-fit: contain;
    border-radius: var(--radius-xs);
    vertical-align: text-bottom;
}

/* "Publish to store" dot toggles — clickable pills with a fill-in dot,
   replacing plain checkboxes for choosing where a product is published. */
.store-dots { display: flex; flex-wrap: wrap; gap: .5rem; }
.store-dots .btn-check { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.store-dot {
    display: inline-flex; align-items: center; gap: .45rem;
    padding: .3rem .75rem; border-radius: var(--radius-pill);
    border: 1px solid var(--n-200); background: var(--n-0); color: var(--text-muted);
    cursor: pointer; user-select: none; font-size: var(--text-sm); line-height: 1.3;
    transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.store-dot .dot {
    width: .7rem; height: .7rem; border-radius: var(--radius-round);
    border: 2px solid var(--n-200); background: transparent; flex: 0 0 auto;
    transition: background .12s ease, border-color .12s ease;
}
.store-dot:hover { border-color: var(--hub-primary); color: var(--text-primary); }
.btn-check:checked + .store-dot {
    border-color: var(--hub-primary); background: var(--accent-tint); color: var(--p-700); font-weight: 600;
}
.btn-check:checked + .store-dot .dot { border-color: var(--hub-primary); background: var(--hub-primary); }
.btn-check:focus-visible + .store-dot { box-shadow: var(--focus-ring); }

/* Per-row publish dots in the product list (one clickable pill per store). */
.store-dots-row { display: flex; flex-wrap: wrap; gap: .3rem; }
.store-dot-btn {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .12rem .55rem; border-radius: var(--radius-pill);
    border: 1px solid var(--n-200); background: var(--n-0); color: var(--text-muted);
    cursor: pointer; font-size: var(--text-xs); line-height: 1.3;
    transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.store-dot-btn .dot {
    width: .6rem; height: .6rem; border-radius: var(--radius-round);
    border: 2px solid var(--n-200); background: transparent; flex: 0 0 auto;
    transition: background .12s ease, border-color .12s ease;
}
.store-dot-btn:hover { border-color: var(--hub-primary); color: var(--text-primary); }
.store-dot-btn.on {
    border-color: var(--hub-primary); background: var(--accent-tint); color: var(--p-700); font-weight: 600;
}
.store-dot-btn.on .dot { border-color: var(--hub-primary); background: var(--hub-primary); }
.store-dot-btn.blocked { opacity: .5; cursor: not-allowed; }
.store-dot-btn.htmx-request { opacity: .5; }
/* One live toggle + its optional "open on store" link. */
.store-dot-cell { display: inline-flex; align-items: center; }
.store-open {
    display: inline-flex; align-items: center; margin-left: -.15rem; padding: .15rem .3rem;
    color: var(--hub-primary); text-decoration: none; font-size: var(--text-sm); border-radius: var(--radius-sm);
}
.store-open:hover { background: var(--accent-tint); }

/* ---- Page header (inline icon + title + subtitle + primary action) ----
   v3: the 46px brand-tinted icon plate is gone — a quiet inline glyph and a
   Linear-scale title (color is dosed; identity comes from type, not a box). */
.page-head {
    display: flex;
    align-items: center;
    gap: .7rem;
    margin-bottom: 1.25rem;
}
.page-head-icon {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--n-400);
    font-size: 1.25rem;
}
/* text stretches; actions sit flush right with the flex gap — never give each
   action its own margin-left:auto (two autos split the free space and the
   first button drifts to the center — audit P3). */
.page-head-text { min-width: 0; flex: 1 1 auto; }
.page-head-title {
    font-size: 21px;
    font-weight: var(--font-weight-semibold);
    letter-spacing: -0.02em;
    color: var(--text-primary);
    line-height: 1.2;
    margin: 0;
}
.page-head-sub { font-size: var(--text-sm); color: var(--text-muted); }
.page-head-action { flex: 0 0 auto; }
/* one control height inside every page header — btn-sm leftovers grow to standard */
.page-head .btn-sm {
    padding: .5rem .8rem;
    font-size: var(--text-sm);
    border-radius: var(--radius-md);
}

/* ---- Dashboard at-a-glance stat tiles ----
   Number-forward operator metrics: a small label, one big tabular figure and a
   contextual foot line (often a filtered link into the list). Tiles that flag
   work needing attention (unprocessed products, unmapped categories) carry a
   warning tint on the whole surface — never a side stripe. */
.hub-stat-group { margin-bottom: var(--space-6); }
.hub-stat-group-label {
    font-size: var(--text-2xs);
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: var(--space-3);
}
.hub-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: var(--space-4);
}
.hub-stat {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-5);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    text-decoration: none;
    color: inherit;
    transition: border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}
a.hub-stat:hover {
    border-color: var(--p-200);
    box-shadow: var(--shadow-sm);
    transform: translateY(-1px);
}
.hub-stat-label {
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--text-muted);
}
.hub-stat-value {
    font-size: var(--text-2xl);
    font-weight: 700;
    line-height: 1.1;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}
.hub-stat-foot {
    font-size: var(--text-sm);
    color: var(--text-muted);
}
.hub-stat-foot .hub-num { font-weight: 600; color: var(--text-secondary); }
.hub-stat-ic {
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    font-size: 1.1rem;
    color: var(--n-400);
}
/* Attention state — a full warm-tinted surface, not a colored edge. */
.hub-stat.is-attention {
    background: color-mix(in srgb, var(--warning) 7%, var(--surface));
    border-color: color-mix(in srgb, var(--warning) 28%, var(--border));
}
.hub-stat.is-attention .hub-stat-value,
.hub-stat.is-attention .hub-stat-ic { color: var(--warning); }
.hub-stat.is-ok .hub-stat-ic { color: var(--success); }

/* ---- Filter toolbar (search + funnel + drop-downs) ---- */
.hub-card { border-radius: var(--radius-lg); }
.hub-filterbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .6rem;
    margin-bottom: 1.1rem;
}
.hub-search { position: relative; flex: 1 1 260px; min-width: 220px; }
.hub-search .form-control {
    padding-left: 2.2rem;
    border-radius: var(--radius-md);
    background: var(--surface);
    border-color: var(--border-control);
}
.hub-search .form-control:focus {
    background: var(--n-0);
    border-color: var(--p-500);
    box-shadow: var(--focus-ring);
}
.hub-search-icon {
    position: absolute; left: .85rem; top: 50%; transform: translateY(-50%);
    color: var(--n-400); pointer-events: none;
}
.hub-filter-funnel {
    flex: 0 0 auto;
    width: 38px; height: 38px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--border-control); border-radius: var(--radius-md);
    background: var(--n-0); color: var(--text-muted);
}
.hub-filter-funnel:hover { border-color: var(--hub-primary); color: var(--hub-primary); background: var(--accent-tint); }
.hub-filter-select {
    flex: 0 1 auto;
    width: auto; min-width: 150px;
    border-radius: var(--radius-md);
    border-color: var(--border-control);
    color: var(--n-700);
}
.hub-filter-reset {
    flex: 0 0 auto;
    color: var(--n-400); padding: .25rem .4rem;
}
.hub-filter-reset:hover { color: var(--hub-primary); }

/* ---- «Найдено N» над таблицей (ecom-5191) ----
   Цифра уже была на странице, но только внутри панели массовых действий, а та
   показывается лишь когда что-то отмечено галкой. Отдельная строка над
   таблицей — чтобы объём отбора читался сразу, без выделения строк. Цифры
   табличные: при смене фильтра число не должно «прыгать» по ширине. */
.hub-list-count {
    /* 04.09.2026: прижато вправо. Заказчик в ecom-5191 просил счётчик «в правой
       верхней части страницы — рядом с блоком фильтров или над таблицей». Над
       таблицей он стоял с самого начала, а по горизонтали лежал у левого края:
       блок растянут на всю ширину, а содержимое без justify-content липнет
       влево. Замер до правки на 1600px: текст занимал x 288-437. */
    display: flex; align-items: center; justify-content: flex-end; gap: .4rem;
    margin: -.35rem 0 .7rem;
    font-size: .8125rem; line-height: 1.2;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}
.hub-list-count i { font-size: .875rem; color: var(--n-400); }

/* ---- Table row density (master catalog + store lists) ----
   Tightened from the old "airy" .8rem to an 8px rhythm — a data-tool wants rows
   the operator can scan, not a comfortable web-app spacing. The product-name
   column still wraps to its full text (shipped requirement), so long names
   still grow their row; short-name rows now pack far tighter. */
.hub-table-airy thead th { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.hub-table-airy td { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.hub-name-link { color: var(--text-primary); font-weight: 500; text-decoration: none; }
.hub-name-link:hover { color: var(--hub-primary); }
.hub-code { text-decoration: none; }
/* Numeric columns: tabular figures so digits line up column-wise (prices,
   margins, stock). Identifiers: monospaced + slashed zero + tabular. */
.hub-num { font-family: var(--font-mono); font-size: 12.5px; font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
/* Column-header abbreviations (ЗЦ/РЦ/…): keep the operator-known short code but
   carry the full name for tooltip + screen reader; drop the browser's dotted rule. */
.hub-table thead th abbr[title] { text-decoration: none; cursor: help; border: 0; }
.hub-id, .hub-code code {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums slashed-zero;
    font-feature-settings: "tnum", "zero";
}
.hub-status { font-weight: 600; border-radius: var(--radius-pill); padding: .3rem .6rem; }

/* ---- Sortable column headers (sort_th tag) ---- */
.hub-sort {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
}
.hub-sort:hover { color: var(--hub-primary); }
.hub-sort-ic { font-size: .7rem; opacity: .4; transition: opacity .1s; }
.hub-sort:hover .hub-sort-ic { opacity: .8; }
.hub-sort.is-active { color: var(--hub-primary); }
.hub-sort.is-active .hub-sort-ic { opacity: 1; }
/* Right-aligned columns: keep the arrow tucked beside right-justified labels. */
.hub-table th.text-end .hub-sort { flex-direction: row-reverse; }

/* ---- Per-row quick actions: a faint icon strip that brightens on row hover
   (folder = category, image = media, sliders = characteristics, compare =
   competitors, files = duplicate). Each opens the product editor on the right
   tab/modal; duplicate POSTs to product_clone. ---- */
/* ---- Stable column widths (fixed layout) -------------------------------
   Sortable lists set their column widths via a <colgroup>; fixed layout makes
   those widths authoritative so re-sorting (which brings differently-sized rows
   onto the page) never recomputes — the table no longer "jumps" on each click.
   Cells that can overflow their column clip with an ellipsis. Pair with a
   <colgroup> in the template. width:auto + min-width:100% = size to the
   colgroup, but never narrower than the container (scrolls in .table-responsive
   when the columns need more room). */
.hub-table-fixed { table-layout: fixed; width: 100%; }
.hub-table-fixed th,
.hub-table-fixed td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Multi-line price cells — per-site internet prices (ИЦ) and competitor prices —
   stack one "<label> <amount>" line per site/competitor. Clip each LINE, not the
   whole cell.

   Inside a line the AMOUNT is the payload and must survive: it keeps its
   intrinsic width, and the label shrinks with an ellipsis instead (the full
   label is already on the row's title=""). Before this, a label longer than the
   column ("xiaomistore.md") pushed the amount past the fixed-column edge, where
   overflow:hidden cut it off completely and silently — the operator saw a site
   name with no price at all (CRM ecom-5010, 07.08.2026). The colgroup widths are
   sized for today's real data; this rule is what keeps the next longer name from
   costing a price again. */
.hub-table-fixed .site-prices,
.hub-table-fixed .competitor-prices { white-space: normal; }
.hub-table-fixed .site-prices > div,
.hub-table-fixed .competitor-prices > div {
    display: flex;
    align-items: baseline;
    overflow: hidden;
    /* Flex drops the inter-element whitespace the inline layout used to give
       (label · amount · ▼), so the gap becomes explicit — and the .ms-1 the
       markup carries for that same spacing is neutralised to avoid doubling. */
    column-gap: .25rem;
}
.hub-table-fixed .site-prices > div > .ms-1,
.hub-table-fixed .competitor-prices > div > .ms-1 { margin-left: 0 !important; }
.hub-table-fixed .site-prices > div { justify-content: flex-end; }
.hub-table-fixed .site-prices > div > *,
.hub-table-fixed .competitor-prices > div > * { flex: 0 0 auto; }
.hub-table-fixed .site-prices > div > :first-child,
.hub-table-fixed .competitor-prices > div > :first-child {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Store-publish dots wrap freely inside their own (flex) cell. */
.hub-table-fixed .store-dots-row { white-space: normal; }
/* Opt-out of the ellipsis clip for cells that must show their full content
   (product name column): wrap onto multiple lines instead of truncating.
   The <colgroup> width still caps the column — the row grows in height. */
.hub-table-fixed td.hub-cell-wrap {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    word-break: break-word;
}

/* ---- TomSelect dropdowns parked on <body> -------------------------------
   Selects inside a modal OR inside a .table-responsive box get
   dropdownParent:'body' (see dropdownParentFor in base.html): both containers
   clip the absolutely-positioned dropdown — the scrollable .modal-body at the
   modal edge (CRM ecom-4930 п.2), the table box at its bottom edge because of
   the overflow-y: hidden below (CRM ecom-5156). Parked on <body>, the dropdown
   must stack above the Bootstrap modal (z-index 1055). Selects elsewhere keep
   the default in-place dropdown, which this selector does not match. */
body > .ts-dropdown { z-index: 1060; }

/* ---- No phantom vertical scrollbar on wide listings ---------------------
   .table-responsive only ever needs to scroll SIDEWAYS (its height is auto and
   grows with the table). But overflow-x: auto computes overflow-y to auto too,
   and with classic (non-overlay) scrollbars — external mouse / macOS "show
   scroll bars: always" — sub-pixel row rounding leaves the box 1-15px "taller"
   than its client height. Chrome then draws a second, near-useless vertical
   scrollbar that latches wheel events: the page freezes until the inner
   scroller exhausts its few pixels.

   Vertical clipping is only safe because nothing that has to escape the box is
   positioned inside it: the Bootstrap kebab menus render against the viewport
   via a fixed-strategy Popper (see the kebab notes below), and TomSelect —
   which does NOT use Popper — parks its dropdown on <body> (dropdownParentFor
   in base.html). Removing that parenting and keeping this line cuts the option
   list off at the bottom of the table, which is exactly what ecom-5156 was. */
.table-responsive { overflow-y: hidden; }

/* ---- Sticky horizontal scrollbar (wide list tables) ---------------------
   Proxy scrollbar fixed to the bottom edge of the viewport, created by
   static/portal/js/sticky-hscroll.js for [data-sticky-hscroll] containers.
   Shown only while the container overflows sideways and its native scrollbar
   is below the fold. The explicit scrollbar styling opts out of overlay
   scrollbars, so the bar is always visible — not only mid-scroll. */
.hub-sticky-hscroll {
    display: none;
    position: fixed;
    bottom: 0;
    height: 14px;
    z-index: 1030;
    background: var(--border);
    border-top: 1px solid var(--n-150);
    cursor: pointer;
    touch-action: none;              /* our pointer handlers own the gestures */
    user-select: none;
}
.hub-sticky-hscroll.is-visible { display: block; }
/* Custom thumb — the native scrollbar of a proxy div stays INVISIBLE until the
   first scroll on macOS overlay mode (ecom-4930-bf1 баг 2), so the bar draws
   its own always-visible dark thumb and handles dragging itself. */
.hub-sticky-hscroll-thumb {
    position: absolute;
    top: 2px;
    height: 10px;
    background: var(--text-muted);
    border-radius: var(--radius-xs);
    cursor: grab;
}
.hub-sticky-hscroll-thumb:hover { background: var(--text-secondary); }
.hub-sticky-hscroll.is-dragging .hub-sticky-hscroll-thumb {
    background: var(--n-700);
    cursor: grabbing;
}

/* ---- Selection bar (bulk actions over a long list) ----------------------
   Pinned to the BOTTOM edge of the viewport while its list scrolls — the same
   position: sticky trick .portal-topbar uses at the top, and the reason the
   merge button no longer sits in the filter row: ticking a value at the end of
   the page meant scrolling back up to reach it (CRM ecom-5010, Iuliana 07.08
   12:12). It settles into place at the end of the card once the list is fully
   scrolled, so it never permanently covers the last rows.
   z-index stays below the topbar (10) — the sticky top chrome wins. */
.hub-selection-bar {
    position: sticky;
    bottom: 0;
    z-index: 5;
    margin-top: .75rem;
    padding: .55rem .85rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-pop);
}

/* ---- Row quick-actions kebab (⋮) ---------------------------------------
   Consolidates the per-row quick actions into one ⋮ menu pinned to the left of
   the row (always visible — no horizontal scroll to reach it). The cell opts
   out of the ellipsis clip so the menu can render; Popper keeps the menu inside
   the .table-responsive scroll box (flips up near the bottom edge). */
.hub-table-fixed th.hub-actions-cell,
.hub-table-fixed td.hub-actions-cell { overflow: visible; }
.hub-row-actions-menu { line-height: 1; }
.hub-actions-toggle {
    padding: .25rem .4rem;
    line-height: 1;
    border: 0;
    background: transparent;
    color: var(--n-400);
    border-radius: var(--radius-md);
}
.hub-actions-toggle:hover,
.hub-actions-toggle:focus,
.hub-actions-toggle[aria-expanded="true"] {
    color: var(--hub-primary);
    background: var(--accent-tint);
}
.hub-actions-toggle::after { display: none; }  /* no Bootstrap caret */
.hub-actions-menu {
    --bs-dropdown-min-width: 12rem;
    z-index: 1030;
    box-shadow: 0 8px 28px rgba(15, 23, 42, .14);
}
.hub-actions-menu .dropdown-item { display: flex; align-items: center; }
.hub-actions-menu form { margin: 0; }

/* Quick favorite ⭐ toggle in the product-list row (next to the ⋮ menu). */
.hub-row-tools { display: inline-flex; align-items: center; gap: .1rem; }
.hub-fav-toggle {
    padding: .25rem .35rem;
    line-height: 1;
    border: 0;
    background: transparent;
    color: var(--n-200);
    border-radius: var(--radius-md);
}
.hub-fav-toggle:hover,
.hub-fav-toggle:focus { color: var(--star); background: #fffbeb; }
.hub-fav-toggle.is-fav { color: var(--star); }

/* ---- Aligned field grids ------------------------------------------------
   In a row of side-by-side fields, a long label (esp. RU) wraps to two lines in
   a narrow column while its neighbours' labels fit on one — pushing that field's
   input down (or a short label pulling its input up), so inputs no longer line
   up (CRM 8780 #3/#4). Reserve a consistent two-line label height at md+ and
   bottom-align the text, so every input in the row starts at the same y. On
   mobile the fields stack, so no reserved height is needed. */
@media (min-width: 768px) {
  .hub-aligned-fields > [class*="col-"] > .form-label {
    min-height: 3rem;  /* reserve two label lines so a wrapped long label and a
                          one-line neighbour keep their inputs on the same line */
    display: flex;
    align-items: flex-end;
    margin-bottom: .25rem;
  }
}

/* Collapsed rows in a long attribute-value list — hidden but kept in the DOM so
   the formset still submits them (CRM 8780 #5). */
tr.value-collapsed { display: none; }

/* ---- Image drop-zone + drag-reorder (product editors) ---- */
.hub-dropzone {
    border: 2px dashed var(--n-200);
    border-radius: var(--radius-md);
    padding: 1.25rem 1rem;
    text-align: center;
    color: var(--text-muted);
    cursor: pointer;
    transition: border-color .15s, background .15s;
}
.hub-dropzone:hover { border-color: var(--n-300); background: var(--n-25); }
.hub-dropzone.is-dragover { border-color: var(--p-400); background: var(--accent-tint); color: var(--p-800); }
.hub-drag-handle { cursor: grab; align-self: center; }
.hub-drag-handle:active { cursor: grabbing; }
.image-row.sortable-ghost { opacity: .4; }
.image-row.sortable-chosen { background: var(--n-25); border-radius: var(--radius-md); }

/* ---- Dropdown-tree select (js-treeselect) -------------------------------
   Browse mode: collapsed roots + chevrons, indent via padding (never dashes).
   Search mode (.is-searching): flat matches with a muted breadcrumb path.
   The control shows the clean selected name and never accepts inline typing. */
.ts-wrapper.hub-filter-select { min-width: 230px; max-width: 320px; }
/* The dropdown outgrows the control (Ant: dropdownMatchSelectWidth=false) —
   tree rows need room for real category names. */
.ts-wrapper.hub-filter-select .ts-dropdown { min-width: 320px; max-width: 420px; width: max-content; }
.ts-wrapper.hub-filter-select .ts-control { padding-right: 2rem; flex-wrap: nowrap; }
.ts-wrapper.hub-filter-select .ts-control > div {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
/* Hierarchy = indent CELLS (each with a guide line) + chevron. The guides sit
   in dedicated flex cells before the chevron/text (VS Code / file-tree pattern),
   so a line can never cross the label. Single-line labels, no wrapping. */
.ts-tree-row { display: flex; align-items: center; gap: 0;
    min-height: 2rem; padding: 0 0.75rem 0 0.5rem; }
.ts-tree-hidden { display: none; }
/* One 20px indent cell per ancestor level; the guide line runs down its middle,
   full row height, so lines connect across stacked siblings. */
.ts-tree-indent { flex: 0 0 20px; align-self: stretch; position: relative; }
.ts-tree-indent::before { content: ""; position: absolute;
    left: 9px; top: 0; bottom: 0; border-left: 1px solid var(--n-150); }
.ts-tree-chevron { flex: 0 0 1.1rem; width: 1.1rem; font-size: var(--text-xs); line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--text-muted); cursor: pointer; transition: transform 0.15s ease-out; }
.ts-tree-chevron:hover { color: var(--text-primary); }
.ts-tree-chevron.is-open { transform: rotate(90deg); }
.ts-tree-chevron-none { cursor: default; }
.ts-tree-label { min-width: 0; flex: 1 1 auto; padding-left: 0.2rem; }
.ts-tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts-tree-path { font-size: var(--text-xs); color: var(--text-muted); display: none;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.is-searching .ts-tree-path { display: block; }
/* Search = flat list: no indent cells, no chevrons, path shown instead. */
.is-searching .ts-tree-indent { display: none; }
.is-searching .ts-tree-chevron { display: none; }
@media (prefers-reduced-motion: reduce) {
    .ts-tree-chevron { transition: none; }
}

/* ==== Form controls ==========================================================
   One vocabulary for every input: gray-300 border, 8px radius, soft 4px teal
   halo on focus (no layout shift — Untitled UI ring pattern). */
.form-control, .form-select {
    border-color: var(--border-control);
    border-radius: var(--radius-md);
    color: var(--text-primary);
    transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.form-control:focus, .form-select:focus {
    border-color: var(--p-500);
    box-shadow: var(--focus-ring);
}
.form-control::placeholder { color: var(--n-400); }
.form-label { font-size: var(--text-sm); font-weight: var(--font-weight-medium); color: var(--n-700); }
.form-check-input:checked { background-color: var(--accent); border-color: var(--accent); }
.form-check-input:focus { border-color: var(--p-400); box-shadow: var(--focus-ring); }
.form-text { color: var(--text-muted); }

/* ==== TomSelect: match the input vocabulary ==================================
   In the bootstrap5 theme the visible box is the WRAPPER (.ts-wrapper.form-select
   / .form-control), not .ts-control — style both. */
.ts-wrapper.form-select,
.ts-wrapper.form-control,
.ts-wrapper .ts-control {
    border-color: var(--border-control);
    border-radius: var(--radius-md);
    box-shadow: none;
    transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.ts-wrapper.form-select.focus,
.ts-wrapper.form-control.focus,
.ts-wrapper.focus .ts-control { border-color: var(--p-500); box-shadow: var(--focus-ring); }
.ts-dropdown {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-pop);
    margin-top: 4px;
    padding: .3rem;
}
.ts-dropdown .option { border-radius: var(--radius-sm); padding: .4rem .6rem; }
.ts-dropdown .option.active { background: var(--accent-tint); color: var(--p-900); }
.ts-dropdown .create.active { background: var(--accent-tint); color: var(--p-900); }
.ts-dropdown .dropdown-input {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: none;
    padding: .4rem .6rem;
}
.ts-dropdown .dropdown-input:focus { border-color: var(--p-500); box-shadow: var(--focus-ring); }
.ts-wrapper.multi .ts-control > div {
    background: var(--n-100);
    color: var(--n-700);
    border-radius: var(--radius-sm);
}

/* ==== Cards / modals / alerts ================================================ */
.card {
    --bs-card-border-color: var(--border);
    --bs-card-border-radius: var(--radius-lg);
    --bs-card-cap-bg: var(--n-25);
}
.shadow-sm { box-shadow: var(--shadow-sm) !important; }  /* soften the utility to hairline scale */
.modal-content {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
}
.modal-header { border-bottom-color: var(--border); }
.modal-footer { border-top-color: var(--border); }
.portal-content .alert { border-radius: var(--radius-md); }

/* ---- Scrollable popup whose header/body/footer live inside a <form> ------
   Bootstrap's .modal-dialog-scrollable only works while .modal-header /
   .modal-body / .modal-footer are DIRECT children of .modal-content: that box
   is the flex column which caps the body's height, and .modal-body scrolls
   ONLY because the cap exists. Every quick-action fragment of the portal wraps
   the three in one <form> (the whole popup is a single POST), and a plain
   block <form> in between breaks the chain — .modal-body keeps its automatic
   height, its `overflow-y: auto` has nothing left to scroll, and the
   `overflow: hidden` that .modal-dialog-scrollable puts on .modal-content
   simply CUTS the rest of the popup off.

   Harmless while every popup was short. The storefront-description editor
   (ecom-5138) is ~3 200px of CKEditor on a card that stands on three sites, so
   the operator got a window with no scrollbar, a dead wheel, a dead PageDown —
   and «Сохранить» 2 400px below the bottom edge of the screen. Returned by the
   customer 17.08.2026: «Не работает скролл в данном редакторе».

   The form becomes the flex column instead of breaking it. `min-height: 0` is
   the load-bearing line: without it the form's automatic minimum size is its
   full content height, it refuses to shrink inside .modal-content, and nothing
   changes. The height cap itself comes from flex-shrink against the
   .modal-content box — NOT from a `max-height: 100%` here, which resolves to
   `none` because .modal-content's own used height is auto (checked in the
   browser: the popup scrolls identically with and without it). Fixed here
   rather than in the fragment because all seven quick popups share the shape. */
.modal-dialog-scrollable .modal-content > form {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* The bar that body now grows is deliberately left as the platform's own — no
   ::-webkit-scrollbar here. It is already the right bar: measured in Chrome 146
   on the operator's own machine, the popup body reports a 15px classic track
   (the browser honours `::-webkit-scrollbar` there too, so styling it is a
   choice, not a necessity). Two traps for whoever changes their mind: Chrome
   ignores every ::-webkit-scrollbar rule as soon as `scrollbar-width` is set on
   the same element, and headless Chromium on macOS reports a 0px overlay bar
   whatever the CSS says — so a browser test can neither see nor prove the
   restyle. */

/* ==== Bootstrap remap sweep ================================================
   Components whose defaults still shipped raw blue/green/yellow. Every state
   (hover / focus / active / disabled) rebound to tokens. */

/* Pagination — sits under EVERY list; was default-blue links + blue active. */
.pagination {
    --bs-pagination-color: var(--text-secondary);
    --bs-pagination-bg: var(--n-0);
    --bs-pagination-border-color: var(--n-150);
    --bs-pagination-border-radius: var(--radius-md);
    --bs-pagination-hover-color: var(--accent);
    --bs-pagination-hover-bg: var(--n-50);
    --bs-pagination-hover-border-color: var(--n-200);
    --bs-pagination-focus-color: var(--accent);
    --bs-pagination-focus-bg: var(--n-50);
    --bs-pagination-focus-box-shadow: var(--focus-ring);
    --bs-pagination-active-color: var(--n-0);
    --bs-pagination-active-bg: var(--n-900);      /* concept: ink page chip */
    --bs-pagination-active-border-color: var(--n-900);
    --bs-pagination-disabled-color: var(--n-400);
    --bs-pagination-disabled-bg: var(--n-0);
    --bs-pagination-disabled-border-color: var(--n-150);
}
.page-link { font-size: var(--text-sm); }

/* Raw nav-pills (category filters block) — active was bootstrap blue. */
.nav-pills {
    --bs-nav-pills-link-active-bg: var(--accent);
    --bs-nav-pills-link-active-color: var(--n-0);
    --bs-nav-pills-border-radius: var(--radius-md);
}
/* Raw .nav-tabs (source detail) — unify with the hub underline vocabulary. */
.nav-tabs {
    --bs-nav-tabs-border-width: 0;
    border-bottom: 1px solid var(--n-150);
}
.nav-tabs .nav-link {
    border: 0;
    color: var(--text-muted);
    border-bottom: 2px solid transparent;
    border-radius: 0;
    margin-bottom: -1px;
}
.nav-tabs .nav-link:hover { color: var(--text-primary); }
.nav-tabs .nav-link.active {
    color: var(--hub-primary);
    background: transparent;
    border-bottom-color: var(--hub-primary);
    font-weight: var(--font-weight-medium);
}

/* Blue/green/gray utilities → tokens (bootstrap defines them !important). */
.text-primary { color: var(--accent) !important; }
.bg-primary { background-color: var(--accent) !important; }
.bg-primary-subtle { background-color: var(--p-50) !important; }
.text-primary-emphasis { color: var(--p-800) !important; }
.text-success { color: var(--success) !important; }
.text-danger { color: var(--danger) !important; }
.text-muted { color: var(--text-muted) !important; }
.text-secondary { color: var(--text-secondary) !important; }
.bg-secondary { background-color: var(--n-500) !important; }

/* Review-gate negative action ("Mark unprocessed") — amber token, not #ffc107. */
.btn-outline-warning {
    --bs-btn-color: var(--warning);
    --bs-btn-border-color: color-mix(in srgb, var(--warning) 55%, var(--border-control));
    --bs-btn-hover-bg: var(--warning);
    --bs-btn-hover-border-color: var(--warning);
    --bs-btn-hover-color: var(--n-0);
    --bs-btn-active-bg: var(--warning);
    --bs-btn-active-border-color: var(--warning);
    --bs-btn-active-color: var(--n-0);
    --bs-btn-disabled-color: var(--warning);
    --bs-btn-disabled-border-color: var(--warning);
}

/* Validation — bootstrap red #dc3545 → our danger, focus halo included. */
.form-control.is-invalid, .form-select.is-invalid { border-color: var(--danger); }
.form-control.is-invalid:focus, .form-select.is-invalid:focus {
    border-color: var(--danger);
    box-shadow: 0 0 0 4px rgba(220, 38, 38, .15);
}
.invalid-feedback { color: var(--danger); }

/* ==== Empty states =========================================================
   One vocabulary for "nothing here" cells: roomy, quiet, 13px; an icon inside
   the cell (when present) becomes a soft block glyph. */
.hub-table td.hub-empty, td.hub-empty {
    text-align: center;
    color: var(--text-muted);
    font-size: var(--text-sm);
    padding-top: 2.75rem !important;
    padding-bottom: 2.75rem !important;
}
td.hub-empty > .bi, td.hub-empty > i.bi {
    display: block;
    font-size: 1.5rem;
    color: var(--n-300);
    margin-bottom: .5rem;
}

/* ==== Skeleton loader (HTMX blocks / modal bodies) ========================= */
.hub-skeleton { display: flex; flex-direction: column; gap: 10px; padding: 6px 0; }
.hub-skeleton i {
    display: block;
    height: 12px;
    border-radius: 6px;
    background: linear-gradient(90deg, var(--n-100) 25%, var(--n-50) 45%, var(--n-100) 65%);
    background-size: 200% 100%;
    animation: hub-shimmer 1.2s linear infinite;
}
.hub-skeleton i:nth-child(2) { width: 82%; }
.hub-skeleton i:nth-child(3) { width: 58%; }
@keyframes hub-shimmer { to { background-position: -200% 0; } }

/* ==== Entry stagger (motion conveys arrival, capped to the visible top) ====
   Only the first 12 rows animate — deep lists (4235-row category tree) must
   not composite thousands of layers. Tiles stagger on the dashboard. */
@media (prefers-reduced-motion: no-preference) {
    .hub-table tbody tr:nth-child(-n+12) { animation: hub-row-in .3s var(--ease-out) backwards; }
    .hub-table tbody tr:nth-child(2)  { animation-delay: 25ms; }
    .hub-table tbody tr:nth-child(3)  { animation-delay: 50ms; }
    .hub-table tbody tr:nth-child(4)  { animation-delay: 75ms; }
    .hub-table tbody tr:nth-child(5)  { animation-delay: 100ms; }
    .hub-table tbody tr:nth-child(6)  { animation-delay: 125ms; }
    .hub-table tbody tr:nth-child(7)  { animation-delay: 150ms; }
    .hub-table tbody tr:nth-child(8)  { animation-delay: 175ms; }
    .hub-table tbody tr:nth-child(9)  { animation-delay: 200ms; }
    .hub-table tbody tr:nth-child(10) { animation-delay: 225ms; }
    .hub-table tbody tr:nth-child(11) { animation-delay: 250ms; }
    .hub-table tbody tr:nth-child(12) { animation-delay: 275ms; }
    .hub-stats .hub-stat { animation: hub-row-in .35s var(--ease-out) backwards; }
    .hub-stats .hub-stat:nth-child(2) { animation-delay: 40ms; }
    .hub-stats .hub-stat:nth-child(3) { animation-delay: 80ms; }
    .hub-stats .hub-stat:nth-child(4) { animation-delay: 120ms; }
}
@keyframes hub-row-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
}
