:root {
  --color-accent: #2563eb;
  --color-accent-hover: #1d4ed8;
  --color-text: #111827;
  --color-text-muted: #6b7280;
  --color-border: #d1d5db;
  --color-border-light: #e5e7eb;
  --color-bg: #ffffff;
  --color-bg-page: #f9fafb;
  --color-bg-card: #ffffff;
  --color-bg-subtle: #f3f4f6;
  --color-error: #b00020;
  --color-success: #15803d;
  --radius: 8px;
  --radius-sm: 4px;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Density pass (2026-07-28) -- one spacing scale, referenced
     everywhere below instead of scattered magic numbers, so tuning
     density later means editing these eight values, not forty call
     sites. rem-based, roughly geometric so consecutive steps read as
     genuinely different. Does NOT apply to input/select/button padding
     (see the comment on those rules) -- those stay fixed regardless of
     this scale, since they're the interactive-target-size constraint,
     not a whitespace one. */
  --space-1: 0.25rem;
  --space-2: 0.375rem;
  --space-3: 0.5rem;
  --space-4: 0.75rem;
  --space-5: 1rem;
  --space-6: 1.25rem;
  --space-7: 1.5rem;
  --space-8: 2rem;
}

* { box-sizing: border-box; }

/* The actual document-level horizontal scroll box, not body (2026-08-05,
   mobile drawer bleed-through fix) -- body's own overflow-x:hidden below
   only clips body's normal-flow content. The mobile sidebar drawer is
   position:fixed (see the @media block further down), so its containing
   block is the viewport's initial containing block, not body -- nothing
   in the ancestor chain creates a new containing block for it. Without
   this rule, the closed drawer's off-canvas geometry could still register
   in html's own scrollable width on some mobile browsers, making it
   reachable via horizontal scroll even though transform visually moves it
   off-screen. */
html { overflow-x: hidden; }

body {
  font-family: var(--font-sans);
  color: var(--color-text);
  background: var(--color-bg-page);
  /* 1000px (the original content column) + 240px (the sidebar, added
     Section 10.1/v29) -- so existing pages keep their original readable
     width alongside the new sidebar, rather than being squeezed by it. */
  max-width: 1240px;
  margin: 0 auto;
  padding: 0;
  line-height: 1.5;
  /* overflow-x:hidden REMOVED here (2026-08-15) -- found while building
     /entries/project's sticky-top-panel that this rule silently breaks
     position:sticky for every descendant, site-wide. Root cause: giving
     BOTH html and body their own independent overflow-x:hidden makes
     each one independently compute overflow-y:auto (the CSS overflow-
     pairing rule), so body becomes its own "scroll container" per the
     sticky-positioning spec -- but body's own box always sizes to fit
     its content (nothing here ever gives it a fixed height), so it
     never actually scrolls; window/html does the real scrolling
     instead. Any sticky descendant then computes its stuck offset
     against body's permanently-zero scroll position instead of the
     page's real scroll, so it silently never sticks. Confirmed directly
     (not just reasoned): with this rule active, .sticky-top-panel's
     getBoundingClientRect().top went negative while scrolling (i.e.
     scrolled past its own "stuck" offset, exactly like ordinary static
     content); removing only this line -- html's own overflow-x:hidden
     below untouched -- snapped it to exactly its `top` value instead.
     Confirmed SAFE to remove, not just convenient: html's own rule
     (added 2026-08-05, see its comment) already documents itself as
     "the actual document-level horizontal scroll box, not body" --
     tested directly in isolation that html's rule alone, with no
     overflow-x rule on body at all, still fully blocks horizontal
     scroll (window.scrollTo({left:500}) left window.scrollX at 0) --
     so this was always the redundant, older half of two overlapping
     fixes, and the only one actually causing harm. */
}

h1 { font-size: 1.5rem; margin: 0 0 var(--space-4); }
h2 { font-size: 1.15rem; margin: var(--space-6) 0 var(--space-3); }

/* Environment banner — shown at the very top of every page (nav.ejs)
   when running on Dev/Sandbox or QA, so the environment is obvious even
   though Render doesn't allow changing a service's .onrender.com
   subdomain after creation (Dev/Sandbox's URL doesn't say "dev"
   anywhere). Full-bleed across the actual browser viewport, not just
   body's own centered 1000px column — the standard "100vw + negative
   margin" technique for breaking a child out of a max-width container. */
