/* ── Reset & Base ─────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* ── Theme toggle circle-reveal (theme-toggle.js) ─────
   Disables the browser's own default crossfade animation on the View
   Transition snapshots — without this, the built-in fade would run at the
   same time as, and visually fight, the custom clip-path circle animation
   theme-toggle.js drives by hand via the Web Animations API. The old
   snapshot stays static underneath (z-index 1) while the new one expands
   its clip-path circle on top (z-index 2), revealing the new theme through
   a growing circle instead of the old screenshot just fading out in place. */
::view-transition-old(root), ::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}
::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 2; }

:root {
  /* Tells the browser to render native form controls (number-input spinner
     arrows, checkboxes, scrollbars) in a dark-appropriate palette — the
     standards-based fix for dark-on-dark spinner arrows, instead of a
     browser-specific filter hack on ::-webkit-inner-spin-button. */
  color-scheme: dark;
  /* Backgrounds — neutral gray-blue, not deep blue-black */
  --bg:          #191B1F;
  --surface:     #202226;
  --surface2:    #26282D;
  --surface3:    #2E3136;
  --border:      #383B41;
  --border-l:    #46494F;

  /* Primary — neutral blue */
  --primary:     #4C9AFF;
  --primary-d:   #3884E5;
  --primary-l:   #7CB6FF;
  --primary-glow:rgba(76,154,255,0.25);

  /* Accent — muted rose (used sparingly) */
  --pink:        #C77DAF;
  --pink-glow:   rgba(199,125,175,0.18);

  /* Extra accents */
  --cyan:        #4FD1E0;
  --green:       #00D68F;
  --amber:       #FFB703;

  /* Text */
  --text:        #E8EAED;
  --subtext:     #9098A3;
  --muted:       #5C6470;

  /* Semantic */
  --success:     #00D68F;
  --danger:      #FF4D6D;
  --warning:     #FFB703;

  /* Layout */
  --sidebar-w:   256px;
  --sidebar-collapsed-w: 76px;
  --header-h:    72px;
  /* Single source of truth for .sidebar-user's row height, so anything
     elsewhere that needs to visually line up with it (e.g. billing.css's
     .billing-tabs) reads this instead of re-measuring and hardcoding its
     own copy of the number — the two drifted by a hair (68.98px vs 69px,
     a font-metric rounding difference, not a real CSS mismatch) the last
     time this was done as two independent hardcoded values. */
  --sidebar-user-h: 69px;
  --radius:      12px;
  --radius-sm:   8px;
  --radius-lg:   18px;

  --sidebar-bg:  linear-gradient(180deg, #17181C 0%, #131417 100%);
  --topbar-bg:   rgba(25,27,31,0.75);
}

/* ── Light theme ──────────────────────────────────────
   Overrides the same token set above; nearly everything in this
   stylesheet already reads color via var(--x), so redefining the
   tokens here is what makes the toggle work app-wide. The sidebar now
   goes light too (a reversal of an earlier "keep it dark" design
   call, per explicit direction that light mode should be light
   everywhere) — its logo is a solid white-on-transparent PNG, so it
   gets an `invert(1)` filter below rather than a second baked asset,
   since inverting a genuinely monochrome image is exact, not an
   approximation. */
:root[data-theme="light"] {
  color-scheme: light;
  --bg:          #F4F5F7;
  --surface:     #FFFFFF;
  --surface2:    #F0F1F4;
  --surface3:    #E7E9ED;
  --border:      #DFE1E6;
  --border-l:    #CCD0D6;

  --primary:     #2F6FE4;
  --primary-d:   #1F56C4;
  --primary-l:   #4C86E8;
  --primary-glow:rgba(47,111,228,0.15);

  --pink:        #B25690;
  --pink-glow:   rgba(178,86,144,0.15);

  --cyan:        #1D93A0;
  --green:       #00A876;
  --amber:       #B8790A;

  --text:        #181A1F;
  --subtext:     #565C66;
  --muted:       #868C97;

  --success:     #00A876;
  --danger:      #E0324F;
  --warning:     #B8790A;

  --sidebar-bg:  linear-gradient(180deg, #FFFFFF 0%, #F7F8FA 100%);
  --topbar-bg:   rgba(244,245,247,0.75);
}

:root[data-theme="light"] .sidebar-logo img { filter: invert(1); }

:root[data-theme="light"] .client-dropdown,
:root[data-theme="light"] .product-dropdown {
  background: var(--surface);
  border-color: var(--border);
  box-shadow: 0 8px 24px rgba(24,26,31,0.12);
}
:root[data-theme="light"] .client-dropdown .pd-item,
:root[data-theme="light"] .pd-item {
  border-bottom-color: var(--border);
}

html { font-size: 16px; }

body {
  font-family: 'Inter', Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a { color: var(--primary-l); text-decoration: none; }
a:hover { color: #A0AFFF; }

/* ── Layout ───────────────────────────────────────── */
.app-wrapper {
  display: flex;
  min-height: 100vh;
}

/* ── Sidebar ──────────────────────────────────────── */
.sidebar {
  width: var(--sidebar-w);
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0; left: 0; bottom: 0;
  z-index: 100;
  /* overflow:visible (not auto/hidden) — CSS overflow-x/-y can't independently
     be "clip-y, visible-x" on one box (per spec, a non-visible value on one
     axis forces the other to 'auto' too), which would silently clip every
     collapsed-rail hover tooltip below (they render via ::after positioned
     past this element's own width) — .sidebar-nav's own nav-link tooltips
     included, so this can't be pushed onto a nested scroll wrapper either.
     No scroll fallback is kept: this app's nav is a small fixed item list
     (never user-generated/growing) that comfortably fits any real viewport
     height, so the vertical auto-scroll this replaces was never actually
     load-bearing. */
  overflow: visible;
  /* ease-out (fast start, smooth deceleration into rest) rather than the
     symmetric-ish cubic-bezier(0.4,0,0.2,1) this — and the other 4
     collapse/expand transitions below (.sidebar-logo padding,
     .sidebar-logo-mark width, .sidebar-user padding, .main-content
     margin-left) — used before, per explicit request for a smoother feel.
     The MOBILE drawer's own slide transform (further down this file) is a
     different interaction (full-screen overlay, not this width collapse)
     and was deliberately left on its own curve. */
  transition: width 0.28s ease-out;
}

/* ── Collapsed (icon-only) sidebar — desktop only; mobile's off-canvas
   drawer (the @media max-width:768px block further down) is a completely
   separate mechanism and this never applies there. Persisted server-side
   (users.sidebar_collapsed, same pattern as theme_preference) so the class
   is already correct in the very first HTML byte — no flash of the wrong
   width on a fresh page load, which a client-only (localStorage) toggle
   can't avoid in a multi-page app like this one. ────────────────────── */
.sidebar.collapsed { width: var(--sidebar-collapsed-w); }

/* ── Sidebar footer / collapse toggle — lives after .sidebar-nav in the
   markup, so it lands at the bottom for free (.sidebar-nav already has
   flex:1 inside .sidebar's flex column, no margin-top:auto trick needed).
   Styled as a normal-flow row matching .nav-link's own collapse treatment
   (centered icon-only + hover tooltip when collapsed) instead of the old
   absolutely-positioned floating circle, for visual consistency with the
   nav items directly above it. */
.sidebar-footer {
  border-top: 1px solid var(--border);
  padding: 10px;
  flex-shrink: 0;
}
.sidebar-collapse-btn {
  width: 100%;
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: var(--radius-sm);
  background: none; border: none; color: var(--subtext); cursor: pointer;
  font: inherit; font-size: 15px; font-weight: 500; text-align: left;
  transition: all 0.15s ease;
}
.sidebar-collapse-btn:hover { background: var(--primary-glow); color: var(--text); }
.sidebar-collapse-btn .icon { width: 18px; height: 18px; flex-shrink: 0; opacity: 0.8; }
.sidebar-collapse-btn:hover .icon { opacity: 1; }
.sidebar-collapse-label { white-space: nowrap; transition: opacity 0.15s ease; }

.sidebar.collapsed .sidebar-collapse-btn {
  justify-content: center; gap: 0; padding: 9px 0; position: relative;
}
.sidebar.collapsed .sidebar-collapse-label { opacity: 0; width: 0; overflow: hidden; }
/* Same size bump + hover tooltip treatment as collapsed .nav-icon/.nav-link
   below, so the footer toggle reads as part of the same icon-only rail. */
.sidebar.collapsed .sidebar-collapse-btn .icon { width: 20px; height: 20px; }
.sidebar.collapsed .sidebar-collapse-btn:hover::after {
  content: attr(data-label);
  position: absolute; left: calc(100% + 12px); top: 50%; transform: translateY(-50%);
  background: var(--surface3); color: var(--text); font-size: 12.5px; font-weight: 600;
  padding: 6px 11px; border-radius: 6px; white-space: nowrap;
  box-shadow: 0 6px 18px rgba(0,0,0,0.35); border: 1px solid var(--border-l);
  z-index: 200; pointer-events: none;
}

/* Same collapsed-rail tooltip treatment as .nav-link/.sidebar-collapse-btn
   above, extended to the two icon-only sidebar-user buttons that were
   previously left with only a native (unstyled) title="" tooltip.
   .notif-bell-btn already carries position:relative for its unread badge;
   .profile-trigger-btn needs it added here, scoped to collapsed only. */
.sidebar.collapsed .profile-trigger-btn { position: relative; }
.sidebar.collapsed .profile-trigger-btn:hover::after,
.sidebar.collapsed .notif-bell-btn:hover::after {
  content: attr(data-label);
  position: absolute; left: calc(100% + 12px); top: 50%; transform: translateY(-50%);
  background: var(--surface3); color: var(--text); font-size: 12.5px; font-weight: 600;
  padding: 6px 11px; border-radius: 6px; white-space: nowrap;
  box-shadow: 0 6px 18px rgba(0,0,0,0.35); border: 1px solid var(--border-l);
  z-index: 200; pointer-events: none;
}

/* Height deliberately matches the plain .topbar's exact 72px
   (14 padding-top + 43 logo + 14 padding-bottom + 1 border = 72), so the
   sidebar and the page header line up on every module. Previously this
   section was content-driven (22/18 padding + a width:100%-scaled image)
   and rendered ~16px taller than the topbar — a real, page-independent
   misalignment, not specific to any one module's header. */
.sidebar-logo {
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  position: relative;
  transition: padding 0.28s ease-out;
}
/* A perfect square, edge-to-edge, matching --header-h exactly (not
   --sidebar-collapsed-w) so the border line directly beneath it lands at
   the same y-coordinate as .topbar's own bottom border on the right —
   every horizontal divider on the page lines up. Zero padding: the image
   fills the square as a true background/tile, not a smaller icon floating
   inside a larger dark box (that was the earlier, corrected attempt). The
   square sits centered in the 76px-wide collapsed rail, so a ~2px sliver
   of the rail's own background shows on each side — an accepted tradeoff
   of "perfect square + exact header height" both holding at once, given
   the rail itself (76px) and header height (72px) aren't equal. */
.sidebar.collapsed .sidebar-logo { padding: 0; height: var(--header-h); justify-content: center; align-items: center; }

/* The crop window around the logo — its own element (not the <img> itself)
   so collapsing can shrink JUST the visible window while the image keeps
   rendering at full natural size underneath, revealing only its left
   edge. Rocketship's own logo happens to be a wide horizontal lockup with
   a distinct icon mark on the left followed by a gap before the wordmark
   starts — the same "crop the existing asset" trick works for any
   custom-uploaded logo with a similar icon-then-wordmark layout, and
   degrades to "shows whatever's on the left edge" for one that isn't,
   which is still reasonable (a solid brand color/first letter typically). */
.sidebar-logo-mark {
  display: block;
  overflow: hidden;
  transition: width 0.28s ease-out;
}
.sidebar.collapsed .sidebar-logo-mark { width: var(--header-h); height: var(--header-h); border-radius: 0; }

/* Two logo spans now sit side by side in the markup — the expanded (full
   wordmark) one is what actually shows day to day; the collapsed one only
   appears once .sidebar carries .collapsed. Toggled the same way the
   collapse button's own two icon spans already are, just via a CSS class
   instead of inline style (no client-side src-swap needed either way,
   since both images are always present in the DOM). */
.sidebar-logo-mark-collapsed { display: none; }
.sidebar.collapsed .sidebar-logo-mark-expanded { display: none; }
.sidebar.collapsed .sidebar-logo-mark-collapsed { display: block; }

/* Both the real-uploaded-square-asset case and the fallback (cropping the
   same wide wordmark image used expanded) now use object-fit: cover — the
   explicit ask was for the logo to read as a background image filling the
   square, not a smaller icon letterboxed inside it, even if that means
   cropping a real uploaded square asset slightly on a non-square upload.
   object-position: left on the fallback keeps the crop anchored on the
   wordmark's own icon mark (its left edge) rather than centering into
   whatever the image's literal middle happens to be — usually blank space
   or the start of the wordmark text for a left-icon-then-text lockup. */
.sidebar.collapsed .sidebar-logo-mark-collapsed img {
  width: 100%; height: 100%; max-width: none; max-height: none; margin: 0;
  object-fit: cover; object-position: left center;
}

.sidebar-logo-mark img {
  /* max-height (not width:100%;height:auto) also closes a latent bug: a
     custom-uploaded logo with a different aspect ratio than the default
     would previously render at a different height per-tenant, silently
     reintroducing this same misalignment. Capping height keeps every
     uploaded logo — whatever its own aspect ratio — within the same
     72px-tall section. */
  max-height: 43px;
  max-width: 100%;
  width: auto;
  height: auto;
  display: block;
  opacity: 0.92;
}

.sidebar-user {
  padding: 14px 18px;
  min-height: var(--sidebar-user-h);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 12px;
  position: relative;
  transition: padding 0.28s ease-out;
}
/* Collapsed: bell stacks above the avatar (a column instead of a row) —
   reordered visually via `order` rather than moving its markup, since the
   bell comes after the profile trigger in the DOM and this is the smallest
   change that flips their visual order only in this one state. */
.sidebar.collapsed .sidebar-user { padding: 14px 10px; justify-content: center; flex-direction: column; gap: 8px; }
.sidebar.collapsed .sidebar-user-info,
.sidebar.collapsed .profile-trigger-chevron { display: none; }
.sidebar.collapsed .profile-trigger-btn { flex: none; }
.sidebar.collapsed .notif-bell-btn { margin-left: 0; order: -1; }

.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--primary), var(--pink));
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 14px;
  color: #fff;
  flex-shrink: 0;
  overflow: hidden;
}

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

.sidebar-user-info .name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 160px;
}

