:root {
    /* ── Cool sage-green scale ─────────────────────────────── */
    --forest-950: #0c1a11;
    --forest-900: #162c1d;
    --forest-800: #26422f;
    --forest-700: #365841;
    --forest-600: #466e53;
    --forest-500: #4e7a5d;       /* darkened from #568465 → ~4.9:1 on white (WCAG AA) */
    --forest-400: #78a085;
    --forest-300: #9cbda6;
    --forest-200: #c1d8c8;
    --forest-100: #dbeadf;
    --forest-50: #edf5ef;

    /* ── Sage neutrals (cool green tint) ───────────────────── */
    --sage-50: #f1f5f2;
    --sage-100: #e2eae4;
    --sage-200: #cdd8d0;
    /* 300/400 were REFERENCED by mappings/reconcile.html + reconcile.js long before
       they were defined — 11 of those sites had no fallback, so the declarations were
       invalid and silently dropped (N/A slot borders rendered as currentColor).
       --sage-300 is pinned to #cbd5c0, the value the fallback sites already used, so
       this defines the broken sites WITHOUT changing the ones that were working. */
    --sage-300: #cbd5c0;
    --sage-400: #aebcb2;
    --cream: #f1f5f2;

    /* ── Gold / warning accent ─────────────────────────────── */
    /* The app already used a de-facto amber ramp (96 amber-* utilities) and
       reconcile.html started formalising it as --gold-500/--gold-400 without ever
       declaring it — so the "new match" dot rendered transparent. Values are the
       amber steps the existing fallbacks already named. */
    --gold-400: #fbbf24;
    --gold-500: #f59e0b;
}

/* ── Global typography ───────────────────────────────────── */
body {
    font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
    -webkit-font-smoothing: antialiased;
    letter-spacing: -0.011em;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: #1a2e20;
}

h1, h2, h3, .font-serif {
    font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
    font-weight: 600;
    letter-spacing: -0.02em;
}

/* ── Sidebar ─────────────────────────────────────────────── */
.sidebar {
    background: #ffffff;
    border-right: 1px solid var(--sage-200);
}

.sidebar-link {
    transition: all 0.15s ease;
    border-left: 2px solid transparent;
}

.sidebar-link:hover {
    background: var(--sage-50);
    border-left-color: var(--forest-300);
}

.sidebar-link.active {
    background: var(--forest-50);
    border-left-color: var(--forest-600);
    font-weight: 500;
    color: var(--forest-900);
}

/* ── Data tables ─────────────────────────────────────────── */
.data-table {
    border-collapse: separate;
    border-spacing: 0;
}

.data-table thead th {
    font-weight: 600;
    font-size: 0.75rem;          /* 12px — was 11px; readability on data-dense pages */
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--forest-600);    /* darker than -500 for AA on small uppercase headers */
    border-bottom: 1px solid var(--sage-200);
    padding: 0.625rem 1rem;
    white-space: nowrap;
    text-align: left;
}

.data-table tbody td {
    padding: 0.625rem 1rem;
    font-size: 0.875rem;         /* 14px — was 13px; readability for hours-long use */
    border-bottom: 1px solid var(--sage-100);
    color: var(--forest-900);
}

.data-table tbody tr {
    transition: background 0.12s ease;
}

.data-table tbody tr:hover {
    background: var(--sage-50);
}

.data-table tbody tr:last-child td {
    border-bottom: none;
}

/* Sortable column headers */
.data-table thead th .sort-arrow {
    color: var(--forest-600);
    pointer-events: none;
}

/* ── Async operations spinner ────────────────────────────── */
@keyframes spin { to { transform: rotate(360deg); } }
.animate-spin { animation: spin 1s linear infinite; }

/* ── One-shot attention pulse ─────────────────────────────── */
/* stock/session_entry.html has called markDirty() -> add('animate-pulse-once')
   on the Save-All button since it was written, but the class was never defined
   anywhere — so the "you have unsaved counts" cue silently did nothing. */
@keyframes pulse-once {
    0%   { transform: scale(1);    box-shadow: 0 0 0 0 rgba(70, 110, 83, 0.45); }
    50%  { transform: scale(1.04); box-shadow: 0 0 0 6px rgba(70, 110, 83, 0); }
    100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(70, 110, 83, 0); }
}
.animate-pulse-once { animation: pulse-once 0.6s ease-out 2; }
@media (prefers-reduced-motion: reduce) {
    .animate-pulse-once { animation: none; }
}