.env-banner {
  width: 100vw;
  position: relative;
  left: 50%;
  right: 50%;
  margin-left: -50vw;
  margin-right: -50vw;
  margin-bottom: var(--space-5);
  text-align: center;
  font-weight: 700;
  letter-spacing: 0.05em;
  padding: var(--space-3);
  font-size: 0.85rem;
  color: #ffffff;
}
.env-banner-dev { background: #d97706; }
.env-banner-qa { background: #7c3aed; }

/* Empty-state banner — a lightweight, dismiss-by-adding-data nudge
   (nav.ejs) for a brand-new company with zero workers/projects, not a
   full onboarding wizard. Card-styled like the rest of the app rather
   than full-bleed like .env-banner, since this is routine guidance, not
   an environment warning. */
.empty-state-banner {
  background: var(--color-bg-subtle);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-6);
  font-size: 0.9rem;
}
.empty-state-banner a { color: var(--color-accent); font-weight: 600; }

/* Left-hand sidebar navigation (Section 10.1, v29, 2026-07-27) --
   replaces the old flat top nav. .app-layout is the flex row
   (sidebar + main-content); nav.ejs opens both .app-layout and
   .main-content, each including view closes them right before </body>.
   The env/empty-state banners above stay in normal flow, full-bleed,
   before this row -- so they span above the sidebar too, same as
   before. */
.app-layout {
  display: flex;
  align-items: flex-start;
}
.sidebar {
  flex: 0 0 220px;
  width: 220px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  background: var(--color-bg-card);
  border-right: 1px solid var(--color-border-light);
  padding: var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
  /* justify-content: space-between removed (2026-08-11) -- that was
     pinning .sidebar-footer to the visual bottom while .sidebar-links
     stayed at the top, which only made sense when the footer was the
     LAST child. Now that the identity block is the FIRST child (see
     nav.ejs's own comment for why), the two sections should just stack
     top-to-bottom in document order -- the default (no justify-content
     override) does exactly that, with any leftover space simply
     sitting below the last nav group, which is the expected look for a
     sidebar. */
}
.sidebar-links { display: flex; flex-direction: column; }
.sidebar-group { margin-bottom: var(--space-3); }

/* Collapsible groups (v29 amendment, 2026-07-27): real <details>/
   <summary> elements -- free expand/collapse + keyboard support, no JS
   of our own needed for the toggle. Every group (including Approvals
   and Platform Admin, both single-item) is a <summary>, so this styling
   applies uniformly -- there's no plain-<div> header left in the
   sidebar. */
.sidebar summary { cursor: pointer; list-style: none; }
.sidebar summary::-webkit-details-marker { display: none; }
.sidebar summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-radius: var(--radius-sm);
  transition: background-color 0.15s ease;
}
/* Hover feedback on the whole row (2026-07-27 polish pass), not just
   the chevron -- makes clear the entire header is the click target,
   not a narrow arrow. */
.sidebar summary:hover { background: var(--color-bg-subtle); }
.sidebar summary::after {
  content: '\25B8'; /* ▸ */
  font-size: 18px;
  line-height: 1;
  color: var(--color-text-muted);
  flex-shrink: 0;
  margin-left: var(--space-3);
  /* Slower than a typical hover transition (0.15s elsewhere) --
     rotation reads better a little more deliberate than a color fade. */
  transition: transform 0.25s ease;
}
.sidebar details[open] > summary::after { transform: rotate(90deg); }

/* Category headers, per real usage feedback (v29 amendment): must be
   unmistakably bigger/bolder than the item links beneath them, not
   just a small muted uppercase label -- the opposite of how these
   looked before this pass. Negative horizontal margin + matching
   padding (2026-07-27 polish pass) lets the hover/tap background above
   bleed slightly past the text to the sidebar's own edges, and gives
   the row enough height to be a comfortable touch target in the mobile
   drawer, not just a desktop click target. */
.sidebar-group-label {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--color-text);
  margin: 0 -0.4rem var(--space-1);
  padding: var(--space-3) 0.4rem;
}
.sidebar-subgroup {
  margin: var(--space-1) 0 var(--space-1) var(--space-3);
}
.sidebar-subgroup-label {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--color-text);
  margin: var(--space-1) -0.4rem var(--space-1);
  padding: var(--space-2) 0.4rem;
}
.sidebar a {
  display: block;
  text-decoration: none;
  color: var(--color-text);
  font-size: 0.85rem;
  font-weight: 400;
  padding: var(--space-2) 0 var(--space-2) var(--space-3);
}
.sidebar a:hover { color: var(--color-accent); }
.sidebar a.active { color: var(--color-accent); font-weight: 600; }
/* border-bottom/padding-bottom, not -top (2026-08-11): .sidebar-footer
   is now the FIRST child of .sidebar (identity block moved above the
   nav links -- see nav.ejs's own comment for why), so the separator
   has to sit on the side facing the links below it, not the side
   facing the top of the sidebar. */
