/**
 * =====================================================================
 * COMPONENTS  assets/css/components.css
 * =====================================================================
 * Reusable UI building blocks: buttons, forms, tables, cards, alerts.
 * Every component here assumes base.css has already been loaded.
 *
 * USAGE PATTERNS (so next developer knows what goes where):
 * - Use .btn-primary (green) for CREATE / SAVE / YES actions
 * - Use .btn-secondary (gray) for EDIT / SECONDARY actions
 * - Use .btn-danger (red) for ARCHIVE / DELETE actions
 * - Buttons should always have role="button" or be <button> elements
 * - Use .alert-success after an action succeeds, .alert-error for failures
 * - Wrap form fields in .form-group for consistent spacing
 * =====================================================================
 *
 * TABLE OF CONTENTS  line numbers as of 2026-09-22, a navigation aid
 * not a guarantee; if they've drifted, `grep -n "^/\* ====="` finds
 * every section's real opening line again.
 *   Buttons ................................................. L43
 *   Keyboard focus .......................................... L170
 *   Forms (+ file inputs, password toggle, Quill editor,
 *     back-link, report toolbar/pagination) ................. L192
 *   Tables .................................................. L678
 *     List selection / bulk-action toolbar ................. L772
 *     Modal .................................................. L840
 *   Alerts / messages ....................................... L1083
 *   Badges ................................................... L1134
 *   Cards .................................................... L1153
 *     Switches & Role Permissions (2.8.4) .................. L1273
 *     Role Permissions page matrix .......................... L1361
 *   Navigation (navbar, sidebar) ............................ L1645
 *     Dark-mode toggle ...................................... L1921
 *   Responsive adjustments (mobile breakpoint) .............. L2167
 *   Resident portal components (public navbar, profile
 *     dropdown, notification bell  SHARED with the admin
 *     bell despite living here, see that section's own note,
 *     feature/announcement cards, lightbox, pagination,
 *     chatbot widget) ....................................... L2472
 *   One-time login codes (OTP) .............................. L3555
 * =====================================================================
 */

/* =====================================================================
 * BUTTONS
 * ===================================================================== */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-8);

    padding: var(--spacing-8) var(--spacing-16);
    border: 1px solid transparent;
    border-radius: var(--border-radius-md);

    font-family: var(--font-family);
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-bold);

    cursor: pointer;
    transition: all 0.2s ease;

    text-decoration: none;
}

.btn:hover {
    box-shadow: var(--shadow-sm);
    transform: translateY(-1px);
}

.btn:active {
    transform: translateY(0px);
}

.btn-primary {
    background-color: var(--color-success);
    color: var(--color-white);
    border-color: var(--color-success);
}

.btn-primary:hover {
    background-color: #059669;
    border-color: #059669;
}

.btn-secondary {
    background-color: var(--color-surface);
    color: var(--color-text);
    border-color: var(--color-border);
}

.btn-secondary:hover {
    background-color: var(--color-surface-2);
    border-color: var(--color-text-light);
}

.btn-danger {
    background-color: var(--color-danger);
    color: var(--color-white);
    border-color: var(--color-danger);
}

.btn-danger:hover {
    background-color: #dc2626;
    border-color: #dc2626;
}

/* Icon-only button (e.g. the Announcements page's RSS link)  square,
   no label, just a centered icon. Pair with .btn-secondary/.btn-sm etc.
   for color/size; the icon itself should be an inline <svg width="18"
   height="18"> so it scales with the button instead of a background
   image. */
.btn-icon {
    padding: var(--spacing-8);
    width: 36px;
    height: 36px;
    flex-shrink: 0;
}

.btn-icon.btn-sm {
    width: 30px;
    height: 30px;
    padding: var(--spacing-4);
}

/*
 * SUBMIT-IN-PROGRESS SPINNER
 * assets/js/ui.js (initSubmitProtection) adds this class to a submit
 * button the instant its form is actually submitting, so a double-click
 * can't fire the same add/edit/archive request twice. Hides the
 * button's own label (color: transparent) and centers a small spinner
 * over it instead  works on any .btn variant since the spinner border
 * color is white regardless of the button's background.
 */
.btn.is-submitting {
    position: relative;
    color: transparent !important;
    cursor: not-allowed;
    pointer-events: none;
}

.btn.is-submitting::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 16px;
    height: 16px;
    margin: -8px 0 0 -8px;
    border: 2px solid rgba(255, 255, 255, 0.4);
    border-top-color: #fff;
    border-radius: 50%;
    animation: btn-spin 0.6s linear infinite;
}

@keyframes btn-spin {
    to {
        transform: rotate(360deg);
    }
}

/* Button in a form should not have extra padding  just the text */
button[type="submit"],
button[type="button"],
button[type="reset"] {
    cursor: pointer;
}

/* =====================================================================
 * KEYBOARD FOCUS
 * A visible ring on every interactive element for keyboard users
 * (inputs/selects/textareas define their own focus style above). Only
 * on :focus-visible, so a mouse click never leaves a lingering ring.
 * ===================================================================== */
a:focus-visible,
button:focus-visible,
.btn:focus-visible,
summary:focus-visible,
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
    outline: 2px solid var(--color-primary-light);
    outline-offset: 2px;
}

/* On the dark navbar a blue ring barely shows  use white there. */
.navbar a:focus-visible,
.navbar button:focus-visible {
    outline-color: var(--color-white);
}

/* =====================================================================
 * FORMS
 * ===================================================================== */

.form-group {
    margin-bottom: 18px;
}

label {
    display: block;
    margin-bottom: 6px;
    font-weight: var(--font-weight-bold);
    color: var(--color-text);
}

/* CRUD add/edit forms (every _form.php has id="<name>-form") get a white
   card instead of sprawling full-bleed on the gray page background. */
.main-content > form[id$="-form"] {
    background-color: var(--color-surface);
    padding: var(--spacing-24) 28px;
    border-radius: var(--border-radius-lg);
    box-shadow: var(--shadow-sm);
    max-width: 760px;
}

/* The many-field forms (Residents, Officials, Blotters) add
   class="form-grid" so their fields flow into columns and use the width
   instead of one long single-column scroll. Wide controls  a textarea,
   a file upload, the button row, an error summary  span every column. */
.main-content > form.form-grid {
    max-width: 1060px;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    column-gap: var(--spacing-24);
    align-content: start;
    align-items: start;
}

.form-grid > .form-actions,
.form-grid > .alert,
.form-grid > .form-group:has(textarea),
.form-grid > .form-group:has(input[type="file"]) {
    grid-column: 1 / -1;
}

/* A long CRUD form split into labeled fieldsets (first used by Resident
   Records, modules/admin/residents/_form.php  a ~20-field form that
   used to be one flat, unbroken .form-grid). Each fieldset is its own
   card; .form-section-grid inside it is the SAME column layout as
   .form-grid above, just usable on a plain <div> instead of requiring
   a top-level <form>, since the <form> itself is no longer the grid
   container once its fields are split across sections. Keep the two
   grid rules in sync by hand if one ever changes  CSS has no "extend". */
/* The <form> wrapping one or more .form-section fieldsets, capped and
   centered at the same width .form-grid uses  so the button row
   (.form-actions, a plain block child of this form, not part of any
   section) lines up with the sections above it instead of stretching
   to whatever .main-content's own width happens to be. */
.main-content > form.form-sections {
    max-width: 1060px;
    margin: 0 auto;
}

.form-section {
    margin-bottom: var(--spacing-24);
    padding: var(--spacing-16) var(--spacing-24) var(--spacing-24);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-lg);
    background-color: var(--color-surface);
}

.form-section legend {
    padding: 0 var(--spacing-8);
    font-size: var(--font-size-large);
    font-weight: var(--font-weight-bold);
    color: var(--color-text);
}

.form-section-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    column-gap: var(--spacing-24);
    align-content: start;
    align-items: start;
}

.form-section-grid > .form-group:has(textarea),
.form-section-grid > .form-group:has(input[type="file"]) {
    grid-column: 1 / -1;
}

/* Generic searchable "pick one from many" combobox (assets/js/ui.js
   initCombobox())  a text input that filters a dropdown list and
   writes the chosen option's value into a hidden field, same submitted
   shape as a plain <select>. First used by Resident Records' Household
   field once a flat <select> of every household became hard to scan;
   reusable anywhere else a form needs to pick one row out of a list
   too long to comfortably scroll. See ui.js's initCombobox() docblock
   for the markup contract. */
.combobox {
    position: relative;
}

.combobox-options {
    position: absolute;
    z-index: 20;
    top: calc(100% + var(--spacing-4));
    left: 0;
    right: 0;
    max-height: 240px;
    overflow-y: auto;
    margin: 0;
    padding: var(--spacing-4);
    list-style: none;
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-md);
}

.combobox-options li {
    padding: var(--spacing-8) var(--spacing-12);
    border-radius: var(--border-radius-sm);
    cursor: pointer;
}

.combobox-options li:hover,
.combobox-options li.is-active {
    background-color: var(--color-surface-2);
}

.combobox-options li[aria-selected="true"] {
    font-weight: var(--font-weight-bold);
}

.combobox-empty {
    padding: var(--spacing-8) var(--spacing-12);
    color: var(--color-text-light);
    font-size: var(--font-size-small);
}

/* All input types should have the same baseline appearance */
.form-input,
.form-select,
.form-textarea,
input[type="text"],
input[type="email"],
input[type="password"],
input[type="date"],
input[type="number"],
select,
textarea {
    width: 100%;
    padding: var(--spacing-8) var(--spacing-16);

    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    background-color: var(--color-surface);

    font-family: var(--font-family);
    font-size: var(--font-size-base);
    color: var(--color-text);

    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="date"]:focus,
input[type="number"]:focus,
select:focus,
textarea:focus {
    outline: none;
    border-color: var(--color-primary-light);
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

/* Show validation errors */
.form-input.is-invalid,
input.is-invalid,
select.is-invalid,
textarea.is-invalid {
    border-color: var(--color-danger);
}

.form-input.is-invalid:focus {
    box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}

/* Help text under form fields */
.form-help {
    display: block;
    margin-top: var(--spacing-4);
    font-size: var(--font-size-small);
    color: var(--color-text-light);
}

/*
 * ANNOUNCEMENT RICH-TEXT EDITOR (Quill, assets/js/vendor/) 
 * modules/admin/announcements/_form.php + initAnnouncementEditor() in
 * ui.js. Quill's own "snow" theme (assets/css/vendor/quill.snow.min.css)
 * handles the toolbar/editor internals; this just makes the outer
 * border/radius/invalid-state match every other .form-input on the page
 * instead of Quill's default look.
 */
[data-rich-editor] {
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    overflow: hidden; /* clips the toolbar's own top corners to match */
}

[data-rich-editor] .ql-toolbar.ql-snow {
    border: none;
    border-bottom: 1px solid var(--color-border);
    background-color: var(--color-surface-2);
    font-family: var(--font-family);
}

[data-rich-editor] .ql-container.ql-snow {
    border: none;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
}

[data-rich-editor] .ql-editor {
    min-height: 160px;
    color: var(--color-text);
}

[data-rich-editor] .ql-editor.ql-blank::before {
    color: var(--color-text-light);
    font-style: normal;
}

[data-rich-editor].is-invalid {
    border-color: var(--color-danger);
}

/*
 * FILE INPUTS
 * The native control (grey "Choose File" + "no file chosen") looks
 * nothing like the rest of the form. Strip the wrapper back  some
 * _form.php files put .form-input on it, some don't  and restyle just
 * the button half to match .btn-secondary, keeping the filename text
 * that follows it readable.
 */
input[type="file"] {
    display: block;
    width: 100%;
    max-width: 420px;
    padding: var(--spacing-8);
    border: 1px dashed var(--color-border);
    border-radius: var(--border-radius-md);
    background: var(--color-surface-2);
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    cursor: pointer;
}

input[type="file"]::file-selector-button {
    margin-right: var(--spacing-16);
    padding: var(--spacing-8) var(--spacing-16);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-bold);
    cursor: pointer;
    transition: background-color 0.2s ease, border-color 0.2s ease;
}

input[type="file"]::file-selector-button:hover {
    background-color: var(--color-surface-2);
    border-color: var(--color-text-light);
}

input[type="file"]:focus-visible {
    outline: none;
    border-color: var(--color-primary-light);
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

/*
 * BACK LINK  the "&larr; Back to list" affordance above CRUD forms
 * (form.php pages give the <a> class="back-link"). A quiet pill that
 * lifts on hover, instead of a bare 12px text link that's easy to miss.
 */
.back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-4);
    padding: var(--spacing-4) var(--spacing-16) var(--spacing-4) var(--spacing-8);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: var(--color-surface);
    color: var(--color-text-light);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    text-decoration: none;
    transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease;
}

.back-link:hover {
    border-color: var(--color-primary-light);
    color: var(--color-accent);
    background: var(--color-backlink-hover-bg);
    text-decoration: none;
}

/*
 * PASSWORD SHOW/HIDE TOGGLE
 * assets/js/ui.js (initPasswordToggle) wraps every input[type="password"]
 * on the page in a .password-field div and appends a .password-toggle
 * button  no PHP template needs to add this markup itself, it's
 * automatic on every page that loads ui.js.
 */
.password-field {
    position: relative;
}

.password-field input[type="password"],
.password-field input[type="text"] {
    padding-right: 44px;
}

/*
 * Scoped as ".password-field .password-toggle" (two classes) rather
 * than just ".password-toggle" (one class) so this reliably beats
 * page-local rules like ".login-card button { width: 100%; }" 
 * those match on one class + one element (button), which otherwise
 * ties or wins against a single-class selector and stretches this
 * button to cover the whole input, making it an invisible click trap.
 */
