*, *::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;
  --ok-line: #a7f3d0; --warn-line: #fde68a; --bad-line: #fecdd3; --info-line: #e0e7ff;

  --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;
  --ok-line: #14532d; --warn-line: #6b5518; --bad-line: #6b1d33; --info-line: #2c3b63;

  --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);
}

/* ---------------------------------------------------------------- brands
   A company picks its palette (accounts.Company.brand_theme) and the server
   writes it to data-brand on <html>. Only the variables above are restated;
   every rule in the rest of this file already reads them, so a brand cannot
   drift from the layout.

   "gold" is black-and-gold, taken from a client logo of a gold monogram on
   near-black. The trap with gold is contrast: #d4af37 on white is 2.1:1,
   which fails WCAG AA for text by a wide margin and is exactly how a smart
   palette turns into an unreadable one. So the bright gold is used for
   surfaces, borders and large accents, and TEXT gold is a darker bronze in
   light mode (#8a6a12 on white = 5.06:1). Every pair below was measured;
   the lowest is 5.06:1 against a 4.5:1 floor. */
html[data-brand="gold"] {
  --bg: #faf8f3;
  --surface: #ffffff;
  --ink: #191510;
  --ink-2: #463c28;
  --muted: #6b6152;
  --line: #e6dcc6;
  --line-soft: #f2ebdc;

  --primary: #8a6a12;          /* text/icon gold — AA on white */
  --primary-strong: #6d5310;
  --primary-soft: #fdf6e3;
  --link: #8a6a12;
  --primary-line: #dcc98d;
  --surface-2: #fdfbf6;
  --nav-bg: rgb(255 255 255 / .9);
  --grad: linear-gradient(135deg, #d4af37 0%, #8a6a12 100%);

  --info-soft: #fdf6e3; --info-ink: #6d5310;

  --side-bg: #000000;          /* the logo's own ground */
  --side-ink: #b9ae95;
  --side-ink-hi: #f5eeda;

  --shadow: 0 1px 2px rgb(25 21 16 / .05), 0 10px 30px -18px rgb(25 21 16 / .22);
  --shadow-lift: 0 2px 4px rgb(25 21 16 / .07), 0 16px 40px -20px rgb(138 106 18 / .38);
}

html[data-brand="gold"][data-theme="dark"] {
  --bg: #0b0b0d;
  --surface: #16161a;
  --ink: #f2ece0;
  --ink-2: #d8cfbc;
  --muted: #9d9482;
  --line: #2e2a24;
  --line-soft: #232019;

  --primary: #d4af37;          /* on near-black the bright gold is 9.35:1 */
  --primary-strong: #f0d97b;
  --primary-soft: #241d0c;
  --link: #e8c85c;
  --primary-line: #4a3d1a;
  --surface-2: #1d1d22;
  --nav-bg: rgb(11 11 13 / .93);

  --ok: #34d399;      --ok-soft: #0c2a1f;   --ok-ink: #6ee7b7;
  --warn: #f0b429;    --warn-soft: #2e2409; --warn-ink: #fcd34d;
  --bad: #f87171;     --bad-soft: #2e1416;  --bad-ink: #fca5a5;
  --info-soft: #241d0c; --info-ink: #e8c85c;

  --side-bg: #000000;
  --shadow: 0 1px 2px rgb(0 0 0 / .5), 0 10px 30px -18px rgb(0 0 0 / .75);
  --shadow-lift: 0 2px 4px rgb(0 0 0 / .55), 0 16px 40px -20px rgb(212 175 55 / .28);
}

/* Logos further down get a white plate, because most client logos are dark
   ink and would vanish on the dark sidebar. A logo that carries its OWN
   dark ground is the opposite case: the plate puts a black rectangle inside
   a white rounded box. This brand's sidebar is already black, so the mark
   sits straight on it and the join is invisible. */
html[data-brand="gold"] .side-logo,
html[data-brand="gold"] .top-logo,
html[data-brand="gold"] .paylogo { background: none; padding: 0; }

/* Shift chips in the brand's own colours. The stock pastels (blue day,
   green night) are legible but read as somebody else's palette on a black
   and gold page. What the roster actually has to answer at a glance is
   "day or night", so the two are separated by temperature — warm gold for
   the 09:00 shift, cool slate for the 21:00 one — rather than by hue alone.
   Measured, all four well past the 4.5:1 floor: 9.20 and 10.22 in light,
   10.87 and 8.85 in dark. */
html[data-brand="gold"] .chip-day,
html[data-brand="gold"] .chip-morning {
  background: #f6e6b8; color: #4a3708; border-color: #d4af37;
}
html[data-brand="gold"] .chip-night,
html[data-brand="gold"] .chip-night-12h {
  background: #dfe3ee; color: #23304d; border-color: #7d8aa8;
}
html[data-brand="gold"][data-theme="dark"] .chip-day,
html[data-brand="gold"][data-theme="dark"] .chip-morning {
  background: #2e2408; color: #f0d97b; border-color: #6b551a;
}
html[data-brand="gold"][data-theme="dark"] .chip-night,
html[data-brand="gold"][data-theme="dark"] .chip-night-12h {
  background: #1b2233; color: #b3c2e0; border-color: #3d4a66;
}

/* "blue" is navy and sky blue, from a client logo of a navy globe with a
   bright blue swoosh. In light mode navy carries text and filled buttons
   (#0b5ea8 on white, and white on it, 6.6:1); in dark mode the sky blue carries
   links (#6cc0ef on #0e1d30, 8.5:1) and buttons keep a deeper blue so their
   white text still reads (#1675b3, 4.96:1). Measured in tests/test_brand_theme. */
html[data-brand="blue"] {
  --bg: #f3f6fa;
  --surface: #ffffff;
  --ink: #0c1b2e;
  --ink-2: #2f4257;
  --muted: #5f6b7a;
  --line: #dbe4ee;
  --line-soft: #eaf0f6;

  --primary: #0b5ea8;
  --primary-strong: #044778;
  --primary-soft: #e6f1fb;
  --link: #0b5ea8;
  --primary-line: #b6d4ee;
  --surface-2: #f8fbfe;
  --nav-bg: rgb(255 255 255 / .9);
  --grad: linear-gradient(135deg, #044778 0%, #0c8ecd 100%);

  --info-soft: #e6f1fb; --info-ink: #044778;

  --side-bg: #03213f;
  --side-ink: #9db3c9;
  --side-ink-hi: #f1f6fb;

  --shadow: 0 1px 2px rgb(12 27 46 / .05), 0 10px 30px -18px rgb(12 27 46 / .2);
  --shadow-lift: 0 2px 4px rgb(12 27 46 / .06), 0 16px 40px -20px rgb(12 142 205 / .35);
}

html[data-brand="blue"][data-theme="dark"] {
  --bg: #07121f;
  --surface: #0e1d30;
  --ink: #e5eef7;
  --ink-2: #bfd0e2;
  --muted: #8aa0b6;
  --line: #1f3550;
  --line-soft: #172a41;

  --primary: #1675b3;
  --primary-strong: #5bb8ec;
  --primary-soft: #0c2a45;
  --link: #6cc0ef;
  --primary-line: #1f4f78;
  --surface-2: #122640;
  --nav-bg: rgb(7 18 31 / .92);

  --info-soft: #0c2a45; --info-ink: #8fd0f5;

  --side-bg: #031526;
  --side-ink: #9db3c9;
  --side-ink-hi: #f1f6fb;
  --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(12 142 205 / .35);
}

/* The lit menu item is a literal indigo gradient further down; restated in
   the brand's blues so no indigo shows through. */
html[data-brand="blue"] .side-link.active {
  background: linear-gradient(90deg, rgb(12 142 205 / .35), rgb(4 71 120 / .18));
}
/* The same for gold, which showed a violet bar on its black sidebar. */

/* "green" is Konzept Garden: a dark-grey wordmark inside a green ring, on a
   white disc. Green carries text and filled buttons; the crimson leaf in
   their mark is deliberately NOT used as an accent, because --bad is already
   crimson and a brand colour that reads as an error is a trap.

   Every pair measured, both modes. Light: ink 16.69 on surface, muted 5.19,
   primary 7.09, white-on-primary 7.09, side-ink 7.52. Dark: ink 14.52,
   muted 6.26, link 9.33, white-on-primary 5.50, side-ink 8.70. The lowest
   anywhere is 5.19, comfortably past the 4.5 floor.

   The dark block follows the same split blue uses: --primary stays DEEP so a
   .btn's hardcoded white label keeps 5.50, and --link is the light green that
   carries text on the surface. Using one green for both fails one or other. */
html[data-brand="green"] {
  --bg: #f3f7f4;
  --surface: #ffffff;
  --ink: #12211a;
  --ink-2: #33453b;
  --muted: #5f7168;
  --line: #dde7e1;
  --line-soft: #eaf1ec;

  --primary: #00663a;          /* text/icon green - 7.09 on white */
  --primary-strong: #004f2c;
  --primary-soft: #e7f4ec;
  --link: #00663a;
  --primary-line: #b3d9c4;
  --surface-2: #f9fcfa;
  --nav-bg: rgb(255 255 255 / .9);
  --grad: linear-gradient(135deg, #00703c 0%, #004f2c 100%);

  --info-soft: #e7f4ec; --info-ink: #004f2c;

  --side-bg: #0d2119;          /* a deep garden green, not black */
  --side-ink: #9bb3a6;
  --side-ink-hi: #eef6f1;

  --shadow: 0 1px 2px rgb(18 33 26 / .05), 0 10px 30px -18px rgb(18 33 26 / .2);
  --shadow-lift: 0 2px 4px rgb(18 33 26 / .07), 0 16px 40px -20px rgb(0 102 58 / .35);
}

/* Every variable this block leaves out keeps its LIGHT value, not the indigo
   default - the global dark block has the same specificity and comes first in
   the file, so the brand block wins either way. Omitting --surface here would
   paint a white card in dark mode. */
html[data-brand="green"][data-theme="dark"] {
  --bg: #071310;
  --surface: #0f2019;
  --ink: #e6f0ea;
  --ink-2: #c2d5cb;
  --muted: #8aa397;
  --line: #1f3a2d;
  --line-soft: #172d23;

  --primary: #14784a;          /* deep: .btn paints white on it, 5.50 */
  --primary-strong: #5cc48a;
  --primary-soft: #0c2b1d;
  --link: #6fd49c;             /* light: carries text, 9.33 on surface */
  --primary-line: #2f7a52;
  --surface-2: #132a21;
  --nav-bg: rgb(7 19 16 / .92);

  --info-soft: #0c2b1d; --info-ink: #9fe0bd;

  --side-bg: #050f0c;

  --shadow: 0 1px 2px rgb(0 0 0 / .4), 0 10px 30px -18px rgb(0 0 0 / .55);
  --shadow-lift: 0 2px 4px rgb(0 0 0 / .45), 0 16px 40px -20px rgb(20 120 74 / .5);
}

/* The lit menu item is a literal indigo gradient further down; restated so no
   indigo shows through on a green sidebar. */
html[data-brand="green"] .side-link.active {
  background: linear-gradient(90deg, rgb(0 112 60 / .34), rgb(0 79 44 / .16));
}

/* The payslip letterhead is a hardcoded indigo gradient for every brand. It
   is the one document staff keep, and a green logo on an indigo band reads as
   somebody else's stationery. Restated for this brand only - gold and blue are
   untouched, deliberately, because changing them would alter payslips two
   other companies have already issued. */
html[data-brand="green"] .payslip-head {
  background: linear-gradient(135deg, #0d2119 0%, #14432c 100%);
  --payhead-ink: #9fe0bd;
}

html[data-brand="gold"] .side-link.active {
  background: linear-gradient(90deg, rgb(212 175 55 / .24), rgb(138 106 18 / .12));
}

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; }
/* 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; scrollbar-width: thin; }
.nav-section {
  font-size: .67rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  /* Dimmer than the links, but past 4.5:1 on every brand's sidebar; the
     old fixed #475569 was 2.1-2.8:1. */
  color: color-mix(in srgb, var(--side-ink) 80%, var(--side-bg)); padding: 1.05rem .6rem .35rem;
}
.side-link {
  display: flex; align-items: center; gap: .5rem;
  color: var(--side-ink); font-weight: 500; font-size: .9rem;
  padding: .48rem .6rem; border-radius: 9px; margin-bottom: 1px;
  /* One row, always: Correction approvals and its count wrapped at 236px. */
  white-space: nowrap;
}
.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);
}
/* The email above, the buttons below. As one row it fitted three buttons and
   broke on the fourth: a flex item defaults to min-width:auto, so the email
   would not shrink past its own text and overflowed the sidebar instead of
   ellipsising — it read as text showing through from behind the buttons. */
