/**
 * IB Portal Dashboard — Responsive layer
 *
 * SINGLE RESPONSIBILITY: adapt the approved desktop UI to smaller viewports.
 *
 * RULES THIS FILE OBEYS (do not break them when editing):
 *   1. Every declaration lives inside an @media block. Nothing here applies at
 *      desktop widths, so the desktop design is untouched by construction.
 *   2. No colour, font-family, icon, radius or brand token is redefined. Only
 *      layout, flow, spacing scale and hit-area size change.
 *   3. Loaded AFTER portal.css, so narrower blocks are declared last and win
 *      over wider ones. Keep the blocks in descending width order.
 *   4. !important is used only where it is required to beat an existing
 *      !important rule or an inline style attribute in the view templates.
 *
 * BREAKPOINT SCALE
 *   Desktop            >= 1281px   no rules here — pixel-identical to before
 *   Laptop             <= 1280px   density only
 *   Tablet landscape   <= 1024px   off-canvas sidebar starts here
 *   Tablet portrait    <=  900px
 *   Mobile large       <=  768px
 *   Mobile medium      <=  480px
 *   Mobile small       <=  380px
 *   Touch input        (pointer: coarse) — hit areas, width-independent
 *
 * @package IB_Portal_Dashboard
 * @since   1.25.0
 */


/* ═══════════════════════════════════════════════════════════
   OFF-CANVAS DRAWER SCRIM
   Element ships in sidebar.php but is inert until a small
   viewport activates it, so desktop pays nothing for it.
═══════════════════════════════════════════════════════════ */
#ibp-app .ibp-sidebar-backdrop,
#ibp-app .ibp-appbar-brand,
#ibp-app .ibp-appbar-bell,
#ibp-app .ibp-appbar-avatar,
#ibp-app .ibp-mobile-tabbar,
#ibp-app .ibp-btn-icon,
#ibp-app .ibp-btn-text-short {
  display: none;
}


/* ═══════════════════════════════════════════════════════════
   LAPTOP — <= 1280px
   Sidebar still docked. Only relieve the densest grids.
═══════════════════════════════════════════════════════════ */
@media (max-width: 1280px) {
  #ibp-app #ibp-view-performance .ibp-stats {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}


/* ═══════════════════════════════════════════════════════════
   TABLET LANDSCAPE — <= 1024px
   The docked 240px sidebar leaves too little room for the
   content grids on an iPad, so it becomes an off-canvas
   drawer here and the main column reclaims the full width.
═══════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {

  /* ── Sidebar → BOTTOM SHEET ───────────────────────────
     "More" sits at the bottom-right of the tab bar, so a
     panel flying in from the far left breaks the link
     between control and result. Rising from the same edge
     the control lives on keeps that link intact.

     Markup is untouched: the same .is-open class drives it,
     so no JS changed. Selector list and !important mirror
     the base rule in portal.css, which is !important-heavy.

     It stops short of the tab bar so "More" stays visible
     and can toggle the sheet shut.                        */
  #ibp-app .ibp-sidebar,
  #ibp-app aside.ibp-sidebar,
  .ibp-app .ibp-sidebar {
    top: auto !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 56px;
    bottom: calc(56px + env(safe-area-inset-bottom, 0px));
    width: 100% !important;
    max-width: none !important;
    /* Replace min-height:100vh — a sheet is content-sized. */
    min-height: 0 !important;
    height: auto !important;
    max-height: 68vh;
    max-height: 68dvh;
    transform: translateY(100%);
    border-right: none !important;
    border-top: 1px solid var(--ibp-border) !important;
    border-radius: 18px 18px 0 0 !important;
    padding-top: 10px !important;
    box-shadow: none !important;
  }

  #ibp-app .ibp-sidebar.is-open,
  #ibp-app aside.ibp-sidebar.is-open {
    transform: translateY(0);
    box-shadow: 0 -8px 32px rgba(15, 23, 42, .16) !important;
  }

  /* Grab handle. portal.css kills this pseudo-element with
     !important, so it has to be re-enabled the same way. */
  #ibp-app .ibp-sidebar::before,
  #ibp-app aside.ibp-sidebar::before {
    display: block !important;
    content: "" !important;
    width: 40px !important;
    height: 4px !important;
    margin: 0 auto 8px !important;
    background: #cbd5e1 !important;
    border-radius: 999px !important;
    flex-shrink: 0 !important;
  }

  /* Brand is already in the app bar; reclaim the height. */
  #ibp-app .ibp-sidebar .ibp-brand {
    display: none !important;
  }

  /* flex:1 alone leaves min-height:auto, which would let the
     nav push past max-height instead of scrolling inside. */
  #ibp-app .ibp-sidebar .ibp-nav-wrap {
    min-height: 0 !important;
  }

  /* ── Scrim ───────────────────────────────────────────── */
  #ibp-app .ibp-sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 999; /* sidebar is 1000 */
    background: rgba(15, 23, 42, .45);
    opacity: 0;
    visibility: hidden;
    transition: opacity .25s ease, visibility .25s ease;
  }

  #ibp-app .ibp-sidebar-backdrop.is-visible {
    opacity: 1;
    visibility: visible;
  }

  /* Lock background scroll while the drawer is open. */
  body.ibp-sidebar-open {
    overflow: hidden;
  }

  /* ── Main column reclaims the gutter ─────────────────── */
  #ibp-app .ibp-main {
    margin-left: 0;
  }

  #ibp-app .ibp-menu-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    margin-left: -8px; /* optically align the glyph with the chip below */
    border-radius: 8px;
  }

  #ibp-app .ibp-menu-toggle:focus-visible {
    outline: 2px solid var(--ibp-blue);
    outline-offset: 2px;
  }

  /* ── Overflow safety net ──────────────────────────────
     overflow-x:clip (not hidden) so the sticky topbar keeps
     working — clip does not create a scroll container.    */
  #ibp-app .ibp-content,
  #ibp-app .ibp-view {
    max-width: 100%;
    overflow-x: clip;
  }

  #ibp-app .ibp-card,
  #ibp-app .ibp-col-left,
  #ibp-app .ibp-col-right {
    min-width: 0;
  }

  #ibp-app .ibp-content {
    padding: 24px;
  }

  /* ── Grid density ─────────────────────────────────────── */
  #ibp-app #ibp-view-performance .ibp-stats {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  #ibp-app #ibp-view-erp .ibp-erp-stats,
  #ibp-app #ibp-view-todo .ibp-todo-stats,
  #ibp-app #ibp-view-achievement .ibp-ach-stats {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  #ibp-app .ibp-members-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  /* Grid items default to min-width:auto, so an unbreakable
     string (a name that is actually an email) sets the column
     min-content and spills past the card edge. */
  #ibp-app .ibp-member-card {
    min-width: 0;
  }

  /* anywhere, not break-word: guarantees a break for strings
     with no natural opportunity, e.g. cdiesta@insboss.net.
     Safe here because the grid column is already 1fr. */
  #ibp-app .ibp-member-card__name,
  #ibp-app .ibp-member-card__position,
  #ibp-app .ibp-member-card__dept {
    overflow-wrap: anywhere;
    min-width: 0;
  }

  /* Was nowrap+ellipsis at a fixed 200px, which truncated
     mid-address inside a card narrower than that. */
  #ibp-app .ibp-member-card__dept {
    white-space: normal;
  }
}


