*, *::before, *::after { box-sizing: border-box; }

:root {
  --bg: #f4f6fa;
  --surface: #ffffff;
  --ink: #0f172a;
  --ink-2: #334155;
  --muted: #64748b;
  --line: #e2e8f0;
  --line-soft: #eef2f7;

  --primary: #4f46e5;
  --primary-strong: #4338ca;
  --primary-soft: #eef2ff;
  --link: #4f46e5;
  --primary-line: #c7d2fe;
  --surface-2: #fafbfd;
  --nav-bg: rgb(255 255 255 / .88);
  --grad: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);

  --ok: #059669;      --ok-soft: #ecfdf5;   --ok-ink: #047857;
  --warn: #d97706;    --warn-soft: #fffbeb; --warn-ink: #b45309;
  --bad: #e11d48;     --bad-soft: #fff1f2;  --bad-ink: #be123c;
  --info-soft: #eef2ff; --info-ink: #4338ca;

  --side-bg: #0b1428;
  --side-ink: #94a3b8;
  --side-ink-hi: #f1f5f9;

  --radius: 14px;
  --radius-sm: 10px;
  --shadow: 0 1px 2px rgb(15 23 42 / .05), 0 10px 30px -18px rgb(15 23 42 / .18);
  --shadow-lift: 0 2px 4px rgb(15 23 42 / .06), 0 16px 40px -20px rgb(79 70 229 / .35);
}

html[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0b1220;
  --surface: #121c31;
  --ink: #e6ecf5;
  --ink-2: #c2cde0;
  --muted: #8b9ab3;
  --line: #24334f;
  --line-soft: #1c2942;

  --primary: #6366f1;
  --primary-strong: #818cf8;
  --primary-soft: #1c2752;
  --link: #97a7fd;
  --primary-line: #3b4a80;
  --surface-2: #172341;
  --nav-bg: rgb(13 20 36 / .92);

  --ok: #10b981;      --ok-soft: #0a2b22;   --ok-ink: #5eead4;
  --warn: #d97706;    --warn-soft: #33240a; --warn-ink: #fbbf24;
  --bad: #e11d48;     --bad-soft: #3a1424;  --bad-ink: #fb7185;
  --info-soft: #1c2752; --info-ink: #a5b4fc;

  --side-bg: #070d1a;
  --shadow: 0 1px 2px rgb(0 0 0 / .4), 0 10px 30px -18px rgb(0 0 0 / .6);
  --shadow-lift: 0 2px 4px rgb(0 0 0 / .5), 0 16px 40px -20px rgb(79 70 229 / .4);
}

html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 15.5px/1.55 "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, sans-serif;
}
h1 { font-size: 1.45rem; font-weight: 700; letter-spacing: -.015em; margin: .25rem 0 1.1rem; }
h1 .badge { vertical-align: middle; }
a { color: var(--link); text-decoration: none; }
a:hover { color: var(--primary-strong); }
.muted { color: var(--muted); }
.small { font-size: .85rem; }
.icon { width: 1.15em; height: 1.15em; vertical-align: -.22em; stroke-width: 1.8; }

/* ---------- app shell ---------- */
/* One navigation for every screen: below 900px the sidebar slides in as a
   drawer, above it the same markup is a fixed column. Nothing is dropped
   from the mobile menu — it is the desktop menu, fitted. */
.sidebar {
  display: flex; flex-direction: column;
  position: fixed; inset: 0 auto 0 0; z-index: 60;
  width: min(280px, 84vw); max-width: 100%;
  background: var(--side-bg); color: var(--side-ink);
  padding: 1.1rem .8rem .9rem;
  transform: translateX(-100%);
  /* visibility keeps the closed drawer out of the tab order, and only flips
     back once the slide-out has finished */
  visibility: hidden;
  transition: transform .22s ease, visibility 0s linear .22s;
  overflow-y: auto;
}
.sidebar.open {
  transform: none; visibility: visible;
  transition: transform .22s ease, visibility 0s;
  box-shadow: 0 0 60px rgb(0 0 0 / .5);
}
.nav-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: rgb(2 6 23 / .55); backdrop-filter: blur(2px);
}
body.nav-open { overflow: hidden; }
.navtoggle { padding: .3rem .45rem; }
@media (prefers-reduced-motion: reduce) { .sidebar { transition: none; } }
/* Sidebar internals apply at EVERY width — the drawer is the same nav. */
.side-brand {
  display: flex; align-items: center; gap: .6rem; color: var(--side-ink-hi);
  font-weight: 700; padding: .2rem .55rem 1rem; letter-spacing: -.01em;
}
.side-brand .small { display: block; font-weight: 500; color: var(--side-ink); letter-spacing: 0; }
/* a logo is its own wordmark — give it the full column instead of a flex slot */
.side-brand.has-logo { display: block; }
.side-nav { flex: 1; overflow-y: auto; }
.nav-section {
  font-size: .67rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: #475569; padding: 1.05rem .6rem .35rem;
}
.side-link {
  display: flex; align-items: center; gap: .6rem;
  color: var(--side-ink); font-weight: 500; font-size: .92rem;
  padding: .48rem .6rem; border-radius: 9px; margin-bottom: 1px;
}
.side-link .icon { opacity: .85; }
.side-link:hover { color: var(--side-ink-hi); background: rgb(148 163 184 / .10); }
.side-link.active {
  color: #fff; background: linear-gradient(90deg, rgb(79 70 229 / .35), rgb(124 58 237 / .16));
  box-shadow: inset 2.5px 0 0 var(--primary);
}
.side-user {
  border-top: 1px solid rgb(148 163 184 / .18); padding: .8rem .55rem 0;
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  font-size: .8rem; color: var(--side-ink);
}
.side-user .email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.topbar {
  position: sticky; top: 0; z-index: 20;
  display: flex; justify-content: space-between; align-items: center; gap: .8rem;
  background: var(--nav-bg); backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line); padding: .65rem 1rem;
}
.brand { display: flex; align-items: center; gap: .55rem; font-weight: 700; letter-spacing: -.01em; }
.logomark {
  width: 1.9rem; height: 1.9rem; border-radius: 9px; background: var(--grad);
  color: #fff; display: inline-flex; align-items: center; justify-content: center;
  font-size: .95rem; font-weight: 700; box-shadow: var(--shadow-lift);
}
.container { max-width: 74rem; margin: 0 auto; padding: 1.1rem 1rem 2rem; }
@media (min-width: 1200px) { .container { padding: 1.7rem 2.4rem 2.6rem; } }

/* Cards that sit side by side when there's room, full-width when alone.
   min(23rem, 100%) rather than a bare 23rem: minmax treats its first
   argument as a floor the column may never go below, so on a phone narrower
   than 23rem (368px) plus the container's padding the grid stayed 368px wide
   and pushed the whole page sideways. That is most Android phones at 360px
   and the iPhone SE and 13 mini at 375px — and it hit nine pages at once,
   every one that lays its cards out this way. */
.flow-cols { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(23rem, 100%), 1fr)); align-items: start; }
.flow-cols > .card { margin-bottom: 0; }

/* ==================================================== LEAVE PAGE LAYOUT
   The application form is a narrow column beside the tables rather than a
   half-width card under them: it is eight short fields, and giving it half
   the page pushed the balances and the request history apart for nothing.
   One column until there is room for two. */
.leave-layout { display: grid; gap: 1rem; align-items: start; }
.leave-layout > .card { margin-bottom: 0; }
@media (min-width: 1000px) {
  .leave-layout { grid-template-columns: minmax(0, 1fr) 19rem; }
  .leave-balances { grid-column: 1; grid-row: 1; }
  .leave-requests { grid-column: 1; grid-row: 2; }
  /* Spans both rows so the form sits alongside the whole left column. */
  .leave-apply { grid-column: 2; grid-row: 1 / span 2; }
}
/* Tight enough that the whole form is on screen at once — the point of
   moving it. Labels sit on their own line above full-width inputs. */
.form-tight p { margin: 0 0 .55rem; }
.form-tight label { display: block; font-size: .78rem; color: var(--muted); margin-bottom: .15rem; }
.form-tight input[type="text"], .form-tight input[type="date"],
.form-tight input[type="number"], .form-tight input[type="file"],
.form-tight select, .form-tight textarea {
  width: 100%; box-sizing: border-box;
}
.form-tight textarea { min-height: 3.4rem; }
.form-tight .helptext, .form-tight .small { font-size: .72rem; color: var(--muted); }

