/* ==========================================================================
   Havenland Maintenance — global design tokens & shared UI (see DESIGN.md)
   ========================================================================== */

:root {
    --bs-primary: #2d4a3e;
    --bs-primary-rgb: 45, 74, 62;
    --bs-primary-hover: #263f35;
    --bs-primary-active: #20342b;
    --text-muted: #94a3b8;
    --border-color: #e2e8f0;
    --border-light: #f1f5f9;
    --success: #10b981;
    --warning: #f59e0b;
    --danger: #ef4444;

    /* Bootstrap's own --bs-warning (#ffc107, a very bright yellow) is what
       .text-warning/.bg-warning/.btn-warning actually read from — the
       --warning token above was never wired to it. Bright yellow text on a
       near-white "Reviewed" badge background has very poor contrast (fails
       WCAG AA), which is why it read as washed-out/hard to look at. A
       darker amber keeps it recognizably "yellow/warning" but readable. */
    --bs-warning: #b45309;
    --bs-warning-rgb: 180, 83, 9;

    /* Three-font type system: title (page/card/modal headings — elegant
       display serif), body (everything else — unchanged), accent (stat
       values and other short emphasis text — same serif family, used at
       a size/weight that reads as a highlight rather than a heading). */
    --font-title: 'Fraunces', Georgia, serif;
    --font-body: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-accent: 'Fraunces', Georgia, serif;
    --font-family: var(--font-body);

    --sidebar-width: 250px;
    --sidebar-width-collapsed: 76px;
    --topbar-height: 64px;
}

body {
    font-family: var(--font-family);
    background: #f8fafc;
    color: #1e293b;
}

/* ---- No blue anywhere: Bootstrap's default blue (#0d6efd) leaks in from
   several places that DON'T automatically follow the --bs-primary override
   above — plain links, focus rings, checked checkboxes/radios, and
   .alert-info all ship with their own hardcoded/independent blue values.
   These overrides route every one of them through the app's brand color
   instead, in one place, so nothing needs fixing per-page. ---- */
a {
    color: var(--bs-primary);
}

a:hover {
    color: var(--bs-primary-hover);
}

/* Softer, "filled" input style instead of Bootstrap's flat white box with a
   sharp 0.375rem corner — a light gray fill and no visible border at rest,
   which turns white with a colored border only once focused. */
.form-control,
.form-select {
    border-radius: 0.65rem;
    background-color: var(--border-light);
    border-color: transparent;
}

.form-control:focus,
.form-select:focus {
    background-color: #fff;
    border-color: rgba(var(--bs-primary-rgb), 0.5);
    box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
}

.form-control:disabled,
.form-select:disabled {
    background-color: var(--border-light);
    opacity: 0.65;
}

.dropdown-menu {
    /* Same fixed-at-build-time issue — .dropdown-item.active/:active would
       otherwise flash Bootstrap's default blue. */
    --bs-dropdown-link-active-bg: var(--bs-primary);
}

/* A danger item (Logout) inside that same dropdown would otherwise inherit
   the green active/hover fill above while keeping its red text — a
   background/text mismatch that doesn't read as "danger" at all. Give it
   its own red-tinted hover/active instead. */
.dropdown-item.text-danger:hover,
.dropdown-item.text-danger:focus,
.dropdown-item.text-danger:active {
    background-color: rgba(239, 68, 68, 0.1);
    color: var(--danger);
}

.form-check-input:checked {
    background-color: var(--bs-primary);
    border-color: var(--bs-primary);
}

.form-check-input:focus {
    border-color: rgba(var(--bs-primary-rgb), 0.5);
    box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
}

.alert-info {
    --bs-alert-color: var(--bs-primary-active);
    --bs-alert-bg: rgba(var(--bs-primary-rgb), 0.1);
    --bs-alert-border-color: rgba(var(--bs-primary-rgb), 0.25);
}

.font-mono {
    font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
}

a {
    color: var(--bs-primary);
}

/* ---- Typography scale (WAJIB dipakai, jangan pakai h1-h6 default Bootstrap) ---- */
.fs-page-title,
.sa-title,
.fs-dialog-title,
.fs-card-header,
.sa-card-title,
.modal-title {
    font-family: var(--font-title);
    font-weight: 600;
}

.fs-page-title,
.sa-title {
    font-size: 1.2rem;
}