/* ═══════════════════════════════════════════════════════════
   TABLET PORTRAIT — <= 900px
═══════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #ibp-app #ibp-view-performance .ibp-stats,
  #ibp-app #ibp-view-erp .ibp-erp-stats,
  #ibp-app #ibp-view-todo .ibp-todo-stats,
  #ibp-app #ibp-view-lms .ibp-lms-stats,
  #ibp-app #ibp-view-achievement .ibp-ach-stats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* Meetings toolbar: tab strip and count stop competing. */
  #ibp-app .ibp-um-toolbar {
    flex-wrap: wrap;
    gap: 10px;
  }

  #ibp-app #ibp-um-tabs {
    flex-wrap: wrap;
  }
}


/* ═══════════════════════════════════════════════════════════
   MOBILE LARGE — <= 768px
   Single column throughout. Multi-column data tables become
   labelled cards so nothing needs sideways dragging.
═══════════════════════════════════════════════════════════ */
@media (max-width: 768px) {

  /* ── Page + card chrome ──────────────────────────────── */
  #ibp-app .ibp-page-header {
    flex-direction: column;
    align-items: flex-start;
  }

  /* Meetings header keeps an action cluster pinned right on
     desktop via an inline margin-left:auto — unpin it. */
  #ibp-app #ibp-view-upcoming-meetings .ibp-page-header > div:last-child {
    margin-left: 0 !important;
    width: 100%;
    flex-wrap: wrap;
  }

  #ibp-app .ibp-card-head {
    flex-wrap: wrap;
  }

  /* ── Topbar action buttons wrap into even rows ────────── */
  #ibp-app .ibp-topbar-actions .ibp-btn {
    flex: 1 1 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    white-space: nowrap;
  }

  /* ── Forms go full width ─────────────────────────────── */
  #ibp-app .ibp-members-search-wrap {
    flex: 1 1 100%;
    min-width: 0;
    width: 100%;
  }

  #ibp-app .ibp-members-filter {
    width: 100%;
    min-width: 0;
  }

  #ibp-app #ibp-view-announcements .ibp-ann-search-form {
    flex-wrap: wrap !important;
  }

  #ibp-app #ibp-view-announcements .ibp-ann-search-input {
    flex: 1 1 100% !important;
  }

  #ibp-app #ibp-view-announcements .ibp-ann-search-btn {
    flex: 1 1 100% !important;
  }

  #ibp-view-faq .faq-search-wrap {
    max-width: none;
  }

  /* ── Subtask / agenda tables: labelled card rows ───────
     portal.css already flips these to blocks at this width;
     add card framing plus a label/value split so the
     stacked cells stay readable.                          */
  #ibp-app .ibp-st-table tr {
    padding: 12px 14px;
    margin-bottom: 10px;
    border: 1px solid var(--ibp-border);
    border-radius: 10px;
    background: var(--ibp-surface);
  }

  #ibp-app .ibp-st-table td {
    padding: 4px 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 10px;
  }

  #ibp-app .ibp-st-table td::before {
    flex: 0 0 92px;
    margin-bottom: 0;
    letter-spacing: .04em;
  }

  #ibp-app .ibp-st-table td:empty {
    display: none;
  }

  /* Horizontal scroll wrappers are redundant once stacked. */
  #ibp-app .ibp-st-table {
    min-width: 0;
  }

  /* ── Calendar widget: touch-sized day cells ──────────── */
  #ibp-app .ibp-cw-cell {
    min-height: 44px;
  }

  /* ── Modals fit the viewport ─────────────────────────── */
  .ibp-note-modal {
    padding: 16px;
  }

  .ibp-note-modal__dialog {
    max-height: calc(100vh - 32px);
    max-height: calc(100dvh - 32px);
  }

  .ibp-note-modal__head {
    padding: 14px 16px;
  }

  .ibp-note-modal__head h3 {
    font-size: 17px;
  }

  .ibp-note-modal__body {
    padding: 16px 16px 20px;
  }

  .ibp-member-modal {
    padding: 16px !important;
  }

  .ibp-member-modal__dialog {
    max-height: calc(100vh - 32px) !important;
    max-height: calc(100dvh - 32px) !important;
    overflow-y: auto !important;
  }

  /* ── Tab strip: hide the scrollbar, keep the swipe ────── */
  #ibp-app .ibp-tab-bar {
    scrollbar-width: none;
  }

  #ibp-app .ibp-tab-bar::-webkit-scrollbar {
    display: none;
  }

  /* ── Meetings table → labelled cards ──────────────────
     Inline style attributes on these cells force
     !important here. data-label values come from
     view-upcoming-meetings.php.                           */
  #ibp-app #ibp-um-table thead {
    display: none;
  }

  #ibp-app #ibp-um-table,
  #ibp-app #ibp-um-table tbody,
  #ibp-app #ibp-um-table tr,
  #ibp-app #ibp-um-table td {
    display: block;
    width: 100%;
  }

  #ibp-app #ibp-um-table tr {
    padding: 12px 14px !important;
    margin-bottom: 10px;
    border: 1px solid var(--ibp-border) !important;
    border-radius: 10px;
    background: var(--ibp-surface);
  }

  #ibp-app #ibp-um-table td {
    padding: 4px 0 !important;
    min-width: 0 !important;
    white-space: normal !important;
    text-align: left !important;
    border: none !important;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 10px;
  }

  #ibp-app #ibp-um-table td::before {
    content: attr(data-label);
    flex: 0 0 92px;
    font-size: 10px;
    font-weight: 600;
    /* Literal #64748b, not --ibp-muted, so these labels match the
       existing .ibp-st-table td::before rule in portal.css exactly. */
    color: #64748b;
    text-transform: uppercase;
    letter-spacing: .04em;
  }

  /* Actions cell: full-width buttons, no label. */
  #ibp-app #ibp-um-table td[data-label="Actions"]::before {
    display: none;
  }

  #ibp-app #ibp-um-table td[data-label="Actions"] > div {
    justify-content: flex-start !important;
    flex-wrap: wrap;
    width: 100%;
    padding-top: 6px;
  }

  /* The stacked table no longer needs its scroll container. */
  #ibp-app #ibp-view-upcoming-meetings .ibp-card > div[style*="overflow-x"] {
    overflow-x: visible !important;
  }
}