.sidebar-user-info .role-badge {
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.8px;
  font-weight: 600;
}

/* ── Notification bell (global — appears in both sidebars) ────── */
.notif-bell-btn {
  position: relative; margin-left: auto; background: none; border: none;
  color: var(--subtext); cursor: pointer; width: 32px; height: 32px;
  display: flex; align-items: center; justify-content: center; border-radius: var(--radius-sm);
  font-size: 16px; flex-shrink: 0;
}
.notif-bell-btn:hover { background: var(--surface2); color: var(--text); }

/* A second trigger for the SAME #notifDropdown panel, inserted into the
   mobile topbar's row 1 (next to the hamburger) by the sidebar partials'
   shared script — the sidebar's own bell (which this duplicates) is
   off-canvas on mobile until the hamburger drawer is opened, which used to
   mean notifications were a two-tap flow on a phone. Hidden entirely on
   desktop, where the sidebar's own bell is already visible. */
.notif-bell-btn-mobile { display: none; }

/* Mobile-only Search icon button in a topbar's .topbar-actions row (Billing
   list pages) — reuses whatever search the page already has (here,
   window.openBillingSearch(), already loaded by partials/billing-tabs.ejs
   on every list page) rather than being a new search implementation.
   Hidden on desktop, where that page's own search entry point (the
   billing-tabs sub-nav trigger) is already visible in the page body. */
.topbar-mobile-search-btn { display: none; }
.mobile-fab { display: none; }
.mobile-save-fab { display: none; }

/* One-time attention-getter on page load when there's something unread —
   not a repeating/looping shake, which would just be an annoying tic every
   time someone glances at the sidebar. Rotation-based (not translate) so it
   reads as the bell's own clapper swinging, not the whole button sliding. */
@keyframes bell-shake {
  0%, 100% { transform: rotate(0deg); }
  15% { transform: rotate(14deg); }
  30% { transform: rotate(-12deg); }
  45% { transform: rotate(9deg); }
  60% { transform: rotate(-7deg); }
  75% { transform: rotate(4deg); }
  90% { transform: rotate(-2deg); }
}
.notif-bell-btn.shake-once { animation: bell-shake 0.7s ease-in-out; transform-origin: 50% 0%; }
.notif-bell-badge {
  position: absolute; top: 2px; right: 2px;
  background: var(--danger); color: #fff; font-size: 10px; font-weight: 700;
  min-width: 15px; height: 15px; border-radius: 8px; padding: 0 3px;
  display: flex; align-items: center; justify-content: center; line-height: 1;
}
.notif-dropdown {
  position: fixed; z-index: 300;
  background: var(--surface2); border: 1px solid var(--border-l); border-radius: var(--radius);
  width: 320px; max-width: calc(100vw - 40px); max-height: 420px; overflow: hidden;
  display: none; flex-direction: column;
  box-shadow: 0 20px 48px rgba(0,0,0,0.5);
}
.notif-dropdown.open { display: flex; }
.notif-dropdown-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 14px; border-bottom: 1px solid var(--border); font-size: 14px; font-weight: 700; color: var(--text);
}
.notif-mark-all-btn { background: none; border: none; color: var(--primary-l); font-size: 12px; font-weight: 600; cursor: pointer; }
.notif-mark-all-btn:hover { color: var(--primary); }
.notif-list { overflow-y: auto; flex: 1; }
.notif-empty { padding: 28px 16px; text-align: center; font-size: 13px; color: var(--muted); }
.notif-item {
  display: block; padding: 11px 14px; border-bottom: 1px solid var(--border);
  color: var(--text); font-size: 13.5px; line-height: 1.5; cursor: pointer; text-decoration: none;
}
.notif-item:hover { background: var(--surface3); color: var(--text); }
.notif-item.unread { background: rgba(76,154,255,0.06); }
.notif-item.unread::before {
  content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--primary); margin-right: 6px; vertical-align: middle;
}
.notif-item-time { font-size: 11px; color: var(--muted); margin-top: 3px; display: block; }

