/* One stylesheet, implementing docs/DESIGN.md ("Fils").
 *
 * The application is dense, internal, and read on a desktop by people who use
 * it all day, so it is laid out for scanning tables rather than for a first
 * impression. Every rule below traces to a numbered section of that document;
 * the section is named where the reason is not obvious from the code.
 *
 * Two mechanical rules govern this file and are enforced by
 * tests/test_design_tokens.py:
 *
 *   - No colour is defined only under dark. The full light set lives on
 *     :root and dark redefines the same names (DESIGN.md 2.4).
 *   - No physical left/right property. Logical properties only, so turning
 *     Arabic on is a direction flip and not a rewrite (13).
 */

/* ── faces ────────────────────────────────────────────────────────────────
 * Self-hosted, committed, never fetched. The host may have no egress and
 * runs a strict CSP, which is the same reason jQuery is vendored (15).
 *
 * Only the Latin1 subset of each face ships. Arabic (13) is a third file
 * added when the locale is switched on, not a redesign.
 */

/* Inter is the UI face (3). The WOFF2 files are not committed yet, so the
   stack below falls through to the platform's own neutral grotesque -- SF on
   macOS, Segoe on Windows -- which is far closer to Inter than IBM Plex Sans
   was. Drop Inter-{Regular,Medium,SemiBold}-Latin1.woff2 into static/fonts/,
   uncomment the three blocks below, and add the preload to base.html.

@font-face {
  font-family: "Inter";
  src: url("../fonts/Inter-Regular-Latin1.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Inter";
  src: url("../fonts/Inter-Medium-Latin1.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Inter";
  src: url("../fonts/Inter-SemiBold-Latin1.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

*/

@font-face {
  font-family: "IBM Plex Mono";
  src: url("../fonts/IBMPlexMono-Regular-Latin1.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("../fonts/IBMPlexMono-Medium-Latin1.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* ── tokens ───────────────────────────────────────────────────────────── */

:root {
  color-scheme: light;

  /* Foundation (2.1). ReUI is pure white on a neutral grey scale; the warm
     paper of the previous revision fought those greys. */
  --paper: #FFFFFF;
  --surface: #FFFFFF;
  --surface-sunken: #FAFAFA;
  --surface-muted: #F5F5F5;
  --ink: #0A0A0A;
  --ink-muted: #737373;
  --ink-faint: #A1A1A1;          /* decorative only. Never carries information. */
  --rule: #E5E5E5;
  --rule-strong: #D4D4D4;

  /* Primary (2.2). Near-black, not a brand colour. That is what frees the
     palette for signals, and it is why Principle 6 can be as strict as it is.
     This is the token ABK's brand sheet replaces; nothing else moves. */
  --primary: #171717;
  --primary-hover: #262626;
  --primary-ink: #FAFAFA;

  /* Links, focus and selection only -- never a button (2.2). */
  --accent: #1447E6;
  --accent-hover: #1035B5;
  --accent-subtle: #EEF2FF;

  /* Signal (2.3) -- the only semantic colours. Fill and ink are separate
     tokens because emerald-500 on white is about 2:1: fine for a 6px dot and
     illegible as a word (14). Only the -ink half may carry text. */
  --signal-good: #00BC7D;
  --signal-warn: #F0B100;
  --signal-bad: #E40014;
  --signal-none: #A1A1A1;

  --signal-good-ink: #004F3B;
  --signal-warn-ink: #733E0A;
  --signal-bad-ink: #9F0712;
  --signal-none-ink: #737373;

  --signal-good-wash: #E8F8F1;
  --signal-warn-wash: #FDF3D6;
  --signal-bad-wash: #FDEAEA;

  /* Typography (3). Inter for the UI, Plex Mono for every numeral compared
     column-to-column: a KWD figure in a proportional face is a defect. */
  --font-ui: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* Space (4). 4px base. These eight steps and nothing else. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s6: 24px;
  --s8: 32px;
  --s12: 48px;
  --s16: 64px;

  /* Density (4). One setting for the whole application, chosen in the header
     and remembered locally -- an operator comparing two screens should not
     have to re-set it. */
  --row-h: 40px;
  --cell-pad-block: 8px;
  --cell-pad-inline: 12px;

  /* Radius (4), from ReUI's --radius: .625rem. */
  --radius-sm: 6px;
  --radius: 10px;
  --radius-lg: 14px;

  /* Motion (12). Tailwind's ease-out, which is what ReUI transitions on.
     Everything not listed there changes state instantly. */
  --ease: cubic-bezier(0, 0, 0.2, 1);
  --motion-fast: 100ms;
  --motion: 150ms;

  /* Elevation (2.5). Exactly two, and no third: a hairline ring draws the
     edge of a container, a lift belongs to things that genuinely float. */
  --ring-hairline: 0 0 0 1px rgba(10, 10, 10, 0.10);
  --lift: 0 8px 24px rgba(10, 10, 10, 0.14);

  --sidebar-w: 256px;
  --sidebar-w-rail: 56px;
  --chrome-h: 48px;
  --context-h: 48px;
  --container-app: 80rem;

  --z-sticky: 20;
  --z-drawer: 40;
}

/* Dark (2.4) redefines the same token names and nothing else. Signals shift
   up in lightness to hold 4.5:1 on #101215. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --paper: #0A0A0A;
  --surface: #171717;
  --surface-sunken: #171717;
  --surface-muted: #262626;
  --ink: #FAFAFA;
  --ink-muted: #A1A1A1;
  --ink-faint: #737373;
  --rule: rgba(255, 255, 255, 0.10);
  --rule-strong: rgba(255, 255, 255, 0.15);

  --primary: #E5E5E5;
  --primary-hover: #FAFAFA;
  --primary-ink: #171717;

  --accent: #7CA0FF;
  --accent-hover: #A5BEFF;
  --accent-subtle: #16203A;

  /* On a dark ground the 500 carries text, so fill and ink converge. */
  --signal-good: #00BC7D;
  --signal-warn: #F0B100;
  --signal-bad: #FF6568;
  --signal-none: #525252;

  --signal-good-ink: #00BC7D;
  --signal-warn-ink: #F0B100;
  --signal-bad-ink: #FF6568;
  --signal-none-ink: #A1A1A1;

  --signal-good-wash: #06251C;
  --signal-warn-wash: #2A1F06;
  --signal-bad-wash: #2B0F12;

  --ring-hairline: 0 0 0 1px rgba(255, 255, 255, 0.10);
  --lift: 0 8px 24px rgba(0, 0, 0, 0.5);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --paper: #0A0A0A;
    --surface: #171717;
    --surface-sunken: #171717;
    --surface-muted: #262626;
    --ink: #FAFAFA;
    --ink-muted: #A1A1A1;
    --ink-faint: #737373;
    --rule: rgba(255, 255, 255, 0.10);
    --rule-strong: rgba(255, 255, 255, 0.15);

    --primary: #E5E5E5;
    --primary-hover: #FAFAFA;
    --primary-ink: #171717;

    --accent: #7CA0FF;
    --accent-hover: #A5BEFF;
    --accent-subtle: #16203A;

    /* On a dark ground the 500 carries text, so fill and ink converge. */
    --signal-good: #00BC7D;
    --signal-warn: #F0B100;
    --signal-bad: #FF6568;
    --signal-none: #525252;

    --signal-good-ink: #00BC7D;
    --signal-warn-ink: #F0B100;
    --signal-bad-ink: #FF6568;
    --signal-none-ink: #A1A1A1;

    --signal-good-wash: #06251C;
    --signal-warn-wash: #2A1F06;
    --signal-bad-wash: #2B0F12;

    --ring-hairline: 0 0 0 1px rgba(255, 255, 255, 0.10);
    --lift: 0 8px 24px rgba(0, 0, 0, 0.5);
  }
}

:root[data-density="compact"] {
  --row-h: 32px;
  --cell-pad-block: 4px;
}

/* ── base ─────────────────────────────────────────────────────────────── */

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 14px/20px var(--font-ui);
  -webkit-font-smoothing: antialiased;
}

h1 { font: 600 20px/28px var(--font-ui); margin: 0; }
h2 { font: 600 16px/24px var(--font-ui); margin: 0; }
h3 { font: 600 14px/20px var(--font-ui); margin: 0; }

a { color: var(--accent); }

/* Visible on every interactive element (14). Never removed without a
   replacement. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.icon {
  width: 16px;
  height: 16px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ── shell ────────────────────────────────────────────────────────────── */

.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100%;
  transition: grid-template-columns var(--motion) var(--ease);
}

:root[data-sidebar="rail"] .app { grid-template-columns: var(--sidebar-w-rail) minmax(0, 1fr); }

/* ── sidebar ──────────────────────────────────────────────────────────── */

.sidebar {
  position: sticky;
  inset-block-start: 0;
  align-self: start;
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s2);
  background: var(--surface-sunken);
  border-inline-end: 1px solid var(--rule);
  overflow: hidden;
  z-index: var(--z-drawer);
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--s2);
  height: var(--chrome-h);
  padding-inline: var(--s2);
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
  white-space: nowrap;
}

.sidebar-brand .mark {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: var(--radius-sm);
  background: var(--primary);
  color: var(--primary-ink);
  font: 600 11px/1 var(--font-mono);
  letter-spacing: 0.02em;
}

.sidebar .nav { display: flex; flex-direction: column; gap: var(--s3); overflow-y: auto; flex: 1; }
.sidebar .nav-group { display: flex; flex-direction: column; gap: 1px; }

.sidebar .nav-label {
  padding: var(--s2) var(--s2) var(--s1);
  font: 500 12px/16px var(--font-ui);
  color: var(--ink-faint);
  white-space: nowrap;
}

.sidebar .nav a {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 32px;
  padding: var(--s2);
  border-radius: var(--radius);
  color: var(--ink-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}

.sidebar .nav a:hover { background: var(--surface-muted); color: var(--ink); }

.sidebar .nav a[aria-current="page"] {
  background: var(--accent-subtle);
  color: var(--accent);
  font-weight: 500;
}

.sidebar-foot {
  border-block-start: 1px solid var(--rule);
  padding-block-start: var(--s2);
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: 44px;
}

.user-chip {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
  flex: 1;
  padding-inline: var(--s2);
}

.user-chip .mark {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 50%;
  background: var(--surface-muted);
  border: 1px solid var(--rule);
  color: var(--ink-muted);
  font: 600 11px/1 var(--font-ui);
  text-transform: uppercase;
}

.user-chip .who {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink);
}

/* The rail keeps the icons and drops the words. Section 14 requires an
   accessible name on an icon-only control, so the label is hidden visually
   and stays in the accessibility tree.

   .signout is deliberately absent from this list: this is the pre-Metronic
   shell (.sidebar/.nav/.user-chip render nothing in the current templates),
   kept only so nothing here regresses. The Metronic footer's sign-out form
   still carries the shared `.signout` class name, so this rule used to clip
   it to a 1x1px box with no :hover escape -- and unlike the Metronic
   minimize state, `:root[data-sidebar="rail"]` is restamped from
   localStorage on every load (abk-boot.js), so once collapsed once, sign-out
   stayed invisible on every later visit even when the sidebar looked
   expanded. The real, hover-aware treatment lives at #kt_app_sidebar_footer
   .signout below. */
