/* =============================================================================
   base.css — the foundation. LOGICAL PROPERTIES ONLY.
   =============================================================================
   Doctrine §12.1 decides this before the first stylesheet exists, not after:

       «العربية RTL والإنجليزية LTR. لو كُتب أول stylesheet بـ margin-left
        و float: left، فالنسخة الإنجليزية إعادة كتابة لكل ملف CSS في المشروع.
        نفس وضع dom.js بالضبط: يجب أن يكون صحيحاً في الملف الأول، ولا يُرحَّل
        إليه بعد ذلك بثمن معقول.»

   So there is no `margin-left`, no `padding-right`, no `float`, no
   `text-align: right`, and no `[dir="rtl"]` override anywhere in this project.
   The logical properties flip themselves when `dir` changes on <html>, which is
   one attribute and no second stylesheet.

       margin-left      ⟶  margin-inline-start
       padding-right    ⟶  padding-inline-end
       text-align: left ⟶  text-align: start
       border-left      ⟶  border-inline-start
       left: 0          ⟶  inset-inline-start: 0
       float: left      ⟶  there is no logical float. Use flex or grid.

   `tools/check-css.sh` refuses every one of the physical forms, and it was
   induced before it was trusted.

   WHAT THIS FILE IS NOT. It is not a design. F1 builds the foundation and no
   screens — «STOP AFTER F1» — so what is here is the type scale, the colour
   tokens, the direction-agnostic layout primitives, and the three states every
   screen needs: loading, empty, and error.
   ============================================================================= */

:root {
  /* =========================================================================
     Colour. Named by ROLE, not by hue, so a theme change is this block and
     nothing else — and `A` proved that: the whole system was repainted from
     here on 5 September 2026 and not one of the 3,900 lines below it moved.
     =========================================================================
     THE NAMES DID NOT CHANGE AND THAT IS A DECISION. The prototype says
     `--surface` and `--danger`; this file has always said `--paper` and
     `--stop`. Renaming would mean rewriting every `var()` below for ZERO
     visual change, and every one of those edits is a chance to leave a colour
     undefined — which renders as invisible text on the screen nobody opened.
     `--radius-1` was exactly that, found by `A`: used nine times, defined
     never, so nine rounded corners have been square since 038.

     AND THERE IS NO DARK THEME. Nobody asked for one, and every rule in this
     file that carries a hard-coded colour — the print block, the two shadows —
     was written against a white ground. */
  --ink: #0E1A24;
  --ink-soft: #3B4E5B;
  --ink-faint: #68808F;
  /* A FOURTH TEXT LEVEL — the new tables need it for a column heading that has
     to be readable and must not compete with the values under it. */
  --ink-faintest: #93A7B3;
  --paper: #FFFFFF;
  --paper-soft: #F4F7F8;
  /* A SECOND SURFACE — table headers and inset rows. `--paper-soft` is the
     page ground, so a header painted with it disappeared into the page. */
  --paper-inset: #EDF2F4;
  --line: #DBE4E9;
  /* A HEAVIER RULE — table footers and totals, where a hairline stops saying
     «this line is different from the forty above it». */
  --line-strong: #C6D4DC;

  /* =========================================================================
     ★★ THE ACCENT IS A LANE, NOT A COLOUR — and green and blue MEAN something
     =========================================================================
     Green is SALES. Blue is OPERATIONS. They are not decoration and they are
     not interchangeable: a screen painted green is a screen a salesman owns.

     `--sales` is sampled from Nile's own logo.

     ⛔ EVERY COMPONENT PAINTS FROM `--accent`. Never from `--sales` or `--ops`
        directly. That is what makes a lane ONE attribute on ONE element instead
        of a second stylesheet — the same discipline as the logical properties,
        and the same price if it is got wrong now rather than later.

     MASTER DATA — accounts, charge types, parties, users, the log — is neither
     lane, so it takes the default and stays blue. That is deliberate: a screen
     with no lane must not borrow the colour that means «sales». */
  --sales: #00A050;
  --sales-soft: #E4F5EC;
  --sales-line: #9DDCBB;
  --ops: #1F6FA8;
  --ops-soft: #E5EFF7;
  --ops-line: #A7C8E1;

  --accent: var(--ops);
  --accent-soft: var(--ops-soft);
  /* ★★ F §3 · A THIRD TOKEN IN THE PAIR, AND IT MOVES WITH THEM. The step ⑤
     panel and the «filled in for you» fields are BORDERED, and a border painted
     from `--sales-line` on a screen the operations lane also opens is exactly
     what `[data-lane]` exists to prevent — a component that cannot change lane.
     Caught by `test_A::test_the_lane_is_one_attribute_and_every_component_
     paints_from_accent`, which is the guard doing its job on this task. */
  --accent-line: var(--ops-line);

  --warn: #B26A00;
  --warn-soft: #FBF0DC;
  --stop: #B0242C;
  --stop-soft: #FBE9EA;

  /* MONEY THAT PASSES THROUGH NILE AND CANNOT CARRY A MARGIN. `C` needs it;
     it is defined here so there is ONE place where a colour is born. */
  --pass: #7A6A46;
  --pass-soft: #F6F1E4;
  --pass-line: #DFD2AF;

  /* ★★ 041 · AND THE SEMANTIC NAMES THE COMPONENTS ACTUALLY USE. The guard
     `test_the_lane_is_one_attribute_and_every_component_paints_from_accent`
     refuses a `var(--pass…)` or a `var(--sales…)` ANYWHERE below the token
     block — and it is right: a component that names a lane colour is a
     component that cannot change lane, «and one of those is enough to make the
     lane a second stylesheet instead of one attribute».

     ⟹ So the pass-through band is given a name that says what it IS, bound
       here, where a colour is born. `18 §٣.٢` is why it must not be the
       accent: «TWO BANDS, AND THEY LOOK DIFFERENT» — the client's money moving
       through Nile is not Nile revenue, and painting it in the lane's own
       colour would say it was. */
  --band-pass: var(--pass);
  --band-pass-soft: var(--pass-soft);
  --band-pass-line: var(--pass-line);

  /* THE TWO SHADOWS, AS TOKENS. They were the only hard-coded colours in this
     file outside the print block — `rgba(26,26,26,.34)` and `rgba(0,0,0,.12)`,
     found by grepping for a literal after the repaint, which is exactly what
     they would have gone on being: two greys that did not move when everything
     around them did. Built from the ink rather than from black, so a shadow
     over the new cool paper is the same hue as the text on it. */
  --shadow-panel: 0 2px 4px rgba(14, 26, 36, 0.06), 0 12px 32px -12px rgba(14, 26, 36, 0.22);
  --shadow-drop: 0 6px 18px rgba(14, 26, 36, 0.12);
  --veil: rgba(14, 26, 36, 0.34);

  /* Spacing, one scale. */
  --gap-1: 0.25rem;
  --gap-2: 0.5rem;
  --gap-3: 1rem;
  --gap-4: 1.5rem;
  --gap-5: 2.5rem;

  --radius: 6px;
  --line-width: 1px;

  /* =========================================================================
     Type. Arabic and Latin in one stack, Arabic first: a Latin-first stack
     renders Arabic in whatever the browser falls back to, which is usually
     something with the wrong baseline.
     =========================================================================
     ⛔ THE UI FONT DOES NOT CHANGE, AND THAT IS A DECISION TAKEN AGAINST THE
        PROTOTYPE. It draws every screen in Archivo. Segoe UI ⟶ Archivo changes
        the width of EVERY WORD ON EVERY SCREEN: a button that just fitted
        wraps, a table heading overflows, and it fails SILENTLY across thirteen
        screens, half of them Arabic. Wrong risk two days before twenty-two
        people arrive, for a change nobody asked for by name.

     ✔ THE MONO FONT DOES. It touches only numbers, codes and dates — short
       strings already inside table cells — so it is in no button and no
       heading and cannot wrap a control. What it buys is TABULAR NUMERALS:
       every digit the same width, so a column of money lines up decimal under
       decimal.

     ★★ AND IT IS SELF-HOSTED OR IT IS NOTHING — spec-02 §د.٥.
        `default-src 'self'` blocks fonts.gstatic.com (the font FILES) and
        `style-src 'self'` blocks fonts.googleapis.com (the stylesheet). There
        is no `font-src` exception in the nine directives. A blocked font does
        not error — it falls back, and the screen is subtly wrong with nothing
        in the console. The prototype links Google Fonts because it is a
        standalone file with no policy over it.

     ⚠⚠ THE FILES ARE NOT IN THIS REPOSITORY YET — 5 September 2026, and this
        says so rather than pretending. `A` had no network from the machine it
        ran on: cdn.jsdelivr.net did not resolve from the shell and
        `fetch()` from the app origin failed, so the three `.woff2` files could
        not be fetched and `@font-face` is NOT declared below. Declaring one
        over files that are absent is a 404 on every page load and the same
        silent fallback the paragraph above refuses.

        `frontend/fonts/README.md` carries the four commands that finish it —
        three files, the licence, and the `@font-face` block to paste back. The
        stack below is already correct: with the files present the face is
        used, and without them it falls through to Cascadia Mono exactly as it
        did before, which is what every fallback in it is for. */
  --font: "Segoe UI", "Noto Naskh Arabic", "Tahoma", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", "Cascadia Mono", "Consolas", "Courier New", monospace;
  --size-0: 0.75rem;   /* 046: captions on the master-data grid - the smallest size in use */
  --size-1: 0.8125rem;
  --size-2: 0.9375rem;
  --size-3: 1.125rem;
  --size-4: 1.5rem;
}

/* =============================================================================
   ★★ THE LANE — one attribute, and every component follows it
   =============================================================================
   Put `data-lane="sales"` on any element and everything inside it that paints
   from `--accent` turns green; `data-lane="ops"` turns it blue. Nothing else
   changes and no second stylesheet exists.

   `--accent-soft` moves WITH `--accent` and is not optional: the pair is what a
   chip is made of, and a sales chip drawn as green text on a blue tint is a
   chip that says two things at once.

   NOTHING SETS THE ATTRIBUTE YET — the quotation screens are `C`. It is
   defined here because this is where a colour is born, and because a lane
   invented on the screen that needs it first is a lane the second screen
   spells differently. */
[data-lane="sales"] { --accent: var(--sales); --accent-soft: var(--sales-soft);
                     --accent-line: var(--sales-line); }
[data-lane="ops"]   { --accent: var(--ops);   --accent-soft: var(--ops-soft);
                     --accent-line: var(--ops-line); }

/* =============================================================================
   ★★ S §1 · THE NILE DOCUMENT — the palette of the two PRINTED pages
   =============================================================================
   Karim, 9 September 2026, note ⑧: «make it coloured and put Nile logo and make
   it look nice and professional… maybe a manager say to them print that request
   and bring it to me».

   ⛔⛔ THIS IS NOT A FOURTH ACCENT AND IT IS NOT A REPAINT. §9.4 forbids
      revisiting the palette, and nothing above this line moves: `--accent`,
      `--sales` and `--ops` are untouched, and no SCREEN paints from a token
      below. These belong to the two documents that go on PAPER — the internal
      request and the client's quotation — and to the one panel that borrows
      their layout without becoming a third printable thing.

   ★ AND THE THREE BRAND COLOURS ARE MEASURED, NOT CHOSEN. `documents/brand/`
     says how: they were read off the PIXELS of the logo lifted out of
     `Quotations.pdf` with its `/SMask`, not sampled from a screenshot and not
     picked by eye. The first attempt was traced from a screenshot, Karim said
     so, and the file is the fact.

   ⚠ THE LIME IS NOT A TEXT COLOUR. Its contrast on white is poor — it belongs
     to the mark and to a rule, and the document's headings take the deep green
     the quotation Nile already issues uses. ⛔ Never `color: var(--nile-lime)`
     on body text.

   ★ AND THE DOCUMENT HAS ITS OWN INK. A quotation is read on paper under an
     office light, not on this palette's cool screen ground — so `--doc-ink` is
     warmer and softer than `--ink`, and the greys below are the ones measured
     off the document Nile issues today rather than the screen's four levels. */
:root {
  --nile-blue: #245A94;         /* the wordmark                              */
  --nile-cyan: #00B4E4;         /* the left form of the mark                 */
  --nile-lime: #B0E020;         /* the right form — a rule, never a word     */
  --nile-green: #16745C;        /* the heading green of the issued document  */
  --nile-green-deep: #115C49;   /* the table head and the totals band        */
  --nile-amber: #E0842B;        /* the meta column, and the customs rate     */
  --nile-mint: #E7F2EE;         /* the services band                         */

  --doc-ink: #22333D;
  --doc-ink-soft: #5A6B75;
  --doc-ink-faint: #8A9AA3;
  --doc-line: #D5DEE2;
  --doc-line-soft: #E8EEF0;
  /* ⛔ AND THERE IS NO SECTION-BAND TOKEN, WHICH IS A DECISION AND NOT AN
     OVERSIGHT. Karim's own sheet groups its charges under two headings —
     «Customs official receipts» and «Nile Logistics services» — and that
     grouping is `charge_lines.band`, which `OfferChargeLine` leaves out ON
     PURPOSE: «pass-through tells a client which lines Nile makes nothing on».
     Two tokens for it were written here and deleted again the moment
     `test_no_token_is_defined_and_unused` asked what reads them. Whether the
     client's sheet should carry those two headings is Karim's to answer. */
  --doc-band-ink: #12463A;
  /* A CURRENCY WITH NO MONEY IN IT IS GREYED, NOT DROPPED — `18 §٣.١`'s shape
     on paper: the client sees that EUR and GBP were considered and are zero. */
  --doc-off: #B6C2C8;
  --doc-rate-soft: #FDF0E1;
  --doc-rate-ink: #9C5511;
}

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

html {
  font-family: var(--font);
  font-size: 16px;
  color: var(--ink);
  background: var(--paper-soft);
}

body {
  margin: 0;
  padding: var(--gap-4);
  font-size: var(--size-2);
  line-height: 1.6;
  /* `start`, not `right`. In an English page this is the left edge and no rule
     anywhere had to say so. */
  text-align: start;
}

h1, h2, h3 { margin: 0 0 var(--gap-3); line-height: 1.3; font-weight: 600; }
h1 { font-size: var(--size-4); }
h2 { font-size: var(--size-3); }
h3 { font-size: var(--size-2); text-transform: none; }

p { margin: 0 0 var(--gap-3); }

code, pre, .mono { font-family: var(--font-mono); font-size: var(--size-1); font-variant-numeric: tabular-nums; }

/* --- layout primitives ---------------------------------------------------- */

.page {
  max-width: 60rem;
  /* `margin-inline` centres in both directions with one declaration. */
  margin-inline: auto;
}

.stack > * + * { margin-block-start: var(--gap-3); }

.row {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  /* No `float`, anywhere. Flex has no direction of its own to get wrong — it
     follows the writing mode, which is what `dir` sets. */
  flex-wrap: wrap;
}

.card {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-3) var(--gap-4);
}

.muted { color: var(--ink-soft); }
.faint { color: var(--ink-faint); font-size: var(--size-1); }

/* --- controls -------------------------------------------------------------- */

button {
  font: inherit;
  color: var(--ink);
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  cursor: pointer;
}

button:hover { background: var(--paper-soft); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- the three states every screen needs ---------------------------------- */
/* Principle 1.2 in the interface, and doctrine §2.3 states it as a rule for
   every table: «كل جدول: صفر صف — تظهر «لا بيانات» لا شاشة فارغة تبدو معطّلة».
   An empty area is read as "broken"; the word is what makes it an answer. */

.state {
  padding: var(--gap-4);
  border: var(--line-width) dashed var(--line);
  border-radius: var(--radius);
  text-align: center;
  color: var(--ink-soft);
}

.state-empty { background: var(--paper); }
.state-loading { background: var(--paper); color: var(--ink-faint); }

/* An error carries three things — code, reason, what to do now (doctrine §15) —
   so it needs room for three, not a one-line strip. */
.state-error {
  text-align: start;
  background: var(--stop-soft);
  border: var(--line-width) solid var(--stop);
  border-radius: var(--radius);
  padding: var(--gap-3) var(--gap-4);
  /* The accent bar is on the START edge: the right in Arabic, the left in
     English, from one declaration. `border-left` here would be the single most
     visible thing wrong with the English build. */
  border-inline-start: 4px solid var(--stop);
}

.state-error h3 { color: var(--stop); }

.error-field { display: flex; gap: var(--gap-2); align-items: baseline; }
.error-label { color: var(--ink-soft); font-size: var(--size-1); min-width: 6rem; }
/* ★ THE REQUEST ID IS SELECTED WHOLE BY ONE CLICK — 038, and it is the answer
   to a question `spec-04 §ش.٣` asks and does not settle: «ومن يقرأ رفضاً على
   الشاشة يجب أن يستطيع مناولةَ الرقم».

   `user-select: all` selects the ENTIRE value on a single click, so a person
   reporting a refusal presses once and copies rather than dragging across
   thirty-two characters of hex and getting thirty-one of them.

   **AND NOT A COPY BUTTON**, which was the obvious answer and is the wrong one:
   `navigator.clipboard.writeText` requires a SECURE CONTEXT. It works on
   `localhost` and behind TLS and **fails silently on plain http over a LAN** —
   which is exactly how this system is reached today. A control that does
   nothing and says nothing is ERROR-BOOK 1's family: a difference between the
   machine it was written on and the machine it runs on that does not announce
   itself. `user-select` has no such condition and needs no permission.

   It applies to `.error-value` and so covers the CODE as well as the id, which
   is right: both are things somebody quotes and neither is prose. */
.error-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
  user-select: all;
}

/* 135 · the why line, and the fold for the engineer under every refusal */
.error-cause span:last-child { color: var(--ink); }
.error-engineer { margin-block-start: var(--gap-2); }
.error-engineer summary { cursor: pointer; color: var(--ink-soft); font-size: var(--size-1); }
.error-detail {
  margin: var(--gap-2) 0;
  padding: var(--gap-2) var(--gap-3);
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: var(--size-1);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  direction: ltr;
  text-align: start;
  user-select: all;
}
.error-copy { font: inherit; font-size: var(--size-1); padding: 0.25rem 0.75rem; border: var(--line-width) solid var(--line); border-radius: var(--radius); background: var(--paper); color: var(--ink); cursor: pointer; }

.notice {
  background: var(--warn-soft);
  border-inline-start: 4px solid var(--warn);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  color: var(--warn);
}

/* --- lists ---------------------------------------------------------------- */

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

.list li {
  padding-block: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
}

.list li:last-child { border-block-end: none; }

.key {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
  color: var(--accent);
  /* `margin-inline-end` — the gap after the key, whichever way the text runs. */
  margin-inline-end: var(--gap-2);
}

/* =============================================================================
   F2 — the shell, and the two auth screens
   =============================================================================
   EVERY RULE BELOW EXISTS BECAUSE A `style="..."` ATTRIBUTE COULD NOT.
   `documents/design/nile-screens.html` is written entirely in inline styles —
   it is a design file and says so — and the deployed policy is
   `style-src 'self'` with no `unsafe-inline` (spec-02 §د.٥). An inline style
   attribute therefore APPLIES on this laptop and is INERT behind nginx: the
   page looks finished here and arrives unstyled there, with nothing announcing
   the difference. `tools/check-css.sh` refuses one in any HTML under
   `frontend/`, and it was induced before it was trusted.

   AND EVERY RULE IS LOGICAL. The design file uses `dir="rtl"` on its frames
   and physical properties in places, because it was pinned to one direction.
   The system is not — doctrine §12.1. What is copied from the design is the
   measurement and the colour, never the direction.

   THE TOKENS ARE THE ONES ALREADY AT THE TOP OF THIS FILE. The design's
   `#0f5c4a` is `--accent`, `#dcdcd6` is `--line`, `#f6f6f4` is `--paper-soft`,
   `#888` is `--ink-faint`, `#555` is `--ink-soft`. Not one hex value is
   repeated below; a theme change stays one block.
   ============================================================================= */

/* --- sizes the design fixes, kept as tokens so they are named once --------- */
:root {
  --header-height: 56px;
  --sidebar-width: 206px;
  --auth-width: 400px;
  --auth-width-wide: 440px;
  --mark-size: 26px;
  --mark-size-lg: 40px;
  --avatar-size: 28px;
}

/* --- the shell ------------------------------------------------------------ */
/* Read from design screen 01, which is the only screen that shows it whole. */

/* =============================================================================
   ★★ THE PAGE SCROLLS · THE CHROME STICKS — A, and it UNDOES 031's decision
   =============================================================================
   Karim: «I want the request on the whole screen, and when I scroll down it
   scrolls with me. The only part that doesn't scroll is the sidebar.»

   031 GAVE `.app` A CEILING — `height: 100vh; height: 100dvh` — and its
   reasoning was sound as far as it went. It had found «the page scrolled while
   two correctly written scroll panes sat unreachable», and pinning the shell
   gave those panes a definite height to divide, so they scrolled.

   ⛔ THE QUESTION UNDERNEATH WAS NEVER ASKED: should a list of 454 rows and its
      detail be two independent scrolling panes at all? For something a person
      PAGES THROUGH, no. Pinning the shell made the panes work and made every
      list a four-row window — measured on this laptop at 1536×730, in Arabic,
      before one line of this block moved:

          charge types   310px of chrome · FIVE of 454 rows · a 630px pane —
                         41% of the window — reading «Select a charge type»
          the change log 419px of chrome · FOUR of 43 rows
          requests       459px of chrome · THREE of 7 rows
          users          the list a 320px column with BOTH scrollbars, a
                         four-word name printing ONE WORD PER LINE in a 16px
                         box, and 66% of the window reading «Choose an employee»

      Karim reached the same place without reading the CSS: «I think this was
      because from the beginning it is splitting the screen and Claude Code
      solved it in the wrong way.»

   ⟹ SO THE CEILING GOES BACK TO BEING A FLOOR, and the inner scrollers go with
     it. `min-block-size: 100vh` says «never shorter than the window» and
     nothing about «never taller», which is exactly right for a document: the
     PAGE grows and the PAGE scrolls, once, with one scrollbar the wheel always
     finds.

   ★ 031'S LESSON IS KEPT AND ITS FIX IS NOT. What it discovered is that a
     scrolling pane needs a definite height to divide. The answer `A` gives is
     that these panes should not be scrolling panes — so nothing needs a
     definite height, and `min-block-size` cannot re-create the defect because
     there is no pane left for it to starve.

   ★ AND `100dvh` IS NOT WRITTEN HERE ANY MORE. A floor of `100vh` on a phone
     is the height with the URL bar retracted — slightly too tall — and the
     failure mode of a floor that is too tall is a few blank pixels under the
     content, not a shell taller than the screen with unreachable panes. 031
     needed both units because a browser that DROPPED `100dvh` was left with no
     height at all. A floor has no such cliff, so it is one declaration.

   `test_A_the_screens.py` reads this rule and refuses a `.app` that carries a
   `height`, which is the way back. */
.app {
  display: flex;
  flex-direction: column;
  min-block-size: 100vh;
  background: var(--paper-soft);
}

/* The `.page` primitive centres and caps its width; the shell must do neither,
   so the shell's body drops the page padding instead of inheriting it. */

/* =============================================================================
   THE STICKY STACK — four things stick, and each sits below the one above it
   =============================================================================
       0                        .app-header    z-index 30
       var(--header-height)     .sidebar       z-index 20
       var(--header-height)     .list-top      z-index 20   ← the page toolbar
                                                              AND the column
                                                              names, in ONE box
       above all of them        .modal-backdrop / .modal  40 / 41

   ★ THE COLUMN NAMES ARE INSIDE `.list-top` RATHER THAN STICKY ON THEIR OWN,
     and that is the whole reason this stack has three entries and not four. A
     column row sticking under the toolbar needs `header + toolbar height` as
     its offset — a number no stylesheet knows, because the toolbar is a
     different height in Arabic and in English and on every one of the eight
     list screens. Put it INSIDE the sticky box and there is no second offset
     to get wrong.

   ⚠⚠ `position: sticky` SILENTLY DOES NOTHING if any ancestor has `overflow`
      set to anything but `visible`. That is why the block below deletes the
      inner scrollers rather than leaving them «harmless»: with `.app-main`
      still carrying `overflow: auto`, every rule in this stack would compute,
      cascade, and have no effect at all — and nothing would say so. Measured
      in a browser after the change, not assumed. */
.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-3);
  background: var(--paper);
  border-block-end: var(--line-width) solid var(--line);
  padding-inline: var(--gap-4);
  min-height: var(--header-height);
  flex-shrink: 0;
  position: sticky;
  inset-block-start: 0;
  z-index: 30;
}

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

.brand-mark {
  width: var(--mark-size);
  height: var(--mark-size);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--paper);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.brand-name { font-weight: 600; }

.header-tools { display: flex; align-items: center; gap: var(--gap-2); }

/* The language toggle. Quieter than a `button` default because it is chrome,
   not an action on the data. */
.btn-quiet {
  font-size: var(--size-1);
  color: var(--ink-soft);
  padding: var(--gap-1) var(--gap-3);
}

/* `border-inline-start` — the separator before the user block sits on the
   START side, which is the right in Arabic and the left in English, from one
   declaration. `border-left` here is the single most visible thing that would
   be wrong in the English build. */
.user-block {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding-inline-start: var(--gap-3);
  border-inline-start: var(--line-width) solid var(--line);
}

.user-menu { position: relative; }

.user-menu-button {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  border: none;
  background: transparent;
  padding: var(--gap-1) var(--gap-2);
  border-radius: var(--radius);
}

.user-menu-button:hover { background: var(--paper-soft); }

.avatar {
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--size-1);
  font-weight: 600;
  flex-shrink: 0;
}

.user-name { font-size: var(--size-1); color: var(--ink-soft); }

/* The menu is shown and hidden with the `hidden` ATTRIBUTE and this one rule,
   never with an inline `style="display:none"` — see the header of this
   section. `hidden` also tells assistive technology, which a style does not. */
.user-menu-panel {
  position: absolute;
  inset-block-start: 100%;
  /* `inset-inline-end: 0` — flush with the END edge of the button, which flips
     with the document. `right: 0` would pin it to the far side in one of the
     two builds and nothing would say which. */
  inset-inline-end: 0;
  min-width: 12rem;
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-1);
  margin-block-start: var(--gap-1);
  z-index: 20;
}

.user-menu-panel[hidden] { display: none; }

/* ★ THE LINK AND THE BUTTON ARE ONE MENU AND LOOK LIKE ONE — A. «My profile»
   is an `<a>` because it navigates and «Sign out» is a `<button>` because it
   performs an action, and that distinction belongs in the MARKUP and not on the
   screen: drawn as an underlined link beside a plain button, one menu read as
   two kinds of thing. Seen in a browser. */
.user-menu-panel a,
.user-menu-panel button {
  display: block;
  width: 100%;
  text-align: start;
  border: none;
  background: transparent;
  color: inherit;
  text-decoration: none;
  padding: var(--gap-1) var(--gap-2);
  border-radius: var(--radius);
}

.user-menu-panel a:hover,
.user-menu-panel button:hover { background: var(--paper-soft); color: var(--ink); }

.app-columns { display: flex; flex: 1; }

/* THE ONE THING THAT DOES NOT SCROLL — Karim's sentence, and the only pane in
   the system that keeps a scrollbar of its own. It is a fixed list of links,
   not data: it has a known length, it never pages, and a person who has
   scrolled 800px down a list still needs to be able to leave the screen.
   `--header-height` and not `56px`: the offset and the header are one number.

   `block-size` — the sidebar is as tall as the window under the header, so its
   own overflow only ever appears when the navigation itself is longer than the
   screen. */
.sidebar {
  width: var(--sidebar-width);
  flex-shrink: 0;
  background: var(--paper);
  border-inline-end: var(--line-width) solid var(--line);
  padding: var(--gap-3) var(--gap-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  inset-block-start: var(--header-height);
  z-index: 20;
  block-size: calc(100vh - var(--header-height));
  overflow-y: auto;
}

.sidebar-group {
  font-size: var(--size-1);
  color: var(--ink-faint);
  letter-spacing: 0.04em;
  padding: var(--gap-2) var(--gap-2) var(--gap-1);
}

/* ★★ G §4 — A GROUP HEADING THAT OPENS AND CLOSES.

   Karim: «when the ERP gets all of its departments it will be so big — i want,
   when i click on sales, it opens the sales pages under it, and click again it
   closes».

   IT IS A `<button>`, and that is the whole accessibility of this control: it
   is in the tab order, Enter and Space work on it for free, and
   `aria-expanded` is what a screen reader announces. A `<div>` with a click
   handler would look identical and be unreachable without a mouse.

   THE STATE LIVES ON THE ELEMENT AND THE DRAWING FOLLOWS IT. The chevron turns
   because `aria-expanded="true"` is set, never the other way round — so there
   is no second copy of «is this open» that can disagree with what is announced.

   `background: none; border: 0` because a button element brings both, and this
   is a heading that happens to be pressable rather than a button that happens
   to be a heading. It keeps the same size, colour and letter-spacing as the
   headings above it — those it replaces — so nothing about the column moved
   except that it now opens. */
.sidebar-group.is-toggle {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  inline-size: 100%;
  background: none;
  border: 0;
  font: inherit;
  font-size: var(--size-1);
  letter-spacing: 0.04em;
  color: var(--ink-faint);
  text-align: start;
  cursor: pointer;
  border-radius: var(--radius);
}

.sidebar-group.is-toggle:hover { color: var(--ink-soft); }

.sidebar-group.is-toggle[aria-expanded="true"] {
  color: var(--accent);
  font-weight: 600;
}

/* `margin-inline-start: auto` puts the chevron at the far edge in BOTH builds —
   the left of the sidebar in Arabic, the right in English — with no `[dir]`
   rule, which is what the logical properties are for. */
.sidebar-group-name { flex: 1 1 auto; }

.nav-chevron {
  flex: 0 0 auto;
  margin-inline-start: auto;
  /* A literal, not `var(--fast)`. This project defines no timing token and
     `tools`' own guard caught the invention — a `var()` that names nothing
     falls back silently, which is the whole reason that check exists. */
  transition: transform 120ms ease;
}

/* ⛔ THE MARK POINTS DOWN AND UP, NEVER LEFT OR RIGHT — and that is a doctrine
   §12.1 decision, not a taste one. A sideways chevron has to be flipped for the
   Arabic build, and flipping it means a `[dir="rtl"]` rule, which
   `tools/check-css.sh` refuses by name: «the logical properties flip
   themselves; a [dir] override is what a project writes when it did not use
   them». The first draft of this block had exactly that rule and the guard
   stopped it.

   DOWN means «there is more under this heading»; UP means «press to fold it
   away». Neither has a reading direction, so ONE rule serves both builds. */
.sidebar-group.is-toggle[aria-expanded="true"] .nav-chevron {
  transform: rotate(180deg);
}

/* The items of one group. `[hidden]` is what closes it — the attribute the
   button's `aria-controls` points at — rather than a class, so the element is
   removed from the accessibility tree and from the tab order together. A
   `display: none` driven by a class would need the same thing said twice. */
.nav-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-block-end: var(--gap-2);
}