/* Secondary headline for status/confirm screens (empty-state, confirm modal,
   invalid-link pages, wizard success) — several of these were left as bare
   <h5>/<h3> with no scale class, so they silently fell back to Bootstrap's
   default 1.25rem/1.75rem instead of the app's own scale. */
.fs-dialog-title {
    font-size: 1.05rem;
}

.fs-card-header,
.sa-card-title {
    font-size: 0.9rem;
}

.fs-body-sm {
    font-size: 0.875rem;
}

.fs-cell {
    font-size: 0.85rem;
}

.fs-cell-sm {
    font-size: 0.8rem;
}

.fs-cell-xs {
    font-size: 0.75rem;
}

.fs-badge,
.fs-badge-mobile {
    font-size: 0.7rem;
    font-weight: 700;
}

.modal-title {
    font-size: 1rem;
}

/* Bootstrap's modal-header/body/footer flex-column sizing (what makes
   .modal-dialog-scrollable able to scroll just the body) only works when
   they're DIRECT children of .modal-content. Every form-based modal in
   this app wraps them in a <form> instead, so the whole thing submits as
   one — that extra nesting level silently breaks the flex chain: the form
   just grows to its natural height and gets clipped by modal-content's
   overflow instead of the body ever becoming scrollable. This restores the
   same flex-column behavior on the <form> itself, so EVERY form-modal in
   the app scrolls correctly, not just one page's modal. */