.password-field .password-toggle {
    position: absolute;
    top: 50%;
    right: var(--spacing-4);
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--color-text-light);
    cursor: pointer;
    border-radius: var(--border-radius-md);
    line-height: 0;
}

.password-field .password-toggle:hover {
    color: var(--color-text);
    background-color: var(--color-surface-2);
}

.password-field .password-toggle:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}

.password-field .password-toggle svg {
    width: 18px;
    height: 18px;
}

/* Row of buttons at the bottom of a form (Save / Cancel) */
.form-actions {
    display: flex;
    gap: var(--spacing-16);
    flex-wrap: wrap;
    margin-top: var(--spacing-24);
    padding-top: var(--spacing-16);
    border-top: 1px solid var(--color-border);
}

/* Report-page action row (Back / Print). The per-page inline <style>
   blocks only hide this in print; this gives it screen spacing. */
.report-actions {
    display: flex;
    gap: var(--spacing-8);
    flex-wrap: wrap;
    margin-bottom: var(--spacing-24);
}

/* Filter bar  the row of selects/date inputs above a report or a
   filtered list. Used by every reports/*.php and the Revenue view. */
.report-toolbar {
    display: flex;
    gap: var(--spacing-16);
    flex-wrap: wrap;
    align-items: flex-end;
    background-color: var(--color-surface);
    padding: var(--spacing-16);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-sm);
    margin-bottom: var(--spacing-24);
}

.report-toolbar .form-group {
    margin-bottom: 0;
    min-width: 150px;
}

/* A min/max or from/to pair inside one .report-toolbar field. */
.report-filter-range {
    display: flex;
    align-items: center;
    gap: var(--spacing-8);
}

.report-filter-range input[type="number"] {
    width: 5.5rem;
}

.report-filter-range input[type="date"] {
    width: 9.5rem;
}

/* A checkbox filter ("Eligible & not yet registered", "Incomplete data"). */
.report-toolbar .report-filter-flag {
    align-self: center;
}

.report-toolbar .report-filter-flag label {
    display: flex;
    align-items: center;
    gap: var(--spacing-8);
    margin: 0;
    font-weight: var(--font-weight-normal);
    white-space: nowrap;
}

.report-summary {
    font-size: var(--font-size-large);
    margin-bottom: var(--spacing-16);
}

/* Numbered report pager (includes/report-export.php's
   render_report_pagination())  Prev/Next + a windowed page-number list
   with "..." gaps for anything past 7 pages, used by every 2.7 report
   (Population, Gender, Voter Status, Senior Citizen, Deceased, Blotter,
   Audit Trail). */
.report-pagination {
    margin-top: var(--spacing-24);
    text-align: center;
}

.report-pagination-summary {
    margin: 0 0 var(--spacing-8) 0;
    font-size: var(--font-size-small);
}

.report-pagination-controls {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--spacing-4);
}

.report-pagination-controls .btn {
    min-width: 40px;
}

.report-pagination-current {
    background-color: var(--color-primary);
    color: var(--color-white);
    border-color: var(--color-primary);
    cursor: default;
}

.report-pagination-controls [aria-disabled="true"] {
    opacity: 0.4;
    cursor: not-allowed;
}

.report-pagination-ellipsis {
    padding: 0 var(--spacing-4);
    color: var(--color-text-light);
}

/* =====================================================================
 * TABLES
 * ===================================================================== */

.table-container {
    overflow-x: auto;
    margin-bottom: var(--spacing-24);
}

table {
    width: 100%;
    border-collapse: collapse;
    background-color: var(--color-surface);
    box-shadow: var(--shadow-sm);
    border-radius: var(--border-radius-md);
    overflow: hidden;
}

thead {
    background-color: var(--color-primary);
    color: var(--color-white);
}

th {
    padding: 10px var(--spacing-16);
    text-align: left;
    font-weight: var(--font-weight-bold);
    font-size: var(--font-size-small);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    white-space: nowrap;
}

td {
    padding: 11px var(--spacing-16);
    border-bottom: 1px solid var(--color-border);
    vertical-align: middle;
}

/* Keep CRUD list-table cells on one line: on a narrow screen a data table
   reads far better scrolling sideways inside .table-container than with
   every name and date wrapping to three lines. Scoped to the selectable
   list tables so the Reports tables (which have long free-text columns
   and are printed) are untouched. Opt a column back in with
   `white-space: normal` on that cell where wrapping is actually wanted. */
table[data-selectable] td {
    white-space: nowrap;
}

/* Sortable column header  a full-cell link (see sort_th() in
   includes/list-controls.php). The ▲/▼ sits in a fixed-width slot so the
   heading text doesn't shift when the arrow appears. */
th.th-sort {
    padding: 0;
}

th.th-sort a {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 10px var(--spacing-16);
    color: inherit;
    text-decoration: none;
}

th.th-sort a:hover {
    background-color: rgba(255, 255, 255, 0.12);
}

th.th-sort.is-sorted a {
    text-decoration: underline;
}

th.th-sort__arrow,
.th-sort__arrow {
    display: inline-block;
    min-width: 0.9em;
    font-size: 0.85em;
}

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

/* Alternate row colors  a faint tint, clearly still "inside the card"
   (not var(--color-secondary), which is the page background). */
.table-striped tbody tr:nth-child(odd) {
    background-color: var(--color-surface-2);
}

tbody tr:hover {
    background-color: var(--color-surface-3);
}

/* =====================================================================
 * LIST SELECTION  checkbox column + above-table action toolbar
 * (see includes/list-controls.php + initTableSelection() in ui.js)
 * ===================================================================== */

/* The narrow first column that holds each row's checkbox. */
th.select-col,
td.select-col {
    width: 36px;
    text-align: center;
    padding-left: var(--spacing-8);
    padding-right: var(--spacing-8);
}

th.select-col input,
td.select-col input {
    cursor: pointer;
    width: 16px;
    height: 16px;
}

/* A ticked row. !important so it wins over the stripe + hover rules
   above without needing a more specific selector for each. */
table[data-selectable] tbody tr.is-selected,
table[data-selectable] tbody tr.is-selected:hover {
    background-color: var(--color-surface-selected) !important;
}

/* Rows are clickable to select  hint that. */
table[data-selectable] tbody tr {
    cursor: pointer;
}

table[data-selectable] tbody tr td.text-center {
    cursor: default;
}

.selection-toolbar {
    display: flex;
    align-items: center;
    gap: var(--spacing-16);
    flex-wrap: wrap;
    background-color: var(--color-surface);
    padding: var(--spacing-8) var(--spacing-16);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-sm);
    margin-bottom: var(--spacing-16);
}

.selection-toolbar__actions {
    display: flex;
    gap: var(--spacing-8);
    flex-wrap: wrap;
    margin-left: auto;
}

.selection-count {
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    color: var(--color-text-light);
}

.btn[data-sel-action]:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    box-shadow: none;
}

/* =====================================================================
 * MODAL  small centered confirmation dialog (bulk-action confirm, etc.)
 * ===================================================================== */

.modal-overlay {
    position: fixed;
    inset: 0;
    background-color: var(--color-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-16);
    z-index: 1000;
}

.modal-overlay[hidden] {
    display: none;
}

.modal {
    background-color: var(--color-surface);
    border-radius: var(--border-radius-lg);
    box-shadow: var(--shadow-lg);
    max-width: 440px;
    width: 100%;
    padding: var(--spacing-24);
}

/* Centers the full-page mobile login's card (modules/resident/
   login-page.php)  same max-width as .modal above so the two feel
   like the same design, just one's a dialog and the other's a page. */
.login-page-card {
    max-width: 440px;
    margin: 0 auto;
}

/* Centered heading/subtext, same convention as the login MODAL's own
   title (.login-modal-header, text-align: center above). */
.login-page-header {
    text-align: center;
}

.login-page-header h1 {
    font-size: 36px;
    font-weight: var(--font-weight-bold);
    margin-bottom: var(--spacing-8);
}

/* Shared by the full login page and the login modal (partials/login-modal.php). */
.login-page-consent {
    font-size: var(--font-size-small);
    text-align: center;
    margin: var(--spacing-16) 0 0;
}

/* Mobile: the full login page drops the boxed-card look entirely  fields
   sit directly on the page (same gutter every other resident page already
   gets from .resident-content's own padding) instead of inside a second,
   redundant white box. The card keeps its max-width/margin:auto centering
   from the base rule above (so it doesn't stretch edge-to-edge on a wider
   phone/small tablet still under this breakpoint) and gets its own
   left/right padding since dropping .card-body's padding left it with
   only .resident-content's outer gutter. Desktop keeps the boxed card as
   a fallback in case this page is ever reached directly at a wide
   viewport (normally desktop uses the modal instead  see
   initLoginModal() in assets/js/ui.js). */
@media (max-width: 767.98px) {
    .login-page-card {
        padding: 0 var(--spacing-16);
    }

    .login-page-card .card {
        background: none;
        box-shadow: none;
        border-radius: 0;
    }

    .login-page-card .card-body {
        padding: 0;
    }

    /* No divider under the heading/subtext on this page  .page-header's
       base border-bottom (layout.css) reads as a stray line between the
       subtext and the first field once there's no card separating them.
       Compound selector (not just .login-page-header) needed to outrank
       .page-header's own rule: layout.css loads after this file, and an
       unconditional single-class rule there would otherwise win the
       cascade over an equally-specific single-class rule in here, media
       query or not. */
    .page-header.login-page-header {
        border-bottom: none;
        padding-bottom: 0;
    }

    /* Footer and the floating chatbot button (#chatbot, partials/
       footer.php) are dropped on this page specifically  a focused
       login screen, not the usual portal chrome. :has() scopes this to
       pages containing .login-page-header (only login-page.php) without
       touching the shared header/footer partials themselves; already
       used elsewhere in this codebase (see layout.css's p:has(...) rule). */
    body:has(.login-page-header) .resident-footer,
    body:has(.login-page-header) #chatbot {
        display: none;
    }

    /* With the footer gone, .resident-content (flex:1 inside
       .resident-container's column flex  layout.css) now stretches to
       fill all the remaining viewport height by itself instead of
       sharing it with a footer below. Center its child
       (.resident-content-inner, holding the heading + login card)
       vertically in that space instead of leaving it pinned to the top
       with empty gray space under it. */
    body:has(.login-page-header) .resident-content {
        display: flex;
        flex-direction: column;
        justify-content: center;
    }
}

.modal-title {
    font-size: var(--font-size-large);
    margin-bottom: var(--spacing-8);
}

.modal-body {
    color: var(--color-text-light);
    margin-bottom: var(--spacing-24);
    line-height: 1.5;
}

.modal-actions {
    display: flex;
    gap: var(--spacing-8);
    justify-content: flex-end;
}

/* Document modal  a long, scrollable read-then-accept dialog (the
   Terms/Privacy consent step on modules/resident/register.php). Fixed
   header + footer, only the document body scrolls, so the action
   buttons never scroll out of view. Full-screen sheet on phones. */
.modal--document {
    max-width: 760px;
    max-height: 88vh;
    display: flex;
    flex-direction: column;
    padding: 0;
    overflow: hidden;
}

.modal-document-header {
    padding: var(--spacing-24) var(--spacing-24) var(--spacing-16);
    border-bottom: 1px solid var(--color-border);
}

.modal-document-header .form-help {
    margin: 0;
}

.modal-document-step {
    margin: 0 0 var(--spacing-4);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    color: var(--color-primary-light); /* --color-primary is too dark on the dark-mode surface */
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.modal-document-body {
    flex: 1 1 auto;
    min-height: 0; /* lets the flex child shrink so overflow-y engages */
    overflow-y: auto;
    padding: var(--spacing-16) var(--spacing-24);
    line-height: 1.6;
}

/* Focused programmatically on open (so keyboard users can scroll it
   with arrows/PgDn straight away)  inset ring instead of the global
   offset outline, which the header/footer borders would clip. */
.modal-document-body:focus-visible {
    outline-offset: -3px;
}

/* The policy partials are written as page .section cards; inside the
   dialog they read as plain flowing text instead of boxes-in-a-box. */
.modal-document-body .section {
    background: none;
    box-shadow: none;
    border-radius: 0;
    padding: 0;
    margin-bottom: var(--spacing-24);
}

.modal-document-body h2 {
    font-size: var(--font-size-base);
}

.modal-document-footer {
    padding: var(--spacing-16) var(--spacing-24);
    border-top: 1px solid var(--color-border);
    background-color: var(--color-surface-2);
}

.modal-document-hint {
    margin: 0 0 var(--spacing-12);
    font-size: var(--font-size-small);
    color: var(--color-text-light);
}

/* No app-wide .btn:disabled look exists (only the list toolbar's), and
   a not-yet-scrolled "I Agree" has to read as unavailable. */
.modal--document .btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    box-shadow: none;
}

.modal-document-body[hidden],
.modal--document .btn[hidden] {
    display: none; /* .btn's inline-flex would otherwise beat [hidden] */
}

@media (max-width: 600px) {
    .modal-overlay:has(.modal--document) {
        padding: 0;
    }

    .modal--document {
        max-width: none;
        max-height: none;
        height: 100%;
        border-radius: 0;
    }

    .modal-document-header,
    .modal-document-body,
    .modal-document-footer {
        padding-left: var(--spacing-16);
        padding-right: var(--spacing-16);
    }

    .modal--document .modal-actions {
        flex-wrap: wrap;
    }

    .modal--document .modal-actions .btn {
        flex: 1 1 auto;
    }
}

/* =====================================================================
 * ALERTS / MESSAGES
 * ===================================================================== */

.alert {
    padding: var(--spacing-16);
    margin-bottom: var(--spacing-24);
    border-radius: var(--border-radius-md);
    border-left: 4px solid transparent;
    font-weight: var(--font-weight-normal);
    overflow: hidden;
}

