/* DPI Accounting — application theme. Light is the default (:root);
   [data-theme="dark"] overrides. Same token names either way, so templates
   never care which theme is active. Theme is stamped on <html> before first
   paint by the inline script in base.html.

   Layout modes: below 1100px the navigation is a slide-in drawer (phones and
   small laptops); at 1100px and up it is a fixed left sidebar. The 1100px
   line is deliberate — a rail plus the content column needs the room, and
   below it the drawer keeps the full width for wide report tables. */

/* ------------------------------------------------------------------ *
 * Tokens
 * ------------------------------------------------------------------ */
:root {
  color-scheme: light;

  /* Surfaces & ink */
  --bg: #f2f4f8;
  --card: #ffffff;
  --card-2: #e9edf4;
  --field: #ffffff;
  --hover: #eef1f7;
  --ink: #1a2233;
  --mut: #556072;
  --line: #cdd5e3;          /* hairlines: row rules, dividers */
  --line-strong: #b8c2d6;   /* structure: card edges, input borders */

  /* Accent & status. Values are contrast-checked against --card and --bg. */
  --acc: #2a58d6;
  --acc-hover: #1f47b8;
  --acc-soft: #e5ebfa;
  --acc-ink: #ffffff;
  --ring: rgba(42, 88, 214, .28);
  --good: #157a48;
  --good-bg: #e3efe9;
  --bad: #c02334;
  --bad-ink: #ffffff;
  --bad-bg: #f7e5e7;
  --warn: #8a5c00;
  --warn-bg: #fdf0d5;

  /* Elevation — tinted, never pure black. */
  --shadow-1: 0 1px 2px rgba(16, 24, 40, .06);
  --shadow-2: 0 8px 24px -6px rgba(16, 24, 40, .16), 0 2px 6px rgba(16, 24, 40, .08);

  /* Geometry */
  --r-1: 6px;
  --r-2: 8px;
  --r-3: 10px;
  --radius: var(--r-3);
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --control-h: 36px;
  --row-pad-y: 6px;
  --row-pad-x: 10px;
  --wrap: 1180px;
  --sb-w: 248px;
  /* Height of the desktop top bar, and of the brand block in the rail beside
     it — one token so the two hairlines meet whatever the rail is doing. */
  --tb-h: 48px;

  /* Type. System stacks only — nothing is downloaded, so the app still
     renders correctly offline and behind a tunnel. Every family listed
     carries fixed-width lining figures, which is what keeps money columns
     aligned without a webfont. */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text",
             "Segoe UI", system-ui, Roboto, "Noto Sans", Cantarell,
             "DejaVu Sans", Arial, sans-serif;
  --font-num: var(--font-ui);
  --fs-h1: 20px;
  --fs-kpi: 21px;
  --fs-h2: 15px;
  --fs-body: 14px;
  --fs-table: 14px;
  --fs-sm: 13px;
  --fs-micro: 11.5px;
  --lh-tight: 1.3;
  --lh-body: 1.5;
  --lh-table: 1.4;
  --fw-medium: 500;
  --fw-semi: 600;
  --ls-tight: -.012em;
  --ls-micro: .05em;

  /* Height of the phone tab bar, without its safe-area padding. Anything
     pinned to the bottom of the viewport has to clear it. */
  --tabbar-h: 56px;

  /* Stacking order, in one place. */
  --z-rail: 20;
  --z-tabbar: 30;
  /* Toasts sit above the tab bar they float over, but under the scrim: with
     the drawer open the page behind it is meant to look inert. */
  --z-toast: 33;
  --z-overlay: 35;
  --z-drawer: 40;
  --z-skip: 100;
}
[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0f1420;
  --card: #182031;
  --card-2: #1d2740;
  --field: #0f1420;
  --hover: #212c45;
  --ink: #e8edf6;
  --mut: #93a1b8;
  --line: #2e3950;
  --line-strong: #3c4864;
  --acc: #4f8cff;
  --acc-hover: #6ba0ff;
  --acc-soft: #213152;
  --acc-ink: #0b1526;
  --ring: rgba(79, 140, 255, .32);
  --good: #38c172;
  --good-bg: #1c373a;
  --bad: #f2707d;
  --bad-ink: #0b1526;
  --bad-bg: #372b3c;
  --warn: #f6b93b;
  --warn-bg: #373532;
  --shadow-1: 0 1px 2px rgba(0, 0, 0, .45);
  --shadow-2: 0 10px 28px -8px rgba(0, 0, 0, .65), 0 2px 8px rgba(0, 0, 0, .5);
}

/* ------------------------------------------------------------------ *
 * Base & type
 * ------------------------------------------------------------------ */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-variant-numeric: lining-nums;
  -webkit-text-size-adjust: 100%;
}
a { color: var(--acc); text-decoration: none; }
a:hover { text-decoration: underline; }
h1 {
  font-size: var(--fs-h1); line-height: var(--lh-tight);
  font-weight: var(--fw-semi); letter-spacing: var(--ls-tight);
  margin: 0 0 2px; text-wrap: balance;
}
h2 {
  font-size: var(--fs-h2); line-height: var(--lh-tight);
  font-weight: var(--fw-semi); margin: 0 0 var(--s3);
}
h3 { font-size: var(--fs-body); font-weight: var(--fw-semi); margin: 0 0 var(--s2); }

/* One uppercase micro-label recipe, used by table headers, KPI labels,
   nav group headings and the stacked-table cell labels. */
th,
.kpi .label,
.nav-group-title,
table.line-editor td[data-label]::before,
table[data-stack] td[data-label]::before {
  font-size: var(--fs-micro);
  font-weight: var(--fw-semi);
  line-height: 1.35;
  text-transform: uppercase;
  letter-spacing: var(--ls-micro);
  color: var(--mut);
}

/* Tabular figures everywhere a number appears — including inputs, whose
   `font: inherit` would otherwise reset font-variant-numeric. */
td.num, th.num, .num, .kpi .value, .editor-status,
input.num, input[type="number"], input[type="date"],
input[inputmode="decimal"], input[inputmode="numeric"] {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

/* Inline SVG icons inherit colour from their link/button. */
.ico {
  width: 18px; height: 18px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Not display:none — that stops <use> resolving in older WebKit. */
.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ------------------------------------------------------------------ *
 * Shell — skip link, navigation, content column
 * ------------------------------------------------------------------ */
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: var(--z-skip);
}
.skip-link:focus {
  left: var(--s2); top: var(--s2); padding: var(--s2) var(--s3);
  background: var(--card); color: var(--ink);
  border: 1px solid var(--acc); border-radius: var(--r-2);
  box-shadow: var(--shadow-2);
}

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 18px var(--s4) 60px; }
main:focus { outline: none; }

