*, *::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": 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;
}

/* ---------- Navy and gold (2026-09-24) ----------------------------------
   For a navy wordmark with a gold monogram. It exists because the black and
   gold palette above was the wrong home for it, and in a way that is worth
   recording: gold DROPS THE WHITE PLATE from behind a logo, because that
   palette was built for a mark carrying its own near-black ground. Hand it a
   logo whose lettering is navy ink on transparent and the wordmark lands on
   black and disappears - which is exactly what happened here.

   So this palette deliberately does NOT restate the plate rule. Every logo
   keeps its white rounded plate, which is what makes a dark-ink wordmark
   readable on a dark sidebar, and it means the palette does not care which of
   a client's logo files they upload.

   GOLD IS ACCENT ONLY, never text: #FEAC09 on white is 1.89:1, far under the
   4.5 floor. It appears on the lit menu item and the payslip letterhead,
   where it sits on the brand's own navy and carries no words. Text is navy,
   which is 17.32:1 on white and takes white back at the same ratio on a
   filled button.

   Measured, not eyeballed - the floor is 4.5 and the lowest here is 5.93
   (light --muted); dark's lowest is 6.20, the same variable. */
html[data-brand="navygold"] {
  --bg: #f4f5fb;
  --surface: #ffffff;
  --ink: #111233;
  --ink-2: #333663;
  --muted: #5d6090;
  --line: #dcdef0;
  --line-soft: #eceef8;

  --primary: #00006B;          /* the brand's own navy - 17.32 on white */
  --primary-strong: #00004d;
  --primary-soft: #e9e9f7;
  --link: #00006B;
  --primary-line: #b6b7e0;
  --surface-2: #fafbfe;
  --nav-bg: rgb(255 255 255 / .9);
  --grad: linear-gradient(135deg, #00006B 0%, #000045 100%);

  --info-soft: #e9e9f7; --info-ink: #00004d;

  --side-bg: #00006B;          /* the sidebar IS the brand's navy */
  --side-ink: #b5b7e2;
  --side-ink-hi: #f2f2fb;

  --shadow: 0 1px 2px rgb(17 18 51 / .05), 0 10px 30px -18px rgb(17 18 51 / .22);
  --shadow-lift: 0 2px 4px rgb(17 18 51 / .07), 0 16px 40px -20px rgb(0 0 107 / .38);
}

/* Same rule as green's: anything omitted here keeps its LIGHT value, so every
   variable that changes between modes has to be restated, --surface included
   or a white card would sit in a dark page. */
html[data-brand="navygold"][data-theme="dark"] {
  --bg: #05061a;
  --surface: #10123a;
  --ink: #e9eaf8;
  --ink-2: #c3c5e6;
  --muted: #9294c4;
  --line: #232766;
  --line-soft: #1a1d50;

  --primary: #3a3cb5;          /* deep: a .btn paints white on it, 8.42 */
  --primary-strong: #9a9cf0;
  --primary-soft: #16184a;
  --link: #a9abf5;             /* light: carries text, 8.40 on surface */
  --primary-line: #3c3f8f;
  --surface-2: #141744;
  --nav-bg: rgb(5 6 26 / .92);

  --info-soft: #16184a; --info-ink: #b9bbf8;

  /* Restated rather than inherited, even though the light values would pass
     on this deeper navy (10.20 and 17.76). The sidebar inks are the one pair
     a reader checks against --side-bg, and leaving them out of the block
     that changes --side-bg means checking two places to answer one
     question. */
  --side-bg: #03042f;
  --side-ink: #b5b7e2;
  --side-ink-hi: #f2f2fb;

  --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(58 60 181 / .5);
}

/* The lit menu item is a literal indigo gradient further down, and the gold
   accent belongs here rather than in any text colour. */
html[data-brand="navygold"] .side-link.active {
  background: linear-gradient(90deg, rgb(254 172 9 / .26), rgb(254 172 9 / .08));
}

/* The payslip letterhead is a hardcoded indigo gradient for every brand, and
   the payslip is the document staff keep. Restated for this brand only, so no
   other company's stationery moves. */
html[data-brand="navygold"] .payslip-head {
  background: linear-gradient(135deg, #00006B 0%, #12137f 100%);
  --payhead-ink: #ffc94d;
}

/* ---------- Charcoal and safety orange (2026-09-24) ----------------------
   Skylift-hire colours, the ones on the machines themselves:
   charcoal and safety orange. Charcoal carries the sidebar and the payslip
   letterhead; safety orange is the ACCENT - the lit menu item and the
   letterhead's ink, where it sits on charcoal and reads at 7.03:1.

   SAFETY ORANGE IS NEVER TEXT ON WHITE: #ff7a00 on white is 2.61:1, far
   under the 4.5 floor, and it is the colour a designer reaches for first.
   Text, links and buttons take a burnt orange (#b3440a, 5.60 on white, and
   white takes it back at the same ratio on a filled button).

   The logo keeps its white plate, as navy and gold does: no .side-logo or
   .paylogo rule here, so any logo file the company uploads stays readable
   on the charcoal sidebar.

   Measured, not eyeballed - the floor is 4.5 and the lowest text pair is
   5.60 (light --primary and --link); dark's lowest is 6.56 (--muted). */
html[data-brand="orange"] {
  --bg: #f5f5f3;
  --surface: #ffffff;
  --ink: #1b1e23;
  --ink-2: #3a3f47;
  --muted: #5c636d;
  --line: #e2e3e0;
  --line-soft: #eeefec;

  --primary: #b3440a;          /* burnt orange - 5.60 on white, both ways */
  --primary-strong: #8a3406;
  --primary-soft: #fdeee3;
  --link: #b3440a;
  --primary-line: #f0c4a4;
  --surface-2: #fbfaf8;
  --nav-bg: rgb(255 255 255 / .9);
  --grad: linear-gradient(135deg, #b3440a 0%, #8a3406 100%);

  --info-soft: #fdeee3; --info-ink: #8a3406;

  --side-bg: #1e2227;          /* the sidebar is the charcoal */
  --side-ink: #c3c8d0;
  --side-ink-hi: #f4f5f7;

  --shadow: 0 1px 2px rgb(27 30 35 / .05), 0 10px 30px -18px rgb(27 30 35 / .22);
  --shadow-lift: 0 2px 4px rgb(27 30 35 / .07), 0 16px 40px -20px rgb(27 30 35 / .38);
}

/* Every variable that changes between modes restated, --surface included,
   or a white card would sit in a dark page. */
html[data-brand="orange"][data-theme="dark"] {
  --bg: #0e1013;
  --surface: #191c21;
  --ink: #eceef1;
  --ink-2: #c6cad1;
  --muted: #9aa1ab;
  --line: #2c3037;
  --line-soft: #23272d;

  --primary: #b8490c;          /* a button fill: white on it 5.25. Not text on a card. */
  --primary-strong: #ffb27a;
  --primary-soft: #3a2415;
  --link: #ff9a52;             /* light: carries text, 8.13 on surface */
  --primary-line: #6b3a1c;
  --surface-2: #1f2328;
  --nav-bg: rgb(14 16 19 / .92);

  --info-soft: #3a2415; --info-ink: #ffc296;

  /* Restated for the same reason as navy and gold's: the sidebar inks are
     checked against --side-bg, so they live in the block that sets it. */
  --side-bg: #0b0d10;
  --side-ink: #c3c8d0;
  --side-ink-hi: #f4f5f7;

  --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(255 122 0 / .35);
}

/* The lit menu item is a literal indigo gradient further down; safety orange
   belongs here, on charcoal, where it carries no words of its own. */
html[data-brand="orange"] .side-link.active {
  background: linear-gradient(90deg, rgb(255 122 0 / .28), rgb(255 122 0 / .08));
}

/* The payslip letterhead: charcoal, with safety-orange ink (7.03 on the
   lighter end). Restated for this brand only, so no other company's
   stationery moves. */
html[data-brand="orange"] .payslip-head {
  background: linear-gradient(135deg, #23282e 0%, #1a1e23 100%);
  --payhead-ink: #ff9a3c;
}

html[data-brand="gold"] .side-link.active {
  background: linear-gradient(90deg, rgb(212 175 55 / .24), rgb(138 106 18 / .12));
}

/* ---------- Maroon (2026-09-26) ------------------------------------------
   For a maroon mark with a warm grey beside it (Halvora Packaging's logo:
   maroon #730a1b, grey #8a7d7d). A factory's HR system, not a sale: the
   maroon is deep and quiet, the grey warms every neutral, and nothing here
   is red - --bad stays the crimson it is everywhere, and the maroon sits far
   enough from it to read as the brand rather than as an error.

   Light: maroon carries text, links and filled buttons (#7a1024, 10.90 on
   white, and white on it at the same ratio). The sidebar is a wine-black,
   not the logo's maroon: a whole column of #730a1b is a wall, and the lit
   menu item then has nowhere to go. Dark: the fill lifts to a muted wine
   (#ad4a5a, white on it 5.38) and an old rose carries text (#e6adb7, 9.51
   on the surface) - the split blue and green make, because one maroon
   cannot both hold white text and read as text on a near-black card.

   The dark accents are MUTED on purpose. Maroon and the error crimson share
   a hue, and in light mode lightness keeps them apart (the maroon is 27%
   lightness, --bad 50%); in the dark both have to be light, so only
   saturation is left to tell a link from an error. A first cut with a
   vivid rose (#ec8fa0 links, #c2415a fills) sat within dE2000 7 of
   --bad-ink and --bad - the same colour to the eye, and the trap the green
   palette's note warns of: a brand colour that reads as an error. The old
   rose sits 15 from --bad-ink, the wine fill 10 from --bad; every other
   palette's dark link is 27+ from the error ink, so this is the closest
   the app comes, and tests/test_brand_themes.py holds the distances.

   Measured, not eyeballed - the floor is 4.5 and the lowest text pair is
   5.34 (light --muted on --bg); dark's lowest is 5.05 (--primary-strong on
   --primary-soft). The logo keeps its white plate, as navy and gold does:
   no .side-logo or .paylogo rule here. --info-line is restated too, which
   the older palettes leave alone: it draws the shift chip's border, and an
   indigo hairline round a rose chip reads as a mistake. */
html[data-brand="maroon"] {
  --bg: #f7f4f4;
  --surface: #ffffff;
  --ink: #1d1416;
  --ink-2: #4b3c3f;
  --muted: #6e6265;
  --line: #e9e0e1;
  --line-soft: #f3edee;

  --primary: #7a1024;          /* the logo's maroon, a shade lifted - 10.90 on white */
  --primary-strong: #5a0b1a;
  --primary-soft: #fbeef0;
  --link: #7a1024;
  --primary-line: #e6b9c1;
  --surface-2: #fcfafa;
  --nav-bg: rgb(255 255 255 / .9);
  --grad: linear-gradient(135deg, #7a1024 0%, #4a0812 100%);

  --info-soft: #fbeef0; --info-ink: #5a0b1a; --info-line: #f1cfd5;

  --side-bg: #1b0c0f;          /* wine-black: the maroon's own dark */
  --side-ink: #c8b4b8;
  --side-ink-hi: #f8f0f1;

  --shadow: 0 1px 2px rgb(29 20 22 / .05), 0 10px 30px -18px rgb(29 20 22 / .22);
  --shadow-lift: 0 2px 4px rgb(29 20 22 / .07), 0 16px 40px -20px rgb(122 16 36 / .38);
}

/* Every variable that changes between modes restated, --surface included,
   or a white card would sit in a dark page. */
html[data-brand="maroon"][data-theme="dark"] {
  --bg: #120a0c;
  --surface: #1e1316;
  --ink: #f4eaec;
  --ink-2: #d6c5c9;
  --muted: #a5959a;
  --line: #3b292d;
  --line-soft: #2d1f22;

  --primary: #ad4a5a;          /* a fill, a muted wine: white on it 5.38. Not text on a card. */
  --primary-strong: #c47787;   /* hover fill (white 3.31) and hovered link (5.46 on the surface) */
  --primary-soft: #331419;     /* deep enough that the fill still shows on it as a glyph, 3.11 */
  --link: #e6adb7;             /* old rose: carries text, 9.51 on the surface */
  --primary-line: #6f3540;
  --surface-2: #271a1d;
  --nav-bg: rgb(18 10 12 / .92);

  --info-soft: #331419; --info-ink: #f3b5bf; --info-line: #5a2a33;

  /* Restated for the same reason as navy and gold's: the sidebar inks are
     checked against --side-bg, so they live in the block that sets it. */
  --side-bg: #0e0608;
  --side-ink: #c8b4b8;
  --side-ink-hi: #f8f0f1;

  --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(173 74 90 / .4);
}

/* The lit menu item is a literal indigo gradient further down; restated in
   the brand's maroon so no indigo shows through on the wine-black. */
html[data-brand="maroon"] .side-link.active {
  background: linear-gradient(90deg, rgb(163 22 47 / .42), rgb(163 22 47 / .14));
}

/* The payslip letterhead, in the same family: deep maroon, with a rose ink
   that reads at 6.82 on the lighter end. Restated for this brand only, so
   no other company's stationery moves. */
html[data-brand="maroon"] .payslip-head {
  background: linear-gradient(135deg, #4a0812 0%, #7a1024 100%);
  --payhead-ink: #f2c0c8;
}

/* ---------- Teal (2026-09-26) --------------------------------------------
   For a blue-teal globe with a silver swoosh (Kinetix Global's logo). Not
   the navy-and-sky-blue palette above, which the demo's car wash already
   wears, and not the logo's own navy either: teal is what the globe reads
   as at a glance, and it is the one hue the other palettes leave alone.

   Light: a deep teal carries text, links and filled buttons (#0d6e67, 6.10
   on white, and white on it at the same ratio); the sidebar is a dark
   blue-green slate rather than black. Dark: the fill stays deep so a .btn's
   white label reads (#127d75, 4.98) and a bright teal carries text (#3fd2c0,
   9.02 on the surface) - blue's and green's split again.

   Measured, not eyeballed - the floor is 4.5 and the lowest text pair is
   4.90 (light --muted on --bg); dark's lowest is 5.11 (the letterhead's
   ink on its lighter end). The logo keeps its white plate. */
html[data-brand="teal"] {
  --bg: #f2f7f7;
  --surface: #ffffff;
  --ink: #0e1f22;
  --ink-2: #2f4548;
  --muted: #5b6f72;
  --line: #d8e5e6;
  --line-soft: #e8f1f1;

  --primary: #0d6e67;          /* deep teal - 6.10 on white, both ways */
  --primary-strong: #0a5550;
  --primary-soft: #e0f2f0;
  --link: #0d6e67;
  --primary-line: #9fd6cf;
  --surface-2: #f8fcfb;
  --nav-bg: rgb(255 255 255 / .9);
  --grad: linear-gradient(135deg, #0f766e 0%, #0b4a55 100%);

  --info-soft: #e0f2f0; --info-ink: #0a5550; --info-line: #b9e4df;

  --side-bg: #0c232a;          /* blue-green slate */
  --side-ink: #9fbcc0;
  --side-ink-hi: #eef7f7;

  --shadow: 0 1px 2px rgb(14 31 34 / .05), 0 10px 30px -18px rgb(14 31 34 / .22);
  --shadow-lift: 0 2px 4px rgb(14 31 34 / .07), 0 16px 40px -20px rgb(13 110 103 / .38);
}

/* Every variable that changes between modes restated, --surface included,
   or a white card would sit in a dark page. */
html[data-brand="teal"][data-theme="dark"] {
  --bg: #071417;
  --surface: #0e1f24;
  --ink: #e6f1f2;
  --ink-2: #bfd3d6;
  --muted: #88a2a6;
  --line: #1e363c;
  --line-soft: #172b30;

  --primary: #127d75;          /* a fill: white on it 4.98. Not text on a card. */
  --primary-strong: #2ba394;
  --primary-soft: #0a2628;
  --link: #3fd2c0;             /* bright: carries text, 9.02 on the surface */
  --primary-line: #1e6b66;
  --surface-2: #132a30;
  --nav-bg: rgb(7 20 23 / .92);

  --info-soft: #0a2628; --info-ink: #7fe3d6; --info-line: #1c504f;

  /* Success is a GREEN here, not the default's mint (#10b981 / #5eead4).
     Under a teal brand that mint sits dE2000 4-6 from the brand's own link
     and info ink: an "In" badge and an "overnight" badge were the same
     chip, and a link read as a status. Restated the way gold restates its
     set; the green ink is 16 from the link, white on the fill 3.49 (the
     default's is 2.54), and the ink 10.1 on its tint. Light mode keeps the
     default green, where the deep teal and the deep green already differ
     in hue. */
  --ok: #22a55a;      --ok-soft: #0b2b1a;   --ok-ink: #8ce6a0;   --ok-line: #1a5a33;

  /* Restated for the same reason as navy and gold's: the sidebar inks are
     checked against --side-bg, so they live in the block that sets it. */
  --side-bg: #061419;
  --side-ink: #9fbcc0;
  --side-ink-hi: #eef7f7;

  --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 184 166 / .35);
}

/* The lit menu item is a literal indigo gradient further down; restated in
   the brand's teal so no indigo shows through on the slate. */
html[data-brand="teal"] .side-link.active {
  background: linear-gradient(90deg, rgb(20 184 166 / .30), rgb(20 184 166 / .10));
}

/* The payslip letterhead: slate into teal, with a light teal ink that reads
   at 5.11 on the lighter end. Restated for this brand only, so no other
   company's stationery moves. */
html[data-brand="teal"] .payslip-head {
  background: linear-gradient(135deg, #0c232a 0%, #0f5e5a 100%);
  --payhead-ink: #8fe3d8;
}

/* The scrollbar's room is kept on every page (owner, 2026-10-16: "the whole
   site glitches when I press into other sections"). Without it a page taller
   than the window had a scrollbar and a shorter one did not, so on Windows
   every move between them shifted the whole layout sideways. */
html { -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; }
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; }
/* The desktop's tuck button (below, in the 900px block); a phone has the
   drawer instead. */
.side-tuck { display: none; }
/* Sidebar internals apply at EVERY width — the drawer is the same nav. */
.side-brand {
  display: flex; align-items: center; gap: .6rem; color: var(--side-ink-hi);
  font-weight: 700; padding: .2rem .55rem 1rem; letter-spacing: -.01em;
}
.side-brand .small { display: block; font-weight: 500; color: var(--side-ink); letter-spacing: 0; }
/* a logo is its own wordmark — give it the full column instead of a flex slot */
.side-brand.has-logo { display: block; }
.side-nav { flex: 1; overflow-y: auto; 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; }

  /* THE SIDEBAR TUCKS AWAY (owner, 2026-10-12, every company). A round
     button sits on the sidebar's edge; pressed, the sidebar becomes a slim
     strip in its own colour holding only that button, and the page takes
     the width. html[data-nav="tucked"] is set before first paint (base.html)
     from this browser's own choice. Not an icons-only rail: half the links
     share an icon, so the strip would not say where each goes. */
  .side-tuck {
    display: inline-flex; align-items: center; justify-content: center;
    position: fixed; top: 1.05rem; left: 234px; z-index: 31;
    width: 28px; height: 28px; padding: 0; border-radius: 50%;
    background: var(--surface); color: var(--ink-2);
    border: 1px solid var(--line); box-shadow: var(--shadow); cursor: pointer;
  }
  .side-tuck:hover { color: var(--ink); border-color: var(--muted); }
  .side-tuck:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
  .side-tuck .icon { width: 16px; height: 16px; vertical-align: 0; stroke-width: 2.2; }
  html[data-nav="tucked"] .sidebar { width: 44px; padding: 0; overflow: hidden; }
  html[data-nav="tucked"] .sidebar > * { display: none; }
  html[data-nav="tucked"] .sidebar ~ .shell { margin-left: 44px; }
  html[data-nav="tucked"] .side-tuck { left: 8px; }
  html[data-nav="tucked"] .side-tuck .icon { transform: rotate(180deg); }
}


/* ---------- 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; }

/* Claim categories side by side: the first half of the list on the left,
   the second on the right, one under the other on a narrow screen. One line
   per category: the name box takes what the buttons leave. */
.cat-columns { display: grid; gap: .4rem 2rem; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 31rem), 1fr)); }
.cat-table { width: 100%; }
.cat-table td { vertical-align: middle; }
.cat-table td:first-child { width: 100%; }
.cat-table .cat-name { display: flex; flex-wrap: nowrap; gap: .4rem; }
.cat-table .cat-name input { flex: 1 1 auto; width: 100%; min-width: 7rem; }
.cat-table .cat-actions { white-space: nowrap; }
.cat-table .cat-actions .inline-form { gap: .3rem; flex-wrap: nowrap; }
.cat-table .cat-desc { text-align: center; }
.cat-table thead th:last-child { text-align: center; min-width: 6.5rem; line-height: 1.25;
  overflow-wrap: normal; word-break: normal; }
@media (max-width: 599px) {
  .cat-table .cat-name, .cat-table .cat-actions .inline-form { flex-wrap: wrap; }
  .cat-table .cat-actions { white-space: normal; }
  .cat-table thead th:last-child { min-width: 4.5rem; }
}

.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; }
/* A row of labelled fields and the buttons they feed (2026-09-27): the bank
   file's crediting date beside Download bank file, the transferred date and
   reference beside Mark as transferred. Each label holds its text AND its
   field, so a row centring its items put a button halfway down the label
   and a link beside it higher still; lining up bottom edges was 4px out as
   well, because the form rule's .25rem label margin and the line an inline
   field sits on both hang below the field. Here the field is a block with
   nothing under it, the row lines up on bottom edges, and fields and buttons
   share one height (a date box draws 2px taller than a text box, a small
   button 7px shorter than both), so tops and bottoms meet. Below .btn-row
   and .inline-form on purpose: same specificity, so this wins where a row
   is both. */
.field-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: flex-end; }
.field-row label { margin: 0; }
.field-row label > :is(input:not([type=checkbox]):not([type=radio]), select) {
  display: block; margin-top: .25rem; }