:root[data-sidebar="rail"] .sidebar { padding-inline: var(--s1); }
:root[data-sidebar="rail"] .sidebar .nav a { justify-content: center; padding-inline: 0; }
:root[data-sidebar="rail"] .sidebar .nav a span,
:root[data-sidebar="rail"] .sidebar .nav-label,
:root[data-sidebar="rail"] .sidebar-brand span,
:root[data-sidebar="rail"] .user-chip .who {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
:root[data-sidebar="rail"] .sidebar-brand { justify-content: center; padding-inline: 0; }
:root[data-sidebar="rail"] .user-chip { justify-content: center; padding-inline: 0; }

/* ── chrome strip ─────────────────────────────────────────────────────── */

.chrome {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--s2);
  height: var(--chrome-h);
  padding-inline: var(--s4);
  background: var(--paper);
  border-block-end: 1px solid var(--rule);
}

.crumbs {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex: 1;
  min-width: 0;
  color: var(--ink-muted);
}

.crumbs .sep { color: var(--ink-faint); }
.crumbs strong { color: var(--ink); font-weight: 600; }

/* ── context bar ──────────────────────────────────────────────────────── */

/* Period and company are global state (4). A user must never have to guess
   which month a number belongs to, so the selectors live here rather than
   being repeated on every screen that reads them. */
.context {
  position: sticky;
  inset-block-start: var(--chrome-h);
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
  min-height: var(--context-h);
  padding: var(--s2) var(--s4);
  background: var(--surface);
  border-block-end: 1px solid var(--rule);
}

.context .field { flex-direction: row; align-items: center; gap: var(--s2); }

/* ── page ─────────────────────────────────────────────────────────────── */

.page {
  padding: var(--s6) var(--s6) var(--s16);
  max-width: var(--container-app);
  margin-inline: auto;
}

.page-head {
  display: flex;
  align-items: start;
  gap: var(--s4);
  flex-wrap: wrap;
  margin-block-end: var(--s6);
}

.page-head .titles { flex: 1; min-width: 260px; }
.page-head .actions { display: flex; gap: var(--s2); flex-wrap: wrap; }

/* Prose is capped; tables are not -- they earn the full viewport (4). */
.lede { color: var(--ink-muted); margin: var(--s1) 0 0; max-width: 72ch; }

/* ── card ─────────────────────────────────────────────────────────────── */

.card {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--ring-hairline);
  margin-block-end: var(--s4);
  overflow: hidden;
}

.card-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  border-block-end: 1px solid var(--rule);
}

.card-head .titles { flex: 1; min-width: 200px; }
.card-head .lede { font-size: 14px; }
.card-body { padding: var(--s4); }

/* A grid card's header row (filters, or a title with its toolbar) sits on
   Metronic's 70px minimum height plus the page's pt-6, which centred a 44px
   control in an empty band above it. The row is as tall as what it holds:
   20px above it, 12px down to the table it filters, so the filters read as
   part of the grid rather than floating over it. The side padding stays
   Metronic's. The selector outranks Metronic's `.card .card-header` and its
   `!important` .pt-6, which loads after this file. */
.card > .card-header.pt-6 {
  min-height: 0;
  align-items: center;
  row-gap: 8px;
  padding-top: 20px !important;
  padding-bottom: 12px;
}
/* Metronic's own margins on the title and toolbar (and the pages' my-1 on
   the search box) stacked another ~10px on each side; the row-gap above is
   what spaces them instead once a narrow screen wraps the toolbar. */
.card > .card-header.pt-6 > .card-title,
.card > .card-header.pt-6 > .card-toolbar { margin-block: 0; }
.card > .card-header.pt-6 .my-1 { margin-block: 0 !important; }
/* A grid's filter bar (.grid-filters): the search box and every filter read
   left to right from the start edge; Reset alone sits at the end edge. The
   toolbar takes the remaining width instead of Metronic's push to the end,
   and Reset's auto margin is what moves it there (logical, so it mirrors in
   Arabic). */
.card > .card-header.grid-filters { flex-wrap: wrap; column-gap: 8px; }
.card > .card-header.grid-filters > .card-title { flex: 0 0 auto; }
.card > .card-header.grid-filters > .card-toolbar { flex: 1 1 auto; margin-inline-start: 0; }
.card > .card-header.grid-filters > .card-toolbar > .d-flex { width: 100%; justify-content: flex-start !important; }
.card > .card-header.grid-filters [data-filter-reset] { margin-inline-start: auto; }

/* On a phone the filters stack full width under the search instead of
   hugging the right edge at whatever width each happens to be. */
@media (max-width: 575.98px) {
  .card > .card-header.pt-6 > .card-title,
  .card > .card-header.pt-6 > .card-toolbar { flex: 1 1 100%; }
  .card > .card-header.pt-6 > .card-title > *,
  .card > .card-header.pt-6 > .card-title input,
  .card > .card-header.pt-6 > .card-toolbar > .d-flex { width: 100% !important; }
  .card > .card-header.pt-6 > .card-toolbar > .d-flex > :not(label):not(.visually-hidden),
  .card > .card-header.pt-6 > .card-toolbar > .d-flex .select2-container { flex: 1 1 100%; width: 100% !important; }
}

.toolbar {
  display: flex;
  align-items: end;
  gap: var(--s2);
  flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  border-block-end: 1px solid var(--rule);
}

/* A toolbar of controls that is not sitting above a table wants the grouping
   without the sunken band. */
.toolbar.bare { border: 0; padding: 0; }

/* ── form grid ────────────────────────────────────────────────────────── */

/* `.toolbar` is a flex row built for a search box and a button above a table.
   The setup forms grew to eleven fields inside one, and `align-items: end` plus
   `flex-wrap` then places controls by their own height rather than on a grid:
   measured on the charge form, eleven controls landed on five ragged rows
   starting at four different x positions, with widths from 182px to 443px, and
   the first field in the DOM rendered on the *second* visual line. Tab order
   stopped matching reading order, which is a §14 failure and not a cosmetic one.

   A form is a grid. Columns are equal, every row starts at the same edge, and
   auto-placement follows document order -- so what a keyboard reaches next is
   what the eye reads next, by construction rather than by luck. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s4) var(--s3);
  align-items: start;
  padding: var(--s3) var(--s4);
}

/* Unlike `.toolbar`, a form grid is not a band above a table, so it carries no
   rule of its own; the card it sits in already draws the edge. */
.form-grid.banded { border-block-end: 1px solid var(--rule); }

/* Controls fill their cell. Several inputs still carry a `size` attribute from
   the flex layout, where it was the only thing giving them a sensible width;
   in a grid it fights the column, so the column wins. */
.form-grid > .field { inline-size: auto; }
.form-grid > .field > input,
.form-grid > .field > select,
.form-grid > .field > textarea { inline-size: 100%; }

/* Labels are 12px/16px. Reserving that one line keeps the controls on a row
   aligned when a neighbouring label happens to be short enough not to wrap. */
.form-grid > .field > .label { min-block-size: 16px; }

/* A field whose value is prose -- a reason, a legal name -- reads badly in a
   220px column. `reason` measured 219px wide and floated mid-canvas; these give
   the long ones room without breaking the column rhythm. */
.form-grid > .field.span-2 { grid-column: span 2; }
.form-grid > .field.span-all { grid-column: 1 / -1; }

@media (max-width: 720px) {
  /* One column: a span is the whole width either way, and a 2-column span in a
     2-column grid is a full-width row that looks like a mistake. */
  .form-grid { grid-template-columns: minmax(0, 1fr); }
  .form-grid > .field.span-2 { grid-column: auto; }
}

/* The registration wizard's form sits beside a step rail rather than above a
   table, so an auto-fit multi-column grid there gets whatever sliver of width
   is left over -- on the Tranches step that measured under 20px per column and
   wrapped "Period" one letter per line. One field per row removes the
   arithmetic entirely, and the wider gap and roomier hint give each answer its
   own reading line instead of packing the step tight. */
#rg-pane .form-grid {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s6) 0;
  max-width: 640px;
}
#rg-pane .form-grid > .field.span-2,
#rg-pane .form-grid > .field.span-all { grid-column: auto; }
#rg-pane .form-grid > .field { gap: var(--s2); }
#rg-pane .form-grid > .field > .hint,
#rg-pane .form-grid > .form-note { margin-block-start: var(--s1); }
#rg-pane > .hint { margin-block-start: var(--s4); }

/* style.bundle.css sets `[data-kt-stepper-element=content].current` to
   `display: flex` with the default row direction, so every direct child of a
   step pane lays out side by side -- the GL step's "Reading" select next to
   its dropzone/manual block, same on Tranches. KYC only dodged it by luck
   (its two paragraphs happen to wrap). One column matches every other step,
   whose content is a single `.form-grid`. */
#rg-pane > [data-kt-stepper-element="content"].current {
  flex-direction: column;
  gap: var(--s6);
}

/* The register wizard's alert reuses Metronic's own dialog-style alert
   (icon, title, dashed separator, content, buttons) instead of the app's
   `.alert` strip -- ABK.alertCard builds the markup, #error only toggles it.
   `.alert` below already sets `display: none`; `[data-open]` says which
   state applies, and the inner Metronic classes (`d-flex flex-center
   flex-column`) do the rest once shown, so nothing here fights `html
   [hidden]` (§1). */
#error.alert {
  display: none;
  border: 0;
  margin-block-end: 0;
  padding: 0;
  background: none;
}
#error.alert[data-open] { display: block; }
#error .rg-alert-issues { text-align: start; margin-block-end: 0; padding-inline-start: var(--s5); }
#error .rg-alert-issues li { margin-block-end: var(--s1); }
#error .rg-alert-fix { font-weight: 600; }
#error .rg-alert-stats .badge { font-family: var(--font-mono); }

/* ── job panel (`ABK.jobPanel`) ──────────────────────────────────────────
   Every queued write renders the same status/progress/log block. The log
   is a management command's captured stdout+stderr: useful detail (which row
   failed, and why), but plain <pre> renders it as a wall of black-on-white
   text with no separation from the badge above it. A sunken, scrollable
   panel with breathing room reads as "supporting detail" rather than as an
   error dump, and caps how much vertical space one failure can take over. */
[data-job-status] { margin: 0 0 var(--s4); }
[data-job-log] {
  margin: var(--s3) 0 0;
  padding: var(--s4);
  background: var(--surface-sunken);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  line-height: 1.7;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 320px;
  overflow-y: auto;
}

/* A FAILED/REJECTED job's own output is the reason a write did not happen --
   the same fact ABK.showError's alert states beside it -- so it gets the
   same Metronic danger language instead of the neutral "supporting detail"
   treatment every other status keeps. */
[data-job-log].is-failed {
  background-color: var(--bs-danger-light);
  border-color: transparent;
  color: var(--ink);
}

/* Actions get their own row rather than a cell. In a cell they would top-align
   with the labels beside them, which reads as a button that has come loose. */
.form-actions {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
}

/* Notes and blocker lists belong to the whole form, not to the last column. */
.form-grid > .form-note,
.form-grid > .form-blockers { grid-column: 1 / -1; margin: 0; }
.form-grid > .form-blockers { padding-inline-start: var(--s4); }

/* Tables scroll inside their own container; the page body never scrolls
   horizontally (4). */
.scroll { overflow-x: auto; }

/* ── tables ───────────────────────────────────────────────────────────── */