/*
 * Applied by assets/js/ui.js (initAlertAutoDismiss) a few seconds after
 * a .alert-success renders, then the element is removed once this
 * transition finishes. Only success messages auto-dismiss  errors
 * stay put until the user has read and acted on them.
 */
.alert-dismissing {
    transition: max-height 0.5s ease, opacity 0.5s ease, margin 0.5s ease, padding 0.5s ease;
    opacity: 0;
    margin-bottom: 0;
    padding-top: 0;
    padding-bottom: 0;
}

.alert-success {
    background-color: var(--alert-success-bg);
    border-left-color: var(--color-success);
    color: var(--alert-success-text);
}

.alert-error {
    background-color: var(--alert-error-bg);
    border-left-color: var(--color-danger);
    color: var(--alert-error-text);
}

.alert-warning {
    background-color: var(--alert-warning-bg);
    border-left-color: var(--color-warning);
    color: var(--alert-warning-text);
}

.alert-info {
    background-color: var(--alert-info-bg);
    border-left-color: var(--color-primary-light);
    color: var(--alert-info-text);
}

/* =====================================================================
 * BADGES
 * =====================================================================
 * Small colored status/role pills, e.g. blotter status or a staff
 * user's role. The base class sets shape/spacing; each caller sets its
 * own background-color inline based on the value being displayed (see
 * modules/admin/blotters/index.php and modules/admin/users/index.php).
 * ===================================================================== */

.badge {
    display: inline-block;
    padding: var(--spacing-4, 4px) var(--spacing-8);
    border-radius: var(--border-radius-md);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    color: #fff;
    white-space: nowrap;
}

/* =====================================================================
 * CARDS
 * ===================================================================== */

.card {
    background-color: var(--color-surface);
    border-radius: var(--border-radius-lg);
    box-shadow: var(--shadow-md);
    overflow: hidden;
    margin-bottom: var(--spacing-24);
}

.card-header {
    background-color: var(--color-primary);
    color: var(--color-white);
    padding: var(--spacing-16);
    border-bottom: 1px solid var(--color-border);
}

.card-body {
    padding: var(--spacing-24);
}

/* A row of cards side by side, e.g. Backup & Restore's Create/Restore
   pair  a small reusable wrapper so a page needing this doesn't
   hand-roll its own flex styles inline. */
.card-row {
    display: flex;
    gap: var(--spacing-24);
    flex-wrap: wrap;
    margin-bottom: var(--spacing-24);
}

.card-row > .card {
    flex: 1;
    min-width: 280px;
    margin-bottom: 0;
}

/* A .card-header's title, optionally preceded by a nav_icon() svg
   (includes/admin-sidebar.php) so a card can echo the same icon as its
   sidebar entry. Safe to apply globally  a plain text-only h2 lays out
   identically under flex. */
.card-header h2 {
    display: flex;
    align-items: center;
    gap: var(--spacing-8);
    margin: 0;
}

.card-header .nav-icon {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
}

/* The "Using: mysqldump / PHP fallback" note inside Backup & Restore's
   Create Backup card (modules/admin/utilities/backup-restore.php)  a
   small tinted callout, same surface-2 treatment as the notification
   toggle rows on modules/resident/profile/index.php. */
.backup-method-status {
    display: flex;
    align-items: center;
    gap: var(--spacing-16);
    flex-wrap: wrap;
    padding: var(--spacing-12) var(--spacing-16);
    background-color: var(--color-surface-2);
    border-radius: var(--border-radius-md);
    margin: var(--spacing-16) 0;
}

/* A clickable "pick one of these" card: a radio/checkbox plus its own
   title and description in one bordered box, instead of a bare label
   next to a shared paragraph of explanation below several options.
   First used by Backup & Restore's key-mode picker
   (modules/admin/utilities/backup-restore.php). Same shape as
   modules/admin/survey-review/review.php's page-scoped .merge-option
   that page hasn't been migrated onto this shared class yet, flagging
   here for whoever next touches it to consolidate. */
.option-card {
    display: block;
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    padding: var(--spacing-12) var(--spacing-16);
    margin-bottom: var(--spacing-8);
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}

.option-card:hover {
    border-color: var(--color-primary-light);
}

.option-card:has(input:checked) {
    border-color: var(--color-primary);
    background-color: var(--color-surface-2);
}

.option-card:has(input:disabled) {
    opacity: 0.5;
    cursor: not-allowed;
}

.option-card input[type="radio"],
.option-card input[type="checkbox"] {
    margin-right: var(--spacing-8);
}

.option-card-title {
    font-weight: var(--font-weight-bold);
}

.option-card-desc {
    display: block;
    margin: var(--spacing-4) 0 0 calc(1em + var(--spacing-8));
    color: var(--color-text-light);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-normal);
}

/* =====================================================================
 * SWITCHES & ROLE PERMISSIONS (2.8.4)
 * =====================================================================
 * A reusable toggle switch component, first used by Role Permissions
 * (2.8.4 matrix). Appears as a small on/off control and responds to
 * focus-visible with the existing global checkbox outline rule.
 * ===================================================================== */

.switch {
    --switch-w: 36px;
    --switch-h: 20px;
    --switch-thumb: 16px;

    position: relative;
    display: inline-flex;
    align-items: center;
    width: var(--switch-w);
    height: var(--switch-h);
    cursor: pointer;
    flex-shrink: 0;
}

.switch input[type="checkbox"] {
    position: absolute;
    inset: 0;
    opacity: 0;
    margin: 0;
    cursor: pointer;
    width: 100%;
    height: 100%;
}

.switch__track {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: 999px;
    background: var(--color-border);
    transition: background-color 0.15s ease;
}

.switch__thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: var(--switch-thumb);
    height: var(--switch-thumb);
    border-radius: 50%;
    background: var(--color-surface);
    box-shadow: var(--shadow-sm);
    transition: transform 0.15s ease;
}

.switch input[type="checkbox"]:checked ~ .switch__track {
    background: var(--color-success);
}

.switch input[type="checkbox"]:checked ~ .switch__track .switch__thumb {
    transform: translateX(calc(var(--switch-w) - var(--switch-h)));
}

.switch--sm {
    --switch-w: 30px;
    --switch-h: 17px;
    --switch-thumb: 13px;
}

.switch--xs {
    --switch-w: 26px;
    --switch-h: 15px;
    --switch-thumb: 11px;
}

.switch--custom::after {
    content: '';
    position: absolute;
    top: -3px;
    right: -3px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-warning);
}

.switch--dirty .switch__track {
    box-shadow: 0 0 0 2px var(--color-accent) inset;
}

/* =====================================================================
 * ROLE PERMISSIONS PAGE (2.8.4)
 * =====================================================================
 * The admin matrix for toggling staff role permissions and e-service
 * flags. Includes search, collapsible groups, select-all toggles, and
 * a sticky save bar at the bottom.
 * ===================================================================== */

.perm-reset-form {
    flex-shrink: 0;
}

.perm-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--spacing-16);
    margin-bottom: var(--spacing-16);
    flex-wrap: wrap;
}

.perm-search {
    display: flex;
    align-items: center;
    gap: var(--spacing-8);
    flex: 1 1 auto;
}

.perm-search-icon {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    color: var(--color-text-light);
}

.perm-search-input {
    flex: 1 1 auto;
    padding: var(--spacing-8) var(--spacing-12);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    font-size: var(--font-size-base);
    background: var(--color-surface);
    color: var(--color-text);
    transition: border-color 0.15s ease;
}

.perm-search-input:focus {
    outline: none;
    border-color: var(--color-primary);
}

.perm-matrix-wrap {
    position: relative;
}

/* .table-container only declares overflow-x, but per spec a non-visible
   overflow-x forces overflow-y to compute as auto too (it can't be
   overridden back to visible, even with !important  the two axes'
   computed values are resolved together). That silently makes this box
   a vertical scroll container as well, and since it never actually grew
   taller than its content, that scroll container never scrolled  which
   left the thead's sticky top permanently inert. Giving it a real
   max-height makes that already-forced overflow-y actually engage, so
   the matrix scrolls (and the header sticks) within its own bounded
   panel instead of relying on the whole page to scroll. */
.perm-matrix-wrap .table-container {
    max-height: 65vh;
}

.perm-table {
    width: 100%;
    /* separate, not collapse: Chromium/WebKit don't honor position:sticky
       on table cells when the table's borders are collapsed, which would
       silently break both the sticky header and the sticky label column. */
    border-collapse: separate;
    border-spacing: 0;
    font-size: var(--font-size-small);
    /* The generic `table { overflow: hidden }` rule above (for rounded-
       corner tables elsewhere) turns the table itself into a clipping
       box, which becomes the sticky containing context instead of
       .table-container  silently breaking every sticky cell. */
    overflow: visible;
    border-radius: 0;
    box-shadow: none;
}

.perm-table th,
.perm-table td {
    padding: var(--spacing-8) var(--spacing-12);
    border-bottom: 1px solid var(--color-border);
    text-align: left;
}

.perm-table thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--color-surface);
    color: var(--color-text);
    text-transform: none;
    letter-spacing: 0;
    border-bottom: 2px solid var(--color-border);
}

.perm-col-label {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--color-surface);
}

.perm-table thead th.perm-col-label {
    z-index: 3;
}

.perm-table th.role-col,
.perm-table td.role-col {
    text-align: center;
}

.perm-role-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: var(--spacing-8);
}

.perm-table tr.group-row td {
    background: var(--color-surface-3);
    padding: 0;
    border: none;
}

.perm-group-toggle {
    display: flex;
    align-items: center;
    gap: var(--spacing-8);
    width: 100%;
    padding: var(--spacing-8) var(--spacing-12);
    border: none;
    background: inherit;
    font-weight: var(--font-weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 11px;
    color: var(--color-text-light);
    text-align: left;
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.perm-group-toggle:hover {
    background-color: var(--color-surface-2);
}

.perm-chevron {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    transition: transform 0.15s ease;
}

.perm-group-toggle[aria-expanded="false"] .perm-chevron {
    transform: rotate(-90deg);
}

.perm-table tr.module-row td {
    background: var(--color-surface-2);
    font-weight: var(--font-weight-bold);
    padding: var(--spacing-8) var(--spacing-12);
}

.perm-module-head {
    display: flex;
    align-items: center;
    gap: var(--spacing-8);
}

.perm-module-head svg {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    color: var(--color-text);
}

/* No display/width override here on purpose: .perm-switch is the SAME
   element as .switch (<label class="switch perm-switch">), not a
   separate wrapper around it  a width:100% here would stretch the
   switch pill itself to fill the column (that was tried and looked
   wrong), not center it. .switch's own default `inline-flex` is an
   inline-level box, so the existing `text-align: center` on
   .perm-table td.role-col centers it correctly with no extra rule
   needed; the mobile media query's own `td.role-col { display: flex }`
   positions it there instead once this becomes a flex item. */

.perm-empty {
    background: var(--color-surface-2);
}

.perm-empty td {
    text-align: center;
    color: var(--color-text-light);
    font-style: italic;
    padding: var(--spacing-24);
}

/* A plain settings-style list, not a card grid  there are only three
   flags, so one row each (icon, label, switch) reads more simply than a
   grid of bordered/highlighted boxes, and needs no separate mobile
   treatment since a single column already works at any width. .is-on
   is still toggled by assets/js/permissions.js on each change but no
   longer styled  the switch's own on/off color is indicator enough. */
.perm-flag-grid {
    display: flex;
    flex-direction: column;
    margin-top: var(--spacing-12);
}

.perm-flag-card {
    display: flex;
    align-items: center;
    gap: var(--spacing-12);
    padding: var(--spacing-12) 0;
    border-bottom: 1px solid var(--color-border);
}

.perm-flag-card:last-child {
    border-bottom: none;
}

.perm-flag-card svg {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    color: var(--color-text-light);
}

.perm-flag-card__label {
    flex: 1 1 auto;
    font-size: var(--font-size-base);
    color: var(--color-text);
}

.perm-flag-card .switch {
    margin-left: auto;
}

.perm-savebar {
    position: sticky;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-16);
    padding: var(--spacing-16);
    margin-top: var(--spacing-16);
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-sm);
    flex-wrap: wrap;
}

.perm-savebar[hidden] {
    display: none;
}

.perm-savebar__count {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    font-weight: var(--font-weight-bold);
}

.perm-savebar__actions {
    display: flex;
    gap: var(--spacing-12);
}

.perm-note {
    color: var(--color-text-light);
    font-size: var(--font-size-small);
}

/* =====================================================================
 * NAVIGATION
 * ===================================================================== */

.navbar {
    position: sticky;
    top: 0;
    z-index: 30;

    /* subtle depth from a top-lit gradient, not a flat fill */
    background-color: var(--color-primary);
    background-image: linear-gradient(180deg, #24479e 0%, var(--color-primary) 100%);
    color: var(--color-white);

    min-height: 56px;
    padding: var(--spacing-8) var(--spacing-24);
    display: flex;
    align-items: center;
    justify-content: space-between;

    border-bottom: 3px solid var(--color-primary-light);
    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.18);
}

.navbar-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-8);
    font-size: var(--font-size-large);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.01em;
    color: var(--color-white);
    text-decoration: none;
}

.navbar-brand::before {
    content: "";
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    border-radius: var(--border-radius-sm);
    background: rgba(255, 255, 255, 0.14) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='3' width='16' height='18' rx='2'/%3E%3Cpath d='M9 3v3h6V3'/%3E%3Cpath d='M8 12h8M8 16h5'/%3E%3C/svg%3E") center / 16px no-repeat;
}