@media (min-width: 900px) {
  body { padding-bottom: 2rem; }
  .topbar { display: none; }
  .sidebar {
    width: 236px; z-index: 30;
    transform: none; visibility: visible; box-shadow: none; transition: none;
  }
  .nav-backdrop, .navtoggle { display: none !important; }
  /* Only indent for the sidebar when one is actually rendered. Logged-out
     pages (login) have no <aside>, and an unconditional margin pushed their
     centred card 236px off-centre. */
  .sidebar ~ .shell { margin-left: 236px; }
}


/* ---------- cards & tiles ---------- */
.card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.1rem 1.2rem; margin-bottom: 1rem; box-shadow: var(--shadow);
}
.card h2 { margin: 0 0 .7rem; font-size: 1.02rem; font-weight: 700; letter-spacing: -.01em; }
.card .card { box-shadow: none; background: var(--surface-2); }
.card-warn { border-color: #fde68a; background: #fffdf5; }
.card-bad { border-color: var(--bad); background: var(--bad-soft); }
html[data-theme="dark"] .card-warn { border-color: #6b5518; background: #241c08; }

.grid { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fill, minmax(13.5rem, 1fr)); }

/* Section heading above a group of tiles (Manage page) */
.manage-heading {
  font-size: .74rem; font-weight: 700; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); margin: 1.7rem 0 .7rem;
}
.manage-heading:first-of-type { margin-top: .3rem; }

/* A row of buttons/links above a card — replaces repeated inline flex styles */
.btn-row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: 0 0 1rem; }
.kpis { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr)); margin-bottom: 1rem; }

.tile {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .95rem 1rem; display: block; color: var(--ink); box-shadow: var(--shadow);
  transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
}
a.tile:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); border-color: var(--primary-line); color: var(--ink); }
.tile .tile-head { display: flex; align-items: center; gap: .5rem; color: var(--muted); font-size: .78rem; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; }
.tile .count { display: block; font-size: 1.7rem; font-weight: 700; letter-spacing: -.02em; margin-top: .25rem; font-variant-numeric: tabular-nums; }
.tile .sub { font-size: .8rem; color: var(--muted); margin-top: .1rem; }
.tile.tile-warn .count { color: var(--warn-ink); }
.tile.tile-ok .count { color: var(--ok-ink); }

/* A long figure steps down a size rather than running out past the rounded
   corner — "RM83,518.86" has no space in it to wrap at. Which band applies is
   decided by core.templatetags.ui.fit_count, because the length of the number
   is the thing CSS cannot measure. Anything short enough to fit is given no
   class at all and keeps the 1.7rem above. */
.tile .count-md { font-size: 1.35rem; }
.tile .count-sm { font-size: 1.1rem; }
.tile .count-xs { font-size: .9rem; }

.icon-chip {
  width: 1.7rem; height: 1.7rem; border-radius: 8px; display: inline-flex;
  align-items: center; justify-content: center; background: var(--primary-soft); color: var(--primary);
  flex: none;
}
.icon-chip.ok { background: var(--ok-soft); color: var(--ok); }
.icon-chip.warn { background: var(--warn-soft); color: var(--warn); }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .45rem;
  border: 1px solid transparent; background: var(--primary); color: #fff;
  border-radius: var(--radius-sm); padding: .55rem 1.05rem; font-size: .95rem; font-weight: 600;
  cursor: pointer; transition: transform .1s ease, box-shadow .12s ease, background .12s ease;
  box-shadow: 0 1px 2px rgb(79 70 229 / .3);
}
.btn:hover { background: var(--primary-strong); transform: translateY(-1px); box-shadow: var(--shadow-lift); }
.btn:active { transform: translateY(0); }
.btn:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
.btn-secondary { background: var(--surface); color: var(--link); border-color: var(--primary-line); box-shadow: none; }
.btn-secondary:hover { background: var(--primary-soft); }
.btn-danger { background: var(--bad); box-shadow: 0 1px 2px rgb(225 29 72 / .3); }
.btn-danger:hover { background: var(--bad-ink); }
.btn-ok { background: var(--ok); box-shadow: 0 1px 2px rgb(5 150 105 / .3); }
.btn-ok:hover { background: var(--ok-ink); }
.btn-ghost { background: transparent; border-color: var(--line); color: var(--ink-2); box-shadow: none; }
.btn-ghost:hover { background: var(--line-soft); transform: none; box-shadow: none; }
.btn-sm { padding: .3rem .7rem; font-size: .84rem; }
.btn-big { width: 100%; padding: 1rem; font-size: 1.12rem; font-weight: 700; border-radius: 12px; }
.inline-form { display: inline-flex; gap: .6rem; align-items: center; margin: 0; flex-wrap: wrap; }

/* ---------- tables ---------- */
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; }
th, td { border-bottom: 1px solid var(--line-soft); padding: .55rem .65rem; text-align: left; vertical-align: top; }
th {
  color: var(--muted); font-weight: 600; font-size: .78rem; letter-spacing: .04em;
  text-transform: uppercase; white-space: nowrap; border-bottom: 1px solid var(--line);
}
tr:hover > td { background: var(--surface-2); }
tr:last-child > td { border-bottom: none; }
.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* A count that is read rather than added up — days, hours, a rank — sits under
   its own heading instead of hugging the next column. Money still uses .num,
   where right-aligning is what makes two figures comparable at a glance. */