/* The table is the primary component of this product (6). */
table { width: 100%; border-collapse: separate; border-spacing: 0; }

caption {
  text-align: start;
  padding: var(--s3) var(--s4);
  color: var(--ink-muted);
}

th, td {
  padding: var(--cell-pad-block) var(--cell-pad-inline);
  text-align: start;
  border-block-end: 1px solid var(--rule);
  height: var(--row-h);
}

thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: 2;
  background: var(--surface);
  color: var(--ink);
  font: 500 14px/20px var(--font-ui);
  white-space: nowrap;
}

/* Rounded header band (6). The vendor paints `thead.bg-light` square and
 * edge to edge; the radius lives on the corner cells instead -- a radius on
 * the row group itself cannot clip, and the cells must carry the background
 * anyway: Bootstrap's cell rule beats the `background` above, so sticky
 * headers would otherwise let scrolled rows ghost through transparent
 * cells. Scoped to `.bg-light` so no other thead is touched; the `fw-bold`
 * utility (600) beats the 500 in the `thead th` rule above. The tracking
 * is the companion to small uppercase headers: without it they read
 * cramped. Logical properties only (13). */
thead.bg-light th {
  background-color: var(--bs-light);
  letter-spacing: 0.025em;
}

thead.bg-light > tr:first-child > th:first-child {
  border-start-start-radius: var(--radius-sm);
  border-end-start-radius: var(--radius-sm);
}

thead.bg-light > tr:first-child > th:last-child {
  border-start-end-radius: var(--radius-sm);
  border-end-end-radius: var(--radius-sm);
}

/* Sortable column headers (6.2). Only headers carrying `data-sort` -- set by
 * ABK.sortable for columns that opted in with `sort:` -- take the pointer
 * and the keyboard focus; the rest are inert text. The chevrons reuse the
 * vendor's own mask glyphs, so they draw identically to DataTables', in the
 * header's own colour. Mask positioning is direction-neutral; logical
 * properties only (13). */
th[data-sort] { cursor: pointer; }

/* A customer row opens its profile; the number in its first cell stays the
   keyboard's link (parties/customers.html, the workspace Customers tab). */
.abk-row-links tbody tr { cursor: pointer; }
th[data-sort]:hover { color: var(--bs-primary); }

th[aria-sort="ascending"]::after,
th[aria-sort="descending"]::after {
  content: "";
  display: inline-block;
  inline-size: 0.6rem;
  block-size: 0.6rem;
  margin-inline-start: 0.35rem;
  background-color: currentColor;
  mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
}

th[aria-sort="ascending"]::after {
  mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6'%3e%3cpath d='M3.23571 2.72011L4.97917 4.46358C5.15176 4.63618 5.43158 4.63617 5.60417 4.46358C5.77676 4.29099 5.77676 4.01118 5.60417 3.83861L3.29463 1.52904C3.13192 1.36629 2.86809 1.36629 2.70538 1.52904L0.395812 3.83861C0.22325 4.01117 0.22325 4.29099 0.395812 4.46358C0.568437 4.63617 0.84825 4.63617 1.02081 4.46358L2.76429 2.72011C2.89446 2.58994 3.10554 2.58994 3.23571 2.72011Z'/%3e%3c/svg%3e");
  -webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6'%3e%3cpath d='M3.23571 2.72011L4.97917 4.46358C5.15176 4.63618 5.43158 4.63617 5.60417 4.46358C5.77676 4.29099 5.77676 4.01118 5.60417 3.83861L3.29463 1.52904C3.13192 1.36629 2.86809 1.36629 2.70538 1.52904L0.395812 3.83861C0.22325 4.01117 0.22325 4.29099 0.395812 4.46358C0.568437 4.63617 0.84825 4.63617 1.02081 4.46358L2.76429 2.72011C2.89446 2.58994 3.10554 2.58994 3.23571 2.72011Z'/%3e%3c/svg%3e");
}

th[aria-sort="descending"]::after {
  mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6'%3e%3cpath d='M2.76429 3.27989L1.02083 1.53642C0.848244 1.36382 0.568419 1.36383 0.395831 1.53642C0.223244 1.70901 0.223244 1.98882 0.395831 2.16139L2.70537 4.47096C2.86808 4.63371 3.13191 4.63371 3.29462 4.47096L5.60419 2.16139C5.77675 1.98883 5.77675 1.70901 5.60419 1.53642C5.43156 1.36383 5.15175 1.36383 4.97919 1.53642L3.23571 3.27989C3.10554 3.41006 2.89446 3.41006 2.76429 3.27989Z'/%3e%3c/svg%3e");
  -webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 6'%3e%3cpath d='M2.76429 3.27989L1.02083 1.53642C0.848244 1.36382 0.568419 1.36383 0.395831 1.53642C0.223244 1.70901 0.223244 1.98882 0.395831 2.16139L2.70537 4.47096C2.86808 4.63371 3.13191 4.63371 3.29462 4.47096L5.60419 2.16139C5.77675 1.98883 5.77675 1.70901 5.60419 1.53642C5.43156 1.36383 5.15175 1.36383 4.97919 1.53642L3.23571 3.27989C3.10554 3.41006 2.89446 3.41006 2.76429 3.27989Z'/%3e%3c/svg%3e");
}

/* Edge insets (6). The header band reads as a panel only if its first and
 * last titles stand off the band's edges; the body cells take the same
 * insets or the columns drift apart. The centred empty state is exempt --
 * it spans the row and must stay centred.
 *
 * The `table.table` prefix is load-bearing: the vendor zeroes first- and
 * last-cell padding on borderless tables
 * (`.table:not(.table-bordered) td:first-child`, 0,3,1 and the `:last-child`
 * twin), which beats a bare element selector and is exactly the flush edges
 * being fixed. Logical properties only (13), so RTL flips for free; margin
 * on a table cell is ignored, so the margin-end breathing room is carried
 * as `padding-inline-*`, matching Metronic's own inset idiom. */
thead.bg-light > tr:first-child > th:first-child,
table.table > tbody > tr > td:first-child:not(.empty) {
  padding-inline-start: var(--s6);
}

thead.bg-light > tr:first-child > th:last-child,
table.table > tbody > tr > td:last-child:not(.empty) {
  padding-inline-end: var(--s6);
}

tbody td { font-size: 14px; line-height: 20px; }

/* No zebra striping. It fights the density toggle and adds noise to columns
   that are already aligned (6). */
tbody tr:hover td { background: var(--surface-muted); }
tbody tr:last-child td { border-block-end: 0; }

tbody tr[aria-selected="true"] td { background: var(--accent-subtle); }
tbody tr[aria-selected="true"] td:first-child { box-shadow: inset 2px 0 0 var(--accent); }

/* The first column is the identity and stays put when the table scrolls
   sideways (6). */
.sticky-id td:first-child,
.sticky-id th:first-child {
  position: sticky;
  inset-inline-start: 0;
  z-index: 1;
  background: var(--surface);
  border-inline-end: 1px solid var(--rule);
}

.sticky-id thead th:first-child { z-index: 3; background: var(--surface); }
.sticky-id tbody tr:hover td:first-child { background: var(--surface-muted); }

/* Money and rates are compared down the column, so they are end-aligned and
   tabular: proportional digits make 1,111 look narrower than 8,888 (5.1). */
td.num, th.num, .mono {
  text-align: end;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

th.num { text-align: end; }
tfoot td, tfoot th { font-weight: 500; background: var(--surface-muted); }

/* Identifiers: loan and invoice numbers, GL codes, hashes, UUIDs. Isolated
   from the bidi algorithm, because a period label reordered by it is a wrong
   period label (5.5, 13). */
.ident {
  font-family: var(--font-mono);
  font-size: 13px;
  direction: ltr;
  unicode-bidi: isolate;
  white-space: nowrap;
}

/* Several chips or identifiers in one cell. Without the gap they run
   together into one unreadable token -- ACAMANABABTAIN reads as a company
   code, and it is three. */
.chips { display: inline-flex; flex-wrap: wrap; gap: var(--s1); }

/* A timestamp is one value. Wrapped across four lines in a narrow column it
   is unreadable, and the zone -- which 5.4 requires it to carry -- ends up
   orphaned on a line of its own. */
.stamp { white-space: nowrap; }

button.ident {
  border: 0;
  background: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}

/* Absence is not zero (Principle 1). A null money cell is a dash in the
   absence colour, never 0.000, never blank. */
.absent { color: var(--signal-none-ink); font-family: var(--font-mono); }

/* The only cells where the number itself carries signal colour (5.2). Inside
   the band the variance is explained, not tolerated, so it stays ink. */
.variance.out { color: var(--signal-bad-ink); }
.variance.in { color: var(--ink); }

td.empty { padding: var(--s12) var(--s4); border-block-end: 0; text-align: center; }

/* Empty state names why it is empty and what to do -- never a bare
   "No data" (6). Centred like Metronic's dataTables_empty, and -- unlike a
   naively centred row -- never stranded off-screen: .table-responsive is a
   sized container, so the state sizes itself to exactly the visible width
   (100cqw) and sticks to the scroll origin. Centring across the full table
   width instead would put the explanation behind a horizontal scroll on
   exactly the wide tables that most need it. */
.table-responsive { container-type: inline-size; }

.empty-state {
  position: sticky;
  inset-inline-start: 0;
  display: grid;
  justify-items: center;
  gap: var(--s2);
  text-align: center;
  inline-size: 100cqw;
  padding-inline: var(--s4);
}
/* Readable line length on ultra-wide viewports; the block itself stays
   full scrollport width so the centring holds. */
.empty-state .reason,
.empty-state .next { max-width: 60ch; }
.empty-state .icon { width: 24px; height: 24px; color: var(--ink-faint); }
.empty-state .reason { color: var(--ink); font-weight: 600; }
.empty-state .next { color: var(--ink-muted); }

/* Container query units need Chrome 105 / Safari 16 / Firefox 110. Older
   than that, the state falls back to the visible left edge rather than
   centring -- always readable, never off-screen. */
@supports not (inline-size: 100cqw) {
  .empty-state {
    inline-size: auto;
    justify-items: start;
    text-align: start;
  }
}

/* Loading is skeleton rows at the current density -- neutral bars, and never
   over a money cell, because a fake number on a bank screen is worse than a
   blank one (5.1, 6). */
.skeleton {
  display: block;
  height: 10px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
}

/* ── pagination ───────────────────────────────────────────────────────── */

/* Forward via the API's next cursor, back via a client-held stack. The
   control reads Previous / Next and nothing else: no totals, no page
   numbers, no "of N" (6.1). The pager is the card footer for the table
   above it (border, padding, end-aligned); the control itself is the
   Metronic DataTables pagination -- div.dataTables_paginate holding a
   ul.pagination of li.page-item.previous / .next with .page-link
   controls -- built by ABK.paginate, so its shape follows
   style.bundle.css rather than the button rules below. */
/* The pager is the card footer for the table above it (border, padding);
 * the inner DataTables .row carries the two columns (length select + info
 * line, then the numbered buttons), so this stays out of their layout. */
.pager {
  padding: var(--s3) var(--s4);
  border-block-start: 1px solid var(--rule);
}

.pager .pagination { margin: 0; }

/* The length select sizes to its content, like the vendor demo: a full-width
 * select would push the info line onto its own row. */
.pager .dt-toolbar select {
  inline-size: auto;
  flex: none;
}

/* The global button rule below would otherwise restyle every .page-link
   (32px minimums, primary fill, medium weight, halved opacity when
   disabled). These undo exactly those properties so the Metronic
   pagination tokens -- colour, padding, active fill, disabled grey --
   resolve, while a number stays centred on one line. */
.pager .page-link {
  min-height: 0;
  min-width: 0;
  min-block-size: 0;
  min-inline-size: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  font-weight: 400;
}

.pager .page-link[disabled] {
  opacity: 1;
  background-color: transparent;
  border-color: transparent;
  color: var(--bs-pagination-disabled-color);
  cursor: default;
}

/* ── status ───────────────────────────────────────────────────────────── */

/* The default badge is a dot plus a word (7). The word carries the meaning
   and the dot only reinforces it, so "never colour alone" holds (14). */
.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  color: var(--ink);
  white-space: nowrap;
}