.side-user {
  border-top: 1px solid rgb(148 163 184 / .18); padding: .8rem .55rem 0;
  display: flex; flex-direction: column; align-items: stretch; gap: .45rem;
  font-size: .8rem; color: var(--side-ink);
}
.side-user .email {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0;   /* without this the rule above never takes effect in flex */
}
.side-user-actions {
  display: flex; align-items: center; gap: .25rem; flex-wrap: wrap;
}
/* FIVE BUTTONS ON ONE LINE, in 205px.
   The row holds a language glyph per language the reader is not in, then
   light/dark, change password and log out. A third language took it to five
   buttons at 218px, so log out dropped to a line of its own under the other
   four and read as a stray control rather than part of the row.
   The buttons lose horizontal padding rather than the row losing a button:
   .4rem puts the five at about 168px, and leaves room for the fourth
   language the markup already plans for (six buttons, about 200px). Their
   height is untouched, so the tap target is the same size it was.
   flex-wrap stays as the backstop — a seventh would wrap rather than
   overflow the sidebar.
   min-height evens them up: a glyph button is as tall as its line box (32px)
   and an icon button only as tall as its svg (26px), so the row of five sat
   visibly ragged. .btn already centres its content, so the shorter ones just
   grow into the taller ones' height. */
.side-user-actions .btn-sm { padding-inline: .4rem; min-height: 2rem; }
.side-user-actions .lang-switch { gap: .25rem; }
/* The sidebar is dark in both themes, so these buttons take the brand's
   sidebar ink, and their hover matches a nav link's. The page-wide ghost
   hover is --line-soft, a near-white pill that swallowed the icon here. */