.field-row :is(input:not([type=checkbox]):not([type=radio]):not([type=hidden]), select) {
  height: 2.5rem; }
/* A minimum, not a height: a long button (the bank portal's name) may wrap
   to two lines on a phone. */
.field-row .btn { min-height: 2.5rem; }
/* A tick in the row ("Download again") is shorter than the fields: given
   their height and centred in it, it sits level with the buttons instead of
   on the row's floor. */
.field-row > label:has(> input[type=checkbox]) {
  display: inline-flex; align-items: center; gap: .4rem; min-height: 2.5rem; }

/* ---------- 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-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-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); }
/* A temporary password (core.passwords.show_password_once): no click-to-close,
   so no ×; it has its own Copy and Close buttons. */
.msg-sticky { cursor: default; padding-right: 1rem; }
.msg-sticky::after { content: none; }
.msg .otp {
  display: inline-block; margin: .1rem 0; padding: .1rem .45rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 1.05rem;
  letter-spacing: .03em; background: var(--surface); color: var(--ink);
  border: 1px solid currentColor; border-radius: var(--radius-sm);
  user-select: all; -webkit-user-select: all; word-break: break-all;
}
.msg-actions { display: flex; gap: .4rem; margin-top: .55rem; }

/* ---------- installers (installers app) ---------- */
/* A payroll run's office and installers, each its own card (installers/
   _run_tables.html): the part's name over its table, and All's total after
   both - every column's figure in a band of its own, Net the largest. */
