/* =====================================================================
   NetBilling â€” design system
   ===================================================================== */

/* ---------- Tokens --------------------------------------------------- */
:root {
  --bg:            #f5f6fa;
  --surface:       #ffffff;
  --surface-2:     #fafbfd;
  --surface-3:     #f2f4f8;
  --border:        #e5e8f0;
  --border-strong: #d3d8e4;
  --text:          #101828;
  --text-2:        #475467;
  --muted:         #8792a8;

  --primary:       #4f46e5;
  --primary-600:   #4338ca;
  --primary-2:     #8b5cf6;
  --primary-3:     #06b6d4;
  --primary-soft:  #eef2ff;
  --primary-ring:  rgba(79, 70, 229, .18);

  --ok:            #059669;
  --ok-soft:       #e7f7f1;
  --warn:          #b45309;
  --warn-soft:     #fef4e6;
  --danger:        #dc2626;
  --danger-soft:   #fdeced;
  --info:          #0284c7;
  --info-soft:     #e6f4fd;

  --sidebar-bg:    #0f172a;
  --sidebar-text:  #94a3b8;
  --sidebar-active:#ffffff;
  --sidebar-hover: rgba(255,255,255,.06);

  /* Tight and mostly-flat: elevation comes from a 1px border first, shadow
     second, so surfaces separate without the soft "floating template" glow a
     wide blur radius reads as. */
  --shadow-xs: 0 1px 2px rgba(16,24,40,.04);
  --shadow-sm: 0 1px 2px rgba(16,24,40,.06), 0 1px 1px rgba(16,24,40,.03);
  --shadow-md: 0 4px 12px rgba(16,24,40,.07), 0 1px 3px rgba(16,24,40,.04);
  --shadow-lg: 0 12px 28px rgba(16,24,40,.10), 0 3px 8px rgba(16,24,40,.05);
  --glow-primary: 0 6px 16px -4px rgba(79,70,229,.32);

  /* Restrained across the board: --r-sm is the small-control baseline
     (inputs, small buttons), --r/--r-lg/--r-xl scale up for cards, panels
     and modals - noticeably tighter than a rounded consumer-app look. */
  --r-sm:  8px;
  --r:     10px;
  --r-lg:  14px;
  --r-xl:  18px;
  --r-2xl: 22px;
  --r-full: 999px;

  --ease: cubic-bezier(.4, 0, .2, 1);
  --ease-spring: cubic-bezier(.2, .9, .3, 1.15);

  --sidebar-w: 258px;
  --topbar-h:  64px;

  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace;
}

[data-bs-theme="dark"] {
  --bg:            #0a0f1d;
  --surface:       #111827;
  --surface-2:     #161f30;
  --surface-3:     #1c2739;
  --border:        #232f45;
  --border-strong: #2f3d57;
  --text:          #e8ecf4;
  --text-2:        #a9b4c8;
  --muted:         #7b879f;

  --primary:       #6366f1;
  --primary-600:   #818cf8;
  --primary-2:     #a78bfa;
  --primary-3:     #22d3ee;
  --primary-soft:  rgba(99,102,241,.16);
  --primary-ring:  rgba(99,102,241,.28);

  --ok:            #34d399;
  --ok-soft:       rgba(52,211,153,.14);
  --warn:          #fbbf24;
  --warn-soft:     rgba(251,191,36,.14);
  --danger:        #f87171;
  --danger-soft:   rgba(248,113,113,.14);
  --info:          #38bdf8;
  --info-soft:     rgba(56,189,248,.14);

  --sidebar-bg:    #080d18;
  --sidebar-hover: rgba(255,255,255,.05);

  --shadow-xs: 0 1px 2px rgba(0,0,0,.24);
  --shadow-sm: 0 1px 2px rgba(0,0,0,.3);
  --shadow-md: 0 4px 12px rgba(0,0,0,.35);
  --shadow-lg: 0 12px 28px rgba(0,0,0,.45);
  --glow-primary: 0 6px 16px -4px rgba(99,102,241,.4);
}

/* Told up front so native, un-themed UI - scrollbars, the checkbox/date
   pickers browsers draw themselves, autofill - already renders in the right
   palette on first paint instead of flashing light before script/CSS
   settles it, which is exactly the kind of flicker this is about removing. */
:root { color-scheme: light; }
[data-bs-theme="dark"] { color-scheme: dark; }

/* Every transition and animation this file declares - buttons, cards lifting
   on hover, the field fades app.js drives, page-load and cross-page fades
   below - collapses to effectively instant here, once, rather than needing
   an exception on each rule. prefers-reduced-motion is an accessibility
   setting the user turned on deliberately; motion is not "smoother" to them,
   it is the thing they asked this site to stop doing. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- Reset ---------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ---------- Page-load motion -------------------------------------------
   @view-transition { navigation: auto } used to run here too, crossfading
   the whole page shell across a navigation. Removed: on a real navigation
   (full server round trip, not a same-document swap) the browser only has
   the *old* page's snapshot ready instantly - the *new* one's isn't
   captured until that page has actually loaded, and a slow request (a
   loaded MySQL query, a cold asset cache after a tab sat idle a while)
   stretched that gap enough to sometimes reveal a blank/unstyled frame
   before the crossfade had anything real to blend into. That failure mode
   lives in the browser's own transition timing, not in this app's CSS, so
   no amount of tuning the *content* here could close it. This plain fade-in
   has no such gap - it only ever plays once the new page's own content has
   already loaded, the same for every navigation regardless of how long the
   request took. */
@keyframes content-fade-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* ease-out, not the ease-in-out var(--ease) this used to carry: an
   ease-in-out spends its opening frames barely moving, so the new page read
   as sitting still for a moment before it appeared - a delay on top of the
   request the browser already made the user wait for. Leaving at full speed
   and decelerating into place makes the identical fade read as immediate.
   .24s rather than .32s, and 4px rather than 6px: still a fade rather than a
   cut, but short enough that switching menus never feels like waiting for an
   animation to end. There is deliberately no animation-delay anywhere here -
   the very first frame is already moving. */
.content,
.auth { animation: content-fade-in .24s cubic-bezier(0, 0, .2, 1) both; }

h1, h2, h3, h4, h5 { margin: 0; font-weight: 650; letter-spacing: -.015em; color: var(--text); }
h1 { font-size: 22px; }
h2 { font-size: 18px; }
h3 { font-size: 15px; }
p  { margin: 0 0 10px; }
a  { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-600); }
img { max-width: 100%; }
hr { border: 0; border-top: 1px solid var(--border); margin: 18px 0; }

::selection { background: var(--primary-ring); }

/* Scrollbars */
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 20px; border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: content-box; }

/* Bootstrap Icons is a glyph font (icon() emits <i class="bi bi-x">, the
   shape itself comes from a ::before content glyph) - sized via font-size,
   not width/height like the inline SVGs this replaced. inline-flex (not
   plain inline) matters because .ico always sits as a direct flex/grid item
   of its parent (e.g. .icon-btn) - including the theme toggle, whose two
   <i data-theme-icon> glyphs are siblings there rather than each wrapped in
   its own <span>. A plain inline element's baseline alignment would reserve
   a sliver of descender space below the glyph, nudging it a couple pixels
   off true center; sitting as a direct flex item sidesteps that. */
.ico { display: inline-flex; align-items: center; justify-content: center; font-size: 18px; line-height: 1; flex: none; }
.ico-sm { font-size: 15px; }
.ico-lg { font-size: 22px; }

/* =====================================================================
   Shell
   ===================================================================== */
.shell { display: flex; min-height: 100vh; }

/* ---------- Sidebar -------------------------------------------------- */
.sidebar {
  width: var(--sidebar-w);
  flex: none;
  background: var(--sidebar-bg);
  color: var(--sidebar-text);
  display: flex;
  flex-direction: column;
  position: fixed;
  inset: 0 auto 0 0;
  /* Above Bootstrap's own --bs-offcanvas-zindex (1045, see its
     .offcanvas-backdrop) on purpose: this rule's z-index wins over
     .offcanvas-lg's regardless of viewport width (app.css loads after
     bootstrap.min.css, so it wins every property they both set, media
     query or not) - so whatever value sits here has to already work for
     the mobile drawer case too, not just desktop. Below Bootstrap's own
     modal/toast z-indices (1055/1090) on purpose too, so either can still
     cover the sidebar as expected. */
  z-index: 1046;
  transition: transform .25s cubic-bezier(.4,0,.2,1);
}

/* Bootstrap's .offcanvas-lg sets `background-color: transparent !important`
   above its lg breakpoint (that variant assumes the panel dissolves into
   the page once it stops being a drawer). !important beats the plain
   `background` above no matter what order the stylesheets load in, so the
   sidebar was rendering transparent on desktop and showing --bg through
   itself. Dark mode hid the bug - --bg is near-black there, so a
   transparent sidebar still looked right - but in light mode --bg is
   near-white, and every colour in here is a light-on-dark value (the brand
   text, the menu labels, the muted user role, the demo card), so the whole
   panel went unreadable at once.
   This sidebar is deliberately always dark in both themes, the same way
   .auth__form is on the login screen, so the fix is to hold that
   background rather than to re-colour the text per theme - dark text would
   just move the same problem into dark mode. Two classes beats Bootstrap's
   one, which is what settles it when both sides are !important. */
.sidebar.offcanvas-lg { background-color: var(--sidebar-bg) !important; }

.sidebar__brand {
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 0 20px;
  flex: none;
  border-bottom: 1px solid rgba(255,255,255,.06);
}
.sidebar__logo {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-sm);
  background: linear-gradient(135deg, #6366f1, #8b5cf6 55%, #d946ef);
  display: grid; place-items: center;
  color: #fff;
  box-shadow: 0 3px 10px rgba(99,102,241,.35);
}
.sidebar__logo svg { width: 19px; height: 19px; }
.sidebar__name { font-size: 15px; font-weight: 700; color: #fff; letter-spacing: -.02em; line-height: 1.15; }
.sidebar__sub  { font-size: 10.5px; color: #64748b; letter-spacing: .09em; text-transform: uppercase; font-weight: 600; }

/* An uploaded logo is drawn at a fixed size here, always - the Pengaturan
   Umum slider only sizes the login page's copy of the same image now, so
   whatever is picked there can never stretch or wrap this fixed-height row.
   Redefining --logo-size is enough: .brand-logo below reads it for both its
   width and its proportional corner radius, same as everywhere else it's used. */
.sidebar__brand--custom { --logo-size: 40px; }

/* ---------- Uploaded logo --------------------------------------------
   Drawn on its own: no plate, no border, just the image with rounded corners.
   The radius follows --logo-size rather than being fixed, because the login
   page's slider spans 10px to 300px: one fixed value would round a small
   mark into a circle and leave a large banner looking square. The cap keeps
   a wide banner from turning into a pill. Shared by the sidebar (fixed at
   40px), the login screen (the slider's value) and the settings preview, so
   all three round the same way at whatever size they each draw it. */
.brand-logo {
  display: block; flex: none;
  width: var(--logo-size, 120px); max-width: 100%; height: auto;
  object-fit: contain;
  border-radius: clamp(3px, calc(var(--logo-size, 120px) * .16), 22px);
}

.sidebar__nav { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 14px 12px 20px; }
.sidebar__nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.12); background-clip: content-box; }
.sidebar__nav { scrollbar-color: rgba(255,255,255,.16) transparent; }