.tag::before {
  content: "";
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: 50%;
  background: var(--signal-none);
}

/* ABK.badge still builds a leading icon for every status. In the dot form the
   dot is the marker, so the icon is suppressed rather than shown alongside it
   -- two markers on one three-word label is noise. The filled form keeps it,
   because that is the one that has to be findable at a glance (7). */
.tag .icon { display: none; }

.tag.good::before { background: var(--signal-good); }
.tag.warn::before { background: var(--signal-warn); }
.tag.accent::before { background: var(--accent); }

/* Absence renders as a hollow ring: structurally different from the other
   three, and it must never read as good or bad (2.3, Principle 1). */
.tag.none::before {
  background: none;
  box-shadow: inset 0 0 0 1px var(--signal-none);
}

/* The filled form, for the states that must survive out of context: FAILED,
   REJECTED, MOVED, and a DIFF outside its band. A user scanning a job list
   must not have to read carefully to find the failure (7). */
.tag.bad {
  padding: 2px var(--s2);
  border-radius: var(--radius);
  background: var(--signal-bad-wash);
  color: var(--signal-bad-ink);
  font-size: 12px;
  line-height: 16px;
}

.tag.bad::before { background: var(--signal-bad); }
.tag.bad .icon { display: inline-block; width: 12px; height: 12px; color: currentColor; }

/* is_poci, is_non_accrual, is_derived. A derived collection is not an
   observed one, and the difference must survive a glance (7). */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  padding: 1px var(--s2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  color: var(--ink-muted);
  font: 500 12px/16px var(--font-ui);
  white-space: nowrap;
}

.chip.stage-1 { color: var(--signal-good-ink); border-color: currentColor; }
.chip.stage-2 { color: var(--signal-warn-ink); border-color: currentColor; }
.chip.stage-3 { color: var(--signal-bad-ink); border-color: currentColor; }

/* Batch progression is a pipeline and should look like one, not six separate
   badges (7). */
/* One line, always. The table scrolls inside its own container (4); a
   pipeline broken across five rows stops reading as a pipeline.
   :not(.stepper-pills) is load-bearing, not style: "stepper" also names
   Metronic's unrelated wizard-nav component (register.html's #rg-stepper,
   the operation pages' #op-steps, both "stepper stepper-pills"), and this
   rule's display:flex/align-items:center/nowrap was leaking onto that
   component's outer container -- register.html happened to survive it only
   because #rg-stepper carries its own higher-specificity ID override;
   #run-stepper carries none, so operations.html's "Run a step" list was
   forced into a single crushed row instead of Metronic's own vertical
   stepper-nav layout, and the two-word class collision does not deserve
   its own comment stripped by the next person who touches this file. */
.stepper:not(.stepper-pills) { display: flex; align-items: center; gap: var(--s1); flex-wrap: nowrap; }

/* A loose group of chips in a cell, which may wrap. */
.chips { display: flex; align-items: center; gap: var(--s1); flex-wrap: wrap; }
.stepper:not(.stepper-pills) .step {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  padding: 2px var(--s2);
  border-radius: var(--radius);
  border: 1px solid var(--rule-strong);
  color: var(--ink-faint);
  font: 400 12px/16px var(--font-ui);
  white-space: nowrap;
}
.stepper:not(.stepper-pills) .step[data-state="done"] { color: var(--signal-good-ink); border-color: currentColor; }
.stepper:not(.stepper-pills) .step[data-state="current"] { color: var(--ink); border-color: var(--ink); font-weight: 500; }
.stepper:not(.stepper-pills) .step[data-state="failed"] { color: var(--signal-bad-ink); border-color: currentColor; }
.stepper:not(.stepper-pills) .arrow { color: var(--ink-faint); }

/* Two or three controls sharing a table cell. Distinct from .stepper, which
   is a pipeline and is meant to wrap. */
.row-actions { display: flex; gap: var(--s2); align-items: center; }

/* ── controls ─────────────────────────────────────────────────────────── */

input, select, textarea, button { font: inherit; color: inherit; }

/* The browser's own [hidden] rule sets display:none, and any author rule
   setting display beats it -- so `.field` below made every
   `<label class="field" hidden>` in the application visible. The provisional
   period field on the onboarding wizard has been showing for a company whose
   tranches are loaded from a purchase history since it was written. State it
   once, here, so `el.hidden` means hidden whatever a component's display is. */
[hidden] { display: none !important; }

/* ...whatever an !important display utility says. Bootstrap's display
   utilities (`.d-flex`, `.d-block`, ...) are `!important` and Metronic's
   style.bundle.css loads AFTER this file, so on equal specificity (a lone
   class vs a lone attribute) the vendor rule wins by source order and a
   `hidden` element still renders. That is how Setup → Companies showed its
   whole detail panel -- with live, enabled onboarding buttons -- while no
   company was chosen, and recording KYC posted to `companies//kyc/data/`.
   `html [hidden]` outranks any single-class utility by specificity, so it
   holds regardless of load order. (A multi-class display utility could still
   outrank it; none exists in the vendor bundles, and templates must not
   combine `hidden` with display utilities and rely on it.) */
html [hidden] { display: none !important; }

.field { display: inline-flex; flex-direction: column; gap: var(--s1); min-width: 0; }

.field > .label,
label > .label {
  font-size: 12px;
  line-height: 16px;
  color: var(--ink-muted);
}

.req { color: var(--signal-bad-ink); }

input, select, textarea {
  min-height: 32px;
  padding: var(--s1) var(--s3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
}

input::placeholder, textarea::placeholder { color: var(--ink-faint); }

/* Checkboxes, radios and switches are Metronic's `.form-check-input`, which
   sizes them itself (1.75rem square, or the .form-check-sm/-lg sizes) but
   sets no min-height or padding -- so the rule above stretched every one of
   them to 32px tall and padded it, and a 24px radio became an oval. Undo
   exactly those two, and never let a flex row squeeze one narrower. */
.form-check-input {
  min-height: 0;
  min-block-size: 0;
  padding: 0;
  flex-shrink: 0;
}

/* Radios: a true circle, and on the solid (filled grey) variant a visible
   ring while unchecked, so an unchosen option still reads as a control on a
   light card. Checked keeps Metronic's primary fill and its white centre
   dot (half the diameter), which reads clearly once the circle is round.
   `html` lifts these over the vendor's multi-class rules, which load after
   this file. Checkboxes keep the vendor's rounded square; switches
   (`.form-switch`, always checkboxes) are left alone. */
html .form-check-input[type="radio"] {
  aspect-ratio: 1 / 1;
  border-radius: 50%;
}
html .form-check-solid .form-check-input[type="radio"]:not(:checked) {
  box-shadow: inset 0 0 0 1px var(--bs-gray-400);
}
html .form-check:not(.form-switch) .form-check-input[type="checkbox"] {
  aspect-ratio: 1 / 1;
  border-radius: 0.45em;
}

/* The browser's own file widget is the one control the design system does not
   reach. It is given the button treatment below rather than left native. */
input[type="file"] { padding-block: var(--s1); }

input[type="file"]::file-selector-button {
  margin-inline-end: var(--s3);
  min-height: 24px;
  padding: var(--s1) var(--s3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}

input[type="file"]::file-selector-button:hover { border-color: var(--ink-muted); }

/* ── single-file dropzones ─────────────────────────────────────────────
   The Metronic queue dressing (dropzone-queue, Attach button, one item row)
   over a deferred upload. The vendor bundle already paints .dropzone itself;
   this only adds what the queue variant needs on top: a visible drop target
   state, a pointer over the message, truncation for long workbook names, and
   the error colour. There is deliberately no progress bar: nothing uploads
   until the page's own button queues the job, and the job panel is the
   progress surface. Only existing tokens are referenced, so both themes
   resolve every name used here. */
.dropzone-queue .dz-message { cursor: pointer; }
.dropzone-queue[data-dragover="true"] { outline: 2px dashed var(--primary); outline-offset: 3px; }
.dropzone-queue .dropzone-select:focus-visible,
.dropzone-queue [data-dz-remove]:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.dropzone-queue .dropzone-item { inline-size: 100%; }
.dropzone-queue .dropzone-filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dropzone-queue .dropzone-error { color: var(--signal-bad-ink); }

input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--signal-bad);
}

.hint { font-size: 12px; line-height: 16px; color: var(--ink-muted); }
.hint.bad { color: var(--signal-bad-ink); }

/* Minimum target 24x24, including compact-density row controls (14). */
/* a.button is for navigation that belongs beside actions -- the readiness
   board's link to where a close is actually requested. It stays an anchor
   because it goes somewhere; it only looks like the buttons next to it. */
button,
a.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: 32px;
  min-width: 32px;
  padding: var(--s1) var(--s3);
  border: 1px solid var(--primary);
  border-radius: var(--radius);
  background: var(--primary);
  color: var(--primary-ink);
  font-weight: 500;
  cursor: pointer;
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}

button:hover,
a.button:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
a.button { text-decoration: none; }

button[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
  background: var(--primary);
  border-color: var(--primary);
}

button.secondary,
a.button.secondary { background: var(--surface); color: var(--ink); border-color: var(--rule); }
button.secondary:hover,
a.button.secondary:hover { background: var(--surface-muted); border-color: var(--rule-strong); }
button.secondary[disabled] { background: var(--surface); color: var(--ink-muted); }

/* Destructive actions use the danger signal and sit apart from the primary
   one (9). */
button.danger { background: var(--signal-bad); border-color: var(--signal-bad); color: #fff; }
button.danger:hover { filter: brightness(1.1); background: var(--signal-bad); }
button.danger[disabled] { background: var(--signal-bad); border-color: var(--signal-bad); }

button.link {
  background: none;
  border: 0;
  min-height: 24px;
  min-width: 0;
  padding: 0;
  color: var(--accent);
  font-weight: 400;
  text-decoration: underline;
}
button.link:hover { background: none; color: var(--accent-hover); }

button.icon-button {
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--ink-muted);
  padding: var(--s1);
}
button.icon-button:hover { background: var(--surface-muted); border-color: var(--rule); color: var(--ink); }
button.icon-button .icon { width: 18px; height: 18px; }

.signout { display: inline-flex; margin: 0; }

/* ── tabs ─────────────────────────────────────────────────────────────── */

.tabs {
  display: flex;
  gap: var(--s1);
  padding-inline: var(--s4);
  border-block-end: 1px solid var(--rule);
  overflow-x: auto;
}