h2.run-part { font-size: 1rem; margin: 0 0 .6rem; }
.run-total { background: var(--primary-soft); border-color: var(--primary-line); }
.run-total-figures { display: grid; gap: .7rem 1.2rem;
                     grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); }
.run-total-figures > div { display: flex; flex-direction: column; gap: .1rem; }
.run-total .figure-label { color: var(--muted); font-size: .7rem; font-weight: 600;
                           letter-spacing: .04em; text-transform: uppercase; }
.run-total .figure { color: var(--ink); font-size: 1.15rem; font-weight: 700;
                     font-variant-numeric: tabular-nums; white-space: nowrap; }
.run-total .figure-main { color: var(--primary-strong); font-size: 1.35rem; }
/* The month grids (the tally, the installation value) are the run lines'
   table: their own sideways scroll inside the card, the name pinned, nothing
   breaking inside a word. Every day column is the same width, set from the
   widest figure the page shows (--day-ch, in digits), so a day with figures
   and one without line up; the weekday sits small under the date, and
   Sundays are shaded. */
table.dense.run-lines.day-grid th.day, table.dense.run-lines.day-grid td.day {
  width: calc(var(--day-ch, 3) * 1ch + .7rem); min-width: calc(var(--day-ch, 3) * 1ch + .7rem);
  padding-inline: .35rem; text-align: right; font-variant-numeric: tabular-nums; }