/* Resident portal only (modules/resident/portal/partials/header.php):
   "Barangay Balibago Lian" is longer than the staff side's brand, and
   on a 320px phone it pushed the hamburger onto a second line. Same fix
   as the public bar's .resident-navbar-brand: grow from 0 so the brand
   gives way (ellipsizing its <span> if it must), and a step smaller on
   the narrowest phones so it normally fits in full. */
.resident-portal-brand {
    flex: 1 1 0;
    min-width: 0;
}

.resident-portal-brand::before {
    display: none;
}

.resident-portal-brand .navbar-logo {
    width: 32px;
    height: 32px;
}

.resident-portal-brand span {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

@media (max-width: 359.98px) {
    .resident-portal-brand {
        font-size: 16px;
    }
}

@media (min-width: 768px) {
    .resident-portal-brand {
        flex: 0 0 auto;
    }
}

.navbar-menu {
    display: flex;
    gap: var(--spacing-12);
    align-items: center;
}

/* .navbar-menu's non-bell content (theme toggle, identity chip, Log
   out) is moved at runtime into a .navbar-menu-panel wrapper by ui.js's
   initNavMenuPanel()  see the mobile media query below for why. This
   base rule (applying everywhere, not just mobile) mirrors
   .navbar-menu's own flex/gap/align-items exactly, so the extra level
   of DOM nesting is visually invisible on desktop: the panel is just
   another flex row, and its children still line up with the same
   spacing they had as .navbar-menu's own direct children before.
   Every selector below that used to read ".navbar-menu > X" now reads
   ".navbar-menu-panel > X" for the same reason  those children really
   do live one level deeper now. */
.navbar-menu-panel {
    display: flex;
    gap: var(--spacing-12);
    align-items: center;
}

/* Groups the identity chip as "your stuff" and sets Log out apart as
   the one exit action, instead of items floating in an undifferentiated
   row (shared by every admin page and the resident portal  see
   modules/admin/.../header.php and modules/resident/.../header.php).
   NOTE (2026-09-22): this comment used to spell that last path with a
   wildcard segment between two slashes, which  read as plain
   characters rather than English  is the two-character sequence that
   closes a CSS comment. That silently ended THIS comment early and
   dropped the entire rule below (invalid selector prelude, whole
   ruleset discarded by the parser) for however long it went
   unnoticed. Lesson: never write a path shorthand, glob, or anything
   else inside a CSS comment that could contain "asterisk immediately
   followed by slash" as plain text  spell it out instead, the way the
   line above this note now does. */
.navbar-menu-panel > a:last-child {
    margin-left: var(--spacing-4);
    padding-left: var(--spacing-16);
    border-left: 1px solid rgba(255, 255, 255, 0.22);
    border-top: none;
    border-right: none;
    border-bottom: none;
    border-radius: 0;
    background: none !important;
}

.navbar-menu-panel > a:last-child:hover {
    background: none !important;
    color: rgba(255, 255, 255, 0.75);
}

/* the logged-in username reads as an identity chip with a small avatar
   .navbar-identity (Phase 17: the username is now a real link to
   modules/admin/profile/index.php on every admin page) gets the exact
   same look; higher specificity than the plain ".navbar-menu a" ghost-
   button rule below, so it doesn't pick up that border/hover style. */
.navbar-menu-panel > span,
.navbar-menu-panel > .navbar-identity {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-8);
    padding: var(--spacing-4) var(--spacing-16) var(--spacing-4) var(--spacing-4);
    background: rgba(255, 255, 255, 0.12);
    border-radius: 999px;
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    text-transform: capitalize;
    text-decoration: none;
}

.navbar-menu-panel > span::before,
.navbar-menu-panel > .navbar-identity::before {
    content: "";
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    border-radius: 999px;
    background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%231e3a8a'%3E%3Ccircle cx='12' cy='8.5' r='4'/%3E%3Cpath d='M4 21c0-4.4 3.6-7.5 8-7.5s8 3.1 8 7.5z'/%3E%3C/svg%3E") center / 16px no-repeat;
}

.navbar-menu-panel > .navbar-identity:hover {
    background: rgba(255, 255, 255, 0.2);
}

/* staff_navbar_avatar_html() (includes/helpers.php) adds this modifier
   + an <img> when the account has a real photo  suppress the generic
   silhouette ::before so the two don't stack, and give the photo the
   same 24px circular treatment. */
.navbar-menu-panel > .navbar-identity--has-photo::before {
    content: none;
}

.navbar-identity-photo {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    border-radius: 999px;
    object-fit: cover;
}

/* The larger avatar on a profile page itself  staff's own
   (modules/admin/profile/index.php, users.profile_pic) and a
   resident's (modules/resident/profile/index.php, resident_records.
   image). Both are read-only displays of a staff-set photo, shared
   here so they match instead of each page styling its own copy.
   Falls back to the same generic-person silhouette as the navbar
   chip's default ::before icon when there's no photo on file. */
.profile-photo {
    width: 96px;
    height: 96px;
    border-radius: 999px;
    object-fit: cover;
    margin-bottom: var(--spacing-24);
    background: var(--color-surface-2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2394a3b8'%3E%3Ccircle cx='12' cy='8.5' r='4'/%3E%3Cpath d='M4 21c0-4.4 3.6-7.5 8-7.5s8 3.1 8 7.5z'/%3E%3C/svg%3E") center / 48px no-repeat;
}

/* Read-only key/value sections on a profile page (staff's own 
   modules/admin/profile/index.php  and a resident's  modules/resident/
   profile/index.php). Groups fields under a section title inside a
   .section card; shared here so both profile pages match instead of
   each styling its own copy. */
.profile-info-section {
    margin-bottom: var(--spacing-32);
}

.profile-info-section:last-child {
    margin-bottom: 0;
}

.profile-info-section-title {
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    color: var(--color-text-light);
    text-transform: uppercase;
    letter-spacing: 0.3px;
    margin-bottom: var(--spacing-16);
    padding-bottom: var(--spacing-8);
    border-bottom: 1px solid var(--color-border);
}

.profile-info-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--spacing-24) var(--spacing-32);
}

.profile-info-item {
    display: flex;
    flex-direction: column;
}

.profile-info-label {
    font-size: var(--font-size-small);
    text-transform: uppercase;
    color: var(--color-text-light);
    font-weight: var(--font-weight-bold);
    margin-bottom: var(--spacing-8);
    letter-spacing: 0.3px;
}

.profile-info-value {
    font-size: var(--font-size-base);
    color: var(--color-text);
    font-weight: var(--font-weight-normal);
    word-break: break-word;
}

.profile-info-placeholder {
    font-size: var(--font-size-base);
    color: var(--color-text-light);
    font-weight: var(--font-weight-normal);
    font-style: italic;
}

/* "Log out" is a real action  give it a ghost-button affordance */
.navbar-menu a {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-8);
    padding: var(--spacing-8) var(--spacing-16);
    border: 1px solid rgba(255, 255, 255, 0.4);
    border-radius: var(--border-radius-md);
    color: var(--color-white);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    text-decoration: none;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

/* Sat as bare text before  every other row in this menu (theme toggle,
   profile identity) has a leading icon, so Log out looked inconsistent
   without one, especially once it's a full list row on mobile. Same
   inline-SVG-as-background-image approach as .navbar-brand::before /
   .navbar-identity::before above, so no markup change is needed in any
   of the ~38 admin pages (or the resident portal header) that render
   this link. */
.navbar-menu a::before {
    content: "";
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'/%3E%3Cpolyline points='16 17 21 12 16 7'/%3E%3Cline x1='21' y1='12' x2='9' y2='12'/%3E%3C/svg%3E") center / contain no-repeat;
}

.navbar-menu a:hover {
    background-color: rgba(255, 255, 255, 0.16);
    border-color: rgba(255, 255, 255, 0.75);
    text-decoration: none;
}

.hamburger {
    display: none;
    flex-direction: column;
    gap: var(--spacing-4);
    cursor: pointer;
    background: none;
    border: none;
    color: var(--color-white);
    font-size: 24px;
}

.hamburger span {
    display: block;
    width: 24px;
    height: 3px;
    border-radius: 2px;
    background-color: var(--color-white);
    transition: all 0.3s ease;
}

/* =====================================================================
 * DARK-MODE TOGGLE
 * =====================================================================
 * One button, injected by assets/js/ui.js (initThemeToggle) into
 * whichever navbar the page has (.navbar-menu on the admin side,
 * .resident-nav-auth on the portal). Those bars sit on the blue chrome,
 * so the default styling is the same white-outline "ghost" treatment as
 * the Log out link next to it. On a page with no navbar (login, 404)
 * the script adds .theme-toggle--floating instead  a fixed pill that
 * uses surface colours because it sits on the page background.
 * ===================================================================== */
.theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, 0.4);
    border-radius: var(--border-radius-md);
    background-color: transparent;
    color: var(--color-white);
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

.theme-toggle:hover {
    background-color: rgba(255, 255, 255, 0.16);
    border-color: rgba(255, 255, 255, 0.75);
}

.theme-toggle svg {
    width: 24px;
    height: 24px;
}

/* Both icons are in the DOM; CSS shows the one for the ACTION the click
   will take, matching the button's aria-label:
     light mode  → moon  (--icon--dark)  "Switch to dark mode"
     dark  mode  → sun   (--icon--light) "Switch to light mode"
   Purely presentational, so it's driven by the same theme state the
   rest of the palette is  no JS needed to swap it. */
.theme-toggle__icon--light { display: none; }
.theme-toggle__icon--dark { display: block; }

:root[data-theme="dark"] .theme-toggle__icon--light { display: block; }
:root[data-theme="dark"] .theme-toggle__icon--dark { display: none; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]):not([data-theme="dark"]) .theme-toggle__icon--light { display: block; }
    :root:not([data-theme="light"]):not([data-theme="dark"]) .theme-toggle__icon--dark { display: none; }
}

.theme-toggle--floating {
    position: fixed;
    bottom: var(--spacing-16);
    left: var(--spacing-16);
    z-index: 900;
    width: 40px;
    height: 40px;
    border-radius: 999px;
    border: 1px solid var(--color-border);
    background-color: var(--color-surface);
    color: var(--color-text-light);
    box-shadow: var(--shadow-md);
}

.theme-toggle--floating:hover {
    background-color: var(--color-surface-2);
    border-color: var(--color-text-light);
    color: var(--color-text);
}

@media print {
    .theme-toggle {
        display: none !important;
    }
}

.sidebar {
    background-color: var(--color-surface);
    border-right: 1px solid var(--color-border);
    padding: var(--spacing-16) var(--spacing-8);
    min-width: 240px;
}

.sidebar-menu {
    list-style: none;
    margin: 0;
    padding: 0;
}

.sidebar-menu li {
    margin-bottom: var(--spacing-2);
}

/* Every clickable row  top-level links and group summaries  shares
 * this shape so the nav reads as one consistent list. */
.sidebar-menu a,
.sidebar-menu .nav-group__summary {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 12px;
    color: var(--color-text);
    font-weight: var(--font-weight-bold);
    border-radius: var(--border-radius-md);
    transition: background-color 0.15s ease, color 0.15s ease;
}

.sidebar-menu a:hover,
.sidebar-menu .nav-group__summary:hover {
    background-color: var(--color-surface-2);
    text-decoration: none;
}

.sidebar-menu a.active {
    background-color: var(--color-nav-active-bg);   /* soft blue tint */
    color: var(--color-accent);
    box-shadow: inset 3px 0 0 var(--color-accent);
}

.sidebar-menu .nav-icon {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    opacity: 0.85;
}

.sidebar-menu a.active .nav-icon {
    opacity: 1;
}

.sidebar-menu .nav-divider {
    height: 1px;
    background-color: var(--color-border);
    margin: var(--spacing-8) 12px;
}

/* -------------------------------------------------------------------
 * Collapsible nav group (a <details> in the sidebar, e.g. Reports)
 * ------------------------------------------------------------------- */
.sidebar-menu .nav-group {
    margin-bottom: var(--spacing-2);
}

.sidebar-menu .nav-group__summary {
    cursor: pointer;
    list-style: none;              /* Firefox: hide default triangle */
    user-select: none;
}

.sidebar-menu .nav-group__summary::-webkit-details-marker {
    display: none;                 /* Chrome/Safari: hide default triangle */
}

.sidebar-menu .nav-group__summary span {
    flex: 1;
}

.sidebar-menu .nav-group__summary::after {
    content: "";
    width: 6px;
    height: 6px;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);     /* points right (collapsed) */
    transition: transform 0.15s ease;
    opacity: 0.7;
    margin-right: 3px;
}

.sidebar-menu .nav-group__details[open] > .nav-group__summary::after {
    transform: rotate(45deg);      /* points down (expanded) */
}

.sidebar-menu .nav-group__menu {
    list-style: none;
    margin: 2px 0 0;
    padding: 0 0 0 24px;           /* line up under the summary label */
}

.sidebar-menu .nav-group__menu li {
    margin-bottom: var(--spacing-2);
}

.sidebar-menu .nav-group__menu a {
    padding: 8px 12px;
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-normal);
    color: var(--color-text-light);
}

.sidebar-menu .nav-group__menu a:hover {
    color: var(--color-text);
}

.sidebar-menu .nav-group__menu a::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: currentColor;
    flex-shrink: 0;
    opacity: 0.4;
}

.sidebar-menu .nav-group__menu a.active::before {
    opacity: 1;
}

/* -------------------------------------------------------------------
 * Desktop sidebar collapse toggle  shrinks the sidebar to a slim
 * icon-only rail (layout.css's .sidebar-collapsed rules, min-width:768px
 * only; initSidebarCollapse() in assets/js/ui.js). Hidden on mobile 
 * the existing hamburger already shows/hides the whole sidebar there.
 * ------------------------------------------------------------------- */