.side-user-actions .btn-ghost { color: var(--side-ink); border-color: rgb(148 163 184 / .25); }
.side-user-actions .btn-ghost:hover { color: var(--side-ink-hi); background: rgb(148 163 184 / .10); }
/* The language switcher is one form holding a button per language the
   reader is not in (two, with three languages). Laid out as its own flex row
   so the buttons sit at the same gap as their neighbours, whether the form
   is in the sidebar, the top bar or the login card. */
.lang-switch { display: inline-flex; align-items: center; gap: .3rem; margin: 0; }
.auth-lang.lang-switch { display: flex; justify-content: center; gap: .5rem; margin-top: .9rem; }

.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);
}
/* The top bar is one row at every width. The actions used to be a wrapping
   .inline-form, and a 375px phone was 4px short of the 379px they needed with
   the logo, so "Log out" dropped to a second line and the bar doubled to 88px.
   Now the buttons never wrap or shrink; the brand is the part that gives way —
   a logo scales down, a name without a logo ends in an ellipsis. */
.topbar > .inline-form { flex: none; flex-wrap: nowrap; }
.topbar .brand { min-width: 0; }
.topbar .top-logo { min-width: 0; }
.topbar .logomark { flex: none; }
.brand-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.logout-icon { display: none; }
@media (max-width: 479px) {
  /* Height untouched (vertical padding stays .3rem), only the air between.
     The action gap is .45rem because that is how far the bell's count
     overhangs its button — any less and the badge sits on the 中 button. */
  .topbar { gap: .5rem; }
  .topbar > .inline-form { gap: .45rem; }
  .topbar .btn-sm { padding-inline: .5rem; }
}
@media (max-width: 359px) {
  /* 320px-class phones: the word no longer fits beside a logo (the bar needs
     330px with it, 297px without), so Log out becomes its icon. The label is
     clipped, not display:none, so it is still the button's accessible name. */
  .logout-icon { display: inline; }
  .logout-label {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
  }
}
.container { max-width: 74rem; margin: 0 auto; padding: 1.1rem 1rem 2rem; }