header.top {
  position: sticky; top: 0; z-index: var(--z-rail);
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
nav.main {
  max-width: var(--wrap); margin: 0 auto; padding: 10px var(--s4);
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
}
nav.main .brand {
  display: flex; align-items: center; gap: var(--s2);
  font-weight: var(--fw-semi); color: var(--ink);
  min-width: 0; margin-right: var(--s3);
}
nav.main .brand:hover { text-decoration: none; }
.brand-mark { width: 22px; height: 22px; flex: 0 0 22px; border-radius: var(--r-1); }
.brand-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.nav-list { list-style: none; margin: 0; padding: 0; }
nav.main a.navlink, .navlink {
  display: flex; align-items: center; gap: var(--s2);
  color: var(--mut); padding: 6px var(--s2); border-radius: var(--r-2);
  font-weight: var(--fw-medium);
}
nav.main a.navlink:hover, .navlink:hover {
  color: var(--ink); background: var(--hover); text-decoration: none;
}
/* Style off the ARIA state, so the visual cue and the announced one cannot
   drift apart. The inset bar carries the meaning colour alone would not. */
nav.main a.navlink[aria-current="page"] {
  color: var(--ink); background: var(--acc-soft);
  font-weight: var(--fw-semi); box-shadow: inset 3px 0 0 var(--acc);
}
nav.main a.navlink[aria-current="page"] .ico { color: var(--acc); }
.navlink .ico { opacity: .9; }
.navlink .nav-label { min-width: 0; }
/* Urdu sits under its English label so narrow rails never truncate it. */
.navlink .ur { display: block; line-height: 1.1; font-size: .9em; }
/* The logout / theme controls are buttons dressed as nav rows. */
.navlink.as-btn {
  width: 100%; background: transparent; border: 0; cursor: pointer;
  font: inherit; font-weight: var(--fw-medium); text-align: left;
}
.nav-group-title { margin: var(--s2) var(--s2) 2px; }

/* --- The account menu ----------------------------------------------- *
 * Identity is not navigation, so it does not live in the rail: the chip
 * sits at the right of the top bar, and the three things you can do about
 * your own account hang off it. Native <details>, so a browser with no
 * JavaScript still opens it.
 * -------------------------------------------------------------------- */
.user-bar { display: flex; margin-left: auto; }
.user-bar-inner { display: flex; align-items: center; gap: var(--s1); }
/* The theme toggle is the other non-navigation control, and the only one
   worth a click of its own rather than a place in the menu. Icon only: it
   carries an aria-label already, and a "Dark theme" caption beside a name
   makes the bar read as two menus. */
.user-bar [data-theme-toggle] { width: auto; padding: 7px; }
.user-bar [data-theme-toggle] .nav-label { display: none; }
.user-menu { position: relative; }
.user-chip {
  display: flex; align-items: center; gap: var(--s2); min-width: 0;
  padding: var(--s1) var(--s2); border-radius: var(--r-2);
  color: var(--mut); font-weight: var(--fw-medium); cursor: pointer;
  /* Both spellings: WebKit still ships the pseudo-element marker. */
  list-style: none;
}
.user-chip::-webkit-details-marker { display: none; }
.user-chip:hover, .user-menu[open] > .user-chip { background: var(--hover); }
.user-chip:focus-visible {
  outline: 2px solid var(--acc); outline-offset: -2px;
}
.avatar {
  width: 28px; height: 28px; flex: 0 0 28px;
  display: flex; align-items: center; justify-content: center;
  background: var(--acc-soft); color: var(--acc);
  border-radius: 50%; font-weight: var(--fw-semi);
  font-size: var(--fs-sm); text-transform: uppercase;
}
.who-name {
  color: var(--ink); font-weight: var(--fw-medium);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A display name can be sixty characters. In the bar it shares a row with the
   company name, so it gives way rather than pushing the brand out. */
.user-chip > .who-name { max-width: 14rem; }
/* Not capitalised: half of this line is a login name, and a login name is
   whatever it was typed as. */
.who-role { font-size: var(--fs-micro); color: var(--mut); }
.ico-caret { width: 14px; height: 14px; transition: transform .15s ease; }
.user-menu[open] .ico-caret { transform: rotate(180deg); }

.user-pop {
  position: absolute; top: calc(100% + var(--s1)); right: 0;
  z-index: var(--z-drawer);
  min-width: 232px; padding: var(--s1);
  background: var(--card); border: 1px solid var(--line-strong);
  border-radius: var(--r-3); box-shadow: var(--shadow-2);
}
/* The chip shows one name. This carries the two facts it cannot fit: which
   login it is, and what that login is allowed to do. */
.user-pop-head {
  display: flex; flex-direction: column;
  padding: var(--s2); margin-bottom: var(--s1);
  border-bottom: 1px solid var(--line);
}
.user-pop .navlink { padding: var(--s2); }
.user-pop form { margin: 0; }
/* On a phone the header row is already carrying the company name and the
   hamburger; the avatar alone says whose session this is. Hidden the same
   way the collapsed rail hides its labels rather than with display:none —
   the name is the button's accessible name, and dropping it would leave a
   control announced as nothing but its caret. */
@media (max-width: 719px) {
  .user-chip { gap: var(--s1); padding-inline: var(--s1); }
  .user-chip > .who-name {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; white-space: nowrap; clip-path: inset(50%);
  }
}

/* --- Search in the rail head --------------------------------------- */
.nav-search { position: relative; display: flex; align-items: center; }
.nav-search input {
  padding-left: 32px; min-height: 34px;
  background: var(--field); border-radius: var(--r-2);
}
.nav-search-ico {
  position: absolute; left: 9px; color: var(--mut); pointer-events: none;
  width: 16px; height: 16px;
}
/* One of the two is always hidden: the field where there is room for it,
   the icon link where there is not. Selector matches the weight of the
   generic `nav.main a.navlink { display: flex }` rule above, or the link
   would win and both would show. */
nav.main a.nav-search-link { display: none; }

/* Desktop-only chrome, off by default. */
nav.main .nav-toggle, nav.main .nav-collapse { display: none; }
/* Below the sidebar breakpoint the drawer wrapper dissolves into the flex
   row. (nav.main prefix outranks the later .btn display rule.) */
.nav-links { display: contents; }
.nav-scroll { display: contents; }
.nav-overlay { display: none; }

/* Theme toggle shows the theme it switches TO, swapped in CSS so no script
   has to rewrite the button's contents. */
:root:not([data-theme="dark"]) .ico-sun,
:root:not([data-theme="dark"]) .th-light { display: none; }
[data-theme="dark"] .ico-moon,
[data-theme="dark"] .th-dark { display: none; }
/* Collapse button mirrors the rail's current state. */
html:not([data-nav-collapsed]) .ico-narrow { display: none; }
html[data-nav-collapsed] .ico-wide { display: none; }

/* --- Sidebar (>=1100px) ------------------------------------------- */
@media (min-width: 1100px) {
  /* The rail down the left, the account bar across the top of what is left. */
  body.has-shell {
    padding-left: var(--sb-w); padding-top: var(--tb-h);
    transition: padding-left .18s ease;
  }

  header.top {
    position: fixed; top: 0; left: 0; bottom: 0; width: var(--sb-w);
    display: flex; align-items: stretch;
    background: var(--card); border-right: 1px solid var(--line);
    border-bottom: 0; transition: width .18s ease;
  }
  nav.main {
    flex: 1 1 auto; min-width: 0; min-height: 0;
    max-width: none; margin: 0; padding: 0 0 var(--s2);
    display: flex; flex-direction: column; flex-wrap: nowrap;
    align-items: stretch; gap: 0;
  }
  /* Height rather than padding, so the rule under the brand lands on exactly
     the same line as the top bar's, collapsed or expanded. */
  nav.main .brand {
    height: calc(var(--tb-h) - 1px);
    margin: 0 var(--s3) var(--s3);
    /* right padding clears the collapse button parked in the corner */
    padding: 0 30px 0 0;
    border-bottom: 1px solid var(--line); font-size: var(--fs-h2);
  }
  nav.main .nav-collapse {
    display: flex; align-items: center; justify-content: center;
    position: absolute; top: calc((var(--tb-h) - 31px) / 2); right: var(--s2);
    width: 30px; height: 30px; padding: 0;
    background: transparent; border: 0; border-radius: var(--r-1);
    color: var(--mut); cursor: pointer;
  }
  nav.main .nav-collapse:hover { background: var(--hover); color: var(--ink); }

  .nav-links {
    display: flex; flex-direction: column; flex: 1 1 auto;
    min-height: 0; padding: 0 var(--s2);
  }
  /* Only the link list scrolls; identity and log out stay reachable on a
     768px-tall laptop where the full rail does not fit. */
  .nav-scroll {
    display: block; flex: 1 1 auto; min-height: 0;
    overflow-y: auto; overscroll-behavior: contain;
    padding-bottom: var(--s2);
    /* Room inside the scrollport for the focus ring, which would otherwise
       be clipped flat against both rail edges. */
    margin-inline: calc(var(--s1) * -1); padding-inline: var(--s1);
  }
  .nav-group-title { display: block; }
  .nav-scroll > .nav-group-title:first-child { margin-top: 0; }

  /* The top bar. Fixed rather than in the rail's column: its parent is the
     sidebar, and this has to span the content beside it. `left` tracks the
     rail token, so collapsing the rail widens the bar with it. */
  .user-bar {
    position: fixed; top: 0; right: 0; left: var(--sb-w);
    height: var(--tb-h); z-index: var(--z-rail);
    background: var(--bg); border-bottom: 1px solid var(--line);
    transition: left .18s ease;
  }
  .user-bar-inner {
    width: 100%; max-width: var(--wrap); height: 100%;
    margin-inline: auto; padding-inline: var(--s4);
    justify-content: flex-end; gap: var(--s2);
  }

  /* Collapsed rail: labels are hidden from view but stay in the
     accessibility tree, so every icon still has a name. */
  html[data-nav-collapsed] { --sb-w: 68px; }
  html[data-nav-collapsed] .nav-links .nav-label,
  html[data-nav-collapsed] .brand-text {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; white-space: nowrap; clip-path: inset(50%);
  }
  /* Not .navlink.as-btn wholesale: the theme toggle in the top bar is not in
     the rail and keeps its label. */
  html[data-nav-collapsed] nav.main a.navlink,
  html[data-nav-collapsed] .nav-links .navlink.as-btn,
  html[data-nav-collapsed] .brand { justify-content: center; gap: 0; }
  html[data-nav-collapsed] nav.main .brand {
    margin-inline: var(--s2); padding: 0;
  }
  html[data-nav-collapsed] .nav-collapse { position: static; margin: 0 auto var(--s2); }
  html[data-nav-collapsed] .nav-group-title {
    height: 1px; margin: var(--s3) var(--s2); padding: 0;
    overflow: hidden; color: transparent; background: var(--line);
  }

  .nav-search { margin: 0 var(--s2) var(--s2); }
  /* 68px of rail has no room for a field, but search still has to be one
     click away. */
  html[data-nav-collapsed] .nav-search { display: none; }
  html[data-nav-collapsed] nav.main a.nav-search-link { display: flex; }
}
/* Mid band: a narrower rail so the content column keeps its width. */
@media (min-width: 1100px) and (max-width: 1279px) {
  :root { --sb-w: 216px; }
}

/* --- Drawer (<=1099px) -------------------------------------------- */
@media (max-width: 1099.98px) {
  /* header.top is a stacking context (sticky + z-index), so the drawer
     nested inside it can only paint above the scrim if the header does.
     Without this the overlay covers the drawer and every nav tap lands on
     the scrim's close handler instead of the link. */
  header.top { z-index: var(--z-drawer); }
  nav.main { flex-wrap: nowrap; }
  nav.main .nav-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    padding: 6px;
  }
  .nav-links {
    display: flex; flex-direction: column; gap: 2px;
    position: fixed; top: 0; bottom: 0; left: 0; z-index: var(--z-drawer);
    width: min(78vw, 300px);
    background: var(--card); border-right: 1px solid var(--line);
    box-shadow: var(--shadow-2);
    padding: 14px var(--s3) calc(14px + env(safe-area-inset-bottom));
    overflow-y: auto;
    /* visibility (not just transform) takes the closed drawer out of the
       tab order — an offscreen transform stays focusable. It flips to
       visible instantly on open, and back only after the slide-out ends,
       so the first link is focusable the moment the drawer opens. */
    visibility: hidden;
    transform: translateX(-105%);
    transition: transform .2s ease, visibility 0s linear .2s;
  }
  body[data-nav-open] .nav-links {
    transform: none; visibility: visible;
    transition: transform .2s ease, visibility 0s linear 0s;
  }
  .nav-links a.navlink, .navlink.as-btn {
    display: flex; padding: 10px var(--s2); font-size: var(--fs-body);
  }
  /* The drawer has the width for the real field, so the icon link stays off
     — restated here because the rule above would otherwise re-show it. */
  nav.main a.nav-search-link { display: none; }
  .nav-search { margin-bottom: var(--s2); }
  .nav-group-title { display: block; }
  /* No bar of its own down here — the header already is one, and the chip
     takes its right-hand end. `margin-left: auto` comes from the base rule;
     the drawer beside it is fixed, so it takes no room in this row. */
  .user-bar { flex: 0 0 auto; }
  body[data-nav-open] .nav-overlay {
    display: block; position: fixed; inset: 0; z-index: var(--z-overlay);
    background: rgba(10, 14, 24, .45);
  }
}
@media (prefers-reduced-motion: reduce) {
  .nav-links, header.top, body.has-shell, .btn, .card, a.card, .navlink,
  .user-bar, .ico-caret {
    transition: none !important; animation: none !important;
  }
}

/* --- Bottom tab bar (phones only) ---------------------------------- *
 * Four destinations plus the drawer, within thumb reach. It is not a
 * second navigation: the sections are the sidebar's, the active item is
 * decided by the same rule, and "More" opens the same drawer.
 * ------------------------------------------------------------------ */
.tabbar { display: none; }
@media (max-width: 719px) {
  /* The bar is fixed, so the last row of every list would sit under it. */
  body.has-shell {
    padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  }
  .tabbar {
    display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-tabbar);
    background: var(--card); border-top: 1px solid var(--line-strong);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .tabbar-link {
    position: relative;
    display: flex; flex-direction: column; align-items: center;
    justify-content: center; gap: 2px;
    min-height: var(--tabbar-h); padding: 6px 2px;
    border: 0; background: none; font: inherit; cursor: pointer;
    font-size: var(--fs-micro); font-weight: var(--fw-medium);
    line-height: 1.2; text-align: center; color: var(--mut);
  }
  .tabbar-link:hover { text-decoration: none; }
  .tabbar-link .ico { width: 22px; height: 22px; }
  /* Any aria-current: "page" on the four links, "true" on More, which stands
     in for a section that is not one of them. */
  .tabbar-link[aria-current] { color: var(--acc); }
  /* Colour alone would not survive a bright factory floor or a colour-blind
     reader; the rule above the icon is the redundant cue. */
  .tabbar-link[aria-current]::before {
    content: ""; position: absolute; top: 0; left: 20%; right: 20%;
    height: 2px; background: var(--acc); border-radius: 0 0 2px 2px;
  }
  .tabbar-link:focus-visible { outline-offset: -3px; }
  .tabbar-text { display: block; }
  .tabbar .ur { display: none; }
}

.page-head { display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap; margin: var(--s3) 0 var(--s4); }
.sub { color: var(--mut); font-size: var(--fs-sm); }
.actions { display: flex; gap: var(--s2); flex-wrap: wrap; }

/* Visible only to screen readers — used where an icon carries the meaning
   and the word behind it still has to be announced. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; white-space: nowrap; clip-path: inset(50%);
}

/* --- Breadcrumbs --------------------------------------------------- */
.crumbs { margin: var(--s3) 0 calc(var(--s2) * -1); font-size: var(--fs-sm); }
.crumbs ol { display: flex; flex-wrap: wrap; align-items: center; gap: 2px var(--s1);
  list-style: none; margin: 0; padding: 0; }
.crumbs li { display: flex; align-items: center; gap: var(--s1); color: var(--mut); }
.crumbs li + li::before { content: "›"; color: var(--line-strong); }
.crumbs a { color: var(--mut); }
.crumbs a:hover { color: var(--acc); }
.crumb-here { color: var(--ink); font-weight: var(--fw-medium); }
/* The trail follows the page head straight into the title. */
.crumbs + .page-head { margin-top: var(--s2); }

/* --- Filter summary: result count + removable chips ---------------- */
.result-bar {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--s2) var(--s3); margin: calc(var(--s2) * -1) 0 var(--s3);
  color: var(--mut); font-size: var(--fs-sm);
}
.result-count strong { color: var(--ink); font-weight: var(--fw-semi);
  font-variant-numeric: tabular-nums lining-nums; }