.modal-content > form {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.modal-dialog-scrollable .modal-content > form {
    max-height: 100%;
    overflow: hidden;
}

.modal-content > form > .modal-body {
    flex: 1 1 auto;
    min-height: 0;
}

/* Ensures a scrollable modal (.modal-dialog-scrollable) actually engages
   momentum/touch scrolling on mobile Safari — without this it can feel
   "stuck" when the body has a lot of fields. Global: applies to any modal
   using the scrollable variant, not just one page's modal. */
.modal-dialog-scrollable .modal-body {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}

/* On touch devices a modal can end up opened/closed in quick succession
   (fast tap-tap on mobile Safari) faster than Bootstrap's own backdrop
   bookkeeping keeps up, leaving a second stray .modal-backdrop stacked
   under the page after every modal has actually closed — two overlapping
   semi-transparent backdrops read as a persistent dark "shadow" behind the
   content. app.js also actively removes stray backdrops on hidden.bs.modal,
   but this is a belt-and-suspenders visual guard in case one still slips
   through: only the first/topmost backdrop should ever be visible. */
.modal-backdrop + .modal-backdrop {
    display: none;
}

/* Mobile Safari/Chrome auto-zoom the whole page in the moment a form
   control is focused if its font-size is under 16px — every field in this
   app is deliberately smaller than that (.sa-form-input's 0.875rem/14px,
   toolbar's 0.8rem, etc.) to match the compact design scale. The viewport
   meta tag deliberately still allows manual pinch-zoom (no
   maximum-scale/user-scalable lock) — only the *automatic* zoom-on-focus
   is being suppressed here.
   Bumping every field's resting font-size to 16px would suppress the zoom
   too, but it visibly breaks the design (a field's text turns noticeably
   larger than its label and every other field around it, e.g. "Catatan
   Kondisi Awal" in the wizard). iOS decides whether to zoom from the
   font-size AT THE MOMENT OF FOCUS, so bumping it only on :focus gets the
   same zoom suppression without touching the resting/unfocused size at
   all — the field is back to its normal compact size the instant it's
   blurred. !important is needed because several of these controls are
   styled by more specific selectors in per-page component stylesheets
   (e.g. components/toolbar.css) that would otherwise win the cascade. */
@media (max-width: 767.98px) {
    input[type="text"]:focus,
    input[type="search"]:focus,
    input[type="email"]:focus,
    input[type="tel"]:focus,
    input[type="number"]:focus,
    input[type="password"]:focus,
    input[type="date"]:focus,
    input[type="datetime-local"]:focus,
    input[type="time"]:focus,
    input[type="url"]:focus,
    select:focus,
    textarea:focus {
        font-size: 16px !important;
    }
}

/* ---- Utility ---- */
.mw-400 {
    max-width: 400px;
}

.mw-640 {
    max-width: 640px;
}

/* ---- Guest layout container (login, invalid-link pages) ----
   Narrow by default (a single centered card), widened only for pages that
   opt in via @section('wrapper_class', 'guest-container-wide') — currently
   just login, which needs room for its two-column image + form layout. */
.guest-container {
    max-width: 420px;
}

.guest-container-wide {
    max-width: 820px;
}

/* Chart.js canvases were dropped straight into a .card-body with no fixed
   height, only a `height="220"` canvas attribute. With `responsive: true`
   (Chart.js default) the canvas resizes to fit its parent, but the parent's
   own height came from the canvas's rendered size — a circular dependency
   that made charts shrink a little further on every resize/redraw instead
   of settling. A fixed-height wrapper breaks that loop: only the width is
   ever responsive, height stays put, combined with maintainAspectRatio:
   false in each chart's JS options. */
.chart-container {
    position: relative;
    height: 260px;
    width: 100%;
}

.rounded-4 {
    border-radius: 1rem !important;
}

.form-check {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    /* Bootstrap's default .form-check keeps padding-left: 1.5em to make
       room for the float trick below — with that trick replaced by gap,
       the padding just shifts the whole row right, out of alignment with
       everything else on the page that starts flush at the left edge. */
    padding-left: 0;
}

/* Bootstrap positions the checkbox/radio via padding-left on .form-check
   plus a compensating margin-left: -1.5em on the input itself (a float-era
   trick) — switching .form-check to flex above breaks that trick (flex
   ignores float, so the negative margin just shoves the input left with
   nothing to fill the gap), leaving the input and its label touching with
   no space between them. Zeroing the margin and using gap above instead
   fixes spacing for every checkbox/radio in the app at once. */
.form-check .form-check-input {
    margin-top: 0;
    margin-left: 0;
    flex-shrink: 0;
}

/* ---- Buttons ---- */
/* Bootstrap's default .btn font-size is 1rem. Pages have been relying on
   remembering to add .sa-btn-text (0.875rem) to every button by hand, which
   is exactly how buttons like Export Data's "Export Excel"/"Export PDF" and
   the report detail admin panel's "Approve"/"Hapus Laporan" ended up still
   oversized — the class was simply never added. Setting the size here makes
   0.875rem the default for every button app-wide; .btn-action-equal (0.75rem)
   and other more specific rules defined below still win where a smaller size
   is wanted, so nothing needs to opt in by hand anymore. */
.btn {
    font-weight: 500;
    font-size: 0.875rem;
}

/* "Back to X" navigation link, reusable on any detail page (wrap it in a
   card via the page's own markup — this class only styles the link). */
.back-link {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: #64748b;
    text-decoration: none;
    font-size: 0.8rem;
    font-weight: 600;
}

.back-link:hover {
    color: var(--bs-primary);
}

.btn-action-equal {
    font-size: 0.75rem;
    padding: 0.35rem 0.75rem;
    white-space: nowrap;
}

/* Desktop table action cells (Villa/Worker/Report index) place these
   buttons — some bare, some wrapped in a <form class="d-inline"> for the
   toggle/delete actions — directly as siblings inside the <td> with no gap
   wrapper, so they end up touching each other. The mobile card view already
   wraps its buttons in a `.d-flex.gap-2` so this only targets the table. */
.table td > .btn-action-equal:not(:first-child),
.table td > form.d-inline:not(:first-child) {
    margin-left: 0.4rem;
}

/* Primary/header-level buttons ("Tambah Villa", "Simpan", "Edit" in modal
   footers, etc). Was referenced across every page's header button but never
   actually defined, so it silently fell back to Bootstrap's default 1rem
   button text — visibly oversized next to the rest of the app's ~0.85rem
   scale. Defining it here fixes every button using this class at once. */
.sa-btn-text {
    font-size: 0.875rem;
}

.btn-primary {
    --bs-btn-bg: var(--bs-primary);
    --bs-btn-border-color: var(--bs-primary);
    --bs-btn-hover-bg: var(--bs-primary-hover);
    --bs-btn-hover-border-color: var(--bs-primary-hover);
    --bs-btn-active-bg: var(--bs-primary-active);
    --bs-btn-active-border-color: var(--bs-primary-active);
}

/* Same story as .btn-primary — .btn-warning bakes its own bright-yellow
   #ffc107 bg/border/hover/active at build time, independent of the
   --bs-warning override above, so "Tandai Reviewed" would otherwise stay
   the old washed-out yellow even after badges got fixed. White text reads
   better than Bootstrap's default black now that the bg is a darker amber. */
.btn-warning {
    --bs-btn-color: #fff;
    --bs-btn-bg: var(--bs-warning);
    --bs-btn-border-color: var(--bs-warning);
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: #92400e;
    --bs-btn-hover-border-color: #92400e;
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: #78350f;
    --bs-btn-active-border-color: #78350f;
    --bs-btn-disabled-color: #fff;
    --bs-btn-disabled-bg: var(--bs-warning);
    --bs-btn-disabled-border-color: var(--bs-warning);
}

/* ---- Cards ---- */
.card {
    border-color: var(--border-color);
}

.card-header {
    background: #fff;
}

/* ---- Badges ---- */
.badge.rounded-pill {
    font-weight: 700;
    letter-spacing: 0.02em;
}

/* ---- Tables ----
   Flat/underlined header instead of a shaded gray fill — no background,
   normal case (not uppercase), darker+bolder text with a solid separator
   line doing the work the fill used to do. */
.sa-table thead th,
.table thead th {
    background: transparent;
    text-transform: none;
    letter-spacing: normal;
    font-weight: 700;
    color: #1e293b;
    border-bottom: 2px solid var(--border-color);
}

.sa-table tbody tr,
.table tbody tr {
    border-top: 1px solid var(--border-light);
}

.sa-table tbody tr:hover,
.table tbody tr:hover {
    background: #fafbfe;
}

/* Extra breathing room for narrow "in-between" table columns (report
   counts, status badges, etc.) that otherwise sit flush against their
   neighbors. Reusable on any table cell/header that needs it. */
.col-spacer {
    padding-left: 1.5rem !important;
    padding-right: 1.5rem !important;
}

/* ---- Pagination ---- */
.sa-pagination-btn {
    width: 32px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 !important;
}

.sa-pagination-icon {
    font-size: 0.75rem;
}

.sa-pagination-text {
    font-size: 0.8rem;
}

/* ---- Empty state ---- */
.sa-empty-container,
.empty-state-box {
    min-height: 300px;
}

.sa-empty-icon-box,
.empty-state-icon-circle {
    width: 64px;
    height: 64px;
}

/* Icon size was left to whatever utility class (fs-1/fs-2/fs-3) each page
   happened to add by hand — up to 2.5rem in one place, 1.75rem in another.
   Fixed to the circle itself so every empty-state icon matches. */
.sa-empty-icon-box i,
.empty-state-icon-circle i {
    font-size: 1.5rem;
}

.sa-empty-text-maxw,
.empty-state-text {
    max-width: 300px;
}

/* ---- Modal icon circle ---- */
.sa-modal-icon-circle-56 {
    width: 56px;
    height: 56px;
}

.sa-modal-icon-circle-56 i {
    font-size: 1.4rem;
}

.sa-modal-icon-circle-72 {
    width: 72px;
    height: 72px;
}

.sa-modal-icon-circle-72 i {
    font-size: 1.6rem;
}

/* ---- Form ----
   .sa-form-input is applied to virtually every input/select/textarea in
   the app but never had its own font-size, so every field silently fell
   back to Bootstrap's default 1rem (16px) — noticeably larger than the
   rest of the UI's ~0.8-0.9rem scale, especially obvious on big screens.
   Fixing it here brings every field in the app back in line at once. */
.sa-form-input {
    font-size: 0.875rem;
}

.sa-form-input:focus {
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.15);
}

