/* AttendBase — small overrides/utilities layered on top of Tabler Core. */

:root {
  /* 'Noto Sans Khmer'/'Noto Sans SC' only kick in per-character, for glyphs
     none of the earlier fonts in this stack actually contain (Khmer script,
     CJK ideographs) — added for the platform's Khmer/English/Chinese i18n
     support (see i18n.js). They must be loaded via a <link> for this to
     work (see admin/settings.html's <head> for the reference include);
     pages that don't load them just fall through further down the stack
     (largely Windows' own Khmer/CJK system fonts), same as before. */
  --ab-font-sans: 'Google Sans', 'Noto Sans Khmer', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* Tabler components (buttons, form controls, badges, ...) read this
     variable directly for their font-family rather than inheriting body's
     computed style, so overriding body alone leaves them on Tabler's
     default Inter/system stack. */
  --tblr-font-sans-serif: var(--ab-font-sans);
  /* Shared by the top navbar and the sidebar's brand row so the two line up
     at the same height across the top of the layout. */
  --ab-topbar-h: 4rem;
  --tblr-page-padding-y: 1rem;
}
/* Khmer UI language (i18n.js sets <html lang="km">): Khmer script renders in
   Battambang. Self-hosted Khmer-only subset (Google Fonts' own khmer files,
   SIL Open Font License) with a Khmer unicode-range, so it can sit FIRST in
   the stack — Google Sans itself now covers Khmer and would otherwise win —
   while Latin letters and digits still fall through to Google Sans. The
   files download only once Khmer text is shown in Khmer mode. */
@font-face {
  font-family: 'AB Battambang Khmer';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/battambang-khmer-400.woff2) format('woff2');
  unicode-range: U+1780-17FF, U+19E0-19FF, U+200C-200D, U+25CC;
}
@font-face {
  font-family: 'AB Battambang Khmer';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(../fonts/battambang-khmer-700.woff2) format('woff2');
  unicode-range: U+1780-17FF, U+19E0-19FF, U+200C-200D, U+25CC;
}
html:lang(km) {
  --ab-font-sans: 'AB Battambang Khmer', 'Google Sans', 'Noto Sans Khmer', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* Chinese UI language (<html lang="zh">): Chinese renders in Noto Serif SC.
   Google Sans has no CJK glyphs, so Latin letters and digits keep Google Sans
   and only the Chinese characters fall through to Noto Serif SC. Loaded by the
   pages' shared Google Fonts <link> (per-character-range files, fetched only
   for characters actually shown, i.e. only in Chinese mode). */
html:lang(zh) {
  --ab-font-sans: 'Google Sans', 'Noto Serif SC', 'Noto Sans SC', 'Noto Sans Khmer', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* Noto Serif SC is thin at the regular weight, so in Chinese mode "normal"
     text is medium (500) instead of 400 — body text through Tabler's own
     variables, plus the Tabler components that hard-code 400 below. Bold/
     semibold text keeps its own weight. CSS can't weight scripts separately,
     so Latin letters/digits in that text are medium too (Chinese mode only).
     Weight 500 of both fonts is already in the shared Google Fonts <link>. */
  --tblr-body-font-weight: 500;
  --tblr-font-weight-normal: 500;
}
html:lang(zh) :is(.form-control, .form-select, .input-group-text, .btn-link, .dropdown-item, .tooltip, .popover, .card-subtitle) { font-weight: 500; }
html:lang(zh) .fw-normal { font-weight: 500 !important; }
body { font-family: var(--ab-font-sans); }

html, body { height: 100%; }
body { display: flex; flex-direction: column; }
.page { flex: 1; }

/* Page scrollbar — hidden entirely. Scrolling still works (wheel, trackpad,
   touch, keyboard); only the visible track/thumb is removed. */
html { scrollbar-width: none; -ms-overflow-style: none; }
html::-webkit-scrollbar { display: none; width: 0; height: 0; }

/* Auth loading overlay — pure CSS (no body markup needed on every page): the
   inline head snippet adds 'ab-auth-pending' to <html> before first paint,
   and auth.js removes it once Firebase resolves the real auth state (see
   auth.js header comment). Covers the whole viewport so nothing underneath
   — protected content, or the login form for an already-signed-in user —
   is ever visible before that decision is made. */