.chips { display: flex; flex-wrap: wrap; gap: var(--s1) 6px; align-items: center; }
.chip {
  display: inline-flex; align-items: center; gap: var(--s1);
  min-height: 26px; padding: 2px var(--s2);
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: 99px;
  font-size: var(--fs-sm); line-height: 1.3;
}
.chip:hover { text-decoration: none; border-color: var(--acc); background: var(--hover); }
.chip-key { color: var(--mut); }
.chip-key::after { content: ":"; }
.chip-x { color: var(--mut); font-size: 15px; line-height: 1; }
.chip:hover .chip-x { color: var(--bad); }
.chip-clear { border-style: dashed; color: var(--mut); }
@media (pointer: coarse) { .chip { min-height: 34px; padding-inline: var(--s3); } }

/* --- Record pages ---------------------------------------------------- *
 * Substance in the main column, fixed facts on a rail. Below 900px it is one
 * column with the rail first — on a phone the party and the total are what
 * the page was opened for, and the line table can wait.
 * -------------------------------------------------------------------- */
.record {
  display: grid; gap: var(--s3); align-items: start;
  grid-template-columns: minmax(0, 1fr) 300px;
}
.record > * { min-width: 0; }
@media (max-width: 899.98px) {
  .record { grid-template-columns: 1fr; }
  .record-rail { order: -1; }
}
/* Sticky to the viewport, so it has to clear the fixed top bar rather than
   slide under it. Below 1100px there is no bar and no stick. */
.record-rail .card { position: sticky; top: calc(var(--tb-h) + var(--s3)); }
@media (max-width: 1099.98px) { .record-rail .card { position: static; } }