/* ═══════════════════════════════════════════════════════════
   MOBILE MEDIUM — <= 480px
═══════════════════════════════════════════════════════════ */
@media (max-width: 480px) {
  #ibp-app .ibp-content {
    padding: 12px;
  }

  #ibp-app .ibp-hero-banner,
  #ibp-app .ibp-hero-banner__content {
    min-height: 0;
  }

  #ibp-app .ibp-hero-banner__content {
    padding: 20px 16px;
  }

  #ibp-app .ibp-hero-banner__title {
    font-size: 24px;
  }

  #ibp-app .ibp-page-header h1 {
    font-size: 20px;
  }

  /* Long unbroken strings (emails, URLs, IDs) must not push
     the layout wider than the screen. */
  /* break-word, not anywhere: `anywhere` participates in
     min-content sizing, which is what allowed flexbox to
     crush headings into one-word-per-line columns. */
  #ibp-app .ibp-page-header h1,
  #ibp-app .ibp-card-head h3,
  #ibp-app .ibp-list-title,
  #ibp-app .ibp-list-desc,
  #ibp-app .ibp-stat-label,
  #ibp-app .ibp-stat-sub {
    overflow-wrap: break-word;
  }

  #ibp-app .ibp-topbar-actions .ibp-btn {
    flex: 1 1 calc(50% - 4px);
  }

  #ibp-app .ibp-app-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  #ibp-app .ibp-status-list,
  #ibp-app .ibp-stack {
    padding: 16px;
  }

  #ibp-app .ibp-list-item {
    padding: 12px 16px;
  }

  #ibp-app .ibp-card-head {
    padding: 14px 16px;
  }

  #ibp-app .ibp-members-pagination {
    margin-top: 16px;
    padding: 8px 0;
  }

  #ibp-view-faq #ibp-faq-cat-grid {
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  }

  #ibp-view-faq .ibp-faq-question {
    padding: 14px 16px;
    font-size: 14px;
  }

  #ibp-view-faq .ibp-faq-item.is-open > .ibp-faq-answer {
    padding: 4px 16px 18px 19px;
  }

  /* Member modal: revive the compact treatment that the
     later global !important block in portal.css was
     accidentally overriding. */
  .ibp-member-modal__dialog {
    width: calc(100% - 24px) !important;
    border-radius: 14px !important;
  }

  .ibp-member-modal__top {
    height: 60px !important;
  }

  .ibp-member-modal__profile {
    margin-top: -32px !important;
    padding: 0 18px !important;
  }

  .ibp-member-modal__avatar-wrap {
    width: 64px !important;
    height: 64px !important;
  }

  .ibp-member-modal__avatar {
    font-size: 20px !important;
  }

  .ibp-member-modal__name {
    font-size: 18px !important;
  }

  .ibp-member-modal__body {
    padding: 14px 18px 20px !important;
  }

  /* Label/value rows stack rather than squeeze. */
  .ibp-mm-row {
    flex-wrap: wrap !important;
    gap: 2px 10px !important;
  }

  .ibp-mm-value {
    text-align: left !important;
    overflow-wrap: anywhere;
  }

  .ibp-note-modal {
    padding: 12px;
  }

  .ibp-note-modal__dialog {
    max-height: calc(100vh - 24px);
    max-height: calc(100dvh - 24px);
    border-radius: 14px;
  }

  .ibp-note-modal__content {
    padding: 14px;
  }

  /* Stacked table labels stop reserving a fixed column. */
  #ibp-app .ibp-st-table td::before,
  #ibp-app #ibp-um-table td::before {
    flex: 0 0 100%;
  }
}