/* ── Profile dropdown (clicking the avatar/name in the sidebar) ── */
.profile-trigger-btn {
  display: flex; align-items: center; gap: 12px; background: none; border: none; padding: 0;
  cursor: pointer; text-align: left; flex: 1; min-width: 0; color: inherit; font: inherit;
}
.profile-trigger-chevron { font-size: 11px; color: var(--muted); flex-shrink: 0; margin-left: 2px; }
.profile-dropdown {
  position: fixed; z-index: 300;
  background: var(--surface2); border: 1px solid var(--border-l); border-radius: var(--radius);
  width: 220px; padding: 6px; display: none; flex-direction: column; gap: 2px;
  box-shadow: 0 20px 48px rgba(0,0,0,0.5);
}
.profile-dropdown.open { display: flex; }
.profile-dropdown-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: var(--radius-sm);
  color: var(--subtext); font-size: 14px; font-weight: 500; text-decoration: none;
}
.profile-dropdown-item:hover { background: var(--surface3); color: var(--text); }
.profile-dropdown-item.danger { color: var(--danger); }
.profile-dropdown-item.danger:hover { background: rgba(255,77,109,0.1); color: var(--danger); }
/* Reset for the theme-toggle item, which is a <button> (it performs an
   in-place action, not a navigation) rather than the <a> every other
   dropdown item is. */
button.profile-dropdown-item { background: none; border: none; width: 100%; text-align: left; font: inherit; cursor: pointer; }
#themeIconSun { color: #F5A623; }
#themeIconMoon { color: var(--primary-l); }
.profile-dropdown-divider { height: 1px; background: var(--border); margin: 4px 2px; }

.sidebar-nav {
  flex: 1;
  padding: 14px 10px;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  color: var(--subtext);
  font-size: 15px;
  font-weight: 500;
  transition: all 0.15s ease;
  margin-bottom: 4px;
}
.nav-link:last-child { margin-bottom: 0; }

.nav-label { white-space: nowrap; transition: opacity 0.15s ease; }

/* ── Boards workspace list — nested under the "Boards" nav link, only
   rendered (server-side) while currentPath is under /projects, so it never
   shows on any other module's pages. A plain text list, not the icon-tile
   rail this replaced. */
.sidebar-workspace-list {
  display: flex; flex-direction: column; gap: 1px;
  margin: 0 0 4px 0; padding-left: 30px;
}
.sidebar-workspace-item {
  display: block; padding: 6px 10px; border-radius: var(--radius-sm);
  color: var(--muted); font-size: 13px; font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background 0.15s, color 0.15s;
}
.sidebar-workspace-item:hover { background: var(--surface2); color: var(--text); }
.sidebar-workspace-item.active { color: var(--primary-l); font-weight: 600; }
.sidebar.collapsed .sidebar-workspace-list { display: none; }
/* A board tile or category dragged from the boards grid, hovering over a
   sidebar workspace entry as a drop target — see sidebar-workspaces.js. */
.sidebar-workspace-item.drop-hover {
  background: var(--primary); color: #fff; outline: 2px dashed var(--primary-l); outline-offset: -2px;
}

/* ── HR sub-nav — nested under the "HR" nav link (Payroll/People/Leave
   Requests), same shape as .sidebar-workspace-list above: a plain text
   list, server-rendered only while currentPath is under one of those three
   routes, so it never shows on any other module's pages. */
.sidebar-hr-list {
  display: flex; flex-direction: column; gap: 1px;
  margin: 0 0 4px 0; padding-left: 30px;
}
.sidebar-hr-item {
  display: block; padding: 6px 10px; border-radius: var(--radius-sm);
  color: var(--muted); font-size: 13px; font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background 0.15s, color 0.15s;
}
.sidebar-hr-item:hover { background: var(--surface2); color: var(--text); }
.sidebar-hr-item.active { color: var(--primary-l); font-weight: 600; }
.sidebar.collapsed .sidebar-hr-list { display: none; }

/* ── Billing sub-nav — nested under the "Billing" nav link (Quotes/
   Invoices/Recurring Invoices/Clients/Products), same shape as
   .sidebar-hr-list above: a plain text list, server-rendered only while
   currentPath is under one of those five routes. */
.sidebar-billing-list {
  display: flex; flex-direction: column; gap: 1px;
  margin: 0 0 4px 0; padding-left: 30px;
}
.sidebar-billing-item {
  display: block; padding: 6px 10px; border-radius: var(--radius-sm);
  color: var(--muted); font-size: 13px; font-weight: 500;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background 0.15s, color 0.15s;
}
.sidebar-billing-item:hover { background: var(--surface2); color: var(--text); }
.sidebar-billing-item.active { color: var(--primary-l); font-weight: 600; }
.sidebar.collapsed .sidebar-billing-list { display: none; }

.sidebar.collapsed .nav-link { justify-content: center; gap: 0; padding: 9px 0; position: relative; }
.sidebar.collapsed .nav-label { opacity: 0; width: 0; overflow: hidden; }

/* Icon-only nav items lose their own visible label, so a hover tooltip
   (reading from the same text already in the link's own `data-label`
   attribute — kept in sync with the visible label by hand, same as any
   other duplicated a11y string in this app) replaces it rather than
   leaving icons ambiguous. CSS-only, no JS needed for the tooltip itself. */
.sidebar.collapsed .nav-link:hover::after {
  content: attr(data-label);
  position: absolute; left: calc(100% + 12px); top: 50%; transform: translateY(-50%);
  background: var(--surface3); color: var(--text); font-size: 12.5px; font-weight: 600;
  padding: 6px 11px; border-radius: 6px; white-space: nowrap;
  box-shadow: 0 6px 18px rgba(0,0,0,0.35); border: 1px solid var(--border-l);
  z-index: 200; pointer-events: none;
}

/* Base class for every inline Lucide SVG icon (config/icons.js) — stroke
   color inherits via currentColor, so existing `color`/hover/active rules
   already written for the old Font Awesome icons keep working unchanged. */
.icon { display: inline-block; vertical-align: middle; flex-shrink: 0; }

.nav-icon {
  width: 18px;
  text-align: center;
  flex-shrink: 0;
  opacity: 0.8;
  /* Solid light-blue (matches the brand-accent color already used for
     .topbar-title heading icons app-wide, e.g. `color:var(--primary)`
     inline styles) — a two-color gradient stroke was tried and reverted
     per explicit feedback: it didn't read well, and inconsistently failed
     to render at all for some icons (confirmed live — the Boards nav item
     lost its icon entirely). currentColor (which .icon's own comment
     describes as the default) is deliberately overridden here so nav
     icons stay this fixed color regardless of .nav-link's own text color
     in any state. */
  color: var(--primary);
}

/* Collapsed rail is icon-only real estate — a couple extra px keeps icons
   legible without the label text they lose in this state. */
.sidebar.collapsed .nav-icon { width: 20px; height: 20px; }

.nav-link:hover {
  background: var(--primary-glow);
  color: var(--text);
}

.nav-link.active {
  background: linear-gradient(135deg, var(--primary-glow), var(--pink-glow));
  color: var(--text);
  font-weight: 600;
}

.nav-link.active .nav-icon,
.nav-link:hover .nav-icon {
  opacity: 1;
}

.sidebar-logo a {
  display: block;
  line-height: 0;
}

/* ── Main Content ─────────────────────────────────── */
.main-content {
  margin-left: var(--sidebar-w);
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  transition: margin-left 0.28s ease-out;
}
/* .sidebar and .main-content are siblings inside .app-wrapper — the general
   sibling combinator keeps this a single-source-of-truth toggle (just the
   .collapsed class on #sidebar) instead of needing a second class kept in
   sync on a separate wrapper element. */
.sidebar.collapsed ~ .main-content { margin-left: var(--sidebar-collapsed-w); }

.topbar {
  height: var(--header-h);
  background: var(--topbar-bg);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 28px;
  position: sticky;
  top: 0;
  z-index: 50;
}

.topbar-title {
  font-size: 29px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.5px;
}

@media (min-width: 769px) {
  .topbar-title { font-size: 37px; }
}

/* Page-title Lucide icons size themselves off the heading's own font-size
   (1em) instead of the icon() helper's hardcoded 18px default — matching
   how Google Drive's un-migrated Font Awesome heading icon has always sized
   itself (it has no explicit font-size, so it just inherits .topbar-title's).
   One rule here keeps every Billing/Settings/native-page heading icon in
   sync with that same reference size at both breakpoints automatically,
   instead of hand-passing a `size` option to dozens of individual icon()
   call sites. */
.topbar-title .icon { width: 1em; height: 1em; }

/* Small, low-contrast document-number badge next to a Quote/Invoice
   builder's title (e.g. "New Quote  QUOTE-6368 (next)") — the number is
   real, useful context but purely informational (auto-assigned
   server-side, never itself an editable field), so it reads as a quiet
   aside next to the title rather than competing with it. Sized off rem,
   not .topbar-title's own em-based icon sizing, since it should NOT scale
   up at the desktop breakpoint the way the heading text and its icon do —
   it's a fixed-size label at any viewport. */