.record-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s3) var(--s4); flex-wrap: wrap; margin: var(--s3) 0 var(--s4);
}
.rh-text { min-width: 0; flex: 1 1 260px; }
.rh-status { margin-bottom: var(--s1); }
.record-head h1 { font-size: clamp(20px, 3vw, 26px); }
.rh-amount {
  font-size: clamp(22px, 4vw, 30px); font-weight: 700;
  letter-spacing: var(--ls-tight); line-height: 1.15; white-space: nowrap;
}
/* A reversed document is not a live one. Tint the whole header, so the state
   cannot be missed the way a small pill halfway down the page can. */
.record-head.is-reversed {
  background: var(--bad-bg); border: 1px solid var(--bad);
  border-radius: var(--radius); padding: var(--s3);
}
.record-head.is-reversed .rh-amount { color: var(--bad); text-decoration: line-through; }

.facts { margin: 0; }
.fact { padding: var(--s2) 0; border-bottom: 1px solid var(--line); }
.fact:last-child { border-bottom: 0; padding-bottom: 0; }
.fact:first-child { padding-top: 0; }
.fact dt {
  font-size: var(--fs-micro); font-weight: var(--fw-semi);
  text-transform: uppercase; letter-spacing: var(--ls-micro); color: var(--mut);
}
.fact dd { margin: 2px 0 0; overflow-wrap: anywhere; }

/* --- Section tabs ---------------------------------------------------- *
 * Tabs move you between views of one subject; the buttons top-right do
 * something. Mixing the two in one row made a destination and a posting
 * action look identical.
 * -------------------------------------------------------------------- */
.tabs {
  display: flex; gap: var(--s1); flex-wrap: wrap;
  margin: calc(var(--s2) * -1) 0 var(--s4);
  border-bottom: 1px solid var(--line);
}
.tab {
  padding: var(--s2) var(--s3); color: var(--mut);
  font-weight: var(--fw-medium); border-bottom: 2px solid transparent;
  margin-bottom: -1px; white-space: nowrap;
}
.tab:hover { color: var(--ink); text-decoration: none; background: var(--hover); }
.tab[aria-current] {
  color: var(--acc); border-bottom-color: var(--acc); font-weight: var(--fw-semi);
}
@media (pointer: coarse) { .tab { min-height: 44px; display: flex; align-items: center; } }

/* --- Search results -------------------------------------------------- */
.hits { list-style: none; margin: 0; padding: 0; }
.hits li + li { border-top: 1px solid var(--line); }
.hit {
  display: grid; align-items: baseline; gap: 0 var(--s3);
  grid-template-columns: minmax(0, 1fr) auto;
  padding: var(--s2) var(--s1); color: var(--ink);
}
.hit:hover { text-decoration: none; background: var(--hover); }
.hit-label { font-weight: var(--fw-medium); min-width: 0; }
.hit-sub {
  grid-column: 1; color: var(--mut); font-size: var(--fs-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hit-meta {
  grid-column: 2; grid-row: 1 / span 2;
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 2px; color: var(--mut); font-size: var(--fs-sm); text-align: right;
}
.hit-amount { color: var(--ink); font-weight: var(--fw-semi); }
.mb-0 { margin-bottom: 0; }

/* --- Empty states -------------------------------------------------- */
.empty-state { display: flex; flex-direction: column; align-items: center;
  gap: var(--s2); padding: var(--s5) var(--s3); text-align: center; }
.empty-title { margin: 0; color: var(--ink); font-weight: var(--fw-semi); }
.empty-hint { margin: 0; color: var(--mut); font-size: var(--fs-sm); max-width: 46ch; }

/* ------------------------------------------------------------------ *
 * Cards & grids
 * ------------------------------------------------------------------ */
.card {
  background: var(--card); border: 1px solid var(--line-strong);
  border-radius: var(--radius); padding: var(--s3) var(--s4);
  margin-bottom: var(--s3); box-shadow: var(--shadow-1);
}
.grid { display: grid; gap: var(--s3); grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
a.card { display: block; color: inherit; transition: border-color .15s ease, box-shadow .15s ease; }
a.card:hover { text-decoration: none; border-color: var(--acc); box-shadow: var(--shadow-2); }
@media (max-width: 420px) {
  .grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
}
/* Six KPI cards: explicit column counts that divide evenly, so the strip
   never ends in a lone orphan card the way auto-fit does. */
.kpi-row { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 720px) { .kpi-row { grid-template-columns: repeat(3, 1fr); } }
/* 1600px, not 1400: the rail takes 248px off the content column, and six
   tracks below ~210px wrap the larger amounts onto a second line. */
@media (min-width: 1600px) { .kpi-row { grid-template-columns: repeat(6, 1fr); } }
/* Figures over a list, sized for four or fewer cards — narrower tracks than
   .grid so a two-card strip does not stretch each card across half a page. */
.stat-strip { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-bottom: var(--s3); }
.stat-strip .card { margin-bottom: 0; padding: var(--s2) var(--s3) var(--s3); }
.kpi .label { display: block; }
.kpi .value {
  font-size: clamp(17px, 2.2vw, var(--fs-kpi)); font-weight: 700;
  letter-spacing: var(--ls-tight); margin-top: var(--s1);
}

/* ------------------------------------------------------------------ *
 * Tables
 * ------------------------------------------------------------------ */
.table-scroll { overflow-x: auto; position: relative; }
table { border-collapse: collapse; width: 100%; font-size: var(--fs-table); }
th, td {
  padding: var(--row-pad-y) var(--row-pad-x);
  border-bottom: 1px solid var(--line);
  line-height: var(--lh-table); text-align: left;
}
th { background: var(--card-2); border-bottom: 1px solid var(--line-strong); }
td.num, th.num { text-align: right; white-space: nowrap; }
input.num { text-align: right; }
tr.tot td { font-weight: var(--fw-semi); border-top: 2px solid var(--line-strong); }
tr:hover td { background: var(--hover); }
.empty { color: var(--mut); padding: var(--s5) 0; text-align: center; }
.empty-state .btn { text-decoration: none; }
/* Whole-row navigation. The identifying cell keeps a real anchor — this only
   extends its hit area, so keyboard and screen-reader users are unaffected
   and a 40px "view →" link at the bottom of a phone card stops being the
   only way in. */
tr.row-link { cursor: pointer; }
tr.row-link:hover td { background: var(--hover); }
tr.row-link:focus-within td { background: var(--hover); }

/* A figure that was posted and then reversed. It stays on the record — the
   books are append-only and hiding it would be a second kind of lie — but a
   struck number is read as "not counted" at a glance. The row's Reversed
   pill is what carries the same fact to a screen reader.

   Qualified with the element on purpose: a bare .void would tie with .pos
   and .neg, and a colorised cell would keep its green while claiming not to
   count. */
td.void { text-decoration: line-through; color: var(--mut); }

/* Sortable column headers. The whole label is the link, so the target is the
   size of the heading rather than a 12px arrow. */
th > a.sort {
  display: inline-flex; align-items: center; gap: 4px;
  color: inherit; text-decoration: none;
}
th > a.sort:hover { color: var(--acc); text-decoration: underline; }
th > a.sort .sort-mark { font-size: .7em; color: var(--mut); }
/* The inactive columns keep their arrow in the layout but not in the ink:
   removing it would shift every header sideways on each click. */
th[aria-sort="none"] > a.sort .sort-mark { visibility: hidden; }
th > a.sort:hover .sort-mark, th > a.sort:focus-visible .sort-mark {
  visibility: visible; color: var(--acc);
}
th[aria-sort]:not([aria-sort="none"]) > a.sort { color: var(--acc); }

/* ------------------------------------------------------------------ *
 * Buttons & forms
 * ------------------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: var(--control-h);
  background: var(--acc); color: var(--acc-ink);
  border: 1px solid transparent; border-radius: var(--r-2);
  padding: 6px var(--s3); font-size: var(--fs-sm); font-weight: var(--fw-semi);
  cursor: pointer; transition: background-color .15s ease, border-color .15s ease;
}
.btn:hover { background: var(--acc-hover); text-decoration: none; }
.btn:active { transform: translateY(1px); }
/* Neutral, not accent — so Cancel / Clear / Print stop competing with the
   one real action on the page. */
.btn.ghost { background: var(--card); color: var(--ink); border-color: var(--line-strong); }
.btn.ghost:hover { background: var(--hover); border-color: var(--mut); }
/* Selected state for ghost buttons used as a segmented switch. */
.btn.ghost[aria-current] {
  background: var(--acc-soft); color: var(--acc); border-color: var(--acc);
}
.btn.danger { background: var(--bad); color: var(--bad-ink); }
.btn.danger:hover { background: var(--bad); filter: brightness(.92); }
.btn.sm { min-height: 30px; padding: 3px var(--s2); }
/* Where the button IS the row rather than sitting in one — the sign-in form.
   A class, so it can be themed and measured; it replaces a style="width:100%"
   that no stylesheet could reach. */
.btn.block { width: 100%; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
/* Touch devices get comfortable targets — but only where fingers are the
   input, and never in the desktop rail. */
@media (pointer: coarse) and (max-width: 1099.98px) {
  :root { --control-h: 44px; }
  .btn.sm { min-height: 40px; padding: var(--s2) var(--s3); font-size: var(--fs-body); }
  nav.main a.navlink, .navlink.as-btn { min-height: 44px; }
  nav.main .nav-toggle, nav.main [data-theme-toggle] { min-width: 44px; min-height: 44px; }
}

form.inline { display: inline; }
label {
  display: block; color: var(--mut); font-size: var(--fs-sm);
  font-weight: var(--fw-medium); line-height: 1.35; margin-bottom: var(--s1);
}
input, select, textarea {
  width: 100%; min-height: var(--control-h);
  background: var(--field); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: var(--r-1);
  padding: 6px 9px; font: inherit; font-size: var(--fs-body);
}
/* 16px floor stops iOS Safari zooming on focus; only phones need it. */
@media (pointer: coarse) { input, select, textarea { font-size: max(16px, 1em); } }
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--acc); outline-offset: 1px; border-color: var(--acc);
}
a:focus-visible, button:focus-visible, .btn:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--acc); outline-offset: 2px; box-shadow: 0 0 0 4px var(--ring);
}
/* Inside the rail an offset ring would be clipped by the scroll container. */
.navlink:focus-visible { outline-offset: -2px; }
input[type="checkbox"], input[type="radio"] { width: auto; min-height: 0; }
/* A checkbox and the wording that explains it, on one line — the whole
   label is the hit area, and it reads as a statement rather than a field
   caption, so it takes ink instead of the muted label colour. */