.sidebar-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin: 0 0 var(--spacing-8) auto;
    background: transparent;
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    color: var(--color-text-light);
    cursor: pointer;
}

.sidebar-toggle:hover {
    background-color: var(--color-surface-2);
    color: var(--color-text);
}

.sidebar-toggle svg {
    width: 14px;
    height: 14px;
    transition: transform 0.2s ease;
}

.sidebar-collapsed .sidebar-toggle svg {
    transform: rotate(180deg);
}

/* =====================================================================
 * RESPONSIVE ADJUSTMENTS
 * ===================================================================== */

/* 767.98, not 768: the sidebar's own breakpoint is `min-width: 768px`, so
   an exact `max-width: 768px` here would double-fire at 768  permanent
   sidebar AND a wrapped navbar with a useless hamburger. */
@media (max-width: 767.98px) {
    /* Brand + hamburger (+ the bell, on a page that has one) share the
       top row. .navbar-menu-panel (below) no longer lives in this flow
       at all once expanded  it's an absolutely-positioned dropdown  so
       this row never needs to make room for it; flex-wrap/row-gap here
       are just a generic fallback for an unexpectedly long brand/title,
       not load-bearing for the bell/menu the way they used to be. */
    .navbar {
        flex-wrap: wrap;
        row-gap: var(--spacing-8);
        padding-left: var(--spacing-16);
        padding-right: var(--spacing-16);
        /* Multi-line flex containers default to stretching/packing lines
           from the cross-axis start (align-content: normal), which is
           why the brand+hamburger row previously sat noticeably above
           .navbar's own vertical center instead of in it. center
           distributes any leftover cross-space evenly around the line(s)
           instead. */
        align-content: center;
    }

    .navbar-brand {
        flex: 1 1 auto;
        min-width: 0;
    }

    /* Bare .hamburger on purpose  both the admin .navbar and the
       resident .resident-navbar reuse this same button class, and both
       want it visible at this width. Only `order` (below) is scoped to
       .navbar: it's meant to slot the button after the bell, and was
       leaking onto the resident hamburger too, sorting it after its nav
       menu instead of next to its brand once .resident-navbar also
       wraps on mobile. */
    .hamburger {
        display: flex;
    }

    /* Brand, then the bell (.navbar-menu, order 1 below), then the
       hamburger last  puts the notification bell to the LEFT of the
       hamburger button, per design. */
    .navbar .hamburger {
        order: 2;
    }

    /* order:1 ONLY here  no width/flex-wrap/justify-content. On mobile,
       .navbar-menu's only PERMANENT child is the notification bell
       (admin); everything else that used to live directly inside it
       (theme toggle, profile identity, Log out) is moved at runtime
       into a separate .navbar-menu-panel wrapper by ui.js's
       initNavMenuPanel()  see that function's docblock for the full
       reasoning. Sizing .navbar-menu to just the bell keeps it a small,
       normal inline flex item that sits in the SAME row as the
       brand/hamburger PERMANENTLY, collapsed or expanded: opening the
       hamburger no longer changes .navbar-menu's own size or position
       at all (.navbar-menu-panel, which DOES expand, is
       position:absolute and anchored to .navbar instead, below)  which
       is what stops the bell from visibly jumping around when the menu
       opens. (It used to: .navbar-menu itself went width:100% on open,
       dragging the bell inside it down into a second row along with
       everything else.) */
    .navbar-menu {
        order: 1;
        /* .navbar's own flex `gap` (8px) sits between .navbar-menu and
           .hamburger, but .badge-count (the unread-count pill on the
           bell) is position:absolute with `right: -8px`  it overhangs
           the bell button by exactly that gap, so the badge visually
           touched/overlapped the hamburger button next to it. Extra
           margin here widens the real gap without affecting the badge's
           own offset. */
        margin-right: var(--spacing-8);
    }

    /* .navbar-menu-panel (built at runtime, see ui.js's
       initNavMenuPanel()) holds the theme toggle / profile identity /
       Log out (admin) or profile identity / Log out (resident portal).
       ui.js's initMobileMenu() toggles .mobile-open on THIS element
       directly now, not on .navbar-menu itself.
       position:absolute anchored to .navbar (already position:sticky,
       which is a valid containing block on its own  no extra
       positioning context needed on .navbar-menu) is what lets this
       expand into a full-width dropdown WITHOUT .navbar-menu (and the
       bell inside it) having to grow or reflow to make room: an
       absolutely positioned element has zero effect on its parent's own
       box. left:0/right:0 relative to .navbar (not .navbar-menu, which
       is only ever as wide as the bell) spans the full bar's width,
       matching how the old width:100% .navbar-menu approach looked. */
    .navbar-menu-panel {
        display: none;
    }

    .navbar-menu-panel.mobile-open {
        display: flex;
        flex-direction: column;
        /* Overrides the base (desktop) .navbar-menu-panel rule's
           gap:12px/align-items:center  each row already gets its own
           vertical spacing from padding:12px 0 below, and align-items
           needs to go back to flex-start (stretch's default cross-axis
           behavior) now that this is a column, not a row. */
        gap: 0;
        align-items: stretch;
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        padding: var(--spacing-8) var(--spacing-16) var(--spacing-4);
        background-color: var(--color-primary);
        border-top: 1px solid rgba(255, 255, 255, 0.12);
        box-shadow: var(--shadow-lg);
        z-index: 25;
    }

    /* Each row (theme toggle / profile identity / Log out) reads as a
       plain stacked list item  one full-width row each, divided by a
       hairline  instead of their desktop pill/ghost-button chrome
       squeezed into a right-aligned row, the same "account menu" shape
       as a typical mobile nav drawer. No :not(.notification-bell)
       exclusion needed here (unlike the .navbar-menu.mobile-open rule
       this replaces)  the bell was never moved into this wrapper to
       begin with, so every child of .navbar-menu-panel gets the same
       treatment. */
    .navbar-menu-panel.mobile-open > * {
        display: flex;
        justify-content: flex-start;
        margin: 0;
        padding: var(--spacing-12) 0;
        border: none;
        border-radius: 0;
        background: none !important;
    }

    .navbar-menu-panel.mobile-open > *:not(:last-child) {
        border-bottom: 1px solid rgba(255, 255, 255, 0.15);
    }

    /* .theme-toggle is normally an icon-only 34x34 square (its
       aria-label is enough on desktop, where a tooltip/screen reader is
       the only audience)  as a full list row it needs a visible label
       too, shown via the same dynamic "Switch to light/dark mode" text
       ui.js already keeps aria-label/title in sync with, so no JS
       change is needed to surface it here. gap:8px (not 12) matches the
       profile identity chip and Log out link below it, so all three
       labels start at the same x position instead of the theme row's
       sitting 4px further right. */
    .navbar-menu-panel.mobile-open > .theme-toggle {
        width: 100%;
        height: auto;
        gap: var(--spacing-8);
    }

    .navbar-menu-panel.mobile-open > .theme-toggle::after {
        content: attr(aria-label);
        /* --font-size-small (12px), not -base (14px)  matches the
           profile identity chip and Log out link below it, which were
           already on -small; this was the odd one out. */
        font-size: var(--font-size-small);
        font-weight: var(--font-weight-bold);
        color: var(--color-white);
    }

    .sidebar {
        display: none;
        width: 100%;
        border-right: none;
        border-bottom: 1px solid var(--color-border);
        padding: var(--spacing-16);
    }

    .sidebar.mobile-open {
        display: block;
    }

    .table-container {
        font-size: var(--font-size-small);
    }

    th, td {
        padding: var(--spacing-8);
    }

    /* Full-width buttons only where stacking genuinely helps  the form
       button row and the list-page selection toolbar. Pagination and
       inline toolbars keep buttons at their natural width so they sit in
       a row instead of a tall stack of bars. (The filter bar's own Apply
       button goes full-width too, but that's set alongside its stacked
       fields  see the .filterbar rules in layout.css.) */
    .form-actions .btn,
    .selection-toolbar__actions .btn,
    .perm-savebar__actions .btn,
    .perm-reset-form .btn {
        width: 100%;
    }

    .selection-toolbar__actions {
        width: 100%;
        margin-left: 0;
    }

    .perm-toolbar {
        flex-direction: column;
        align-items: stretch;
    }

    .perm-search {
        width: 100%;
    }

    /* Below 768px the matrix becomes a stacked list of cards, one per
       action, instead of a horizontally-scrolling table  3 role columns
       plus a sticky label column don't fit a phone width without an
       awkward scroll-within-a-scroll (the panel is already vertically
       scrollable). This is a pure display:block restacking of the SAME
       <table>/<tr>/<td> markup (the classic responsive-table technique)
        assets/js/permissions.js's selectors and behavior (filtering,
       group collapsing, dirty-tracking) are completely untouched, since
       they operate on these same elements regardless of their CSS
       display value. Each checkbox already carries a full "Role 
       permission" aria-label, so hiding <thead> outright here (rather
       than just visually) loses nothing for assistive tech. */
    .perm-matrix-wrap .table-container {
        max-height: none;
        overflow: visible;
    }

    .perm-table thead {
        display: none;
    }

    .perm-table,
    .perm-table tbody,
    .perm-table tr,
    .perm-table td {
        display: block;
        width: auto;
    }

    /* The generic [hidden] UA-stylesheet rule loses to the block-display
       rules above (an author-origin rule always beats user-agent origin)
       unless restated here  without this, every row the filter/group-
       collapse JS hides via the `hidden` property would reappear. */
    .perm-table tr[hidden] {
        display: none;
    }

    .perm-table .perm-col-label {
        position: static;
    }

    .perm-table tr.perm-action-row {
        margin: var(--spacing-12) 0;
        padding: var(--spacing-12);
        background: var(--color-surface);
        border: 1px solid var(--color-border);
        border-radius: var(--border-radius-md);
    }

    .perm-table tr.perm-action-row td.action-label {
        padding: 0 0 var(--spacing-8);
        border-bottom: none;
        font-weight: var(--font-weight-bold);
        color: var(--color-text);
    }

    .perm-table tr.perm-action-row td.role-col {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: var(--spacing-8) 0;
    }

    .perm-table tr.perm-action-row td.role-col:last-child {
        border-bottom: none;
    }

    .perm-table tr.perm-action-row td.role-col::before {
        content: attr(data-label);
        color: var(--color-text-light);
    }
}

@media (max-width: 480px) {
    h1 {
        font-size: var(--font-size-xl);
    }

    h2 {
        font-size: var(--font-size-large);
    }

    .form-input,
    .form-select,
    .form-textarea,
    input,
    select,
    textarea {
        font-size: 16px; /* prevents auto-zoom on iOS */
    }
}

/* =====================================================================
 * RESIDENT PORTAL COMPONENTS
 * =====================================================================
 * Public-facing building blocks used across every page under
 * /modules/resident/ (Home, Services, About, Announcements). These sit
 * inside the .resident-header/.resident-content/.resident-footer
 * layout classes (layout.css)  same design tokens as the admin side,
 * different chrome: a top navbar instead of a sidebar, no "utilitarian"
 * table density, more breathing room.
 * ===================================================================== */

/* Small button variant  fits the compact navbar/hero action rows
   without the full-size .btn padding taking over on mobile. */
.btn-sm {
    padding: var(--spacing-4) var(--spacing-16);
    font-size: var(--font-size-small);
}

/* --- Top navbar (brand + links + auth area) ------------------------ */

.resident-navbar {
    display: flex;
    /* wrap matters even on mobile: .resident-nav-menu is display:none
       until .mobile-open, but once open it's a 3rd flex child (after
       brand + hamburger) with width:100%  without wrap it has to
       squeeze into whatever's left of THAT row instead of dropping to
       its own full-width row below, which squashed the brand text into
       two lines and crammed the links into a narrow column. */
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    /* row-gap: 0, not the shorthand  a plain `gap` also inserts spacing
       BETWEEN WRAPPED LINES, and .resident-nav-menu becomes its own
       wrapped line here (see the wrap note above). That 16px applied
       even while the menu is collapsed to 0 height (its own margin-top
       already goes 0 -> 16px on .mobile-open, see below), so the bar
       was reserving an extra invisible 16px under the brand/hamburger
       row at all times  which pushed that visible row above the bar's
       true center instead of centering it. column-gap has no visible
       effect either way (justify-content: space-between already pushes
       brand/hamburger to the row's own edges) but is kept for any future
       item added to this row. */
    column-gap: var(--spacing-16);
    row-gap: 0;
    max-width: 1100px;
    margin: 0 auto;
}

/* Brand: name, plus a "Lian, Batangas" line from tablet up (see the
   768px block near the end of this file). margin-right:auto pushes the
   phone Login button + hamburger to the right edge as one group. */
/* The seal beside the name; also used by the logged-in portal bar. */
.navbar-logo {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    object-fit: contain;
}

.resident-navbar-brand-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.resident-navbar-brand {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--spacing-8);
    /* Basis 0 + grow, not auto: in a wrapping flex row the line break is
       decided from each item's full content width BEFORE anything
       shrinks, so on a 320px phone the hamburger wrapped to a second
       line. Starting from 0, the brand just takes whatever is left (and
       ellipsizes if that's not enough), which also pushes the
       hamburger to the right edge. */
    flex: 1 1 0;
    min-width: 0;
    color: var(--color-white);
    line-height: 1.2;
}