/* ── Cards ────────────────────────────────────────────────── */
.card {
    background: white;
    border: 1px solid var(--sage-200);
    border-radius: 0.625rem;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.card:hover {
    border-color: var(--forest-200);
    box-shadow: 0 2px 12px rgba(22, 44, 29, 0.05);
}

/* ── Form inputs ──────────────────────────────────────────── */
.form-input {
    border: 1px solid var(--sage-200);
    border-radius: 0.5rem;
    padding: 0.5rem 0.75rem;
    font-size: 0.8125rem;
    transition: all 0.15s ease;
    background: white;
    color: var(--forest-900);
}

.form-input:focus {
    outline: none;
    border-color: var(--forest-400);
    box-shadow: 0 0 0 3px rgba(70, 110, 83, 0.12);
}

.form-input[readonly] {
    background: var(--sage-50);
    color: var(--forest-500);
}

/* ── Buttons ──────────────────────────────────────────────── */
.btn-primary {
    background: var(--forest-800);
    color: white;
    padding: 0.5rem 1.25rem;
    border-radius: 0.5rem;
    font-size: 0.8125rem;
    font-weight: 500;
    letter-spacing: 0.01em;
    transition: all 0.15s ease;
    border: 1px solid transparent;
}

.btn-primary:hover {
    background: var(--forest-700);
    box-shadow: 0 1px 4px rgba(22, 44, 29, 0.15);
}

.btn-secondary {
    background: white;
    color: var(--forest-800);
    padding: 0.5rem 1.25rem;
    border-radius: 0.5rem;
    font-size: 0.8125rem;
    font-weight: 500;
    border: 1px solid var(--sage-200);
    transition: all 0.15s ease;
}

.btn-secondary:hover {
    background: var(--sage-50);
    border-color: var(--forest-300);
}

.btn-export {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 0.5rem;
    border: 1px solid var(--sage-200);
    background: white;
    color: var(--forest-600);
    cursor: pointer;
    transition: all 0.15s;
}
.btn-export:hover {
    background: var(--forest-50);
    border-color: var(--forest-400);
    color: var(--forest-700);
}

.btn-danger {
    color: #b91c1c;
    font-size: 0.8125rem;
    font-weight: 500;
    transition: color 0.15s ease;
}

.btn-danger:hover {
    color: #991b1b;
}

/* Filled danger button — the one source of truth for destructive confirm
   actions (modals, delete buttons). Replaces ad-hoc `bg-red-700 text-white`. */
.btn-danger-solid {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.5rem 1.25rem;
    font-size: 0.8125rem;
    font-weight: 500;
    color: #fff;
    background: #b91c1c;
    border-radius: 0.375rem;
    transition: background 0.15s ease;
}
.btn-danger-solid:hover { background: #991b1b; }

/* ── Decision action chips (accept / reject / skip) ─────────
   One source of truth for the decision verbs, used in the pending
   queue cards, the decided-history table, the bulk bar, and the JS
   action rebuild. Previously these were re-styled inline in 4 places.
   .btn-chip = shape/size base; -sm / -lg = density variants. */
.btn-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.375rem 0.75rem;
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.2;
    border-radius: 0.375rem;
    border: 1px solid transparent;
    white-space: nowrap;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}
.btn-chip-sm { padding: 0.25rem 0.5rem; }
.btn-chip-lg { padding: 0.5rem 1rem; font-size: 0.8125rem; }

.btn-accept  { background: #ecfdf5; color: #047857; }
.btn-accept:hover  { background: #d1fae5; }
.btn-reject  { background: #fef2f2; color: #dc2626; }
.btn-reject:hover  { background: #fee2e2; }
.btn-skip    { background: #f3f4f6; color: #4b5563; }
.btn-skip:hover    { background: #e5e7eb; }

/* Outline chip — the fill variant the family was missing.
   .btn-chip is a SHAPE base: padding, radius, min-height and `border: 1px solid
   transparent`. It needs a fill modifier (accept/reject/skip). There was no
   OUTLINE variant, so six templates independently reached for `.btn-secondary`
   alongside it — which does not work: .btn-chip is declared LATER at equal
   specificity, so its transparent border wins while .btn-secondary's white
   background stays. The result is a white button with no border on a white
   card: no affordance at all. Shipped live in #366 on the Reporting filters.
   Use this instead of mixing the two families. */
.btn-chip-outline { background: white; border-color: var(--sage-200); color: var(--forest-700); }
.btn-chip-outline:hover { background: var(--sage-50); border-color: var(--forest-300); }

/* ── Status badges ────────────────────────────────────────── */
.badge {
    display: inline-flex;
    align-items: center;
    padding: 0.125rem 0.625rem;
    border-radius: 9999px;
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.03em;
    text-transform: uppercase;
}

.badge-active   { background: var(--forest-50); color: var(--forest-700); }
.badge-inactive { background: #f3f4f6; color: #6b7280; }
.badge-superadmin { background: #fef2f2; color: #991b1b; }
.badge-am       { background: var(--forest-100); color: var(--forest-700); }
.badge-brand-admin { background: #faf5ff; color: #6b21a8; }
.badge-warehouse { background: #fffbeb; color: #92400e; }
.badge-production { background: #f0fdf4; color: #166534; }

/* Semantic status badges — use these instead of ad-hoc bg-*-50/text-*-700.
   All foreground colours meet WCAG AA on their tint. */
.badge-success { background: #f0fdf4; color: #166534; }
.badge-warning { background: #fffbeb; color: #92400e; }
.badge-danger  { background: #fef2f2; color: #991b1b; }
.badge-info    { background: #eff6ff; color: #1e40af; }
.badge-neutral { background: #f3f4f6; color: #4b5563; }

/* ── Pagination ───────────────────────────────────────────── */
.pagination-link {
    padding: 0.375rem 0.75rem;
    font-size: 0.8125rem;
    border: 1px solid var(--sage-200);
    border-radius: 0.5rem;
    color: var(--forest-800);
    transition: all 0.15s ease;
}

.pagination-link:hover {
    background: var(--sage-50);
    border-color: var(--forest-300);
}

/* ── Search input ─────────────────────────────────────────── */
.search-input {
    border: 1px solid var(--sage-200);
    border-radius: 0.5rem;
    padding: 0.375rem 0.75rem;
    padding-left: 2.25rem;
    font-size: 0.8125rem;
    background: white;
    transition: all 0.15s ease;
    width: 16rem;
    color: var(--forest-900);
}

.search-input:focus {
    outline: none;
    border-color: var(--forest-400);
    box-shadow: 0 0 0 3px rgba(70, 110, 83, 0.12);
}

.search-input::placeholder {
    color: var(--forest-500);    /* was forest-400 — more legible hint text */
    opacity: 0.8;
}

/* ── HTMX indicators ─────────────────────────────────────── */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-flex; }
.htmx-request.htmx-indicator { display: inline-flex; }

/* ── Sidebar nav counts ───────────────────────────────────── */
.nav-count {
    font-size: 0.6875rem;        /* 11px — was 10px */
    font-weight: 500;
    color: var(--forest-600);    /* was forest-400 (2.9:1, failed AA on white) */
    min-width: 1.25rem;
    text-align: right;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.nav-count-visible {
    opacity: 0.9;                /* was 0.6 — opacity compounded the low contrast */
}

/* ── Divider labels ───────────────────────────────────────── */
.divider-label {
    font-size: 0.6875rem;        /* 11px — was 10px */
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--forest-600);    /* was forest-400 — AA on white */
}

/* ── Scrollbar ────────────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--sage-200); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--forest-300); }

/* ── Credential cards ─────────────────────────────────────── */
.cred-card {
    transition: all 0.2s ease;
}

.cred-card:hover {
    border-color: var(--forest-200);
    box-shadow: 0 2px 12px rgba(22, 44, 29, 0.05);
}

.cred-initials-badge {
    letter-spacing: 0.05em;
}

.cred-field-label {
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--forest-600);    /* was forest-400 — AA on white */
    min-width: 5.5rem;
    flex-shrink: 0;
}

.cred-action-btn {
    flex-shrink: 0;
    padding: 0.25rem;
    border-radius: 0.25rem;
    color: var(--forest-300);
    transition: all 0.15s ease;
    opacity: 0;
}

.cred-field-row:hover .cred-action-btn,
.cred-action-btn:focus {
    opacity: 1;
}

.cred-action-btn:hover {
    color: var(--forest-600);
    background: var(--forest-50);
}

/* ── Stats cards ──────────────────────────────────────────── */
.stat-card {
    background: white;
    border: 1px solid var(--sage-200);
    border-radius: 0.625rem;
    padding: 1rem 1.25rem;
    transition: all 0.2s ease;
}

.stat-card:hover {
    border-color: var(--forest-200);
    box-shadow: 0 2px 8px rgba(22, 44, 29, 0.04);
}

/* ── Empty state ──────────────────────────────────────────── */
.empty-state {
    padding: 3rem 1rem;
    text-align: center;
    color: var(--forest-600);    /* was forest-400 — AA on white */
}

/* ── Link styles ──────────────────────────────────────────── */
.link-primary {
    color: var(--forest-600);
    transition: color 0.15s ease;
}

.link-primary:hover {
    color: var(--forest-800);
}

/* ── Brand switcher ───────────────────────────────────────── */
.brand-select {
    background: var(--sage-50);
    border: 1px solid var(--sage-200);
    color: var(--forest-900);
    font-size: 0.8125rem;
    border-radius: 0.5rem;
    padding: 0.375rem 0.5rem;
    width: 100%;
    transition: border-color 0.15s ease;
}

.brand-select:focus {
    outline: none;
    border-color: var(--forest-400);
}

.brand-select option {
    background: white;
    color: var(--forest-900);
}

/* ── Toast notifications ──────────────────────────────────── */
.toast {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.75rem 1rem;
    border-radius: 0.625rem;
    font-size: 0.8125rem;
    background: white;
    border: 1px solid var(--sage-200);
    box-shadow: 0 8px 30px rgba(12, 26, 17, 0.1), 0 2px 8px rgba(12, 26, 17, 0.05);
    transform: translateX(calc(100% + 2rem));
    opacity: 0;
    transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1);
    position: relative;
    overflow: hidden;
    min-width: 280px;
    max-width: 420px;
}

.toast-enter {
    transform: translateX(0);
    opacity: 1;
}

.toast-exit {
    transform: translateX(calc(100% + 2rem));
    opacity: 0;
}

.toast-success { border-left: 3px solid var(--forest-500); }
.toast-error   { border-left: 3px solid #ef4444; }
.toast-warning { border-left: 3px solid #f59e0b; }
.toast-info    { border-left: 3px solid #3b82f6; }

.toast-icon { flex-shrink: 0; display: flex; align-items: center; }
.toast-text { flex: 1; color: var(--forest-900); line-height: 1.4; }

.toast-close {
    flex-shrink: 0;
    border: none;
    background: none;
    cursor: pointer;
    color: var(--forest-300);
    font-size: 1.125rem;
    padding: 0 0.125rem;
    transition: color 0.15s ease;
    line-height: 1;
}

.toast-close:hover { color: var(--forest-600); }

.toast-progress {
    position: absolute;
    bottom: 0;
    left: 0;
    height: 2px;
    width: 100%;
    animation: toast-timer 4s linear forwards;
}

.toast-progress-success { background: var(--forest-500); }
.toast-progress-error   { background: #ef4444; }
.toast-progress-warning { background: #f59e0b; }
.toast-progress-info    { background: #3b82f6; }

@keyframes toast-timer {
    from { width: 100%; }
    to { width: 0%; }
}

/* ── Selection / accent override ──────────────────────────── */
::selection {
    background: var(--forest-200);
    color: var(--forest-900);
}

/* ── Focus-visible ring ───────────────────────────────────── */
*:focus-visible {
    outline: 2px solid var(--forest-400);
    outline-offset: 2px;
}

/* ── Mobile sidebar ──────────────────────────────────────── */
.sidebar-open {
    transform: translateX(0) !important;
}

/* ══════════════════════════════════════════════════════════
   MOBILE RESPONSIVENESS
   ══════════════════════════════════════════════════════════ */

/* ── Responsive tables ───────────────────────────────────── */
/* Any card/div containing a data-table becomes scrollable on small screens */
.card:has(.data-table),
.data-table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

@media (max-width: 1023px) {
    .data-table {
        min-width: 600px;
    }
}

/* ── Small-screen table compaction ───────────────────────── */
@media (max-width: 639px) {
    .data-table thead th {
        padding: 0.5rem 0.625rem;
        font-size: 0.625rem;
    }
    .data-table tbody td {
        padding: 0.5rem 0.625rem;
        font-size: 0.75rem;
    }
    /* Hide lower-priority columns on tiny screens */
    .data-table .hide-mobile {
        display: none;
    }
}

/* Touch devices can't hover, so row actions/controls revealed only on hover
   (opacity-0 group-hover:opacity-100) are invisible & unusable. Force them
   visible wherever the primary input has no hover (phones/tablets). */
@media (hover: none) {
    .opacity-0.group-hover\:opacity-100 {
        opacity: 1 !important;
    }
}

/* ── Responsive stat cards ───────────────────────────────── */
@media (max-width: 639px) {
    .stat-card {
        padding: 0.75rem 1rem;
    }
    .stat-card .text-2xl {
        font-size: 1.25rem;
    }
}

/* ── Responsive form rows ────────────────────────────────── */
@media (max-width: 639px) {
    .form-row-responsive {
        flex-direction: column;
    }
    .form-row-responsive > * {
        width: 100%;
    }
}

/* ── Mobile page headers ─────────────────────────────────── */
@media (max-width: 639px) {
    h1.font-serif, h1.text-2xl {
        font-size: 1.25rem;
    }
    /* Stack header + action buttons vertically */
    .mb-6.flex.items-center.justify-between,
    .mb-6.flex.items-start.justify-between {
        flex-direction: column;
        align-items: stretch;
        gap: 0.75rem;
    }
    /* Full-width action buttons on mobile */
    .btn-primary {
        text-align: center;
    }
}

/* ── Mobile cards ────────────────────────────────────────── */
@media (max-width: 639px) {
    .card {
        border-radius: 0.5rem;
    }
    .card .px-6 {
        padding-left: 1rem;
        padding-right: 1rem;
    }
    .card .p-6 {
        padding: 1rem;
    }
    .card .p-5 {
        padding: 0.875rem;
    }
}

/* ── Mobile search input ─────────────────────────────────── */
@media (max-width: 639px) {
    .search-input {
        width: 100%;
    }
}

/* ── Mobile toolbar spacing ──────────────────────────────── */
@media (max-width: 639px) {
    .flex.items-center.gap-3 {
        flex-wrap: wrap;
        gap: 0.5rem;
    }
}

/* ── Mobile badge text ───────────────────────────────────── */
@media (max-width: 639px) {
    .badge {
        font-size: 0.5625rem;
        padding: 0.0625rem 0.5rem;
    }
}

/* ── Touch-friendly tap targets ──────────────────────────── */
@media (max-width: 1023px) {
    .sidebar-link {
        padding-top: 0.625rem;
        padding-bottom: 0.625rem;
    }
    .btn-primary, .btn-secondary {
        min-height: 2.5rem;
    }
    .btn-export {
        width: 2.5rem;
        height: 2.5rem;
    }
}

/* ── Mobile variant tags ─────────────────────────────────── */
@media (max-width: 639px) {
    .variant-tag {
        font-size: 0.75rem;
        padding: 0.25rem 0.5rem;
    }
}

/* ── Full-width modals on mobile ─────────────────────────── */
@media (max-width: 639px) {
    .fixed .bg-white.rounded-lg.shadow-xl {
        margin: 0.5rem;
        max-width: 100%;
    }
}

/* ══════════════════════════════════════════════════════════
   ANIMATIONS & TRANSITIONS
   ══════════════════════════════════════════════════════════ */

/* ── Smooth scroll ──────────────────────────────────────── */
html { scroll-behavior: smooth; }

/* ── Page loading bar (top) ─────────────────────────────── */
#page-loader {
    position: fixed;
    top: 0;
    left: 0;
    height: 2.5px;
    background: linear-gradient(90deg, var(--forest-400), var(--forest-600), var(--forest-400));
    background-size: 200% 100%;
    z-index: 9999;
    width: 0%;
    opacity: 0;
    transition: opacity 0.2s;
    pointer-events: none;
}

#page-loader.loading {
    opacity: 1;
    animation: loader-progress 1.8s cubic-bezier(0.4, 0, 0.2, 1) forwards,
               loader-shimmer 1.5s ease-in-out infinite;
}

#page-loader.done {
    width: 100% !important;
    opacity: 0;
    transition: width 0.15s ease-out, opacity 0.35s ease 0.15s;
}

@keyframes loader-progress {
    0%   { width: 0%; }
    20%  { width: 30%; }
    50%  { width: 60%; }
    80%  { width: 82%; }
    100% { width: 90%; }
}

@keyframes loader-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* ── Content entrance ───────────────────────────────────── */
/* `backwards`, NOT `both` (changed 2026-09-14).
 *
 * `both` kept the final keyframe after the entrance ended, and that keyframe is
 * `transform: translateY(0)` — an identity transform, but still a transform. So this
 * wrapper, which holds ALL page content (base.html), stayed the containing block for every
 * position:fixed descendant. Toasts, confirm/summary modals and the fixed Save / bulk bars
 * anchored to the bottom and middle of the whole scrolled content instead of the screen,
 * and on a long page rendered thousands of pixels below the fold.
 *
 * Measured on a replica of the real layout, 388px viewport: with `both` the toast sat
 * 3,388px down and the confirm dialog 1,687px down; with `backwards`, 324px and 163px.
 *
 * `backwards` still applies the `from` state before the first frame, so there is no flash;
 * it only stops holding the transform once the 0.35s entrance is over. Anything that
 * appears DURING those 0.35s is still anchored here until the entrance finishes.
 * The hand-rolled move-to-<body> in decisions/log.html and production/batch_list.html is now
 * belt-and-braces and harmless; product-picker.js still reparents, because .card:hover adds
 * its own translateY(-1px). */
.page-enter {
    animation: fadeSlideUp 0.35s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}

@keyframes fadeSlideUp {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* ── Card hover highlight (no lift) ─────────────────────── */
/* A card under the mouse is highlighted with its border and shadow ONLY — never a transform.
   It used to rise 1px (transform: translateY(-1px); :active put it back with translateY(0)).
   ANY transform makes the card its own stacking context, so a list opened inside it — the
   app-wide custom select, status menus, search suggestions — was painted UNDER whatever came
   next on the page wherever it hung past the card's edge, and a click there landed on that
   next thing instead. Measured 2026-09-15 on six live lists (Bulk Map product suggestions and
   Store list, Inactive stock product list, Credentials 'Type', Reporting 'Saved format',
   Batches status 'Cancelled'): a quarter to nine-tenths clickable with the lift, all of it
   without. Guarded by tests/webapp/test_cards_and_rows_never_cover_dropdowns.py. */
.card {
    transition: border-color 0.25s ease, box-shadow 0.25s ease;
}

/* The dashboard's brand tiles ride on .card too. A `.brand-card` variant that lifted
   the tile on hover was deleted 2026-09-16: nothing wore the class, and it was an
   unused copy of exactly the lift the note above forbids. Do not re-add one. */
.card:hover {
    border-color: var(--forest-200);
    box-shadow: 0 4px 16px rgba(22, 44, 29, 0.07);
}

/* ── Stat card hover ────────────────────────────────────── */
.stat-card {
    transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}

.stat-card:hover {
    /* border + shadow only — no transform, for the reason given above .card */
    border-color: var(--forest-200);
    box-shadow: 0 4px 16px rgba(22, 44, 29, 0.06);
}

/* ── .btn-primary / .btn-secondary need a flex context ────────────────────
   These two were the ONLY .btn-* rules with no `display`, so a <button>
   stayed inline-block. Invisible until a button contains an icon, then:
     · `justify-center` at the call site is INERT — there is no flex context.
     · no `gap`, so icon and label are separated only by collapsed whitespace.
     · the SVG sits on the TEXT BASELINE, riding low and adding descender
       space — the Reporting export button measured 54px against 40px for the
       text-only button directly beside it.
   .btn-chip / .btn-export / .btn-danger-solid already declare inline-flex, so
   they are NOT re-declared here; .btn-danger is deliberately a text-style
   button (colour only) and is left inline. */
.btn-primary,
.btn-secondary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    min-height: 2.5rem;
    line-height: 1.2;
}
.btn-primary > svg,
.btn-secondary > svg { flex-shrink: 0; }

/* Already inline-flex — it just never had a gap for an icon, and sits beside a
   .btn-secondary in modal footers, so match that height. */
.btn-danger-solid { gap: 0.5rem; min-height: 2.5rem; }

/* ── Button press animation ─────────────────────────────── */
.btn-primary {
    transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

.btn-primary:active {
    transform: scale(0.97);
    box-shadow: none;
}

.btn-secondary:active {
    transform: scale(0.97);
}

.btn-export:active {
    transform: scale(0.92);
}

/* ── Table row stagger entrance ─────────────────────────── */
/* `backwards`, not `both`: rows still fade and slide in, and stay hidden through their stagger
   delay, exactly as before — but let go of the transform once they land. `both` kept every row
   at translateY(0) forever, which is still a transform, so each row stayed its own stacking
   context: a list opening from one row sat under the next row, and anything position:fixed
   inside a row anchored to the row. Same mistake, and same fix, as .page-enter. */
.data-table tbody tr {
    animation: tableRowIn 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}

@keyframes tableRowIn {
    from {
        opacity: 0;
        transform: translateY(4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Stagger delays for first 20 rows */
.data-table tbody tr:nth-child(1)  { animation-delay: 0.02s; }
.data-table tbody tr:nth-child(2)  { animation-delay: 0.04s; }
.data-table tbody tr:nth-child(3)  { animation-delay: 0.06s; }
.data-table tbody tr:nth-child(4)  { animation-delay: 0.08s; }
.data-table tbody tr:nth-child(5)  { animation-delay: 0.10s; }
.data-table tbody tr:nth-child(6)  { animation-delay: 0.12s; }
.data-table tbody tr:nth-child(7)  { animation-delay: 0.14s; }
.data-table tbody tr:nth-child(8)  { animation-delay: 0.16s; }
.data-table tbody tr:nth-child(9)  { animation-delay: 0.18s; }
.data-table tbody tr:nth-child(10) { animation-delay: 0.20s; }
.data-table tbody tr:nth-child(11) { animation-delay: 0.22s; }
.data-table tbody tr:nth-child(12) { animation-delay: 0.24s; }
.data-table tbody tr:nth-child(13) { animation-delay: 0.26s; }
.data-table tbody tr:nth-child(14) { animation-delay: 0.28s; }
.data-table tbody tr:nth-child(15) { animation-delay: 0.30s; }
.data-table tbody tr:nth-child(n+16) { animation-delay: 0.32s; }

/* Once a table has been sorted its rows are already on screen — they must not enter again.
   A sort re-inserts every row, and re-inserting an element restarts its CSS animation, so
   each click replayed the entrance above: the whole table blanked and faded back in over
   ~0.3-0.45s, even on the restore click and for rows that did not move (measured 12 of 12
   rows invisible on the production Batches cost table, with this stylesheet and with the one
   from before `backwards`). table-sort.js adds .rows-settled on the first sort.
   Guarded by tests/webapp/test_sort_does_not_blank_or_misplace.py. */
.data-table.rows-settled tbody tr {
    animation: none;
}

/* ── Clickable table rows ───────────────────────────────── */
.data-table tbody tr[onclick],
.data-table tbody tr a {
    cursor: pointer;
}

.data-table tbody tr:hover {
    background: var(--sage-50);
    transition: background 0.15s ease;
}

/* ── Modal transitions ──────────────────────────────────── */
/* The ONLY .modal-backdrop rule. An identical-specificity copy sat earlier in the
   file with the same background and blur but no animation; being earlier it lost
   every declaration to this block, so deleting it changed nothing that renders.
   Keep it single — a second copy is how a backdrop silently loses its fade. */
.modal-backdrop {
    background: rgba(12, 26, 17, 0.45);
    backdrop-filter: blur(3px);
    animation: backdropIn 0.2s ease both;
}

@keyframes backdropIn {
    from { opacity: 0; backdrop-filter: blur(0px); }
    to   { opacity: 1; backdrop-filter: blur(3px); }
}

/* ── Badge entrance animation ───────────────────────────── */
.badge {
    animation: badgeIn 0.25s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes badgeIn {
    from { opacity: 0; transform: scale(0.8); }
    to   { opacity: 1; transform: scale(1); }
}

/* ── Form input focus glow ──────────────────────────────── */
.form-input {
    transition: border-color 0.2s ease, box-shadow 0.25s ease;
}

.form-input:focus {
    border-color: var(--forest-400);
    box-shadow: 0 0 0 3px rgba(70, 110, 83, 0.12);
}

/* ── Checkbox & toggle smooth ───────────────────────────── */
input[type="checkbox"] {
    transition: all 0.15s ease;
}

input[type="checkbox"]:checked {
    animation: checkPop 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes checkPop {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.15); }
    100% { transform: scale(1); }
}

/* ── Sidebar nav count fade in ──────────────────────────── */
.nav-count {
    transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

/* ── Sidebar link smooth interactions ───────────────────── */
.sidebar-link {
    transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
    border-left: 2px solid transparent;
}

.sidebar-link:hover {
    background: var(--sage-50);
    border-left-color: var(--forest-300);
    padding-left: calc(0.75rem + 2px);
}

.sidebar-link.active {
    background: var(--forest-50);
    border-left-color: var(--forest-600);
    font-weight: 500;
    color: var(--forest-900);
}

/* ── Sidebar link icons ─────────────────────────────────── */
.sidebar-link svg {
    transition: opacity 0.2s ease;
}

.sidebar-link:hover svg {
    opacity: 0.7;
}

.sidebar-link.active svg {
    opacity: 0.8;
}

/* ── Colour/size tag transitions ────────────────────────── */
.variant-tag, [class*="bg-forest-50"] {
    transition: all 0.15s ease;
}

/* ── Search input expand on focus ───────────────────────── */
.search-input {
    transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}

.search-input:focus {
    width: 20rem;
}

@media (max-width: 639px) {
    .search-input:focus {
        width: 100%;
    }
}

/* ── Link underline animation ───────────────────────────── */
a.link-animated {
    position: relative;
    text-decoration: none;
}

a.link-animated::after {
    content: '';
    position: absolute;
    bottom: -1px;
    left: 0;
    width: 0;
    height: 1px;
    background: currentColor;
    transition: width 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}

a.link-animated:hover::after {
    width: 100%;
}

/* ── Skeleton loading shimmer ───────────────────────────── */
.skeleton {
    background: linear-gradient(90deg, var(--sage-100) 25%, var(--sage-50) 50%, var(--sage-100) 75%);
    background-size: 200% 100%;
    animation: skeletonShimmer 1.5s ease-in-out infinite;
    border-radius: 0.375rem;
}

@keyframes skeletonShimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* ── Dropdown/select smooth ─────────────────────────────── */
select, .brand-select {
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

select:focus, .brand-select:focus {
    box-shadow: 0 0 0 3px rgba(70, 110, 83, 0.1);
}

/* ── Export button tooltip ──────────────────────────────── */
.btn-export {
    position: relative;
}

.btn-export::after {
    content: 'Export CSV';
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%) translateY(4px);
    padding: 0.25rem 0.625rem;
    font-size: 0.6875rem;
    font-weight: 500;
    white-space: nowrap;
    background: var(--forest-900);
    color: white;
    border-radius: 0.375rem;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}

.btn-export:hover::after {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

/* ── Batch card status dropdown ─────────────────────────── */
.status-select {
    transition: all 0.2s ease;
}

/* ── Tab switching animation ────────────────────────────── */
.tab-content {
    /* `backwards` for the same reason as .data-table rows. No page uses this class today. */
    animation: tabFade 0.25s ease backwards;
}

@keyframes tabFade {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* ══════════════════════════════════════════════════════════
   CUSTOM SELECT DROPDOWNS
   ══════════════════════════════════════════════════════════ */

/* ── Wrapper ─────────────────────────────────────────────── */
.custom-select {
    position: relative;
    width: 100%;
}

/* ── Trigger button ──────────────────────────────────────── */
.custom-select-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    border: 1px solid var(--sage-200);
    border-radius: 0.5rem;
    padding: 0.5rem 0.75rem;
    font-size: 0.8125rem;
    background: white;
    color: var(--forest-900);
    cursor: pointer;
    transition: border-color 0.2s ease, box-shadow 0.25s ease, background 0.15s ease;
    user-select: none;
    min-height: 2.375rem;
    text-align: left;
    line-height: 1.4;
}

.custom-select-trigger:hover {
    border-color: var(--forest-300);
    background: var(--sage-50);
}

.custom-select-trigger:focus {
    outline: none;
    border-color: var(--forest-400);
    box-shadow: 0 0 0 3px rgba(70, 110, 83, 0.12);
}

.custom-select.open .custom-select-trigger {
    border-color: var(--forest-400);
    box-shadow: 0 0 0 3px rgba(70, 110, 83, 0.12);
}

/* ── Trigger text ────────────────────────────────────────── */
.custom-select-value {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.custom-select-placeholder {
    color: var(--forest-500);    /* was forest-400 @ .6 (2.9:1) — now AA */
    opacity: 0.85;
}

/* ── Chevron icon ────────────────────────────────────────── */
.custom-select-arrow {
    flex-shrink: 0;
    width: 1rem;
    height: 1rem;
    margin-left: 0.5rem;
    color: var(--forest-400);
    transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1), color 0.15s ease;
}

.custom-select.open .custom-select-arrow {
    transform: rotate(180deg);
    color: var(--forest-600);
}

/* ── Dropdown panel ──────────────────────────────────────── */
.custom-select-dropdown {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    background: white;
    border: 1px solid var(--sage-200);
    border-radius: 0.5rem;
    box-shadow: 0 8px 24px rgba(12, 26, 17, 0.1), 0 2px 8px rgba(12, 26, 17, 0.04);
    z-index: 60;
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    transform: translateY(-4px);
    transition: max-height 0.3s cubic-bezier(0.22, 1, 0.36, 1),
                opacity 0.2s ease,
                transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
    pointer-events: none;
}

.custom-select.open .custom-select-dropdown {
    max-height: 15rem;
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
}

/* ── Options scrollable area ─────────────────────────────── */
.custom-select-options {
    overflow-y: auto;
    max-height: 14.5rem;
    padding: 0.25rem;
}

.custom-select-options::-webkit-scrollbar { width: 4px; }
.custom-select-options::-webkit-scrollbar-track { background: transparent; }
.custom-select-options::-webkit-scrollbar-thumb { background: var(--sage-200); border-radius: 2px; }

/* ── Type-to-filter search box (long lists only) ─────────── */
.custom-select-search {
    display: block;
    width: calc(100% - 0.5rem);
    margin: 0.25rem;
    padding: 0.45rem 0.6rem;
    font-size: 0.8125rem;
    line-height: 1.2;
    color: var(--forest-900);
    background: var(--sage-50);
    border: 1px solid var(--sage-200);
    border-radius: 0.375rem;
    outline: none;
    box-sizing: border-box;
}
.custom-select-search:focus { border-color: var(--forest-400); background: #fff; }
.custom-select-search::placeholder { color: var(--forest-900); opacity: 0.4; }

.custom-select-noresults {
    padding: 0.6rem 0.75rem;
    font-size: 0.8125rem;
    color: var(--forest-900);
    opacity: 0.5;
    text-align: center;
}

/* Make room for the search box without clipping the option list */
.custom-select.open .custom-select-dropdown.cs-has-search { max-height: 17rem; }
.custom-select-dropdown.cs-has-search .custom-select-options { max-height: 13rem; }

/* ── Individual option ───────────────────────────────────── */
.custom-select-option {
    padding: 0.5rem 0.75rem;
    font-size: 0.8125rem;
    color: var(--forest-900);
    border-radius: 0.375rem;
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease, padding-left 0.2s cubic-bezier(0.22, 1, 0.36, 1);
    display: flex;
    align-items: center;
    gap: 0.5rem;
    position: relative;
}

.custom-select-option:hover,
.custom-select-option.highlighted {
    background: var(--forest-50);
    color: var(--forest-800);
    padding-left: 1rem;
}

.custom-select-option.selected {
    background: var(--forest-50);
    color: var(--forest-800);
    font-weight: 500;
}

/* Selected check mark */
.custom-select-option.selected::after {
    content: '';
    display: block;
    width: 0.375rem;
    height: 0.625rem;
    margin-left: auto;
    border: solid var(--forest-600);
    border-width: 0 1.5px 1.5px 0;
    transform: rotate(45deg);
    flex-shrink: 0;
    animation: checkIn 0.2s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes checkIn {
    from { opacity: 0; transform: rotate(45deg) scale(0.5); }
    to   { opacity: 1; transform: rotate(45deg) scale(1); }
}

/* ── Size variants ───────────────────────────────────────── */
.custom-select-trigger.text-sm {
    font-size: 0.8125rem;
    padding: 0.375rem 0.625rem;
    min-height: 2.125rem;
}

.custom-select-trigger.text-xs {
    font-size: 0.75rem;
    padding: 0.25rem 0.5rem;
    min-height: 1.875rem;
}

.custom-select-trigger.text-xs .custom-select-arrow,
.custom-select-trigger.text-sm .custom-select-arrow {
    width: 0.875rem;
    height: 0.875rem;
}

/* ── Disabled state ──────────────────────────────────────── */
.custom-select.disabled .custom-select-trigger {
    background: var(--sage-50);
    color: #6b7280;
    cursor: not-allowed;
    border-color: var(--sage-200);
}

.custom-select.disabled .custom-select-trigger:hover {
    border-color: var(--sage-200);
    background: var(--sage-50);
}

/* ── Native select hidden (but still submits form) ───────── */
.custom-select select {
    position: absolute;
    width: 0;
    height: 0;
    opacity: 0;
    pointer-events: none;
    overflow: hidden;
}

/* ── Mobile ──────────────────────────────────────────────── */
@media (max-width: 639px) {
    .custom-select-dropdown {
        max-height: 12rem;
    }
    .custom-select.open .custom-select-dropdown {
        max-height: 12rem;
    }
}

/* ══════════════════════════════════════════════════════════
   STOCK ENTRY MATRIX — MOBILE (375 px phone optimised)
   ══════════════════════════════════════════════════════════ */

/*
 * Warehouse staff count stock on their phones. The colour×size
 * matrix grid must be usable at 375 px.  Key constraints:
 *   – font-size ≥ 16 px on <input> prevents iOS auto-zoom
 *   – touch targets ≥ 40 px tall
 *   – colour column sticks left, but truncates to save space
 *   – horizontal scroll still works for many-size products
 */

@media (max-width: 639px) {
    /* ── Matrix quantity inputs ─────────────────────────────── */
    .qty-input {
        font-size: 1rem !important;       /* 16 px — prevents iOS zoom */
        width: 2.75rem !important;         /* 44 px — narrower than desktop 64 px */
        min-height: 2.5rem;                /* 40 px touch target */
        padding: 0.375rem 0 !important;    /* vertical only — no side padding */
        border-radius: 0.375rem;
    }

    /* ── Tighter table cell padding ────────────────────────── */
    .product-form table td {
        padding: 0.125rem 0.125rem;
    }
    .product-form table th {
        padding: 0.375rem 0.125rem;
        font-size: 0.5625rem;             /* 9 px — compact headers */
        min-width: unset !important;       /* override inline min-w-[70px] */
        letter-spacing: 0;
    }

    /* ── Sticky colour column — truncated ──────────────────── */
    .product-form td.sticky,
    .product-form th.sticky {
        max-width: 4.5rem;                 /* 72 px */
        padding-left: 0.375rem !important;
        padding-right: 0.25rem !important;
        font-size: 0.6875rem;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* ── Row / column / grand total cells ──────────────────── */
    .product-form .row-total,
    .product-form .col-total,
    .product-form .grand-total {
        font-size: 0.6875rem;
        padding: 0.25rem 0.125rem !important;
    }

    /* ── Product card header compact ───────────────────────── */
    .product-form > .px-6 {
        padding: 0.625rem 0.75rem !important;
    }
    .product-form .font-serif {
        font-size: 0.875rem;
    }

    /* ── Product nav chips — horizontal scroll ─────────────── */
    #product-nav > .flex {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        padding-bottom: 0.375rem;
        scrollbar-width: none;             /* Firefox */
    }
    #product-nav > .flex::-webkit-scrollbar {
        display: none;                     /* Chrome/Safari */
    }
    .product-chip {
        flex-shrink: 0;
        font-size: 0.75rem;
        padding: 0.375rem 0.625rem;
    }

    /* ── Save bar compact ──────────────────────────────────── */
    #save-bar {
        padding-left: 1rem !important;
        padding-right: 1rem !important;
        padding-top: 0.5rem !important;
        padding-bottom: 0.5rem !important;
    }
    #save-bar .btn-primary {
        padding: 0.375rem 0.875rem;
        font-size: 0.75rem;
    }

    /* ── Session controls card — stack on mobile ───────────── */
    .session-controls {
        flex-direction: column;
        gap: 0.75rem !important;
    }
    .session-controls > div {
        width: 100%;
    }
    .session-controls .form-input,
    .session-controls .custom-select-trigger {
        width: 100%;
    }

    /* ── Add-product section — stack ───────────────────────── */
    .add-product-row {
        flex-direction: column;
        gap: 0.75rem !important;
    }
    .add-product-row > div {
        width: 100%;
        min-width: unset !important;
    }
    .add-product-row .custom-select {
        width: 100%;
    }

    /* ── Batch import row — stack ──────────────────────────── */
    .batch-import-row {
        flex-direction: column;
        gap: 0.75rem !important;
    }
    .batch-import-row > div,
    .batch-import-row > button {
        width: 100%;
        min-width: unset !important;
    }

    /* ── Summary modal — full-width on phone ───────────────── */
    #summary-modal .bg-white {
        margin: 0.25rem;
        max-height: 90vh;
        border-radius: 0.75rem;
    }
    #summary-modal .px-6 {
        padding-left: 1rem;
        padding-right: 1rem;
    }

    /* ── Page header compact ───────────────────────────────── */
    .stock-entry-header {
        flex-wrap: wrap;
        gap: 0.375rem;
    }
    .stock-entry-header h1 {
        font-size: 1.125rem;
    }
    .stock-entry-header .badge {
        font-size: 0.5625rem;
    }
}

/* Read-only matrix quantity display — also needs touch-friendly sizing */
@media (max-width: 639px) {
    .qty-display {
        font-size: 0.875rem;
        min-width: 2.75rem;
        display: inline-block;
        text-align: center;
    }
}

/* ── Reduce motion for accessibility ────────────────────── */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
    html { scroll-behavior: auto; }
}

/* ── Command palette (Ctrl+K) ───────────────────────────────────────────────
   DOM built by static/js/command-palette.js; data from #cmdk-data in base.html. */
.cmdk-overlay {
    position: fixed;
    inset: 0;
    z-index: 80;
    background: rgba(22, 44, 29, 0.32);
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 12vh 1rem 0;
}
.cmdk-overlay.hidden { display: none; }
.cmdk-panel {
    width: 100%;
    max-width: 34rem;
    background: #fff;
    border: 1px solid var(--sage-200);
    border-radius: 0.75rem;
    box-shadow: 0 16px 48px rgba(22, 44, 29, 0.18);
    overflow: hidden;
    animation: cmdk-pop 0.12s ease-out;
}
@keyframes cmdk-pop {
    from { opacity: 0; transform: translateY(-6px) scale(0.99); }
    to   { opacity: 1; transform: none; }
}
.cmdk-inputrow {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--sage-100);
}
.cmdk-icon { height: 1rem; width: 1rem; color: var(--forest-400); flex-shrink: 0; }
.cmdk-input {
    flex: 1;
    border: none;
    outline: none;
    background: transparent;
    font-size: 0.875rem;
    color: var(--forest-900, #162c1d);
    font-family: inherit;
}
.cmdk-kbd {
    font-size: 0.625rem;
    color: var(--forest-400);
    border: 1px solid var(--sage-200);
    border-radius: 0.25rem;
    padding: 0.0625rem 0.375rem;
}
.cmdk-list { max-height: 19rem; overflow-y: auto; padding: 0.375rem 0 0.5rem; }
.cmdk-group {
    font-size: 0.625rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--forest-400);
    font-weight: 600;
    padding: 0.625rem 1rem 0.25rem;
}
.cmdk-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin: 0 0.375rem;
    padding: 0.4375rem 0.625rem;
    border-radius: 0.375rem;
    font-size: 0.8125rem;
    color: #1a2e20;
    cursor: pointer;
}
.cmdk-item.cmdk-sel { background: var(--forest-50); }
.cmdk-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmdk-hint { font-size: 0.6875rem; color: var(--forest-400); flex-shrink: 0; }
.cmdk-empty { padding: 1.25rem 1rem; font-size: 0.8125rem; color: var(--forest-400); text-align: center; }
.cmdk-foot {
    display: flex;
    gap: 1rem;
    padding: 0.5rem 1rem;
    border-top: 1px solid var(--sage-100);
    font-size: 0.6875rem;
    color: var(--forest-400);
}
.cmdk-foot b {
    font-weight: 500;
    border: 1px solid var(--sage-200);
    border-radius: 0.25rem;
    padding: 0 0.25rem;
    font-size: 0.625rem;
}
.cmdk-foot-right { margin-left: auto; }

/* ── Grid entry (stock supplied / counts / moves / gifting) ────────────────
   Behavior lives in static/js/grid-entry.js. */
/* A counted (non-zero) cell — visibly distinct from untouched ones, so a
   half-done session reads at a glance. */
.qty-input.qty-filled {
    background: var(--forest-50);
    border-color: var(--forest-300);
    font-weight: 600;
    color: var(--forest-800);
}
.key-hint {
    font-weight: 500;
    border: 1px solid var(--sage-200);
    border-radius: 0.25rem;
    padding: 0 0.3125rem;
    font-size: 0.625rem;
    color: var(--forest-900);
    opacity: 0.6;
}

/* Range highlight (Shift+arrows / Shift+click) — the rectangle Ctrl+R/Ctrl+D
   act on. Declared after .qty-filled so selection wins visually. */
.qty-input.qty-range {
    background: var(--forest-100);
    border-color: var(--forest-500);
    box-shadow: inset 0 0 0 1px var(--forest-500);
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAYOUT PRIMITIVES
   ───────────────────────────────────────────────────────────────────────────
   Added 2026-07-20 after the UI audit found the app had a rich COMPONENT
   vocabulary but no LAYOUT vocabulary — so every page hand-rolled its own
   header (20 distinct wrapper chains across 47 pages), its own tab strip (5
   implementations, 3 of them in inline <style>), and its own field labels
   (there was no .form-label at all, which is why 86% of inputs ended up with
   no associated <label>).

   These are the missing primitives. Prefer them over ad-hoc utility chains.
   See docs/ui-professionalism-audit-2026-07-20.md.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Page header ──────────────────────────────────────────────────────────
   Use templates/components/_page_header.html rather than these directly.
   Stacks on mobile: the old hand-rolled headers used `flex items-center
   justify-between` with no wrap, so a title + 2 buttons squashed into one row
   on a 375px phone. */
.page-header {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-bottom: 1.75rem;
}
@media (min-width: 640px) {
    .page-header {
        flex-direction: row;
        align-items: flex-start;
        justify-content: space-between;
        gap: 1rem;
    }
}
.page-header-main { display: flex; align-items: flex-start; gap: 0.875rem; min-width: 0; }
/* 44px icon tile — the #364 pattern, hidden on phones where it costs more
   width than it earns. */
.page-header-icon {
    display: none;
    flex-shrink: 0;
    place-items: center;
    height: 2.75rem;
    width: 2.75rem;
    border-radius: 0.75rem;
    background: var(--forest-50);
    color: var(--forest-600);
    box-shadow: inset 0 0 0 1px var(--forest-100);
}
@media (min-width: 640px) { .page-header-icon { display: grid; } }
.page-header-eyebrow {
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--forest-600);
    margin-bottom: 0.375rem;
}
.page-header-titlerow { display: flex; align-items: center; gap: 0.625rem; flex-wrap: wrap; }
.page-header-title { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.02em; color: var(--forest-900); }
/* AA-safe: the hand-rolled headers used text-gray-400 (#9ca3af, 2.8:1). */
.page-header-sub { font-size: 0.875rem; color: var(--forest-600); margin-top: 0.375rem; max-width: 42rem; line-height: 1.55; }
.page-header-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; flex-shrink: 0; }
.page-header-back {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.75rem;
    color: var(--forest-600);
    margin-bottom: 0.5rem;
    transition: color 0.15s ease;
}
.page-header-back:hover { color: var(--forest-800); }

/* ── Segmented control ────────────────────────────────────────────────────
   Replaces 4 ad-hoc implementations that used three DIFFERENT active greens.
   The active state is driven by [aria-selected="true"] / .is-active in CSS,
   NOT by JS class-toggling — reporting/home.html styled its tabs only from JS,
   so both tabs rendered identical and unstyled until the script ran (a visible
   flash on every page load). */
.segmented {
    display: inline-flex;
    padding: 0.1875rem;
    border-radius: 0.5rem;
    border: 1px solid var(--sage-200);
    background: var(--sage-50);
    gap: 0.1875rem;
}
.segmented-item {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    min-height: 2.25rem;
    padding: 0.375rem 1rem;
    border-radius: 0.375rem;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--forest-600);
    background: transparent;
    border: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.segmented-item:hover { color: var(--forest-800); }
.segmented-item.is-active,
.segmented-item[aria-selected="true"] {
    background: white;
    color: var(--forest-900);
    box-shadow: 0 1px 2px rgba(22, 44, 29, 0.08);
}
.segmented-item:disabled { opacity: 0.5; cursor: not-allowed; }

/* ── Tab strip (underline style) ──────────────────────────────────────────
   Promoted from 3 byte-identical inline <style> copies in production/, all of
   which referenced --gray-400/--gray-700 (tokens that never existed) and so
   fell through to hardcoded, contrast-failing hex. */
.tab-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    min-height: 2.75rem;
    padding: 0.625rem 0.125rem;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--forest-600);
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.15s ease, border-color 0.15s ease;
}
.tab-btn:hover { color: var(--forest-800); }
.tab-btn.active,
.tab-btn[aria-selected="true"] { color: var(--forest-900); border-bottom-color: var(--forest-600); }
/* Horizontally scrollable on narrow screens — _warehouse_nav.html's 4 tabs
   measured ~440px and the last one was clipped and unreachable at 375px. */
.tab-scroll { display: flex; gap: 1.25rem; overflow-x: auto; white-space: nowrap; -ms-overflow-style: none; scrollbar-width: none; }
.tab-scroll::-webkit-scrollbar { display: none; }

/* ── Form primitives ──────────────────────────────────────────────────────
   There was no label class at all before this. */
.form-label {
    display: block;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--forest-600);
    margin-bottom: 0.375rem;
}
.form-label .form-label-note { text-transform: none; letter-spacing: 0; font-weight: 400; color: var(--forest-500); }
/* Replaces the text-[0.7rem] text-gray-400 helper-text pattern (2.8:1). */
.field-help { font-size: 0.75rem; color: var(--forest-600); margin-top: 0.375rem; line-height: 1.5; }
.field-group + .field-group { margin-top: 1.25rem; }
/* Bordered scroll box for checkbox lists (store pickers, column pickers). */
.checklist {
    border: 1px solid var(--sage-200);
    border-radius: 0.5rem;
    padding: 0.5rem;
    max-height: 9rem;
    overflow-y: auto;
    background: white;
}
/* min-height is the TOUCH TARGET, not decoration: the checkbox glyph is 16px, so
   the clickable <label> row is what has to clear the 36px floor. */
.checklist-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 2.25rem;
    padding: 0.375rem 0.375rem;
    font-size: 0.8125rem;
    color: var(--forest-800);
    cursor: pointer;
    border-radius: 0.25rem;
}
/* Same rule for radio rows (store level / view / mode). */
.radio-item {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    min-height: 2.25rem;
    padding: 0.25rem 0.25rem;
    font-size: 0.875rem;
    color: var(--forest-800);
    cursor: pointer;
}
.radio-item input[type="radio"] { accent-color: var(--forest-600); }
.checklist-item:hover { background: var(--sage-50); }
/* 16px minimum — several pickers shipped 12-14px boxes. */
.checklist-item input[type="checkbox"], .check-box { height: 1rem; width: 1rem; accent-color: var(--forest-600); flex-shrink: 0; }

/* ── Card modifiers ───────────────────────────────────────────────────────── */
.card-muted { background: var(--sage-50); }
.card-section-title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--forest-600);
    margin-bottom: 0.875rem;
}
/* Small numbered step marker, so a multi-part builder reads as a sequence
   rather than as N equal-weight control groups. */
.card-step {
    display: inline-grid;
    place-items: center;
    height: 1.25rem;
    width: 1.25rem;
    border-radius: 9999px;
    background: var(--forest-50);
    color: var(--forest-700);
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0;
    box-shadow: inset 0 0 0 1px var(--forest-100);
}

/* ── Disabled buttons ─────────────────────────────────────────────────────
   custom.css had NO :disabled rule, yet 5+ call sites set btn.disabled during
   async work — the button stayed full-saturation with a live hover state while
   being unclickable. */
.btn-primary:disabled, .btn-secondary:disabled, .btn-danger:disabled, .btn-chip:disabled {
    opacity: 0.55;
    cursor: not-allowed;
    box-shadow: none;
}
.btn-primary:disabled:hover { background: var(--forest-800); }
.btn-primary:disabled:active, .btn-secondary:disabled:active { transform: none; }

/* Touch-target floor for the chip family — .btn-chip computed to 28.4px and
   .btn-chip-sm to 24.4px, and those are the ONLY controls that accept or
   reject a recommendation in decisions/. */
/* Both variants clear 36px. -sm stays visually smaller (tighter padding, 12px
   text) but is no longer a smaller TARGET — it was 24.4px, and in decisions/ it
   is the control that reverses an accepted recommendation. Row heights in dense
   tables grow slightly; that is the intended trade. */
.btn-chip { min-height: 2.25rem; }
.btn-chip-sm { min-height: 2.25rem; }

/* ── Draggable column picker (reporting builder) ──────────────────────────
   Moved out of an inline <style> in reporting/home.html, which hardcoded
   #f0f5f0 / #cbd5c0 / #6b7280 off-token. The drag handle was #cbd5c0 on white
   — about 1.5:1, i.e. invisible until hover, on the page's only reorder
   affordance. */
.col-row { cursor: grab; display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.5rem; font-size: 0.8125rem; user-select: none; }
.col-row:hover { background: var(--sage-50); }
.col-row.dragging { opacity: 0.4; cursor: grabbing; background: var(--forest-50); }
.col-row.drop-target { border-top: 2px solid var(--forest-500); }
.col-row .drag-handle { color: var(--sage-300); cursor: grab; font-size: 0.95rem; line-height: 1; user-select: none; transition: color 0.15s ease; }
.col-row:hover .drag-handle { color: var(--forest-600); }
.col-row .col-measure { color: var(--gold-500); }

/* ── Product picker (quick-add rows) ──────────────────────────────────────
   Replaces the native <input list="…"> dropdown, which is unstyled browser
   chrome and hides the list until poked (owner, 2026-08-16). On-token, and
   sized so the whole catalogue is scannable without covering the row beneath
   the one being typed into. */
.pp-wrap { position: relative; }
.pp-panel {
    display: none;
    position: absolute;
    z-index: 30;
    top: calc(100% + 0.25rem);
    left: 0;
    /* At least readable, never wider than the screen: on a phone the product
       input is only a few rem wide and the panel inherited that, wrapping every
       product name down one letter per line. */
    width: max(100%, 12rem);
    max-width: min(22rem, 90vw);
    background: #fff;
    border: 1px solid var(--sage-200);
    border-radius: 0.375rem;
    box-shadow: 0 8px 20px -6px rgba(22, 44, 29, 0.18);
    overflow: hidden;
}
.pp-wrap.pp-open .pp-panel { display: block; }
.pp-list { max-height: 15rem; overflow-y: auto; padding: 0.25rem; }
.pp-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: 0.375rem 0.5rem;
    font-size: 0.8125rem;
    color: var(--forest-800);
    background: transparent;
    border: 0;
    border-radius: 0.25rem;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.pp-item:hover, .pp-item.is-active { background: var(--forest-50); color: var(--forest-900); }
.pp-empty { padding: 0.5rem; font-size: 0.75rem; color: var(--forest-600); }
.pp-foot {
    padding: 0.3125rem 0.625rem;
    border-top: 1px solid var(--sage-100);
    font-size: 0.6875rem;
    color: var(--forest-600);
    background: var(--sage-50);
}

/* ── Product History layout ───────────────────────────────────────────────
   The records have the page to themselves. A products rail used to sit beside
   them (owner, 2026-08-16: "all products visible and filterable"); the owner
   had it removed on 2026-08-18, so the two-column grid, the rail and the rail's
   own link styling went with it. The pp-* rules below did NOT — those belong to
   the quick-add product picker, which is a different control.

   min-width: 0 stays: the wide records table has to shrink and scroll inside
   its own wrapper rather than push the page sideways.

   Written here rather than as Tailwind utilities on the element: the
   production stylesheet is TREE-SHAKEN from a scan of the templates, and a
   layout that silently loses its rules in prod while looking right in dev is a
   failure this codebase has already had once (the decisions bulk-bar). */
.history-records { min-width: 0; }

/* The picker panel overflows the entry row and has to paint over the records
   below it. z-index on the panel is NOT enough inside a <table>: a later cell
   still wins, and so does raising the cell the panel lives in. Verified in the
   browser — lifting the ROW is what works. */
.quick-add-row { position: relative; z-index: 20; }

/* Keep the columns that carry NAMES readable.
   The entry row's date fields are native controls with a fixed intrinsic width
   that ignores whatever the column wants. Measured on Discounts at 1440px: the
   two date pickers took 296px of a 657px row, leaving the Product field 63px and
   Store 46px — the placeholders rendered as "Produc!" and "Ston", and the product
   name wrapped onto two lines. Capping the dates at what they actually need hands
   that space back to the fields being typed into. */
.history-records .quick-add-row input[type="date"] { width: 8rem; min-width: 0; }
.history-records .data-table th:first-child,
.history-records .data-table td:first-child { min-width: 10rem; }
/* A floor so the row never crushes: below this the wrapper scrolls, which is the
   honest outcome on a narrow screen. Sized for the widest of the three (Discounts:
   product + store + discount + two dates + actions). */
.history-records .data-table { min-width: 40rem; }
/* Slightly tighter gutters than a read-only table. These carry an inline entry
   row, so every column holds a CONTROL rather than a word, and the standard 16px
   gutters spent 192px of a 750px table on whitespace — enough that "Store" could
   not render its own placeholder. 12px keeps the same rhythm and buys the row back. */
.history-records .data-table th,
.history-records .data-table td { padding-left: 0.75rem; padding-right: 0.75rem; }

/* The card head on the Product History pages: a long title next to an action.
   On a phone both were wrapping — "COGS history — all products" over two lines
   and "+ Add COGS" broken across two as well, which is the scruffiest thing on
   the page. The title gives way, the action never breaks. (COGS has had no card
   action since 2026-09-15 — its entry row does everything that form did — so
   this now serves "+ Add Price" and "+ Add Discount".) */
.history-card-head { gap: 0.75rem; }
.history-card-head h2 { min-width: 0; }
.history-card-head .btn-primary { white-space: nowrap; flex-shrink: 0; }

/* The COGS entry row's "Default" switch (owner, 2026-09-15). It shares the Start
   date cell with the date it stands in for. The switch is a real checkbox inside a
   chip-shaped label — the .btn-chip family's outline variant, so it reads like every
   other chip — and the page script adds .is-default to the cell while it is on,
   which hides the (then disabled) date and shows the chip as pressed. */
.qa-date-cell { display: flex; align-items: center; gap: 0.375rem; }
.qa-date-cell.is-default input[type="date"] { display: none; }
/* ...and the cell reads "default" — the badge the saved row will carry — in the
   date's place, at the date's own width (the 8rem cap on .quick-add-row date
   inputs above). Without the width, hiding the date shrank the column: measured
   at 1280px, the chip jumped 134px left out from under the pointer that had just
   clicked it, and the cost field moved 120px. */
.qa-no-date { display: none; width: 8rem; }
.qa-date-cell.is-default .qa-no-date { display: inline-block; }
.qa-default-switch input { margin: 0; accent-color: var(--forest-600); }
.qa-default-switch:focus-within { outline: 2px solid var(--forest-400); outline-offset: 1px; }
.qa-date-cell.is-default .qa-default-switch {
    background: var(--forest-50);
    border-color: var(--forest-400);
    color: var(--forest-900);
}
/* Under the cost field while the switch is on: the product's current default.
   It appears while the operator is typing in Product, and it makes the row
   taller — so it must move nothing already there. The row's cells align on
   their first BASELINE (the hint is a second line, below it, so the row grows
   downward only), and the hint adds no width of its own (0, then 100% of its
   cell) rather than widening the cost column. Measured 2026-09-15 at 1280px and
   349px: middle alignment re-centred every cell as the row grew — Product moved
   4-11px down mid-typing, the chip 4-11px — and the text widened the cost column
   26px. Now nothing moves; with no hint, baseline sits exactly where middle did. */
.quick-add-row[data-qa-count="cogs"] > td { vertical-align: baseline; }
.qa-default-now { width: 0; min-width: 100%; margin: 0.25rem 0 0; font-size: 0.6875rem; line-height: 1.3; color: var(--forest-700); text-align: right; }
/* On a saved default row, beside its badge: "replaced — was 320". */
.qa-default-note { margin-left: 0.375rem; font-size: 0.6875rem; color: var(--forest-600); }

/* ── Production batch — Items table ────────────────────────────────────────
   production/batch_detail.html, the Items tab.

   ALIGNMENT IS PER COLUMN, NOT PER CELL. The table used to set `text-right` on
   every header and then let each <td> choose for itself; the cells drifted out of
   agreement with the headers above them (a right-aligned "Received vs target"
   header over a left-reading "awaiting receipt" cell, etc.) and the whole table
   read as crooked. Now one class per column is carried by BOTH the <th> and every
   <td> under it, so the two cannot disagree — text left, numbers right.
   These beat `.data-table thead th`/`.data-table tbody td` on specificity
   (two classes + an element vs one class + two elements). */
.items-table th.col-text,
.items-table td.col-text { text-align: left; }

.items-table th.col-num,
.items-table td.col-num,
.items-table th.col-act,
.items-table td.col-act { text-align: right; }

/* ── The Target, read-only (costing methods, 2026-09-16) ───────────────────
   production/batch_detail.html. The Target is worked out on the Fabric tab (Total KGs
   ÷ fabric per piece, rounded down) and shown, not typed, on the Items tab — so the
   cell holds text, and its header needs no extra pad to sit over an input's digits.
   A line with no Target yet is WAITING: nothing is folded away, the waiting line is
   the one the reader most needs to see. Its note is a button that opens the empty box
   on the Fabric tab. These rules live here, not in Tailwind, because the page script
   rebuilds the cell after a save (tailwind.min.css is tree-shaken from the templates). */
.item-target-cell,
.fabric-target-cell { white-space: nowrap; }

.item-target-value,
.fabric-target {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--forest-900, #1f3526);
}

.target-working {
    display: block;
    font-size: 0.6875rem;
    color: var(--forest-500, #4e7a5d);
}
.target-working:empty { display: none; }

.waiting-link {
    font-size: 0.75rem;
    font-weight: 500;
    color: #b45309;
    background: none;
    border: 0;
    padding: 0;
    border-bottom: 1px dashed #f59e0b;
    cursor: pointer;
    white-space: nowrap;
}
.waiting-link:hover { color: #92400e; border-bottom-color: #b45309; }
.waiting-link:focus-visible { outline: 2px solid var(--forest-600); outline-offset: 2px; border-radius: 2px; }

.fabric-waiting {
    font-family: inherit;
    font-size: 0.75rem;
    font-weight: 500;
    color: #b45309;
}

/* "≈180" while typing: what the save will make, beside the saved Target. */
.fabric-target-preview {
    margin-left: 0.25rem;
    font-size: 0.75rem;
    color: var(--forest-500, #4e7a5d);
}
.fabric-target-preview:empty { display: none; }

/* A check the owner should read: a stored Target that disagrees with its own fabric
   figures, fabric per piece outside 0.05–3 kg, less fabric than one piece. */
.target-flag {
    display: block;
    max-width: 16rem;
    margin-left: auto;
    font-size: 0.6875rem;
    line-height: 1.3;
    color: #b45309;
    white-space: normal;
    text-align: right;
}

/* ── Production batch — "Add Product to Batch" colour tick boxes ───────────
   production/batch_detail.html, under the Items table. One tick box per colour of the
   chosen product (costing methods, 2026-09-16): there is no quantity on this form any
   more, a ticked colour is added as a line waiting for its Target. The rows are built
   by the page script, so their rules live here where they cannot be tree-shaken. */
.colour-ticks {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.colour-tick-row,
.colour-tick-all {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 2.25rem;
    padding: 0.25rem 0.75rem;
    border: 1px solid var(--sage-200, #cdd8d0);
    border-radius: 0.375rem;
    background: #fff;
    font-size: 0.875rem;
    color: var(--forest-800, #26422f);
    cursor: pointer;
}
.colour-tick-all { border-style: dashed; font-size: 0.75rem; min-height: 1.75rem; padding: 0.125rem 0.5rem; }
.colour-tick-all.hidden { display: none; }

.colour-tick-row:has(.colour-tick:checked) {
    border-color: var(--forest-600, #466e53);
    background: var(--sage-50, #f6f7f6);
}

.colour-tick {
    width: 1rem;
    height: 1rem;
    accent-color: var(--forest-600, #466e53);
}

.colour-tick-row.is-new-colour .colour-name::after {
    content: " new";
    display: inline-block;
    margin-left: 0.375rem;
    font-size: 0.625rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--forest-500, #4e7a5d);
}

/* ── Batches: a product opening onto its production runs ──────────────────
   Written here rather than as Tailwind utilities on the elements: the production
   stylesheet is TREE-SHAKEN from a scan of the templates, and a layout that looks
   right in dev and loses its rules in prod is a failure this codebase has had. */
.batch-product-row { cursor: pointer; }
.batch-product-row:hover { background: var(--forest-50); }
.batch-caret {
    width: 0.75rem; height: 0.75rem; display: inline-block; vertical-align: -1px;
    margin-right: 0.35rem; color: var(--forest-500);
    transition: transform 120ms ease;
}
.batch-product-row[aria-expanded="true"] .batch-caret { transform: rotate(90deg); }
.batch-detail-row > td { background: var(--sage-50); }
.batch-inner { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.batch-inner th {
    text-align: left; padding: 0.5rem 1.5rem; font-size: 0.6875rem;
    text-transform: uppercase; letter-spacing: 0.04em; color: var(--forest-600);
}
.batch-inner td { padding: 0.5rem 1.5rem; border-top: 1px solid var(--sage-100); }
.batch-inner .text-right { text-align: right; }

/* ── Printable garment labels ─────────────────────────────────────────────
   REBUILT 2026-08-19 ("the UI for labels page is very poor"). The old sheet used
   grid auto-fill with max-content tracks; against a 101mm-wide barcode that
   resolved to ONE column, so the page read as a loose vertical stack of images.
   Flex-wrap on natural-width cards tiles predictably, because the card is as wide
   as the code inside it and nothing has to guess a track size. */
.label-group { margin-bottom: 2rem; }
.label-group-title {
    font-family: var(--font-serif, Georgia, serif);
    font-size: 1rem; color: var(--forest-900);
    margin: 0 0 0.75rem; display: flex; align-items: baseline; gap: 0.5rem;
}
.label-group-title span { font-family: inherit; font-size: 0.75rem; color: var(--forest-500); }

.label-sheet { display: flex; flex-wrap: wrap; gap: 5mm; align-items: flex-start; }

.label-card {
    border: 1px solid var(--sage-200); border-radius: 0.25rem;
    padding: 3mm 3mm 2mm; background: #fff;
    break-inside: avoid; page-break-inside: avoid;
    margin: 0;                      /* <figure> carries a default margin */
    display: inline-flex; flex-direction: column; gap: 1.5mm;
}
.label-head { display: flex; flex-direction: column; line-height: 1.25; }
.label-product { font-size: 3.2mm; font-weight: 600; color: #000; }
.label-variant { font-size: 2.8mm; color: #444; }
.label-bars { line-height: 0; }     /* kill the inline-SVG descender gap */
.label-bars svg { display: block; }
.label-foot {
    font-size: 0.6875rem; color: var(--forest-500);
    border-top: 1px solid var(--sage-100); padding-top: 1.5mm;
}

@media print {
    /* Everything that is not a label is noise on a sheet of garment tags. */
    .no-print, .page-header, nav, header, footer, aside { display: none !important; }
    .label-card { border-color: #000; }
    .label-sheet { gap: 3mm; }
    .label-group { margin-bottom: 0; }

    /* Every label, not the first page's worth. The signed-in frame (base.html) is one
       screen tall and scrolls inside <main>; on paper that frame is ONE page and the
       rest is clipped — measured 2026-09-16: 60 labels printed as 8. Released here for
       the label sheet only (:has(.label-sheet) matches no other page), so no other
       page's print changes. The phone-size top bar is a plain div that `header, nav`
       does not catch, and paper is narrower than the large-screen breakpoint, so it
       printed the brand name above the labels. */
    html:has(.label-sheet), html:has(.label-sheet) body { height: auto !important; }
    html:has(.label-sheet) .app-frame {
        display: block !important; height: auto !important; overflow: visible !important;
    }
    html:has(.label-sheet) .app-main { overflow: visible !important; width: auto !important; }
    html:has(.label-sheet) .app-topbar { display: none !important; }
}