.check { display: flex; align-items: center; gap: var(--s2); color: var(--ink); }
.field { margin-bottom: var(--s2); }
.field .err { color: var(--bad); font-size: var(--fs-sm); margin-top: 2px; }
.row { display: flex; gap: var(--s2); flex-wrap: wrap; }
.row > .field { flex: 1; min-width: 140px; }
.row > .card { flex: 1 1 260px; min-width: 0; }
.row.end { justify-content: flex-end; }
.row.middle { align-items: center; }
.row.bottom { align-items: flex-end; }
/* Cards side by side stretch to the tallest by default, which on a pair of
   unrelated panels leaves the short one padded out with nothing. */
.row.top { align-items: flex-start; }

/* Field width utilities (replace ad-hoc inline min-widths). Fixed-size on
   wide screens, full-width on phones. */
.row > .field.w-xs { flex: 0 0 auto; min-width: 110px; }
.row > .field.w-sm { flex: 0 0 auto; min-width: 155px; }
.row > .field.w-md { flex: 0 0 auto; min-width: 185px; }
.row > .field.w-lg { flex: 0 0 auto; min-width: 220px; }
@media (max-width: 719px) {
  .row > .field.w-xs, .row > .field.w-sm,
  .row > .field.w-md, .row > .field.w-lg { flex: 1 1 100%; }
}

.center-box { max-width: 480px; margin: 8vh auto; text-align: center; }

/* Small layout utilities */
.row > .field.self-end { flex: 0 0 auto; align-self: flex-end; }
.mt-0 { margin-top: 0; }
.mb-sm { margin-bottom: var(--s2); }
.nowrap { white-space: nowrap; }
input.w-auto, select.w-auto { width: auto; }
.card.narrow { max-width: 540px; }
.form-errors {
  background: var(--bad-bg); border: 1px solid var(--bad);
  border-radius: var(--r-2); padding: var(--s2) var(--s3); margin-bottom: var(--s3);
}
.form-errors:focus { outline: 2px solid var(--bad); outline-offset: 2px; }
.form-errors-title { margin: 0 0 var(--s1); font-weight: var(--fw-semi); }
.form-errors ul { margin: 0; padding-left: 18px; }
/* The field itself carries the failure, not just the summary at the top. */
.field.has-error > label { color: var(--bad); }
.field.has-error input, .field.has-error select, .field.has-error textarea,
[aria-invalid="true"] { border-color: var(--bad); }

/* --- Document totals ------------------------------------------------ *
 * The running total of what is being written, beside the lines that make
 * it up. Right-aligned in a narrow column so the figures stack into one
 * scannable line of digits.
 * ------------------------------------------------------------------- */
.doc-totals {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--s1); margin-top: var(--s3);
  padding-top: var(--s3); border-top: 1px solid var(--line);
}
.doc-totals dl { margin: 0; width: min(320px, 100%); }
.tot-line {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s4); padding: 3px 0;
}
.tot-line dt { color: var(--mut); }
.tot-line dd {
  margin: 0; font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}
.tot-line.grand {
  margin-top: var(--s1); padding-top: var(--s2);
  border-top: 2px solid var(--line-strong);
}
.tot-line.grand dt { color: var(--ink); font-weight: var(--fw-semi); }
.tot-line.grand dd { font-size: var(--fs-kpi); font-weight: 700;
  letter-spacing: var(--ls-tight); }
.totals-note { margin: var(--s1) 0 0; color: var(--mut); font-size: var(--fs-micro); }

/* --- Sticky form action bar ------------------------------------------ *
 * Bottom at every width, not top on desktop: the total belongs next to the
 * button that commits it, and one position means one set of behaviours to
 * get right on a phone.
 * -------------------------------------------------------------------- */
.form-actions {
  position: sticky; bottom: 0; z-index: var(--z-rail);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap;
  margin: var(--s3) 0 0;
  padding: var(--s2) var(--s3);
  padding-bottom: calc(var(--s2) + env(safe-area-inset-bottom));
  background: var(--card); border: 1px solid var(--line-strong);
  border-radius: var(--radius); box-shadow: var(--shadow-2);
}
.fa-total { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.fa-total .label {
  font-size: var(--fs-micro); font-weight: var(--fw-semi);
  text-transform: uppercase; letter-spacing: var(--ls-micro); color: var(--mut);
}
.fa-total strong { font-size: var(--fs-kpi); letter-spacing: var(--ls-tight); }
.fa-buttons { display: flex; gap: var(--s2); flex-wrap: wrap; margin-left: auto; }
/* A locked button still has to read as the same control, not vanish. */
.btn[data-locked] { opacity: .7; cursor: progress; }
/* Below 720px the tab bar owns the bottom edge — including the safe area —
   so the action bar stands on top of it instead of behind it. */
@media (max-width: 719px) {
  .form-actions {
    bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
    padding-bottom: var(--s2);
  }
}
@media (max-width: 480px) {
  .form-actions { gap: var(--s2); }
  .fa-buttons { width: 100%; }
  .fa-buttons .btn { flex: 1; }
}

/* ------------------------------------------------------------------ *
 * Flash messages
 * ------------------------------------------------------------------ */
.flash {
  border-radius: var(--r-2); padding: var(--s2) 38px var(--s2) var(--s3);
  margin-bottom: var(--s2); position: relative;
}
.flash-close {
  position: absolute; top: 4px; right: 4px; border: 0; background: transparent;
  color: inherit; font: inherit; cursor: pointer; padding: 5px 9px; border-radius: var(--r-1);
}
.flash-close:hover { background: color-mix(in srgb, var(--ink) 10%, transparent); }
.flash.success { background: var(--good-bg); border: 1px solid var(--good); }
.flash.error { background: var(--bad-bg); border: 1px solid var(--bad); }
.flash.warning { background: var(--warn-bg); border: 1px solid var(--warn); }
.flash.info { background: var(--card-2); border: 1px solid var(--line-strong); }
.toasts:empty { display: none; }

/* Confirmations rise from the bottom on a phone, beside the thumb that
   caused them and clear of the page they are about. On a wide screen there
   is nothing to get out of the way of, so they stay where they were. */
@media (max-width: 719px) {
  .toasts {
    position: fixed; z-index: var(--z-toast);
    left: var(--s2); right: var(--s2);
    bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom) + var(--s2));
    display: flex; flex-direction: column; gap: var(--s1);
  }
  .toasts .flash { margin: 0; box-shadow: var(--shadow-2); }
  .flash.toast { animation: toast-in .22s ease-out; }
}
@keyframes toast-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .flash.toast { animation: none; }
}