.tabs button {
  background: none;
  border: 0;
  border-block-end: 2px solid transparent;
  border-radius: 0;
  color: var(--ink-muted);
  padding: var(--s3) var(--s2);
  white-space: nowrap;
}

.tabs button:hover { background: none; color: var(--ink); }
.tabs button[aria-selected="true"] { color: var(--ink); border-block-end-color: var(--ink); font-weight: 500; }

/* ── alerts ───────────────────────────────────────────────────────────── */

/* Errors the user must act on render inline at form level, never in a toast:
   a disappearing instruction is not an error message (8). */
.alert {
  display: none;
  gap: var(--s3);
  padding: var(--s3);
  margin-block-end: var(--s4);
  border: 1px solid var(--signal-bad);
  border-inline-start-width: 3px;
  border-radius: var(--radius);
  background: var(--signal-bad-wash);
  color: var(--ink);
}

.alert[data-open] { display: flex; }
.alert .icon { color: var(--signal-bad); margin-block-start: 2px; }
.alert .body { flex: 1; min-width: 0; }
.alert .detail { font-weight: 600; }

/* code, and the X-Request-ID behind it, is what makes a support call
   resolvable against the audit log (8). */
.alert .meta {
  margin-block-start: var(--s1);
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--ink-muted);
  overflow-wrap: anywhere;
}

/* 429 is a passive banner and never an error dialog (8). */
.alert.passive { border-color: var(--signal-warn); background: var(--signal-warn-wash); }
.alert.passive .icon { color: var(--signal-warn); }

.notice { color: var(--ink-muted); margin: var(--s2) 0; }

/* ── jobs ─────────────────────────────────────────────────────────────── */

/* A determinate bar wherever the API supplies progress. Never a spinner where
   a progress value exists (11, 17). */
.progress {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-muted);
  overflow: hidden;
}

.progress > span {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width var(--motion) var(--ease);
}

.log {
  margin: 0;
  padding: var(--s3);
  max-height: 240px;
  overflow: auto;
  background: var(--surface-muted);
  border-radius: var(--radius);
  font: 400 12px/18px var(--font-mono);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Inside a scrollable modal the dialog body scrolls; a second scrollbar on
   the log itself would trap the wheel. */
.log.log-full { max-height: none; overflow: visible; }

/* Long-text cells (ABK.detailCell -- the activity footer's Detail column,
   the Jobs page's Output column): a job's own output
   reads directly in the grid, up to DETAIL_PREVIEW_LIMIT, rather than behind
   a click into a toast. Past the limit the preview text itself is the only
   click target into the full-text modal -- no underline, no icon button, a
   screen reader announces it as a button either way. The smaller size sets
   the dense command output apart from the surrounding 14px grid text. */
.activity-detail-cell { display: flex; align-items: flex-start; gap: var(--s2); }
.activity-detail-text {
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  font-size: 12px;
  line-height: 18px;
}
.activity-detail-text-mono { font-family: var(--font-mono); }
.activity-detail-text-clickable {
  cursor: pointer;
}
.activity-detail-text-clickable:hover,
.activity-detail-text-clickable:focus-visible {
  color: var(--accent);
}

/* The compact activity footer (templates/_activity_footer.html). It sits
   under a page's own work, so it claims as little height as it can: a
   shorter header than a card's 70px default, small toolbar controls, and
   Detail / Reason cut to one line with "View" for the rest. The Audit log
   and Jobs pages keep the full-size grid -- they are where "View all" goes. */
.activity-footer .activity-footer-head { min-height: 56px; }
.activity-footer.is-collapsed .activity-footer-head { padding-bottom: var(--s3); }
/* A collapsed footer has nothing to filter. Not [hidden]: the d-flex
   utility is !important and would win over it. */
.activity-footer.is-collapsed .activity-filters { display: none !important; }
.activity-footer .activity-toggle i { transition: transform var(--motion) var(--ease); }
.activity-footer.is-collapsed .activity-toggle i { transform: rotate(-90deg); }
[dir="rtl"] .activity-footer.is-collapsed .activity-toggle i { transform: rotate(90deg); }
[dir="rtl"] .activity-footer .activity-view-all-arrow { transform: scaleX(-1); }
.activity-clamp-cell {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}
.activity-clamp {
  display: inline-block;
  max-inline-size: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: middle;
}
.activity-clamp-view { flex-shrink: 0; }

/* ── definition grid ──────────────────────────────────────────────────── */

.defs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--s4);
  margin: 0;
}

.defs > div { display: grid; gap: var(--s1); min-width: 0; }
.defs dt { font-size: 12px; color: var(--ink-muted); }
.defs dd { margin: 0; }

/* An identifier does not wrap mid-token anywhere else (5.5), because a period
   label broken across lines is a wrong period label. In a definition grid the
   value has a cell of its own and nothing to be confused with, and a long one
   -- an absolute export path -- otherwise overflows into the term beside it.
   Here, and only here, it wraps. */
.defs dd .ident { white-space: normal; overflow-wrap: anywhere; }

/* ── sign in ──────────────────────────────────────────────────────────── */

/* The sign-in page is its own design (static/css/login.css), loaded only by
   core/login.html and scoped under #abk-login. */

/* ── responsive ───────────────────────────────────────────────────────── */

/* Below 1024 the sidebar becomes an off-canvas drawer. It moves with a
   logical margin rather than a physical transform, so the direction flips
   with the document (13). */
@media (max-width: 1023px) {
  .app { grid-template-columns: minmax(0, 1fr); }
  :root[data-sidebar="rail"] .app { grid-template-columns: minmax(0, 1fr); }

  .sidebar {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    width: var(--sidebar-w);
    margin-inline-start: calc(-1 * var(--sidebar-w));
    transition: margin-inline-start var(--motion) var(--ease);
  }

  :root[data-drawer="open"] .sidebar { margin-inline-start: 0; }
  :root[data-sidebar="rail"] .sidebar { padding-inline: var(--s2); }
  :root[data-sidebar="rail"] .sidebar .nav a { justify-content: start; padding-inline: var(--s2); }
  :root[data-sidebar="rail"] .sidebar .nav a span,
  :root[data-sidebar="rail"] .sidebar .nav-label,
  :root[data-sidebar="rail"] .sidebar-brand span,
  :root[data-sidebar="rail"] .user-chip .who {
    position: static;
    width: auto;
    height: auto;
    clip-path: none;
  }
  :root[data-sidebar="rail"] .sidebar-brand,
  :root[data-sidebar="rail"] .user-chip { justify-content: start; padding-inline: var(--s2); }

  .page { padding-inline: var(--s4); }
  .chrome, .context { padding-inline: var(--s4); }
}

@media (min-width: 1024px) {
  .drawer-toggle { display: none; }
}

/* ── motion ───────────────────────────────────────────────────────────── */

/* Not optional (12). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── metronic alignment ───────────────────────────────────────────────
 * Setup → Companies renders inside the Metronic app-container, so it must
 * not re-introduce the legacy gutters: the centred 80rem .page plus its own
 * inline padding on top of container-fluid, and a .form-grid's own padding
 * inside a .card-body that already pads (content then sits twice-removed
 * from the card border).
 */
.card-body > .form-grid { padding: 0; }

/* Filter-bar Select2 (card headers such as Setup → Companies, plus the page
 * toolbar in base.html such as Period control): the controls
 * are a single row of peers -- search, status, model, actions -- that only
 * wraps on narrow screens. Select2 replaces each <select> with a
 * .select2-container; left at width:100% every filter takes its own line.
 * Auto width plus a content-sized minimum keeps the two dropdowns on one
 * line beside the search and the buttons. flex:none keeps them from growing
 * into full rows and from squeezing the buttons beside them; the parent's
 * flex-wrap still lets them wrap on small screens. .abk-filter-bar opts a
 * filter row outside a card header into the same rule (the Approvals inbox). */
.card-header .select2-container,
.card-toolbar .d-flex .select2-container,
.abk-filter-bar .select2-container,
#kt_app_toolbar_container .select2-container {
  inline-size: auto !important;
  width: auto !important;
  min-inline-size: 150px;
  max-inline-size: 100%;
  flex: none;
}

/* Period control toolbar action (docs/ui/metronic.md §4): the "Request
 * close" link is one inline row -- icon beside its short label -- never a
 * wrapped box. The visible label is shortened and the full wording rides as
 * a native tooltip (title) plus aria-label. nowrap keeps the label on a
 * single line, shrink-0 keeps the flex toolbar from squeezing it under the
 * period select, and the intrinsic auto width (capped at the container)
 * keeps it clear of overlap in both LTR and RTL. The utilities on the
 * markup carry this too; this rule is the backstop whatever else loads. */
#kt_app_toolbar_container #go-close {
  white-space: nowrap;
  flex-shrink: 0;
  inline-size: auto;
  width: auto;
  max-inline-size: 100%;
}

/* The sidebar footer: "Powered by neruos.tech" and the version. Sign out
 * left for the header's account menu (29 Oct meeting). Colours are the dark
 * sidebar's own (style.bundle.css, data-kt-app-layout=dark-sidebar): the
 * menu-heading grey for the words, the menu-link grey for the link, white on
 * hover, and the logo row's dashed rule above, so the footer reads as part of
 * the sidebar in either theme. */
#kt_app_sidebar_footer .sidebar-credit {
  padding-block-start: var(--s3);
  border-block-start: 1px dashed #1f212a;
  color: #636674;
  white-space: nowrap;
}
#kt_app_sidebar_footer .sidebar-credit a { color: #9a9cae; text-decoration: none; font-weight: 600; }
#kt_app_sidebar_footer .sidebar-credit a:hover,
#kt_app_sidebar_footer .sidebar-credit a:focus-visible { color: #f5f5f5; }
#kt_app_sidebar_footer .sidebar-version { flex-shrink: 0; font-variant-numeric: tabular-nums; }

/* On the minimized icon rail (~75px) the credit cannot fit; only the version
 * stays, centred. Scoped to :not(:hover) so the hoverable expand restores the
 * full row while hovering, as Demo1 does for its own footer action. */
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar_footer,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) #kt_app_sidebar_footer {
  padding-inline: 0;
}
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar_footer .sidebar-credit,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) #kt_app_sidebar_footer .sidebar-credit {
  justify-content: center !important;
}
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar_footer .sidebar-credit-text,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) #kt_app_sidebar_footer .sidebar-credit-text {
  display: none;
}

/* ── brand ────────────────────────────────────────────────────────────────
 * The near-black Demo1 sidebar carries the white ABK lockup directly (and
 * the white emblem alone on the minimized rail), so no light plate is
 * needed. Display is left to the vendor: .app-sidebar-logo-default /
 * -minimize swap on the minimized rail in style.bundle.css, and setting
 * display here would show both at once. Below lg the sidebar is a drawer
 * with no logo row, so the header carries the colour logo; the plate only
 * appears where the header is dark. */
.app-sidebar-brand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-block: 6px;
  padding-inline: 8px;
  border-radius: 10px;
  transition: background-color var(--motion) var(--ease);
}
.app-sidebar-brand:hover { background-color: rgb(255 255 255 / 0.06); }
.app-sidebar-brand:focus-visible { outline: 2px solid rgb(255 255 255 / 0.7); outline-offset: 2px; }
.app-sidebar-brand img { block-size: 42px; inline-size: auto; vertical-align: middle; }
/* A touch over the rail's 20px menu icons, so the emblem leads without
   outweighing them. */