.mid {
  text-align: center; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
th.mid { text-align: center; }

/* ---------- badges & chips ---------- */
.badge {
  display: inline-flex; align-items: center; gap: .3rem; border-radius: 999px;
  padding: .13rem .6rem; font-size: .74rem; font-weight: 600; background: var(--line-soft); color: var(--ink-2);
}
.badge-ok { background: var(--ok-soft); color: var(--ok-ink); }
.badge-warn { background: var(--warn-soft); color: var(--warn-ink); }
.badge-bad { background: var(--bad-soft); color: var(--bad-ink); }
.badge-info { background: var(--info-soft); color: var(--info-ink); }

.shift-chip {
  display: inline-block; border-radius: 7px; padding: .16rem .5rem;
  font-size: .76rem; font-weight: 600; margin: .1rem .1rem .1rem 0; white-space: nowrap;
  background: var(--primary-soft); color: var(--info-ink); border: 1px solid #e0e7ff;
}
/* The client's own roster colours, carried over from their workbook so a
   shift reads the same here as on the sheet they have used for years:
   早 #9E8EFA Morning 07:00-17:00 · 早 #F4CCCC Office 09:00-18:00
   中 #00FFFF Mid 13:00-23:00   · 晚 #BDF483 Night 22:00-07:00
   休 rest #FF6699 · AL #FF9900 · MC #3C78D8 · EL #FF0000
   The two 早 shifts are told apart by colour ALONE on the sheet, which is
   why the palette has to be exact. Backgrounds are a tint of each with the
   true colour on the border, so a full cell of them stays readable. */
.chip-morning { background: #ece9fe; color: #3f2f9e; border-color: #9e8efa; }
.chip-office  { background: #fdeceb; color: #8a2f28; border-color: #f4cccc; }
.chip-mid     { background: #dbfbfb; color: #0b4f4f; border-color: #00d4d4; }
.chip-night   { background: #eefadc; color: #3c5a10; border-color: #bdf483; }
.chip-rest    { background: #ffe8ef; color: #9f1239; border-color: #ff6699; }
.chip-al      { background: #fff2dd; color: #8a4b00; border-color: #ff9900; }
.chip-mc      { background: #e4edfb; color: #1c4587; border-color: #3c78d8; }
.chip-el      { background: #ffe5e5; color: #a10000; border-color: #ff0000; }
/* kept for companies not using the 早/中/晚 pattern */
.chip-day { background: #e0f2fe; color: #075985; border-color: #bae6fd; }
.chip-late { background: #ede9fe; color: #5b21b6; border-color: #ddd6fe; }
.chip-closing { background: #fff1f2; color: #9f1239; border-color: #fecdd3; }
/* clock stamps on the attendance calendar */
.chip-in { background: #ecfdf5; color: #065f46; border-color: #a7f3d0; }
.chip-out { background: #f8fafc; color: #334155; border-color: #e2e8f0; }
.chip-break_start, .chip-break_end { background: #fffbeb; color: #92400e; border-color: #fde68a; }
html[data-theme="dark"] .shift-chip { border-color: #2c3b63; }
html[data-theme="dark"] .chip-day { background: #0c2e42; color: #7dd3fc; border-color: #155e75; }
html[data-theme="dark"] .chip-late { background: #251b47; color: #c4b5fd; border-color: #4c3a8f; }
html[data-theme="dark"] .chip-morning { background: #241c47; color: #c4b5fd; border-color: #6b5cc7; }
html[data-theme="dark"] .chip-office  { background: #3a201e; color: #f7b7b1; border-color: #9c554d; }
html[data-theme="dark"] .chip-mid     { background: #06333a; color: #7fe6ea; border-color: #0f8f97; }
html[data-theme="dark"] .chip-night   { background: #1f3311; color: #c3ee92; border-color: #6f9c3c; }
html[data-theme="dark"] .chip-rest    { background: #3a1424; color: #fda4af; border-color: #a83a5c; }
html[data-theme="dark"] .chip-al      { background: #3a2708; color: #fbbf24; border-color: #a86e12; }
html[data-theme="dark"] .chip-mc      { background: #10233f; color: #93b8f5; border-color: #2f5ea8; }
html[data-theme="dark"] .chip-el      { background: #3d1111; color: #fca5a5; border-color: #a13030; }
html[data-theme="dark"] .chip-closing { background: #3a1424; color: #fda4af; border-color: #6b1d33; }
html[data-theme="dark"] .chip-in { background: #0b2f22; color: #6ee7b7; border-color: #14603f; }
html[data-theme="dark"] .chip-out { background: #1a2436; color: #cbd5e1; border-color: #33415a; }
html[data-theme="dark"] .chip-break_start,
html[data-theme="dark"] .chip-break_end { background: #33240a; color: #fcd34d; border-color: #6b5518; }

.sched-cell { min-width: 7.2rem; }
.is-today { background: var(--primary-soft) !important; }
th.is-today { color: var(--primary-strong); }

/* ---------- messages (toasts) ---------- */
.messages {
  position: fixed; top: .8rem; right: .8rem; z-index: 60;
  width: min(24rem, calc(100vw - 1.6rem)); margin: 0; padding: 0;
}
.msg {
  border-radius: var(--radius-sm); padding: .65rem 2.2rem .65rem 1rem; margin-bottom: .45rem;
  background: var(--info-soft); color: var(--info-ink); font-weight: 500; font-size: .92rem;
  border: 1px solid #e0e7ff; box-shadow: var(--shadow-lift);
  cursor: pointer; position: relative;
  animation: toast-in .28s ease both;
}
.msg::after { content: "×"; position: absolute; right: .8rem; top: .55rem; font-weight: 700; opacity: .5; }
.msg.msg-out { opacity: 0; transform: translateY(-8px); transition: opacity .3s ease, transform .3s ease; }
@keyframes toast-in { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }
.msg-success { background: var(--ok-soft); color: var(--ok-ink); border-color: #a7f3d0; }
.msg-error { background: var(--bad-soft); color: var(--bad-ink); border-color: #fecdd3; }
.msg-warning { background: var(--warn-soft); color: var(--warn-ink); border-color: #fde68a; }
html[data-theme="dark"] .msg { border-color: #2c3b63; }
html[data-theme="dark"] .msg-success { border-color: #14532d; }
html[data-theme="dark"] .msg-error { border-color: #6b1d33; }
html[data-theme="dark"] .msg-warning { border-color: #6b5518; }

/* ---------- forms ---------- */
form p { margin: 0 0 .85rem; }
form label { display: block; font-size: .84rem; font-weight: 600; color: var(--ink-2); margin-bottom: .25rem; }
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=date], input[type=time], select, textarea {
  width: 100%; padding: .55rem .7rem; border: 1px solid var(--line);
  border-radius: var(--radius-sm); font-size: .96rem; background: var(--surface); color: var(--ink);
  transition: border-color .12s ease, box-shadow .12s ease;
}
input:focus, select:focus, textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgb(79 70 229 / .12); outline: none; }
textarea { min-height: 4.5rem; }
ul { padding-left: 1.2rem; }
form ul { list-style: none; padding: 0; margin: 0; }
form ul li { margin-bottom: .3rem; }
.helptext { display: block; font-size: .77rem; font-weight: 400; color: var(--muted); margin-top: .2rem; }
.errorlist { color: var(--bad-ink); margin: 0 0 .4rem; padding-left: 1.1rem; font-size: .87rem; }

/* ---------- login ---------- */
.auth-wrap { min-height: calc(100vh - 10rem); display: flex; align-items: center; justify-content: center; padding: 1rem; }
.auth-card {
  width: 100%; max-width: 26rem; border-radius: 18px; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-lift);
}
.auth-form { padding: 2.2rem 2rem; }

/* ---------- payslip document ---------- */
.payslip-doc {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); overflow: hidden; margin-bottom: 1rem;
}
.payslip-head {
  background: linear-gradient(135deg, #0b1428 0%, #1e1b4b 100%); color: #fff;
  padding: 1.3rem 1.4rem; display: flex; align-items: center; flex-wrap: wrap; gap: 1rem;
}
.payslip-head .eyebrow { font-size: .7rem; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; color: #a5b4fc; }
.pay-entity-name { font-size: 1.5rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.15; }
.pay-entity-sub { font-size: .78rem; color: #a5b4fc; margin-top: .15rem; }
.pay-period { margin-left: auto; text-align: right; }
.pay-period .small { color: #a5b4fc; text-transform: uppercase; letter-spacing: .14em; font-size: .65rem; font-weight: 700; }
.payslip-head .period { font-size: 1.45rem; font-weight: 800; letter-spacing: -.01em; }

/* employee detail block */
.pay-meta {
  display: grid; gap: .45rem 1.4rem; margin-bottom: 1.1rem;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.pay-meta > div { display: flex; flex-direction: column; gap: .05rem; }
.pay-meta span {
  font-size: .64rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}
.pay-meta strong { font-size: .92rem; font-weight: 600; }

/* earnings | deductions, side by side like a printed payslip */
.pay-cols { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
.pay-col table { width: 100%; }
.pay-col th {
  font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted);
  border-bottom: 1px solid var(--line);
}
.pay-net {
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  margin-top: 1rem; padding: .75rem 1rem; border-radius: var(--radius);
  background: var(--line-soft); border: 1px solid var(--line);
}
.pay-net span { font-size: .7rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.pay-net strong { font-size: 1.4rem; font-weight: 800; letter-spacing: -.02em; }
.pay-stat { font-size: .84rem; }
.pay-stat th { font-size: .62rem; letter-spacing: .06em; }

/* signature lines */
.pay-sign { display: flex; gap: 2.5rem; margin-top: 2rem; flex-wrap: wrap; }
.pay-sign > div { font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.pay-sign .line { display: block; width: 12rem; border-bottom: 1px solid var(--ink-2); margin-bottom: .3rem; height: 1.6rem; }

.payslip-body { padding: 1.2rem 1.4rem; }
.payslip-body h2 {
  font-size: .74rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); margin: 1.1rem 0 .4rem;
}
.payslip-body h2:first-child { margin-top: 0; }
.total-row td, .total-row th { border-top: 1px solid var(--line); font-weight: 700; font-size: 1rem; }

.empty { color: var(--muted); font-size: .92rem; padding: .4rem 0; }
.is-muted-row > td { background: var(--line-soft); }

/* ---------- empty states ---------- */
.empty-state { text-align: center; padding: 2.6rem 1.2rem; }
.empty-state .icon-chip {
  width: 3.1rem; height: 3.1rem; border-radius: 14px; margin: 0 auto .9rem;
}
.empty-state .icon-chip .icon { width: 1.5rem; height: 1.5rem; }
.empty-state h3 { margin: 0 0 .35rem; font-size: 1.08rem; font-weight: 700; letter-spacing: -.01em; }
.empty-state p { color: var(--muted); font-size: .93rem; margin: 0 auto 1.1rem; max-width: 27rem; }

/* ---------- "needs you" action list (dashboard) ---------- */
.needs-you { margin-bottom: 1.1rem; }
.needs-list { display: grid; gap: .5rem; }
.needs-item {
  display: flex; align-items: center; gap: .75rem;
  background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--warn);
  border-radius: var(--radius-sm); padding: .7rem .9rem; color: var(--ink);
  transition: transform .12s ease, box-shadow .12s ease;
}
.needs-item:hover { transform: translateX(2px); box-shadow: var(--shadow); color: var(--ink); }
.needs-item .label { flex: 1; font-weight: 600; }
.needs-item .count {
  background: var(--warn-soft); color: var(--warn-ink); border-radius: 999px;
  padding: .1rem .6rem; font-size: .82rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.all-clear {
  display: flex; align-items: center; gap: .7rem;
  background: var(--ok-soft); border: 1px solid #a7f3d0; color: var(--ok-ink);
  border-radius: var(--radius); padding: .9rem 1.1rem; font-weight: 600; margin-bottom: 1.1rem;
}
html[data-theme="dark"] .all-clear { border-color: #14532d; }

/* ---------- clock screen ---------- */
.clock-hero {
  text-align: center; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 1.5rem 1.1rem 1.6rem; margin-bottom: 1rem;
}
.clock-time {
  font-size: 3.5rem; font-weight: 700; letter-spacing: -.035em; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.clock-date { color: var(--muted); font-size: .95rem; margin-top: .4rem; }
.clock-status { margin-top: .9rem; display: flex; gap: .4rem; justify-content: center; flex-wrap: wrap; }
.clock-primary {
  width: 100%; padding: 1.25rem; font-size: 1.3rem; font-weight: 700;
  border-radius: 14px; margin-top: 1.2rem;
}
.clock-secondary {
  display: grid; gap: .5rem; margin-top: .6rem;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
}
.clock-secondary .btn { padding: .7rem .6rem; font-size: .92rem; }

/* Clocked in, the two live choices are opposites: end the day, or step out
   and come back. Stacked full-width they were two bars a thumb could confuse;
   side by side, a mispress needs the wrong HALF of the screen. Ending the day
   sits left, away from where a right thumb rests — the same reasoning that
   puts Reject left of Approve on every approval queue. */
.clock-choice {
  display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; margin-top: 1.2rem;
}
.clock-choice .clock-half {
  width: 100%; padding: 1.1rem .5rem; font-size: 1.05rem; font-weight: 700;
  border-radius: 14px; min-height: 3.9rem;
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
}
.clock-extra { width: 100%; }

/* A disabled punch button had no styling of its own: full colour, pointer
   cursor, and it still lifted and darkened on hover while doing nothing. The
   only explanation — your IP is not whitelisted — lives in the response to a
   submit a disabled button never sends, so the page has to say it by looking
   unavailable. */
.btn:disabled, .btn[disabled] {
  opacity: .45; cursor: not-allowed; filter: grayscale(35%);
}
.btn:disabled:hover, .btn[disabled]:hover {
  transform: none; box-shadow: none; filter: grayscale(35%);
}
@media (min-width: 700px) { .clock-time { font-size: 4rem; } }

/* ---------- notifications ---------- */
.notice {
  display: flex; align-items: flex-start; gap: .7rem;
  padding: .7rem .2rem; border-bottom: 1px solid var(--line-soft);
}
.notice:last-child { border-bottom: none; }
.notice-unread { background: var(--primary-soft); border-radius: 8px; padding-inline: .6rem; }
.bell { position: relative; }
.bell-dot {
  position: absolute; top: -.35rem; right: -.45rem;
  background: var(--bad); color: #fff; border-radius: 999px;
  font-size: .62rem; font-weight: 700; line-height: 1;
  padding: .18rem .34rem; min-width: 1.05rem; text-align: center;
}
/* Same red count on any nav item with work waiting on it, so leave and
   feedback are as hard to walk past as an unread notification. */
.nav-count {
  margin-left: auto; background: var(--bad); color: #fff; border-radius: 999px;
  font-size: .62rem; font-weight: 700; line-height: 1;
  padding: .2rem .36rem; min-width: 1.05rem; text-align: center;
}

.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; border-radius: 50%; flex: none;
  color: #fff; font-size: .68rem; font-weight: 700; letter-spacing: .02em;
  vertical-align: middle; margin-right: .45rem;
}
/* Company logos sit on dark surfaces (sidebar, dark-mode topbar and cards), and
   most client logos are drawn in dark ink for white paper. The white plate keeps
   them legible everywhere; on light surfaces it's invisible anyway. */
.side-logo, .top-logo, .paylogo {
  display: block; border-radius: 7px; background: #fff;
  padding: .4rem .55rem; object-fit: contain;
}
/* generous height cap so a wide wordmark is bound by width and fills the
   column — a tagline at 94px wide is unreadable. A square logo just sits at 4.5rem. */
.side-logo { max-height: 4.5rem; max-width: 100%; margin: 0 auto; }
.top-logo { max-height: 1.7rem; max-width: 8.5rem; }
/* The payslip is the document staff keep — the entity's mark should read
   clearly, not sit as a thumbnail. */
.paylogo { max-height: 84px; max-width: 17rem; padding: .5rem .7rem; }

/* ---------- clock-in success flash ---------- */
.stamp-flash {
  display: flex; align-items: center; gap: .55rem;
  background: var(--ok-soft); color: var(--ok-ink); border: 1px solid #a7f3d0;
  border-radius: var(--radius); padding: .8rem 1rem; margin-bottom: 1rem;
  font-weight: 700; font-size: 1.05rem;
  animation: stamp-pop .45s cubic-bezier(.2, 1.4, .4, 1) both;
}
.stamp-flash .icon { width: 1.5em; height: 1.5em; color: var(--ok); animation: stamp-check .6s ease .1s both; }
html[data-theme="dark"] .stamp-flash { border-color: #14532d; }
@keyframes stamp-pop { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: scale(1); } }
@keyframes stamp-check { 0% { transform: scale(0); } 60% { transform: scale(1.25); } 100% { transform: scale(1); } }

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ---------- print (payslip PDF) ---------- */
@media print {
  /* Always print light, even in dark mode */
  html, html[data-theme="dark"] {
    color-scheme: light;
    --bg: #ffffff; --surface: #ffffff; --surface-2: #ffffff;
    --ink: #0f172a; --ink-2: #334155; --muted: #64748b;
    --line: #e2e8f0; --line-soft: #eef2f7;
  }
  body { background: #fff; padding: 0; font-size: 12.5px; }
  .sidebar, .topbar, .nav-backdrop, .messages, .no-print, .btn, form { display: none !important; }
  .shell { margin: 0 !important; }
  .container { max-width: none; padding: 0; }
  .payslip-doc { border: none; box-shadow: none; }
  .payslip-head {
    background: #fff !important; color: var(--ink);
    border-bottom: 3px solid var(--ink); border-radius: 0; padding: 0 0 .8rem;
  }
  .payslip-head .eyebrow { color: var(--ink-2); }
  .payslip-head .small { color: var(--muted) !important; }
  .payslip-body { padding: .8rem 0; }
  /* on paper the logo needs no white plate, and the head text must be dark */
  .paylogo { background: none; padding: 0; max-height: 76px; }
  .pay-entity-name { color: var(--ink); }
  .pay-entity-sub, .pay-period .small { color: var(--ink-2) !important; }
  .payslip-head .period { color: var(--ink); }
  .pay-net { background: none; border: none; border-top: 2px solid var(--ink); border-radius: 0; padding: .5rem 0; }
  /* keep each block whole rather than split across a page break */
  .pay-cols, .pay-net, .pay-stat, .pay-sign, .pay-meta { break-inside: avoid; page-break-inside: avoid; }
  tr:hover > td { background: transparent; }
  a { color: inherit; }
}

/* ---------- monthly calendar (roster, leave, my shifts, my attendance) ---------- */
.month-cal { table-layout: fixed; min-width: 980px; }
.month-cal th { text-align: left; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.cal-cell { vertical-align: top; border: 1px solid var(--line); padding: .4rem .45rem; height: 7rem; }
.cal-empty { background: var(--line-soft); }
.cal-weekend { background: var(--line-soft); }
.cal-holiday { background: color-mix(in srgb, var(--line-soft) 60%, transparent); }
/* --accent was never declared anywhere, so an unresolvable var() made the
   whole declaration invalid and today's cell drew exactly like every other
   one. A fallback rather than a new variable: --primary is what every
   other 'this one' marker in the file already uses. */
.cal-today { box-shadow: inset 0 0 0 2px var(--accent, var(--primary)); }
.cal-date { font-weight: 700; margin-bottom: .3rem; display: flex; gap: .3rem; align-items: center; flex-wrap: wrap; }
.cal-shift { border-radius: 7px; padding: .25rem .4rem; margin-bottom: .25rem; background: var(--line-soft); }
.cal-shift-name { display: block; font-size: .68rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
/* Someone off, in the same cell as the shifts — dashed so a day's cover and
   a day's absence never read as the same thing at a glance. */
.cal-away { background: transparent; border: 1px dashed var(--line); opacity: .85; }
/* month grid blocks share the shift palette, keyed off the same label */
.cal-shift.chip-office, .cal-shift.chip-morning, .cal-shift.chip-mid, .cal-shift.chip-night,
.cal-shift.chip-rest, .cal-shift.chip-al, .cal-shift.chip-mc, .cal-shift.chip-el {
  border-width: 1px; border-style: solid;
}
/* A cover rule broken on this day — loud on purpose, it needs a decision. */
.cal-cover { background: var(--bad-soft); border: 1px solid var(--bad); }
.cal-cover .cal-shift-name { color: var(--bad-ink); }
.cal-away .cal-shift-name { color: var(--muted); }
.cal-names { font-size: .78rem; line-height: 1.35; }

/* ---------- weekly roster grid ----------
   One character per cell, the way the client's own workbook writes it, so a
   week of seven days fits the screen instead of scrolling sideways. */
.roster { table-layout: fixed; width: 100%; border-collapse: collapse; }
.roster th, .roster td {
  padding: .34rem .2rem; text-align: center; vertical-align: middle;
  border-bottom: 1px solid var(--line-soft);
}
/* Space between the bands, so five blocks read as five blocks rather than one
   wall of cells. */
.roster-group th { border-top: 3px solid var(--line); }
.roster-count td { border-bottom: 2px solid var(--line); }
.roster .roster-name {
  width: 10.5rem; text-align: left; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; padding-left: .45rem;
}
.roster td[data-name] { cursor: pointer; }
.roster td[data-name]:hover { background: var(--surface-2); }
.roster-person { font-weight: 600; }
.roster-day {
  cursor: pointer; font-size: .7rem; line-height: 1.15;
  color: var(--muted); font-weight: 700;
}
.roster-day:hover { background: var(--surface-2); }
.roster-dow { display: block; text-transform: uppercase; letter-spacing: .04em; }
.roster-dom { display: block; color: var(--ink); font-size: .78rem; }
.roster-hol {
  display: inline-block; margin-top: .1rem; font-size: .55rem; padding: 0 .2rem;
  border-radius: 4px; background: var(--info-soft); color: var(--info-ink);
}
.roster .is-today { background: color-mix(in srgb, var(--accent, var(--primary)) 12%, transparent); }

/* Department band, then its own on-duty tally underneath. */
.roster-group th {
  text-align: left; font-size: .68rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
  background: var(--surface-2); padding-top: .45rem; padding-bottom: .45rem;
}
.roster-count td {
  font-size: .72rem; font-weight: 700; color: var(--muted);
  background: color-mix(in srgb, var(--ok) 8%, transparent);
}
.roster-count .roster-name {
  text-transform: none; letter-spacing: 0; font-size: .68rem; font-weight: 600;
}

/* The character cell itself. */
.sh {
  display: inline-block; min-width: 1.55rem; padding: .12rem .18rem;
  border-radius: 6px; border: 1px solid transparent;
  font-size: .82rem; font-weight: 700; line-height: 1.3;
}
.sh-rest { background: #ffe8ef; color: #9f1239; border-color: #ffc2d4; }
.sh-leave { background: #fff4d6; color: #7a4b00; border-color: #f0c86a; font-size: .68rem; }
.sh-clash { background: var(--bad-soft); color: var(--bad-ink); border-color: var(--bad); }

/* Click a name or a date to keep your place across a wide week.
   The selection is made by DIMMING everything else rather than tinting the
   selection: a faint tint on one row of a full grid is invisible, which is
   the whole problem it was meant to solve. */
.roster.row-focus tr:not(.row-on):not(.roster-group):not(:first-child) .sh,
.roster.row-focus tr:not(.row-on):not(.roster-group):not(:first-child) .roster-name,
.roster.row-focus tr.roster-count td { opacity: .2; }
.roster.col-focus td:not(.col-on) .sh,
.roster.col-focus th.roster-day:not(.col-on) { opacity: .2; }

/* The role stripe and its tag are the loudest thing on a dimmed row, and the
   stripe is a BORDER — it survives an opacity that is doing its job on the
   text, so a "dimmed" row still shouts its colour. Muted explicitly, in both
   focus modes: picking a date used to dim only the shift cells and left every
   role marker at full strength down the side. */
.roster.row-focus tr:not(.row-on) .roster-name.role-mgmt,
.roster.row-focus tr:not(.row-on) .roster-name.role-lead,
.roster.col-focus .roster-name.role-mgmt,
.roster.col-focus .roster-name.role-lead { border-left-color: transparent; }
.roster.col-focus .roster-name .role-tag,
.roster.col-focus .roster-name .roster-person { opacity: .35; }

.roster tr.row-on td {
  background: color-mix(in srgb, var(--primary) 22%, transparent);
}
.roster tr.row-on td[data-name] { font-weight: 800; }
.roster td.col-on, .roster th.col-on {
  background: color-mix(in srgb, var(--primary) 22%, transparent);
}
.roster th.col-on { box-shadow: inset 0 -3px 0 var(--primary); }
.roster .sh, .roster .roster-name, .roster td, .roster th {
  transition: opacity .12s ease, background .12s ease;
}
@media (prefers-reduced-motion: reduce) {
  .roster .sh, .roster .roster-name, .roster td, .roster th { transition: none; }
}

.roster-legend {
  display: flex; flex-wrap: wrap; gap: .5rem 1.1rem;
  font-size: .76rem; color: var(--muted); align-items: center;
}

@media (max-width: 599px) {
  .roster th, .roster td { padding: .22rem .08rem; }
  .roster .roster-name { width: 4.6rem; font-size: .68rem; }
  .roster .roster-name .avatar { display: none; }
  .roster-day { font-size: .58rem; }
  .roster-dom { font-size: .68rem; }
  .sh { min-width: 1.15rem; font-size: .72rem; padding: .08rem .1rem; }
  .sh-leave { font-size: .56rem; }
  .roster-legend { font-size: .68rem; gap: .35rem .7rem; }

}

html[data-theme="dark"] .sh-rest { background: #3d1526; color: #ffa7c4; border-color: #7d2d4a; }
html[data-theme="dark"] .sh-leave { background: #3a2c07; color: #f5d183; border-color: #7a5d15; }

/* A whole day square is the tap target, not just the number in it. */
.cal-day-link {
  display: block; color: inherit; text-decoration: none;
  min-height: 100%; border-radius: var(--radius-sm);
}
.cal-day-link:hover { background: var(--surface-2); color: inherit; }

/* Phones: the grid is 980px wide by design, so a month could only be read by
   scrolling sideways a screen at a time. Fit it to the screen instead —
   narrower columns, cells that grow to their content rather than sitting at a
   fixed 7rem, and type scaled to match. max-width only: the desktop grid
   above is untouched. */
@media (max-width: 599px) {
  .month-cal { min-width: 0; width: 100%; }
  .month-cal th {
    font-size: .55rem; letter-spacing: .02em; padding: .25rem .15rem;
  }
  .cal-cell { padding: .22rem .2rem; height: auto; min-height: 3.4rem; }
  .cal-date { font-size: .72rem; margin-bottom: .18rem; gap: .15rem; }
  .cal-shift { padding: .14rem .22rem; margin-bottom: .14rem; border-radius: 5px; }
  .cal-shift-name { font-size: .52rem; letter-spacing: .01em; }
  /* Times are the first thing to go: the shift colour already says which
     shift it is, and the names are what you actually read. */
  .cal-shift-name .muted { display: none; }
  /* At 43px a column wraps a name list into a tower — one Operations day ran
     to 500px, two thirds of the screen for a single date. Clamp to three
     lines: the ellipsis says there is more, and the full list is a tap away
     on the week view. */
  .cal-names {
    font-size: .6rem; line-height: 1.25;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .cal-cell .badge { font-size: .5rem; padding: .05rem .22rem; }
}

/* ---------- feedback board ---------- */
.fb-post { display: flex; gap: .9rem; align-items: flex-start; }
.fb-body { flex: 1; min-width: 0; }
.fb-meta { margin: 0 0 .35rem; display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }
.fb-message { margin: 0; white-space: pre-wrap; }
/* The facts an approver weighs, given room instead of grey small print.
   Used by every approval queue so they read the same way. */
.decide-facts {
  display: flex; flex-wrap: wrap; gap: .5rem; margin: .6rem 0;
}
.decide-facts > div {
  flex: 1 1 14rem; min-width: 0;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: .5rem .7rem;
}
.decide-facts dt {
  font-size: .66rem; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); margin-bottom: .15rem;
}
.decide-facts dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }

/* The decision bar at the foot of every approval card: what you can adjust
   on the left, what you can do on the right, separated from the facts above
   so the buttons stop looking like they landed there by accident. */
.decide-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  margin: .9rem -1.2rem -1.1rem; padding: .75rem 1.2rem;
  border-top: 1px solid var(--line); background: var(--surface-2);
  border-radius: 0 0 var(--radius) var(--radius);
}
.decide-bar .field { display: flex; align-items: center; gap: .4rem; }
.decide-bar label {
  font-size: .72rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); margin: 0;
}
.decide-bar input[type="number"] { width: 5.5rem; }
.decide-bar .decide-note { flex: 1 1 12rem; min-width: 0; }
.decide-actions { display: flex; gap: .5rem; margin-left: auto; }
@media (max-width: 599px) {
  .decide-actions { margin-left: 0; width: 100%; }
  .decide-actions .btn { flex: 1; }
}

/* a whole notification row is clickable, not just its title */
.notice-link { color: inherit; cursor: pointer; border-radius: var(--radius-sm); }
.notice-link:hover { color: inherit; background: var(--surface-2); }
.notice-link strong { color: var(--ink); }

.fb-comment {
  margin-top: .5rem; padding: .45rem .7rem; border-radius: var(--radius-sm);
  background: var(--surface-2); border-left: 3px solid var(--line);
}
.fb-comment-author { border-left-color: var(--warn); }
.fb-followup { margin-top: .5rem; }
.fb-followup textarea { width: 100%; }
.fb-reply {
  margin-top: .6rem; padding: .5rem .7rem; border-radius: var(--radius-sm);
  background: var(--surface-2); border-left: 3px solid var(--primary);
}
/* Attached screenshot: capped so a phone photo can't run away with the card. */
.fb-shot { display: inline-block; margin-top: .6rem; }
.fb-shot img {
  display: block; max-width: 100%; max-height: 18rem;
  border-radius: var(--radius-sm); border: 1px solid var(--line);
}

/* User accounts: the email box and both buttons on one line, so a row is one
   band of controls instead of a stack three deep. */
.acct-actions { display: flex; flex-wrap: nowrap; gap: .4rem; align-items: center; }
.acct-actions .inline-form { gap: .3rem; margin: 0; flex-wrap: nowrap; }
.acct-actions input[type="email"] { width: 9.5rem; min-width: 0; }
.acct-actions .btn { white-space: nowrap; }
/* narrow screens: let it stack rather than push the table sideways */
@media (max-width: 899px) { .acct-actions { flex-wrap: wrap; } }

/* ---------- roster tree (owner) ---------- */
/* One block per rung down a single spine. Each gets room: the position and
   its access on top, the people under it, then a quiet line saying what the
   position permits. Cramming those into table columns made it unreadable. */
.rungs { list-style: none; margin: .3rem 0 0; padding: 0 0 0 1.2rem; position: relative; }
.rungs::before {
  content: ""; position: absolute; left: 0; top: .6rem; bottom: .6rem;
  border-left: 2px solid var(--line);
}
.rung-row { position: relative; padding: .85rem 0; }
.rung-row + .rung-row { border-top: 1px solid var(--line-soft); }
.rung-row::before {
  content: ""; position: absolute; left: -1.2rem; top: 1.35rem;
  width: .85rem; border-top: 2px solid var(--line);
}
.rung-owner .rung-name { color: var(--primary); }
.rung-main { display: flex; flex-wrap: wrap; align-items: center; gap: .55rem; }
.rung-name { font-weight: 700; font-size: 1rem; letter-spacing: -.01em; }
.rung-main .inline-form { margin: 0; }
.rung-main select { font-size: .82rem; padding: .18rem .4rem; }
.rung-count { margin-left: auto; font-size: .8rem; color: var(--muted); }
.rung-people { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .55rem; }
.rung-can { margin: .5rem 0 0; font-size: .82rem; color: var(--muted); line-height: 1.5; }

/* Departments side by side rather than stacked down the page. */
.dept-grid { display: grid; gap: .9rem; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); }
.dept {
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: .85rem .95rem; background: var(--surface-2);
}
.dept-headless { border-color: var(--bad); }
.dept-head { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; }
.dept-people { list-style: none; margin: .6rem 0 0; padding: 0; }
.dept-people li {
  display: flex; justify-content: space-between; gap: .6rem;
  padding: .3rem 0; border-top: 1px solid var(--line-soft); font-size: .88rem;
}
.chip {
  display: inline-flex; align-items: center; gap: .4rem;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 999px; padding: .2rem .65rem .2rem .25rem; font-size: .85rem;
}
.chip .avatar { width: 1.45rem; height: 1.45rem; font-size: .62rem; }
.chip .small { padding-left: .2rem; }

/* ---------- edit shifts ---------- */
/* A card per day rather than one long table: finding Tuesday should not mean
   scrolling past every other shift in the month. */
.shift-day { padding: .8rem 1rem; margin-bottom: .6rem; }
.shift-today { border-color: var(--primary); }
.shift-day-head {
  display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap;
  padding-bottom: .5rem; border-bottom: 1px solid var(--line-soft);
}
.shift-day-head .small { margin-left: auto; }
.shift-line {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .5rem 0; border-bottom: 1px solid var(--line-soft);
}
.shift-line:last-child { border-bottom: none; padding-bottom: 0; }
.shift-names { flex: 1 1 12rem; min-width: 0; font-size: .9rem; }
.shift-actions { display: flex; gap: .3rem; margin-left: auto; }
.shift-actions .inline-form { margin: 0; }

/* ---------------------------------------------------------------- claims: attaching files

   The drop zone, the chosen-file list and the attachment thumbnails. All
   built on the existing tokens so they follow the theme toggle without a
   second set of dark-mode rules. */

.dropzone {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .25rem;
  padding: 1.5rem 1rem;
  margin: .35rem 0 .5rem;
  border: 2px dashed var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  text-align: center;
  cursor: pointer;
  transition: border-color .15s, background .15s;
}
.dropzone:hover,
.dropzone:focus-visible { border-color: var(--primary-line); background: var(--primary-soft); }
.dropzone.dropping { border-color: var(--primary); background: var(--primary-soft); }
.dropzone p { margin: 0; }
.dropzone .icon { width: 1.5rem; height: 1.5rem; color: var(--muted); }
.dropzone .link { color: var(--link); text-decoration: underline; }
/* Kept reachable rather than display:none — the picker is the fallback when
   the script does not run, and a screen reader still needs to find it. */
.dropzone input[type=file] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.filelist { list-style: none; margin: 0 0 .75rem; padding: 0; }
.filelist li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .35rem .6rem;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  margin-bottom: .3rem;
  font-size: .85rem;
  background: var(--surface-2);
}

.thumbs { display: flex; flex-wrap: wrap; gap: .6rem; }
.thumb {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  text-decoration: none;
  color: var(--ink-2);
}
.thumb:hover { border-color: var(--primary-line); background: var(--primary-soft); }
.thumb-icon { display: inline-flex; color: var(--muted); }

/* ------------------------------------------------------------- claim detail: the signatures

   A step that has not happened is dimmed rather than hidden, so what is still
   owed and by whom is readable at a glance. */
.timeline { list-style: none; margin: 0 0 .75rem; padding: 0; }
.timeline li {
  position: relative;
  padding: .35rem 0 .35rem 1.4rem;
  border-left: 2px solid var(--line);
  opacity: .55;
}
.timeline li.done { opacity: 1; border-left-color: var(--ok); }
.timeline li::before {
  content: "";
  position: absolute;
  left: -.42rem; top: .7rem;
  width: .7rem; height: .7rem;
  border-radius: 50%;
  background: var(--line);
}
.timeline li.done::before { background: var(--ok); }
.timeline strong { display: block; font-size: .9rem; }

.detail-list { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: .35rem .9rem; }
.detail-list dt { color: var(--muted); font-size: .85rem; }
.detail-list dd { margin: 0; font-size: .9rem; }
/* The amount an approver is signing for. Same list, but it should not read
   at the same weight as the invoice number three rows below it. */
.detail-list dd.figure { font-size: 1.05rem; font-weight: 650; }

/* ------------------------------------------------------------------- filter bars and tiles */
.filter-bar { flex-wrap: wrap; gap: .5rem; align-items: center; }
.filter-bar label { margin: 0; }

/* A tile that is also the active filter. Tiles are links on the Attendance
   page, so the selected one has to look pressed rather than merely hovered. */
.tile-active {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}

@media (max-width: 640px) {
  .detail-list { grid-template-columns: 1fr; gap: .1rem .5rem; }
  .detail-list dd { margin-bottom: .5rem; }
}

/* -------------------------------------------------------- clock: break vs clock out

   These two sit next to each other and end very differently — one pauses the
   day, the other closes it. They used to differ only by their words, and
   people reaching for lunch hit the red one. Break gets its own colour and
   icon so the choice is readable at arm's length on a phone. */

.clock-break {
  background: var(--warn-soft);
  color: var(--warn-ink);
  border: 2px solid var(--warn);
  font-weight: 650;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  padding-block: .7rem;
}
.clock-break:hover { background: var(--warn); color: #fff; }
.clock-break .icon { width: 1.1rem; height: 1.1rem; }
.clock-primary { display: inline-flex; align-items: center; justify-content: center; gap: .5rem; }

/* ------------------------------------------------------------- roster: colour by role

   A different colour per name is noise — thirty hues say nothing. The question
   this page is actually asked is "is a leader on today", which needs four
   colours: management, team lead, assistant team lead, everyone else. */

.roster-name.role-mgmt { border-left: 4px solid var(--primary); font-weight: 650; }
.roster-name.role-lead { border-left: 4px solid #db2777; font-weight: 620; }
.roster-name.role-cs   { border-left: 4px solid transparent; }

.role-tag {
  display: inline-block;
  margin-left: .35rem;
  padding: .05rem .35rem;
  border-radius: 999px;
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
  vertical-align: middle;
}
.role-mgmt .role-tag,
.role-tag.role-mgmt { background: var(--primary-soft); color: var(--primary-strong); }
.role-lead .role-tag,
.role-tag.role-lead { background: #fce7f3; color: #9d174d; }
[data-theme="dark"] .role-mgmt .role-tag { background: #312e81; color: #c7d2fe; }
[data-theme="dark"] .role-lead .role-tag { background: #831843; color: #fbcfe8; }

.roster-band-head { text-align: left; font-weight: 500; padding: .3rem .5rem; }
.roster-band-head .badge { margin-right: .3rem; }

/* Below the minimum on the floor — the number somebody has to fix today. */
.roster-count .count-short {
  background: var(--bad-soft);
  color: var(--bad-ink);
  font-weight: 700;
}
[data-theme="dark"] .roster-count .count-short { background: #4c0519; color: #fecdd3; }

/* A month is 31 columns. It now FITS rather than scrolling: table-layout is
   fixed and the table is 100% wide, so once the name column has its width the
   31 day columns divide what is left equally. Three things bought the room —
   a one-letter weekday instead of "SAT", the position as an acronym instead of
   "Assistant Team Lead", and no avatar. Roughly 27px a column on a laptop,
   which a single 早/中/晚 comfortably fits.

   overflow-x stays as the escape hatch for a genuinely narrow window; it just
   is not the normal case any more. */
.roster-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; }
.roster-month { width: 100%; min-width: 0; }
.roster-month th.roster-name,
.roster-month td.roster-name {
  position: sticky;
  left: 0;
  z-index: 2;
  width: 7.6rem;
  padding-left: .4rem;
  background: var(--surface);
  box-shadow: 1px 0 0 var(--line);
}
.roster-month .roster-group th.roster-name,
.roster-month .roster-count td.roster-name { background: var(--surface-2); }
.roster-month .roster-person { font-size: .74rem; font-weight: 600; }
.roster-month .roster-day,
.roster-month .roster-cell { padding: .26rem 0; }
.roster-month .sh {
  min-width: 0; width: 92%; padding: .1rem 0;
  font-size: .72rem; border-radius: 4px;
}
.roster-month .sh-leave { font-size: .58rem; }
.roster-month .roster-dow { font-size: .58rem; letter-spacing: 0; }
.roster-month .roster-dom { font-size: .68rem; }
.roster-month .role-tag { margin-left: .2rem; font-size: .56rem; padding: 0 .22rem; }

/* The band header is one cell across the whole row, so it has all the space
   it needs and nothing can land on top of it. */
.roster-band-head { text-align: left; padding: .38rem .5rem; }
.band-label { font-weight: 700; letter-spacing: .06em; }
.roster-band-head .small { margin-left: .4rem; text-transform: none; letter-spacing: 0; }
.roster-band-head .badge { margin-left: .5rem; }

/* A name is the one thing that must never be cut off: "Kai Leng ..." is not a
   person you can look up. */
.roster .roster-name { overflow: visible; }
.roster-person { display: inline-block; max-width: 100%; }

/* The band header runs across the grid, so it must not be squeezed into the
   name column and truncated — "MORNING 07:00–1…" told nobody anything. */
.roster-month .roster-group th.roster-name { white-space: nowrap; overflow: visible; }
.roster-band-head { padding-block: .35rem; }

/* A name is the one thing that must never be cut off: "Kai Leng ..." is not a
   person you can look up. */
.roster .roster-name { overflow: visible; }
.roster-person { display: inline-block; max-width: 100%; }

/* The monthly rest-day tally, at the far end of each row. */
.roster-off {
  width: 2.2rem; text-align: center; font-weight: 700; font-size: .72rem;
  font-variant-numeric: tabular-nums;
  border-left: 2px solid var(--line);
}

/* A company trip day: the whole column, so it reads as "everyone" rather than
   as something about one person. Pale enough to sit under the shift chips. */
.roster .is-trip { background: color-mix(in srgb, #f6c343 18%, transparent); }
.roster th.is-trip { background: color-mix(in srgb, #f6c343 30%, transparent); }
.trip-mark { display: block; font-size: .7rem; line-height: 1; }
[data-theme="dark"] .roster .is-trip { background: color-mix(in srgb, #f6c343 14%, transparent); }
[data-theme="dark"] .roster th.is-trip { background: color-mix(in srgb, #f6c343 24%, transparent); }

/* One week at a time on a phone. Hidden on anything larger — the desktop grid
   already fits, and paging a table you can see all of is friction. */
.week-pager { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.week-pager [data-week-of] { margin-left: auto; }
@media (min-width: 600px) { .week-pager { display: none !important; } }

/* The post a notification jumped to. Without this the anchor scroll lands
   somewhere on a page of identical cards and nothing says which one it meant. */
.fb-post:target {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  scroll-margin-top: 5rem;
}

/* The roster tree's rules table is prose, not figures: its row headings are
   whole phrases, and th{white-space:nowrap} gave the first column a ~15rem
   floor that pushed the PAGE sideways on a phone. Headings wrap here. */
.rules-table th { white-space: normal; min-width: 7rem; }
.rules-table td { min-width: 12rem; }

/* One question at a time. Two cards each with their own form used to sit on
   this page at once; the second was space taken whether or not anybody wanted
   it, and the two forms overwrote each other's query string. */
.mode-tabs {
  display: inline-flex; gap: .2rem; margin: 0 0 1rem;
  background: var(--surface-2); padding: .25rem; border-radius: 10px;
}
.mode-tab {
  padding: .4rem 1.1rem; border-radius: 8px; font-size: .9rem; font-weight: 600;
  color: var(--muted); text-decoration: none; line-height: 1.6;
}
.mode-tab:hover { color: var(--ink); }
.mode-tab.is-on {
  background: var(--surface); color: var(--primary-strong);
  box-shadow: var(--shadow-lift);
}

/* ======================================================= DENSE MATRICES
   Wide money tables — allowances paid, leave entitlements, a payroll run —
   are matrices, and a matrix that scrolls sideways hides the columns that
   matter most. Tighter type, and headings may wrap to two lines: a wrapped
   word beats a hidden column. */
table.dense { font-size: .8rem; }
table.dense th, table.dense td { padding: .35rem .4rem; }
table.dense th {
  white-space: normal; overflow-wrap: anywhere;
  font-size: .66rem; letter-spacing: .02em;
}
table.dense .btn-sm { padding: .15rem .45rem; font-size: .72rem; }
table.dense .total-row td, table.dense .total-row th {
  font-size: .85rem; white-space: nowrap; overflow-wrap: normal;
}
table.dense input[type="number"] { width: 3.6rem; }

/* The allowance editor's boxes hold real money — 1,700.00 is eleven glyphs and
   a fixed 3.6rem clipped it to "1,7". The table is width:100%, so on a wide
   screen its columns are wide and the boxes were leaving the gap you could see
   either side of them. They fill the cell instead: wide screen, wide box. The
   min-width is the floor when the table is squeezed. */
table.dense.adjust-table input[type="number"] {
  width: 100%; min-width: 3.4rem; max-width: 8rem;
  box-sizing: border-box; text-align: right;
}
/* Right-aligned inside a right-aligned cell, so the boxes line up under their
   heading instead of drifting left of it once they stop filling the column. */
table.dense.adjust-table td.num { text-align: right; }

/* ------------------------------------------------------------- punch sheet
   The range view of Attendance: one row per person per day, showing the
   punches themselves rather than totals of them, so somebody can read down a
   person and see whether the times are right. Times are tabular so the
   columns line up digit for digit — a 09:00 that should be 08:00 is easier
   to catch when the hours sit in one vertical line. */
table.dense.punch-table td.punch { white-space: nowrap; }
.punch-time { font-variant-numeric: tabular-nums; }
/* A corrected punch is not a wrong punch, so it is marked rather than
   warned about — underlined where it stands, with the pencil beside it
   carrying the reason and the approver. */
.punch-fixed {
  text-decoration: underline dotted;
  text-underline-offset: .18em;
  text-decoration-color: var(--ok-ink);
}
.punch-mark { color: var(--ok-ink); cursor: help; font-size: .85em; }
/* A checkbox that sits on the filter bar's baseline beside the selects
   rather than dropping to its own line and breaking the row. */
.check-inline {
  display: inline-flex; align-items: center; gap: .35rem;
  white-space: nowrap; cursor: pointer;
}
.check-inline input { margin: 0; }
/* The five scheme switches on Adjust. Wraps rather than scrolls, so all five
   are readable on a phone without a sideways nudge. */
.scheme-ticks { display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; margin: .2rem 0 .6rem; }

/* ------------------------------------------------- leave supporting document
   The attachment shown where the decision is made. Bounded on purpose: an MC
   photographed on a phone is several thousand pixels wide, and unbounded it
   would push the approve/reject buttons off the bottom of the screen — the
   two things the page exists for. Big enough to read a date and a clinic
   name; tap it for full size. */
.leave-doc { margin: .6rem 0; }
.leave-doc img {
  display: block;
  max-width: min(22rem, 100%);
  max-height: 15rem;
  width: auto; height: auto;
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--surface-2);
  object-fit: contain;
}

/* A checkbox that IS the cell — the TO EPF column on Adjust. Padded so a
   thumb has something to hit, since the box itself is smaller than a finger
   and the decision behind it changes somebody's contributions. */
label.tick {
  display: block; padding: .25rem; cursor: pointer; text-align: center;
}
label.tick input { margin: 0; width: 1.05rem; height: 1.05rem; cursor: pointer; }

/* ---------------------------------------------------------- form sections
   Grouped fields on the employee record. Thirty inputs under no headings is
   how a switch that changes somebody's SOCSO deduction gets reported as not
   existing. */
fieldset.form-section {
  border: 0; border-top: 1px solid var(--line);
  margin: 1.4rem 0 0; padding: .2rem 0 0;
}
fieldset.form-section:first-of-type { border-top: 0; margin-top: 0; }
fieldset.form-section > legend {
  padding: 0 .5rem 0 0;
  font-size: .78rem; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted);
}
fieldset.form-section .helptext { display: block; }

/* ============================================================= PHONE ROSTER
   Kept at the END of the file on purpose. Media queries add no specificity,
   so between equal selectors source order decides — and the phone rules have
   twice been silently killed by desktop blocks appended after them. Anything
   added below this line must not style .roster-month. */
@media (max-width: 599px) {
  /* The pager hides columns with the hidden attribute; nothing may resurrect
     them. */
  .roster th[hidden], .roster td[hidden] { display: none !important; }

  .roster-month th.roster-name,
  .roster-month td.roster-name { width: 5.6rem; padding-left: .3rem; }
  .roster-month .roster-person { font-size: .68rem; }
  /* The acronym sits UNDER the name on a phone — beside it, the pair would
     not fit 5.6rem and the name is the one that must never truncate. */
  .roster-month .roster-person { display: block; line-height: 1.15; }
  .roster-month .role-tag { margin: 0; font-size: .54rem; padding: 0 .2rem; }
  .roster-month .roster-day,
  .roster-month .roster-cell { padding: .2rem .04rem; }
  .roster-month .sh { width: auto; min-width: 1.35rem; font-size: .72rem; padding: .1rem .12rem; }
  .roster-month .sh-leave { font-size: .56rem; }
  .roster-month .roster-dow { font-size: .56rem; }
  .roster-month .roster-dom { font-size: .72rem; }
  .roster-month .roster-group th.roster-name { white-space: normal; }
  .roster-band-head { white-space: normal; }
  .roster-off { width: 2rem; font-size: .68rem; }

  /* The pager already presents the month one week at a time, so the separate
     week view is the same thing with worse navigation. The link goes; the
     URL still works for a desktop bookmark opened on a phone, and the Month
     button stays visible there as the way back. */
  .week-link { display: none; }
}

/* The allowance editor on a phone. The statutory yes/excluded badge is a fact
   about the TYPE, not about the amount being typed into the row, and it is
   the column that pushes this table into a sideways swipe. It stays on the
   desktop and on the Allowance types page, which is where it is decided.
   Touches nothing in .roster-month — see the note above. */
@media (max-width: 599px) {
  /* Selectors must out-specify the table.dense block above — a media query
     adds no specificity of its own, which is how phone rules in this file
     have twice been silently overridden. */
  /* The Statutory column used to be hidden here entirely, which was fine
     while it was a read-only badge. It is a TICK now — the control that
     decides whether an allowance feeds EPF/SOCSO/EIS — so hiding it would
     put that decision out of reach of anyone on a phone. A checkbox is
     narrow; it stays, and the heading shortens instead. */
  table.dense.adjust-table th.opt { font-size: 0; }
  table.dense.adjust-table th.opt::after {
    content: "EPF"; font-size: .62rem; letter-spacing: 0;
  }
  table.dense.adjust-table td.opt { padding-left: 0; padding-right: 0; text-align: center; }
  table.dense.adjust-table th, table.dense.adjust-table td { padding: .3rem .2rem; }
  /* 375px has room for a name and three number columns only if the digits
     are small. "1000.00" has to be readable at a glance, not just while the
     cursor is in the box. */
  table.dense.adjust-table input[type="number"] {
    min-width: 2.4rem; font-size: .7rem; padding: .25rem .2rem;
  }
  table.dense.adjust-table td, table.dense.adjust-table th { font-size: .68rem; }
  /* Amount holds the biggest figure and is the one being typed on a monthly
     row, so it takes its width from rate and units rather than the reverse. */
  table.dense.adjust-table input[name$="_amount"] { min-width: 3.4rem; }
  table.dense.adjust-table .total-row th { font-size: .75rem; }
}

/* ---- correcting a leave balance from the report row it appears on -------
   A <details> per row rather than a modal: no JavaScript, it collapses back
   to a button's worth of space, and only one row's form is ever open in
   front of somebody. The panel is positioned out of the table's flow because
   a form growing inside a cell drags every other row's height with it. */
.row-fix { display: inline-block; position: relative; }
.row-fix > summary {
  list-style: none; cursor: pointer; user-select: none; white-space: nowrap;
}
.row-fix > summary::-webkit-details-marker { display: none; }
.row-fix[open] > summary { background: var(--surface-2); }
.row-fix > .stack-sm {
  position: absolute; right: 0; z-index: 30; margin-top: .35rem;
  min-width: 15rem; padding: .75rem;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0,0,0,.18);
  text-align: left;
}
.row-fix > .stack-sm > * { display: block; width: 100%; }
.row-fix > .stack-sm > label { margin-top: .5rem; }
.row-fix > .stack-sm > input { margin-top: .15rem; }
.row-fix > .stack-sm > button { margin-top: .7rem; }
.row-fix > .stack-sm p { margin: 0 0 .2rem; }

/* The last rows of a long report would open their panel below the fold, so
   those flip upward. Nothing else in the table needs to know. */
tr:nth-last-child(-n+3) .row-fix > .stack-sm { top: auto; bottom: 100%; margin: 0 0 .35rem; }

/* ---- receipts, shown rather than named ---------------------------------
   ClaimAttachment.is_image decides which of these two an attachment gets.
   A picture is what an approver can actually check a claim against; a
   filename is not. Non-images keep .thumb and stay a download. */
.receipts { display: flex; flex-wrap: wrap; gap: .7rem; align-items: flex-start; margin: .6rem 0; }
.receipt { margin: 0; }
.receipt img {
  display: block;
  max-width: min(16rem, 100%);
  max-height: 12rem;
  width: auto; height: auto;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2);
  object-fit: contain;
}
.receipt:hover img { border-color: var(--primary-line); }
.receipt figcaption { margin-top: .25rem; max-width: 16rem; }

/* In a table cell the picture has to stay small enough that the row is still
   a row — big enough to tell a taxi receipt from a hotel bill, and clickable
   through to full size for anything closer than that. */
.receipt-mini { display: inline-block; margin: 0 .25rem .25rem 0; }
.receipt-mini img {
  display: block;
  width: 3.4rem; height: 3.4rem;
  object-fit: cover;
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--surface-2);
}
.receipt-mini:hover img { border-color: var(--primary-line); }