/* ═══════════════════════════════════════════════════════════
   MOBILE SMALL — <= 380px
═══════════════════════════════════════════════════════════ */
@media (max-width: 380px) {
  #ibp-app .ibp-content {
    padding: 10px;
  }

  #ibp-app .ibp-stats {
    grid-template-columns: 1fr;
  }

  #ibp-app #ibp-view-performance .ibp-stats,
  #ibp-app #ibp-view-erp .ibp-erp-stats,
  #ibp-app #ibp-view-todo .ibp-todo-stats,
  #ibp-app #ibp-view-lms .ibp-lms-stats,
  #ibp-app #ibp-view-achievement .ibp-ach-stats {
    grid-template-columns: 1fr;
  }

  #ibp-app .ibp-app-grid {
    grid-template-columns: 1fr;
  }

  #ibp-app .ibp-topbar-actions .ibp-btn {
    flex: 1 1 100%;
  }

  #ibp-app .ibp-hero-banner__title {
    font-size: 22px;
  }

  #ibp-app .ibp-topbar-left {
    gap: 8px;
  }

  /* Breadcrumb separator eats width it cannot afford. */
  #ibp-app .ibp-crumb + .ibp-crumb::before {
    margin-right: 8px;
  }
}


/* ═══════════════════════════════════════════════════════════
   TOUCH INPUT — width independent
   Scoped to coarse pointers so hit areas grow on phones and
   tablets while mouse-driven desktop metrics stay exact.
═══════════════════════════════════════════════════════════ */
@media (hover: none) and (pointer: coarse) {

  /* 44x44 minimum per WCAG 2.5.8 / platform HIG. */
  #ibp-app .ibp-nav-item,
  #ibp-app button.ibp-nav-item,
  #ibp-app a.ibp-nav-item {
    min-height: 44px !important;
  }

  #ibp-app .ibp-nav-chevron {
    width: 32px !important;
    height: 32px !important;
  }

  #ibp-app .ibp-btn,
  #ibp-app .ibp-page-btn,
  #ibp-app .ibp-tab-bar button,
  #ibp-app .ibp-tab-bar .ibp-tab-btn {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  #ibp-app .ibp-page-num {
    min-width: 44px;
    height: 44px;
  }

  #ibp-app .ibp-um-tab {
    min-height: 44px !important;
    padding: 10px 16px !important;
  }

  #ibp-app .ibp-nudge-filter,
  #ibp-view-faq .ibp-faq-cat-card {
    min-height: 44px;
  }

  #ibp-app .ibp-note-modal__close,
  .ibp-note-modal__close {
    width: 44px;
    height: 44px;
  }

  #ibp-app .ibp-member-modal__close,
  .ibp-member-modal__close {
    width: 44px !important;
    height: 44px !important;
  }

  #ibp-app .ibp-appbar-avatar {
    width: 44px !important;
    height: 44px !important;
  }

  #ibp-app .ibp-members-search,
  #ibp-app .ibp-members-filter {
    min-height: 44px;
  }

  #ibp-app #ibp-cw-prev,
  #ibp-app #ibp-cw-next {
    min-width: 44px !important;
    min-height: 44px !important;
  }

  /* Meeting join / open links inside stacked table rows. */
  #ibp-app #ibp-um-table td a {
    min-height: 40px;
    padding-top: 8px !important;
    padding-bottom: 8px !important;
  }

  /* iOS Safari zooms the whole page when a focused input is
     under 16px. Applies to text entry only, so display
     typography is untouched. */
  #ibp-app input[type="text"],
  #ibp-app input[type="search"],
  #ibp-app input[type="email"],
  #ibp-app input[type="tel"],
  #ibp-app input[type="number"],
  #ibp-app input[type="date"],
  #ibp-app select,
  #ibp-app textarea,
  #ibp-view-faq .faq-search-wrap input {
    font-size: 16px;
  }
}