/* ---------- company settings: a section rail beside one column ----------
   This page carries 25 fields in 9 groups whose sizes run from one field to
   five. It used to be CSS multi-column, which packs cards by HEIGHT: related
   groups landed in different columns, the eye had to run down column one and
   back up to column two, and the ragged bottoms left the holes the columns
   were introduced to remove (2026-09-16, "so much empty space").

   A rail fixes the reading order without giving the width back: sections go
   top to bottom in one readable column, and the horizontal space earns its
   keep as a list you can jump from. No JavaScript — anchors and position:
   sticky, so there is nothing here that can fail the way template script has
   before. Below 60rem it collapses to the plain stacked page a phone wants,
   rail first. */
.settings-layout {
  display: grid; grid-template-columns: 13rem minmax(0, 44rem);
  gap: 1.6rem; align-items: start;
}
.settings-nav {
  position: sticky; top: 1rem; display: flex; flex-direction: column; gap: .1rem;
  padding: .5rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2);
}
.settings-nav a {
  padding: .4rem .6rem; border-radius: var(--radius-sm); color: var(--ink-2);
  font-size: .9rem; font-weight: 600; text-decoration: none;
}
.settings-nav a:hover { background: var(--primary-soft); color: var(--primary-strong); }
/* :target styles the section the URL names, so the rail shows where you are
   without a scroll listener. */
.settings-section:target > h2 { color: var(--primary); }
/* Anchored sections must not slide under the sticky header. */
.settings-section { scroll-margin-top: 1rem; }

/* INPUTS SIZED TO THEIR DATA. Every one used to stretch to the card, so a
   day-of-month between 1 and 31 was as wide as a postal address, which is
   most of why the page read as noise. */
.settings-layout input[type="number"],
#id_pay_day, #id_roster_min_morning, #id_roster_min_day, #id_roster_min_mid,
#id_roster_min_night, #id_roster_rest_days, #id_unpaid_leave_divisor,
#id_hrdf_rate, #id_claim_signatures_required { max-width: 7rem; }
#id_registration_number, #id_tax_number, #id_epf_number, #id_socso_number,
#id_eis_number, #id_hrdf_number, #id_bank_account, #id_bank_name,
#id_payroll_frequency, #id_claim_first_signer, #id_brand_theme,
#id_leave_day_counting { max-width: 22rem; }

@media (max-width: 60rem) {
  .settings-layout { grid-template-columns: minmax(0, 1fr); }
  .settings-nav {
    position: static; flex-direction: row; flex-wrap: wrap;
    margin-bottom: .4rem;
  }
}
@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; }
/* The row keeps the gap its cards gave up, so the card after it does not
   sit on their borders; last in its box it adds nothing (2026-09-15). */
.flow-cols:not(:last-child) { margin-bottom: 1rem; }
/* A claim with several files: Attached takes the wider column. */
@media (min-width: 1100px) { .flow-cols.flow-cols-files { grid-template-columns: 1fr 2fr; } }
/* A table beside a short form: the table takes the wider column. Allowance
   types is five nowrap columns (~555px) and half the page at 1280 is a
   422px card, so Edit/Remove sat behind a sideways scrollbar. */
@media (min-width: 1100px) { .flow-cols.flow-cols-table-first { grid-template-columns: 2fr 1fr; } }
/* The claim and its receipts side by side, the same height. */
.flow-cols.claim-top { align-items: stretch; }

/* ==================================================== 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; }
/* The balances and the requests are one column, stacked by their own grid.
   They used to be two rows of the outer grid with the apply form spanning
   both — and the form is taller than the pair, so the surplus was shared
   between the rows it spanned while `align-items: start` pinned each card
   to the top of its row. The result was a band of empty page between the
   two cards that read as a panel that had failed to load. Nothing spans
   now, so there is no surplus to distribute. */
.leave-main { display: grid; gap: 1rem; align-content: start; }
.leave-main > .card { margin-bottom: 0; }
@media (min-width: 1000px) {
  .leave-layout { grid-template-columns: minmax(0, 1fr) 19rem; }
}
/* 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="file"] { width: 100%; }
.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: 248px; 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 off-centre. */
  .sidebar ~ .shell { margin-left: 248px; }
}


