/* ═══════════════════════════════════════════════════════════════════════
   HiERP — the application shell.

   DIRECTION CONTRACT
   THESIS ·  HiERP is an enterprise ERP application, and it looks like one.
             It refuses the trade-counter price book the earlier system built:
             the people in here are operators running a business, not readers
             of a document, and they need to know where they are, what state
             the thing in front of them is in, and what they may do about it —
             in that order, on every screen, without learning anything new.
   OWN-WORLD Deep navy command bar over a cool neutral ground. White work
             surfaces on hairline borders with one level of real elevation.
             ONE action blue, and a five-role semantic set that never says
             anything with colour alone. 4px geometry, no ornament. ONE
             typeface — Vazirmatn, at 13px, for language AND for every figure,
             SKU, code and date; tabular so a column of money is a column.
   STORY ·   Land → «where am I» (bar, rail, title) → «what state is this in»
             (status strip) → «what can I do» (one primary, the rest quiet).
   VIEWPORT  48px command bar · 256px module rail (inline-start = RIGHT) ·
             work area: page header with title + status + actions, a facts
             strip, then dense grids.
   FORM ·    The canon — SAP Fiori 3 / Dynamics 365 / NetSuite — played
             straight, at full fidelity, with no irony and nothing smuggled in.

   ── WHAT THIS SHEET IS ──────────────────────────────────────────────────

   A separate docroot on a separate host under its own CSP, so this is copied
   and never linked. The CLASS VOCABULARY is the contract: 96 screens emit
   these names and none of them was edited to land this design. Rename nothing.

   ── THE RULES THAT ARE NOT STYLE ────────────────────────────────────────

   1 · NO letter-spacing anywhere Persian can appear. Vazirmatn's letters
       JOIN; tracking them apart breaks the join and makes a word unreadable.
       This is a fact about the script, not a preference. Latin-only runs in
       .ltr may be tracked.
   2 · Every figure is tabular, and since ۷ شهریور ۱۴۰۵ that rule does ALL
       the work on its own. Vazirmatn's Latin digits are uniform (1151 units
       each) but its PERSIAN digits are not — ۰ is 864 and ۳ is 1349, a 56%
       spread — so with the mono face gone, a column of «۱۲٬۵۰۰٬۰۰۰» would
       have gone ragged the moment the token changed. It does not, because
       `font-variant-numeric: tabular-nums` is set on body and the font ships
       a real `tnum`. Do not remove it from body thinking it is decoration.
   3 · Colour never speaks alone. Every status carries its word; the fill is
       the second signal, for the people who can find it.
   4 · A Latin run inside Persian — a SKU, an IBAN, a Jalali date in digits —
       sits in .ltr: isolated, left-to-right, regardless of the paragraph.
   5 · Logical properties only (inline-start/end, margin-inline). This app is
       RTL and the stylesheet must never assume it.
   ═══════════════════════════════════════════════════════════════════════ */

/* ⚠️ The 300 face is GONE as of ۴ شهریور ۱۴۰۵. It was declared here and never
   once used — no rule in this sheet sets font-weight: 300 — so it was 51KB
   downloaded to render nothing. It is also the weight the guidance tells you
   not to reach for at this size («in general, avoid light font weights»), so
   there is no version of this app that wants it back. 400/500/700 carry the
   whole hierarchy. */
@font-face { font-family: "Vazirmatn"; font-style: normal; font-weight: 400; font-display: swap;
             src: url("fonts/vazirmatn-400.woff2") format("woff2"); }
@font-face { font-family: "Vazirmatn"; font-style: normal; font-weight: 500; font-display: swap;
             src: url("fonts/vazirmatn-500.woff2") format("woff2"); }
@font-face { font-family: "Vazirmatn"; font-style: normal; font-weight: 700; font-display: swap;
             src: url("fonts/vazirmatn-700.woff2") format("woff2"); }
/* ⚠️ ONE TYPEFACE, deliberately — amended ۷ شهریور ۱۴۰۵ by the owner.
   Saira Condensed (Latin brand mark) and Spline Sans Mono (figures, SKUs,
   codes, dates) are GONE. Vazirmatn now sets every glyph in the application.
   --font-display, --font-latin-display and --font-mono still exist and are
   still emitted by 96 screens; they now all resolve to the same family, so
   the vocabulary survives and a future session can re-split them by editing
   four lines instead of a thousand. See "the figures still line up" below. */

/* ═══════════ tokens ═══════════ */

:root {
  /* Grounds. The work ground is a cool neutral, not white: a white page
     behind white cards has no edges, and this app is read for hours. */
  --bg:            #edeef2;
  --surface:       #ffffff;
  --surface-2:     #f7f8fa;   /* insets, table heads, quiet strips */
  --surface-3:     #e9eaee;   /* pressed / selected quiet */

  /* The command bar. Dark, so the work area is unambiguously the work area. */
  --shell-bg:      #ffffff;
  --shell-bg-2:    #f4f5f7;
  --shell-fg:      #1a1a1a;
  --shell-fg-dim:  #5f6b7a;
  --shell-line:    #d9dce1;

  /* Ink */
  --ink:           #1a1a1a;   /* primary text        — 15.8:1 on surface */
  --ink-2:         #45536a;   /* secondary           —  8.0:1 */
  /* ⚠️ DARKENED ۴ شهریور ۱۴۰۵, from #66748a, and the reason is that the
     earlier audit measured this token against WHITE only. On white it was
     4.74:1 and passed. It is not on white: rows band to --surface-2, quiet
     panels sit on --surface-3, and the page ground is --bg. Measured there
     it was 4.49 / 4.11 / 4.18 — under the 4.5:1 that 12–13px type needs.
     What that actually cost: `tr.is-void td` and `.amt.is-zero` are --ink-3
     and the even rows are --surface-2, so a VOID DOCUMENT'S LINE — the row
     an auditor opens the ledger to read — was below AA on half of them.
     #5e6b7f is the same hue one step down: 5.40 / 5.12 / 4.69 / 4.77. */
  --ink-3:         #5f6b7a;   /* muted / captions — 4.69:1 on the WORST ground */
  --ink-on-accent: #ffffff;

  /* Lines. Enterprise grids are held together by hairlines, not by fills. */
  --line:          #e8e9ec;
  --line-strong:   #d5d7dc;
  /* ⚠️ A CONTROL BORDER IS NOT A HAIRLINE, and this is the token that says
     so. Inputs used to borrow --line-strong, which is 1.62:1 on white —
     under half of WCAG 1.4.11's 3:1 for the visual boundary of a control.
     A hairline BETWEEN things may be faint; the edge that says «you may type
     here» may not, because for a low-vision operator it is the only thing
     that says a field exists at all. 3.23:1 on --surface, 3.06 on
     --surface-2. */
  --line-control:  #8b9099;

  /* The one action colour. */
  --accent:        #006bff;
  --accent-hover:  #0057d9;
  --accent-press:  #0049b8;
  --accent-tint:   #e8f1ff;
  --accent-tint-2: #cfe2ff;

  /* ── The NetSuite layer ──────────────────────────────────────────────
     ⚠️ ADDED ۵ شهریور ۱۴۰۵ by the owner: «copy everything from NetSuite —
     the palette, the charts, the buttons, the header, the search». Every
     hex below was SAMPLED from the reference screenshots (median of a
     solid region, not eyeballed), so this block is evidence rather than
     taste. Where a sampled value is repeated here under a NetSuite name
     it is because the name is what the rules downstream read.

     Two values are deliberately NOT the sampled ones, and both are
     accessibility floors this project already committed to:
       · the link blue is #4a6fa8, not NetSuite's #6487c1 — theirs is
         3.63:1 on white and body text needs 4.5. Same hue, one step down,
         5.08:1.
       · --line-control is #8b9099, not their input grey #b5b8bd (1.99:1)
         — a control boundary owes 3:1 under WCAG 1.4.11. 3.21:1.
     Everything else is literal: #006bff carries white at 4.62:1 and
     #607799 at 4.57:1, so NetSuite's own two loudest colours passed. */

  --nav-bg:        #607799;   /* the menu bar — sampled */
  --nav-fg:        #ffffff;
  --nav-fg-dim:    #d7dee9;
  --nav-hover:     #6d829f;
  --nav-active-bg: #ffffff;   /* the current module wears a white tab */
  --nav-active-fg: #33475e;

  --link:          #4a6fa8;   /* see the note above */
  --link-hover:    #335180;

  /* Row banding. NetSuite bands its data grids at #f1f1f1 and leaves the
     header WHITE with tiny grey caps — which is why --band is its own
     token and not --surface-2: the two used to be one value and the
     header would have banded with the rows. */
  --band:          #f1f1f1;

  /* The launcher tiles. NetSuite's are PASTEL with dark ink, not
     saturated with white ink — the label is the first signal and the
     colour is the second, which is the same rule this project already
     wrote down as «colour never speaks alone». Dark ink clears 14:1 on
     every one of them. */
  --tile-1: #ffe9d9;  --tile-2: #eaf2da;  --tile-3: #d5eef2;
  --tile-4: #ffe3e2;  --tile-5: #e5e4f5;  --tile-6: #fdf3d2;
  --tile-ink: #1a1a1a;

  /* Portlet chrome */
  --portlet-grip: #c3c7cf;
  --caps:         #5f6b7a;   /* the tiny uppercase column/field labels */

  /* Semantic roles — five, and no more. Each ships a fill and an ink that
     clears 4.5:1 on that fill. */
  --pos:           #1a7f37;  --pos-tint:  #e7f4ea;
  --neg:           #c9372c;  --neg-tint:  #fdecea;
  --warn:          #a15c07;  --warn-tint: #fff4e0;
  --info:          #006bff;  --info-tint: #e8f1ff;
  --neutral:       #56637a;   --neutral-tint: #eceff4;

  /* Money keeps one point of colour of its own — it is the thing this whole
     system exists to get right, and one hue in a field of black numbers is
     the fastest way for an eye to find the figure it came for. */
  --money:         #7c3f00;

  /* Geometry — enterprise, not consumer. 4px base. */
  --r-1: 3px; --r-2: 5px; --r-3: 8px;
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px; --sp-6: 32px;

  /* One level of real elevation, plus an overlay level. Offset AND blur. */
  --sh-1: 0 1px 2px rgba(16,26,38,.05), 0 1px 3px rgba(16,26,38,.08);
  --sh-2: 0 4px 12px rgba(16,26,38,.12), 0 1px 3px rgba(16,26,38,.08);

  /* Density */
  --h-ctl:  32px;   /* input / select / button */
  --h-row:  34px;   /* grid row */
  --top-h:  44px;   /* NetSuite's masthead is thin */
  --nav-h:  34px;   /* and its menu bar thinner still */
  --rail-w: 256px;
  --gutter: 20px;
  --tap:    44px;
  --status-h: 28px;
  /* ⚠️ AMENDED ۵ شهریور ۱۴۰۵ by the owner. This was 1680px, and on the
     shop's own 2000px screen that left a dead band beside the work area
     while the ردیف grid inside it was still too narrow to hold a line.
     پارسیان fills the window because a business application is not a
     document, and this is the instruction: fill it. What kept the reading
     measure sane was never the page cap — it is the per-column sizing on
     the grid and the auto-FILL tracks on the form, both below. */
  --content-max: none;

  /* A cell you may type in. پارسیان tints every editable cell in its
     document grids — a different hue per column — and that single device is
     why an operator knows where the caret can go without being told. One
     warm tint here rather than five: the message is «typeable», and five
     hues spend five signals saying one thing. */
  --edit-tint:   #fffaeb;
  --edit-line:   #f0d9a8;

  /* The magnifier. One definition, three consumers: the scan box, the
     palette and any filter bar that grows one. */
  --icon-search: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.8'%20stroke-linecap='round'%3E%3Ccircle%20cx='9'%20cy='9'%20r='5.4'/%3E%3Cpath%20d='M13.2%2013.2%2017%2017'/%3E%3C/svg%3E");

  --font-body:    "Vazirmatn", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: "Vazirmatn", system-ui, sans-serif;
  --font-latin-display: "Vazirmatn", system-ui, sans-serif;
  --font-mono:    "Vazirmatn", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Kept: a later phase draws charts, and re-deriving "the same blue" a
     second time is how two of them stop being the same blue. */
  --s1: #5ecbe0; --s2: #70c84f; --s3: #e6d757; --s4: #f0a04b;
  --s5: #6c7ae0; --s6: #e0709f; --s7: #ae60b4; --s8: #4a90c4;
  --chart-grid: #e4e6ea; --chart-axis: #c8ccd4; --chart-ink: #5f6b7a;
  --gauge: #7fbf7b; --gauge-track: #e0e3e8;
  --area: rgba(149,193,134,.28); --spark: #7bb661;
  --spark-neg: #e06666; --area-neg: rgba(224,102,102,.22);

  /* Legacy aliases from the retired «جریان / glass» system. ⚠️ PRUNED
     ۴ شهریور ۱۴۰۵: eleven of these were checked against the whole tree —
     `--plane --cobalt --cobalt-deep --cobalt-hover --copper --copper-deep
     --brown-core --earth-green --earth-green-deep --earth-yellow` — and had
     ZERO references in any .php or .css. They were kept «so nothing renders
     unstyled while the vocabulary settles»; the vocabulary settled two
     phases ago — and on re-checking, the remaining six had zero consumers
     too, so the block is gone rather than trimmed. The chart tokens below
     stay: they are unused ON PURPOSE, because a later phase draws charts and
     re-deriving «the same blue» a second time is how two of them stop being
     the same blue. `--font-latin-display` stays for the same kind of reason
     — it is the seam if the faces are ever re-split. Unused-and-documented
     is not the same as dead. */
  --wash: var(--surface-2);
  --wash-hover: var(--surface-3);
  --wash-strong: var(--surface-2);
  --wash-strong-hover: var(--surface-3);
  --row-hover: #eef3fb;
  --gap-plane: 1px;
  --radius-xs: var(--r-1);
  --radius-sm: var(--r-2);
  --radius-md: var(--r-2);
  --radius-lg: var(--r-3);
  --card-x: var(--sp-4);
}

/* ═══════════ reset & page ═══════════ */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
  color-scheme: light;
  /* The page never pans sideways. Wide content scrolls INSIDE its own
     container — a grid in .erp-tbl-wrap, the module strip in .erp-rail — and
     anything that still escapes is a bug, not a feature the reader should
     have to discover by dragging the whole document.
     `clip`, never `hidden`: hidden makes the root a scroll container and
     silently kills every position:sticky inside it, which here is the command
     bar, the rail and every grid header. */
  overflow-x: clip;
}

body {
  min-block-size: 100vh;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: .8125rem;          /* 13px — enterprise density */
  line-height: 1.55;
  font-weight: 400;
  /* ⚠️ Load-bearing — see rule 2 in the header. Vazirmatn's Persian digits
     are proportional by default and every rial figure in this app is in
     Persian digits. This one line is what keeps a ledger column a column. */
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  display: flex;
  flex-direction: column;
  overflow-x: clip;
}

img, svg { max-inline-size: 100%; display: block; }

a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

b, strong { font-weight: 700; }

/* One focus treatment, everywhere, and it is never removed. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: var(--r-1);
}

/* A Latin run inside Persian text. Isolated so a SKU cannot reorder the
   sentence around it, and mono so a column of them lines up. */
.ltr {
  direction: ltr;
  unicode-bidi: isolate;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.erp-sr {
  position: absolute; inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Hidden UPWARD, not sideways. `inset-inline-start: -100%` sends it off the
   RIGHT in an RTL document, and a fixed box parked past the right edge widens
   the scrollable area — a 320px phone reported a 413px document and the whole
   page could be panned. Vertical is direction-agnostic. */
.erp-skip {
  position: fixed; inset-block-start: 0; inset-inline-start: 0;
  transform: translateY(-200%);
  z-index: 200; padding: var(--sp-3) var(--sp-4);
  background: var(--accent); color: var(--ink-on-accent);
  font-weight: 500; border-end-end-radius: var(--r-2);
}
.erp-skip:focus { transform: translateY(0); }

/* ═══════════ the masthead ═══════════ */

/* ⚠️ REBUILT ۵ شهریور ۱۴۰۵ — «copy everything from NetSuite … the header,
   the search». It was a DARK navy bar carrying brand · fiscal year · user ·
   خروج. NetSuite's is WHITE, thin, hairlined at the bottom, and it carries
   four things in a fixed order: the mark, a global search box in the MIDDLE
   of the window, the help affordances, and a person block that is a name
   over a small grey line saying which role and which company file you are
   in.

   That last shape is why this port is not a loss. The fiscal-year chip used
   to be a separate pill, and CLAUDE.md says why it exists at all: «چرا این
   رقم صفر است» is answered by it more often than by anything else. NetSuite
   puts exactly that fact in exactly that place — the subtitle under the
   operator's name — so the chip did not get dropped, it got moved into the
   slot the reference already had for it.

   Mirrored, not translated: the mark sits inline-start, the person block
   inline-end. Logical properties throughout, so the same rules lay out LTR
   if this app ever renders one. */

.erp-top {
  position: sticky; inset-block-start: 0; z-index: 60;
  block-size: var(--top-h);
  background: var(--shell-bg);
  color: var(--shell-fg);
  border-block-end: 1px solid var(--shell-line);
}
.erp-top-in {
  block-size: 100%;
  max-inline-size: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex; align-items: center; gap: var(--sp-3);
}

.erp-brand {
  display: flex; align-items: center; gap: var(--sp-2);
  color: var(--shell-fg); text-decoration: none;
  flex: none;
}
.erp-brand:hover { text-decoration: none; }
.erp-brand .erp-logo { block-size: 20px; inline-size: auto; }
.erp-brand-sub {
  font-size: .6875rem; color: var(--shell-fg-dim); font-weight: 500;
  white-space: nowrap;
  padding-inline-start: var(--sp-2);
  margin-inline-start: var(--sp-1);
  border-inline-start: 1px solid var(--shell-line);
}

/* ── The global search ──────────────────────────────────────────────────
   A BUTTON that looks like a field, and the distinction is deliberate. It
   opens the F3 palette — a <dialog> — so the control's job is «open a
   thing», which is a button's job and not a text input's. Announced with
   aria-haspopup="dialog"; a real <input> here would promise a text box that
   swallows the first keystroke and then hands it to a modal anyway.

   It also means no second JSON route. The search law in CLAUDE.md says
   lookup.php is the only one and must stay the only one; the palette is
   server-rendered anchors, already filtered by the same capability check
   the menu runs. */
.erp-gsearch {
  flex: 1 1 auto;
  max-inline-size: 460px;
  margin-inline: auto;
  block-size: 28px;
  display: flex; align-items: center; gap: var(--sp-2);
  padding-inline: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line-control);
  border-radius: var(--r-2);
  color: var(--ink-3);
  font: inherit; font-size: .8125rem; font-family: var(--font-body);
  text-align: start;
  cursor: text;
}
.erp-gsearch:hover { border-color: var(--ink-3); }
.erp-gsearch::before {
  content: ""; flex: none;
  inline-size: 15px; block-size: 15px;
  background-color: var(--ink-3);
  -webkit-mask: var(--icon-search) center / contain no-repeat;
          mask: var(--icon-search) center / contain no-repeat;
}
.erp-gsearch span { flex: 1 1 auto; }
.erp-gsearch kbd {
  flex: none;
  font-family: var(--font-body); font-size: .6875rem; font-weight: 600;
  color: var(--ink-3);
  border: 1px solid var(--line-strong); border-radius: var(--r-1);
  padding: 1px 5px; background: var(--surface-2);
}

.erp-top-end {
  flex: none;
  display: flex; align-items: center; gap: var(--sp-3);
  margin-inline-start: auto;
}

/* The person block: avatar, name, and the line under it that says which
   role and which fiscal year. */
.erp-who {
  display: flex; align-items: center; gap: var(--sp-2);
  color: var(--shell-fg); text-decoration: none;
  padding-inline-start: var(--sp-3);
  border-inline-start: 1px solid var(--shell-line);
  white-space: nowrap;
}
.erp-who:hover { text-decoration: none; }
.erp-who:hover .erp-who-n { color: var(--link); }
.erp-who-av {
  flex: none;
  inline-size: 26px; block-size: 26px; border-radius: 50%;
  background: var(--nav-bg); color: #fff;
  display: grid; place-items: center;
  font-size: .75rem; font-weight: 700;
}
.erp-who-t { display: flex; flex-direction: column; line-height: 1.25; }
.erp-who-n { font-size: .75rem; font-weight: 600; color: var(--shell-fg); }
.erp-who-r { font-size: .6875rem; color: var(--shell-fg-dim); }

/* ⚠️ The environment banner. It may not be shortened, hidden or ellipsised —
   APP_ENV does not choose the database, so the ONLY way to know which one a
   screen reached is for it to say so out loud. It keeps its saturated
   treatment against the new white masthead rather than blending into it. */
.erp-env {
  font-size: .6875rem; font-weight: 700;
  padding: 3px var(--sp-2); border-radius: var(--r-1);
  background: var(--warn-tint); color: var(--warn);
  border: 1px solid currentColor;
  white-space: nowrap;
}

/* ── تعویض سال مالی ─────────────────────────────────────────────────────
   The switcher sits between the environment banner and the person block,
   because the year is already named inside that block and a control belongs
   next to the thing it changes.

   ⚠️ IT CHANGES COLOUR WHEN IT IS NOT THE LIVE YEAR, and that is the whole
   point of it being here rather than on a settings screen. Every figure on
   every page below is then from a closed year, and the difference between
   «موجودی ۱۴۰۳» and «موجودی امروز» is not visible in the numbers themselves. */
.erp-fysw {
  flex: none;
  display: flex; align-items: center; gap: var(--sp-1);
}
.erp-fysw select {
  font: inherit; font-size: .75rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  background: var(--surface);
  padding: 3px var(--sp-2);
  border: 1px solid var(--line-strong); border-radius: var(--r-1);
  cursor: pointer;
}
.erp-fysw select:hover  { border-color: var(--ink-2); color: var(--ink); }
.erp-fysw select:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }

/* ⚠️ [hidden] LOSES TO A CLASS THAT SETS display. js/erp.js hides the «برو»
   button once it has wired the select to submit on change, but `.erp-btn` sets
   display: inline-flex and an author rule beats the UA sheet's
   `[hidden] { display: none }` outright — so the button stayed on screen next
   to a select that no longer needed it. The same trap is already worked around
   for `.erp-status-i[hidden]` further down this file. */
.erp-fysw [hidden] { display: none !important; }

.erp-fysw.is-archive select {
  background: var(--warn-tint);
  color: var(--warn);
  border-color: currentColor;
}

/* ── «you are looking at a closed year» ─────────────────────────────────
   A full-width strip, not a chip. It is the one piece of chrome that has to
   survive being ignored: somebody who lands on ۱۴۰۳ from a bookmark and reads
   a موجودی off it has been misled by the software, not by their own mistake. */
.erp-archive {
  display: flex; align-items: center; gap: var(--sp-3);
  flex-wrap: wrap;
  padding: var(--sp-2) var(--gutter);
  background: var(--warn-tint);
  color: var(--warn);
  border-block-end: 1px solid currentColor;
  font-size: .8125rem;
}
.erp-archive b {
  font-weight: 700;
  padding: 2px var(--sp-2);
  border: 1px solid currentColor; border-radius: var(--r-1);
  white-space: nowrap;
}
.erp-archive span { flex: 1 1 auto; min-inline-size: 0; }
.erp-archive .erp-btn {
  background: var(--surface); border-color: currentColor; color: var(--warn);
}
.erp-archive .erp-btn:hover { background: #fff; color: var(--warn); }

/* The status bar compartment, matching — it names the year being LOOKED AT. */
.erp-status-i.is-archive b,
.erp-status-i.is-archive span { color: #fbbf24; }
.erp-status-i.is-archive b { opacity: 1; }

.erp-top .erp-btn {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--ink-2);
}
.erp-top .erp-btn:hover { background: var(--surface-2); color: var(--ink); }

/* ═══════════ shell: rail + work area ═══════════ */

.erp-shell {
  flex: 1;
  inline-size: 100%;
  max-inline-size: var(--content-max);
  margin-inline: auto;
  /* ⚠️ ONE COLUMN as of ۴ شهریور ۱۴۰۵. This was
     `grid-template-columns: var(--rail-w) minmax(0, 1fr)` — the rail's column
     and the work area's. The rail is a menu bar above the shell now, so the
     shell is just the work area, and `--rail-w` no longer describes a layout;
     it survives as the width of a dropdown panel. */
  display: block;
}

/* ═══════════ the menu bar ═══════════

   ⚠️ REBUILT ۴ شهریور ۱۴۰۵, by the owner: «shift the right navbar to upside
   of the window, like parsian».

   پارسیان's navigator is a single right-aligned row of module names directly
   under the title bar — خرید و فروش، اطلاعات انبار، حسابداری، دریافت و
   پرداخت … — and each one drops a white panel of commands beneath it. The
   open one is filled, not underlined, which is why an operator can see which
   menu is down from across a counter.

   Two things fall out of that shape and both are gains:

   1 · The work area gets 256px back across every screen in the application,
       on the axis where a ردیف grid actually wants it. The «fill the window»
       amendment was fighting for exactly this space.
   2 · The current module is now stated in ONE place instead of two — the
       filled menu title — rather than a highlighted rail row that could
       scroll out of sight, which is the fault the drawers were introduced to
       fix in the first place.

   What it costs, stated: sixty destinations are no longer all visible at
   once. That is the same trade پارسیان makes, and the palette (F3) is the
   answer for anyone who would rather type than point. */
.erp-rail {
  position: sticky; inset-block-start: var(--top-h); z-index: 60;
  /* ⚠️ RE-SKINNED ۵ شهریور ۱۴۰۵ — NetSuite. The bar was white paper with a
     coloured icon per module; NetSuite's is a solid slate band carrying bold
     white words and nothing else. Flat: no shadow, because the masthead's
     hairline and this band's own colour already separate the chrome from the
     work area, and NetSuite stacks the two with no elevation between them.
     #607799 carries white at 4.57:1 — the sampled value passed AA unchanged,
     so it is used literally. */
  background: var(--nav-bg);
  color: var(--nav-fg);
}
.erp-rail-in {
  display: flex; align-items: stretch;
  max-inline-size: var(--content-max);
  margin-inline: auto;
  padding-inline: calc(var(--gutter) - var(--sp-2));
  /* ⚠️ NO `overflow-x` HERE, and that is not an oversight — it is the whole
     reason the panels are visible.

     The obvious build gives this bar `overflow-x: auto` so thirteen module
     names can scroll on a narrow screen. It costs you every dropdown: a box
     with `overflow-x: auto` and a visible `overflow-y` computes overflow-y to
     `auto` as well, so a 39px-tall scroller CLIPS an absolutely-positioned
     panel hanging 335px below it. The menu opens, the title fills, the panel
     lays out at the right size and place, and nothing appears — which reads
     as the software being broken rather than as a CSS rule doing its job.

     Nor may it scroll at any OTHER width, for the same reason — and
     `position: fixed` on the panel is not the way out either, because a fixed
     child escapes the <details> slot that hides a CLOSED menu (see the 64rem
     block). So the bar WRAPS. Thirteen module names fit one line down to
     about 1000px and take a second line below that; a navigator that grows a
     row on a phone is a cost the reader can see and reason about, which is
     more than can be said for one that silently hides four modules past an
     edge with no scrollbar. */
  flex-wrap: wrap;
}

/* ── the home tab ────────────────────────────────────────────────────────
   NetSuite's bar opens with a white tab carrying a house. It is the only
   icon-only control in the whole chrome, so it carries a title AND a visually
   hidden label — «colour never speaks alone» applies to glyphs too, and an
   icon with no accessible name is the single most common way a navigator
   becomes unusable with a screen reader. */
.erp-rail-home {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  inline-size: 40px; block-size: var(--nav-h);
  margin-inline-end: var(--sp-2);
  color: var(--nav-fg);
  border-start-start-radius: var(--r-1);
  border-start-end-radius: var(--r-1);
  transition: background-color 120ms ease-out, color 120ms ease-out;
}
.erp-rail-home svg { inline-size: 18px; block-size: 18px; }
.erp-rail-home:hover { background: var(--nav-hover); color: var(--nav-fg); text-decoration: none; }
.erp-rail-home.is-here {
  background: var(--nav-active-bg);
  color: var(--nav-active-fg);
}
.erp-rail-home:focus-visible { outline: 2px solid #fff; outline-offset: -3px; }
.erp-rail-home.is-here:focus-visible { outline-color: var(--nav-active-fg); }

/* ── the drawers ─────────────────────────────────────────────────────────
   ⚠️ AMENDED ۷ شهریور ۱۴۰۵. Thirteen groups and sixty items, every one of
   them open, made a rail about two and a half screens tall. Reaching
   «تنظیمات» meant scrolling past every module in the business, and by the
   time you got there the highlighted row saying where you actually were had
   gone off the top. Each group is a drawer now, and the one holding the
   current page is the one that opens.

   <details>, and NOT a button with a class, on purpose: it collapses with no
   JavaScript at all, a screen reader already calls it a disclosure widget
   and announces expanded/collapsed, and Space and Enter already work on the
   summary. js/erp.js adds exactly one thing on top — opening a drawer closes
   its siblings — and if that file never loads, the rail is still completely
   navigable. Nothing here may become the only way to reach a page. */
/* Each group is a menu: the summary is its title in the bar, the list is the
   panel that drops from it. `position: relative` is what the panel anchors
   to, so a menu near the end of the bar still opens under its own title. */
.erp-rail-grp { position: relative; flex: none; }

.erp-rail-g {
  display: flex; align-items: center; gap: var(--sp-2);
  block-size: var(--nav-h);
  padding-inline: var(--sp-3);
  /* Bold, and white. NetSuite's menu words are the heaviest type in its
     whole chrome — they are the only thing on the band, so they carry it. */
  font-size: .8125rem; font-weight: 700; color: var(--nav-fg);
  cursor: pointer; user-select: none;
  white-space: nowrap;
  border-radius: 0;
  list-style: none;                 /* Firefox's disclosure triangle */
  border-block-end: 2px solid transparent;
  transition: background-color 120ms ease-out, color 120ms ease-out;
}
.erp-rail-g::-webkit-details-marker { display: none; }
.erp-rail-g:hover { background: var(--nav-hover); color: var(--nav-fg); }
/* On a dark band the house focus ring (--accent, a mid blue) is nearly
   invisible. White is the only outline that reads on every one of these
   states — band, hover and the white open tab — so the ring goes white and
   flips dark on the tab states where white would vanish. */
.erp-rail-g:focus-visible { outline: 2px solid #fff; outline-offset: -3px; }
.erp-rail-grp[open] > .erp-rail-g:focus-visible,
.erp-rail-grp.is-here > .erp-rail-g:focus-visible { outline-color: var(--nav-active-fg); }

/* ⚠️ The OPEN menu is FILLED, not tinted and not underlined. This is the one
   thing پارسیان's bar does that carries across a room: its open menu is a
   solid block of colour, and that is why its operators can see which menu is
   down without focusing on it. Ours fills in the module's own colour rather
   than one house orange, which says two things at once — «this menu is open»
   and «you are in خزانه‌داری». White on `--g` was measured for all thirteen
   when the launcher tiles were built: 4.76–10.35:1. */
/* ⚠️ MONOCHROME AS OF ۵ شهریور ۱۴۰۵, and this reverses the paragraph above
   deliberately. The open menu used to fill in the MODULE's own colour —
   پارسیان's device, and the reason its operators find a menu from across a
   counter. NetSuite's bar is one slate colour and its open/current tab is
   WHITE, which is the tab metaphor: the sheet below is the same paper and
   the tab is where it joins the band.

   Colour-by-function is not lost, it MOVED: `--g` still drives the launcher
   tiles on the overview, which is where NetSuite itself puts its colour.
   The token is untouched and every screen still emits `data-g`, so this is
   one rule to flip if the owner wants the coloured bar back. */
.erp-rail-grp[open] > .erp-rail-g {
  background: var(--nav-active-bg);
  color: var(--nav-active-fg);
  border-start-start-radius: var(--r-1);
  border-start-end-radius: var(--r-1);
}
.erp-rail-grp[open] > .erp-rail-g::before { background-color: var(--nav-active-fg); }

/* The module you are IN, whether or not its menu is open — a 2px rule in the
   module's colour under its title. Without it, closing the menu would leave
   the bar saying nothing about where you are, which is exactly the fault the
   rail's highlighted row used to cure. */
/* The module you are IN — NetSuite's white tab, exactly as its Home tab is
   drawn. It persists whether or not the menu is down, so closing a menu
   never leaves the bar silent about where you are. */
.erp-rail-grp.is-here > .erp-rail-g {
  background: var(--nav-active-bg);
  color: var(--nav-active-fg);
  border-start-start-radius: var(--r-1);
  border-start-end-radius: var(--r-1);
  border-block-end-color: transparent;
}
.erp-rail-grp.is-here[open] > .erp-rail-g { color: var(--nav-active-fg); }

/* The chevron. Down is closed, up is open — a rotation that reads the same
   in both directions, which a ‹ / › pair would not in an RTL shell. */
/* ⚠️ NO CHEVRON IN THE BAR. The rail's summaries carried one — down for
   closed, up for open — because a drawer in a vertical stack gives no other
   sign that it is a drawer. A menu bar does: thirteen of them in a row, each
   a word, is a shape every operator here has used since Windows 95, and
   پارسیان's own bar prints no marker either. Rendered, thirteen chevrons sat
   one per item and read as SEPARATORS between the words rather than as
   affordances belonging to them — which is worse than nothing, because it
   makes the bar look like a breadcrumb.

   The signal that a menu is open is the fill, which is stronger than a 6px
   rotation and is the thing پارسیان relies on. */
.erp-rail-g::after { content: none; }

/* ── the panel ───────────────────────────────────────────────────────────
   A white sheet on a hairline with the overlay shadow, anchored under its own
   title and aligned to the title's inline-START — which in this RTL shell is
   its right edge, exactly where پارسیان hangs its menus.

   It is an OVERLAY, so `--sh-2` and not `--sh-1`, and it is not a card: the
   rows are the content and the panel is only the paper they sit on. */
.erp-rail-list {
  list-style: none; padding: var(--sp-1) 0; margin: 0;
  position: absolute;
  inset-block-start: 100%;
  inset-inline-start: 0;
  z-index: 70;
  min-inline-size: var(--rail-w);
  max-inline-size: min(22rem, 92vw);
  max-block-size: min(30rem, calc(100vh - var(--top-h) - 5rem));
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--surface);
  border: 1px solid var(--line);
  border-start-start-radius: 0;
  border-radius: 0 0 var(--r-3) var(--r-3);
  box-shadow: var(--sh-2);
}
/* A menu near the far end of the bar would otherwise hang off the window. */
.erp-rail-grp:nth-last-child(-n+3) > .erp-rail-list {
  inset-inline-start: auto;
  inset-inline-end: 0;
  border-radius: 0 0 var(--r-3) var(--r-3);
}

.erp-rail-a {
  position: relative;
  display: block;
  padding: 8px var(--sp-4);
  border-radius: 0;
  color: var(--ink-2);
  font-size: .8125rem;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 120ms ease-out, color 120ms ease-out;
}
.erp-rail-a:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }

/* The current module. A tinted row plus a weight step plus a marker on the
   inline edge — three signals, because this is the one thing on the rail a
   reader must never have to hunt for. */
.erp-rail-a.is-on {
  background: var(--accent-tint);
  color: var(--accent);
  font-weight: 500;
}
.erp-rail-a.is-on::before {
  content: ""; position: absolute;
  inset-block: 5px; inset-inline-start: 0;
  inline-size: 3px; border-radius: 3px;
  background: var(--accent);
}
.erp-rail-a.is-soon {
  color: var(--ink-3); cursor: default;
  display: flex; align-items: center; gap: var(--sp-2);
}
.erp-rail-a.is-soon:hover { background: none; color: var(--ink-3); }
.erp-rail-soon {
  font-style: normal; font-family: var(--font-mono); font-size: .6875rem;
  padding: 1px 5px; border-radius: var(--r-1);
  background: var(--surface-3); color: var(--ink-3);
}

.erp-main {
  min-inline-size: 0;
  /* Belt and braces with the root rule: a grid, a wide form or one very long
     unbreakable token inside a screen must scroll in its own box or wrap, and
     must never widen the page. `clip` keeps sticky headers alive; `hidden`
     would not. */
  overflow-x: clip;
  padding: var(--sp-5) var(--gutter) var(--sp-6);
}

/* ═══════════ page header ═══════════ */

/* Title, the date it is as of, and a one-line lede. The object's identity
   lives here and nowhere else, so a printed page and a screen agree. */
.erp-h {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-2) var(--sp-3);
  border-block-end: 1px solid var(--line);
  /* ⚠️ THE MODULE COLOUR REACHES THE WORK SURFACE — added ۴ شهریور ۱۴۰۵.
     `--g` had three consumers and all three were navigators: the rail, the
     launcher tiles, the palette band. So the colour said where you were
     GOING and disappeared the moment you arrived, and every screen in the
     application looked the same whether it was خزانه‌داری or فروش.
     پارسیان's colour is continuous — that is the whole reason it works — and
     this is the cheapest honest way to carry it: the heading's own rule,
     which every screen already has, taking the module's hue instead of a
     neutral hairline. `<body data-g>` supplies it (see _bootstrap.php); the
     fallback keeps a screen outside the navigator looking exactly as before.

     ⚠️ ONE THING LEFT FOR THE OWNER — SETTLED ۵ شهریور, read the next
     paragraph: `[data-g="sales"]` is #0b57d0, which WAS `--accent` to the
     digit (it is #006bff now, so they no longer collide). On a sales screen this rule and the action blue
     are the same colour, which is the one thing «avoid using the same colour
     to mean different things» actually warns about. It reads acceptably —
     a 2px rule under a heading is not a button — but sales wants its own
     hue, and repainting a learned module colour is not a call to make on
     somebody's behalf. */
  /* ⚠️ THE MODULE RULE IS OFF AS OF ۵ شهریور ۱۴۰۵ — NetSuite. Everything
     above stayed true and stopped being what was asked for: NetSuite's title
     row is a name on the page ground over ONE hairline, identical on every
     screen in the product, and a 2px coloured rule under the heading is the
     most conspicuous thing this app had that it does not.

     It also retires the open question the paragraph above left the owner —
     `[data-g="sales"]` being #0b57d0, the same hex as the old --accent — by
     removing the rule that collided with it rather than by repainting a
     module colour somebody has already learned. `--g` and `data-g` are
     untouched; restore `2px` and `var(--g, …)` here to bring it back. */
  border-block-end-width: 1px;
  border-block-end-color: var(--line);
  padding-block-end: var(--sp-2);
  margin-block-end: var(--sp-3);
}
.erp-h h1 {
  font-family: var(--font-display);
  /* NetSuite's page title is quiet — 18px semibold, not a 22px display
     heading. The work below it is the loud thing. */
  font-size: 1.125rem; font-weight: 600; line-height: 1.3;
  color: var(--ink);
}
/* The aside beside the page title. It carries `dir="auto"` — see erp_title()
   in _ui.php for why it is no longer `.ltr` — so the DIRECTION comes from its
   own first strong character. The isolation stays here: without it a note
   beginning with a Latin code reorders the heading it sits inside. */
.erp-h-note {
  font-family: var(--font-mono); font-size: .75rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  unicode-bidi: isolate;
}
.erp-h-lede {
  flex-basis: 100%;
  max-inline-size: 76ch;
  color: var(--ink-2); font-size: .8125rem;
}

/* ═══════════ section heads ═══════════ */

.erp-sec { margin-block-start: var(--sp-5); }
.erp-sec-h {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--sp-2) var(--sp-3);
  margin-block: var(--sp-5) var(--sp-2);
}
.erp-sec-h:first-child { margin-block-start: 0; }
.erp-sec-h h2 {
  font-family: var(--font-display);
  font-size: .9375rem; font-weight: 700; color: var(--ink);
}
.erp-sec-h a { font-size: .75rem; font-weight: 500; }
.erp-card-h {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--sp-2);
  margin-block-end: var(--sp-3);
}
/* NetSuite's portlet title: a shade larger than the body, semibold, dark,
   and sitting over the portlet's own hairline rather than floating. */
.erp-card-h {
  padding-block-end: var(--sp-3);
  border-block-end: 1px solid var(--line);
}
.erp-card-h h3 { font-size: .9375rem; font-weight: 600; color: var(--ink); }
.erp-card-h a { font-size: .75rem; color: var(--link); }

/* ═══════════ surfaces ═══════════ */

/* ⚠️ A PORTLET, as of ۵ شهریور ۱۴۰۵ — NetSuite. Same element, same class,
   different paper: a hairline box with a 4px corner and NO shadow. NetSuite
   separates a portlet from the ground with one grey line and the ground's
   own tint, and stacks dozens of them without a single drop shadow — which
   is why its dashboard reads as one surface rather than as a pile of cards.

   NOT ported: the six-dot drag grip on the top edge of every NetSuite
   portlet. It is a handle for rearranging the dashboard, and this app cannot
   rearrange anything. A control that looks draggable and is not is worse
   than a missing one — the reader tries it, nothing happens, and from then
   on they distrust the affordances that DO work. */
.erp-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  box-shadow: none;
  padding: var(--sp-4);
}
.erp-card + .erp-card { margin-block-start: var(--sp-3); }
/* ⚠️ …but NOT inside a container that already has a `gap`.
   The rule above is for cards in normal flow, where nothing else separates
   them. In a grid or a flex column the gap does that job, so the margin is
   added a second time — every dashboard card sat 24px from its neighbour
   instead of 12 — and worse, it defeats `align-items: stretch`: the item
   stretches to the row height and the margin then eats 12px off it, so the
   cards in one کارنامه row shared a BOTTOM edge and started 12px apart at the
   top. Measured 437.6px / 425.6px / 425.6px in one row. */
.erp-grid > .erp-card + .erp-card,
.erp-sc-grid > .erp-card + .erp-card,
.erp-dash-col > .erp-card + .erp-card { margin-block-start: 0; }
.erp-card-inset {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  padding: var(--sp-3);
}

/* ── the dashboard grid ──────────────────────────────────────────────────
   ⚠️ ADDED ۵ شهریور ۱۴۰۵ — NetSuite. Its dashboard is THREE columns and the
   widths are not equal: a narrow one for reminders and meters, a wide middle
   for the tiles and the data grids, and a narrow one for charts. Measured off
   the reference at 1493px it is 292 / 861 / 285, so ~20 / 58 / 22.

   `align-items: start` is load-bearing. Grid items stretch by default, so
   without it a two-row portlet in the left column grows to the height of the
   eight-row grid beside it and prints a field of white under three lines of
   text — which is exactly what a dashboard must not do, because empty space
   in a portlet reads as missing data rather than as layout. */
.erp-dash {
  display: grid; gap: var(--sp-3);
  grid-template-columns: minmax(0, 20fr) minmax(0, 58fr) minmax(0, 22fr);
  align-items: start;
}
/* The right column folds under the middle first — it holds charts, which are
   the thing that survives being wider. Then everything stacks. */
@media (max-width: 84rem) {
  .erp-dash { grid-template-columns: minmax(0, 30fr) minmax(0, 70fr); }
  .erp-dash-c3 { grid-column: 1 / -1; }
}
@media (max-width: 56rem) {
  .erp-dash { grid-template-columns: minmax(0, 1fr); }
  .erp-dash-c3 { grid-column: auto; }
}
.erp-dash-col {
  display: flex; flex-direction: column; gap: var(--sp-3);
  min-inline-size: 0;
}

.erp-grid { display: grid; gap: var(--sp-3); }
.erp-grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); }
.erp-grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }

/* ═══════════ the facts strip (KPI cells) ═══════════ */