/* ═══════════════════════════════════════════════════════════
   REDUCED MOTION
   The drawer slide is the only motion this layer introduces.
═══════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  #ibp-app .ibp-sidebar,
  #ibp-app aside.ibp-sidebar,
  .ibp-app .ibp-sidebar {
    transition: none !important;
  }

  #ibp-app .ibp-sidebar-backdrop {
    transition: none;
  }
}


/* ═══════════════════════════════════════════════════════════
   MOBILE / TABLET SHELL  — <= 1024px
   Implements the approved mobile reference: compact white app
   bar, bottom tab bar, no hamburger.

   The app bar is the SAME element as the desktop blue banner
   (.ibp-main-header), restyled — not a parallel component.
   That matters because the banner hosts the
   [insboss_pass_widget] and [user_profile_dropdown]
   shortcodes; hiding it and rebuilding would have dropped
   both third-party widgets or duplicated their DOM ids.
═══════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {

  /* The desktop -26px pull exists to tuck the banner under the
     theme header; with a sticky app bar it just hides it. */
  body.page #ibp-app,
  body.buddypress #ibp-app,
  #ibp-app {
    margin-top: 0;
  }

  /* ── Blue banner → white app bar ─────────────────────── */
  #ibp-app .ibp-main-header {
    margin-top: 0;
    min-height: 56px;
    background: #ffffff;
    border-bottom: 1px solid var(--ibp-border);
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    box-shadow: none;
    position: sticky;
    top: 0;
    z-index: 40;
  }

  /* The gradient wash only makes sense over the photo. */
  #ibp-app .ibp-main-header__overlay {
    display: none;
  }

  #ibp-app .ibp-main-header__content {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    min-height: 56px;
    gap: 10px;
    padding: 8px 14px;
    /* Inherited from the desktop banner rule; meaningless in
       grid and a contributor to the collapse under flex. */
    justify-content: normal;
  }

  /* Retired in the app bar: it stole row space from the brand
     and forced the ellipsis. __right pins itself with an auto
     margin, so no spacer is needed. */
  #ibp-app .ibp-main-header__spacer {
    display: none;
  }

  /* Widget slots must yield to the brand, not force it to wrap. */
  #ibp-app .ibp-main-header__apps,
  #ibp-app .ibp-main-header__profile {
    min-width: 0;
    flex-shrink: 1;
  }

  /* Drop the desktop drop-shadow filter — it was tuned for
     light-on-dark over the banner image. */
  #ibp-app .ibp-main-header__apps > *,
  #ibp-app .ibp-main-header__profile > * {
    filter: none;
  }

  #ibp-app .ibp-main-header__right {
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    /* Grid column 2 places this; an auto margin here fought
       the brand for free space. */
    margin-left: 0;
    min-width: 0;
  }

  /* Dots glyph was white-on-blue; needs to read on white now. */
  #ibp-app .ibp-main-header .ibpw-dots svg {
    fill: var(--ibp-mid) !important;
    color: var(--ibp-mid) !important;
    opacity: 1 !important;
  }

  /* ── App-bar brand ─────────────────────────────────────
     Grid column 1, so it always receives the leftover width.
     The name only ellipsises if it truly cannot fit. */
  #ibp-app .ibp-appbar-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
  }

  #ibp-app .ibp-appbar-brand-icon {
    flex: 0 0 auto;
  }

  /* Must be able to claim the row and must never reach 0. */
  #ibp-app .ibp-appbar-brand-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
  }

  #ibp-app .ibp-appbar-brand-icon {
    width: 34px;
    height: 34px;
    border-radius: 9px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--ibp-blue);
    color: #fff;
    font-size: 13px;
    font-weight: 700;
    flex-shrink: 0;
  }

  #ibp-app .ibp-appbar-brand-text {
    min-width: 0;
  }

  #ibp-app .ibp-appbar-brand-name,
  #ibp-app .ibp-appbar-brand-sub {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  #ibp-app .ibp-appbar-brand-name {
    font-size: 15px;
    font-weight: 700;
    color: var(--ibp-text);
    line-height: 1.25;
  }

  #ibp-app .ibp-appbar-brand-sub {
    font-size: 11px;
    color: var(--ibp-muted);
    line-height: 1.25;
  }

  /* ── Notification bell ───────────────────────────────── */
  #ibp-app .ibp-appbar-bell {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 44px;
    height: 44px;
    border: none;
    background: transparent;
    color: var(--ibp-mid);
    cursor: pointer;
    padding: 0;
    border-radius: 999px;
    flex-shrink: 0;
  }

  #ibp-app .ibp-appbar-bell:focus-visible {
    outline: 2px solid var(--ibp-blue);
    outline-offset: 2px;
  }

  #ibp-app .ibp-appbar-bell-badge {
    position: absolute;
    top: 3px;
    right: 2px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    /* Same red the sidebar unread badge already uses. */
    background: var(--ibp-red);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    box-shadow: 0 0 0 2px #fff;
  }

  /* ── Static profile avatar (fallback only) ───────────── */
  #ibp-app .ibp-appbar-avatar {
    display: inline-flex;
    position: relative;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: none;
    border-radius: 999px;
    /* Same tint pair the sidebar user avatar already uses. */
    background: var(--ibp-blue-lt);
    color: var(--ibp-blue);
    font-size: 14px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    overflow: hidden;
    flex-shrink: 0;
  }

  #ibp-app .ibp-appbar-avatar:focus-visible {
    outline: 2px solid var(--ibp-blue);
    outline-offset: 2px;
  }

  /* Layered over the initial; hides itself via onerror if the
     avatar service is unreachable, leaving the initial. */
  #ibp-app .ibp-appbar-avatar-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 999px;
    display: block;
    max-width: none;
    border: none;
  }

  /* ── Topbar action card ──────────────────────────────── */
  #ibp-app .ibp-topbar {
    flex-direction: column;
    align-items: stretch;
    height: auto;
    margin: 12px 12px 0;
    padding: 12px;
    gap: 10px;
    /* Was sticky at top:60px, which assumed a fixed app-bar
       height. Two stacked sticky bars clipped this card, and
       an action card does not need to follow the scroll. */
    position: static;
    background: var(--ibp-surface);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  #ibp-app .ibp-topbar-left {
    gap: 10px;
  }

  /* Breadcrumbs are redundant — the page header states the
     department directly. */
  #ibp-app .ibp-crumb {
    display: none;
  }

  #ibp-app .ibp-topbar-actions {
    width: 100%;
    justify-content: center;
    gap: 8px;
  }

  /* Icon + short label in, long label out. */
  #ibp-app .ibp-btn-icon {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
  }

  #ibp-app .ibp-btn-text-short {
    display: inline;
  }

  #ibp-app .ibp-btn-text-full {
    display: none;
  }

  #ibp-app .ibp-topbar-actions .ibp-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    min-height: 44px;
    padding: 10px 14px !important;
    white-space: nowrap;
  }

  /* ── Bottom tab bar ──────────────────────────────────── */
  #ibp-app .ibp-mobile-tabbar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Above the scrim (999) and the sheet (1000) so the bar
       stays visible and "More" can close what it opened. */
    z-index: 1001;
    background: var(--ibp-surface);
    border-top: 1px solid var(--ibp-border);
    padding-bottom: 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-shadow: 0 -2px 12px rgba(15, 23, 42, .05);
  }

  #ibp-app .ibp-tabbar-item {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-height: 56px;
    padding: 7px 2px;
    border: none;
    background: transparent;
    color: var(--ibp-muted);
    font-size: 10px;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  #ibp-app .ibp-tabbar-item.is-active {
    color: var(--ibp-blue);
  }

  #ibp-app .ibp-tabbar-item:focus-visible {
    outline: 2px solid var(--ibp-blue);
    outline-offset: -2px;
  }

  #ibp-app .ibp-tabbar-label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Clear the fixed bar so the last card is never trapped
     under it or under the iOS home indicator.
     Plain value first: if a browser lacks env() the calc() is
     invalid and would be dropped entirely, leaving the last
     card unreachable behind the bar. */
  #ibp-app .ibp-content {
    padding-bottom: 76px;
    padding-bottom: calc(76px + env(safe-area-inset-bottom, 0px));
  }

  /* Drawer must clear the tab bar too. */
  #ibp-app .ibp-sidebar,
  #ibp-app aside.ibp-sidebar,
  .ibp-app .ibp-sidebar {
    z-index: 1000 !important;
  }

  /* ── Page header: title left, meta right ───────────────
     Meta must be allowed to shrink AND to wrap onto its own
     line. With flex-shrink:0 a long title was crushed to a
     ~100px column and broke mid-word.                     */
  #ibp-app .ibp-page-header {
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 8px 14px;
  }

  /* Basis, not width: the title keeps a sane minimum, and
     the meta wraps below rather than squeezing it. */
  #ibp-app .ibp-page-header > h1,
  #ibp-app .ibp-page-header > div:first-child {
    flex: 1 1 220px;
    min-width: 0;
  }

  #ibp-app .ibp-page-meta {
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
    flex: 0 1 auto;
    min-width: 0;
    margin-left: auto;
    text-align: right;
  }

  /* ── .ibp-card-head used as a vertical container ───────
     view-reading.php nests a KPI strip and a progress bar
     inside .ibp-card-head, but portal.css styles that as a
     horizontal row with align-items:center. The progress
     row therefore collapsed to min-content and rendered
     "Overall Progress0%" with a dead gap above it.
     Scoped to this view so ordinary card heads stay rows. */
  #ibp-app #ibp-view-reading .ibp-card > .ibp-card-head {
    flex-direction: column;
    align-items: stretch;
  }

  /* ── "View All" reads as a pill, per the reference ────── */
  #ibp-app .ibp-card-head .ibp-btn-ghost {
    background: var(--ibp-blue);
    color: #fff;
    border: none;
    border-radius: 8px;
    min-height: 36px;
    padding: 8px 16px;
    font-size: 12px !important;
    font-weight: 600;
    flex-shrink: 0;
  }
}