/* ------------------------------------------------------------------ *
 * Pills & badges
 * ------------------------------------------------------------------ */
.pill {
  display: inline-block; font-size: var(--fs-micro); font-weight: var(--fw-semi);
  padding: 1px var(--s2); border-radius: 99px;
  border: 1px solid var(--line-strong); color: var(--mut); vertical-align: middle;
}
.pill.warn { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
/* Which copy of the books this is, rendered only when ENV_LABEL is set — so
   production carries nothing. The auto margin sends it to the far left of the
   desktop top bar, which is otherwise empty space; below 1100px there is no
   free space to distribute and it simply sits inline before the toggle. */
.env-badge {
  margin-right: auto; text-transform: uppercase;
  letter-spacing: var(--ls-micro); white-space: nowrap;
}
.pill.bad { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
.pill.good { background: var(--good-bg); color: var(--good); border-color: transparent; }
/* Urdu companion labels (appended by the ur() template global). */
.ur {
  font-family: "Noto Naskh Arabic", "Urdu Typesetting", "Jameel Noori Nastaleeq",
    "Geeza Pro", "Noto Nastaliq Urdu", serif;
  font-size: .95em; line-height: 1.15; color: var(--mut);
  font-weight: 400; text-transform: none; letter-spacing: 0;
}
h1 .ur, h2 .ur { font-size: .8em; }
.btn .ur { color: inherit; opacity: .85; font-size: .9em; }
.navlink[aria-current="page"] .ur, .nav-group-title .ur { color: inherit; }

.pos { color: var(--good); }
.neg { color: var(--bad); }
.muted { color: var(--mut); }
.badge-ok { color: var(--good); font-weight: var(--fw-semi); }
.badge-bad { color: var(--bad); font-weight: var(--fw-semi); }

/* ------------------------------------------------------------------ *
 * Line editor
 * ------------------------------------------------------------------ */
table.line-editor input, table.line-editor select { min-width: 90px; }
table.line-editor td { vertical-align: top; }
.editor-status { margin: var(--s2) 0; }
.editor-status .delta, .editor-status.delta { color: var(--warn); font-weight: 700; }
.editor-status .ok { color: var(--good); font-weight: 700; }

/* Phones: each editor row becomes a labelled card. Cell labels come from
   data-label attributes that line_editor.js copies off the thead. */
@media (max-width: 719px) {
  table.line-editor, table.line-editor tbody, table.line-editor tfoot { display: block; }
  table.line-editor thead { display: none; }
  table.line-editor tbody tr {
    display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2) var(--s3);
    border: 1px solid var(--line-strong); border-radius: var(--radius);
    background: var(--card-2);
    padding: var(--s2) var(--s2) var(--s3); margin-bottom: var(--s2); position: relative;
  }
  table.line-editor td { display: block; border: 0; padding: 0; min-width: 0; }
  table.line-editor td:has(select),
  table.line-editor td:has(input[name$="-description"]),
  table.line-editor td:has(input[name$="-memo"]),
  table.line-editor td:has(input[name$="-packages"]) { grid-column: 1 / -1; }
  table.line-editor td[data-label]::before {
    content: attr(data-label); display: block; margin-bottom: 3px;
  }
  table.line-editor td[data-role="amount"] { text-align: right; font-weight: 700; }
  table.line-editor input, table.line-editor select { min-width: 0; width: 100%; }
  table.line-editor td:has([data-remove-row]) {
    position: absolute; top: 2px; right: 2px;
  }
  table.line-editor td:has([data-remove-row])::before { display: none; }
  table.line-editor [data-remove-row] { min-width: 40px; min-height: 40px; }
  table.line-editor tfoot tr {
    display: flex; justify-content: space-between; gap: var(--s3); flex-wrap: wrap;
    /* Clear of the tab bar, like the action bar it sits above. */
    position: sticky; z-index: 5;
    bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
    background: var(--card); border-top: 2px solid var(--line-strong);
    padding: var(--s2) var(--s1);
  }
  table.line-editor tfoot td { border: 0; padding: 0; }
  table.line-editor tfoot td:empty { display: none; }
}

/* Generic stacked tables for phone-width lists: <table data-stack> gets its
   cells labelled by app.js from the header row. */
@media (max-width: 719px) {
  table[data-stack], table[data-stack] tbody { display: block; }
  table[data-stack] tbody tr, table[data-stack] > tr {
    display: block; border: 1px solid var(--line-strong); border-radius: var(--radius);
    padding: var(--s2) var(--s3); margin-bottom: var(--s2);
  }
  /* After the row rule so the hide wins at equal specificity. */
  table[data-stack] thead, table[data-stack] tr:has(> th) { display: none; }
  table[data-stack] td {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: var(--s3); border: 0; padding: 3px 0; text-align: right;
  }
  table[data-stack] td[data-label]::before {
    content: attr(data-label); text-align: left; flex: 0 0 auto;
  }
  table[data-stack] td:not([data-label]) { justify-content: flex-end; }
  table[data-stack] td.num { white-space: normal; }
  table[data-stack] td.empty { display: block; text-align: center; }
  /* The empty state is its own composed block; the card chrome the stacked
     layout puts round every row would frame it twice. */
  table[data-stack] tr.no-rows { border: 0; padding: 0; margin: 0; }
  table[data-stack] tr.tot td { border-top: 0; font-weight: 700; }
  table[data-stack] tr:hover td { background: transparent; }
  /* Action cells with embedded forms (e.g. cheque clear) stack full-width. */
  table[data-stack] td:has(form) { display: block; text-align: left; }
  table[data-stack] td:has(form)::before { display: block; margin-bottom: var(--s1); }
  table[data-stack] td form.inline {
    display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
    margin-bottom: 6px;
  }
  table[data-stack] td form.inline input,
  table[data-stack] td form.inline select { width: auto; flex: 1 1 130px; }
  table[data-stack] tfoot { display: block; }
  table[data-stack] tfoot tr {
    display: block; border: 1px solid var(--line-strong); border-radius: var(--radius);
    padding: var(--s2) var(--s3); margin-bottom: var(--s2);
  }
  table[data-stack] input { width: 100%; max-width: none; }
  /* A cell with nothing in it still printed its label — "PO REFERENCE" over
     a blank. app.js marks them; an absent line reads as absent data. */
  table[data-stack] td.is-blank { display: none; }
}

/* --- Two-line list rows (phones) ------------------------------------- *
 * A labelled line per column turns a five-column list into thirty lines of
 * "LABEL value". Tag the cells that carry the answer instead — who, how
 * much, which document — and the row reads in one glance. Everything left
 * untagged keeps the labelled-line fallback below, so no column is lost.
 *
 *   data-role="title"   the name you scan for (party, item, memo)
 *   data-role="amount"  the money, first line, right
 *   data-role="meta"    the document's own identity, second line
 *   data-role="status"  second line, right
 *   data-role="date"    the grouping key; hidden once JS has grouped by it
 * -------------------------------------------------------------------- */
@media (max-width: 719px) {
  /* Only rows that actually carry a role reflow. Matching every `tr` would
     tie on specificity with the rule that hides the header row — and a tie
     goes to whichever came last, which is this one. The header would then
     render as a card of its own column names. Totals rows and the date
     headings are excluded for free by the same test. */
  table[data-cards] tbody tr:has(> td[data-role]),
  table[data-cards] > tr:has(> td[data-role]) {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--s3); row-gap: 2px; align-items: baseline;
  }
  table[data-cards] td[data-role] {
    display: block; padding: 0; border: 0; text-align: left;
  }
  table[data-cards] td[data-role]::before { content: none; }
  table[data-cards] td[data-role="title"] {
    order: 1; grid-column: 1;
    font-weight: var(--fw-semi); font-size: var(--fs-body); color: var(--ink);
  }
  table[data-cards] td[data-role="amount"] {
    order: 2; grid-column: 2; text-align: right;
    font-weight: var(--fw-semi); white-space: nowrap;
  }
  table[data-cards] td[data-role="meta"] {
    order: 3; grid-column: 1; color: var(--mut); font-size: var(--fs-sm);
  }
  table[data-cards] td[data-role="status"] {
    order: 4; grid-column: 2; text-align: right;
  }
  table[data-cards] td[data-role="date"] {
    order: 5; grid-column: 1 / -1; color: var(--mut); font-size: var(--fs-sm);
  }
  /* The untagged remainder keeps its label:value line, under the two that
     matter. */
  table[data-cards] td:not([data-role]) { order: 6; grid-column: 1 / -1; }
  /* Grouped: the heading above the row carries the date, so the cell inside
     it would only repeat itself. Gated on the class app.js adds, so without
     JS the date stays in the row where it can still be read. */
  table[data-cards].is-grouped td[data-role="date"] { display: none; }
  /* A tagged cell with nothing in it would otherwise beat the blank rule on
     specificity and leave an empty grid slot with a gap under it. */
  table[data-cards] td[data-role].is-blank { display: none; }
  /* Reversed and inactive rows are already muted; keep the title honest. */
  table[data-cards] tr.muted td[data-role="title"] { color: inherit; }
}