/* Not "hero metrics". These are the object's own key figures — the invoice's
   net, its VAT, its total — and they are read together, so they align on a
   shared baseline rather than each floating in its own tile. */
/* ⚠️ The floor is 150px, not 180px, and the reason is arithmetic rather than
   taste: SIX tiles is the commonest strip in this app, and at 180px the sixth
   one orphans onto a row of its own the moment the work area drops under
   ~1150px — one tile beside four empty cells, a 200px band of nothing in the
   middle of the page. At 150px all six hold one line down to ~960px, and
   below that the explicit counts take over so the strip always breaks evenly
   (3+3, then 2+2+2) instead of 5+1. */
.erp-kpis {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
  margin-block-end: var(--sp-4);
}
/* 1025–1280px is the awkward band: the rail still takes 256px, so the work
   area is ~900 and six 150px tiles need ~960. Rather than let it break 5+1,
   the count becomes explicit and the strip reads 3+3. */
@media (max-width: 80rem) { .erp-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.erp-kpi {
  display: flex; flex-direction: column;
  min-inline-size: 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  box-shadow: none;
}
/* NetSuite's KPI label: small, grey, semibold — the caption over a figure,
   not a sentence. Its own is tracked small-caps; ours is not, for the reason
   given on the grid header (Vazirmatn's letters join). */
.erp-kpi-l {
  display: block; color: var(--caps);
  font-size: .6875rem; font-weight: 600; line-height: 1.4;
  margin-block-end: var(--sp-1);
}
/* Figures across a row must start on one line, or the row stops being
   something you can read across. Pinning the VALUE to the bottom did that
   and left a hole under every short label; giving the LABEL a two-line floor
   does the same job with no hole — the labels occupy equal height whether
   they wrap or not, so the figures under them line up by construction. */
/* The figure NetSuite prints at ~28px, light in weight and dark in colour —
   the largest type anywhere on its dashboard. */
.erp-kpi-v {
  display: block;
  font-family: var(--font-mono); font-weight: 400;
  font-size: 1.625rem; line-height: 1.2;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  text-align: start;
}
.erp-kpi-n { white-space: nowrap; }
.erp-kpi-n.is-text { white-space: normal; font-family: var(--font-body); }
.erp-kpi-v.is-money { font-size: 1.125rem; color: var(--money); }
/* The unit rides with the figure, and at narrow widths it takes its own
   line for EVERY cell rather than for whichever ones happen not to fit —
   a row where «ریال» sits in three different places is the tell of a
   layout that was tuned instead of decided. */
.erp-kpi-u {
  font-family: var(--font-body); font-weight: 400;
  font-size: .6875rem; color: var(--ink-3);
  margin-inline-start: var(--sp-1);
}
.erp-kpi-note { font-size: .75rem; color: var(--ink-3); line-height: 1.45; margin-block-start: var(--sp-1); }
.erp-kpi-foot {
  display: flex; align-items: center; gap: var(--sp-2);
  margin-block-start: var(--sp-1); min-block-size: 1.15rem;
}

/* This period over last period, in one column — erp_cmp_cell(). The current
   figure keeps the column's own type; the comparison sits under it, smaller
   and muted, so the row reads as one fact with its history rather than as two
   figures competing. Both are `nowrap`: they are in a `.n` cell, and a money
   figure that breaks is three figures. */
.erp-cmp-n { display: block; white-space: nowrap; }
.erp-cmp-p {
  display: block;
  font-size: .6875rem; color: var(--ink-3); line-height: 1.4;
  /* ⚠️ `normal`, and NOT inherited from the `.n` cell's nowrap. «قبل» plus a
     nine-digit figure is 105px on one line, which set the whole column's
     minimum and left the «تغییر» column beside it 52px — one word per line.
     Allowed to break at its own space the column asks for the FIGURE's width
     instead, and the figure still cannot break: a comma-grouped number has no
     break opportunity in it now that `overflow-wrap: anywhere` is gone. */
  white-space: normal;
}

/* The change badge. A percentage is one token and never breaks. */
.erp-d {
  font-size: .75rem; font-family: var(--font-mono); font-weight: 500;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.erp-d-up      { color: var(--pos); }
.erp-d-down    { color: var(--neg); }
.erp-d-flat    { color: var(--ink-3); }
.erp-d-partial { color: var(--warn); }
/* ⚠️ …but `.is-word` is not a percentage, and inheriting the nowrap made it
   the widest thing in a portlet. It is a PHRASE — «دورهٔ قبل ناقص»، «قابل
   مقایسه نیست»، «بدون تغییر» — and at 96px it was setting the width of the
   «تغییر» column in a 241px card, pushing the table past the portlet and
   scrolling the leading digit of the figure beside it off the edge. A phrase
   breaks at its space like any other phrase; only the token must not. */
.erp-d.is-word {
  font-family: var(--font-body); color: var(--ink-3);
  white-space: normal;
  /* But it must not collapse to one word per line either. Left to the table's
     own column arithmetic the «تغییر» column shrank to 52px and «دورهٔ قبل
     ناقص» came out on three lines, one word each, in a row three times taller
     than the figure beside it. A floor of two words is the shape the phrase
     has; `inline-block` is what lets a floor apply to an inline run at all. */
  display: inline-block;
  min-inline-size: 4.25rem;
}

/* ═══════════ the data grid ═══════════ */

/* An ERP's table is not a summary of something that happened elsewhere — it
   IS the document. A سند, a کاردکس, a تراز آزمایشی. It has to survive being
   read for an hour, printed, argued about, and filed for years.

   ⚠️ THE CANYON, and why the table is not 100% wide.
   A width:100% table distributes all slack among its columns, so on a wide
   screen a two-column statement put «موجودی نقد و بانک» on one edge and its
   figure ~900px away on the other, and the eye had to walk the gap to
   connect them. Measured: 26 screens, worst 991px. So the grid sizes to its
   CONTENT (max-content) and caps at the container. Leftover space now
   collects OUTSIDE the table, where it costs nothing, instead of between a
   label and the number it belongs to. Wide grids are unaffected — they
   already fill the width — and the wrapper scrolls when they exceed it. */
/* ⚠️ AMENDED ۵ شهریور ۱۴۰۵ by the owner: THE GRID FILLS ITS CONTAINER.
   It was fit-content, and the reasoning above was sound about the CANYON and
   wrong about the cure. Sizing the whole table to its content pays for one
   bad case — the two-column statement — by leaving every document grid in
   the app narrower than the screen it is read on, which is the case that
   actually happens all day. فاکتور فروش in پارسیان is the window.

   The canyon is beaten where it is actually made, in the COLUMNS:

     · every numeric cell is `inline-size: 1%` + nowrap, so it shrink-wraps
       to its own figure and gives its slack back;
     · the slack therefore collects in the TEXT column, which is the item
       name — the one column in this system that can always use it;
     · and the rows are BANDED, so on the two-column statement that is left
       the eye tracks «موجودی نقد و بانک» across to its figure along a tint
       rather than across white paper. That is پارسیان's own answer to the
       same problem, and it is worth more than a narrow table.

   `min-inline-size: max-content` is what keeps the second half of the old
   rule: a grid wider than the space it is given still refuses to squeeze,
   and the wrapper scrolls instead of clipping a rial figure in half. */
.erp-tbl-wrap {
  inline-size: 100%;
  max-inline-size: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  /* ⚠️ Not decoration, and not an incantation. A scroll container CLIPS its
     contents visually, but in Chrome those contents still count toward the
     ANCESTORS' scrollable overflow — so a 496px grid inside a 292px wrapper
     left the document itself 93px pannable on a phone while the wrapper
     scrolled perfectly. `contain: paint` is the declaration that says nothing
     inside may escape this box, and it is the only thing that clears it.
     It belongs here, on the element that owns the scrolling, rather than on
     whichever form happens to wrap a grid on some page. */
  contain: paint;
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: var(--sh-1);

  /* ⚠️ A GRID THAT SCROLLS SIDEWAYS NOW SAYS SO — added ۴ شهریور ۱۴۰۵.
     This was `background: var(--surface)` and nothing else, so eight columns
     of a تراز with three more past the edge looked exactly like a table that
     ended. Worse here than in an LTR app: the horizontal scrollbar sits at
     the far end from where a Persian reader's eye rests, and every numeric
     cell shrink-wraps, so the grid reads as deliberately sized rather than
     cut off.

     Four layers, and the trick is the ATTACHMENT. The two `local` gradients
     are painted in the CONTENT's coordinate space, so they travel with the
     scroll and cover the shadows when there is nothing more that way; the
     two `scroll` shadows are pinned to the box. At rest both shadows are
     hidden under their cover; scroll, and the cover on the side you came
     from moves away and its shadow appears. No script, no scroll listener,
     no class to keep in sync — which matters, because a grid must be
     readable on the counter's old browser with JS off. */
  background:
    linear-gradient(to right, var(--surface) 30%, rgba(255,255,255,0)) 0 0 / 32px 100% no-repeat local,
    linear-gradient(to left,  var(--surface) 30%, rgba(255,255,255,0)) 100% 0 / 32px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%,   rgba(16,26,38,.16), rgba(16,26,38,0)) 0 0 / 14px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, rgba(16,26,38,.16), rgba(16,26,38,0)) 100% 0 / 14px 100% no-repeat scroll,
    var(--surface);
}

.erp-tbl {
  /* ⚠️ 100% AND NOTHING ELSE, and the «nothing else» is the load-bearing half.
     `min-inline-size: max-content` was here for one draft, to stop a squeezed
     table clipping a rial figure. It did something else entirely: a table
     sized to max-content has no DEFINITE width, so the `inline-size: 1%` on
     every numeric cell below had nothing to resolve against and the
     circularity ran away — measured, on this file, at 15,600px of table with
     every column but «کالا» pinned to 156px and the whole grid scrolled off
     the side of a 1,424px card. Two columns visible out of eight.

     It was also unnecessary. A table can never be laid out narrower than its
     own MIN-content, whatever width is asked of it, and min-content here is
     «every nowrap money cell intact» — that is the exact guarantee the
     max-content floor was added to provide. So the figures stay whole, the
     wrapper scrolls when even that will not fit, and the only thing given up
     is that a very long item NAME wraps onto a second line instead of forcing
     the grid sideways. Which is the better of the two. */
  inline-size: 100%;
  border-collapse: collapse;
  font-size: .8125rem;
}

/* Where the slack goes. A numeric column is exactly as wide as the widest
   figure in it and not one pixel wider; `1%` is the classic declaration for
   «shrink-wrap me» in an auto-layout table, and nowrap is already on these
   cells for a different reason. Everything left over lands in the columns
   that have words in them. */
.erp-tbl .n, .erp-tbl .amt,
.erp-tbl th.n, .erp-tbl th.amt { inline-size: 1%; }

/* Banding. Not decoration and not nostalgia: it is what lets an eye cross a
   wide row without losing the line, and it is the reason a full-width table
   can be full-width at all. Kept faint — one step of the neutral ramp — so a
   tinted STATE (an editable cell, a highlighted row, a negative figure) is
   still louder than the stripe it sits on. */
/* ⚠️ --band, NOT --surface-2, and they used to be one value. NetSuite bands
   its rows at #f1f1f1 and leaves the HEADER white; sharing one token meant
   the header banded with the rows and the first data row was indistinguish-
   able from the caption above it. */
.erp-tbl tbody tr:nth-child(even) { background: var(--band); }

/* The head is a caption, not a plinth: quiet, small, and it STICKS, because
   a reader forty rows into a کاردکس still needs to know which column is
   بدهکار.

   ⚠️ NetSuite sets these labels in SMALL CAPS WITH TRACKING. We take the
   size, the weight and the grey and we do NOT take the tracking, because
   the first of the three rules that are not style says so: Vazirmatn's
   letters join, and tracking them apart breaks the join. That is a fact
   about the script, not a preference, and it applies to a column heading
   exactly as it applies to a sentence. Size + weight + colour carry the
   same «this is a label, not data» signal without it. */
.erp-tbl thead th {
  position: sticky; inset-block-start: 0; z-index: 2;
  background: var(--surface);
  padding: var(--sp-2) var(--sp-3);
  text-align: start;
  font-size: .6875rem; font-weight: 700; color: var(--caps);
  white-space: nowrap;
  border-block-end: 1px solid var(--line-strong);
}

/* No rule between rows: the banding already separates them, and NetSuite
   draws one or the other but never both — a hairline UNDER every banded row
   turns the grid into a ladder and doubles the horizontal noise on exactly
   the screens that carry the most rows. */
.erp-tbl tbody td {
  padding: 0 var(--sp-3);
  block-size: var(--h-row);
  border-block-end: 0;
  vertical-align: middle;
}
.erp-tbl tbody tr:last-child td { border-block-end: 0; }
.erp-tbl tbody tr { transition: background-color 100ms ease-out; }
/* After the band, so hover wins on an even row too. */
.erp-tbl tbody tr:hover, .erp-tbl tbody tr:nth-child(even):hover { background: var(--row-hover); }

/* Money and quantity. Aligned to the inline END so the units digits line up
   down the page, tabular so the digits are the same width, and floored at
   9ch so a debit column and a credit column agree on their width whether or
   not a given row uses them both. */
.erp-tbl .amt, .erp-tbl .num, .amt {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: end;
  white-space: nowrap;
  min-inline-size: 9ch;
}
/* A zero is PRINTED, never quietly omitted — an empty cell in a ledger is a
   question and a «۰» is an answer. */
.erp-tbl .amt.is-zero { color: var(--ink-3); }
/* Every numeric cell — money, quantity, percentage, a Jalali date, a code.
   Mono and tabular so a column of them is a column, and end-aligned so the
   units digits stack. `.n` is on the HEADER too, which is what keeps a
   heading over the figures it names. */
.erp-tbl .n {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: end;
  white-space: nowrap;
}
/* ⚠️ …unless the cell holds a SENTENCE with figures in it rather than a
   figure. «۰ سفارش، ۰ ریال» is a numeric cell by column — it belongs under
   «این دوره» with the figures above it — but it is prose, and nowrap made it
   the widest thing in the table: 113px in a 241px portlet, set by a phrase
   nobody needs on one line. Marked with `.erp-nw` around each figure, the
   sentence breaks at its comma and every FIGURE inside it still cannot
   break, which is the only part of the rule that was ever load-bearing. */
.erp-tbl .n:has(.erp-nw) { white-space: normal; }
.erp-tbl .n .erp-nw { white-space: nowrap; }

/* A total is weight and a held breath of space, never a fill. */
.erp-tbl tr.is-total td {
  font-weight: 700;
  border-block-start: 1.5px solid var(--ink);
  border-block-end: 0;
  background: var(--surface);
}
.erp-tbl tr.is-total:hover td { background: var(--surface); }

/* erp_ledger() prints its totals in a <tfoot>, and the rule above only
   caught tr.is-total. Same treatment: weight and a rule, never a fill. */
.erp-tbl tfoot td {
  padding: 0 var(--sp-3);
  block-size: var(--h-row);
  font-weight: 700;
  border-block-start: 1.5px solid var(--ink);
  background: var(--surface);
}
.erp-tbl tfoot .n, .erp-tbl tfoot .amt {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: end;
}

/* Nothing is ever hard-deleted here, so a voided document has to stay
   LEGIBLE as a state rather than vanish or read as a blank row. */
.erp-tbl tr.is-void td { color: var(--ink-3); }
.erp-tbl tr.is-void td:first-child { text-decoration: line-through; }

/* A key/value table — an object's facts, not a list of records. It is the
   one grid that must NOT fill: «تلفن» and its number belong beside each
   other, and there is no third column to absorb the distance between them.
   No band either, for the same reason — five facts are not a list. */
.erp-tbl-kv { inline-size: auto; min-inline-size: 0; max-inline-size: 100%; }
/* ⚠️ …AND SO DOES THE BOX AROUND IT. Added ۶ شهریور ۱۴۰۵.
   `.erp-tbl-kv` is the one table in this app that sizes to its content — five
   facts about one object are not a list, and there is no third column to
   absorb the slack, which is why the canyon rule exempts it. But its wrapper
   is still `inline-size: 100%`, and the wrapper is the thing that draws the
   hairline and paints the white. Measured on «وضعیت سامانه»: a 393px table
   inside a 1400px bordered panel — 1,007px of blank paper with a border
   round it, at the bottom of the dashboard. Empty space inside a drawn box
   does not read as layout, it reads as a panel that failed to load, which is
   the exact fault `align-items: start` was added to the dash grid to avoid.
   The box is as wide as what it holds, and still scrolls if that is too
   wide for the page. */
.erp-tbl-wrap:has(> .erp-tbl-kv) { inline-size: max-content; max-inline-size: 100%; }
.erp-tbl-kv tbody tr:nth-child(even) { background: transparent; }
.erp-tbl-kv th {
  position: static;
  background: none; border-block-end: 1px solid var(--line);
  color: var(--ink-3); font-weight: 400;
  padding-inline-end: var(--sp-5);
}

.erp-empty {
  padding: var(--sp-6) var(--sp-4);
  text-align: center; color: var(--ink-3); font-size: .8125rem;
}

/* ── a cell you may type in ──────────────────────────────────────────────
   پارسیان's document grids tint every editable cell, and it is the reason an
   operator knows where the caret can go without reading a heading. Here the
   value is edited in the COLUMN it belongs to — مقدار under «مقدار», قیمت
   واحد under «قیمت واحد» — so the heading is the label and no box is
   anonymous. That is a fix to a real fault: two unlabelled inputs parked in a
   «ویرایش» column, which is what this app shipped.

   A <form> cannot wrap <td>s, so the form lives in one cell and every control
   names it with the HTML `form` attribute. That is markup, not script: with
   JavaScript off the row still posts. */
.erp-tbl td.is-edit {
  background: var(--edit-tint);
  box-shadow: inset 0 0 0 1px var(--edit-line);
  padding: 3px var(--sp-2);
}
.erp-tbl tbody tr:hover td.is-edit { background: var(--edit-tint); }
/* The control inside it is the cell: no second border, no second corner, no
   inset inside an inset. It fills, and the column sizing decides how wide
   that is. */
/* ⚠️ `.erp-tbl` is on the front of every one of these and it is load-bearing,
   not tidiness: `.erp-inp` sets block-size, inline-size and a border of its
   own and is declared LATER in this file, so at equal specificity it would
   win every one of them and the cell editor would render as a 32px boxed
   control sitting inside a tinted cell — two borders and two corners saying
   one thing. One extra class settles it in the right direction.

   No `.ltr` on these, deliberately. That class floors an input at 14ch, which
   in a مقدار column is wider than the column, and it forces a direction these
   fields do not need: a run of digits is laid out left-to-right by the bidi
   algorithm whatever the paragraph direction is. `text-align: end` puts the
   units digit against the column's own inline end, where the figure above it
   already sits. */
.erp-tbl .erp-cell-inp {
  inline-size: 100%;
  min-inline-size: 0;
  max-inline-size: none;
  block-size: 26px;
  padding-inline: 6px;
  font-size: .75rem;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: end;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-1);
}
.erp-tbl .erp-cell-inp:hover { background: var(--surface); border-color: var(--edit-line); }
.erp-tbl .erp-cell-inp:focus {
  outline: none;
  background: var(--surface);
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-tint);
}
/* The editable columns shrink-wrap like the numeric ones they sit among —
   otherwise a 100%-wide control in an auto-sized column takes the slack that
   belongs to the item's name. */
.erp-tbl td.is-edit, .erp-tbl th.is-edit { inline-size: 1%; }
.erp-tbl th.is-edit { text-align: end; }

/* Chart of accounts indentation — a tree rendered in a flat table. */
.erp-acc-lvl { display: inline-block; }
.erp-acc-lvl.lvl-0 { padding-inline-start: 0; font-weight: 700; }
.erp-acc-lvl.lvl-1 { padding-inline-start: var(--sp-4); }
.erp-acc-lvl.lvl-2 { padding-inline-start: var(--sp-6); }