/* ═══════════════════════════════════════════════════════════
   MOBILE SHELL — <= 768px
   Stat strip becomes a snap-scrolling carousel, per the
   reference. This is a deliberate, approved exception to the
   "no horizontal scrolling" rule; it is opt-in per component,
   not a page-level overflow.
═══════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
  #ibp-app .ibp-stats,
  #ibp-app #ibp-view-performance .ibp-stats,
  #ibp-app #ibp-view-erp .ibp-erp-stats,
  #ibp-app #ibp-view-todo .ibp-todo-stats,
  #ibp-app #ibp-view-lms .ibp-lms-stats,
  #ibp-app #ibp-view-achievement .ibp-ach-stats {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    /* proximity, not mandatory: mandatory can force a scroll
       shift on load that swallows the leading inset. */
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* No negative-margin bleed. .ibp-view sets overflow-x:clip
       and has no padding, so a bleed is clipped away and the
       first card loses its left inset — it reads as cut off.
       The strip stays inside the content box and aligns with
       every other card on the page; the partially visible next
       card is the scroll affordance. */
    margin-left: 0;
    margin-right: 0;
    padding: 2px 0 4px;
    gap: 12px;
  }

  #ibp-app .ibp-stats::-webkit-scrollbar,
  #ibp-app #ibp-view-performance .ibp-stats::-webkit-scrollbar {
    display: none;
  }

  #ibp-app .ibp-stats .ibp-stat-card {
    flex: 0 0 clamp(160px, 44vw, 200px);
    scroll-snap-align: start;
  }

  /* Page header stacks once the title needs the full width. */
  #ibp-app .ibp-page-header {
    flex-direction: row;
  }

  #ibp-app .ibp-page-header > div:first-child,
  #ibp-app .ibp-page-header h1 {
    min-width: 0;
  }
}