/* Date headings inserted by app.js. They only make sense in the stacked
   card layout — the table has a Date column of its own. */
tr.row-group { display: none; }
@media (max-width: 719px) {
  table[data-stack] tr.row-group {
    display: block; border: 0; background: none;
    padding: 0; margin: var(--s3) 0 var(--s1);
  }
  table[data-stack] tr.row-group:first-child { margin-top: 0; }
  table[data-stack] tr.row-group td {
    display: block; padding: 0; border: 0; text-align: left;
    font-size: var(--fs-micro); font-weight: var(--fw-semi);
    text-transform: uppercase; letter-spacing: var(--ls-micro); color: var(--mut);
  }
}

/* --- Foldable sections ------------------------------------------------ *
 * A record's line table is the bulk of the page and the least of what you
 * opened it for on a phone — the header already carries the party, the
 * total and the status. Open everywhere by default (that is the no-JS
 * state); app.js closes it below 720px until the reader says otherwise.
 * -------------------------------------------------------------------- */
.fold > summary { display: none; }
/* The desktop stand-in for a hidden summary. Deliberately not the summary
   itself made visible: a summary is always a toggle, and one clicked shut up
   here would leave no control to open it again. */
.fold-heading { margin: 0 0 var(--s2); font-size: var(--fs-h2); }
@media (max-width: 719px) {
  .fold-heading { display: none; }
  .fold > summary {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--s2); cursor: pointer; list-style: none;
    padding: var(--s1) 0; margin-bottom: var(--s1);
    font-weight: var(--fw-semi);
  }
  .fold > summary::-webkit-details-marker { display: none; }
  .fold > summary::after {
    content: ""; flex: 0 0 auto; width: 8px; height: 8px; margin-right: 3px;
    border-right: 2px solid var(--mut); border-bottom: 2px solid var(--mut);
    transform: rotate(45deg) translate(-2px, -2px);
  }
  .fold[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
  .fold > summary:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
  .fold-hint { font-weight: 400; color: var(--mut); font-size: var(--fs-sm);
    margin-left: auto; }
}
/* Payroll grid inputs stay compact only in the columnar (desktop) layout. */
@media (min-width: 720px) {
  #paygrid input.num { max-width: 110px; }
}

/* Tall report tables: sticky header inside their own scroll area. */
.table-scroll.tall { max-height: 72vh; overflow: auto; }
.table-scroll.tall thead th, .table-scroll.tall tr:first-child th {
  position: sticky; top: 0; z-index: 2; background: var(--card-2);
}
/* ...and a totals row pinned to the foot of that same area. A closing
   balance that scrolls off the bottom of a 400-row ledger is the one figure
   the reader came for, and they had to fall off the end to reach it.
   The rule above it is drawn as an inset shadow: a collapsed-border table
   drops the real border when the cell goes sticky. */
.table-scroll.tall tr.tot td {
  position: sticky; bottom: 0; z-index: 2;
  background: var(--card-2); border-top: 0;
  box-shadow: inset 0 2px 0 var(--line-strong);
}
/* Hover repaints the cell background, which on a sticky row means the rows
   scrolling under it show through. */
.table-scroll.tall tr.tot:hover td { background: var(--card-2); }
/* Wide tables: pin the first column while scrolling sideways. */
.table-scroll.pin-first th:first-child, .table-scroll.pin-first td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--card);
}
.table-scroll.pin-first th:first-child { background: var(--card-2); }
/* The corner cell has to outrank both the sticky header row and the pinned
   column, or sideways scrolling paints another header over it. */
.table-scroll.tall.pin-first thead th:first-child,
.table-scroll.tall.pin-first tr:first-child th:first-child { z-index: 3; }
/* Out-specify the pinned cell so row hover still reaches the first column. */
.table-scroll.pin-first tr:hover td:first-child { background: var(--hover); }
/* A spanning cell is not an identity column. Pinned to the left it slides
   across the very figure it labels — scrolled to the right-hand end of a
   ledger, "Closing balance" sat on top of the closing balance. `left: auto`
   rather than `position: static`, so the totals row still pins to the foot
   of the scroll area; it is only the sideways stick that is wrong here. */
.table-scroll.pin-first td[colspan]:first-child,
.table-scroll.pin-first th[colspan]:first-child { left: auto; }
/* Ledgers and the columnar reports on a phone. These are the pages M4 left
   as scrolling tables on purpose: a running balance is read down a column,
   and a two-line card destroys the one thing the page is for. So instead of
   restacking them, make the sideways scroll survivable — the date stays
   pinned on the left, the column names stay pinned at the top, the closing
   balance stays pinned at the foot, and the type tightens so a six-column
   ledger needs one short swipe rather than three. */
@media (max-width: 719px) {
  .table-scroll.tall { max-height: 60vh; }
  .table-scroll.tall table { font-size: var(--fs-sm); }
  .table-scroll.tall th, .table-scroll.tall td { padding: 6px var(--s2); }
  /* The pinned column is the row's identity, so it may not be the column
     that wraps to four lines when the rest is squeezed. */
  .table-scroll.pin-first th:first-child, .table-scroll.pin-first td:first-child {
    white-space: nowrap;
  }
}

/* Scroll-edge hint shadows (classes toggled by app.js). */
.table-scroll.has-overflow-right {
  background-image: linear-gradient(to left, color-mix(in srgb, var(--ink) 12%, transparent), transparent 24px);
}

/* Review dialog for destructive actions */
dialog.confirm {
  border: 1px solid var(--line-strong); border-radius: var(--r-3);
  background: var(--card); color: var(--ink);
  padding: var(--s5); max-width: 26rem; width: calc(100% - var(--s5));
  box-shadow: var(--shadow-2);
}
dialog.confirm::backdrop { background: rgba(16, 24, 40, .55); }
dialog.confirm h2 { margin: 0 0 var(--s2); font-size: var(--fs-h1); }
.confirm-detail { margin: 0 0 var(--s4); color: var(--mut); }
/* Cancel first in the source, so it is what the dialog opens focused on and
   what Enter does — the destructive button has to be aimed at. */
.confirm-actions { display: flex; gap: var(--s2); justify-content: flex-end; }

/* Pagination */
.pager { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s3);
  color: var(--mut); font-size: var(--fs-sm); }

/* --- Filter bars ---------------------------------------------------- *
 * The bar occupies the content column instead of huddling at the left of
 * it: the fields share the width and the buttons sit on the same right
 * edge as the cards and the table, so nothing floats in dead space.
 * Phones stack it — one field per row, buttons splitting the last one.
 * ------------------------------------------------------------------- */
form.filters {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--s2) var(--s3); margin-bottom: var(--s3);
}
form.filters .field { margin: 0; flex: 1 1 100%; min-width: 0; }
/* From and To are one thought: they stay side by side even on a phone,
   where every other field gets its own row. */
form.filters .field:has(input[type="date"]),
form.filters .field:has(input[type="month"]) { flex: 1 1 45%; }
form.filters .check { flex: 1 1 100%; margin-bottom: 0; }
form.filters .filter-actions { display: flex; gap: var(--s2); flex: 1 1 100%; }
form.filters .filter-actions > .btn { flex: 1 1 auto; }

@media (min-width: 641px) {
  form.filters .field { flex: 1 1 10rem; max-width: 22rem; }
  /* A date is the same size on any screen — widening one only pushes the
     day further from the month it belongs to. */
  form.filters .field:has(input[type="date"]),
  form.filters .field:has(input[type="month"]) { flex: 0 0 11.5rem; }
  /* Free text is the field that actually wants the room left over, so it
     takes the slack the fixed-width ones give back. */
  form.filters .field:has(input[type="search"]),
  form.filters .field:has(input[type="text"]),
  form.filters .field:has(input:not([type])) { flex: 4 1 14rem; max-width: none; }
  form.filters .check { flex: 0 1 auto; }
  form.filters .filter-actions { flex: 0 0 auto; margin-left: auto; }
  form.filters .filter-actions > .btn { flex: 0 0 auto; }
  /* A posting form borrowing this layout keeps its button beside the field
     it acts on — sending it to the far edge would read as a filter. */
  form.filters.actions-inline .filter-actions { margin-left: 0; }
}