.sidebar-footer { border-bottom: 1px solid var(--color-border-light); padding-bottom: var(--space-3); margin-bottom: var(--space-3); }
#user-button-root { display: flex; align-items: center; }

/* Nav identity display (testing aid, 2026-07-28) -- deliberately small
   and muted, sidebar-footer text rather than a banner of its own; the
   platform-admin marker borrows .env-banner's bold/letter-spaced badge
   language (nav.ejs) at sidebar scale instead of full-bleed page width. */
.user-identity { margin-bottom: var(--space-2); font-size: 0.8rem; }
.user-identity-email { font-weight: 600; word-break: break-all; }
.user-identity-meta { color: var(--color-text-muted); margin-top: 0.1rem; }
.platform-admin-badge {
  display: inline-block;
  margin-top: var(--space-2);
  padding: 0.15rem 0.4rem;
  background: #d97706;
  color: #ffffff;
  font-weight: 700;
  font-size: 0.65rem;
  letter-spacing: 0.05em;
  border-radius: var(--radius-sm);
}

/* Hamburger toggle + backdrop: hidden on desktop (sidebar always
   visible), shown only below the mobile breakpoint further down. */
.sidebar-toggle {
  display: none;
  position: fixed;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 110;
  font-size: 1.25rem;
  line-height: 1;
  background: var(--color-bg-card);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0.4rem 0.6rem;
  cursor: pointer;
}
.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(17, 24, 39, 0.4);
  z-index: 90;
}

.main-content {
  flex: 1;
  min-width: 0;
  padding: var(--space-6) var(--space-4) var(--space-7);
}

/* Card — the main "Minimal SaaS" building block: white panel, soft border,
   rounded corners, wraps forms and picker/filter bars. */
.card {
  background: var(--color-bg-card);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius);
  padding: var(--space-5) var(--space-6) var(--space-6);
  margin-bottom: var(--space-5);
}
.card h2 { margin-top: 0; }
/* Accept-invite's identity line (AcceptInvite.jsx, Section C build item
   3, 2026-08-02) -- pulled tight against the h2 right above it (default
   browser <p> margin, ~1em, otherwise leaves it reading as a separate
   block of page chrome rather than part of the same "who is this for"
   header unit). margin-bottom stays closer to normal so it's still
   visually distinct from "Set a password below," the instruction line
   that follows. */
.invite-context { margin-top: 0.25rem; }

/* Tables — kept as real tables (not restructured into card-lists) and
   wrapped in a horizontally-scrolling container so wide grids/lists don't
   break the layout on narrow screens. */
.table-wrap {
  overflow-x: auto;
  margin-bottom: var(--space-5);
  -webkit-overflow-scrolling: touch;
  background: var(--color-bg-card);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius);
}
table { border-collapse: collapse; width: 100%; }
/* Row height / cell padding is the single biggest density lever in the
   whole app (Timesheet Entries, Change Report, every SQL report) --
   tightened from 0.6rem/0.75rem to the scale below, plus an explicit
   font-size and line-height (previously unset, so cells rendered at
   the browser default ~16px/1.5 -- taller than table data needs).
   14px body / 13px header are the legibility floors this project set
   for this pass; both stay at exactly the floor's rounding, not below
   it, on purpose. */
th, td {
  border-bottom: 1px solid var(--color-border-light);
  padding: var(--space-2) var(--space-4);
  text-align: left;
  white-space: nowrap;
  font-size: 0.875rem; /* 14px */
  line-height: 1.3;
}
tbody tr:last-child td { border-bottom: none; }
thead th {
  background: var(--color-bg-subtle);
  font-size: 0.8125rem; /* 13px -- the floor, not below it */
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
  font-weight: 600;
}
th a { color: inherit; text-decoration: none; }
th a:hover { text-decoration: underline; }
th a .sort-indicator { opacity: 0.4; }
th a.active .sort-indicator { opacity: 1; }