.erp-gl-future td { color: var(--ink-3); }
.erp-diff { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ═══════════ charts ═══════════
   ⚠️ ADDED ۵ شهریور ۱۴۰۵ — «copy … the charts». lib/chart.php emits every
   one of these as inline SVG with PRESENTATION attributes for colour, so
   there is almost nothing for CSS to do: what is here is layout and the two
   things a presentation attribute cannot express, which are the container's
   behaviour at narrow widths and print.

   The series palette lives in the tokens (--s1…--s8) AND in lib/chart.php's
   own constant, and that duplication is deliberate — an SVG attribute cannot
   read a custom property, so PHP has to know the hex. If one changes, change
   both; there is no third place. */

.erp-chart {
  inline-size: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.erp-chart > svg { max-inline-size: 100%; block-size: auto; }

/* The KPI-card sparkline. It is decoration beside a figure that is already
   stated in text — aria-hidden in the markup — so it may shrink freely. */
.erp-spark {
  inline-size: 100%; max-inline-size: 210px; block-size: auto;
  margin-block-start: var(--sp-2);
}

.erp-chart-empty { color: var(--ink-3); }

/* ⚠️ A CHART IS LTR GEOMETRY, and this is a bug fix rather than a stylistic
   choice. Added ۶ شهریور ۱۴۰۵.

   Every coordinate in lib/chart.php is authored left-to-right: `plotLeft` <
   `plotRight`, categories are laid out chronologically with x increasing, the
   value axis sits at `plotLeft - 6`. But an inline <svg> inherits `direction`
   from the document, and this document is `dir="rtl"` — and `text-anchor` is
   DIRECTION-RELATIVE. `start` means the right edge in an RTL context and
   `end` means the left.

   So every label in every chart was anchored from the wrong side, by its own
   width. On the value axis the labels are one or two glyphs, so it read as
   very slightly off and nobody could say why. On the legend they are WORDS,
   and «سفارش» — anchored `start` at the swatch's x + 12 — was drawn leftward
   straight over the swatch it was naming.

   Setting the direction on the SVG root makes the anchors agree with the
   arithmetic that placed them. The LABELS are unaffected: a Persian word is
   a strong right-to-left run and bidi still lays its letters out correctly
   inside an LTR box — what changes is only where the box starts. */
.erp-chart-line, .erp-chart-bar, .erp-chart-donut,
.erp-chart-gauge, .erp-chart-empty, .erp-spark { direction: ltr; }

/* A chart is data. It prints. */
@media print {
  .erp-chart { overflow: visible; }
}

/* ═══════════ status ═══════════ */

/* A pill is a FILL plus its WORD. Never colour alone — the word is the
   signal and the fill is the second one, for whoever can find it. */
.erp-pill {
  display: inline-flex; align-items: center;
  padding: 1px var(--sp-2);
  border-radius: var(--r-2);
  font-size: .75rem; font-weight: 500;
  white-space: nowrap;
  background: var(--neutral-tint); color: var(--ink-2);
}
.erp-pill.is-go   { background: var(--pos-tint);  color: var(--pos); }
.erp-pill.is-now  { background: var(--info-tint); color: var(--info); }
.erp-pill.is-wait { background: var(--warn-tint); color: var(--warn); }
.erp-pill.is-stop { background: var(--neg-tint);  color: var(--neg); }
.erp-pill.is-off  { background: var(--neutral-tint); color: var(--ink-3); }

/* ═══════════ messages ═══════════ */

/* A message strip. Border on the block-start only — a thick coloured bar
   down one side is the category's tell, and the icon-free word does the job. */
.erp-alert {
  padding: var(--sp-3) var(--sp-4);
  margin-block: var(--sp-3);
  background: var(--warn-tint);
  color: #6b3d05;
  border: 1px solid #f0ddb8;
  border-radius: var(--r-2);
  font-size: .8125rem; line-height: 1.6;
}
.erp-flash {
  padding: var(--sp-3) var(--sp-4);
  margin-block-end: var(--sp-4);
  border-radius: var(--r-2);
  font-size: .8125rem; font-weight: 500;
  border: 1px solid transparent;
}
.erp-flash.is-ok  { background: var(--pos-tint); color: #065634; border-color: #b9e2cd; }
.erp-flash.is-err { background: var(--neg-tint); color: #8c1b12; border-color: #f5c9c5; }

.erp-note      { color: var(--ink-2); font-size: .8125rem; line-height: 1.6; max-inline-size: 82ch; }
.erp-sub-note  { color: var(--ink-3); font-size: .75rem; line-height: 1.55; }
.erp-field-note{ display: block; color: var(--ink-3); font-size: .75rem; margin-block-start: 3px; }
/* ⚠️ Added ۴ شهریور ۱۴۰۵ with erp_field()'s $error. Every validation failure
   in this application used to be one sentence at the top of the page,
   detached from the field that caused it — which on a form with forty fields
   is a sentence about a problem you now have to go and find. The message
   belongs under the box, and it says what to do rather than what went wrong:
   «شناسهٔ ملی باید ۱۱ رقم باشد» beats «مقدار نامعتبر». */
.erp-field-err {
  display: block; color: var(--neg); font-size: .75rem;
  font-weight: 500; margin-block-start: 3px;
}
.erp-field.is-bad .erp-inp,
.erp-field.is-bad .erp-sel,
.erp-field.is-bad .erp-textarea { border-color: var(--neg); }
.erp-field.is-bad .erp-lbl { color: var(--neg); }
.erp-h-note + .erp-note { margin-block-start: var(--sp-2); }

/* ═══════════ controls ═══════════ */

/* One height for every control on a row, so a filter bar is a straight line
   and not a staircase. Fields are white on a hairline — an enterprise form
   reads as fields you may type in, not as tinted slots. */
.erp-inp, .erp-sel, .erp-textarea {
  font-family: inherit; font-size: .8125rem; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-control);
  border-radius: var(--r-1);
  padding: 0 var(--sp-3);
  block-size: var(--h-ctl);
  inline-size: 100%;
  transition: border-color 120ms ease-out, box-shadow 120ms ease-out;
}
.erp-inp::placeholder { color: var(--ink-3); }

/* ⚠️ A PLACEHOLDER READS IN ITS OWN DIRECTION. Added ۶ شهریور ۱۴۰۵.

   About forty-five fields in this app are `.ltr` — `direction: ltr` — and
   they are right to be: they hold a کد کالا, a serial, a rial figure, a
   Jalali date. Their HINTS are Persian sentences, and a Persian sentence in
   an LTR box loses its punctuation to the wrong end of the line. Measured on
   the barcode bench: «بارکد را اسکن یا تایپ کنید…» rendered with the ellipsis
   at the LEFT, because a trailing neutral resolves to the paragraph's
   direction and the paragraph is the FIELD's.

   `unicode-bidi: plaintext` on the pseudo-element takes the direction from
   the placeholder's own first strong character, so this is not «make hints
   RTL» — proven side by side in Chrome: the Persian hint keeps its ellipsis
   at the end and «CAB-H07-2.5» is untouched, left-aligned, in the same rule.
   The field's own direction, and therefore what the operator TYPES, does not
   move at all. */
::placeholder { unicode-bidi: plaintext; }
.erp-inp:hover, .erp-sel:hover, .erp-textarea:hover { border-color: var(--ink-3); }
.erp-inp:focus, .erp-sel:focus, .erp-textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}
.erp-inp:disabled, .erp-sel:disabled {
  background: var(--surface-3); color: var(--ink-3); cursor: not-allowed;
}
.erp-inp.ltr, .erp-inp-sm.ltr, .erp-inp-xs.ltr {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
}
/* A Jalali date, an amount, a SKU — these have a known length, and a 1fr
   grid cell handed them a 490px box that reads as a field expecting an
   essay. Persian free-text inputs are unaffected and still fill their cell. */
.erp-inp.ltr, .erp-inp-sm.ltr { max-inline-size: 26ch; }
.erp-jdate { max-inline-size: 16ch; }
/* And a FLOOR, which matters more than the ceiling. Squeezed into a narrow
   table column these collapsed to 45px, so «۱۵,۰۰۰,۰۰۰» rendered as a box
   showing four digits — on payroll/params.php, where the whole point of the
   screen is reading back the figure you typed. A rial amount with its
   separators is about twelve characters; the field is never narrower than
   that, and the table around it scrolls instead. */
.erp-inp.ltr, .erp-inp-sm.ltr, .erp-inp-xs.ltr { min-inline-size: 14ch; }
.erp-inp-sm { block-size: 28px; font-size: .75rem; padding-inline: var(--sp-2); }
.erp-inp-xs { block-size: 26px; font-size: .75rem; padding-inline: 6px; inline-size: auto; }

/* ⚠️ The arrow was WRONG and had been since the redesign — on every select
   on every screen. It was built from the classic two-triangle trick:

     linear-gradient(45deg,  transparent 50%, c 50%)   at  calc(sp-3 + 4px)
     linear-gradient(135deg, c 50%, transparent 50%)   at  sp-3

   That recipe needs the 45° half FIRST and the 135° half 5px AFTER it, the
   two tiles meeting edge to edge. Here the order was reversed and the offset
   was 4px against a 5px tile, so the halves were swapped and overlapped by a
   pixel: what rendered was a broken, upward, notched shape, and at 13px it
   read as a stray glyph sitting in the field rather than as a control.

   One SVG chevron, one background, nothing to misalign. */
.erp-sel {
  appearance: none;
  padding-inline-end: var(--sp-6);
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2010%206'%20fill='none'%20stroke='%234b5563'%20stroke-width='1.5'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M1%201l4%204%204-4'/%3E%3C/svg%3E");
  /* Physical `left`, and deliberately: this shell is RTL, `padding-inline-end`
     put the room on the left, and `background-position` has no logical form
     that is safe to rely on. The rule was already physical before this fix —
     it is called out here so it is a decision and not an oversight. */
  background-position: left var(--sp-3) center;
  background-size: 10px 6px;
  background-repeat: no-repeat;
}
.erp-sel:disabled { background-image: none; }

/* ⚠️ `.erp-textarea` is the MACHINE-FORMAT box — the CSV paste on
   settings/import.php is its only user, and `direction: ltr` + `white-space:
   pre` are right for it: a comma-separated file has columns, and they only
   line up left-to-right and unwrapped. Persian VALUES inside it still read
   correctly; they are strong right-to-left runs and bidi handles them.
   Prose textareas do not wear this class — see the rule under it. */
.erp-textarea {
  block-size: auto; min-block-size: 9rem;
  padding: var(--sp-2) var(--sp-3);
  font-family: var(--font-mono); font-size: .75rem; line-height: 1.6;
  direction: ltr; text-align: left; white-space: pre; overflow: auto;
}

/* ⚠️ …and the other seven textareas in this app wear `.erp-inp`, which is a
   fixed-height CONTROL: `block-size: var(--h-ctl)` — 32px — and `padding: 0`
   on the block axis. Added ۶ شهریور ۱۴۰۵.

   So «یادداشت» on gl/manual.php asks for `rows="2"`, «توضیح» on
   parties/activities.php asks for `rows="3"`, and both render as a 32px slot
   holding one line: measured, at every width. `rows` cannot win — an author
   `block-size` beats the attribute — so the box says «write a paragraph» and
   shows you a sixth of it, and the rest scrolls under the caret while you
   type. These are the fields a شرح سند and a پیگیری get written into.

   The element selector rather than a new class, because the fault is that a
   textarea was given a one-line control's height, and that is true of a
   textarea whatever else it is wearing. `rows` decides the height again,
   which is what the markup was always asking for. */
textarea.erp-inp, textarea.erp-inp-sm, textarea.erp-inp-xs {
  block-size: auto;
  padding-block: var(--sp-2);
  line-height: 1.6;
  /* Vertical only: a textarea dragged wider inside a grid column drags the
     column with it. */
  resize: vertical;
}

/* ── buttons ────────────────────────────────────────────────────────────
   Exactly one primary per view. Everything else is quiet: a toolbar of five
   filled buttons tells a reader nothing about which one to press. */
.erp-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-2);
  block-size: var(--h-ctl);
  padding-inline: var(--sp-4);
  /* NetSuite's buttons are barely rounded — 3px, not 5. Its primary is
     #006bff, which --accent now is, and white on it measures 4.62:1, so the
     sampled colour is used literally rather than darkened. */
  border-radius: var(--r-1);
  border: 1px solid transparent;
  background: var(--accent); color: var(--ink-on-accent);
  font-family: inherit; font-size: .8125rem; font-weight: 600;
  cursor: pointer; white-space: nowrap; text-decoration: none;
  transition: background-color 120ms ease-out, border-color 120ms ease-out;
}
.erp-btn:hover  { background: var(--accent-hover); color: var(--ink-on-accent); text-decoration: none; }
.erp-btn:active { background: var(--accent-press); }
.erp-btn[disabled], .erp-btn:disabled {
  background: var(--surface-3); color: var(--ink-3);
  cursor: not-allowed; border-color: var(--line);
}
.erp-btn-quiet {
  background: var(--surface); color: var(--ink);
  border-color: var(--line-strong);
}
.erp-btn-quiet:hover { background: var(--surface-2); color: var(--ink); border-color: var(--ink-3); }
.erp-btn-quiet:active { background: var(--surface-3); }
.erp-btn-sm { block-size: 28px; padding-inline: var(--sp-3); font-size: .75rem; }
.erp-btn-full { inline-size: 100%; }

/* ═══════════ consequence, which is a second axis ═══════════
   ⚠️ Added ۴ شهریور ۱۴۰۵.

   Until now `.erp-btn` had ONE dimension — prominence: primary, quiet, small,
   full. That is enough for an app where every button is a navigation. It is
   not enough here, because the central distinction this software has to draw
   is between «this filters a list» and «this writes to the ledger forever»,
   and those were the same blue.

   Thirty-six buttons carrying data-erp-confirm wore the primary fill,
   including «این سند ثبت دائم شود؟» and «قفل این فصل؟». The rule they broke
   is not a matter of taste: a prominent button attracts the click BEFORE the
   word is read, which is precisely the read the confirmation exists to force.
   So an irreversible action is never the primary; it is its own thing.

   Colour never speaks alone here either — danger is a fill AND the word AND,
   once armed, a weight change. Three signals, because this is the one button
   in the system where being wrong is expensive. */
.erp-btn-danger {
  background: var(--neg); color: #fff; border-color: transparent;
}
.erp-btn-danger:hover  { background: #9a1e14; color: #fff; }
.erp-btn-danger:active { background: #82190f; }

/* ⚠️ THE ARMED STATE HAD NO APPEARANCE AT ALL. `js/erp.js` has added
   `.is-armed` since the two-press confirm was built, and this stylesheet had
   no rule for it — so «ثبت دائم» became «این سند ثبت دائم شود؟» in the same
   blue, at the same weight, in the same place, and the only thing that
   changed was the sentence. A button that looks identical before and after
   arming is a button that gets pressed twice without being read, which is
   the entire failure the mechanism was built to prevent.

   Armed is now unmistakable: the danger fill regardless of the button's
   resting style, heavier type, and a ring that separates it from the row it
   sits in. The ring is `outline`, not `box-shadow`, so it survives
   forced-colors — see the block at the foot of this sheet. */
.erp-btn.is-armed,
.erp-btn-quiet.is-armed,
.erp-btn-danger.is-armed {
  background: var(--neg); color: #fff;
  border-color: transparent;
  font-weight: 700;
  outline: 2px solid var(--neg);
  outline-offset: 2px;
}
.erp-btn.is-armed:hover { background: #9a1e14; color: #fff; }

/* A control that is a link by nature — «جزئیات», «همه» — and a target, not
   inline text. */
.erp-linkbtn {
  background: none; border: 0; padding: 0;
  color: var(--accent); font: inherit; font-size: .75rem;
  cursor: pointer; text-decoration: none;
}
.erp-linkbtn:hover { text-decoration: underline; text-underline-offset: 2px; }

.erp-inline { display: inline-flex; align-items: center; gap: var(--sp-2); }

/* ── forms ─────────────────────────────────────────────────────────────── */

/* ⚠️ AMENDED ۷ شهریور ۱۴۰۵ — the form itself flows now.
   `.erp-form` used to be a ONE-COLUMN grid, and two fields only shared a line
   when the screen remembered to wrap them in `.erp-field-row`. 52 of the 64
   forms in this app remembered. The other twelve rendered as a single column
   of full-width boxes running down the page: a «کد» field 900px wide holding
   four characters, «مهلت پرداخت (روز)» given a whole row to hold «۳۰», and
   the ثبت button pushed under the fold on a screen with nine fields.
   The row wrapper is now an OPTIMISATION, not a requirement — it survives,
   all 52 screens keep emitting it, and it still means the one thing the bare
   form cannot say: «these belong together, keep them on one line».
   `--fld-min` is the knob: below it a column splits rather than squeezing.

   ⚠️ AMENDED ۵ شهریور ۱۴۰۵ — auto-FILL, not auto-fit, now that the page
   fills the window. They are the same until a form has fewer fields than the
   row has room for, and then they are opposites: auto-fit COLLAPSES the empty
   tracks and stretches what is left, so a three-field form on a 2000px screen
   became three 640px boxes — the «کد field 900px wide holding four
   characters» fault the one-column grid was replaced to fix, arriving from
   the other direction. auto-fill keeps the empty tracks, so a field is the
   width of a field wherever it is, and the leftover is leftover. */
.erp-form {
  --fld-min: 15rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(var(--fld-min), 100%), 1fr));
  gap: var(--sp-3) var(--sp-4);
  align-items: start;
}
.erp-field { display: block; min-inline-size: 0; }
.erp-lbl {
  display: block; margin-block-end: 3px;
  font-size: .75rem; font-weight: 500; color: var(--ink-2);
  /* One line's worth even when empty, so a field with no label still starts
     its control on the same line as the field beside it. */
  min-block-size: 1.15rem;
}

/* Everything that is NOT a field owns the full width. A grid that floats the
   ثبت button into column two beside a text box has learned the wrong lesson
   from the columns, and a warning note narrowed to a third of the form is a
   warning nobody reads.
   ⚠️ INVERTED ۶ شهریور ۱۴۰۵. This used to be a list of nineteen selectors
   naming everything allowed to span, and a hand-maintained allowlist gets one
   thing wrong and then stays wrong quietly.

   What it got wrong: `.erp-sec-h`. The list carried `.erp-form > h2` — but
   `erp_sec()` does not emit a bare <h2>, it emits a <div class="erp-sec-h">
   with the heading inside it, so every section heading inside a form was a
   one-column grid item. And on gl/map.php the same omission caught
   `.erp-gl-future`, which WRAPS A WHOLE LEDGER: measured at 1440, a
   four-column table in a 267px track, 1,426px tall, with its own section
   heading laid out 283px away in a different column of a different row.

   A form's columns exist for FIELDS. Nothing else in a form has ever wanted
   one, and the inventory says so — swept across ten representative forms, the
   only direct children are fields, rows of fields, actions, notes, tables and
   hidden inputs. So the rule states the thing that is actually true and the
   next `<div>` somebody wraps a table in is right by default.

   ⚠️ `:where()` on the second line, and it is load-bearing. `:where()`
   contributes NO specificity, so this pair weighs (0,1,0) — which leaves the
   two rules below it, `.erp-field:has(textarea)` at (0,1,1) and
   `.erp-field.is-wide` at (0,2,0), still able to win. Written as a plain
   `.erp-form > .erp-field` it weighs (0,2,0) and beats `:has(textarea)`,
   and every essay field in the app — «یادداشت», «شرح»، «شرایط» — silently
   goes back to being one 267px column wide. Caught by measuring
   gl/manual.php after making exactly that mistake. */
.erp-form > * { grid-column: 1 / -1; }
.erp-form > :where(.erp-field) { grid-column: auto; }

/* A field holding an essay — a نشانی, a شرح سند, a یادداشت — owns the row.
   Textareas are 9rem tall; beside a 32px input they leave a hole. `.is-wide`
   is the same claim made by hand, for a single-line input whose CONTENT is
   prose or a list: «اصفهان، خیابان فردوسی، نرسیده به چهارراه فلسطین، مجتمع
   حکیم» in a 15rem column reads as «…چهارراه فلس», which looks like a field
   somebody filled in wrong rather than one the layout cut off. */
.erp-field:has(.erp-textarea),
.erp-field:has(textarea),
.erp-field.is-wide { grid-column: 1 / -1; }
/* Spanning the row is a claim about which COLUMN it starts in, not licence to
   be 2000px of input. A نشانی wants about eighty characters; past that the
   box stops looking like a field and starts looking like a page. */
.erp-field.is-wide > .erp-inp,
.erp-field:has(.erp-textarea) > .erp-textarea,
.erp-field:has(textarea) > textarea { max-inline-size: 72rem; }

/* ⚠️ auto-FILL and `--fld-min`, since ۶ شهریور ۱۴۰۵. This selector was
   `repeat(auto-fit, minmax(min(200px, 100%), 1fr))` — the exact construction
   the rule above was rewritten on ۵ شهریور to get rid of, left behind in the
   one place it does the most damage.

   `auto-fit` COLLAPSES the tracks nothing lands in and stretches what is left.
   Measured on settings/kpi.php at 1440: a two-field row reported
   `tracks = 677px 677px 0px 0px 0px 0px`. Four tracks collapsed to nothing and
   «کد» — which holds `my_metric` — was given 677px. On the three-field rows it
   is 447px each, so «ترتیب», whose value is «۱۰۰», got 447px of box. That is
   «the 900px کد box» exactly, and it is on all 52 screens that emit this
   wrapper.

   The `min` matches `.erp-form`'s own `--fld-min` as well, so a field is the
   SAME width whether or not it happens to be inside a row wrapper. That is
   what the wrapper is supposed to mean: «these belong together, keep them on
   one line» — a statement about grouping, never about width. */
.erp-field-row {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fill, minmax(min(var(--fld-min, 15rem), 100%), 1fr));
  align-items: start;
}
.erp-form-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  margin-block-start: var(--sp-2);
}

.erp-check, .erp-checks label, .erp-lbl:has(input[type="checkbox"]) {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: .8125rem; color: var(--ink); cursor: pointer;
}
.erp-checks { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); }
input[type="checkbox"], input[type="radio"] {
  inline-size: 16px; block-size: 16px;
  accent-color: var(--accent);
  cursor: pointer; flex: none;
}

/* The Jalali date field. The weekday under it is the check that catches a
   typo before it becomes a posting date. */
.erp-jdate-wrap { display: block; }
.erp-jdate { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: start; }
.erp-jdate-note { display: block; font-size: .6875rem; color: var(--ink-3); margin-block-start: 2px; }

/* ── filter bars and chips ─────────────────────────────────────────────── */

.erp-rangeform, .erp-rangebar {
  display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: var(--sh-1);
  margin-block-end: var(--sp-3);
}
.erp-rangeform .erp-field, .erp-rangebar .erp-field { inline-size: auto; min-inline-size: 150px; }

.erp-chips, .erp-chips-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1);
}
.erp-chip {
  display: inline-flex; align-items: center;
  padding: 4px var(--sp-3);
  border-radius: var(--r-2);
  border: 1px solid var(--line-strong);
  background: var(--surface); color: var(--ink-2);
  font-size: .75rem; font-weight: 500;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: background-color 120ms ease-out, color 120ms ease-out, border-color 120ms ease-out;
}
a.erp-chip:hover, .erp-chip:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.erp-chip.is-on {
  background: var(--accent); border-color: var(--accent);
  color: var(--ink-on-accent);
}
.erp-chip.is-on:hover { background: var(--accent-hover); color: var(--ink-on-accent); }