table.dense.run-lines.day-grid th.day { line-height: 1.15; vertical-align: bottom; }
table.dense.run-lines.day-grid th.day a { color: inherit; text-decoration: underline dotted; }
table.dense.run-lines.day-grid .dow { display: block; font-size: .58rem; font-weight: 500;
                                      letter-spacing: 0; }
table.dense.run-lines.day-grid .sunday { background: color-mix(in srgb, var(--muted) 8%, transparent); }
table.dense.run-lines.day-grid tr:hover > td.sunday { background: color-mix(in srgb, var(--muted) 14%, transparent); }
table.dense.run-lines.day-grid tr.total-row > th.day { font-size: .78rem; }
/* A name opens the person's own page; it reads as the name, not a link. */
table.dense.run-lines a.grid-name { color: inherit; text-decoration: none; }
table.dense.run-lines a.grid-name:hover { text-decoration: underline; }
/* The tally's days hold one mark each, centred. */
table.dense.run-lines.tally-grid th.day, table.dense.run-lines.tally-grid td.day {
  text-align: center; padding-inline: .15rem; }
/* Who had nothing in the period: one line under the grid, not empty rows. */
.grid-idle { margin: .7rem 0 0; }
/* The grids' totals are pinned at the right as the name is at the left, so a
   row's totals stay beside its days wherever the days are scrolled. Each
   pinned column has a set width (--pe-1 the last, --pe-2 and --pe-3 the ones
   before it, on the grid's wrapper) that places the next one along. A
   phone's scroll area has no room for them beside the name, so there they
   scroll with the days. */
@media (min-width: 600px) {
  table.dense.run-lines .pin-end { position: sticky; z-index: 1; background: var(--surface); }
  table.dense.run-lines .pe-1 { right: 0; width: var(--pe-1); min-width: var(--pe-1); }
  table.dense.run-lines .pe-2 { right: var(--pe-1); width: var(--pe-2); min-width: var(--pe-2); }
  table.dense.run-lines .pe-3 { right: calc(var(--pe-1) + var(--pe-2));
                                width: var(--pe-3); min-width: var(--pe-3); }
  table.dense.run-lines .pin-edge { box-shadow: -1px 0 0 var(--line); }
  table.dense.run-lines tr:hover > td.pin-end { background: var(--surface-2); }
  .run-lines-wrap.pins-end {
    scroll-padding-right: calc(var(--pe-1, 0px) + var(--pe-2, 0px) + var(--pe-3, 0px)); }
}
/* A list grouped by day (overtime, a person's jobs, a day's crews): the
   first row of each group carries the date or crew and a rule above it. */
tr.group-start > td { border-top: 1px solid var(--line); }
/* Overtime's rows are run lines too: the crew's names may wrap, and the
   hours and note boxes are the run lines' compact size, so a row with its
   buttons stays one line. */
table.dense.run-lines.ot-table td.crew { white-space: normal; min-width: 7rem; max-width: 8rem; }
table.dense.run-lines.ot-table .inline-form { gap: .35rem; }
table.dense.run-lines.ot-table input[type=number], table.dense.run-lines.ot-table input[type=text] {
  padding: .2rem .4rem; font-size: .78rem; }
/* A tally day: clocked and on the message, one of the two, or HR's word. */
.tally { display: inline-block; min-width: 1.4rem; padding: .05rem .2rem; border-radius: .3rem;
         border: 1px solid transparent; font: inherit; font-size: .8rem; line-height: 1.2;
         background: none; cursor: pointer; }
.tally.t-both { color: var(--ok-ink); }
.tally.t-clock, .tally.t-list { background: var(--warn-soft); color: var(--warn-ink); }
.tally.t-marked { background: var(--info-soft); color: var(--info-ink); }
.tally.t-off { text-decoration: line-through; opacity: .7; }
span.tally { cursor: default; }
/* The meal allowance week (installers/meals.html): the paper sheet's three
   boxes a day - AM, the sun, the moon - under the day's date, a rule where
   each day starts, and the boxes the run lines' compact size, wide enough
   for 500.00. */
table.dense.run-lines.meal-grid th.meal-day { text-align: center; line-height: 1.15;
                                              border-left: 1px solid var(--line); }
table.dense.run-lines.meal-grid .box { min-width: 4.6rem; padding-inline: .2rem;
                                       text-align: right; font-variant-numeric: tabular-nums; }
table.dense.run-lines.meal-grid th.box { text-align: center; }
table.dense.run-lines.meal-grid .box.d-first { border-left: 1px solid var(--line); }
table.dense.run-lines.meal-grid tr.total-row > th.box { font-size: .78rem; text-align: right; }
table.dense.run-lines.meal-grid input[type=number] { width: 4.4rem; padding: .2rem .3rem;
                                                     font-size: .78rem; text-align: right; }
table.dense.run-lines.meal-grid input[aria-invalid="true"] { border-color: var(--bad-ink);
                                                            background: var(--bad-soft); }

/* ---------- a reimbursement sheet (claims.sheets) ---------- */
.sheet-head { display: flex; justify-content: space-between; align-items: flex-start;
  gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; }
.sheet-head h1 { margin: 0 0 .25rem; font-size: 1.35rem; }
.sheet-total { text-align: right; }
.sheet-total .big { font-size: 1.6rem; font-weight: 700; color: var(--primary-strong); }
.sheet-part { display: flex; align-items: baseline; gap: .5rem; font-size: 1rem;
  margin: 1.1rem 0 .4rem; }
.sheet-part .sheet-sub { margin-left: auto; font-weight: 700; }
table.sheet-table th a { color: inherit; text-decoration: none; }
table.sheet-table th a.on { text-decoration: underline; }