.resident-navbar-brand-name {
    font-size: var(--font-size-large);
    font-weight: var(--font-weight-bold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.resident-navbar-brand-sub {
    display: none;
    font-size: var(--font-size-small);
    color: rgba(255, 255, 255, 0.8);
}

.resident-navbar-brand:hover {
    text-decoration: none;
    opacity: 0.9;
}

/* Visual order on phone: brand, hamburger, then the menu on its own
   wrapped row. Login + Register live inside that menu on phone. */
.resident-navbar .hamburger {
    order: 2;
    /* 44px tap target around the 24px bars. */
    width: 44px;
    height: 44px;
    align-items: center;
    justify-content: center;
    border-radius: var(--border-radius-md);
}

.resident-navbar .hamburger:focus-visible {
    outline: 2px solid var(--color-white);
    outline-offset: 2px;
}

/* Open state: the three bars fold into an X. 3px bars + 4px gaps, so
   the outer bars travel 7px to meet the middle one. */
.resident-navbar .hamburger[aria-expanded="true"] span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
}

.resident-navbar .hamburger[aria-expanded="true"] span:nth-child(2) {
    opacity: 0;
}

.resident-navbar .hamburger[aria-expanded="true"] span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
}

/* Collapsible on mobile (same show/hide mechanics as .sidebar, toggled
   by its own hamburger  see initResidentNav() in ui.js). Holds two
   groups  .resident-nav-links (page nav) and .resident-nav-auth
   (theme toggle + login state)  stacked on mobile, one row on desktop. */
.resident-nav-menu {
    /* display stays 'flex' always (not none/flex-toggled)  a transition
       can't play across a display:none boundary, it just jumps instantly
       either way. Collapsed state is done entirely with max-height +
       opacity + visibility instead, which DO animate. visibility's own
       transition is delayed on close (and instant on open) so the menu
       stays out of the tab order while collapsed without popping away
       mid-fade. */
    display: flex;
    order: 3; /* after the phone Login + hamburger, see above */
    width: 100%;
    flex-direction: column;
    gap: var(--spacing-16);
    margin-top: 0;
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    visibility: hidden;
    transition: max-height 0.3s ease, opacity 0.3s ease, margin-top 0.3s ease, visibility 0s linear 0.3s;
}

.resident-nav-menu.mobile-open {
    margin-top: var(--spacing-16);
    max-height: 1000px;
    opacity: 1;
    visibility: visible;
    transition: max-height 0.3s ease, opacity 0.3s ease, margin-top 0.3s ease, visibility 0s linear 0s;
}

.resident-nav-links {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--spacing-4);
}

.resident-nav-links a:not(.btn) {
    display: block;
    color: var(--color-white);
    padding: var(--spacing-8) var(--spacing-12);
    border-radius: var(--border-radius-md);
    transition: background-color 0.15s ease;
}

.resident-nav-links a:not(.btn):hover {
    background-color: rgba(255, 255, 255, 0.12);
    text-decoration: none;
}

/* "You are here" (header.php sets .is-current + aria-current="page").
   Phone: a filled pill in the stacked menu. Desktop swaps it for an
   underline bar (768px block near the end of this file). */
.resident-nav-links a.is-current {
    background-color: rgba(255, 255, 255, 0.16);
    font-weight: var(--font-weight-bold);
}

.resident-nav-auth {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-8);
    flex-wrap: wrap;
}

.resident-welcome {
    color: var(--color-white);
    font-size: var(--font-size-small);
}

/* Name + profile icon linking into the login-gated portal (Phase 16) 
   same pill-with-avatar affordance as the admin navbar's identity chip
   (.navbar-menu > span in this file), just a real link here since it
   navigates instead of only labelling. */
.resident-profile-link {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-8);
    padding: var(--spacing-4) var(--spacing-16) var(--spacing-4) var(--spacing-4);
    background: rgba(255, 255, 255, 0.12);
    border-radius: 999px;
    color: var(--color-white);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    text-transform: capitalize;
    text-decoration: none;
    transition: background-color 0.15s ease;
}

.resident-profile-link:hover {
    background: rgba(255, 255, 255, 0.2);
    text-decoration: none;
}

.resident-profile-link__icon {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    border-radius: 999px;
    background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%231e3a8a'%3E%3Ccircle cx='12' cy='8.5' r='4'/%3E%3Cpath d='M4 21c0-4.4 3.6-7.5 8-7.5s8 3.1 8 7.5z'/%3E%3C/svg%3E") center / 16px no-repeat;
}

/* .resident-profile-link is a <button> here (opens the dropdown below,
   doesn't navigate directly) instead of an <a>  reset the button
   defaults it would otherwise pick up (border/font/cursor), everything
   else about its look stays the same as before this menu existed. */
.profile-menu-toggle.resident-profile-link {
    border: none;
    font: inherit;
    cursor: pointer;
}

/* --- Public header profile dropdown (Profile / Log out) ------------- */
/* Same click-to-toggle / click-outside-closes pattern as the
   notification bell below, just simpler (two static links, no unread
   state)  see initProfileMenu() in assets/js/ui.js. */

.profile-menu {
    position: relative;
}

.profile-dropdown {
    display: none;
    position: absolute;
    top: calc(100% + var(--spacing-8));
    right: 0;
    min-width: 160px;
    background-color: var(--color-surface);
    color: var(--color-text);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
    z-index: 20;
}

.profile-dropdown.open {
    display: block;
}

.profile-dropdown a {
    display: block;
    padding: var(--spacing-12) var(--spacing-16);
    color: var(--color-text);
    border-bottom: 1px solid var(--color-border);
}

.profile-dropdown a:last-child {
    border-bottom: none;
}

.profile-dropdown a:hover {
    background-color: var(--color-surface-2);
    text-decoration: none;
}

@media (max-width: 767.98px) {
    /* .resident-nav-auth's default is a wrapping ROW (align-items:center),
       fine when it only ever held small fixed-height items (the theme
       toggle, a compact chip). Now that .profile-dropdown below renders
       inline instead of floating, .profile-menu (chip + dropdown
       stacked) is a much taller flex item  sharing a wrapped row with
       the theme toggle made align-items:center vertically center the
       toggle against that whole tall block, visually overlapping it.
       Column stacks the theme toggle and the profile block onto their
       own full rows instead, matching how .resident-nav-links above it
       already stacks Home/Services/Announcements/About. */
    .resident-nav-auth {
        flex-direction: column;
    }

    /* Theme toggle here reads as another action in the same list as
       Home/Services/Announcements/About above it, not a self-contained
       widget  same ghost-pill treatment as those links
       (.resident-nav-links a:not(.btn): padding/border-radius/hover),
       plus a visible label (icon-only only makes sense in desktop's
       compact bar). Scoped to .resident-nav-auth specifically so this
       doesn't touch the admin/resident-portal .navbar-menu variant,
       which already has its own full-width list-row treatment (see
       .navbar-menu-panel.mobile-open > .theme-toggle above)  intentionally
       different chrome for a intentionally different-looking menu. */
    .resident-nav-auth .theme-toggle {
        width: auto;
        height: auto;
        padding: var(--spacing-8) var(--spacing-12);
        border: none;
        border-radius: var(--border-radius-md);
        gap: var(--spacing-8);
        background-color: transparent;
    }

    .resident-nav-auth .theme-toggle::after {
        content: attr(aria-label);
        color: var(--color-white);
    }

    .resident-nav-auth .theme-toggle:hover {
        background-color: rgba(255, 255, 255, 0.12);
    }

    /* Below 768px the floating-popover treatment fights the mobile nav's
       own overflow:hidden (needed for its open/close slide animation
       see .resident-nav-menu above): an absolutely-positioned dropdown
       that hangs below the toggle gets clipped by that ancestor the same
       way anything else overflowing it would.

       display: flex + column + align-items:center (not block) centers
       the one link left in it (see :first-child below) the same way
       .resident-nav-auth centers the theme toggle and the chip next to
       it, instead of stretching it full width.

       Not gated behind .open goes a step further: once the hamburger
       reveals the auth area at all, Log out is just there  no tap on
       the username chip first to reveal it (the chip itself now
       navigates straight to Profile on mobile, see modules/resident/
       partials/header.php and initProfileMenu() in ui.js  .open no
       longer controls visibility here, so JS still toggling that class
       on click is harmless). */
    .profile-dropdown {
        display: flex;
        flex-direction: column;
        align-items: center;
        position: static;
        width: 100%;
        background: none;
        box-shadow: none;
        border: none;
        border-radius: 0;
        margin-top: var(--spacing-8);
    }

    /* The chip is a real link to Profile on mobile now (see the comment
       on .profile-dropdown above), so the dropdown's own "Profile" link
       (always the first of the two) is the same destination one tap
       later  hide it here. Desktop keeps both: the chip doesn't
       navigate there (initProfileMenu() still preventDefaults + toggles
       the dropdown at that width), so it's the only way to reach
       Profile from this menu on desktop. */
    .profile-dropdown a:first-child {
        display: none;
    }

    /* Log out, the only link left visible here: same ghost-pill
       treatment as the theme toggle above it and the nav links above
       that  one consistent set of actions, not a flush-left full-width
       list row (that's the admin/resident-portal .navbar-menu variant's
       look, a deliberately different menu  see the theme toggle
       comment above) or the dropdown's original desktop-card treatment
       (light surface background, border between items, dark text). */
    .profile-dropdown a {
        display: inline-flex;
        align-items: center;
        gap: var(--spacing-8);
        padding: var(--spacing-8) var(--spacing-12);
        border-radius: var(--border-radius-md);
        color: var(--color-white);
        border-bottom: none;
    }

    .profile-dropdown a::before {
        content: "";
        width: 24px;
        height: 24px;
        flex-shrink: 0;
        background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'/%3E%3Cpolyline points='16 17 21 12 16 7'/%3E%3Cline x1='21' y1='12' x2='9' y2='12'/%3E%3C/svg%3E") center / contain no-repeat;
    }

    .profile-dropdown a:hover {
        background-color: rgba(255, 255, 255, 0.12);
        text-decoration: none;
    }
}

/* --- Notification bell ---------------------------------------------- */

.notification-bell {
    position: relative;
}

/* Resident portal only: the bell sits directly in .navbar (a sibling of
   .navbar-menu, not inside it  see modules/resident/portal/partials/
   header.php) so it stays visible and clickable even while the mobile
   hamburger menu is collapsed, since it opens its own popup rather than
   navigating away. This selector can't match the admin topbar  there
   render_staff_notification_bell() renders the same markup nested
   inside .navbar-menu instead. margin-left: auto pulls it (and
   .navbar-menu after it) flush to the right edge, same grouping as
   before the bell moved out of the menu; on mobile .navbar-brand's own
   flex-grow already does this job, so the auto margin is a no-op there. */
.navbar > .notification-bell {
    margin-left: auto;
    margin-right: var(--spacing-12);
}

.bell-toggle {
    position: relative;
    background: none;
    border: none;
    color: var(--color-white);
    font-size: var(--font-size-large);
    cursor: pointer;
    padding: var(--spacing-4);
    line-height: 1;
}

.badge-count {
    position: absolute;
    top: -4px;
    right: -8px;
    background-color: var(--color-danger);
    color: var(--color-white);
    font-size: 10px;
    font-weight: var(--font-weight-bold);
    min-width: 16px;
    height: 16px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 4px;
}

.notification-dropdown {
    display: none;
    position: absolute;
    top: calc(100% + var(--spacing-8));
    right: 0;
    width: 280px;
    max-width: 80vw;
    background-color: var(--color-surface);
    color: var(--color-text);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
    z-index: 20;
}

@media (max-width: 767.98px) {
    /* On mobile the nav collapses into a stacked hamburger menu, so the
       bell can end up anywhere near the left edge of the screen 
       `right: 0` (set above) would anchor the dropdown off that spot and
       run it off the edge. Switch to position:fixed and let
       initNotificationBell() (assets/js/ui.js) place + clamp it inside
       the viewport when it opens. */
    .notification-dropdown {
        position: fixed;
        top: 0;
        right: auto;
        left: 0;
        width: calc(100vw - 32px);
        max-width: 320px;
    }
}

.notification-dropdown.open {
    display: block;
}

.notification-dropdown-header {
    padding: var(--spacing-16);
    font-weight: var(--font-weight-bold);
    background-color: var(--color-surface-2);
    border-bottom: 1px solid var(--color-border);
}

/* Resident bell: a title above the sections, since the dropdown mixes
   announcements with the resident's own request/reservation updates. */
.notification-dropdown-title {
    padding: var(--spacing-12) var(--spacing-16);
    font-size: var(--font-size-large);
    font-weight: var(--font-weight-bold);
    border-bottom: 1px solid var(--color-border);
}

.notification-dropdown-title + .notification-dropdown-header {
    border-top: none; /* the title's own border is the divider */
}

/* Staff bell only: several sections (Document Requests, Court
   Reservations, Resident Registrations) share one dropdown. Every
   section header is a literal sibling in the same .notification-dropdown
   (see render_staff_notification_bell()), so :first-child reliably
   matches only the first one and this divider skips it. */
.notification-dropdown-header:not(:first-child) {
    border-top: 2px solid var(--color-border);
}

/* Correction (2026-09-22): this comment used to claim this bell sits
   inside .navbar-menu and blame a ".navbar-menu a" bleed, same as
   .staff-notif-item below  that's wrong for THIS bell. The resident
   portal bell (.notification-item, modules/resident/portal/partials/
   header.php) is a SIBLING of .navbar-menu, not a descendant (see the
   ".navbar > .notification-bell" comment further up), so ".navbar-menu
   a" can never match it at all. (.staff-notif-item's own note below is
   still accurate  that one really is nested inside .navbar-menu via
   render_staff_notification_bell().) The specificity scoping under
   .notification-dropdown here is harmless to keep and still reads as a
   plain divider list either way, but don't go looking for a
   .navbar-menu bleed on this element  there isn't one. */
.notification-dropdown .notification-item {
    display: block;
    padding: var(--spacing-16);
    border: none;
    border-bottom: 1px solid var(--color-border);
    border-radius: 0;
    background: none;
    color: var(--color-text);
}

.notification-dropdown .notification-item:last-child {
    border-bottom: none;
}