.topbar-title-badge {
  font-family: 'Inter', sans-serif;
  font-size: 13px;
  font-weight: 500;
  color: var(--muted);
  letter-spacing: 0;
  margin-left: 10px;
  vertical-align: middle;
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.page-body {
  padding: 28px;
  flex: 1;
}

/* ── Auth Layout ──────────────────────────────────── */
.auth-wrapper {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  /* Plain background — the animated Aurora canvas (aurora-background.js) is
     the only visual effect here now, no static gradient underneath it. */
  background: var(--bg);
  padding: 24px;
}

.aurora-background {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.auth-card {
  position: relative;
  z-index: 1;
  /* Translucent glass effect — lets the animated Aurora background
     (aurora-background.js) show through softly rather than being fully
     hidden behind an opaque card. */
  background: rgba(32,34,38,0.55);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 44px;
  width: 100%;
  max-width: 420px;
  box-shadow: 0 0 80px rgba(76,154,255,0.07), 0 40px 80px rgba(0,0,0,0.4);
}

.auth-logo {
  text-align: center;
  margin-bottom: 36px;
}

.auth-logo img {
  width: 168px;
  height: auto;
  opacity: 0.92;
}

.auth-logo .brand {
  font-size: 18px;
  font-weight: 800;
  letter-spacing: 1px;
  background: linear-gradient(135deg, var(--primary-l), var(--pink));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.auth-logo .sub {
  font-size: 11px;
  font-weight: 700;
  color: var(--muted);
  letter-spacing: 2.5px;
  text-transform: uppercase;
  margin-top: 10px;
}

.auth-title {
  font-size: 23px;
  font-weight: 800;
  letter-spacing: -0.4px;
  margin-bottom: 6px;
}

.auth-subtitle {
  font-size: 14px;
  color: var(--subtext);
  margin-bottom: 28px;
  line-height: 1.5;
}

/* ── Cards ────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px;
  transition: border-color 0.2s;
}

.card:hover { border-color: var(--border-l); }

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 20px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}

.card-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.1px;
}

.card-subtitle {
  font-size: 13px;
  color: var(--subtext);
  margin-top: 2px;
}

/* ── Stat Cards ───────────────────────────────────── */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 16px;
  margin-bottom: 28px;
}

.stat-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 22px 24px;
  position: relative;
  overflow: hidden;
  transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
}

.stat-card:hover {
  border-color: var(--border-l);
  transform: translateY(-2px);
  box-shadow: 0 12px 32px rgba(0,0,0,0.3);
}


.stat-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 10px;
}

.stat-value {
  font-size: 31px;
  font-weight: 800;
  color: var(--text);
  line-height: 1;
  letter-spacing: -1.5px;
}

.stat-value.mono {
  font-size: 23px;
  font-family: 'Inter', sans-serif;
  font-weight: 500;
  letter-spacing: -0.5px;
}

.stat-sub {
  font-size: 12px;
  color: var(--muted);
  margin-top: 8px;
}

/* Small icon above the label — kept muted/monochromatic (not a per-metric
   rainbow) so it reads as a quiet visual anchor, never competing with the
   number itself for attention. */
.stat-icon {
  font-size: 15px;
  color: var(--muted);
  margin-bottom: 10px;
}

/* ── Admin Dashboard ──────────────────────────────────
   Previously an inline <style> block inside views/admin/dashboard.ejs,
   disconnected from this shared design-system file every other page's CSS
   lives in — moved here verbatim-plus-polish, same class names, no HTML
   restructuring needed beyond what's already changed for other reasons. */
.ad-hero {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 32px;
  margin-bottom: 24px;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  box-shadow: 0 8px 24px rgba(0,0,0,0.18);
}
.ad-hero-left { flex: 1; min-width: 220px; }
.ad-hero-eyebrow {
  font-size: 11px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase;
  color: var(--muted); margin-bottom: 10px;
}
.ad-hero-payroll {
  font-size: 46px; font-weight: 800; color: var(--text);
  font-family: 'Inter', sans-serif; letter-spacing: -2px; line-height: 1;
}
.ad-hero-sub { font-size: 14px; color: var(--subtext); margin-top: 8px; }
.ad-hero-actions { display: flex; gap: 10px; flex-wrap: wrap; flex-shrink: 0; margin-top: 20px; }

/* Pay date countdown */
.payday-badge {
  background: rgba(0,214,143,0.09);
  border: 1px solid rgba(0,214,143,0.2);
  border-radius: var(--radius);
  padding: 18px 24px;
  text-align: center;
  flex-shrink: 0;
}
.payday-badge .pb-num { font-size: 34px; font-weight: 800; color: var(--success); line-height: 1; }
.payday-badge .pb-label { font-size: 11px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); margin-top: 6px; }
.payday-badge .pb-date { font-size: 13px; color: var(--subtext); margin-top: 4px; }

/* This page's own stat-tile density — 4 KPIs read better as smaller,
   tighter tiles than the app-wide 210px default. Explicitly re-declared
   (not just overridden once) inside the mobile breakpoint below, so the
   deliberate 2-column mobile collapse every other stats-grid gets isn't
   silently skipped here — this used to be an inline style= attribute,
   which beats any stylesheet media query regardless of source order,
   meaning the app-wide mobile collapse never actually applied on this
   page at all. */
.dashboard-stats-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); margin-bottom: 24px; }

/* Hours/salary status rows (Freelancer Hours / Full-Time & Admins cards) */
.hs-row { display: flex; align-items: center; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--border); }
.hs-row:last-child { border-bottom: none; }
.hs-avatar {
  width: 36px; height: 36px; border-radius: 50%;
  background: linear-gradient(135deg, var(--primary), var(--pink));
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 700; color: #fff; flex-shrink: 0;
}
.hs-name { flex: 1; font-size: 14px; font-weight: 600; }
.hs-position { font-size: 12px; color: var(--muted); }
.hs-hours { font-size: 15px; font-weight: 700; font-family: 'Inter', sans-serif; flex-shrink: 0; }
.hs-pay { font-size: 13px; color: var(--primary-l); font-family: 'Inter', sans-serif; flex-shrink: 0; min-width: 90px; text-align: right; }
.hs-status { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.hs-status.ok { background: var(--success); box-shadow: 0 0 6px rgba(0,214,143,0.5); }
.hs-status.warn { background: var(--warning); box-shadow: 0 0 6px rgba(255,183,3,0.5); }
.hs-status.zero { background: var(--danger); box-shadow: 0 0 6px rgba(255,77,109,0.4); }

/* Two-col layout for the Freelancer Hours / Full-Time & Admins cards, and
   (reused) the executive Dashboard's chart/list widget cards below. */
.ad-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 900px) { .ad-grid { grid-template-columns: 1fr; } }

/* Executive Dashboard chart widgets (services/dashboardWidgets.js,
   public/js/dashboard-charts.js) — generic version of
   .billing-chart-wrap/.billing-chart-empty (public/css/billing.css), which
   stay billing-scoped since the Billing dashboard's own historical chart
   rendering still uses them directly inside dashboardWidgets.js's own
   query functions. */
.dash-chart-wrap { position: relative; height: 220px; }
.dash-chart-wrap canvas.hidden { display: none; }
.dash-chart-empty {
  display: flex; align-items: center; justify-content: center;
  height: 100%; color: var(--muted); font-size: 13px;
}
.dash-chart-empty.hidden { display: none; }

@media (max-width: 768px) {
  .ad-hero { padding: 20px; gap: 16px; }
  .ad-hero-payroll { font-size: 34px; letter-spacing: -1px; }
  .ad-hero-actions { width: 100%; }
  .ad-hero-actions .btn { flex: 1; justify-content: center; }
  .payday-badge { padding: 14px 18px; width: 100%; }
  .payday-badge .pb-num { font-size: 28px; }
  /* Explicit, not inherited — see the comment on .dashboard-stats-grid's
     base rule above for why this can't just be left to cascade order. */
  .stats-grid, .dashboard-stats-grid { grid-template-columns: 1fr 1fr; }
}

/* ── Buttons ──────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 20px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  font-weight: 600;
  font-family: 'Inter', sans-serif;
  /* Without this, a <button class="btn"> and an <a class="btn"> with
     identical padding/font-size render at different heights — browsers
     give form controls (<button>) their own UA-stylesheet line-height
     (commonly "normal", ~1.15) distinct from what an <a> inherits from
     the page's own line-height. Fixing it here (not per-instance) keeps
     every button/link styled as .btn uniformly tall app-wide. */
  line-height: 1;
  cursor: pointer;
  border: none;
  transition: all 0.15s ease;
  white-space: nowrap;
  text-decoration: none;
  letter-spacing: 0.1px;
}

.btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* Reusable small inline spinner for async button loading states (e.g.
   "Sending Payslips…") — a spinning ring built from a transparent top
   border, no image/icon asset needed. */
