/* ===========================================================================
   iFinity Hub - Brand Theme
   ---------------------------------------------------------------------------
   The brand colours are CHOSEN BY THE OPERATOR and injected as CSS variables by
   includes/header.php (computed in includes/theme.php). Nothing in this file
   may hard-code a brand colour: a stylesheet that assumes orange looks fine
   until someone picks a light yellow, and then every link and heading on a
   white card becomes unreadable.
   The rule is:
     background is the brand   ->  background: var(--brand-primary)
                                   color:      var(--on-primary)
     text is the brand, light surface -> color: var(--brand-primary-readable)
     text is the brand, dark surface  -> color: var(--brand-primary-on-dark)
   =========================================================================== */
:root {
    /* Fallbacks only. header.php overrides all of these on every page; these
       values keep the file sane if it is ever opened standalone. */
    --brand-primary: #FFA800;
    --on-primary: #111111;
    --brand-primary-readable: #996500;
    --brand-primary-on-dark: #FFA800;
    --brand-secondary: #000000;
    --on-secondary: #ffffff;
    --brand-secondary-readable: #000000;
    --brand-secondary-on-dark: #808080;

    --surface-dark: #0f172a;
    --surface-dark-raised: #1e293b;
    --on-dark: #ffffff;
    --on-dark-secondary: #cbd5e1;
    --on-dark-muted: #94a3b8;

    --danger-on-dark: #ff6b6b;
    --success-on-dark: #4ade80;
    --warning-on-dark: #fbbf24;
    --info-on-dark: #60a5fa;

    --primary-color: var(--brand-primary);
    --primary-hover: #e59700;
    --secondary-color: var(--brand-secondary);
    --bg-body: #f1f5f9;
    --text-main: #0f172a;
    --text-muted: #64748b;
    --card-bg: #ffffff;
    --sidebar-bg: var(--brand-secondary);
    --heading-color: var(--brand-primary-readable);

    /* Body text floor: 15px at >= 4.5:1 on every panel. */
    --text-body-size: 0.9375rem;
    /* #64748b is 4.76:1 on #ffffff - the lightest muted grey that still passes. */
    --text-muted-accessible: #64748b;
}

body {
    font-size: var(--text-body-size);
}

body {
    background: var(--bg-body);
    color: var(--text-main);
    font-family: 'Inter', system-ui, -apple-system, sans-serif;
    min-height: 100vh;
}

/* Glass Card / Clean Card Container */
.glass-card {
    background: #ffffff !important;
    border: 1px solid #e2e8f0 !important;
    border-radius: 0.75rem;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.04);
    transition: all 0.25s ease;
    color: #0f172a !important;
}

.glass-card:hover {
    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
}

/* Headings */
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
    color: #0f172a;
    font-weight: 700;
}

.text-brand {
    color: var(--brand-primary-readable) !important;
}

/* The same brand colour, for text sitting on one of the dark panels. */
.text-brand-on-dark {
    color: var(--brand-primary-on-dark) !important;
}

/* Form Controls & Labels */
.form-label, label {
    color: #0f172a !important;
    font-weight: 600;
    font-size: 0.78rem;
    letter-spacing: 0.01em;
}

.form-control, .form-select {
    background: #ffffff !important;
    color: #0f172a !important;
    border: 1px solid #cbd5e1 !important;
}

.form-control:focus, .form-select:focus {
    border-color: var(--brand-primary) !important;
    box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--brand-primary) 35%, transparent) !important;
}

/* Keyboard focus must be visible on BOTH surfaces. Bootstrap's default ring is
   a translucent brand colour, which disappears against a dark sidebar when the
   brand is dark. Two rings - one brand, one white - are visible on either. */
:focus-visible {
    outline: 2px solid var(--brand-primary-readable);
    outline-offset: 2px;
}

.sidebar :focus-visible,
.dark-metric-box :focus-visible,
.kanban-surface :focus-visible {
    outline-color: var(--brand-primary-on-dark);
}

/* Sidebar */
.sidebar {
    background: #000000 !important;
    min-height: 100vh;
    box-shadow: 4px 0 15px rgba(0, 0, 0, 0.15);
    scrollbar-width: none !important; /* Firefox */
    -ms-overflow-style: none !important; /* IE / Edge */
}

.sidebar::-webkit-scrollbar {
    display: none !important;
    width: 0 !important;
    height: 0 !important;
    background: transparent !important;
}

/* The menu stays put while the page scrolls.
   --------------------------------------------------------------------------
   This carried Bootstrap's .position-sticky, which sets `position: sticky` and
   nothing else. A sticky element with no `top` never sticks - it behaves like
   `relative` - so the menu scrolled away with the page and the lower entries
   were unreachable once you were past the first screen.

   The <nav> around it is a grid column and stretches to the full height of the
   row, so sticking to top:0 keeps the menu in view for the whole page. Below
   the md breakpoint the sidebar is a collapsible drawer and must scroll with
   the page as normal. */
@media (min-width: 768px) {
    #sidebarStickyContainer {
        position: sticky;
        top: 0;
        height: 100vh;
        overflow-y: auto;
        overscroll-behavior: contain;   /* running out of menu must not scroll the page */
    }
}

#sidebarStickyContainer {
    scrollbar-width: none !important;
    -ms-overflow-style: none !important;
}

#sidebarStickyContainer::-webkit-scrollbar {
    display: none !important;
    width: 0 !important;
    height: 0 !important;
    background: transparent !important;
}

/* Collapsing a sidebar group animates its HEIGHT, and Bootstrap's .nav is
   flex-wrap:wrap - so as the shrinking box drops below its content height,
   the column re-wraps its items into side-by-side columns mid-animation: the
   submenu visibly "breaks into a line" before it hides. A vertical menu must
   never wrap; with nowrap the items simply slide away under the clip. */
.sidebar .nav {
    flex-wrap: nowrap;
}

.sidebar .nav-link {
    color: #94a3b8;
    padding: 0.8rem 1.2rem;
    font-weight: 500;
}

.sidebar .nav-link:hover {
    color: var(--brand-primary-on-dark);
    background: color-mix(in srgb, var(--brand-primary) 14%, transparent);
}

.sidebar .nav-link.active {
    background: var(--brand-primary) !important;
    color: var(--on-primary) !important;
    font-weight: 700;
}

/* On #000000 this is 4.6:1; the previous #64748b was 3.9:1 and failed. */
.sidebar-heading {
    color: var(--on-dark-muted) !important;
}

/* Modals */
.modal-content {
    background: #ffffff !important;
    color: #0f172a !important;
    border: 1px solid #cbd5e1 !important;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2) !important;
}

.modal-header {
    border-bottom: 1px solid #e2e8f0 !important;
}

.modal-footer {
    border-top: 1px solid #e2e8f0 !important;
}

.modal-title {
    color: #0f172a !important;
    font-weight: 700;
}

.btn-close-white {
    filter: none !important;
}

/* Tables */
.table {
    color: #0f172a !important;
}

.table th {
    color: #475569 !important;
    font-weight: 700;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    border-bottom: 2px solid #e2e8f0 !important;
}

.table td {
    color: #0f172a !important;
    border-bottom: 1px solid #f1f5f9;
}

.table-hover tbody tr:hover {
    background-color: color-mix(in srgb, var(--brand-primary) 8%, transparent) !important;
}

/* Buttons */
/* Brand background -> computed foreground. This is the rule that used to say
   `color: #000000` and turned invisible the moment a dark brand was chosen. */
.btn-primary, .btn-warning {
    background-color: var(--brand-primary) !important;
    border-color: var(--brand-primary) !important;
    color: var(--on-primary) !important;
    font-weight: 700 !important;
}