.notification-dropdown .notification-item:hover {
    background-color: var(--color-surface-2);
    border-color: var(--color-border);
    text-decoration: none;
}

.notification-item-title {
    font-weight: var(--font-weight-bold);
    margin-bottom: var(--spacing-4);
}

.notification-item-date {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
}

.notification-empty {
    padding: var(--spacing-16);
    color: var(--color-text-light);
    font-size: var(--font-size-small);
    text-align: center;
}

/* Same correction as .notification-item's comment above: this footer
   link is also part of the resident bell, also a sibling of
   .navbar-menu rather than a descendant, so it was never actually
   fighting a ".navbar-menu a" bleed either. Whatever the original bug
   was, this had no color of its own and silently inherited white text,
   nearly invisible on the light --color-surface-2 background; nesting
   under .notification-dropdown fixed it and is harmless to keep. */
.notification-dropdown .notification-dropdown-footer {
    display: block;
    padding: var(--spacing-8) var(--spacing-16);
    text-align: center;
    font-size: var(--font-size-small);
    background-color: var(--color-surface-2);
    color: var(--color-primary);
    border: none;
    border-radius: 0;
}

.notification-dropdown .notification-dropdown-footer:hover {
    background-color: var(--color-surface-3);
    color: var(--color-primary);
}

/* Staff notification bell redesign (2026-09-15) 
   clean, scannable list of pending-item queues, each row is a link. */

.staff-notif-header {
    padding: var(--spacing-12) var(--spacing-16);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    color: var(--color-text-light);
    background-color: var(--color-surface-2);
    border-bottom: 1px solid var(--color-border);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* Specificity note: .staff-notif-item is an <a> nested inside
   .navbar-menu (the admin topbar), which has a generic ".navbar-menu a"
   rule (border + border-radius + padding, for the topbar's own pill
   buttons like Log out) that otherwise wins over a bare ".staff-notif-item"
   selector and made every row look like its own boxed button. Scoping
   under .notification-dropdown raises the specificity above ".navbar-menu
   a" and the border/border-radius resets below strip that button skin,
   leaving a plain divider list. */
.notification-dropdown .staff-notif-item {
    display: flex;
    align-items: center;
    gap: var(--spacing-12);
    padding: var(--spacing-12) var(--spacing-16);
    border: none;
    border-bottom: 1px solid var(--color-border);
    border-radius: 0;
    color: var(--color-text);
    font-weight: var(--font-weight-normal);
    text-decoration: none;
    transition: color 150ms ease;
}

/* Same ".navbar-menu a" bleed as the border/padding reset above, but for
   ::before specifically  that rule also draws a hardcoded-white Log-out
   arrow icon on every <a> in the topbar, .staff-notif-item included.
   Each row already renders its own correct, theme-aware icon via a real
   element (.staff-notif-item__icon, from render_staff_notification_bell()
   in includes/staff-notifications.php), so the inherited ::before was a
   second, wrong icon layered in ahead of it, not a replacement for it.
   The border/padding fix above never touched content, so this slipped
   through until reported directly. */
.notification-dropdown .staff-notif-item::before {
    content: none;
}

.notification-dropdown .staff-notif-item:last-child {
    border-bottom: none;
}

.notification-dropdown .staff-notif-item:hover {
    background-color: transparent;
    border-color: transparent;
}

.notification-dropdown .staff-notif-item:hover .staff-notif-item__title {
    color: var(--color-accent);
}

.staff-notif-item__icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
}

.staff-notif-item__icon .nav-icon {
    width: 20px;
    height: 20px;
    color: var(--color-text);
}

.staff-notif-item__content {
    flex: 1;
    min-width: 0;
}

.staff-notif-item__title {
    font-weight: var(--font-weight-bold);
    font-size: var(--font-size-base);
    margin-bottom: var(--spacing-2);
    color: var(--color-text);
}

.staff-notif-item__status {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
}

.staff-notif-item__count {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 28px;
    height: 28px;
    padding: 0 6px;
    background-color: var(--color-primary);
    color: var(--color-white);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
    border-radius: 999px;
}

/* --- Feature / service cards ----------------------------------------- */

.feature-card {
    text-align: center;
    transition: box-shadow 0.15s ease, transform 0.15s ease;
}

.feature-card:hover,
.feature-card:focus-within {
    box-shadow: var(--shadow-lg);
    transform: translateY(-2px);
}

.feature-card .card-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--spacing-8);
}

.feature-card-icon {
    font-size: 36px;
    line-height: 1;
    margin-bottom: var(--spacing-8);
}

.feature-card p {
    color: var(--color-text-light);
    flex: 1;
}

/* --- About cards (index.php #about) ------------------------------------
   Scoped .about-* rather than restyling the shared .section box, which
   many admin and resident pages also use. One column on phone; from
   tablet up, a two-column grid where --wide cards (History, About This
   Portal) span both columns and Mission + Vision sit side by side. All
   colours are theme tokens, so dark mode needs no extra rules. */

.about-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--spacing-16);
}

.about-card {
    position: relative;
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 12px;
    padding: var(--spacing-24);
    box-shadow: var(--shadow-sm);
    transition: box-shadow 0.2s ease, transform 0.2s ease;
}

.about-card:hover {
    box-shadow: var(--shadow-md);
    transform: translateY(-2px);
}

.about-card-header {
    display: flex;
    align-items: center;
    gap: var(--spacing-12);
    margin-bottom: var(--spacing-12);
}

.about-card-header h3 {
    margin: 0;
    font-size: var(--font-size-large);
    color: var(--color-accent-strong);
}

/* Icon badge: rounded square tinted with the accent's quiet surface. */
.about-card-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 10px;
    background-color: var(--color-surface-3);
    color: var(--color-accent);
}

.about-card-icon svg {
    width: 22px;
    height: 22px;
}

.about-card p {
    margin: 0;
    line-height: 1.7;
    color: var(--color-text-light);
    /* Long lines on a 1100px-wide card get hard to track; cap the
       measure even when the card spans both columns. */
    max-width: 75ch;
}

.about-card p + p {
    margin-top: var(--spacing-12);
}

/* About This Portal: a tinted closing note with an accent bar on the
   left, so it reads as "about the site" rather than another barangay
   fact. */
.about-card--highlight {
    background-color: var(--color-surface-2);
    border-left: 4px solid var(--color-accent);
}

@media (min-width: 768px) {
    .about-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--spacing-24);
    }

    .about-card {
        padding: var(--spacing-32);
    }

    .about-card--wide {
        grid-column: 1 / -1;
    }
}

@media (prefers-reduced-motion: reduce) {
    .about-card {
        transition: none;
    }

    .about-card:hover {
        transform: none;
    }
}

/* --- Service cards (index.php #services) --------------------------------
   Same family as the About cards above (surface, border, 12px radius,
   tinted icon badge) so the two home sections read as one design. Each
   card is a single link: the title's <a> is stretched over the card with
   ::after, the "Get started ->" line is decoration only. Replaces the old
   centered .feature-card + green button on this page; .feature-card
   rules stay: .feature-card-icon is still used by verification-notice.php. */

.service-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--spacing-16);
}

.service-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--spacing-12);
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 12px;
    padding: var(--spacing-24);
    box-shadow: var(--shadow-sm);
    transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
}

.service-card:not(.service-card--disabled):hover,
.service-card:not(.service-card--disabled):focus-within {
    box-shadow: var(--shadow-md);
    border-color: var(--color-accent);
    transform: translateY(-2px);
}

.service-card-header {
    display: flex;
    align-items: center;
    gap: var(--spacing-12);
}

.service-card-header h3 {
    margin: 0;
    font-size: var(--font-size-large);
    color: var(--color-accent-strong);
}

.service-card-icon {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 10px;
    background-color: var(--color-surface-3);
    color: var(--color-accent);
}

.service-card-icon svg {
    width: 22px;
    height: 22px;
}

.service-card-link {
    color: inherit;
    text-decoration: none;
}

/* Stretch the title link over the whole card: one big tap target. */
.service-card-link::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
}

/* The focus ring goes on the card outline, not the (tiny) title text,
   since the whole card is what's clickable. */
.service-card-link:focus-visible {
    outline: none;
}

.service-card-link:focus-visible::after {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

.service-card p {
    margin: 0;
    flex: 1;
    line-height: 1.7;
    color: var(--color-text-light);
}

.service-card-cta {
    font-weight: 600;
    color: var(--color-accent);
}

.service-card:not(.service-card--disabled):hover .service-card-cta {
    text-decoration: underline;
    text-underline-offset: 4px;
}

/* "Login required" / "Temporarily unavailable" pill under the title. */
.service-card-tag {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-4);
    padding: 2px var(--spacing-8);
    border-radius: 999px;
    font-size: var(--font-size-small);
    font-weight: 600;
    background-color: var(--color-surface-2);
    border: 1px solid var(--color-border);
    color: var(--color-text-light);
}

.service-card-tag svg {
    width: 12px;
    height: 12px;
}

/* Switched off in Role Permissions: no link, flat, dashed border. Uses
   muted colours rather than opacity so the text still meets contrast. */
.service-card--disabled {
    /* Transparent, not a surface tint: in dark mode every surface is
       LIGHTER than the page, so a tint made the disabled card look
       highlighted. Showing the page through reads as "off" in both. */
    background-color: transparent;
    border-style: dashed;
    box-shadow: none;
}

.service-card--disabled .service-card-header h3 {
    color: var(--color-text-light);
}

.service-card--disabled .service-card-icon {
    color: var(--color-text-light);
    background-color: var(--color-surface);
}

.service-card-tag--off {
    color: var(--color-text);
}

@media (min-width: 768px) {
    .service-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--spacing-24);
    }

    .service-card {
        padding: var(--spacing-32);
    }
}

@media (prefers-reduced-motion: reduce) {
    .service-card {
        transition: none;
    }

    .service-card:hover,
    .service-card:focus-within {
        transform: none !important;
    }
}

/* --- Announcement cards ------------------------------------------------ */

.announcement-meta {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    margin-bottom: var(--spacing-8);
}

/* Resident-side grid of announcement cards (modules/resident/announcements.php).
   Cards link through to announcements-view.php for the full post + full-size
   infographic. */
.announcement-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--spacing-24);
    margin-bottom: var(--spacing-32);
}

.announcement-card {
    display: flex;
    flex-direction: column;
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    color: inherit;
    text-decoration: none;
    transition: box-shadow 0.15s ease, transform 0.15s ease;
}

.announcement-card:hover,
.announcement-card:focus-visible {
    box-shadow: var(--shadow-lg);
    transform: translateY(-2px);
    text-decoration: none;
}

.announcement-card-media {
    aspect-ratio: 16 / 9;
    width: 100%;
    object-fit: cover;
    display: block;
    background-color: var(--color-surface-2);
}

.announcement-card-mediawrap {
    position: relative;
}

/* "N photos" badge on a card whose announcement has several images. */
.announcement-card-count {
    position: absolute;
    right: var(--spacing-8);
    bottom: var(--spacing-8);
    padding: 2px var(--spacing-8);
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.65);
    color: #fff;
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-bold);
}

/* Placeholder shown when an announcement has no image attached. */
.announcement-card-media--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 40px;
    color: var(--color-primary-light);
}

.announcement-card-body {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-4);
    padding: var(--spacing-16);
    flex: 1;
}

.announcement-card-body h2 {
    font-size: var(--font-size-large);
    margin: 0;
    color: var(--color-accent-strong);
}

.announcement-card-excerpt {
    color: var(--color-text-light);
    margin: 0;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* --- Announcement detail (announcements-view.php) --------------------- */

.announcement-detail {
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    margin-bottom: var(--spacing-24);
}

/* Full infographic  never cropped; readable at its own aspect ratio. */
.announcement-detail-image {
    width: 100%;
    height: auto;
    display: block;
    cursor: zoom-in;
    background-color: var(--color-surface-2);
}

/* Several images (announcements-view.php): stage with Previous / Next, then a thumbnail strip. */
.announcement-gallery-stage {
    position: relative;
}

.gallery-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-size: 28px;
    line-height: 1;
    cursor: pointer;
}

.gallery-nav:hover,
.gallery-nav:focus-visible {
    background: rgba(0, 0, 0, 0.8);
}

.gallery-nav--prev { left: var(--spacing-8); }
.gallery-nav--next { right: var(--spacing-8); }

.gallery-count {
    position: absolute;
    right: var(--spacing-8);
    bottom: var(--spacing-8);
    padding: 2px var(--spacing-8);
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.65);
    color: #fff;
    font-size: var(--font-size-small);
}

.gallery-thumbs {
    display: flex;
    gap: var(--spacing-8);
    padding: var(--spacing-12) var(--spacing-24) 0;
    overflow-x: auto;
}

.gallery-thumb {
    flex: 0 0 auto;
    width: 84px;
    height: 60px;
    padding: 0;
    border: 2px solid transparent;
    border-radius: var(--border-radius-md);
    background: none;
    overflow: hidden;
    cursor: pointer;
    opacity: 0.7;
}

.gallery-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.gallery-thumb.is-active,
.gallery-thumb:hover {
    border-color: var(--color-primary);
    opacity: 1;
}

.announcement-detail-body {
    padding: var(--spacing-24);
}

.announcement-detail-body h1 {
    margin-top: 0;
}

.announcement-detail-body .announcement-text {
    margin-top: var(--spacing-16);
    line-height: 1.7;
}

/* Typography for the sanitized rich-text markup includes/html-sanitizer.php
   allows through (p, h3, ul/ol/li, blockquote, a, strong/em/u, br)  a
   legacy plain-text post (rendered via nl2br(esc()) instead, see
   render_announcement_body() in modules/resident/announcements-view.php)
   is just text + <br>, so these rules simply don't match anything on it. */