/* --- Sign in ---------------------------------------------------------- *
 * The one screen with no shell around it, so it carries the brand itself:
 * mark, company name, the form, and nothing else. Everything below is the
 * ordinary card and field styling — this block only sets the column.
 * ---------------------------------------------------------------------- */
.auth-page main.wrap {
  /* dvh, not vh: on iOS Safari 100vh includes the toolbar that is actually
     covering the page, which pushed the card down behind it. */
  min-height: 100dvh;
  max-width: 400px; padding-block: var(--s5);
  display: flex; flex-direction: column; justify-content: center;
}
/* Narrowing the column is also what fixes the flashes. They are rendered by
   base.html above this block, so at the old full width "Too many failed
   attempts" sat adrift at the top of a 1180px page instead of over the form
   it was about. */
.auth-head {
  display: flex; align-items: center; justify-content: center;
  gap: var(--s2); margin-bottom: var(--s1);
}
.auth-head .brand-mark { width: 28px; height: 28px; flex: 0 0 28px; }
.auth-head h1 { margin: 0; }
.auth-sub { color: var(--mut); text-align: center; margin: 0 0 var(--s4); }
/* Left in the flow at order 0, so it lands under any rejection and directly
   above the form — the last thing read before typing. */
.auth-env {
  margin: 0 0 var(--s3); padding: var(--s2) var(--s3);
  background: var(--warn-bg); color: var(--warn);
  border-radius: var(--r-2); font-size: var(--fs-sm);
}
/* base.html renders the flash regions ahead of the page content, which here
   would stack "Wrong username or password" above the company name rather
   than over the form it is about. Reordering the column is enough: the
   message keeps its place in the DOM, so it is still the first thing a
   screen reader reaches on the way to the fields. */
.auth-head { order: -2; }
.auth-sub { order: -1; }
/* The card is the whole of the rest of the page, so its usual bottom margin
   would only push the centred column off-centre. */
.auth-page .card { margin-bottom: 0; order: 1; }
.auth-page .field { margin-bottom: var(--s3); }
/* Fixed to the viewport corner rather than placed in the column: it belongs
   to the page, not to the form, and the column is vertically centred. */
.auth-bar { position: fixed; top: var(--s3); right: var(--s3); }
.auth-bar .navlink.as-btn { width: auto; }

@media (max-width: 640px) {
  nav.main { gap: 2px 6px; }
  .wrap { padding: var(--s3) var(--s2) 40px; }
}

/* ------------------------------------------------------------------ *
 * Meter — one quantity filling toward a known total (solar payback).
 * Same anatomy in both themes: track is a recessed surface, fill is
 * the good tone because reaching it is the point.
 * ------------------------------------------------------------------ */
.meter {
  height: 14px;
  border-radius: 7px;
  background: var(--card-2);
  border: 1px solid var(--line);
  overflow: hidden;
}
.meter-fill { height: 100%; background: var(--good); }

/* ------------------------------------------------------------------ *
 * Activity timeline — what was done to a record, on the record.
 * ------------------------------------------------------------------ */
.timeline { list-style: none; margin: 0; padding: 0; }
.tl-item {
  position: relative;
  padding: var(--s2) 0 var(--s2) var(--s4);
  border-bottom: 1px solid var(--line);
}
.tl-item:last-child { border-bottom: 0; padding-bottom: 0; }
.tl-item:first-child { padding-top: 0; }
/* A rail with a node per event, drawn rather than imaged so it inherits the
   theme. The line stops short of the last node instead of trailing off the
   bottom of the card into nothing. */
.tl-item::before {
  content: ""; position: absolute; left: 3px; top: 13px;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--card); border: 2px solid var(--line-strong);
}
.tl-item:first-child::before { top: 5px; background: var(--acc); border-color: var(--acc); }
.tl-item::after {
  content: ""; position: absolute; left: 6px; top: 22px; bottom: -1px;
  width: 1px; background: var(--line);
}
.tl-item:first-child::after { top: 14px; }
.tl-item:last-child::after { display: none; }

.tl-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s1) var(--s2); margin: 0;
}
.tl-what { font-weight: var(--fw-medium); }
.tl-who { color: var(--mut); }
/* Pushed right where there is room and allowed to wrap under on a phone,
   rather than truncated — the time is half of what the reader came for. */
.tl-when {
  margin-left: auto; color: var(--mut);
  font-size: var(--fs-sm); font-variant-numeric: tabular-nums;
}
.tl-detail { margin: var(--s1) 0 0; color: var(--mut); font-size: var(--fs-sm); }

.tl-changes { margin-top: var(--s1); }
.tl-changes > summary {
  cursor: pointer; color: var(--acc);
  font-size: var(--fs-sm); width: fit-content;
}
.tl-changes > summary:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
.tl-diff { margin: var(--s2) 0 0; font-size: var(--fs-sm); }
.tl-field {
  display: flex; flex-wrap: wrap; gap: var(--s1) var(--s2);
  padding: var(--s1) 0;
}
.tl-field dt { color: var(--mut); flex: 0 0 auto; min-width: 100px; }
.tl-field dd { margin: 0; overflow-wrap: anywhere; }
/* The old value is struck rather than only dimmed: the diff has to survive
   being read at a glance, and two greys side by side do not say which one
   is the past. */
.tl-old { color: var(--mut); text-decoration: line-through; }
.tl-arrow { color: var(--mut); margin: 0 2px; }
.tl-new { font-weight: var(--fw-medium); }

/* ------------------------------------------------------------------ *
 * Print — the shell is chrome, not content. Without this a fixed rail
 * leaves a blank gutter on every printed page of a report.
 * ------------------------------------------------------------------ */
@media print {
  /* Paper is white whichever theme is on screen — otherwise a dark-mode
     user prints pale grey headings and dark card fills. */
  :root, [data-theme="dark"] {
    color-scheme: light;
    --bg: #fff; --card: #fff; --card-2: #fff; --field: #fff; --hover: #fff;
    --ink: #000; --mut: #333; --line: #999; --line-strong: #666;
    --acc: #000; --acc-soft: #fff; --acc-ink: #fff;
    --good: #0a5c36; --bad: #a3111f; --warn: #6b4500;
    --good-bg: #fff; --bad-bg: #fff; --warn-bg: #fff;
    --ring: transparent; --shadow-1: none; --shadow-2: none;
  }
  .skip-link, header.top, .nav-overlay, [data-nav-drawer], .tabbar,
  .actions, .pager, .flash-close, .crumbs, .chips, .filter-actions,
  .form-actions, .totals-note, .fold > summary { display: none !important; }
  /* Two things survive that pass: the row count, which on a filtered list is
     part of what was printed, and the filter fields themselves — on a report
     whose heading does not repeat the period, the dates in them are the only
     record on paper of what was asked for. */
  .result-bar { color: #000; margin-bottom: var(--s2); }
  body, body.has-shell {
    padding-left: 0 !important; padding-top: 0 !important;
    padding-bottom: 0 !important;
    background: #fff; color: #000;
  }
  /* A toast is a screen affordance; on paper it is just the last thing that
     happened, so it prints where it was written rather than floating. */
  .toasts { position: static !important; }
  .wrap { max-width: none !important; margin: 0 !important; padding: 0 !important; }
  .card { border: 1px solid #999; box-shadow: none; break-inside: avoid; }
  .table-scroll, .table-scroll.tall { overflow: visible !important; max-height: none !important; }
  /* Nothing sticks to paper: an un-scrolled page has no viewport to pin to,
     and a sticky totals row prints on top of the rows it was floating over. */
  .table-scroll.tall thead th, .table-scroll.tall tr:first-child th,
  .table-scroll.tall tr.tot td,
  .table-scroll.pin-first th:first-child, .table-scroll.pin-first td:first-child {
    position: static !important; box-shadow: none !important;
  }
  .table-scroll.tall tr.tot td { border-top: 2px solid #666; }
  th { background: transparent; }
  /* The sort arrow is a control; the column it sorted is already sorted. */
  .sort-mark { display: none !important; }
  /* A meter's fill is the figure. Printers drop background colour by
     default, which would leave an empty track saying nothing — the border
     keeps the bar readable as an outline, and the caption above it carries
     the number either way. */
  .meter { border: 1px solid #666; }
  .meter-fill {
    background: #666 !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
}