.btn-primary:hover, .btn-primary:focus, .btn-warning:hover, .btn-warning:focus {
    background-color: color-mix(in srgb, var(--brand-primary) 86%, var(--on-primary)) !important;
    border-color: color-mix(in srgb, var(--brand-primary) 86%, var(--on-primary)) !important;
    color: var(--on-primary) !important;
}

/* Outline buttons put the brand on the CARD, so they need the readable variant
   for the label and may keep the raw colour for the border. */
.btn-outline-primary {
    color: var(--brand-primary-readable) !important;
    border-color: var(--brand-primary) !important;
    font-weight: 600;
}

.btn-outline-primary:hover,
.btn-outline-primary:focus,
.btn-outline-primary.active {
    background-color: var(--brand-primary) !important;
    border-color: var(--brand-primary) !important;
    color: var(--on-primary) !important;
}

.btn-secondary {
    background-color: var(--brand-secondary) !important;
    border-color: var(--brand-secondary) !important;
    color: var(--on-secondary) !important;
}

/* Disabled must stay distinguishable but still clear 3:1 - Bootstrap's default
   .65 opacity drops a mid-brand button to roughly 2:1. */
.btn:disabled, .btn.disabled {
    opacity: 1 !important;
    background-color: #cbd5e1 !important;
    border-color: #cbd5e1 !important;
    color: #475569 !important;
}

/* Dark Accent Metric Boxes inside Cards */
.dark-metric-box {
    background: var(--surface-dark) !important;
    color: var(--on-dark) !important;
    border: 1px solid #334155 !important;
    border-radius: 8px;
    padding: 14px;
}

.dark-metric-box .metric-label {
    color: var(--on-dark-muted) !important;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    display: block;
    margin-bottom: 4px;
}

.dark-metric-box .metric-value {
    color: var(--on-dark) !important;
    font-weight: 800;
}

/* The brand accent on a dark panel - lightened automatically when the chosen
   brand is too dark to read here. */
.dark-metric-box .metric-value-gold {
    color: var(--brand-primary-on-dark) !important;
    font-weight: 800;
}

/* #22c55e is 3.9:1 on #0f172a; --success-on-dark is 7.5:1. */
.dark-metric-box .metric-value-success {
    color: var(--success-on-dark) !important;
    font-weight: 800;
}

/* Tabs */
.nav-tabs .nav-link {
    color: #475569 !important;
    font-weight: 600;
    border: 1px solid transparent;
    border-top-left-radius: 8px;
    border-top-right-radius: 8px;
    padding: 10px 18px;
}

.nav-tabs .nav-link:hover {
    color: var(--brand-primary-readable) !important;
}

.nav-tabs .nav-link.active {
    color: var(--on-primary) !important;
    background-color: var(--brand-primary) !important;
    border-color: var(--brand-primary) var(--brand-primary) #ffffff !important;
    font-weight: 700;
}

/* Dashboard Widgets */
.stat-widget {
    padding: 1.5rem;
    text-align: center;
    background: #ffffff !important;
}

.stat-value {
    font-size: 2rem;
    font-weight: 800;
    color: #0f172a !important;
    display: block;
}

.stat-label {
    color: #475569 !important;
    font-weight: 700;
    text-transform: uppercase;
    font-size: 0.72rem;
    letter-spacing: 0.05em;
}

/* Animations */
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(5px); }
    /* MUST end at `none`, and the class below must not keep the frame applied
       (`forwards`): a retained transform - even translateY(0) - makes the
       animated wrapper the containing block for every position:fixed
       descendant. That is how the attachments modal inside the tab content
       opened 3000px down at zero width, leaving only its backdrop visible. */
    to { opacity: 1; transform: none; }
}

.fade-in {
    animation: fadeIn 0.35s ease-out;
}

/* Subtle Light-Yellow Row Highlight for Updated Records (Fades out cleanly after 3.5s) */
@keyframes rowFadeOut {
    0% {
        background-color: color-mix(in srgb, var(--brand-primary) 22%, transparent) !important;
        --bs-table-accent-bg: color-mix(in srgb, var(--brand-primary) 22%, transparent) !important;
        --bs-table-bg: color-mix(in srgb, var(--brand-primary) 22%, transparent) !important;
    }
    60% {
        background-color: color-mix(in srgb, var(--brand-primary) 13%, transparent) !important;
        --bs-table-accent-bg: color-mix(in srgb, var(--brand-primary) 13%, transparent) !important;
        --bs-table-bg: color-mix(in srgb, var(--brand-primary) 13%, transparent) !important;
    }
    100% {
        background-color: transparent !important;
        --bs-table-accent-bg: transparent !important;
        --bs-table-bg: transparent !important;
    }
}

.row-highlight-pulse,
.row-highlight-pulse > td,
.row-highlight-pulse > th {
    animation: rowFadeOut 3.5s ease-out forwards !important;
}




/* ===========================================================================
   Kanban board (shared) — the glass redesign
   ---------------------------------------------------------------------------
   ONE definition for the dashboards, the Tasks module, the Project Hub and the
   dedicated Board page. There were two looks: a light board rendered inline by
   tasks.php and a dark one from the shared component. The light one is the
   keeper (the owner's call — tests/test_issue_fixes.php §3 enforces it), so
   the glasswork here is translucent LIGHT panels tinted by the per-status
   variable --kb-tint and the brand variables. No hard-coded dark surfaces.
   =========================================================================== */

/* The board row: equal flex columns on desktop; snap-scroll panes on phones. */
.kb-board {
    display: flex;
    align-items: stretch;
    gap: 12px;
}

.kb-col-shell {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

/* Desktop: when five columns get too tight the board scrolls sideways, with
   edge fades as the hint that there is more. The .kb-fade-l / .kb-fade-r
   classes are set from the scroll position (see kbBindBoard). */
@media (min-width: 768px) {
    .kb-board {
        overflow-x: auto;
        scrollbar-width: thin;
    }
    .kb-col-shell { min-width: 215px; }
    .kb-board.kb-fade-r {
        -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent);
        mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent);
    }
    .kb-board.kb-fade-l {
        -webkit-mask-image: linear-gradient(to right, transparent, #000 32px);
        mask-image: linear-gradient(to right, transparent, #000 32px);
    }
    .kb-board.kb-fade-l.kb-fade-r {
        -webkit-mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent);
        mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent);
    }
}

/* Phones: one column ≈ 88vw, swipeable, snapped. */
@media (max-width: 767.98px) {
    .kb-board {
        overflow-x: auto;
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
        scroll-padding-left: 4px;
        padding-bottom: 4px;
    }
    .kb-col-shell {
        flex: 0 0 88vw;
        max-width: 88vw;
        scroll-snap-align: start;
    }
}

.kanban-col-wrapper {
    background: rgba(248, 250, 252, .82);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    border: 1px solid #e2e8f0;
    border-top: 3px solid var(--kb-tint, var(--brand-primary));
    border-radius: 12px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, .04);
    padding: 12px;
    height: 100%;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.kanban-col-wrapper .kanban-col-header {
    font-size: .92rem;
    font-weight: 700;
    color: var(--text-main);
    min-width: 0;
}

.kb-col-sub { font-size: .72rem; }