/* Forms */
form label {
  display: block;
  margin-top: var(--space-4);
  margin-bottom: var(--space-1);
  font-size: 0.75rem;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.field-hint {
  margin: var(--space-1) 0 0;
  font-size: 0.8rem;
  color: var(--color-text-muted);
}
/* Required/optional field marking (Section C build item 1, 2026-08-02):
   required inputs use the native `required` attribute alone, no visual
   marker -- optional ones get this muted inline "(optional)" next to
   their label instead. No colour-only indicator: the word itself, not
   just a colour, is what communicates optional. */
.optional-marker {
  font-size: 0.75rem;
  font-weight: normal;
  text-transform: none;
  letter-spacing: normal;
  color: var(--color-text-muted);
}
/* The Hours field specifically, when the selected status doesn't allow
   hours at all (entry_status.hours_requirement = 'not_allowed') --
   disabled outright rather than hidden, with this note explaining why,
   so the field's absence-of-purpose is visible, not silently missing. */
.not-allowed-note {
  margin: var(--space-1) 0 0;
  font-size: 0.8rem;
  font-style: italic;
  color: var(--color-text-muted);
}
/* Same note, compact for Weekly Entry's grid cells (bulk-entry.ejs) --
   the grid has no room for the full single-row version's spacing. */
.cell-not-allowed-note {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.7rem;
  font-style: italic;
  color: var(--color-text-muted);
}
/* input/select font-size is UNCHANGED by the density pass above and by
   this fix -- shrinking it would make the WCAG 2.1 AA touch-target gap
   worse, not better. Vertical padding bumped here specifically to close
   that gap: measured at 38px before (font-size 0.95rem + line-height
   1.5 + 2px border + 0.5rem top/bottom padding), short of the 44px
   minimum interactive-target dimension. Horizontal padding untouched --
   only the dimension that was actually short changes. */
input, select {
  font-family: inherit;
  font-size: 0.95rem;
  padding: 0.75rem 0.6rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text);
  width: 100%;
  max-width: 320px;
}
input:focus, select:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
  border-color: var(--color-accent);
}
input:disabled { background: var(--color-bg-subtle); color: var(--color-text-muted); }
.error { color: var(--color-error); margin-bottom: var(--space-5); }

/* Actions cell with multiple controls in one table cell (Team
   Invitations, 2026-07-31 layout fix) -- stacks controls onto their own
   rows instead of packing them inline, so a role-select + 2 buttons
   never overlap or force the table to scroll horizontally. Deliberately
   does NOT touch .button-link/input/select sizing -- Section 10.2's
   44px-minimum note above still applies; this only changes how the
   controls around them are grouped. */
.actions-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  white-space: normal; /* th/td default is nowrap -- these rows need to stack, not run together */
  min-width: 200px; /* keeps Resend+Cancel paired on one line -- without this, table auto-layout can squeeze the column once wrapping is allowed, forcing every control onto its own line even where there's room for two */
}
.actions-stack .actions-row,
.actions-stack form.actions-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin: 0;
}
.actions-stack form { margin: 0; }
.actions-stack select {
  width: auto;
  max-width: 140px;
}
.actions-stack .button-link {
  margin: 0;
}
.actions-stack .button-link:disabled {
  background: var(--color-bg-subtle);
  color: var(--color-text-muted);
  cursor: not-allowed;
}
.saved-message { color: var(--color-success); margin-bottom: var(--space-5); }

/* Buttons — vertical padding bumped for the same WCAG 2.1 AA reason as
   input/select above: measured at 37px before, short of 44px. Shares
   its new vertical padding with .button-link below (both are
   interactive controls of the same real-world size; they differ in
   background/border, not height) rather than each getting its own
   separately-tuned value -- two padding rules fixing three measured
   gaps (submit, button-link, and input/select), not three. */
.btn, form button[type="submit"] {
  display: inline-block;
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.5;
  padding: 0.75rem 1.25rem;
  background: var(--color-accent);
  color: #ffffff;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: none;
  text-align: center;
  margin-top: var(--space-5);
}
.btn:hover, form button[type="submit"]:hover { background: var(--color-accent-hover); }

/* A plain <a> link styled to look like a <button>, for page-to-page
   navigation (not form submission) that should still look clickable.
   Vertical padding matches .btn/submit above (see that rule's own
   comment) -- was 34px before, the shortest of the three measured
   gaps. Horizontal padding and font-size stay smaller/lighter on
   purpose: this is still meant to read as the secondary action next to
   a primary .btn, just no longer short on height. */
.button-link {
  display: inline-block;
  padding: 0.75rem 0.8rem;
  background: var(--color-bg-subtle);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  text-decoration: none;
  font-size: 0.9rem;
}
.button-link:hover { background: var(--color-border-light); }

.filter-bar, .picker-bar {
  display: flex;
  align-items: flex-end;
  gap: var(--space-5);
  flex-wrap: wrap;
  background: var(--color-bg-card);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-5);
}
.filter-bar label, .picker-bar label { margin-top: 0; }
.filter-bar input, .filter-bar select,
.picker-bar input, .picker-bar select { max-width: 200px; }
.filter-bar button, .picker-bar button { margin-top: 0; }