.announcement-text p {
    margin: 0 0 var(--spacing-16);
}

.announcement-text p:last-child {
    margin-bottom: 0;
}

.announcement-text h3 {
    margin: var(--spacing-24) 0 var(--spacing-8);
    font-size: var(--font-size-large);
}

.announcement-text ul,
.announcement-text ol {
    margin: 0 0 var(--spacing-16);
    padding-left: var(--spacing-24);
}

.announcement-text li {
    margin-bottom: var(--spacing-4);
}

.announcement-text blockquote {
    margin: 0 0 var(--spacing-16);
    padding-left: var(--spacing-16);
    border-left: 3px solid var(--color-border);
    color: var(--color-text-light);
}

.announcement-text a {
    color: var(--color-accent);
}

/* --- Image lightbox (shared: infographic zoom) ----------------------- */

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-24);
    background-color: rgba(0, 0, 0, 0.85);
    cursor: zoom-out;
}

.lightbox img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    box-shadow: var(--shadow-lg);
}

/* --- Pagination --------------------------------------------------------- */

.pagination {
    display: flex;
    justify-content: center;
    gap: var(--spacing-8);
    flex-wrap: wrap;
    margin-top: var(--spacing-24);
}

.pagination a,
.pagination span {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    height: 36px;
    padding: 0 var(--spacing-8);
    border-radius: var(--border-radius-md);
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    color: var(--color-text);
}

.pagination a:hover {
    background-color: var(--color-surface-2);
    text-decoration: none;
}

.pagination .pagination-current {
    background-color: var(--color-primary);
    color: var(--color-white);
    border-color: var(--color-primary);
}

/* --- Resident-side responsive rules ------------------------------------ */

@media (min-width: 768px) {
    .resident-navbar .hamburger {
        display: none;
    }

    .resident-nav-menu {
        display: flex !important;
        width: auto;
        flex: 1;
        flex-direction: row;
        align-items: center;
        justify-content: flex-end;
        gap: var(--spacing-24);
        margin-top: 0;
        /* The mobile collapse animation below (max-height/opacity/
           visibility/overflow, gated on .mobile-open) is mobile-only 
           reset all four here so desktop doesn't need the JS-toggled
           class just to be visible. overflow specifically must go back
           to visible, not just the collapse height/opacity/visibility 
           otherwise it clips anything that intentionally hangs outside
           this nav's own box, like the profile dropdown below it. */
        max-height: none;
        opacity: 1;
        visibility: visible;
        overflow: visible;
    }

    .resident-nav-links {
        flex-direction: row;
        gap: var(--spacing-8);
    }

    .resident-nav-links a.is-current {
        position: relative;
        background-color: transparent;
    }

    /* Underline bar under the current link, inset to the text. */
    .resident-nav-links a.is-current::after {
        content: "";
        position: absolute;
        left: var(--spacing-12);
        right: var(--spacing-12);
        bottom: 2px;
        height: 2px;
        border-radius: 2px;
        background-color: var(--color-white);
    }

    .resident-nav-auth {
        padding-left: var(--spacing-16);
        border-left: 1px solid rgba(255, 255, 255, 0.25);
    }

    /* Desktop: back to its natural width. The phone rule's grow-from-0
       made the brand compete with .resident-nav-menu (flex:1) for space,
       which squeezed Register/Login/toggle into a column at ~800px. */
    .resident-navbar-brand {
        flex: 0 0 auto;
    }

    .resident-navbar-brand-sub {
        display: block;
    }
}

/* --- FAQ chatbot widget (MODULE_SPEC.md 1.2) ----------------------- */

.chatbot {
    position: fixed;
    right: var(--spacing-24);
    bottom: var(--spacing-24);
    z-index: 1000;
    font-size: var(--font-size-base);
}

.chatbot-toggle {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    border: none;
    background-color: var(--color-success);
    color: var(--color-white);
    font-size: var(--font-size-xl);
    line-height: 1;
    cursor: pointer;
    box-shadow: var(--shadow-lg);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.15s ease, transform 0.15s ease;
}

.chatbot-toggle:hover {
    background-color: #059669;
    transform: translateY(-2px);
}

.chatbot-window {
    display: none;
    position: absolute;
    right: 0;
    bottom: calc(56px + var(--spacing-16));
    width: 340px;
    max-width: calc(100vw - var(--spacing-32));
    height: 460px;
    max-height: calc(100vh - 120px);
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-lg);
    box-shadow: var(--shadow-lg);
    flex-direction: column;
    overflow: hidden;
}

.chatbot--open .chatbot-window {
    display: flex;
}

.chatbot--open .chatbot-toggle {
    background-color: #059669;
}

.chatbot-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--spacing-16);
    background-color: var(--color-success);
    color: var(--color-white);
    font-weight: var(--font-weight-bold);
}

.chatbot-close {
    background: none;
    border: none;
    color: var(--color-white);
    font-size: var(--font-size-large);
    line-height: 1;
    cursor: pointer;
    padding: 0 var(--spacing-4);
}

.chatbot-messages {
    flex: 1;
    overflow-y: auto;
    padding: var(--spacing-16);
    display: flex;
    flex-direction: column;
    gap: var(--spacing-8);
    background-color: var(--color-surface-2);
}

.chatbot-msg {
    max-width: 85%;
    padding: var(--spacing-8) var(--spacing-12);
    border-radius: var(--border-radius-md);
    line-height: var(--line-height-base);
    word-wrap: break-word;
}

.chatbot-msg--bot {
    align-self: flex-start;
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    color: var(--color-text);
}

.chatbot-msg--bot a {
    color: var(--color-accent);
    text-decoration: underline;
}

/* Quick-start chips shown under the opening greeting only  removed
   the moment the visitor sends anything (see sendMessage() in
   assets/js/ui.js), so they never clutter the actual conversation. */
.chatbot-suggestions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-8);
    align-self: flex-start;
    max-width: 100%;
}

.chatbot-suggestion {
    border: 1px solid var(--color-border);
    background-color: var(--color-surface);
    color: var(--color-accent);
    padding: var(--spacing-4) var(--spacing-12);
    border-radius: 999px;
    font-size: var(--font-size-small);
    font: inherit;
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

.chatbot-suggestion:hover {
    background-color: var(--color-surface-2);
    border-color: var(--color-accent);
}

.chatbot-msg--user {
    align-self: flex-end;
    background-color: var(--color-success);
    color: var(--color-white);
}

.chatbot-msg--typing {
    color: var(--color-text-light);
    font-style: italic;
}

.chatbot-form {
    display: flex;
    gap: var(--spacing-8);
    padding: var(--spacing-8);
    border-top: 1px solid var(--color-border);
    background-color: var(--color-surface);
}

.chatbot-form input {
    flex: 1;
    min-width: 0;
    padding: var(--spacing-8) var(--spacing-12);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    font: inherit;
}

.chatbot-send {
    border: none;
    background-color: var(--color-success);
    color: var(--color-white);
    padding: 0 var(--spacing-16);
    border-radius: var(--border-radius-md);
    cursor: pointer;
    font-size: var(--font-size-large);
    line-height: 1;
}

.chatbot-send:hover {
    background-color: #059669;
}

@media (max-width: 480px) {
    .chatbot {
        right: var(--spacing-16);
        bottom: var(--spacing-16);
    }

    .chatbot-window {
        width: calc(100vw - var(--spacing-32));
    }
}

/* Data retention schedule (modules/resident/partials/retention-policy.php)
   on the Privacy Policy page and in the registration consent modal. */
.retention-list li {
    margin-bottom: var(--spacing-12);
}

.retention-when {
    font-weight: var(--font-weight-bold);
}

/* =====================================================================
 * One-time login codes (docs/plans/OTP_PLAN.md, 2026-10): the code screen
 * rendered by render_otp_step() (includes/otp.php) in the resident
 * login modal, the mobile login page and modules/admin/verify-otp.php,
 * plus the "Confirm your email" box on modules/resident/register.php.
 * ===================================================================== */
.btn-link {
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    color: var(--color-accent-strong, var(--color-primary));
    text-decoration: underline;
    cursor: pointer;
}
.btn-link:disabled {
    color: var(--color-text-light);
    text-decoration: none;
    cursor: default;
}
.otp-title {
    font-size: 24px;
    text-align: center;
    margin-bottom: var(--spacing-8);
}
.otp-sent {
    text-align: center;
    color: var(--color-text-light);
    margin-bottom: var(--spacing-16);
}
.otp-input {
    font-size: 24px;
    letter-spacing: 8px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.otp-remember {
    display: flex;
    align-items: center;
    gap: var(--spacing-8);
    font-weight: var(--font-weight-medium);
}
.otp-links {
    margin-top: var(--spacing-16);
    text-align: center;
    font-size: var(--font-size-small);
}
.otp-links p {
    margin: 0 0 var(--spacing-8);
}
.email-confirm {
    display: flex;
    gap: var(--spacing-8);
    flex-wrap: wrap;
    align-items: center;
    margin-top: var(--spacing-8);
}
.email-confirm .otp-input {
    max-width: 200px;
    font-size: 18px;
    letter-spacing: 4px;
}
.email-confirm-status {
    margin-top: var(--spacing-8);
    font-size: var(--font-size-small);
}
.email-confirm-status.is-ok {
    color: var(--color-success);
    font-weight: var(--font-weight-bold);
}
.email-confirm-status.is-error {
    color: var(--color-danger);
}

/* =====================================================================
 * REPORTS UI PASS (2026-10)  includes/report-view.php
 * Page title + total + chips, a roomier table (no wrapping dates, long
 * text clamped to three lines), and the "Columns" picker.
 * ===================================================================== */
.report-head {
    margin: 0 0 var(--spacing-16);
}

.report-title {
    margin: 0;
    font-size: 1.5rem;
    line-height: 1.25;
}

.report-total {
    margin: var(--spacing-4) 0 0;
    color: var(--color-text);
}

.report-summary {
    margin: 2px 0 0;
    font-size: var(--font-size-small);
}

.report-chips {
    margin-top: var(--spacing-8);
}

.report-table td.nowrap {
    white-space: nowrap;
}

.report-clamp {
    min-width: 180px;
    max-width: 340px;
}

.report-clamp span {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.col-picker {
    position: relative;
    display: inline-block;
}

.col-picker summary {
    list-style: none;
    cursor: pointer;
}

.col-picker summary::-webkit-details-marker {
    display: none;
}

.col-picker__panel {
    position: absolute;
    z-index: 30;
    top: calc(100% + 6px);
    left: 0;
    min-width: 230px;
    padding: var(--spacing-12) var(--spacing-16);
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-md);
}

.col-picker__row {
    display: flex;
    gap: var(--spacing-8);
    align-items: center;
    margin: 6px 0;
    font-weight: normal;
    cursor: pointer;
}

@media print {
    .report-clamp {
        max-width: none;
    }

    .report-clamp span {
        display: inline;
        -webkit-line-clamp: unset;
        overflow: visible;
    }

    .report-head .chip__x {
        display: none;
    }

    .report-title {
        font-size: 1.25rem;
    }
}

/* The Age / date-range filters label a group of two inputs with a <span> (a <label> with no single
   control to point at is an accessibility error): style it exactly like the <label>s beside it. */
.report-toolbar .form-group > .form-label {
    display: block;
    margin-bottom: 6px;
    font-weight: var(--font-weight-bold);
    color: var(--color-text);
}

/* Export CSV menu (includes/report-export.php): same dropdown behaviour as the Columns box. */
.export-menu {
    position: relative;
    display: inline-block;
}

.export-menu summary {
    list-style: none;
    cursor: pointer;
}

.export-menu summary::-webkit-details-marker {
    display: none;
}

.export-menu summary::after {
    content: "\25BE";
    margin-left: var(--spacing-8);
}

.export-menu__panel {
    position: absolute;
    z-index: 30;
    top: calc(100% + 6px);
    left: 0;
    min-width: 270px;
    padding: var(--spacing-8);
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-md);
    box-shadow: var(--shadow-md);
}

.export-menu__panel a {
    display: block;
    padding: var(--spacing-8) var(--spacing-12);
    border-radius: var(--border-radius-md);
    color: var(--color-text);
    text-decoration: none;
}

.export-menu__panel a:hover,
.export-menu__panel a:focus-visible {
    background-color: var(--color-surface-selected);
}

.export-menu__panel small {
    display: block;
    color: var(--color-text-light);
}


/* Reports: breadcrumb above the title, and the name that opens the resident record. */
.report-crumb {
    margin-bottom: var(--spacing-4);
    font-size: var(--font-size-small);
    color: var(--color-text-light);
}

.report-link {
    color: inherit;
    text-decoration: underline dotted;
    text-decoration-color: var(--color-text-light);
    text-underline-offset: 4px;
}

.report-link:hover,
.report-link:focus-visible {
    color: var(--color-primary-light);
    text-decoration-color: currentColor;
}

/* Read-only record pages (includes/record-view.php): sections of label / value pairs. */
.record-view__actions {
    display: flex;
    gap: var(--spacing-8);
    flex-wrap: wrap;
    margin-bottom: var(--spacing-16);
}

.record-view__photo {
    display: block;
    width: 120px;
    height: 120px;
    object-fit: cover;
    border-radius: var(--border-radius-md);
    border: 1px solid var(--color-border);
    margin-bottom: var(--spacing-16);
}

.record-view__section {
    margin-bottom: var(--spacing-24);
}

.record-view__section h2 {
    font-size: var(--font-size-large);
    margin-bottom: var(--spacing-8);
    padding-bottom: var(--spacing-4);
    border-bottom: 1px solid var(--color-border);
}

.record-view {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--spacing-12) var(--spacing-24);
    margin: 0;
}

.record-view dt {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
}

.record-view dd {
    margin: 0;
    word-break: break-word;
}