/* ---------- cards & tiles ---------- */
/* One raised surface for cards, tiles, the clock hero and the payslip document. */
.card, .tile, .clock-hero, .payslip-doc {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.card { padding: 1.1rem 1.2rem; margin-bottom: 1rem; }
.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); }
/* One-column settings/forms cards (my settings, two-factor, branch settings,
   claim categories, leave amend): capped so a short form does not stretch
   across the whole container. */
.card-narrow { max-width: 34rem; }

/* A standalone page that is one narrow column - heading and card together -
   centred in the container rather than flush against its left edge.
   .card-narrow caps the card's width but sets no auto margin, so in a 74rem
   container a 34rem card sat hard left with half the page empty beside it.
   The WHOLE block is wrapped, not just the card, so the h1 stays over its
   card instead of being stranded on the far left. */
.page-narrow { max-width: 34rem; margin-inline: auto; }
/* Written twice on purpose. A warn card nested inside another card - the
   repeat-claim warning sits inside Submit a claim - was losing its fill to
   `.card .card` above, which is two classes to this modifier's one, so the
   one box in the app that stops somebody claiming the same money twice drew
   as an ordinary grey sub-card in light mode and yellow only in dark. */
.card.card-warn, .card-warn { border-color: var(--warn-line); background: #fffdf5; }
.card-bad { border-color: var(--bad); background: var(--bad-soft); }
html[data-theme="dark"] .card.card-warn,
html[data-theme="dark"] .card-warn { background: #241c08; }

.grid { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fill, minmax(13.5rem, 1fr)); }
/* The state tick-boxes under "Import Malaysian public holidays" on Leave
   settings. They were an inline style on that page - the one grid in the app
   written outside this file - so neither this stylesheet's phone rules nor
   the column-floor check in test_css_fits_a_phone could see it. Same values;
   11rem already fits a phone, so nothing moves, but the next change to it is
   checked. */
.holiday-states {
  display: grid; gap: .2rem; margin-bottom: .7rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 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; }
/* The same row pushed to the right edge of a table cell — the actions column
   of a list. On .btn-row the 1rem bottom margin goes too: a cell has nothing
   below it to breathe from. */
.justify-end { justify-content: flex-end; }
.btn-row.justify-end { margin: 0; }
.kpis { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr)); margin-bottom: 1rem; }
/* Six small count tiles in a row (Attendance) rather than wrapping to two. */
.kpis-compact { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); margin-bottom: 1rem; }

.tile {
  padding: .95rem 1rem; display: block; color: var(--ink);
  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 .sub-line { display: block; }  /* one fact a line, no dots */
.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 ---------- */
/* A <button> takes the browser's control font (Arial, line-height normal)
   while an <a class="btn"> inherits the page's, so 'PDF / print' sat 5px
   shorter than the CSV link beside it. Both inherit now. */
.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;
  font-family: inherit; line-height: inherit;
  cursor: pointer; transition: transform .1s ease, box-shadow .12s ease, background .12s ease;
  box-shadow: 0 1px 2px color-mix(in srgb, var(--primary) 30%, transparent);
}
.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 color-mix(in srgb, var(--bad) 30%, transparent); }
.btn-danger:hover { background: var(--bad-ink); }
.btn-ok { background: var(--ok); box-shadow: 0 1px 2px color-mix(in srgb, var(--ok) 30%, transparent); }
.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; }
/* An action form that sits inside running text or a table cell as plain
   inline content (a select or one button, nothing to space out). Must stay
   below .inline-form: same specificity, so order decides the display. */
.inline-plain { display: inline; }
/* A signature form as wide as its card, so the note never pushes Approve
   onto a line of its own; the pair wraps together if it must. */
.decide-form { display: flex; }
.btn-pair { display: inline-flex; gap: .6rem; flex-wrap: nowrap; }

/* ---------- 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);
}
/* A report's grand total is a row of <th> so it reads like the heading it
   closes, but a heading's one-line rule was never meant for a total cell:
   the attendance total's "N day(s) worked · N late · N absent" sat nowrap in
   the Date column and pushed the table 211px past its card at 1280. Column
   headings keep their one line; a total wraps like the subtotal rows do. */
.total-row th:not(.num) { white-space: normal; }
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; }
/* A cell that must stay on one line without changing how its text sits —
   a timestamp, a name beside its avatar, a pair of action buttons. .num and
   .mid already forbid wrapping but also realign; this is the wrap rule alone. */
.nowrap { white-space: nowrap; }

/* A page heading's date or count. Beside the title on a desktop; its own line
   on a phone, where "Good afternoon, Lim · Wednesday, 16 September" wrapped
   mid-phrase and stranded "September" below it. The leading separator is only
   there to join the two, so it goes when the line break arrives. */
.h1-sub { font-weight: 500; }