.nav-group[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .nav-chevron { transition: none; }
}

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-2);
  border-radius: var(--radius);
  color: var(--ink-soft);
  text-decoration: none;
}

.nav-item.is-active {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

/* A MODULE WITH NO SCREEN YET — F4.1, principle 1.2 applied to navigation.
   «صفر مخالفة» و«صفر مفحوص» ليسا الشيء نفسه، ولا يجوز أن يظهرا بالشكل نفسه:
   a thing that is absent and a thing that is failing must not look the same,
   and until F4.1 a `href: null` entry rendered through the same `.nav-item` as
   a working link.

   `cursor: default` and not `not-allowed`: nothing is being refused. The screen
   does not exist yet, which is a different statement from "you may not".

   THE STYLING IS NOT THE ANNOUNCEMENT. Dimming is invisible to a screen reader
   and to anybody whose display flattens it, so the entry also carries the words
   «لم يُبنَ بعد» in `.pending-tag`, from the catalogue, in both languages. The
   colour is the fast signal and the word is the true one. */
.nav-item.is-pending {
  color: var(--ink-faint);
  cursor: default;
}

.nav-item.is-pending .nav-icon { opacity: 0.55; }

.pending-tag {
  font-size: var(--size-1);
  line-height: 1.4;
  padding: 0 var(--gap-1);
  border-radius: var(--radius);
  border: var(--line-width) dashed var(--line);
  color: var(--ink-faint);
  background: var(--paper-soft);
  white-space: nowrap;
  /* `margin-inline-start: auto` pushes it to the far edge in BOTH builds — in
     Arabic that is the left of the sidebar, in English the right, and no rule
     anywhere had to say which. */
  margin-inline-start: auto;
  flex-shrink: 0;
}

.nav-icon { flex-shrink: 0; }

/* ⛔ `overflow: auto` IS GONE, AND IT IS THE KEYSTONE OF THE WHOLE CHANGE.
   While it was here every `position: sticky` below it was inert — a rule that
   parses, cascades and does nothing — and the page could not scroll because
   this box swallowed the scroll instead. */
.app-main {
  flex: 1;
  min-width: 0;
  padding: var(--gap-4);
}

/* --- the two auth screens ------------------------------------------------- */
/* Design screens 10 and 11. One layout, two widths. */

.auth {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gap-4);
  background: var(--paper-soft);
}

.auth-column {
  width: 100%;
  max-width: var(--auth-width);
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

.auth-column.is-wide { max-width: var(--auth-width-wide); }

.auth-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-2);
  text-align: center;
}

.auth-mark {
  width: var(--mark-size-lg);
  height: var(--mark-size-lg);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--paper);
  display: flex;
  align-items: center;
  justify-content: center;
}

.auth-heading { text-align: center; }
.auth-title { font-size: var(--size-3); font-weight: 600; margin: 0; }
.auth-sub { color: var(--ink-faint); font-size: var(--size-1); margin: 0; }
.auth-lede { color: var(--ink-soft); font-size: var(--size-1); margin: 0; }

.auth-card {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-4);
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

.auth-note {
  font-size: var(--size-1);
  color: var(--ink-faint);
  text-align: center;
  margin: 0;
}

.auth-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-3);
  font-size: var(--size-1);
  color: var(--ink-faint);
}

/* --- form controls -------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: var(--gap-1); }

.field-label { font-size: var(--size-1); color: var(--ink-soft); }

.input {
  font: inherit;
  color: var(--ink);
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  width: 100%;
  /* `start`, not `left`. An email typed into an Arabic page still reads
     left-to-right, and the `dir="ltr"` ATTRIBUTE on the input is what says so —
     an attribute on the element, not a direction baked into a stylesheet. */
  text-align: start;
}

.input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ★★ 052 · A READ-ONLY FIELD LOOKS READ-ONLY — and until now none of them did.
   MEASURED before it was written: `master-data-forms.js` has set `readonly` on
   a code field since `046` and `master-data-render.js` on the settings value,
   and there was NO rule anywhere in this stylesheet for the state. A browser's
   own default for a read-only textarea is nothing at all, so «it is locked» was
   a fact only the keyboard could discover. Rule 1b.2: a control either WORKS,
   or is refused with the reason visible — and the third of those needs a look.

   ⛔ NOTHING IS INVENTED. `--paper-inset` is the surface `.req-row.is-head`
   already uses for «this row is not data you edit», and `--ink-soft` is the
   text colour beside it. The value stays fully readable and fully selectable:
   this is not a disabled control and must not read as one.

   `[readonly]` and not `:read-only`, because `:read-only` matches every
   non-editable element in the document — every `<div>` on the page — and a
   selector that broad would put this background somewhere nobody asked. */
.input[readonly] {
  background: var(--paper-inset);
  color: var(--ink-soft);
  cursor: default;
}

/* A password is a Latin string in both builds. The monospace face is for
   reading it back once it is revealed, and it is a FONT choice, not a
   direction one. */
.input-password { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* The field and its reveal button share one bordered box, so the button reads
   as part of the control rather than as a second control beside it. */
.input-row {
  display: flex;
  align-items: stretch;
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
}

.input-row:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }

.input-row .input {
  border: none;
  border-radius: 0;
  background: transparent;
}

.input-row .input:focus-visible { outline: none; }

.reveal {
  font: inherit;
  font-size: var(--size-1);
  color: var(--accent);
  background: transparent;
  border: none;
  border-radius: var(--radius);
  padding-inline: var(--gap-3);
  display: flex;
  align-items: center;
  gap: var(--gap-1);
  flex-shrink: 0;
  cursor: pointer;
}

.reveal:hover { background: var(--accent-soft); }

.check-row { display: flex; align-items: center; gap: var(--gap-2); }
.check-row label { font-size: var(--size-1); color: var(--ink-soft); }
.check-row input { accent-color: var(--accent); }

.btn-primary {
  font: inherit;
  color: var(--paper);
  background: var(--accent);
  border: var(--line-width) solid var(--accent);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  width: 100%;
  cursor: pointer;
}

.btn-primary:hover { background: var(--ink); border-color: var(--ink); }

.btn-primary[disabled] {
  background: var(--ink-faint);
  border-color: var(--ink-faint);
  cursor: default;
}

/* The two live hints under the new-password field. Both are labels with the
   number appended AFTER them by the caller — card D's count rule. */
.field-hint {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  font-size: var(--size-1);
  color: var(--ink-faint);
}

.field-hint.is-met { color: var(--accent); }

/* --- links ---------------------------------------------------------------- */

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

/* A link rendered as a button, for the language switch on the auth screens.
   A `<button>` and not an `<a href="#">`: it performs an action on this page
   and navigates nowhere, and `safeUrl` would send `#` anyway. */
.link-button {
  font: inherit;
  font-size: var(--size-1);
  color: var(--accent);
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
}

/* =============================================================================
   F3 — the chart of accounts
   =============================================================================
   THE SAME TWO RULES AS EVERY BLOCK ABOVE, and F3 is where they bite hardest
   because the design file for this screen is the largest one in the project:

     · NO `style="…"` ANYWHERE. `style-src 'self'` carries no `unsafe-inline`
       (spec-02 §د.٥), so an inline attribute applies on this laptop and is
       inert behind nginx. `tools/check-css.sh` refuses one in any HTML under
       `frontend/`, and design/nile-screens.html is written entirely in them.
     · LOGICAL PROPERTIES ONLY — doctrine §12.1. The design frames are pinned
       to `dir="rtl"`; the system is not.

   INDENTATION IS FIVE CLASSES, NOT A COMPUTED VALUE, and that is forced by the
   first rule: the natural way to indent a tree is `style="--depth:3"`, which is
   an inline style. The tree is bounded at FIVE LEVELS by
   `ck_acc_level_matches_code`, so five classes cover it exactly and a sixth
   would be a level the database refuses to store.

   **IT SAID FOUR UNTIL 27 AUGUST 2026 AND THE RULE HAD MOVED UNDER IT.** `024`
   made the code nine digits in five levels and `.tree-row.depth-5` was added at
   the time; this paragraph was not, so it went on describing a four-level tree
   above a stylesheet that draws five. `025` is the task that first PUTS
   ACCOUNTS at level five — the banks and the treasury currencies — which is why
   it is corrected here rather than left.
   ============================================================================= */

:root {
  /* THE NARROWEST A COMPANY NAME MAY BE — 040. It is a token rather than a
     number in one rule because four row classes now honour it, and a floor
     that two of them agree on and two do not is not a floor. */
  --name-floor: 11rem;
  /* ⛔ `--tree-width: 700px` IS GONE. It was the width of the list COLUMN, and
     there is no column: the list is the content area and the detail is a
     slide-over. An unused token is a thing somebody re-applies by name later,
     which is ERROR-BOOK item 32's shape in a stylesheet — and this one would
     put the two-pane layout back one rule at a time. The `040` comment at
     `.party-name` still names it, because that is history and history is what
     comments are for. */
  /* THE SLIDE-OVER'S WIDTH. It was 27rem when the detail was a column that took
     whatever `flex: 1` left it and this token was never the thing that decided
     it. Now it IS the thing: 33rem is 528px, inside the 460–560 the design
     asks for, and wide enough for the two-column forms to become one clean
     column rather than two starved ones. */
  --detail-width: 33rem;
  --indent-step: 28px;
  /* THE CODE COLUMN — 9ch, and it is a MEASURED width rather than a guess.
     It was 40px, sized when a code was four digits; `024` made every code NINE
     and nothing widened it, so the column relied on `min-width` losing and the
     name column absorbing the difference. `ch` in a monospace face is exactly
     one digit, so nine digits fit and no row's name starts at a different
     inline offset from its neighbour's. §6و — the code shown is the code
     stored, so nine is the only width there will ever be. */
  --code-column: 9ch;
  --balance-column: 150px;
  /* THE ROW-ACTION SLOT, AND IT IS RESERVED ON EVERY ROW — F4.1.
     Four controls of 22px, a 1px gap between them, 2px of padding either side
     and a 1px border: 88 + 3 + 4 + 2 = 97, rounded up so the widest possible
     bar never touches the edges.

     THE DESIGN RESERVES 34px HERE AND THE BAR IT DRAWS IS ~97px WIDE — so the
     mockup's own hovered row is 63px wider than its neighbours, and the name
     column absorbs the difference. That is the layout shift a row must not
     have: a row that moves under the cursor makes people click the wrong
     thing. So the slot is sized to the BAR rather than to the mockup's
     placeholder, and it is present on every row whether or not a bar is
     drawn in it. */
  --actions-column: 100px;
}

/* --- the two-column body inside the shell's main region ------------------- */

/* ★★ THE LIST IS THE FULL WIDTH OF THE CONTENT AREA — A.
   `--tree-width: 700px` and `flex-shrink: 0` made it a column beside a pane;
   the pane is now a slide-over (see `.chart-detail` below) and there is nothing
   left to sit beside. The token is deleted with the rule that used it. */
/* `flex: 1` — `.app-main.is-chart` is itself a flex container, so without it
   this box sizes to its CONTENT and the list stops 350px short of the window.
   Seen in a browser at 1536px, in Arabic, on the first paint after the pane was
   removed. `min-inline-size: 0` so a wide row makes the row scroll and not the
   page. */
.chart {
  display: flex;
  flex: 1;
  min-inline-size: 0;
  gap: 0;
}

.chart-tree {
  flex: 1;
  min-inline-size: 0;
  background: var(--paper);
  display: flex;
  flex-direction: column;
}

/* =============================================================================
   ★★ THE DETAIL IS A PANEL OVER THE LIST, NOT A COLUMN BESIDE IT — A
   =============================================================================
   Karim: «I want the screen full, and when I click on something it splits —
   okay — to show it to me, but it must have an X button so after I look at the
   information I can click it and the screen expands again. Not a fixed thing
   like that.»

   ★★ AND `position: fixed` IS THE POINT, NOT THE STYLING. His oldest complaint
      about Milestone 1: «if I scrolled down and clicked on something, it opens
      its information but I can't see it — I have to go all the way up.» That
      happens because a detail placed in the DOCUMENT renders at the top of its
      column while the reader is 800px down it. A panel pinned to the WINDOW is
      in view wherever the page is scrolled. One property, and the complaint
      cannot recur.

   ⚠ AND IT KEEPS AN `overflow-y`, WHICH THE REST OF THIS TASK REMOVES.
     That is not an oversight and it is the one exception. Every other pane lost
     its scroll because the PAGE can scroll instead; this one cannot use that
     escape, because it is pinned to the window and the page moving does not
     move it. The accounts card measures 900–1400px against a 674px panel — so
     without this the last third of it, including the action row, would be
     unreachable by any means. A fixed box that clips is worse than the column
     it replaced.

   ⚠ NOTHING PHYSICAL. `inset-inline-end` and not `right`: the panel comes in
     from the READING END, which is the left in Arabic and the right in
     English, from one declaration. And there is NO `transform` centring here —
     ERROR-BOOK's `.modal` trap, where `inset-inline-start` flips under `rtl`
     and a `translate(-50%)` does not, so the two disagree and the box lands
     half its own width off. A panel anchored to ONE edge cannot have it.

   `[hidden]` is the closed state and it is an ATTRIBUTE, which also tells
   assistive technology — a `display: none` class does not. The rule is
   explicit because `display: flex` here would otherwise outrank the user
   agent's `[hidden]`, which is the same trap `.search-clear[hidden]` and
   `.user-menu-panel[hidden]` already carry. */
.chart-detail {
  position: fixed;
  inset-block-start: var(--header-height);
  inset-block-end: 0;
  inset-inline-end: 0;
  inline-size: min(var(--detail-width), 100vw);
  z-index: 25;
  overflow-y: auto;
  background: var(--paper);
  border-inline-start: var(--line-width) solid var(--line);
  box-shadow: var(--shadow-panel);
  padding: var(--gap-3) var(--gap-4) var(--gap-5);
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

.chart-detail[hidden] { display: none; }

/* The panel's CONTENT, and it is a box of its own so that every screen's
   `setHTML(document.getElementById('detail'), …)` keeps working untouched —
   all forty-one call sites across seven screens. The ✕ lives OUTSIDE it, in
   `.panel-head`, because a control inside would be wiped on the first repaint.
   It carries the column layout `.chart-detail` used to. */
.chart-detail-body {
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

/* The ✕. It sits at the END of the panel's own head row, which flips with the
   document — `margin-inline-start: auto` and no side is named. It is drawn by
   `shell.js` OUTSIDE `#detail`, because every screen's render replaces the
   whole of `#detail` and a control inside it would be wiped on the first
   repaint. */
.panel-head {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  position: sticky;
  inset-block-start: calc(-1 * var(--gap-3));
  background: var(--paper);
  margin-block-start: calc(-1 * var(--gap-3));
  padding-block: var(--gap-2);
  z-index: 1;
}

.panel-close {
  font: inherit;
  margin-inline-start: auto;
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink-soft);
  cursor: pointer;
  inline-size: 26px;
  block-size: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.panel-close:hover { background: var(--paper-soft); color: var(--ink); }
.panel-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* A form inside a 27rem panel: the two-column grids that were written for a
   630px column would give each field 12rem and break every label. One column,
   and only inside the panel. */
.chart-detail .field-grid,
.chart-detail .derived-grid,
.chart-detail .profile-cards,
.chart-detail .stale-diff,
.chart-detail .kind-grid { grid-template-columns: minmax(0, 1fr); }

/* The shell's main region pads itself; the chart owns its own edges. */
.app-main.is-chart { padding: 0; display: flex; }

/* =============================================================================
   ★ THE PAGE TOOLBAR — one sticky box, and the column names ride inside it
   =============================================================================
   Karim: «the only thing that can appear, but on the top and its size
   decreases, is the search.»

   THE MEASUREMENT THIS EXISTS TO FIX. Up to 459px of a 730px window went on
   chrome before a row was drawn: a title, a subtitle, a count line of its own,
   a FULL-WIDTH search box, three separate rows of chips, a dropdown, a Filters
   button and the sentence «No filter is applied». Three of those seven lines
   said nothing a reader could act on and are gone (see the report); the rest
   are on TWO lines instead of six.

   ⛔ THE SUBTITLE IS NOT IN HERE. `.chart-lead` and the log's lede moved ABOVE
      `.list-top` in the markup, so they are read once at the top of the page
      and scroll away — «the subtitle may live in the full header at scroll-top
      and disappear on scroll», which is what a sentence explaining what a list
      IS is worth once somebody is reading row 200.

   ★ AND THE COLUMN NAMES ARE THE LAST CHILD OF `.list-top`. A 454-row table
     that page-scrolls loses its column names after row twelve, and then nobody
     knows which column is Balance. See the stack at `.app-header`. */
.list-top {
  position: sticky;
  inset-block-start: var(--header-height);
  z-index: 20;
  background: var(--paper);
}

.chart-head {
  padding: var(--gap-2) var(--gap-4);
  border-block-end: var(--line-width) solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
  flex-shrink: 0;
}

/* THE TITLE, THE COUNTS AND THE ACTIONS ON ONE LINE. `margin-inline-start:
   auto` on the actions pushes them to the far edge in BOTH builds — the left in
   Arabic, the right in English — and no rule anywhere had to say which. It
   replaces `justify-content: space-between`, which put the counts in the middle
   of the row the moment a third child arrived. */
.chart-title-row {
  display: flex;
  align-items: center;
  gap: var(--gap-2) var(--gap-3);
  flex-wrap: wrap;
}

.chart-title-row .head-actions,
.chart-title-row .log-head-tools { margin-inline-start: auto; }

/* ⚠ `.btn-primary` CARRIES `width: 100%` — it was written for the login card,
   where it is the only control on its row. On the users screen it is a direct
   child of the title row and rendered as a full-width blue BAR across the whole
   window, which reads as a banner rather than as a button. Seen in a browser in
   both languages. Same defect and same remedy as `.req-form-foot .btn-primary`;
   one more scope. */
.chart-title-row > .btn-primary { margin-inline-start: auto; width: auto; }

/* THE TOOLBAR'S CONTROLS ARE TOOLBAR-SIZED. `.btn-primary` is written for the
   login card at 39px tall; in a title row it is the tallest thing on the line
   and sets the height of a strip that is on the screen for ever. */
.chart-title-row .btn-primary,
.chart-title-row .btn-secondary { padding-block: var(--gap-1); }

/* THE SECOND LINE — the search, the state chips, and whatever filter control
   the screen owns. `flex-wrap` so it becomes two lines on a narrow window
   rather than pushing anything off the edge. */
.chart-tools {
  display: flex;
  align-items: center;
  gap: var(--gap-2) var(--gap-3);
  flex-wrap: wrap;
}

/* ★ COMPACT, NOT FULL WIDTH. `.search-box` had no width of its own, so it took
   the whole toolbar and pushed the chips onto a line of their own. It is one
   control among four now, and 22rem is comfortably more than the longest thing
   anybody types into it. */
/* ★ AND THE CONTROL ITSELF IS SHORTER IN HERE. `.input` is written for a
   stacked form, where 42px is right; in a toolbar every 8px is 8px of the list.
   Scoped to `.chart-tools`, so no form anywhere else changes — the same move
   `.app-main.is-logs .log-form .input` already makes. */
/* 18rem, MEASURED RATHER THAN CHOSEN. At 22rem the requests screen's lane
   strip was squeezed to 217px and broke into two lines, which cost 15px of
   every window on the screen twenty-two people live in. 288px is still more
   than twice the longest thing anybody types into it. */
.chart-tools .search-box { flex: 0 1 18rem; min-inline-size: 11rem; }
.chart-tools .search-box .input { padding-block: var(--gap-1); }
.chart-tools .filter-bar { flex: 1 1 auto; min-inline-size: 0; }

/* ★ THE EMPTY CONTAINERS THE SCREENS FILL ARE ROWS, NOT STACKS — and this is
   measured rather than tidy-minded. `#req-lanes` holds «طلباتي» and the lane
   strip as two SIBLINGS; the div itself is `display: block`, so they stacked
   however wide the toolbar was and the strip was 49px tall instead of 34. That
   is 15px of every window on the screen twenty-two people live in.

   `div:not([class])` and not a list of ids: every one of these boxes —
   `#req-states`, `#req-lanes`, `#req-filter-bar`, `#req-applied`,
   `#parties-filter-bar`, `#parties-applied`, `#roles-filter-bar`,
   `#roles-applied`, `#category-filter-box` — is a class-less div that exists
   only to be filled by JavaScript, and the next one somebody adds will be too.
   An id selector here would be the same rule written nine times. */
.chart-tools > div:not([class]) {
  display: flex;
  align-items: center;
  gap: var(--gap-1) var(--gap-2);
  flex-wrap: wrap;
  min-inline-size: 0;
}

.chart-title { margin: 0; font-size: var(--size-3); font-weight: 600; }

/* Every number on this line is TERMINAL after a label — card D's count rule.
   «عدد الحسابات: ١٩٨» is right at every value; «١٩٨ حساباً» is wrong at 1
   and at 2. */
.chart-counts {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
  color: var(--ink-faint);
  /* It sits ON the title line now rather than under it. `margin: 0` because a
     `p` carries a block margin from the element rule, and 16px of it under a
     line that is beside a heading is 16px of nothing. */
  margin: 0;
}

.search-box {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding-inline: var(--gap-3);
  background: var(--paper-soft);
}

.search-box:focus-within {
  border-color: var(--accent);
  background: var(--paper);
}

.search-box .input {
  border: none;
  background: transparent;
  padding-inline: 0;
}

.search-box .input:focus-visible { outline: none; }

.search-clear {
  font: inherit;
  font-size: var(--size-1);
  color: var(--ink-faint);
  background: transparent;
  border: none;
  cursor: pointer;
  padding: var(--gap-1);
}

.search-clear:hover { color: var(--ink); }
.search-clear[hidden] { display: none; }

.chip-row { display: flex; gap: var(--gap-1); flex-wrap: wrap; }

.chip {
  font: inherit;
  font-size: var(--size-1);
  padding: 2px var(--gap-3);
  border-radius: 99px;
  background: var(--paper-soft);
  color: var(--ink-soft);
  border: var(--line-width) solid var(--line);
  cursor: pointer;
}

.chip:hover { background: var(--accent-soft); }

.chip.is-on {
  background: var(--accent);
  color: var(--paper);
  border-color: var(--accent);
}

/* --- the column header and the rows -------------------------------------- */

.tree-columns {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-1) var(--gap-3);
  background: var(--paper-soft);
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-1);
  color: var(--ink-faint);
  flex-shrink: 0;
}

.tree-scroll { padding: var(--gap-1); }

.tree-row {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-1) var(--gap-2);
  border-radius: var(--radius);
  width: 100%;
  border: none;
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: start;
  cursor: pointer;
}

.tree-row:hover { background: var(--paper-soft); }
.tree-row.is-selected { background: var(--accent-soft); }
.tree-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* FOUR CLASSES, ONE PER LEVEL — see the header of this block. `ck_acc_level`
   bounds the tree at four, so a fifth class would describe a row the database
   will not store. `padding-inline-start` flips with `dir`; `padding-left`
   would indent the English build from the wrong edge. */
.tree-row.depth-1 { padding-inline-start: var(--gap-2); }
.tree-row.depth-2 { padding-inline-start: calc(var(--gap-2) + var(--indent-step)); }
.tree-row.depth-3 { padding-inline-start: calc(var(--gap-2) + var(--indent-step) * 2); }
.tree-row.depth-4 { padding-inline-start: calc(var(--gap-2) + var(--indent-step) * 3); }
/* depth-5 arrived with migration 024 — five levels, and the fifth is where the
   banks and the cash currencies go. Logical property, as every rule here is. */
.tree-row.depth-5 { padding-inline-start: calc(var(--gap-2) + var(--indent-step) * 4); }

/* The chevron is a button of its own so expanding does not select, and
   selecting does not expand. Two intentions, two targets. */
.tree-toggle {
  font: inherit;
  border: none;
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
  padding: 0;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tree-toggle:hover { color: var(--accent); }
.tree-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* DOWN WHEN OPEN — which is what design screen 01 draws on its two expanded
   parents — and rotated when closed.

   A rotation and not a second icon, and the axis is VERTICAL on purpose: the
   obvious alternative is a chevron pointing toward the inline start when
   closed, which needs `[dir="rtl"]` to flip and `tools/check-css.sh` refuses
   one. Up/down means the same thing in both builds and needs no override. */
.tree-toggle svg { transform: rotate(180deg); }
.tree-toggle.is-open svg { transform: none; }
.tree-toggle.is-leaf { visibility: hidden; cursor: default; }

.tree-code {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
  color: var(--accent);
  min-width: var(--code-column);
  flex-shrink: 0;
}

.tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.tree-row.is-inactive .tree-name,
.tree-row.is-inactive .tree-code { color: var(--ink-faint); }

/* The balance column is sized for 999,999,999.99 so the decimal points line up
   whatever the value — the design says so in its own footnote. `text-align:
   end` and not `right`: in the English build the numbers still sit against the
   inline end of their column. */
.tree-balance {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
  color: var(--ink-soft);
  min-width: var(--balance-column);
  text-align: end;
  flex-shrink: 0;
}

/* =============================================================================
   THE ROW ACTION BAR — design screen 01, built in F4.1
   =============================================================================
   Screen 01 draws four controls on the hovered row: a child account, edit, the
   log, and deactivate. Until F4.1 `.tree-row:hover` changed the background and
   nothing else, and every action lived in the detail panel — so each one cost a
   click to select plus a trip across the screen.

   THREE PROPERTIES DECIDE THIS BLOCK.

   1 · NO LAYOUT SHIFT. `visibility: hidden` and NOT `display: none`: a hidden
       box still occupies its slot, so a row is exactly the same width whether
       the bar is drawn or not and nothing moves under the cursor. It also takes
       the controls out of the tab order and out of the accessibility tree while
       hidden, which `opacity: 0` would not — an invisible but focusable button
       is a tab stop nobody can see.

   2 · HOVER **AND** KEYBOARD FOCUS. `:focus-within` and not `:focus`, because
       once focus moves INTO the bar the row itself is no longer focused, and a
       bar that vanished at the moment it was entered could not be used at all.
       `:focus-within` also matches the row when the row itself holds focus,
       which is what makes the bar visible before F2 is pressed — a
       `visibility: hidden` element cannot receive focus, so the reveal has to
       happen first.

   3 · A ROW WITH NO PERMITTED ACTION SHOWS NO BAR. Not a bar of greyed icons —
       an empty slot. `.row-actions.is-empty` is that slot: it keeps the width
       so the columns still line up, and it draws nothing.
   ============================================================================= */

.row-actions {
  min-width: var(--actions-column);
  flex-shrink: 0;
  display: flex;
  /* `flex-end` is the END of the main axis, which follows `dir` — the bar hugs
     the row's outer edge in both builds and no rule had to name a side. */
  justify-content: flex-end;
  align-items: center;
  gap: 1px;
  visibility: hidden;
}

.row-actions.is-empty { visibility: hidden; }

/* **`.charge-row` IS ON THIS SELECTOR AND WAS NOT, AND THE BAR WAS THEREFORE
   INVISIBLE ON THE WHOLE CHARGE-TYPES SCREEN.** `028` built the row bar there —
   `rowActions` in `charge-types-render.js`, the ✓ control, the F2 keyboard
   path, `RUNBOOK K4` — over a `.row-actions` whose only reveal named
   `.tree-row`. The base rule's `visibility: hidden` therefore never lifted: no
   icon was drawn, and per note 1 above a `visibility: hidden` element cannot
   take focus either, so F2 called `.focus()` on a control that could not have
   it and nothing happened.

   Found by READING this file while the charge list was being rebuilt in 029;
   no test could see it, because every guard on this screen asserts markup and
   this is a stylesheet refusing to show markup that is correct.
   `test_the_row_action_bar_is_revealed_on_every_screen_that_draws_one` now
   compares the classes that DRAW a bar against the classes that reveal one, so
   the next screen to grow a row bar fails rather than shipping it invisible. */
.tree-row:hover .row-actions:not(.is-empty),
.tree-row:focus-within .row-actions:not(.is-empty),
.charge-row:hover .row-actions:not(.is-empty),
.charge-row:focus-within .row-actions:not(.is-empty),
/* 038 · `.party-row`. THE THIRD SCREEN TO GROW A BAR, AND THE FIRST NOT TO
   SHIP IT INVISIBLE — because `test_the_row_action_bar_is_revealed_on_every_
   screen_that_draws_one` was written after item 48 and reads this selector
   against the functions that call `rowActions`/`partyRowActions`. The guard
   fails on a missing class rather than the screen shipping blind for two days.
   `visibility: hidden` ALSO removes an element from the tab order, so without
   this line the documented F2 path would call `.focus()` on a control that
   cannot take it — which is the half of item 48 that is easiest to miss. */
.party-row:hover .row-actions:not(.is-empty),
.party-row:focus-within .row-actions:not(.is-empty),
/* 040 · `.role-row`. THE FOURTH CLASS TO GROW A BAR — the customers and
   suppliers lists. Their rows carry `.party-row` as well, so the line above
   already reveals them; this one is here ANYWAY, because
   `test_the_row_action_bar_is_revealed_on_every_screen_that_draws_one` reads
   the class the ROW BUILDER names, and a bar that works today by inheriting
   another screen's selector is a bar that goes invisible the day somebody
   splits the two classes. ERROR-BOOK 48 shipped for two days on exactly that
   kind of coincidence. */
.role-row:hover .row-actions:not(.is-empty),
.role-row:focus-within .row-actions:not(.is-empty) { visibility: visible; }

.row-actions-inner {
  display: flex;
  gap: 1px;
  padding: 2px;
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

.row-action {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  border: var(--line-width) solid transparent;
  background: transparent;
  color: var(--ink-soft);
  font: inherit;
  padding: 0;
  cursor: pointer;
}

button.row-action:hover { background: var(--paper-soft); color: var(--accent); }
button.row-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The design's own colours: the child action is the accented one, deactivate is
   the stop colour, and the other two are ordinary. */
.row-action.is-child { background: var(--accent-soft); color: var(--accent); }
.row-action.is-remove { color: var(--stop); }
button.row-action.is-remove:hover { background: var(--stop-soft); color: var(--stop); }

/* THE TWO NON-CONTROLS, AND THEY ARE `<span>` RATHER THAN `<button disabled>`.
   A disabled button is a control that is refusing; neither of these is
   refusing anything. One points at a screen that does not exist yet, and one
   describes a parent with no free code — and both carry the reason as their
   accessible name, so the explanation is READ OUT rather than left to a hover
   that a touch screen cannot perform.

   DASHED FOR PENDING, SOLID FOR UNAVAILABLE, and the difference is deliberate:
   a dashed edge reads as "not there yet" and a solid grey one as "not here".
   In a 22px box there is no room for the word the sidebar can afford — which is
   why the accessible name carries it instead, and why the whole bar is a
   labelled group rather than four bare icons. */
.row-action.is-pending,
.row-action.is-unavailable {
  cursor: default;
  color: var(--ink-faint);
  opacity: 0.5;
}

.row-action.is-pending { border-style: dashed; border-color: var(--line); }

.row-action.is-unavailable {
  border-color: var(--line);
  background: var(--paper-soft);
}

/* The column header's fourth cell. It names the slot and carries the key that
   reaches it — once, in the header, rather than on every row where it would
   either shift the row or never be read. */
.tree-actions-column {
  min-width: var(--actions-column);
  flex-shrink: 0;
  text-align: end;
}

.badge {
  font-size: var(--size-1);
  padding: 0 var(--gap-2);
  border-radius: 99px;
  background: var(--paper-soft);
  color: var(--ink-faint);
  border: var(--line-width) solid var(--line);
  flex-shrink: 0;
  white-space: nowrap;
}

.badge.is-cost-center {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: var(--warn-soft);
}

.badge.is-inactive {
  background: var(--stop-soft);
  color: var(--stop);
  border-color: var(--stop-soft);
}

.badge.is-confirm {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: var(--warn);
}

/* ★★ `badge-warn` WAS USED AND NEVER DEFINED — found on the 051 tour.
   `users-render.js` has drawn «كلمة مرور مؤقتة» as `class="badge badge-warn"`
   since F3, and no rule anywhere in this stylesheet matched the second class.
   So the one badge on the users list that is a STATE somebody has to act on
   rendered in exactly the same grey as «النظام», which is a permanent label
   about machinery. Two things that mean different things looked identical.

   ⚠ IT IS `badge-warn` AND NOT `.badge.is-warn`. Every other modifier on this
     element is `is-*` and this one is not, which is what let it go unnoticed —
     a reader scanning for `.badge.is-` finds nothing missing. The class in the
     markup is not renamed here: `042`'s drift test quotes it, and a rename is a
     second file's change for no gain. What was missing was the rule. */

/* ★★ E §2.1 · A LOCKED ACCOUNT. `--stop`, not `--warn`: a temporary password is
   a state the person walks through on their next sign-in, and a lockout is a
   person who cannot get in at all until somebody acts. They are the two badges
   that can sit on the same row, so they must not be the same colour. */

/* --- the search result list: flat, one parent line per row ---------------- */
/* Decision 3: SEARCH FLATTENS THE TREE. A row knows its IMMEDIATE parent and
   nothing above it — `AccountOut` carries `parent_code` and no grandparent —
   so the label is «تحت», never «المسار». Calling it a path would promise a
   chain the row cannot supply. */

.result-row {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  padding: var(--gap-2);
  border-radius: var(--radius);
  border-block-end: var(--line-width) solid var(--line);
  width: 100%;
  border-inline: none;
  border-block-start: none;
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: start;
  cursor: pointer;
}

.result-row:hover { background: var(--paper-soft); }
.result-row.is-selected { background: var(--accent-soft); }

.result-parent {
  font-size: var(--size-1);
  color: var(--ink-faint);
  display: flex;
  gap: var(--gap-1);
  align-items: baseline;
}

/* The parent name is a link that clears the search and jumps to it — decision
   3's second half. A `button`, because it navigates nowhere. */
.parent-link {
  font: inherit;
  font-size: var(--size-1);
  color: var(--accent);
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
}

/* --- the empty search state — design screen 14 --------------------------- */
/* «لا نتيجة ليست خطأً — ولا تُعرض كخطأ». No red, no alert role, no border of
   the error kind. */

.empty-search {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--gap-2);
  padding: var(--gap-5) var(--gap-4) var(--gap-4);
  color: var(--ink-soft);
}

.empty-search h3 { margin: 0; font-size: var(--size-3); color: var(--ink); }
.empty-search .faint { max-width: 34rem; }

.tip-box {
  background: var(--paper-soft);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  margin: 0 var(--gap-3) var(--gap-3);
  font-size: var(--size-1);
  color: var(--ink-soft);
}

.tip-box h4 { margin: 0 0 var(--gap-1); font-size: var(--size-1); color: var(--ink); }
.tip-box ul { margin: 0; padding-inline-start: var(--gap-3); }
.tip-box li { margin-block-end: var(--gap-1); }

.state-foot {
  padding: var(--gap-2) var(--gap-3);
  border-block-start: var(--line-width) solid var(--line);
  background: var(--paper-soft);
  font-size: var(--size-1);
  color: var(--ink-faint);
  display: flex;
  justify-content: space-between;
  gap: var(--gap-3);
  flex-wrap: wrap;
}

/* --- the loading state — design screen 15 -------------------------------- */
/* Skeleton rows rather than a spinner: the design shows the column header and
   the row rhythm already in place, so the screen does not jump when the data
   arrives. */

.skeleton-row {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-2);
}