/* ---- Tabs (nav-pills) ----
   Same story as .sa-form-input: every nav-pills tab (report detail tabs,
   wizard type tabs) inherited Bootstrap's default 1rem link size with
   nothing scaling it down. Fixed globally so every nav-pills instance in
   the app matches the rest of the UI's scale at once. */
.nav-pills {
    /* Outline style: colored border, transparent fill — not a solid color
       block. Bootstrap bakes the active-bg var to a fixed #0d6efd at build
       time (doesn't follow --bs-primary), so it's overridden here anyway. */
    --bs-nav-pills-link-active-bg: transparent;
    --bs-nav-pills-link-active-color: var(--bs-primary);
}

.nav-pills .nav-link {
    font-size: 0.85rem;
    font-weight: 600;
    color: #475569;
    border: 1px solid transparent;
}

.nav-pills .nav-link.active {
    font-weight: 700;
    border-color: var(--bs-primary);
}

/* ---- Segmented-control tabs (Settings page, or anywhere pill/underline
   tabs don't fit) — a gray track with the active tab floating on top as
   its own white card, like iOS/Linear/Notion segmented controls. Third
   attempt at this component: pill row and flat-underline were both tried
   and rejected before landing here. */
.app-tabs {
    display: inline-flex;
    flex-wrap: nowrap;
    gap: 0.2rem;
    background: var(--border-light);
    padding: 0.3rem;
    border-radius: 0.85rem;
    margin-bottom: 1.25rem;
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}