.app-sidebar-brand .app-sidebar-logo-minimize { block-size: 24px; }
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar_logo,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) #kt_app_sidebar_logo {
  justify-content: center;
  padding-inline: 0 !important;
}
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar_logo .app-sidebar-brand,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) #kt_app_sidebar_logo .app-sidebar-brand {
  padding: 7px;
}

/* Collapsed to icons, the sidebar has no titles left to say where you are,
   and Metronic's active fill is a shade off the sidebar's own. So the current
   page's link -- or, for a page inside a section, that section's icon (the
   accordion Metronic marks `here`) -- gets a primary tint, a primary icon and
   an accent bar on its leading edge (inset-inline, so it mirrors in Arabic). */
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar .menu > .menu-item > .menu-link.active,
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar .menu > .menu-item.here > .menu-link,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) .menu > .menu-item > .menu-link.active,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) .menu > .menu-item.here > .menu-link {
  position: relative;
  background-color: rgba(var(--bs-primary-rgb), 0.18) !important;
}
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar .menu > .menu-item > .menu-link.active::before,
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar .menu > .menu-item.here > .menu-link::before,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) .menu > .menu-item > .menu-link.active::before,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) .menu > .menu-item.here > .menu-link::before {
  content: "";
  position: absolute;
  inset-block: 8px;
  inset-inline-start: 0;
  inline-size: 3px;
  border-radius: 3px;
  background-color: var(--bs-primary);
}
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar .menu > .menu-item > .menu-link.active .menu-icon i,
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar .menu > .menu-item.here > .menu-link .menu-icon i,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) .menu > .menu-item > .menu-link.active .menu-icon i,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) .menu > .menu-item.here > .menu-link .menu-icon i {
  color: var(--bs-primary) !important;
}
/* Collapsed, a link keeps its full expanded width and is only clipped by the
   75px rail, so its highlight ran into the rail's end edge. Sized to the
   rail less the 11px it already starts in by, the pill sits with the same
   margin on both sides. */
body[data-kt-app-sidebar-minimize="on"]:not([data-kt-app-sidebar-hoverable="true"]) #kt_app_sidebar .menu > .menu-item > .menu-link,
body[data-kt-app-sidebar-minimize="on"][data-kt-app-sidebar-hoverable="true"] #kt_app_sidebar:not(:hover) .menu > .menu-item > .menu-link {
  inline-size: calc(var(--bs-app-sidebar-width) - 22px);
}

.app-header-brand {
  display: inline-flex;
  align-items: center;
  border-radius: 8px;
}
.app-header-brand img { block-size: 30px; inline-size: auto; }
[data-bs-theme="dark"] .app-header-brand { padding-block: 4px; padding-inline: 8px; background: #fff; }

/* Select2 v4 renders its clear control as a <button type="button">. The
 * global button rule above (32px minimum, primary fill, padding, white text)
 * would otherwise turn that "x" into a large filled button. Undo every
 * property the global rule sets so only the Select2 theme's own "x" remains;
 * the theme keeps its own positioning and colours. Both physical and logical
 * minimums are reset (the global rule uses physical longhands). */
button.select2-selection__clear {
  display: block;
  min-height: 0;
  min-width: 0;
  min-block-size: 0;
  min-inline-size: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  background-color: transparent;
  color: inherit;
  font-weight: inherit;
  line-height: 1;
  cursor: pointer;
  transition: none;
}
button.select2-selection__clear:hover {
  background: transparent;
  background-color: transparent;
  border-color: transparent;
  color: inherit;
}

/* Toastr renders its dismiss control as a <button type="button"> carrying
 * .toastr-close-button. The vendor rule already undoes padding, background
 * and border; without this the global button rule's 32px minimums would
 * still stretch it into a large empty box around the × glyph. Both physical
 * and logical minimums are reset (the global rule uses physical longhands).
 * Position needs no override: ABK.notify uses the bundle's own
 * toastr-top-right-nav-margin slot, which already clears the fixed header.
 */
button.toastr-close-button {
  min-height: 0;
  min-width: 0;
  min-block-size: 0;
  min-inline-size: 0;
}

/* Inline validation on Select2 controls. Select2 hides the original <select>
 * (is-invalid on it paints nothing), so ABK.validator mirrors the state
 * onto the visible .select2-selection box. Colours are the theme's own
 * danger/success tokens, defined for light and dark by the vendor, so no
 * new colour is introduced here. */
select.is-invalid + .select2-container .select2-selection {
  border-color: var(--bs-danger);
}
select.is-valid + .select2-container .select2-selection {
  border-color: var(--bs-success);
}

/* Inline validation on dropzone controls. The file input is hidden behind
 * the dropzone box, so ABK.validator frames the box itself, in the same
 * theme tokens. */
.dropzone.is-invalid {
  border-color: var(--bs-danger);
}
.dropzone.is-valid {
  border-color: var(--bs-success);
}

/* Tab nav ([data-tabs]) is a single row, never a column. The legacy
 * `.sidebar .nav` rule above is column by design; without this block a
 * generic `.nav` column declaration would also match `ul.nav[data-tabs]`
 * (same specificity, source order decides) and stack every tab above the
 * next. The flex-direction here is the guarantee, whatever else loads.
 *
 * `nowrap` + horizontal scroll (not wrap) is what keeps the promise on
 * narrow screens too: wrapping would put tabs on a second row, which is
 * the bug being fixed.
 *
 * The `-2px` pulldown Metronic puts on `.nav-item` is neutralised below.
 * It exists so the active tab's border overlaps the bar's own bottom edge,
 * but `overflow-x: auto` makes the bar a scroll container, so those 2px
 * become vertical overflow: a stray vertical scrollbar with nothing to
 * scroll to. The bar's track stays invisible (`border-transparent`).
 *
 * The global button rule (32px minimums, primary fill, medium weight,
 * radius, 1px box border) would otherwise repaint each tab as a dark pill.
 * Like `.pager .page-link` above, this undoes exactly those properties so the
 * Metronic nav-line-tabs tokens -- grey text, transparent ground, primary
 * underline, bold via the ul's fw-bold -- resolve. The border reset is
 * load-bearing: without it the button's 1px box border survives wherever the
 * vendor `border: 0` loses the cascade, and the active underline below has
 * nothing clean to sit on. Padding is left to Metronic + the py-5 / me-10
 * utilities; font-weight inherits so the ul's fw-bold reaches the button.
 * Logical properties only (13). */
ul.nav[data-tabs] {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: flex-end;
  gap: 0;
  overflow-x: auto;
  padding-block-end: 2px;
  flex: none;
  list-style: none;
  margin-block-end: 0;
  padding-inline-start: 0;
}

[data-tabs] [data-tab] {
  min-height: 0;
  min-width: 0;
  min-block-size: 0;
  min-inline-size: 0;
  background: transparent;
  background-color: transparent;
  border: 0;
  border-radius: 0;
  font-weight: inherit;
  white-space: nowrap;
}

[data-tabs] [data-tab]:hover {
  background: transparent;
  background-color: transparent;
}

/* No pulldown inside our tab bars. Metronic's `.nav-item { margin-bottom:
 * -2px }` drags every tab 2px below the bar so its border can overlap the
 * bar's edge; inside a scroll container that overhang is a 2px vertical
 * overflow, i.e. a permanent vertical scrollbar. The overlap trick is dead
 * here anyway -- the indicator below sits inside the button -- so the
 * margin goes. Narrow screens still scroll horizontally via overflow-x. */
ul.nav[data-tabs] .nav-item {
  margin-bottom: 0;
}

/* The tab look is Metronic Demo1's current `nav-line-tabs nav-line-tabs-2x`,
 * stated here because the vendored style.bundle.css predates it: that build
 * greys an idle tab to gray-500 and gives hover no colour. Current Demo1:
 * gray-700 idle; primary text over a 2px primary rule on hover and when
 * selected; colour eased over .2s. The global button rule's centring goes
 * too -- an <a> tab starts its text, so a <button> tab does.
 *
 * The rule is a real border inside the button's own box. With the vendor's
 * -2px `.nav-item` pulldown gone (above) nothing hangs below the scroll
 * container, so nothing is clipped, and the idle tab carries the same 2px in
 * transparent so selecting one moves no text. The vendor's
 * `.nav-link:focus-visible` box-shadow ring is left untouched (14). */
ul.nav[data-tabs] [data-tab] {
  justify-content: flex-start;
  text-align: start;
  color: var(--bs-gray-700);
  border-block-end: 2px solid transparent;
  transition: color 0.2s ease;
}

ul.nav[data-tabs] [data-tab].active,
ul.nav[data-tabs] [data-tab][aria-selected="true"],
ul.nav[data-tabs] [data-tab]:hover:not(.disabled):not(:disabled) {
  color: var(--bs-primary);
  border-block-end-color: var(--bs-primary);
}

/* Registration wizard: the nav is horizontal (Metronic's own general-
 * stepper layout -- `.stepper.stepper-pills`, no `.stepper-column`
 * modifier -- lays an item's icon and label out as a row by default), and
 * wraps onto more than one row on narrow viewports rather than overflowing.
 * Each item keeps its natural width instead of stretching to fill the row.
 * Nav items and panes must each stay siblings under their own parent:
 * KTStepper marks current/completed by sibling index, and flattening them
 * would break the marking. */
#rg-stepper .stepper-item { flex: none; }

/* The step content reads as one centred column below the nav, narrower than
 * the now full-width card it sits in -- Metronic's own `w-lg-500px mx-auto`
 * convention for a wizard form, applied to whatever a step holds and not
 * only to its `.form-grid` (a bare paragraph on the KYC/Activation steps
 * gets the same reading width). */
#rg-pane { max-width: 640px; margin-inline: auto; width: 100%; }

/* Step numbers share one vertical line: a wrapping title or description
 * grows its item taller than its neighbours, and the icons drift off each
 * other's horizontal. Labels never wrap inside this rail, so every 40px
 * icon sits at the same height. */
#rg-stepper .stepper-label,
#rg-stepper .stepper-title,
#rg-stepper .stepper-desc { white-space: nowrap; }

/* Wizard rhythm: nav on top, inputs beneath it, actions pinned to the
 * bottom. Short panes (KYC is two paragraphs) would otherwise leave Back /
 * Continue floating right under the text, jumping up and down as steps
 * change. The column plus a minimum height keeps the footer in one
 * predictable place; margin-top auto does the pinning.
 *
 * :not([hidden]) is load-bearing, not style: the done pane swaps this
 * element hidden, and a bare #rg-stepper display rule would outrank the
 * html [hidden] backstop by specificity and leave both panes visible. */
#rg-stepper:not([hidden]) {
  display: flex;
  flex-direction: column;
  min-height: 480px;
}
#rg-stepper .rg-foot { margin-top: auto; }

/* The six steps of the month (api/_operation_steps.html #op-steps): a
 * horizontal Metronic stepper-pills nav of links, one page per step. Items
 * share the row and wrap onto a second line on a narrow screen instead of
 * overflowing it. Metronic spaces the icon with a physical margin-right; the
 * link's own gap spaces it instead, so the icon sits on the correct side in
 * RTL too. The state slot collapses until readiness fills it. */