.skeleton-bar {
  height: 10px;
  border-radius: 99px;
  background: var(--line);
  opacity: 0.6;
}

.skeleton-bar.is-code { width: var(--code-column); flex-shrink: 0; }
.skeleton-bar.is-name { flex: 1; }
.skeleton-bar.is-balance { width: 5rem; flex-shrink: 0; }

/* --- the detail panel ----------------------------------------------------- */

.detail-head {
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
  padding-block-end: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
}

.detail-code {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-3);
  font-weight: 600;
  color: var(--accent);
}

.detail-name { margin: 0; font-size: var(--size-3); font-weight: 600; }
.detail-name-en { color: var(--ink-faint); font-size: var(--size-1); }

.detail-chips { display: flex; gap: var(--gap-1); flex-wrap: wrap; }

.detail-actions { display: flex; gap: var(--gap-2); flex-wrap: wrap; align-items: center; }

/* WHY A BUTTON IN THIS ROW IS DISABLED — F4.1. The panel has the width the row
   bar has not, so the reason is a SENTENCE here rather than an accessible name
   on a 22px icon. `flex-basis: 100%` puts it on its own line under the buttons
   in both directions and needs no side. */
.detail-action-reason {
  flex-basis: 100%;
  font-size: var(--size-1);
  color: var(--ink-faint);
}

.btn-secondary {
  font: inherit;
  font-size: var(--size-1);
  color: var(--ink-soft);
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-1) var(--gap-3);
  cursor: pointer;
}

.btn-secondary:hover { background: var(--paper-soft); }
.btn-secondary[disabled] { color: var(--ink-faint); cursor: default; background: var(--paper-soft); }

.btn-danger {
  font: inherit;
  font-size: var(--size-1);
  color: var(--paper);
  background: var(--stop);
  border: var(--line-width) solid var(--stop);
  border-radius: var(--radius);
  padding: var(--gap-1) var(--gap-3);
  cursor: pointer;
}

.btn-danger[disabled] {
  background: var(--ink-faint);
  border-color: var(--ink-faint);
  cursor: default;
}

.btn-warn {
  font: inherit;
  font-size: var(--size-1);
  color: var(--paper);
  background: var(--warn);
  border: var(--line-width) solid var(--warn);
  border-radius: var(--radius);
  padding: var(--gap-1) var(--gap-3);
  cursor: pointer;
}

.balance-card {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-3);
}

.balance-net {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-4);
  font-weight: 600;
}

.balance-split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gap-2);
  padding-block-start: var(--gap-2);
  margin-block-start: var(--gap-2);
  border-block-start: var(--line-width) solid var(--line);
  font-size: var(--size-1);
}

.balance-split .num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  margin-inline-start: var(--gap-1);
}

/* ★ AND THE ABSENT STATES NEED THE SAME GAP AS THE FIGURES — 040, seen on the
   card. `.balance-split .num` carries a start margin and «لا حركات» is NOT a
   `.num` (deliberately: it is a sentence, not a value), so the label and the
   words ran together — «لنا عليه:لا حركات». The gap belongs to the CELL rather
   than to the number in it. */
.balance-split > div {
  display: flex;
  gap: var(--gap-1);
  align-items: baseline;
  flex-wrap: wrap;
}

.detail-list { display: flex; flex-direction: column; gap: var(--gap-1); font-size: var(--size-1); }

.detail-line { display: flex; justify-content: space-between; gap: var(--gap-3); }
.detail-line dt { color: var(--ink-soft); margin: 0; }
.detail-line dd { margin: 0; text-align: end; }

/* --- the panel that holds a form ----------------------------------------- */

.form-card {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-3);
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

.form-head {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  padding-block-end: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
}

.form-head h3 { margin: 0; font-size: var(--size-2); }

.parent-strip {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  background: var(--paper-soft);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  flex-wrap: wrap;
}

.code-proposed {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-3);
  font-weight: 600;
  color: var(--accent);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  background: var(--paper);
}

.code-row { display: flex; align-items: center; gap: var(--gap-3); flex-wrap: wrap; }

.kind-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gap-2);
}

/* The debit/credit choice — decision D9, 27 August 2026. Two options, so a
   grid of two; the OPTION itself is `.kind-option` and is not duplicated,
   because a second copy of that block would drift from the first the day one of
   them is restyled. */
.side-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gap-2);
}

.kind-option {
  font: inherit;
  text-align: start;
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  background: var(--paper);
  cursor: pointer;
  color: inherit;
}

.kind-option.is-on {
  border-color: var(--accent);
  border-width: 2px;
  background: var(--accent-soft);
  color: var(--accent);
}

.kind-option strong { display: block; font-size: var(--size-2); }
.kind-option span { font-size: var(--size-1); color: var(--ink-faint); }
.kind-option.is-on span { color: var(--accent); }

.derived-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gap-3);
}

.derived-box,
.options-box {
  background: var(--paper-soft);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
  font-size: var(--size-1);
}

.options-box { background: var(--paper); }

.form-foot {
  display: flex;
  gap: var(--gap-2);
  align-items: center;
  padding-block-start: var(--gap-2);
  border-block-start: var(--line-width) solid var(--line);
  flex-wrap: wrap;
}

.form-foot .faint { margin-inline-start: auto; }

/* --- the two conflict notices — design screens 03 and 04 ----------------- */

.notice-confirm {
  background: var(--warn-soft);
  border-inline-start: 4px solid var(--warn);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  color: var(--warn);
}

.notice-warn {
  background: var(--paper-soft);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
}

/* ★★ E §2.1 · the lockout notice. `--stop`, matching its badge on the row: the
   two are the same fact seen from the list and from the detail, and a person who
   clicked a red badge and landed on a grey box would wonder whether they had
   opened the right person. */
.notice-warn.is-locked {
  border-color: var(--stop);
  background: var(--stop-soft);
}

/* The button and the sentence that says what it does NOT do, on one line while
   there is room and stacked when there is not — the hint is longer than the
   button and must never push it off the card. */

.conflict-row {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-1) var(--gap-2);
  font-size: var(--size-1);
  flex-wrap: wrap;
}

/* --- delete / deactivate — design screen 05 ------------------------------ */

.remove-card {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
}

.danger-box {
  background: var(--stop-soft);
  border-inline-start: 4px solid var(--stop);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  color: var(--stop);
}

.danger-box strong { display: block; }

.warn-box {
  background: var(--warn-soft);
  border-inline-start: 4px solid var(--warn);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  color: var(--warn);
}

.warn-box strong { display: block; }

/* THE TYPE-TO-CONFIRM BOX. `max-width` is generous on purpose and is NOT nine
   characters: the normaliser still admits `1604` and `1-60-40`, so the box has
   to hold the longest spelling somebody may type, not the shortest. §6و
   changed what the LABEL shows, never what the box accepts. */
.confirm-code-input {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.15em;
  max-width: 14rem;
}

/* --- the parent-full screen — design 13 ---------------------------------- */

.code-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 16rem;
  overflow-y: auto;
}

.code-list-row {
  display: flex;
  gap: var(--gap-2);
  align-items: baseline;
  padding: var(--gap-1) var(--gap-2);
  border-radius: var(--radius);
  background: var(--paper-soft);
  font-size: var(--size-1);
}

.code-list-row .num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  min-width: var(--code-column);
}

/* --- the 409 panel — decision 6, and there is no overwrite button -------- */

.stale-diff {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gap-2);
  font-size: var(--size-1);
}

.stale-col {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2);
}

.stale-col h4 { margin: 0 0 var(--gap-1); font-size: var(--size-1); color: var(--ink-soft); }

/* --- a transient confirmation of a completed write ----------------------- */

.toast {
  background: var(--accent-soft);
  border-inline-start: 4px solid var(--accent);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  color: var(--accent);
  font-size: var(--size-1);
}

.toast[hidden] { display: none; }

/* =============================================================================
   F4 · THE THIRD OUTCOME — write.js
   =============================================================================
   A write has three endings, and until F4 this stylesheet had classes for two.
   The third is «we do not know», and it is deliberately NOT the error colour:

     .state-error     the server answered and refused          — stop red
     .state-unknown   the request left and nothing came back   — warn amber

   The distinction is the whole point of the outcome. A refusal is a fact about
   what the system did; an unknown is a fact about what nobody can see. Painting
   them the same colour tells the user the write failed, which is exactly the
   lie that makes them press the button again and create the same row twice.

   IT DOES NOT FADE. `.toast-done` does — outcome 1 announces something the
   screen still shows. This one carries the only instruction available and has
   to survive the user looking away. */
.state-unknown {
  text-align: start;
  background: var(--warn-soft);
  border: var(--line-width) solid var(--warn);
  border-radius: var(--radius);
  padding: var(--gap-3) var(--gap-4);
  border-inline-start: 4px solid var(--warn);
}

.state-unknown h3 { color: var(--warn); }
.state-unknown p { color: var(--warn); }

/* A refusal renders INSIDE the panel it belongs to, beside the fields that
   produced it — never floating in a corner while the form disappears. The
   margin is what separates it from the last field; the panel provides the
   rest. */
.write-refusal { margin-block-start: var(--gap-3); }
.write-refusal:focus,
.write-unknown:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
.write-unknown { margin-block-start: var(--gap-3); }

/* Outcome 1. It names its subject, so it is wider than a bare «تم» and wraps
   rather than truncating: a confirmation that has lost the name of the thing
   it confirmed is back to being uncheckable. */
.toast-done { line-height: 1.5; }

/* The row a write just changed, after the strip has faded. The screen still
   answers "which one?" when the announcement is gone.

   THE MARKER IS A BORDER AND NOT `box-shadow: inset 3px 0 0`, which is what the
   design file uses for the selected row and what this rule was first written
   as. A box-shadow's offsets are PHYSICAL — there is no logical form of them —
   so `inset 3px` puts the bar on the left in both languages, and the English
   build gets a marker on the wrong edge from a rule that looks fine.
   `check-css.sh` did not catch it, and now does: the pattern was added in F4
   with a control line beside it. */
.row-just-changed {
  background: var(--accent-soft);
  border-inline-start: 3px solid var(--accent);
}

/* =============================================================================
   F4 · SCREENS 06 · 07 · 08 · 09
   =============================================================================
   Logical properties only — doctrine §12.1, and tools/check-css.sh refuses the
   physical ones and a [dir] override. Every rule below flips itself when the
   document direction changes, which is what makes the English build a language
   switch rather than a second stylesheet. */

/* --- screen 06 · the user column ------------------------------------------ */
/* ★ 320px WAS THE WHOLE DEFECT ON THIS SCREEN — A.
   Four-word names are the NORMAL Egyptian name in this company (23 of 28 rows
   on 5 September 2026), and in a 320px column beside a 66%-of-the-window pane
   reading «Choose an employee» the name cell measured SIXTEEN PIXELS: «Mohamed
   Zakaria Eid Awaad» printed one word per line, four lines tall, and «Awaad»
   landed on the same pixels as the department's «Not stated». Measured in the
   English build at 1536px, and photographed before anything moved.

   The column is the content area now, and the pane it was squeezed by is a
   slide-over. */
.user-column {
  flex: 1;
  min-inline-size: 0;
  background: var(--paper);
  display: flex;
  flex-direction: column;
}

/* ⛔ AND NO `overflow-y` HERE EITHER — which is what took the HORIZONTAL
   scrollbar off a list of names as well. `overflow-y: auto` alone makes the
   OTHER axis compute to `auto` too, so a 320px column with a name one pixel
   too wide grew a second scrollbar nobody asked for. Measured: scrollWidth 318
   against clientWidth 304. */
.user-scroll { padding: var(--gap-1); }

/* ⚠ `align-items: flex-start` AND NOT `center`, AND THAT IS WHY «Awaad» AND
   «stated» PRINTED ON THE SAME PIXELS. Centred, a four-line name and a
   one-line department are both centred on the row's middle, so the department
   sits level with the name's THIRD line rather than its first. Start-aligned
   they both begin at the top and cannot cross, whatever either of them wraps
   to. */
.user-row {
  inline-size: 100%;
  display: flex;
  align-items: flex-start;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  border-radius: var(--radius);
  border: var(--line-width) solid transparent;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: start;
}

.user-row:hover { background: var(--paper-soft); }
.user-row.is-selected {
  background: var(--accent-soft);
  border-inline-start: 3px solid var(--accent);
}

/* An account that cannot sign in is quieter, not hidden: this is the one screen
   where somebody asks "who are these six people", and the most frequent author
   in the log must not be invisible on it. */
.user-row.is-quiet { opacity: 0.62; }

/* A FLOOR, not just a `min-inline-size: 0`. Zero is what let the name starve
   to 16px, and it is the same measurement and the same remedy `040` wrote for
   `.party-name`: the name may never be narrower than `--name-floor`, and every
   other cell may shrink before it does. */
.user-row-body {
  flex: 1 1 auto;
  min-inline-size: var(--name-floor);
  display: flex;
  flex-direction: column;
}
/* 033 - the department, at the end of the user row. `flex: none` so a long
   department name never squeezes the person's own name, which owns the row. */

.detail-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap-3);
  flex-wrap: wrap;
}
/* ★ `align-items: center` IS NOT COSMETIC — it is the guard. A flex row's
   default is `stretch`, so ONE tall child makes EVERY sibling as tall as
   itself. `052` shipped a Delete button whose label was a whole sentence; it
   wrapped to three lines and «Export» and «New request» grew with it, and the
   header looked broken. The label was shortened (ERROR-BOOK ١١٤) — and this
   line means no future label can ever do it again. */
.head-actions { display: flex; gap: var(--gap-2); align-items: center; }

/* --- screen 06 · the permission grid -------------------------------------- */
/* It scrolls INSIDE ITS OWN BOX rather than making the page scroll sideways:
   five verb columns plus a module name is wider than the panel in both
   languages, and a page that scrolls horizontally loses the sidebar. */
.grid {
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  overflow-x: auto;
  background: var(--paper);
}

.grid-row {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-2);
  /* 076 · 640 ⟶ 760: a seventh verb («File chat») joined the six; the box still scrolls inside itself */
  min-inline-size: 760px;
}
.grid-row:last-child { border-block-end: none; }

.grid-row.is-blind { background: var(--warn-soft); }
.blind-note { font-size: var(--size-1); color: var(--warn); }

/* A cell is a BUTTON, not a checkbox — a grant and a revoke are two endpoints.
   aria-pressed carries the state so the control is announced correctly even
   though it is not an input. */
.bx {
  inline-size: 20px;
  block-size: 20px;
  border-radius: 4px;
  border: var(--line-width) solid var(--line);
  background: var(--paper);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: pointer;
  color: var(--paper);
}

/* Inherited from the blanket row — ticked, and visibly not this row to remove. */
.bx:disabled { cursor: default; opacity: 0.6; }

/* --- screen 07 · the forms ------------------------------------------------ */
.field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gap-3);
}
.required { color: var(--stop); }
.choice {
  text-align: start;
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  background: var(--paper);
  font: inherit;
  color: inherit;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
}
.choice span { font-size: var(--size-1); color: var(--ink-faint); }
.choice.is-on {
  border-color: var(--accent);
  border-width: 2px;
  background: var(--accent-soft);
}
.choice.is-on strong, .choice.is-on span { color: var(--accent); }

/* --- the one-time password ------------------------------------------------ */

/* --- screen 08 · the profile ---------------------------------------------- */
.profile {
  display: flex;
  flex-direction: column;
  gap: var(--gap-4);
  max-inline-size: 1020px;
}
.profile-identity { display: flex; align-items: center; gap: var(--gap-3); }
.avatar-large { inline-size: 44px; block-size: 44px; font-size: var(--size-3); }

.profile-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gap-3);
}
/* NAMED `.profile-card` AND NOT `.card`, and the difference matters: F1's
   `.card` is used by index.html's two sections, and adding `display: flex` to
   the shared class would have restyled a page this task never touched. A rule
   that changes a screen nobody was working on is the kind of change that is
   found by somebody else, later, with no idea what did it. */
.profile-card {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-3) var(--gap-4);
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
}
.card-title { font-weight: 600; }
.card-line { display: flex; justify-content: space-between; font-size: var(--size-1); color: var(--ink-soft); }
.username-row { display: flex; gap: var(--gap-2); align-items: center; }
.username-row .input { flex: 1; }

.access {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  overflow-x: auto;
}
.access-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-3);
  padding: var(--gap-3) var(--gap-4);
  border-block-end: var(--line-width) solid var(--line);
  flex-wrap: wrap;
}
.access-row {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-4);
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-2);
  min-inline-size: 560px;
}
.access-head { background: var(--paper-soft); font-size: var(--size-1); color: var(--ink-faint); }
.access-module { flex: 1; min-inline-size: 12rem; display: flex; flex-direction: column; }
.access-cell { inline-size: 7rem; text-align: center; }
.access-row.is-blind { background: var(--warn-soft); }
.access-foot {
  padding: var(--gap-2) var(--gap-4);
  background: var(--paper-soft);
  font-size: var(--size-1);
  color: var(--ink-faint);
  min-inline-size: 560px;
}

/* --- screen 09 · the modal ------------------------------------------------ */
/* ⚠ THE MODAL OUTRANKS EVERY STICKY THING — and it did not until `A`.
   `.app-header` is now `z-index: 30` and `.list-top` is 20, and a `position:
   fixed` box whose z-index is `auto` counts as 0: a confirmation would open
   UNDER the header with a backdrop that dimmed nothing. `047` had already met
   this against the requests screen's sticky column row and fixed it THERE,
   scoped, with `.req-screen .modal { z-index: 21 }` — those two lines are gone,
   because the number belongs on the modal rather than on each screen that
   grows something sticky. */
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: var(--veil);
}
/* ⚠⚠ THE HORIZONTAL CENTRING IS `margin-inline: auto`, NOT A TRANSFORM, AND
   THAT IS A CORRECTION. It was `inset-inline-start: 50%` with
   `transform: translate(-50%, -50%)`, which centres correctly in English and
   NOT in Arabic: `inset-inline-start` flips to the right edge under `rtl` and
   `transform` does NOT — it is physical — so the two disagree and the dialog
   lands half its own width left of centre. Measured on the Arabic build:
   470px wide at x = 63 in a 1536px viewport, where centred is x = 533.
   `inset-inline: 0` plus a fixed `inline-size` plus `auto` margins centres in
   both directions with nothing physical in it, which is doctrine §12.1's whole
   argument applied to one box. The vertical half keeps its transform, because
   there is no writing mode in which the block axis flips here.
   ★ AND THIS IS NOT A REQUEST-SCREEN RULE — it fixes the Arabic build of every
     dialog in the project, including the users screen's password modal. */
.modal {
  position: fixed;
  inset-inline: 0;
  margin-inline: auto;
  inset-block-start: 50%;
  transform: translateY(-50%);
  z-index: 41;
  inline-size: min(470px, calc(100vw - 2 * var(--gap-4)));
  max-block-size: calc(100vh - 2 * var(--gap-4));
  overflow-y: auto;
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-panel);
  padding: var(--gap-4);
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
}

/* THE TRANSLATE IS THE ONE PHYSICAL VALUE HERE AND IT IS CORRECT IN BOTH
   DIRECTIONS. The element is centred ON A POINT, so the offset is half its own
   size rather than a distance from an edge: inset-inline-start is what makes
   the point flip, and the translate is symmetric about it. A -50% that had been
   an inset-inline-end would be the bug this comment exists to prevent. */
.modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap-3);
}
.modal-title { font-size: var(--size-3); font-weight: 600; }
.modal-warn { font-size: var(--size-1); color: var(--warn); line-height: 1.5; }
.icon-button {
  background: none;
  border: none;
  color: var(--ink-faint);
  cursor: pointer;
  padding: var(--gap-1);
  display: inline-flex;
}
.input-row { display: flex; gap: var(--gap-2); align-items: center; }
.input-row .input { flex: 1; }
.field-hint.is-ok { color: var(--accent); }
.field-hint.is-bad { color: var(--stop); }

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

/* Between "proven" and "unknown": the server confirmed it and this screen has
   no read that would check. Not green — it is not a proof — and not red,
   because nothing failed. */
.state-unconfirmed {
  text-align: start;
  background: var(--paper-soft);
  border: var(--line-width) solid var(--line);
  border-inline-start: 4px solid var(--ink-faint);
  border-radius: var(--radius);
  padding: var(--gap-3) var(--gap-4);
}
.state-unconfirmed h3 { color: var(--ink-soft); }
.write-unconfirmed { margin-block-start: var(--gap-3); }

.visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* =============================================================================
   F5 · SCREEN 12 — the change log
   =============================================================================
   LOGICAL PROPERTIES ONLY, like every rule above it. There is no `left`, no
   `margin-right` and no `[dir="rtl"]` anywhere here: the table below is a grid
   whose columns flip with `dir` because a grid's inline axis IS the writing
   direction. That is the whole reason the table is a grid rather than a set of
   fixed-width spans — the design's fixed pixel width on the date column reads
   the same in both directions only by accident, and a grid is right by
   construction.

   AND IT IS A GRID RATHER THAN A TABLE ELEMENT FOR ONE MORE REASON: a group is
   a variable number of field lines inside one visual row, which a table cell
   holds badly and a grid cell holds naturally. The header row and the group
   rows share `grid-template-columns`, declared once. */

.app-main.is-logs { padding: 0; display: flex; }

.logs {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}

.log-head-tools {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  flex-shrink: 0;
}

/* «للقراءة فقط بالبناء». A padlock and a sentence, not a colour: the fact is
   structural and permanent, so it is stated rather than signalled. */
.log-readonly {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-2);
  font-size: var(--size-1);
  color: var(--ink-soft);
  background: var(--paper-soft);
  border: var(--line-width) solid var(--line);
  border-radius: 99px;
  padding-block: 0.2rem;
  padding-inline: var(--gap-3);
}

/* What the screen does NOT cover — doctrine §1.2 on a screen. Bordered on the
   inline start so it reads as a standing statement about the page rather than
   as a transient notice about the current filter. */
.log-scope {
  background: var(--paper-soft);
  border-inline-start: 3px solid var(--ink-faint);
  border-radius: var(--radius);
  padding: var(--gap-2) var(--gap-3);
  font-size: var(--size-1);
  line-height: 1.6;
}

/* --- the filter form ------------------------------------------------------ */

.log-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--gap-3);
  padding: var(--gap-3) var(--gap-4);
  background: var(--paper);
  border-block-end: var(--line-width) solid var(--line);
}

.log-form .field { min-inline-size: 9rem; }
.log-form .input { min-inline-size: 9rem; }

.log-form-actions {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  margin-inline-start: auto;
}

.log-form-note {
  padding-inline: var(--gap-4);
  padding-block-end: var(--gap-2);
  background: var(--paper);
}

/* --- the applied filter, drawn from the response -------------------------- */

.log-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-4);
  font-size: var(--size-1);
}

/* A filtered view must not look unfiltered. The tint is the coarse signal and
   the chips are the precise one — neither alone is enough: the tint says
   «something is on» from across the room and the chips say what. */
.log-filters.is-on {
  background: var(--accent-soft);
  border-block-end: var(--line-width) solid var(--accent);
}

.log-filters.is-none { color: var(--ink-faint); }

.log-filters-lede { color: var(--accent); font-weight: 600; }

.log-chip {
  font: inherit;
  font-size: var(--size-1);
  display: inline-flex;
  align-items: center;
  gap: var(--gap-2);
  background: var(--paper);
  color: var(--accent);
  border: var(--line-width) solid var(--accent);
  border-radius: 99px;
  padding-block: 0.2rem;
  padding-inline: var(--gap-3);
  cursor: pointer;
}

.log-chip:hover { background: var(--accent-soft); }
.log-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.log-chip-label { color: var(--ink-faint); }
.log-chip-x { font-size: var(--size-1); }

/* The record chip is the one the chart of accounts hands over, so it is the one
   a reader is most likely to arrive already carrying and least likely to have
   chosen. It is marked so «why am I only seeing this account?» has a visible
   answer. */
.log-chip.is-record { background: var(--accent); color: var(--paper); }
.log-chip.is-record .log-chip-label { color: var(--accent-soft); }

.log-tables {
  padding-inline: var(--gap-4);
  padding-block-end: var(--gap-2);
}

/* --- the table ------------------------------------------------------------ */

/* ⛔ NO `overflow-y` — the page scrolls instead. */
.log-scroll {
  padding: var(--gap-3) var(--gap-4);
}

/* ⚠ `overflow: hidden` IS GONE AND IT HAD TO GO. It was here to clip the row
   backgrounds inside the rounded border — and an ancestor with any `overflow`
   but `visible` makes `position: sticky` on `.log-head` below SILENTLY DO
   NOTHING. The radius is given to the head row itself instead, in its logical
   form, so the corner is right in both directions. */
.log-table {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
}

/* ONE DECLARATION, TWO USERS — the header and every group row. A second copy
   would drift the first time a column was widened. */
.log-head,
.log-group {
  display: grid;
  grid-template-columns:
    minmax(7rem, 8rem)
    minmax(8rem, 10rem)
    minmax(7rem, 10rem)
    minmax(12rem, 1fr)
    minmax(5rem, 6rem);
  gap: var(--gap-3);
  padding: var(--gap-2) var(--gap-3);
  align-items: baseline;
}

/* ★ THE COLUMN NAMES STAY. This screen's toolbar is a FORM — eight fields
   somebody fills once and then reads past — so it is not sticky and scrolls
   away, which is the whole point of the page scrolling. The five column names
   are the part a reader still needs at row 40, so they are the one thing here
   that sticks, and they stick directly under the app header because nothing
   else on this screen is sticky above them. */