/* When there are more tabs than fit (small/mobile screens), scroll the
   strip horizontally instead of wrapping tabs onto a second row. */
.app-tabs::-webkit-scrollbar {
    display: none;
}

.app-tab-link {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.55rem 1rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: #64748b;
    text-decoration: none;
    background: transparent;
    border: none;
    border-radius: 0.6rem;
    white-space: nowrap;
    transition: color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}

.app-tab-link:hover {
    color: #1e293b;
}

.app-tab-link.active {
    color: var(--bs-primary);
    background: #fff;
    box-shadow: 0 1px 4px rgba(15, 23, 42, 0.14);
}

/* ---- App shell layout ----
   .app-shell is pinned to exactly one viewport height with its own
   overflow clipped, and .app-content (below) is what actually scrolls —
   the document/body itself never does. This is the real fix for the
   sidebar's white flash on fast scroll: a position:fixed element only
   needs repainting when the thing that's ACTUALLY scrolling is the
   document itself. Once .app-content owns its own scroll instead, the
   fixed sidebar never has to keep up with anything — it just sits still
   the entire time, no compositing race to lose. */
.app-shell {
    display: flex;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
}

.app-sidebar {
    width: var(--sidebar-width);
    flex-shrink: 0;
    background: var(--bs-primary);
    box-shadow: none;
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    /* top:0 + bottom:0 alone should stretch this the full viewport height,
       but an explicit height is a belt-and-suspenders fix for the white
       gap that showed up below the last nav item once the page had
       scrolled — 100dvh (dynamic viewport height) tracks the *actual*
       visible viewport, including mobile browser chrome collapsing, which
       a plain vh/top-bottom combo doesn't always repaint correctly for. */
    height: 100vh;
    height: 100dvh;
    z-index: 1030;
    display: flex;
    flex-direction: column;
    transition: width 0.2s ease, transform 0.2s ease;
    overflow-x: hidden;
}

.app-sidebar.collapsed {
    width: var(--sidebar-width-collapsed);
}

.app-sidebar-brand {
    height: var(--topbar-height);
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0 1.1rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
    flex-shrink: 0;
}

/* The logo file itself is a dark wordmark (correct on the login page's
   white panel) — brightness(0) then invert(1) forces it to solid white
   regardless of its original color, which is what's needed once the
   sidebar behind it is dark. */
.app-sidebar-brand-logo {
    filter: brightness(0) invert(1);
}

.app-sidebar-brand-text .fw-bold {
    color: #fff;
}

.app-sidebar-brand-text .text-muted {
    color: rgba(255, 255, 255, 0.6) !important;
}

#sidebarMobileClose {
    filter: brightness(0) invert(1);
}

.app-sidebar-brand-icon {
    width: 34px;
    height: 34px;
    border-radius: 0.6rem;
    background: var(--bs-primary);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

/* Larger variant reused for the wizard's type-selection cards — was an
   inline style="width:56px;height:56px;font-size:1.4rem" before, which is
   both a project-rule violation (no inline styles) and how it ended up
   inconsistent with every other icon-circle size in the app. */
.app-sidebar-brand-icon-lg {
    width: 46px;
    height: 46px;
    font-size: 1.15rem;
    border-radius: 0.85rem;
}

.app-sidebar-brand-icon-md {
    width: 48px;
    height: 48px;
    font-size: 1.1rem;
    border-radius: 0.85rem;
}

/* Wordmark logo (public/images/logo.png) replacing the old fa-leaf icon
   chip — sized by height only since it's a wide horizontal mark, not a
   square glyph, so it keeps its own aspect ratio instead of being
   squashed into the old 34x34 box. Shrinks further when the sidebar is
   collapsed so it still fits the narrower rail. */
.app-sidebar-brand-logo {
    height: 32px;
    width: auto;
    flex-shrink: 0;
}

.app-sidebar.collapsed .app-sidebar-brand-logo {
    height: 22px;
}

.app-sidebar-brand-text {
    line-height: 1.15;
    overflow: hidden;
}

.app-sidebar.collapsed .app-sidebar-brand-text,
.app-sidebar.collapsed .app-nav-label,
.app-sidebar.collapsed .app-sidebar-role {
    display: none;
}

/* With the label hidden, .app-nav-item still stretched to the sidebar's
   full inner width with its icon left-aligned inside it — so the
   hover/active pill rendered as a wide off-center bar instead of a neat
   badge around the icon. Shrink it to a centered square instead. */
.app-sidebar.collapsed .app-nav-item {
    width: 44px;
    height: 44px;
    padding: 0;
    margin-left: auto;
    margin-right: auto;
    justify-content: center;
    gap: 0;
}

.app-sidebar-role {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: rgba(255, 255, 255, 0.5);
    padding: 0.75rem 1.1rem 0.25rem;
}

.app-nav {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0.25rem 0.7rem;
}

.app-nav-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.75rem;
    border-radius: 0.6rem;
    /* Sidebar background is solid --bs-primary now, not white — muted
       white instead of the old dark slate keeps unselected items legible
       without competing with the active/hover states below. */
    color: rgba(255, 255, 255, 0.75);
    text-decoration: none;
    font-size: 0.875rem;
    font-weight: 500;
    margin-bottom: 0.4rem;
    white-space: nowrap;
}

