/* ==========================================================================
   General Shared Styles
   ========================================================================== */

/*-------- Google Fonts Import -------- */
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap');

/* ---- Reset & Base ---- */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
    font-family: 'Manrope', sans-serif;
    /* font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; */
    font-size: 14px;
    line-height: 1.5;
    color: #1a1a2e;
    background: #f0f2f5;
    /* Full-width shell: the topbar/footer/QA banner are flex children of <body>,
       so the body must span the viewport at any zoom level. Content width is
       capped/centered by inner wrappers (.page-wrapper/.page-wrap = 1500px,
       .app-footer-inner = 1140px), not by the body. */
    width: 100%;
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

/* ---- Topbar ---- */
.topbar {
    background: #1d1d1b;
    height: 56px;
    display: flex;
    align-items: center;
    padding: 0 24px;
    border-bottom: 3px solid #0f3460;
    position: sticky;
    top: 0;
    z-index: 100;
}
.topbar-back {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 13px;
    color: #999;
    text-decoration: none;
    margin-right: 18px;
    padding-right: 18px;
    border-right: 1px solid rgba(255,255,255,.12);
}
.topbar-back:hover { color: #fff; }
.topbar-back svg { opacity: .7; }
.topbar-brand {
    font-size: 15px;
    font-weight: 600;
    color: #e0e0e0;
    letter-spacing: .3px;
}
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }

/* ---- User Dropdown ---- */
.user-dropdown { position: relative; display: inline-block; }
.user-btn {
    background: none;
    border: 1px solid rgba(255,255,255,.15);
    border-radius: 4px;
    padding: 7px 14px;
    font-size: 13px;
    color: #ccc;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 8px;
}
.user-btn:hover { background: rgba(255,255,255,.08); color: #fff; }
.user-btn svg { opacity: .7; }
.user-name { text-transform: uppercase; color: #fff; }
.caret { font-size: 9px; color: rgba(255,255,255,.4); }

/* Styled to echo the left main-menu drawer: rounded icon chips, item titles,
   chevrons and the same hover treatment (see .menu-drawer-item). */
.dropdown-menu {
    display: none;
    position: absolute;
    right: 0;
    top: calc(100% + 6px);
    background: #fff;
    border: 1px solid #e3e7ec;
    border-radius: 12px;
    list-style: none;
    min-width: 252px;
    padding: 7px;
    margin: 0;
    z-index: 1000;
    box-shadow: 0 14px 34px rgba(15,28,54,.18);
}
.dropdown-menu.open { display: block; }
.dropdown-menu li { list-style: none; }
.dropdown-head {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 7px 10px 10px;
    margin-bottom: 5px;
    border-bottom: 1px solid #e8ebef;
}
.dropdown-head-eyebrow {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .8px;
    color: #8a909a;
}
.dropdown-head-name {
    font-size: 13.5px;
    font-weight: 700;
    color: #1a1a2e;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Secondary line under the full name: the username/display name, styled to read
   as quieter metadata (lighter weight + muted color) so it's clearly distinct
   from the bold full name above it. */
.dropdown-head-user {
    margin-top: 1px;
    font-size: 12px;
    font-weight: 500;
    color: #707682;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.dropdown-menu li a {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 8px 9px;
    border-radius: 9px;
    color: #1a1a2e;
    text-decoration: none;
    font-size: 13px;
}
.dropdown-menu li a:hover { background: #f2f6fb; }
.dropdown-ico {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 9px;
    background: #f0f4ff;
    color: #0f3460;
}
.dropdown-label { flex: 1 1 auto; min-width: 0; font-weight: 600; }
.dropdown-chev {
    flex-shrink: 0;
    display: inline-flex;
    color: #c2c8cf;
    transition: color .15s, transform .15s;
}
.dropdown-menu li a:hover .dropdown-chev { color: #0f3460; transform: translateX(2px); }
.dropdown-menu .divider { background: #e8ebef; margin: 5px 8px; }
.dropdown-menu li a.dropdown-item--danger:hover { background: #fdf2f2; }
.dropdown-menu li a.dropdown-item--danger:hover .dropdown-ico { background: #fde8e8; color: #c0392b; }
.dropdown-menu li a.dropdown-item--danger:hover .dropdown-chev { color: #c0392b; }
.divider { height: 1px; background: #eee; }

/* ---- Page Wrapper ---- */
.page-wrapper {
    width: 100%;
    max-width: 1500px;
    margin-left: auto;
    margin-right: auto;
    padding: 32px 20px 0 24px;
    flex: 1;
}
.page-wrap {
    width: 100%;
    max-width: 1500px;
    margin-left: auto;
    margin-right: auto;
    flex: 1;
    padding: 32px 20px 0 24px;
}

/* ---- Page Header ---- */
.page-header { margin-bottom: 24px; }
.page-title {
    font-size: 22px;
    font-weight: 700;
    color: #1a1a2e;
    margin: 0 0 6px 0;
}
.page-info {
    font-size: 13px;
    color: #666;
    line-height: 1.6;
}

/* ---- Page Hero ---- */
.page-hero {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    padding: 20px 24px;
    background: #f7f8fa;
    border: 1px solid #e8e8e8;
    border-radius: 10px;
    margin-bottom: 26px;
}
.page-hero-icon {
    flex-shrink: 0;
    width: 46px;
    height: 46px;
    border-radius: 10px;
    background: #fdeee8;
    color: #e8451c;
    display: flex;
    align-items: center;
    justify-content: center;
}
.page-hero-text { flex: 1; min-width: 0; }
.page-hero-title {
    font-size: 22px;
    font-weight: 700;
    margin: 0 0 4px 0;
    color: #1a1a2e;
}
.page-hero-desc {
    font-size: 13px;
    line-height: 1.6;
    color: #666;
}
.page-hero-desc p { margin: 0 0 6px 0; }
.page-hero-desc p:last-child { margin-bottom: 0; }
.page-hero-desc a { color: #e8451c; }
.page-hero-aside { flex-shrink: 0; align-self: center; }

@media (max-width: 640px) {
    .page-hero {
        flex-wrap: wrap;
        padding: 16px 18px;
        gap: 12px;
    }
    .page-hero-aside { flex: 1 1 100%; align-self: stretch; }
}

/* ---- Buttons ---- */
.btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 6px 14px;
    border-radius: 5px;
    font-size: 12px;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    border: none;
    line-height: 1.4;
    white-space: nowrap;
    letter-spacing: .2px;
}
.btn-sm { padding: 5px 12px; font-size: 12px; }
.btn-primary { background: #0f3460; color: #fff; }
.btn-primary:hover { background: #1a4a7a; }
.btn-warning { background: #f39c12; color: #fff; }
.btn-warning:hover { background: #d68910; }
.btn-success { background: #27ae60; color: #fff; }
.btn-success:hover { background: #219a52; }
.btn-info { background: #2980b9; color: #fff; }
.btn-info:hover { background: #2471a3; }
.btn-link {
    background: none;
    color: #0f3460;
    padding: 5px 6px;
    font-weight: 500;
    border: none;
}
.btn-link:hover { text-decoration: underline; }
.btn-secondary {
    background: #f0f0f0;
    color: #333;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 6px 16px;
    font-size: 13px;
    cursor: pointer;
    font-weight: 500;
}
.btn-secondary:hover {
    background: #e4e4e4;
}
/* Standard "View/Details" action button for record cards (list + grid cards).
   Outline style; goes full-width inside a mobile card (see the 700px rules). */
.btn-card-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    font-size: 12px;
    padding: 6px 14px;
    border-radius: 4px;
    background: #fff;
    color: #0f3460;
    border: 1px solid #0f3460;
    cursor: pointer;
    text-decoration: none;
}
.btn-card-action:hover { background: #0f3460; color: #fff; }
.btn-outline {
    padding: 8px 18px;
    font-size: 13px;
    font-weight: 500;
    color: #555;
    background: #fff;
    border: 1px solid #ccc;
    border-radius: 5px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.btn-outline:hover {
    background: #f5f7fa;
    border-color: #999;
    color: #333;
}
/* Shared disabled / pending button state. `.btn-pending` keeps the element
   clickable (so handlers can explain why it's not ready) while looking inactive;
   `:disabled` is the hard-disabled state used during in-flight requests. */
.btn:disabled,
.btn.btn-pending {
    opacity: .45;
    cursor: not-allowed;
    box-shadow: none;
}

/* ---- Back Link ---- */
.back-link {
    font-size: 13px;
    color: #0f3460;
    text-decoration: none;
    font-weight: 500;
}
.back-link:hover { text-decoration: underline; }

/* ---- Desktop/Mobile Visibility ---- */
.mobile-only { display: none; }
.desktop-only { display: inline; }

/* ---- Empty State ---- */
.empty-state {
    text-align: center;
    padding: 80px 24px;
    color: #888;
    font-size: 15px;
    background: #fff;
    border-radius: 8px;
    border: 1px solid #d5d5d5;
}
.empty-state-icon { margin-bottom: 16px; }
.empty-state-icon svg { display: block; margin: 0 auto; }
.empty-state h3 {
    font-size: 17px;
    color: #555;
    font-weight: 600;
    margin-bottom: 8px;
}
.empty-state p { max-width: 420px; margin: 0 auto; line-height: 1.6; color: #888; font-size: 14px; }

/* ---- Collapsible Sections ---- */
.detail-sections { margin-bottom: 48px; }

.detail-section {
    background: #fff;
    border: 1px solid #d5d5d5;
    border-radius: 8px;
    margin-bottom: 12px;
    overflow: hidden;
}

.section-heading {
    display: flex;
    align-items: center;
    padding: 14px 20px;
    font-size: 14px;
    font-weight: 700;
    color: #1a1a2e;
    cursor: pointer;
    user-select: none;
    list-style: none;
    border-left: 4px solid #0f3460;
    background: #fafbfc;
}
.section-heading::-webkit-details-marker { display: none; }
.section-heading::before {
    content: "\25B6";
    font-size: 9px;
    color: #999;
    margin-right: 10px;
    transition: transform 0.15s ease;
    display: inline-block;
}
details[open] > .section-heading::before {
    transform: rotate(90deg);
}
.section-heading:hover { background: #f0f2f5; }

.section-body { padding: 16px 20px 20px; }

/* ---- Subsection Heading ---- */
.subsection-heading {
    font-size: 14px;
    font-weight: 700;
    color: #0f3460;
    margin: 20px 0 12px;
    padding-bottom: 6px;
    border-bottom: 1px solid #e0e0e0;
}

/* ---- Field Table ---- */
.field-table {
    width: 100%;
    border-collapse: collapse;
}
.field-table tr { border-bottom: 1px solid #f4f4f4; }
.field-table tr:last-child { border-bottom: none; }
.field-table td {
    padding: 7px 8px;
    vertical-align: top;
    font-size: 13px;
}
.field-table td.lbl {
    font-weight: 600;
    color: #555;
    width: 260px;
    white-space: nowrap;
}
.field-table td.val { color: #222; }

/* ---- Section Note ---- */
.section-note {
    background: #f8f9fb;
    border-left: 3px solid #0f3460;
    padding: 10px 14px;
    margin-bottom: 14px;
    font-size: 13px;
    color: #555;
    line-height: 1.5;
}
.section-note p { margin: 0 0 6px 0; }
.section-note p:last-child { margin-bottom: 0; }
.section-note a { color: #0f3460; }

/* ---- File Link ---- */
.file-link {
    color: #0f3460;
    text-decoration: none;
    font-weight: 500;
}
.file-link:hover { text-decoration: underline; }

/* ---- Form System ---- */
.form-row {
    margin-bottom: 16px;
}
.form-label {
    display: block;
    font-size: 13px;
    font-weight: 600;
    color: #444;
    margin-bottom: 5px;
}
.req-star {
    color: #c0392b;
    font-weight: 700;
}

.form-field {
    display: block;
    width: 100%;
    max-width: 520px;
    padding: 8px 12px;
    font-size: 13px;
    color: #222;
    background: #fff;
    border: 1px solid #ccc;
    border-radius: 4px;
    line-height: 1.4;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
}
.form-field:focus {
    outline: none;
    border-color: #0f3460;
}
.form-field[readonly] {
    background: #f5f6f8;
    color: #666;
}
select.form-field {
    max-width: 520px;
    padding-right: 28px;
    background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
}
.form-textarea {
    resize: vertical;
    min-height: 60px;
}
.form-value-readonly {
    font-size: 13px;
    color: #555;
    padding: 8px 0;
    line-height: 1.4;
}
.form-help-text {
    font-size: 12px;
    color: #888;
    margin-top: 4px;
    line-height: 1.4;
}

/* ---- Info tooltip (reusable label/field affordance) ----
   Discoverable "(i)" icon that reveals an explanatory bubble on hover, keyboard
   focus, or tap. Markup:
     <span class="app-tip" tabindex="0" role="note" aria-label="...explanatory text...">
        <svg class="app-tip-ico" ...>...</svg>
        <span class="app-tip-bubble" role="tooltip">...explanatory text...</span>
     </span>
   Keep aria-label and the bubble text identical so AT and sighted users match.
   NOTE: the bubble is position:absolute, so an ancestor with overflow:hidden
   (e.g. .detail-section) clips it. Place the trigger where there is room above
   and to the sides; for a trigger that can sit near a clipped edge, a
   JS-positioned tooltip is the more robust choice. */
.app-tip {
    position: relative;
    display: inline-flex;
    align-items: center;
    margin-left: 2px;
    padding: 3px;            /* roomier hover/tap target around the 14px glyph */
    color: #8b8ba3;
    cursor: help;
    vertical-align: middle;
}
.app-tip:hover,
.app-tip:focus { color: #0f3460; outline: none; }
.app-tip-ico { display: block; }
.app-tip-bubble {
    position: absolute;
    left: 50%;
    bottom: calc(100% + 9px);
    transform: translateX(-50%) translateY(4px);
    z-index: 50;
    width: 240px;
    max-width: 70vw;
    background: #1a1a2e;
    color: #e8e8ef;
    border: 1px solid #2d2d44;
    border-radius: 6px;
    padding: 9px 12px;
    font-size: 12px;
    font-weight: 400;
    line-height: 1.5;
    text-align: left;
    white-space: normal;
    box-shadow: 0 4px 14px rgba(0, 0, 0, .22);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity .18s ease, transform .18s ease, visibility .18s;
}
.app-tip-bubble::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    margin-left: -6px;
    border: 6px solid transparent;
    border-top-color: #1a1a2e;
}
/* Reveal on hover/focus of the tip itself, and (convenience) when hovering the
   whole form label that contains a tip — so the user never has to land on the
   small icon or click to see it. */
.app-tip:hover .app-tip-bubble,
.app-tip:focus .app-tip-bubble,
.app-tip:focus-within .app-tip-bubble,
.form-label:hover .app-tip-bubble {
    opacity: 1;
    visibility: visible;
    transform: translateX(-50%) translateY(0);
}

/* ---- Callout Banner (reusable) ---- */
.callout-banner {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 18px;
    margin-bottom: 16px;
    border: 1px solid #d5d5d5;
    border-left: 4px solid #888;
    border-radius: 6px;
    background: #f9f9f9;
}
.callout-banner-icon {
    flex-shrink: 0;
    color: #666;
    margin-top: 1px;
}
.callout-banner-content {
    min-width: 0;
}
.callout-banner-label {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #666;
    margin-bottom: 3px;
}
.callout-banner-value {
    font-size: 15px;
    font-weight: 600;
    color: #1a1a2e;
    line-height: 1.4;
}
.callout-banner--warn {
    background: #fffbeb;
    border-color: #f0d77c;
    border-left-color: #d4a017;
}
.callout-banner--warn .callout-banner-icon { color: #b8860b; }
.callout-banner--warn .callout-banner-label { color: #92711a; }
.callout-banner--info {
    background: #eff6ff;
    border-color: #b3cde8;
    border-left-color: #2c5282;
}
.callout-banner--info .callout-banner-icon { color: #2c5282; }
.callout-banner--info .callout-banner-label { color: #2c5282; }
.callout-banner--success {
    background: #f0fdf4;
    border-color: #a3d5b3;
    border-left-color: #16a34a;
}
.callout-banner--success .callout-banner-icon { color: #16a34a; }
.callout-banner--success .callout-banner-label { color: #155724; }
.callout-banner--error {
    background: #fef2f2;
    border-color: #f5c6cb;
    border-left-color: #c0392b;
}
.callout-banner--error .callout-banner-icon { color: #c0392b; }
.callout-banner--error .callout-banner-label { color: #721c24; }

/* ---- Form Actions ---- */
.form-actions {
    padding-top: 16px;
    border-top: 1px solid #eee;
    margin-top: 8px;
}
.save-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    padding-top: 16px;
    border-top: 1px solid #eee;
    margin-top: 8px;
}

/* ---- Radio Pills ---- */
.radio-group {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.radio-pill {
    display: inline-flex;
    cursor: pointer;
    margin: 0;
}
.radio-pill input[type="radio"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}
.radio-pill-text {
    display: inline-block;
    padding: 6px 16px;
    font-size: 13px;
    font-weight: 500;
    color: #555;
    background: #f0f2f5;
    border: 1px solid #d0d0d0;
    border-radius: 4px;
    line-height: 1.4;
    white-space: nowrap;
}
.radio-pill input[type="radio"]:checked + .radio-pill-text {
    background: #0f3460;
    color: #fff;
    border-color: #0f3460;
}
.radio-pill:hover .radio-pill-text {
    border-color: #999;
}

/* ---- Lookup Fields ---- */
.lookup-field {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    padding: 6px 10px;
    border: 1px solid transparent;
    border-radius: 6px;
    transition: border-color .15s, background-color .15s;
}
.lookup-field.form-field-error {
    border-color: #dc2626 !important;
    background-color: #fef2f2 !important;
}
.lookup-display {
    font-size: 14px;
    color: #222;
    font-weight: 500;
    min-height: 20px;
}
.lookup-display:empty::before {
    content: "None selected";
    color: #aaa;
    font-weight: 400;
    font-style: italic;
}
.lookup-btn { flex-shrink: 0; }
.lookup-clear-btn {
    font-size: 18px;
    line-height: 1;
    padding: 2px 6px;
    color: #c00;
}

/* ---- File Uploads ---- */
.file-upload-area {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.form-field-file {
    font-size: 0;
    width: 0;
    height: 0;
    overflow: hidden;
    position: absolute;
    opacity: 0;
}
.file-upload-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 16px;
    font-size: 13px;
    font-weight: 500;
    color: #2962cc;
    background: #f0f5ff;
    border: 1px dashed #a0bfee;
    border-radius: 5px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
    width: fit-content;
}
.file-upload-btn:hover {
    background: #e2edff;
    border-color: #6e9fe0;
}
.file-upload-btn svg {
    width: 16px;
    height: 16px;
    fill: currentColor;
}
.file-upload-hint {
    font-size: 12px;
    color: #8a93a0;
}
.file-upload-area.drag-over,
.form-row.drag-over {
    outline: 2px dashed #6e9fe0;
    outline-offset: 3px;
    background: #f0f5ff;
    border-radius: 6px;
}
.file-upload-area.drag-over .file-upload-btn,
.form-row.drag-over .file-upload-btn {
    background: #e2edff;
    border-color: #6e9fe0;
}
.file-upload-area.drag-over .file-upload-hint,
.form-row.drag-over .file-upload-hint {
    color: #2962cc;
}

.upload-file-list {
    margin-bottom: 4px;
}
.upload-file-list:empty {
    display: none;
}
.upload-file-item {
    display: flex;
    align-items: center;
    padding: 7px 10px;
    margin-bottom: 4px;
    background: #f8f9fb;
    border: 1px solid #e2e5ea;
    border-radius: 5px;
    font-size: 13px;
    transition: background 0.12s;
}
.upload-file-item:hover {
    background: #eef1f5;
}
.upload-file-item .file-icon {
    margin-right: 8px;
    color: #6b7a8d;
    flex-shrink: 0;
}
.upload-file-item .file-name {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: #333;
}
.upload-file-item .file-remove {
    background: none;
    border: none;
    color: #aab2bd;
    cursor: pointer;
    font-size: 18px;
    font-weight: 400;
    padding: 0 2px;
    margin-left: 10px;
    line-height: 1;
    transition: color 0.15s;
}
.upload-file-item .file-remove:hover {
    color: #c33;
}
.upload-file-item.existing-file .file-name a {
    color: #2962cc;
    text-decoration: none;
}
.upload-file-item.existing-file .file-name a:hover {
    text-decoration: underline;
}
.upload-file-item.pending-file .file-name {
    color: #555;
    font-style: italic;
}
.upload-file-item.pending-file .file-name::after {
    content: ' (pending upload)';
    font-size: 11px;
    color: #999;
    font-style: italic;
}

/* ---- Checkbox List ---- */
.checkbox-list {
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 8px 12px;
    max-height: 200px;
    overflow-y: auto;
    background: #fff;
}
.checkbox-list label {
    display: block;
    padding: 4px 0;
    font-size: 13px;
    cursor: pointer;
}
.checkbox-list label:hover {
    background: #f5f7fa;
}
.checkbox-list input[type="checkbox"] {
    margin-right: 8px;
}
.checkbox-list-loading {
    color: #999;
    font-size: 13px;
    padding: 6px 0;
}

/* ---- Validation ---- */
.validation-summary {
    background: #fef2f2;
    border: 1px solid #fca5a5;
    border-left: 4px solid #dc2626;
    border-radius: 6px;
    padding: 14px 18px;
    margin-bottom: 20px;
    font-size: 13px;
    color: #991b1b;
}
.validation-summary strong { display: block; margin-bottom: 6px; }
.validation-summary ul { margin: 0; padding-left: 18px; }
.validation-summary li { margin-bottom: 2px; }

.form-field-error {
    border-color: #dc2626 !important;
    background-color: #fef2f2 !important;
}
.form-field-error-file {
    outline: 2px solid #dc2626;
    outline-offset: 2px;
    border-radius: 4px;
}
.form-field.is-invalid,
.form-field.form-field-error,
select.form-field-error {
    border-color: #dc2626 !important;
    background-color: #fef2f2 !important;
}
.radio-group.is-invalid .radio-pill-text {
    border-color: #dc2626;
    background-color: #fef2f2;
}
.field-error {
    font-size: 12px;
    color: #c0392b;
    margin-top: 4px;
}
.email-error-msg {
    font-size: 12px;
    color: #c0392b;
    margin-top: 4px;
}

/* ---- Info Blocks ---- */
.section-info-block {
    background: #f0f4ff;
    border: 1px solid #c9d6f0;
    border-radius: 6px;
    padding: 14px 18px;
    margin-bottom: 18px;
    font-size: 13px;
    line-height: 1.6;
    color: #333;
}
.section-info-block p { margin: 0 0 8px; }
.section-info-block p:last-child { margin-bottom: 0; }
.section-info-block ul { margin: 4px 0 8px; }
.section-info-block li { margin-bottom: 2px; }
.section-info-block a { color: #4a6fa5; text-decoration: underline; }
.section-info-block-elevated {
    background: #fff8e1;
    border-color: #e8d48a;
}

.section-sub-heading {
    font-size: 14px;
    font-weight: 600;
    color: #1a1a2e;
    margin: 0 0 14px;
    padding-bottom: 6px;
    border-bottom: 1px solid #e0e0e0;
}

/* ---- Context Banners ---- */
.context-banner {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    background: #f0f4ff;
    border: 1px solid #c9d6f0;
    border-left: 4px solid #4a6fa5;
    border-radius: 6px;
    padding: 14px 18px;
    margin-bottom: 24px;
}
.context-banner-icon {
    flex-shrink: 0;
    color: #4a6fa5;
    margin-top: 1px;
}
.context-banner-body {
    flex: 1;
    min-width: 0;
}
.context-banner-line {
    font-size: 13.5px;
    color: #1a1a2e;
    line-height: 1.6;
}
.context-banner-line .context-label {
    font-weight: 600;
    color: #333;
}
.context-banner-note {
    font-size: 12px;
    color: #667;
    margin-top: 6px;
    line-height: 1.5;
    font-style: italic;
}
.context-banner-link {
    display: inline-block;
    margin-top: 8px;
    font-size: 13px;
    font-weight: 600;
    color: #4a6fa5;
    text-decoration: none;
}
.context-banner-link:hover {
    color: #2d4f7e;
    text-decoration: underline;
}

/* ---- Modals ---- */
.modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 9000;
    background: rgba(0, 0, 0, .45);
    justify-content: center;
    align-items: center;
}
.modal-overlay.open { display: flex; }
.modal-dialog {
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 8px 30px rgba(0,0,0,.25);
    width: 560px;
    max-width: 94vw;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
    animation: modalIn .2s ease;
}
@keyframes modalIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}
.modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 16px 20px;
    border-bottom: 1px solid #e8e8e8;
}
.modal-header h2 { font-size: 16px; font-weight: 600; margin: 0; }
.modal-close {
    background: none;
    border: none;
    font-size: 22px;
    color: #888;
    cursor: pointer;
    padding: 0 4px;
    line-height: 1;
}
.modal-close:hover { color: #333; }
.modal-body {
    padding: 20px;
    overflow-y: auto;
    position: relative;
}
.modal-body .doc-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 12px 0;
    border-bottom: 1px solid #f0f0f0;
}
.modal-body .doc-row:last-child { border-bottom: none; }
.modal-body .doc-label {
    font-weight: 500;
    color: #555;
    min-width: 160px;
    flex-shrink: 0;
}
.modal-body .doc-value { color: #333; }
.modal-body .doc-value a { color: #1a73e8; text-decoration: none; }
.modal-body .doc-value a:hover { text-decoration: underline; }
.modal-body .doc-empty { color: #999; font-style: italic; }

.modal-md {
    width: 640px;
    max-width: 94vw;
}
.modal-lg {
    width: 840px;
    max-width: 94vw;
    max-height: 90vh;
}
.modal-loading {
    text-align: center;
    padding: 40px 20px;
    color: #888;
    font-size: 15px;
}
.modal-body-scroll {
    max-height: 78vh;
    overflow-y: auto;
    padding: 16px 24px;
}
/* ---- Modal detail header/actions (shared by subscription + capital call modals) ---- */
.detail-modal-header-info {
    font-size: 14px;
    color: #555;
    margin-bottom: 14px;
}
.detail-modal-actions {
    margin-bottom: 20px;
}

.modal-subsection {
    font-size: 14px;
    font-weight: 700;
    color: #0f3460;
    margin: 20px 0 12px;
    padding-bottom: 6px;
    border-bottom: 1px solid #e0e0e0;
}
.modal-subsection:first-child { margin-top: 0; }

/* ---- Lookup Modal Elements ---- */
.lookup-search-row {
    margin-bottom: 12px;
}
.lookup-search-input {
    max-width: 100% !important;
}
.lookup-results {
    min-height: 80px;
    max-height: 320px;
    overflow-y: auto;
    border: 1px solid #e0e0e0;
    border-radius: 4px;
    background: #fafafa;
}
.lookup-loading,
.lookup-empty {
    text-align: center;
    padding: 24px 12px;
    font-size: 13px;
    color: #888;
}
.lookup-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.lookup-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 14px;
    border-bottom: 1px solid #eee;
    font-size: 13px;
    color: #333;
}
.lookup-item:last-child { border-bottom: none; }
.lookup-item:hover { background: #f0f4ff; }
.lookup-item-name { flex: 1; margin-right: 10px; }
.lookup-select-btn { flex-shrink: 0; }
.lookup-add-new-row {
    padding: 10px 0 0;
    text-align: left;
}

/* ---- Tabs ---- */
.contact-tabs {
    margin-bottom: 16px;
}
.contact-tab-bar {
    display: flex;
    border-bottom: 2px solid #dee2e6;
    margin-bottom: 0;
}
.contact-tab {
    padding: 9px 20px;
    font-size: 13px;
    font-weight: 500;
    color: #666;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -2px;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
}
.contact-tab:hover {
    color: #333;
}
.contact-tab.active {
    color: #2962cc;
    border-bottom-color: #2962cc;
}
.contact-tab-panel {
    display: none;
    padding-top: 16px;
}
.contact-tab-panel.active {
    display: block;
}

/* ---- Inner Sections ---- */
.inner-section {
    border: 1px solid #e0e0e0;
    border-radius: 6px;
    margin-bottom: 16px;
    overflow: hidden;
}
.inner-section-heading {
    display: flex;
    align-items: center;
    padding: 10px 16px;
    font-size: 13px;
    font-weight: 600;
    color: #0f3460;
    cursor: pointer;
    user-select: none;
    list-style: none;
    background: #f8f9fb;
}
.inner-section-heading::-webkit-details-marker { display: none; }
.inner-section-heading::before {
    content: "\25B6";
    font-size: 8px;
    color: #999;
    margin-right: 8px;
    display: inline-block;
}
details[open] > .inner-section-heading::before {
    transform: rotate(90deg);
}
.inner-section-body {
    padding: 12px 16px;
}

/* ---- Misc Shared ---- */
@keyframes fieldHighlight {
    0% { box-shadow: 0 0 0 0 rgba(204, 51, 51, 0.5); }
    50% { box-shadow: 0 0 0 4px rgba(204, 51, 51, 0.3); }
    100% { box-shadow: 0 0 0 0 rgba(204, 51, 51, 0); }
}
.field-highlight-pulse {
    animation: fieldHighlight 0.75s ease 2;
}

.saving-spinner {
    width: 32px;
    height: 32px;
    border: 3px solid #e0e0e0;
    border-top-color: #0f3460;
    border-radius: 50%;
    margin: 0 auto;
    animation: spin 0.8s linear infinite;
}
@keyframes spin {
    to { transform: rotate(360deg); }
}

.save-success-bar {
    background: #27ae60;
    color: #fff;
    text-align: center;
    padding: 10px 16px;
    font-size: 14px;
    font-weight: 500;
    transition: opacity 0.3s ease;
}

/* ---- Layout Shell (sidebar + main) ---- */
.layout {
    display: flex;
    gap: 28px;
    align-items: flex-start;
}
.summary-sidebar {
    width: 220px;
    flex-shrink: 0;
    position: sticky;
    top: 72px;
    max-height: calc(100vh - 90px);
    overflow-y: auto;
    background: #fff;
    border: 1px solid #e2e5ea;
    border-radius: 8px;
    padding: 0 0 8px 0;
    box-shadow: 0 1px 4px rgba(0,0,0,0.05);
    align-self: flex-start;
}
.summary-sidebar-header {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #999;
    padding: 14px 16px 10px;
    border-bottom: 1px solid #eef0f3;
    margin-bottom: 4px;
}
.page-main {
    flex: 1;
    min-width: 0;
    max-width: 1150px;
    padding: 0 0 48px 0;
}

/* ---- Sidebar Summary Items ---- */
.summary-sidebar .summary-item {
    padding: 11px 16px;
    display: flex;
    align-items: center;
    gap: 12px;
    border-left: 3px solid transparent;
    position: relative;
    transition: background 0.12s, color 0.12s;
}
.summary-item[data-filter-category=""]              { border-left-color: #8899aa; }
.summary-item[data-filter-category="action_needed"]  { border-left-color: #3498db; }
.summary-item[data-filter-category="in_progress"]    { border-left-color: #e67e22; }
.summary-item[data-filter-category="completed"]          { border-left-color: #27ae60; }
.summary-item[data-filter-category="approved_modified"]  { border-left-color: #d4880f; }
.summary-item[data-filter-category="approved"]           { border-left-color: #2980b9; }
.summary-item[data-filter-category="pending_approval"]   { border-left-color: #e67e22; }
.summary-clickable { cursor: pointer; transition: background-color 0.15s ease; }
.summary-clickable:hover { background: #f5f7fa; }
.summary-clickable.active[data-filter-category=""]              { background: #f4f6f8; border-left-color: #8899aa; }
.summary-clickable.active[data-filter-category="action_needed"]  { background: #eef6fc; border-left-color: #3498db; }
.summary-clickable.active[data-filter-category="in_progress"]    { background: #fef5ec; border-left-color: #e67e22; }
.summary-clickable.active[data-filter-category="completed"]      { background: #edfbf3; border-left-color: #27ae60; }
.summary-clickable.active[data-filter-category="approved_modified"] { background: #fef8ec; border-left-color: #d4880f; }
.summary-clickable.active[data-filter-category="approved"]       { background: #eef3fc; border-left-color: #2980b9; }
.summary-clickable.active[data-filter-category="pending_approval"] { background: #fef5ec; border-left-color: #e67e22; }
.summary-icon {
    display: flex;
    flex-shrink: 0;
}
.summary-icon svg { display: block; }
.summary-count {
    font-size: 20px;
    font-weight: 700;
    color: #1a1a2e;
    line-height: 1;
    min-width: 24px;
}
.summary-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: .4px;
    color: #888;
    font-weight: 500;
    line-height: 1.2;
}
.summary-meta {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* ---- Filter Row ---- */
.filter-row {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: 18px;
    font-size: 13px;
    color: #888;
}
.filter-row .record-count {
    margin: 0;
    font-size: 13px;
}
.filter-row .record-count strong { color: #555; }
.filter-sep { color: #d0d0d0; }
.filter-select {
    padding: 6px 28px 6px 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    font-size: 13px;
    color: #333;
    background: #fff;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.filter-select:focus { outline: none; border-color: #0f3460; }

/* ---- AppSelect (shared searchable dropdown — the standard filter control) ---- */
.app-select { position: relative; display: inline-block; }
.app-select-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    padding: 6px 10px;
    font-size: 13px;
    color: #333;
    background: #fff;
    border: 1px solid #ccc;
    border-radius: 5px;
    cursor: pointer;
    text-align: left;
}
.app-select-trigger:hover { border-color: #0f3460; }
.app-select.open .app-select-trigger,
.app-select--active .app-select-trigger { border-color: #0f3460; color: #0f3460; }
.app-select--active .app-select-trigger { background: #f0f4ff; }
.app-select-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.app-select-caret { color: #999; flex-shrink: 0; transition: transform 0.15s, color 0.15s; }
.app-select.open .app-select-caret { color: #0f3460; transform: rotate(180deg); }
.app-select-panel {
    display: none;
    position: absolute;
    z-index: 40;
    top: calc(100% + 4px);
    left: 0;
    /* Grow to fit the widest option so long labels (e.g. full investor names)
       are not clipped, but never narrower than the trigger nor wider than the
       viewport. Anything still longer than the cap wraps (see -item-text). */
    width: max-content;
    min-width: 100%;
    max-width: min(440px, calc(100vw - 24px));
    background: #fff;
    border: 1px solid #d5d5d5;
    border-radius: 6px;
    box-shadow: 0 6px 20px rgba(20, 30, 50, 0.12);
}
.app-select.open .app-select-panel { display: block; }
/* When a widened panel would spill past the right edge of the viewport, JS adds
   this class so the panel right-aligns to the trigger and stays on-screen. */
.app-select--flip-right .app-select-panel { left: auto; right: 0; }
/* On mobile the filter grid is single-column and the trigger already spans the
   row, so keep the panel at the trigger width (long labels wrap within it)
   rather than growing — a wider panel can't be kept on-screen beside a
   near-full-width trigger. */
@media (max-width: 700px) {
    .app-select-panel { width: auto; max-width: 100%; }
}
.app-select-search { padding: 8px 10px; border-bottom: 1px solid #eef0f3; }
.app-select-search-input {
    width: 100%;
    padding: 6px 10px;
    font-size: 13px;
    border: 1px solid #ddd;
    border-radius: 5px;
    outline: none;
    box-sizing: border-box;
}
.app-select-search-input:focus { border-color: #0f3460; }
/* width:max-content lets the list (and thus the panel) size to its widest
   option even though it is a vertical scroll container; max-width:100% keeps it
   within the panel's cap so long labels wrap rather than scroll horizontally. */
.app-select-list { width: max-content; min-width: 100%; max-width: 100%; max-height: 240px; overflow-y: auto; padding: 4px 0; }
.app-select-item {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 8px;
    padding: 7px 12px;
    font-size: 13px;
    color: #333;
    cursor: pointer;
}
.app-select-item:hover { background: #f5f7fa; }
.app-select-item--active { color: #0f3460; font-weight: 600; background: #f0f4ff; }
/* Wrap long labels (full investor names etc.) instead of clipping them with an
   ellipsis, so the whole value is always readable. */
.app-select-item-text { min-width: 0; white-space: normal; overflow-wrap: break-word; word-break: break-word; line-height: 1.35; }
.app-select-check { flex-shrink: 0; margin-top: 2px; color: #0f3460; }
/* Section header for an AppSelect built from a <select> with <optgroup>s. */
.app-select-group {
    padding: 7px 12px 3px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #94a3b8;
}
.app-select-group:not(:first-child) { margin-top: 2px; border-top: 1px solid #f1f5f9; }

.btn-clear-filters {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 5px 10px;
    font-size: 12px;
    color: #c0392b;
    background: none;
    border: 1px solid #e0c8c5;
    border-radius: 5px;
    cursor: pointer;
    white-space: nowrap;
    transition: border-color 0.15s, color 0.15s;
}
.btn-clear-filters:hover {
    color: #a93226;
    border-color: #c0392b;
}

/* Export CSV button standard: reuse the borderless, navy .app-grid-export style
   (defined with the AppGrid toolbar below). When placed in the outside
   .filter-row toolbar of a card list page, it sits at the top-right, opposite
   the record count; .app-grid-export already hides itself on mobile. */
.filter-row .app-grid-export { margin-left: auto; }

/* ---- Shared Sort control (.app-sort) ----
   The "Sort" dropdown + direction-toggle bar for hand-built card-list pages
   (Indicative Transactions, Initial Subscriptions, Onboarding, Additional Subs,
   Capital Calls). Markup: views/general/app_sort_control.php; behavior:
   window.AppSort (assets/general/app_sort.js). It is NOT a filter input, so it
   lives in the .filter-row toolbar OUTSIDE the collapsible AppFilters panel and
   stays a full-width sort bar on mobile (the Export button hides on mobile). */
.app-sort { display: inline-flex; align-items: center; gap: 6px; }
.app-sort-label { font-size: 12px; font-weight: 600; color: #8a94a0; }
.app-sort-select { max-width: 150px; }
.app-sort-dir {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border: 1px solid #d7dbe0;
    border-radius: 6px;
    background: #fff;
    color: #0f3460;
    cursor: pointer;
    flex-shrink: 0;
}
.app-sort-dir:hover { background: #f2f7fc; border-color: #0f3460; }
.app-sort-dir svg { transition: transform 0.15s ease; }
.app-sort-dir.is-asc svg { transform: rotate(180deg); } /* default arrow = desc; asc flips it up */
/* In a .filter-row toolbar, push Sort + Export to the right as a group: .app-sort
   owns the auto margin; an Export button that FOLLOWS it drops its own auto margin
   (the .filter-row .app-grid-export rule above) so the two sit together. */
.filter-row .app-sort { margin-left: auto; }
.filter-row .app-sort ~ .app-grid-export { margin-left: 0; }

/* Visibility wrapper for an OUTSIDE (mobile) Clear Filters button used with the
   AppFilters panel: hidden until a filter is active (.is-on, toggled by JS), and
   never shown on desktop (the panel header's "Clear all" handles desktop). Pair
   with .btn-clear-filters for the styling. */
.app-filter-clear-mobile { display: none; margin-bottom: 12px; }
.app-filter-clear-mobile.is-on { display: inline-flex; }
@media (min-width: 701px) { .app-filter-clear-mobile { display: none !important; } }

/* ============================================================================
 * Search & Filter panel (AppFilters) — shared, reusable. See assets/general/
 * app_filters.js. Desktop-collapsible header (count badge + Clear all), a
 * prominent search field, an auto-fitting dropdown grid of cascading AppSelects,
 * and removable active-filter chips. Mobile collapses behind .app-filter-toggle.
 * Features only style the cell/card content they inject — not this panel.
 * ========================================================================== */
.app-filter-panel {
    display: block;
    background: #fff;
    border: 1px solid #e4e8ee;
    border-radius: 12px;
    box-shadow: 0 1px 2px rgba(20, 30, 50, .04),
                0 12px 32px -24px rgba(20, 30, 50, .45);
    margin-bottom: 18px;
    overflow: visible; /* let AppSelect dropdown panels overflow the card */
}

/* ---- Header strip (doubles as the desktop collapse toggle) ---- */
.app-filter-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 13px 18px;
    border-bottom: 1px solid #eef1f5;
    background: linear-gradient(180deg, #fbfcfe 0%, #f4f7fa 100%);
    border-radius: 12px 12px 0 0;
}
.app-filter-head-l { display: inline-flex; align-items: center; gap: 10px; min-width: 0; }
.app-filter-title {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 700;
    color: #1a2332;
    letter-spacing: .2px;
}
.app-filter-title svg { color: #0f3460; flex-shrink: 0; }
/* Active-filter count pill — revealed by JS (.is-on) when >0 filters apply.
   Lives both in the header (desktop) and the mobile toggle button. */
.app-filter-count {
    display: none;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 7px;
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    color: #fff;
    background: #0f3460;
    border-radius: 20px;
}
.app-filter-count.is-on { display: inline-flex; }
.app-filter-head-r { display: inline-flex; align-items: center; gap: 8px; }
/* "Clear all" lives in the header on desktop (revealed by JS .is-on). */
.app-filter-clear {
    display: none;
    align-items: center;
    gap: 5px;
    font-size: 12px;
    font-weight: 600;
    color: #c0392b;
    background: none;
    border: none;
    cursor: pointer;
    padding: 4px 8px;
    border-radius: 6px;
    transition: background .15s, color .15s;
}
.app-filter-clear:hover { background: #fbecec; color: #a93226; }
.app-filter-clear.is-on { display: inline-flex; }
.app-filter-head-toggle { color: #9aa4b1; flex-shrink: 0; transition: transform .15s; }
.app-filter-panel.is-expanded .app-filter-head-toggle { transform: rotate(180deg); }

/* ---- Body ---- */
.app-filter-body { padding: 16px 18px 18px; }

/* ---- Search field (leading magnifier icon) ---- */
.app-filter-search { position: relative; margin-bottom: 16px; }
.app-filter-search-ico {
    position: absolute;
    left: 13px;
    top: 50%;
    transform: translateY(-50%);
    color: #9aa4b1;
    pointer-events: none;
}
.app-filter-search-input {
    width: 100%;
    max-width: 400px; /* capped on desktop; full width on mobile (see @media) */
    padding: 10px 14px 10px 38px;
    font-size: 13.5px;
    color: #1a2332;
    border: 1px solid #d8dee6;
    border-radius: 9px;
    background: #fff;
    box-sizing: border-box;
    -webkit-appearance: none;
    transition: border-color .15s, box-shadow .15s;
}
.app-filter-search-input::placeholder { color: #9aa4b1; }
.app-filter-search-input:focus {
    outline: none;
    border-color: #0f3460;
    box-shadow: 0 0 0 3px rgba(15, 52, 96, .10);
}

/* ---- Dropdown grid (neat auto-fitting columns instead of ragged wrap) ---- */
.app-filter-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 14px 16px;
}
.app-filter-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}
.app-filter-field label {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .5px;
    color: #8a93a0;
}
.app-filter-field .app-select { width: 100%; min-width: 0; }

/* Tune the AppSelect trigger to match this panel's taller, rounder controls
   (scoped to .app-filter-panel so the global widget is untouched elsewhere). */
.app-filter-panel .app-select-trigger {
    padding: 9px 12px;
    border-radius: 9px;
    border-color: #d8dee6;
    font-size: 13px;
    transition: border-color .15s, box-shadow .15s, background .15s;
}
.app-filter-panel .app-select-trigger:hover { border-color: #b9c3cf; }
/* Open dropdown gets the focus ring; the active (selected) state is signalled by
   the navy border + light fill from the AppSelect base, kept ring-free. */
.app-filter-panel .app-select.open .app-select-trigger {
    box-shadow: 0 0 0 3px rgba(15, 52, 96, .10);
}

/* ---- AppSelect toggle variant (segmented button group) ---- */
.app-select-toggle {
    display: inline-flex;
    border: 1px solid #d8dee6;
    border-radius: 9px;
    overflow: hidden;
    background: #f4f6f9;
}
.app-select-toggle-btn {
    appearance: none;
    border: 0;
    border-left: 1px solid #d8dee6;
    background: transparent;
    padding: 9px 14px;
    font-size: 13px;
    font-weight: 600;
    color: #5a6473;
    cursor: pointer;
    transition: background .12s, color .12s;
}
.app-select-toggle-btn:first-child { border-left: 0; }
.app-select-toggle-btn:hover:not(:disabled):not(.is-active) { background: #e9eef4; }
.app-select-toggle-btn.is-active { background: #0f3460; color: #fff; }
.app-select-toggle-btn:disabled { color: #b6bcc5; cursor: not-allowed; background: #f4f6f9; }
/* In a filter panel field, fill the column width and split evenly so it aligns
   with the other (full-width) dropdown controls. */
.app-filter-field .app-select-toggle { display: flex; width: 100%; }
.app-filter-field .app-select-toggle-btn { flex: 1 1 0; text-align: center; }

/* ---- Active-filter chips (revealed by JS .is-on) ---- */
.app-filter-chips {
    display: none;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 16px;
    padding-top: 14px;
    border-top: 1px dashed #e7ebf0;
}
.app-filter-chips.is-on { display: flex; }
.app-chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 5px 6px 5px 12px;
    background: #eef3f9;
    border: 1px solid #d9e3ef;
    border-radius: 20px;
    font-size: 12px;
    color: #1a3354;
    max-width: 100%;
}
.app-chip-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 260px;
}
.app-chip-key { font-weight: 700; color: #0f3460; }
.app-chip-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 17px;
    height: 17px;
    border-radius: 50%;
    background: rgba(15, 52, 96, .10);
    color: #0f3460;
    border: none;
    cursor: pointer;
    flex-shrink: 0;
    transition: background .15s, color .15s;
}
.app-chip-x:hover { background: #0f3460; color: #fff; }

/* ---- Mobile-only collapse toggle (a polished white card button) ---- */
.app-filter-toggle {
    display: none;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    width: 100%;
    margin-bottom: 12px;
    padding: 13px 16px;
    background: #fff;
    border: 1px solid #e4e8ee;
    border-radius: 10px;
    box-shadow: 0 1px 2px rgba(20, 30, 50, .04),
                0 10px 26px -22px rgba(20, 30, 50, .45);
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: .2px;
    color: #1a2332;
    cursor: pointer;
}
.app-filter-toggle:hover { border-color: #d3dae3; }
.app-filter-toggle-l { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
.app-filter-toggle-l > svg { color: #0f3460; flex-shrink: 0; }
.app-filter-toggle-ico { color: #9aa4b1; flex-shrink: 0; transition: transform .15s; }
.app-filter-toggle.is-open .app-filter-toggle-ico { transform: rotate(180deg); }

/* Desktop: the header strip is the collapse toggle; the body collapses beneath
   it. Default is collapsed (no .is-expanded); AppFilters remembers the user's
   choice in localStorage. Scoped to desktop widths (mobile uses the toggle). */
@media (min-width: 701px) {
    .app-filter-head { cursor: pointer; }
    .app-filter-head:hover { background: linear-gradient(180deg, #f7fafc 0%, #eef3f8 100%); }
    .app-filter-body { display: none; }
    .app-filter-panel.is-expanded .app-filter-body { display: block; }
    /* Collapsed: the header is the only visible part — fully round it + drop the
       divider so it reads as a single pill. */
    .app-filter-panel:not(.is-expanded) .app-filter-head {
        border-bottom: none;
        border-radius: 12px;
    }
}

/* Mobile (the standard 700px breakpoint): show the toggle, collapse the panel,
   hide the desktop header, stack one filter per full-width row. */
@media (max-width: 700px) {
    .app-filter-toggle { display: flex; }
    .app-filter-panel.is-collapsed { display: none; }
    .app-filter-head { display: none; }
    .app-filter-body { padding: 14px; }
    .app-filter-grid { grid-template-columns: 1fr; }
    .app-filter-field .app-select { min-width: 0; width: 100%; }
    .app-filter-search-input { font-size: 16px; max-width: 100%; } /* 16px avoids iOS focus zoom */
}

/* ---- Card Hero Row ---- */
.card-hero {
    display: flex;
    gap: 1px;
    margin: 12px 20px 0;
    background: #e2e6ea;
    border-radius: 6px;
    overflow: hidden;
}
.card-hero-item {
    flex: 1;
    padding: 10px 16px;
    background: #f7f9fb;
}
.card-hero-item:first-child {
    border-radius: 6px 0 0 6px;
}
.card-hero-item:last-child {
    border-radius: 0 6px 6px 0;
}
.card-hero-label {
    font-size: 10px;
    font-weight: 600;
    color: #778;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 2px;
}
.card-hero-value {
    font-size: 16px;
    font-weight: 700;
    color: #1a2332;
    font-variant-numeric: tabular-nums;
}
.card-hero-accent {
    background: #eef3ff;
}
.card-hero-accent .card-hero-value {
    color: #0d6efd;
}

/* ---- Card Action Buttons ---- */
.btn-view-detail {
    background: #0f3460;
    color: #fff;
}
.btn-view-detail:hover {
    background: #0b2a4e;
    color: #fff;
}
.btn-download-pdf {
    background: #f8f9fa;
    color: #333;
    border: 1px solid #ddd;
}
.btn-download-pdf:hover {
    background: #e9ecef;
    color: #333;
}

/* ---- Skeleton Placeholders ---- */
.skel-text {
    display: inline-block;
    width: 72px;
    height: 12px;
    background: #e8eaed;
    border-radius: 4px;
    vertical-align: middle;
    animation: skel-pulse 1.2s ease-in-out infinite;
}
.skel-sm { width: 28px; }
.skel-card { pointer-events: none; }
@keyframes skel-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.4; }
}

/* ---- Responsive ---- */
@media (max-width: 800px) {
    .user-name { display: none; }
    .user-btn { padding: 7px 8px; }
}

@media (max-width: 640px) {
    /* Topbar */
    .topbar { padding: 0 12px; position: static; }
    .topbar-brand { display: none; }
    .desktop-only { display: none; }
    .user-name { display: none; }
    .user-btn { padding: 7px 8px; }

    /* Page wrapper */
    .page-wrapper { padding: 20px 12px 0; }

    /* Layout shell */
    .layout { display: block; }
    .summary-sidebar { display: none; }
    .page-main { padding: 0 0 20px; }
    .mobile-only { display: block; }
    .filter-row { flex-direction: column; align-items: stretch; gap: 8px; }
    .filter-row .filter-sep { display: none; }
    .filter-select { width: 100%; max-width: 100%; }
    /* Sort control becomes a full-width sort bar on mobile (stacked, always
       visible — it's not a filter input, so it never collapses). */
    .filter-row .app-sort { margin-left: 0; width: 100%; }
    .app-sort-select { max-width: none; flex: 1; }
    /* Standard: Clear Filters is full-width with centered text on mobile (it
       sits on its own row in the stacked filter bar). */
    .btn-clear-filters { width: 100%; justify-content: center; box-sizing: border-box; }

    /* Card hero */
    .card-hero { flex-direction: column; }
    .card-hero-item:first-child { border-radius: 6px 6px 0 0; }
    .card-hero-item:last-child  { border-radius: 0 0 6px 6px; }

    /* Modals */
    .modal-dialog { width: 95%; min-width: 0; }
    .modal-body { padding: 14px; }
    .modal-header { padding: 14px 16px; }
    .modal-header h2 { font-size: 15px; }

    /* Field table */
    .field-table td.lbl {
        width: auto;
        white-space: normal;
        display: block;
        padding-bottom: 0;
    }
    .field-table td.val {
        display: block;
        padding-top: 0;
        padding-bottom: 10px;
    }
    .field-table tr { display: block; }

    /* Modal doc rows (Documents, Help, Funding Instructions) */
    .modal-body .doc-row {
        flex-direction: column;
        gap: 2px;
    }
    .modal-body .doc-label {
        min-width: 0;
    }

    /* Form fields */
    .form-field, select.form-field { max-width: 100%; }
    /* .radio-group { flex-direction: column; } */
    /* .lookup-field { flex-direction: column; align-items: flex-start; } */
    .lookup-display { flex: 1; min-width: 0; word-break: break-word; }
}

/* ============================================================================
 * Data Grid (AppGrid)  -- shared, reusable table grid. See assets/general/app_grid.js
 * Standard table styling: toolbar, sticky header, sortable headers, zebra/hover/
 * selected rows, skeleton loading, empty/no-results/error states, footer with
 * load-more or pagination, column menu, and a stacked-card mobile layout.
 * ========================================================================== */
.app-grid { display: block; }

/* Toolbar (quick-search / columns / export) */
.app-grid-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 10px;
    flex-wrap: wrap;
}
.app-grid-toolbar-left {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.app-grid-toolbar-right {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-left: auto;
}
.app-grid-search { position: relative; flex: 1 1 220px; max-width: 320px; }
.app-grid-search-input {
    width: 100%;
    padding: 7px 12px;
    font-size: 13px;
    border: 1px solid #d7dbe0;
    border-radius: 6px;
    color: #333;
    box-sizing: border-box;
}
.app-grid-search-input:focus { outline: none; border-color: #0f3460; }

/* Modern CSV export button -- borderless, no background, subtle hover */
.app-grid-export {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 10px;
    font-size: 12px;
    font-weight: 600;
    color: #0f3460;
    background: none;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    transition: color .15s, background .15s;
}
.app-grid-export:hover { color: #0a2747; background: #eef3f9; }
.app-grid-export svg { flex-shrink: 0; }

/* Scroll container + table. Default keeps the page as the scroll context so the
   sticky header follows page scroll; opt into horizontal scrolling per grid with
   scrollX:true (which makes the wrapper the scroll context for wide tables). */
.app-grid-scroll {
    width: 100%;
    border: 1px solid #e8eaed;
    border-radius: 8px;
    background: #fff;
}
.app-grid-scroll--x { overflow-x: auto; }
/* Click-and-drag horizontal panning (set by JS when the table overflows). */
.app-grid-scroll--draggable { cursor: grab; }
.app-grid-scroll--grabbing { cursor: grabbing; user-select: none; }
.app-grid-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
    color: #333;
}
.app-grid-th {
    position: sticky;
    /* Offset by the sticky topbar height (56px) so headers tuck beneath it. */
    top: 56px;
    z-index: 2;
    background: #f7f9fb;
    text-align: left;
    font-weight: 600;
    color: #5a6573;
    padding: 10px 12px;
    /* box-shadow keeps the divider visible while the header is stuck
       (collapsed-border tables drop the cell border on scroll). */
    box-shadow: inset 0 -1px 0 #e3e7eb;
    white-space: nowrap;
    user-select: none;
}
/* In horizontal-scroll mode the wrapper itself becomes the sticky containing
   block (overflow-x:auto forces overflow-y to compute to auto), so the 56px
   topbar offset is measured from the wrapper's top — pushing the header down
   and leaving a gap above the first row. Pin it to the wrapper's top instead.
   (scrollX already trades away page-scroll stickiness, per the AppGrid docs.) */
.app-grid-scroll--x .app-grid-th { top: 0; }
/* JS-driven floating header clone for horizontal-scroll grids: shown fixed
   under the topbar while the real header is scrolled past, restoring the
   page-scroll stickiness that overflow-x scrolling otherwise disables. */
.app-grid-floathead {
    position: fixed;
    z-index: 20;
    display: none;
    overflow: hidden;
    background: #f7f9fb;
    box-shadow: 0 4px 8px rgba(15, 52, 96, 0.08);
}
.app-grid-floathead .app-grid-table { table-layout: fixed; border-collapse: collapse; }
.app-grid-floathead .app-grid-th { position: static; }
.app-grid-center { text-align: center; }
.app-grid-right  { text-align: right; }
.app-grid-th.app-grid-center .app-grid-th-label,
.app-grid-th.app-grid-right  .app-grid-th-label { justify-content: center; }

.app-grid-sortable { cursor: pointer; }
.app-grid-sortable:hover { background: #eef2f6; }
.app-grid-th-label { display: inline-flex; align-items: center; gap: 4px; }
.app-grid-sort-ico {
    display: inline-block;
    width: 0; height: 0;
    margin-left: 4px;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-bottom: 5px solid #c2c9d1;
    opacity: 0.5;
    vertical-align: middle;
}
.app-grid-th.is-sorted-asc  .app-grid-sort-ico {
    border-bottom: 5px solid #0f3460; border-top: 0; opacity: 1;
}
.app-grid-th.is-sorted-desc .app-grid-sort-ico {
    border-top: 5px solid #0f3460; border-bottom: 0; opacity: 1;
}

.app-grid-cell {
    padding: 9px 12px;
    border-bottom: 1px solid #eef1f4;
    vertical-align: middle;
}
.app-grid-row:nth-child(even) { background: #fbfcfd; }
.app-grid-row:hover { background: #f2f7fc; }
.app-grid-row.is-selected { background: #e8f1fb; }
.app-grid-row.is-selected:hover { background: #ddeafa; }

/* Optional per-record sub-row (rowDetailRender) — a full-width secondary row
   directly under its record. Zebra moves to explicit record-pair classes
   (nth-child would desync once sub-rows interleave). */
.app-grid--subrows .app-grid-row:nth-child(even) { background: none; }
.app-grid--subrows .app-grid-row--even,
.app-grid-subrow--even { background: #fbfcfd; }
.app-grid-row.has-subrow .app-grid-cell { border-bottom: 0; }
.app-grid-subcell {
    padding: 0 12px 9px;
    border-bottom: 1px solid #eef1f4;
}
/* Hovering the record highlights its sub-row too (and vice versa). */
.app-grid-row:hover + .app-grid-subrow,
.app-grid-subrow:hover { background: #f2f7fc; }

/* Selection checkbox column */
.app-grid-check {
    width: 36px;
    text-align: center;
    padding-left: 12px;
    padding-right: 6px;
}
.app-grid-check input { cursor: pointer; }

/* Skeleton rows */
.app-grid-skelrow td { padding: 11px 12px; border-bottom: 1px solid #eef1f4; }
.app-grid-skelrow .skel-text { width: 80%; }

/* State rows (empty / no-results / error) */
.app-grid-state {
    text-align: center;
    padding: 40px 20px;
    color: #888;
}
.app-grid-state-icon { margin-bottom: 12px; color: #c4ccd4; }
.app-grid-state-title { font-size: 15px; font-weight: 600; color: #555; margin-bottom: 6px; }
.app-grid-state-msg { font-size: 13px; color: #8a94a0; line-height: 1.6; }
.app-grid-state--error .app-grid-state-title { color: #c0392b; }
.app-grid-retry { margin-left: 8px; }

/* Footer: count + pager */
.app-grid-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 2px;
    flex-wrap: wrap;
}
.app-grid-count { font-size: 12px; color: #6b7480; }
.app-grid-count strong { color: #333; }
.app-grid-pager { display: flex; align-items: center; gap: 6px; }
.app-grid-pageinfo { font-size: 12px; color: #6b7480; padding: 0 4px; }
.app-grid-pgbtn {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 4px 9px;
    font-size: 12px;
    font-weight: 600;
    color: #0f3460;
    background: #fff;
    border: 1px solid #d7dbe0;
    border-radius: 6px;
    cursor: pointer;
    transition: background .15s, border-color .15s;
}
.app-grid-pgbtn:hover:not(:disabled) { background: #f2f7fc; border-color: #0f3460; }
.app-grid-pgbtn:disabled { opacity: .45; cursor: not-allowed; }
.app-grid-pgbtn svg { flex-shrink: 0; }
.app-grid-pagesize {
    padding: 5px 8px;
    font-size: 12px;
    border: 1px solid #d7dbe0;
    border-radius: 6px;
    color: #333;
}
.app-grid-loadmore { min-width: 160px; }

/* Columns visibility menu */
.app-grid-cols { position: relative; }
.app-grid-cols-panel {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 4px;
    background: #fff;
    border: 1px solid #d7dbe0;
    border-radius: 8px;
    box-shadow: 0 6px 20px rgba(20, 30, 50, 0.12);
    padding: 8px;
    z-index: 30;
    min-width: 180px;
}
.app-grid-cols.open .app-grid-cols-panel { display: block; }
.app-grid-cols-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    font-size: 13px;
    color: #333;
    border-radius: 6px;
    cursor: pointer;
}
.app-grid-cols-item:hover { background: #f2f7fc; }

/* ---- Mobile card layout (JS toggles .app-grid--mobile via matchMedia) ---- */
/* Layout swap is class-driven (not pure media query) so JS is the single source
   of truth for which layout is rendered. */
.app-grid-cards { display: none; }
.app-grid-stateblock { display: none; }
.app-grid--mobile .app-grid-table { display: none; }
.app-grid--mobile .app-grid-cards { display: block; }
.app-grid--mobile .app-grid-scroll { border: none; background: transparent; overflow: visible; }
.app-grid--state .app-grid-stateblock { display: block; }
.app-grid--state .app-grid-table,
.app-grid--state .app-grid-cards { display: none; }

/* Mobile sort / select-all bar (shown/hidden via inline display from JS) */
.app-grid-mobilebar {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 10px;
    flex-wrap: wrap;
}
.app-grid-mselect {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    font-weight: 600;
    color: #2b3440;
}
.app-grid-mselect input { width: 16px; height: 16px; }
.app-grid-msort { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
.app-grid-msort-label { font-size: 12px; font-weight: 600; color: #8a94a0; }
.app-grid-msort-select {
    padding: 6px 28px 6px 10px;
    font-size: 13px;
    border: 1px solid #ccc;
    border-radius: 5px;
    color: #333;
    background: #fff;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    max-width: 170px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.app-grid-msort-dir {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border: 1px solid #d7dbe0;
    border-radius: 6px;
    background: #fff;
    color: #0f3460;
    cursor: pointer;
}
.app-grid-msort-dir:hover { background: #f2f7fc; border-color: #0f3460; }

/* The cards themselves */
.app-grid-card {
    display: flex;
    gap: 10px;
    border: 1px solid #e3e7eb;
    border-radius: 10px;
    background: #fff;
    padding: 14px;
    margin-bottom: 10px;
    box-shadow: 0 1px 2px rgba(20, 30, 50, 0.04);
}
.app-grid-card.is-selected { border-color: #0f3460; background: #f3f8fd; }
.app-grid-card-check { flex: 0 0 auto; padding-top: 2px; }
.app-grid-card-check input { width: 18px; height: 18px; }
.app-grid-card-body { flex: 1 1 auto; min-width: 0; }
.app-grid-card-title {
    font-size: 15px;
    font-weight: 700;
    color: #1d2733;
    margin-bottom: 8px;
    word-break: break-word;
}
.app-grid-card-meta { display: flex; flex-direction: column; gap: 8px; }
/* Stacked label-over-value (standard): avoids the ragged wrapping of
   side-by-side label/value on narrow screens. */
.app-grid-kv { display: flex; flex-direction: column; gap: 1px; font-size: 13px; }
.app-grid-kv-k {
    color: #8a94a0;
    font-weight: 600;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.3px;
}
.app-grid-kv-v { color: #2b3440; word-break: break-word; }
/* Card action buttons (e.g. View): a full-width row at the bottom of the card. */
.app-grid-card-actions { margin-top: 12px; }
.app-grid-card-actions .btn-card-action,
.app-grid-card-actions .btn { width: 100%; }
.app-grid-skelcard { flex-direction: column; gap: 8px; }
.app-grid-skelcard .skel-text { height: 13px; }

/* Finger-friendly footer + export hidden on mobile (standard) */
@media (max-width: 700px) {
    .app-grid-export { display: none; }
    .app-grid-footer { flex-direction: column-reverse; align-items: stretch; gap: 10px; }
    .app-grid-pager { justify-content: center; }
    .app-grid-pgbtn { padding: 9px 16px; font-size: 13px; }
    .app-grid-pageinfo { font-size: 13px; }
    .app-grid-count { text-align: center; }
    .app-grid-loadmore { width: 100%; }
    /* Standard: record-card View/Details buttons go full-width on mobile. */
    .btn-card-action { width: 100%; }
    /* Standard: the mobile sort bar spans the full width (not right-aligned). */
    .app-grid-mobilebar { flex-direction: column; align-items: stretch; }
    .app-grid-msort { width: 100%; margin-left: 0; }
    .app-grid-msort-select { flex: 1; max-width: none; }
}


/* ==========================================================================
   Topbar Alerts (shared bell + items drawer)
   Markup: bell button in views/general/app_topbar.php; drawer DOM is built by
   assets/general/app_alerts.js and appended to <body>. Desktop: anchored panel
   under the topbar. Mobile (700px): full-height slide-in drawer + dim overlay.
   ========================================================================== */
.topbar-alerts-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    background: none;
    border: 1px solid rgba(255,255,255,.15);
    border-radius: 4px;
    color: #ccc;
    cursor: pointer;
    flex-shrink: 0;
}
.topbar-alerts-btn:hover { background: rgba(255,255,255,.08); color: #fff; }
.topbar-alerts-count {
    position: absolute;
    top: -6px;
    right: -6px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: 999px;
    background: #e8451c;
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 17px;
    text-align: center;
    box-shadow: 0 0 0 2px #1d1d1b; /* ring matching the topbar background */
}
.topbar-alerts-count[hidden] { display: none; }

/* Overlay: invisible click-catcher on desktop, dim scrim on mobile. */
.alerts-overlay {
    position: fixed;
    top: 0; right: 0; bottom: 0; left: 0;
    z-index: 1100;
    background: transparent;
}
.alerts-overlay[hidden] { display: none; }

.alerts-drawer {
    position: fixed;
    top: 64px;
    right: 14px;
    z-index: 1101;
    display: flex;
    flex-direction: column;
    width: 390px;
    max-width: calc(100vw - 28px);
    max-height: calc(100vh - 84px);
    background: #fff;
    border: 1px solid #e3e7eb;
    border-radius: 12px;
    box-shadow: 0 18px 50px rgba(15, 23, 42, 0.22);
    overflow: hidden;
    animation: alerts-drop 0.16s ease-out;
}
.alerts-drawer[hidden] { display: none; }
@keyframes alerts-drop {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: none; }
}

.alerts-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 13px 16px;
    border-bottom: 1px solid #eef1f4;
    flex-shrink: 0;
}
.alerts-title {
    flex: 1;
    min-width: 0;
    font-size: 14px;
    font-weight: 700;
    color: #1a1a2e;
}
.alerts-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    background: none;
    border: none;
    border-radius: 6px;
    color: #8a94a0;
    cursor: pointer;
    flex-shrink: 0;
}
.alerts-close:hover { background: #f0f2f5; color: #1a1a2e; }

.alerts-body {
    flex: 1;
    overflow-y: auto;
    padding: 6px;
}
.alerts-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border-radius: 8px;
    text-decoration: none;
    color: inherit;
    cursor: pointer;
}
.alerts-item:hover { background: #f2f7fc; }
.alerts-item-ico {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 8px;
    color: #0f3460;
    background: #eef3f9;
}
.alerts-item-main { flex: 1; min-width: 0; }
.alerts-item-name {
    display: block;
    font-size: 13px;
    font-weight: 600;
    color: #1a1a2e;
    overflow-wrap: break-word;
}
.alerts-item-fund {
    display: block;
    font-size: 12px;
    color: #6b7480;
    margin-top: 1px;
    overflow-wrap: break-word;
}
.alerts-item-count {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 26px;
    padding: 2px 9px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
    color: #0f3460;
    background: #eef3f9;
}

/* Loading / empty / error states inside the drawer body. */
.alerts-state {
    padding: 30px 16px;
    text-align: center;
}
.alerts-state-ico { display: inline-block; color: #16a34a; margin-bottom: 8px; }
.alerts-state-title { font-size: 13.5px; font-weight: 600; color: #555; }
.alerts-state-msg { font-size: 12.5px; color: #8a94a0; margin-top: 3px; }
.alerts-state .alerts-retry { margin-top: 12px; }

.alerts-footer {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 12px 16px;
    border-top: 1px solid #eef1f4;
    background: #f9fafb;
    font-size: 13px;
    font-weight: 600;
    color: #0f3460;
    text-decoration: none;
    flex-shrink: 0;
}
.alerts-footer:hover { background: #f2f7fc; }

@media (max-width: 700px) {
    .alerts-overlay { background: rgba(15, 23, 42, 0.45); animation: alerts-fade 0.18s ease-out; }
    .alerts-drawer {
        top: 0;
        right: 0;
        bottom: 0;
        width: 86%;
        max-width: 340px;
        max-height: none;
        border-radius: 0;
        border: none;
        animation: alerts-slide 0.2s ease-out;
    }
    body.alerts-open { overflow: hidden; }
}
@keyframes alerts-slide {
    from { transform: translateX(100%); }
    to   { transform: none; }
}
@keyframes alerts-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}


/* ==========================================================================
   Topbar Main Menu Drawer (shared hamburger + left slide-in menu)
   Markup: hamburger button + drawer include in views/general/app_topbar.php;
   drawer DOM in views/general/app_menu_drawer.php (rendered from the shared
   menu definition data/general/menu_items.php); behavior in
   assets/general/app_menu_drawer.js. The backdrop/panel layer (1200/1201)
   sits above the topbar (100), user dropdown (1000) and alerts drawer
   (1100/1101), and below modals (9000).
   ========================================================================== */
.topbar-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    background: none;
    border: 1px solid rgba(255,255,255,.15);
    border-radius: 4px;
    color: #ccc;
    cursor: pointer;
    flex-shrink: 0;
    margin-right: 16px;
}
.topbar-menu-btn:hover { background: rgba(255,255,255,.08); color: #fff; }

.menu-drawer-backdrop {
    position: fixed;
    top: 0; right: 0; bottom: 0; left: 0;
    z-index: 1200;
    background: rgba(15, 23, 42, 0.45);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    opacity: 0;
    transition: opacity 0.25s ease;
}
.menu-drawer-backdrop.is-open { opacity: 1; }
.menu-drawer-backdrop[hidden] { display: none; }

.menu-drawer {
    position: fixed;
    top: 0; bottom: 0; left: 0;
    z-index: 1201;
    display: flex;
    flex-direction: column;
    width: 400px;
    max-width: calc(100vw - 48px);
    background: #fff;
    box-shadow: 0 18px 50px rgba(15, 23, 42, 0.28);
    transform: translateX(-102%);
    transition: transform 0.28s cubic-bezier(0.32, 0.72, 0.33, 1);
}
.menu-drawer.is-open { transform: none; }
.menu-drawer[hidden] { display: none; }

.menu-drawer-head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 16px 20px;
    background: #1d1d1b;
    border-bottom: 3px solid #0f3460;
    flex-shrink: 0;
}
.menu-drawer-head-text { flex: 1; min-width: 0; }
.menu-drawer-eyebrow {
    display: block;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 1.4px;
    text-transform: uppercase;
    color: #8a94a0;
}
.menu-drawer-title {
    display: block;
    font-size: 16px;
    font-weight: 700;
    color: #fff;
    letter-spacing: .3px;
}
.menu-drawer-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    background: none;
    border: none;
    border-radius: 6px;
    color: #9aa3ad;
    cursor: pointer;
    flex-shrink: 0;
}
.menu-drawer-close:hover { background: rgba(255,255,255,.1); color: #fff; }

.menu-drawer-body {
    flex: 1;
    overflow-y: auto;
    padding: 12px 14px 18px;
}

.menu-drawer-section { margin-top: 18px; }
.menu-drawer-section:first-child { margin-top: 4px; }
.menu-drawer-section-title {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .8px;
    color: #8a909a;
    padding: 0 10px 7px;
    border-bottom: 1px solid #e8ebef;
    margin-bottom: 7px;
}

.menu-drawer-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px;
    border-radius: 8px;
    text-decoration: none;
    color: inherit;
}
.menu-drawer-item:hover { background: #f2f6fb; }
.menu-drawer-item[hidden] { display: none; }
.menu-drawer-item-ico {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 9px;
    background: #f0f4ff;
    color: #0f3460;
}
.menu-drawer-item--primary .menu-drawer-item-ico { background: #0f3460; color: #fff; }
.menu-drawer-item-main { flex: 1; min-width: 0; }
.menu-drawer-item-title {
    display: block;
    font-size: 13.5px;
    font-weight: 600;
    color: #1a1a2e;
}
.menu-drawer-item-chev {
    flex-shrink: 0;
    color: #c2c8cf;
    transition: color 0.15s, transform 0.15s;
}
.menu-drawer-item:hover .menu-drawer-item-chev { color: #0f3460; transform: translateX(2px); }

/* "You are here" highlight (set by app_menu_drawer.js) */
.menu-drawer-item.is-current { background: #eef3f9; }
.menu-drawer-item.is-current::before {
    content: '';
    position: absolute;
    left: 0;
    top: 9px;
    bottom: 9px;
    width: 3px;
    border-radius: 2px;
    background: #0f3460;
}
.menu-drawer-item.is-current .menu-drawer-item-title { color: #0f3460; }

/* Compact live-counts strip (hydrated from landing_counts.php) */
.menu-drawer-item-metrics {
    display: flex;
    flex-wrap: wrap;
    gap: 3px 10px;
    margin-top: 3px;
}
.menu-drawer-item-metrics[hidden] { display: none; }
.mdm-metric {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 11px;
    color: #667085;
    white-space: nowrap;
}
.mdm-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex-shrink: 0;
}
.mdm-dot--total     { background: #8899aa; }
.mdm-dot--action    { background: #3498db; }
.mdm-dot--funding   { background: #e67e22; }
.mdm-dot--completed { background: #27ae60; }
.mdm-count { font-weight: 700; }
.mdm-metric--attention {
    background: #fff3e0;
    color: #bf5b00;
    font-weight: 600;
    border-radius: 9px;
    padding: 1px 7px 1px 5px;
}
.mdm-metric--attention .mdm-dot { box-shadow: 0 0 0 2px #fff3e0; }
.mdm-skel {
    display: inline-block;
    width: 14px;
    height: 8px;
    border-radius: 4px;
    background: #e8ebef;
    animation: mdm-skel-pulse 1.2s ease-in-out infinite;
}
@keyframes mdm-skel-pulse {
    0%, 100% { opacity: .45; }
    50%      { opacity: 1; }
}

.menu-drawer-footer {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    padding: 13px 16px;
    border-top: 1px solid #eef1f4;
    background: #f9fafb;
    font-size: 13px;
    font-weight: 600;
    color: #0f3460;
    text-decoration: none;
    flex-shrink: 0;
}
.menu-drawer-footer:hover { background: #f2f7fc; }
/* MENU-3: "you are here" highlight for the footer "Open full menu" link — applied
   by app_menu_drawer.js when on app_main_menu.php itself (the footer is the only
   drawer entry for the home page and is NOT a .menu-drawer-item, so the item
   highlight rule above never matches it). Footer is centered, so use the same
   #eef3f9 wash as the items plus a heavier weight rather than the items' left bar. */
.menu-drawer-footer.is-current { background: #eef3f9; font-weight: 700; }

/* Staggered section entrance while the panel slides in (CSS-only) */
.menu-drawer-section,
.menu-drawer-footer {
    opacity: 0;
    transform: translateX(-10px);
    transition: opacity 0.32s ease, transform 0.32s ease;
}
.menu-drawer.is-open .menu-drawer-section,
.menu-drawer.is-open .menu-drawer-footer {
    opacity: 1;
    transform: none;
}
.menu-drawer.is-open .menu-drawer-section:nth-child(1) { transition-delay: 0.05s; }
.menu-drawer.is-open .menu-drawer-section:nth-child(2) { transition-delay: 0.11s; }
.menu-drawer.is-open .menu-drawer-section:nth-child(3) { transition-delay: 0.17s; }
.menu-drawer.is-open .menu-drawer-section:nth-child(4) { transition-delay: 0.23s; }
.menu-drawer.is-open .menu-drawer-section:nth-child(n+5) { transition-delay: 0.29s; }
.menu-drawer.is-open .menu-drawer-footer { transition-delay: 0.29s; }

body.menu-drawer-open { overflow: hidden; }

@media (max-width: 700px) {
    .menu-drawer { width: 88%; max-width: 360px; }
}

@media (prefers-reduced-motion: reduce) {
    .menu-drawer,
    .menu-drawer-backdrop,
    .menu-drawer-section,
    .menu-drawer-footer,
    .menu-drawer-item-chev { transition: none; }
    .mdm-skel { animation: none; }
}
/* Reduced-motion parity for the adjacent alerts drawer (pre-existing section
   above had no guard; kept here so both topbar drawers behave alike). */
@media (prefers-reduced-motion: reduce) {
    .alerts-drawer,
    .alerts-overlay { animation: none; }
}

/* ============================================================
   Download progress buttons (AppDownload, assets/general/app_download.js)
   Applied to a .btn while a data-dl-progress download is running.
   ============================================================ */
.btn.is-downloading {
    position: relative;
    overflow: hidden;
    pointer-events: none;
    opacity: 0.92;
}
.btn .dl-spinner {
    display: inline-block;
    width: 13px;
    height: 13px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    vertical-align: -2px;
    margin-right: 6px;
    opacity: 0.9;
    animation: spin 0.8s linear infinite;
}
.btn .dl-bar {
    position: absolute;
    left: 0;
    bottom: 0;
    height: 3px;
    width: 0;
    background: currentColor;
    opacity: 0.75;
    transition: width 0.2s ease;
}
.btn.is-dl-error {
    background: #c0392b;
    border-color: #c0392b;
    color: #fff;
}
.dl-hidden-frame { display: none; }
@media (prefers-reduced-motion: reduce) {
    .btn .dl-spinner { animation-duration: 1.6s; }
    .btn .dl-bar { transition: none; }
}

/* ==========================================================================
   Unread Data Room files badge (topbar icon + menu pills)
   Markup: topbar icon link in views/general/app_topbar.php (reuses the
   .topbar-alerts-btn square-button look + .topbar-alerts-count pill); the
   [data-dru-badge] pills sit beside the Data Rooms title in
   views/general/app_menu_drawer.php and views/main_menu/menu_view.php
   (rendered from the 'badge' key in data/general/menu_items.php).
   Hydrated by assets/general/app_dr_unread.js from
   api/data_rooms/unread_count.php — everything stays hidden at count 0.
   ========================================================================== */
/* The Investor Statements topbar icon + menu pills (app_stmts_unread.js) reuse
   the same square-button look and count-pill styles below; they're grouped here
   so the two badges never drift. */
.topbar-dru-btn, .topbar-stmts-btn { text-decoration: none; }
.topbar-dru-btn[hidden], .topbar-stmts-btn[hidden] { display: none; }

/* Count pill on a menu entry title (drawer item / main-menu tile). For Data
   Rooms (.dru-badge, app_dr_unread.js) and Investor Statements (.stmts-badge,
   app_stmts_unread.js) the menu entry links to the full list and clicking the
   pill deep-links to that item's Unread-filtered list; the To-Do entry
   (.todo-badge, app_alerts.js) already links to its list, so its pill is
   display-only. All three are cursor:pointer below. */
.dru-badge, .stmts-badge, .todo-badge {
    display: inline-block;
    min-width: 18px;
    padding: 1px 7px;
    margin-left: 6px;
    border-radius: 999px;
    background: #e8451c;
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
    vertical-align: 1px;
    cursor: pointer;
}
.dru-badge:hover, .stmts-badge:hover, .todo-badge:hover { background: #c93a16; }
.dru-badge[hidden], .stmts-badge[hidden], .todo-badge[hidden] { display: none; }

/* ------------------------------------------------------------------ *
 * Access Restricted modal (window.appAccessDenied — general.js)
 * Shown when a user attempts a screen-permission-gated client action (the
 * Export buttons today). Mirrors the page-level access-denied interstitial
 * (views/general/access_denied_view.php) but as a dismissible overlay. z-index
 * sits above the app's stacked modals (launchpad modals ~9000-9100) so it shows
 * even when an export button is clicked inside another modal.
 * ------------------------------------------------------------------ */
.app-deny-overlay {
    position: fixed;
    inset: 0;
    z-index: 9600;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    box-sizing: border-box;
    background: rgba(15, 52, 96, 0.45);
}
.app-deny-card {
    width: 100%;
    max-width: 420px;
    background: #fff;
    border-radius: 16px;
    box-shadow: 0 24px 60px rgba(15, 52, 96, 0.28);
    padding: 32px 28px 24px;
    text-align: center;
}
.app-deny-icon {
    width: 60px;
    height: 60px;
    margin: 0 auto 16px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #fdecea;
    color: #c0392b;
}
.app-deny-title {
    margin: 0 0 10px;
    font-size: 20px;
    font-weight: 700;
    color: #0f3460;
}
.app-deny-msg {
    margin: 0 0 22px;
    font-size: 14px;
    line-height: 1.6;
    color: #4a5568;
}
.app-deny-actions {
    display: flex;
    justify-content: center;
}
.app-deny-ok {
    min-width: 110px;
    justify-content: center;
}

/* ---- Idle-session timeout warning modal (assets/general/app_session_timeout.js) ----
   Mirrors the Access Restricted modal's look (same overlay/card/icon system) with
   an amber "clock" treatment and a two-button action row. z-index matches the
   modal band (above the menu drawer at 1201; see README §6). */
.app-idle-overlay {
    position: fixed;
    inset: 0;
    z-index: 9600;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    box-sizing: border-box;
    background: rgba(15, 52, 96, 0.45);
}
.app-idle-card {
    width: 100%;
    max-width: 420px;
    background: #fff;
    border-radius: 16px;
    box-shadow: 0 24px 60px rgba(15, 52, 96, 0.28);
    padding: 32px 28px 24px;
    text-align: center;
}
.app-idle-icon {
    width: 60px;
    height: 60px;
    margin: 0 auto 16px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #fef5e7;
    color: #d68910;
}
.app-idle-title {
    margin: 0 0 10px;
    font-size: 20px;
    font-weight: 700;
    color: #0f3460;
}
.app-idle-msg {
    margin: 0 0 22px;
    font-size: 14px;
    line-height: 1.6;
    color: #4a5568;
}
.app-idle-count {
    font-weight: 700;
    color: #0f3460;
}
.app-idle-actions {
    display: flex;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
}
.app-idle-actions .btn {
    min-width: 130px;
    justify-content: center;
}
@media (max-width: 480px) {
    .app-idle-actions { flex-direction: column-reverse; }
    .app-idle-actions .btn { width: 100%; }
}

/* ---- Document viewer modal (assets/general/app_doc_viewer.js) ----
   Rides the shared .modal-overlay backdrop (z-index 9000 band — above the
   topbar drawers, below nothing). The dialog owns the height; the PDF.js
   viewer.html iframe (PDFs) or the scrollable image wrap fills it. */
body.docv-open { overflow: hidden; }
.docv-dialog {
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 8px 30px rgba(0,0,0,.25);
    width: min(1100px, 96vw);
    height: min(900px, 92vh);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: modalIn .2s ease;
}
.docv-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    border-bottom: 1px solid #e8e8e8;
}
.docv-title {
    font-size: 15px;
    font-weight: 600;
    margin: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.docv-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
}
.docv-download { display: inline-flex; align-items: center; gap: 6px; }
/* #525659 = the PDF.js viewer's own background, so the frame edges never
   flash white while a document loads. */
.docv-body {
    flex: 1;
    min-height: 0;
    display: flex;
    background: #525659;
}
.docv-frame { flex: 1; width: 100%; height: 100%; border: 0; }
.docv-imgwrap {
    flex: 1;
    overflow: auto;
    background: #fff;
    padding: 12px;
}
.docv-img { display: block; max-width: 100%; height: auto; margin: 0 auto; }
.docv-img-msg {
    margin: 24px auto;
    text-align: center;
    color: #666;
    font-size: 14px;
}
@media (max-width: 700px) {
    .docv-dialog {
        width: 100%;
        height: 100%;
        max-width: none;
        border-radius: 0;
    }
}

/* ==========================================================================
   SHARED TAB BAR WITH LIVE COUNTS (.app-tabs / .app-tab / .app-tab-count)

   Added 2026-08-21 with the Tickets screen (app_tickets.php), which needs four
   lifecycle tabs each carrying its population size.

   WHY THIS IS SHARED AND NOT ANOTHER FEATURE-LOCAL COPY. This tree already had
   THREE hand-rolled tab bars — .contact-tab* (subscriptions step 1), .ms-tab*
   (MFA Exemptions) and .diag-tab* (Diagnostics) — and none of them carries a
   count badge. A fourth private copy would have been the third time the same
   strip was re-typed. The visual language is taken from .diag-tab so this reads
   as native rather than as an import.

   ⚠ COLOURS ARE LITERALS, NOT var(--tokens). This stylesheet declares NO custom
   properties (measured: zero `var(--` uses in 3,131 lines), so a borrowed
   var(--navy) would silently resolve to nothing. #0f3460 is this file's navy.

   Markup contract:
     <div class="app-tabs" role="tablist">
       <button class="app-tab is-active" role="tab" aria-selected="true">
         Label <span class="app-tab-count">0</span>
       </button>
     </div>
   The count is filled by the feature's JS from a SERVER-computed figure; nothing
   here assumes a number is present.
   ========================================================================== */

.app-tabs {
    display: flex;
    align-items: stretch;
    gap: 2px;
    border-bottom: 2px solid #dee2e6;
    margin-bottom: 16px;
    overflow-x: auto;
    /* A tab strip that scrolls sideways must not also show a scrollbar gutter on
       desktop, where it never overflows. */
    scrollbar-width: thin;
}
.app-tab {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 9px 16px;
    font-size: 13px;
    font-weight: 600;
    color: #666;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -2px;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.15s, border-color 0.15s;
}
.app-tab:hover { color: #1a1a2e; }
.app-tab.is-active {
    color: #0f3460;
    border-bottom-color: #0f3460;
}
.app-tab:focus-visible {
    outline: 2px solid #2962cc;
    outline-offset: -2px;
}

/* The population badge. Neutral until its tab is active, so the strip reads as
   one control rather than as a row of competing numbers. */
.app-tab-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    padding: 1px 6px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    line-height: 1.5;
    background: #eef1f4;
    color: #666;
}
.app-tab.is-active .app-tab-count {
    background: #f0f4ff;
    color: #0f3460;
}

@media (max-width: 700px) {
    .app-tab { padding: 9px 12px; font-size: 12.5px; }
}


/* ==========================================================================
   SHARED MARKDOWN SKIN (.app-md) — the default output of
   data/general/app_markdown.php (app_md_render / app_md).

   Added 2026-08-21 with the Tickets screen's record page, where authored prose
   (a dev request, its plan, its outcome, its per-page release notes) is written
   in Markdown. Before this tree had a renderer at all, that prose could only be
   shown as escaped pre-wrap text — punctuation soup.

   ⚠ THE CONTAINER MUST NOT SET `white-space: pre-wrap`. The renderer emits real
   block elements, so preserving the whitespace BETWEEN those tags adds a blank
   line before every paragraph and list. Wrapping is the browser's job here.
   (pre-wrap is still right for a RAW value — see .tk-files, a newline-separated
   file list, in assets/tickets/tickets.css.)

   ⚠ THE RENDERER ESCAPES ITS OWN OUTPUT. A view echoes app_md() RAW. Passing it
   through htmlspecialchars() a second time puts `&lt;p&gt;` on the page.

   ⚠ HEADING TAG vs HEADING CLASS. The renderer emits the source level as the
   CLASS and level+1 as the TAG (`##` -> `<h3 class="app-md-h2">`), because the
   host page already owns the h1. The rules below therefore target the CLASS.

   Type scale here is deliberately a SCALE: 14px body, headings stepping down
   from 16px, and a muted 13px for the quieter blocks. Uniform sizing is what
   made the client portal's first version of this page hard to read.
   ========================================================================== */

.app-md {
    font-size: 14px;
    line-height: 1.68;
    color: #333;
    overflow-wrap: anywhere;
}

/* First and last child never add outer space — the panel owns its padding. */
.app-md > *:first-child { margin-top: 0; }
.app-md > *:last-child  { margin-bottom: 0; }

.app-md p { margin: 0 0 11px; }

.app-md-h2,
.app-md-h3,
.app-md-h4 {
    margin: 20px 0 8px;
    color: #1a1a2e;
    font-weight: 700;
    line-height: 1.35;
}
.app-md-h2 { font-size: 16px; }
.app-md-h3 { font-size: 14.5px; }
.app-md-h4 {
    font-size: 12.5px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #888;
}

.app-md-l { margin: 0 0 11px; padding-left: 22px; }
.app-md-l li { margin: 0 0 5px; }
.app-md-l li:last-child { margin-bottom: 0; }
/* A nested list tightens up rather than repeating the outer spacing. */
.app-md-l .app-md-l { margin: 5px 0 0; }

.app-md-c {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12.5px;
    padding: 1px 5px;
    border-radius: 4px;
    background: #eef1f4;
    color: #333;
}

.app-md-pre {
    margin: 0 0 12px;
    padding: 11px 13px;
    border: 1px solid #e0e0e0;
    border-radius: 7px;
    background: #f8f9fb;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12.5px;
    line-height: 1.55;
    /* A code block is the ONE place long lines scroll rather than wrap — a
       wrapped command is a command you cannot copy correctly. */
    white-space: pre;
    overflow-x: auto;
}

.app-md-q {
    margin: 0 0 12px;
    padding: 2px 0 2px 13px;
    border-left: 3px solid #b3cde8;
    color: #555;
}

.app-md-hr { border: 0; border-top: 1px solid #e0e0e0; margin: 18px 0; }

.app-md-tscroll { overflow-x: auto; margin: 0 0 12px; }
.app-md-t { width: 100%; border-collapse: collapse; font-size: 13px; }
.app-md-t th {
    text-align: left;
    padding: 7px 12px 7px 0;
    border-bottom: 1px solid #e0e0e0;
    color: #888;
    font-size: 11.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
}
.app-md-t td {
    padding: 7px 12px 7px 0;
    border-bottom: 1px solid #eee;
    vertical-align: top;
}
.app-md-t tr:last-child td { border-bottom: 0; }

.app-md strong { font-weight: 700; color: #1a1a2e; }
.app-md em { font-style: italic; }
.app-md a { color: #0f3460; text-decoration: underline; text-underline-offset: 2px; }
.app-md a:hover { color: #2962cc; }

/* The quieter variant, for a sub-block inside a card (an item's plan, say). */
.app-md--sm { font-size: 13px; line-height: 1.6; color: #555; }
.app-md--sm .app-md-h2 { font-size: 14px; }
.app-md--sm .app-md-h3 { font-size: 13px; }

/* ============================================================================
   SHARED VOCABULARY PORTED FROM THE CLIENT PORTAL, 2026-08-25
   ----------------------------------------------------------------------------
   Everything from here to the end of the file arrived with the Tickets analytics
   dashboard (app_tickets.php?page=analytics). Source of every block:

       2_new_client_portal/_release_client_20260330_v1.35/app/assets/general/general.css

   That tree OWNS these classes — it is where they are developed and where a fix
   belongs first. The rules below are its copy, so re-porting a later version is a
   diff and not a merge.

   ⚠ WHY THIS IS APPENDED AND NOTHING ABOVE IS TOUCHED. Every screen in this tree
   depends on the rules above, and the client's general.css is roughly twice this
   one — "make them match" would mean replacing the shared stylesheet of a live
   application to serve one new dashboard. Instead: only NEW selectors are added,
   so no existing rule changes its meaning, and nothing above this banner was
   edited. If a class below ever collides with one above, the one above wins on
   nothing but source order — check before adding to this section.

   WHAT IS HERE, and who needs it:
     :root tokens          the design tokens the blocks below and assets/general/
                           app_hc.css / app_hc_theme.css reference. THIS TREE HAD
                           NO TOKENS AT ALL (it writes literal colours), so this is
                           the first :root block in the file — see its own note.
     .qa-only              the developer-only marker (the QA pill + three forms)
     .app-subnav*          the family section bar — Tickets' Requests / Analytics
                           band, views/tickets/tickets_subnav_partial.php
     .app-panel* .app-pill the span-title section card and its head pill
     .app-figures/.app-figure*  the KPI band the dashboard's seven tiles use
     .app-drill*           the drill-through modal (assets/general/app_drill.js),
                           which every chart's "View as table" opens

   NOT ported, and deliberately: .app-card (this tree's `.tk-card` and the other
   feature-local cards already cover it), and the client's own `.app-grid` growth
   model, which this tree's general.css states differently near its own foot.
   ============================================================================ */

/* ---- THE DESIGN TOKENS ----
   ⚠ THIS IS THE FIRST :root BLOCK IN THIS FILE, AND IT IS DELIBERATELY MINIMAL.
   The client portal drives its whole stylesheet from ~180 tokens; this tree writes
   literal values throughout and has never had any. Importing all 180 would create
   two vocabularies for the same colour with nothing to keep them agreeing, so what
   is declared here is EXACTLY the set the ported blocks and the two ported
   app_hc*.css files reference — nothing speculative.

   Values are the client's, unchanged, so a chart drawn here and the same chart
   drawn there are the same chart. A custom property is inert until something
   references it, so adding these cannot affect one existing rule above.

   ⚠ DO NOT start converting the literals above to these. That is a whole-file
   change to a live shared stylesheet, and it is not this dashboard's to make. */
:root {
    /* Brand + text */
    --accent: #e8451c;          /* brand orange — accents, active underline */
    --navy: #0f3460;            /* brand navy — headings, primary, active */
    --text: #1a2332;            /* primary text */
    --text-soft: #33415c;
    --text-muted: #667085;      /* secondary text */

    /* Rules and fills */
    --hairline: #e3e7ec;        /* standard card/table border */
    --hairline-soft: #eef1f5;   /* lighter inner divider */
    --head-bg: #f7f9fb;         /* flat panel-head / table-head fill */

    /* Small-control ladder (the chart cell's "View as table" button, the
       dashboard's range pills) */
    --control-border: #cbd3dc;
    --control-focus: #2563eb;
    --control-radius: 5px;
    --control-h-sm: 30px;
    --control-px-sm: 11px;
    --control-font-sm: 12px;

    /* The gap between two controls in an action row */
    --control-gap: 6px;

    /* Status pill inks — amber/red for the chart layer's warning rows, gray for
       the dashboard's per-panel population pill (assets/tickets/tickets.css) */
    --pill-amber-bg: #fff1e6;
    --pill-amber-bd: #f6cfae;
    --pill-red-fg: #b3261e;
    --pill-gray-bg: #eef1f5;
    --pill-gray-fg: #52627a;
    --pill-gray-bd: #d8dee7;

    /* The DESTRUCTIVE button (see .btn-danger at the foot of this section) */
    --btn-danger-bg: #c0392b;
    --btn-danger-bg-hover: #a93226;

    /* The QA / developer-only marker */
    --qa-accent: #e8451c;
    --qa-accent-ink: #ba3716;
    --qa-accent-wash: #fff4f0;

    /* The section bar */
    --subnav-surface: #ffffff;      /* the band itself */
    --subnav-edge: #dde3ea;         /* its bottom hairline */
    --subnav-tab: #3f4d63;          /* idle tab label (8.56:1 on white) */
    --subnav-tab-icon: #7a8899;     /* idle tab icon — a step back from the label */
    --subnav-hover: #f4f7fb;        /* hover / open-door wash */
    --subnav-marker: var(--accent); /* active-tab underline */
}

/* ⚠ ONE TOKEN IS NOT DECLARED HERE ON PURPOSE: --app-grid-wrap-px. The client's
   AppGrid publishes it per-`th` at runtime as the px it capped a wrapping column
   at, and this tree's app_grid.js does not. Its only consumer below reads it as
   `var(--app-grid-wrap-px, 0)` — the fallback IS the unported behaviour, so the
   rule degrades to "no minimum width" instead of breaking. Declaring a fixed
   value here would be worse than not declaring it: it would apply one column's
   cap to every column. */


/* ---- ported block: .qa-only ---- */
/* ---- `.qa-only` — THE marker for a control or value that exists ONLY on a
   QATESTING url (operator standard, 2026-08-18: "any button or value that is
   only for display in QA_TESTING should be styled ... outlined in an orange
   color ... very obvious and consistent").

   IT IS THE SECOND HALF OF A TWO-PART CONTRACT, AND THE FIRST HALF IS SERVER-SIDE.
   The class SAYS "developer-only"; `app_is_qa_url()` (data/general/app_db.php) is
   what MAKES it developer-only. A control the browser is merely told to conceal is
   still in the page source and still clickable from a console, and these controls
   open panels that publish per-proc database timings — so the gate must be a PHP
   `if` that renders nothing, exactly as app_connect_insights.php's $ciShowPerf
   does. Never `.qa-only { display: none }` in production, and never this class on
   an element a client can see: an orange ring that a client CAN reach is a lie
   about the environment, which is worse than no ring at all.

   THREE FORMS, one vocabulary — picked by what the element ALREADY IS, never by
   the call site, which writes `qa-only` and nothing else:
     · a BUTTON (`btn qa-only`) keeps its variant's fill — the operator kept these
       as ghost buttons on purpose, they are not promoted to actions — and spends
       its existing border on the orange, widened to 2px. `box-sizing: border-box`
       + `min-height` means that extra pixel costs no layout;
     · a BLOCK (`app-panel qa-only` / `app-card qa-only`) takes the accent as a 3px
       LEFT RULE, the shape `.page-hero` has used all along. A 2px ring round a
       full-width card is loud without being informative;
     · anything ELSE (a value, a pill, a menu row) has no border to spend, so it
       takes an `outline`, which reserves no space and therefore cannot move a
       layout that was signed off without it.
   All three carry the same "QA" pill, which is what makes them read as one marker
   rather than as three orange things.

   The pill is a `::after`, so it needs no markup change at the call site and
   cannot be forgotten — but that also means it does NOT reach an element whose
   content is replaced by JS. Put the class on the stable wrapper, not on the node
   a renderer rewrites. `content` IS announced by most screen readers; that is
   wanted here (a developer control should say so), which is why the string is the
   word "QA" and not a glyph.

   ⚠ THE BASE CLASS SETS THE PILL AND NOTHING ELSE. Every box property belongs to
   one of the three forms below, because this class lands on elements that already
   have a signed-off box: an innocent-looking `border-radius: 3px` on `.qa-only`
   would out-order `.btn`'s 5px (this block is BELOW it) and quietly re-corner
   every QA button, and it would race `.app-card, .app-panel`'s 12px on source
   order alone. */
.qa-only::after {
    content: 'QA';
    display: inline-block;
    margin-left: 6px;
    padding: 0 5px;
    border-radius: 3px;
    background: var(--qa-accent-ink);
    color: #fff;
    font-size: 9.5px;
    font-weight: 800;
    line-height: 15px;
    letter-spacing: .6px;
    text-transform: uppercase;
    vertical-align: 1px;
    /* The pill is a LABEL, never a target — a click on it must reach the control
       it sits inside (it is inside the button's own box). */
    pointer-events: none;
}
/* Form 1 — anything with no border of its own (a value, a pill, a menu row): the
   layout-neutral `outline`.
   THE EXCLUSION LIST IS THE WHOLE MECHANISM, and it is written as `:not()` rather
   than as a second opt-in class so a call site never has to CHOOSE a form — one
   word, `qa-only`, and the right treatment falls out of what the element already
   is. It also has to carry every form below it: `:not()` contributes its
   argument's specificity, so this selector is (0,5,0) and would otherwise beat
   the (0,2,0) button and block rules and paint a second ring around them. */
.qa-only:not(.btn):not(.btn-card-action):not(.app-panel):not(.app-card) {
    outline: 2px solid var(--qa-accent);
    outline-offset: 2px;
    /* ⚠ AND NOTHING ELSE — in particular NO `border-radius`. `outline` already
       follows whatever radius the element has, and this selector is (0,5,0): a
       "harmless" 3px here out-ranks the 9px on `.dropdown-menu li a` (0,1,2) and
       silently re-corners the topbar's UI Standards row. An element with no radius
       of its own gets a square ring, which reads perfectly well as a marker. Do
       not add a box property to a rule whose only job is to annotate. */
}
/* The button form. (0,2,0) compounds, declared AFTER every variant and every size
   modifier so `btn btn-ghost qa-only` / `btn btn-outline qa-only` / a card action
   all land on the orange without !important — the same "compose, don't fork" shape
   as `.btn-card-action.btn-danger` above. The ink moves too: a 2px ring alone
   still reads as "a button with a border" at a glance, and the ticket asked for
   very obvious. */
.btn.qa-only,
.btn-card-action.qa-only {
    border-width: 2px;
    border-color: var(--qa-accent);
    color: var(--qa-accent-ink);
}
.btn.qa-only:hover,
.btn-card-action.qa-only:hover {
    background: var(--qa-accent-wash);
    border-color: var(--qa-accent-ink);
    color: var(--qa-accent-ink);
}
/* A QA-only PANEL (the "Where the time goes" splits) is a whole block, and a 2px
   ring round a full-width card is loud without being informative — it takes the
   accent as a left rule instead, the shape the app already uses for a hero, plus
   the pill on its head. Same class, so a screen still marks a QA surface with one
   word. */
.app-panel.qa-only,
.app-card.qa-only {
    border-left: 3px solid var(--qa-accent);
}
.app-panel.qa-only::after,
.app-card.qa-only::after { content: none; }
/* ⚠ THE PILL HANGS ON THE HEAD'S FIRST CHILD, NOT ON THE HEAD. `.app-panel-head`
   is `flex-direction: column; gap: 10px` (one child = one row, the 2026-07-24
   standard), so an `::after` on the head itself is a FLEX ITEM and lands on its
   own row 10px BELOW the title — a floating orange chip under the heading. Hung on
   the head's first child (`<h2>` on the three dashboards, `<span>` on the
   standards page) it is an inline box at the end of the heading text, which is
   where it belongs. `:first-child` rather than `h2` so both shapes are covered. */
.app-panel.qa-only > .app-panel-head > :first-child::after,
.app-card.qa-only > .app-card-head > :first-child::after {
    content: 'QA';
    display: inline-block;
    vertical-align: 2px;
    margin-left: 8px;
    padding: 0 5px;
    border-radius: 3px;
    background: var(--qa-accent-ink);
    color: #fff;
    font-size: 9.5px;
    font-weight: 800;
    line-height: 15px;
    letter-spacing: .6px;
    text-transform: uppercase;
    pointer-events: none;
}

/* ---- ported block: .app-subnav* ---- */
/* ---- App Subnav (the family section bar — a SECOND CHROME ROW) ----
   The section navigation shared across a feature family's landing pages (e.g.
   Invoices / Payments / Auto Pay / Recipients / Fees). Promoted VERBATIM from
   Opus Invoices (was .oi-subnav*, 2026-07-23); the reference server partial is
   views/opus_invoices/subnav_partial.php: tabs are permission-gated server-side
   and the ACTIVE tab is derived per page (aria-current="page"). Two
   presentations, swapped at the 700px breakpoint below: the desktop tab row
   (.app-subnav) and a compact <details> section dropdown (.app-subnav-select) —
   no horizontal scrolling on phones.

   ══ THE 2026-07-31 STANDARD: THIS BAR IS CHROME, NOT PAGE CONTENT ══
   It renders as a full-bleed band flush under the topbar. It REPLACES the
   previous order (hero -> .page-actions -> .app-subnav -> content), in which the
   bar was the first thing inside .page-wrap and left 32px of page background
   stranded between the two bars. The old look was a light "folder tab" strip
   sitting on a hairline baseline; that costume is retired.

   ══ THE 2026-08-17 REDESIGN: A LIGHT BAND, NOT A SECOND DARK SLAB ══
   The band's POSITION is the 2026-07-31 standard above and is unchanged; its
   SURFACE is not. It was a navy slab (a gradient from #0c2646 into --navy) that
   absorbed the topbar's rule so the two rows read as one block of chrome. The
   operator's brief: that "feels visually dated and creates unnecessary visual
   weight". It is now a WHITE surface under the topbar's own 3px navy rule, which
   is once again the hard edge between them — see --subnav-* at the top of this
   file for the full reasoning. What changed, and what to keep in mind:
     · The two rows now read as SEPARATE (dark app frame, then a light section
       bar belonging to the page) rather than as one graded chrome block. The
       `body:has(.app-subnav-bar) .topbar` rule that repainted the topbar's rule
       to blend them is GONE — a band page's topbar is now identical to every
       other page's.
     · Tabs are text-first: 13.5px/500 near-black on white, a 52px row for a
       comfortable click target, and the current tab marked by weight + navy +
       a 3px --accent underline with NO fill behind it. Do not put a fill back
       on .is-active — the underline IS the marker, and a fill on white reads as
       a second, competing one.
     · Everything painted for the dark band had to flip: the tab labels/icons,
       the group summary + chevron, the whole mobile <details> summary. The two
       white overlay PANELS (.app-subnav-group-menu, .app-subnav-select-menu)
       were already light and now sit on a light band, so they gained a top
       border and a stronger shadow to stay separable from it.

   REQUIRED MARKUP — a body-level SIBLING of nav.topbar, placed immediately
   after it and BEFORE .page-wrap (never inside it: a child of the width-capped
   wrap cannot go full-bleed without viewport hacks that the max-content
   grid-grow model at the foot of this file breaks):

       <nav class="topbar">…</nav>
       <div class="app-subnav-bar">
         <div class="app-subnav-bar-inner">
           <nav class="app-subnav">…tabs…</nav>
           <details class="app-subnav-select">…mobile…</details>
         </div>
       </div>
       <div class="page-wrap …">…hero, .page-actions, content…</div>

   FOUR load-bearing facts behind the geometry:
   1. Global `box-sizing: border-box` (top of this file) means .topbar's
      `height:56px` INCLUDES its 3px navy bottom rule, and
      `body:has(nav.topbar){padding-top:56px}` reserves exactly 56px — so the
      first IN-FLOW body child lands on that rule's bottom edge. Flush, with no
      magic number.
   2. The band is deliberately NOT position:fixed. 56px is hard-coded as a sticky
      offset in FOUR places — `body:has(nav.topbar)` above, the grid's sticky
      thead `top:56px` near the foot of this file, and app_grid.js:457
      (`topOffset` default) + :1390 (the float-head clone). A fixed band would
      tuck every grid's sticky header underneath itself, and since only SOME
      pages carry a sub-nav it would need topOffset plumbed per page. In flow it
      scrolls away exactly as the old in-page bar did.
   3. body is `display:flex; flex-direction:column`, so the band stretches to the
      body's width for free — including on a grid page where the body grows to
      max-content, so it never leaves a bare strip when the page scrolls right.
   4. The band sits DIRECTLY on .topbar's 3px navy rule, which since the
      2026-08-17 redesign is the separator between the dark app frame and the
      light section bar. There is no longer any band-page override of that rule
      (the 2026-08-05 `body:has(.app-subnav-bar) .topbar` recolour, which
      repainted it to the band's own top tone so the two blended, is retired) —
      every page's topbar now renders identically, band or no band. */
.app-subnav-bar {
    position: relative;   /* positioning context for the ≤700px menu overlay */
    z-index: 60;          /* over page content + grid chrome (20-50); under .topbar (100) */
    /* A clean white surface (see --subnav-* at the top of this file). The
       hairline + the soft drop shadow are what lift the band off the page
       beneath it now that it no longer has a dark fill to do that job — without
       them a white band on a #f0f2f5 page has no bottom edge at all. */
    background: var(--subnav-surface);
    border-bottom: 1px solid var(--subnav-edge);
    box-shadow: 0 2px 8px rgba(15, 28, 54, .07);
}
/* Line the tabs up with the page content below by reusing .page-wrap's own box
   (max-width + the asymmetric 24/20 gutters), so a tab's left edge and the
   hero's left edge share a column. */
.app-subnav-bar-inner {
    max-width: 1750px;
    margin: 0 auto;
    padding: 0 20px 0 24px;
}
/* Wide (page-scrolling) grid pages: keep the tabs parked at the viewport-left
   when the page scrolls right instead of panning away with the content. Same
   `sticky; left:<offset>` trick and the same `:has()` discriminator used for
   .app-footer-inner near the foot of this file — but applied to the NAV, not to
   the width-capped inner wrapper, which is the whole point:

   position:sticky is clamped to its CONTAINING BLOCK, so a sticky element can
   only slide (container width - own width). Sticking the 1750px inner inside a
   band grown to 2680px caps the slide at 930px, and a 1415px scroll left the
   tabs 485px off-screen — measured 2026-07-31, which is how this rule got
   rewritten. Sticking the NAV instead makes the slide range (band - nav), and
   since the nav is narrower than the viewport whenever it fits on screen at all,
   its range always EXCEEDS the page's scroll range (body - viewport). So the pin
   holds for the FULL scroll rather than most of it. NOTE: .app-footer-inner
   still carries the original clamp; the same rewrite would fix it.
   The offset re-adds .page-wrap's 24px gutter so the pinned position is
   continuous with the resting one — no 24px jump when sticking starts. */
body:has(.app-grid:not(.app-grid--scrollx)) .app-subnav-bar-inner {
    max-width: none;
    margin-inline: 0;
}
body:has(.app-grid:not(.app-grid--scrollx)) .app-subnav-bar-inner > .app-subnav,
body:has(.app-grid:not(.app-grid--scrollx)) .app-subnav-bar-inner > .app-subnav-select {
    position: sticky;
    left: max(24px, calc((100vw - 1750px) / 2 + 24px));
    /* Load-bearing: .app-subnav is a block-level flex container, so it would
       otherwise stretch to the whole grown band — width == container means a
       slide range of ZERO and sticky silently does nothing. Shrink-to-fit gives
       it the range. (It also stops the tabs wrapping on a grown page, which is
       what you want there: the page already scrolls horizontally.) */
    width: max-content;
}
/* With the band supplying the chrome edge, .page-wrap's 32px top padding reads
   loose against it — 24px sits the hero closer without crowding the shadow.
   Keyed on the BAND's presence, so a family's sub-pages that carry no sub-nav
   (record pages, forms, and chrome-less direct-access views) keep the shared
   32px: there is no band above them to tighten against. */
body:has(.app-subnav-bar) .page-wrap { padding-top: 24px; }

/* Desktop tabs. Full-height rows in the band, current section marked by a 3px
   --subnav-marker underline on the band's bottom edge — it points down into the
   page it opens. flex-wrap stays ON: a family with more or longer tabs than fit
   degrades to a two-row band rather than overflowing or clipping a door. */
.app-subnav {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 0;
    margin: 0;
}
/* TEXT-FIRST (2026-08-17). The label carries the tab; the icon is a quiet aid
   beside it and the row has no chrome of its own at rest. 13.5px/500 near-black
   on white is legible where 12.5px/600 at 72% white on navy was not, and the
   52px row (was 42) is a comfortable pointer target that also gives the band the
   breathing room the operator asked for. The 3px transparent inset is the active
   marker's rest state — it is declared here so the underline has a slot to
   animate into rather than appearing from nothing. */
.app-subnav-tab {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    font-size: 13.5px;
    font-weight: 500;
    color: var(--subnav-tab);
    text-decoration: none;
    background: none;
    border: none;
    border-radius: 0;
    padding: 0 18px;
    min-height: 52px;
    white-space: nowrap;
    box-shadow: inset 0 -3px 0 transparent;   /* the active marker's rest state */
    transition: background .15s, color .15s, box-shadow .15s;
}
.app-subnav-tab svg { color: var(--subnav-tab-icon); flex-shrink: 0; }
/* The SECTION icon only (:first-of-type), sized up to 16px against the 13.5px
   label so the pair reads as a modern nav item rather than a glyph the same size
   as its own text. Deliberately NOT `.app-subnav-tab svg`, which would also
   catch the grouped tab's chevron (13px by design) — the off-site new-tab glyph
   is already safe, being wrapped in a .app-subnav-tab-newtab span rather than a
   direct child. Set in CSS because the icons are authored at 14px inside the
   partials' raw SVG markup, in menu_families.php and in eight other places. */
.app-subnav-tab > svg:first-of-type { width: 16px; height: 16px; }
/* Hover previews the marker at a quarter strength — the same 3px slot the active
   tab fills, so the underline reads as sliding in rather than appearing. */
.app-subnav-tab:hover {
    background: var(--subnav-hover);
    color: var(--navy);
    box-shadow: inset 0 -3px 0 #c8d3e0;
}
.app-subnav-tab:hover svg { color: var(--navy); }
/* .is-active is LAST at equal specificity, so it also wins on hover — the current
   tab never flickers to the hover marker. NO FILL: on white the underline alone
   is the marker (weight + navy do the rest), and a wash behind it would read as a
   second, competing one. That is also why the fill has to be reset explicitly —
   without it the tab would keep the hover wash while the pointer is on it. */
.app-subnav-tab.is-active {
    background: none;
    color: var(--navy);
    font-weight: 700;
    box-shadow: inset 0 -3px 0 var(--subnav-marker);
}
.app-subnav-tab.is-active svg { color: var(--navy); }
/* The band is a light surface now, so these take the app's standard focus ring
   (the white one they used to carry was for navy and is invisible here). Inset,
   because a full-height tab's ring would otherwise be clipped by the band edge. */
.app-subnav-tab:focus-visible,
.app-subnav-select-summary:focus-visible,
.app-subnav-group-summary:focus-visible {
    outline: 2px solid var(--control-focus);
    outline-offset: -2px;
}
/* ---- An OFF-SITE tab ----
   Added 2026-08-12 with the NAV consolidation, whose "Reports on ShareFile" item is
   specified as an external link rather than an in-app screen. Same tab chrome, plus
   a trailing new-tab glyph so a door that LEAVES the application says so before it
   is clicked (the topbar's own external entries carry the identical glyph as
   `.dropdown-newtab`). Such a tab never takes `.is-active` and never `aria-current`
   — you are never "on" it — so the marker slot below it simply stays empty.
   The negative pull cancels most of the parent's 9px flex gap, so the glyph reads as
   part of the label rather than as a second icon; on the white mobile panel it
   inherits navy from `.app-subnav-select-item svg`, like every other icon there. */
.app-subnav-tab-newtab {
    display: inline-flex;
    align-items: center;
    margin-left: -5px;
    opacity: .8;
}

/* ---- Grouped tab (a sub-menu INSIDE the band) ----
   Added 2026-08-10 with the Treasury consolidation, which folds a family's three
   configuration screens (SmartWire / Wire Templates / Holdback Settings) behind a
   single "Settings" door instead of spending three of the bar's tab slots on
   screens nobody visits daily.

   Use it SPARINGLY and only for a real sub-group: a section that is one of the
   family's working surfaces belongs on the bar as a plain tab, because a door
   behind a door costs a click and hides the label from a scan. One group of
   settings-ish screens at the END of the bar is the shape this exists for.

   MARKUP — a native <details> (no JS needed to open; general.js only adds the
   outside-click / ESC dismissal every dropdown owes the user):

       <nav class="app-subnav">
         <a class="app-subnav-tab …">…plain tabs…</a>
         <details class="app-subnav-group">
           <summary class="app-subnav-tab app-subnav-group-summary">
             <svg/> Settings <svg class="app-subnav-group-chevron"/>
           </summary>
           <div class="app-subnav-group-menu">
             <a class="app-subnav-select-item" href="…"><svg/> …</a>
           </div>
         </details>
       </nav>

   The summary IS a tab (it carries .app-subnav-tab, so it inherits the row
   geometry, hover and the active marker) and takes .is-active when the CURRENT
   page is one of its members — otherwise the band would show no marker at all on
   three of the family's screens. The panel reuses .app-subnav-select-item, the
   same white-overlay link the mobile section dropdown uses, because it is the
   same object: a section link on a light panel over the band. Only the size
   differs (mobile is a touch target; this is a pointer menu).

   Below 700px the whole group disappears with .app-subnav — the mobile <details>
   lists the members inline under an .app-subnav-select-label caption instead, so a
   phone never has to open two dropdowns to reach a screen. */
.app-subnav-group {
    position: relative;   /* positioning context for the panel */
    display: flex;
}
.app-subnav-group-summary {
    list-style: none;
    cursor: pointer;
}
.app-subnav-group-summary::-webkit-details-marker { display: none; }
.app-subnav-group-chevron {
    color: var(--subnav-tab-icon);
    transition: transform .15s;
}
.app-subnav-group[open] > .app-subnav-group-summary {
    background: var(--subnav-hover);
    color: var(--navy);
}
.app-subnav-group[open] > .app-subnav-group-summary svg { color: var(--navy); }
.app-subnav-group[open] .app-subnav-group-chevron { transform: rotate(180deg); }
/* Hangs off the band's bottom edge, left-aligned with its own tab. min-width
   keeps a two-word label from a ragged 90px panel; max-width stops a long one
   from running off a narrow viewport.
   The top border and the deeper shadow arrived with the 2026-08-17 light band:
   a white panel hanging off a NAVY band separated itself, but off a white one it
   would just be a shadow with no edge. `top:100%` lands the panel exactly on the
   band's own 1px bottom hairline, so the two coincide and still read as one
   line — do not add a margin-top to "clear" it, that reveals a double rule. */
.app-subnav-group-menu {
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 210px;
    max-width: min(320px, 90vw);
    background: #fff;
    border: 1px solid #d8dee6;
    border-radius: 0 0 12px 12px;
    overflow: hidden;
    box-shadow: 0 10px 22px rgba(15, 28, 54, .13);
}
.app-subnav-group-menu .app-subnav-select-item {
    padding: 9px 13px;
    font-size: 12.75px;
    white-space: nowrap;
}

/* Mobile section dropdown (shown ≤700px; hidden on desktop). Lives in the same
   band, so it goes LIGHT with it (2026-08-17) — the summary was white-on-navy
   and would have been invisible on the new surface. The row is the phone's whole
   section navigation, so it keeps the heavier 14px/700 treatment: unlike a
   desktop tab it has no siblings to be read against. */
.app-subnav-select { display: none; }
.app-subnav-select-summary {
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 12px 20px 12px 24px;
    min-height: 50px;
    background: none;
    border: none;
    border-radius: 0;
    font-size: 14px;
    font-weight: 700;
    color: var(--navy);
    cursor: pointer;
}
.app-subnav-select-summary::-webkit-details-marker { display: none; }
.app-subnav-select[open] .app-subnav-select-summary { background: var(--subnav-hover); }
.app-subnav-select-current { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
/* Navy, matching .app-subnav-select-item svg in the panel this row opens — the
   app's language for an icon on a light surface. (It used to take the marker
   colour because the marker was the band's white; --accent here would put an
   orange glyph beside a navy label for no reason.) */
.app-subnav-select-current svg { color: var(--navy); flex-shrink: 0; }
.app-subnav-select-chevron { color: var(--subnav-tab-icon); flex-shrink: 0; transition: transform .15s; }
.app-subnav-select[open] .app-subnav-select-chevron { transform: rotate(180deg); }
/* The section list OVERLAYS the page rather than pushing the hero down — the band
   is the positioning context and its z-index puts the panel over the content.
   Scrollable so a long family cannot run off a short viewport. Full border +
   deeper shadow since 2026-08-17, for the same reason as .app-subnav-group-menu:
   a white panel needs its own edge now that the band behind it is white too. */
.app-subnav-select-menu {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 0;
    background: #fff;
    border: 1px solid #d8dee6;
    border-radius: 0 0 12px 12px;
    overflow: hidden;
    max-height: calc(100vh - 150px);
    overflow-y: auto;
    box-shadow: 0 10px 22px rgba(15, 28, 54, .13);
}
.app-subnav-select-item {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 12px 14px;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-soft);
    text-decoration: none;
    border-bottom: 1px solid var(--hairline-soft);
}
.app-subnav-select-item:last-child { border-bottom: none; }
/* Caption row for a grouped tab's members (see .app-subnav-group): the desktop
   sub-menu is flattened into this list on mobile, so the group name has to survive
   as a heading or its members read as three more top-level sections. Not a link —
   the group itself has no page. */
.app-subnav-select-label {
    display: block;
    padding: 10px 14px 4px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--text-muted);
    background: var(--head-bg);
    border-bottom: 1px solid var(--hairline-soft);
}
/* The members themselves — indented so the caption reads as their heading. A
   modifier, not a sibling selector: a group is not necessarily last in the list. */
.app-subnav-select-item--sub { padding-left: 26px; }
/* These sit on the WHITE overlay panel, not the band, so the marker colour would
   be invisible — they take navy, matching .dropdown-ico in the topbar's own menu
   (the app's language for an icon on a light panel). */
.app-subnav-select-item svg { color: var(--navy); flex-shrink: 0; }
.app-subnav-select-item:hover { background: var(--head-bg); }
/* The panel's "you are here". The 3px --subnav-marker on the LEADING edge is the
   band's underline turned through 90° — same marker, same colour, rotated to the
   axis this list runs on (2026-08-17; before that the tint + weight were the only
   cue, which a scan can miss in a list of otherwise identical rows). */
.app-subnav-select-item.is-active {
    background: var(--subnav-hover);
    color: var(--navy);
    font-weight: 700;
    box-shadow: inset 3px 0 0 var(--subnav-marker);
}
@media (max-width: 700px) {
    /* Swap the tab row for the compact dropdown — no horizontal scroll. */
    .app-subnav { display: none; }
    .app-subnav-select { display: block; margin: 0; }
    /* The summary row spans the band edge-to-edge at this width. */
    .app-subnav-bar-inner { padding: 0; }
    /* Mobile is fully fluid and never page-scrolls horizontally (the body-grow
       release near the foot of this file), so drop the wide-page pin — mirrors
       what the same breakpoint does for .app-footer-inner. */
    body:has(.app-grid:not(.app-grid--scrollx)) .app-subnav-bar-inner > .app-subnav,
    body:has(.app-grid:not(.app-grid--scrollx)) .app-subnav-bar-inner > .app-subnav-select {
        position: static;
        width: auto;
    }
}

/* ---- Inner Sections ---- */

/* ---- ported block: .app-panel* / .app-pill ---- */
/* ---- App Panel (the span-title section card) ----
   Consolidated 2026-07-23 from the 17 byte-identical pre-.app-card feature
   cards (adh/aml/amlr/cct/cdd/def/dl/hut/ic/ica/ni/ns/pt/pta/pa/q/tr-card*),
   then visually CONVERGED onto the .app-card look (operator-approved, same
   day): the box + head-surface rules are grouped with .app-card above. What
   remains here is only what differs from .app-card:
   - the head carries the title typography DIRECTLY (markup is
     <div class="app-panel-head"><span>Title</span> …optional pill…</div>,
     not the .app-card-ico/.app-card-heading structure) and STACKS its children,
     so a trailing pill gets its own left-aligned row under the title (2026-07-24
     — it used to be space-betweened to the top-right; see the rule below);
   - the panel keeps the legacy overflow clip — partial-migration features'
     LOCAL heads (ic/ni/ns/tr/dl/pta…) rely on it to round their top corners.
     .app-panel--grid opts out: a panel wrapping an AppGrid must NOT clip
     (overflow:hidden would become the sticky containing block and break the
     grid's sticky top:56px header) — and AppSelect dropdowns inside a clipped
     panel need the same escape (the .tr-step pattern).
   NEW pages should prefer .app-card. */
.app-panel { overflow: hidden; }
.app-panel--grid { overflow: visible; }
/* The head STACKS its children: the title, then anything that follows it — a
   status pill, a tag group — on its OWN row underneath, LEFT-aligned
   (operator standard, 2026-07-24; this replaces `justify-content: space-between`,
   which pushed that trailing element to the top-RIGHT).
   EXCEPTION — an inline QUALIFIER of the title (an id/reference chip such as
   payable_transactions' "Transaction #48210") belongs ON the title line: nest it
   INSIDE the title span rather than adding a second child. That is the whole
   mechanism — one child = one row — so no modifier class is needed. Only a
   STATUS badge / tag group is a separate child. Same reasoning as
   .app-card-actions and .page-actions: a right-parked element sits far from the
   content it describes on a wide page (and can scroll off-screen entirely under
   the wide-grid max-content width model), and crowds the title on a narrow one.
   `flex-direction: column` + `align-items: flex-start` is what gives each child
   its own row WITHOUT stretching it — a pill is a shrink-wrapped inline-block, so
   the `flex-basis: 100%` trick used for the card's button row would balloon it to
   the full card width. Markup is unchanged everywhere (the pill stays the head's
   last child); every one of the 115 heads in the app opens with the title span,
   so a single-child head renders identically to before. */
.app-panel-head {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 10px;
    width: 100%;
    font-size: 15.5px;
    font-weight: 700;
    color: var(--text);
    text-align: left;
}
/* The head IS the title, so a head that wraps its title in a HEADING element for
   document structure inherits the head's typography instead of the UA's h2/h3
   metrics. Added 2026-07-24 with the panel-head convergence: ten features write
   <div class="app-panel-head"><h2>Title</h2></div> rather than the <span> the
   examples use, and keeping the heading is worth more than matching the example
   (it is the only heading structure those record pages have). Scoped to
   :first-child so a TRAILING .app-pill / tag group in the head is untouched —
   `font: inherit` on a pill would blow it up to 15.5px. */
.app-panel-head > h2:first-child,
.app-panel-head > h3:first-child { font: inherit; color: inherit; margin: 0; }
/* A head that is also a COLLAPSE control keeps its chevron on the title line:
   the shared head stacks its children (that is right for a status badge, wrong
   for a disclosure affordance). Opt in with .app-panel-head--toggle. */
.app-panel-head--toggle {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
}
/* …and when that control is a <button>, drop the UA chrome the shared head does
   not reset (it only sets border-BOTTOM). */
button.app-panel-head { border: 0; cursor: pointer; font-family: inherit; text-align: left; }
/* (.app-panel-body / .app-panel-grid are aliased onto the .app-card rules above
   — identical paddings by construction.) */

/* ---- A COLLAPSIBLE panel (shared, 2026-08-24) ----
   Markup — a native <details>, so it needs no JS at all and Find-in-page still
   reaches a closed section in Chrome:

     <details class="app-panel app-panel--collapsible">
       <summary class="app-panel-head app-panel-head--toggle">
         <h2>Title</h2>
         <span class="app-panel-toggle-hint">Show</span>
       </summary>
       <div class="app-panel-body">…</div>
     </details>

   Promoted here on its SECOND consumer (the Tickets record page's implementation
   plan; the first was crm.css's `.crm-card--collapsible`, which stays as it is —
   its head class is entangled with that screen's own card vocabulary). A third
   feature must use this one.

   The chevron is on the SUMMARY, not on the title, so a head carrying a count
   pill or a hint keeps the affordance at its right edge where every other
   disclosure in this app puts it. */
.app-panel--collapsible > summary { cursor: pointer; list-style: none; }
.app-panel--collapsible > summary::-webkit-details-marker { display: none; }
.app-panel--collapsible > summary::marker { content: ''; }
.app-panel--collapsible > summary::after {
    content: '';
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    margin-left: 4px;
    border-right: 2px solid #8494a7;
    border-bottom: 2px solid #8494a7;
    transform: rotate(45deg) translateY(-2px);
    transition: transform .15s;
}
.app-panel--collapsible[open] > summary::after { transform: rotate(225deg) translateY(-1px); }
.app-panel--collapsible > summary:hover::after { border-color: var(--navy); }
/* The word beside the chevron. Two states from ONE element via ::before, so the
   markup carries no copy that has to be kept in sync with the open state. */
.app-panel-toggle-hint {
    margin-left: auto;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
}
.app-panel-toggle-hint::before { content: 'Show'; }
.app-panel--collapsible[open] .app-panel-toggle-hint::before { content: 'Hide'; }

/* ---- App Pill (shared status-pill base) ----
   Consolidated from the byte-identical feature status pills (aml/amlr/cdd/def/
   hut/pt, 2026-07-23). COLOR variants stay feature-local — each feature maps
   its own statuses to a palette (use the --pill-* tokens for the standard
   five). --sm is the 11.5px variant (the def/hut priority pills). */
.app-pill {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
    line-height: 1.5;
    white-space: nowrap;
}
.app-pill--sm { font-size: 11.5px; }


/* ---- ported block: .app-figures / .app-figure* ---- */
/* ---- Key-figures band (shared) ----
   The row of figure TILES that leads a detail page or modal — the amounts a reader
   opened the record to see, at figure weight, above the reference fields.

   WHY IT IS SHARED, and why it is the answer to "my numeric fields look ragged":
   money in a <dl> field grid has no good alignment (see .app-field--num's comment —
   right-alignment was tried and rejected in an operator round because it splits a
   card's cells between two alignments). The fix is not to align the money; it is to
   promote it OUT of the field grid, leaving the grid a clean single-spine list of
   reference values. This band is where it goes. Promoted from payable_transactions'
   `.pt-figures` on 2026-08-05, when the third screen needed it and two of the three
   could not see that stylesheet.

     <div class="app-figures [app-figures--compact]">
       <div class="app-figure [app-figure--accent|--warn|--ok]">
         <span class="app-figure-label">Estimated Balance</span>
         <span class="app-figure-value">3,160,000.00 <em>USD</em></span>
       </div>
     </div>

   --compact  reference scale, for figures that SUPPORT the headline ones rather
              than compete with them (a holdback %, what has already been paid).
   --accent   the one figure the decision turns on (teal).
   --warn / --ok  a COUNT whose value carries meaning: 0 outstanding is the good
              answer, non-zero blocks something. Colour says which without prose.
   <em> inside the value is the muted qualifier (a currency code, a unit). */
.app-figures {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-bottom: 18px;
}
/* FLEX BASIS 168px, not 180px (2026-08-19). The tiles carry `flex-grow: 1`, so on
   any surface with room the row looks IDENTICAL either way — the basis only
   decides HOW MANY fit before the band wraps. At 180px a FOUR-tile band needs
   756px and a `.modal-lg` record body offers ~743 once the dialog padding, the
   card padding and the scrollbar are taken out, so it wrapped 3 + 1: three tiles
   and then one stretched across the full width, which reads as a mistake. 168px
   needs 708px and fits. Found on the shared Capital Summary record body
   (views/general/capital_summary_partial.php), whose band is four tiles and whose
   two hosts are a 1100px page AND an 840px modal. `min-width: 158px` still stops
   a wide band from crushing its figures. */
.app-figure {
    flex: 1 1 168px;
    min-width: 158px;
    padding: 12px 14px;
    background: var(--head-bg);
    border: 1px solid var(--hairline);
    border-radius: 9px;
}
.app-figure-label {
    display: block;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: #7a8798;
    margin-bottom: 4px;
    line-height: 1.3;
}
.app-figure-value {
    display: block;
    font-size: 18px;
    font-weight: 700;
    color: #22303f;
    font-variant-numeric: tabular-nums;
    font-feature-settings: 'tnum' 1;
}
.app-figure-value em { font-style: normal; font-size: 12px; font-weight: 600; color: #8a939f; }
/* A QUALIFIER LINE under the figure — "mean 44 min", "3.8 per request", "67
   release notes". The third slot of the tile, after the label and the value.

   Added here rather than on a feature (2026-08-24) because the operator standard
   is explicit: "any customized CSS that is on the KPI boxes should be removed and
   incorporated into the general.css". It arrived as `.tka-figure-sub` on the
   Tickets analytics dashboard, tools/check_shared_css.php reported it under
   [kpi-figure], and the checker was right — a second line inside the shared tile
   is an app-wide statement about how a figure reads, not one screen's business.

   It is NOT the same thing as `.app-figure-value em`, which is a unit or suffix
   INSIDE the figure ("4.6M"). This is a separate fact about the figure, on its own
   line, and a tile without one is unchanged. */
.app-figure-sub {
    display: block;
    margin-top: 2px;
    font-size: 11px;
    font-weight: 600;
    color: #8a939f;
    line-height: 1.35;
}
.app-figures--compact .app-figure-sub { font-size: 10.5px; }
.app-figure--accent { background: #e6f4f1; border-color: #b8e0d8; }
.app-figure--accent .app-figure-label { color: #0f766e; }
.app-figure--accent .app-figure-value { color: #0c5d57; }
.app-figures--compact { gap: 10px; }
.app-figures--compact .app-figure {
    flex: 1 1 150px;
    min-width: 132px;
    padding: 9px 12px;
    background: #fff;
}
.app-figures--compact .app-figure-value { font-size: 15px; }
.app-figures--compact .app-figure-value em { font-size: 11px; }
.app-figure--warn { background: #fdf3e7; border-color: #f0d3a8; }
.app-figure--warn .app-figure-label { color: #8a5a12; }
.app-figure--warn .app-figure-value { color: #7a4a0c; }
.app-figure--ok .app-figure-value { color: #1d6b3a; }
/* [hidden] companion — `display:flex` is an AUTHOR rule and out-ranks the UA's
   [hidden]{display:none}, so a conditionally-shown band would paint anyway (the
   same trap .app-fields[hidden] and .callout-banner[hidden] carry). */
.app-figures[hidden] { display: none !important; }

/* ---- A figure that LINKS: .app-figure--link ----
   The KPI tile whose figure opens the screen that owns the records behind it.

   WHY IT LOOKS LIKE THIS. It used to be `color: inherit` + a 1px DOTTED bottom
   border, authored locally in assets/opus_connect/connect_insights.css. Operator,
   2026-08-17: "where the KPI boxes include a link - the value should look more
   like a clickable value. the dotted underline doesn't look obvious as clickable",
   with a suggestion of "a `View →`" or "an arrow in the kpi box".

   A `View →` row was declined: the band's whole job is LABEL over FIGURE at a
   glance, and a third line of chrome in every tile buys a word the arrow already
   says. What ships instead:
     · the figure takes the app's LINK colour instead of inheriting body text —
       "the same colour as every other link on the page" is the strongest signal
       available, and it is the one the dotted border threw away;
     · a `→` follows the figure, so the affordance survives a greyscale print and
       a reader who cannot separate navy from near-black;
     · THE WHOLE TILE is the hit area, not the ten characters of the number (the
       stretched `::after` below), and the tile's border and shadow answer the
       hover — a 158px-wide target instead of a 60px one;
     · the arrow slides 2px on hover, which is the cheapest possible "this goes
       somewhere" without animating the figure itself.

   Markup — the anchor wraps the VALUE, never the tile, and the tile carries the
   modifier (that is what keeps the label out of the link text a screen reader
   announces, and it is why this is not `.app-tile--link`, which linkifies a whole
   tile of prose):

     <div class="app-figure app-figure--link">
       <span class="app-figure-label">Total Commitments</span>
       <span class="app-figure-value"><a href="…">3,160,000</a></span>
     </div>

   The modifier is what draws the arrow, so a tile that has no link cannot grow
   one, and an `<a>` that lands in a plain figure still reads as a link.

   ⚠ An .app-figure--link may hold NOTHING else interactive. The stretched overlay
   covers the tile, so a second control inside it would be unreachable. Same rule
   .app-tile--link carries, same reason. */
.app-figure--link { position: relative; transition: border-color .12s ease, box-shadow .12s ease; }
.app-figure--link:hover { border-color: var(--navy); box-shadow: 0 2px 8px rgba(15, 52, 96, .10); }
.app-figure-value a {
    color: var(--navy);
    text-decoration: none;
    font: inherit;
}
/* The hit area. `inset: 0` over the TILE (the positioned ancestor), so a click
   anywhere in the box follows the link — including on the label. */
.app-figure--link .app-figure-value a::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 9px;      /* matches .app-figure, so the focus ring fits it */
}
/* The arrow is on the VALUE, not on the anchor, because the anchor's ::after is
   already spent on the hit area above. */
.app-figure--link .app-figure-value::after {
    content: ' \2192';
    display: inline-block;
    margin-left: 6px;
    font-size: .78em;
    font-weight: 700;
    color: var(--navy);
    transform: translateX(0);
    transition: transform .12s ease;
}
.app-figure--link:hover .app-figure-value::after { transform: translateX(2px); }
.app-figure--link:hover .app-figure-value a { text-decoration: underline; }
/* Keyboard parity: the ring goes on the TILE, because the anchor's own box is
   just the text while the thing that acts is the whole tile. */
.app-figure--link .app-figure-value a:focus-visible { outline: none; }
.app-figure--link:focus-within {
    border-color: var(--navy);
    outline: 2px solid var(--navy);
    outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
    .app-figure--link,
    .app-figure--link .app-figure-value::after { transition: none; }
}

/* ---- Direction states on a figure: .is-up / .is-down / .is-flat ----
   For a figure that MOVED — a period P&L, a return. Promoted from
   assets/daily_pnl/daily_pnl.css on 2026-08-17 with the KPI-box consolidation
   (operator: "any customized CSS that is on the KPI boxes should be removed and
   incorporated into the general.css").

   DIRECTION IS NEVER CARRIED BY COLOUR ALONE — the glyph is the primary signal
   and the colour reinforces it, because a colour-vision-deficient reader must
   still be able to tell a gain from a loss. The pair is steel/terracotta, never
   green/red: green/red fails this app's CVD gate (see the palette provenance in
   app_chart.css, and AppHC.COLORS.pos/neg, which use the same two hues).

   The class goes on the TILE and the server decides it — nothing in the browser
   inspects a number to pick one. */
.app-figure.is-up   .app-figure-value { color: #2b6ea1; }
.app-figure.is-down .app-figure-value { color: #b65c30; }
.app-figure.is-up   .app-figure-value::before { content: '\25B2 '; font-size: .72em; }
.app-figure.is-down .app-figure-value::before { content: '\25BC '; font-size: .72em; }
.app-figure.is-flat .app-figure-value::before { content: '\2013 '; color: var(--text-muted); }

/* A NEGATIVE figure, where the sign is the whole story and there is no direction
   to draw (a period P&L on the NAV dashboard). Promoted from `.nd-figure--neg`.
   The sign is ALSO written into the value by the formatter's explicit "+"/"−", so
   this is reinforcement and never the only cue. */
.app-figure--neg .app-figure-value { color: var(--pill-red-fg); }

/* "Not available" IN PLACE OF a figure — a real answer, and deliberately not a
   zero, which would read as a genuine nought. Smaller and muted so it cannot be
   mistaken for a number. Promoted from `.nd-figure-na`. */
.app-figure-na { font-size: 15px; font-weight: 600; color: var(--text-muted); }

/* The value's skeleton, sized ONCE. Three screens each carried their own
   (82×20, 96×22, 96×20) and the differences were accidental — a reserved height
   only has to stop the tile resizing when the figure lands, and .app-figure-value
   is an 18px/1.2 line, so 22px is the height that does it for all of them. */
.app-figure-skel { display: inline-block; width: 96px; height: 22px; }

/* ---- ported block: .app-drill* (the "View as table" modal) ---- */
/* ---- Drill-through modal (assets/general/app_drill.js) ----
   The standard "a value in a grid opens a modal with another grid in it" host.
   Rides the shared .modal-overlay / .modal-dialog / .modal-header / .modal-body
   vocabulary (z-index 9000 band) and adds only what an in-modal GRID needs: a
   wide dialog, the Back button for a nested drill, and the sticky-header offset
   reset. Generalized from the NAV Pack drill modal (2026-07-28). */
body.app-drill-open { overflow: hidden; }
/* Wider than .modal-lg: the payload is a table, and its columns are the reason
   the user drilled. Still viewport-capped, so it never overflows a laptop. */
.app-drill-dialog {
    width: 1180px;
    max-width: 94vw;
    max-height: 88vh;
}
/* The shared width modifiers, passed as the view's `dialogClass`. Use one of
   these rather than authoring a per-feature drill width — the app already had
   eight different modal widths before this component (1180 / 1000 / 1400 / 1100
   / 580 / 480 / 560 / 440).
   --xwide is the app's WIDE PAGE tier (1750px, the .page-wrap cap) reused as a
   dialog width, for a drill whose payload is a genuinely wide table: the NAV Pack
   Position Details grid is 13 columns, and at 1180 the operator was reading it
   through a horizontal scrollbar (QA 2026-08-11). It is not a new number, and
   max-width: 94vw still keeps it on a laptop. */
.app-drill-dialog--xwide { width: 1750px; }
.app-drill-dialog--wide  { width: 1400px; }
.app-drill-dialog--md    { width: 760px; }
.app-drill-head-left {
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 0;
}
.app-drill-head-left h2 {
    font-size: 16px;
    font-weight: 700;
    color: var(--navy);
    margin: 0;
}
.app-drill-back {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    border: 1px solid #d3dae2;
    background: #fff;
    color: var(--navy);
    border-radius: 6px;
    padding: 4px 10px 4px 6px;
    font-size: 12.5px;
    font-weight: 600;
    font-family: inherit;   /* a <button> otherwise renders in the UA font, not Manrope */
    cursor: pointer;
    white-space: nowrap;
}
.app-drill-back:hover { background: #eef2f7; }
/* Load-bearing: the base rule is display:inline-flex, which out-specifies the
   UA [hidden] rule — without this the Back button shows at the first level. */
.app-drill-back[hidden] { display: none; }

/* ---- The Search & Filter region (a view's `filters` option, 2026-08-11) ----
   A modal grid is still a LIST, so once it has real filters it owes the app-wide
   list anatomy: search box + dropdowns TOGETHER in one .app-filter-panel above the
   grid (README §2, app_standards.php#std-list). AppDrill GENERATES that panel — a
   drill has no view file to author it in — and puts it HERE, as a sibling of
   .app-drill-body rather than inside it.
   WHY OUT HERE, and do not "tidy" it back in: .app-drill-body is `overflow: hidden`
   (that is what makes the grid the bounded scroller, just above), and an AppSelect
   option panel is `position: absolute` — so a filter dropdown inside the body is
   clipped invisible at the body's edge, the AppSelect clip trap. .app-drill-dialog
   sets no overflow of its own, so out here a dropdown may overhang the dialog
   exactly as it overhangs a card on a page.
   No `display` is declared: the region is toggled with the `hidden` attribute, and
   an author `display` would out-specify the UA [hidden] rule (the .app-drill-back
   trap directly above). `flex` is a different property, so it is safe. */
.app-drill-filters { flex: 0 0 auto; padding: 14px 18px 0; }
/* The panel is the shared component, unchanged — only its PLACEMENT is scoped:
   its page-level 18px bottom margin would double up with .app-drill-body's own top
   padding, and inside a dialog the gap wants to be tighter than between page
   sections. */
.app-drill-filters .app-filter-panel { margin-bottom: 0; }
/* Mobile: the generated .app-filter-toggle is the only way to open the panel below
   700px (general.css hides the head strip and hides the panel while collapsed), so
   it must not be flush against the dialog header. */
@media (max-width: 700px) {
    .app-drill-filters { padding: 12px 12px 0; }
}

/* THE GRID SCROLLS, NOT THE MODAL BODY (2026-07-28) — load-bearing for the
   sticky header, and the one thing the hand-rolled drill modals got wrong.
   An AppGrid's sticky `<th>` pins to its nearest SCROLLING ancestor, which is
   `.app-grid-scroll`. Left alone, the shared `.modal-body { overflow-y: auto }`
   is the real vertical scroller while `.app-grid-scroll` (auto height) never
   scrolls vertically — so `top: 0` pins the header to a scrollport that never
   moves and the header scrolls clean out of view with the rows. (Nor does the
   JS float-head clone stand in: `_updateFloatHead` bails while the grid's
   `rect.top >= topOffset`, which is always true for a centered dialog.)
   So: stop the body scrolling, and give the grid's own scroller the bounded
   height instead. The header block, the toolbar/search and the pager then stay
   put and only the ROWS scroll — which is what a drill modal should do. */
.app-drill-body {
    padding: 14px 18px 18px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    min-height: 0;
}
/* The optional block a view renders above its grid (a caption, a field list
   describing the record the rows belong to). Never shrinks. */
.app-drill-head-block { margin-bottom: 14px; flex: 0 0 auto; }
.app-drill-caption { margin: 0 0 12px; font-size: 13px; color: #5a6b7f; }
.app-drill-grid {
    flex: 1 1 auto;
    min-height: 120px;
    display: flex;
    flex-direction: column;
}
/* A view.content body (a chart, not a list) — see app_drill.js. Same box as the
   grid gets, because the reason is the same: `.app-drill-body` is the bounded
   flex column, and a child that does not claim the free space renders at its
   content height in the middle of an empty dialog. `min-height: 0` is what lets a
   chart that measures its own host shrink INSIDE the box instead of pushing it. */
.app-drill-content {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
/* The bounded vertical scrollport — this is what makes `top: 0` below real. */
.app-drill-body .app-grid-scroll { flex: 1 1 auto; min-height: 0; }
/* Chrome around the rows keeps its natural height. */
.app-drill-body .app-grid-toolbar,
.app-drill-body .app-grid-mobilebar,
.app-drill-body .app-grid-footer { flex: 0 0 auto; }
/* Pairs with the forced topOffset:0 in app_drill.js: the shared .app-grid-th
   offsets by the 56px FIXED page topbar, which does not exist in a dialog. */
.app-drill-body .app-grid-th { top: 0; }
/* Make AppGrid's per-column `wrap` REAL inside a drill (2026-07-28). Every drill
   grid is forced scrollX, and `.app-grid-scroll--x .app-grid-cell { nowrap }`
   (0,2,0) out-specifies `.app-grid-cell--wrap { normal }` (0,1,0) — so a long
   free-text column stayed on one line and the dialog scrolled sideways forever.
   These 0,3,0 selectors win it back; the max-width AppGrid already sets inline is
   what then does the capping. Scoped to the drill ON PURPOSE: on a wide LIST page
   the nowrap-wins behaviour is the deliberate one (see the AppGrid wrap docs), and
   a dialog is the case where wrapping beats a horizontal scrollbar. */
.app-drill-body .app-grid-scroll--x .app-grid-th--wrap,
.app-drill-body .app-grid-scroll--x .app-grid-cell--wrap { white-space: normal; overflow-wrap: anywhere; }
/* …and give that column a FLOOR as well as the cap (2026-08-04). The cap above is
   only half of a width: it cannot stop the column being SQUEEZED. In here the table is
   already at its min-content total — every other column is nowrap, so there is no
   slack to distribute — and `overflow-wrap: anywhere` drops THIS column's min-content
   to one character (`break-word` would not: only `anywhere` shrinks the intrinsic
   minimum). Auto table layout then hands the column exactly that: the 25-column
   "Select a Wire Template" picker rendered "Bank Additional Details" as a 36px column,
   one letter per line — a 326px header row over 919px rows.
   AppGrid publishes the px it caps with as --app-grid-wrap-px on the th (see
   _renderHeadRow), so `wrap: <px>` means ONE definite width inside a drill. Measured
   on that picker: column 36px -> 260px, header row 326px -> 38px, row 919px -> 55px,
   and the whole table only 4021px -> 4245px.
   Two deliberate limits:
     - the th alone carries the floor (a column is as wide as its widest cell), so the
       rows keep their plain max-width markup and no per-row style is emitted;
     - only a NUMERIC `wrap` publishes the property, so a `wrap: true` column keeps
       sizing to its content instead of being pinned at the 420px default it never
       asked for (data_rooms' Company pickers). The var() default makes this rule inert
       for them.
   NOTE: capital_calls.css keeps a twin of the wrap-release rule above for
   #ccApproveGrid. It needs no floor while that table still fits its scroller (the
   "KEPT now the table fits" note at its AppGrid.create) — the day it stops fitting,
   its `wrap: 320` Investor column collapses the same way and wants this line too. */
.app-drill-body .app-grid-scroll--x .app-grid-th--wrap { min-width: var(--app-grid-wrap-px, 0); }
/* PIN the row-action column while a wide drill grid scrolls sideways (2026-08-04).
   The app-wide rule is that row actions render LEFT-most so they stay reachable
   without scrolling right (see isActionCol() in app_grid.js) — but in a drill the
   scrolling happens INSIDE the dialog, so on a genuinely wide grid (the 25-column
   "Select a Wire Template" picker is ~4100px) the Select button scrolls out of
   view to the left and the user has to scroll all the way back to act on the row
   they just read. Sticky-left keeps it on screen; the `th` is already sticky for
   the vertical axis, so this only adds the horizontal one.
   The cell needs its OWN opaque background per row state or the columns sliding
   underneath show through — hence the four background rules (base / both zebra
   models / hover / selected). Scoped to the drill: a scrollX LIST page scrolls the
   PAGE, where the leftmost column is already the last thing to leave the viewport. */
.app-drill-body .app-grid-scroll--x .app-grid-th--actions,
.app-drill-body .app-grid-scroll--x .app-grid-cell--actions { position: sticky; left: 0; }
.app-drill-body .app-grid-scroll--x .app-grid-th--actions  { z-index: 4; }
/* Row-state backgrounds, in the SAME cascade order as the row rules they shadow
   (base -> ungrouped zebra -> grouped/subrow zebra -> hover -> selected), so a
   grouped row that is also nth-child(even) lands on its --even colour and not on
   the reset. */
.app-drill-body .app-grid-scroll--x .app-grid-cell--actions { z-index: 3; background: #fff; }
.app-drill-body .app-grid-scroll--x .app-grid-row:nth-child(even) > .app-grid-cell--actions { background: #fbfcfd; }
.app-drill-body .app-grid--grouped .app-grid-scroll--x .app-grid-row:nth-child(even) > .app-grid-cell--actions,
.app-drill-body .app-grid--subrows .app-grid-scroll--x .app-grid-row:nth-child(even) > .app-grid-cell--actions { background: #fff; }
.app-drill-body .app-grid--grouped .app-grid-scroll--x .app-grid-row--even > .app-grid-cell--actions,
.app-drill-body .app-grid--subrows .app-grid-scroll--x .app-grid-row--even > .app-grid-cell--actions { background: #fbfcfd; }
.app-drill-body .app-grid-scroll--x .app-grid-row:hover > .app-grid-cell--actions { background: #f2f7fc; }
.app-drill-body .app-grid-scroll--x .app-grid-row.is-selected > .app-grid-cell--actions { background: #e8f1fb; }
/* The trigger anchor: reads as a link, keeps its real href for ctrl-click. */
.app-drill-link { color: var(--navy); font-weight: 600; text-decoration: none; }
.app-drill-link:hover { text-decoration: underline; }
@media (max-width: 700px) {
    .app-drill-dialog { max-height: 92vh; }
    /* Hand the scrolling BACK to the modal body below 700px. AppGrid switches to
       stacked cards here and releases the scroller's overflow to `visible`, so
       the bounded-scrollport model above would spill the whole card list over
       the pager. Nothing is lost: cards have no `<th>`, so there is no sticky
       header to preserve. */
    .app-drill-body {
        padding: 12px 12px 14px;
        display: block;
        overflow-y: auto;
    }
    .app-drill-grid { display: block; }
    .app-drill-body .app-grid-scroll { flex: none; min-height: 0; }
    /* Back is the dialog's only navigation affordance, so give it the app's
       DEFAULT 34px control height on touch instead of the .btn-sm 30px (the
       size ladder is 30 / 34 / 44 — this is not a new size). */
    .app-drill-back { min-height: 34px; padding-left: 8px; padding-right: 12px; }

    /* …UNLESS THE DRILL GRID KEPT ITS TABLE. `mobileCards: false` (app_grid.js)
       marks the grid `.app-grid--nocards`, and every premise of the release above
       is then false: there is no card list to spill over the pager, and there IS a
       sticky `<th>` to preserve — the one thing the release said it was losing
       nothing by dropping. So put the desktop bounded-scrollport model back for
       exactly those dialogs: body is the non-scrolling flex column again, the grid
       claims the free space, and only the ROWS scroll under a header that holds.
       Today that is the Digital NAV Pack's four drills (nav_pack.js drillGrid);
       daily_pnl, digital_subs and every chart "View as table" modal still stack
       cards and still take the release above.

       Same-block ON PURPOSE, after the rules it overrides: `:has()` costs no
       specificity, so `.app-drill-body:has(…)` and `.app-drill-body` are both
       (0,1,0) and source order is the entire tiebreak. Moving these up, or into a
       second @media block earlier in the file, silently reverts them. The padding
       is NOT re-declared — 12px is right at this width either way. */
    .app-drill-body:has(.app-grid--nocards) {
        display: flex;
        flex-direction: column;
        overflow: hidden;
    }
    .app-drill-body:has(.app-grid--nocards) .app-drill-grid {
        display: flex;
        flex-direction: column;
        flex: 1 1 auto;
    }
    .app-drill-body:has(.app-grid--nocards) .app-grid-scroll { flex: 1 1 auto; min-height: 0; }
}

/* ---- .btn-danger — the DESTRUCTIVE button ----
   Added 2026-08-25 with the Tickets delete action (api/tickets/delete.php), and it
   FIXES A LATENT INCONSISTENCY rather than inventing a name: this file has referred
   to `.btn-card-action.btn-danger` in a comment since before the port (search
   "compose, don't fork") while never declaring the variant. A `btn btn-danger` in
   any view up to now rendered as a plain default button — the destructive one,
   looking exactly like Cancel next to it.

   ⚠ THAT COMMENT SAYS "above" AND THIS IS BELOW IT. The comment's point stands
   (compose a modifier onto .btn, never fork it); only its direction is stale, and it
   is left alone rather than edited because it is describing the `.qa-only` rule it
   sits on, not this one.

   Values are the client portal's, so a destructive button is the same red in both
   trees. Filled, not outlined: this is the one action shape that should not be
   quiet, and the ladder here is btn-primary (navy, the normal act) / btn-secondary
   (outline, the alternative) / btn-danger (red, the one that takes something away).
   `border-color` rather than the `border` shorthand, because the .btn foundation
   already declares `1px solid transparent` and every variant sets colour ONLY. */
.btn-danger {
    background: var(--btn-danger-bg);
    border-color: var(--btn-danger-bg);
    color: #fff;
}
.btn-danger:hover:not(:disabled) {
    background: var(--btn-danger-bg-hover);
    border-color: var(--btn-danger-bg-hover);
    color: #fff;
}
/* Disabled state spelled out: a red button that stays fully red while it is doing
   something reads as still-clickable, and this is the one button where a second
   click matters (the Tickets delete disables it for the round trip). */
.btn-danger:disabled { opacity: .6; cursor: default; }