@media (max-width: 599px) {
  h1 .h1-sub { display: block; margin-top: .2rem; }
  h1 .h1-sub .sep { display: none; }
  /* A phone leaves about 343px for a card. An UPPERCASE heading that refuses
     to wrap ("PAYOUT CONFIRMED" is 130px of it) becomes that column's floor
     and pushes a plain four-column table into a sideways swipe. The tables
     that must keep a heading on one line — the roster, the run lines — say so
     themselves and out-specify this. */
  th { white-space: normal; }
  th, td { padding: .5rem .3rem; }
  /* A badge inside a cell sets that column's width, and My payslips sat 17px
     over its card until both of these came in. Measured at 375px: the table
     now ends exactly where the card does. */
  .badge { padding: .13rem .45rem; }

  /* fit_count picks these bands by counting characters, against a tile
     measured at 147px on a desktop. A phone puts two tiles in a row at
     136-160px, so every band landed on or past the inner edge and
     "RM128,747.55" sat 2px over it at 390px. Each band steps down one
     notch. Worst case per band against a 136px tile, measured: 10 chars
     125px, 12 chars 120px, 15 chars 119px, 16 chars 108px. */
  .tile .count { font-size: 1.45rem; }
  .tile .count-md { font-size: 1.15rem; }
  .tile .count-sm { font-size: .95rem; }
  .tile .count-xs { font-size: .8rem; }
}

/* ---------- 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); }
/* A badge turned into a wrapping block paragraph (rostered / cover-clash
   warnings, allowance notes). Same specificity as .badge, later in source. */
.badge-block { display: block; white-space: normal; }

.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 var(--info-line);
}
/* 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; }
/* 中2 / 中4 / 晚2 — Mid late, Mid 12h, Night 12h — are the same band worked
   at different hours, and Shift.chip slugifies their labels, so they arrive
   as chip-mid-late, chip-mid-12h and chip-night-12h. They take the band's
   colour: the client's legend gives them no fill of their own, and with no
   rule at all they drew as a bare glyph, which reads as no shift. */
.chip-mid, .chip-mid-late, .chip-mid-12h {
  background: #dbfbfb; color: #0b4f4f; border-color: #00d4d4;
}
.chip-night, .chip-night-12h {
  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; }
html[data-theme="dark"] .chip-day { background: #0c2e42; color: #7dd3fc; border-color: #155e75; }
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,
html[data-theme="dark"] .chip-mid-late,
html[data-theme="dark"] .chip-mid-12h  { background: #06333a; color: #7fe6ea; border-color: #0f8f97; }
html[data-theme="dark"] .chip-night,
html[data-theme="dark"] .chip-night-12h { 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; }

.is-today { background: var(--primary-soft); }
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 var(--info-line); 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: var(--ok-line); }
.msg-error { background: var(--bad-soft); color: var(--bad-ink); border-color: var(--bad-line); }
.msg-warning { background: var(--warn-soft); color: var(--warn-ink); border-color: var(--warn-line); }

/* ---------- 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], input[type=search], 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 color-mix(in srgb, var(--primary) 12%, transparent); outline: none; }
textarea { min-height: 4.5rem; }

/* iOS Safari zooms the whole page in when a field it is focusing prints text
   under 16px, and leaves it zoomed: the page is then wider than the screen,
   scrolls sideways, and a centred card reads as pushed to the right. That was
   the whole of the login page's "slanted and scrollable" — the card is centred
   to the pixel at every width. 16px exactly because it is Safari's threshold,
   not a size anyone picked. This block has to sit AFTER the rule above: a
   media query adds no specificity, so the one written later wins. */
@media (max-width: 599px) {
  input[type=text], input[type=email], input[type=password], input[type=number],
  input[type=date], input[type=time], input[type=search], select, textarea {
    font-size: 16px;
  }
}
ul { padding-left: 1.2rem; }
/* A list that hugs its card: no outer margin, bullets pulled in slightly.
   Class specificity outranks the bare `ul` rule above. */
.list-tight { margin: 0; padding-left: 1.1rem; }
form ul { list-style: none; padding: 0; margin: 0; }
form ul li { margin-bottom: .3rem; }
/* Recovery codes, shown once after enrolment and again after re-issue: two
   columns of fixed-width text so eight codes fit one card without scrolling. */
.recovery-codes { columns: 2; font-family: monospace; font-size: 1.1rem; line-height: 2; }
.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; }
.auth-lang { text-align: center; margin: .9rem 0 0; }

/* ---------- payslip document ---------- */
.payslip-doc { overflow: hidden; margin-bottom: 1rem; }
.payslip-head {
  /* dim ink on the fixed dark letterhead; the same in both themes */
  --payhead-ink: #a5b4fc;
  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: var(--payhead-ink); }
.pay-entity-name { font-size: 1.5rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.15; }
.pay-entity-sub { font-size: .78rem; color: var(--payhead-ink); margin-top: .15rem; }
.pay-period { margin-left: auto; text-align: right; }
.pay-period .small { color: var(--payhead-ink); text-transform: uppercase; letter-spacing: .14em; font-size: .65rem; font-weight: 700; }
.payslip-head .pay-dim { color: var(--payhead-ink); }
.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 th {
  font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted);
}
.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; }
/* A person's closing line in a per-person report: their total, set apart from
   the day rows above it and, with the heavier rule beneath, from the next
   person below. */
.subtotal-row td {
  font-weight: 700; color: var(--ink); background: var(--primary-soft);
  border-top: 1px solid var(--primary-line); border-bottom: 3px solid var(--primary-line);
}

.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 h2 { 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 var(--ok-line); color: var(--ok-ink);
  border-radius: var(--radius); padding: .9rem 1.1rem; font-weight: 600; margin-bottom: 1.1rem;
}