/* Add Entry form (/entries) -- was a single stacked column of full-
   width fields, the main scroll driver on that page (2026-07-28
   density pass). Each field is now its own .field wrapper (label +
   control + optional hint, e.g. #rate-warning or the Hours field's
   requirement note) so the fields can lay out as a real grid instead
   of relying on <label>'s own margin for vertical rhythm. 3 columns
   desktop, 2 tablet, 1 phone -- reuses this stylesheet's existing
   900px/600px breakpoints rather than inventing new ones. Row-gap is
   intentionally small (fields are short); column-gap uses --space-6
   for real visual separation between columns. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2) var(--space-6);
  margin-bottom: var(--space-4);
}
.form-grid .field { min-width: 0; }
.form-grid .field label { margin-top: 0; }

/* Tablet: tighten spacing, let bars wrap more readily, and collapse the
   sidebar into a slide-out drawer -- reusing this same breakpoint
   (rather than introducing a second one) since a permanently-visible
   220px sidebar leaves too little room for content once the viewport
   itself is this narrow (Section 10.1, v29). */
@media (max-width: 900px) {
  .main-content { padding: var(--space-5) var(--space-4) var(--space-8); }
  .card { padding: var(--space-4) var(--space-5) var(--space-5); }
  .form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .sidebar-toggle { display: block; }
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    height: 100vh;
    width: 80%;
    max-width: 280px;
    z-index: 100;
    transform: translateX(-100%);
    /* Belt-and-suspenders alongside the html overflow-x fix above
       (2026-08-05): transform alone only moves the drawer visually --
       it stays a real, hit-testable, potentially-paintable box the
       whole time. visibility:hidden actually removes it from
       rendering/hit-testing while closed, so even if some mobile
       browser's scroll-width calculation for a position:fixed element
       ever disagrees with its transformed position again, there's
       nothing there to see. Delayed 0.25s on close (matching the
       transform's own duration) so the slide-closed animation still
       plays before the drawer disappears; 0s delay on open so it's
       visible from the very first frame of the slide-open animation --
       the standard idiom for animating a property that has no
       intermediate visual state.
    */
    visibility: hidden;
    transition: transform 0.25s ease, visibility 0s linear 0.25s;
    /* Same fix as .main-content's own padding-top below, applied to the
       drawer itself (2026-08-11) -- confirmed by reading the CSS, not
       assumed, that moving the identity block to the top (nav.ejs)
       does NOT by itself clear .sidebar-toggle: the button is
       position:fixed at top/left 0.75rem, independent of whatever
       content the drawer renders first, and it sits at a higher
       z-index (110) than the open drawer (100). Without this, the
       first line of whatever's now first in the drawer -- currently
       the identity block's email -- would be the thing rendered behind
       the button instead of "Timesheets", not a fix.
       overflow-y: auto (inherited from the base .sidebar rule above,
       unconditionally -- this block never overrides it) is what makes
       the drawer's content scrollable when it's taller than the
       viewport; nothing else to add here for that. */
    padding-top: 3.5rem;
  }
  .sidebar.open {
    transform: translateX(0);
    visibility: visible;
    transition: transform 0.25s ease, visibility 0s linear 0s;
  }
  .sidebar-backdrop.open { display: block; }
  .main-content { padding-top: 3.5rem; } /* clears the fixed hamburger button */
}

/* Phone: full-width inputs, tighter padding, stacked bars */
@media (max-width: 600px) {
  .main-content { padding: 3.5rem 0.6rem var(--space-7); }
  h1 { font-size: 1.3rem; }
  .card { padding: var(--space-3) var(--space-4) var(--space-4); }
  .form-grid { grid-template-columns: 1fr; }
  input, select { max-width: 100%; }
  .filter-bar, .picker-bar { padding: var(--space-3) var(--space-4); }
  .filter-bar input, .filter-bar select,
  .picker-bar input, .picker-bar select { max-width: 100%; }
  .btn, form button[type="submit"] { width: 100%; }
}

/* Status pills (Section 9.4, v32) -- the invite list screen's live
   Clerk status per row. Colors: pending=neutral, accepted=success,
   revoked/expired=muted-error, matching --color-success/--color-error
   already used elsewhere rather than inventing a new palette. */
.status-pill {
  display: inline-block;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: capitalize;
}
.status-pending { background: var(--color-bg-subtle); color: var(--color-text-muted); }
.status-accepted { background: #dcfce7; color: var(--color-success); }
.status-revoked { background: #fee2e2; color: var(--color-error); }
.status-expired { background: #fef3c7; color: #92400e; }