#op-steps .stepper-item { flex: 1 1 11rem; min-width: 0; }
#op-steps .stepper-item .stepper-icon { margin-inline-end: 0; }
#op-steps .stepper-wrapper { color: inherit; }
#op-steps .stepper-title { margin-bottom: 0; }
#op-steps [data-op-state]:empty { display: none; }

@media print {
  .sidebar, .chrome, .context, .toolbar, .pager { display: none; }
  .app { grid-template-columns: minmax(0, 1fr); }
}

/* ── locale: Arabic (13) ────────────────────────────────────────────────
 * Inter has no Arabic sibling, so the locale swaps the family rather than a
 * face: Noto Sans Arabic for the UI, IBM Plex Mono untouched so money columns
 * align identically in both locales. Numerals stay Western Arabic (0-9):
 * Arabic-Indic is a user preference, never a locale default. The stack after
 * Noto names the platform Arabic grotesques, so the page renders correctly
 * even before the webfont arrives or where it is blocked.
 *
 * Directional icons (arrows, chevrons, exit/entry) mirror via .flip-rtl;
 * meaningful icons (padlock, warning, check, upload) never take the class.
 * The transform is transition-free so a language switch does not animate. */

:root:lang(ar) {
  --font-ui: "Noto Sans Arabic", "Inter", system-ui, -apple-system,
    "Segoe UI", Tahoma, Arial, sans-serif;
  /* Metronic sets everything but headings in Inter, Helvetica: no Arabic
     glyphs, so the OS fell back to its own Arabic face beside Noto's
     headings. Point Bootstrap's family (body, buttons, inputs, labels) at
     the locale's, so the page reads in one face. */
  --bs-font-sans-serif: var(--font-ui);
  --bs-body-font-family: var(--font-ui);
}
/* The same stack also written literally on body/html and on chart text. */
:root:lang(ar),
:lang(ar) body { font-family: var(--font-ui); }
:lang(ar) .apexcharts-text,
:lang(ar) .apexcharts-title-text,
:lang(ar) .apexcharts-legend-text { font-family: var(--font-ui) !important; }

:lang(ar) body {
  /* Arabic glyphs run smaller than Latin at the same px: a one-step
     line-height pass so 14px Arabic does not clip or crowd. */
  line-height: 1.7;
}

[dir="rtl"] .flip-rtl {
  transform: scaleX(-1);
  transition: none;
}

/* DataTables pager chevrons (DESIGN.md 13). The vendor draws .previous /
 * .next as SVG mask-images, and a mask has no direction for rtlcss to flip:
 * the RTL bundle ships the same left-pointing "previous" chevron, so the
 * pager reads backwards. Mirroring the glyph (not swapping the buttons:
 * DOM order and aria-labels already localise) restores previous-points-to-
 * start. The vendor sets display:block on both, so the transform applies.
 * Specificity (attribute + two classes) outranks the vendor's two classes
 * regardless of bundle order. */
[dir="rtl"] .page-item .page-link .previous,
[dir="rtl"] .page-item .page-link .next {
  transform: scaleX(-1);
}

/* Flatpickr's RTL bundle moves previous to the right and next to the left,
 * but its inline SVG arrows remain LTR. Mirror the glyphs only; the controls'
 * click semantics and RTL physical placement stay unchanged. */
[dir="rtl"] .flatpickr-months .flatpickr-prev-month svg,
[dir="rtl"] .flatpickr-months .flatpickr-next-month svg {
  transform: scaleX(-1);
}

/* Operations period input uses Flatpickr's official monthSelectPlugin. Keep
 * its 12-month grid aligned with the Metronic calendar and document RTL. */
.flatpickr-calendar.flatpickr-monthSelect-theme-light {
  inline-size: 290px;
  min-inline-size: 290px;
}

.flatpickr-calendar .flatpickr-monthSelect-months {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  padding: 10px;
  direction: inherit;
}

.flatpickr-calendar .flatpickr-monthSelect-month {
  box-sizing: border-box;
  flex: 0 0 calc(33.333% - 2px);
  min-block-size: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  color: var(--bs-gray-800);
  font-size: 0.9rem;
  cursor: pointer;
}

.flatpickr-calendar .flatpickr-monthSelect-month:hover,
.flatpickr-calendar .flatpickr-monthSelect-month:focus-visible,
.flatpickr-calendar .flatpickr-monthSelect-month.selected {
  background: var(--bs-primary);
  color: var(--bs-white);
  outline: 0;
}

[dir="rtl"] .flatpickr-calendar .flatpickr-monthSelect-months {
  direction: rtl;
}


/* Select2 dropdown anchoring (DESIGN.md 13). Select2 positions its dropdown
 * from JavaScript with an explicit inline `left` in document pixels, which
 * is direction-agnostic by construction -- upstream Select2 never flips its
 * own positioning for RTL, only inner alignment. The blanket rtlcss flip
 * rewrote the parked/open rules to `right:-100000px` / `right:0`, and an
 * absolutely-positioned box with inline left + stylesheet right + width is
 * over-constrained: in RTL the inline `left` loses and the dropdown pins to
 * the offset parent's right edge, opening far from its input. (The parked
 * `right:-100000px` can additionally stretch the RTL scrollable area, where
 * the LTR `left:-100000px` cannot.) In RTL, inline-end is physical left, so
 * the open dropdown must use inline-end `0`; the vendor's physical `right:0`
 * otherwise places it one width to the left of the zero-width wrapper.
 * `!important` is required because that vendor rule is loaded later. Inner
 * RTL alignment (caret, clear, options) still comes from the `dir` option in
 * metronic-bridge.js. */
[dir="rtl"] .select2-dropdown {
  inset-inline-start: auto !important;
  inset-inline-end: -100000px !important;
}

[dir="rtl"] .select2-container--open .select2-dropdown {
  inset-inline-start: auto !important;
  inset-inline-end: 0 !important;
}

/* The stepper pipeline reads in the inline direction; the separator chevron
 * is directional, so it mirrors with the document rather than carrying the
 * class on every instance. */
[dir="rtl"] .stepper .arrow {
  transform: scaleX(-1);
  display: inline-block;
}

/* The sign-in page has no header, so its locale switcher pins to the
 * viewport corner instead of sitting in a toolbar. Logical inset, so the
 * corner flips with the document. */
.signin-locale {
  position: fixed;
  inset-block-start: var(--s4);
  inset-inline-end: var(--s4);
  z-index: var(--z-sticky);
}

/* ── locale switcher (13) ─────────────────────────────────────────────
 * The sign-in page's language control (signed-in pages use the account
 * menu's Language submenu) is the Demo1 account language menu: the trigger
 * shows the current flag, the menu lists both locales with the active one
 * carrying .active. Flags are 3:2 SVGs in static/img/flags, rendered one
 * step below the vendor symbol-20px box (the smallest size the bundle ships)
 * so both flags align in a single column and stack vertically.
 *
 * The option buttons carry `btn` for the migrated-page gate, so the two
 * properties the global primary-fill button rule sets that the vendor
 * .menu-link leaves alone are undone here -- the same pattern as
 * `.pager .page-link` and `[data-tabs] [data-tab]`. Colour, padding, hover
 * and the .active highlight stay Demo1's own (menu-active-bg / menu-hover-bg
 * on the menu, whose state rules outrank this base), so the rows read
 * exactly like the vendor account menu. Only existing tokens are referenced,
 * so both themes resolve every name used here; logical properties only. */
.lang-switcher {
  position: relative;
  display: inline-flex;
  align-items: center;
}

/* A locale row (_language_options.html), wherever it is listed -- the header
 * flag menu and the account menu's Language submenu alike. It is a <button>
 * standing in for Demo1's `<a class="menu-link d-flex">`, and a button only
 * grows as wide as its content, so it is stretched across its menu item
 * explicitly; the global button rule's centring goes too. Flag and name
 * then start from the reading direction's own start, the flag's `me-4` the
 * only space between them, so both names line up whatever their width. */
.menu-link.lang-option {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0;
  inline-size: 100%;
  text-align: start;
  white-space: nowrap;
}

.lang-menu .lang-option {
  min-block-size: 42px;
  padding-block: var(--s2);
  padding-inline: var(--s4);
  border-color: transparent;
  background-color: transparent;
  color: var(--bs-gray-800);
}

.lang-menu .menu-item {
  padding-inline: var(--s2);
}

.lang-menu .lang-option:hover,
.lang-menu .lang-option:focus-visible,
.lang-menu .lang-option.active {
  border-color: transparent;
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-light-primary);
  color: var(--bs-primary);
}

/* Flags ship as 3:2 art and render in a fixed 22-by-15 box, in rows and
 * trigger alike. Logical properties only. */
.lang-menu .menu-item .symbol,
.account-lang-menu .menu-item .symbol,
.lang-switcher .lang-trigger .symbol {
  display: inline-flex;
  flex: 0 0 22px;
  inline-size: 22px;
  block-size: 15px;
  align-items: center;
  justify-content: center;
}

.lang-menu .menu-item .symbol > img,
.account-lang-menu .menu-item .symbol > img,
.lang-switcher .lang-trigger .symbol > img {
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
}

/* ── account menu (header, beside the sidebar collapse control) ─────────
 * Demo1's header user menu, class for class (docs/ui/metronic.md §18).
 * §14: initial-letter symbols only, never a demo photo -- the trigger and
 * the dropdown header reuse Metronic's own .symbol-label component instead
 * of an <img>, in Demo1's rounded square. The dropdown's Sign Out row and
 * the Language submenu's rows are <button>s (POST forms, like the
 * sidebar footer's own sign-out); every bare <button> in this app inherits
 * the legacy primary-fill look from the `button { ... }` rule above, and
 * .menu-link never sets background/border itself, so that legacy fill would
 * otherwise win. This undoes exactly those two properties -- the same
 * treatment .lang-option gives its own POST buttons above.
 *
 * Vendor .btn (display:inline-block, text-align:center) and vendor
 * .menu-link (display:flex) are both single-class rules of equal
 * specificity, so whichever the bundle happens to define last wins the
 * cascade -- on this build that was .btn, which centred the Sign Out row's
 * text instead of leading it like the My Profile row beside it. This
 * two-class selector (.account-menu .menu-link) outranks either on
 * specificity alone, so it wins regardless of source order: flex row,
 * icon+label leading edge, text starting from the reading direction's own
 * start (left in LTR, right in RTL) with no explicit left/right to flip. */
.account-menu {
  position: relative;
  display: inline-flex;
  align-items: center;
  margin-inline-start: var(--s2);
}

/* The trigger is Demo1's bare `div.cursor-pointer.symbol.symbol-35px`, made
 * a <button class="btn p-0 border-0"> so a keyboard reaches it and the §4
 * gate holds. The global button rule's fill and 32px minimums are undone
 * so only the symbol shows; the vendor .btn focus ring is borrowed for
 * keyboard focus (14). */
.account-menu .account-trigger {
  min-height: 0;
  min-width: 0;
  min-block-size: 0;
  min-inline-size: 0;
  padding: 0;
  border: 0;
  border-radius: var(--bs-border-radius-lg);
  background: transparent;
}

.account-menu .account-trigger:hover { background: transparent; }

/* From lg up the trigger also carries the display name and role (29 Oct
 * meeting). Text leads from the reading direction's start, and a long name
 * or several roles truncate rather than push the header. */