/* ---------- clock screen ---------- */
.clock-hero { text-align: center; 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; gap: .5rem;
}
.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; 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;
}
@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; }
/* 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. */
.bell-dot, .nav-count {
  background: var(--bad); color: #fff; border-radius: 999px;
  font-size: .62rem; font-weight: 700; line-height: 1;
  min-width: 1.05rem; text-align: center;
}
.bell-dot { position: absolute; top: -.35rem; right: -.45rem; padding: .18rem .34rem; }
.nav-count { margin-left: auto; padding: .2rem .36rem; }

.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 var(--ok-line);
  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; }
@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 { 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; }
/* 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-mid-late, .cal-shift.chip-mid-12h, .cal-shift.chip-night-12h,
.cal-shift.chip-day,
.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-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; }
.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;
  padding-left: .45rem;
}
/* The department under a name is a subtitle, and was set at .small (.85rem)
   — near enough the name's own size that "Project Management" read as a
   second name, overflowed a 10.5rem column and pushed every row taller. Small
   enough to be secondary, on one line, and truncated with the full name in
   the title rather than allowed to collide with what is beside it. */
.roster-name .roster-dept {
  display: block;
  max-width: 100%;
  font-size: .62rem;
  line-height: 1.05;
  font-weight: 400;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.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 .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 { 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; }

/* 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,
.roster td.col-on, .roster th.col-on {
  background: color-mix(in srgb, var(--primary) 22%, transparent);
}
.roster tr.row-on td[data-name] { font-weight: 800; }
.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;
}

.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-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;
  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-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; } }

/* ---------- 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);
}

.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);
  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; }
/* The claim process runs across its full-width card (owner, 2026-09-15):
   the bar and dots along the top. A narrow screen keeps it top to bottom. */
@media (min-width: 700px) {
  .timeline-across { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
  .timeline-across li { border-left: 0; border-top: 2px solid var(--line); padding: 1rem .75rem 0 0; }
  .timeline-across li.done { border-top-color: var(--ok); }
  .timeline-across li::before { left: 0; top: -.42rem; }
}

.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; }
/* A leave record's facts: label left, value right. Values are text (a type,
   a reason, a name), so they wrap rather than push the table sideways. */
.record-facts td + td { text-align: right; overflow-wrap: anywhere; }

/* ------------------------------------------------------------------- filter bars and tiles */
.filter-bar { flex-wrap: wrap; gap: .5rem; align-items: center; }
.filter-bar label { margin: 0; }
/* The form rule gives every input width:100%, which in a filter bar put each
   select, date and search box on a row of its own on a desktop. Here they
   size to their content. Search is left out of the first rule on purpose:
   its :not() chain out-specifies input[type=search] and would undo the 14rem. */
.filter-bar select,
.filter-bar input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=search]) { width: auto; max-width: 100%; }
.filter-bar input[type=search] { width: 14rem; max-width: 100%; }
/* A filter bar is a flat row of label, control, label, control — so a label
   and the control it names sit .5rem apart, and so do two unrelated controls.
   Everything reads as one undifferentiated line: on the lateness summary the
   "From" label landed hard against the "Last month" button, which is what it
   looked like it belonged to.

   Any label that FOLLOWS a control starts a new group, so this puts the air
   between groups without needing a wrapper element on every filter bar in the
   app — and a page added later inherits it. The first label in a bar has no
   preceding sibling to match, so it is left alone. */
.filter-bar > :not(label) + label { margin-left: 1.1rem; }
/* The preset buttons ("This month", "Last month") are one control between
   them, so they keep the tight gap and separate from the fields as a pair. */
.filter-bar > .btn + .btn { margin-left: -.15rem; }
/* A preset button is .btn-sm (32px) beside 39px fields, and the bar centres
   its items, so the button's top and bottom both missed the field edges by
   3px. Only the vertical padding grows: text size and widths are untouched. */
.filter-bar .btn-sm { padding-block: .5rem; }

/* 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;
  padding-block: .7rem;
}
.clock-break:hover { background: var(--warn); color: #fff; }
.clock-break .icon { width: 1.1rem; height: 1.1rem; }

/* ------------------------------------------------------------- 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(--info-soft); color: var(--info-ink); }
.role-lead .role-tag,
.role-tag.role-lead { background: #fce7f3; color: #9d174d; }
html[data-theme="dark"] .role-lead .role-tag,
html[data-theme="dark"] .role-tag.role-lead { background: #831843; color: #fbcfe8; }

/* Below the minimum on the floor — the number somebody has to fix today.
   Two selectors, because the class is written on four cells and only one of
   them sits in a .roster-count row: the rest-day tally over the allowance and
   Insights' absence and leave counts carried the class and no colour at all.
   The scoped form stays because `.roster-count td` sets its own background
   and outranks a bare class; the `td.` form is there so a row hover, which is
   also one class and one element, does not wash the flag off again. */
.roster-count .count-short,
td.count-short {
  background: var(--bad-soft);
  color: var(--bad-ink);
  font-weight: 700;
}
html[data-theme="dark"] .roster-count .count-short,
html[data-theme="dark"] td.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-count td.roster-name { background: var(--surface-2); }
.roster-month .roster-person { font-size: .74rem; }
.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. Its padding and alignment come
   from .roster th and .roster-group th, which out-specify a bare class. */