/* Small round icon buttons in the column header (quick add, collapse). */
.kb-icon-btn {
    --bs-btn-padding-x: .35rem;
    --bs-btn-padding-y: .1rem;
    border: 1px solid transparent;
    color: #64748b;
    border-radius: 8px;
    line-height: 1;
}
.kb-icon-btn:hover, .kb-icon-btn:focus-visible {
    background: color-mix(in srgb, var(--kb-tint, var(--brand-primary)) 14%, #ffffff);
    color: var(--text-main);
    border-color: color-mix(in srgb, var(--kb-tint, var(--brand-primary)) 35%, transparent);
}

/* The count badge's soft WIP hint - purely visual. */
.kanban-count.kb-wip-over {
    background-color: var(--bs-warning) !important;
    color: #212529 !important;
}

.kb-quick-add {
    background: #ffffff;
    border: 1px dashed color-mix(in srgb, var(--kb-tint, var(--brand-primary)) 45%, #cbd5e1);
    border-radius: 8px;
    padding: 8px;
    margin-bottom: 10px;
}

.kanban-column {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-top: 4px;
    overflow-y: auto;
    scrollbar-width: thin;
    min-height: 180px;
    flex: 1 1 auto;
}

/* Embedded boards (dashboards, tasks page, project hub) flow with the page;
   each column scrolls its own cards past this height. The dedicated Board
   page overrides this with the full-viewport rules further down. */
.kb-embedded .kanban-column { max-height: 62vh; }

.kanban-card {
    background: #ffffff;
    border: 1px solid #e2e8f0;
    border-left: 4px solid var(--kb-pri, var(--brand-primary));
    border-radius: 10px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, .04);
    padding: 12px;
    cursor: grab;
    transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}

.kanban-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(0, 0, 0, .08);
    border-color: color-mix(in srgb, var(--kb-pri, var(--brand-primary)) 55%, #e2e8f0);
    border-left-color: var(--kb-pri, var(--brand-primary));
}

.kanban-card:active {
    cursor: grabbing;
}

.kanban-card .task-title {
    color: var(--text-main);
    font-weight: 600;
    font-size: .92rem;
    line-height: 1.3;
    cursor: pointer;
    transition: color .15s ease;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.kanban-card .task-title:hover {
    color: var(--brand-primary-readable);
}

/* The chip row on a card. */
.kb-chip {
    font-size: .68rem;
    font-weight: 600;
    max-width: 140px;
    overflow: hidden;
    text-overflow: ellipsis;
}
.kb-chip-project {
    background: color-mix(in srgb, var(--brand-primary) 12%, #ffffff);
    color: var(--brand-primary-readable);
    border: 1px solid color-mix(in srgb, var(--brand-primary) 30%, transparent);
}
.kb-chip-client {
    background: #f1f5f9;
    color: #475569;
    border: 1px solid #e2e8f0;
}
.kb-due-normal  { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
.kb-due-soon    { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
.kb-due-over    { background: #fee2e2; color: #b91c1c; border: 1px solid #fca5a5; }

/* A card waiting on somebody's decision. */
.kanban-card.kb-await {
    border-left-color: #f59e0b;
    background: linear-gradient(0deg, rgba(245, 158, 11, .05), rgba(245, 158, 11, .05)), #ffffff;
}
.kb-awaiting { color: #92400e; font-weight: 600; }

/* Footer: avatars, comment count, progress ring. */
.kb-foot { border-top: 1px solid #f1f5f9; }

.kb-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--surface-dark-raised);
    color: var(--on-dark);
    font-size: .6rem;
    font-weight: 700;
    border: 2px solid #ffffff;
    margin-right: -7px;
}
.kb-avatar:last-child { margin-right: 0; }
.kb-avatar-more { background: #64748b; }
.kb-avatar-none { background: #e2e8f0; color: #64748b; }

.kb-ring { display: inline-flex; }
.kb-ring svg { transform: rotate(-90deg); }
.kb-ring-track { fill: none; stroke: #e2e8f0; stroke-width: 3; }
.kb-ring-bar {
    fill: none;
    stroke: var(--brand-primary);
    stroke-width: 3;
    stroke-linecap: round;
}

/* --- drag polish ----------------------------------------------------------- */
/* The placeholder left where the card will land. */
.kanban-ghost {
    opacity: .45;
    background: color-mix(in srgb, var(--brand-primary) 25%, #ffffff) !important;
    border: 2px dashed var(--brand-primary) !important;
}

/* The card actually under the pointer: lifted, slightly tilted. */
.kb-drag {
    transform: scale(1.03) rotate(1.5deg) !important;
    box-shadow: 0 12px 28px rgba(15, 23, 42, .22) !important;
    opacity: .95;
}
.kb-chosen { cursor: grabbing; }

/* The column the card is currently over. */
.kanban-column.kb-drop-hint {
    background: color-mix(in srgb, var(--kb-tint, var(--brand-primary)) 7%, transparent);
    border-radius: 8px;
    outline: 2px dashed color-mix(in srgb, var(--kb-tint, var(--brand-primary)) 45%, transparent);
    outline-offset: -2px;
}

body.kb-dragging { cursor: grabbing; }

@media (prefers-reduced-motion: reduce) {
    .kanban-card, .kanban-card .task-title { transition: none; }
    .kanban-card:hover { transform: none; }
    .kb-drag { transform: none !important; }
}

/* The drop target when a column is empty - Sortable needs something to aim at.
   It is always in the markup and hidden while the column holds a card. Sortable
   inserts the dragged element into the target list DURING the drag, so :has()
   flips the moment a card crosses the boundary: the placeholder vanishes under
   the pointer and reappears in the column the card left, with no reload and no
   script. .kanban-empty-hidden is the JavaScript fallback for browsers without
   :has(); where :has() works the two agree. */
.kanban-empty {
    border: 1.5px dashed #cbd5e1;
    border-radius: 10px;
}

.kanban-column:has(.kanban-card) .kanban-empty,
.kanban-empty.kanban-empty-hidden {
    display: none;
}

/* A capped column says what it is showing rather than pretending it is all. */
.kanban-more {
    font-size: .75rem;
}
.kb-completed-link { font-size: .78rem; border-color: #e2e8f0 !important; }

/* --- collapsed columns (desktop only) -------------------------------------- */
.kb-col-collapsed { display: none; }

@media (min-width: 768px) {
    .kb-col-shell.kb-collapsed { flex: 0 0 44px; min-width: 44px; }
    .kb-col-shell.kb-collapsed .kanban-col-wrapper { display: none; }
    .kb-col-shell.kb-collapsed .kb-col-collapsed {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 10px;
        padding: 12px 6px;
        height: 100%;
        min-height: 220px;
        background: rgba(248, 250, 252, .82);
        border: 1px solid #e2e8f0;
        border-top: 3px solid var(--kb-tint, var(--brand-primary));
        border-radius: 12px;
        cursor: pointer;
    }
    .kb-col-collapsed .kb-collapsed-label {
        writing-mode: vertical-rl;
        font-size: .8rem;
        font-weight: 700;
        color: var(--text-main);
        letter-spacing: .02em;
    }
    .kb-col-collapsed:hover {
        background: color-mix(in srgb, var(--kb-tint, var(--brand-primary)) 8%, #f8fafc);
    }
}

/* --- the mobile dot indicator ---------------------------------------------- */
.kb-dots {
    display: flex;
    justify-content: center;
    gap: 6px;
    padding-top: 10px;
}
.kb-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    border: 0;
    padding: 0;
    background: #cbd5e1;
}
.kb-dot.active { background: var(--brand-primary); width: 20px; border-radius: 4px; }

/* --- the dedicated Board page (modules/board.php) -------------------------- */
/* The page itself never scrolls: the toolbar is slim and sticky, and the
   columns take 100% of the remaining height, each scrolling its own cards. */
@media (min-width: 768px) {
    body.ih-board-page main {
        height: 100vh;
        display: flex;
        flex-direction: column;
        overflow: hidden;
    }
    /* The height chain from the 100vh main down to the columns must not break.
       Instant navigation (WP3) wraps every page's content in #app-content, so
       the region is a grandchild of main, not a child - each link in the chain
       has to carry flex:1 1 auto + min-height:0 or the board overflows the
       hidden main and nothing scrolls. */
    body.ih-board-page main > #app-content,
    body.ih-board-page #app-content > .kb-page-region,
    body.ih-board-page main > .kb-page-region {
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }
    body.ih-board-page main > header {
        padding-bottom: .35rem !important;
        margin-bottom: .5rem !important;
    }
}
@media (max-width: 767.98px) {
    body.ih-board-page main {
        height: 100dvh;
        padding-top: calc(56px + .5rem) !important;
        display: flex;
        flex-direction: column;
        overflow: hidden;
    }
    body.ih-board-page main > #app-content,
    body.ih-board-page #app-content > .kb-page-region,
    body.ih-board-page main > .kb-page-region {
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }
    /* The page header is chrome the board cannot afford on a phone. */
    body.ih-board-page main > header { display: none !important; }
}

body.ih-board-page footer { display: none; }

.kb-toolbar {
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--body-bg, #f8fafc);
    padding-bottom: .5rem;
}

.kb-page-fill {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
.kb-board.kb-full {
    flex: 1 1 auto;
    min-height: 0;
}
.kb-board.kb-full .kanban-column { max-height: none; }

/* Phones: the dots are the scroll indicator, so the scrollbar itself goes. */
@media (max-width: 767.98px) {
    .kb-board.kb-full { scrollbar-width: none; }
    .kb-board.kb-full::-webkit-scrollbar { display: none; }
    .kb-toolbar .form-select { flex: 1 1 40%; max-width: none !important; }
    .kb-toolbar .position-relative { flex: 1 1 60%; }
    .kb-toolbar input[type="search"] { max-width: none !important; width: 100%; }
}

/* ===========================================================================
   Sortable table headers
   =========================================================================== */
.task-sort-link {
    white-space: nowrap;
}

.task-sort-link:hover {
    color: var(--brand-primary-readable) !important;
}

/* The pager sits under a table; it must not look like another row. */
.pager-bar .pagination {
    margin-bottom: 0;
}

.pager-bar .pager-per-page {
    min-width: 4.5rem;
}

/* ===========================================================================
   Project Hub
   =========================================================================== */
.pv-avatar {
    background: var(--surface-dark, #0f172a);
    color: var(--on-dark, #ffffff);
    line-height: 1;
}

/* Stacked avatars: each overlaps the previous, first one flush left. */
.pv-avatar-stack > * + * {
    margin-left: -8px;
}

.pv-legend-dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    margin-right: 4px;
    vertical-align: middle;
}

.pv-date-bar {
    background: #e2e8f0;
}

.dark-metric-box .metric-value-danger {
    color: var(--danger-on-dark, #ff6b6b) !important;
    font-weight: 800;
}


/* ===========================================================================
   Status colours and muted text (WP5)
   ---------------------------------------------------------------------------
   Bootstrap's saturated defaults are tuned for a white background. On the dark
   panels .text-danger (#dc3545) is 3.3:1 and .text-success (#198754) is 2.8:1 -
   both below the 4.5:1 floor. These override them only where the surface is
   dark, so the light pages keep Bootstrap's familiar palette.
   =========================================================================== */
.sidebar .text-danger,
.dark-metric-box .text-danger,
.kanban-surface .text-danger,
.text-danger-on-dark {
    color: var(--danger-on-dark) !important;
}

.sidebar .text-success,
.dark-metric-box .text-success,
.kanban-surface .text-success,
.text-success-on-dark {
    color: var(--success-on-dark) !important;
}

.sidebar .text-warning,
.dark-metric-box .text-warning,
.kanban-surface .text-warning,
.text-warning-on-dark {
    color: var(--warning-on-dark) !important;
}

.sidebar .text-info,
.dark-metric-box .text-info,
.kanban-surface .text-info,
.text-info-on-dark {
    color: var(--info-on-dark) !important;
}

/* Muted text on a light card. Bootstrap's .text-muted resolves to #6c757d,
   which is 4.68:1 on white - it passes, but only just, and several places had
   layered it over a tinted panel where it does not. Pinned to a value measured
   against the card background. */
.glass-card .text-muted,
.modal-content .text-muted {
    color: var(--text-muted-accessible) !important;
}

/* Muted text on a dark surface: never lighter than rgba(255,255,255,.65). */
.sidebar .text-muted,
.dark-metric-box .text-muted,
.kanban-surface .text-muted {
    color: var(--on-dark-muted) !important;
}

/* Form validation text. #dc3545 on white is 4.5:1 exactly; on the tinted
   .bg-light panels the modals use it drops below, so it is darkened. */
.invalid-feedback,
.form-text.text-danger {
    color: #b02a37 !important;
}

/* Settings > Branding: live contrast preview (WP5) */
.contrast-swatches {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.contrast-swatch {
    font-size: 0.7rem;
    font-weight: 700;
    padding: 4px 9px;
    border-radius: 5px;
    border: 1px solid transparent;
    white-space: nowrap;
}

.contrast-hint {
    line-height: 1.4;
}

/* ===========================================================================
   Top bar: global search + notification bell (WP3.1 / WP3.4)
   =========================================================================== */
.ih-search {
    min-width: 190px;
}

.ih-search-input {
    padding-left: 30px;
}

.ih-search-icon {
    position: absolute;
    left: 10px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-muted-accessible);
    font-size: 0.85rem;
    pointer-events: none;
}

.ih-search-results {
    display: none;
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: auto;
    min-width: 340px;
    max-width: 420px;
    max-height: 420px;
    overflow-y: auto;
    padding: 6px 0;
}

.ih-search-results.show {
    display: block;
}

.ih-search-group {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted-accessible);
    padding: 8px 14px 4px;
}

.ih-search-item {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 6px 14px;
    white-space: normal;
}

.ih-search-item.active,
.ih-search-item:hover,
.ih-search-item:focus {
    background: color-mix(in srgb, var(--brand-primary) 14%, transparent);
    color: var(--text-main);
}

.ih-search-title {
    font-weight: 600;
    font-size: 0.85rem;
    color: var(--text-main);
}

.ih-search-meta {
    font-size: 0.72rem;
    color: var(--text-muted-accessible);
}

/* --- bell ---------------------------------------------------------------- */
.ih-notif-menu {
    width: 350px;
    max-width: calc(100vw - 24px);
    padding: 0;
}

.ih-notif-list {
    max-height: 360px;
    overflow-y: auto;
}

.ih-notif-item {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    padding: 10px 14px;
    border-bottom: 1px solid #f1f5f9;
    text-decoration: none;
    color: inherit;
}

a.ih-notif-item:hover,
a.ih-notif-item:focus {
    background: color-mix(in srgb, var(--brand-primary) 12%, transparent);
}

.ih-notif-unread {
    background: color-mix(in srgb, var(--brand-primary) 7%, transparent);
}

.ih-notif-item-icon {
    color: var(--brand-primary-readable);
    font-size: 1rem;
    margin-top: 2px;
    flex-shrink: 0;
}

.ih-notif-item-body {
    min-width: 0;
}

.ih-notif-item-title {
    font-size: 0.83rem;
    font-weight: 600;
    color: var(--text-main);
}

.ih-notif-item-sub {
    font-size: 0.74rem;
    color: var(--text-muted-accessible);
}

.ih-notif-item-ago {
    font-size: 0.68rem;
    color: var(--text-muted-accessible);
    margin-top: 2px;
}

/* Notification history page */
.notif-icon {
    width: 34px;
    height: 34px;
    background: color-mix(in srgb, var(--brand-primary) 16%, transparent);
    color: var(--brand-primary-readable);
}

.notif-unread {
    background: color-mix(in srgb, var(--brand-primary) 7%, transparent);
}

.min-width-0 {
    min-width: 0;
}

@media (max-width: 767.98px) {
    .ih-search { min-width: 130px; }
    .ih-search-results { min-width: 260px; }
}

/* ===========================================================================
   Task comments (WP3.5)
   =========================================================================== */
.task-comment-thread {
    border-top: 1px solid #e2e8f0;
    padding-top: 12px;
    margin-top: 12px;
}

.tc-list {
    max-height: 240px;
    overflow-y: auto;
    margin-bottom: 10px;
}

.tc-item {
    display: flex;
    gap: 9px;
    align-items: flex-start;
    padding: 7px 0;
    border-bottom: 1px solid #f1f5f9;
}

.tc-item:last-child {
    border-bottom: 0;
}

.tc-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}

.tc-avatar-initials {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--surface-dark);
    color: var(--on-dark);
    font-size: 0.66rem;
    font-weight: 700;
}

.tc-body {
    min-width: 0;
    flex-grow: 1;
}

.tc-head {
    display: flex;
    align-items: baseline;
    gap: 7px;
}

.tc-author {
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--text-main);
}

.tc-time {
    font-size: 0.68rem;
    color: var(--text-muted-accessible);
}

.tc-text {
    font-size: 0.83rem;
    color: var(--text-main);
    white-space: pre-wrap;
    word-break: break-word;
}

.tc-mention {
    color: var(--brand-primary-readable);
    font-weight: 600;
}

.tc-delete {
    opacity: 0;
    transition: opacity 0.15s ease;
}

.tc-item:hover .tc-delete,
.tc-delete:focus-visible {
    opacity: 1;
}

.tc-mention-menu {
    top: auto;
    bottom: 100%;
    left: 8px;
    max-height: 190px;
    overflow-y: auto;
}

.tc-mention-option {
    font-size: 0.82rem;
}

/* Dashboard analytics (WP3.6) — a fixed box so Chart.js has a height to fill */
.da-chart-wrap {
    position: relative;
    height: 260px;
}

/* ===========================================================================
   Shared segmented view toggle (Item 1)
   ---------------------------------------------------------------------------
   ONE definition for the Tasks module and the Project Hub. The Project Hub
   previously used .btn-outline-primary, whose :hover and .active resolve to the
   same declaration block — so hovering an inactive segment painted it exactly
   like the active one and the control stopped indicating which view you were
   in. Here hover, active and focus are three visibly different states.
   =========================================================================== */
.view-toggle {
    display: inline-flex;
    gap: 4px;
    padding: 4px;
    border-radius: 9px;
    background: #f1f5f9;
    border: 1px solid #e2e8f0;
}

.view-toggle-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: 0;
    border-radius: 6px;
    padding: 6px 14px;
    font-size: 0.85rem;
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    white-space: nowrap;
    background: transparent;
    /* #475569, not the usual --text-muted-accessible: that token is calibrated
       against a WHITE card and measures only 4.34:1 on this toggle's own
       #f1f5f9 track. This is 6.92:1 there. */
    color: #475569;
    transition: background-color .18s ease, color .18s ease, box-shadow .18s ease;
}

/* Hover on an INACTIVE segment: a neutral tint and darker text. Deliberately
   not the brand colour — that is reserved for "this is the current view". */
.view-toggle-btn:hover:not(.active) {
    background: rgba(15, 23, 42, .06);
    color: var(--text-main, #0f172a);
}

.view-toggle-btn.active {
    background: var(--brand-primary);
    color: var(--on-primary);
    font-weight: 700;
    /* Two shadows. The first is a 1px ring in the READABLE brand variant: with
       a near-white brand the active background is almost the same colour as the
       #f1f5f9 track, so without a ring the segment is legible but you cannot
       tell which one is selected. The ring is brand-derived, so it is always
       visible whatever colour is picked. */
    box-shadow:
        0 0 0 1px color-mix(in srgb, var(--brand-primary-readable) 45%, transparent),
        0 2px 6px color-mix(in srgb, var(--brand-primary) 34%, transparent);
}

/* The active segment must not change appearance on hover - it is already the
   answer to "which view am I in?". */
.view-toggle-btn.active:hover {
    background: var(--brand-primary);
    color: var(--on-primary);
}

/* The ring would disappear into a dark panel; use the on-dark variant there. */
.kanban-surface .view-toggle-btn.active,
.dark-metric-box .view-toggle-btn.active,
.sidebar .view-toggle-btn.active {
    box-shadow:
        0 0 0 1px color-mix(in srgb, var(--brand-primary-on-dark) 45%, transparent),
        0 2px 6px rgba(0, 0, 0, .35);
}

.view-toggle-btn:focus-visible {
    outline: 2px solid var(--brand-primary-readable);
    outline-offset: 2px;
}

.view-toggle-btn i {
    font-size: 0.95em;
}

/* The toggle sits on a dark panel in a few places (kanban headers). */
.kanban-surface .view-toggle,
.dark-metric-box .view-toggle,
.sidebar .view-toggle {
    background: rgba(255, 255, 255, .07);
    border-color: rgba(255, 255, 255, .16);
}

.kanban-surface .view-toggle-btn:not(.active),
.dark-metric-box .view-toggle-btn:not(.active),
.sidebar .view-toggle-btn:not(.active) {
    color: var(--on-dark-muted);
}

.kanban-surface .view-toggle-btn:hover:not(.active),
.dark-metric-box .view-toggle-btn:hover:not(.active),
.sidebar .view-toggle-btn:hover:not(.active) {
    background: rgba(255, 255, 255, .12);
    color: var(--on-dark);
}

.kanban-surface .view-toggle-btn:focus-visible,
.dark-metric-box .view-toggle-btn:focus-visible,
.sidebar .view-toggle-btn:focus-visible {
    outline-color: var(--brand-primary-on-dark);
}

@media (max-width: 575.98px) {
    .view-toggle-btn { padding: 6px 10px; }
    .view-toggle-btn span { display: none; }   /* icons only on the narrowest screens */
}

/* ===========================================================================
   Active Clients panel (Item 4)
   =========================================================================== */
.active-client-card {
    border: 1px solid #e2e8f0;
    border-left: 3px solid var(--brand-primary);
    border-radius: 9px;
    padding: 14px;
    background: #ffffff;
    transition: box-shadow .18s ease, transform .18s ease;
}

.active-client-card:hover {
    box-shadow: 0 6px 16px rgba(0, 0, 0, .07);
    transform: translateY(-2px);
}

.active-client-name {
    font-weight: 700;
    font-size: 0.92rem;
    color: var(--text-main);
    /* Long client names must not push the source badge off the card. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.active-client-name:hover,
.active-client-name:focus-visible {
    color: var(--brand-primary-readable);
}

.active-client-outstanding {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    padding-top: 8px;
    border-top: 1px dashed #e2e8f0;
}

.active-client-outstanding strong {
    color: var(--text-main);
    font-size: 0.85rem;
}

/* At 375px the four-up grid is already one column via col-12; this keeps the
   card itself from overflowing on very narrow screens. */
@media (max-width: 400px) {
    .active-client-card { padding: 11px; }
    .active-client-name { font-size: 0.86rem; }
}

/* ===========================================================================
   Outstanding invoices panel + bulk reminder bar (Item 6.2)
   =========================================================================== */
.outstanding-bulkbar {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 14px;
    margin-bottom: 10px;
    border-radius: 8px;
    background: color-mix(in srgb, var(--brand-primary) 14%, #ffffff);
    border: 1px solid var(--brand-primary);
    box-shadow: 0 2px 10px rgba(0, 0, 0, .06);
}

.outstanding-bulkbar strong {
    color: var(--text-main);
}

/* Rows the operator has ticked, so the selection is obvious while scrolling. */
#outstandingTable tr.is-selected > td {
    background: color-mix(in srgb, var(--brand-primary) 9%, transparent) !important;
}

/* A per-invoice result line in the confirmation modal. */
.remind-result {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 7px 0;
    border-bottom: 1px solid #f1f5f9;
    font-size: .84rem;
}

.remind-result:last-child { border-bottom: 0; }
.remind-result .ri-icon { flex-shrink: 0; margin-top: 1px; }
.remind-result.ok      .ri-icon { color: #198754; }
.remind-result.fail    .ri-icon { color: #b02a37; }
.remind-result.skip    .ri-icon { color: var(--text-muted-accessible); }
.remind-result .ri-no  { font-weight: 700; color: var(--text-main); min-width: 92px; }
.remind-result .ri-msg { color: var(--text-muted-accessible); }

/* The "recently reminded" block in the confirm modal. */
.remind-warn-row {
    background: color-mix(in srgb, #ffc107 18%, transparent);
    border: 1px solid color-mix(in srgb, #ffc107 55%, transparent);
    border-radius: 7px;
    padding: 9px 11px;
    margin-bottom: 7px;
}

@media (max-width: 575.98px) {
    .outstanding-bulkbar { flex-wrap: wrap; }
}

/* ==========================================================================
   Modal scrolling
   --------------------------------------------------------------------------
   Every modal dialog carries .modal-dialog-scrollable, so a form that grows -
   because an inline error appeared, or because line items were added - scrolls
   its body instead of running its footer off the bottom of the screen.

   The scrollbar itself is hidden. It is a small dialog on top of a page that
   already scrolls, and two visible bars is visual noise; the content still
   scrolls by wheel, trackpad, touch, arrow keys, Page Up/Down and Home/End.
   ========================================================================== */
/* Bootstrap's scrollable layout assumes .modal-header / .modal-body /
   .modal-footer are DIRECT children of .modal-content. Nearly every modal here
   wraps them in a <form>, so the form became the flex item - a plain block that
   grows to its content - and the body inside it never got a height to scroll
   within. The dialog was correctly capped at the viewport and the form simply
   overflowed it, which is why a modal that grew put its own footer off-screen.

   Make that wrapper the flex column instead. Anything nested this way works:
   a form, a div, whatever the page wrapped its content in. */
.modal-dialog-scrollable .modal-content > form,
.modal-dialog-scrollable .modal-content > div:not(.modal-header):not(.modal-body):not(.modal-footer) {
    display: flex;
    flex-direction: column;
    min-height: 0;                      /* or the content's size becomes the floor */
    max-height: 100%;
    overflow: hidden;
    width: 100%;
}

.modal-dialog-scrollable .modal-content > form > .modal-body,
.modal-dialog-scrollable .modal-content > div > .modal-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

.modal-dialog-scrollable .modal-body {
    scrollbar-width: none;              /* Firefox */
    -ms-overflow-style: none;           /* old Edge */
    overscroll-behavior: contain;       /* reaching the end must not scroll the page behind */
}

.modal-dialog-scrollable .modal-body::-webkit-scrollbar {
    width: 0;
    height: 0;
}

/* A scrollable body loses the rounded corner the dialog gives it, and the
   overflow clips square against the footer. Put it back. */
.modal-dialog-scrollable .modal-content {
    overflow: hidden;
}

/* The footer must stay reachable and readable when the body scrolls under it. */
.modal-dialog-scrollable .modal-footer {
    flex-shrink: 0;
}

/* ==========================================================================
   Payment-method picker  (includes/payment_method_picker.php)
   --------------------------------------------------------------------------
   A 15px checkbox inside a large card meant the obvious thing to click - the
   card - did nothing. The whole card is the control now. The checkbox is still
   a real checkbox, so the label association, the keyboard behaviour and the
   form value are unchanged; only the hit area and the selected state moved.
   ========================================================================== */
.pm-card {
    display: flex;
    align-items: flex-start;
    gap: .7rem;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: .85rem;
    background: var(--surface-muted, #f8f9fa);
    border: 1px solid var(--border-subtle, #e9ecef);
    border-radius: .5rem;
    cursor: pointer;
    transition: border-color .12s ease, background-color .12s ease, box-shadow .12s ease;
}

.pm-card:hover {
    border-color: var(--brand-primary, #0d6efd);
    background: #fff;
}

/* Selected is a class, not :checked, so a box ticked by script paints too. */
.pm-card.is-selected {
    background: #fff;
    border-color: var(--brand-primary, #0d6efd);
    box-shadow: inset 0 0 0 1px var(--brand-primary, #0d6efd);
}

.pm-card-check {
    flex: 0 0 auto;
    width: 1.15rem;
    height: 1.15rem;
    margin-top: .1rem;
    cursor: pointer;
}

/* Keyboard focus lands on the checkbox; show it on the card the user sees. */
.pm-card:focus-within {
    outline: 2px solid var(--brand-primary, #0d6efd);
    outline-offset: 2px;
}

.pm-card-body   { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.pm-card-title  { font-size: .85rem; font-weight: 700; color: var(--text-strong, #212529); }
.pm-card-sub    { font-size: .75rem; color: var(--text-muted, #6c757d); overflow-wrap: anywhere; }

/* ===========================================================================
   Search-select — the picker that asks the server for its options
   ---------------------------------------------------------------------------
   Deliberately built out of the same form-control the <select> it replaces
   used, so a form does not change shape when a field is upgraded.
   =========================================================================== */
.search-select {
    position: relative;
}

.search-select .ss-control {
    position: relative;
}

/* Room for the clear button, which only appears once something is chosen. */
.search-select .ss-input {
    padding-right: 2.25rem;
}

.search-select .ss-clear {
    position: absolute;
    top: 50%;
    right: .35rem;
    transform: translateY(-50%);
    display: none;
    padding: .15rem .35rem;
    line-height: 1;
    color: var(--text-muted, #64748b);
    background: transparent;
    border: 0;
}

.search-select.ss-has-value .ss-clear {
    display: block;
}

.search-select .ss-clear:hover {
    color: var(--brand-primary-readable);
}

.search-select .ss-menu {
    position: absolute;
    z-index: 1080;              /* above a Bootstrap modal's own stacking */
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    max-height: 15rem;
    overflow-y: auto;
    display: none;
    background: #ffffff;
    border: 1px solid #e2e8f0;
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
}

.search-select.ss-open .ss-menu {
    display: block;
}

.search-select .ss-option {
    padding: .5rem .75rem;
    cursor: pointer;
    border-bottom: 1px solid #f1f5f9;
}

.search-select .ss-option:last-child {
    border-bottom: 0;
}

.search-select .ss-option:hover,
.search-select .ss-option.ss-active {
    background: color-mix(in srgb, var(--brand-primary) 12%, #ffffff);
}

.search-select .ss-option-text {
    display: block;
    font-weight: 600;
    color: var(--text-main);
    font-size: .9rem;
}

.search-select .ss-option-meta {
    display: block;
    font-size: .75rem;
    color: var(--text-muted, #64748b);
}

/* "Type at least 2 characters", "Searching…", "No matches." */
.search-select .ss-note {
    padding: .6rem .75rem;
    font-size: .8rem;
    color: var(--text-muted, #64748b);
}

/* The dropdown lists unread only; this is the way to everything else. */
.ih-notif-all {
    border-top: 1px solid #e2e8f0;
    color: var(--brand-primary-readable);
    font-weight: 600;
}

.ih-notif-all:hover {
    background: color-mix(in srgb, var(--brand-primary) 10%, #ffffff);
}

/* ---------------------------------------------------------------------------
   Collapsible sidebar groups (IA pass).
   The heading was a <h6>; it is now a <button> so it can be focused and can
   announce its own expanded state. These rules only make the button look like
   the heading it replaced - no new colours, no new spacing.
   --------------------------------------------------------------------------- */
.sidebar .ih-nav-group {
    color: var(--on-dark-muted) !important;
    font-weight: 600;
    letter-spacing: 0.02em;
    background: none;
    box-shadow: none;
}
.sidebar .ih-nav-group:hover,
.sidebar .ih-nav-group:focus-visible {
    color: var(--brand-primary-on-dark) !important;
}
.sidebar .ih-nav-caret {
    font-size: 0.7rem;
    transition: transform 0.15s ease-in-out;
}
.sidebar .ih-nav-group[aria-expanded="false"] .ih-nav-caret {
    transform: rotate(-90deg);
}

/* ===========================================================================
   Mobile & tablet
   ---------------------------------------------------------------------------
   Below md the sidebar is an offcanvas drawer (includes/sidebar.php) opened
   by the hamburger in the page header (includes/header.php). From md up the
   offcanvas classes are inert and nothing below changes the desktop look.
   =========================================================================== */
.sidebar.offcanvas-md {
    --bs-offcanvas-width: 280px;
}

/* The phone top bar: logo left, menu toggle right, fixed to the top. The
   ih-bar-hidden class (added by the scroll watcher in includes/header.php)
   slides it away while scrolling down and back while scrolling up. z-index
   sits below the offcanvas drawer (1045) and its backdrop (1040), so the
   open menu covers the bar rather than the reverse. */
.ih-mobile-bar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 56px;
    background: var(--sidebar-bg, #000000);
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
    z-index: 1030;
    transition: transform 0.25s ease;
}

.ih-mobile-bar.ih-bar-hidden {
    transform: translateY(-100%);
}

.ih-mobile-toggle {
    color: #ffffff;
    font-size: 1.7rem;
    line-height: 1;
    border: 0;
}

.ih-mobile-toggle:hover,
.ih-mobile-toggle:focus-visible {
    color: var(--brand-primary-on-dark);
}

@media (max-width: 767.98px) {
    /* Room for the fixed bar. Kept constant even while the bar is hidden -
       content that jumps 56px every time the bar comes and goes is worse
       than the space it saves. */
    main.pt-4 {
        padding-top: calc(56px + 1rem) !important;
    }

    /* The drawer fills its own height and scrolls itself. The sticky rules
       further up apply only from md, so only the 100vh minimum needs undoing
       here - inside a fixed-position drawer it fights the panel's own size. */
    .sidebar { min-height: 0; }
    .sidebar #sidebarStickyContainer {
        height: 100%;
        overflow-y: auto;
    }

    /* The page header stacks: hamburger + title first, then the toolbar
       (search, bell, account) on its own full-width line, with the search
       box taking whatever width the buttons leave. */
    main > header .btn-toolbar { width: 100%; }
    main > header .ih-search { flex: 1 1 auto; min-width: 0; }
    main > header .ih-search-input { width: 100%; }
    main > header .ih-search-results { min-width: 0; width: 100%; max-width: none; }
    h1.h2 { font-size: 1.35rem; }

    /* Cards and filter bars breathe a little less on a phone. */
    .glass-card.p-4 { padding: 1rem !important; }
}

/* ===========================================================================
   WP2 — the mobile app layer: bottom tab bar, bottom-sheet menu, touch polish
   =========================================================================== */

/* ---- the bottom tab bar (phones only) ------------------------------------ */
.ih-bnav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1030;
    display: flex;
    align-items: stretch;
    justify-content: space-around;
    background: rgba(255, 255, 255, .96);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border-top: 1px solid #e2e8f0;
    padding-bottom: env(safe-area-inset-bottom, 0);
    transition: transform .2s ease;
}

.ih-bnav-hidden { transform: translateY(110%); }

.ih-bnav-item {
    flex: 1 1 0;
    min-height: 56px;              /* comfortably past the 44px floor */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    border: 0;
    background: transparent;
    color: #64748b;
    text-decoration: none;
    font-size: .68rem;
    font-weight: 600;
}

.ih-bnav-item .ih-bnav-icon { font-size: 1.25rem; line-height: 1; }
.ih-bnav-item.active { color: var(--brand-primary-readable); }
.ih-bnav-item:active { color: var(--brand-primary-readable); transform: scale(.94); }

.ih-bnav-badge {
    position: absolute;
    top: -6px;
    right: -10px;
    font-size: .58rem;
}

/* The centre action. Raised, round, brand-coloured, one tap. */
.ih-bnav-fab {
    flex: 0 0 auto;
    align-self: center;
    width: 52px;
    height: 52px;
    margin: 0 6px;
    margin-top: -18px;             /* floats above the bar line */
    border-radius: 50%;
    border: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--brand-primary);
    color: var(--on-primary);
    font-size: 1.45rem;
    box-shadow: 0 6px 16px color-mix(in srgb, var(--brand-primary) 45%, transparent);
    text-decoration: none;
}
.ih-bnav-fab:active { transform: scale(.94); }
.ih-bnav-fab.ih-fab-out { background: #10b981; color: #ffffff; }

/* Content must never hide behind the bar. */
@media (max-width: 767.98px) {
    body:has(.ih-bnav) main {
        padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)) !important;
    }
    /* :has() fallback: the bar is only ever rendered on phones for signed-in
       pages, so padding every main under md is the safe over-approximation. */
    main { padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); }

    body.ih-board-page main {
        /* the board page has no page scroll; the region shrinks instead */
        padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px)) !important;
    }

    /* The top bar respects a notch. */
    .ih-mobile-bar { padding-top: env(safe-area-inset-top, 0); }
}

/* ---- the sidebar as a bottom sheet on phones ------------------------------ */
@media (max-width: 767.98px) {
    #sidebarMenu.offcanvas-md {
        top: auto;
        bottom: 0;
        left: 0;
        right: 0;
        width: 100vw !important;
        max-width: 100vw;
        height: 78dvh;
        /* !important because a pre-existing .sidebar.offcanvas-md rule (the
           280px desktop drawer width) and Bootstrap's own offcanvas transform
           both sit at a specificity this has to clear on BOTH states, or the
           sheet parks off-screen with the class present. */
        transform: translateY(100%) !important;
        border-radius: 18px 18px 0 0;
        border-top: 3px solid var(--brand-primary);
    }
    #sidebarMenu.offcanvas-md.show,
    #sidebarMenu.offcanvas-md.showing { transform: translateY(0) !important; }
    /* Bootstrap keeps .show ON for the whole hide transition and adds .hiding
       alongside it, only dropping .show at the very end. Without this rule the
       .show line above pinned the sheet OPEN the entire time, then it vanished
       the instant .show was removed - no slide. This wins (same specificity,
       declared later) so a backdrop tap, the close button, the handle and an
       instant-nav link all slide the sheet DOWN instead of blinking shut. */
    #sidebarMenu.offcanvas-md.hiding { transform: translateY(100%) !important; }
    #sidebarMenu .pt-3 {
        height: 100%;
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        padding-bottom: env(safe-area-inset-bottom, 0);
    }

    /* The bottom-sheet header: a centred grab handle plus a close button. */
    .ih-sheet-header {
        position: relative;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 10px 12px 6px;
        touch-action: none;   /* the whole strip is a drag target */
    }
    .ih-sheet-grab {
        width: 44px;
        height: 5px;
        min-height: 5px;
        padding: 0;
        border: 0;
        border-radius: 999px;
        background: rgba(255, 255, 255, .35);
        cursor: grab;
    }
    .ih-sheet-grab:active { cursor: grabbing; }
    .ih-sheet-close {
        position: absolute;
        top: 8px;
        right: 12px;
    }

    /* While a swipe-down is in progress the sheet must track the finger with no
       transition; a JS-set inline transform drives it, and the class removes
       the eased close so it does not fight the drag. */
    #sidebarMenu.ih-sheet-dragging {
        transition: none !important;
    }
}

/* ---- install nudge -------------------------------------------------------- */
.ih-install-card {
    position: fixed;
    left: 12px;
    right: 12px;
    bottom: calc(78px + env(safe-area-inset-bottom, 0px));
    z-index: 1029;
    display: flex;
    align-items: center;
    gap: 10px;
    background: #ffffff;
    border: 1px solid #e2e8f0;
    border-left: 4px solid var(--brand-primary);
    border-radius: 12px;
    padding: 12px 14px;
}

/* ---- pull-to-refresh indicator -------------------------------------------- */
.ih-ptr {
    position: fixed;
    top: -44px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1040;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: #ffffff;
    border: 1px solid #e2e8f0;
    box-shadow: 0 4px 12px rgba(15, 23, 42, .15);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #64748b;
    transition: transform .15s ease;
}
.ih-ptr.ih-ptr-armed { color: var(--brand-primary-readable); border-color: var(--brand-primary); }
.ih-ptr.ih-ptr-busy  { transform: translateX(-50%) translateY(60px) !important; }

/* ---- touch & feel polish -------------------------------------------------- */
@media (max-width: 767.98px) {
    /* iOS zooms any focused field under 16px. */
    .form-control, .form-select, .input-group-text { font-size: 16px; }

    /* The 44px tap floor. kb-icon-btn and btn-close stay small by design
       (generous surroundings); everything a thumb aims at grows. */
    .btn:not(.kb-icon-btn):not(.btn-close):not(.ih-bnav-item) { min-height: 44px; }
    .dropdown-item { padding-top: .6rem; padding-bottom: .6rem; }
    .nav-pills .nav-link { padding: .55rem .9rem; }
    .page-link { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }

    /* Sticky table headers: the wrapper becomes the scroll container so the
       header row can pin to its top. */
    .table-responsive { max-height: 72vh; -webkit-overflow-scrolling: touch; }
    .table-responsive thead th {
        position: sticky;
        top: 0;
        z-index: 2;
        background: #f8fafc;
        box-shadow: 0 1px 0 #e2e8f0;
    }
}

/* Touch feedback + hover-only affordances made visible where hover is not a
   thing. .tc-delete (comment delete) was invisible until mouse-over. */
@media (hover: none) {
    .tc-delete { opacity: 1; }
    .kanban-card:active { background: #f8fafc; }
    .glass-card a:active, .list-group-item:active { opacity: .85; }
}

/* ===========================================================================
   WP3 — instant navigation: progress bar + skeleton placeholders
   =========================================================================== */
.ih-nav-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 2px;
    width: 0;
    background: var(--brand-primary);
    z-index: 2000;
    box-shadow: 0 0 8px color-mix(in srgb, var(--brand-primary) 60%, transparent);
    pointer-events: none;
}

/* The skeletons: a calm shimmer in the shape of the page family that is
   loading, instead of a spinner that says nothing about what is coming. */
.ih-sk { padding: 4px 0; }
@keyframes ihSkPulse { 0% { opacity: .55; } 50% { opacity: 1; } 100% { opacity: .55; } }
.ih-sk-line, .ih-sk-row, .ih-sk-card, .ih-sk-field {
    background: #e9eef4;
    border-radius: 8px;
    animation: ihSkPulse 1.2s ease-in-out infinite;
}
.ih-sk-line  { height: 16px; margin: 10px 0; }
.ih-sk-line.w40 { width: 40%; }
.ih-sk-line.w60 { width: 60%; }
.ih-sk-row   { height: 44px; margin: 8px 0; }
.ih-sk-field { height: 38px; margin: 6px 0 16px; }
.ih-sk-form  { max-width: 640px; }
.ih-sk-grid  { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }
.ih-sk-card  { height: 90px; }
.ih-sk-card.tall { height: 150px; }
.ih-sk-board { display: flex; gap: 12px; }
.ih-sk-board .ih-sk-col { flex: 1 1 0; }

@media (prefers-reduced-motion: reduce) {
    .ih-sk-line, .ih-sk-row, .ih-sk-card, .ih-sk-field { animation: none; }
    .ih-nav-progress { box-shadow: none; }
}

/* ===========================================================================
   Project case study — masonry galleries + lightbox
   =========================================================================== */
.cs-masonry {
    column-count: 3;
    column-gap: 12px;
}
@media (max-width: 991.98px) { .cs-masonry { column-count: 2; } }
@media (max-width: 575.98px) { .cs-masonry { column-count: 1; } }

.cs-item {
    position: relative;
    break-inside: avoid;
    margin-bottom: 12px;
    border: 1px solid #e2e8f0;
    border-radius: 10px;
    overflow: hidden;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, .04);
}
.cs-lb { display: block; cursor: zoom-in; }
.cs-item img { display: block; width: 100%; height: auto; transition: transform .2s ease; }
.cs-item:hover img { transform: scale(1.03); }
.cs-item .cs-cap {
    font-size: .78rem;
    color: #64748b;
    padding: 6px 10px;
    border-top: 1px solid #f1f5f9;
}
.cs-del {
    position: absolute;
    top: 6px;
    right: 6px;
    margin: 0;
    opacity: 0;
    transition: opacity .15s ease;
}
.cs-item:hover .cs-del, .cs-del:focus-within { opacity: 1; }
@media (hover: none) { .cs-del { opacity: 1; } }

/* The lightbox overlay. Above everything, including modals. */
.cs-lightbox {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(15, 23, 42, .93);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
}
.cs-lightbox.open { display: flex; }
body.cs-lb-lock { overflow: hidden; }

.cs-lb-figure {
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    max-width: 94vw;
}
.cs-lb-img {
    max-width: 94vw;
    max-height: 82vh;
    object-fit: contain;
    border-radius: 6px;
    box-shadow: 0 12px 48px rgba(0, 0, 0, .55);
}
.cs-lb-cap {
    color: #e2e8f0;
    font-size: .9rem;
    margin-top: .75rem;
    text-align: center;
    max-width: 80vw;
}
.cs-lb-close {
    position: absolute;
    top: 12px;
    right: 18px;
    z-index: 2;
    background: transparent;
    border: 0;
    color: #fff;
    font-size: 2.1rem;
    line-height: 1;
    cursor: pointer;
    opacity: .85;
}
.cs-lb-close:hover { opacity: 1; }
.cs-lb-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 52px;
    height: 72px;
    border: 0;
    border-radius: 8px;
    background: rgba(255, 255, 255, .12);
    color: #fff;
    font-size: 2.4rem;
    line-height: 1;
    cursor: pointer;
}
.cs-lb-nav:hover { background: rgba(255, 255, 255, .26); }
.cs-lb-prev { left: 16px; }
.cs-lb-next { right: 16px; }
.cs-lb-counter {
    position: absolute;
    bottom: 16px;
    left: 50%;
    transform: translateX(-50%);
    color: #cbd5e1;
    font-size: .85rem;
}
@media (max-width: 575.98px) {
    .cs-lb-nav { width: 40px; height: 60px; font-size: 1.8rem; }
    .cs-lb-prev { left: 6px; } .cs-lb-next { right: 6px; }
}