.log-head {
  background: var(--paper-inset);
  border-block-end: var(--line-width) solid var(--line);
  border-start-start-radius: var(--radius);
  border-start-end-radius: var(--radius);
  font-size: var(--size-1);
  color: var(--ink-faint);
  position: sticky;
  inset-block-start: var(--header-height);
  z-index: 10;
}

.log-group {
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-2);
}

.log-group:last-child { border-block-end: none; }

/* An override is an `update` carrying a `reason` — not a sixth action. The tint
   is what makes it findable down a page; the reason line is what says what it
   was. The design tints the same row for the same reason and then also gives it
   a badge, which is the part that is wrong. */
.log-group.has-reason { background: var(--warn-soft); }

.log-when { font-family: var(--font-mono); font-size: var(--size-1); color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.log-time { white-space: nowrap; }

.log-who { display: flex; align-items: baseline; gap: var(--gap-2); flex-wrap: wrap; }

.log-module { color: var(--ink-soft); font-size: var(--size-1); }

/* A table nobody mapped to a module. NOT a blank cell: blank reads as «this
   change has no module», which is a claim about the change, and the truth is a
   fact about the map. */
.log-module.is-unmapped {
  display: inline-flex;
  flex-direction: column;
  color: var(--warn);
}

.log-what { min-inline-size: 0; }

.log-record { display: flex; align-items: baseline; gap: var(--gap-2); flex-wrap: wrap; }

.log-fields {
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
  margin-block-start: var(--gap-1);
}

.log-field {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  flex-wrap: wrap;
  font-size: var(--size-1);
}

.log-field-name { color: var(--ink-faint); }

.log-values {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  flex-wrap: wrap;
  min-inline-size: 0;
}

/* The old value is struck through by its element and dimmed here; the new one
   is not. `overflow-wrap` because these are values somebody typed and one of
   them will one day be a paragraph — a cell that stretched instead would push
   the action column off the screen. */
.log-old {
  color: var(--ink-faint);
  overflow-wrap: anywhere;
}

.log-new {
  color: var(--ink);
  overflow-wrap: anywhere;
}

.log-arrow { color: var(--ink-faint); }

/* NULL — the value was absent, which is not the same as the value being empty
   text. Italic and faint so it cannot be mistaken for content. */
.log-none { color: var(--ink-faint); font-style: italic; }

/* A password. Not blank and not truncated: truncated is still a leak, and blank
   reads as «no value», which is the one thing that is certainly false. */
.log-masked {
  color: var(--ink-faint);
  background: var(--paper-soft);
  border: var(--line-width) dashed var(--line);
  border-radius: var(--radius);
  padding-inline: var(--gap-2);
  font-size: var(--size-1);
}

.log-reason {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  flex-wrap: wrap;
  margin-block-start: var(--gap-1);
  font-size: var(--size-1);
  color: var(--warn);
  line-height: 1.5;
}

.log-reason-label { font-weight: 600; }

/* A group whose remaining fields may be on the next page. Only ever on the
   first and last group of a page, and only when a neighbouring page exists. */
.log-partial {
  margin-block-start: var(--gap-1);
  font-size: var(--size-1);
  color: var(--ink-faint);
  font-style: italic;
}

/* --- the five actions ----------------------------------------------------- */
/* Five, from the CHECK constraint on `audit_log.action`. `is-unknown` is the
   sixth rule and it exists for a value the constraint should have refused — it
   renders the raw string rather than nothing, because an unrenderable value
   that names itself is at least searchable. */

.log-action {
  display: inline-block;
  font-size: var(--size-1);
  border-radius: 99px;
  padding-block: 0.1rem;
  padding-inline: var(--gap-2);
  border: var(--line-width) solid var(--line);
  background: var(--paper-soft);
  color: var(--ink-soft);
  white-space: nowrap;
}

.log-action.is-insert { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.log-action.is-delete { background: var(--stop-soft); color: var(--stop); border-color: var(--stop); }
.log-action.is-deactivate { color: var(--stop); }
.log-action.is-activate { color: var(--accent); }
.log-action.is-unknown { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }

.log-action-col { font-size: var(--size-1); }

/* --- paging and export ---------------------------------------------------- */

.log-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-3);
  padding: var(--gap-2) var(--gap-4);
  background: var(--paper-soft);
  border-block-start: var(--line-width) solid var(--line);
  flex-shrink: 0;
}

.log-pager-buttons { display: flex; gap: var(--gap-2); }

.log-export-note {
  margin-inline: var(--gap-4);
  margin-block-start: var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  border-radius: var(--radius);
  font-size: var(--size-1);
  line-height: 1.6;
}

/* The refusal is not an error — nothing failed and no file was written. It is
   the cap announcing itself, which doctrine requires a cap to do. */
.log-export-note.is-refused {
  background: var(--warn-soft);
  border-inline-start: 3px solid var(--warn);
  color: var(--warn);
}

.log-export-note.is-note {
  background: var(--paper-soft);
  border-inline-start: 3px solid var(--ink-faint);
  color: var(--ink-soft);
}

/* WHICH RECORD — and an id is never dressed as an account code.
   `record_key` equals `record_id` on every id-keyed table (measured over 200
   rows), so the number in this cell is an ID. Account codes are four digits and
   a mature `accounts.id` is four digits too, so the green monospace the chart
   uses for a code would make the two indistinguishable on the one screen read
   to find out what happened. It gets a label and a neutral face instead. */
.log-record-label { color: var(--ink-faint); font-size: var(--size-1); }

.log-record-id {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
  color: var(--ink-soft);
}

/* A composite or text key — `CONCURRENT_MODIFICATION|en`. This one IS the
   handle, so it is the prominent thing in the cell. */
.log-record-key {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
  color: var(--ink);
  overflow-wrap: anywhere;
}

/* A chip's VALUE. It inherits the chip's own foreground colour, which is what
   the first version got wrong: `.tree-code` carries the chart's green, and on
   the record chip's dark green background that is invisible — the chip read
   «Record id ✕» with no number in it. A chip that does not say what it filtered
   to is worse than no chip: it asserts a filter and withholds it. */
.log-chip-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: inherit;
  overflow-wrap: anywhere;
}

/* ===========================================================================
   ★★ 042 · THE CHANGE LOG FILLS ITS WINDOW — AND EVERY RULE BELOW IS SCOPED
   ===========================================================================
   The owner: «this small view of two logs and i have to scroll down and see two
   by two only» · «i wanna atleast see 8 in the audit log».

   ★★★ THE RULE THIS BLOCK OBEYS, AND WHY IT IS PROOF RATHER THAN CARE.
       Every selector here begins with `.app-main.is-logs`. NOT ONE bare
       `.log-*` rule above was edited.

       Four of them — `.log-chip`, `.log-chip-label`, `.log-chip-value`,
       `.log-chip-x` — are also drawn on the parties, customers, suppliers and
       «عميل ومورد» screens, by `parties-render.js appliedFilters` and
       `role-lists-render.js roleApplied`. «Be careful» is not a property anyone
       can check afterwards; a descendant selector is. Measured in this
       repository:

         · `is-logs` appears in exactly one HTML file — `logs.html`
         · `class="logs"` appears in exactly one HTML file — `logs.html`
         · no JavaScript anywhere adds or removes either class
         · `test_042_first_real_use.py` reads this stylesheet and refuses a
           `.log-*` rule that is not under an `.app-main.is-logs` ancestor

       Those chips sit inside `.app-main` WITHOUT `.is-logs`, so no rule written
       this way can reach them — not «should not», cannot.

   ★★ WHAT WAS ACTUALLY WRONG, MEASURED IN A BROWSER BEFORE ONE LINE MOVED.
      The prompt's hypothesis was two nested scrolling regions — `.app-main`
      with its `overflow: auto` un-overridden by `.is-logs`, and `.log-scroll`
      inside it. **That hypothesis is wrong, and it was tested rather than
      assumed:** on `logs.html` at a 945px viewport `.app-main` measures
      `scrollHeight === clientHeight === 889`. It does not scroll. Neither does
      it on `accounts.html`'s `.is-chart`, which the same reasoning suspected.

      THERE WERE TWO SCROLLBARS. The second one was on the DOCUMENT, and it was
      1088px long, and everything under it was blank:

          document.documentElement.scrollHeight   2033
          window.scrollTo(0, 9999) ⟹ scrollY     1088
          .app.getBoundingClientRect().top        −1088   ⟵ the whole shell,
                                                            scrolled off the top

      `.visually-hidden` is `position: absolute` with no `inset`, and NO
      ANCESTOR OF IT ON THIS SCREEN IS POSITIONED. Its containing block is
      therefore the initial containing block, so `.log-scroll`'s `overflow-y`
      does not clip it — measured: `overflow: hidden !important` on `.log-scroll`
      left the document scrolling exactly 1088px. `logs-render.js valuePair`
      puts one of these spans in every changed-field line, and the lowest one on
      a 50-row page sat at y = 2032.

      **So the owner was scrolling a phantom page into white space**, and the
      inner region he wanted was never what his wheel was moving.

      `position: relative` on the row is the whole fix for that half: it makes
      the row the containing block, and `html.scrollHeight` falls from 2033 to
      945 with the document's scroll range at 0. */
.app-main.is-logs .log-group { position: relative; }

/* --- HEIGHT FIRST, DENSITY SECOND, and in that order for a reason ---------- */
/* With the phantom scrollbar gone the screen still spent 468px of every window
   on chrome before a row was drawn, and 9 rows fitted at 945. The reclaim below
   is padding and dead margin only — no sentence was removed from this screen,
   because what it says is what makes a log readable six months later.

   `.chart-head` and `.field` are SHARED classes and are reached here only as
   descendants of `.is-logs`, which is the point of the rule above. */
.app-main.is-logs .chart-head { padding: var(--gap-2) var(--gap-4); gap: var(--gap-1); }
/* `p` carries `margin-block-end: var(--gap-3)` from the element rule — 16px of
   nothing under the lede and another 16 under the counts, both at the bottom of
   a box that already pads itself. */
.app-main.is-logs .chart-head p { margin-block-end: 0; }
.app-main.is-logs .log-scope { padding: var(--gap-1) var(--gap-3); }
.app-main.is-logs .log-form { padding: var(--gap-2) var(--gap-4); gap: var(--gap-2) var(--gap-3); }
/* The two `<select>`s on this bar were already `.is-compact` at 27px and the
   four text/number/date boxes were 42–44px, so the row was as tall as its
   tallest control and the bar looked ragged as well as costing 17px. One
   height, and it is the compact one. */
.app-main.is-logs .log-form .input { font-size: var(--size-1); padding: var(--gap-1) var(--gap-2); }
.app-main.is-logs .log-form .btn-primary,
.app-main.is-logs .log-form .btn-secondary { padding: var(--gap-1) var(--gap-3); font-size: var(--size-1); }
.app-main.is-logs .log-form-note { padding-block-end: var(--gap-1); }
.app-main.is-logs .log-filters { padding: var(--gap-1) var(--gap-4); }
.app-main.is-logs .log-scroll { padding: var(--gap-2) var(--gap-4); }
.app-main.is-logs .log-pager { padding: var(--gap-1) var(--gap-4); }

/* THEN THE ROW, because the window alone did not reach eight on a laptop.
   43px → 33px: the block padding halves and the line-height comes off 1.6 to
   1.45, which is still a comfortable line for 15px text in a table. Nothing
   else about the row changes — no font shrinks, no column narrows, no value is
   truncated. The owner authorised density in advance and bounded it: «if this
   can affect any other thing make only exception for that». It cannot; see the
   rule at the top of this block. */
.app-main.is-logs .log-head,
.app-main.is-logs .log-group { padding: var(--gap-1) var(--gap-3); line-height: 1.45; }
.app-main.is-logs .log-fields { gap: 0; margin-block-start: 0; }

/* ===========================================================================
   charge types — module 2 of phase 1 · 028
   ===========================================================================
   LOGICAL PROPERTIES ONLY. `tools/check-css.sh` refuses `margin-left`,
   `padding-right`, `float`, `text-align: right` and `[dir="rtl"]` outright, and
   doctrine §12.1 says why: the direction is a property of the DOCUMENT, set
   once by `shell.js`, and every rule below follows it without knowing which one
   it is.

   ===========================================================================
   THE COLOUR IS APPLIED HERE AND DECIDED IN THE DATA
   ===========================================================================
   `.is-guessed` and `.is-flagged` come from `link_state` and `data_state`,
   which are COLUMNS on `charge_types` — `charge-types-render.js stateClass`
   derives the class on every paint and never remembers it.

   **If the yellow and the red lived only in this file they would vanish on
   reload and «how many are still unverified» would be a question with no answer
   anywhere in the system.** `GET /charge-types/counts` can answer it because
   the states are stored. What this file owns is only which colour a stored
   state gets — never which rows have one.

   RED BEATS YELLOW, and it is written once here and once in `stateClass`: a
   flag is a question about the DATA and a guess is a note about a choice we
   made, so the first outranks the second. The panel shows BOTH regardless. */

/* ===========================================================================
   THE ROW IS AS TALL AS ITS NAME — 029
   ===========================================================================
   The list carried five columns of content in the width beside a detail panel,
   so every one of them was cut: «رسوم صحة التو…», «CC BANK FEES - B…», «Cust…»
   three times. **A name the reader cannot finish is what dropped this list to
   two columns**, so nothing below may shorten one:

     no `overflow: hidden`      no `text-overflow: ellipsis`
     no `white-space: nowrap`   no fixed `block-size` on the row

   `min-block-size` is a MINIMUM — a one-line row is 34px and a three-line one
   is as tall as three lines. `align-items: flex-start` is what makes the code
   and the action bar sit beside the FIRST line of a wrapped name rather than
   floating at the middle of a tall row.

   `test_the_list_name_is_not_clipped_by_the_stylesheet` reads this block and
   refuses all four by name, because the natural way to tidy a ragged column is
   to add exactly one of them back. */
.charge-row {
  display: flex;
  align-items: flex-start;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  border-block-end: var(--line-width) solid var(--line);
  cursor: pointer;
  min-block-size: 34px;
}

.charge-row:hover { background: var(--paper-soft); }
.charge-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.charge-row.is-selected { background: var(--accent-soft); }

/* The two review states. Pale enough that black text on them is comfortable —
   the same reasoning the export's palette carries, because the two are read by
   the same people for the same reason. The inline START border is what stays
   visible when the row is also selected or hovered: a background can be
   overridden by another state, and a border on the reading edge cannot be
   mistaken for one. */
.charge-row.is-guessed {
  background: var(--warn-soft);
  border-inline-start: 3px solid var(--warn);
}

.charge-row.is-flagged {
  background: var(--stop-soft);
  border-inline-start: 3px solid var(--stop);
}

/* Selected wins on the BACKGROUND and the state keeps its border, so a
   selected flagged row still says it is flagged. Losing the state colour on
   selection would make the review disappear exactly while somebody is
   reviewing it. */
.charge-row.is-guessed.is-selected,
.charge-row.is-flagged.is-selected { background: var(--accent-soft); }

.charge-row.is-inactive .charge-name,
.charge-row.is-inactive .tree-code { color: var(--ink-faint); }

/* THE NAME, IN FULL, ON AS MANY LINES AS IT NEEDS.
   `overflow-wrap: anywhere` is for the one case a soft wrap cannot handle: a
   single token longer than the column, which without it would push the row
   sideways and make the PAGE scroll. It breaks a word rather than the layout.

   `display: block` — 031. The name is still the direct content of this element
   and the clipping guard still reads THIS rule for the four properties it
   refuses; what the block context adds is that `.charge-links` below starts its
   own line under the name instead of running on after it. */
.charge-name {
  flex: 1;
  min-inline-size: 0;
  overflow-wrap: anywhere;
  line-height: 1.5;
  display: block;
}

/* =============================================================================
   THE ACCOUNT LINE — 031. VERTICAL, NOT HORIZONTAL
   =============================================================================
   029 cut five columns to two because five cells fighting one width clipped
   every one of them. It also took away the only thing that let a reviewer
   answer the question this table exists for — «is this charge pointed at the
   right account?» — so answering it meant opening 454 panels.

   **A SECOND LINE DOES NOT COMPETE FOR WIDTH.** It uses the room a wrapped name
   has already opened. The row roughly doubles in height — about 25 rows on
   screen instead of 50 — and that is the accepted cost of the question being
   answerable at all, not a defect to design around.

   NOTHING FROM 029 IS GIVEN BACK. `overflow-wrap: anywhere` and no
   `overflow: hidden`, no `text-overflow`, no `white-space: nowrap`, no fixed
   `block-size` — on the line as a whole and on the account name inside it. A
   clipped account name would be «مدفوعات مستر…», which is the same defect one
   line lower. `test_the_list_name_is_not_clipped_by_the_stylesheet` reads
   `.charge-links` and `.charge-link-name` alongside `.charge-name` for exactly
   the four, because tidying a ragged second line is the natural place to put
   one of them back.

   `flex-wrap: wrap` — the two accounts sit side by side while there is room and
   the cost account drops under the revenue one when there is not. `--gap-3`
   between them and `--gap-1` inside a pair, so «label code name» reads as one
   thing and the two pairs read as two. */
.charge-links {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--gap-1) var(--gap-3);
  margin-block-start: 2px;
  font-size: var(--size-1);
  color: var(--ink-faint);
  line-height: 1.5;
}

.charge-link {
  display: flex;
  align-items: baseline;
  gap: var(--gap-1);
  min-inline-size: 0;
}

/* «إيراد» and «تكلفة». Quieter than the value it introduces — it is the
   question, and the account is the answer. */
.charge-link-label { color: var(--ink-faint); flex-shrink: 0; }

/* NINE DIGITS, AS STORED — §٦و. `tabular-nums` so the two codes line up under
   each other when the pair wraps, and `flex-shrink: 0` so the digits are never
   the thing that gives way. Not `.tree-code`: that class is the list's first
   column and is 9ch wide and accent-coloured, and putting two more of it in a
   row would make the row disagree with its own heading row. */
.charge-link-code {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  flex-shrink: 0;
}

.charge-link-name { overflow-wrap: anywhere; min-inline-size: 0; }

/* A selected or state-coloured row keeps the second line legible: `--ink-faint`
   on `--accent-soft` is still comfortable, and the row's own colour is what
   says which review state it is in. Only the INACTIVE row dims further, the
   same way its name and code already do. */
.charge-row.is-inactive .charge-links { color: var(--ink-faint); }

/* `.charge-name-en`, `.charge-account` and `.charge-account-name` were the
   other three columns and are GONE with them — the other name and the two
   accounts are in the panel now. Deleted rather than left: an unused rule is a
   thing somebody re-applies by name later, which is ERROR-BOOK item 32's shape
   in a stylesheet. `.charge-name-en` also carried the only hand-written
   `direction` in this file, and that is gone with it: the one name the list
   draws is always the interface's own language, so it never needs telling
   which way it runs. */

.charge-columns {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  padding: var(--gap-1) var(--gap-3);
  border-block-end: var(--line-width) solid var(--line);
  color: var(--ink-faint);
  font-size: var(--size-1);
  background: var(--paper-soft);
}

/* The heading cells carry the row's own classes, so the columns line up because
   they are one definition and not two lists of widths that drift. */

/* =============================================================================
   THE SORTING HEADING — 031
   =============================================================================
   The heading cell keeps its class, because that class is what makes the column
   line up with the row's cell. What is INSIDE it became a button.

   A `button` and not a `span` with a click handler: it has to be reachable by
   Tab, operable by Enter and Space, and announced as something that can be
   pressed — three things a handler on a span gives up and each of which has to
   be rebuilt by hand and gets one of them wrong.

   It inherits the heading's own size and colour rather than looking like the
   `button` default, because it is a HEADING that can be pressed and not a
   control that happens to sit in a heading. The affordance is the hover and the
   cursor; the state is the accent colour and the caret.

   THE CARET POINTS UP FOR ASCENDING and is rotated for descending — the same
   choice, and the same reason, as `.tree-toggle svg`: up and down mean the same
   thing in an RTL build and an LTR one, and an inline-direction arrow would
   need a `[dir="rtl"]` rule that `tools/check-css.sh` refuses. */
.sort-button {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-1);
  font: inherit;
  font-size: inherit;
  color: inherit;
  background: transparent;
  border: none;
  border-radius: var(--radius);
  padding: 0 var(--gap-1);
  margin-inline-start: calc(-1 * var(--gap-1));
  cursor: pointer;
}

.sort-button:hover { background: var(--line); color: var(--ink); }
.sort-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.sort-button.is-on { color: var(--accent); font-weight: 600; }
.sort-button.is-desc .sort-caret { transform: rotate(180deg); }

/* The ✓ control. Green rather than neutral, because it is the only affirmative
   action on this screen. Disabled when the row needs no review — `title` says
   why, and `charge.confirmAlready` is the sentence.

   **IT IS PRESSABLE ON THE ROWS THAT NEED REVIEW, NOT ON EVERY ROW** —
   corrected in 031. The sentence here used to justify the colour by claiming a
   reviewer presses this control once for every row in the table, and that was
   wrong by two orders of magnitude — in a comment whose whole job was to
   justify a design decision. **THE OLD SENTENCE IS DESCRIBED AND NOT QUOTED**,
   because quoting it would put the number back into this file and the guard
   that refuses the number cannot tell a claim from a quotation of one; that is
   ERROR-BOOK item 33's remedy applied where item 33 happened.

   THE RULE is `rowActions`' own:
   `link_state === 'guessed' || data_state === 'flagged'`.
   Everything else draws it DISABLED by the rule directly below — which is the
   point of drawing it at all, because removing it would make «this row needs
   nothing» and «you may not confirm» look the same.

   THE COUNT IS A MEASUREMENT WITH A DATE, NOT A PROPERTY OF THE SCREEN. On
   30 August 2026 `GET /charge-types/counts` answered 2 guessed · 5 flagged ·
   0 both over 454 rows — SEVEN pressable and 447 disabled. It goes DOWN as the
   team reviews, which is the whole purpose, so no rule and no guard is written
   around the number.

   The green still earns its place at seven: it is what a reviewer is looking
   for on a screen where the great majority of rows offer nothing to press. */
.row-action.is-confirm { background: var(--accent-soft); color: var(--accent); }
.row-action.is-confirm[disabled] {
  background: transparent;
  color: var(--ink-faint);
  cursor: default;
}

/* The two state chips in the panel. They carry the colour the row carries, so a
   reader who opens a yellow row finds the same yellow naming which half is
   doubted. `is-link-verified` and `is-data-verified` are deliberately plain: a
   settled state is not news. */
.badge.is-link-guessed { background: var(--warn-soft); color: var(--warn); }
.badge.is-data-flagged { background: var(--stop-soft); color: var(--stop); }

/* The panel head takes the row's colour as a stripe on the reading edge, so the
   panel and the row agree at a glance about what is being looked at. */
.detail-head.is-guessed { border-inline-start: 3px solid var(--warn); padding-inline-start: var(--gap-2); }
.detail-head.is-flagged { border-inline-start: 3px solid var(--stop); padding-inline-start: var(--gap-2); }

/* =============================================================================
   THE SERVICE-CATEGORY FILTER — 031
   =============================================================================
   It sits on its own line under the four chips rather than among them, and that
   is a decision about what the chips MEAN. The chips are the review state — «is
   this row settled?» — and this is the service group. Dropping «IOR» into that
   row would read as a fifth review state.

   A `<label>` WRAPPING THE CONTROL, and a `for` on it as well. The wrapper makes
   the words a click target; the `for` is what an assistive technology reads.
   Neither is redundant: the second is the announcement and the first is the
   target, and this project has already recorded once that the styling is not
   the announcement (`.nav-item.is-pending`).

   `is-compact` — `.input` is `width: 100%` because every other one of them is
   in a stacked form. This one is in a row beside its own label, so it takes the
   width of its content and stops. */
.filter-field {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
  flex-wrap: wrap;
}

.filter-label { font-size: var(--size-1); color: var(--ink-soft); }

.input.is-compact {
  width: auto;
  max-width: 100%;
  padding: var(--gap-1) var(--gap-2);
  font-size: var(--size-1);
}

.input.is-compact[disabled] {
  color: var(--ink-faint);
  background: var(--paper-soft);
  cursor: default;
}

.charge-pager {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  padding: var(--gap-2) var(--gap-3);
  border-block-start: var(--line-width) solid var(--line);
}

/* The picker's list. `size="6"` on the select makes it a list rather than a
   dropdown, which is what turns «choose an account» into «scan the accounts» —
   and it is 181 rows, so scanning is the operation. */
.field select.input {
  block-size: auto;
  padding: var(--gap-1);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
}

/* ===========================================================================
   THE FORM'S THREE GROUPS — 029
   ===========================================================================
   The create form was nine identical boxes in one column, with nothing between
   a NAME and a PROFIT CENTRE. These are the three questions it actually asks —
   who it is · where it posts · what it belongs to — and each is a box with a
   heading and room around it.

   `--gap-3` between the fields INSIDE a group and `--gap-3` between the groups
   comes from `.form-card`; what separates them is the border and the heading,
   not a bigger number, because a form separated only by whitespace goes back to
   being one column the moment somebody adds a field. */
.form-group {
  display: flex;
  flex-direction: column;
  gap: var(--gap-3);
  padding: var(--gap-3);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-soft);
}

.form-group-head {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  flex-wrap: wrap;
  padding-block-end: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
}

.form-group-head h4 { margin: 0; font-size: var(--size-2); }

/* The note beside a group's heading wraps under it on a narrow panel rather
   than squeezing the heading — `flex-wrap` above — and it is the SENTENCE that
   says why the group exists, so it is not `--ink-faint`. */
.form-group-note {
  font-size: var(--size-1);
  color: var(--ink-soft);
}

/* ===========================================================================
   THE CLASSIFICATION PICKER — the same shape as the account picker
   ===========================================================================
   A label, a search box, a `size="6"` list and a scope line — because it is the
   same operation and the same control should not look like two. What it adds is
   a second MODE: a text box for a value that does not exist yet, one press
   away, and one press back.

   The two modes are swapped with the `hidden` attribute, and each carries its
   own `[hidden]` rule for the reason `.search-clear[hidden]` and
   `.user-menu-panel[hidden]` do: a class that sets `display` outranks the user
   agent's `[hidden]`, so a box with `display: flex` and `hidden` on it stays
   visible. */
.vocab-field { gap: var(--gap-2); }

.vocab-mode {
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
}

.vocab-mode[hidden] { display: none; }

/* The two mode buttons sit at the end of their box and do not stretch across
   it: they are an escape hatch, not the operation. `align-self` follows the
   inline direction on its own — no side is named. */
.vocab-mode .btn-secondary {
  align-self: flex-end;
  margin-block-start: var(--gap-1);
}

/* =============================================================================
   038 — THE PARTIES SCREEN
   =============================================================================
   THE SAME TWO RULES AS EVERY BLOCK ABOVE, and neither is relaxed here:

     · LOGICAL PROPERTIES ONLY. No `margin-left`, no `text-align: right`, no
       bare `left:`, no `[dir=…]`. `tools/check-css.sh` reads this file with its
       comments blanked and refuses all of them by name
     · EVERY RULE IS A CLASS. `style-src 'self'` carries no `unsafe-inline`, so
       an inline `style=` attribute applies on this laptop and is INERT behind
       nginx — ERROR-BOOK 1's family, and the reason the parties DESIGN page
       (which is written entirely in inline styles) is a reference and not a
       source to copy

   AND THE SHELL'S HEIGHT IS NOT TOUCHED. `.app` carries `height: 100vh` then
   `height: 100dvh` and no `min-height` since `031`, and every scrolling pane on
   this screen declares its own `overflow-y: auto; min-height: 0; flex: 1` —
   ERROR-BOOK 52, «قاعدةٌ أعطت أرضيةً ولا سقف». This screen reuses `.chart`,
   `.chart-tree`, `.chart-detail` and `.tree-scroll` exactly as they are, so it
   inherits that and adds nothing that could undo it.

   ★ AND THE NARROW SCREEN IS NOT BUILT — SAID HERE RATHER THAN LEFT SILENT.
   There is not one `@media` query in this stylesheet, and this screen is WIDER
   than the accounts tree it sits beside: five list columns instead of three,
   and a detail pane carrying six panels instead of one. `DEFERRED-UNLOCKS.md`
   holds the decision («الشاشة الضيقة في شجرة الحسابات») and 038 extends its
   scope to name this screen with a measurement — see the report and that item.
   The first thing that breaks below the design width is the LIST, because
   `.party-row` is a five-column grid with a fixed identity column; the detail
   pane goes second. Neither is designed, and «فلا تُخترع». */

/* --- the list ------------------------------------------------------------- */
/* FIVE CELLS, and the heading row uses the SAME classes in the SAME order — so
   the two line up because they are one definition rather than two lists of
   widths that drift. `029`'s rule, third screen. */
.party-row {
  display: flex;
  align-items: flex-start;
  gap: var(--gap-2);
  padding: var(--gap-2);
  border-radius: var(--radius);
  cursor: pointer;
  position: relative;
  /* A MINIMUM AND NOT A FIXED HEIGHT — the name wraps and the row grows to fit
     it. `029` cut five columns to two because every one of them was clipped,
     and the rule that came out of it is that the thing the reader came for is
     never shortened. */
  min-block-size: 42px;
}

.party-row:hover { background: var(--paper-soft); }
.party-row.is-selected { background: var(--accent-soft); }
.party-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.party-row.is-inactive .party-name,
.party-row.is-inactive .party-identity { color: var(--ink-faint); }

/* THE NAME IS NEVER CLIPPED. No `overflow: hidden`, no `text-overflow`, no
   `white-space: nowrap` — the four properties
   `test_the_list_name_is_not_clipped_by_the_stylesheet` refuses by name on the
   charge-types screen, and the same four are refused here for the same reason:
   a company name is what the reader came for. */
/* ★ MEASURED IN A BROWSER, 040: THE NAME CELL WAS **64 PIXELS**.
   The list pane is 700px by design (`--tree-width`), the four fixed cells took
   192 + 144 + 128 + 100 and the gaps took the rest, so `flex: 1` over
   `min-inline-size: 0` gave the name whatever was left — which was two Arabic
   characters per line. On the customers screen, which carries one cell more, it
   was **zero**, and every name rendered one character per line.

   `029` cut the charge list from five content columns to two over exactly this
   and wrote the rule: «خمسُ خلايا تتنازع عرضاً واحداً تُقصّ كلُّها», and
   `test_the_party_name_is_not_clipped_by_the_stylesheet` guards the four
   properties that CLIP a name — `overflow: hidden`, `text-overflow`,
   `white-space: nowrap` and a fixed height. **None of them was present.** The
   name was not clipped; it was starved, which no property forbids and no test
   was looking for.

   THE FIX IS A FLOOR AND A SHRINK, and it is the pair that matters:
     · the name may never be narrower than `--name-floor`
     · every other cell may SHRINK — they were `flex-shrink: 0`, which is what
       made the name the only thing that could give
   So the row fits at the design width, and below it the badges narrow before
   the name does. */