.btn-spinner {
  display: inline-block; width: 14px; height: 14px;
  border: 2px solid rgba(255,255,255,0.4); border-top-color: #fff;
  border-radius: 50%; animation: btn-spin 0.7s linear infinite;
  flex-shrink: 0;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

.btn-primary {
  background: linear-gradient(135deg, var(--primary) 0%, #7B5CEA 100%);
  color: #fff;
  box-shadow: 0 4px 14px rgba(76,154,255,0.3);
}
.btn-primary:hover {
  background: linear-gradient(135deg, var(--primary-l) 0%, #8B6CFF 100%);
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 6px 20px rgba(76,154,255,0.45);
}

.btn-pink {
  background: linear-gradient(135deg, var(--pink), #B84088);
  color: #fff;
  box-shadow: 0 4px 14px rgba(199,125,175,0.28);
}
.btn-pink:hover {
  opacity: 0.9;
  color: #fff;
  transform: translateY(-1px);
  box-shadow: 0 6px 20px rgba(199,125,175,0.4);
}

.btn-success {
  background: rgba(0,214,143,0.12);
  color: var(--success);
  border: 1px solid rgba(0,214,143,0.25);
}
.btn-success:hover { background: rgba(0,214,143,0.2); color: var(--success); }

.btn-danger {
  background: rgba(255,77,109,0.12);
  color: var(--danger);
  border: 1px solid rgba(255,77,109,0.25);
}
.btn-danger:hover { background: rgba(255,77,109,0.2); color: var(--danger); }

.btn-ghost {
  background: transparent;
  color: var(--subtext);
  border: 1px solid var(--border);
}
.btn-ghost:hover { background: var(--surface2); color: var(--text); border-color: var(--border-l); }

.btn-sm { padding: 7px 14px; font-size: 13px; }
.btn-lg { padding: 13px 30px; font-size: 15px; font-weight: 700; }
.btn-block { width: 100%; justify-content: center; }

/* ── Forms ────────────────────────────────────────── */
/* min-width:0 overrides the CSS Grid spec's implicit min-width:auto on
   grid items (.form-group sits inside .form-grid/.form-grid-3), which
   otherwise refuses to shrink a track below its content's own intrinsic
   minimum width — harmless at normal widths, but the standard root cause
   whenever a form field pushes its grid row wider than the viewport on a
   narrow phone (confirmed case: native <input type="date">, whose
   intrinsic minimum width in mobile Safari can exceed a squeezed grid
   track even though the same field renders fine in a desktop browser). */
.form-group { margin-bottom: 18px; min-width: 0; }

.form-label {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.8px;
  color: var(--subtext);
  text-transform: uppercase;
  margin-bottom: 6px;
}

.form-control {
  width: 100%;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 10px 14px;
  font-size: 15px;
  font-family: 'Inter', sans-serif;
  transition: border-color 0.15s, box-shadow 0.15s;
  outline: none;
}

.form-control:focus {
  border-color: var(--primary);
  background: var(--surface3);
  box-shadow: 0 0 0 3px rgba(76,154,255,0.1);
}

.form-control::placeholder { color: var(--muted); }
select.form-control option { background: var(--surface2); color: var(--text); }
textarea.form-control { resize: vertical; min-height: 80px; }

.form-hint { font-size: 12px; color: var(--muted); margin-top: 4px; }

/* ── Automation template placeholder chips ── */
.automation-chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.automation-chip-btn {
  background: var(--surface2); border: 1px solid var(--border); border-radius: 999px;
  color: var(--subtext); font-size: 12px; font-weight: 600; padding: 5px 12px;
  cursor: pointer; transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.automation-chip-btn:hover { background: var(--surface3); color: var(--text); border-color: var(--border-l); }

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }

@media (max-width: 640px) {
  .form-grid, .form-grid-3 { grid-template-columns: 1fr; }
  .form-grid [style*="grid-column: span 2"],
  .form-grid [style*="grid-column:span 2"] { grid-column: span 1; }
  /* Belt-and-suspenders alongside .form-group's min-width:0 above — native
     <input type="date"> (what flatpickr swaps to on touch devices, see
     invoice-builder.js's initBuilderDatePickers) has its own UA-stylesheet
     sizing in some mobile browsers that doesn't always defer to a percentage
     width the way a plain text input does. */
  input[type="date"] { width: 100%; max-width: 100%; }
}

.form-check { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.form-check input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; }
.form-check-label { font-size: 14px; color: var(--subtext); cursor: pointer; }

/* ── Tables ───────────────────────────────────────── */
.table-wrapper { overflow-x: auto; border-radius: var(--radius); }

table { width: 100%; border-collapse: collapse; }

thead th {
  background: var(--surface2);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--muted);
  padding: 12px 16px;
  text-align: left;
  white-space: nowrap;
}

tbody tr {
  border-bottom: 1px solid var(--border);
  transition: background 0.1s;
}

tbody tr:hover { background: var(--surface2); }

tbody td {
  padding: 14px 16px;
  font-size: 14px;
  color: var(--text);
  vertical-align: middle;
}

.td-muted { color: var(--subtext); }

/* ── People module (Full-Time / Freelancer tables) ────
   table-layout:fixed + a shared colgroup width contract so matching
   columns (Name, Email, Start Date, Status) render the same width in
   both tables regardless of that table's own content — previously each
   table auto-sized independently with no shared rule, which is what
   actually caused the misalignment (not a styling bug on either table).
   Position moved out of its own column entirely into a muted subtitle
   line under the employee's name, plain text rather than a badge. */
.people-table { table-layout: fixed; }
.people-col-name { width: 30%; }
.people-col-email { width: 26%; }
.people-col-rate { width: 16%; }
.people-col-date { width: 16%; }
.people-col-status { width: 12%; }
.people-col-actions { width: 50px; }
.people-position-subtitle {
  color: var(--subtext); font-size: 12px; font-weight: 500;
  margin-top: 2px;
}

/* ── Row action menu (kebab) ──────────────────────────
   A single "⋮" button revealing View/Edit/Deactivate/Delete etc. instead
   of a wall of individual buttons per row — same overflow-menu visual
   language as .board-toolbar (Boards module). */
.row-menu { position: relative; display: inline-block; }
.row-menu-btn {
  background: none; border: none; color: var(--subtext); cursor: pointer;
  width: 30px; height: 30px; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
}
.row-menu-btn:hover { background: var(--surface3); color: var(--text); }
/* position: fixed, not absolute — a row-menu can sit inside .table-wrapper
   (overflow-x: auto), which per CSS spec forces overflow-y to compute as
   auto too the moment overflow-x isn't "visible" (there's no way to keep
   just one axis clipping and the other not). An absolutely-positioned
   dropdown would get clipped at the table's edge; fixed positioning escapes
   that entirely, with its coordinates computed in JS from the trigger
   button's getBoundingClientRect() (positionRowMenuDropdown, in each page's
   own script) — same proven pattern already used for .notif-dropdown. */
.row-menu-dropdown {
  display: none; position: fixed;
  background: var(--surface2); border: 1px solid var(--border-l); border-radius: var(--radius-sm);
  box-shadow: 0 16px 40px rgba(0,0,0,0.3); padding: 6px; z-index: 300;
  flex-direction: column; min-width: 180px; gap: 2px;
}
.row-menu-dropdown.open { display: flex; }
.row-menu-item {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  background: none; border: none; color: var(--text); font: inherit; font-size: 13.5px;
  padding: 9px 10px; border-radius: 6px; cursor: pointer; text-decoration: none;
}
.row-menu-item .icon { width: 15px; height: 15px; color: var(--subtext); }
.row-menu-item:hover { background: var(--surface3); }
.row-menu-item.danger { color: var(--danger); }
.row-menu-item.danger .icon { color: var(--danger); }
.row-menu-item.danger:hover { background: rgba(255,77,109,0.1); }
.row-menu-divider { height: 1px; background: var(--border); margin: 4px 2px; }

/* ── Badges ───────────────────────────────────────── */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 20px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
}

.badge-freelancer {
  background: rgba(76,154,255,0.15);
  color: var(--primary-l);
  border: 1px solid rgba(76,154,255,0.25);
}

.badge-full-time {
  background: rgba(199,125,175,0.15);
  color: #EE88CC;
  border: 1px solid rgba(199,125,175,0.25);
}

.badge-admin {
  background: rgba(255,183,3,0.12);
  color: var(--warning);
  border: 1px solid rgba(255,183,3,0.25);
}

.badge-success {
  background: rgba(0,214,143,0.12);
  color: var(--success);
  border: 1px solid rgba(0,214,143,0.22);
}

.badge-danger {
  background: rgba(255,77,109,0.12);
  color: var(--danger);
  border: 1px solid rgba(255,77,109,0.22);
}

.badge-neutral {
  background: rgba(255,255,255,0.06);
  color: var(--subtext);
  border: 1px solid var(--border);
}

/* ── Alerts ───────────────────────────────────────── */
.alert {
  padding: 12px 16px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  margin-bottom: 20px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-weight: 500;
}

.alert-success {
  background: rgba(0,214,143,0.1);
  border: 1px solid rgba(0,214,143,0.25);
  color: var(--success);
}

.alert-error {
  background: rgba(255,77,109,0.1);
  border: 1px solid rgba(255,77,109,0.25);
  color: var(--danger);
}

.alert-info {
  background: rgba(76,154,255,0.1);
  border: 1px solid rgba(76,154,255,0.25);
  color: var(--primary-l);
}

/* ── Section Headers ──────────────────────────────── */
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

.section-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.3px;
}

.section-subtitle {
  font-size: 13px;
  color: var(--subtext);
  margin-top: 2px;
}

/* ── Cutoff Bar ───────────────────────────────────── */
.cutoff-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 20px;
  margin-bottom: 24px;
  flex-wrap: wrap;
  gap: 12px;
}

.cutoff-label {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
}

.cutoff-label span {
  color: var(--primary-l);
  font-weight: 700;
}

/* ── Pay Amount Display ───────────────────────────── */
.pay-amount { font-weight: 700; font-size: 15px; }
.pay-amount.positive { color: var(--success); }
.pay-amount.negative { color: var(--danger); }
.pay-amount.neutral  { color: var(--text); }
.pay-amount.primary  { color: var(--primary-l); }

/* ── Upload Zones ─────────────────────────────────── */
.upload-zone {
  border: 2px dashed var(--border);
  border-radius: var(--radius);
  padding: 28px;
  text-align: center;
  cursor: pointer;
  transition: all 0.2s;
  position: relative;
}

.upload-zone:hover {
  border-color: var(--primary);
  background: rgba(76,154,255,0.04);
}