.app-nav-item i {
    width: 18px;
    text-align: center;
    flex-shrink: 0;
}

.app-nav-item:hover {
    background: var(--bs-primary-hover);
    color: #fff;
}

.app-nav-item.active {
    /* A darker shade (like the old --bs-primary fill) would now blend
       straight into the sidebar's own background, since that background
       IS --bs-primary. A light overlay reads clearly against it instead —
       same "brightest element on the rail" treatment as the reference. */
    background: rgba(255, 255, 255, 0.16);
    color: #fff;
    font-weight: 700;
}

/* ---- Expandable nav item (Monitoring > Pool/Garden) ---- */
.app-nav-item-toggle {
    width: 100%;
    border: none;
    background: transparent;
    cursor: pointer;
}

.app-nav-caret {
    font-size: 0.65rem;
    transition: transform 0.2s ease;
}

.app-nav-item-toggle[aria-expanded="true"] .app-nav-caret {
    transform: rotate(180deg);
}

/* A thin connector line under the parent's icon, roughly, ties the
   sub-items together as a group instead of them just floating as plain
   indented text with no visual link back to "Monitoring" above. */
.app-nav-submenu {
    margin-left: 1.3rem;
    padding-left: 0.9rem;
    padding-top: 0.15rem;
    border-left: 1px solid rgba(255, 255, 255, 0.15);
}

.app-nav-subitem {
    padding-top: 0.45rem;
    padding-bottom: 0.45rem;
    padding-right: 0.6rem;
    font-size: 0.825rem;
}

.app-nav-subitem i {
    width: 15px;
    font-size: 0.78rem;
}

.app-sidebar.collapsed .app-nav-caret,
.app-sidebar.collapsed .app-nav-submenu {
    display: none;
}

.app-content {
    flex: 1;
    margin-left: var(--sidebar-width);
    display: flex;
    flex-direction: column;
    min-width: 0;
    /* .app-content itself does NOT scroll (that was pulling the topbar's
       own scrollbar-gutter into its width calc, making it a few px
       narrower than the sidebar's edge). .app-main below is the actual
       scroll container instead, so the sticky topbar sits outside that
       scrolling box entirely and always spans the full, exact width. */
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    transition: margin-left 0.2s ease;
}

.app-content.sidebar-collapsed {
    margin-left: var(--sidebar-width-collapsed);
}

.app-topbar {
    height: var(--topbar-height);
    width: 100%;
    background: #fff;
    border-bottom: 1px solid var(--border-color);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 1.25rem;
    position: sticky;
    top: 0;
    z-index: 1020;
}

.app-main {
    padding: 1.5rem;
    flex: 1;
    /* The real scroll container — see .app-content's comment above.
       Deliberately NOT -webkit-overflow-scrolling: touch: every page's
       modals (@yield('content')) render as descendants of this element,
       and that legacy property makes iOS Safari composite position:fixed
       descendants as if they belonged to THIS scrolling layer instead of
       the viewport — the modal still paints in the right place, but touch
       hit-testing inside it (inputs, buttons) goes to the wrong
       coordinates, so it looks fine but nothing inside is tappable. Every
       iOS version in real use (13+) already does momentum scrolling on a
       plain `overflow-y: auto` without this property, so removing it costs
       nothing. Modals declared outside this container (global-confirm-modal
       / global-info-modal, in layouts/app.blade.php) were never affected —
       that's why "simple" modals like the logout confirm always worked. */
    overflow-y: auto;
    overflow-x: hidden;
}