.nav-group { margin-bottom: 4px; }
.nav-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: #4d5b76; padding: 14px 10px 7px;
}
.nav-link {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 14px; margin-bottom: 2px;
  border-radius: var(--r-full);
  color: var(--sidebar-text);
  font-weight: 500; font-size: 13.5px;
  transition: background .2s var(--ease), color .2s var(--ease), box-shadow .2s var(--ease);
  position: relative;
}
/* Sub-rows backed by a form post (demo actions) render as a button; strip
   the UA styling so it matches the plain <a> rows around it. */
button.nav-link {
  width: 100%; border: 0; background: none; cursor: pointer;
  font-family: inherit; text-align: left;
}
/* :not(.is-active) on every hover rule, because the sub-row hover further
   down carries the same specificity as .nav-link.is-active and would win on
   source order alone - repainting an active pill's text mid-hover. */
.nav-link:not(.is-active):hover { background: rgba(255,255,255,.05); color: #e2e8f0; }
/* The active row is a solid pill, not a tinted background with a rail or
   dot beside it - one unambiguous "you are here" mark instead of several
   indicators layered on top of each other. */
.nav-link.is-active {
  background: var(--primary);
  color: #fff; font-weight: 700;
  box-shadow: 0 4px 10px rgba(0,0,0,.35);
}
.nav-link.is-active:hover { background: var(--primary-600); color: #fff; }
.nav-link .ico { opacity: .85; }
.nav-link.is-active .ico { opacity: 1; color: #fff; }
.nav-link__text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-link__count {
  font-size: 11px; font-weight: 700;
  background: rgba(255,255,255,.09); color: #cbd5e1;
  padding: 1px 7px; border-radius: var(--r-full); min-width: 22px; text-align: center;
}
.nav-link.is-active .nav-link__count { background: rgba(255,255,255,.18); color: #fff; }
.nav-link__count.is-alert { background: rgba(239,68,68,.2); color: #fca5a5; }

/* ---------- Sub menu ------------------------------------------------- */
.nav-chevron {
  margin-left: 2px; opacity: .5;
  /* Same .22s and same ease-out curve as .nav-sub's panel below, so the
     chevron and the panel it describes accelerate and settle together. */
  transition: transform .22s cubic-bezier(0, 0, .2, 1), opacity .15s var(--ease);
}
.nav-item.is-open > .nav-link--parent .nav-chevron { transform: rotate(90deg); opacity: .85; }
.nav-link--parent:hover .nav-chevron { opacity: .85; }

/* The parent of the current page stays highlighted while its list is open. */
.nav-item.is-current > .nav-link--parent { color: #dbe3f0; }
.nav-item.is-current > .nav-link--parent .ico { opacity: 1; color: #a5b4fc; }

/* Grid-template-rows (0fr <-> 1fr), not a JS-measured pixel height - this
   was bootstrap.Collapse for one turn (which animates a scrollHeight it
   reads via JS, then writes as an inline height style), rolled back after
   it was still asked to be smoother twice more with nothing left to tune:
   height is a layout property, so every frame forces the browser to
   recompute layout for the whole sidebar stack below it - a real ceiling on
   how smooth it can ever be, independent of easing or duration.
   grid-template-rows needs no measurement at all - the browser's own grid
   engine interpolates the row size and repaints, which is the actually
   smoother path, not just a re-tuned version of the same technique.
   Ancestor-keyed off .nav-item.is-open (matching .nav-chevron/.nav-sub__list
   below), not a class on .nav-sub itself, and deliberately not Bootstrap's
   "collapse"/"show" pair either: .collapse:not(.show){display:none} would
   stop this from ever transitioning (a display:none element can't animate),
   so sidebar.php doesn't render those two on this element. The server still
   renders .is-open (and .nav-sub__clip, the grid's one row/child, sizes
   with it) for whichever group holds the current page, so a page that
   hasn't finished loading app.js yet still renders it open, no JS required -
   same guarantee this always made. */
.nav-sub {
  display: grid;
  grid-template-rows: 0fr;
  /* ease-out, not the ease-in-out this used to be: the panel leaves at full
     speed and decelerates into place, which reads as responding instantly
     to the click. An ease-in-out spends its first frames barely moving,
     which is exactly the "sluggish to start" feel being tuned out here. */
  transition: grid-template-rows .22s cubic-bezier(0, 0, .2, 1);
}
.nav-item.is-open > .nav-sub { grid-template-rows: 1fr; }
.nav-sub__clip { overflow: hidden; min-height: 0; }
/* No connector rail or bullet: nesting is carried by the indent alone, so an
   active sub-row shows the same single pill the top level does.

   The fade is deliberately *asymmetric* - the two rules below carry
   different transitions, so opening and closing don't play the same curve
   backwards.

   In (the .is-open rule): no delay at all, and the exact same .22s and the
   exact same ease-out curve as .nav-sub's panel above, so the rows fade up
   as one movement with the panel carrying them instead of trailing behind
   it. The .05s delay that used to sit here was the whole reason opening read
   as hesitant - the panel was already moving while the rows still waited,
   and a pause before a fade is felt as lag no matter how small it is.
   Out (this rule, the resting/closed state): faster than the panel on
   purpose, so the rows have cleared before the height finishes collapsing -
   otherwise the last frames look like content being crushed rather than
   dismissed. That is a shorter duration, never a delay. */
.nav-sub__list {
  margin: 2px 0 6px 14px;
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity .12s cubic-bezier(.4, 0, 1, 1), transform .12s cubic-bezier(.4, 0, 1, 1);
}
.nav-item.is-open > .nav-sub .nav-sub__list {
  opacity: 1;
  transform: translateY(0);
  transition: opacity .22s cubic-bezier(0, 0, .2, 1), transform .22s cubic-bezier(0, 0, .2, 1);
}
.nav-sub__list form { margin: 0; }
.nav-link--sub {
  font-size: 13px; font-weight: 500;
  padding: 7px 14px; gap: 9px;
  color: #7d8ba4;
}
.nav-link--sub:not(.is-active):hover { color: #cbd5e1; }
/* Keyboard focus has to stay visible on the dark surface. */
.nav-link:focus-visible {
  outline: 2px solid rgba(165,180,252,.75); outline-offset: -2px;
}

/* Suppress the open/close animation while the initial state is applied.
   .nav-sub is back in this list now that its transition is unconditional
   again (grid-template-rows above, not gated behind a transitional class
   the way Bootstrap's .collapsing briefly was) - same reasoning as the
   original pre-Bootstrap version of this rule. */
.sidebar__nav.is-static .nav-sub,
.sidebar__nav.is-static .nav-sub__list { transition: none; }

.sidebar__foot { padding: 12px; border-top: 1px solid rgba(255,255,255,.06); flex: none; }

/* ---------- Demo card (developer only) -------------------------------- */
.sidebar__demo {
  padding: 11px 12px 12px;
  margin-bottom: 10px;
  border-radius: var(--r);
  background: rgba(255,255,255,.04);
  border: 1px solid rgba(255,255,255,.07);
}
.sidebar__demo--on { background: rgba(251,191,36,.10); border-color: rgba(251,191,36,.26); }

.sidebar__demo-head { display: flex; align-items: center; gap: 8px; }
.sidebar__demo-head .ico { color: #94a3b8; }
.sidebar__demo--on .sidebar__demo-head .ico { color: #fbbf24; }
.sidebar__demo-title { font-size: 12.5px; font-weight: 650; color: #e2e8f0; }
.sidebar__demo-hint  { font-size: 11px; line-height: 1.45; color: #64748b; margin-top: 4px; }
.sidebar__demo-hint .mono { color: #94a3b8; word-break: break-all; }

.sidebar__demo-btn {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  width: 100%; margin-top: 10px; padding: 7px 10px;
  border: 0; border-radius: var(--r-sm); cursor: pointer;
  font-family: inherit; font-size: 12px; font-weight: 600;
  background: rgba(99,102,241,.22); color: #c7d2fe;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.sidebar__demo-btn:hover { background: rgba(99,102,241,.34); color: #fff; }
.sidebar__demo-btn--exit { background: rgba(248,113,113,.16); color: #fca5a5; }
.sidebar__demo-btn--exit:hover { background: rgba(248,113,113,.28); color: #fff; }
.sidebar__demo-btn:focus-visible { outline: 2px solid rgba(165,180,252,.75); outline-offset: 2px; }
.sidebar__user {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 10px; border-radius: var(--r-sm); background: rgba(255,255,255,.04);
}
.sidebar__user:hover { background: rgba(255,255,255,.08); }
.sidebar__user .avatar { width: 32px; height: 32px; font-size: 12px; }
.sidebar__user-info { min-width: 0; flex: 1; }
.sidebar__user-name { font-size: 13px; font-weight: 600; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar__user-role { font-size: 11px; color: #64748b; }
.sidebar__logout { color: #64748b; padding: 6px; border-radius: var(--r-sm); display: grid; place-items: center; }
.sidebar__logout:hover { color: #f87171; background: rgba(248,113,113,.12); }

/* .sidebar__backdrop is gone - the mobile drawer is a real bootstrap.Offcanvas
   now (see .sidebar's offcanvas/offcanvas-lg classes and the media query
   below), and Bootstrap creates and manages its own .offcanvas-backdrop
   element automatically. Its colour is bridged to this app's palette in
   theme.css rather than restated here. */

/* ---------- Main ----------------------------------------------------- */
.main {
  flex: 1;
  min-width: 0;
  margin-left: var(--sidebar-w);
  display: flex;
  flex-direction: column;
}

.topbar {
  height: var(--topbar-h);
  flex: none;
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 14px;
  padding: 0 24px;
  /* Solid, not translucent+blurred: backdrop-filter on an ancestor makes it
     the containing block for any position:fixed descendant, which is
     exactly what the user-menu dropdown now is (Bootstrap's own Dropdown,
     Popper strategy "fixed" - see partials/sidebar.php/layout.php) - the
     menu would anchor to *this bar*, not the viewport, landing in the wrong
     place. The custom dropdown JS this replaced dodged the same trap by
     hoisting the open menu out to <body>; Bootstrap's doesn't reparent
     anything, so removing the trap at its source here is the fix instead. */
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.topbar__toggle {
  display: none;
  background: none; border: 0; color: var(--text-2); cursor: pointer;
  padding: 7px; border-radius: var(--r-sm);
}
.topbar__toggle:hover { background: var(--surface-3); }

.topbar__title { display: flex; align-items: center; gap: 8px; font-size: 17px; font-weight: 650; letter-spacing: -.02em; }
.topbar__title .ico { color: var(--muted); }
.topbar__spacer { flex: 1; }
.topbar__actions { display: flex; align-items: center; justify-content: center; gap: 8px; }

.icon-btn {
  width: 40px; height: 40px; flex: none;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text-2);
  cursor: pointer; position: relative;
  transition: background .15s var(--ease), color .15s var(--ease), border-color .15s var(--ease),
              transform .15s var(--ease), box-shadow .15s var(--ease);
}
.icon-btn:hover {
  background: var(--surface-3); color: var(--text); border-color: var(--border-strong);
  transform: translateY(-1px); box-shadow: var(--shadow-sm);
}
.icon-btn__dot {
  position: absolute; top: 7px; right: 8px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--danger); border: 2px solid var(--surface);
}

.content { flex: 1; padding: 24px; max-width: 1560px; width: 100%; }

.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 20px;
}
.page-head__text h1 { display: flex; align-items: center; gap: 10px; margin-bottom: 3px; }
.page-head__text h1 .ico { color: var(--muted); font-size: 20px; }
.page-head__text p { color: var(--muted); font-size: 13.5px; margin: 0; }
.page-head__actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* =====================================================================
   Buttons
   ===================================================================== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 38px; padding: 0 15px;
  font-family: inherit; font-size: 13.5px; font-weight: 600; line-height: 1;
  border-radius: var(--r-sm); border: 1px solid var(--border);
  background: var(--surface); color: var(--text);
  cursor: pointer; white-space: nowrap;
  transition: transform .15s var(--ease), box-shadow .15s var(--ease), background .15s var(--ease), border-color .15s var(--ease), color .15s var(--ease);
  box-shadow: var(--shadow-xs);
}
.btn:hover { background: var(--surface-3); border-color: var(--border-strong); color: var(--text); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid var(--primary-ring); outline-offset: 1px; }
.btn[disabled], .btn.is-loading { opacity: .6; pointer-events: none; }

/* Solid actions lift slightly on hover, matching how the icon buttons and
   stat cards respond - a small, consistent signal that the whole app is
   interactive rather than static chrome. */
.btn--primary, .btn--danger, .btn--ok, .btn--warn { border-color: transparent; color: #fff; }
.btn--primary:hover, .btn--danger:hover, .btn--ok:hover, .btn--warn:hover { transform: translateY(-1px); color: #fff; }
.btn--primary:active, .btn--danger:active, .btn--ok:active, .btn--warn:active { transform: translateY(0); }

.btn--primary { background: var(--primary); box-shadow: 0 1px 2px rgba(79,70,229,.3); }
.btn--primary:hover { background: var(--primary-600); box-shadow: var(--glow-primary); }
.btn--danger  { background: var(--danger); box-shadow: 0 1px 2px color-mix(in srgb, var(--danger) 30%, transparent); }
.btn--danger:hover { filter: brightness(1.06); box-shadow: 0 6px 16px -4px color-mix(in srgb, var(--danger) 38%, transparent); }
/* Money actions: --ok confirms cash received, --warn takes it back. Both read
   their colour from the same tokens the badges use, so they follow the theme. */
.btn--ok      { background: var(--ok); box-shadow: 0 1px 2px color-mix(in srgb, var(--ok) 30%, transparent); }
.btn--ok:hover { filter: brightness(1.06); box-shadow: 0 6px 16px -4px color-mix(in srgb, var(--ok) 38%, transparent); }
.btn--warn    { background: var(--warn); box-shadow: 0 1px 2px color-mix(in srgb, var(--warn) 30%, transparent); }
.btn--warn:hover { filter: brightness(1.06); box-shadow: 0 6px 16px -4px color-mix(in srgb, var(--warn) 38%, transparent); }
.btn--ghost   { background: transparent; border-color: transparent; box-shadow: none; }
.btn--ghost:hover { background: var(--surface-3); }
.btn--soft    { background: var(--primary-soft); border-color: transparent; color: var(--primary); box-shadow: none; }
.btn--soft:hover { background: var(--primary-soft); filter: brightness(.96); color: var(--primary); }
.btn--sm { height: 32px; padding: 0 11px; font-size: 12.5px; border-radius: 10px; }
.btn--xs { height: 28px; padding: 0 9px; font-size: 12px; border-radius: 8px; gap: 5px; }
.btn--icon { width: 38px; padding: 0; }
.btn--icon.btn--sm { width: 32px; }
.btn--block { width: 100%; }

.btn.is-loading { position: relative; color: transparent !important; }
.btn.is-loading::after {
  content: ""; position: absolute; width: 15px; height: 15px;
  border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
  color: #fff; animation: spin .6s linear infinite;
}
/* The spinner stays white on any solid fill and goes dark on the pale ones. */
.btn--ghost.is-loading::after,
.btn:not(.btn--primary):not(.btn--danger):not(.btn--ok):not(.btn--warn).is-loading::after { color: var(--text-2); }
@keyframes spin { to { transform: rotate(360deg); } }

.btn-group { display: inline-flex; gap: 6px; }

/* =====================================================================
   Cards
   ===================================================================== */
.card {
  /* display/position explicit on purpose: Bootstrap's own .card (now loaded
     first, see the layouts) sets display:flex/flex-direction:column and
     position:relative - harmless once a page's markup is actually migrated
     to Bootstrap's card structure, but a real layout change for every page
     still using this component the old way. Restating the plain-block
     defaults here is what keeps every not-yet-migrated .card looking exactly
     as it did before Bootstrap's stylesheet entered the picture. */
  display: block;
  position: static;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.card__head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 15px 18px; border-bottom: 1px solid var(--border);
}
.card__head h2, .card__head h3 { font-size: 14.5px; font-weight: 650; }
.card__head p { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
.card__body { padding: 18px; }
.card__body--flush { padding: 0; }
.card__foot { padding: 13px 18px; border-top: 1px solid var(--border); background: var(--surface-2); }

.grid { display: grid; gap: 16px; }
.grid--stats  { grid-template-columns: repeat(auto-fit, minmax(215px, 1fr)); }
.grid--2      { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.grid--3      { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid--main   { grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr); align-items: start; }
.grid--form   { grid-template-columns: minmax(0, 1fr) minmax(300px, 360px); align-items: start; }
@media (max-width: 1100px) { .grid--main, .grid--form { grid-template-columns: 1fr; } }

/* ---------- Stat cards ----------------------------------------------- */
.stat {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 17px 18px;
  box-shadow: var(--shadow-sm);
  position: relative; overflow: hidden;
  transition: box-shadow .2s var(--ease), transform .2s var(--ease), border-color .2s var(--ease);
}
.stat:hover { box-shadow: var(--shadow-md); border-color: var(--border-strong); transform: translateY(-2px); }
.stat::after {
  content: ""; position: absolute; right: -34px; top: -34px;
  width: 130px; height: 130px; border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, var(--tone, var(--primary)) 0%, var(--tone-soft, var(--primary-soft)) 42%, transparent 72%);
  opacity: .28;
}
.stat__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; position: relative; }
.stat__label { font-size: 12.5px; color: var(--muted); font-weight: 550; }
.stat__icon {
  width: 38px; height: 38px; border-radius: var(--r-sm); display: grid; place-items: center;
  background: var(--tone-soft, var(--primary-soft)); color: var(--tone, var(--primary));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tone, var(--primary)) 22%, transparent);
  position: relative;
}
.stat__value { font-size: 25px; font-weight: 700; letter-spacing: -.03em; margin-top: 9px; position: relative; line-height: 1.15; }
.stat__meta { display: flex; align-items: center; gap: 6px; margin-top: 7px; font-size: 12px; color: var(--muted); position: relative; }
.stat__delta { display: inline-flex; align-items: center; gap: 3px; font-weight: 650; }
.stat__delta--up   { color: var(--ok); }
.stat__delta--down { color: var(--danger); }
.stat__delta .ico  { font-size: 13px; }

.stat--ok      { --tone: var(--ok);      --tone-soft: var(--ok-soft); }
.stat--warn    { --tone: var(--warn);    --tone-soft: var(--warn-soft); }
.stat--danger  { --tone: var(--danger);  --tone-soft: var(--danger-soft); }
.stat--info    { --tone: var(--info);    --tone-soft: var(--info-soft); }

/* =====================================================================
   Tables
   ===================================================================== */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.table th {
  text-align: left; font-weight: 600; font-size: 11.5px;
  text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
  padding: 11px 16px; background: var(--surface-2);
  border-bottom: 1px solid var(--border); white-space: nowrap;
}
.table td { padding: 13px 16px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background .15s var(--ease); }
.table tbody tr:hover { background: var(--surface-2); }
/* A softly rounded highlight instead of a hard-edged row - background-clip
   respects the radius even under border-collapse, so this is free. */
.table tbody tr:hover td:first-child { border-top-left-radius: var(--r-sm); border-bottom-left-radius: var(--r-sm); }
.table tbody tr:hover td:last-child  { border-top-right-radius: var(--r-sm); border-bottom-right-radius: var(--r-sm); }
.table td.num, .table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.table td.tight { width: 1%; white-space: nowrap; }
.table .muted { color: var(--muted); }

.table--compact td { padding: 9px 14px; }
.table--compact th { padding: 8px 14px; }

.cell-main { font-weight: 600; color: var(--text); display: block; }
.cell-sub  { font-size: 12px; color: var(--muted); display: block; margin-top: 1px; }

.row-user { display: flex; align-items: center; gap: 11px; min-width: 0; }
.row-user__text { min-width: 0; }

/* ---------- Avatar / badge / chip ------------------------------------ */
.avatar {
  width: 36px; height: 36px; flex: none; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12.5px; font-weight: 700; letter-spacing: .02em; line-height: 1;
  background: hsl(var(--hue, 250) 70% 94%);
  color: hsl(var(--hue, 250) 62% 38%);
  box-shadow: inset 0 0 0 1px hsl(var(--hue, 250) 55% 82% / .6);
}
[data-bs-theme="dark"] .avatar {
  background: hsl(var(--hue, 250) 45% 22%); color: hsl(var(--hue, 250) 75% 78%);
  box-shadow: inset 0 0 0 1px hsl(var(--hue, 250) 45% 40% / .5);
}
.avatar--lg { width: 52px; height: 52px; font-size: 17px; }
.avatar--sm { width: 28px; height: 28px; font-size: 11px; }

.badge {
  /* color explicit for the same reason .card now states display/position -
     Bootstrap's own .badge defaults text to white, and every real usage
     here pairs this with a --modifier that sets its own color, but a bare
     .badge with no modifier would otherwise silently go white-on-white. */
  color: var(--text);
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: var(--r-full);
  font-size: 11.5px; font-weight: 650; line-height: 1.5;
  white-space: nowrap;
}
.badge::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.badge--ok     { background: var(--ok-soft);     color: var(--ok); }
.badge--warn   { background: var(--warn-soft);   color: var(--warn); }
.badge--danger { background: var(--danger-soft); color: var(--danger); }
.badge--info   { background: var(--info-soft);   color: var(--info); }
.badge--muted  { background: var(--surface-3);   color: var(--muted); }
/* The indigo the whole UI is already built around (--primary/--primary-soft
   swap themselves in dark mode), for the one badge role the set was missing:
   a change that is neither good news nor a warning, just this product's own
   accent. No new colour is introduced here - only the pairing. */
.badge--primary { background: var(--primary-soft); color: var(--primary); }
.badge--plain::before { display: none; }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-radius: var(--r-sm);
  background: var(--surface-3); color: var(--text-2);
  font-size: 12px; font-weight: 550;
}
.chip .ico { font-size: 14px; }

.dot { width: 8px; height: 8px; border-radius: 50%; flex: none; display: inline-block; }
.dot--ok { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
.dot--danger { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); }
.dot--warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }
.dot--muted { background: var(--muted); }
.dot--pulse { animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }

/* =====================================================================
   Forms
   ===================================================================== */
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px; }
.form-grid--1 { grid-template-columns: 1fr; }
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field--full { grid-column: 1 / -1; }

.label { font-size: 12.5px; font-weight: 600; color: var(--text-2); display: flex; align-items: center; gap: 5px; }
.label .req { color: var(--danger); }
.hint { font-size: 11.5px; color: var(--muted); line-height: 1.45; }

.input, .select, .textarea {
  width: 100%; font-family: inherit; font-size: 13.5px; color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r-sm); padding: 9px 12px; height: 40px;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease), background .15s var(--ease);
  appearance: none;
}
.textarea { height: auto; min-height: 84px; resize: vertical; line-height: 1.55; padding: 10px 12px; }
.input::placeholder, .textarea::placeholder { color: var(--muted); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--muted); }
.input:focus, .select:focus, .textarea:focus {
  outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px var(--primary-ring);
}
.input:disabled, .select:disabled, .textarea:disabled { background: var(--surface-3); color: var(--muted); cursor: not-allowed; }
.input.is-invalid, .select.is-invalid { border-color: var(--danger); }

.select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238792a8' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 11px center; background-size: 15px;
  padding-right: 34px; cursor: pointer;
}

/* .input-group / .input-addon: an *interactive* segment attached to the side
   of a field, wide enough to need its own box - now only the login captcha
   swatch. The square icon buttons that used to live here (copy URL, generate
   password, show password) moved to .input-icon below, which overlays them
   inside the field instead.

   flex-wrap: nowrap is load-bearing, not tidiness. Bootstrap's own
   .input-group sets flex-wrap: wrap, and this rule restating only `display`
   let that leak straight through: .input is width:100%, so it claimed the
   whole line and the addon wrapped underneath as a detached 40px box on a
   second row. Restate every property Bootstrap sets on a shared class name -
   load order only helps for the ones actually written here. */
.input-group { display: flex; flex-wrap: nowrap; }
.input-group .input { border-radius: var(--r-sm) 0 0 var(--r-sm); min-width: 0; }
.input-group .input-addon {
  display: grid; place-items: center; padding: 0 12px; height: 40px;
  border: 1px solid var(--border-strong); border-left: 0; border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: var(--surface-3); color: var(--muted); font-size: 12.5px; font-weight: 600; white-space: nowrap;
}
.input-group--pre .input { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
.input-group--pre .input-addon { border-left: 1px solid var(--border-strong); border-right: 0; border-radius: var(--r-sm) 0 0 var(--r-sm); }

/* ---------- Icon button inside a field (copy, show/hide, generate) --------
   A single square icon button riding *inside* the field's right edge, on the
   field's own border, instead of the separate bordered box .input-addon draws
   beside it. Unlike the variable-width unit labels in .input-unit below - where
   a fixed padding could never fit every string - this button is always exactly
   one 32px square, so reserving 48px of padding-right is precise rather than
   guesswork, and the icon can be lifted out of flow entirely.

   Absolute positioning also means the button cannot be reflowed anywhere by
   the field's own width: the value here is a long readonly URL, and in normal
   flow a flex row is one leaked flex-wrap away from dropping it to a second
   line (which is exactly what .input-group did). .hint help text stays a
   sibling of this wrapper, so it still starts at the field's left edge. */
.input-icon { position: relative; display: flex; align-items: center; width: 100%; }
.input-icon .input { padding-right: 48px; }
.input-icon__btn {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0;
  background: none; border: 0; border-radius: var(--r-sm);
  color: var(--muted); cursor: pointer;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.input-icon__btn:hover { background: var(--surface-3); color: var(--text); }
.input-icon__btn:focus-visible { outline: none; color: var(--text); box-shadow: 0 0 0 3px var(--primary-ring); }
.input-icon__btn .ico { display: block; }

/* ---------- Unit field (Rp, %, hari, Tgl, Mbps, ...) --------------------
   A non-interactive unit label sitting *inside* the field, sharing one
   unbroken border with it - rather than the separate bordered box
   .input-addon draws, which reads as a second control and (on a narrow
   grid column) could wrap under the input entirely.

   The wrapper carries the border/background/height that .input normally
   would, and the real <input> inside is stripped to a transparent,
   borderless box. That is deliberately *not* the absolute-positioning +
   fixed padding-left/right approach: the unit strings here range from "%"
   to "tiap bulan", so any hardcoded padding is either wasteful for the
   short ones or overlapped by the long ones. Letting the label sit in
   normal flow and having the input flex into the remaining space makes the
   spacing correct for every label automatically, and stays correct if a
   label is ever reworded.

   Everything else about a field is unchanged: same 40px height, same
   --r-sm radius, same border token, and .hint help text stays outside this
   wrapper exactly as before. */
.input-unit {
  display: flex; align-items: center; gap: 7px;
  height: 40px; padding: 0 12px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease), background .15s var(--ease);
  /* Reads as one text field, so the whole box should say "type here" -
     including the label and the padding either side of the input. */
  cursor: text;
}
.input-unit__label {
  flex: none; color: var(--muted);
  font-size: 12.5px; font-weight: 600; line-height: 1; white-space: nowrap;
  /* Decoration, not content: it should never be selectable as text, and
     dragging across the field shouldn't pick it up along with the value. */
  user-select: none;
}
/* The field itself keeps its own font/colour but hands its box over to the
   wrapper: one border, one focus ring, one height for the pair. */
.input-unit > .input {
  border: 0; background: none; height: auto; padding: 0;
  flex: 1; min-width: 0; border-radius: 0;
}
.input-unit > .input:focus { outline: none; box-shadow: none; }
.input-unit > .input:hover { border-color: transparent; }

/* Hover/focus/invalid/disabled all move up to the wrapper, so the pair
   reacts as the single control it now looks like. */
.input-unit:hover { border-color: var(--muted); }
.input-unit:focus-within { border-color: var(--primary); box-shadow: 0 0 0 4px var(--primary-ring); }
.input-unit:has(> .input.is-invalid) { border-color: var(--danger); }
.input-unit:has(> .input:disabled) { background: var(--surface-3); }
.input-unit:has(> .input:disabled) .input-unit__label { opacity: .6; }

/* Number spinners would otherwise land between the value and a suffix
   label, colliding with it - they were harmless before only because the
   old addon sat outside the input's own box. */
.input-unit > .input[type="number"] { -moz-appearance: textfield; }
.input-unit > .input[type="number"]::-webkit-outer-spin-button,
.input-unit > .input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.input-search { position: relative; }
/* Direct-child combinator on purpose: the decorative left icon is a direct
   child of .input-search, but a right-side .input-icon-btn (the password
   show/hide toggle) lives in this same wrapper too, several levels deeper
   (button > span > i.ico). A plain ".input-search .ico" descendant
   selector would reach through the button and yank that icon out of its own
   flex-centering with this absolute positioning meant for the left icon -
   which is exactly the "off center" bug this was silently causing. */
.input-search > .ico {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  color: var(--muted); pointer-events: none; font-size: 16px;
}
.input-search .input { padding-left: 36px; }

/* A right-side icon *button* overlaid on the input itself - the interactive
   counterpart to .input-search's decorative left icon - for things like a
   password field's show/hide toggle that need to sit inside the field with
   no separate bordered box the way .input-addon draws one. :has() scopes the
   padding to only inputs that actually have one of these, so plain
   .input-search fields (no right icon) are unaffected. */
.input-search:has(.input-icon-btn) .input { padding-right: 44px; }
.input-icon-btn {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0;
  background: none; border: 0; border-radius: var(--r-sm);
  color: var(--muted); cursor: pointer;
  transition: color .15s var(--ease), background .15s var(--ease);
}
.input-icon-btn:hover { color: var(--text); background: var(--surface-3); }
.input-icon-btn:focus-visible { outline: 2px solid var(--primary-ring); outline-offset: 1px; }
/* Each icon a toggle button swaps between (e.g. eye / eye-off) gets its own
   centered box too - belt and suspenders alongside this button's own flex
   centering, and alongside .ico's display:block (near the top of this file)
   which is what actually stops an inline SVG's baseline "descender" gap from
   nudging it off-center inside a plain wrapper span like these. */
.input-icon-btn > span {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; line-height: 1;
}

.error-text { font-size: 11.5px; color: var(--danger); font-weight: 550; }

/* File picker: the native control is left in place (it is the only reliable
   way to open the dialog) and only its button is restyled. */
.input--file { height: auto; padding: 7px 8px; cursor: pointer; line-height: 1.4; }
.input--file::file-selector-button {
  font-family: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--text-2); background: var(--surface-3);
  border: 1px solid var(--border-strong); border-radius: 9px;
  padding: 6px 12px; margin-right: 11px; cursor: pointer;
  transition: background .15s var(--ease), color .15s var(--ease), border-color .15s var(--ease);
}
.input--file::file-selector-button:hover { background: var(--primary-soft); color: var(--primary); border-color: var(--primary); }
.input--file:disabled, .input--file:disabled::file-selector-button { cursor: not-allowed; }

/* Range slider */
.range {
  width: 100%; height: 22px; padding: 0; margin: 0;
  background: transparent; appearance: none; cursor: pointer;
}
.range:focus { outline: none; }
.range::-webkit-slider-runnable-track {
  height: 6px; border-radius: 20px;
  background: var(--surface-3); border: 1px solid var(--border);
}
.range::-moz-range-track {
  height: 6px; border-radius: 20px;
  background: var(--surface-3); border: 1px solid var(--border);
}
.range::-webkit-slider-thumb {
  appearance: none; width: 18px; height: 18px; margin-top: -7px;
  border-radius: 50%; background: var(--primary); border: 3px solid var(--surface);
  box-shadow: var(--shadow-sm); transition: box-shadow .15s var(--ease), transform .15s var(--ease);
}
.range::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--primary); border: 3px solid var(--surface);
  box-shadow: var(--shadow-sm); transition: box-shadow .15s var(--ease), transform .15s var(--ease);
}
.range:hover::-webkit-slider-thumb { transform: scale(1.08); }
.range:hover::-moz-range-thumb     { transform: scale(1.08); }
.range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px var(--primary-ring); }
.range:focus-visible::-moz-range-thumb     { box-shadow: 0 0 0 4px var(--primary-ring); }
.range:disabled { cursor: not-allowed; }
.range:disabled::-webkit-slider-thumb { background: var(--muted); }
.range:disabled::-moz-range-thumb     { background: var(--muted); }

.range__value {
  margin-left: auto; font-family: var(--mono); font-size: 11.5px; font-weight: 600;
  color: var(--primary); background: var(--primary-soft);
  padding: 2px 8px; border-radius: var(--r-full);
}
.range__scale {
  display: flex; justify-content: space-between;
  font-size: 11px; color: var(--muted); font-family: var(--mono);
}

/* ---------- Logo manager (Pengaturan Umum) --------------------------- */
.logo-manager { display: grid; grid-template-columns: minmax(0, 340px) minmax(0, 1fr); gap: 22px; align-items: start; }
.logo-manager__controls { display: grid; gap: 18px; }

/* This slider only sizes the login page's copy of the logo now (the
   sidebar's is fixed - see .sidebar__brand--custom), so the stage is drawn
   on the login screen's own gradient and holds the very same .brand-logo it
   draws, rather than an approximation of it. */
.logo-stage {
  display: grid; place-items: center;
  min-height: 150px; padding: 18px;
  border-radius: var(--r); border: 1px dashed var(--border-strong);
  background-image:
    linear-gradient(135deg, rgba(255,255,255,.04), rgba(255,255,255,0)),
    linear-gradient(150deg, var(--sidebar-bg) 0%,
      color-mix(in srgb, var(--primary) 40%, var(--sidebar-bg)) 50%,
      color-mix(in srgb, var(--primary-2) 50%, var(--sidebar-bg)) 100%);
  overflow: hidden;
}
.logo-stage__empty { display: grid; justify-items: center; gap: 8px; color: #64748b; font-size: 12.5px; }
.logo-stage__caption { margin-top: 8px; font-size: 11.5px; color: var(--muted); }

/* Captcha length preview (Pengaturan Aplikasi): a small standalone box, full
   corners on every side - unlike .auth__captcha-addon, which is the same
   idea but has to butt up against an input on its left instead. */
.captcha-preview-box {
  display: inline-block; flex: none; width: 128px; height: 40px;
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  overflow: hidden; line-height: 0; background: var(--surface-3);
}
.captcha-preview-box img { display: block; width: 100%; height: 100%; }

@media (max-width: 820px) {
  .logo-manager { grid-template-columns: 1fr; }
}

.check {
  display: flex; align-items: flex-start; gap: 10px; cursor: pointer;
  padding: 11px 13px; border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--surface-2);
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.check:hover { border-color: var(--border-strong); }
.check input { margin: 2px 0 0; width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; flex: none; }
.check__text { font-size: 13px; font-weight: 550; }
.check__hint { font-size: 11.5px; color: var(--muted); font-weight: 400; margin-top: 1px; }
.check:has(input:checked) { border-color: var(--primary); background: var(--primary-soft); }

.switch { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch__track {
  width: 40px; height: 23px; border-radius: var(--r-full); background: var(--border-strong);
  position: relative; transition: background .2s var(--ease); flex: none;
}
.switch__track::after {
  content: ""; position: absolute; top: 2.5px; left: 2.5px;
  width: 18px; height: 18px; border-radius: 50%; background: #fff;
  transition: transform .2s var(--ease); box-shadow: var(--shadow-xs);
}
.switch input:checked + .switch__track { background: var(--primary); }
.switch input:checked + .switch__track::after { transform: translateX(17px); }
.switch input:focus-visible + .switch__track { box-shadow: 0 0 0 3.5px var(--primary-ring); }

.radio-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 9px; }
.radio-card {
  position: relative; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--r); padding: 12px;
  background: var(--surface-2); text-align: center;
  transition: border-color .18s var(--ease), background .18s var(--ease), box-shadow .18s var(--ease);
}
.radio-card input { position: absolute; opacity: 0; }
.radio-card:hover { border-color: var(--border-strong); }
.radio-card__title { font-size: 13px; font-weight: 650; }
.radio-card__desc  { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.radio-card:has(input:checked) {
  border-color: var(--primary); background: var(--primary-soft);
  box-shadow: 0 0 0 1px var(--primary), 0 8px 20px -8px var(--primary-ring);
}
.radio-card:has(input:checked) .radio-card__title { color: var(--primary); }

/* ---------- Filter bar ------------------------------------------------ */
.filters {
  display: flex; gap: 9px; flex-wrap: wrap; align-items: center;
  padding: 13px 16px; border-bottom: 1px solid var(--border); background: var(--surface-2);
}
.filters .input-search { flex: 1 1 240px; min-width: 190px; }
.filters .select { width: auto; min-width: 140px; }

/* ---------- Invoice item rows ---------------------------------------- */
.invoice-row {
  display: grid; gap: 9px; align-items: end;
  grid-template-columns: minmax(0, 1fr) 78px 132px 118px 34px;
  padding: 11px 0; border-bottom: 1px dashed var(--border);
}
.invoice-row:last-child { border-bottom: 0; }
.invoice-row__amount {
  text-align: right; font-weight: 650; font-variant-numeric: tabular-nums;
  padding-bottom: 9px; font-size: 13.5px;
}
.invoice-row__head {
  display: grid; gap: 9px;
  grid-template-columns: minmax(0, 1fr) 78px 132px 118px 34px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); padding-bottom: 8px; border-bottom: 1px solid var(--border);
}
.invoice-row__head span:nth-child(n+2) { text-align: right; }
@media (max-width: 720px) {
  .invoice-row { grid-template-columns: minmax(0, 1fr) 34px; }
  .invoice-row__head { display: none; }
  .invoice-row__amount { text-align: left; padding-bottom: 0; }
}


/* =====================================================================
   Feedback
   ===================================================================== */
.alert {
  display: flex; gap: 11px; align-items: flex-start;
  padding: 13px 15px; border-radius: var(--r); font-size: 13.5px;
  border: 1px solid transparent; margin-bottom: 16px;
}
.alert .ico { flex: none; margin-top: 1px; }
.alert__title { font-weight: 650; margin-bottom: 2px; }
.alert--success { background: var(--ok-soft);     color: var(--ok);     border-color: color-mix(in srgb, var(--ok) 25%, transparent); }
.alert--error   { background: var(--danger-soft); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 25%, transparent); }
.alert--warning { background: var(--warn-soft);   color: var(--warn);   border-color: color-mix(in srgb, var(--warn) 25%, transparent); }
.alert--info    { background: var(--info-soft);   color: var(--info);   border-color: color-mix(in srgb, var(--info) 25%, transparent); }

/* Named .notif, not .toast: Bootstrap ships .toast:not(.show){display:none},
   whose two-class specificity beats any plain .toast rule here no matter
   which stylesheet loads last - the notification was built and appended, then
   hidden outright. Its .toast{width:350px} and font/color/background rules
   leaked in the same way. A name Bootstrap has no opinion about removes the
   whole collision instead of out-shouting it rule by rule. */
.notifs {
  /* 1090 is Bootstrap's own --bs-toast-zindex, and deliberately above its
     modal (1055) and modal-backdrop (1050): a toast fired while the confirm
     dialog is open has to stay readable. The old value (200) cleared the
     custom modal this app used to have (150), but would now sit *behind*
     Bootstrap's. */
  position: fixed; top: 18px; right: 18px; z-index: 1090;
  display: flex; flex-direction: column; gap: 10px;
  max-width: 380px; width: 100%;
}
.notif {
  position: relative; /* .notif__progress anchors to this, not the viewport */
  display: flex; gap: 11px; align-items: flex-start;
  padding: 13px 15px;
  /* Glass surface: translucent + blurred rather than the flat opaque card
     this used to be, matching the same color-mix + backdrop-filter idiom
     .topbar/.auth__form already use elsewhere. Safe to blur here (unlike
     .topbar, which had this removed) - backdrop-filter only traps a
     position:fixed *descendant*'s containing block, and .notif has none;
     .notifs (the fixed part) does the blurring's ancestor, not this. */
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(14px) saturate(160%);
  border: 1px solid var(--border);
  border-left: 3px solid var(--tone, var(--primary));
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden; /* clips .notif__progress to the same rounded corners */
  animation: notif-in .3s cubic-bezier(0,0,.2,1);
}
.notif.is-out { animation: notif-out .2s cubic-bezier(.4,0,1,1) forwards; }
.notif__icon { color: var(--tone, var(--primary)); flex: none; margin-top: 1px; }
.notif__body { flex: 1; min-width: 0; }
.notif__title { font-size: 13px; font-weight: 650; color: var(--text); margin-bottom: 1px; }
.notif__msg { font-size: 13px; color: var(--text-2); line-height: 1.5; }
.notif__close { margin-left: auto; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 4px; border-radius: var(--r-sm); flex: none; transition: background .15s var(--ease), color .15s var(--ease); }
.notif__close:hover { color: var(--text); background: var(--surface-3); }
/* Auto-dismiss indicator: a bar shrinking left-to-right over the toast's
   real remaining lifetime (app.js sets --notif-duration to match its own
   setTimeout exactly, so the two can never drift apart), paused together
   with that timer on hover - see app.js - so a toast someone is actually
   reading never disappears out from under their cursor. */
.notif__progress {
  position: absolute; left: 0; right: 0; bottom: 0; height: 2.5px;
  background: var(--tone, var(--primary));
  transform: scaleX(1); transform-origin: left;
  animation: notif-progress linear forwards;
  animation-duration: var(--notif-duration, 5s);
}
.notif--success { --tone: var(--ok); }
.notif--error   { --tone: var(--danger); }
.notif--warning { --tone: var(--warn); }
.notif--info    { --tone: var(--info); }
@keyframes notif-in  { from { opacity: 0; transform: translateX(32px) scale(.96); } }
@keyframes notif-out { to   { opacity: 0; transform: translateX(32px) scale(.96); } }
@keyframes notif-progress { to { transform: scaleX(0); } }

.empty { text-align: center; padding: 48px 24px; }
.empty__icon {
  width: 56px; height: 56px; margin: 0 auto 14px; border-radius: var(--r-lg);
  display: grid; place-items: center; background: var(--surface-3); color: var(--muted);
}
.empty__icon .ico { font-size: 26px; }
.empty h3 { font-size: 15px; margin-bottom: 5px; }
.empty p { color: var(--muted); font-size: 13px; max-width: 380px; margin: 0 auto 16px; }

/* ---------- Progress -------------------------------------------------- */
.progress { height: 7px; border-radius: var(--r-full); background: var(--surface-3); overflow: hidden; }
.progress__bar { height: 100%; border-radius: var(--r-full); background: var(--tone, var(--primary)); transition: width .5s var(--ease); }
.progress--ok     { --tone: var(--ok); }
.progress--warn   { --tone: var(--warn); }
.progress--danger { --tone: var(--danger); }

.meter { display: flex; align-items: center; gap: 9px; }
.meter__label { font-size: 11.5px; color: var(--muted); min-width: 34px; }
.meter .progress { flex: 1; }
.meter__value { font-size: 12px; font-weight: 650; min-width: 38px; text-align: right; font-variant-numeric: tabular-nums; }

/* =====================================================================
   Charts (inline SVG)
   ===================================================================== */
/* The wrapper owns the chart's box: a width it fills, a height it is told
   (never one inferred from the SVG's aspect ratio), and the gutters that keep
   the drawing clear of its own edges. --chart-pad-top leaves room for a peak
   dot, --chart-pad-bottom for the x labels, --chart-gutter for the y labels.
   position: relative is what the dots and labels position against. */
.chart-wrap {
  --chart-h: 240px;
  --chart-pad-top: 10px;
  --chart-pad-bottom: 20px;
  --chart-gutter: 62px;
  position: relative;
  width: 100%;
  /* The stated height is the plot area; the label gutter sits below it. */
  height: calc(var(--chart-h) + var(--chart-pad-bottom));
  padding-left: var(--chart-gutter);
  padding-top: var(--chart-pad-top);
  overflow: visible;
}

/* The plot box proper - what the 0-100 viewBox and the dot percentages both
   measure against. Overflow stays visible so a dot sitting exactly on 0% or
   100% is drawn whole rather than sliced in half by this edge. */
.chart-plot {
  position: relative;
  height: var(--chart-h);
  overflow: visible;
}

.chart { width: 100%; height: 100%; display: block; overflow: visible; }
.chart__grid { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 4; }
.chart__axis { fill: var(--muted); font-size: 10.5px; font-family: var(--font); }
/* No fill here on purpose: each chart names its own gradient id via the
   element's fill attribute, and a CSS fill would override that (CSS beats a
   presentation attribute) and point every chart at one shared id. */
.chart__line { fill: none; stroke: var(--primary); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.chart__line--alt { stroke: var(--muted); stroke-dasharray: 4 4; stroke-width: 1.8; }
.chart__dot { fill: var(--surface); stroke: var(--primary); stroke-width: 2.5; }
.chart__hit { fill: transparent; cursor: pointer; }
.chart__hit:hover + .chart__dot, .chart__dot.is-active { r: 6; }

/* Y axis: one label per gridline, spread across the plot height. Each sits on
   its line rather than under it, so the number reads level with what it
   labels. The gutter is wide enough for "Rp 999,9 jt" without wrapping. */
.chart-yaxis {
  position: absolute;
  left: 0; top: var(--chart-pad-top);
  width: var(--chart-gutter);
  height: var(--chart-h);
  display: flex; flex-direction: column; justify-content: space-between;
  padding-right: 10px;
  pointer-events: none;
}
.chart-yaxis__tick {
  font-size: 10.5px; line-height: 1; color: var(--muted);
  text-align: right; white-space: nowrap;
  transform: translateY(-50%);
}
/* The first and last ticks would otherwise hang half outside the plot box. */
.chart-yaxis__tick:first-child { transform: translateY(0); }
.chart-yaxis__tick:last-child  { transform: translateY(-100%); }

/* X axis: labels centred on their data point, clamped at the ends so the
   first and last stay inside the card instead of overhanging it. */
.chart-xaxis {
  position: relative;
  height: var(--chart-pad-bottom);
  margin-top: 6px;
}
.chart-xaxis__tick {
  position: absolute; top: 0;
  transform: translateX(-50%);
  font-size: 10.5px; line-height: 1.4; color: var(--muted); white-space: nowrap;
}
.chart-xaxis__tick:first-child { transform: translateX(0); }
.chart-xaxis__tick:last-child  { transform: translateX(-100%); }

/* HTML dots: round at any stretch, and a bigger hover target than an SVG
   circle of the same visual size. */
.chart-dot {
  position: absolute;
  width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--surface);
  border: 2.5px solid var(--primary);
  box-sizing: border-box;
  transition: transform .15s var(--ease), box-shadow .15s var(--ease);
  cursor: pointer;
}
.chart-dot:hover { transform: scale(1.35); box-shadow: 0 0 0 4px var(--primary-ring); }

@media (max-width: 700px) {
  /* Money shortens to "Rp 25 rb" here, so the gutter can give width back to
     the plot on the screens that have least of it to spare. */
  .chart-wrap { --chart-h: 190px; --chart-gutter: 52px; }
}

.bars { display: flex; align-items: flex-end; gap: 8px; height: 130px; }
.bars__col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; gap: 6px; align-items: center; min-width: 0; }
.bars__bar {
  width: 100%; max-width: 42px; border-radius: 6px 6px 3px 3px;
  background: linear-gradient(180deg, var(--primary), color-mix(in srgb, var(--primary) 55%, transparent));
  min-height: 4px; transition: opacity .15s var(--ease);
}
.bars__col:hover .bars__bar { opacity: .8; }
.bars__label { font-size: 11px; color: var(--muted); white-space: nowrap; }

.legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: var(--muted); }
.legend__item { display: flex; align-items: center; gap: 6px; }
.legend__swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--primary); flex: none; }

/* ---------- Live router rows ------------------------------------------ */
.live-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 13px 18px; border-bottom: 1px solid var(--border);
}
.live-row:last-child { border-bottom: 0; }
.live-row__meters { display: grid; gap: 5px; min-width: 148px; flex: none; }
@media (max-width: 520px) {
  .live-row { flex-direction: column; align-items: stretch; gap: 9px; }
  .live-row__meters { min-width: 0; }
}

/* =====================================================================
   Modal
   =====================================================================
   The confirm dialog (App.confirm() in app.js) now builds real Bootstrap
   modal markup (.modal-dialog/.modal-content/...) and runs on
   bootstrap.Modal - styled by bootstrap.min.css + this app's brand colours
   via theme.css's --bs-* bridge, not by rules here. .modal__head/__icon
   stay: no other Bootstrap class fills that "small icon badge beside a
   title" role, and .modal__icon is still used by the confirm dialog. The
   old .modal/.modal__box/.modal__box--wide/.modal__body/.modal__foot rules
   (and .modal itself as a bare selector) are gone with it - nothing else in
   the app ever used them. */
/* padding-bottom, not 0: this used to lean entirely on .modal-body's own
   16px top padding for the gap under the title, which is too little once the
   body actually scrolls - History Changelog's rows slid up to within a few
   pixels of the icon and read as running into it. 14px here plus that 16px
   gives the header room of its own.

   align-items: center, not flex-start: both heads in the app (this one and
   App.confirm's) hold exactly one line of title beside a 42px icon - the
   message in the confirm dialog lives in .modal-body, not here - so
   flex-start pinned that single line to the icon's top edge and left it
   visibly high of centre.

   flex: none for the same reason .modal-footer carries flex-shrink: 0:
   inside .modal-dialog-scrollable the content box is a flex column capped at
   max-height, and a header is never the part that should give way. Nothing
   is currently squashing it - a flex item's automatic minimum size happens
   to hold it at the icon's height - but that is a side effect of what is in
   it, not a rule, and it would stop protecting a header whose icon ever
   became optional. */
.modal__head {
  padding: 20px 22px 14px;
  display: flex; gap: 14px; align-items: center;
  flex: none;
}
.modal__icon { width: 42px; height: 42px; border-radius: var(--r-sm); display: grid; place-items: center; flex: none;
               background: var(--danger-soft); color: var(--danger); }
/* ---------- Dropdown ----------------------------------------------------
   Row-action and topbar dropdowns now use real Bootstrap markup
   (.dropdown-menu/.dropdown-item/.dropdown-divider) and run on
   bootstrap.Dropdown (strategy "fixed", set per-trigger via
   data-bs-strategy - see the views) instead of the hand-rolled
   getBoundingClientRect + hoist-to-<body> positioning this replaced.
   "Fixed" strategy is what still keeps a row's action menu from being
   clipped by .table-wrap's overflow-x or .card's overflow: hidden, the same
   problem the old code solved a different way. .dropdown itself
   (position: relative) is untouched - Bootstrap's own dropdown wants the
   exact same thing, so it was never actually something to migrate away
   from. .dropdown__item--danger's replacement (.dropdown-item--danger)
   lives in theme.css, next to the rest of the Bootstrap brand bridge.

   The variables below tune Bootstrap's own defaults (1rem items, edge-to-
   edge hover band, 10rem min-width) down to this app's compact 13.5px UI
   scale, and give each row an inset pill highlight instead of a hard
   rectangle - the same proportions .nav-link already uses. */
.dropdown-menu {
  --bs-dropdown-font-size: 13.5px;
  --bs-dropdown-min-width: 190px;
  --bs-dropdown-padding-x: 6px;
  --bs-dropdown-item-padding-x: 10px;
  --bs-dropdown-item-padding-y: 8px;
}
.dropdown-item {
  display: flex; align-items: center; gap: 10px;
  border-radius: var(--r-sm);
  font-weight: 500;
}
.dropdown-item .ico { opacity: .75; }
.dropdown-item:hover .ico, .dropdown-item:focus .ico, .dropdown-item.active .ico {
  opacity: 1;
}
.dropdown-header { font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }

/* =====================================================================
   Pagination
   ===================================================================== */
.pagination {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 13px 18px; border-top: 1px solid var(--border); flex-wrap: wrap;
}
.pagination__info { font-size: 12.5px; color: var(--muted); }
.pagination__pages { display: flex; gap: 4px; }
.pagination__link {
  min-width: 33px; height: 33px; padding: 0 9px;
  display: grid; place-items: center; border-radius: var(--r-sm);
  font-size: 13px; font-weight: 600; color: var(--text-2);
  border: 1px solid transparent;
  transition: background .15s var(--ease), color .15s var(--ease), box-shadow .15s var(--ease);
}
.pagination__link:hover { background: var(--surface-3); color: var(--text); }
.pagination__link.is-active { background: var(--primary); color: #fff; box-shadow: 0 1px 3px rgba(79,70,229,.35); }
.pagination__link.is-disabled { opacity: .4; pointer-events: none; }

/* =====================================================================
   Detail lists / misc
   ===================================================================== */
/* Satu baris hasil pemeriksaan: label pendek dengan keterangan sepanjang
   kalimat di bawahnya. Pola .dl di bawah tidak dipakai di sini karena nilainya
   bukan satu-dua kata, dan rata kanan membuat kalimat panjang sulit dibaca. */
.check-row { display: flex; align-items: flex-start; gap: 10px; padding: 9px 0; border-bottom: 1px dashed var(--border); }
.check-row:last-child { border-bottom: 0; }
.check-row .dot { margin-top: 6px; }

.dl { display: grid; gap: 0; }
.dl__row { display: flex; justify-content: space-between; gap: 16px; padding: 10px 0; border-bottom: 1px dashed var(--border); }
.dl__row:last-child { border-bottom: 0; }
.dl__key { font-size: 12.5px; color: var(--muted); flex: none; }
.dl__val { font-size: 13.5px; font-weight: 600; text-align: right; word-break: break-word; }

.kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; }
.kv__item .kv__key { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; font-weight: 600; }
.kv__item .kv__val { font-size: 14px; font-weight: 600; margin-top: 3px; }

.mono { font-family: var(--mono); font-size: 12.5px; }
.copyable { cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
.copyable:hover { color: var(--primary); }
.copyable .ico { font-size: 13px; opacity: .5; }
.copyable:hover .ico { opacity: 1; }

.timeline { position: relative; padding-left: 26px; }
.timeline::before { content: ""; position: absolute; left: 8px; top: 6px; bottom: 6px; width: 2px; background: var(--border); }
.timeline__item { position: relative; padding-bottom: 16px; }
.timeline__item:last-child { padding-bottom: 0; }
.timeline__item::before {
  content: ""; position: absolute; left: -22px; top: 5px;
  width: 9px; height: 9px; border-radius: 50%; background: var(--primary);
  box-shadow: 0 0 0 3px var(--surface), 0 0 0 4px var(--border);
}
.timeline__title { font-size: 13px; font-weight: 600; }
.timeline__meta { font-size: 11.5px; color: var(--muted); margin-top: 1px; }

/* ---------- Changelog -------------------------------------------------
   A dense "what changed" feed: one line per change, tagged by kind, grouped
   under the version it shipped in. Deliberately not .timeline - that one
   draws a rail and a dot per item, which is the right shape for a handful of
   events on a customer, and the wrong one for a list this long: the rail
   turns into a second vertical line competing with the text for attention.
   Here the only separator is a 1px rule between versions.

   Every colour, radius, font and surface below is a token this file already
   defines. The three tags reuse the existing .badge modifiers rather than
   declaring their own palette: .badge--ok for a new capability,
   .badge--muted for a change to one that already existed, .badge--primary
   for a fix. That is what keeps them following the theme (including every
   dark-mode override) without a single colour being restated here. */
.changelog { display: flex; flex-direction: column; }

/* Only *between* groups, never above the first - a rule at the very top
   would read as the card's own border doubled. */
.changelog__group + .changelog__group {
  margin-top: 12px; padding-top: 14px; border-top: 1px solid var(--border);
}

/* baseline, not center: the tag is a fixed-height box next to text that may
   wrap to three lines, and centering it against the whole block would float
   it away from the line it labels. */
.changelog__row { display: flex; align-items: baseline; gap: 10px; padding: 5px 0; }

/* Optional line above a version's rows: when it was published, whether it is
   the one installed, where to read it on GitHub. Indented by exactly the tag
   column (46px + the 10px row gap) so it starts where the descriptions do
   rather than hanging out to the left of everything.

   The version itself is deliberately absent here - every row underneath
   already opens with it, and printing it twice makes the group read as a
   heading plus a repetition of that heading. */
.changelog__meta {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 2px 0 4px 56px;
  font-size: 11.5px; color: var(--muted);
}

/* min-width, not width: NEW/UPD/FIX happen to be the same length, but a
   longer tag should grow rather than have its text clipped - the column
   stays aligned either way. */
.changelog__tag {
  flex: none; min-width: 46px; justify-content: center;
  padding: 3px 7px; border-radius: var(--r-sm);
  font-family: var(--mono); font-size: 10px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase;
}

/* min-width: 0 so a long unbroken string (a URL, a config key) wraps inside
   the row instead of pushing the tag off the edge. */
.changelog__text {
  margin: 0; min-width: 0;
  font-size: 13.5px; line-height: 1.65; color: var(--text-2);
}
/* The version and the module name are the two things being scanned for, so
   both sit at full --text contrast while the description stays at --text-2. */
.changelog__version { font-weight: 700; color: var(--text); white-space: nowrap; }
.changelog__module  { font-weight: 600; color: var(--text); }


.divider-text { display: flex; align-items: center; gap: 12px; color: var(--muted); font-size: 12px; margin: 18px 0; }
.divider-text::before, .divider-text::after { content: ""; flex: 1; height: 1px; background: var(--border); }

.section-title {
  font-size: 11.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 12px; display: flex; align-items: center; gap: 8px;
}
.section-title::after { content: ""; flex: 1; height: 1px; background: var(--border); }

/* Utilities */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-4 { gap: 4px; } .gap-6 { gap: 6px; } .gap-8 { gap: 8px; }
.gap-12 { gap: 12px; } .gap-16 { gap: 16px; }
.flex-1 { flex: 1; } .flex-wrap { flex-wrap: wrap; }
.mt-0 { margin-top: 0; } .mt-4 { margin-top: 4px; } .mt-8 { margin-top: 8px; }
.mt-12 { margin-top: 12px; } .mt-16 { margin-top: 16px; } .mt-24 { margin-top: 24px; }
.mb-0 { margin-bottom: 0; } .mb-8 { margin-bottom: 8px; } .mb-16 { margin-bottom: 16px; }
.text-muted { color: var(--muted); }
.text-sm { font-size: 12.5px; }
.text-xs { font-size: 11.5px; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.fw-600 { font-weight: 600; } .fw-700 { font-weight: 700; }
.text-ok { color: var(--ok); } .text-danger { color: var(--danger); }
.text-warn { color: var(--warn); } .text-primary { color: var(--primary); }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w-full { width: 100%; }
.hidden { display: none !important; }

/* Conditional form fields (data-show-for / data-hide-for): app.js fades these
   in and out with matching inline opacity/transform instead of an instant
   cut. The rule lives here, not on [data-show-for] itself, so it also covers
   whatever the module is pointed at next - a field, a button, an alert. */
[data-show-for], [data-hide-for] {
  opacity: 1; transform: translateY(0);
  transition: opacity .22s var(--ease), transform .22s var(--ease);
}

/* =====================================================================
   Auth screen - single centered card, no split panel.
   ===================================================================== */
/* The sidebar's own dark navy, not the theme-switching page --bg: the login
   screen has no stored theme preference to read yet, and the app's one
   constant across both themes is that the chrome (sidebar, this screen) is
   always dark - so that is what the whole screen borrows from, not just a
   side panel. A faint dot-grid layered under the gradient reads as a
   circuit board / data-mesh from a distance without competing with the
   card on top of it. Every stop is mixed from --sidebar-bg/--primary/
   --primary-2/--primary-3 - the same tokens the sidebar and every
   "primary" control in the app draw from - instead of a one-off palette
   that only happens to look similar. */
.auth {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  padding: 40px 24px;
  color: #fff;
  background-image:
    linear-gradient(150deg, var(--sidebar-bg) 0%,
      color-mix(in srgb, var(--primary) 40%, var(--sidebar-bg)) 50%,
      color-mix(in srgb, var(--primary-2) 50%, var(--sidebar-bg)) 100%),
    radial-gradient(circle, rgba(255,255,255,.14) 1px, transparent 1px);
  background-size: auto, 26px 26px;
  position: relative; overflow: hidden;
}
.auth::before, .auth::after {
  content: ""; position: absolute; border-radius: 50%; filter: blur(90px); pointer-events: none;
}
.auth::before { width: 420px; height: 420px; background: color-mix(in srgb, var(--primary-2) 42%, transparent); top: -130px; right: -110px; }
.auth::after  { width: 380px; height: 380px; background: color-mix(in srgb, var(--primary-3) 30%, transparent); bottom: -140px; left: -100px; }

.auth__center {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center;
  width: 100%; max-width: 400px;
  gap: 30px;
}

.auth__header { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
/* Logo above the name, not beside it - this is the one place --logo-size
   (the Pengaturan Umum slider) actually applies now; the sidebar's copy of
   the same image is fixed regardless, see .sidebar__brand--custom above. */
.auth__brand { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.auth__brand-name { font-size: 20px; font-weight: 700; color: #fff; letter-spacing: -.02em; }
/* Bigger and rounder than the sidebar's own 34px mark - this is the sole
   piece of branding on the page, not a corner of a dense nav bar. */
.auth__logo {
  width: 56px; height: 56px; flex: none;
  border-radius: var(--r-xl);
  background: linear-gradient(135deg, #6366f1, #8b5cf6 55%, #d946ef);
  display: grid; place-items: center; color: #fff;
  box-shadow: 0 10px 24px rgba(99,102,241,.45);
}
.auth__logo svg { width: 30px; height: 30px; }
.auth__tagline { font-size: 14px; color: rgba(255,255,255,.68); max-width: 320px; line-height: 1.5; }

/* A floating glass panel rather than bare inputs on the gradient - fixed
   dark regardless of the app's light/dark toggle (the gradient behind it
   is too, for the same reason noted on .auth above), so every shared
   control it hosts (.label/.input/...) gets its own light-on-dark colors
   further down instead of inheriting whichever the toggle currently has
   the shared --text/--border/--surface tokens pointed at. */
.auth__form {
  width: 100%;
  background: color-mix(in srgb, var(--sidebar-bg) 82%, transparent);
  backdrop-filter: blur(20px);
  border: 1px solid rgba(148,163,184,.22);
  border-radius: var(--r-2xl);
  box-shadow: 0 25px 60px -12px rgba(0,0,0,.5), 0 0 70px -18px color-mix(in srgb, var(--primary-2) 40%, transparent);
  padding: 36px 32px;
  color: #fff;
}
.auth__title { font-size: 22px; font-weight: 700; color: #fff; margin-bottom: 6px; letter-spacing: -.02em; }
.auth__subtitle { color: rgba(255,255,255,.62); font-size: 14px; margin-bottom: 28px; }

.auth__form .label { color: rgba(255,255,255,.8); }
.auth__form .input {
  background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.16); color: #fff;
}
.auth__form .input::placeholder { color: rgba(255,255,255,.35); }
.auth__form .input:hover { border-color: rgba(255,255,255,.28); }
.auth__form .input:focus {
  border-color: var(--primary-2);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary-2) 30%, transparent);
}
.auth__form .input.is-invalid { border-color: #fca5a5; }
.auth__form .input-search > .ico { color: rgba(255,255,255,.45); }
.auth__form .input-icon-btn { color: rgba(255,255,255,.55); }
.auth__form .input-icon-btn:hover { color: #fff; background: rgba(255,255,255,.1); }
.auth__form .error-text { color: #fca5a5; }
.auth__form .input:disabled { opacity: .5; cursor: not-allowed; }
.auth__form .input-icon-btn:disabled { opacity: .35; cursor: not-allowed; pointer-events: none; }
.auth__form .btn[disabled] { pointer-events: none; }

/* The plain (non--primary) .btn variant - "Cek Koneksi" on the db-recovery
   screen is the first place this card needs one. Same gap every other
   shared control here has: the base rule reads var(--surface)/var(--text),
   which is near-white-on-white against this permanently dark card whenever
   the app's own light/dark toggle is in light mode. */
.auth__form .btn { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.16); color: #fff; }
.auth__form .btn:hover { background: rgba(255,255,255,.12); border-color: rgba(255,255,255,.28); color: #fff; }

/* Lockout banner: same translucent-on-glass idiom as .auth__hint below, in a
   warm amber accent instead of neutral white so it reads as a warning at a
   glance - hardcoded rather than the --warn token since this card ignores
   the light/dark toggle the token would otherwise follow. */
.auth__lockout {
  display: flex; gap: 10px; align-items: flex-start;
  margin-bottom: 20px; padding: 12px 14px;
  background: rgba(251,191,36,.12);
  border: 1px solid rgba(251,191,36,.32);
  border-radius: var(--r);
  color: #fde68a;
  font-size: 13px; line-height: 1.55;
}
.auth__lockout .ico { flex: none; margin-top: 1px; color: #fbbf24; }
.auth__lockout strong { color: #fff; font-variant-numeric: tabular-nums; }

/* "Ingat saya": no box - just the checkbox and its label sitting directly on
   the card, unlike the bordered .check component used elsewhere in the app.
   A login form's remember-me row reads better as a plain inline option than
   as its own little panel. */
.auth__remember {
  display: flex; align-items: center; gap: .5rem;
  margin-bottom: 4px; cursor: pointer;
  font-size: 13px; font-weight: 550; color: #fff;
}
.auth__remember input {
  width: 16px; height: 16px; flex: none;
  accent-color: var(--primary-2); cursor: pointer;
}
.auth__remember:has(input:disabled) { opacity: .5; cursor: not-allowed; }
.auth__remember:has(input:disabled) input { cursor: not-allowed; }

/* Text captcha: the SVG (drawn server-side, see Captcha::renderSvg) sits as
   the .input-group's right-hand segment, right beside the answer field
   instead of on a banner row of its own - and doubles as its own refresh
   control (it is a link back to the login screen, which draws a new one). */
.auth__captcha-addon {
  flex: none; width: 128px; height: 40px; padding: 0; margin: 0;
  display: block; line-height: 0; overflow: hidden;
  border: 1px solid var(--border-strong); border-left: 0;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: none; cursor: pointer;
}
.auth__captcha-addon svg { display: block; width: 100%; height: 100%; }
.auth__captcha-addon.is-loading { opacity: .5; pointer-events: none; }
.auth__form .auth__captcha-addon { border-color: rgba(255,255,255,.16); }

.auth__hint {
  margin-top: 22px; padding: 12px 14px; border-radius: var(--r);
  background: rgba(255,255,255,.06); font-size: 12.5px; color: rgba(255,255,255,.75); line-height: 1.6;
}
.auth__hint code { font-family: var(--mono); background: rgba(255,255,255,.1); padding: 1px 6px; border-radius: 5px; border: 1px solid rgba(255,255,255,.16); color: #fff; }

.auth__foot { font-size: 12px; color: rgba(255,255,255,.5); text-align: center; }

/* =====================================================================
   Print (invoice / receipt)
   ===================================================================== */
.paper {
  max-width: 800px; margin: 28px auto; background: #fff; color: #111827;
  border-radius: var(--r-lg); box-shadow: var(--shadow-md); padding: 44px;
}
.paper__head { display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap; padding-bottom: 24px; border-bottom: 2px solid #eef0f5; }
.paper__brand-name { font-size: 19px; font-weight: 700; color: #0f172a; }
.paper__brand-meta { font-size: 12.5px; color: #64748b; line-height: 1.65; margin-top: 5px; }
.paper__doc { text-align: right; }
.paper__doc-type { font-size: 25px; font-weight: 700; letter-spacing: -.03em; color: #4f46e5; }
.paper__doc-no { font-family: var(--mono); font-size: 13px; color: #475569; margin-top: 3px; }
.paper__parties { display: flex; justify-content: space-between; gap: 32px; flex-wrap: wrap; margin: 26px 0; }
.paper__party-label { font-size: 10.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: #94a3b8; margin-bottom: 7px; }
.paper__party-name { font-size: 15px; font-weight: 650; color: #0f172a; }
.paper__party-meta { font-size: 12.5px; color: #64748b; line-height: 1.65; margin-top: 3px; }
.paper table { width: 100%; border-collapse: collapse; font-size: 13px; }
.paper table th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: #64748b;
                  padding: 10px 12px; background: #f8fafc; border-bottom: 1px solid #e2e8f0; }
.paper table td { padding: 12px; border-bottom: 1px solid #f1f5f9; color: #1e293b; }
.paper__totals { margin-left: auto; width: 100%; max-width: 300px; margin-top: 18px; }
.paper__total-row { display: flex; justify-content: space-between; padding: 7px 0; font-size: 13px; color: #475569; }
.paper__total-row--grand {
  border-top: 2px solid #e2e8f0; margin-top: 7px; padding-top: 12px;
  font-size: 17px; font-weight: 700; color: #0f172a;
}
.paper__stamp {
  display: inline-flex; align-items: center; gap: 8px; margin-top: 22px;
  padding: 9px 16px; border: 2px solid; border-radius: 10px;
  font-weight: 700; font-size: 13px; letter-spacing: .04em; text-transform: uppercase;
}
.paper__stamp--paid   { color: #059669; border-color: #059669; background: #ecfdf5; }
.paper__stamp--unpaid { color: #dc2626; border-color: #dc2626; background: #fef2f2; }
.paper__foot { margin-top: 32px; padding-top: 20px; border-top: 1px solid #eef0f5; font-size: 12px; color: #64748b; line-height: 1.7; }
.paper__sign { display: flex; justify-content: space-between; margin-top: 40px; gap: 40px; }
.paper__sign-box { text-align: center; min-width: 175px; }
.paper__sign-line { border-bottom: 1px solid #cbd5e1; height: 62px; margin-bottom: 7px; }

.print-bar {
  max-width: 800px; margin: 20px auto 0; display: flex; gap: 8px; justify-content: flex-end; padding: 0 20px;
}

@media print {
  .print-bar, .no-print { display: none !important; }
  body { background: #fff; }
  .paper { box-shadow: none; margin: 0; max-width: none; padding: 0; border-radius: 0; }
  @page { margin: 15mm; }
}

/* =====================================================================
   Responsive
   ===================================================================== */
/* The drawer's own show/hide transform, visibility and backdrop are all
   Bootstrap's offcanvas-lg media query now (bootstrap.min.css) - nothing to
   restate here. This block keeps only what was never part of that: the main
   content reflowing to full width, and the topbar toggle button's own
   layout. 991.98px (not 1024px, and not a plain 992px either) matches
   Bootstrap's own offcanvas-lg breakpoint exactly, .98 included - Bootstrap
   uses that fractional shave throughout its own breakpoint media queries to
   dodge subpixel rounding making a max-width and a min-width query both
   match (or both miss) at the exact boundary value. Any mismatch here would
   open a window where the sidebar is already permanent but .main hasn't
   made room for it yet, or the reverse. */
@media (max-width: 991.98px) {
  .main { margin-left: 0; }
  .topbar__toggle { display: grid; place-items: center; }
  .content { padding: 18px; }
}

@media (max-width: 700px) {
  :root { --topbar-h: 58px; }
  .content { padding: 14px; }
  h1 { font-size: 19px; }
  .page-head { margin-bottom: 16px; }
  .page-head__actions { width: 100%; }
  .page-head__actions .btn { flex: 1; }
  .grid { gap: 12px; }
  .grid--stats { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
  .stat { padding: 14px; }
  .stat__value { font-size: 20px; }
  .card__body { padding: 14px; }
  .card__head { padding: 13px 14px; }
  .table th, .table td { padding: 10px 12px; }
  .filters { padding: 11px 12px; }
  .filters .select { flex: 1 1 130px; min-width: 0; }
  .paper { padding: 22px; margin: 12px; }
  .notifs { left: 12px; right: 12px; max-width: none; top: 12px; }
  .modal__box { max-width: none; }
  .hide-sm { display: none !important; }
  .auth { padding: 22px 14px; }
  .auth__form { padding: 26px 20px; border-radius: var(--r-lg); }
}

@media (max-width: 480px) {
  .hide-xs { display: none !important; }
  .grid--stats { grid-template-columns: 1fr 1fr; }
}