.upload-zone input[type="file"] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.upload-icon { font-size: 33px; margin-bottom: 8px; }
.upload-title { font-size: 15px; font-weight: 600; color: var(--text); }
.upload-hint  { font-size: 13px; color: var(--muted); margin-top: 4px; }

/* ── Profile Page ─────────────────────────────────── */
.profile-avatar-section {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 24px;
}

.profile-avatar {
  width: 80px;
  height: 80px;
  border-radius: 18px;
  background: linear-gradient(135deg, var(--primary), var(--pink));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 29px;
  font-weight: 700;
  color: #fff;
  overflow: hidden;
  flex-shrink: 0;
  border: 2px solid var(--border-l);
}

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

.profile-avatar-clickable { position: relative; cursor: pointer; }
.profile-avatar-overlay {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.5); color: #fff; font-size: 18px; opacity: 0; transition: opacity 0.15s;
}
.profile-avatar-clickable:hover .profile-avatar-overlay { opacity: 1; }

.avatar-crop-container {
  width: 100%; height: 320px; margin: 16px 0; background: #000;
  border-radius: var(--radius-sm); overflow: hidden;
}
.avatar-crop-container img { max-width: 100%; display: block; }

.tenure-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: linear-gradient(135deg, rgba(76,154,255,0.12), rgba(199,125,175,0.07));
  border: 1px solid rgba(76,154,255,0.22);
  border-radius: 20px;
  padding: 4px 14px;
  font-size: 12px;
  font-weight: 600;
  color: var(--primary-l);
}

/* ── Modal ────────────────────────────────────────── */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}

.modal-overlay.open { opacity: 1; pointer-events: all; }

.modal {
  background: var(--surface);
  border: 1px solid var(--border-l);
  border-radius: var(--radius-lg);
  padding: 30px;
  width: 100%;
  max-width: 500px;
  transform: translateY(24px);
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
  box-shadow: 0 40px 80px rgba(0,0,0,0.5);
}

.modal-overlay.open .modal { transform: translateY(0); }

.modal-title {
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.2px;
  margin-bottom: 6px;
}

.modal-subtitle {
  font-size: 14px;
  color: var(--subtext);
  margin-bottom: 24px;
}

.modal-footer {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  margin-top: 24px;
}

/* ── Gradient Divider ─────────────────────────────── */
.gradient-line {
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--primary), var(--pink), transparent);
  border: none;
  margin: 24px 0;
}

/* ── Empty State ──────────────────────────────────── */
.empty-state {
  text-align: center;
  padding: 60px 24px;
  color: var(--subtext);
}

.empty-state .icon { font-size: 49px; margin-bottom: 12px; opacity: 0.65; }
.empty-state .title { font-size: 17px; font-weight: 700; color: var(--text); margin-bottom: 6px; }
.empty-state .desc { font-size: 14px; }

/* ── Scrollbar ────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-l); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* ── Utilities ────────────────────────────────────── */
.flex { display: flex; }
.flex-center { display: flex; align-items: center; justify-content: center; }
.flex-between { display: flex; align-items: center; justify-content: space-between; }
.gap-2 { gap: 8px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.mt-1 { margin-top: 4px; }
.mt-2 { margin-top: 8px; }
.mt-3 { margin-top: 12px; }
.mt-4 { margin-top: 16px; }
.mb-3 { margin-bottom: 12px; }
.mb-4 { margin-bottom: 16px; }
.mb-6 { margin-bottom: 24px; }
.text-sm { font-size: 13px; }
.text-muted { color: var(--muted); }
.text-subtext { color: var(--subtext); }
.text-success { color: var(--success); }
.text-danger { color: var(--danger); }
.text-primary { color: var(--primary-l); }
.text-pink { color: var(--pink); }
.font-bold { font-weight: 700; }
.font-medium { font-weight: 500; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.w-full { width: 100%; }
.hidden { display: none !important; }

/* ── Mobile Hamburger ─────────────────────────────── */
.mobile-menu-btn {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  cursor: pointer;
  font-size: 16px;
  flex-shrink: 0;
  transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
}
.mobile-menu-btn .icon { width: 21px; height: 21px; }
.mobile-menu-btn:hover { background: var(--surface3); border-color: var(--border-l); color: var(--text); }
.mobile-menu-btn:active { transform: scale(0.94); }

.mobile-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  z-index: 99;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.mobile-backdrop.open {
  opacity: 1;
  pointer-events: all;
}

/* ── Responsive ───────────────────────────────────── */
@media (max-width: 768px) {
  .sidebar {
    transform: translateX(-100%);
    transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: none;
  }
  .sidebar.open {
    transform: translateX(0);
    box-shadow: 8px 0 40px rgba(0,0,0,0.5);
  }
  .main-content { margin-left: 0; }
  .page-body { padding: 16px; }
  .stats-grid { grid-template-columns: 1fr 1fr; }
  .mobile-menu-btn { display: flex; }
  .topbar { padding: 0 16px; gap: 10px; }
  /* Bigger on mobile than the old 16px (per explicit request) — min-width:0
     is required alongside white-space:nowrap/overflow:hidden for the fade
     to actually engage instead of the flex item just growing to fit a long
     title, same CSS-flex-item footgun documented on .form-group elsewhere
     in this file. The mask-image fade (same technique as .billing-tabs'
     right-edge scroll fade in billing.css) reads as "trails off" rather
     than an abrupt hard clip when a title doesn't fit. */
  .topbar-title {
    flex: 1; min-width: 0; font-size: 28px;
    white-space: nowrap; overflow: hidden;
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%);
    mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%);
  }

  /* Collapse-to-icons is a desktop concept — mobile already has its own
     off-canvas drawer above. Since the collapsed state is server-rendered
     (persisted per-user, not re-detected per viewport), a user who
     collapsed the sidebar on desktop and later opens this same app on
     their phone would otherwise inherit `.collapsed`'s width/margin rules
     here too — both explicitly overridden back to the normal mobile
     values. Equal specificity to their desktop counterparts (two classes
     each), so being later in the file is what makes these win, not just
     appearing inside a media query. */
  .sidebar.collapsed { width: var(--sidebar-w); }
  .sidebar.collapsed ~ .main-content { margin-left: 0; }
  /* Hides the whole footer wrapper (not just the button) so no bare
     border-top line is left dangling at the bottom of the mobile drawer. */
  .sidebar-footer { display: none; }
  .sidebar.collapsed .sidebar-logo { padding: 14px 20px; justify-content: flex-start; }
  .sidebar.collapsed .sidebar-logo-mark-expanded { display: block; }
  .sidebar.collapsed .sidebar-logo-mark-collapsed { display: none; }
  .sidebar.collapsed .sidebar-logo-mark-expanded { width: auto; }
  .sidebar.collapsed .sidebar-logo-mark-expanded img { max-width: 100%; width: auto; height: auto; }
  .sidebar.collapsed .sidebar-user { padding: 14px 18px; justify-content: flex-start; flex-direction: row; }
  .sidebar.collapsed .sidebar-user-info { display: block; }
  .sidebar.collapsed .profile-trigger-chevron { display: inline-block; }
  .sidebar.collapsed .profile-trigger-btn { flex: 1; }
  .sidebar.collapsed .notif-bell-btn { margin-left: auto; order: 0; }
  .sidebar.collapsed .nav-link { justify-content: flex-start; gap: 10px; padding: 9px 12px; }
  .sidebar.collapsed .nav-label { opacity: 1; width: auto; overflow: visible; }
  .sidebar.collapsed .nav-link:hover::after { content: none; }
  /* Real bug found while checking the sidebar-workspace-item font-size
     change below: a user who ever collapsed the sidebar on desktop (its
     collapsed state is a persisted per-user preference, not re-detected
     per viewport) had the Workspaces sub-link list disappear ENTIRELY on
     mobile — .sidebar.collapsed .sidebar-workspace-list { display:none }
     (this rule's own desktop-only reason for existing: collapsed desktop
     is icon-only, no room for sub-links) was never included in this
     mobile override block the way every other collapsed-desktop-only rule
     above already is. */
  .sidebar.collapsed .sidebar-workspace-list { display: flex; }
  /* Same collapsed-desktop-preference-vs-mobile bug, same fix, applied
     proactively to the new HR sub-list — see the comment right above. */
  .sidebar.collapsed .sidebar-hr-list { display: flex; }
  /* Same collapsed-desktop-preference-vs-mobile fix, applied to the new
     Billing sub-list. */
  .sidebar.collapsed .sidebar-billing-list { display: flex; }

  /* Workspace sub-links (under "Workspaces" in the drawer) were reading
     too small on a phone — bumped to match .nav-link's own 15px, with a
     lighter weight (400 vs. .nav-link's 500) so they still read as
     subordinate to the parent item rather than identical to it, on top of
     the indent (.sidebar-workspace-list's own padding-left) already doing
     that job visually. Desktop keeps the original smaller 13px/500 sizing
     — not reported as a problem there. Same sizing applied to the HR and
     Billing sub-lists for the same reason. */
  .sidebar-workspace-item, .sidebar-hr-item, .sidebar-billing-item { font-size: 15px; font-weight: 400; }
  .sidebar.collapsed .nav-icon { width: 18px; height: 18px; }
}