/* ---------- 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; }
/* ---------- login sky ----------
   Dawnbreaker Digital's sky behind the sign-in card (owner, 2026-09-25), taken
   from the dawnbreaker site itself: its night (navy, the blue floor glow, the
   four-pointed sparkle tile, film grain, slow comets) in dark mode. Only the
   login page draws it (accounts/login.html, block backdrop); everything here
   is decoration, fixed behind the page and never in the way of a click. */
.auth-sky {
  position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
}
.auth-sky::before, .auth-sky::after { content: ""; position: absolute; inset: 0; }
/* The page sits above the sky: the card, and any message shown on it. */
.auth-sky ~ .shell { position: relative; z-index: 1; }
/* A comet: a thin streak with a glowing head, crossing on a slow loop. The
   site also slides them with scroll; a login page does not scroll. */
.auth-comet {
  --v: .5; --len: 150px; --op: .4;
  position: absolute; width: var(--len); height: 1px; border-radius: 1px;
  background: linear-gradient(to right, transparent 0%, rgb(59 130 246 / .35) 55%, rgb(147 197 253 / .95) 100%);
  rotate: 146deg; opacity: 0;
  animation: auth-comet calc(26s - var(--v) * 12s) linear infinite;
  animation-delay: calc(var(--v) * -19s);
}
.auth-comet::after {
  content: ""; position: absolute; right: -1px; top: 50%; width: 3px; height: 3px;
  margin-top: -1.5px; border-radius: 50%; background: #93c5fd;
  box-shadow: 0 0 8px 2px rgb(147 197 253 / .55);
}
@keyframes auth-comet {
  0%   { transform: translateX(-340px); opacity: 0; }
  18%  { opacity: var(--op); }
  82%  { opacity: var(--op); }
  100% { transform: translateX(560px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .auth-comet { animation: none; opacity: calc(var(--op) * .6); }
}

/* Night (dark mode). */
html[data-theme="dark"] .auth-sky {
  background:
    radial-gradient(ellipse 80% 50% at 50% 110%, rgb(30 58 138 / .28) 0%, rgb(59 130 246 / .10) 40%, transparent 65%),
    #080c12;
}
/* The site's starfield tile: white, gold and blue sparkles in 600px. */
html[data-theme="dark"] .auth-sky::after {
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600'%3E%3Cg fill='%23fff'%3E%3Cpath d='M90 116L91 119L94 120L91 121L90 124L89 121L86 120L89 119Z' opacity='.65'/%3E%3Cpath d='M485 72L486 74L488 75L486 76L485 78L484 76L482 75L484 74Z' opacity='.5'/%3E%3Cpath d='M165 460L166 464L170 465L166 466L165 470L164 466L160 465L164 464Z' opacity='.6'/%3E%3Cpath d='M520 487L521 489L523 490L521 491L520 493L519 491L517 490L519 489Z' opacity='.45'/%3E%3C/g%3E%3Cg fill='%23d4a574'%3E%3Cpath d='M240 196L241 199L244 200L241 201L240 204L239 201L236 200L239 199Z' opacity='.6'/%3E%3Cpath d='M420 377L421 379L423 380L421 381L420 383L419 381L417 380L419 379Z' opacity='.45'/%3E%3C/g%3E%3Cg fill='%2393c5fd'%3E%3Cpath d='M60 297L61 299L63 300L61 301L60 303L59 301L57 300L59 299Z' opacity='.5'/%3E%3Cpath d='M560 236L561 239L564 240L561 241L560 244L559 241L556 240L559 239Z' opacity='.6'/%3E%3C/g%3E%3Ccircle cx='300' cy='540' r='.7' fill='%23fff' opacity='.45'/%3E%3Ccircle cx='380' cy='100' r='.6' fill='%23d4a574' opacity='.5'/%3E%3Ccircle cx='130' cy='360' r='.5' fill='%2393c5fd' opacity='.4'/%3E%3C/svg%3E");
  background-size: 600px 600px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 5%, #000 95%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 5%, #000 95%, transparent 100%);
}
/* Film grain, as on the site: a still texture, blended in faintly. */
html[data-theme="dark"] .auth-sky::before {
  background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: .06; mix-blend-mode: overlay;
}
/* The card, a touch of glass over the night so the sky reads through its edge. */
html[data-theme="dark"] .auth-sky ~ .shell .auth-card {
  background: rgb(18 28 49 / .86); border-color: rgb(148 163 184 / .16);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  box-shadow: 0 24px 60px -28px rgb(0 0 0 / .8), 0 0 0 1px rgb(59 130 246 / .06);
}

/* Dawn (light mode): the moment the same site dawns (its DAWN section). The
   night drains out to the parchment of its cream panel (#f1ebde), the blue
   floor glow warms and swells into gold (#d4a574), the last stars hang on
   faintly near the top and the comets have gone out. Chosen over keeping the
   night in light mode, a full sunrise and a daylight star chart (panel of three
   judges, 2026-09-25). Still, on purpose: a page opened every morning, and
   after every wrong password, should not perform. */
html:not([data-theme="dark"]) .auth-sky {
  --dawn-navy-lg: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15'%3E%3Cpath transform='translate%287.5 7.5%29' d='M0-7L1.61-1.61L7 0L1.61 1.61L0 7L-1.61 1.61L-7 0L-1.61-1.61Z' fill='%231e2a44' opacity='0.38'/%3E%3C/svg%3E");
  --dawn-navy-md: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12'%3E%3Cpath transform='translate%286 6%29' d='M0-5.5L1.27-1.27L5.5 0L1.27 1.27L0 5.5L-1.27 1.27L-5.5 0L-1.27-1.27Z' fill='%231e2a44' opacity='0.5'/%3E%3C/svg%3E");
  --dawn-navy-sm: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3E%3Cpath transform='translate%285 5%29' d='M0-4.5L1.04-1.04L4.5 0L1.04 1.04L0 4.5L-1.04 1.04L-4.5 0L-1.04-1.04Z' fill='%231e2a44' opacity='0.46'/%3E%3C/svg%3E");
  --dawn-bronze-lg: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15'%3E%3Cpath transform='translate%287.5 7.5%29' d='M0-7L1.61-1.61L7 0L1.61 1.61L0 7L-1.61 1.61L-7 0L-1.61-1.61Z' fill='%23a8743e' opacity='0.5'/%3E%3C/svg%3E");
  --dawn-bronze-sm: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3E%3Cpath transform='translate%285 5%29' d='M0-4.5L1.04-1.04L4.5 0L1.04 1.04L0 4.5L-1.04 1.04L-4.5 0L-1.04-1.04Z' fill='%23a8743e' opacity='0.6'/%3E%3C/svg%3E");
  background: linear-gradient(to bottom, #dde5f1 0%, #edebe5 32%, #f1ebde 58%, #f1ebde 100%);
}
/* The glow: a wide gold swell that only fades outward (so it never draws a
   ring), and a warm-white core on the horizon; on a light ground a glow
   without a bright core reads as a stain, not light. */
html:not([data-theme="dark"]) .auth-sky::before {
  background:
    radial-gradient(ellipse 46% 24% at 50% 102%,
      rgb(255 249 238 / .95) 0%, rgb(255 240 214 / .55) 50%, transparent 100%),
    radial-gradient(ellipse 120% 70% at 50% 114%,
      rgb(236 186 124 / .55) 0%, rgb(222 174 122 / .36) 28%,
      rgb(212 165 116 / .08) 54%, transparent 76%);
}
/* The last stars: the site's four-pointed sparkles in navy and bronze, placed
   rather than tiled, so nothing repeats, and gone before the glow.
   They can never sit on the card's edge. The card is centred, so a star
   placed by a percentage of the height crosses its top edge at SOME height
   (390x844 did). So each star is either in a top band fixed in pixels, above
   the highest the card ever starts (33.6px: the container's and the wrap's
   padding), or beside the card, where it cannot reach at that width: under
   600px the 32px gutter, 600-899px outside 12%-88%, from 900px outside
   24%-76% (the card is 416px wide and centred from 480px up). Proved with 8px
   to spare at 3,848 measured sizes, 320-2560 x 480-1200, English and Chinese,
   with and without the wrong-password line. Phone layout first; the tablet
   and wide ones below keep the same twelve stars in the same order. */
html:not([data-theme="dark"]) .auth-sky::after {
  background-image:
    var(--dawn-navy-sm),
    var(--dawn-navy-sm),
    var(--dawn-navy-md),
    var(--dawn-navy-sm),
    var(--dawn-navy-lg),
    var(--dawn-navy-sm),
    var(--dawn-navy-sm),
    var(--dawn-bronze-sm),
    var(--dawn-bronze-lg),
    var(--dawn-bronze-sm),
    var(--dawn-bronze-sm),
    var(--dawn-bronze-sm);
  background-position: 24% 10px, 44% 13px, 76% 6px, left 6px top 21%, 12% 3px,
    left 9px top 42%, right 7px top 33%, left 12px top 7%, 58% 4px, right 10px top 4%,
    left 14px top 30%, right 12px top 19%;
  background-repeat: no-repeat;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 30%, transparent 72%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 30%, transparent 72%);
}
@media (min-width: 600px) {
  html:not([data-theme="dark"]) .auth-sky::after {
    background-position: 30% 12px, 47% 6px, 70% 10px, 3% 21%, 8% 12%, 10% 42%, 93% 33%,
      5% 3.5%, 58% 4px, 91% 4%, 6% 30%, 95% 19%;
  }
}
@media (min-width: 900px) {
  html:not([data-theme="dark"]) .auth-sky::after {
    background-position: 24% 4.5%, 43% 14px, 77% 7%, 3% 21%, 19% 15%, 13% 42%, 80% 33%,
      5% 3.5%, 58% 8px, 91% 4%, 9% 31%, 84% 19%;
  }
}
html:not([data-theme="dark"]) .auth-comet { display: none; }
/* The card stays solid white; its edge is a navy hairline and a soft navy
   shadow (a warm-brown one left a tan smudge on the cream). */
html:not([data-theme="dark"]) .auth-sky ~ .shell .auth-card {
  border-color: rgb(11 18 32 / .11);
  box-shadow: 0 1px 2px rgb(11 18 32 / .05), 0 20px 44px -24px rgb(11 18 32 / .18);
}
/* Behind the sky too, so a phone's overscroll bounce shows sky, not a strip
   of the app's own background. */
html:has(> body > .auth-sky) { background: #f1ebde; }
html[data-theme="dark"]:has(> body > .auth-sky) { background: #080c12; }

/* ---------- error pages ----------
   The 404 and the 500 (owner, 2026-09-25): the login card's shape holding a
   short message and one way out. templates/404.html draws the sky behind it
   when nobody is signed in and sits in the person's own shell when somebody
   is; templates/500.html stands alone and always draws the sky, because a
   500 renders with no context and cannot know whose system it is. Every
   rule here is scoped to .auth-msg, which no other page uses. */
.auth-msg { text-align: center; }
/* balance: a centred sentence of two lines otherwise left one word on the
   second ("moved.", "anda."); browsers without it wrap as before. */
.auth-msg h1, .auth-msg h2, .auth-msg p { text-wrap: balance; }
.auth-msg h1 { margin: 0 0 .4rem; }
.auth-msg p { margin: 0; color: var(--ink-2); }
.auth-msg .btn { margin-top: 1.5rem; min-width: 9rem; }
/* The 500 says it once per language, English first, because a 500 has no
   request language to trust. Each repeat sits under a hairline, a size down,
   so the card reads as one message said three times rather than three. */
.auth-msg-also { margin-top: 1.05rem; padding-top: 1.05rem; border-top: 1px solid var(--line); }
.auth-msg-also h2 { margin: 0 0 .3rem; font-size: 1.1rem; font-weight: 700; letter-spacing: -.01em; }
.auth-msg-also p { font-size: .92rem; }
/* The 500's one button carries the word in every language. .btn lays its
   content out as a flex row, which on a 360px phone made each word its own
   item and wrapped INSIDE it (首 / 页, Laman / utama, dots on lines of their
   own). A block of centred text instead: it fits one line at 360px, and when
   it cannot it breaks only after a dot (the template's no-break spaces), with
   every word kept whole. */
.auth-msg-home { display: inline-block; white-space: normal; padding-left: .9rem; padding-right: .9rem; }
.auth-msg-home span { white-space: nowrap; }

/* ---------- 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; }
/* The people who have left, after everybody still here (EA forms). */
tr.divider-row > th { background: var(--surface-2); color: var(--muted); padding-top: .6rem; }
.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, .auth-logo {
  display: block; border-radius: 7px; background: #fff;
  padding: .4rem .55rem; object-fit: contain;
}
/* The sign-in card's logo: as tall as the sidebar's, centred over the form. */
.auth-logo { max-height: 4.5rem; max-width: 100%; margin: 0 auto; }
/* 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) ---------- */
/* ---------- Borang EA (C.P.8A), templates/payroll/ea_form.html ----------
   Drawn to read like the LHDN sheet: boxed parts, item numbers on the left,
   amounts right-aligned in one RM column. Every rule is under .ea-doc (or
   the toolbar beside it), so nothing here reaches any other page. Plain
   percentage columns and wrapping text, so it fits a phone without a grid
   floor (test_css_fits_a_phone). */
.ea-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: space-between; margin-bottom: .6rem; }
.ea-doc { background: #fff; color: #0f172a; border: 1px solid var(--line); padding: 1rem; margin-bottom: 1rem;
  font-size: .82rem; line-height: 1.35; overflow-wrap: anywhere; }
.ea-doc .ea-en { color: #475569; font-style: italic; }
/* A sheet of paper in either theme: the app's row hover (a dark surface in
   the dark theme) left dark text on a dark row. */
.ea-doc tr:hover > td { background: transparent; }
.ea-head { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem 1rem; border-bottom: 2px solid #0f172a; padding-bottom: .5rem; }
.ea-ref { font-size: .72rem; flex-basis: 100%; }
.ea-country { font-weight: 600; }
.ea-mark { font-size: 2rem; font-weight: 800; letter-spacing: .05em; }
.ea-kind { font-weight: 600; flex: 1 1 12rem; }
.ea-ids, .ea-table, .ea-sign { width: 100%; border-collapse: collapse; margin: .5rem 0; }
.ea-ids td, .ea-table td, .ea-table th, .ea-sign td { border: 1px solid #cbd5e1; padding: .3rem .4rem; vertical-align: top; }
.ea-doc .ea-val { font-weight: 600; }
.ea-year { font-weight: 700; text-align: center; margin: .6rem 0 .3rem; }
.ea-banner { text-align: center; font-weight: 600; font-size: .75rem; border: 1px solid #0f172a; padding: .3rem; margin: .3rem 0 .6rem; }
.ea-part { margin-top: .6rem; break-inside: avoid; page-break-inside: avoid; }
.ea-part h2 { font-size: .85rem; margin: 0 0 .2rem; background: #f1f5f9; padding: .25rem .4rem; }
.ea-note { font-size: .72rem; margin: .2rem 0; }
.ea-table .ea-no { width: 2rem; white-space: nowrap; }
.ea-table .ea-amt { width: 22%; text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.ea-sub { padding-left: 1rem; font-size: .76rem; }
.ea-total td { font-weight: 700; }
.ea-sign { margin-top: .8rem; break-inside: avoid; page-break-inside: avoid; }
.ea-sign td { width: 33%; }
.ea-warning { border-width: 2px; }

@media print {
  /* A reimbursement sheet (claims.sheets): no card chrome on paper. */
  .sheet { box-shadow: none; border: 0; padding: 0; }
  table.sheet-table { font-size: 9pt; }
  /* 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, .side-tuck, .topbar, .nav-backdrop, .messages, .no-print, .btn, form { display: none !important; }
  .shell { margin: 0 !important; }
  /* Nothing prints but the page itself: the browser's own header and footer
     (date and time, page title, web address, page number) are drawn in the
     page margins, so every page prints with none (@page below) and the
     content carries the margin instead - cloned onto each printed page, so a
     long report keeps its top and bottom margins on page two and after. */
  .container { max-width: none; padding: 12mm 14mm;
    -webkit-box-decoration-break: clone; box-decoration-break: clone; }
  .container:has(.ea-doc) { padding: 10mm; }
  .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; }
  /* Borang EA on ONE A4 sheet, as LHDN's own is (owner, 2026-10-07: it
     printed on four). The named page keeps the size and margins to the EA
     alone - and the Incomplete notice above the form is on it too, or the
     change of page name puts the form on a fresh sheet. Each Malay label
     runs on into its English one on the same line instead of the next, and
     type and spacing come down to the official sheet's. The screen keeps
     its roomier layout. */
  .ea-doc { page: ea; border: none; padding: 0; margin: 0; font-size: 7pt; line-height: 1.12; }
  .ea-doc table, .ea-doc th { font-size: inherit; }
  .ea-doc .ea-table .ea-amt { width: 14%; }
  .ea-doc br:has(+ .ea-en) { display: none; }
  .ea-doc br + .ea-en::before { content: " / "; }
  .ea-doc .ea-table td, .ea-doc .ea-table th, .ea-doc .ea-ids td, .ea-doc .ea-sign td { padding: 1pt 3pt; }
  .ea-doc .ea-head { padding-bottom: 2pt; gap: 1pt 8pt; }
  .ea-doc .ea-ref, .ea-doc .ea-note, .ea-doc .ea-sub, .ea-doc .ea-banner { font-size: 6.5pt; }
  .ea-doc .ea-mark { font-size: 16pt; }
  .ea-doc .ea-ids, .ea-doc .ea-table, .ea-doc .ea-sign { margin: 2pt 0; }
  .ea-doc .ea-year { margin: 3pt 0 1pt; }
  .ea-doc .ea-banner { margin: 1pt 0 2pt; padding: 1pt; }
  .ea-doc .ea-part { margin-top: 3pt; }
  .ea-doc .ea-part h2 { font-size: 7pt; padding: 1pt 3pt; margin: 0; }
  .ea-doc .ea-note { margin: 1pt 0; }
  .ea-doc .ea-sub { padding-left: 8pt; }
  .ea-doc .ea-sign { margin-top: 4pt; }
  .ea-warning { page: ea; border: 1px solid #000; padding: 3pt; margin: 0 0 4pt; font-size: 7pt;
    box-shadow: none; border-radius: 0; }
  /* Payslips and the EA print on A4 with no page margin, as every page does
     (see .container above, which carries the margin). */
  .payslip-doc { page: payslip; padding: 0; }
}
@page { margin: 0; }
@page ea { size: A4; margin: 0; }
@page payslip { size: A4; margin: 0; }

/* ---------- 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-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; }

/* Sites and outlets (sites app, 2026-09-24), drawn only where a company has
   sites switched on: the site's two- or three-letter tag under a shift's
   character, the same tag in the legend, and a day worked at ANOTHER site
   when the roster is read for one site - greyed and dashed, so it reads as
   neither a shift here nor a rest day. */
.sh-site {
  display: block; margin-top: .05rem;
  font-size: .56rem; line-height: 1.1; font-weight: 700; letter-spacing: .02em;
  color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sh-site-key {
  display: inline-block; min-width: 1.6rem; margin-top: 0; padding: 0 .2rem;
  text-align: center; border: 1px solid var(--line); border-radius: 4px;
}
.sh-elsewhere {
  background: transparent; color: var(--muted); border: 1px dashed var(--line);
  font-size: .62rem; font-weight: 600;
}

/* 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 .12rem; height: auto; min-height: 3.4rem; }
  .cal-date { font-size: .72rem; margin-bottom: .18rem; gap: .15rem; }
  /* A day is ~40px here. Uppercase MORNING ran 10px past its chip into the
     next day, and the site name was cut mid-word: mixed case, the chip's
     padding trimmed, and an ellipsis if a shift's name is longer still. */
  .cal-shift { padding: .12rem .14rem; margin-bottom: .14rem; border-radius: 5px; overflow: hidden; }
  .cal-shift-name { font-size: .5rem; letter-spacing: 0; text-transform: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* 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: .5rem; 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; }
/* The presets wrapped as the one control they are (same .35rem as the pull
   above gave them), so a phone can give them a row of their own. */
.filter-presets { display: inline-flex; gap: .35rem; }

/* On a phone the flat row of label, field, label, field wrapped wherever it
   ran out of room, so a label landed at the END of the line above the field
   it names ("From [date] To" / "[date] Project"), and one long select (a
   person's full name) pushed the whole bar past its card. Here each label
   and its field share a row, the labels one column; presets get a row. */
@media (max-width: 599px) {
  .inline-form.filter-bar { display: flex; }
  .filter-bar > label:has(+ :is(select, input:not([type=checkbox]):not([type=radio]):not([type=hidden]))) {
    flex: 0 0 4.75rem; margin-left: 0; }
  .filter-bar > label:has(+ :is(select, input:not([type=checkbox]):not([type=radio]):not([type=hidden])))
    + :is(select, input) { flex: 1 1 calc(100% - 5.25rem); min-width: 0; }
  .filter-bar > .filter-presets { flex-basis: 100%; }
}

/* A label and the field it names as one unit (2026-09-25): a filter bar
   wrapped wherever the next item did not fit, which could be between a label
   and its field - "Agent" left at the end of a line, its list starting the
   next - at any width. Wrapped in the templates, so the pair moves together.
   The air between groups is the 1.1rem a bare label after a control had. */
.filter-field { display: inline-flex; align-items: center; gap: .5rem; min-width: 0;
  max-width: 100%; }
.filter-field > label { margin: 0; white-space: nowrap; }
.filter-field > select, .filter-field > input { min-width: 0; }
.filter-bar > :not(label):not([type="hidden"]) + .filter-field { margin-left: .6rem; }
@media (max-width: 599px) {
  /* On a phone each pair is a row, the labels one column. */
  .filter-bar > .filter-field { flex: 1 1 100%; margin-left: 0; }
  .filter-field > label { flex: 0 0 4.75rem; white-space: normal; }
  .filter-field > select, .filter-field > input { flex: 1 1 auto; width: auto; }
}

/* 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; }

/* Somebody who does not clock in (Company.roster_non_clocking): an asterisk
   after the name, explained in the key under the grid - the tag it replaced
   ran past the name column (owner, 2026-10-16). */
.roster-star { margin-left: .15rem; color: var(--muted); font-weight: 700; }
.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; }
/* A month-grid day is 16-21px wide and "Leave" is 25px: there (and in the
   legend under it) the leave chip is the sun, the Leave menu's own icon.
   The week grid has the room and keeps the word. */
.sh-leave .icon { display: none; }
.roster-month .sh-leave .icon,
.roster-legend[data-view="month"] .sh-leave .icon { display: inline-block; width: .8rem; height: .8rem; vertical-align: -.15em; }
.roster-month .sh-leave .leave-word,
.roster-legend[data-view="month"] .sh-leave .leave-word { display: none; }
.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: half a name 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); }

/* ---------- company calendar ----------
   The month grid's cells list labels; the whole square is the link to the
   date. Labels wrap rather than ellipsis - an invoice number cut in half is
   not one you can look up. */
.cal-total { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
.cal-total-figure { font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.cal-entry {
  font-size: .76rem; line-height: 1.3; padding: .15rem .3rem; margin-bottom: .2rem;
  border-radius: 6px; background: var(--line-soft); overflow-wrap: anywhere;
}
.cal-entry-label { font-weight: 600; }
/* Day and night jobs (2026-10-02): amber and indigo, a tint the
   text reads on in either theme and a bar down the side. */
.cal-entry.cal-kind-day { background: rgba(245, 158, 11, .20); box-shadow: inset 3px 0 0 #f59e0b; }
.cal-entry.cal-kind-night { background: rgba(99, 102, 241, .24); box-shadow: inset 3px 0 0 #6366f1; }
.cal-entry-card.cal-kind-day { border-left: 4px solid #f59e0b; }
.cal-entry-card.cal-kind-night { border-left: 4px solid #6366f1; }
.badge.cal-kind-badge.cal-kind-day { background: rgba(245, 158, 11, .22); color: inherit; }
.badge.cal-kind-badge.cal-kind-night { background: rgba(99, 102, 241, .26); color: inherit; }
.cal-entry-by { display: block; font-size: .9em; color: var(--muted); }
.cal-who-hidden .cal-who { display: none; }
.cal-entry-amount, .cal-entry-files { display: inline-block; margin-left: .25rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.cal-entry-note { white-space: normal; overflow-wrap: anywhere; }
.cal-file { display: flex; flex-direction: column; gap: .3rem; align-items: flex-start; max-width: 16rem; }
.cal-file-link { overflow-wrap: anywhere; }
.cal-edit { margin-top: .6rem; }
.cal-edit summary { cursor: pointer; font-weight: 600; font-size: .9rem; }
/* A payroll run's Change history, closed until clicked. */
.run-history > summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: .4rem; }
.run-history > summary::-webkit-details-marker { display: none; }
.run-history > summary::after { content: "\25B8"; color: var(--muted); font-size: .85rem; }
.run-history[open] > summary::after { content: "\25BE"; }
.run-history > summary h2 { margin: 0; }
.run-history[open] > summary { margin-bottom: .7rem; }
.cal-edit form { margin-top: .6rem; }
.cal-delete { margin-top: .4rem; }
@media (max-width: 599px) {
  .cal-entry { font-size: .55rem; padding: .08rem .15rem; }
  .cal-entry-amount, .cal-entry-files { margin-left: .1rem; }
}

/* 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; }
/* A plain payroll month's totals (Company.plain_run_page, and every
   installers' table): not the grey capitals of a heading but the figures in
   ink, bold, on a tinted band under a heavy rule. */
table.dense.run-lines.strong-totals tr.total-row > th {
  color: var(--ink); background: var(--primary-soft); font-size: .9rem; font-weight: 700;
  letter-spacing: 0; text-transform: none; border-top: 2px solid var(--primary-line);
  border-bottom: none; padding-top: .6rem; padding-bottom: .6rem;
}
@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 payroll cost chart (payroll_costs.html). Its text is in the SVG's own
   units, which shrink and grow with the chart: at phone width it is drawn at
   under half its size, so its labels are set twice as large there, and the
   chart leaves room for them (payroll.costs.chart). Mobile-first. */
.cost-chart text { font-size: 24px; fill: var(--muted); }
.cost-chart text.is-current { fill: var(--ink); font-weight: 600; }
@media (min-width: 560px) { .cost-chart text { font-size: 13px; } }
@media (min-width: 900px) { .cost-chart text { font-size: 11px; } }

/* 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; }

.pay-head { align-items: center; margin-bottom: .8rem; }
.pay-head h2 { margin: 0; flex: 1; font-size: 1.15rem; }
table.pay-months th.group { text-align: center; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; font-size: .62rem; }
table.pay-months .start { border-left: 1px solid var(--line); }
table.pay-months th { overflow-wrap: normal; word-break: normal; hyphens: none; min-width: 4.5rem; }
table.pay-months .strong { font-weight: 700; }
table.pay-months td.zero { color: var(--muted); }
table.pay-months .month { position: sticky; left: 0; z-index: 1; background: var(--surface); white-space: nowrap; text-align: left; }
table.pay-months .month .sub-line { display: block; }

/* ======================================================= WHO CAN DO WHAT
   Three views of one set of rules, switched by radio buttons placed before
   the panels: by person, by position, by access level. */
.am-radio { position: absolute; opacity: 0; pointer-events: none; }
.am-tabs .mode-tab { cursor: pointer; }
.am-panel { display: none; }
#am-by-person:checked ~ .am-panel-person,
#am-by-position:checked ~ .am-panel-position,
#am-by-level:checked ~ .am-panel-level { display: block; }
#am-by-person:checked ~ .am-tabs label[for="am-by-person"],
#am-by-position:checked ~ .am-tabs label[for="am-by-position"],
#am-by-level:checked ~ .am-tabs label[for="am-by-level"] {
  background: var(--surface); color: var(--primary-strong); box-shadow: var(--shadow-lift);
}
#am-by-person:focus-visible ~ .am-tabs label[for="am-by-person"],
#am-by-position:focus-visible ~ .am-tabs label[for="am-by-position"],
#am-by-level:focus-visible ~ .am-tabs label[for="am-by-level"] {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
.am-pick { margin-bottom: .6rem; }
.am-pick select { min-width: 16rem; }
.am-hint { margin: 0; }
.am-who h2 { margin: .4rem 0 .6rem; font-size: 1.1rem; }
.am-facts { display: flex; flex-wrap: wrap; gap: .5rem 2rem; margin: 0 0 .7rem; }
.am-facts dt { font-size: .75rem; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.am-facts dd { margin: .1rem 0 0; font-weight: 600; }
.am-extras { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; margin-bottom: .8rem; }
.am-extras > .muted { margin-right: .3rem; }
.am-chip {
  display: inline-block; padding: .12rem .55rem; border-radius: 999px; line-height: 1.5;
  background: var(--info-soft); color: var(--info-ink); font-size: .8rem; margin: .1rem .25rem .1rem 0;
}
.am-sec {
  margin: 1rem 0 .35rem; font-size: .75rem; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em;
}
.am-opens h2 + .am-sec { margin-top: .2rem; }
.am-list { columns: 3 13rem; column-gap: 1.6rem; list-style: none; padding: 0; margin: 0; }
.am-list li { break-inside: avoid; padding: .2rem 0 .2rem .9rem; position: relative; font-size: .9rem; }
.am-list li::before { content: "✓"; position: absolute; left: 0; color: var(--ok); font-size: .8rem; }
.am-more { margin-top: 1rem; border-top: 1px solid var(--line); padding-top: .7rem; }
.am-more summary { cursor: pointer; font-weight: 600; font-size: .9rem; }
.am-more[open] summary { margin-bottom: .5rem; }
.am-more .am-list li::before { color: var(--muted); }
.am-lead { margin-top: 0; }
table.am-positions td { vertical-align: top; }
table.am-positions .am-people { font-size: .88rem; }
table.am-positions .am-people summary { cursor: pointer; color: var(--primary-strong); }
.am-legend { display: flex; flex-wrap: wrap; gap: .5rem 1.4rem; align-items: center; }
.am-yes { color: var(--ok); font-weight: 700; font-size: 1.05rem; }
.am-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
table.am-matrix th.mid, table.am-matrix td.mid { text-align: center; width: 1%; white-space: nowrap; }
table.am-matrix td:first-child { min-width: 11rem; }
@media (max-width: 640px) {
  .am-pick { display: flex; }
  .am-pick select { min-width: 0; width: 100%; }
  .am-tabs { display: flex; width: 100%; }
  .am-tabs .mode-tab { flex: 1; text-align: center; padding-inline: .4rem; }
}