html.ab-auth-pending::before {
  content: '';
  position: fixed; inset: 0; z-index: 2000;
  background: var(--tblr-body-bg, #fff);
}
html.ab-auth-pending::after {
  content: '';
  position: fixed; top: 50%; left: 50%; z-index: 2001;
  width: 2rem; height: 2rem; margin: -1rem 0 0 -1rem;
  border-radius: 50%;
  border: .25rem solid var(--tblr-border-color, #dde1e6);
  border-top-color: var(--tblr-primary, #206bc4);
  animation: ab-auth-spin .6s linear infinite;
}
@keyframes ab-auth-spin { to { transform: rotate(360deg); } }

/* Table row action buttons (view/edit/history) — a soft rounded chip with a
   clear hover state and real spacing between adjacent actions, instead of
   Tabler's bare .btn-icon default (no background/border at rest, so it reads
   as a floating icon rather than a button, and sits flush against its
   neighbor with no visible gap). */
.ab-table-action-btn {
  width: 2rem; height: 2rem; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: .5rem;
  color: var(--tblr-secondary);
  background: var(--tblr-bg-surface-secondary);
  text-decoration: none;
}
.ab-table-action-btn:hover { background: var(--tblr-border-color); color: var(--tblr-body-color); text-decoration: none; }
.ab-table-action-btn + .ab-table-action-btn { margin-left: .375rem; }
/* The whitespace between adjacent action-btn anchors in the markup is itself
   a wrap opportunity — on a cell that isn't shrink-to-fit (only the header
   has .w-1) that whitespace breaks the second button onto its own line
   instead of sitting beside the first. */
td:has(> .ab-table-action-btn) { white-space: nowrap; }
/* Product thumbnail keeps its size when a long name wraps beside it. */
.ab-product-avatar { flex-shrink: 0; }
.ab-product-avatar-empty { overflow: hidden; background: var(--tblr-bg-surface-tertiary, #eef1f5); color: var(--tblr-secondary); }
.ab-product-avatar-empty svg { width: 100%; height: 100%; display: block; opacity: .4; }
/* Click-to-sort table headers (catalog Products list). */
th.ab-sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.ab-sortable .ab-sort-icon { font-size: .9rem; vertical-align: -2px; }
th.ab-sortable .ab-sort-icon.ti-arrows-sort { opacity: .35; }
th.ab-sortable:hover .ab-sort-icon.ti-arrows-sort { opacity: .8; }
/* ⋯ "more actions" menu at the end of a row's action buttons (catalog lists). */
button.ab-table-action-btn { border: 0; }
.ab-row-more { display: inline-block; vertical-align: middle; }
.ab-table-action-btn + .ab-row-more { margin-left: .375rem; }

/* The clear-filters icon-only button reads as an empty input field at rest
   (a bordered box with no visible fill) — give it the same soft chip
   treatment as the table actions above so it's unmistakably a button. */
.ab-clear-filters-btn {
  background: var(--tblr-bg-surface-secondary);
  border-color: transparent;
  color: var(--tblr-secondary);
}
.ab-clear-filters-btn:hover { background: var(--tblr-border-color); color: var(--tblr-body-color); border-color: transparent; }

/* Mobile sidebar — a real sliding off-canvas panel (Bootstrap's .offcanvas-lg:
   full off-canvas overlay w/ backdrop below the lg breakpoint, plain static
   sidebar content at lg+) instead of the collapse-based approach, which just
   clipped the expanded nav list to the collapsed bar's own tiny height. */
@media (max-width: 991.98px) {
  #ab-sidebar-menu.offcanvas-lg { width: 17rem !important; }
  /* The <aside> itself still had a min-height/padding of its own (Tabler's
     collapsed-bar defaults) even once its brand row was hidden — leaving an
     empty dark strip above the real top navbar. Zero it out; the toggle
     button now lives in the top navbar instead, and the offcanvas panel is
     position:fixed so it isn't affected by its now-collapsed parent. */
  aside.navbar-vertical { min-height: 0; height: 0; padding: 0; }
}
#ab-sidebar-menu.offcanvas-lg { background: var(--tblr-bg-surface, #fff); }
#ab-sidebar-menu .offcanvas-header { padding: 1.25rem 1rem; border-bottom: 1px solid var(--tblr-border-color); }
#ab-sidebar-menu .offcanvas-body { padding: 0; }

/* Sidebar. Tabler also draws a 1px divider on the sidebar's right edge via
   an inset box-shadow (not a border) — drop it for a fully seamless join
   with the page-wrapper next to it. */
aside.navbar-vertical {
  background: var(--tblr-bg-surface, #fff);
  box-shadow: none !important;
  border-right: 1px solid var(--tblr-border-color);
}

@media (min-width: 992px) {
  /* Only the nav LIST scrolls — the brand/logo row stays fixed in place
     above it, so a long enabled-module list never pushes the logo out of
     view. Previously the whole <aside> scrolled as one flex-less block
     (overflow-y:auto directly on it), dragging the brand row along with
     the nav. Restructured as a column flex chain (aside > .container-fluid
     > #ab-sidebar-menu) down to .offcanvas-body, the one region that
     actually scrolls — .navbar-brand, sized to its own fixed
     --ab-topbar-h (see below), simply isn't part of that scrolling region
     any more. Scoped to this breakpoint only: below it, .offcanvas-lg
     reverts to Bootstrap's real slide-out offcanvas, which already
     supplies its own header-stays/body-scrolls behavior for free — this
     rule would be redundant (and aside.navbar-vertical is zeroed out
     entirely down there, see above). */
  aside.navbar-vertical {
    display: flex !important;
    flex-direction: column;
    overflow: hidden !important;
  }
  aside.navbar-vertical > .container-fluid {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    padding: 0;
  }
  .navbar-vertical .navbar-brand { flex: 0 0 auto; }
  #ab-sidebar-menu.offcanvas-lg {
    display: flex !important;
    flex-direction: column;
    flex: 1;
    min-height: 0;
  }
  /* Same slim, dark-tinted scrollbar as before (scrollbar-only-when-needed,
     blends in on a real non-overlay Windows scrollbar) — now scoped to the
     nav's own scroll region instead of the whole sidebar. */
  #ab-sidebar-menu .offcanvas-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: rgba(127, 127, 127, .4) transparent;
  }
  #ab-sidebar-menu .offcanvas-body::-webkit-scrollbar { width: 6px; }
  #ab-sidebar-menu .offcanvas-body::-webkit-scrollbar-track { background: transparent; }
  #ab-sidebar-menu .offcanvas-body::-webkit-scrollbar-thumb { background: rgba(127, 127, 127, .4); border-radius: 999px; }
}

/* Sidebar collapse/expand — desktop-only (matches navbar-expand-lg's own
   992px breakpoint). Below that, the sidebar isn't a persistent rail at all —
   it's Tabler's collapsible mobile top bar (full width, off-canvas nav list),
   so forcing a fixed 240px/72px width there broke it (the dark bar stopped
   partway across the screen instead of spanning it). The toggle button
   itself lives in the top navbar (styled as a plain .ab-navbar-icon-btn
   alongside the theme toggle) rather than docked to the sidebar, and is
   hidden on mobile since collapsing isn't a meaningful action there. State
   is persisted in localStorage and applied to <html> (not <body>) so an
   inline <head> snippet — matching the theme-init pattern — can set it
   before first paint, avoiding a flash of the wrong sidebar width. */
:root {
  --ab-sidebar-w: 15rem; --ab-sidebar-w-collapsed: 4.5rem;
  --ab-sidebar-ease: .25s cubic-bezier(.4, 0, .2, 1);
}
@media (min-width: 992px) {
  /* Tabler's own CSS sets `transition: transform .3s` on .navbar-vertical
     (for its mobile off-canvas slide) — since `transition` is a shorthand,
     that rule replaces ours outright rather than adding to it, silently
     dropping the width transition (the sidebar then just snaps instantly
     instead of animating). !important here targets the transition
     declaration itself, not the animated width value — that's a different,
     safe use of !important, and transform-based offcanvas sliding isn't
     relevant at this desktop-only breakpoint anyway. */
  aside.navbar-vertical { width: var(--ab-sidebar-w); transition: width var(--ab-sidebar-ease) !important; }
  .page-wrapper { transition: margin-left var(--ab-sidebar-ease) !important; }

  /* Labels fade+collapse in step with the sidebar's own width transition,
     instead of snapping via display:none — that made the icons/text vanish
     instantly while the sidebar itself was still visibly animating,
     the mismatch is what read as "not smooth". Icon spacing (margin) and
     row alignment (justify-content) transition/settle alongside it; only
     justify-content can't itself be animated (browsers don't interpolate
     it), so it flips near the end of the same transition window rather than
     abruptly at the start. */
  .nav-link-title,
  .ab-nav-chevron {
    opacity: 1; max-width: 10rem; overflow: hidden; white-space: nowrap;
    transition: opacity var(--ab-sidebar-ease), max-width var(--ab-sidebar-ease), margin var(--ab-sidebar-ease);
  }
  .nav-link-icon { transition: margin var(--ab-sidebar-ease); }
  .ab-brand-logo-chip { transition: padding var(--ab-sidebar-ease); }

  html.ab-sidebar-collapsed aside.navbar-vertical { width: var(--ab-sidebar-w-collapsed) !important; }
  html.ab-sidebar-collapsed .page-wrapper { margin-left: var(--ab-sidebar-w-collapsed) !important; }
  html.ab-sidebar-collapsed .nav-link-title,
  html.ab-sidebar-collapsed .ab-nav-chevron { opacity: 0; max-width: 0; margin: 0 !important; }
  html.ab-sidebar-collapsed .ab-nav-sub { display: none !important; }
  html.ab-sidebar-collapsed .ab-brand-link { justify-content: center; }
  html.ab-sidebar-collapsed .ab-brand-logo-chip { padding: .25rem; }
  html.ab-sidebar-collapsed .ab-brand-logo { height: 1.25rem; }
  html.ab-sidebar-collapsed #ab-sidebar-nav { padding: 1rem .5rem 4rem; }
  html.ab-sidebar-collapsed #ab-sidebar-nav .nav-link { justify-content: center !important; padding-left: .5rem; padding-right: .5rem; }
  html.ab-sidebar-collapsed .nav-link-icon { margin: 0 !important; }
}

/* Sidebar brand — pinned to the same height as the top navbar (--ab-topbar-h)
   so the two line up in a single visual row across the top of the layout,
   with a divider under it separating it from the nav list — matching the
   divider the mobile off-canvas header already had. `overflow: hidden` is a
   safety net: it guarantees the row is capped at exactly --ab-topbar-h even
   if the logo image inside ends up taller than its own sizing rule expects. */
.navbar-vertical .navbar-brand {
  width: 100%;
  height: var(--ab-topbar-h);
  margin: 0;
  padding-top: 0; padding-bottom: 0;
  overflow: hidden;
  border-bottom: 1px solid var(--tblr-border-color);
}
.ab-brand-link { display: flex; align-items: center; justify-content: center; height: 100%; }
.ab-brand-logo-chip {
  display: inline-flex; align-items: center; justify-content: center;
  padding: .25rem .5rem;
  max-width: 100%;
}
/* Doubled-up class selector (.navbar-brand .ab-brand-logo) so this beats any
   `.navbar-brand img` sizing rule Tabler itself ships, regardless of load
   order — a single-class selector alone was losing that specificity fight,
   which is why the logo was rendering at its native size instead of this. */
.navbar-brand .ab-brand-logo,
.ab-brand-logo { display: block; height: 2.75rem; width: auto; max-width: 100%; object-fit: contain; }

/* Sidebar — rounded pill hover/active states, spacing, chevrons.
   Tabler's own nav-link styling centers icon+label (justify-content:center)
   below the lg breakpoint, meant for a plain mobile dropdown — since our
   offcanvas panel is full desktop-width-ish, force the same left-aligned,
   icon / label / chevron-at-the-end row at every breakpoint. */
/* Bottom padding so the last item (or a submenu's last child, expanded
   right at the end of the list) has breathing room above the scroll
   region's own edge instead of sitting flush against it — cramped and
   awkward to click precisely. */
#ab-sidebar-nav { padding: 1rem .5rem 4rem; }
#ab-sidebar-nav .nav-link {
  display: flex;
  justify-content: flex-start !important;
  align-items: center;
  padding: .25rem .625rem;
  min-height: 38px;
  border-radius: .5rem;
  margin-bottom: 0;
  color: var(--tblr-secondary);
  transition: background-color .15s ease, color .15s ease;
}
#ab-sidebar-nav > .nav-item > .nav-link:hover,
#ab-sidebar-nav .ab-nav-sub .nav-link:hover { background: var(--tblr-bg-surface-secondary); color: var(--tblr-body-color); }
/* A leaf link (e.g. Dashboard) being the current page gets the same soft
   background as :hover, just always-on — bold body-color text (black in
   light mode, white in dark mode, via --tblr-body-color) is what marks it
   "active" rather than a competing background treatment. A section toggle
   (e.g. Administration) being merely *expanded* is a different, weaker signal —
   the real "you are here" indicator is whichever sub-item is active
   underneath it — so it gets the same treatment, not a stronger one. */
#ab-sidebar-nav > .nav-item > .nav-link.active { background: var(--tblr-bg-surface-secondary); color: var(--tblr-body-color); font-weight: 600; }
#ab-sidebar-nav > .nav-item > .nav-link.ab-nav-toggle.active { background: var(--tblr-bg-surface-secondary); color: var(--tblr-body-color); }
#ab-sidebar-nav .ab-nav-sub .nav-link.active {
  background: transparent; color: var(--tblr-body-color); font-weight: 600; position: relative;
}
#ab-sidebar-nav .ab-nav-sub .nav-link.active::before {
  content: ''; position: absolute; left: -.4375rem; top: 50%; transform: translateY(-50%);
  width: 3px; height: 1rem; border-radius: 2px; background: var(--tblr-primary);
}
.ab-nav-chevron { margin-left: auto; transition: transform .15s ease; font-size: .9rem; opacity: .6; }
.ab-nav-toggle[aria-expanded="true"] .ab-nav-chevron { transform: rotate(180deg); }
.ab-nav-sub { padding-left: 2.25rem; margin-top: .125rem; }
/* a section's submenu: a thin vertical guide line under the section's icon (its centre, 20px in), the items'
   text where it was (36px in), and the current page's blue bar centred on the line */
#ab-sidebar-nav .ab-nav-collapse > .ab-nav-sub { margin-left: 1.1875rem; padding-left: 1rem; border-left: 1px solid var(--tblr-border-color); }
#ab-sidebar-nav .ab-nav-collapse > .ab-nav-sub > .nav-item > .nav-link.active::before { left: -1.125rem; }
.ab-nav-sub .nav-link { padding-top: .2rem; padding-bottom: .2rem; padding-left: .375rem; font-size: .8375rem; border-radius: .375rem; }

/* Present on #ab-sidebar-nav from the moment partials.html injects the
   sidebar markup (2026-09-16) — hides the real nav until
   UA.applySidebarPermissions()'s async module-authorization check
   (applyShopModuleGate(), a real Firestore read for a Shop/Merchant Admin
   session) has actually resolved and removed the class, so a menu that's
   about to be hidden again a moment later is never shown in between.
   Originally just `visibility:hidden` on the real items (a blank gap);
   redesigned same day into a shimmering SKELETON nav shown in that gap
   instead (#ab-sidebar-skeleton, markup in sidebar.html, right after this
   list) — the sibling combinator below ties its visibility directly to
   this same class, no separate JS toggle needed. */
#ab-sidebar-nav.ab-nav-pending { display: none; }
#ab-sidebar-nav.ab-nav-pending ~ #ab-sidebar-skeleton { display: block; }

#ab-sidebar-skeleton { display: none; padding: 1rem .5rem 0; margin: 0; list-style: none; }
.ab-skel-nav-row { display: flex; align-items: center; gap: .625rem; padding: .25rem .625rem; min-height: 38px; }
.ab-skel-nav-icon { width: 1.125rem; height: 1.125rem; border-radius: .3125rem; flex: none; }

/* "No Module Access" empty state — platform/index.html only (2026-09-16).
   Shown by adding class="ab-no-module-access" to <body> from
   assets/js/user-admin.js's applyShopModuleGate(), for a shop with
   nothing enabled at all (including the dashboard itself), which would
   otherwise have nowhere left to redirect to. Deliberately toggled with
   plain, non-!important display rules (never Bootstrap's `.d-flex`
   alongside a `hidden` attribute) — see [[css-hidden-dflex-conflict]]. */
#ab-no-access-panel { display: none; min-height: calc(100vh - 4rem); align-items: center; justify-content: center; padding: 2rem 1rem; }
body.ab-no-module-access #ab-page-shell { display: none; }
body.ab-no-module-access #ab-no-access-panel { display: flex; }

.ab-no-access-card { max-width: 26rem; text-align: center; padding: 2.5rem 2rem; border-radius: 1rem; background: var(--tblr-bg-surface, #fff); box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.06); border: 1px solid var(--tblr-border-color, #e6e7e9); }
.ab-no-access-icon { width: 4rem; height: 4rem; margin: 0 auto 1.25rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--tblr-red-lt, #fff5f5); color: var(--tblr-red, #d63939); font-size: 1.75rem; }
.ab-no-access-title { font-weight: 600; margin-bottom: .5rem; }
.ab-no-access-text { color: var(--tblr-secondary, #6c757d); margin-bottom: 1.5rem; }

/* ab-page-pending (2026-09-16) — index.html's own loading state while
   applyShopModuleGate() hasn't resolved yet, so it's still unknown
   whether to reveal the normal dashboard or the "No Module Access" panel
   above. Declared AFTER both of that panel's own display rules so that,
   on the specificity tie (every selector here is `body.<class> #<id>`),
   source order — not accidental rule ordering — is what guarantees
   pending wins for the brief window a class removal and a class addition
   could ever coexist. */
#ab-page-loading { display: none; }
body.ab-page-pending #ab-page-shell { display: none; }
body.ab-page-pending #ab-no-access-panel { display: none; }
body.ab-page-pending #ab-page-loading { display: block; }

/* Skeleton primitives used by #ab-page-loading's markup — a shimmering
   placeholder shaped like the real dashboard (stat cards + table rows)
   rather than a generic centered spinner, so the swap-in reads as a
   continuation of the same page. Reusable by any future page that wants
   the same treatment: `.ab-skel` is the shimmer surface itself,
   `.ab-skel-text`/`.ab-skel-circle` just set its shape. */
.ab-skel { display: block; border-radius: .375rem; background: linear-gradient(90deg, var(--tblr-body-bg, #eef1f4) 25%, var(--tblr-border-color, #dde1e6) 37%, var(--tblr-body-bg, #eef1f4) 63%); background-size: 400% 100%; animation: ab-skel-shimmer 1.5s ease-in-out infinite; }
@keyframes ab-skel-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
.ab-skel-text { height: .8rem; }
.ab-skel-circle { width: 2.5rem; height: 2.5rem; border-radius: 50%; }
.ab-skel-circle-sm { width: 1.75rem; height: 1.75rem; }
.ab-skel-row { display: flex; align-items: center; gap: 1rem; padding: .625rem 0; }
.ab-skel-row-actions { display: flex; gap: .5rem; margin-left: auto; }

/* Borderless by design (2026-09-16) — the skeleton's own cards/rows drop
   Tabler's default `.card`/`.card-header` border and this file's own
   `.ab-skel-row` divider entirely, so nothing but the shimmering
   placeholders themselves is visible while pending; spacing alone
   separates the rows. Scoped to #ab-page-loading so the REAL dashboard's
   cards (once revealed) keep their normal bordered look untouched. */
#ab-page-loading .card { border: 0; box-shadow: none; }
#ab-page-loading .card-header { border-bottom: 0; }

/* Property report skeletons (report-*.html, markup in each page): same borderless treatment — no card frame, no header divider,
   only the shimmering blocks on their white card surface. Scoped so the real report cards keep their borders. */
.ab-report-skel .card { border: 0; box-shadow: none; }
.ab-report-skel .card-header { border-bottom: 0; }

/* Top navbar — a touch of depth so it reads as a distinct layer above the
   scrolling page body, full-width (no boxed container), and consistent
   rounded hover feedback on every right-side control. */
.navbar.navbar-expand-md.sticky-top {
  height: var(--ab-topbar-h);
  /* box-shadow: 0 1px 2px rgba(0, 0, 0, .05); */
}
.navbar.navbar-expand-md.sticky-top > .container-fluid { height: var(--ab-topbar-h); }
.navbar.navbar-expand-md.sticky-top .navbar-nav.flex-row > .nav-item { margin-left: .25rem; }
.navbar.navbar-expand-md.sticky-top .nav-item > .nav-link {
  border-radius: .5rem;
  transition: background-color .15s ease;
}
.navbar.navbar-expand-md.sticky-top .nav-item > .nav-link:hover,
.navbar.navbar-expand-md.sticky-top .nav-item.show > .nav-link { background: var(--tblr-bg-surface-secondary); }
.navbar.navbar-expand-md.sticky-top .nav-item + .nav-item.dropdown { margin-left: .5rem; }

/* Icon buttons (theme toggle, notifications) — a permanent soft circular chip
   so they read as tappable controls at rest, not just on hover. */
.ab-navbar-icon-btn {
  position: relative;
  width: 2.5rem; height: 2.5rem; padding: 0 !important; border-radius: 999px !important;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--tblr-bg-surface-secondary); color: var(--tblr-secondary); font-size: 1.05rem;
}
.ab-navbar-icon-btn:hover { background: var(--tblr-border-color); color: var(--tblr-body-color); }
/* Tabler's default :focus style for .nav-link/.btn drops the background to a
   near-invisible ~4% tint — after a real click the button keeps browser
   focus (it isn't blurred), so it visibly "disappears" until the user clicks
   elsewhere. Keep the same resting background and show focus as a ring
   instead, so the button never looks like it's vanished. */
.ab-navbar-icon-btn:focus,
.ab-navbar-icon-btn:focus-visible {
  background: var(--tblr-bg-surface-secondary);
  color: var(--tblr-secondary);
  box-shadow: 0 0 0 2px var(--tblr-primary-lt);
}

/* Notification count — a small fixed-size dot pinned just inside the icon
   button's corner, with a surface-colored ring so it reads as sitting on
   top of the icon rather than a mis-scaled Bootstrap badge floating above it. */
.ab-navbar-badge {
  position: absolute; top: -1px; right: -1px;
  min-width: 1.125rem; height: 1.125rem; padding: 0 .25rem;
  font-size: .625rem; font-weight: 600; line-height: 1; color: #fff;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px; border: 2px solid var(--tblr-bg-surface);
}

/* Thin separator between the utility icons and the account menu */
.ab-navbar-divider { width: 1px; height: 1.5rem; background: var(--tblr-border-color); margin: 0 .5rem; align-self: center; }

/* Account menu trigger gets the same rounded hover as the icon buttons, plus
   a touch of horizontal padding for the avatar+name combo. */
.ab-navbar-user { padding: .25rem .5rem .25rem .25rem !important; }

/* Notification panel — colored icon chips, unread dots, a "mark all read"
   header action, and enough width that notification text wraps to a second
   line instead of getting cut off mid-word. */
.ab-notif-menu { width: 23rem; max-width: calc(100vw - 2rem); padding: 0; overflow: hidden; }
.ab-notif-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: .875rem 1rem; border-bottom: 1px solid var(--tblr-border-color);
}
.ab-notif-title { font-size: .9375rem; font-weight: 600; margin: 0; }
.ab-notif-markread { font-size: .75rem; color: var(--tblr-primary); text-decoration: none; }
.ab-notif-markread:hover { text-decoration: underline; }
.ab-notif-list { max-height: 22rem; overflow-y: auto; }
.ab-notif-item {
  display: flex; align-items: flex-start; gap: .75rem;
  padding: .75rem 1rem; text-decoration: none; color: inherit;
  border-bottom: 1px solid var(--tblr-border-color);
  transition: background-color .15s ease;
}
.ab-notif-item:last-child { border-bottom: none; }
.ab-notif-item:hover { background: var(--tblr-bg-surface-secondary); color: inherit; text-decoration: none; }
.ab-notif-icon {
  width: 2.25rem; height: 2.25rem; border-radius: .625rem; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center; font-size: 1.05rem;
}
.ab-notif-body { flex: 1; min-width: 0; }
.ab-notif-text { font-size: .8125rem; color: var(--tblr-body-color); line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ab-notif-time { font-size: .75rem; color: var(--tblr-secondary); margin-top: .125rem; }
.ab-notif-dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--tblr-primary); flex: 0 0 auto; margin-top: .375rem; opacity: 0; }
.ab-notif-item.is-unread .ab-notif-dot { opacity: 1; }

/* Reminders panel (2026-10-06): category tabs, sticky section headers,
   compact rows with a status pill, "view all" links. */
.ab-notif-menu:has(.ab-rem-tabs) { width: 25rem; }
.ab-rem-tabs { display: flex; gap: .25rem; padding: .5rem .75rem; border-bottom: 1px solid var(--tblr-border-color); overflow-x: auto; scrollbar-width: none; }
.ab-rem-tab { border: 0; background: transparent; color: var(--tblr-secondary); font-size: .8125rem; padding: .25rem .625rem; border-radius: 999px; white-space: nowrap; }
.ab-rem-tab:hover { background: var(--tblr-bg-surface-secondary); color: var(--tblr-body-color); }
.ab-rem-tab.active { background: var(--tblr-primary-lt, rgba(32,107,196,.1)); color: var(--tblr-primary); font-weight: 600; }
.ab-rem-tab-count { display: inline-block; min-width: 1.25rem; padding: 0 .3rem; border-radius: 999px; background: var(--tblr-bg-surface-secondary); color: var(--tblr-secondary); font-size: .6875rem; font-weight: 600; text-align: center; }
.ab-rem-tab.active .ab-rem-tab-count { background: var(--tblr-primary); color: #fff; }
.ab-notif-menu .ab-notif-list { max-height: 26rem; }
.ab-rem-section { position: sticky; top: 0; z-index: 1; padding: .375rem 1rem; font-size: .6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--tblr-secondary); background: var(--tblr-bg-surface-secondary); border-bottom: 1px solid var(--tblr-border-color); }
.ab-rem-item { padding: .5rem 1rem; gap: .625rem; align-items: center; }
.ab-rem-icon { width: 1.875rem; height: 1.875rem; border-radius: .5rem; font-size: .95rem; }
.ab-rem-item .ab-notif-time { margin-top: 0; }
/* read state */
.ab-rem-item.is-read { opacity: .6; }
.ab-rem-item.is-read:hover { opacity: .9; }
.ab-rem-item.is-unread .ab-notif-text.fw-bold::before { content: ''; display: inline-block; width: .45rem; height: .45rem; border-radius: 50%; background: var(--tblr-primary); margin-right: .4rem; vertical-align: middle; }
.ab-rem-mark { flex-shrink: 0; width: 1.75rem; height: 1.75rem; border: 0; border-radius: 50%; background: transparent; color: var(--tblr-secondary); display: inline-flex; align-items: center; justify-content: center; }
.ab-rem-mark:hover { background: var(--tblr-primary-lt, rgba(32,107,196,.1)); color: var(--tblr-primary); }
.ab-rem-readall { border: 0; background: transparent; color: var(--tblr-primary); font-size: .75rem; font-weight: 600; padding: .25rem .5rem; border-radius: .375rem; white-space: nowrap; }
.ab-rem-readall:hover { background: var(--tblr-primary-lt, rgba(32,107,196,.1)); }
.ab-rem-more { display: block; padding: .5rem 1rem; font-size: .8125rem; text-align: center; color: var(--tblr-primary); text-decoration: none; border-bottom: 1px solid var(--tblr-border-color); }
.ab-rem-more:hover { background: var(--tblr-bg-surface-secondary); text-decoration: none; }

/* Page rhythm — a touch more air around the header, and a soft elevation on
   every card so content reads as layered above the page background instead
   of relying on borders alone. */
/* .page-header { padding-top: 1.5rem; padding-bottom: 1.25rem; } */
.page-body .card { box-shadow: 0 1px 3px rgba(0, 0, 0, .04); }

/* Stat tiles — icon badge on top, bold auto-compact value, muted label
   beneath (label/value stay in text tokens; the icon badge alone carries
   the status color). */
.ab-stat-card { border-radius: .75rem; }
.ab-stat-value { font-size: 1.5rem; font-weight: 700; line-height: 1.2; letter-spacing: -.01em; }
.ab-stat-label { font-size: .8125rem; color: var(--tblr-secondary); margin-top: .125rem; }

/* Auth page — split-screen layout, deliberately locked to the viewport height
   (no page scroll): brand panel on the left, form panel on the right. The
   form panel keeps its own overflow-y so it degrades gracefully to an inner
   scroll on very short viewports instead of the page scrolling. */
.ab-login-body { height: 100vh; overflow: hidden; margin: 0; }
.ab-login-split { display: flex; height: 100vh; width: 100%; }

.ab-login-aside {
  flex: 0 0 40%;
  max-width: 32rem;
  background: linear-gradient(155deg, var(--tblr-primary) 0%, #0d1b2e 100%);
  position: relative;
}
.ab-login-aside-inner { display: flex; flex-direction: column; justify-content: space-between; height: 100%; padding: 3rem; }
/* .ab-brand-logo-chip's own `justify-content: center` only centers ITS
   content — here it was also getting stretched to the full column width by
   this container's default `align-items: stretch`, which is what actually
   pushed the logo to the middle. Scoped to this container only, so the
   sidebar's own (differently-laid-out) use of the same chip class is unaffected. */
.ab-login-aside-inner > .ab-brand-logo-chip { align-self: flex-start; }
.ab-login-headline { color: #fff; font-weight: 700; font-size: 2.1rem; line-height: 1.2; margin-bottom: 1rem; }
.ab-login-features { margin-top: 1.5rem; }
.ab-login-features li { display: flex; align-items: center; gap: .75rem; color: rgba(255, 255, 255, .85); margin-bottom: 1rem; font-size: .925rem; }
.ab-login-feature-icon {
  width: 2.25rem; height: 2.25rem; border-radius: .5rem; flex: 0 0 auto;
  background: rgba(255, 255, 255, .12); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
}

.ab-login-main {
  flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center;
  padding: 2rem 1.5rem; overflow-y: auto; background: var(--tblr-bg-surface-secondary);
}
.ab-auth-card { width: 100%; max-width: 24rem; }
.ab-login-card { border: none; box-shadow: 0 1rem 3rem rgba(0, 0, 0, .1); }

/* Utility pages (account activation, setup) — a single centered card, no
   split-screen like login. Deliberately NOT .ab-login-body: that locks to
   100vh/overflow:hidden, which relies on .ab-login-main's own inner scroll
   to avoid clipping content on short viewports — a utility page has no such
   inner scroll container, so it needs normal page scroll instead. */
.ab-utility-page { min-height: 100vh; background: radial-gradient(60rem 30rem at 50% -10%, rgba(32, 107, 196, .14), transparent 60%), var(--tblr-body-bg); }
.ab-utility-badge {
  width: 3.5rem; height: 3.5rem; border-radius: 1rem;
  background: linear-gradient(155deg, var(--tblr-primary) 0%, #0d1b2e 100%);
  color: #fff; font-size: 1.6rem;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 .5rem 1.5rem rgba(32, 107, 196, .3);
}
.ab-utility-card { border: none; box-shadow: 0 1rem 3rem rgba(0, 0, 0, .1); }

/* Stat cards */
.ab-stat-icon { width: 2.75rem; height: 2.75rem; border-radius: .625rem; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; }

/* Report KPI tiles (.ab-kpi — Sales Report, property reports & dashboard). One design for every page:
   plain surface, light shadow in the tile colour (--kpi), round gradient icon with a white glyph, then
   label / big value / hint stacked beside it. Optional extras a page may add inside the card:
     .ab-kpi-link  — round chevron in the top-right corner (opens the related page)
     .ab-kpi-viz   — small decorative chart on the right (sparkline, bars, donut, faint icon); card gets .has-viz
   Higher specificity than the pages' own .ab-kpi rules; !important only over inline icon colours / Bootstrap
   utilities. Print / PDF get a flat version in report-export.js (no tint, no extras). */
.card.ab-kpi {
  position: relative; overflow: hidden; container-type: inline-size;
  border: 1px solid var(--tblr-border-color-translucent, rgba(4, 32, 69, .1));
  border-radius: 1rem;
  background: var(--tblr-bg-surface);
  box-shadow: 0 2px 8px -6px color-mix(in srgb, var(--kpi) 30%, transparent);
  transition: box-shadow .15s ease, transform .15s ease;
}
.card.ab-kpi:hover { box-shadow: 0 4px 12px -8px color-mix(in srgb, var(--kpi) 40%, transparent); }
.card.ab-kpi > .card-body { position: relative; z-index: 1; padding: 1.15rem 1.25rem; }
.card.ab-kpi > .card-body > .d-flex { flex-direction: row; align-items: center !important; gap: 1rem !important; }
.card.ab-kpi .ab-kpi-icon {
  width: 3.25rem; height: 3.25rem; border-radius: 50%; font-size: 1.5rem; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  color: #fff !important;
  background: linear-gradient(140deg, color-mix(in srgb, var(--kpi) 62%, #fff) 0%, var(--kpi) 100%) !important;
  box-shadow: 0 3px 8px -5px var(--kpi);
}
.card.ab-kpi .ab-kpi-icon.ab-kpi-icon-img { background: #fff !important; overflow: hidden; padding: 0; box-shadow: 0 3px 8px -5px rgba(15, 23, 42, .35); }
.card.ab-kpi .ab-kpi-icon.ab-kpi-icon-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card.ab-kpi .flex-fill { display: flex; flex-direction: column; min-width: 0; }
.card.ab-kpi .ab-kpi-label { font-size: .875rem; font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--tblr-secondary); }
.card.ab-kpi .ab-kpi-value { font-size: 1.75rem; font-weight: 700; line-height: 1.15; letter-spacing: -.02em; white-space: nowrap; margin: .1rem 0 .2rem; }
/* hint may wrap to a second line (cards in a row keep equal height) rather than being cut off */
.card.ab-kpi .ab-kpi-hint { font-size: .8125rem; line-height: 1.35; color: var(--tblr-secondary); display: block; white-space: normal; }
.card.ab-kpi .ab-kpi-hint > .ti { font-size: 1rem; opacity: .75; margin-right: .35rem; vertical-align: -.15em; }
.card.ab-kpi .ab-kpi-hint:empty { display: none; }
.card.ab-kpi.has-viz .flex-fill { padding-right: 7.25rem; }
.ab-kpi-trend { font-size: .8125rem; font-weight: 700; margin-left: .5rem; letter-spacing: 0; vertical-align: middle; white-space: nowrap; }
.ab-kpi-link {
  position: absolute; top: .8rem; right: .8rem; z-index: 2; width: 1.75rem; height: 1.75rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; font-size: 1rem;
  color: var(--kpi); background: color-mix(in srgb, var(--kpi) 12%, transparent);
}
.ab-kpi-link:hover { color: var(--kpi); background: color-mix(in srgb, var(--kpi) 22%, transparent); text-decoration: none; }
/* chart area starts below the chevron (.8rem + 1.75rem + a .6rem gap) and the chart scales down to fit it */
.ab-kpi-viz { position: absolute; right: 1.1rem; top: 3.15rem; bottom: .9rem; z-index: 0; pointer-events: none; color: var(--kpi); line-height: 0; display: flex; align-items: flex-end; }
.ab-kpi-viz svg { display: block; height: 100%; width: auto; max-height: 60px; }
.ab-kpi-viz .ab-kpi-viz-icon { font-size: 3.5rem; line-height: 1; opacity: .14; }
.ab-kpi-viz .ab-kpi-viz-tile { width: 3rem; height: 3rem; border-radius: .9rem; display: flex; align-items: center; justify-content: center; font-size: 1.6rem; background: color-mix(in srgb, var(--kpi) 14%, transparent); opacity: .9; }
/* not enough room for the extras: drop the chart first, then stack the icon above the text */
@container (max-width: 380px) {
  .card.ab-kpi .ab-kpi-viz { display: none; }
  .card.ab-kpi.has-viz .flex-fill { padding-right: 0; }
}
@container (max-width: 240px) {
  .card.ab-kpi > .card-body > .d-flex { flex-direction: column; align-items: flex-start !important; gap: .6rem !important; }
  .card.ab-kpi .ab-kpi-icon { width: 2.6rem; height: 2.6rem; font-size: 1.25rem; }
  .card.ab-kpi .ab-kpi-value { font-size: 1.5rem; }
  .card.ab-kpi .ab-kpi-hint { white-space: normal; overflow: visible; align-items: flex-start; }
}

/* Calendar */
.ab-calendar-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 1px; background: var(--tblr-border-color); border: 1px solid var(--tblr-border-color); border-radius: var(--tblr-border-radius); overflow: hidden; }
.ab-calendar-head { background: var(--tblr-bg-surface-secondary); padding: .5rem; text-align: center; font-size: .75rem; font-weight: 600; text-transform: uppercase; color: var(--tblr-secondary); }
.ab-calendar-day { background: var(--tblr-bg-surface); min-height: 6.5rem; padding: .4rem; cursor: pointer; display: flex; flex-direction: column; gap: .25rem; }
.ab-calendar-day:hover { background: var(--tblr-bg-surface-secondary); }
.ab-calendar-day.is-other-month { opacity: .4; }
.ab-calendar-day.is-today .ab-calendar-day-num { background: var(--tblr-primary); color: #fff; border-radius: 50%; width: 1.5rem; height: 1.5rem; display: inline-flex; align-items: center; justify-content: center; }
.ab-calendar-day-num { font-size: .8rem; font-weight: 600; }
.ab-calendar-pill { font-size: .7rem; padding: .05rem .4rem; border-radius: .25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Check-in step indicator — connected circle stepper */
.ab-steps { display: flex; align-items: flex-start; padding: .25rem 0 0; }
.ab-step {
  position: relative; flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
  text-align: center;
}
.ab-step:not(:last-child)::after {
  content: ''; position: absolute; z-index: 0;
  top: 1.1rem; left: 50%; width: 100%; height: 2px;
  background: var(--tblr-border-color);
  transition: background-color .3s ease;
}
.ab-step-circle {
  position: relative; z-index: 1; flex: 0 0 auto;
  width: 2.2rem; height: 2.2rem; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  font-size: .95rem; font-weight: 600;
  background: var(--tblr-bg-surface); border: 2px solid var(--tblr-border-color); color: var(--tblr-secondary);
  transition: background-color .3s ease, border-color .3s ease, color .3s ease, box-shadow .3s ease;
}
.ab-step-label { font-size: .78rem; font-weight: 500; color: var(--tblr-secondary); line-height: 1.2; padding: 0 .25rem; transition: color .3s ease, opacity .3s ease; }

.ab-step.is-active .ab-step-circle { border-color: var(--tblr-primary); background: var(--tblr-primary); color: #fff; box-shadow: 0 0 0 4px var(--tblr-primary-lt); }
.ab-step.is-active .ab-step-circle i { animation: ab-step-spin 1s linear infinite; }
.ab-step.is-active .ab-step-label { color: var(--tblr-primary); font-weight: 700; }

.ab-step.is-done .ab-step-circle { border-color: var(--tblr-success); background: var(--tblr-success); color: #fff; }
.ab-step.is-done .ab-step-label { color: var(--tblr-body-color); }
.ab-step.is-done:not(:last-child)::after { background: var(--tblr-success); }

.ab-step.is-failed .ab-step-circle { border-color: var(--tblr-danger); background: var(--tblr-danger); color: #fff; box-shadow: 0 0 0 4px var(--tblr-danger-lt); }
.ab-step.is-failed .ab-step-label { color: var(--tblr-danger); font-weight: 700; }

.ab-step.is-skipped .ab-step-circle { opacity: .45; }
.ab-step.is-skipped .ab-step-label { opacity: .45; }

@keyframes ab-step-spin { to { transform: rotate(360deg); } }

/* Placeholder / coming-soon pages */
.ab-placeholder-icon { font-size: 3rem; opacity: .35; }

/* Group cards */
.ab-group-card { cursor: pointer; transition: box-shadow .15s ease, transform .15s ease; }
.ab-group-card:hover { box-shadow: var(--tblr-box-shadow); transform: translateY(-2px); }

/* Product photo gallery — square tiles with a round remove badge pinned to
   the corner, plus a dashed "add" tile (a <label> wrapping the real, hidden
   file input, so clicking anywhere on the tile opens the file picker). */
.ab-gallery-item, .ab-gallery-add {
  width: 5.5rem; height: 5.5rem; border-radius: .5rem; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
}
.ab-gallery-item {
  position: relative;
  background-color: var(--tblr-bg-surface-secondary); background-size: cover; background-position: center;
  border: 1px solid var(--tblr-border-color);
}
.ab-gallery-remove {
  position: absolute; top: -.4rem; right: -.4rem; width: 1.35rem; height: 1.35rem; padding: 0;
  border: 2px solid var(--tblr-bg-surface); border-radius: 999px;
  background: var(--tblr-danger); color: #fff; font-size: .65rem;
  display: inline-flex; align-items: center; justify-content: center;
}
.ab-gallery-add {
  border: 1px dashed var(--tblr-border-color);
  background: var(--tblr-bg-surface-secondary); color: var(--tblr-secondary); font-size: 1.25rem;
  cursor: pointer; transition: background-color .15s ease, color .15s ease;
}
.ab-gallery-add:hover { background: var(--tblr-border-color); color: var(--tblr-body-color); }

/* Nested tree table (Category > Subcategory > Variant, all on one page) —
   plain table rows toggled via Bootstrap's .collapse rather than nested
   <table>s inside <td>s, so one <tbody> holds every level and indentation
   alone conveys depth. */
.ab-tree-toggle {
  background: transparent; border: none; padding: 0; width: 1.5rem; height: 1.5rem;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--tblr-secondary); cursor: pointer; flex: 0 0 auto;
}
.ab-tree-toggle i { transition: transform .15s ease; }
.ab-tree-toggle[aria-expanded="true"] i { transform: rotate(90deg); }
.ab-tree-toggle-spacer { display: inline-block; width: 1.5rem; flex: 0 0 auto; }
tr.ab-tree-row-sub > td:first-child { padding-left: 1.75rem; }
tr.ab-tree-row-variant > td:first-child { padding-left: 3.5rem; }
tr.ab-tree-row-sub, tr.ab-tree-row-variant { background: var(--tblr-bg-surface-secondary); }
/* Bootstrap's collapse sets display:block on .show, which breaks a <tr>
   (needs table-row, not block, or it stops forming table cells) — restore
   the correct display for row-level collapses specifically. Cover the
   mid-transition .collapsing state too (defensive — see mock-render.js's
   show/hide.bs.collapse listeners for the actual smooth-fade behavior,
   since animating `height` on a <tr> isn't reliable across browsers). */
tr.collapsing, tr.collapse.show { display: table-row; }

/* Tabbed modal forms (Add Product, Add Subcategory, ...) and any other
   form-wrapped scrollable modal (Add User, Add Role, ...) — add
   .ab-modal-tabbed/.ab-modal-scroll to .modal-content so its header/footer
   stay pinned and only the body scrolls. The modal's real direct children
   are .modal-header/.modal-body/.modal-footer, but a <form> wraps all three,
   one level between them and .modal-content. Making .modal-content itself a
   flex column (an earlier attempt) only flex-ed the form as a single opaque
   item — it sat at its natural content height with dead space left below it,
   and .modal-body's own flex/overflow rules did nothing because ITS parent
   (the form) was never a flex container. Put the flex column on the <form>
   instead, so header/body/footer are the actual flex items: header/footer
   keep their natural size, modal-body is the one flexible item that grows to
   fill the rest and scrolls internally. Bootstrap's own .modal-dialog-scrollable
   CSS assumes no such wrapper and silently does nothing once one exists —
   this is why Add User/Add Role (long permission-matrix bodies, no tabs)
   need the same fix under its own, non-tab-implying class name. */
.ab-modal-tabbed, .ab-modal-scroll { height: 85vh; overflow: hidden; }
.ab-modal-tabbed > form, .ab-modal-scroll > form { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.ab-modal-tabbed .modal-header, .ab-modal-tabbed .modal-footer,
.ab-modal-scroll .modal-header, .ab-modal-scroll .modal-footer { flex-shrink: 0; }
.ab-modal-tabbed .modal-body, .ab-modal-scroll .modal-body { flex: 1 1 auto; overflow-y: auto; min-height: 0; }

/* User Admin — permission matrix / role color picker (assets/js/user-admin.js's
   buildMatrixHtml()/buildFocusedModuleHtml()/renderColorPicker()). Ported
   from merchant/assets/css/style.css, translated to Tabler CSS variables.
   Shared here (not a per-page <style> block) because both admin/users.html
   (User Edit — Cashier's focused permission card) and admin/roles.html
   (Add/Edit Role — full matrix + focused card + color picker) render the
   exact same markup via the same shared renderMatrix()/renderColorPicker()
   — a copy living in only one page's <head> silently renders unstyled on
   the other the moment that page hits the same code path (see: Edit User
   on a Cashier-role account renders the focused card via users.html, not
   roles.html). */
.perm-group-header { background: var(--tblr-bg-surface-secondary) !important; font-size: .68rem; font-weight: 700; color: var(--tblr-secondary); letter-spacing: .07em; text-transform: uppercase; padding: 7px 10px 5px !important; }
/* Station permission panel (2026-10-07) — grouped switches under the Permissions grid, laid out like the
   merchant app's Station → Permission screen (user-admin.js buildStationPanelHtml). */
.perm-panel { border: 1px solid var(--tblr-border-color); border-radius: .5rem; overflow: hidden; }
.perm-panel-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .5rem; padding: .6rem .75rem; }
.perm-panel-title { font-weight: 600; font-size: .85rem; }
.perm-panel-group-title { background: var(--tblr-bg-surface-secondary); border-top: 1px solid var(--tblr-border-color); font-size: .68rem; font-weight: 700; color: var(--tblr-secondary); letter-spacing: .07em; text-transform: uppercase; padding: 7px .75rem 5px; }
.perm-panel-flags { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .5rem 1rem; padding: .7rem .75rem; }
@media (max-width: 767.98px) { .perm-panel-flags { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.perm-panel-flag { margin: 0; cursor: pointer; min-width: 0; }
.perm-panel-flag .form-check-label { font-size: .85rem; line-height: 1.3; }
.perm-focused-card { border: 1px solid var(--tblr-border-color); border-radius: var(--tblr-border-radius); overflow: hidden; }
.perm-focused-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; }
.perm-focused-row + .perm-focused-row { border-top: 1px solid var(--tblr-border-color); }
.perm-focused-icon { width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: var(--tblr-bg-surface-secondary); color: var(--tblr-secondary); font-size: .82rem; }
.perm-focused-body { flex: 1; min-width: 0; }
.perm-focused-label { font-size: .85rem; font-weight: 600; }
.perm-focused-desc { font-size: .74rem; color: var(--tblr-secondary); margin-top: 1px; }
.perm-advanced-toggle { margin-top: 12px; }
.perm-advanced-toggle summary { font-size: .78rem; font-weight: 600; color: var(--tblr-secondary); cursor: pointer; list-style: none; user-select: none; }
.perm-advanced-toggle summary::-webkit-details-marker { display: none; }
.perm-advanced-toggle summary::before { content: '\203a'; display: inline-block; width: 10px; margin-right: 4px; transition: transform .15s; }
.perm-advanced-toggle[open] summary::before { transform: rotate(90deg); }
.perm-advanced-toggle .table-responsive { margin-top: 10px; }
.role-color-picker { display: flex; gap: 8px; flex-wrap: wrap; padding-top: 6px; }
.role-color-swatch { width: 22px; height: 22px; flex: 0 0 auto; border-radius: 50%; cursor: pointer; border: 2px solid transparent; display: inline-block; }
.role-color-swatch.selected { border-color: var(--tblr-body-color); box-shadow: 0 0 0 2px var(--tblr-bg-surface) inset; }

/* Person avatar with initials fallback */
.ab-avatar-initials { display: inline-flex; align-items: center; justify-content: center; background: var(--tblr-primary-lt); color: var(--tblr-primary); font-weight: 600; }

@media print {
  .navbar-vertical, .navbar.sticky-top, .footer { display: none !important; }
  .page-wrapper { margin: 0 !important; }
}

/* Page-load entrance. Plain `animation` (not `transition`) so it can't be
   clobbered by, or interfere with, the sidebar-collapse `margin-left`
   transition on the same element — and because there's no prior state to
   transition FROM on a fresh page load anyway. No fill-mode: once it
   finishes it reverts to the normal cascade (opacity:1, transform:none),
   so it never leaves a lingering transform behind. */
@keyframes ab-page-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}
.page-wrapper { animation: ab-page-in .4s ease-out; }

@media (prefers-reduced-motion: reduce) {
  .page-wrapper { animation: none; }
}

/* Tom Select's bootstrap5-themed stylesheet (tom-select.bootstrap5.min.css,
   loaded from the CDN as-is) is written against PLAIN Bootstrap 5's own
   --bs-* custom properties — background, text color, border color/radius/
   width, box-shadow, form-invalid/valid colors, hover/active backgrounds,
   all of it. This app is Tabler, not plain Bootstrap: Tabler defines its
   OWN equivalent set under a --tblr-* prefix and never defines --bs-* at
   all (confirmed against the actual Tabler 1.4.0 CSS — grepping the whole
   file for "--bs-body-bg:" or any other "--bs-*" definition finds nothing).
   Every var(--bs-*) in Tom Select's theme therefore silently resolved to
   nothing — CSS's "guaranteed-invalid value" behavior, which for
   `background` falls back to its initial value (transparent), not an
   error — so the dropdown panel rendered fully see-through, with whatever
   page content sat behind it (table rows, in the reported screenshot)
   bleeding straight through the option text (reported 2026-09-11).
   Aliased here rather than patched inside .ts-dropdown/.ts-wrapper
   specifically — nothing else in this Tabler app ever reads a --bs-*
   variable, so defining these globally on :root is zero-risk and also
   covers any other Bootstrap-5-theme-authored CSS added later, not just
   Tom Select. Each alias points at the Tabler variable actually used for
   that ROLE (not always the same-looking name) — --bs-body-bg in
   particular maps to --tblr-bg-surface (an elevated white/dark PANEL
   background), not --tblr-body-bg (the page's own background, a shade
   different) — matching what Tabler's own native .dropdown-menu/.card/
   .modal-content already use for this exact "floating surface" role, so
   Tom Select's popup reads as consistent with the rest of the app instead
   of blending into the page behind it. All already theme-aware (light/
   dark via [data-bs-theme]) for free, since these just forward to
   variables Tabler itself already switches per theme. */
:root {
  --bs-body-bg: var(--tblr-bg-surface);
  --bs-body-color: var(--tblr-body-color);
  --bs-border-color: var(--tblr-border-color);
  --bs-border-color-translucent: var(--tblr-border-color-translucent);
  --bs-border-radius: var(--tblr-border-radius);
  --bs-border-radius-lg: var(--tblr-border-radius-lg);
  --bs-border-radius-sm: var(--tblr-border-radius-sm);
  --bs-border-width: var(--tblr-border-width);
  --bs-box-shadow-inset: var(--tblr-box-shadow-inset);
  --bs-form-invalid-color: var(--tblr-form-invalid-color);
  --bs-form-valid-color: var(--tblr-form-valid-color);
  --bs-secondary-bg: var(--tblr-secondary-bg);
  --bs-tertiary-bg: var(--tblr-tertiary-bg);
}

/* Tom Select (searchable <select> pickers, 2026-09-11) deliberately does
   NOT use dropdownParent: 'body' (see makeSearchable()'s own comment in
   user-admin.js for why that broke the dropdown_input plugin inside a
   Bootstrap modal) — the dropdown stays nested in its own wrapper, so it
   should already stack above modal siblings without help. This override
   is just cheap insurance in case some other element in a modal ever picks
   its own z-index and starts covering it — 1075 clears Bootstrap 5.3's own
   .modal (1055) and .modal-backdrop (1050) with room to spare, in case a
   descendant-stacking-context edge case (a modal-body ancestor sets one of
   the context-creating properties: transform/filter/opacity<1/etc.) ever
   pulls the dropdown's effective stacking below a modal-level sibling. */
.ts-dropdown { z-index: 1075; }

/* Height parity with sibling native .form-control/.form-select — Tom
   Select's own bootstrap5-theme .ts-control padding/line-height is sized
   for plain Bootstrap 5 (~35px tall wrapper), while Tabler's own
   .form-control/.form-select measures 40px (9px vertical padding + 14px/
   20px-line-height text + 1px border top/bottom) — visibly shorter than a
   plain filter <select> sharing the same row (reported 2026-09-11,
   screenshot showed the two controls' bottom borders misaligned). Matched
   by explicit measurement (Playwright getComputedStyle on a real
   .form-control vs. the real .ts-control in this app), not by guessing —
   see [[platform-real-user-admin]] for the exact before/after numbers.
   !important on padding/font-size is required: the bootstrap5 theme's own
   `.ts-wrapper.form-select .ts-control`/`.ts-wrapper.single .ts-control`
   rules are more specific than a plain `.ts-wrapper .ts-control` override
   and win outright otherwise, regardless of stylesheet load order. */
.ts-wrapper { min-height: 40px; }
.ts-wrapper .ts-control {
  min-height: unset;
  padding: 9px 1rem !important;
  font-size: 14px !important;
  line-height: 20px;
}

/* The dropdown_input plugin's own search box (a real <input class=
   "dropdown-input"> it creates inside the open panel) ships as a bare
   bottom-border-only "underline" field with a hardcoded #d0d0d0 gray,
   ignoring both this app's rounded-input visual language and its dark-mode
   theming entirely. Restyled to read as a normal, full-bordered Tabler
   input with the same border-radius/background/focus-ring the rest of the
   app already uses (values taken directly from Tabler's own .form-control/
   .form-control:focus rules — same blue focus ring, same shadow — rather
   than inventing new ones), reported 2026-09-11 ("make it rounded or other
   else"). Margined in from the panel's own edges (not edge-to-edge) so it
   reads as a distinct field sitting inside the panel, not a panel-wide bar. */
.ts-dropdown .dropdown-input {
  background: var(--tblr-bg-forms);
  color: var(--tblr-body-color);
  border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius);
  box-shadow: var(--tblr-shadow-input);
  margin: .5rem;
  width: calc(100% - 1rem);
  padding: .5rem .75rem;
  font-size: 14px;
}
.ts-dropdown .dropdown-input:focus {
  border-color: rgb(130.5, 183, 232);
  box-shadow: var(--tblr-shadow-input), 0 0 0 .25rem rgba(var(--tblr-primary-rgb), .25);
  outline: 0;
}

/* Property Management — master-detail split pane (Property, Rental, Billing,
   Tenants, Work Orders). Left pane is a scrollable pick-list, right pane is
   the selected record's workspace; both live inside one .card so the whole
   thing reads as a single unit, not two separate cards stacked oddly. */
.ab-split-pane { display: flex; height: calc(100vh - 15rem); min-height: 26rem; }
.ab-split-pane-list {
  flex: 0 0 21rem; max-width: 21rem; overflow-y: auto;
  border-right: 1px solid var(--tblr-border-color);
  padding: .75rem;
}
@media (max-width: 767.98px) { .ab-split-pane { flex-direction: column; height: auto; } .ab-split-pane-list { flex: 0 0 auto; max-width: none; max-height: 20rem; border-right: none; border-bottom: 1px solid var(--tblr-border-color); } }
.ab-split-pane-item {
  padding: .625rem .75rem; border-radius: .625rem; cursor: pointer;
  border: 1px solid transparent; margin-bottom: .375rem;
  transition: background-color .15s ease, border-color .15s ease;
}
.ab-split-pane-item:hover { background: var(--tblr-bg-surface-secondary); }
.ab-split-pane-item.active { background: var(--tblr-primary-lt); border-color: var(--tblr-primary); }
.ab-split-pane-detail { flex: 1 1 auto; min-width: 0; overflow-y: auto; padding: 1.25rem; }
.ab-split-pane-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--tblr-secondary); text-align: center; padding: 2rem; }

/* Flag-based phone country-code picker (customer/customer.html) — a plain
   Bootstrap dropdown rather than the real merchant app's hand-rolled
   position:fixed panel (customer.js's toggleCountryPicker/closeCountryPicker):
   Popper already solves the "inside a transformed modal" positioning problem
   that custom panel existed for, so the simpler native component is enough
   here. Same --tblr-* theme tokens as .ab-split-pane-item above, so this
   stays correct in dark mode with no separate dark-mode rule needed. */
.ab-country-item { cursor: pointer; transition: background-color .12s ease; }
.ab-country-item:hover { background: var(--tblr-bg-surface-secondary); }
.ab-country-item.active { background: var(--tblr-primary-lt); }

/* The picker's own toggle button — transparent (not a filled
   .btn-outline-secondary), so it blends into whatever surface it sits on
   (modal, card, ...) as one segment of the input group instead of reading
   as a separate button on top of it. */
.ab-phone-country-btn { background: transparent; border-color: var(--tblr-border-color); color: var(--tblr-body-color); }
.ab-phone-country-btn:hover, .ab-phone-country-btn:focus { background: var(--tblr-bg-surface-secondary); border-color: var(--tblr-border-color); color: var(--tblr-body-color); }

/* Small "clear" (x) button overlaid on a filter <select> — see
   UA.addFilterClear()'s own comment in user-admin.js for the full
   convention (wrap the <select> in this div, call that helper). Positioned
   to clear both a native select's own dropdown arrow and Tom Select's own
   caret once makeSearchable() has run on it — .ts-wrapper is what Tom
   Select renders inside this div, so the extra right padding has to target
   its own .ts-control too, not just the (then-hidden) original <select>. */
.ab-filter-clearable { position: relative; }
.ab-filter-clearable > select.form-select { padding-right: 3.25rem; }
.ab-filter-clearable .ts-wrapper .ts-control { padding-right: 3.25rem; }
.ab-filter-clear {
  position: absolute; top: 50%; right: 2.1rem; transform: translateY(-50%);
  width: 1.25rem; height: 1.25rem; display: flex; align-items: center; justify-content: center;
  border: 0; background: transparent; border-radius: 999px; color: var(--tblr-secondary);
  cursor: pointer; z-index: 5; padding: 0; line-height: 1;
}
.ab-filter-clear:hover { background: var(--tblr-bg-surface-secondary); color: var(--tblr-body-color); }
.ab-filter-clear i { font-size: .85rem; }

/* Decorative status-filter chips (search/filter inputs across the Property
   pages are non-functional by design — see products.html's own search/
   status <select> precedent — but chips still need a visual toggle state). */
.ab-chip-filter {
  padding: .1875rem .75rem; border-radius: 999px; font-size: .75rem; font-weight: 600;
  border: 1px solid var(--tblr-border-color); background: var(--tblr-bg-surface); color: var(--tblr-secondary);
  cursor: pointer; white-space: nowrap; transition: all .15s ease; display: inline-flex; align-items: center; gap: .3rem;
}
.ab-chip-filter:hover { border-color: var(--tblr-primary); color: var(--tblr-primary); }
.ab-chip-filter.active { background: var(--tblr-primary); border-color: var(--tblr-primary); color: #fff; }

/* Report pages — plain horizontal breakdown bars (occupancy by property,
   work orders by technician, etc.) — a lightweight alternative to a chart
   for a simple per-row proportion. */
.ab-report-bar-row { margin-bottom: .625rem; }
.ab-report-bar-row:last-child { margin-bottom: 0; }
.ab-report-bar-label { display: flex; justify-content: space-between; font-size: .8125rem; margin-bottom: .25rem; }
.ab-report-bar-track { height: .5rem; border-radius: 999px; background: var(--tblr-bg-surface-secondary); overflow: hidden; }
.ab-report-bar-fill { height: 100%; border-radius: 999px; background: var(--tblr-primary); }

/* Clamps a text block to exactly 2 lines with an ellipsis, regardless of
   how long the underlying string is — used by property-general-option.html's
   per-card descriptions so a long one never pushes the Save button further
   down than a short one (every card in a row otherwise wants to match its
   tallest sibling). */
.ab-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* admin/shops.html's Module Access modal (2026-09-16) — a group +
   sub-menu checkbox tree mirroring the whole sidebar (see user-admin.js's
   own MENU_TREE). Each group is its own bordered card so a long list of
   ~11 groups still reads as distinct, scannable sections rather than one
   long undifferentiated checkbox wall.

   .ab-ma-row is a plain flex row, deliberately NOT Bootstrap's own
   `.form-check` — that class positions its checkbox via
   `padding-left: 1.5em` on the label canceled by `margin-left: -1.5em` on
   the input; the moment a row also needs its OWN padding (for the card
   look), overriding that padding broke the cancellation and the checkbox
   rendered outside the card's left border. A flex row with gap + a
   normal-margin checkbox has no such coupling — `.form-check-input`
   stays on the actual `<input>` only for its Tabler-themed visual style
   (border/radius/checked color), never for layout. */
.ab-ma-tree { display: flex; flex-direction: column; gap: .75rem; }
.ab-ma-row { display: flex; align-items: center; gap: .625rem; margin: 0; cursor: pointer; }
.ab-ma-row input.form-check-input { margin: 0; flex-shrink: 0; cursor: pointer; }
.ab-ma-row-label { display: flex; align-items: center; line-height: 1.3; }
.ab-ma-row-label i { margin-right: .5rem; color: var(--tblr-secondary); font-size: 1.05em; }
.ab-ma-standalone {
  padding: .625rem .875rem;
  border: 1px solid var(--tblr-border-color); border-radius: var(--tblr-border-radius);
  background: var(--tblr-bg-surface); transition: border-color .12s ease, background .12s ease;
}
.ab-ma-standalone:hover { border-color: var(--tblr-primary); background: var(--tblr-bg-surface-secondary); }
.ab-ma-group { border: 1px solid var(--tblr-border-color); border-radius: var(--tblr-border-radius); overflow: hidden; }
.ab-ma-group-header { padding: .625rem .875rem; background: var(--tblr-bg-surface-secondary); border-bottom: 1px solid var(--tblr-border-color); }
.ab-ma-group-toggle .ab-ma-row-label { font-weight: 600; }
.ab-ma-group-toggle .ab-ma-row-label i { color: var(--tblr-primary); }
.ab-ma-group-items { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .125rem .75rem; padding: .5rem .875rem; }
.ab-ma-item { padding: .3125rem .5rem; border-radius: .375rem; transition: background .12s ease; }
.ab-ma-item:hover { background: var(--tblr-bg-surface-secondary); }
@media (max-width: 575.98px) {
  .ab-ma-group-items { grid-template-columns: 1fr; }
}

/* Reminders bell: round refresh control in the dropdown header (spins while it re-reads) */
.ab-rem-refresh { width: 1.9rem; height: 1.9rem; flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: 1px solid var(--tblr-border-color); border-radius: 50%; background: transparent; color: var(--tblr-secondary); font-size: 1rem; cursor: pointer; transition: background .15s, color .15s, border-color .15s; }
.ab-rem-refresh:hover { background: var(--tblr-bg-surface-secondary); color: var(--tblr-primary); border-color: var(--tblr-primary); }
.ab-rem-refresh:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 1px; }
.ab-rem-refresh:disabled { cursor: default; opacity: .8; }
.ab-rem-refresh.is-spinning i { animation: ab-rem-spin .7s linear infinite; }
@keyframes ab-rem-spin { to { transform: rotate(360deg); } }
/* Navbar ⟳ Refresh (partials.js wirePageRefresh) — the icon spins while the page's data reloads */
#ab-page-refresh:disabled { cursor: default; opacity: .8; }
#ab-page-refresh.is-spinning .ti { display: inline-block; animation: ab-rem-spin .7s linear infinite; }

/* Activity filter pills (customer View modal): All 48 · Orders 14 · … */
.ab-act-tabs { display: flex; gap: 6px; flex-wrap: wrap; padding-bottom: 10px; margin-bottom: 6px; border-bottom: 1px solid var(--tblr-border-color); }
.ab-act-tab { border: 1px solid var(--tblr-border-color); background: var(--tblr-bg-surface); border-radius: 999px; padding: 4px 14px; font-size: .8rem; font-weight: 600; color: var(--tblr-secondary); cursor: pointer; transition: background .12s ease, color .12s ease, border-color .12s ease; }
.ab-act-tab:hover { background: var(--tblr-bg-surface-secondary); }
.ab-act-tab.active { background: rgba(var(--tblr-primary-rgb), .12); border-color: transparent; color: var(--tblr-primary); }
.ab-act-tab-count { margin-left: 6px; background: var(--tblr-bg-surface-secondary); color: var(--tblr-secondary); border-radius: 999px; padding: 1px 7px; font-size: .68rem; font-weight: 700; }
.ab-act-tab.active .ab-act-tab-count { background: var(--tblr-bg-surface); color: var(--tblr-primary); }

/* Row hover on every table (2026-10-19) — Tabler's own .table-hover effect, switched on
   platform-wide instead of per table: a light tint on the row under the mouse, so long
   rows are easy to follow across to their Actions. Same variables as .table-hover, so it
   follows light/dark theme and never overrides a row's own state (e.g. .table-active,
   .table-danger). Skipped: devices without a mouse, empty-state rows (one cell spanning
   the table) and any table marked .ab-no-hover. */
@media (hover: hover) {
  .table:not(.ab-no-hover) > tbody > tr:not([class*="table-"]):not(:has(> td:only-child)):hover > * {
    --tblr-table-bg-state: rgba(var(--tblr-emphasis-color-rgb), .04);
  }
  .table:not(.ab-no-hover) > tbody > tr > * { transition: box-shadow .12s ease; }
}