.app-account-btn {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    border: none;
    background: transparent;
    padding: 0.3rem 0.5rem;
    border-radius: 0.6rem;
}

/* Bootstrap's .dropdown-toggle::after caret has no color of its own — it
   inherits currentColor, which on this button resolves to the browser's
   default interactive-control tint (blue) on some mobile browsers instead
   of the brand green. */
.app-account-btn.dropdown-toggle::after {
    color: var(--bs-primary);
}

.app-account-btn:hover {
    background: var(--border-light);
}

.app-avatar {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--bs-primary);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
    font-size: 0.85rem;
    flex-shrink: 0;
    overflow: hidden;
}

.app-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.qr-preview-img {
    width: 220px;
    height: 220px;
    max-width: 100%;
}

/* Size variants — reuse these instead of inline width/height/font-size
   wherever an avatar thumbnail is shown (tables, modals, profile forms). */
.app-avatar-sm {
    width: 36px;
    height: 36px;
    font-size: 0.75rem;
}

.app-avatar-lg {
    width: 64px;
    height: 64px;
    font-size: 1.3rem;
}

/* Same "photo picker preview" idea as .app-avatar (wired via the same
   wireAvatarPreview/resetAvatarPreview JS helpers), but a rounded square
   instead of a circle — a villa/pool photo cropped into a circle would
   look wrong the way a person's face doesn't. */
.villa-thumb-preview {
    width: 72px;
    height: 72px;
    border-radius: 0.85rem;
    background: var(--border-light);
    color: #94a3b8;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.4rem;
    flex-shrink: 0;
    overflow: hidden;
}

.villa-thumb-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Background page can't scroll while the mobile/tablet drawer sidebar is
   open — without this, scrolling behind it is what triggered the fixed
   sidebar to visibly desync (white gap under the nav items) on tablet
   touch scroll momentum. Same pattern as the photo lightbox's own
   body.monitoring-preview-lock. */
body.sidebar-open-lock {
    overflow: hidden;
}

.app-sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.45);
    z-index: 1025;
}

.app-sidebar-backdrop.show {
    display: block;
}

@media (max-width: 991.98px) {
    .app-sidebar {
        transform: translateX(-100%);
        width: 270px;
        z-index: 1040;
    }

    .app-sidebar.mobile-open {
        transform: translateX(0);
    }

    .app-sidebar.collapsed .app-sidebar-brand-text,
    .app-sidebar.collapsed .app-nav-label,
    .app-sidebar.collapsed .app-sidebar-role {
        display: block;
    }

    .app-content,
    .app-content.sidebar-collapsed {
        margin-left: 0;
    }
}

@media (max-width: 767.98px) {
    .app-main {
        padding: 1rem;
    }
}