.erp-pager {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  margin-block-start: var(--sp-3);
}
.erp-pager-n {
  font-family: var(--font-mono); font-size: .75rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* ═══════════ the line editor ═══════════ */

/* Where a سند's lines are typed. Denser than a reading grid, because it is
   a spreadsheet in all but name and a row here is a keystroke target. */
.erp-lines {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: .8125rem;
}
.erp-lines thead th {
  position: sticky; inset-block-start: 0; z-index: 2;
  background: var(--surface-2);
  padding: var(--sp-2);
  text-align: start; white-space: nowrap;
  font-size: .6875rem; font-weight: 700; color: var(--ink-2);
  border-block-end: 1px solid var(--line-strong);
}
.erp-lines tbody td {
  padding: 3px var(--sp-1);
  border-block-end: 1px solid var(--line);
  vertical-align: middle;
}
.erp-lines tbody tr:hover { background: var(--row-hover); }
.erp-lines .n {
  font-family: var(--font-mono); font-size: .75rem; color: var(--ink-3);
  text-align: end; inline-size: 1%; padding-inline: var(--sp-2);
}
.erp-lines .erp-inp, .erp-lines .erp-sel { inline-size: 100%; }

/* ── the barcode scan box ──────────────────────────────────────────────── */

/* Its own strip, because it is used with a gun in one hand and it must be
   the thing the caret is already in.

   ⚠️ REBUILT ۵ شهریور ۱۴۰۵. It was a tinted band holding a mono field with a
   Latin text direction, from when the only thing it could take was a کد کالا.
   It takes an item NAME now, so it is a search box, and it is dressed as the
   most important control on the busiest screen in the business: white paper,
   a real magnifier, a field big enough to read a cable's full designation in,
   and the keys printed underneath the way پارسیان prints them. */
.erp-scan-box {
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: var(--sh-1);
  margin-block-end: var(--sp-4);
  /* The accent edge. It says «this one» without spending a fill, and it is on
     the BLOCK start so it reads the same in both writing directions. */
  border-block-start: 2px solid var(--accent);
}
.erp-scan-form {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
}
.erp-scan-box > .erp-scan-form { inline-size: 100%; min-inline-size: 0; }

/* The magnifier lives on the wrapper, not in the markup: it is decoration for
   a control that already has a label, and an <img> would be one more thing to
   escape and one more request. */
.erp-scan-form .erp-sug::before {
  content: "";
  position: absolute; z-index: 1;
  inset-block-start: 50%; transform: translateY(-50%);
  inset-inline-start: var(--sp-3);
  inline-size: 16px; block-size: 16px;
  pointer-events: none;
  background-color: var(--ink-3);
  -webkit-mask: var(--icon-search) center / contain no-repeat;
          mask: var(--icon-search) center / contain no-repeat;
}
.erp-scan-inp {
  block-size: 42px;
  font-size: 1rem;
  padding-inline-start: calc(var(--sp-3) + 16px + var(--sp-2));
  border-color: var(--line-strong);
}
.erp-scan-inp:focus { box-shadow: 0 0 0 3px var(--accent-tint); }
/* A scan box that is NOT wrapped in `.erp-sug` — رسید انبار, انبارگردانی, the
   barcode test bench — is a bare flex item in the row, and `.erp-inp` is
   `inline-size: 100%`. It used to be held back by the 26ch cap on `.ltr`;
   that class is gone from these three boxes (a Persian placeholder cannot
   live in an LTR field — see the markup), and without a flex basis of its own
   the input claimed the whole row and pushed «افزودن» onto a second line.
   Same shape as the `.erp-sug` rule above, for the same reason: it grows into
   the row, and it stops before it becomes a horizon. */
/* …and it gets its padding back. `.erp-scan-inp` reserves 36px at the inline
   start for the magnifier, but the magnifier is drawn by `.erp-sug::before`
   and these three boxes have no `.erp-sug` — so they were indenting their own
   placeholder past an icon that is not there. */
.erp-scan-form > .erp-scan-inp {
  flex: 1 1 24rem; max-inline-size: 56rem;
  padding-inline-start: var(--sp-3);
}
.erp-scan-btn { flex: none; block-size: 42px; padding-inline: var(--sp-5); font-size: .875rem; }

/* The key hints. پارسیان prints «F3 = جستجو   Delete = حذف» along the foot of
   every picker it has, and its users read them. Ours says the two things an
   operator has to know about this box and nothing else. */
.erp-scan-keys {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-4);
  margin-block-start: var(--sp-2);
  font-size: .75rem; color: var(--ink-3);
}
/* Which shelf the goods are coming off, at the far end of the field's own
   row. It is there for two reasons at once: it is the fact an operator needs
   before pressing افزودن, and it is what stops the band beside the search box
   being 600px of nothing — پارسیان fills exactly that space with موجودی,
   آخرین قیمت خرید and گروه کالا. */
.erp-scan-wh { margin-inline-start: auto; font-size: .8125rem; color: var(--ink-3); white-space: nowrap; }
.erp-scan-wh b { color: var(--ink); font-weight: 500; }
.erp-scan-keys kbd {
  font-family: var(--font-mono); font-size: .6875rem;
  padding: 1px 5px; margin-inline-end: 4px;
  border: 1px solid var(--line-strong); border-block-end-width: 2px;
  border-radius: var(--r-1);
  background: var(--surface-2); color: var(--ink-2);
}

/* ═══════════ the command palette ═══════════

   ⚠️ NEW ۵ شهریور ۱۴۰۵. پارسیان prints Ctrl+F, Ctrl+S, Ctrl+G, F3, F2 beside
   every entry in every menu it has, and its operators navigate on those keys
   rather than on the mouse. Sixty pages behind thirteen drawers cannot each
   have a chord worth memorising, so the chord is ONE — F3, because that is
   the key these hands already reach for — and it opens a box that searches
   all of them by their Persian names.

   ⚠️ It said «Ctrl+K, and F3» until ۴ شهریور ۱۴۰۵. Ctrl+K is Chrome's «search
   from the address bar» and Firefox's «focus the search bar», and this
   project's own key law forbids taking a chord the browser owns — it is why
   F2 is unbound and why no Ctrl+letter is taken. One was, quietly, as an
   alias for people who had learned it elsewhere. It is gone.

   <dialog>, not a div: ::backdrop, Esc, the focus trap and the inertness of
   everything behind it are the platform's, already correct, and free. */

.erp-pal {
  inline-size: min(44rem, 94vw);
  max-block-size: min(32rem, 78vh);
  margin: 10vh auto auto;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-3);
  background: var(--surface);
  box-shadow: var(--sh-2), 0 24px 48px rgba(16,26,38,.18);
  color: var(--ink);
  overflow: hidden;
}
.erp-pal::backdrop { background: rgba(16,26,38,.42); }
.erp-pal-form { position: relative; border-block-end: 1px solid var(--line); }
.erp-pal-form::before {
  content: "";
  position: absolute; inset-block-start: 50%; transform: translateY(-50%);
  inset-inline-start: var(--sp-4);
  inline-size: 17px; block-size: 17px;
  pointer-events: none;
  background-color: var(--ink-3);
  -webkit-mask: var(--icon-search) center / contain no-repeat;
          mask: var(--icon-search) center / contain no-repeat;
}
.erp-pal-inp {
  inline-size: 100%; block-size: 52px;
  padding-inline: calc(var(--sp-4) + 17px + var(--sp-3)) var(--sp-4);
  border: 0; border-radius: 0;
  font-family: inherit; font-size: 1rem; color: var(--ink);
  background: transparent;
}
.erp-pal-inp:focus { outline: none; }
.erp-pal-list {
  list-style: none; margin: 0; padding: var(--sp-1) 0;
  max-block-size: calc(min(32rem, 78vh) - 52px - 30px);
  overflow-y: auto; overscroll-behavior: contain;
}
.erp-pal-opt {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 8px var(--sp-4);
  cursor: pointer;
  border-inline-start: 3px solid transparent;
}
/* The module's own colour, as a dot. Thirteen colours already mean thirteen
   things everywhere else in this app; the palette does not get its own. */