.party-name {
  flex: 1 1 auto;
  min-inline-size: var(--name-floor);
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow-wrap: anywhere;
}

.party-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--gap-2);
  font-size: var(--size-1);
  color: var(--ink-faint);
}

.party-industry { overflow-wrap: anywhere; }

/* THE TWO ROLE CODES. `.party-code` and NOT `.tree-code` — that class is the
   ACCOUNTS list's first column, 9ch wide and accent-coloured, and reusing it
   here would put a nine-character box around a `CUS-00001`. `031` made the same
   distinction for the charge row's account codes and said why. */
.party-code {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-1);
  padding-block: 1px;
  padding-inline: 5px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  white-space: nowrap;
}

.party-code.is-customer { color: var(--accent); border-color: var(--accent); }
.party-code.is-supplier { color: var(--warn); border-color: var(--warn); }

.party-identity {
  flex: 0 1 12rem;
  min-inline-size: 6rem;
  font-size: var(--size-1);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.party-tax {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.party-tax.is-foreign {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--gap-1);
  font-family: inherit;
}

.party-roles { flex: 0 1 9rem; min-inline-size: 4.5rem; }

/* ★ 042 · THE ROLE BADGE WRAPS RATHER THAN OVERFLOWS — found by opening the
   parties screen in ENGLISH, which is the same way `040` found the stacked
   heading row above this line.

   `.badge` is `white-space: nowrap` for every other use of it, and rightly:
   «معطَّل» and «Demo data» are short and a broken chip reads as two chips.
   «Customer and supplier» is not short. Measured at 1920px: the badge was
   148px inside a 109px cell whose `overflow` is `visible`, so it ran across
   the Status column and sat under the «Demo data» badge — two rows of the
   eight demo parties, in English only, because «عميل ومورد» fits.

   CLIPPING WOULD HAVE BEEN WORSE THAN OVERFLOWING. A role reading «Customer
   and sup» is a cell that lies about a derived value, and `PartyOut.role` is
   computed precisely so that no flag can — screens page §٩ب, «لا عَلَمَ
   يستطيع أن يكذب». It takes a second line instead; the rows on this
   screen are already two lines tall. */
.party-roles .badge { white-space: normal; }
.party-status {
  flex: 0 1 8rem;
  min-inline-size: 4.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-1);
}

/* ★ THE SELECTOR WAS `.party-columns` AND THE MARKUP SAYS `parties-columns` —
   FOUND BY OPENING THE SCREEN, 040, AND IT SHIPPED IN 038.

   `parties.html` carries `<div class="parties-columns" id="parties-columns">`
   and this rule named the SINGULAR. It therefore matched **nothing at all**:
   the heading row fell back to `display: block` and the five headings —
   الاسم · الرقم الضريبي · الأدوار · الحالة · إجراءات — stacked one under the
   other above the list, on every load, since the day the screen shipped.

   ★ AND NOT ONE GUARD COULD SEE IT. `check-css.sh` reads logical properties;
     `check-html.sh` reads `innerHTML` and the policy; every test on that screen
     asserts MARKUP, and the markup was correct. It is ERROR-BOOK 48 exactly —
     «شريطُ إجراءاتٍ مرسومٌ وصحيحٌ وغيرُ مرئيّ · وكلُّ اختبارٍ على الوسم ينجح» —
     one class over, and it took the same thing to find: somebody looking at the
     screen.

   `test_every_row_cell_class_the_screens_name_has_a_rule` now compares the
   classes the row builders NAME against the classes this stylesheet STYLES, so
   the next one fails instead of shipping. */
.parties-columns,
.role-columns {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  padding-block: var(--gap-1);
  padding-inline: var(--gap-2);
  border-block-end: 1px solid var(--line);
  font-size: var(--size-1);
  color: var(--ink-faint);
}

/* --- the badges ----------------------------------------------------------- */
/* THE ROLE'S COLOUR COMES FROM `row.role`, WHICH IS DERIVED IN SQL. It is not
   remembered anywhere and it is recomputed on every paint — the same argument
   `charge-types-render.js stateClass` makes: a colour that lived only in a
   stylesheet would vanish on reload and «how many are dual» would have no
   answer in the system at all. Here the server answers it in `/counts`. */
.badge.is-role-customer { background: var(--accent-soft); color: var(--accent); }
.badge.is-role-supplier { background: var(--warn-soft); color: var(--warn); }
.badge.is-role-both {
  background: var(--accent-soft);
  color: var(--accent);
  border: 1px solid var(--warn);
}
.badge.is-role-none { color: var(--ink-faint); }
.badge.is-demo { background: var(--warn-soft); color: var(--warn); }
.badge.is-foreign { background: var(--accent-soft); color: var(--accent); }
.badge.is-currency { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.badge.is-primary { background: var(--accent); color: var(--paper); }
.badge.is-scope { color: var(--ink-soft); }
.badge.is-movements { background: var(--stop-soft); color: var(--stop); }
.badge.is-scan { background: var(--warn-soft); color: var(--warn); }

/* --- the filter bar ------------------------------------------------------- */
/* THIRTY-ONE CONTROLS, AND THEY ARE BEHIND A TOGGLE. Drawn open by default they
   would push the list off the pane before a reader saw a single row — and the
   six chips are what the ordinary question needs. `aria-expanded` on the toggle
   is what says which state it is in, not the styling. */
.filter-bar {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
}

.filter-bar-head {
  display: flex;
  align-items: center;
  gap: var(--gap-2);
}

/* A GRID AND NOT A ROW OF BOXES: thirty-one controls in a flex row is
   thirty-one different widths. `auto-fill` with a minimum keeps every field
   readable at any pane width and reflows on its own — which is the ONE
   responsive behaviour in this stylesheet, and it is a property of the grid
   rather than a breakpoint. */
.filter-fields {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: var(--gap-2);
  max-block-size: 22rem;
  /* ITS OWN SCROLL, WITH ITS OWN FLOOR — ERROR-BOOK 52: every scrolling pane
     declares `overflow-y`, and `min-block-size: 0` is what stops a flex child
     from refusing to shrink. */
  overflow-y: auto;
  min-block-size: 0;
  padding: var(--gap-1);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
}

.filter-bar-foot { display: flex; gap: var(--gap-2); }

/* `cost` IS SHOWN, and `indexed` IS NOT TURNED INTO A TICK. `indexed` is
   computed as `spec.index != "scan"` — a string comparison, not a promise, and
   `foreign_tax_ref` reports `true` while its own `cost` says it is a scan
   without `country`. So the SENTENCE is what is drawn. */
.filter-cost {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--gap-1);
  font-size: var(--size-1);
}

.applied-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-1);
  font-size: var(--size-1);
  padding-block: var(--gap-1);
}

.applied-strip.is-none { color: var(--ink-faint); }

/* --- the card's panels ---------------------------------------------------- */
.locked-box {
  background: var(--paper-soft);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2);
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
}

.role-block {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2);
  margin-block-start: var(--gap-2);
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
}

.role-block.is-inactive { opacity: 0.68; }

.role-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-2);
}

.role-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-2);
  margin-block-start: var(--gap-1);
}

/* --- credit limits · FOUR states, and none of them is a colour ------------- */
/* THE MEANING IS A WORD IN A COLUMN, not a tint. «لا صفّ» and «صفر» and «قيمة»
   and «بلا سقف» are four different facts about a customer, and three of them
   have a row while the fourth is the ABSENCE of one — a colour cannot say the
   fourth at all. So the table says the three in words and the empty state says
   the fourth in a sentence. */
.limit-table {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.limit-row {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  padding-block: var(--gap-1);
  padding-inline: var(--gap-2);
  border-block-end: 1px solid var(--line);
  font-size: var(--size-1);
}

.limit-row:last-child { border-block-end: none; }
.limit-row.is-head { background: var(--paper-soft); color: var(--ink-faint); }

.limit-currency { inline-size: 8rem; flex-shrink: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.limit-amount { inline-size: 10rem; flex-shrink: 0; text-align: end; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.limit-means { flex: 1; min-inline-size: 0; }
.limit-actions { display: flex; gap: var(--gap-1); flex-shrink: 0; }

/* --- the party's bank rows ------------------------------------------------ */
/* ★ `.is-banks` CARRIES A VISIBLE EDGE, and it is the only panel on this card
   that does. The owner's instruction was about this panel by name — «make its
   name and its function another name so it does not get mixed with our bank
   accounts» — and the heading, the permanent note and this edge are three of
   the four devices. The fourth is that `party_bank_details` HAS NO
   `gl_account_id` COLUMN, which is the one that cannot be undone by a reader
   in a hurry.

   The edge is `border-inline-start` and not a `box-shadow: inset`: a shadow
   offset is PHYSICAL, has no logical form, and would sit on the same edge in
   both languages — which `check-css.sh` refuses by name after this stylesheet
   grew one in F4. */
.form-card.is-banks {
  border-inline-start: 3px solid var(--warn);
  padding-inline-start: var(--gap-2);
}

.bank-row {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-2);
  margin-block-start: var(--gap-2);
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
}

.bank-row.is-inactive { opacity: 0.68; }

.bank-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-2);
}

/* --- contacts and documents ------------------------------------------------ */
.contact-row,
.doc-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--gap-2);
  padding-block: var(--gap-1);
  border-block-end: 1px solid var(--line);
  font-size: var(--size-1);
}

.contact-row:last-child,
.doc-row:last-child { border-block-end: none; }

.contact-row.is-inactive,
.doc-row.is-inactive { opacity: 0.68; }

.contact-name { font-weight: 600; }
.contact-reach { flex: 1; min-inline-size: 0; overflow-wrap: anywhere; }

/* `original_name` IS USER-CONTROLLED TEXT and it is the only field in this
   module a user writes and the screen displays verbatim. It is escaped by the
   `html``` template — never `raw()` — and it wraps rather than being clipped,
   because a truncated filename is a filename somebody cannot identify. */
.doc-name { flex: 1; min-inline-size: 8rem; overflow-wrap: anywhere; }
.doc-icon { flex-shrink: 0; color: var(--ink-faint); }
.doc-type { inline-size: 8rem; flex-shrink: 0; overflow-wrap: anywhere; }
.doc-size {
  inline-size: 5rem;
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.doc-when { inline-size: 7rem; flex-shrink: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* --- the net position ------------------------------------------------------ */
/* The three figures reuse `.balance-split` and `.balance-net` from the accounts
   screen, because they are the same shape of fact rendered the same way — and
   both of them read `formatAmount`, which is now one function in `money.js`
   rather than two copies. */
.form-card.is-position { background: var(--paper-soft); }

/* --- the re-identification dialog ------------------------------------------ */
/* THE MOVEMENT COUNT IS THE THING THAT MAKES SOMEBODY STOP. «2» and «400» are
   different decisions, so the number is drawn at the size of a decision and not
   at the size of a detail. */
.movements-count {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-3);
  font-weight: 600;
  color: var(--stop);
}

.conflict-row .was { color: var(--ink-faint); }
.conflict-row .now { color: var(--stop); font-weight: 600; }

/* A confirmation that PASSED rather than one that is being asked for — the
   lookup's «no party carries this number» is a green light and reads as one. */
.notice-ok {
  background: var(--accent-soft);
  border-inline-start: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: var(--gap-2);
  display: flex;
  flex-direction: column;
  gap: var(--gap-1);
}

/* =============================================================================
   040 · THE NESTED SIDEBAR GROUP, AND THE THREE ROLE LISTS
   =============================================================================
   LOGICAL PROPERTIES ONLY. `check-css.sh` refuses `margin-left`, `text-align:
   left`, a bare `left:` and any `[dir=…]` selector, on comment-stripped CSS —
   the direction flips from `<html dir>` alone and there is no second
   stylesheet. Doctrine §12.1: it has to be right in the first file, because it
   cannot be migrated to later at a sensible price.
   ========================================================================== */

/* --- the group ------------------------------------------------------------ */
/* THE OWNER SETTLED THE SHAPE: «الأطراف» becomes a group HEADING with three
   items under it — not an icon rail and not a redesign of the shell. His
   reasoning, kept because it is a rule rather than a preference: **a rail's
   second level IS a nested group**, so this is a subset of the rail rather than
   a competing design, and the rail earns its keep at ten-plus items where there
   are seven.

   ★ AND THE GROUP DOES NOT COLLAPSE, WHICH IS A DECISION.
     Three children under one parent, seven items in the whole column: a
     disclosure control would be a second state to persist, to restore, to test,
     and to get wrong on a language switch — for a list that fits on a screen
     twice over. It is the owner's own argument against the rail, one level
     down. The day a group has ten children the control earns its keep, and
     `.nav-children` is where it goes. */
.nav-branch {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* THE INDENT IS `padding-inline-start` AND NOT `margin-left` — the whole point
   of the logical properties: this reads as an indent in an RTL build and in an
   LTR one without a second rule and without a `[dir]` selector. */
.nav-children {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-inline-start: var(--gap-3);
  border-inline-start: var(--line-width) solid var(--line);
  margin-inline-start: var(--gap-2);
}

/* A CHILD IS A LINK LIKE ANY OTHER, one size smaller and no more. It is NOT
   dimmed: dimming is what `.is-pending` means — «this does not exist yet» — and
   a child that looked pending would be telling the reader something false about
   a screen that is there. */
.nav-item.is-child {
  font-size: var(--size-1);
  padding-block: var(--gap-1);
}

/* THE PARENT WHEN THE USER HOLDS IT — an ordinary link that happens to have
   children under it. */
.nav-item.is-branch { font-weight: 600; }

/* ★ AND THE PARENT WHEN THEY DO NOT — a HEADING, in the same shape as
   «البيانات الأساسية» above it.

   `030`'s grant loop copies `customers` grants to `parties` and not the
   reverse, and copies nothing for `suppliers`, so `suppliers.view` without
   `parties.view` is reachable BY CONSTRUCTION. The honest drawing is the
   heading and the screen they hold: it names what the children are and claims
   nothing else. It is deliberately NOT a dimmed nav item — that shape already
   means «not built yet», and inventing a third word («no permission») would be
   a sentence about a module they did not ask about. `spec-04 §ط.٧` option (أ),
   extended one level. */
.sidebar-group.is-branch {
  padding-block-end: 0;
  color: var(--ink-soft);
}

/* --- the three role lists ------------------------------------------------- */
/* SIX CELLS, and the heading row uses the SAME classes in the SAME order — so
   the two line up because they are one definition rather than two lists of
   widths that drift. `029`'s rule, fourth screen.

   `.role-row` also carries `.party-row`, which is where the hover, the
   selection, the focus ring, the minimum height and the never-clipped name come
   from. This block adds only the cells that are this list's own. */
.role-code {
  flex: 0 1 7.5rem;
  min-inline-size: 5.5rem;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
}

/* ★ THE PAYMENT TERMS MOVED OFF THE ROW AND ONTO THE SECOND LINE — 040, after
   measuring. Six cells over a 700px pane left the NAME with zero pixels, and
   `029`'s remedy for the identical measurement was vertical rather than
   horizontal: «العلاج رأسيٌّ لا أفقي». The terms sit beside the industry and
   the tax number, in the room a wrapped name has already opened. */
.role-terms {
  display: inline-flex;
  gap: var(--gap-1);
  align-items: baseline;
}

.role-terms-pair { display: inline-flex; gap: var(--gap-1); flex-wrap: wrap; }

/* ★ THE BALANCE CELL. It is wider than the others because it is what this task
   exists to design in, and because the dual screen puts THREE figures in it —
   «لنا عليه», «له علينا» and the net. */
/* ★ NARROWED FROM 15rem AFTER MEASURING. It carries one figure on two of the
   three screens and three STACKED figures on the dual one, so it never needed
   the width of three side by side — and 15rem was a quarter of the pane. */
.role-balance {
  flex: 0 1 12rem;
  min-inline-size: 7rem;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--size-1);
}

/* ★ IT WRAPS, AND THE LABEL DOES NOT — 040, seen in the ENGLISH build.
   «لنا عليه:» is nine characters and «They owe us:» is twelve, so the cell that
   fits one line in Arabic broke into THREE in English — «They / owe / us:» —
   over a value that then wrapped as well. A column sized in the language it was
   built in is a column that is wrong in the other one, and this screen is
   bilingual by rule (doctrine §12).
   So the pair wraps as a PAIR: the label never breaks mid-phrase, and the value
   drops to its own line when the two do not fit. */
.role-figure {
  display: flex;
  justify-content: space-between;
  gap: var(--gap-1);
  align-items: baseline;
  flex-wrap: wrap;
}

.role-figure .muted { white-space: nowrap; }

/* 102 · the supplier row's link to his page under Finance ▸ Payables — small, under the figure, never wrapping */
.role-page {
  font-size: var(--size-1);
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
  align-self: flex-start;
}

.role-page:hover { text-decoration: underline; }

/* ★ «لا حركات» IS NOT STYLED AS A NUMBER, AND THAT IS THE POINT.
   `.num` is the tabular, monospaced treatment a column of amounts is compared
   down by eye. A sentence in that face would read as a value; this one is
   deliberately in the body face and faint, so the eye tells «there is no figure
   here» from «the figure is small» without reading a word.
   «صفرٌ رصيدٌ وغيابُ الحركة عدم» — in the stylesheet as well as in the words. */
.no-movements { font-style: italic; }

/* --- one sentence saying what a list IS ----------------------------------- */
/* «العملاء» and «كل من يحمل دور عميل» are not the same set, and the difference
   is the whole reason these screens exist: `GET /parties?role=customer` answers
   three of the five. The lead line says which set this is, above the counts
   that prove it. */
/* ★ IT SITS ABOVE `.list-top` NOW, so it owns its own edges — it used to
   inherit them from `.chart-head`. And it scrolls away: see the note in the
   markup. */
.chart-lead {
  margin: 0;
  padding: var(--gap-2) var(--gap-4) 0;
  font-size: var(--size-1);
  color: var(--ink-soft);
}

/* --- a diff cell that has no value to show -------------------------------- */
/* ERROR-BOOK 71: a row that draws nothing means «unchanged», and a row that
   draws nothing BECAUSE THE VALUE NEVER ARRIVED means «I do not know» — and the
   two must not look the same. The sentence is loud on purpose. */
.stale-absent { color: var(--stop); font-weight: 600; }

/* =============================================================================
   047 · THE REQUEST SCREENS — milestone 2, part A
   =============================================================================
   LOGICAL PROPERTIES ONLY, like every rule above it. There is no `margin-left`,
   no `left:`, no `float` and no `[dir="rtl"]` in this block — the Arabic mirror
   is free because the whole stylesheet was written that way in the first file,
   which is doctrine §12.1's entire argument.

   THE TOKENS ARE THE ONES ALREADY DEFINED. Not one new colour: the state chips
   and the age chip use `--accent`, `--warn` and `--stop` with their soft pairs,
   so a theme change is still `:root` and nothing else.

   NO NEW CLASS COLLIDES WITH THE 3,595 LINES ABOVE — every one is prefixed
   `req-`, checked against the class inventory before it was written. The three
   modifiers that are not (`.chip.is-quiet`, `.is-requests`, `.is-request`) are
   scoped under a class that is.
   ============================================================================= */

/* --- the list screen ------------------------------------------------------- */
/* The shell gives `.app-main` its height; this pane declares its own scroll —
   ERROR-BOOK 52, «قاعدةٌ أعطت أرضيةً ولا سقف». `min-height: 0` is what lets a
   flex child shrink below its content and scroll instead of pushing the page. */
.app-main.is-requests { padding: 0; }

.req-screen {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  padding-block: 0 var(--gap-4);
}

.req-head { display: flex; flex-direction: column; gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-4); }
/* The column-name row keeps the list's own inline padding, so a heading sits
   exactly over the cell it names. It used to inherit it from `.req-screen`,
   which no longer pads. */
.req-columns { padding-inline: var(--gap-4); background: var(--paper); }
/* The heading row is a LABEL row, not a data row, so it does not need a data
   row's height. Eight pixels off the one thing that is on the screen for ever. */
.req-columns .req-row.is-head { padding-block: var(--gap-1); }

/* ⛔ NO `overflow-y` — the page scrolls instead. */
.req-scroll { padding-inline: var(--gap-4); }

/* SEVEN CELLS, and the heading row uses the SAME grid — so the two line up
   because they are one definition rather than two lists of widths that drift.
   `029`'s rule, sixth screen.
   ★ 055 · the «with whom» cell is gone: the first cell is the DERIVED word
     («With operations» · «Pricing» · «Won»), and a second cell saying the
     same thing was the old two-catalogue defect drawn twice per row. The
     first column widened to hold Karim's longer words in Arabic. */
.req-row {
  display: grid;
  grid-template-columns:
    9.5rem                    /* where    */
    minmax(9rem, 1.4fr)       /* client   */
    8.5rem                    /* form     */
    minmax(7rem, 1fr)         /* what     */
    minmax(6rem, 0.8fr)       /* route    */
    minmax(9rem, 1.6fr)       /* subject  */
    5rem;                     /* age      */
  align-items: start;
  gap: var(--gap-2);
  padding: var(--gap-2);
  text-decoration: none;
  color: inherit;
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-1);
}

/* ⛔ `position: sticky; inset-block-start: 0` IS GONE FROM HERE — A.
   It stuck to `.req-scroll`, which no longer scrolls. Left as it was, it would
   have pinned itself to the DOCUMENT at y = 0, which is underneath the app
   header. `.list-top` in the markup wraps this row together with the toolbar
   and sticks the pair at `--header-height`, which is one offset instead of
   two that have to agree. */
.req-row.is-head {
  font-weight: 600;
  color: var(--ink-soft);
  background: var(--paper-inset);
  border-block-end: 2px solid var(--line-strong);
}

.req-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* THE TAGS WRAP SIDEWAYS. Stacked, three of them tripled the height of every
   row on the list — seen in a browser, not reasoned about — and a list whose
   rows are three lines tall stops being scannable, which is the one thing this
   screen has to be. */
.req-cell.is-what { flex-direction: row; flex-wrap: wrap; gap: 3px; align-content: start; }
.req-cell.is-age { align-items: center; }

/* ★ 053 · THE ROUTE READS AS PLACES, with the code QUIET beside the name —
   Karim ④. The name is the answer and the code is the reference; drawing them
   at the same weight would be the old cell with more characters in it. A leg
   whose value was never a code has no second half at all. */
/* ★ 053 · «Not sent yet» — the sentence a review page owes its reader. It sits
   above the action bar because that is where the eye lands after a save, and
   the two halves stack so the claim reads before the instruction. */
.req-review-banner {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-block-end: var(--gap-3);
}

.req-leg { display: block; min-width: 0; }
.req-leg-code { font-size: var(--size-0); }
.req-cell > span { overflow-wrap: anywhere; }

/* ★★ 052 · THE TICK COLUMN, before «State».
   `.req-headline` is the head row's own flex wrapper and it exists so the
   heading and the rows line up because they are ONE shape — `029`'s rule, the
   same argument that makes `.req-row` a shared grid. With no `.req-pick` inside
   it (nobody holding `delete`) the row still takes the whole width, so the
   table is byte-for-byte what it was for everyone else.

   ⛔ The tick is a SIBLING of the `<a class="req-row">`, never inside it: an
   `<input>` inside an anchor is invalid markup and behaves differently in
   Chrome and Edge, which are the two browsers Nile has.

   The bottom border matches `.req-line-actions`' — the row's rule has to run
   the whole width or the list looks combed. */
.req-headline { display: flex; align-items: stretch; }
.req-headline > .req-row { flex: 1; min-width: 0; }
.req-pick {
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.5rem;
  flex: 0 0 auto;
  border-block-end: var(--line-width) solid var(--line);
}
.req-pick.is-head { border-block-end: 2px solid var(--line-strong); background: var(--paper-inset); }
/* A tick a finger can hit, and a focus ring that is visible — rule 10. */
.req-tick { inline-size: 1rem; block-size: 1rem; cursor: pointer; accent-color: var(--accent); }
.req-tick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The dialog's list of codes SCROLLS rather than being truncated: the twelve
   rows «and 12 more» would hide are exactly the ones somebody wants to read
   before confirming. */
.delete-list { max-block-size: 40vh; overflow-y: auto; }
.delete-refusal { margin-block-start: var(--gap-2); }

.req-line { display: flex; align-items: stretch; background: var(--paper); }
.req-line:hover { background: var(--paper-soft); }
.req-line > .req-row { flex: 1; min-width: 0; }
.req-line.is-off { opacity: 0.55; }
.req-line-actions {
  display: flex;
  align-items: center;
  padding-inline-end: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
}
.req-line-action { font-size: var(--size-1); white-space: nowrap; }

/* ★★ 053 · THE «VIEW» COLUMN — Karim ⑪. One width, shared by the heading and
   the rows, so the two line up because they are ONE number and not two that
   drift: `029`'s rule, the same argument `.req-row`'s shared grid makes.
   It is a SIBLING of the anchor, never inside it — an `<a>` in an `<a>` is
   invalid and the two browsers Nile has disagree about it. */
.req-line-view {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  inline-size: 5rem;
  flex: none;
  padding-inline-end: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
}
.req-line-view.is-head {
  font-weight: 600;
  color: var(--ink-soft);
  background: var(--paper-inset);
  border-block-end: 2px solid var(--line-strong);
  font-size: var(--size-1);
  padding-block: var(--gap-2);
}
.req-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.req-client-name { font-weight: 600; }
/* A REQUEST NUMBER AND A CLIENT CODE ARE CODES. `REQ-2026-0019` and
   `CUS-00001` are read down a column looking for one of them, which is what the
   mono face is for. */
.req-client-code, .req-code {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.75rem;
  color: var(--ink-faint);
}
.req-subject { color: var(--ink-soft); }
.req-form {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.req-tag {
  display: inline-block;
  padding: 0 5px;
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-soft);
  font-size: 0.72rem;
  white-space: nowrap;
}
.req-notes { color: var(--ink-faint); font-size: 0.75rem; }

/* --- the state chip · the name from `lookups`, the tone from the code ------- */
.req-state {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  border: var(--line-width) solid var(--line);
  background: var(--paper-soft);
  white-space: nowrap;
}
.req-state.is-draft   { background: var(--paper-soft); color: var(--ink-soft); }
.req-state.is-open    { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.req-state.is-asking  { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
.req-state.is-ready   { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.req-state.is-out     { background: var(--accent); color: var(--paper); border-color: var(--accent); }
.req-state.is-won     { background: var(--accent); color: var(--paper); border-color: var(--accent); }
.req-state.is-lost    { background: var(--stop-soft); color: var(--stop); border-color: var(--stop); }
.req-state.is-off     { background: var(--paper-soft); color: var(--ink-faint); }

/* --- the age chip · the only number on the list that changes by itself ------ */
/* FOUR BANDS AND THE NUMBER IS BARE — the noun lives in the column heading,
   because Arabic inflects «يوم» in three bands and card D's count rule forbids
   a count immediately before a noun that must agree with it. */
.req-age {
  display: inline-block;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  min-width: 2.2rem;
  padding: 1px 6px;
  border-radius: var(--radius);
  text-align: center;
  font-size: 0.75rem;
  font-weight: 600;
  border: var(--line-width) solid transparent;
}
.req-age.is-fresh { color: var(--ink-faint); }
.req-age.is-warm  { background: var(--paper-soft); color: var(--ink-soft); border-color: var(--line); }
.req-age.is-hot   { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
.req-age.is-late  { background: var(--stop-soft); color: var(--stop); border-color: var(--stop); }

/* --- the lane strip -------------------------------------------------------- */
.req-lane-strip { display: flex; align-items: center; gap: var(--gap-1); flex-wrap: wrap; }
.req-lane-note { margin: 0; }
.chip.is-quiet { font-size: 0.75rem; padding: 1px 8px; }

/* =============================================================================
   the request card
   ============================================================================= */
.app-main.is-request { padding: 0; }

.req-card {
  padding: var(--gap-3) var(--gap-4) var(--gap-5);
  max-width: 74rem;
  margin-inline: auto;
}

.req-card-actions {
  display: flex;
  gap: var(--gap-2);
  align-items: center;
  flex-wrap: wrap;
  margin-block-end: var(--gap-3);
}

.req-card-head { margin-block-end: var(--gap-3); }
.req-card-title { display: flex; align-items: center; gap: var(--gap-2); flex-wrap: wrap; }
.req-card-title h1 { margin: 0; }
.req-card-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--gap-1) var(--gap-3);
  margin-block-start: var(--gap-2);
}

/* TWO COLUMNS ON THE CARD: what the request SAYS, and what can be DONE about
   it. The side column is the one an operations person works from, so it is
   `start`-aligned and does not stretch to the height of a forty-field form. */
.req-card-body {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(18rem, 1fr);
  gap: var(--gap-4);
  align-items: start;
}
.req-card-main { min-width: 0; }
.req-card-side { display: flex; flex-direction: column; gap: var(--gap-3); }

.req-panel {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-3);
}
.req-panel h3 { margin-block-end: var(--gap-2); }
.req-panel.is-warn { border-color: var(--warn); background: var(--warn-soft); }

/* ★ 055 · `054`'s two headed verb groups («Sales verbs» / «Operations verbs»,
   drawn for an administrator only) are GONE with ruling ③ as amended by its
   author: an administrator sees both sides, each on its own page, and no page
   draws more than one side. `.req-verb-group` and `.req-verb-whose` left with
   the markup that used them. */

/* --- the sections, in `sort_order` ----------------------------------------- */
.req-section {
  background: var(--paper);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap-3);
  margin-block-end: var(--gap-3);
}
.req-section-head {
  margin: 0 0 var(--gap-2);
  font-size: var(--size-1);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}
.req-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--gap-2) var(--gap-3);
}
.req-field { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.req-field.is-bool { justify-content: end; }
.req-field.is-because .input { border-color: var(--warn); }
.req-because { color: var(--warn); }
.req-textarea { resize: vertical; font: inherit; }
.required { color: var(--stop); margin-inline-start: 2px; }

.req-read-lines { display: flex; flex-direction: column; gap: var(--gap-1); }
.req-read-line { display: flex; gap: var(--gap-2); align-items: baseline; }
.req-read-line .field-label { min-width: 12rem; color: var(--ink-soft); }
.req-read-ticks { margin: 0; padding-inline-start: var(--gap-4); }

.req-ticks { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--gap-1); }