/* ---- Stepper (Wizard) ---- */
.wizard-stepper {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.wizard-step {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.wizard-step-circle {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.8rem;
    background: var(--border-light);
    color: #94a3b8;
    flex-shrink: 0;
}

.wizard-step.active .wizard-step-circle {
    background: var(--bs-primary);
    color: #fff;
    box-shadow: 0 0 0 4px rgba(var(--bs-primary-rgb), 0.15);
}

.wizard-step.done .wizard-step-circle {
    background: var(--success);
    color: #fff;
}

.wizard-step-label {
    font-size: 0.8rem;
    font-weight: 600;
    color: #94a3b8;
}

.wizard-step.active .wizard-step-label,
.wizard-step.done .wizard-step-label {
    color: #1e293b;
}

.wizard-step-line {
    width: 40px;
    height: 2px;
    background: var(--border-color);
}

.wizard-step.done + .wizard-step-line {
    background: var(--success);
}

/* ---- Upload zone ---- */
.upload-zone {
    border: 2px dashed var(--border-color);
    border-radius: 0.75rem;
    padding: 1rem;
    text-align: center;
    background: var(--border-light);
}

.upload-zone.has-photos {
    border-style: solid;
    border-color: var(--success);
    background: rgba(16, 185, 129, 0.05);
}

.upload-thumb {
    position: relative;
    width: 84px;
    height: 84px;
    border-radius: 0.5rem;
    overflow: hidden;
    border: 1px solid var(--border-color);
}

.upload-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.upload-thumb .upload-thumb-remove {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: rgba(15, 23, 42, 0.65);
    color: #fff;
    border: none;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.65rem;
    line-height: 1;
}

/* ---- Required field pill / progress ----
   One row, no wrap — scrolls horizontally instead of piling pills onto
   more and more lines as fields get added. Same hide-the-scrollbar
   treatment as .app-tabs elsewhere in the app. */
[data-missing-fields] {
    display: flex;
    flex-wrap: nowrap;
    gap: 0.4rem;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}

[data-missing-fields]:empty {
    display: none;
}

[data-missing-fields]::-webkit-scrollbar {
    display: none;
}

.req-pill {
    flex-shrink: 0;
    white-space: nowrap;
    font-size: 0.7rem;
    font-weight: 600;
    padding: 0.3rem 0.6rem;
    border-radius: 999px;
    border: 1px solid var(--border-color);
    background: #fff;
    color: #64748b;
    cursor: pointer;
}

.req-pill.done {
    background: rgba(16, 185, 129, 0.1);
    color: var(--success);
    border-color: transparent;
}

/* ---- Skeleton ---- */
.skeleton {
    background: linear-gradient(90deg, var(--border-light) 25%, #e9edf3 37%, var(--border-light) 63%);
    background-size: 400% 100%;
    animation: skeleton-shimmer 1.4s ease infinite;
    border-radius: 0.5rem;
}

@keyframes skeleton-shimmer {
    0% {
        background-position: 100% 50%;
    }

    100% {
        background-position: 0 50%;
    }
}

/* ---- Toast container ---- */
.toast-stack {
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    left: 1rem;
    z-index: 1080;
    display: flex;
    flex-direction: column-reverse;
    align-items: flex-end;
    gap: 0.5rem;
    pointer-events: none;
}

.toast-stack .toast {
    pointer-events: auto;
    min-width: 280px;
    max-width: 360px;
    border: none;
    border-radius: 1rem;
    box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
}

/* Color now lives on the icon's soft circle badge (same pattern as every
   other icon-circle in the app) instead of a flat colored strip along the
   toast's edge — matches the rest of the UI's rounded, badge-driven look
   instead of the old flat-bordered box. */
.toast-stack .toast .toast-icon-circle {
    width: 38px;
    height: 38px;
    font-size: 1rem;
}

/* ---- Global backup progress widget ---- */
.global-backup-widget {
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    z-index: 1080;
    min-width: 260px;
    max-width: 320px;
    padding: 0.75rem 1rem;
    background: #fff;
    border-radius: 1rem;
    box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
    transition: min-width 0.15s ease, max-width 0.15s ease, padding 0.15s ease;
}

.global-backup-widget-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.global-backup-widget-body {
    flex: 1;
    min-width: 0;
}

.global-backup-widget-toggle {
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: #94a3b8;
    padding: 0.25rem 0.4rem;
    line-height: 1;
    border-radius: 0.5rem;
}

.global-backup-widget-toggle:hover {
    background: #f1f5f9;
    color: #475569;
}

/* Minimized: collapse down to just the spinner in a small round badge —
   click anywhere on it to expand back. */
.global-backup-widget.is-minimized {
    min-width: 0;
    max-width: none;
    width: 3rem;
    height: 3rem;
    padding: 0;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

.global-backup-widget.is-minimized .global-backup-widget-body,
.global-backup-widget.is-minimized .global-backup-widget-toggle,
.global-backup-widget.is-minimized [data-role="stop"] {
    display: none;
}

/* ---- Lightbox ---- */
.lightbox-overlay {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.9);
    z-index: 1090;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 2rem;
}

.lightbox-overlay.show {
    display: flex;
}

.lightbox-overlay img {
    max-width: 100%;
    max-height: 90vh;
    border-radius: 0.5rem;
}

.lightbox-close {
    position: absolute;
    top: 1.25rem;
    right: 1.5rem;
    background: rgba(255, 255, 255, 0.15);
    border: none;
    color: #fff;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    font-size: 1.1rem;
}

/* ---- Owner public hero ---- */
.owner-hero {
    background: linear-gradient(135deg, var(--bs-primary), var(--bs-primary-hover));
    color: #fff;
    border-radius: 1rem;
    padding: 2rem;
}

/* Villa name here was a bare <h3>, so it rendered at Bootstrap's default
   1.75rem — much larger than every other heading in the app's own scale. */
.owner-hero h3 {
    font-family: var(--font-title);
    font-size: 1.3rem;
}