.erp-pal-opt::before {
  content: ""; flex: none;
  inline-size: 15px; block-size: 15px;
  background-color: var(--g, var(--ink-3));
  -webkit-mask: var(--icon) center / contain no-repeat;
          mask: var(--icon) center / contain no-repeat;
}
.erp-pal-opt:hover { background: var(--surface-2); }
.erp-pal-opt.is-on { background: var(--accent-tint); border-inline-start-color: var(--accent); }
.erp-pal-nm { flex: 1 1 auto; min-inline-size: 0; font-size: .875rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The module band. پارسیان's picker prints a tinted strip per module with its
   commands under it, and that is what makes a list of sixty rows readable at
   a glance instead of a wall. The colour is the module's own — the same one
   the rail, the tile and the dot already carry. */
.erp-pal-hd {
  position: sticky; inset-block-start: 0; z-index: 1;
  padding: 5px var(--sp-4);
  background: var(--g-tint, var(--surface-3));
  color: var(--g, var(--ink-2));
  font-size: .6875rem; font-weight: 700;
  border-block: 1px solid var(--line);
  border-block-start-width: 0;
}
.erp-pal-hd[hidden] { display: none; }
.erp-pal-opt[hidden] { display: none; }
/* Banding, like the picker's own cream/white rows: it is what lets an eye
   cross a wide row to the key printed at the far end of it. */
.erp-pal-opt:nth-of-type(even) { background: var(--surface-2); }
.erp-pal-opt:nth-of-type(even):hover { background: var(--surface-3); }
/* ⚠️ After the band, and it has to be: `.erp-pal-opt.is-on` and
   `.erp-pal-opt:nth-of-type(even)` weigh exactly the same, so on an even row
   the stripe would win on source order alone and «which one will Enter take»
   would be invisible on every second line. */
.erp-pal-opt.is-on, .erp-pal-opt.is-on:hover {
  background: var(--accent-tint);
  border-inline-start-color: var(--accent);
}
.erp-pal-key {
  flex: none; font-family: var(--font-mono); font-size: .6875rem;
  padding: 1px 5px; border-radius: var(--r-1);
  background: var(--surface-3); color: var(--ink-2);
}
.erp-pal-foot {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: 6px var(--sp-4);
  border-block-start: 1px solid var(--line);
  background: var(--surface-2);
  font-size: .6875rem; color: var(--ink-3);
}
.erp-pal-foot kbd {
  font-family: var(--font-mono); font-size: .625rem;
  padding: 0 4px; margin-inline-end: 4px;
  border: 1px solid var(--line-strong); border-radius: var(--r-1);
  background: var(--surface); color: var(--ink-2);
}
.erp-pal-none { padding: var(--sp-5) var(--sp-4); text-align: center; color: var(--ink-3); font-size: .8125rem; }

/* ═══════════ the suggestion list ═══════════

   A box that searches the master while you type. It hangs off .erp-sug, which
   wraps the input and does one job — give the absolutely-positioned list
   something to be positioned against.

   ⚠️ inset-inline, inset-block-start. Never left/top: this app is RTL and a
   list pinned to `left: 0` would hang off the wrong end of every box in it.

   The list is an OVERLAY, so it takes --sh-2 and not --sh-1, and it is NOT a
   card: no padding of its own beyond the rows, no nested surface. A card
   inside a card is always wrong and a card floating over one is worse. */

.erp-sug { position: relative; display: block; min-inline-size: 0; }
/* THE control of فروش حضوری — it takes a whole item name now, not a code,
   and it is the only thing on that strip. Capped anyway: at 1440 an
   uncapped one grew to 1041px, which is not a field, it is a horizon. */
.erp-scan-form .erp-sug { flex: 1 1 24rem; max-inline-size: 56rem; }
/* In a filter bar it is as wide as what it HOLDS — a customer's name — and
   not as wide as the bar. The law that rule comes from is in CLAUDE.md. */
.erp-rangebar .erp-sug, .erp-rangeform .erp-sug { flex: 0 1 22rem; }

/* A box that takes BOTH a Latin barcode and a Persian name.

   .ltr is the wrong tool here and was: it forces direction:ltr, and it caps
   the field at 26ch — the width of a کد کالا and about a third of what a real
   item name needs.

   ⚠️ AMENDED ۵ شهریور ۱۴۰۵. `direction: ltr` was wrong too, and visibly: an
   EMPTY box has no content to take a direction from, so the Persian
   placeholder «بارکد را بزنید…» was laid out left-to-right and sat against
   the far edge with its ellipsis on the wrong side — the first thing anybody
   sees on this screen, reading backwards. The base direction is the page's
   own now; `unicode-bidi: plaintext` still reads the direction from the first
   strong character of what was actually TYPED, so a scanned barcode is still
   laid out and caret-navigated left-to-right. Both cases, one declaration. */
.erp-inp-bidi {
  direction: rtl;
  unicode-bidi: plaintext;
  text-align: start;
  max-inline-size: none;
}

.erp-sug-list {
  position: absolute;
  inset-inline: 0;
  /* Wider than the box it hangs off when there is room for it: an item name
     with its designation is longer than the field, and a picker that has to
     truncate what it is offering is a picker you cannot choose from. */
  min-inline-size: min(46rem, 92vw);
  inset-block-start: calc(100% + 4px);
  z-index: 40;
  margin: 0; padding: var(--sp-1) 0;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-2);
  box-shadow: var(--sh-2);
  max-block-size: 19rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.erp-sug-list[hidden] { display: none; }

/* ⚠️ A ROW IN A PICKER IS A ROW IN A GRID, ۵ شهریور ۱۴۰۵.
   These were flex-wrapped in DOM order — name, code, tag, on-hand — so the
   code sat wherever the name before it happened to end and no two rows lined
   up. پارسیان's item picker is a real grid with کد کالا, عنوان کالا and واحد
   in fixed columns, and the reason you can find a cable in it at a glance is
   that the codes form a column.
   `order` puts them in the picker's order without touching js/erp.js, which
   builds the row in the order the JSON happens to carry. */
.erp-sug-opt {
  display: flex; align-items: center;
  gap: var(--sp-3);
  padding: 7px var(--sp-3);
  cursor: pointer;
  /* Reserved, not added on .is-on — a border that appears when a row is
     highlighted shifts every word in it by 3px as the arrow key moves. */
  border-inline-start: 3px solid transparent;
}
/* One highlight for both the pointer and the arrow keys, plus the bar above.
   Colour never speaks alone, and «which one will Enter take» is exactly the
   question that must not depend on seeing a tint. box-shadow has no logical
   form; a border does, and this app is RTL. */
/* Banded, like پارسیان's own item picker — its rows alternate and that is
   what lets an eye run from «سیم افشان ۱×۲٫۵» all the way across to the «۸۶۰
   متر» pinned at the far end of the row. Without it this list has the canyon
   the grid had, in miniature: measured at 700px between a name and its own
   on-hand figure. */
.erp-sug-opt:nth-of-type(even) { background: var(--surface-2); }
.erp-sug-opt:hover, .erp-sug-opt:nth-of-type(even):hover { background: var(--surface-3); }
/* ⚠️ After the band and after the hover, both of which weigh the same as it.
   «Which one will Enter take» is the one thing on this list that must never
   be decided by source order. */
.erp-sug-opt.is-on, .erp-sug-opt.is-on:hover {
  background: var(--accent-tint);
  border-inline-start-color: var(--accent);
}

.erp-sug-nm {
  order: 2; flex: 1 1 auto; min-inline-size: 0;
  font-size: .8125rem; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.erp-sug-cd {
  order: 1; flex: none;
  inline-size: 9.5rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-mono); font-size: .75rem; color: var(--ink-3);
  direction: ltr; unicode-bidi: isolate; text-align: start;
}
.erp-sug-tag { order: 3; }
/* The shelf figure, pinned to the far end where a number belongs, and given
   the ledger's own tabular treatment so a column of them is a column. */
.erp-sug-mt {
  order: 4; flex: none;
  margin-inline-start: auto;
  font-size: .75rem; color: var(--ink-2); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* «۰ متر» is a fact about the shelf, and it is said in words first — the
   figure and its unit are there to be read. The colour is the second signal. */
.erp-sug-mt.is-zero { color: var(--neg); }
.erp-sug-tag {
  font-size: .6875rem; color: var(--neutral);
  background: var(--neutral-tint);
  border-radius: var(--r-1);
  padding: 1px 6px;
}

.erp-sug-note {
  padding: var(--sp-2) var(--sp-3);
  font-size: .75rem; color: var(--ink-3);
}

/* ═══════════ module map ═══════════ */

.erp-mods {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
}
.erp-mod {
  display: block;
  padding: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: var(--sh-1);
  color: var(--ink); text-decoration: none;
  transition: border-color 120ms ease-out, box-shadow 120ms ease-out;
}
a.erp-mod:hover {
  border-color: var(--accent); box-shadow: var(--sh-2);
  text-decoration: none; color: var(--ink);
}
.erp-mod__n { display: block; font-weight: 700; font-size: .9375rem; margin-block-end: 2px; }
.erp-mod__d { display: block; color: var(--ink-2); font-size: .8125rem; line-height: 1.55; }
.erp-mod__s {
  display: inline-block; margin-block-start: var(--sp-2);
  font-family: var(--font-mono); font-size: .75rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.erp-mod.is-soon { opacity: .6; }

/* ═══════════ odds and ends the screens name ═══════════ */

.erp-code {
  font-family: var(--font-mono); font-size: .75rem;
  padding: 1px 5px; border-radius: var(--r-1);
  background: var(--surface-3); color: var(--ink-2);
  direction: ltr; unicode-bidi: isolate;
}
.erp-u { color: var(--ink-3); font-size: .75rem; }
.erp-ul { list-style: none; padding: 0; margin: 0; }
.erp-ul li { padding-block: 2px; }
.erp-bar {
  block-size: 6px; border-radius: 3px;
  background: var(--surface-3); overflow: hidden;
}
.erp-bar > span { display: block; block-size: 100%; background: var(--accent); }
.erp-scope { font-size: .75rem; color: var(--ink-3); }
/* ⚠️ The <details> wrapper is CHROME, not payload. Amended ۶ شهریور ۱۴۰۵.
   It used to carry `direction: ltr` and `word-break: break-all`, and both
   inherited into the two places they had no business being: the <summary>,
   whose text is «مقدار کامل» — a Persian phrase, laid out left-to-right and
   free to break in the middle of a word — and, more quietly, into `.erp-pre`
   below, where `break-all` beat that rule's own `break-word` and shredded any
   Persian inside the payload. A طرف‌حساب's name in a JSON diff is exactly the
   thing somebody is reading an audit row to check.
   The LTR run is the payload. It is `.erp-pre` that says so. */
.erp-raw {
  font-family: var(--font-mono); font-size: .6875rem; color: var(--ink-3);
}
.erp-pre {
  margin-block-start: var(--sp-2); padding: var(--sp-3);
  border-radius: var(--r-2);
  background: var(--surface-2); border: 1px solid var(--line);
  font-family: var(--font-mono); font-size: .75rem; line-height: 1.6;
  white-space: pre-wrap; word-break: break-word;
  max-block-size: 20rem; overflow: auto;
}

/* The publish/verify flag rows. */
.erp-flags { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.erp-flag {
  display: inline-flex; align-items: baseline; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-2);
  background: var(--surface-2); border: 1px solid var(--line);
}
.erp-flag__l { font-size: .75rem; color: var(--ink-3); }
.erp-flag__n { font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; }
.erp-flag__u { font-size: .6875rem; color: var(--ink-3); }
.erp-flag.is-ok  { background: var(--pos-tint);  border-color: #b9e2cd; }
.erp-flag.is-act { background: var(--warn-tint); border-color: #f0ddb8; }

/* Legend swatches — kept for the charts a later phase draws. */
.l1, .l2 { display: inline-block; inline-size: 10px; block-size: 10px; border-radius: 2px; }
.l1 { background: var(--s1); }
.l2 { background: var(--s2); }

/* ═══════════ the status bar ═══════════ */

/* ⚠️ NEW ۵ شهریور ۱۴۰۵, and it replaces a paragraph of prose.
   The old footer said «داده‌ها روی سرور خودمان می‌ماند…» — true, and a sentence
   nobody reads twice. Every application these people have ever used for this
   work carries a STATUS BAR along the bottom instead: which server, which
   user, what today is, which fiscal year the figures above are scoped to.
   پارسیان's is the last thing on its screen and its operators read it all day,
   because «سال مالی ۱۴۰۵» is the answer to «why is this figure zero» more
   often than anything else in the software.

   It is pinned, so it is true about the screen you are looking at rather than
   about the bottom of a document you have to scroll to. */
.erp-status {
  position: sticky; inset-block-end: 0; z-index: 50;
  margin-block-start: auto;
  block-size: var(--status-h);
  background: var(--shell-bg);
  color: var(--shell-fg-dim);
  border-block-start: 1px solid var(--shell-line);
  /* ⚠️ Added ۴ شهریور ۱۴۰۵. `manifest.webmanifest` is `display: standalone`,
     so this app installs and runs with no browser chrome — and a 1024px
     counter tablet IS an iPad. Sticky to the block end with no inset, this
     bar lands underneath the home indicator. That is not a cosmetic loss:
     this bar is the render sentinel `smoke.php` and `sweep.mjs` both assert,
     and it is the only place on a signed-in page carrying «بانک برق اصفهان»,
     which the copy law requires wherever the brand appears. Pairs with
     `viewport-fit=cover` in _bootstrap.php — the inset resolves to 0 on
     every desktop, so this costs nothing anywhere else. */
  box-sizing: content-box;
  padding-block-end: env(safe-area-inset-bottom, 0px);
}
.erp-status-in {
  block-size: 100%;
  padding-inline: var(--gutter);
  display: flex; align-items: center; gap: 0;
  font-size: .6875rem;
  white-space: nowrap; overflow: hidden;
}
/* Each fact in its own compartment, divided by a rule — the shape every
   status bar in this class of software has, and the reason four facts on one
   line do not read as one sentence. */
.erp-status-i {
  display: flex; align-items: baseline; gap: var(--sp-2);
  padding-inline: var(--sp-3);
  border-inline-end: 1px solid var(--shell-line);
  min-inline-size: 0;
}
.erp-status-i:first-child { padding-inline-start: 0; }
.erp-status-i b { font-weight: 500; color: var(--shell-fg-dim); opacity: .78; }
.erp-status-i span { color: var(--shell-fg); font-variant-numeric: tabular-nums; }
.erp-status-i.is-env span { color: #fbbf24; }
/* ⚠️ Load-bearing, not tidiness: `.erp-status-i` is `display: flex`, and a
   class selector beats the UA's `[hidden] { display: none }`. Without this
   line the unsaved-work compartment would be visible on every page from the
   first paint, announcing that a screen nobody has touched is unsaved —
   which is worse than not having it, because a warning that is always on is
   a warning nobody reads. */
.erp-status-i[hidden] { display: none; }
/* Amber, and the WORD «ذخیره‌نشده» — colour never speaks alone, and this is
   the compartment most likely to be read out of the corner of an eye. */
.erp-status-i.is-dirty b { color: #fbbf24; opacity: 1; }
.erp-status-i.is-dirty span { color: #fbbf24; font-weight: 500; }
/* Whose system this is. The label is the brand and the value is the registered
   name, which is the pairing the copy law requires and the order a status bar
   reads in. */
.erp-status-i.is-brand b { color: var(--shell-fg); font-weight: 700; opacity: 1; }
.erp-status-i.is-brand span { color: var(--shell-fg-dim); }
/* The keys, pushed to the far end. A hint, not a control: it is there to be
   noticed once. */
.erp-status-keys {
  margin-inline-start: auto;
  padding-inline-start: var(--sp-3);
  border-inline-end: 0;
  display: flex; align-items: center; gap: var(--sp-2);
  overflow: hidden;
  /* ⚠️ `flex: none` added ۴ شهریور ۱۴۰۵, and the bug it fixes is one nobody
     would find by reading: at 820px this compartment was rendering 125px wide
     wanting 129, so «F3 جست‌وجوی صفحه‌ها» lost its last glyphs — on every
     page in the application. The cause is not here at all, it is the سرور
     compartment: it prints APP_ENV and the DATABASE NAME, so its width is a
     deployment fact. A longer database name silently eats the key hint.
     The hint is the only place the palette is advertised, so it is the last
     thing that should give way.

     ⚠️ And the slack may NOT come from the سرور compartment, however
     tempting: the law at the 48rem breakpoint below says the environment
     banner «may not be shortened, hidden or ellipsised — it is the answer to
     «which database did I actually reach», and a truncated answer to that is
     worse than none». That reasoning is about the command bar's banner and it
     applies word for word to this compartment, which prints the same two
     facts. An ellipsis here would turn `hb_erp_isolated` into
     `hb_erp_isola…`, and «which database» answered to nine characters is
     exactly the truncated answer the law refuses.

     So the slack comes from the brand instead — see the 64rem block. */
  flex: none;
}
.erp-status kbd {
  font-family: var(--font-mono); font-size: .625rem;
  padding: 0 4px;
  border: 1px solid var(--shell-line); border-radius: var(--r-1);
  background: var(--shell-bg-2); color: var(--shell-fg);
}
/* ⚠️ Added ۴ شهریور ۱۴۰۵. Between 48rem and roughly 60rem the bar was ONE
   PIXEL over its usable width — 781px of compartments inside 820 minus two
   20px gutters — and a flex row that is one pixel over does not fail by one
   pixel: it shrinks whichever item will give, and that item clipped mid-word.
   Measured on every page in the application at 820px.

   The brand is what gives way, because it is the only compartment here whose
   job is identity rather than fact, and because the copy law is satisfied
   either way: «های‌برق» and «بانک برق اصفهان» live in the same compartment,
   so they leave together and the brand is never shown without its registered
   name. سرور keeps its full answer, کاربر and سال مالی keep theirs, and the
   key hint keeps the only advertisement the palette has.

   This also buys the headroom production actually needs: the سرور compartment
   prints a database name, so its width is a deployment fact, and a name a few
   characters longer than the one this was measured against would otherwise
   have re-broken the same bar on a live server with nothing to explain it. */
@media (max-width: 64rem) {
  /* ⚠️ THIS BLOCK USED TO TURN THE VERTICAL RAIL INTO A HORIZONTAL STRIP.
     The rail is a menu bar at every width now, so almost all of it is gone —
     including `.erp-rail-grp { display: contents }`, which dissolved the
     <details> so its summary and links became flex children of the bar. That
     was right for a strip of nine labels and is catastrophic for a menu bar:
     it would spill all sixty destinations into one scrolling row.

     What the narrow case needs instead is nothing clever. The bar already
     scrolls on its own axis, the panels already cap at 92vw, and a menu is a
     menu at 390px exactly as it is at 1440. Only the touch target and the
     panel's reach change. */
  .erp-rail-g { block-size: 44px; }
  .erp-rail-a { padding-block: 10px; }
  /* Kept from the strip this block replaced: the row icon is the first thing
     to go when a panel is the width of a phone. The group's own icon stays,
     because in a scrolling bar it is what a thumb aims at. */
  .erp-rail-a::after { display: none; }

  /* ⚠️ The brand compartment leaves the status bar here. Between 48rem and
     roughly 60rem the bar was ONE PIXEL over its usable width — 781px of
     compartments inside 820 minus two 20px gutters — and a flex row that is
     one pixel over does not fail by one pixel: it shrinks whichever item will
     give, and that item clipped mid-word. It was eating «F3 جست‌وجوی
     صفحه‌ها» on every page in the application.

     The brand is what gives way, because it is the only compartment whose job
     is identity rather than fact, and because the copy law is satisfied
     either way: «های‌برق» and «بانک برق اصفهان» share one compartment, so
     they leave together and the brand is never shown without its registered
     name. The slack may NOT come from سرور — see the 48rem block below, where
     the law says the environment banner may never be shortened or ellipsised,
     because a truncated answer to «which database did I reach» is worse than
     none. It also buys the headroom production needs: that compartment prints
     a database name, so its width is a deployment fact. */
  .erp-status-i.is-brand { display: none; }
  /* ⚠️ A panel here stays `position: absolute`, and the reason is worth
     recording because the obvious alternative is a trap.

     `position: fixed` looks right for a phone — pin the panel to the viewport
     and let it use the full width. It breaks the <details> element itself. A
     CLOSED <details> hides its content through a slot in its UA shadow tree,
     and a fixed child is taken out of flow and escapes that hiding: measured
     at 390px with every menu shut, all thirteen panels were still laid out,
     53px wide, stacked under the bar, their links reporting 51px of box for
     62px of text. The navigator was rendering its entire contents invisibly
     and the clipped-text gate found it.

     Absolute keeps the panel a normal in-flow-ish child that the slot can
     hide. All the narrow case needs on top of that is room: no ancestor may
     scroll (see .erp-rail-in) and the panel may run to the viewport's edge. */
  .erp-rail-list { max-inline-size: min(22rem, calc(100vw - var(--sp-4))); }
  .erp-main { padding-block-start: var(--sp-4); }
}

@media (max-width: 48rem) {
  .erp-status-i.is-opt { display: none; }
  .erp-status-keys { display: none; }
}

/* ═══════════ login ═══════════ */

.erp-login-body {
  min-block-size: 100vh;
  display: grid; place-items: center;
  padding: var(--sp-5);
  background: var(--shell-bg);
}
.erp-login { inline-size: 100%; max-inline-size: 380px; }
/* Two paragraphs of explanation, not a password box. offline.html is the one
   page that uses it. */
.erp-login.is-wide { max-inline-size: 34rem; }
.erp-login-card {
  background: var(--surface);
  border-radius: var(--r-3);
  box-shadow: var(--sh-2);
  padding: var(--sp-6);
}
.erp-login-head { text-align: center; margin-block-end: var(--sp-5); }
.erp-login-head .erp-logo { block-size: 26px; inline-size: auto; margin-inline: auto; }
.erp-login-title { font-size: 1.125rem; font-weight: 700; margin-block-start: var(--sp-3); }
.erp-login-sub { font-size: .8125rem; color: var(--ink-3); margin-block-start: 2px; }

.erp-403 {
  max-inline-size: 520px; margin: 15vh auto; padding: var(--sp-5);
  text-align: center;
}
.erp-legal-page { max-inline-size: 76ch; margin-inline: auto; }

/* ═══════════ input method ═══════════ */

/* Screen size does not tell you input method. A counter tablet is 1024px
   wide and has no mouse; a laptop has a touchscreen. Grow the targets from
   the POINTER, not from the viewport. */
@media (pointer: coarse) {
  .erp-btn, .erp-btn-sm { min-block-size: var(--tap); }
  .erp-inp, .erp-sel, .erp-inp-sm, .erp-inp-xs { min-block-size: var(--tap); }
  .erp-rail-a { min-block-size: var(--tap); display: flex; align-items: center; }
  .erp-chip { min-block-size: 36px; }
  input[type="checkbox"], input[type="radio"] { inline-size: 20px; block-size: 20px; }
  .erp-check, .erp-checks label, .erp-lbl:has(input[type="checkbox"]) { min-block-size: var(--tap); }
  /* A few characters of type on a shared baseline are still a target. Grown
     with padding and taken straight back out with margin, so the baseline
     they share with the heading does not move.
     ⚠️ BLOCK axis only, since ۶ شهریور ۱۴۰۵. It used to take the INLINE
     padding back out too, and a negative inline margin on the last item of a
     `space-between` flex row does not tidy anything up — it pushes the link
     8px past its own container. Measured on five dashboard portlets and two
     section heads: «گزارش همکاران» hanging 8px into the card's padding while
     the heading opposite it sat on the line, which reads as a portlet whose
     right-hand link is falling off. The target's WIDTH was never the problem
     — every one of these links is a whole word — so the inline padding buys
     nothing and is dropped rather than cancelled. */
  .erp-sec-h a, .erp-card-h a, .erp-linkbtn {
    padding-block: 10px; margin-block: -10px;
    display: inline-flex; align-items: center;
  }
  /* The grid stays dense — this is a working tool and a counter tablet still
     has to show twenty rows — but a tapped row gets room to be tapped. */
  .erp-tbl tbody td { block-size: 42px; }
  .erp-tbl a { padding-block: 6px; display: inline-block; }
}

/* ═══════════ small screens ═══════════ */

/* The rail lies down and becomes a scrolling module strip under the command
   bar. The group labels stay: they are the only thing telling a reader that
   «خرید» and «انبار» answer two different questions. */


@media (max-width: 48rem) {
  :root { --gutter: 14px; }
  .erp-top-in { gap: var(--sp-2); }
  .erp-brand { padding-inline-end: var(--sp-2); border-inline-end: 0; }
  .erp-brand-sub { display: none; }
  /* The environment banner may not be shortened, hidden or ellipsised: it is
     the answer to «which database did I actually reach», and a truncated
     answer to that is worse than none. So the command bar grows a line
     instead. Staging only — production never renders it. */
  .erp-top { block-size: auto; min-block-size: var(--top-h); }
  .erp-top-in { flex-wrap: wrap; padding-block: var(--sp-2); row-gap: var(--sp-2); }
  .erp-rail { inset-block-start: 0; }
  .erp-h h1 { font-size: 1.1875rem; }
  .erp-field-row { grid-template-columns: 1fr; }
  .erp-mods { grid-template-columns: 1fr; }
  .erp-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Only here. A two-column strip on a phone makes a ~160px tile, which is
     where «میانگین سفارش فروشگاه» genuinely wraps to two lines — and a row
     whose figures sit at two different heights is a row you cannot read
     across. On a wide tile the label never wraps and the same floor is just
     dead air above every figure, which is what it had become. */
  .erp-kpi-l { min-block-size: 2.8em; }
  .erp-kpi-v { font-size: 1.25rem; }
  .erp-kpi-v.is-money { font-size: 1rem; }
  /* THE UNIT TAKES ITS OWN LINE, for every cell rather than for whichever
     ones happen not to fit. A row where «ریال» sits inline in one cell and
     wrapped in the next is the tell of a layout that was tuned instead of
     decided — and the figures stop aligning across the row. */
  .erp-kpi-u { display: block; margin-inline-start: 0; }
  .erp-card { padding: var(--sp-3); }
}

/* The narrowest phone. A rial total is unbreakable text — a number split
   across two lines is a different number — so the tiles give up their second
   column rather than the figure giving up its digits. */
@media (max-width: 23rem) {
  .erp-kpis { grid-template-columns: 1fr; }
}

/* ═══════════ motion ═══════════ */

/* Everything that moves in here is a colour settling under a pointer. Under
   reduced-motion the colour still changes — instantly — so no state is lost;
   only the travel goes. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
}

/* ═══════════ print ═══════════ */

/* Paper is where this system is finally judged. A فاکتور, a فیش حقوقی, a
   تراز — these get printed, argued about and filed for years, and browsers
   drop background colours from a print by default. So every tone step in
   this sheet prints as nothing at all, and the rules have to come back. */
@media print {
  :root { --gutter: 0; }
  body { background: #fff; color: #000; font-size: 10pt; }
  .erp-top, .erp-rail, .erp-status, .erp-skip,
  .erp-form-actions, .erp-rangeform, .erp-rangebar,
  .erp-chips, .erp-chips-row, .erp-pager, .erp-scan-box, .erp-sug-list,
  .erp-pal, .erp-scan-keys,
  .erp-btn, .erp-linkbtn { display: none !important; }

  .erp-shell { display: block; max-inline-size: none; }
  .erp-main  { padding: 0; }

  .erp-card, .erp-kpi, .erp-tbl-wrap {
    box-shadow: none;
    border: 0.5px solid #999;
    break-inside: avoid;
  }
  .erp-tbl { inline-size: 100%; }
  .erp-tbl thead th {
    position: static;
    background: #fff; border-block-end: 1pt solid #000;
    color: #000;
  }
  .erp-tbl tbody td { border-block-end: 0.5px solid #bbb; block-size: auto; padding-block: 3pt; }
  .erp-tbl tr.is-total td { border-block-start: 1pt solid #000; }
  /* A pill's meaning is its word; on paper the fill is gone, so the word
     gets a rule around it to stay a marked state rather than stray type. */
  .erp-pill { border: 0.5px solid #666; color: #000; background: none; }
  .erp-alert, .erp-flash { border: 0.5px solid #666; color: #000; background: none; }
  a { color: #000; text-decoration: none; }
  thead { display: table-header-group; }
  tr, img { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }
  .erp-print-only { display: block !important; }
}

.erp-print-only { display: none; }

/* A screen route that renders a printable document. Mirrors @media print's
   shell rules so «چاپ فاکتور» previews what the paper will say. */
.erp-print-view .erp-rail,
.erp-print-view .erp-status { display: none; }
.erp-print-view .erp-shell { display: block; }

/* ═══════════ the rail, colour-coded by module ═══════════ */

/* Parsian puts a saturated colour tile behind every action, and that — not
   its typography — is why its users find things without reading. A sixty-item
   navigator cannot be sixty tiles, so the colour moves onto the ICON and the
   active marker, and the tile shape is kept for the launcher on the home
   screen where there are twelve of them and they can be big.

   The icon is a MASK, not an image, so one file's colour follows --g and the
   active/hover states can move it without a second asset. */

/* ⚠️ THE ICONS ARE OFF AS OF ۵ شهریور ۱۴۰۵ — NetSuite. Everything the
   paragraph above says is still true, and it is not what was asked for:
   NetSuite's menu band is bold white words on slate with no glyph anywhere
   in it, and its dropdowns are plain text lists. Both are turned off HERE,
   in one place, rather than by deleting the thirteen `--icon` payloads
   below — those are still what the launcher tiles draw, and re-deriving
   thirteen hand-tuned SVGs is not work anybody should have to redo to undo
   a styling decision. Flip these two `content` values to `""` and the
   colour-coded navigator is back exactly as it was.

   What it costs, stated plainly: the pre-attentive cue is gone from the
   navigator. Colour-by-function survives on the launcher — which is where
   NetSuite puts its own colour, and where there are twelve big targets
   rather than sixty small ones. */
.erp-rail-g { display: flex; align-items: center; gap: var(--sp-2); }
.erp-rail-g::before { content: none; }

.erp-rail-a { display: flex; align-items: center; gap: var(--sp-2); }
.erp-rail-a::after { content: none; }
.erp-rail-a.is-on { background: var(--accent-tint); color: var(--accent); }
.erp-rail-a.is-on::before { background: var(--accent); }

/* The chord, at the far end of the row it belongs to — where پارسیان prints
   it. Quiet: it is a fact about the row, not a second link on it, and an
   operator who does not use keys should be able to read past it. */
.erp-rail-key {
  margin-inline-start: auto;
  font-style: normal;
  font-family: var(--font-mono); font-size: .625rem;
  padding: 0 4px;
  border: 1px solid var(--line); border-radius: var(--r-1);
  background: var(--surface-2); color: var(--ink-3);
}
.erp-rail-a.is-on .erp-rail-key { border-color: currentColor; color: inherit; background: transparent; }

[data-g="home"] { --g: #475569; --g-tint: color-mix(in srgb, #475569 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M3%209.5%2010%204l7%205.5V16a1%201%200%200%201-1%201h-3.5v-4.5h-5V17H4a1%201%200%200%201-1-1z'/%3E%3C/svg%3E"); }
[data-g="sales"] { --g: #0b57d0; --g-tint: color-mix(in srgb, #0b57d0 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M3%205h2l1.6%208.2a1%201%200%200%200%201%20.8h6.8a1%201%200%200%200%201-.8L17%207H6'/%3E%3Ccircle%20cx='8.5'%20cy='16.5'%20r='1.2'/%3E%3Ccircle%20cx='14.5'%20cy='16.5'%20r='1.2'/%3E%3C/svg%3E"); }
[data-g="buy"] { --g: #7c3aed; --g-tint: color-mix(in srgb, #7c3aed 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M5%203h7l4%204v10a1%201%200%200%201-1%201H5a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1z'/%3E%3Cpath%20d='M12%203v4h4'/%3E%3Cpath%20d='M7%2011h6M7%2014h4'/%3E%3C/svg%3E"); }
[data-g="party"] { --g: #0e7490; --g-tint: color-mix(in srgb, #0e7490 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='7.5'%20cy='7'%20r='2.6'/%3E%3Cpath%20d='M3%2017c0-2.5%202-4.2%204.5-4.2S12%2014.5%2012%2017'/%3E%3Ccircle%20cx='14.5'%20cy='8'%20r='2.1'/%3E%3Cpath%20d='M13%2012.9c2.2-.3%204%201.2%204%203.4'/%3E%3C/svg%3E"); }
[data-g="stock"] { --g: #b45309; --g-tint: color-mix(in srgb, #b45309 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M3%207%2010%203.5%2017%207v7L10%2017.5%203%2014z'/%3E%3Cpath%20d='M3%207l7%203.5L17%207M10%2010.5v7'/%3E%3C/svg%3E"); }
[data-g="cash"] { --g: #0a7a4b; --g-tint: color-mix(in srgb, #0a7a4b 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Crect%20x='2.5'%20y='5.5'%20width='15'%20height='9'%20rx='1.5'/%3E%3Ccircle%20cx='10'%20cy='10'%20r='2.2'/%3E%3Cpath%20d='M5.5%2010h.01M14.5%2010h.01'/%3E%3C/svg%3E"); }
[data-g="gl"] { --g: #334155; --g-tint: color-mix(in srgb, #334155 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M4%204.5A1.5%201.5%200%200%201%205.5%203H16v14H5.5A1.5%201.5%200%200%201%204%2015.5z'/%3E%3Cpath%20d='M4%2014.5h12M8%203v14'/%3E%3C/svg%3E"); }
[data-g="asset"] { --g: #9333ea; --g-tint: color-mix(in srgb, #9333ea 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M3%2017h14M4.5%2017V8l5.5-4%205.5%204v9'/%3E%3Cpath%20d='M8.5%2017v-4h3v4'/%3E%3C/svg%3E"); }
[data-g="tax"] { --g: #b42318; --g-tint: color-mix(in srgb, #b42318 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M5%203h6l4%204v11a1%201%200%200%201-1%201H5a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1z'/%3E%3Cpath%20d='M11%203v4h4'/%3E%3Cpath%20d='M8%2015.5%2012%2010'/%3E%3Ccircle%20cx='8.4'%20cy='10.6'%20r='.9'/%3E%3Ccircle%20cx='11.6'%20cy='14.9'%20r='.9'/%3E%3C/svg%3E"); }
[data-g="pay"] { --g: #c2410c; --g-tint: color-mix(in srgb, #c2410c 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='10'%20cy='6.5'%20r='2.8'/%3E%3Cpath%20d='M4%2017c0-3.2%202.7-5.4%206-5.4s6%202.2%206%205.4'/%3E%3C/svg%3E"); }
[data-g="price"] { --g: #0f766e; --g-tint: color-mix(in srgb, #0f766e 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M3.5%2010.6V4.5a1%201%200%200%201%201-1h6.1a1%201%200%200%201%20.7.3l5.4%205.4a1%201%200%200%201%200%201.4l-6.1%206.1a1%201%200%200%201-1.4%200L3.8%2011.3a1%201%200%200%201-.3-.7z'/%3E%3Ccircle%20cx='7.2'%20cy='7.2'%20r='1.1'/%3E%3C/svg%3E"); }
[data-g="report"] { --g: #4f46e5; --g-tint: color-mix(in srgb, #4f46e5 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M3%2017h14'/%3E%3Cpath%20d='M6%2017V9M10%2017V4M14%2017v-6'/%3E%3C/svg%3E"); }
[data-g="settings"] { --g: #64748b; --g-tint: color-mix(in srgb, #64748b 12%, #fff); --icon: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2020%2020'%20fill='none'%20stroke='black'%20stroke-width='1.6'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='10'%20cy='10'%20r='2.6'/%3E%3Cpath%20d='M10%202.5v2M10%2015.5v2M17.5%2010h-2M4.5%2010h-2M15.3%204.7l-1.4%201.4M6.1%2013.9l-1.4%201.4M15.3%2015.3l-1.4-1.4M6.1%206.1%204.7%204.7'/%3E%3C/svg%3E"); }

/* ═══════════ no nested surfaces ═══════════ */

/* A card inside a card is always wrong: two borders and two shadows to say
   one thing, and the inner one reads as a control that has come loose. A
   filter bar and a grid are SURFACES — whichever container they land in
   supplies the paper, and they supply only their own structure. */

.erp-card .erp-tbl-wrap,
.erp-card .erp-rangeform,
.erp-card .erp-rangebar,
.erp-card-inset .erp-tbl-wrap {
  border: 0;
  box-shadow: none;
  border-radius: 0;
  background: transparent;
  min-inline-size: 0;
}
.erp-card > .erp-tbl-wrap:last-child { margin-block-end: calc(var(--sp-4) * -1); }

/* Standalone, the filter bar is a band and not a card — it belongs to the
   grid under it, and boxing it separates the control from the thing it
   controls. A rule underneath is the whole mechanism. */
.erp-rangeform, .erp-rangebar {
  background: transparent;
  border: 0;
  border-block-end: 1px solid var(--line);
  border-radius: 0;
  box-shadow: none;
  padding: 0 0 var(--sp-3);
  margin-block-end: var(--sp-4);
}
.erp-card > .erp-rangeform:first-child,
.erp-card > .erp-rangebar:first-child { padding-block-start: 0; }

/* The chips belong on the same band as the fields they filter beside. */
.erp-rangeform .erp-chips-row, .erp-rangebar .erp-chips-row { margin-inline-start: auto; }

/* ═══════════ scroll affordance ═══════════ */

/* A grid wider than its container scrolls, and a reader has to be able to
   tell. The shadow is cast by the content itself and only while there is
   content to reach — animation-timeline does it with no script. */
@supports (animation-timeline: scroll(self inline)) {
  .erp-tbl-wrap {
    animation: erp-scroll-edge linear both;
    animation-timeline: scroll(self inline);
  }
  @keyframes erp-scroll-edge {
    from { box-shadow: inset -14px 0 12px -14px rgba(16,26,38,.28); }
    to   { box-shadow: inset  14px 0 12px -14px rgba(16,26,38,.28); }
  }
}

/* ═══════════ filter bars flow, they do not stack ═══════════ */

/* .erp-inp/.erp-sel are inline-size:100% because in a FORM a field owns its
   row. In a filter BAR that made every control demand the full width and a
   four-filter bar became four full-width rows of chrome above the grid the
   reader came for — measured at 700px of it on a phone. In a bar a control
   is as wide as what it holds, with a floor so a label still fits and a
   ceiling so one long option cannot push the rest onto another line. */
.erp-rangeform, .erp-rangebar { align-items: end; }
.erp-rangeform > *, .erp-rangebar > * { flex: 0 1 auto; min-inline-size: 0; }

.erp-rangeform .erp-inp, .erp-rangeform .erp-sel,
.erp-rangebar  .erp-inp, .erp-rangebar  .erp-sel,
.erp-rangeform .erp-field, .erp-rangebar .erp-field {
  inline-size: auto;
}
.erp-rangeform .erp-inp, .erp-rangeform .erp-sel,
.erp-rangebar  .erp-inp, .erp-rangebar  .erp-sel {
  min-inline-size: 11rem;
  max-inline-size: 17rem;
}
/* A date or a code has a known length and does not want either. */
.erp-rangeform .erp-inp.ltr, .erp-rangebar .erp-inp.ltr,
.erp-rangeform .erp-jdate,   .erp-rangebar .erp-jdate {
  min-inline-size: 11ch; max-inline-size: 14ch;
}
.erp-rangeform .erp-form-actions, .erp-rangebar .erp-form-actions { margin-block-start: 0; }

/* The same discipline for a bare filter form that never got .erp-rangeform:
   a form whose fields are all short controls should flow, not stack. */
.erp-form > .erp-field-row .erp-inp.ltr,
.erp-form > .erp-field-row .erp-jdate { max-inline-size: 16ch; }

/* In the horizontal strip the group already carries the module's icon and
   colour, and repeating it on every item beside it turns a navigator into a
   row of identical carts. The group marker is the wayfinding; the items are
   the labels. */
/* ⚠️ This block used to bold every menu title and paint it in its module's
   colour below 64rem. That was written for the horizontal STRIP, where the
   row icons were hidden and the label was the only thing left that could
   carry the colour code. The menu bar shows the group's icon at every width,
   so the label painting it too is the same signal twice — and thirteen
   saturated words in a row is a bar that reads as a warning rather than as a
   navigator. The icon carries the colour; the word stays ink; the FILL says
   which menu is open. Same at 320px as at 1440. */

/* An empty slot is not a slot. Most KPI cells carry no change badge, and
   reserving the line for them put 18px of nothing under every figure. */
.erp-kpi-foot:empty, .erp-kpi-note:empty { display: none; }

/* A checkbox label is a LINE OF TEXT with a control in it, not a rigid row.
   inline-flex with no wrap made its intrinsic width the sum of its parts, so
   «ثبت فاکتور فروش  sales.invoice.post» could not fit a 320px card and pushed
   the whole document 93px wider than the phone — the one thing this layout
   does not do. */
.erp-check, .erp-checks label, .erp-lbl:has(input[type="checkbox"]) {
  flex-wrap: wrap;
  max-inline-size: 100%;
}
/* A capability name, a table name, a column name — a long unbreakable Latin
   token in a narrow card. It may break; a figure may not, which is why this
   is on .erp-code and not on .ltr.
   ⚠️ `.erp-raw` dropped ۶ شهریور ۱۴۰۵ for the same reason its `break-all`
   went: that class is on the <details> WRAPPER, so the property inherited
   onto a Persian <summary> and into the payload beneath it. `.erp-pre` keeps
   its own `word-break: break-word`, which breaks a token that cannot fit and
   leaves a word that can — the behaviour this rule wanted, scoped to the one
   element that is actually a blob of Latin. */
.erp-code { overflow-wrap: anywhere; }

/* ═══════════ the launcher ═══════════ */

/* پارسیان opens on a wall of coloured tiles, and that is why its users find
   things without reading. This is that, built properly: the colour is the
   module's own, the icon is the one the rail uses for the same module, and
   the tile is a real target rather than a decorated link. */

/* ⚠️ AMENDED ۵ شهریور ۱۴۰۵ by the owner: THE TILE IS THE COLOUR.
   These were white cards with a 30px tinted square on them, which is a
   diagram of پارسیان's launcher rather than the thing itself. Its desktop is
   a wall of saturated colour with a white glyph on each, and that is the
   reason its users hit «فاکتور فروش» without reading the word — the target
   is the colour, at tile size, from across the counter.
   The word is still there and still first: colour never speaks alone. */
/* ⚠️ PASTEL AS OF ۵ شهریور ۱۴۰۵ — NetSuite. Its «Tiles» portlet is a row of
   PALE panels, each with a saturated illustration at the inline-start and a
   bold DARK label — not a saturated block with white type on it, which is
   what these were and what پارسیان's desktop is.

   The tint is DERIVED from the module colour rather than cycled by position,
   and that is the one place this deviates from the reference on purpose.
   NetSuite cycles four pastels down the row, so its tile colour means
   «second tile» and nothing more. Here `--g` already means «خزانه‌داری», the
   navigator no longer carries it, and this launcher is now the only surface
   in the application where colour still says what a thing IS. A 14% mix on
   white lands in exactly NetSuite's pastel range while keeping that.

   Dark ink on every one of the thirteen mixes clears 13:1, so the label —
   which is still the first signal — is safe without measuring each. */
.erp-mod {
  position: relative;
  display: flex; flex-direction: column; justify-content: center;
  gap: 2px;
  min-block-size: 78px;
  padding: var(--sp-3) var(--sp-4);
  padding-inline-start: calc(var(--sp-4) + 52px);
  /* The label sits at the far end from the glyph, as NetSuite sets it — the
     illustration anchors one edge of the tile and the word the other, so a
     row of tiles reads as a row of words rather than as a row of pictures. */
  text-align: end;
  border: 1px solid color-mix(in srgb, var(--g, var(--accent)) 22%, #fff);
  border-radius: var(--r-2);
  background: color-mix(in srgb, var(--g, var(--accent)) 14%, #fff);
  box-shadow: none;
  overflow: hidden;
  transition: background-color 120ms ease-out, border-color 120ms ease-out;
}
/* The glyph, big and white — the tile's own picture rather than a badge stuck on
   it. Held back to .28 so the WORD is still the first thing read. */
/* The glyph moves INSIDE the tile at the inline-start, saturated, at full
   strength — NetSuite's tile illustration. It was a 74px ghost bleeding off
   the corner at .28 opacity, which only works behind white type. */
.erp-mod::after {
  content: "";
  position: absolute;
  inset-block: 0; margin-block: auto;
  inset-inline-start: var(--sp-4);
  inline-size: 34px; block-size: 34px;
  background-color: var(--g, var(--accent));
  opacity: 1;
  -webkit-mask: var(--icon) center / contain no-repeat;
          mask: var(--icon) center / contain no-repeat;
}
a.erp-mod:hover {
  background: color-mix(in srgb, var(--g, var(--accent)) 22%, #fff);
  border-color: color-mix(in srgb, var(--g, var(--accent)) 40%, #fff);
  text-decoration: none;
}
a.erp-mod:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.erp-mod__n {
  position: relative;
  color: var(--tile-ink); font-size: .875rem; font-weight: 700;
}
.erp-mod__d, .erp-mod__s { position: relative; color: var(--ink-2); font-size: .75rem; }
.erp-mod__s { color: var(--ink-3); }
a.erp-mod:hover .erp-mod__n { color: var(--tile-ink); }
/* Not built yet: the shape is kept — the half-finished system is legible only
   because the finished one is drawn — but the colour is spent, so it is
   drained rather than lit. */
.erp-mod.is-soon {
  opacity: 1;                       /* the base rule's .6 on top of a drained
                                       tile is a tile you cannot read at all */
  background: var(--surface);
  box-shadow: none;
  border: 1px dashed var(--line-strong);
}
.erp-mod.is-soon::after { background-color: var(--ink-3); opacity: .45; }
.erp-mod.is-soon .erp-mod__n { color: var(--ink-2); }
.erp-mod.is-soon .erp-mod__d, .erp-mod.is-soon .erp-mod__s { color: var(--ink-3); }

/* Denser than the module map it was written for — twelve of these is a
   launcher, and a launcher wants to be one glance.

   ⚠️ The floor is 260px, not 210px, and it is arithmetic. The launcher moved
   out of the 798px middle column and across the full 1400px page, where 210px
   bought SIX tracks; thirteen tiles with the till spanning two is fourteen
   cells, so the rows came out 6 + 6 + 2 and the last row left 871px of empty
   paper under a card that had just filled two full rows. At 260px it is five
   tracks, fourteen cells fall 5 + 5 + 4, and the hole is one cell instead of
   four. The tiles also stop being 218px wide, which is narrower than «فاکتور
   تأمین‌کننده» wants. */
.erp-mods { grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }

/* پارسیان's desktop is a MOSAIC, not a uniform grid — کارت حساب, کاردکس کالا
   and امنیت اطلاعات are double-width tiles among single ones, and the size
   difference is doing work: it says which of thirteen equal-looking things is
   not equal. Here that is the till. It is the screen this shop opens in the
   morning and closes at night, and it should not be one of thirteen identical
   rectangles you have to read to find.
   Only where there is more than one column to span, or a phone gets a tile
   twice as wide as its own grid. */
@media (min-width: 48rem) {
  .erp-mods > .erp-mod:first-child { grid-column: span 2; }
}

/* ⚠️ THE LAUNCHER IS TIGHTENED, ۷ شهریور ۱۴۰۵, at the owner's word: «کارهای
   روزمره should be tightened and نسبت جاری and یادآوری‌ها should come up».

   Measured at 1280×900, the window it was reported from. The tiles reserved
   78px for 37px of text, so thirteen of them — fourteen cells, the till spans
   two — fell 4+4+4+2 in a 260px auto-fit grid and the card ran 441px tall.
   That put `.erp-dash` at y=827 on a 900px viewport: نسبت جاری and یادآوری‌ها
   were a heading and nothing under it. The two portlets that ASK for something
   sat below the fold, under a launcher that is meant to be one glance.

   Three changes, and each is arithmetic rather than taste:
   - the tile keeps both of its lines and stops reserving height for a third;
   - the column count is FIXED per breakpoint instead of auto-fit, because a
     grid that reflows between four and six tracks has no row whose remainder
     can be reasoned about — and the remainder is the hole;
   - the last tile widens to close the row when the count leaves ONE track.
   1280 now: five columns, three flush rows, 185px of tiles, dash at y=655.

   Scoped to `.erp-launch` because `.erp-mods` is also the three-tile prose
   block on pricing/rules.php, which wants the roomier original. */
/* The breakpoints are set from ONE measurement: a tile narrower than ~200px
   wraps «فاکتور تأمین‌کننده» or «گردش یک کالا، ردیف به ردیف» onto a second
   line, and a wrapped desc makes its whole row two lines taller — the height
   this rule exists to remove. Each step is the width at which the next track
   still leaves every tile above that floor. On a 320px phone that means one
   column: two put 140px tiles on the screen and broke even the TITLES. */
.erp-launch .erp-mods {
  gap: var(--sp-2);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 30rem) { .erp-launch .erp-mods { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 46rem) { .erp-launch .erp-mods { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 58rem) { .erp-launch .erp-mods { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (min-width: 72rem) { .erp-launch .erp-mods { grid-template-columns: repeat(5, minmax(0, 1fr)); } }

/* ⚠️ SEVEN COLUMNS FROM 90rem, AND THE REASON IS THE ROW COUNT, NOT THE WIDTH.
   Thirteen tiles with the till spanning two are fourteen cells: at five
   columns that is three rows, at seven it is exactly two. On the ۱۴۴۰-wide
   laptop this dashboard is read on, dropping that third row is what lifts
   «نسبت جاری» and «یادآوری‌ها» into the first screen — the card goes 269px
   → 166px and the row of widgets starts at 420 instead of 523.

   Seven columns is 188px each at 1440. The longest description,
   «گردش یک کالا، ردیف به ردیف», measures 137px at 12px and would wrap —
   and one wrapped tile is 74px, which puts the third row back. So this tier
   also buys the text its width back: 8px side padding instead of 12, a 22px
   icon instead of 26, and 11px description type, which measures 125px in a
   142px slot. Seventeen pixels of slack. Do not raise either size here
   without re-measuring that string. */

.erp-launch .erp-mod {
  min-block-size: 0;
  gap: 0;
  padding: var(--sp-2) var(--sp-3);
  padding-inline-start: calc(var(--sp-3) + 38px);
}
.erp-launch .erp-mod::after {
  inline-size: 26px; block-size: 26px;
  inset-inline-start: var(--sp-3);
}
.erp-launch .erp-mod__n { font-size: .8125rem; line-height: 1.45; }
.erp-launch .erp-mod__d { font-size: .75rem;   line-height: 1.45; }
/* `erp_mods()` prints the «state» span whether or not there is a state to
   print, and on the launcher there never is — every tile here is a live route.
   Empty, it still carried its own 8px of margin, which is 24px of the card
   over three rows. */
.erp-launch .erp-mod__s:empty { display: none; }

/* ⚠️ SEVEN COLUMNS FOR A FULL RACK, AND THE POINT IS THE ROW COUNT.
   Thirteen tiles with the till spanning two are fourteen cells: five columns
   is three rows, seven is exactly two. On the ۱۴۴۰-wide laptop this dashboard
   is read on that third row is what pushed «نسبت جاری» and «یادآوری‌ها» off
   the first screen — dropping it takes the card 269px → 190px and starts the
   widgets at 444 instead of 523.

   GUARDED ON THE TILE COUNT, because the rack is capability-filtered and can
   hold anything from two tiles to thirteen: only twelve and thirteen end
   flush at seven columns (13 and 14 cells, the twelve closed by the span
   below). Eleven or fewer would leave a lone tile stranded in row two, so
   they keep the five-column tier and its own closer. A browser without
   `:has()` also lands there — three tidy rows, which is what this looked
   like before, so the fallback costs nothing.

   Seven columns is 188px each at 1440. The longest description,
   «گردش یک کالا، ردیف به ردیف», measures 137px at 12px and would wrap — and
   one wrapped tile is 74px, which puts the third row straight back. So this
   tier buys the text its width back: 8px side padding instead of 12, a 22px
   icon instead of 26, and 11px type, which measures 125px in a 142px slot.
   Do not raise either size without re-measuring that string. */
@media (min-width: 90rem) {
  .erp-launch .erp-mods:has(> :nth-child(12)):not(:has(> :nth-child(14))) {
    grid-template-columns: repeat(7, minmax(0, 1fr));
  }
  .erp-launch .erp-mods:has(> :nth-child(12)):not(:has(> :nth-child(14))) > .erp-mod {
    padding-inline: var(--sp-2);
    padding-inline-start: calc(var(--sp-2) + 30px);
  }
  .erp-launch .erp-mods:has(> :nth-child(12)):not(:has(> :nth-child(14))) > .erp-mod::after {
    inline-size: 22px; block-size: 22px;
    inset-inline-start: var(--sp-2);
  }
  .erp-launch .erp-mods:has(> :nth-child(12)):not(:has(> :nth-child(14))) .erp-mod__d {
    font-size: .6875rem;
  }
  /* thirteen matches 5n+3, so the five-column closer has to be called off
     before the seven-column one is applied to twelve. */
  .erp-launch .erp-mods:has(> :nth-child(12)):not(:has(> :nth-child(14))) > .erp-mod:last-child:nth-child(5n + 3) {
    grid-column: auto;
  }
  .erp-launch .erp-mods:has(> :nth-child(12)):not(:has(> :nth-child(14))) > .erp-mod:last-child:nth-child(7n + 5) {
    grid-column: span 2;
  }
}

/* The card's own chrome, trimmed on the launcher only. A dashboard portlet
   holding prose earns its 16px of paper and a 12px rule under the title;
   a rack of shortcuts does not, and this one is the first thing between the
   period bar and the figures. 16px of the 269 came from here. */
.erp-launch { padding: var(--sp-3); }
.erp-launch > .erp-card-h {
  margin-block-end: var(--sp-2);
  padding-block-end: var(--sp-2);
}

/* Closing the row. The launcher is capability-filtered — thirteen tiles for a
   مدیر, seven for an انباردار — so the count is not a constant this file can
   hard-code. With C columns and the till spanning two, the cells are n+1: the
   grid ends flush when (n+1) mod C is 0, and leaves exactly one empty track
   when (n+1) mod C is C−1, which is n ≡ C−2 (mod C). That one track is the
   only hole worth widening a tile to fill; a two-track hole wants a different
   tile count, not a stretched tile. Thirteen at five columns is that case. */
@media (min-width: 72rem) {
  .erp-launch .erp-mods > .erp-mod:last-child:nth-child(5n + 3) { grid-column: span 2; }
}
@media (min-width: 58rem) and (max-width: 71.999rem) {
  .erp-launch .erp-mods > .erp-mod:last-child:nth-child(4n + 2) { grid-column: span 2; }
}
@media (min-width: 48rem) and (max-width: 57.999rem) {
  .erp-launch .erp-mods > .erp-mod:last-child:nth-child(3n + 1) { grid-column: span 2; }
}

/* .erp-field-row is a GRID with a 200px column floor — correct in a form,
   wrong inside a filter bar, where the flex parent shrinks it to min-content
   and the grid collapses to one column, stacking every date field down the
   page. In a bar it flows. */
.erp-rangeform .erp-field-row, .erp-rangebar .erp-field-row {
  display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-2) var(--sp-3);
}
.erp-rangebar > .erp-rangeform { flex: 1 1 100%; }
.erp-rangebar { flex-wrap: wrap; row-gap: var(--sp-3); }
.erp-rangebar > .erp-chips-row { flex: 1 1 100%; margin-inline-start: 0; }

/* A KPI cell inside a card is the nested-card fault too: the card already
   supplied the paper, so the cells are cells and not tiles. They keep their
   own hairline as a divider and give up the elevation and the corner. */
.erp-card .erp-kpi {
  box-shadow: none;
  border-radius: 0;
  border: 0;
  background: transparent;
  padding: 0;
}
/* The divider sits BETWEEN cells, so it hangs off every cell after the first —
   in RTL that is its inline-start edge, which faces the cell before it. Putting
   it on :last-child instead would rule the outer edge of the group. */
.erp-card .erp-kpi + .erp-kpi {
  border-inline-start: 1px solid var(--line);
  padding-inline-start: var(--sp-4);
}
.erp-card .erp-kpis { gap: var(--sp-3) var(--sp-4); margin-block-end: 0; }

/* The 34rem floor that used to be here went with the fit-content rule it
   propped up. The wrapper is 100% of whatever it is given at every width now,
   and the grid inside it scrolls. Kept as a rule rather than deleted so the
   phone case is stated somewhere. */
@media (max-width: 48rem) {
  .erp-tbl-wrap { min-inline-size: 0; max-inline-size: 100%; }
}

/* ═══════════ a grid inside a card fills the card ═══════════ */

/* The fit-content rule exists to stop a PAGE-WIDTH table handing 900px of
   slack to its label column. A card is already a bounded context — half a
   page at most — so there the rule inverts: a table that hugs its content
   inside a 566px card leaves 163px of white beside it and reads as though
   the panel failed to load. Fill the card; the worst spread left is the
   width of the card, which is the width somebody chose on purpose. */
.erp-card .erp-tbl-wrap,
.erp-card-inset .erp-tbl-wrap {
  inline-size: 100%;
  min-inline-size: 0;
}
.erp-card .erp-tbl,
.erp-card-inset .erp-tbl { inline-size: 100%; }

/* ═══════════ the filter band is a band, not a stack ═══════════ */

/* 155px tall for seven chips and three fields, because the chips and the form
   were each pinned to their own full-width row. They share a line wherever
   there is room for one and wrap only when there is not. */
.erp-rangebar > .erp-chips-row { flex: 0 1 auto; margin-inline-start: 0; }
.erp-rangebar > .erp-rangeform { flex: 1 1 22rem; }
.erp-rangebar {
  align-items: center;
  column-gap: var(--sp-4);
  padding-block-end: var(--sp-2);
  margin-block-end: var(--sp-3);
}
.erp-rangebar .erp-form { gap: var(--sp-2); }

/* The filter band belongs to the figures under it, but it still needs air
   above so it does not butt into whatever section preceded it. */
.erp-rangebar { margin-block-start: var(--sp-4); }
.erp-main > .erp-rangebar:first-child,
.erp-h + .erp-rangebar { margin-block-start: 0; }

/* ═══════════ report panels ═══════════ */

/* 1 · Cards in a grid stretch to the tallest in their row, and the content is
   top-aligned — so a short panel beside a long one carried ~150px of dead
   white below its last row. Let each card be its own height. Ragged bottoms
   read as panels of different lengths, which is what they are; a uniform
   block of emptiness reads as something that failed to load. */
.erp-grid-2, .erp-grid-3 { align-items: start; }

/* 2 · The heading sat on the card's top edge. A card's own air above its
   title is what makes it a panel rather than a box drawn around some text. */
.erp-card > .erp-card-h:first-child { margin-block: 2px var(--sp-3); }

/* 3 · A comparison table inside a panel is «label · value · value · delta»,
   and under auto layout the label column absorbed every spare pixel — so the
   figures sat 260px from the row they belonged to. Fixed layout gives the
   label a generous share and splits the rest evenly, which is the shape these
   four columns actually have. Scoped to cards: a full-page ledger still sizes
   itself to its content. */
/* ⚠️ A WIDTH HINT, never `table-layout: fixed`. Fixed layout gives a column
   exactly its declared share and clips whatever does not fit — and what does
   not fit in a money column is a DIGIT. The sweep caught «۱۱۶,۱۴۵,۰۰۰» losing
   its leading figures inside a report panel, which is the single worst thing
   a ledger can do quietly. Under auto layout this percentage is a preference:
   the label column takes about a third, the numeric columns size to their
   content first, and nothing is ever cut. */
.erp-card .erp-tbl th:first-child,
.erp-card .erp-tbl td:first-child { inline-size: 34%; }

/* A grid in a PANEL is denser than a grid on a page, and this is arithmetic
   rather than taste. Measured on the dashboard's سامانهٔ همکاران portlet:
   241px of card, three columns whose widest INK is 39 + 82 + 68 = 189px —
   and 72px of cell padding on top, at 12px a side. The table's own minimum
   was therefore 261 and it overflowed a card that had room for its content
   twice over. Padding is not why a portlet scrolls sideways; it just was.
   8px a side, which is what NetSuite's own portlet grids use against its
   list views. Full-page ledgers keep the 12. */
.erp-card .erp-tbl thead th,
.erp-card .erp-tbl tbody td,
.erp-card .erp-tbl tfoot td { padding-inline: var(--sp-2); }

/* ⚠️ `overflow-wrap: anywhere` USED TO BE HERE, on every non-numeric cell in
   a card, and it was the single worst line in this stylesheet. Removed
   ۶ شهریور ۱۴۰۵ after the owner reported «broken Farsi» on the dashboard.

   It looks like a safety net — «let a long label break rather than overflow
   a narrow panel». In Persian it is not a break, it is a broken JOIN.
   Vazirmatn's letters connect; «فروش» split after «فرو» renders as «فرو» and
   «ش», which are not the halves of a word, they are two shapes that are not
   that word. Measured on the dashboard: فروش, سفارش, انتظار, چک and پرداخت
   — پرداخت over THREE lines, in a 45px column.

   And it did not stop at words. `anywhere` also breaks a NUMBER, so
   «۲۱۷,۶۰۴,۰۰۰» came out on three lines in the سامانهٔ همکاران portlet:
   three figures where there is one, and a reader who takes the top line is
   out by a factor of a thousand. UAX#14 would never have broken it — «NU IS
   NU» is explicitly unbreakable — so this rule was overriding the one piece
   of line-breaking the browser already had exactly right.

   It also CAUSED the crushing it was added to relieve. `anywhere` sets a
   cell's min-content width to one character, so under auto table layout the
   numeric columns took their full content width first and the label column
   was left with whatever remained — the 45px above. Without it the label
   column's min-content is its longest WORD, the 34% hint has something to
   argue with, and «در انتظار پرداخت» wraps at its space like prose.

   What happens now when a card is genuinely too narrow for its table: the
   table exceeds the card and `.erp-tbl-wrap` scrolls, which is what the
   wrapper is for and what the canyon note above already says. A grid that
   scrolls is legible and obviously scrollable; a grid that shreds its words
   is neither.

   If a cell holds a Latin blob with no spaces — a SKU, a JSON payload, a
   long code — THAT may break anywhere, because Latin letters do not join.
   `.erp-code` and `.erp-raw` already say so, two hundred lines up. */

/* An input inside a reading grid gets a floor of its own. Table columns
   divide the available width between them, and on a phone that squeezed the
   param-title field on payroll/params.php to 58px — «حداقل دستم…» while
   somebody is entering the year's minimum wage, which is precisely the screen
   where a half-read label produces a wrong figure for twelve months. The grid
   scrolls in .erp-tbl-wrap instead; that is what the wrapper is for.
   .erp-lines is excluded: the line editor sizes its own columns deliberately. */
.erp-tbl .erp-inp, .erp-tbl .erp-sel { min-inline-size: 16ch; }

/* ═══════════ dashboard panels ═══════════ */

/* A grid row is as tall as its tallest card, so a short panel beside a long
   one leaves a pocket — stretched into the card if the items stretch, left as
   a gap if they do not. Neither is right. Columns let each panel start where
   the previous one ended, which is what a board of unequal panels wants. */
.erp-panels {
  columns: 2;
  column-gap: var(--sp-3);
  margin-block-end: var(--sp-4);
}
.erp-panels > .erp-card {
  break-inside: avoid;
  margin-block-end: var(--sp-3);
  /* A column box is not a block formatting context, so a card's own margin
     would collapse through the top of a column. */
  display: flow-root;
}
@media (max-width: 60rem) { .erp-panels { columns: 1; } }

/* The band was bottom-heavy: the weekday confirmations («یکشنبه») hang under
   the date fields, so the row's box extended well past the controls and the
   rule sat a long way below anything you could click. The note tucks up under
   its field instead of adding a line to the band. */
.erp-rangeform .erp-jdate-note, .erp-rangebar .erp-jdate-note {
  margin-block-start: 0;
  line-height: 1.2;
  block-size: 0;
  overflow: visible;
}
.erp-rangebar { padding-block-end: var(--sp-3); }

/* ⚠️ AND THE SPACE IT NO LONGER TAKES HAS TO COME FROM SOMEWHERE.
   Added ۶ شهریور ۱۴۰۵.

   `block-size: 0; overflow: visible` is «paint outside my box» — it is what
   keeps «چهارشنبه» from adding a line to the band, and on a wide screen,
   where the band is one row, it is free: there is nothing under the note but
   the band's own padding.

   The moment the band WRAPS there is. The dashboard's شاخص‌های کلیدی portlet
   sat in the middle column of the dashboard grid — 798px measured at 1440,
   766px of card inside it — and the chips row eats most of that line, so the
   form's own `flex: 1 1 22rem` left `.erp-field-row` about a third of the
   band. It broke its three filters over two rows
   — and the weekday under «از تاریخ» landed on top of the «مقایسه با» label
   below it. Measured at 180px² of overlapping ink. That is the fault the
   owner reported as «the inputs are placed awfully», and it is not a
   dashboard fault: any of the ~30 screens carrying a range bar does it as
   soon as the window is narrow enough to wrap the row.

   So the note's height is reserved in the ROW GAP, and only there. A gap
   between rows exists only when there are two rows, which is exactly when
   the note has something to fall on — so a single-row band is unchanged,
   pixel for pixel, and the band cannot collide with itself again. `:has()`
   keeps it off the bands that carry no date field at all. */
.erp-rangebar { --jnote: .95rem; }
.erp-rangebar:has(.erp-jdate-note),
.erp-rangeform:has(.erp-jdate-note) { row-gap: calc(var(--sp-2) + var(--jnote)); }
.erp-rangebar .erp-field-row:has(.erp-jdate-note),
.erp-rangeform .erp-field-row:has(.erp-jdate-note) { row-gap: calc(var(--sp-3) + var(--jnote)); }

/* ═══════════ the till reads at a different distance ═══════════
   ⚠️ Added ۴ شهریور ۱۴۰۵.

   `.erp-scan-inp` is already 42px at 1rem — the scan box was correctly given
   its own size when فروش حضوری was built. Everything UNDER it was not: the
   ردیف grid is `.erp-lines` at .8125rem on 34px rows, which is the density of
   دفتر کل. So an operator scans into a big box and then reads the result at
   accountant density, standing up, at arm's length, with a gun in one hand
   and a customer waiting.

   Those are two different legibility budgets and the guidance is explicit
   about it — a screen used mid-task needs larger type, more contrast and
   bigger targets than the same data read sitting down. This is the only
   screen in the application with that problem, so it is the only screen that
   gets the step: one class on <body>, no new components, and the class
   vocabulary is untouched. */
body.is-till .erp-lines { font-size: .875rem; }
body.is-till .erp-lines td,
body.is-till .erp-lines th { block-size: 40px; }
body.is-till .erp-lines .erp-cell-inp,
body.is-till .erp-lines .erp-inp,
body.is-till .erp-lines .erp-inp-sm,
body.is-till .erp-lines .erp-inp-xs { block-size: 34px; font-size: .875rem; }
/* The figure the customer is told. It is the one number on this screen that
   is read out loud, and it was the same size as the row it sat in. */
body.is-till .erp-tbl tfoot .amt { font-size: 1rem; font-weight: 700; }

/* ═══════════ a printed report says what it is a report of ═══════════
   ⚠️ Added ۴ شهریور ۱۴۰۵, and this is a correctness fix wearing a
   stylesheet's clothes.

   The print block above hides `.erp-rangeform`, `.erp-rangebar` and
   `.erp-chips-row` — the date range and the active filters. Of roughly
   thirty screens carrying one, exactly ONE (fixedassets/depreciate.php)
   restated its range in erp_title() where the header prints it. Every other
   one printed a COUNT: «فاکتورهای فروش — ۴۲ فاکتور», over a list, with
   nothing anywhere saying which forty-two.

   Filter to خرداد and to ثبت‌نشده, print, and the paper is indistinguishable
   from the same report filtered to مرداد and ثبت‌شده. The figures print; the
   scope that produced them does not. For a system whose own law says paper
   is where it is finally judged and that a تراز is «argued about and filed
   for years», that is the argument-in-a-year failure with no defence.

   `erp_scope()` in _ui.php emits the sentence; this makes it quiet on screen
   and unmissable on paper. */
.erp-scope-line {
  margin: calc(var(--sp-3) * -1) 0 var(--sp-4);
  font-size: .75rem;
  color: var(--ink-3);
}
.erp-scope-line b { font-weight: 500; color: var(--ink-2); }

@media print {
  /* It is not chrome on paper. It is the only thing that says what this is. */
  .erp-scope-line {
    display: block !important;
    margin: 0 0 8pt;
    font-size: 9pt;
    color: #000;
    padding-block-end: 3pt;
    border-block-end: 0.5px solid #999;
  }
  .erp-scope-line b { color: #000; }
}

/* ═══════════ high contrast ═══════════
   ⚠️ Added ۴ شهریور ۱۴۰۵. Last in the sheet on purpose, so it wins.

   Windows High Contrast (and any forced-colors mode) discards background
   images, background colours and box-shadows and substitutes a system
   palette. In this sheet that is not a cosmetic degradation, it is a list of
   the devices this design is built out of:

     · every module icon is a mask-image — a background;
     · every status pill is a tinted fill — a background;
     · row banding is a background;
     · and the input focus indicator is `outline: none` plus a border-colour
       change plus a box-shadow halo. The border colour gets overridden by
       the system palette, the halo is dropped, and the `outline: none`
       stays. In high contrast this application had NO VISIBLE FOCUS.

   The pills come out of it well and that is not luck: «colour never speaks
   alone» means every pill already carries its word, so losing the fill costs
   the second signal and not the meaning. They get a border here for the same
   reason the print block gives them one — a marked state should still read
   as marked. */
@media (forced-colors: active) {
  /* Focus first. This is the one that stops the app being unusable. */
  :focus-visible,
  .erp-inp:focus, .erp-sel:focus, .erp-textarea:focus,
  .erp-cell-inp:focus, .erp-scan-inp:focus, .erp-pal-inp:focus {
    outline: 3px solid Highlight !important;
    outline-offset: 1px;
  }

  /* A control's edge must be a real edge, not a tone step. */
  .erp-inp, .erp-sel, .erp-textarea, .erp-btn, .erp-card, .erp-tbl-wrap,
  .erp-kpi, .erp-chip, .erp-mod {
    border: 1px solid CanvasText;
  }

  /* A pill keeps its word; give it back its box. */
  .erp-pill { border: 1px solid CanvasText; forced-color-adjust: none; }

  /* The armed confirm must stay unmistakable — it is the whole point of it. */
  .erp-btn.is-armed, .erp-btn-danger {
    outline: 3px solid Highlight;
    outline-offset: 2px;
  }

  /* Banding is gone (it is a background), so the grid needs its lines back
     or a wide row has nothing to track along. */
  .erp-tbl tbody td { border-block-end: 1px solid CanvasText; }

  /* The mask icons are drawn in `background-color` through a mask, which
     forced-colors would flatten to nothing. Keeping the adjust off lets the
     mask keep painting; the rail entry's WORD is doing the work regardless. */
  .erp-rail-g::before, .erp-rail-a::after, .erp-pal-opt::before,
  .erp-mod::before { forced-color-adjust: none; }

  /* The sideways-scroll hint is four gradients — all backgrounds, all
     dropped. Nothing to restore visually, so make sure the wrapper at least
     reads as a bounded box that can move. */
  .erp-tbl-wrap { border: 1px solid CanvasText; }
}

/* ═══════════ the reminder list ═══════════ */

/* NetSuite's «Reminders» portlet: a count, then what it is about, and the
   whole row is the link. Added ۶ شهریور ۱۴۰۵ with the accounting dashboard.

   It is a list and not a table on purpose — every row is one fact with one
   destination, and a two-column grid would align counts of wildly different
   magnitude into a column that reads as a ranking they are not in.

   `align-items: baseline` and not `center`: the pill is a different size from
   the label beside it, and centring two different type sizes leaves the words
   sitting slightly high against a shape that is obviously not centred on
   anything. */
.erp-remind { list-style: none; margin: 0; padding: 0; }
.erp-remind li + li { margin-block-start: var(--sp-2); }
.erp-remind a {
  display: flex; align-items: baseline; gap: var(--sp-2);
  color: var(--ink); text-decoration: none;
  font-size: .8125rem; line-height: 1.5;
  padding: var(--sp-1) 0;
}
.erp-remind a:hover { color: var(--link); text-decoration: underline; }
/* The pill must not shrink when the label beside it wraps to three lines —
   a «۳۵» squeezed to «۳…» is a count that has stopped being one. */
.erp-remind .erp-pill { flex: 0 0 auto; }

/* ═══════════ KPI tiles — the fixes of ۶ شهریور ۱۴۰۵ ═══════════ */

/* ⚠️ An EVEN break, opted into by the caller. `auto-fit` fills each row with
   as many tracks as fit and stretches the remainder, so six tiles in a 766px
   card land 4 + 2 with the second row's tiles twice as wide as the first's —
   which does not read as a wrap, it reads as two figures having gone missing.
   The caller passes `cols` because it is the only thing that knows the count. */
.erp-kpis-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.erp-kpis-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.erp-kpis-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 64rem) {
  .erp-kpis-3, .erp-kpis-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 34rem) {
  .erp-kpis-2, .erp-kpis-3, .erp-kpis-4 { grid-template-columns: minmax(0, 1fr); }
}

/* ⚠️ The sparkline is pinned to the BOTTOM of its tile, not left to sit where
   the text above it happens to end.

   Grid items stretch, so every tile in a row is already the same height. What
   was not the same was where the content stopped: a tile with a trend line
   filled its box and a tile without one ended after the caption, leaving its
   figure floating in the middle of a tall white rectangle with nothing under
   it. Six tiles, two of them hollow.

   `margin-block-start: auto` inside the flex column pushes the spark to the
   floor, so every tile's chart sits on one line across the row — and a tile
   that genuinely has no series still has a defined bottom edge because
   `.erp-kpi-foot` holds the caption there. */
.erp-kpi > .erp-spark { margin-block-start: auto; align-self: stretch; max-inline-size: none; }
.erp-kpi-foot { margin-block-end: 0; }

/* The tight variant: a کارنامه card is a chart with a number on it, not a
   figures strip, so its tiles lose the padding that separates a strip's
   tiles from each other. */
.erp-kpis.is-tight { gap: var(--sp-2); margin-block-end: var(--sp-2); }
.erp-kpis.is-tight .erp-kpi { padding: var(--sp-2) var(--sp-3); }
.erp-kpis.is-tight .erp-kpi-v { font-size: 1.125rem; }
.erp-kpis.is-tight .erp-kpi-v.is-money { font-size: .9375rem; }
.erp-kpis.is-tight .erp-kpi-l { min-block-size: 0; }

/* The in-cell magnitude bar — erp_chart_minibar(). It is a comparison device
   that sits INSIDE a table row, so it must never widen the column it is in;
   `max-inline-size: 100%` lets it shrink with the cell and `vertical-align`
   keeps it on the text's own baseline rather than hanging below it. */
.erp-minibar { display: inline-block; vertical-align: middle; max-inline-size: 100%; block-size: auto; }

/* A figure printed SHORT with its exact value under it — «۲۴۱ میلیارد» over
   «۲۴۱,۰۵۷,۸۳۰,۰۳۷». The short form is what gets read; the exact one is what
   gets checked, and an accountant will check. */
.erp-amt-short { display: block; font-variant-numeric: tabular-nums; white-space: nowrap; }
.erp-amt-exact {
  display: block; font-size: .6875rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums; white-space: nowrap; line-height: 1.5;
}

/* ═══════════ کارنامهٔ شاخص‌ها — the card grid ═══════════ */

/* ⚠️ Added ۶ شهریور ۱۴۰۵ with the scorecard rebuild. The page used to be one
   full-width card per KPI — thirteen of them, ~6000px of scroll, 37 nested KPI
   tiles and two charts on the one screen whose whole subject is movement.

   `auto-fill` and not `auto-fit`, for the same reason the forms use it: with
   three cards on a 2000px screen `auto-fit` collapses the empty tracks and
   stretches those three to 660px each, which makes a small card into a wide
   one and puts its 380px chart in a sea of white. `auto-fill` keeps the track
   width honest and leaves the leftover as leftover. */
/* ⚠️ The floor is 420px, not 330px, and it is arithmetic rather than taste.
   At 330 the 1400px content column fits FOUR tracks, and the کارنامه carries
   nine cards — 4 + 4 + 1, with the ninth alone against 1059px of nothing. At
   420 it fits three, nine divides by three, and every row is full. The chart
   inside grows from 341px to 458px in the same move, which is what made the
   month labels legible: «اردیبهشت» needs 64px and six of them need 384. */
.erp-sc-grid {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr));
  /* `stretch`, not `start`: cards in one row shared a top edge and nothing
     else, so a two-line title pushed one card's figure 25px below its
     neighbour's and the row read as broken. Equal heights put every headline
     on one baseline; `margin-block-start:auto` on the last child spends the
     slack at the BOTTOM, where it is invisible. */
  align-items: stretch;
  margin-block-end: var(--sp-4);
}
.erp-sc { display: flex; flex-direction: column; gap: var(--sp-2); }
/* The title box is two lines tall whether or not the title needs two, so the
   figure under it starts in the same place on every card in the row. */
.erp-sc .erp-card-h { min-block-size: 2.5rem; align-items: flex-start; }
.erp-sc .erp-card-h { margin-block-end: 0; }
.erp-sc-tag { font-size: .6875rem; color: var(--ink-3); white-space: nowrap; }

/* The figure, once, and large. Its exact value sits under it in 11px — a
   twelve-digit rial cannot be read at a glance at ANY size, so the short form
   is the one that carries and the exact one is there to be checked against
   gl/statements.php. */
.erp-sc-hero { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.erp-sc-v {
  font-size: 1.5rem; line-height: 1.15; color: var(--money);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.erp-sc-exact { font-size: .6875rem; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.erp-sc-delta { font-size: .8125rem; font-weight: 600; white-space: nowrap; }
.erp-sc-delta.is-up { color: var(--go-ink, #2f7a3e); }
.erp-sc-delta.is-dn { color: var(--stop-ink, #b3261e); }
.erp-sc-new { font-size: .75rem; color: var(--ink-3); }

.erp-sc-chart { margin-block: var(--sp-1); }
.erp-sc-nochart {
  font-size: .75rem; color: var(--ink-3); line-height: 1.5;
  padding-block: var(--sp-3); margin: 0;
}

/* دورهٔ قبل / پارسال / حجم واحد — context, and it must READ as context. Three
   equal-weight 26px figures beside the headline is what the old page did, and
   it left a reader with no way to tell which number was the answer. */
.erp-sc-cmp {
  display: flex; flex-wrap: wrap; gap: var(--sp-3);
  padding-block-start: var(--sp-2);
  border-block-start: 1px solid var(--line);
}
.erp-sc-cmp > div { display: flex; flex-direction: column; gap: 1px; min-inline-size: 0; }
.erp-sc-cmp span { font-size: .6875rem; color: var(--ink-3); }
.erp-sc-cmp b {
  font-size: .8125rem; font-weight: 600; color: var(--ink-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* The paired quality metric. One line — but never optional: a throughput
   figure without its quality pair is a figure that is easy to game. */
.erp-sc-pair {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2, #f6f7f9); border-radius: var(--r-1);
  font-size: .75rem;
}
.erp-sc-pair span { color: var(--ink-2); }
.erp-sc-pair b { font-variant-numeric: tabular-nums; color: var(--ink); }

.erp-sc-formula {
  margin: 0; font-size: .6875rem; color: var(--ink-3); line-height: 1.5;
  margin-block-start: auto; padding-block-start: var(--sp-1);
}

/* The short-form money tile. Bigger than `.is-money` on purpose: it is short
   enough to be read at a glance, which is the whole reason it exists, and the
   exact figure that follows it is deliberately small. */
.erp-kpi-v.is-short { font-size: 1.375rem; color: var(--money); }
.erp-kpi .erp-amt-exact { margin-block-start: 1px; }