/* ★★★ 053 · THE SERVICES GRID — Karim ③: «check the CSS in the services asked
   for, because it is broken as you can see». MEASURED before the fix, English,
   1440px, the five-form set (`documents/reports/065-walk/ticks.py`):

       every label box       16px tall AND 16px WIDE
       its <span>            24, 48 or 72px — the text, wrapped
       spilling              8 to 56px past the bottom of its own box
       and the grid rows are 20px apart, so a 72px label covered three of them

   ⛔⛔ AND THE CAUSE WAS NOT `align-items`. It was ONE CLASS NAME DOING TWO
     JOBS. `.req-tick` is the LIST's row-select checkbox — «a tick a finger can
     hit», `inline-size: 1rem; block-size: 1rem`, four sites in two files — and
     the services LABEL was wearing the same class. So a text label was being
     given the box of a checkbox: 16 by 16, and every word after the first
     wrapped out of it. `align-items: baseline` was drawn from that same
     collision and could not have fixed it — a 16px-tall box cannot hold a 72px
     span at ANY alignment.

   ★ SO THE LABEL GETS ITS OWN NAME and `.req-tick` goes back to meaning one
     thing. `029`'s rule about two definitions that drift, met in the form of
     two ELEMENTS answering to one name.

   ⚠ AND THE SELECTOR IS DESCENDED FROM `.req-ticks` DELIBERATELY. `.check-row`
     is declared TWICE in this file — line 964 and again at the print block —
     and the later one carries `align-items: baseline`. A bare `.req-tick-row`
     would lose to it on source order and the fix would silently not apply.
     Two classes beat one; the specificity is the point, not an accident. */
.req-ticks > .req-tick-row {
  align-items: flex-start;
  inline-size: auto;
  block-size: auto;
}

/* --- «before this can be sent» --------------------------------------------- */
/* ONLY WHAT IS MISSING — never a green tick for the fifteen things already
   answered. The panel is the visible reason the «send» button waits, so it is
   loud enough to be found and quiet enough not to look like a failure. */
.req-missing {
  border: var(--line-width) solid var(--warn);
  background: var(--warn-soft);
  border-radius: var(--radius);
  padding: var(--gap-3);
  margin-block-end: var(--gap-3);
}
.req-missing.is-none { border-color: var(--accent); background: var(--accent-soft); }
.req-missing h3 { margin-block-end: var(--gap-2); }
.req-missing-chips { display: flex; flex-wrap: wrap; gap: var(--gap-1); margin-block-end: var(--gap-2); }
.req-missing-chip {
  font-size: var(--size-1);
  padding: 1px var(--gap-2);
  border-radius: 999px;
  border-color: var(--warn);
  background: var(--paper);
  color: var(--warn);
}
.req-missing-chip:hover { background: var(--warn); color: var(--paper); }
.req-missing-list { margin: 0; padding-inline-start: var(--gap-4); }
.req-missing-list li { margin-block-end: 2px; }
.req-missing-list .req-because { margin-inline-start: var(--gap-2); }

/* --- picking the form ------------------------------------------------------ */
.req-form-pick { max-width: 52rem; }
.req-form-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--gap-3);
  margin-block-start: var(--gap-3);
}

/* --- 049 · the tick list ---------------------------------------------------
   Karim, 8 September 2026: «make it a check list». Six rows rather than six
   cards, because a person now ticks SEVERAL — and a grid of cards reads as
   «choose one» whatever the handler does.

   ⚠ A BLOCKED ROW IS DIMMED AND STILL READABLE. `opacity: .45` and not
     `display: none`: «IOR covers freight, clearance and trucking» is only an
     explanation if the thing it explains is still on the screen. The sentence
     under the list says why, and unticking IOR brings the rows back. */
.req-form-ticks {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  margin-block-start: var(--gap-3);
}
.req-form-tick {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: "box name" ". what";
  align-items: center;
  gap: var(--gap-1) var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
}
.req-form-tick input { grid-area: box; }
.req-form-tick-name { grid-area: name; font-weight: 600; }
.req-form-tick-what { grid-area: what; color: var(--ink-soft); font-size: var(--size-1); }
.req-form-tick.is-on { border-color: var(--accent); background: var(--accent-soft); }
.req-form-tick.is-blocked { opacity: .45; cursor: not-allowed; }
.req-form-why {
  margin-block-start: var(--gap-2);
  color: var(--ink-soft);
  font-size: var(--size-1);
}

.req-form-head { display: flex; align-items: center; gap: var(--gap-2); flex-wrap: wrap; }
.req-form-foot { display: flex; gap: var(--gap-2); flex-wrap: wrap; align-items: center; }
/* ⚠ `.btn-primary` CARRIES `width: 100%` — it was written for the login card,
   where it is the only control on the row. In a footer with three controls it
   forces a wrap and the disabled «send» renders as a full-width grey block that
   reads as a broken page. Seen in a browser at A4 width, not reasoned about.
   The override is SCOPED to these screens rather than changing the shared rule,
   which is the move `.app-main.is-logs .log-form .btn-primary` already makes. */
.req-form-foot .btn-primary,
.req-card-actions .btn-primary,
.req-panel .btn-primary,
/* ★ AND IN THE SLIDE-OVER — A, seen in a browser. `.detail-actions` puts two
   or three controls in a row and the primary one rendered as a full-width bar
   across the panel, which reads as a banner rather than as a button. Same
   defect, same remedy, one more scope. */
.chart-detail .btn-primary,
.form-card .btn-primary { width: auto; }
.req-form-foot > a { text-decoration: none; }

/* --- the three pickers ----------------------------------------------------- */
.req-picker { position: relative; }
.req-picker-value { display: flex; align-items: center; gap: var(--gap-2); flex-wrap: wrap; }
.req-picked { font-weight: 600; }
.req-picker-open { font-size: var(--size-1); padding: 2px var(--gap-2); }
/* THE PANEL FLOATS OVER THE FORM RATHER THAN LIVING IN ITS GRID CELL. Inside
   the cell it inherits a 14rem column, and the 403 explanation — which is a
   sentence, a permission name and a fallback list — wrapped to one word a line
   and ran off the bottom of the screen. Seen in a browser at the real width.
   `inset-inline-start: 0` and not `left`, so the Arabic build anchors it to the
   correct edge with no second rule. */
.req-picker-panel {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 100%;
  z-index: 5;
  min-width: min(26rem, 80vw);
  margin-block-start: var(--gap-1);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-drop);
  padding: var(--gap-2);
  max-height: 20rem;
  overflow-y: auto;
}
.req-picker-list { list-style: none; margin: var(--gap-1) 0 0; padding: 0; }
.req-picker-row {
  display: flex;
  justify-content: space-between;
  gap: var(--gap-2);
  width: 100%;
  text-align: start;
  border: none;
  background: none;
  padding: var(--gap-1) var(--gap-2);
}
.req-picker-row:hover { background: var(--accent-soft); }
/* ★ 048 · a group header inside the port picker's list — the country the term
   named, or «matches by name». Not a button: nothing happens when it is pressed,
   so it is not drawn as something that could be. */
.req-picker-group {
  display: flex;
  justify-content: space-between;
  gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-2) var(--gap-1);
  border-block-end: var(--line-width) solid var(--line);
  font-weight: 600;
}
.req-picker-group + .req-picker-group { margin-block-start: var(--gap-1); }
.req-picker-empty { display: block; padding: var(--gap-2); }
.req-picker-blocked { display: flex; flex-direction: column; gap: var(--gap-1); }
.req-picker-fallback { margin-block-start: var(--gap-2); border-block-start: var(--line-width) dashed var(--line); padding-block-start: var(--gap-2); }

/* --- the conversation ------------------------------------------------------ */
.req-thread { display: flex; flex-direction: column; gap: var(--gap-2); }
.req-message {
  border-inline-start: 3px solid var(--accent-soft);
  padding-inline-start: var(--gap-2);
}
.req-message-head { display: flex; gap: var(--gap-2); align-items: baseline; flex-wrap: wrap; }
.req-message-who { font-weight: 600; }
.req-message-body { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.req-composer { display: flex; flex-direction: column; gap: var(--gap-2); margin-block-start: var(--gap-2); }

/* --- 040 · the open list's revealed box, and the computed volume ------------
   ⛔ LOGICAL PROPERTIES ONLY — `check-css.sh`. No `margin-left`, no `float`,
      no `[dir="rtl"]`; the Arabic build flips itself.
   ★ THE REVEALED BOX SITS UNDER ITS OWN SELECT, in the same flex column the
     `.req-field` already is, so nothing has to be positioned. `hidden` is what
     hides it — the property, toggled from `syncOpenControls` — and this rule
     only gives it the gap a second control in one cell needs. */
.req-open-other { margin-block-start: var(--gap-1); }

/* The computed CBM line and its «use this» press, on one wrapping row under the
   volume box. It is EMPTY MARKUP painted once and written into with `setText`
   — the `paintBecause` pattern — so nothing here may depend on it having
   content. */
.req-cbm {
  display: flex;
  align-items: baseline;
  gap: var(--gap-2);
  flex-wrap: wrap;
  margin-block-start: 2px;
}
.req-cbm .field-hint { color: var(--accent); }

/* =============================================================================
   ★★ 050 · THE MEASUREMENT AND ITS UNIT · Karim ⑤
   =============================================================================
   ONE unit control per GROUP, on the group's first shown field, and a read-only
   tag on every other member — so the unit is readable beside every number and
   settable in one place. The number keeps the width it had; the unit takes what
   it needs. ⛔ LOGICAL PROPERTIES ONLY — `check-css.sh`. */
.req-measure { display: flex; align-items: center; gap: var(--gap-1); }
.req-measure .input { flex: 1 1 auto; min-width: 0; }
.req-unit { flex: 0 0 auto; width: auto; }
.req-unit-tag {
  flex: 0 0 auto;
  color: var(--ink-soft);
  padding-inline: var(--gap-1);
  white-space: nowrap;
}

/* ★ THE ROW THE ARROW KEYS ARE ON. It is the same paint as `:hover`, because
   they mean the same thing — «this is the one Enter takes» — and two different
   paints for one state is a person learning two rules. */
.req-picker-row.is-active { background: var(--accent-soft); }

/* ★ A SENTENCE SLOT THAT IS EMPTY TAKES NO ROOM. `[data-said]` and the picker's
   own `[data-list-said]` are ALWAYS in the document so a caller can write into
   them with `setText` at any moment without having to know to unhide them — a
   slot that has to be revealed as well as filled is a slot somebody eventually
   fills and leaves invisible. */
[data-said]:empty,
[data-list-said]:empty { display: none; }

/* =============================================================================
   ★★★ 050 · A FIELD THAT IS MISSING · Karim ⑧
   =============================================================================
   «highlight their boxes in red so it be easier and better — and this is the
   right behavior to be in the UX».

   ⛔ COLOUR IS NEVER THE ONLY SIGNAL. A red border to somebody colour-blind is
      a border. Three signals from one sentence, and this file paints one of
      them: `aria-invalid` is what a screen reader says, `.req-invalid` below is
      what an eye reads, and the panel above the form is the third.

   ⛔ AND THE COLOUR IS `--stop`, WHICH ALREADY EXISTS. `--warn` is amber and
      means «worth noticing»; this is a refusal. No new colour is born here —
      `base.css`'s token block is the one place a colour is born.

   ⛔ LOGICAL PROPERTIES. `border-inline-start`, never `border-left`: this form
      is Arabic first and a left border sits on the wrong side of every box in
      the build twenty-two people use. */
.req-field.is-invalid {
  border-inline-start: 3px solid var(--stop);
  padding-inline-start: var(--gap-2);
}
.req-field.is-invalid .input,
.req-field.is-invalid .req-picker {
  border-color: var(--stop);
}
.req-field.is-invalid .field-label { color: var(--stop); }
.req-invalid { color: var(--stop); font-weight: 600; }
/* ★ AND THE TICK LIST, WHICH IS A SECTION RATHER THAN A FIELD. It is one of
   the fourteen things an empty IOR form is missing — `fn_request_needs_a_
   service` refuses without it — so it goes red with the rest or the count on
   the screen and the count of red boxes disagree. */
.req-section.is-invalid {
  border-inline-start: 3px solid var(--stop);
  padding-inline-start: var(--gap-2);
}
.req-section.is-invalid .req-section-head { color: var(--stop); }

/* --- 040 · the attachments ------------------------------------------------- */
.req-docs { list-style: none; margin: 0; padding: 0;
            display: flex; flex-direction: column; gap: var(--gap-2); }
.req-doc {
  border-inline-start: 3px solid var(--accent-soft);
  padding-inline-start: var(--gap-2);
  display: flex; flex-direction: column; gap: 2px;
}
.req-doc-name { display: flex; align-items: baseline; gap: var(--gap-2); flex-wrap: wrap; }
/* A long filename WRAPS rather than pushing the panel wider — `overflow-wrap:
   anywhere`, the same rule `.req-message-body` carries, because both hold text
   somebody else chose the length of. */
.req-doc-name .link-button { text-align: start; overflow-wrap: anywhere; }
.req-doc-facts { display: flex; gap: var(--gap-2); flex-wrap: wrap; font-size: var(--size-1); }
.req-doc-add { display: flex; flex-direction: column; gap: var(--gap-2); margin-block-start: var(--gap-2); }

.req-audit { display: flex; flex-direction: column; gap: var(--gap-1); margin-block-start: var(--gap-2); }
.req-audit-row { display: flex; gap: var(--gap-2); font-size: var(--size-1); flex-wrap: wrap; }

.req-recent-list { list-style: none; margin: 0; padding: 0; }
.req-recent-list a { display: flex; gap: var(--gap-2); align-items: center; padding: 2px 0; flex-wrap: wrap; }

/* =============================================================================
   ★★ THE PRINT — one of exactly TWO printable documents in this system
   =============================================================================
   Karim, 5 September 2026: the request (this) and the final quotation (prompt
   18). There is no third, and a Print button anywhere else is a defect.

       THE COST NEVER APPEARS ON A PRINTED PAGE. ANYWHERE. EVER.

   On the request that costs nothing to honour: there is no cost column on
   `requests` at all — `charge_lines` hangs off a route, which hangs off a
   quotation. `shipment_value` is the CLIENT's declared cargo value, asked for
   on the IOR sheet because customs and insurance need it; it is the client's
   number and not Nile's. `tools/check-print.py` reads this block and fails if a
   cost field is ever named in it.

   IT IS `@media print` OVER THE SAME DOM — not a second renderer, not a
   template, not a PDF service. What the person is looking at is what comes out,
   minus the controls.

   THE REQUEST IS INTERNAL BY DEFINITION — Nile asking itself a question — so
   every sheet carries the word, who printed it and when. */
.req-print-head, .req-print-foot, .req-print-title { display: none; }
/* ★ S §5 · THE FIRST IMAGE THIS PROJECT HAS EVER SHIPPED, and it is an `<img>`
   rather than a `background-image` FOR THE PRINT'S SAKE. Chrome prints a
   background only when the person ticks «Background graphics» in the dialog; an
   `<img>` prints always. A logo that appears on the screen and is missing from
   the PDF is the defect a client sees first, and it would have been invisible
   in development. `frontend/images/nile-logo.png`, and `img-src 'self'` admits
   it.

   ★ ONE DEFINITION FOR BOTH DOCUMENTS, and outside the print block, because the
     offer's sheet is on the SCREEN as well as on paper and both want the same
     mark at the same size. 11mm is the height at which the wordmark is still
     readable in a stapled corner; the file is 560 × 105, so it is drawn at
     about a third of its pixels and its edges do not show. */
.req-print-logo, .offer-logo { block-size: 11mm; inline-size: auto; }

@page {
  size: A4;
  /* ⚠⚠ THESE WERE 22mm/20mm AND THE REASON THEY WERE IS GONE — S §7 step 1.
     They were deep to make room for two strips lifted OUT of the page by a
     negative inset, and that technique does not do what this file said it did.
     Measured on `REQ-2026-0022` by printing it to PDF and OPENING the PDF:

         negative insets   the head printed at the BOTTOM of sheet 1, the foot
                           at the TOP of sheet 2 printing THROUGH the word
                           CARGO, and a one-page request grew an EMPTY SECOND
                           SHEET carrying nothing but the foot
         zero insets       both strips on EVERY sheet, which is what Karim
                           asked for and what the old comment claimed

     ⟹ So the strips sit INSIDE the page content box at inset 0, and the room
       they need is taken out of the flow below instead of out of the margin.
       14mm is a true margin all round. */
  margin: 14mm;
}

@media print {
  /* The shell's chrome, the controls, and every panel that only makes sense to
     somebody who can press something. */
  .app-header,
  .sidebar,
  /* ★ A `position: fixed` box REPEATS ON EVERY SHEET — that is the technique
     the two strips below rely on, and it is a defect for anything else. The
     slide-over is fixed to the window, so a request printed with a detail panel
     open would carry that panel on every page of the print. */
  .chart-detail,
  .req-card-actions,
  #req-actions,
  .req-composer,
  .req-picker-open,
  .search-box,
  .chip-row,
  .req-lane-strip,
  .filter-bar,
  .applied-strip,
  /* =========================================================================
     ★★ S §2② · THE FOUR BLOCKS THAT COME OFF THE PAPER — Karim's note ⑧
     =========================================================================
     «also remove the part of movements and files and document type and
      conversation from the print· maybe a manager say to them print that
      request and bring it to me, so it should look professional.»

     ⛔⛔ HIDDEN FROM THE PRINT, NOT REMOVED FROM THE SCREEN. All four are how
        somebody working a request actually works it: the movements say when it
        moved, the attachments are the packing list, the picker is how the next
        one is added, and the conversation is where operations asked its
        question. A person reading this request on a screen needs every one of
        them. What a manager holding a sheet of paper needs is the SHIPMENT.

     ★ AND THE DOCUMENT-TYPE PICKER IS INSIDE `#req-documents`, so it comes off
       with the attachments rather than needing its own selector — measured on
       the markup (`.req-doc-add` is a child of that section), not assumed.

     ⚠ `#req-doc-outcome` IS A SIBLING of that section and not inside it — see
       `paintView`'s own note about why it lives outside the box that repaints —
       so it is named here too. A confirmation about a file upload is not on a
       manager's sheet either. */
  #req-movements,
  #req-documents,
  #req-doc-outcome,
  #req-conversation,
  .charge-pager { display: none !important; }

  /* THE SHELL'S HEIGHT IS UNDONE FOR PAPER. `.app` carries `height: 100dvh` and
     every scrolling pane declares `overflow-y: auto`; on a printer both mean
     «show one screen and drop the rest», which is how a long request loses its
     last two sections silently. */
  html, body { background: #fff; }
  body { padding: 0; font-size: 10.5pt; }
  /* Nothing sticks on paper. A sticky box in a print context is at best
     ignored and at worst printed once at the top of every sheet. */
  .list-top, .log-head, .panel-head { position: static; }
  .app, .app-columns, .app-main, .req-card {
    display: block;
    height: auto;
    max-height: none;
    overflow: visible;
    padding: 0;
    margin: 0;
    max-width: none;
  }

  /* =========================================================================
     ★★ S §2③ · IT CARRIES THE LOOK — «it should look professional»
     =========================================================================
     The title block is the sheet's letterhead: the real logo on the start side,
     the document's name on the end side, and a two-tone rule under both in the
     brand's own cyan and lime. It is the same shape the client's quotation
     takes, one rank quieter, so a manager holding both recognises them as one
     company's paper.

     ⛔ AND IT IS STILL THE INTERNAL DOCUMENT. Looking like Nile is not the same
       as being the client's copy — which is exactly what the stamp on every
       sheet below is for. */
  .req-print-title {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--gap-3);
    flex-wrap: wrap;
    margin-block-end: var(--gap-3);
    padding-block-end: 2mm;
    border-block-end: 2pt solid var(--nile-green);
  }
  .req-print-title h1 { margin: 0; font-size: 16pt; color: var(--nile-blue); }
  .req-print-brand { display: flex; flex-direction: column; gap: 1mm; }
  .req-print-tag {
    font-size: 7.5pt;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--doc-ink-faint);
  }
  .req-print-name { display: flex; flex-direction: column; align-items: flex-end; }

  /* =========================================================================
     ★★★ S §2① · ON EVERY SHEET — «not on every page» was Karim's complaint
     =========================================================================
     `position: fixed` inside a print context repeats on each page. A stapled
     request that loses its first sheet must not become a sheet about nobody,
     and the word INTERNAL must be on the page somebody actually picks up.

     ⚠⚠ AND THE INSETS ARE ZERO, WHICH IS A CORRECTION. They were −14mm and
       −12mm, with a comment saying a negative offset «lifts each strip into the
       page MARGIN … so it repeats on every sheet and collides with nothing on
       any of them». THAT IS NOT WHAT IT DOES. Printed to PDF and read back out
       of the file, `REQ-2026-0022`, 9 September 2026:

           head    sheet 1 only, drawn at the BOTTOM of it
           foot    sheet 2 only, drawn at the TOP of it, THROUGH the word CARGO
           and a one-page request grew an EMPTY SECOND SHEET carrying only the
           foot — so «the stamp is on every sheet» was true by accident, one
           different strip per sheet

       Chrome CLIPS a `position: fixed` box to the page content area in print.
       A negative inset does not move it into the margin; it moves it out of
       sight and the box is then laid out once against the document rather than
       once per page. `transform: translateY` was tried for the same purpose and
       is clipped identically — measured, not reasoned about.

     ⟹ SO THE ROOM COMES OUT OF THE FLOW INSTEAD. At inset 0 both strips repeat
       on every sheet, and the two rules below give the content the 7–9mm it
       needs so that nothing is ever printed underneath them:

           .req-card                 padding-block   — sheet 1's letterhead and
                                                       the last sheet's tail
           .req-section / .req-panel padding-block-start — a panel that lands at
                                                       the top of a sheet keeps
                                                       its heading clear

     ⛔ AND `display: table-header-group` WAS TRIED FIRST AND REJECTED, because
       it is the textbook answer and it did not work here: `.req-card` made into
       a table has ANONYMOUS rows, and Chrome repeated neither the header group
       nor the footer group over them. Recorded so nobody spends the afternoon
       on it twice. */
  .req-print-head, .req-print-foot {
    display: flex;
    align-items: center;
    position: fixed;
    inset-inline: 0;
    gap: var(--gap-3);
    font-size: 7.5pt;
    color: var(--doc-ink);
    background: #fff;
  }
  .req-print-head {
    inset-block-start: 0;
    border-block-end: 1pt solid var(--nile-green);
    padding-block-end: 1mm;
  }
  .req-print-foot {
    inset-block-end: 0;
    border-block-start: 1pt solid var(--nile-green);
    padding-block-start: 1mm;
    justify-content: space-between;
  }
  /* =========================================================================
     ★★ S §2① · THE STAMP IS COLOURED — «not on every page» was the complaint
     =========================================================================
     Karim, note ⑧: «when I click on print you only write on the top internal
     not for client but not on every page — and make it coloured».

     ★ IT WAS ALREADY ON EVERY PAGE and it was BLACK, which is why it read as a
       caption. `.req-print-head` and `.req-print-foot` are `position: fixed`
       and that repeats them on every sheet — the technique this block's own
       comment names, twelve lines up. What was missing was that a warning has
       to LOOK like one: black on white beside a black rule is indistinguishable
       from the page furniture around it.

     ⛔⛔ AND `print-color-adjust: exact` IS LOAD-BEARING, NOT A POLISH. Chrome
        drops every background fill from a print unless the person ticks
        «Background graphics» in the dialog — so a red-tinted stamp would print
        as bare text on most of the office's machines and the colour Karim asked
        for would exist only on this laptop. `exact` overrides that per element.
        Written for both spellings because Nile runs Chrome and Edge on Windows
        10 and 11 and neither is pinned to a version — the rule the `break-inside`
        pair below follows for the same reason.

     ⚠ AND IT IS APPLIED TO THE STAMP AND NOT TO THE PAGE. A blanket
       `print-color-adjust: exact` on `body` would force every tint in the card
       onto the paper — the state chips, the warning panels — and turn an
       internal sheet into a colour print job. */
  .req-print-internal {
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    font-size: 7pt;
    color: var(--stop);
    border: 1.5pt solid var(--stop);
    background: var(--stop-soft);
    border-radius: 1mm;
    padding: 0.5mm 2mm;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* NO PAGE BREAK INSIDE A FIELD GROUP. `break-inside` is the current property
     and `page-break-inside` is what older engines read; both are written
     because Nile runs Chrome and Edge on Windows 10 and 11 and neither is
     pinned to a version. */
  /* ★★ S §2① · AND THIS IS WHERE THE ROOM FOR THE TWO STRIPS COMES FROM.
     `break-inside: avoid` makes every panel atomic, so a panel that does not
     fit is moved WHOLE to the next sheet — and it then starts at the very top
     of the content box, which is where the repeating head strip is. Watched on
     sheet 2 of `REQ-2026-0022`: the word CARGO printed underneath the stamp and
     the panel read as one with no heading.

     7mm is the strip's own height plus a millimetre. It costs 7mm of white
     above every panel heading on paper, which reads as a document rather than
     as a screenshot, and it is the only per-page reservation CSS offers when
     the running head is `position: fixed`. */
  .req-section, .req-panel, .req-missing, .req-message, .req-card-head {
    page-break-inside: avoid;
    break-inside: avoid;
    border-color: #999;
    padding-block-start: 7mm;
  }
  /* Sheet 1's letterhead and the last sheet's tail, clear of the same strips.
     `.req-card` is reset to `padding: 0` in the block above — this is the one
     place it takes any back. */
  .req-card { padding-block: 9mm; }
  .req-card-body { display: block; }
  .req-card-side { margin-block-start: var(--gap-3); }
  .req-fields, .req-card-facts { break-inside: auto; }

  /* A LINK ON PAPER IS NOT A LINK. The «what is missing» list is printed —
     a salesman taking a half-filled request to a meeting is carrying the list
     of things to ask about — but its buttons are read as text. */
  .req-missing button, .link-button {
    border: none;
    background: none;
    padding: 0;
    color: #000;
    text-decoration: none;
    font: inherit;
  }
  .req-state, .req-age, .req-tag {
    border: 1pt solid #666;
    background: none;
    color: #000;
  }
  a { color: #000; text-decoration: none; }
}

/* =============================================================================
   041 · THE QUOTATION SCREENS — the three lists, the offer's page, module 200
   =============================================================================
   ⛔ LOGICAL PROPERTIES ONLY. `inset-inline`, `padding-inline`, `margin-block`,
      `text-align: start` — never `left`, `right`, `margin-left`. Doctrine
      §12.1: the direction is a property of the DOCUMENT, set once, and every
      rule below mirrors for Arabic without a second stylesheet.

   ⛔ AND NOTHING HERE INVENTS A COLOUR. Every value is a token from `:root` —
      A settled the palette and Karim signed it off looking at it, and §9.4
      forbids revisiting it. A new hex code in this block would be a fourth
      accent nobody chose.
   ============================================================================= */

/* -- the three lists · one grid, and the heading row uses the SAME one so the
      two line up because they are one definition rather than two lists of
      widths that drift. `029`'s rule, and the request lists' own. --------- */
.quo-screen {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  padding-block: 0 var(--gap-4);
}
.quo-head { display: flex; flex-direction: column; gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-4); }
.quo-columns { padding-inline: var(--gap-4); background: var(--paper); }
.quo-columns .quo-row.is-head { padding-block: var(--gap-1); }
/* ⛔ NO `overflow-y` — the PAGE scrolls. A pane that scrolls inside a page that
      also scrolls is the two-scrollbar defect `042` measured and removed. */
.quo-scroll { padding-inline: var(--gap-4); }

.quo-row {
  display: grid;
  /* ⚠⚠ THE STAGE TRACK IS 11rem AND IT WAS 9rem, AND THE DIFFERENCE WAS
     MEASURED RATHER THAN FELT — §9.1's standard. On a real row at 1536 CSS px:

         stage cell            144px
         «costed» chip          72px
         «Waiting for approval» 144px wide and **47px tall — it WRAPPED**
         the whole row          93px
         rows visible below the chrome   5

     A quotation list where five rows fit is `A`'s Charge Types finding on a
     new screen. The label became one word — «Waiting» / «ينتظر» — and the
     track 11rem, so the two chips sit side by side on one line. Re-measured
     after: see the report. */
  grid-template-columns:
    11rem                     /* stage    */
    minmax(10rem, 1.6fr)      /* client   */
    minmax(9rem, 1.3fr)       /* service  */
    4rem                      /* routes   */
    minmax(7rem, 0.9fr)       /* cost     */
    minmax(7rem, 0.9fr)       /* sell     */
    7rem;                     /* expires  */
  align-items: start;
  gap: var(--gap-2);
  padding: var(--gap-2);
  text-decoration: none;
  color: inherit;
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-1);
}
/* ★ THE CLOSED AND PRICING SCREENS BOTH SHOW THE COST, so one grid serves all
      three. The screen that ever drops a column drops the CELL too — a grid
      with an empty track is a column of white nobody can name. */
.quo-row.is-head { color: var(--ink-faint); font-weight: 600; }
.quo-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.quo-cell > span { overflow-wrap: anywhere; }
.quo-cell.is-stage { flex-direction: row; flex-wrap: wrap; gap: 3px;
  align-content: start; }
.quo-cell.is-routes, .quo-cell.is-expiry { align-items: center; }
/* ★ THE MONEY IS END-ALIGNED AND MONOSPACED so two amounts under each other
      line up on the decimal point — which is the only reason a column of
      figures is readable at all. `text-align: end` mirrors for Arabic. */
.quo-cell.is-money { align-items: flex-end; text-align: end; }
.quo-money-line { display: flex; gap: var(--gap-1); align-items: baseline;
  justify-content: flex-end; }
.quo-amount { font-variant-numeric: tabular-nums; }
.quo-ccy { color: var(--ink-faint); font-size: var(--size-1); }

/* ★ 052 · the quotation head row's flex wrapper — `.req-headline`'s twin, and
   the tick cell itself is `.req-pick` on both lists rather than a second class
   with the same rules. §9.2 item ④: two controls that do the same job must not
   look different. */
.quo-headline { display: flex; align-items: stretch; }
.quo-headline > .quo-row { flex: 1; min-width: 0; }

.quo-line { display: flex; align-items: stretch; background: var(--paper); }
.quo-line:hover { background: var(--paper-soft); }
.quo-line > .quo-row { flex: 1; min-width: 0; }
.quo-line.is-off { opacity: 0.55; }
.quo-client-name, .quo-service { font-weight: 600; }
.quo-code, .quo-origin { color: var(--ink-faint); font-size: var(--size-1); }
.quo-origin.is-direct { font-style: italic; }

/* ★ THE EXPIRY CHIPS. Three states and they are meant to look different — an
      offer lapsing on Thursday is what the pricing screen exists to surface. */