/* ── Invoice Form Layout ─────────────────────────────── */
.client-search-wrap { position: relative; }
.client-dropdown {
  position: absolute; top: 100%; left: 0; right: 0;
  background: #1a1a2e; border: 1px solid rgba(76,154,255,0.3);
  border-radius: 8px; z-index: 200; max-height: 260px; overflow-y: auto;
  box-shadow: 0 8px 24px rgba(0,0,0,0.4); display: none;
}
.client-dropdown .pd-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.15s; }
.client-dropdown .pd-item:hover { background: rgba(76,154,255,0.15); }
.client-dropdown .pd-item:last-child { border-bottom: none; }

.product-search-wrap { position: relative; }
.product-dropdown { position: absolute; top: 100%; left: 0; right: 0; background: #1a1a2e; border: 1px solid rgba(76,154,255,0.3); border-radius: 8px; z-index: 100; max-height: 260px; overflow-y: auto; box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
.pd-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.15s; }
.pd-item:hover { background: rgba(76,154,255,0.15); }
.pd-item:last-child { border-bottom: none; }
.pd-name { font-size: 14px; color: var(--text); }
.pd-price { font-size: 13px; color: var(--success); font-family: 'Inter', sans-serif; }

.line-items-table { width: 100%; }
.line-item-row { display: grid; grid-template-columns: 180px 1fr 80px 120px 110px 36px; gap: 8px; align-items: start; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
.line-items-header { display: grid; grid-template-columns: 180px 1fr 80px 120px 110px 36px; gap: 8px; padding: 8px 0; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--subtext); border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 4px; }
.item-amount { font-family: 'Inter', sans-serif; font-size: 14px; color: var(--text); padding-top: 8px; display: block; }
.btn-remove-item { background: none; border: 1px solid rgba(255,77,109,0.3); color: var(--danger, #ff4d6d); border-radius: 6px; width: 32px; height: 32px; cursor: pointer; font-size: 19px; line-height: 1; margin-top: 4px; transition: all 0.2s; }
.btn-remove-item:hover { background: rgba(255,77,109,0.15); }

.form-grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }

.form-bottom-row { display: grid; grid-template-columns: 1fr 360px; gap: 20px; align-items: start; }
.form-bottom-notes { }
.form-bottom-summary { }

/* ── Mobile responsive for forms ───────────────────── */
@media (max-width: 900px) {
  .form-grid-4 { grid-template-columns: 1fr 1fr; }
  .form-bottom-row { grid-template-columns: 1fr; }
  .form-bottom-summary { order: -1; }
  .line-items-table { overflow-x: auto; display: block; }
  .line-items-header, .line-item-row { min-width: 640px; }
}

@media (max-width: 768px) {
  .form-grid-4 { grid-template-columns: 1fr; }
  .topbar-actions { gap: 6px; flex-wrap: wrap; }
  .page-body { padding: 12px; }
  .card { padding: 16px; }
  /* Belt-and-suspenders against horizontal page scroll on mobile —
     reported three times now on the Quote/Invoice edit page's Issue
     Date/Valid Until fields specifically, but never reproducible in this
     dev environment's own mobile emulation (real iOS Safari and this
     environment's Chromium-based one render flatpickr differently: real
     Safari keeps its own JS calendar/altInput, this environment's
     emulation swaps to a native <input type="date">, so whatever's
     actually 1-2px too wide on a real phone isn't the same code path this
     environment could test against). The first attempt at this same
     belt-and-suspenders fix — overflow-x:hidden on body alone — still
     didn't stop it per the user's own follow-up report; iOS Safari is
     documented to sometimes keep allowing horizontal rubber-band/momentum
     scroll from body-only overflow-x:hidden, since its elastic-scroll
     behavior can operate at the html/document level rather than body's
     own box. Setting it on html too (not just body) is the standard fix
     for that specific iOS quirk. */
  /* overflow-x: clip, not hidden — confirmed live this actually matters,
     not just a style preference. Per the CSS Overflow spec, setting only
     one axis to a non-"visible" value (hidden/auto/scroll/clip all count)
     forces the OTHER axis to compute as "auto" too if it was "visible" —
     explicitly writing `overflow-y: visible` does NOT escape this rule,
     since the spec converts an explicit visible right back to auto the
     same as an implicit one. With overflow-x: hidden, html/body were
     silently becoming their own independent scroll containers, which
     broke .topbar's position:sticky on every mobile page — confirmed live
     by scrolling: the topbar scrolled away with the content instead of
     staying pinned, because sticky resolves against the nearest actual
     scrolling ancestor, and that had silently changed out from under it.
     `clip` is the one overflow value that does NOT establish a scroll
     container at all (no scrollbar, no scrolling mechanism, just a visual
     clip), so it doesn't trigger that same-axis-coupling rule — html/body
     stay exactly as they were before for vertical/sticky purposes, while
     horizontal overflow is still clamped. Supported in Safari since 16
     (2022), safely within this app's real-device support range. */
  html, body { overflow-x: clip; }
  body { position: relative; width: 100%; }

  /* iOS Safari auto-zooms the ENTIRE page in when a focused input's own
     font-size is under 16px — a well-known mobile-web quirk, not a layout
     bug. .form-control's base font-size is 15px (desktop-tuned), just
     under that threshold. This is very plausibly the real mechanism behind
     the repeatedly-reported "Issue Date/Valid Until too big, causes
     horizontal scroll" on the Quote/Invoice edit page — the two previous
     overflow-x fixes above address ELEMENT-width overflow, which can't
     touch a VIEWPORT-zoom-triggered one; tapping into a date field is
     exactly the kind of focus event that would trigger this. Scoped to
     mobile only so desktop's existing 15px sizing is untouched. */
  .form-control { font-size: 16px; }

  .table-wrapper { overflow-x: auto; }
  .table-wrapper table { min-width: 600px; }

  /* ── App-wide mobile touch targets ──────────────────────────────────
     Every .btn/.btn-sm gets a comfortable ~44px minimum tap height on
     mobile specifically — desktop sizing (.btn, .btn-sm defined earlier
     in this file) is untouched. This replaces a rule that used to do the
     opposite (shrink topbar buttons to font-size:13px/padding:6px 10px
     on mobile), which was fighting the exact direction a touchscreen
     actually needs. */
  .btn { min-height: 44px; padding: 10px 16px; }
  .btn-sm { min-height: 40px; padding: 8px 14px; }

  /* Two more app-wide (not board/list-specific) icon-only controls that
     appear on effectively every page — the notification bell in both
     sidebars, and the kebab "⋮" row-action menu used by every data table
     (People, Billing, etc.). */
  .notif-bell-btn { width: 44px; height: 44px; font-size: 18px; }
  .row-menu-btn { width: 40px; height: 40px; }
  .notif-bell-btn-mobile { display: flex; }
  .topbar-mobile-search-btn {
    display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; flex-shrink: 0;
    background: var(--surface2); border: 1px solid var(--border);
    border-radius: var(--radius-sm); color: var(--subtext);
    font-size: 16px; cursor: pointer;
  }
  .topbar-mobile-search-btn:hover { background: var(--surface3); color: var(--text); }

  /* Shared floating-action-button — same visual treatment as Billing's
     .billing-mobile-fab and Boards' .mobile-fab-btn (each module's own CSS
     file keeps its own near-identical copy, same precedent as
     .bulk-action-bar elsewhere in this app), defined once here in the
     always-loaded base file since People has no per-module stylesheet of
     its own to hold a copy. Deliberately placed as a .topbar SIBLING by
     every page that uses it, never nested inside — .topbar's
     backdrop-filter traps a position:fixed descendant otherwise (hit twice
     already this session on the Billing/Workspace FABs). */
  .mobile-fab {
    display: flex; align-items: center; gap: 8px;
    position: fixed; right: 20px; bottom: 24px;
    padding: 14px 22px; border-radius: 999px;
    background: linear-gradient(135deg, var(--primary) 0%, #7B5CEA 100%);
    color: #fff; font-size: 15px; font-weight: 700; border: none; cursor: pointer;
    text-decoration: none;
    box-shadow: 0 12px 32px rgba(0,0,0,0.45);
    z-index: 150;
  }
  .mobile-fab:active { transform: scale(0.96); }

  /* People module: "+ Add Person" moves to the floating .mobile-fab above
     instead of wrapping onto its own row, and the mobile notification
     bell is dropped from this one page's topbar (still reachable via the
     hamburger drawer's own bell) — same reasoning/technique already
     applied to Boards and Billing's mobile headers this session. */
  body.people-page .topbar-actions .btn-primary,
  body.people-page .notif-bell-btn-mobile { display: none; }
  body.people-page .topbar-actions { width: auto; }

  /* Settings pages: floating Save button, appears only once the page's
     form has actually been touched (see public/js/mobile-save-fab.js and
     its opt-in data-mobile-save-fab attribute) — a separate standalone
     class from .mobile-fab above rather than a modifier on it, since
     .mobile-fab's other users (People/Dashboard/Workspaces) should always
     show unconditionally, not gated on a dirty-form state. */
  .mobile-save-fab.dirty {
    display: flex; align-items: center; gap: 8px;
    position: fixed; right: 20px; bottom: 24px;
    padding: 14px 22px; border-radius: 999px;
    background: linear-gradient(135deg, var(--primary) 0%, #7B5CEA 100%);
    color: #fff; font-size: 15px; font-weight: 700; border: none; cursor: pointer;
    box-shadow: 0 12px 32px rgba(0,0,0,0.45);
    z-index: 150;
  }
  .mobile-save-fab.dirty:active { transform: scale(0.96); }

  /* Admin Dashboard: "Quick Create" moves to the floating .mobile-fab
     above; "Payroll Center" is dropped entirely on mobile (not relocated —
     explicit request), same as the mobile notification bell. */
  body.admin-dashboard-page .topbar-actions { display: none; }
  body.admin-dashboard-page .notif-bell-btn-mobile { display: none; }
}

/* ── Payroll topbar controls ──
   Base (desktop) sizing — previously inline styles on the <select>/<form>,
   moved to real classes so the mobile override below can actually win
   against them (an inline style beats almost any class-based rule). */
.payroll-cutoff-form { display: flex; gap: 8px; align-items: center; }
.payroll-cutoff-select { width: auto; padding: 8px 12px; font-size: 14px; }

@media (max-width: 480px) {
  .form-grid { grid-template-columns: 1fr !important; }
  .topbar { flex-wrap: wrap; height: auto; min-height: var(--header-h); padding: 10px 12px; }
  .topbar-actions { width: 100%; justify-content: flex-end; }

  /* Payroll was the one page whose two topbar-actions controls (cutoff
     select + Send Payslips) didn't fit on a single mobile row-2 line even
     after the generic wrap above — genuinely producing a 3rd row. Force
     them onto one compact row instead: the select shrinks/truncates its
     own text natively, the button drops its desktop .btn-lg padding. */
  .payroll-topbar-actions { flex-wrap: nowrap; gap: 8px; }
  .payroll-cutoff-form { flex: 1; min-width: 0; }
  .payroll-cutoff-select { width: 100%; min-width: 0; }
  .payroll-send-btn { flex-shrink: 0; padding: 10px 14px; font-size: 13px; white-space: nowrap; }
}

/* ── Settings (Branding/Google Drive/Gemini/Integrations/Automations) ──
   Persistent left rail + content pane, the standard pattern used by every
   major SaaS settings area (Stripe, GitHub, Linear) — one place to see
   every category instead of a single long scrolling page, and each
   category page includes this same rail so the whole area reads as one
   cohesive "Settings" experience even though Integrations/Automations are
   still their own routes under the hood. */
.settings-layout { display: flex; align-items: flex-start; gap: 32px; max-width: 980px; }

.settings-nav { width: 200px; flex-shrink: 0; position: sticky; top: 28px; }
.settings-nav-label {
  font-size: 11px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
  color: var(--muted); padding: 0 12px; margin-bottom: 14px;
}
.settings-nav-group { margin-bottom: 18px; }
.settings-nav-group-title {
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase;
  color: var(--muted); padding: 0 12px; margin-bottom: 6px; opacity: 0.75;
}
.settings-nav-item {
  display: flex; align-items: center; gap: 10px; padding: 8px 12px;
  border-radius: var(--radius-sm); color: var(--subtext); font-size: 14px; font-weight: 500;
  margin-bottom: 1px;
}
.settings-nav-item i { width: 16px; text-align: center; flex-shrink: 0; opacity: 0.85; font-size: 13px; }
.settings-nav-item:hover { background: var(--surface2); color: var(--text); }
.settings-nav-item.active { background: var(--primary-glow); color: var(--text); font-weight: 600; }
.settings-nav-item.active i,
.settings-nav-item.active .nav-icon,
.settings-nav-item:hover .nav-icon { opacity: 1; }
.settings-nav-subitem { padding-left: 12px; font-size: 13px; }

.settings-content { flex: 1; min-width: 0; }
.settings-section-header { margin-bottom: 16px; scroll-margin-top: 20px; }
.settings-section-header h2 {
  font-size: 18px; font-weight: 700; color: var(--text);
  display: flex; align-items: center; gap: 10px; margin-bottom: 4px;
}
.settings-section-header p { font-size: 13.5px; color: var(--subtext); line-height: 1.5; }

/* Settings page-header icon (Lucide) — two contexts, matching the exact
   spacing the old inline-styled Font Awesome <i> tags had: .topbar-title
   has no flex/gap of its own, so its icon needs its own right-margin;
   .settings-section-header h2 already has `gap: 10px` from the flex rule
   above, so its icon needs color only (adding margin there too would
   double the visual gap to 20px). */
.settings-topbar-icon { margin-right: 10px; color: var(--primary); }
.settings-header-icon { color: var(--primary); }

/* Mobile-only secondary header (partials/settings-mobile-tabs.ejs) + per-
   category accordion (settings-nav.ejs, settings-mobile-accordion.js) —
   hidden on desktop, where the sidebar above is the only nav.

   The header row is a deliberate, exact clone of Billing's own
   .billing-tabs/.billing-tab (billing.css) — same sticky offset, same
   scroll+fade, same active-pill styling — per explicit request to match
   it precisely, right down to structural position: settings-mobile-
   tabs.ejs is included as a direct sibling of .page-body (right after
   .topbar closes), NOT nested inside .settings-layout/.page-body's own
   padding the way an earlier attempt at this had it. That's what makes
   genuine edge-to-edge sticky positioning possible without a fragile
   negative-margin hack — position:sticky here spans .main-content's full
   width the same way .billing-tabs already does, for the same structural
   reason. */
.settings-mobile-tabs { display: none; }
.settings-mobile-accordion { display: none; }

@media (max-width: 900px) {
  .settings-layout { flex-direction: column; gap: 20px; }
  .settings-nav { display: none; }

  .settings-mobile-tabs {
    display: flex; align-items: center; gap: 4px;
    padding: 0 28px;
    min-height: var(--sidebar-user-h);
    border-bottom: 1px solid var(--border);
    background: var(--sidebar-bg);
    position: sticky; top: var(--header-h); z-index: 49;
    overflow-x: auto; overflow-y: hidden;
    touch-action: pan-x; scrollbar-width: none;
    /* Right-edge fade signals there's more to scroll to — same technique
       (and same 48px falloff) as Billing's own .billing-tabs fade. */
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%);
    mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%);
  }
  .settings-mobile-tabs::-webkit-scrollbar { display: none; }
  .settings-mobile-tab {
    display: flex; align-items: center; padding: 9px 14px;
    border-radius: var(--radius-sm); font-size: 15px; font-weight: 500;
    color: var(--subtext); text-decoration: none; white-space: nowrap; flex-shrink: 0;
  }
  .settings-mobile-tab.active {
    background: linear-gradient(135deg, var(--primary-glow), var(--pink-glow));
    color: var(--text); font-weight: 600;
  }

  .settings-mobile-accordion { display: flex; flex-direction: column; gap: 8px; }
  .settings-mobile-item {
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--surface); overflow: hidden;
  }
  .settings-mobile-item-header {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; padding: 13px 16px; min-height: 44px;
    font-size: 14px; font-weight: 600; color: var(--text);
    background: none; border: none; cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .settings-mobile-item-header span { display: flex; align-items: center; gap: 10px; }
  .settings-mobile-item.settings-mobile-subitem .settings-mobile-item-header { padding-left: 28px; font-size: 13px; }
  .settings-mobile-item.open .settings-accordion-chevron { transform: rotate(180deg); }
  .settings-accordion-chevron { transition: transform 0.2s ease; }
  /* Slide-open/close animation for a genuinely dynamic, unknown content
     height — the grid-template-rows 0fr→1fr trick, not a max-height
     transition (which needs a guessed-too-large cap, and either animates
     past the real content or clips a tall one) and not JS measuring
     scrollHeight itself (extra complexity for the same visual result).
     .settings-mobile-item-body-wrap is the grid container that's actually
     animated; .settings-mobile-item-body is the child that gets clipped
     while the row is shrinking. */
  .settings-mobile-item-body-wrap {
    display: grid; grid-template-rows: 0fr;
    transition: grid-template-rows 0.25s ease;
  }
  .settings-mobile-item.open .settings-mobile-item-body-wrap { grid-template-rows: 1fr; }
  /* No padding directly on .settings-mobile-item-body itself — a grid row
     collapsed to 0fr still renders its item's OWN padding-box even at
     zero content height (padding doesn't shrink away with the row), which
     left a persistent ~16px sliver visible after "closing" an item,
     confirmed live via getBoundingClientRect() showing 16px instead of 0
     post-transition. Padding lives one level down instead, on whichever
     real content lands inside (.settings-content when embedded, or
     .settings-mobile-loading while fetching) — those DO shrink to zero
     along with the row, since they're the actual content being clipped
     rather than the box the row's own sizing is measured against. */
  .settings-mobile-item-body { overflow: hidden; min-height: 0; }
  /* Embedded content reuses .settings-content's own inner spacing/cards,
     but not its flex/min-width sizing (which assumed sitting directly in
     .settings-layout) — reset those two so it just fills this box. */
  .settings-mobile-item-body .settings-content { flex: none; min-width: 0; padding: 0 16px 16px; }
  .settings-mobile-item-body .settings-section-header { display: none; } /* already says "General"/etc. — redundant under this same-named accordion header */
  .settings-mobile-loading { padding: 24px 16px; text-align: center; color: var(--muted); font-size: 13.5px; }
}

/* Flatpickr's dark/light theme lives in its own file, public/css/
   flatpickr-theme.css, loaded via each page's extraCss — NOT here.
   style.css always loads first (see head.ejs), before any page's
   extraCss (which is where the CDN's own flatpickr.min.css comes in) —
   these rules need to load AFTER that CDN stylesheet to win the cascade
   (same selectors, same specificity — later file wins), so they can't
   live in this always-first file. See flatpickr-theme.css's own header
   comment for the full story (a real regression this exact setup caused,
   caught and fixed the same day it shipped). */