.account-menu .account-trigger { text-align: start; color: inherit; font-weight: inherit; }
.account-menu .account-trigger-who { max-inline-size: 200px; }
.account-menu .account-trigger-who > span { max-inline-size: 100%; }

.account-menu .account-trigger:focus-visible {
  outline: 0;
  box-shadow: 0 0 0 0.25rem rgba(27, 132, 255, 0.25);
}

.account-menu .account-lang-flag { object-fit: cover; }

.account-menu .menu-link {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  border-color: transparent;
  background-color: transparent;
  text-align: start;
  /* .btn sets its own --bs-btn-font-size (1.1rem); undone so the Sign Out
     button reads at the same size as the My Profile link beside it, both
     inheriting the dropdown's own fs-6. */
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  /* The real culprit behind the row-height mismatch: a vendor rule
     targeting plain, unmodified buttons --
     `.btn:not(.btn-outline):not(...8 more):not(.btn-hover-outline)` --
     outranks .menu-link's own `padding: .65rem 1rem` on specificity alone
     (nine class-equivalents from the :not() chain vs. two here), and sets
     `padding: calc(1px + .775rem) calc(1px + 1.5rem)` instead. It only
     matches elements still carrying every default .btn look, so it hit the
     Sign Out row (a bare <button class="btn menu-link">) but not the My
     Profile row (an <a>, never a .btn candidate at all) beside it -- two
     different vertical paddings, two different row heights. Horizontal
     padding is untouched (already pinned by the vendor's own
     `.px-5 { padding-left/-right: 1.25rem !important }`, physical and
     mirrored by the separate RTL bundle like every other vendor rule);
     only the vertical
     side needs reclaiming, and !important is the only way a two-class
     selector out-scores a nine-class one. */
  padding-block: 0.65rem !important;
}

/* ── page search (Ctrl+K) ─────────────────────────────────────────────
 * The header trigger reads as a solid search field; the palette is a
 * Bootstrap modal whose rows are links. Everything is logical (inline-start
 * / inline-end) so the Arabic build mirrors it for free.
 */
/* Compound with .btn (not just `.page-search-open`): Metronic's Bootstrap
 * `.btn` rule loads after abk.css and shares this element via the `btn`
 * class required by docs/ui/metronic.md §4, so a single-class selector here
 * would lose the padding/border/background/color fight to source order.
 * The compound selector's higher specificity wins regardless of order. */
.page-search-open.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  inline-size: min(100%, 22rem);
  min-block-size: 38px;
  padding-block: 0.35rem;
  padding-inline: 0.9rem;
  border: 0;
  border-radius: 0.475rem;
  background: var(--bs-gray-100, #f9f9f9);
  color: var(--bs-gray-500, #99a1b7);
  text-align: start;
  cursor: pointer;
}
.page-search-open.btn:hover, .page-search-open.btn:focus-visible { background: var(--bs-gray-200, #f1f1f2); color: var(--bs-gray-700, #4b5675); outline: none; }
.page-search-open-label { flex: 1 1 auto; font-size: 0.95rem; }
.page-search-kbd {
  font: 500 0.75rem/1 var(--font-mono, "IBM Plex Mono", monospace);
  padding-block: 0.2rem;
  padding-inline: 0.4rem;
  border: 1px solid var(--bs-gray-300, #dbdfe9);
  border-radius: 0.3rem;
  background: var(--bs-body-bg, #fff);
  color: var(--bs-gray-600, #78829d);
}
@media (max-width: 991.98px) {
  .page-search-open-label,
  .page-search-kbd { display: none; }
  .page-search-open.btn {
    inline-size: auto;
    min-inline-size: 38px;
    padding-inline: 0.5rem;
    gap: 0;
  }
}
.page-search-head { display: flex; align-items: center; gap: 0.75rem; padding-block: 0.75rem; padding-inline: 1.25rem; }
.page-search-input { flex: 1 1 auto; font-size: 1.05rem; }
.page-search-body { max-block-size: min(60vh, 32rem); overflow-y: auto; padding-block-end: 0.5rem; }
.page-search-heading { margin: 0; padding-block: 0.75rem 0.25rem; padding-inline: 1.25rem; font-size: 0.85rem; font-weight: 600; color: var(--bs-gray-600, #78829d); }
.page-search-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-block-size: 40px;
  margin-inline: 0.5rem;
  padding-inline: 0.75rem;
  border-radius: 0.475rem;
  color: var(--bs-gray-800, #252f4a);
  text-decoration: none;
}
.page-search-item:hover, .page-search-item.active { background: var(--bs-gray-100, #f9f9f9); color: var(--bs-gray-900, #071437); }
.page-search-icon { display: inline-flex; inline-size: 1.5rem; justify-content: center; color: var(--bs-gray-500, #99a1b7); }
.page-search-title { flex: 1 1 auto; }
.page-search-section { font-size: 0.85rem; color: var(--bs-gray-500, #99a1b7); }
.page-search-empty { margin: 0; padding-block: 1.5rem; padding-inline: 1.25rem; }

/* ── company workspace (docs/ui/metronic.md §19) ─────────────────────── */

/* A status line with nothing to say takes no gap in the page's column. */
.ws-status:empty { display: none; }

/* The job log opens on demand; with no log there is nothing to open. */
.ws-job-log:has(> pre[hidden]) { display: none; }

/* Filled from the summary; starts empty rather than full width. */
.ws-progress-bar { width: 0; transition: width 0.3s ease; }

.setup-steps { margin: 0; padding: 0; list-style: none; }

.setup-step {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr) auto;
  column-gap: var(--s3);
  row-gap: var(--s1);
  align-items: start;
  padding-block: var(--s3);
  border-block-end: 1px dashed var(--rule);
}
.setup-step:last-child { border-block-end: 0; }
.setup-step__icon { display: flex; justify-content: center; padding-block-start: 2px; }
.setup-step__title { font-weight: 600; color: var(--ink); }
.setup-step__summary { font-size: 13px; color: var(--ink-muted); }
.setup-step--attention .setup-step__summary { color: var(--signal-bad-ink); }
.setup-step--locked .setup-step__title { color: var(--ink-muted); }
.setup-step__action { align-self: center; }
.setup-step__blockers {
  grid-column: 2 / -1;
  margin: 0;
  padding-inline-start: var(--s4);
  font-size: 13px;
  color: var(--signal-bad-ink);
}

.ws-tile { border-radius: var(--radius); }

.ws-coverage th,
.ws-coverage td { white-space: nowrap; }

/* ── dashboard (core/templates/core/dashboard.html) ──────────────────────
 * Metronic demo2 index rhythm on the vendored bundle: a period hero, four
 * counts, a pipeline, meters and a step track. Colours come from the
 * vendor's own --bs-* variables, which the bundle defines for light and
 * dark alike, so no new token is introduced here. Every rule is logical
 * (inline/block), so Arabic is a direction flip and nothing more. */

.dash-hero { border-inline-start: 4px solid var(--bs-primary); }
.dash-eyebrow { letter-spacing: 0.06em; }
.dash-period,
.dash-figure,
.dash-money,
.dash-pct { font-variant-numeric: tabular-nums; }
.dash-pct { min-inline-size: 3.5ch; text-align: end; }

/* Loading placeholders sized like the figure they stand in for. */
.dash-skeleton-title { display: inline-block; inline-size: 7ch; block-size: 1em; vertical-align: middle; }
.dash-skeleton-figure { inline-size: 3ch; block-size: 22px; }
.dash-skeleton-bar { block-size: 6px; }

/* A tile that is a link reads as one: lift on hover, a ring on focus. */
.dash-link {
  color: inherit;
  text-decoration: none;
  transition: box-shadow var(--motion) var(--ease), transform var(--motion) var(--ease);
}
.dash-link:hover { box-shadow: var(--lift); }
.dash-link:focus-visible,
.dash-stage-link:focus-visible,
.dash-dot:focus-visible,
.dash-company:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
}

/* Bars fill from their start edge once the figures arrive. */
.dash-bar { inline-size: 0; transition: width 0.3s var(--ease); }

/* ── onboarding pipeline ── */
.dash-pipeline {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.dash-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  min-inline-size: 0;
}
.dash-stage-link {
  display: block;
  block-size: 100%;
  padding: var(--s3);
  border: 1px dashed var(--bs-gray-300);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--motion) var(--ease), background-color var(--motion) var(--ease);
}
a.dash-stage-link:hover { border-color: var(--bs-primary); background-color: var(--bs-gray-100); }
/* The arrow sits in the gap between two stages, pointing at the next one. */
.dash-stage-arrow {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-end: calc(var(--s3) / -2);
  translate: 50% -50%;
  z-index: 1;
  background: var(--bs-body-bg);
  border-radius: 50%;
}
[dir="rtl"] .dash-stage-arrow { translate: -50% -50%; }

/* One bar, one segment per stage, a 2px gap between fills. */
.dash-segments {
  display: flex;
  gap: 2px;
  block-size: 8px;
  border-radius: 4px;
  overflow: hidden;
}
.dash-segments > span { flex-basis: 0; min-inline-size: 4px; }

/* ── facility utilisation ── */
.dash-facilities { gap: 0; }
.dash-facility {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding-block: var(--s3);
  border-block-end: 1px dashed var(--bs-gray-300);
}
.dash-facility:last-child { border-block-end: 0; }
.dash-facility .skeleton { inline-size: 60%; }
.dash-facility .dash-skeleton-bar { inline-size: 100%; }

/* An empty widget explains itself in the middle of the card (6). */
.dash-empty {
  display: grid;
  justify-items: center;
  gap: var(--s2);
  padding-block: var(--s8);
  text-align: center;
}

/* ── month-end step track ──
 * A dot per step on a hairline running through the row; the line starts at
 * the first dot and ends at the last, so the track reads as one sequence. */
.dash-track td.dash-step {
  position: relative;
  text-align: center;
  min-inline-size: 72px;
}
.dash-track td.dash-step::before {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  inset-inline: 0;
  border-block-start: 2px solid var(--bs-gray-300);
}
.dash-track td.dash-step:nth-child(2)::before { inset-inline-start: 50%; }
.dash-track td.dash-step:nth-last-child(2)::before { inset-inline-end: 50%; }
.dash-dot {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 28px;
  block-size: 28px;
  border-radius: 50%;
  border: 2px solid var(--bs-gray-400);
  background: var(--bs-body-bg);
  color: var(--bs-white);
  transition: transform var(--motion) var(--ease);
}
/* A larger hit area than the mark, without growing the mark. */
.dash-dot::after {
  content: "";
  position: absolute;
  inset: -8px;
}
.dash-dot:hover { transform: scale(1.12); }
.dash-dot[data-state="done"] {
  border-color: var(--bs-success);
  background: var(--bs-success);
}
.dash-dot[data-state="done"] i { color: var(--bs-white); }
.dash-company { min-inline-size: 0; max-inline-size: 28ch; }

/* ── responsive: 1024, 768, 375 ── */
@media (max-width: 1199.98px) {
  .dash-hero .card-body { padding: var(--s6); }
}
@media (max-width: 767.98px) {
  .dash-pipeline { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash-stage-arrow { display: none; }
}
@media (max-width: 575.98px) {
  .dash-kpi .card-body { padding: var(--s4); }
  .dash-kpi .fs-2hx { font-size: 1.75rem !important; }
  .dash-hero .btn { flex: 1 1 100%; justify-content: center; }
}