.chip.is-calm { background: var(--paper-soft); color: var(--ink-soft); }
.chip.is-warm { background: var(--warn-soft); color: var(--warn); }
.chip.is-lapsed { background: var(--stop-soft); color: var(--stop); }
.chip.is-waiting { background: var(--accent-soft); color: var(--accent); }
.chip.is-approved { background: var(--accent-soft); color: var(--accent); }
.chip.is-accepted { background: var(--accent-soft); color: var(--accent); }
.chip.is-locked { background: var(--paper-soft); color: var(--ink-faint); }
.chip.is-role, .chip.is-carried { background: var(--paper-soft); color: var(--ink-soft); }

.quo-loading .quo-row.is-skeleton { display: block; padding: var(--gap-3) var(--gap-2); }

/* -- the offer's own page ------------------------------------------------- */
.quo-card-screen {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  padding-block: 0 var(--gap-4);
}
.quo-card-scroll { padding-inline: var(--gap-4); }
.quo-card-head { display: flex; flex-direction: column; gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-4) 0; }
.quo-card-facts { display: flex; flex-wrap: wrap; gap: var(--gap-3);
  font-size: var(--size-1); }
.fact { display: flex; gap: var(--gap-1); align-items: baseline; }

.quo-tab { display: flex; flex-direction: column; gap: var(--gap-3);
  padding-block: var(--gap-3); }

/* ★ THE APPROVAL BANNER IS THE FIRST THING ON THE FIRST TAB, because it is the
      answer to «why will Send not press». */
.quo-approval { display: flex; flex-direction: column; gap: var(--gap-1);
  align-items: flex-start; }

.route-strip { display: flex; flex-wrap: wrap; gap: var(--gap-2);
  align-items: stretch; }
.route-card { border: var(--line-width) solid var(--line);
  border-radius: var(--radius); background: var(--paper); }
.route-card.is-on { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.route-card.is-off { opacity: 0.55; }
.route-card-head {
  display: flex; flex-wrap: wrap; gap: var(--gap-2); align-items: center;
  padding: var(--gap-2); background: none; border: none; cursor: pointer;
  font: inherit; color: inherit; text-align: start; inline-size: 100%;
}
.route-card-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.route-no { font-weight: 700; }
.route-detail { display: flex; flex-direction: column; gap: var(--gap-2); }
.route-brief { display: flex; flex-wrap: wrap; gap: var(--gap-2);
  align-items: baseline; padding-block: var(--gap-1);
  border-block-end: var(--line-width) solid var(--line); }
.override-block, .money-block { display: flex;
  flex-direction: column; gap: var(--gap-1); }
/* ★★ ONE BLOCK PER OPTION ON THE APPROVALS CARD — C2 §3/D3. A route is a
      PRICED OPTION and the client takes exactly one, so the frozen figures are
      grouped under the option they belong to and NOTHING is added across them.
      The rule down the leading edge is what makes «these three numbers belong
      together» visible without a box for every option. */
.money-option { display: flex; flex-direction: column;
  border-inline-start: 3px solid var(--line); padding-inline-start: var(--gap-2); }
.money-option + .money-option { margin-block-start: var(--gap-2); }
.money-option .route-brief { border-block-end: none; font-weight: 600; }
.chip.is-faint { background: var(--paper-inset); color: var(--ink-faint); }

/* ★★ THE THREE WEIGHTS, AND THE CHARGED ONE IS MARKED. A number a client is
      billed on that appears without its two inputs is a number nobody can
      check — C §5. */
.weights-block { display: flex; flex-wrap: wrap; gap: var(--gap-3);
  align-items: baseline; font-size: var(--size-1); }
.weight { display: flex; gap: var(--gap-1); align-items: baseline; }
.weight.is-charged { font-weight: 700; }
.weight.is-charged::after { content: "★"; color: var(--accent); }
.weight.is-total { border-inline-start: 2px solid var(--accent);
  padding-inline-start: var(--gap-2); }

/* -- the charge sheet ----------------------------------------------------- */
.charge-sheet { display: flex; flex-direction: column; gap: var(--gap-1); }
/* ⛔ THE GRID IS ONE DEFINITION AND THE HEAD ROW SHARES IT. */
.charge-sheet .charge-row {
  display: grid;
  grid-template-columns:
    3rem                      /* order    */
    minmax(10rem, 2fr)        /* what     */
    4.5rem                    /* qty      */
    minmax(7rem, 1fr)         /* cost     */
    minmax(6rem, 0.8fr)       /* profit   */
    minmax(7rem, 1fr)         /* sell     */
    4rem                      /* currency */
    minmax(6rem, 0.8fr);      /* do       */
  gap: var(--gap-2);
  align-items: start;
  padding: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-1);
}
.charge-sheet .charge-row.is-head { color: var(--ink-faint); font-weight: 600; }
.charge-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.charge-cell > span { overflow-wrap: anywhere; }
.charge-cell.is-cost, .charge-cell.is-sell, .charge-cell.is-margin,
.charge-cell.is-qty { align-items: flex-end; text-align: end;
  font-variant-numeric: tabular-nums; }
.charge-cell.is-what { gap: 3px; }
/* ★★ TWO BANDS AND THEY LOOK DIFFERENT — `18 §٣.٢`. A pass-through line is the
      client's money moving through Nile; it is not Nile revenue and it must not
      read like one. */
/* ★ AND A RULE DOWN ITS LEADING EDGE, not only a tint. `18 §٣.٢` says the two
   bands «look different», and on a real IOR shipment EIGHT of twelve lines are
   pass-through — a tint alone over two thirds of the sheet stops being a
   signal. `border-inline-start` mirrors for Arabic; `border-left` would not. */
.charge-row.is-band-pass_through {
  background: var(--band-pass-soft);
  border-inline-start: 3px solid var(--band-pass-line);
}
.charge-row.is-band-pass_through .charge-cell.is-sell { color: var(--ink-faint); }
.chip.is-band-pass_through { background: var(--band-pass-soft); color: var(--band-pass); }
.chip.is-band-nile_service { background: var(--accent-soft); color: var(--accent); }
.chip.is-band-refundable { background: var(--warn-soft); color: var(--warn); }
.charge-rule { color: var(--ink-faint); font-size: var(--size-1); }
.mode-tag { font-size: var(--size-1); color: var(--ink-faint); }
/* ★ AN OVERRIDDEN LINE IS THE ONE AN APPROVER MUST SEE. */
.mode-tag.is-overridden { color: var(--warn); font-weight: 600; }
.mode-tag.is-overridden .faint { display: block; font-weight: 400; }

/* -- the money block · one row per currency, every percentage named ------- */
.money-row {
  display: flex; flex-wrap: wrap; gap: var(--gap-3); align-items: baseline;
  padding: var(--gap-2); border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-1);
}
.money-ccy { font-weight: 700; min-inline-size: 3rem; }
.money-figure { display: flex; gap: var(--gap-1); align-items: baseline;
  font-variant-numeric: tabular-nums; }
.money-label { color: var(--ink-faint); font-size: var(--size-1); }

/* -- the overridden lines, on the approvals card -------------------------- */
.override-row { display: flex; flex-wrap: wrap; gap: var(--gap-2);
  align-items: baseline; padding-block: var(--gap-1);
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-1); }
.override-where { display: flex; gap: var(--gap-1); align-items: baseline;
  flex-wrap: wrap; }
.override-name { font-weight: 600; }
.override-side { display: flex; gap: var(--gap-1); align-items: baseline; }
.override-was { color: var(--ink-faint); text-decoration: line-through; }
.override-now { font-weight: 600; }
.override-rule, .override-ccy { color: var(--ink-faint); font-size: var(--size-1); }

/* -- the parties and documents tabs --------------------------------------- */
.party-row, .doc-row { display: flex; flex-wrap: wrap; gap: var(--gap-2);
  align-items: baseline; padding: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-1); }
.party-name, .doc-name { font-weight: 600; }
.check-grid { display: flex; flex-direction: column; gap: var(--gap-1); }
.check-row { display: flex; gap: var(--gap-2); align-items: baseline; }
.stamp-block, .terms-block { display: flex; flex-direction: column;
  gap: var(--gap-2); padding-block-start: var(--gap-3);
  border-block-start: var(--line-width) solid var(--line); }
.terms-text { white-space: pre-wrap; font: inherit; font-size: var(--size-1);
  background: var(--paper-soft); padding: var(--gap-2);
  border-radius: var(--radius); margin: 0; }

/* =============================================================================
   MODULE 200 · TWO PANES, AND THE REASON IS §9.2 ITEM ①
   =============================================================================
   Karim's own words: «if i scrolled down and i clicked on something it open to
   me its informations but i can't see, i have to go all the way up». A
   list-then-card layout makes a manager scroll back after every decision. */
.apr-screen { display: flex; flex-direction: column; gap: var(--gap-2);
  padding-block: 0 var(--gap-4); }
.apr-panes {
  display: grid;
  grid-template-columns: minmax(20rem, 1fr) minmax(24rem, 1.4fr);
  gap: var(--gap-3);
  padding-inline: var(--gap-4);
  align-items: start;
}
/* ⛔ AND IT COLLAPSES TO ONE COLUMN RATHER THAN SCROLLING SIDEWAYS. §9.2 item
      ⑥: nothing on this page body may need horizontal scrolling. */
@media (max-width: 60rem) {
  .apr-panes { grid-template-columns: 1fr; }
}
.apr-inbox { display: flex; flex-direction: column; min-inline-size: 0; }
.apr-side { border: var(--line-width) solid var(--line);
  border-radius: var(--radius); padding: var(--gap-3);
  background: var(--paper); min-inline-size: 0; }
.apr-row {
  display: grid;
  grid-template-columns: 7.5rem minmax(9rem, 1.4fr) minmax(7rem, 1fr) minmax(8rem, 1fr);
  gap: var(--gap-2);
  align-items: start;
  inline-size: 100%;
  padding: var(--gap-2);
  border: none;
  border-block-end: var(--line-width) solid var(--line);
  background: none;
  font: inherit;
  font-size: var(--size-1);
  color: inherit;
  text-align: start;
  cursor: pointer;
}
.apr-row.is-head { color: var(--ink-faint); font-weight: 600; cursor: default; }
.apr-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.apr-line.is-on .apr-row { background: var(--accent-soft); }
.apr-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.apr-cell.is-money { align-items: flex-end; text-align: end; }
.apr-client, .apr-asker { font-weight: 600; }
.apr-code { color: var(--ink-faint); font-size: var(--size-1); }
.apr-detail { display: flex; flex-direction: column; gap: var(--gap-3); }
.apr-reason { display: flex; flex-direction: column; gap: var(--gap-1); }
.apr-reason-body { margin: 0; font-size: var(--size-1); }
.apr-note { font-size: var(--size-1); }
.apr-decide { display: flex; flex-direction: column; gap: var(--gap-2);
  padding-block-start: var(--gap-3);
  border-block-start: var(--line-width) solid var(--line); }
.apr-loading .apr-row.is-skeleton { display: block; padding: var(--gap-3); }

/* =============================================================================
   ★★ THE PRINTED OFFER — the second of the exactly two printable documents
   =============================================================================
   ⛔⛔ THERE IS NO COST RULE IN THIS BLOCK, AND THERE MUST NEVER BE ONE. A
      `@media print` rule that un-hid a cost column would be the exact accident
      `tools/check-print.py` check 1 exists to stop, and it reads this file.
      **The cost is not on the page because it never reaches the browser** —
      `GET /quotations/{id}/offer` answers with sell-only models.

   =============================================================================
   ★★★ S §1 · AND THE SHAPE IS NOT INVENTED — KARIM ALREADY ISSUES IT
   =============================================================================
   «the template of the quotation we do it before — that was what it look· you
    only take the logo and ignore the rest of the pdf»

   Every measurement below was read off `planning/quotaion part/final
   quotation/Quotations.pdf` — the document Nile sends clients today — and drawn
   back with his corrections in the panel «The Nile quotation» of the 9 September
   canvas. ⛔ IT IS NOT A NEW DESIGN. The job here was to make the system produce
   the document the company already has, which is why nothing below is a taste
   argument and every colour is a token measured off the artwork.

   THE ORDER ON THE PAGE, and it is the order of the paper:

       the logo, top-start          ·  QUOTATION, letterspaced, top-end
       the meta column              ·  date · number · customer id · valid until
       a rule
       THREE PARTIES SIDE BY SIDE   ·  client · supplier · salesperson
       a tinted band                ·  services · currency · cargo value
       a bordered spec grid         ·  twelve cells about the shipment
       the charge table, grouped, with a small grey sub-line where one is needed
       totals BY CURRENCY, the unused ones greyed at zero
       the numbered terms
       the bilingual footer

   ⛔ LOGICAL PROPERTIES ONLY, as everywhere in this file. `check-css.sh` is the
      guard and this document is issued in BOTH languages — the Arabic sheet is
      this one mirrored, not a second stylesheet.

   ★ AND IT IS THE SAME RULES ON SCREEN AND ON PAPER. What operations and sales
     look at before they send it IS the sheet; `@media print` at the end of this
     block removes the chrome around it and changes nothing inside it. A print
     that is a second rendering is a print that can disagree with what was
     approved. */
.offer-toolbar { display: flex; flex-wrap: wrap; gap: var(--gap-3);
  align-items: center; justify-content: space-between; }
.offer-sheet {
  position: relative;
  overflow: hidden;
  background: var(--paper);
  color: var(--doc-ink);
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  /* A4 at a readable width on screen. The print rules below take over on
     paper, where `@page` decides the width. */
  max-inline-size: 48rem;
  font-size: var(--size-1);
  line-height: 1.45;
}
/* ★ THE TWO-TONE RAIL — the cyan and the lime of the mark, standing on end. It
   is the one place the lime appears at full strength, and it carries no text:
   its contrast on white is poor and the brand file says so outright. */
.offer-rail {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 7px;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}
.offer-rail i { display: block; block-size: 52%; background: var(--nile-lime); }
.offer-rail i + i { block-size: 48%; background: var(--nile-cyan); }
.offer-in { padding: var(--gap-4) var(--gap-4) 0; padding-inline-start: var(--gap-5); }

.offer-head { display: flex; flex-wrap: wrap; gap: var(--gap-4);
  justify-content: space-between; align-items: flex-start; }
.offer-brand { display: flex; flex-direction: column; gap: var(--gap-1); }
.offer-tag {
  font-size: var(--size-0);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--doc-ink-faint);
}
.offer-title { display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--gap-2); }
/* ⛔ THE WORD IS LETTERSPACED AND NOT BOLD — it is on the issued document that
      way, and a bold QUOTATION competes with the client's own name below it. */
.offer-word {
  font-size: var(--size-4);
  letter-spacing: 0.22em;
  color: var(--nile-green);
  text-align: end;
}
.offer-meta { display: flex; flex-direction: column; gap: 2px; margin: 0; }
.offer-meta div { display: flex; gap: var(--gap-3); justify-content: flex-end; }
.offer-meta dt { color: var(--doc-ink-faint); }
/* ★ THE FOUR META VALUES ARE AMBER, and that is the issued document's own
     signal: they are the four things somebody looks for when they pick the
     sheet up — what it is, when, for whom, and until when. */
.offer-meta dd {
  margin: 0;
  color: var(--nile-amber);
  font-weight: 600;
  font-family: var(--font-mono);
  min-inline-size: 8rem;
  text-align: end;
}
.offer-rule { block-size: 2px; background: var(--nile-green);
  margin-block: var(--gap-3);
  -webkit-print-color-adjust: exact; print-color-adjust: exact; }

/* ---- the three parties ------------------------------------------------------
   ⛔ THEY STAY SIDE BY SIDE AT EVERY WIDTH — Karim, 9 September. They are three
      short names; stacking them wastes the row and reads as three sections
      instead of one line of context. So there is NO `minmax()` and no
      `auto-fit` here: three equal tracks, and a long name wraps inside its own. */
.offer-parties { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gap-3); margin-block-end: var(--gap-3); }
.offer-lbl {
  font-size: var(--size-0);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--doc-ink-faint);
  margin-block-end: var(--gap-1);
}
.offer-party-name { display: block; font-weight: 600; overflow-wrap: anywhere; }
.offer-party-sub { display: block; color: var(--doc-ink-soft);
  overflow-wrap: anywhere; }

/* ---- the services band ---------------------------------------------------- */
.offer-band {
  display: grid;
  grid-template-columns: 1.6fr 0.8fr 1fr;
  gap: var(--gap-4);
  background: var(--nile-mint);
  padding: var(--gap-2) var(--gap-3);
  border-radius: 3px;
  margin-block-end: var(--gap-3);
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}
.offer-band b { display: block; color: var(--doc-band-ink); font-weight: 600;
  overflow-wrap: anywhere; }

/* ---- the spec grid ----------------------------------------------------------
   ⚠ SIX CELLS A ROW AND THE ROWS ARE BUILT BY THE RENDERER, not by a wrapping
     flex. A cell that wrapped onto the next line would leave a hole in the grid
     with a border round it — which on paper reads as a missing fact rather than
     as a layout. */
.offer-spec { border: var(--line-width) solid var(--doc-line); border-radius: 3px;
  margin-block-end: var(--gap-4); }
.offer-spec-row { display: grid; grid-template-columns: repeat(6, 1fr); }
.offer-spec-row + .offer-spec-row { border-block-start: var(--line-width) solid var(--doc-line); }
.offer-cell { padding: var(--gap-2); border-inline-end: var(--line-width) solid var(--doc-line);
  min-inline-size: 0; }
.offer-cell:last-child { border-inline-end: 0; }
/* ⚠ AND A PADDING CELL DRAWS NO DIVIDER. The last spec row is padded out to six
   so the grid stays rectangular — a four-cell row in a six-track grid leaves two
   columns of white inside a border, which on paper reads as two facts that
   failed to print. But the padding cells kept their own dividers, so the tail of
   the row read as four empty boxes. `:empty` is exactly the right selector here:
   a cell with a label in it is never empty. Seen on the Arabic sheet of
   `Q26IMPOC30`, where the second row carries two facts and four blanks. */
.offer-cell:empty { border-inline-end: 0; }
.offer-cell b { display: block; font-weight: 600; overflow-wrap: anywhere; }

.offer-lead { margin: 0 0 var(--gap-2); }

/* ---- the charge table ------------------------------------------------------- */
.offer-lines { inline-size: 100%; border-collapse: collapse;
  font-size: var(--size-1); }
.offer-lines th {
  background: var(--nile-green-deep);
  color: var(--paper);
  font-weight: 600;
  font-size: var(--size-0);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: var(--gap-2);
  border: 0;
  text-align: start;
  white-space: nowrap;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}
.offer-lines td { padding: var(--gap-1) var(--gap-2);
  border-block-end: var(--line-width) solid var(--doc-line-soft);
  vertical-align: top; }
.offer-no { inline-size: 2.5rem; color: var(--doc-ink-faint); }
.offer-ccy { inline-size: 4rem; }
/* ⛔ END-ALIGNED AND TABULAR, so a column of amounts lines up on the decimal
      point. `text-align: end` mirrors for Arabic; `right` would not. */
.offer-amount { text-align: end; font-variant-numeric: tabular-nums;
  white-space: nowrap; inline-size: 11rem; }
.offer-lines th.offer-amount { text-align: end; }
/* ★★ S §1 · A FOREIGN CURRENCY READS DIFFERENTLY FROM THE POUND — Karim, 9
   September. The pound is the document's default ink; anything else takes the
   brand blue, in the table AND in the totals, so a mixed offer can be scanned
   for its dollar half in one look. The key under the totals says so in a
   sentence, because colour alone is not a legend — and it is the reason this is
   not left to the currency code at the end of each line. */
.offer-fx, .offer-fx b { color: var(--nile-blue); }
.offer-fxkey { display: flex; align-items: center; gap: var(--gap-1);
  font-size: var(--size-0); color: var(--doc-ink-faint);
  margin-block-start: var(--gap-2); }
.offer-fxkey i { inline-size: 9px; block-size: 9px; border-radius: 2px;
  background: var(--nile-blue); display: inline-block;
  -webkit-print-color-adjust: exact; print-color-adjust: exact; }

/* ★ F §6 · the printed line's own note lives further down this file, beside the
     permission groups that were written on the same day — `.offer-note`. */

/* ---- the totals -------------------------------------------------------------
   ⛔ ONE TOTAL PER CURRENCY, STACKED, AND NEVER A COMBINED ONE — `18 §٣.١`. The
      dollar part is paid in dollars and the Egyptian part in pounds; adding them
      produces a number nobody pays. */
.offer-tot { margin-block-start: var(--gap-3); margin-inline-start: auto;
  inline-size: min(22rem, 100%);
  border: var(--line-width) solid var(--doc-line); border-radius: 3px;
  overflow: hidden; }
.offer-tot-h {
  display: flex; justify-content: space-between; gap: var(--gap-2);
  background: var(--nile-green-deep); color: var(--paper);
  font-size: var(--size-0); letter-spacing: 0.1em; text-transform: uppercase;
  padding: var(--gap-1) var(--gap-2); font-weight: 600;
  -webkit-print-color-adjust: exact; print-color-adjust: exact;
}
.offer-tot-r { display: flex; justify-content: space-between; gap: var(--gap-2);
  padding: var(--gap-1) var(--gap-2);
  border-block-end: var(--line-width) solid var(--doc-line-soft); }
.offer-tot-r:last-child { border-block-end: 0; }
.offer-tot-r b { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-weight: 600; }
/* A CURRENCY THIS OFFER DOES NOT USE IS GREYED AT ZERO AND NOT DROPPED — the
   issued document's own habit, and it answers «is there another sheet». */
.offer-tot-r.is-off, .offer-tot-r.is-off b { color: var(--doc-off);
  font-weight: 400; }

/* ---- the terms and the foot ------------------------------------------------- */
.offer-h3 { font-size: var(--size-0); letter-spacing: 0.11em;
  text-transform: uppercase; color: var(--nile-green); font-weight: 700;
  margin: 0 0 var(--gap-2); }
.offer-terms, .offer-disclaimer, .offer-rate { font-size: var(--size-1);
  margin-block-start: var(--gap-4); }
.offer-rate .mono {
  background: var(--doc-rate-soft);
  border: var(--line-width) solid var(--nile-amber);
  color: var(--doc-rate-ink);
  padding: 0 var(--gap-1);
  border-radius: 2px;
  font-weight: 600;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}
/* ★ THE FOOTER IS ON THE DOCUMENT IN BOTH LANGUAGES AT ONCE, and that is not an
     oversight of the translation rule: it is a warning about MONEY sent to a
     company whose staff read Arabic and whose clients' forwarders read English,
     and it has to be legible to whichever of them picks the sheet up. It is the
     one bilingual block in this system. */
.offer-foot { margin-block-start: var(--gap-4);
  border-block-start: var(--line-width) solid var(--doc-line);
  padding-block: var(--gap-2) var(--gap-4);
  text-align: center; color: var(--doc-ink-faint); font-size: var(--size-0);
  line-height: 1.6; }
.offer-foot p { margin: 0; }

/* The gloss under a grouped line, and the Arabic name under a service code. */
.offer-gloss { display: block; color: var(--doc-ink-faint); font-size: var(--size-0); }

/* =============================================================================
   ★★ S §4 · OPERATIONS' STEP-4 REVIEW BORROWS THE LOOK — AND IS NOT A DOCUMENT
   =============================================================================
   Karim ⑯: «in the review part make it show to me the full quotation shape of
   it… and also put it in a template with Nile logo and colours».

   ⛔⛔ AND IT IS NOT A THIRD PRINTABLE THING. `ALLOWED_PRINT_CALLERS` names two
      files and `quotation-card-render.js` is not one of them. What this block
      does is let the SHAPE be recognised — it is the same stylesheet, not a
      second drawing of the same document, so the two cannot drift.

   ★ THE SENTENCE ABOVE IT IS PART OF THE DESIGN, NOT A CAPTION. A panel that
     looks like the sheet a client receives, sitting above a table of what Nile
     paid, has to say on the page which of the two it is. `reviewShape`'s own
     header carries the whole argument. */
.quo-shape { display: flex; flex-direction: column; gap: var(--gap-2);
  margin-block-end: var(--gap-4); }
.quo-shape-note { margin: 0; font-size: var(--size-1); color: var(--ink-soft); }
/* ⛔ SLIGHTLY SMALLER AND NOT FULL WIDTH. It is a preview and it should read as
      one — an exact copy at the same scale beside a working table invites
      somebody to treat it as the finished sheet. */
.offer-sheet.is-shape { max-inline-size: 40rem; opacity: 0.96; }

/* ⛔ THE TERMS ARE NOT A GREY BOX ON A CLIENT'S SHEET. `.terms-text` carries
      `background: var(--paper-soft)` because on the WORKING screens it is one
      block among many and needs an edge; on the document it reads as a code
      listing. The `white-space: pre-wrap` is what is wanted and is kept — the
      numbering «1- … 9-» is IN the rendered text, put there by the terms
      template, so this is not an `<ol>` and must not become one: renumbering a
      client's terms in the browser is the browser deciding what clause 4 is. */
.offer-terms .terms-text, .offer-disclaimer .terms-text {
  background: none;
  padding: 0;
  border-radius: 0;
}

/* =============================================================================
   ⚠ ARABIC LETTERS JOIN — the ONE `:lang()` rule in this stylesheet
   =============================================================================
   `letter-spacing` pulls an Arabic word apart into disconnected glyphs. That is
   not a style somebody may dislike; it is the word rendered wrongly, and at
   `0.22em` on «عرض سعر» it is unreadable.

   ⛔ AND IT IS ABOUT THE SCRIPT, NOT THE DIRECTION. `check-css.sh` forbids a
      `[dir]` override outright and it is right to: the logical properties flip
      themselves and a direction override is what a project writes when it did
      not use them. This is the other question — what the LETTERS do — and no
      logical property answers it. It is scoped to this document's six
      letterspaced labels rather than written as a blanket rule, so nothing on
      any screen moves. */
:lang(ar) .offer-word,
:lang(ar) .offer-tag,
:lang(ar) .offer-lbl,
:lang(ar) .offer-h3,
:lang(ar) .offer-lines th,
:lang(ar) .offer-tot-h,
/* ★ AND THE REQUEST'S TWO, for the same reason. «داخلي — لا يُسلَّم لعميل» is the
     one word on the internal sheet that MUST be read at a glance, and it was
     the most spaced-out thing on the page. A `@media print` rule carries no
     extra specificity, so this reaches inside that block. */
:lang(ar) .req-print-internal,
:lang(ar) .req-print-tag { letter-spacing: 0; }

@media print {
  /* ★ WHILE THE OFFER IS PRINTING, IT IS THE ONLY THING ON THE PAGE. The class
        is put on `<body>` by `offer-print.js` and taken off afterwards — the
        toolbar, the tab row and the offer's own chrome all go. */
  body.is-printing-offer .quo-card-head,
  body.is-printing-offer .offer-toolbar,
  body.is-printing-offer .list-top { display: none !important; }
  body.is-printing-offer .offer-sheet {
    border: none;
    max-inline-size: none;
  }
  /* ⚠ AND THE SCREEN'S GROUND GOES WITH THEM. Seen on the first printed sheet:
     the document sat in a grey frame with a rounded corner, because
     `.quo-screen` and the panes above it paint `--ground` and `--paper` and a
     printer prints what it is given. The request's own print block already
     resets `.app`; these three are the quotation card's. */
  body.is-printing-offer .quo-screen,
  body.is-printing-offer .quo-card,
  body.is-printing-offer .quo-tab,
  body.is-printing-offer .app-main {
    background: #fff;
    padding: 0;
    margin: 0;
    border: none;
    box-shadow: none;
  }
  /* ⛔ THE RAIL AND THE INDENT SURVIVE THE PRINT. They are the document's own
        edge, not screen chrome — the sheet Nile posts has them. What goes is the
        BOX: a border round a page that already has a page edge. */
  body.is-printing-offer .offer-in { padding-block-start: 0; }
  /* Nothing on this sheet may break across a page in the middle of a fact. */
  .offer-head, .offer-parties, .offer-band, .offer-spec, .offer-tot,
  .offer-terms, .offer-foot {
    page-break-inside: avoid;
    break-inside: avoid;
  }
  /* =========================================================================
     ★★ S §4 · AND THE STEP-4 PANEL DOES NOT PRINT — the sentence on it is TRUE
     =========================================================================
     It carries no print control, which is `ALLOWED_PRINT_CALLERS`' half of the
     rule. This is the other half: **the browser's own Ctrl+P is not something
     this system can take away**, and step 4 is the one screen where a page that
     LOOKS like the client's document sits directly above a table of what Nile
     paid. A person pressing Ctrl+P there would get both on one sheet.

     ⛔ SO IT IS HIDDEN FROM PAPER OUTRIGHT. The panel says «this is the internal
       view and it does not print» — `seq.shapeNote` — and until this rule that
       was a claim rather than a fact.

     ★ AND THE WORKING TABLE UNDERNEATH IS **NOT** HIDDEN. Somebody printing a
       costing sheet for themselves is doing an ordinary thing and has been able
       to since `037`; what must never exist is a printed page that could be
       MISTAKEN for the client's copy. Removing the document-shaped panel is
       exactly that distinction, and it is why this rule names the panel and
       nothing else. */
  .quo-shape { display: none !important; }

  /* ⚠ THE LINE TABLE **MAY** BREAK — a twelve-line offer that refused to break
        would push the whole table to a second sheet and leave the first empty.
        What must not break is a ROW, and the header repeats on each page.  */
  .offer-lines { break-inside: auto; }
  .offer-lines tr { page-break-inside: avoid; break-inside: avoid; }
  .offer-lines thead { display: table-header-group; }
}

/* ★ 041 · «who can approve an offer», on the users screen — C §7 */
.chip.is-named { background: var(--accent-soft); color: var(--accent); }
.chip.is-inherited { background: var(--paper-soft); color: var(--ink-soft); }

/* =============================================================================
   ★★ 041 · A ROUTE NUMBER MUST NOT READ BACKWARDS IN ARABIC — §9.2 item ⑩
   =============================================================================
   `/1` is how a route is written on paper the client holds — `Q26IMPOC153 / 1`,
   and `036`'s own header says the «/1» is «printed on paper a client holds». On
   an RTL page the slash is a NEUTRAL character, so the bidi algorithm reorders
   it around the digit and the label renders «1/».

   SEEN, NOT REASONED: the approvals card in Arabic drew the route brief as
   «1/ … CNPVG ⟶ EGCAI».

   ⟹ `unicode-bidi: isolate` makes the span its own run and `direction: ltr`
     fixes that run's order. Both are LOGICAL properties — `direction` is the
     property the whole logical-property system is built on — so this is not a
     physical-side rule and `check-css.sh` agrees.

   ⛔ AND IT IS ON THE CODE SPANS, NOT ON `.mono`. `.mono` is also the class on
     every AMOUNT, and an amount inside an Arabic sentence must keep taking its
     direction from the sentence. What is isolated here is an IDENTIFIER: a
     route number, a quotation code, a customer code. */