/* ═══════════════════════════════════════════════════════════
   MOBILE SHELL — <= 380px
═══════════════════════════════════════════════════════════ */
@media (max-width: 380px) {
  #ibp-app .ibp-content {
    padding-left: 10px;
    padding-right: 10px;
  }

  #ibp-app .ibp-appbar-brand-sub {
    display: none;
  }

  #ibp-app .ibp-tabbar-item {
    font-size: 9px;
  }
}


/* ═══════════════════════════════════════════════════════════
   THIRD-PARTY FLOATING BUTTONS — <= 1024px
   A theme/Elementor scroll-to-top button sits bottom-right,
   exactly where "More" now lives. Lift it clear of the bar.
   Broad selector because the emitting plugin varies; it only
   moves `bottom`, which is safe for any fixed-position FAB.
═══════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {
  .elementor-scroll-to-top,
  [class*="scroll-to-top"],
  [id*="scroll-to-top"],
  [class*="back-to-top"],
  [id*="back-to-top"],
  [class*="scroll-top"],
  [id*="scroll-top"] {
    bottom: 70px !important;
    bottom: calc(70px + env(safe-area-inset-bottom, 0px)) !important;
    z-index: 997 !important;
  }
}


/* ═══════════════════════════════════════════════════════════
   WORDPRESS ADMIN BAR OFFSET
   #wpadminbar is fixed at 32px (>782px) and 46px (601-782px),
   and becomes absolute at <=600px. The sticky app bar has to
   account for it or it hides underneath.
═══════════════════════════════════════════════════════════ */
@media (max-width: 1024px) and (min-width: 783px) {
  body.admin-bar #ibp-app .ibp-main-header {
    top: 32px;
  }
}