.roster-band-head { font-weight: 500; }
.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; margin-right: .3rem; }

/* A name is the one thing that must never be cut off: "Kai Leng ..." is not a
   person you can look up. */
.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; }
html[data-theme="dark"] .roster .is-trip { background: color-mix(in srgb, #f6c343 14%, transparent); }
html[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;
}

/* 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); 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; }
/* Leave entitlements, one column per position: equal widths, and a position
   name breaks only between words ("TREASURER" was split mid-word). Narrow
   screens scroll the table instead of squeezing it. */
table.dense.ent-matrix { table-layout: fixed; width: 100%; min-width: calc(10.5rem + var(--cols, 6) * 4.5rem); }
table.dense.ent-matrix th:first-child { width: 10.5rem; }
table.dense.ent-matrix input[type="number"] { width: 100%; padding-inline: .35rem; }  /* 3.6rem cut 12.0 to 12. */
/* 4.5rem columns fit ten positions in a 1280-wide card; the 4px comes out of
   each cell's side padding, so a box still holds '12.0' and a heading still
   breaks only between words (both 64px of text, was 63px). */
table.dense.ent-matrix th, table.dense.ent-matrix td { padding-inline: .25rem; }
table.dense.ent-matrix th { overflow-wrap: break-word; word-break: normal; hyphens: manual; vertical-align: bottom; }

/* The payroll run's line tables (run-lines). A company with teams puts
   fourteen columns there, and the dense rules above let a heading break
   anywhere to fit: CONFIRMED stacked a letter per line, each team list shrank
   to 43px with no room for the team's name, and Remove ran off the card.
   Here nothing breaks inside a word and the table is as wide as its content;
   on a screen narrower than that it scrolls inside its card, with the name
   column pinned so every row still says whose it is. */
table.dense.run-lines { width: max-content; min-width: 100%; font-size: .78rem; }
table.dense.run-lines th, table.dense.run-lines td { padding: .35rem .3rem; }
table.dense.run-lines th { white-space: nowrap; overflow-wrap: normal; }
table.dense.run-lines td { white-space: nowrap; vertical-align: middle; }
table.dense.run-lines select { width: auto; min-width: 4.5rem; padding: .2rem .4rem; font-size: .78rem; }
table.dense.run-lines .btn-ghost.btn-sm { padding: .1rem .3rem; }
/* The pinned name column. It sticks flush to its own scroll wrapper (the
   padded card let scrolled cells slide past in a strip left of the names),
   has a hairline where figures pass beneath it so a half-hidden amount never
   reads as a whole one, and a capped name width that the scroll padding keeps
   clear, so a Team list reached with Tab is never hidden underneath it. */
table.dense.run-lines tr > :first-child {
  position: sticky; left: 0; z-index: 1; background: var(--surface);
  box-shadow: 1px 0 0 var(--line);
}
table.dense.run-lines tr:hover > td:first-child { background: var(--surface-2); }
table.dense.run-lines .line-name {
  display: inline-block; max-width: 13rem; white-space: normal; vertical-align: middle;
}
.run-lines-wrap { scroll-padding-left: 16rem; }
@media (max-width: 599px) {
  /* A phone's scroll area is about 340px: pinned at full width the names left
     room for nothing else, as the roster found before. */
  table.dense.run-lines td:first-child .avatar { display: none; }
  table.dense.run-lines .line-name { max-width: 6.5rem; }
  .run-lines-wrap { scroll-padding-left: 8.5rem; }
}

/* 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; }

/* The add row at the foot of a ledger table — one box under each column it
   fills. Lived in a <style> block inside the incentive ledger's own page
   until now, which put a piece of app layout inside <main> and out of reach
   of the stylesheet's phone-width guard. Same rules, one home.
   The number boxes are right-aligned for the reason above: Out and In are
   .num columns, so a typed 1,700.00 has to end where the figures above it
   end rather than starting at the far left of a hard-right column. */
tr.addrow td { vertical-align: middle; }
tr.addrow input, tr.addrow select {
  padding: .32rem .45rem; font-size: .86rem; width: 100%; min-width: 5.5rem;
}
tr.addrow input[name="particular"], tr.addrow input[name="note"] { min-width: 12rem; }
tr.addrow input[type="number"] { text-align: right; }

/* Payee names: each "Is really" picker fills its cell so the column reads as
   one control, not a ragged row of boxes at different widths. It lived in a
   <style> block on that page, out of this stylesheet's reach the same way the
   add row above was. Scoped by a class on the cell rather than the selector
   it had there, td > select[name="employee"]: the incentive ledger's Who
   picker is also a select named employee in a cell, so that selector would
   have caught it too and left source order to decide whether it was 14rem
   wide or sized by the add row. */
.payee-pick > select { width: 100%; min-width: 14rem; padding: .3rem .4rem; }

/* ------------------------------------------------------------- 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; }
  /* 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 { font-size: .68rem; 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-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); }

.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .86em; color: var(--ink-2);
}

/* The hidden attribute has to win. The browser default for [hidden] is the
   weakest rule in the cascade, so any class that sets display (.btn, .tile)
   quietly overrode it and things the page had hidden stayed on screen. */
[hidden] { display: none !important; }