.route-no,
.quo-code,
.apr-code,
.override-where .mono,
.route-brief .mono {
  unicode-bidi: isolate;
  direction: ltr;
}

/* =============================================================================
   ★★ F §2 · THE THIRTEEN SCREENS — four grids, two dashboards, one step rail
   =============================================================================
   ⛔ LOGICAL PROPERTIES ONLY, as everywhere in this file. `check-css.sh` refuses
   `margin-left`, `float`, `text-align: right` and `[dir="rtl"]` outright, and
   the Arabic build is a mirror rather than a second stylesheet because of it.

   ★ AND NOTHING BELOW REDEFINES A TOKEN. The prototype has its own palette and
   its own radii and this file does not import them — `prototype-milestone-2.html`
   says so in its own header: «Take the SHAPE, the SEQUENCE, the WORDING and the
   BUTTONS from here», not the CSS. */

/* ---- the four row grids -----------------------------------------------------
   ⚠ ONE GRID PER LAYOUT AND NOT ONE GRID WITH BLANK TRACKS. `.quo-row`'s own
   note: «a grid with an empty track is a column of white nobody can name.» */
.quo-row.is-ready {
  grid-template-columns:
    minmax(8rem, 0.9fr)       /* quotation   */
    minmax(9rem, 1.3fr)       /* client      */
    minmax(9rem, 1.2fr)       /* route       */
    minmax(7rem, 0.9fr)       /* approved by */
    7rem                      /* valid until */
    minmax(13rem, auto);      /* the three buttons */
}
.quo-row.is-client-row {
  grid-template-columns:
    minmax(8rem, 0.9fr)       /* quotation */
    minmax(9rem, 1.4fr)       /* client    */
    7rem                      /* sent      */
    6rem                      /* waiting   */
    minmax(8rem, 1fr)         /* value     */
    minmax(9rem, auto);       /* won · lost */
}
.quo-row.is-archive {
  grid-template-columns:
    minmax(8rem, 0.9fr) minmax(9rem, 1.4fr) minmax(9rem, 1.2fr)
    6rem 7rem minmax(8rem, 1fr);
}
.quo-cell.is-actions { flex-direction: row; flex-wrap: wrap; gap: var(--gap-1);
  align-items: start; align-content: start; }
.quo-route { font-size: var(--size-1); overflow-wrap: anywhere; }
.quo-service-sub { color: var(--ink-faint); font-size: var(--size-1); }
.quo-open { color: inherit; font-weight: 600; }
/* ⚠ `width: auto` IS LOAD-BEARING. `.btn-primary` is `width: 100%` because it
   is a FORM button everywhere else in this system, and inside a row that made
   «Mark it sent» a full-width bar on its own line under the other two. Seen on
   S5 before it was fixed, not reasoned about. */
.btn-row { padding-block: 2px; padding-inline: var(--gap-2);
  font-size: var(--size-1); white-space: nowrap; text-decoration: none;
  width: auto; }
a.btn-row { display: inline-flex; align-items: center; }

/* ★★ THE SECOND PRESS IS THE ANSWER — and the question says what it records.
   ⛔ NOT `window.confirm`: a browser dialog is in one language, unstyled, and
   cannot say «there is no way back from there except Won or Lost». */
.row-confirm { display: flex; flex-direction: column; gap: 2px;
  flex-basis: 100%; padding: var(--gap-2); border-radius: var(--radius);
  background: var(--warn-soft); color: var(--ink-soft);
  font-size: var(--size-1); }
.row-confirm strong { color: var(--warn); }
.btn-row.is-armed { outline: 2px solid var(--warn); outline-offset: 1px; }
.state-refusal { padding: var(--gap-2); border-radius: var(--radius);
  background: var(--stop-soft); color: var(--ink-soft);
  display: flex; flex-direction: column; gap: var(--gap-2);
  align-items: flex-start; font-size: var(--size-1); }
.state-refusal strong { color: var(--stop); }

/* ---- the dashboards · F §8 --------------------------------------------------
   ★ «Every row names the step AND WHO IT IS WAITING ON — the second half is
   what makes it useful rather than decorative.» */
.dash-tiles { display: grid; gap: var(--gap-2); margin-block-end: var(--gap-4);
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); }
.dash-tile { background: var(--paper); border: var(--line-width) solid var(--line);
  border-radius: var(--radius); padding: var(--gap-3); }
/* ⚠ THE ALARM TILE IS A COLOUR **AND** A WORD. Colour alone fails for a reader
   who cannot see it — the sub-line under the number says what it is. */
.dash-tile.is-alarm { border-color: var(--stop); background: var(--stop-soft); }
.dash-tile.is-alarm .dash-value { color: var(--stop); }
.dash-key { font-size: var(--size-1); color: var(--ink-soft); }
.dash-value { font-size: 1.7rem; font-weight: 600; line-height: 1.1;
  margin-block-start: 2px; font-variant-numeric: tabular-nums; }
.dash-sub { font-size: var(--size-1); color: var(--ink-faint); margin-block-start: 2px; }
.dash-row {
  display: grid;
  grid-template-columns:
    minmax(8rem, 0.9fr) minmax(9rem, 1.3fr) minmax(9rem, 1.2fr)
    minmax(7rem, 0.9fr) minmax(6rem, 0.8fr) 4rem;
  gap: var(--gap-2); padding: var(--gap-2);
  border-block-end: var(--line-width) solid var(--line);
  font-size: var(--size-1); align-items: center;
  text-decoration: none; color: inherit;
}
.dash-row.is-head { color: var(--ink-faint); font-weight: 600; }
a.dash-row:hover { background: var(--paper-soft); }
.dash-cell { min-width: 0; overflow-wrap: anywhere; }
.dash-cell.is-days { text-align: end; font-variant-numeric: tabular-nums; }
.dash-table { background: var(--paper); border: var(--line-width) solid var(--line);
  border-radius: var(--radius); overflow: hidden; }
.dash-table-head { padding: var(--gap-2) var(--gap-3);
  border-block-end: var(--line-width) solid var(--line); font-weight: 600; }

/* ---- the step rail · F §3 ---------------------------------------------------
   ★★ «A step rail across the top, showing where you are AND WHAT IS BEHIND YOU.»
   ⛔ IT IS NOT A TAB BAR AND IT IS NOT CLICKABLE. F §3: «Forward and back
   between steps. Nothing else. No tab bar, no jumping to step 4 from step 1.»
   So these are `<div>`s: nothing here is focusable, nothing is announced as a
   control, and there is no handler to forget to write. */
.step-rail { display: flex; border: var(--line-width) solid var(--line);
  border-radius: var(--radius); overflow: hidden; background: var(--paper);
  margin-block-end: var(--gap-3); }
.step-cell { flex: 1 1 0; min-width: 0; padding: var(--gap-2) var(--gap-3);
  border-inline-end: var(--line-width) solid var(--line); }
.step-cell:last-child { border-inline-end: 0; }
.step-no { font-size: 0.6875rem; font-weight: 600; color: var(--ink-faintest);
  letter-spacing: 0.06em; }
.step-name { font-size: var(--size-1); color: var(--ink-faint); margin-block-start: 2px; }
.step-cell.is-done { background: var(--accent-soft); }
.step-cell.is-done .step-no, .step-cell.is-done .step-name { color: var(--accent); }
.step-cell.is-now { background: var(--accent); }
.step-cell.is-now .step-no { color: var(--paper); opacity: 0.75; }
.step-cell.is-now .step-name { color: var(--paper); font-weight: 600; }

/* ---- the five request forms, as cards · F §2 · S2 --------------------------- */
.form-card { display: flex; flex-direction: column; gap: 2px;
  padding: var(--gap-3); text-align: start;
  background: var(--paper); border: var(--line-width) solid var(--line);
  border-radius: var(--radius); cursor: pointer; color: inherit;
  text-decoration: none; font: inherit; }
.form-card:hover { background: var(--paper-soft); border-color: var(--accent); }

/* ★ THE BANNER · «the reason operations gave is ON the banner, not hidden
   behind a click.» */
.came-back { display: flex; gap: var(--gap-2);
  padding: var(--gap-3); border-radius: var(--radius);
  border: var(--line-width) solid var(--stop); background: var(--stop-soft);
  margin-block-end: var(--gap-3); }
.came-back-bar { inline-size: 3px; border-radius: 3px; background: var(--stop);
  flex: 0 0 3px; }
.came-back strong { color: var(--stop); display: block; }
.came-back-why { font-size: var(--size-1); color: var(--ink-soft); }
.came-back-why + .came-back-why { margin-block-start: 2px; }

/* ★★ G §1.1 — THE MANAGER'S OWN SENTENCE, ON THE BANNER.

   «A banner that makes you click to find out why is half a banner» — G §5 ⑥.
   The note is a block of its own under the offer's code and the person who
   wrote it, because a sentence run onto the end of a line of metadata is a
   sentence nobody reads.

   `white-space: pre-line` so the line breaks a manager typed into the box
   survive onto the screen. `overflow-wrap: anywhere` because the alternative on
   a long unbroken token is a banner wider than the page — §9.2 item ⑥, and
   these four screens are the ones a person opens with a client on the phone. */
.came-back-note {
  display: block;
  margin-block-start: 2px;
  color: var(--ink);
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.came-back-why:hover .came-back-note { color: var(--ink); }

/* ★★ THE RETURNED ROW — «coloured in red», Karim's own words, on whichever lane
   it came back to.

   IT IS AN INLINE-START BAR AND A TINT, NOT RED TEXT. Red text on a row whose
   other cells are ordinary would read as «this row is broken»; a bar and a wash
   read as «this one first», which is what the row means. `border-inline-start`
   puts the bar on the reading edge in both builds with one rule.

   ⚠ AND IT DOES NOT FIGHT `.is-off`. A returned offer that was later
   deactivated keeps both classes: the opacity says «cancelled» and the bar says
   «it came back», and the two facts are both true. */
.quo-line.is-returned {
  background: var(--stop-soft);
  border-inline-start: 3px solid var(--stop);
}

.quo-line.is-returned:hover { background: var(--stop-soft); }

/* ---- the permission groups · F §7 -------------------------------------------
   ⛔ THE PAGES ARE GROUPED. THE PEOPLE ARE NOT — all twenty-two stay available
   under every group. There is no CSS here that hides a person. */
.perm-group { border: var(--line-width) solid var(--line);
  border-radius: var(--radius); background: var(--paper);
  margin-block-end: var(--gap-3); }
.perm-group-head { display: flex; align-items: center; gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  border-block-end: var(--line-width) solid var(--line); }
.perm-group-name { font-weight: 600; }
.perm-group-count { margin-inline-start: auto; color: var(--ink-faint);
  font-size: var(--size-1); }

/* ★ A BLOCK WITH A HEADING ABOVE IT. Used by the five request-form cards and by
   the permission groups — one shape, so a reader meets it twice and knows it. */
.card-block { margin-block-end: var(--gap-4); }
.chart-subtitle { margin: 0 0 var(--gap-2); font-size: var(--size-2);
  font-weight: 600; color: var(--ink-soft); }

/* ★ The grid inside a permission group keeps its own head row: four sections
   each need their column names, and a single head above all four would scroll
   away. `.perm-group` already draws the border, so the section drops its own. */
.grid.is-section { border: 0; border-radius: 0; }

/* ★ F §6 · the printed line's own note, under the item description. Smaller and
   quieter than the description, and it prints — it is on the CEO's template. */
.offer-note { display: block; font-size: 0.6875rem; color: var(--ink-faint);
  margin-block-start: 1px; }
@media print { .offer-note { color: #555; } }

/* ★ THE STEP HEADER'S BUTTONS SIT IN A ROW AND KEEP THEIR OWN WIDTH.
   `.btn-primary` is `width: 100%` because it is a FORM button everywhere else,
   and «Back» became a full-width blue bar across the top of the offer. Same
   correction as `.btn-row`, on the other place F put buttons outside a form. */
.step-actions { display: flex; gap: var(--gap-2); flex-wrap: wrap;
  padding: var(--gap-2) 0; }
.step-actions .btn-primary,
.step-actions .btn-secondary { width: auto; }

/* ★ AND THE STEP FOOT'S — `.form-foot` is used both inside modals (where full
   width is right) and at the end of a step (where it is not). */
.quo-tab.is-step > .form-foot { display: flex; gap: var(--gap-2);
  flex-wrap: wrap; }
.quo-tab.is-step > .form-foot > .btn-primary,
.quo-tab.is-step > .form-foot > .btn-secondary { width: auto; }

/* ★★ F §3 · STEP ⑤ IS A SCREEN, NOT A TOAST — «It says the offer has left, and
   who has it now.» */
.done-panel { display: flex; gap: var(--gap-3); padding: var(--gap-4);
  border: var(--line-width) solid var(--accent-line);
  background: var(--accent-soft); border-radius: var(--radius); }
.done-bar { inline-size: 3px; border-radius: 3px; background: var(--accent);
  flex: 0 0 3px; align-self: stretch; }
.done-title { margin: 0 0 var(--gap-1); color: var(--accent); font-size: var(--size-2); }
.done-body { margin: 0; color: var(--ink-soft); font-size: var(--size-1); }

/* ★ THE FIVE GREEN FIELDS ON STEP ① — «filled in for you». `.field` is the
   house field; this is the one that came from the request. */
.field.is-prefilled { background: var(--accent-soft);
  border-inline-start: 2px solid var(--accent-line);
  padding: var(--gap-2); border-radius: var(--radius); }
.field-grid { display: grid; gap: var(--gap-2);
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }

/* =============================================================================
   MASTER DATA - Reference and system > Master data (046, prompt I section 5)
   =============================================================================
   The prototype `documents/prototype-master-data.html`, in logical properties:
   an index column on the START side, one full-width grid per list under a
   sticky toolbar, the slide-over every list screen has, and three special
   screens (the form-fields matrix, the tick lists, the fiscal periods).

   THE GRID IS CSS GRID, NOT A TABLE: forty-eight column sets cannot share one
   `<table>` and its widths. Each list's widths are a `--md-cols` custom
   property set through the CSSOM (`style.setProperty`) - the CSP carries no
   `unsafe-inline`, so an inline `style=` attribute would be silently dead.
   `.md-row` is a `<button>` so a row is a tab stop and Enter opens it, and it
   is in `PANEL_ROW_SELECTOR` so «the same row again» closes the panel.

   LOGICAL PROPERTIES ONLY - `tools/check-css.sh`. */
.app-main.is-master { padding: 0; display: flex; }
.md {
  display: grid;
  grid-template-columns: 15.5rem minmax(0, 1fr);
  inline-size: 100%;
  min-block-size: 100%;
}
.md-index {
  border-inline-end: var(--line-width) solid var(--line);
  background: var(--paper-soft);
  padding: var(--gap-3) var(--gap-2);
  position: sticky;
  inset-block-start: var(--header-height);
  max-block-size: calc(100vh - var(--header-height));
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.md-idx-title {
  font-size: var(--size-1);
  font-weight: 600;
  color: var(--ink-soft);
  padding: 0 var(--gap-2) var(--gap-1);
}
.md-idx-search { margin-block-end: var(--gap-2); }
.md-idx-search .input { padding-block: var(--gap-1); font-size: var(--size-1); }
.md-idx-group {
  font-size: var(--size-0);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-faint);
  padding: var(--gap-3) var(--gap-2) var(--gap-1);
}
.md-idx-item {
  display: flex;
  align-items: center;
  gap: var(--gap-1);
  inline-size: 100%;
  border: none;
  background: none;
  text-align: start;
  font: inherit;
  font-size: var(--size-1);
  color: var(--ink);
  padding: var(--gap-1) var(--gap-2);
  border-radius: var(--radius);
  cursor: pointer;
}
.md-idx-item:hover { background: var(--paper); }
.md-idx-item.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.md-idx-item.is-sub { color: var(--ink-soft); }
.md-idx-item > span:first-of-type { flex: 1 1 auto; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.md-idx-item .md-lock { inline-size: 12px; block-size: 12px; color: var(--ink-faint); flex: none; }
.md-n {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-0);
  color: var(--ink-faint);
  margin-inline-start: auto;
}
.chip .md-n { margin-inline-start: var(--gap-1); }
.md-work { min-inline-size: 0; }
.md-body { padding: var(--gap-2) var(--gap-4) var(--gap-5); }
.md-lede { color: var(--ink-soft); font-size: var(--size-1); line-height: 1.6; margin: 0 0 var(--gap-2); }
.md-lock-line { display: inline-flex; align-items: center; gap: var(--gap-1); }
.md-lock-line .md-lock, .md-floor-note .md-lock { inline-size: 14px; block-size: 14px; color: var(--ink-faint); flex: none; }
.md-floor-note { display: flex; align-items: center; gap: var(--gap-1); flex-wrap: wrap; }
.md-k { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--size-1); }
.md-off { color: var(--ink-faint); }
.md-warn { color: var(--warn); font-size: var(--size-1); }
.md-sub { display: block; font-size: var(--size-0); color: var(--ink-faint); }
.md-diff { color: var(--warn); }
.md-req { color: var(--stop); font-weight: 600; }

/* --- the grid ------------------------------------------------------------- */
.md-grid {
  /* the per-list column widths; master-data.js sets this through the CSSOM
     (`style.setProperty`), the CSP carrying no unsafe-inline. Defined here so
     the token exists before any list is drawn (test_A's rule). */
  --md-cols: minmax(0, 1fr);
  display: flex; flex-direction: column; border-block-start: var(--line-width) solid var(--line);
  /* 122 · a wide list (the ports: seven fixed columns) scrolls INSIDE its grid on a laptop width —
     the page body never scrolls sideways (the sweep of every screen caught it at 1366px) */
  overflow-x: auto; max-inline-size: 100%;
}
.md-row {
  display: grid;
  grid-template-columns: var(--md-cols, minmax(0, 1fr));
  gap: var(--gap-2);
  align-items: center;
  inline-size: 100%;
  border: none;
  border-block-end: var(--line-width) solid var(--line);
  background: none;
  font: inherit;
  color: var(--ink);
  text-align: start;
  padding: var(--gap-1) var(--gap-2);
  min-block-size: 2.4rem;
  cursor: pointer;
}
.md-row:hover { background: var(--paper-soft); }
.md-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.md-row.is-selected { background: var(--accent-soft); }
.md-row.is-off > .md-cell { color: var(--ink-faint); }
.md-row.is-head {
  cursor: default;
  background: var(--paper);
  font-size: var(--size-0);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-faint);
  min-block-size: 2rem;
}
.md-row.is-head:hover { background: var(--paper); }
.md-row.is-group { cursor: default; background: var(--paper-soft); font-weight: 600; font-size: var(--size-1); grid-template-columns: minmax(0, 1fr); }
.md-cell { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--size-1); }
.md-cell.is-num { text-align: end; }
.md-kind { display: inline-flex; align-items: center; gap: var(--gap-1); }
.md-kind-icon { inline-size: 14px; block-size: 14px; color: var(--ink-soft); }
.md-flags { font-family: var(--font-mono); letter-spacing: .15em; }
.md-flags b { color: var(--accent); }
.md-flags i { color: var(--ink-faint); font-style: normal; }
.md-ov-row { grid-template-columns: minmax(12rem, 1.6fr) 5rem 5rem minmax(10rem, 1.6fr) minmax(9rem, 1.2fr) 6rem; }   /* 116 · + read by */
.md-overview .md-lock { inline-size: 12px; block-size: 12px; color: var(--ink-faint); vertical-align: middle; }
.md-empty { padding: var(--gap-5) var(--gap-3); text-align: center; color: var(--ink-soft); display: flex; flex-direction: column; gap: var(--gap-2); align-items: center; }
.md-empty h3 { margin: 0; font-size: var(--size-2); color: var(--ink); }
.md-empty p { margin: 0; font-size: var(--size-1); }
.md-pager { display: flex; gap: var(--gap-2); align-items: center; padding-block-start: var(--gap-2); }

/* --- the panel ----------------------------------------------------------- */
.md-form { display: flex; flex-direction: column; gap: var(--gap-2); margin-block: var(--gap-3); }
.md-field-check { padding-block: var(--gap-1); }
.md-longtext { min-block-size: 6rem; resize: vertical; }
.md-last { margin-block-start: var(--gap-3); }
.md-stop { color: var(--stop); }
.md-confirm { display: flex; flex-direction: column; gap: var(--gap-2); inline-size: 100%; }
.md-confirm .row { display: flex; gap: var(--gap-2); }

/* --- the matrix ----------------------------------------------------------- */
.md-matrix-wrap { overflow-x: auto; border: var(--line-width) solid var(--line); border-radius: var(--radius); }
.md-matrix { border-collapse: collapse; inline-size: 100%; font-size: var(--size-1); }
.md-matrix th, .md-matrix td { border-block-end: var(--line-width) solid var(--line); padding: var(--gap-1) var(--gap-2); text-align: start; vertical-align: middle; }
.md-matrix thead th { position: sticky; inset-block-start: 0; background: var(--paper); z-index: 1; }
.md-matrix th:first-child, .md-matrix td:first-child { position: sticky; inset-inline-start: 0; background: var(--paper); min-inline-size: 14rem; z-index: 2; }
.md-matrix thead th:first-child { z-index: 3; }
.md-fh { display: flex; flex-direction: column; gap: 2px; min-inline-size: 7rem; }
.md-fh-n { font-size: var(--size-0); color: var(--ink-faint); font-weight: normal; }
.md-fh .link-button { font-size: var(--size-0); padding: 0; text-align: start; }
.md-sec td { background: var(--paper-soft); font-size: var(--size-0); text-transform: uppercase; letter-spacing: .04em; color: var(--ink-faint); }
.md-fld { display: flex; flex-direction: column; }
.md-fld-name { font-family: var(--font-mono); font-size: var(--size-0); color: var(--ink-faint); }
.md-c { text-align: center; }
.md-ff {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-inline-size: 3.2rem;
  border: var(--line-width) solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink-faint);
  font: inherit;
  font-size: var(--size-0);
  padding: 2px var(--gap-1);
  cursor: pointer;
}
span.md-ff { cursor: default; }
.md-ff.is-shown { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.md-ff.is-required { background: var(--accent); color: var(--paper); border-color: var(--accent); }
.md-ff.is-floor { opacity: .85; }
.md-ff.is-changed { box-shadow: 0 0 0 2px var(--warn); }
.md-ff .md-lock { inline-size: 10px; block-size: 10px; }
.md-ff:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.md-legend { display: flex; align-items: center; gap: var(--gap-1); font-size: var(--size-0); color: var(--ink-faint); flex-wrap: wrap; }
.md-legend .md-ff { margin-inline-start: var(--gap-2); }

/* --- the tick lists ------------------------------------------------------- */
.md-ticks { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: var(--gap-3); }
.md-tick-col { border: var(--line-width) solid var(--line); border-radius: var(--radius); padding: var(--gap-2) var(--gap-3); }
.md-tick-col h3 { margin: 0 0 var(--gap-2); font-size: var(--size-2); display: flex; gap: var(--gap-2); align-items: baseline; }
.md-tick-list { display: flex; flex-direction: column; gap: var(--gap-1); }
.md-tick { display: flex; align-items: center; gap: var(--gap-2); font-size: var(--size-1); }
/* 078 · stages per service: one service at a time, its stages in the Tracking tab's seven groups */
.md-stages { display: flex; flex-direction: column; gap: var(--gap-3); }
.md-stages-head { display: flex; flex-wrap: wrap; gap: var(--gap-2) var(--gap-4); align-items: end; }
.md-stages-head .field { min-inline-size: 22rem; flex: 1; }
.md-stages-state { margin: 0; font-size: var(--size-1); color: var(--ink-faint); padding-block-end: var(--gap-1); }
.md-stages-all { color: var(--warn, var(--ink)); }
.md-stages-tools { display: flex; flex-wrap: wrap; gap: var(--gap-2); align-items: center; }
.md-stage-groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: var(--gap-3); }
.md-stage-groups.is-one { grid-template-columns: 1fr; } .md-stage-groups.is-one .md-tick-list { columns: 2; column-gap: var(--gap-4); }
.md-stage-groups.is-one .md-tick { break-inside: avoid; }
.md-stages-foot { display: flex; gap: var(--gap-3); align-items: center; }
.md-stages-foot .btn-primary { inline-size: auto; flex: none; }
.md-tick.is-off { color: var(--ink-faint); }
.md-tick .md-k { margin-inline-start: auto; color: var(--ink-faint); font-size: var(--size-0); }
.md-tick-foot { margin-block-start: var(--gap-2); }

/* --- the preview ---------------------------------------------------------- */
.md-pv { display: flex; flex-direction: column; gap: var(--gap-2); margin-block-start: var(--gap-3); }
.md-pv-sec { font-size: var(--size-0); text-transform: uppercase; letter-spacing: .04em; color: var(--ink-faint); padding-block-start: var(--gap-2); }
.md-pv-field { display: flex; flex-direction: column; gap: 2px; }
.md-pv-lab { font-size: var(--size-1); color: var(--ink-soft); display: flex; gap: var(--gap-1); }
.md-pv-lab b { color: var(--stop); font-weight: 600; font-size: var(--size-0); }
.md-pv-box { block-size: 1.9rem; border: var(--line-width) dashed var(--line); border-radius: var(--radius); background: var(--paper-soft); }
.md-pv-box.is-tall { block-size: 4rem; }
.md-pv-ticks { display: flex; flex-wrap: wrap; gap: var(--gap-1); }
.md-pv-ticks span { border: var(--line-width) solid var(--line); border-radius: var(--radius); padding: 2px var(--gap-2); font-size: var(--size-0); }

/* --- the terms editor and the import dialog --------------------------------- */
.md-editor { display: flex; flex-direction: column; gap: var(--gap-3); max-inline-size: 70rem; }
.md-two { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: var(--gap-3); }
.md-editor .md-body { min-block-size: 14rem; padding: var(--gap-2); resize: vertical; }
.md-editor .md-note { min-block-size: 5rem; resize: vertical; }
.md-token { font-family: var(--font-mono); background: var(--paper-soft); padding: 0 var(--gap-1); border-radius: var(--radius); }
.md-import { display: flex; flex-direction: column; gap: var(--gap-2); inline-size: min(640px, calc(100vw - 2 * var(--gap-4))); }
.md-import-text { font-family: var(--font-mono); font-size: var(--size-1); min-block-size: 8rem; resize: vertical; }
.md-imp-rows { display: flex; flex-direction: column; max-block-size: 14rem; overflow-y: auto; border: var(--line-width) solid var(--line); border-radius: var(--radius); }
.md-imp-row { display: grid; grid-template-columns: 3rem 4rem 5rem minmax(6rem, 1fr) 4rem 6rem; gap: var(--gap-2); padding: 2px var(--gap-2); font-size: var(--size-1); border-block-end: var(--line-width) solid var(--line); }
.md-imp-row.is-generic { grid-template-columns: 3rem 8rem minmax(8rem, 1fr) 6rem; }             /* 116 · line · code · name · status */
.md-import-keys { display: flex; align-items: center; gap: var(--gap-2); flex-wrap: wrap; font-size: var(--size-1); }
.md-import-keys code { direction: ltr; unicode-bidi: isolate; white-space: pre; overflow-x: auto; max-inline-size: 100%; }
/* 116 · who reads a list — small chips, on the list head and in the overview */
.md-readers { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 6px; }
.md-readers-label { color: var(--ink-faint); }
.md-reader { display: inline-block; padding: 1px 8px; border-radius: 999px; background: var(--paper-inset); border: var(--line-width) solid var(--line); font-size: var(--size-0); color: var(--ink-soft); white-space: nowrap; margin-inline-end: 4px; }
.md-imp-row.is-new { background: var(--accent-soft); }
.md-imp-row.is-unchanged { color: var(--ink-faint); }

@media (max-width: 900px) {
  .md { grid-template-columns: minmax(0, 1fr); }
  .md-index { position: static; max-block-size: none; border-inline-end: none; border-block-end: var(--line-width) solid var(--line); }
}

/* ---------- 131 · the home ---------- */
.home-hero h1 { margin: 0; font-size: 1.5rem; font-weight: 600; }
.home-hero .faint { margin: 4px 0 0; }
.home-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--gap-3); }
.home-tile { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: var(--gap-2); align-items: center; padding: var(--gap-3); border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper); color: var(--ink); text-decoration: none; }
.home-tile:hover { border-color: var(--accent-line); background: var(--accent-soft); text-decoration: none; }
.home-tile-icon { grid-row: 1 / span 2; color: var(--accent); }
.home-tile-icon .icon { width: 26px; height: 26px; }
.home-tile-big { font-size: 1.75rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.home-tile-label { font-weight: 600; }
.home-tile-hint { grid-column: 2; color: var(--ink-faint); font-size: var(--size-1); }
.home-modules { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--gap-2); }
.home-module { display: flex; align-items: center; gap: var(--gap-2); padding: var(--gap-2) var(--gap-3); border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); text-decoration: none; background: var(--paper); }
.home-module:hover { border-color: var(--accent-line); background: var(--accent-soft); text-decoration: none; }
.home-module svg { width: 18px; height: 18px; flex: none; color: var(--accent); }

/* 152 · the register under a control account, on the chart page */
.register-card .register-hint { margin: 4px 0 8px; color: var(--ink-soft); font-size: var(--size-1); }
.register-card .register-search { display: block; margin-block-end: 8px; }
.register-card .register-search input { width: 100%; }
.register-list { list-style: none; margin: 0; padding: 0; max-height: 40vh; overflow: auto; border: 1px solid var(--line); border-radius: 8px; }
.register-list li { padding: 6px 10px; border-block-end: 1px solid var(--line); font-size: var(--size-1); }
.register-list li:last-child { border-block-end: 0; }
.register-list li:nth-child(even) { background: rgba(0, 0, 0, .02); }
.register-shown { margin-block-start: 6px; }

/* 196 · Master data ▸ Exchange rates: the line under the title turns red when every source failed */
.md-note-bad { color: var(--stop); font-weight: 600; }   /* base.css names its own tokens: --stop, not the CRM set's --danger */
#md-fetch { white-space: nowrap; }

/* 197 · the day's rate beside a rate box: «CBE 07/10» */
.fx-hint { display: inline-block; margin-inline-start: 6px; font-size: var(--size-0); color: var(--ink-faint); white-space: nowrap; }
/* 235 · on a phone the dashboard's wide table scrolls inside its own box instead of being cut */
@media (max-width: 640px) { .dash-table { overflow-x: auto; } }