@media (max-width: 782px) and (min-width: 601px) {
  body.admin-bar #ibp-app .ibp-main-header {
    top: 46px;
  }
}


/* ═══════════════════════════════════════════════════════════
   COMPACT HEADER — mobile & tablet                    (1.26.2)

   The topbar card was stacking: chip on one row, then the six
   action buttons wrapping below it. The earlier breakpoints grow
   the buttons to fill each row —

     <=768px  .ibp-btn { flex: 1 1 auto }
     <=480px  .ibp-btn { flex: 1 1 calc(50% - 4px) }   2 per row
     <=380px  .ibp-btn { flex: 1 1 100% }              1 per row

   so six buttons became three rows at 390px and the header ran to
   283px tall before any content appeared.

   This block puts the chip and the actions back on ONE row and
   makes the actions a single horizontally scrollable strip. Buttons
   keep their natural width instead of growing, and keep the 44px
   touch target. Appended last on purpose: the selectors match the
   earlier ones exactly, so source order decides.

   Desktop (>1024px) is untouched.
   ═══════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {

  #ibp-app .ibp-topbar {
    flex-direction: row;
    align-items: center;
    gap: 10px;
    padding: 6px 10px;
    margin: 10px 12px 0;
  }

  #ibp-app .ibp-topbar-left {
    flex: 0 0 auto;
    gap: 8px;
  }

  #ibp-app .ibp-topbar-actions {
    width: auto;
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: nowrap;
    justify-content: flex-start;
    gap: 8px;
    /* 3px of vertical room so the focus ring is not clipped by the
       scroll container (overflow-x:auto forces overflow-y to auto). */
    padding: 3px 0;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  #ibp-app .ibp-topbar-actions::-webkit-scrollbar {
    display: none;
  }

  #ibp-app .ibp-topbar-actions .ibp-btn {
    /* Natural width — never grow to fill the row. This is the line
       that stops the wrapping. */
    flex: 0 0 auto;
    min-height: 44px;
    padding: 8px 12px !important;
  }
}


/* ═══════════════════════════════════════════════════════════
   HIDE THE PLUGIN'S NUDGE BELL ON MOBILE/TABLET       (1.26.3)

   Notifications are handled by a separate notification plugin,
   whose bell also renders in this app bar — so below 1024px two
   bells appeared side by side.

   The plugin's own bell (.ibp-appbar-bell, data-view="nudge") is
   hidden by default at line 38 and only switched on inside the
   <=1024px block at line 920. This turns that back off.

   Nudge Inbox is NOT stranded: the bottom tab bar's "More" button
   opens the sidebar as a sheet, and that nav has its own
   "Nudge Inbox" item (sidebar.php line 98).

   display:none also removes it from the accessibility tree, so
   screen readers will not announce a button that is not there.

   TO RESTORE: delete this block.
   ═══════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {

  #ibp-app .ibp-appbar-bell,
  #ibp-app .ibp-appbar-bell-badge {
    display: none !important;
  }
}
