/* Pippa Employee Portal — RTL base styles.
 * THEME: "London Atelier v2.1" — visual twin of the Pippa careers site
 *   (pippa-careers-ats / pippa-child). Mulberry accent, powder canvas,
 *   white cards + hairline, pill buttons, editorial Persian type (lh 2).
 *   Retheme is CSS-only: templates + leave/CSRF/HTMX logic are untouched.
 *
 * RULES (PLAN §6.1): CSS LOGICAL PROPERTIES ONLY — never physical left/right
 * or margin-left/right. Use margin-inline / padding-inline / inset-inline /
 * border-inline / text-align:start|end. Self-hosted Vazirmatn (no Google Fonts).
 */

/* ── Self-hosted Vazirmatn (woff2 in /static/fonts) ──────────────────────
 * Weights: 300 (Light, display headings) · 400 (Regular, body) ·
 *          600 (SemiBold, labels/buttons/titles) · 700 (Bold).
 * All four carry full Persian coverage (811 glyphs), so no glyph falls back.
 */
@font-face {
  font-family: "Vazirmatn";
  src: url("/static/fonts/Vazirmatn-Light.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Vazirmatn";
  src: url("/static/fonts/Vazirmatn-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Vazirmatn";
  src: url("/static/fonts/Vazirmatn-SemiBold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Vazirmatn";
  src: url("/static/fonts/Vazirmatn-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ── Self-hosted Bodoni Moda — the editorial Latin wordmark ("Pippa") ─────
 * Ported from the careers site (pippa-careers-ats). Italic 400 + Normal 500.
 * unicode-range pins these faces to Latin ONLY (ASCII + Latin-1 + Extended
 * A/B), so Persian glyphs NEVER fall back to a serif — Vazirmatn keeps all
 * Arabic-script coverage. Zero CDN (PLAN §6.1).
 */
@font-face {
  font-family: "Bodoni Moda";
  src: url("/static/fonts/BodoniModa-Italic-400.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0020-007E, U+00A0-024F;
}
@font-face {
  font-family: "Bodoni Moda";
  src: url("/static/fonts/BodoniModa-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020-007E, U+00A0-024F;
}

:root {
  /* ── Palette (London Atelier v2.1, AA-checked) ── */
  --bg: #F3EAE4;              /* powder — page canvas */
  --surface: #FBF8F6;        /* warm off-white surface */
  --card: #FFFFFF;           /* card fill */
  --band: #EDE4DF;           /* quiet band */
  --ink: #211A1E;            /* primary text */
  --ink-deep: #171215;       /* deepest ink */
  --muted: #6B5A60;          /* taupe — secondary text */
  --secondary: #211A1E;      /* labels resolve to ink */
  --primary: #6D2E46;        /* mulberry accent */
  --primary-deep: #5A2439;   /* mulberry hover */
  --primary-ink: #FFFFFF;
  --ghost: rgba(109, 46, 70, 0.10);   /* mulberry 10% wash */
  --on-ink: #F6EFEA;
  --on-ink-soft: #C9BFC2;
  --numeral: #A0828A;        /* large decorative numerals */
  --border: #E7DCD5;         /* hairline */
  --border-strong: #D8C9C1;  /* hairline strong */
  --chip-grad: linear-gradient(180deg, #7A3550, #6D2E46);
  --danger-bg: #F7ECEE;
  --danger-ink: #A03038;
  --warn-bg: #F4E9DA;        /* warm sand notice (no Teams blue anywhere) */
  --warn-ink: #6E4A16;
  --ok-bg: #E8F0E8;          /* muted sage confirmation, AA on --ok-ink */
  --ok-ink: #2F5D3A;

  /* ── Radius ── */
  --radius: 12px;            /* card / box */
  --radius-field: 8px;       /* input / select / alert */
  --radius-pill: 999px;      /* buttons, chips */

  /* ── Spacing — 8px base scale (mirrors careers London Atelier tokens) ── */
  --card-pad: 32px;          /* card inner padding */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px; --sp-9: 96px;

  /* ── Shadow + focus ──
   * The rest shadow was 0 1px 2px / 0.05 and simply did not survive the powder
   * canvas (--bg #F3EAE4): a white card on warm powder read as flat paper with a
   * hairline, so nothing on the page had depth. 3px at 0.08 is still a hairline
   * shadow, just one you can actually see on this ground. */
  --shadow-rest: 0 1px 3px rgba(33, 26, 30, 0.08);
  --shadow-hover: 0 1px 2px rgba(33, 26, 30, 0.05), 0 8px 24px rgba(33, 26, 30, 0.07);

  /* ── Type scale — 1.25 ratio, base 17px ── */
  --fs-display: 2.625rem;    /* 42 */
  --fs-h2: 2.0625rem;        /* 33 */
  --fs-h2-sm: 1.6875rem;     /* 27 */
  --fs-h3: 1.3125rem;        /* 21 */
  --fs-body: 1.0625rem;      /* 17 */
  --fs-small: 0.85rem;       /* 13.6 */

  /* Vazirmatn first, then platform fallbacks (correct even before woff2 loads). */
  --font: "Vazirmatn", "Segoe UI", Tahoma, Arial, system-ui, "Noto Sans Arabic", sans-serif;
}

* { box-sizing: border-box; }

/* Tabular figures for the whole document keep Jalali dates + counts aligned. */
html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 2;                 /* editorial Persian rhythm */
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

/* ── EP-025: the page frame, so the dark bar is a FOOTER ─────────────────────
 * COO 2026-09-07: «در صفحهٔ ورود، نوار مشکی‌رنگ که تاریخ توش نوشته شده بیاد
 * پایین صفحه و دکمهٔ ورود وسط صفحه قرار بگیره.»
 *
 * The footer was never positioned; it simply followed the content. On a long
 * page that reads as a footer, and on the SHORT ones it does not: the login
 * card is about a third of a screen, so the ink-deep band carrying the Jalali
 * date landed in the middle of the viewport with empty canvas below it, which
 * is what the COO was looking at. Nothing was broken; the page just had no
 * frame.
 *
 * A COLUMN FLEX BODY is that frame, and it is deliberately the whole-portal
 * fix rather than a login-only one: every short page in the portal (an empty
 * queue, a refusal, a «چیزی برای نمایش نیست») had the same stranded band.
 *
 * `100dvh` AFTER `100vh`, in that order, on purpose. `dvh` is the DYNAMIC
 * viewport unit, which is what mobile Safari and the Teams in-app browser
 * actually leave visible once their address bar is on screen; `100vh` there is
 * TALLER than the window and would push the footer just below the fold, which
 * is the same complaint one scroll further down. The `vh` line stays as the
 * fallback for a browser that does not know `dvh`, where it is exactly right.
 *
 * `position: fixed` was NOT used. A pinned bar would cover content on a 320px
 * phone in landscape, and the footer is a colophon, not a toolbar.
 */
body {
  display: flex;
  flex-direction: column;
  min-block-size: 100vh;
  min-block-size: 100dvh;
}

h1, h2, h3, h4, h5, h6 { letter-spacing: 0; color: var(--ink); }
p { letter-spacing: 0; }

a { color: var(--primary); text-decoration: none; transition: color 160ms ease-out; }
a:hover, a:focus { color: var(--primary-deep); text-decoration: underline; text-decoration-thickness: 1px; }

:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* ── Top bar / header ────────────────────────────────────────────────────
 * Powder ground + hairline-strong bottom rule, echoing the careers lockup.
 * A gradient pigment chip precedes the wordmark.
 */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.85rem;
  padding-inline: 1.25rem;
  background: var(--bg);
  border-block-end: 1px solid var(--border-strong);
}
.brand {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  padding-inline-start: 0.85rem;   /* room for the pigment chip */
  line-height: 1.15;
  color: var(--ink);
}
.brand::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 50%;
  translate: 0 -50%;
  inline-size: 6px;
  block-size: 30px;
  border-radius: 2px;
  background: var(--chip-grad);
}
/* Latin wordmark in Bodoni Moda italic — mirrors the careers header lockup. */
.brand-latin {
  font-family: "Bodoni Moda", Didot, "Vazirmatn", serif;
  font-style: italic;
  font-weight: 400;
  font-size: 1.55rem;
  line-height: 1.15;
  letter-spacing: 0;
  color: var(--ink);
}
/* Persian sits below as a taupe subtitle — stays Vazirmatn. */
.brand-fa {
  font-family: var(--font);
  font-weight: 600;
  font-size: var(--fs-small);
  line-height: 1.4;
  color: var(--muted);
}
.topnav { display: flex; align-items: center; gap: 0.75rem; }
/* Ink, not taupe: this is WHO YOU ARE SIGNED IN AS, sitting next to a «خروج»
 * button. At --muted it was quieter than the button beside it, which is the
 * wrong way round for the one piece of identity on the page. */
.user { color: var(--ink); font-size: var(--fs-small); font-weight: 600; }
.logout-form { margin: 0; }

.container {
  max-inline-size: 900px;
  margin-inline: auto;
  padding-inline: 1rem;
  padding-block: 2rem;
  /* EP-025. `<main>` is the only growing child of the column-flex body, so it
   * takes every pixel the header and the footer do not, and the footer is
   * pushed to the bottom edge on a short page without being positioned there.
   * `min-block-size: 0` is NOT set: main must be allowed to grow past the
   * viewport on a long page, which is the default `min-height: auto` for a flex
   * item and the reason nothing here can clip a tall archive. */
  flex: 1 0 auto;
}

/* THE LOGIN PAGE, and (today) only the login page: base.html's `main_class`
 * block. Centres the card in whatever height is left between the header and
 * the footer.
 *
 * `margin-block: auto` on the CARD, not `justify-content: center` on the main.
 * They look identical until the card is taller than the space it is centred in,
 * which at 320px with the break-glass fold OPEN (the `entra_enabled == false`
 * branch) it is: `justify-content: center` overflows a flex container in BOTH
 * directions and the top of the card goes under the header where it cannot be
 * scrolled to. Auto margins collapse to zero on the overflowing side instead,
 * so the card stays reachable and the page simply scrolls.
 *
 * `.card` sets `margin-block-end: 1.25rem`; this shorthand deliberately
 * overrides both edges, which is what makes the two auto margins equal. */
.container--center { display: flex; flex-direction: column; }
.container--center .login-card { margin-block: auto; }

/* Data-dense pages (the leave archive) opt in via base.html's `main_class`.
 * 900px is a READING measure; the archive is a 16-column ledger, and on the
 * COO's 1080p screen it was rendering as a narrow centred card with every
 * useful column pushed off the end. Near-full width with a sensible gutter,
 * capped so a 4K monitor doesn't stretch rows to an unreadable line length.
 * The PAGE still never scrolls sideways — `.tbl-wrap` owns that (see Tables). */
.container-wide {
  max-inline-size: 1680px;
  padding-inline: 1.5rem;
}
@media (max-width: 640px) {
  .container-wide { padding-inline: 0.75rem; }
}

/* Dark footer — ink-deep ground with on-ink text (careers family).
 * The ground stays (it is the careers-family signature), but the BAND is lighter:
 * a wide dark strip carrying one date was the heaviest object on a page whose
 * header is a hairline. Shorter block padding, and the content sits on one
 * centred row at 0.85 opacity so it reads as a colophon, not a second header. */
.footer {
  text-align: center;
  background: var(--ink-deep);
  color: var(--on-ink);
  padding-block: 1.5rem;
  padding-inline: 1rem;
  margin-block-start: 2rem;
  font-size: var(--fs-small);
  border-block-start: 1px solid rgba(246, 239, 234, 0.14);
}
.footer-content {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1.5rem;
  opacity: 0.85;
  line-height: 1.8;
}
/* NOT `.brand-fa`: that class is taupe on powder and would be unreadable on the
 * ink-deep footer ground. Same wordmark, footer palette. */
.footer-brand { font-weight: 600; color: var(--on-ink); }

/* ── Cards — signature: white fill, hairline, rest shadow, pigment edge chip */
.card {
  position: relative;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding-block: var(--card-pad);
  padding-inline: var(--card-pad);
  margin-block-end: 1.25rem;
  box-shadow: var(--shadow-rest);
  overflow: clip;               /* keep the chip inside the rounded corner */
  transition: box-shadow 160ms ease-out;
}
/* Signature pigment chip — 8×40, grows in WIDTH on hover (careers parity). */
.card::before {
  content: "";
  position: absolute;
  inset-inline-start: -1px;
  inset-block-start: 50%;
  translate: 0 -50%;
  inline-size: 8px;
  block-size: 40px;
  background: var(--chip-grad);
  border-start-end-radius: 4px;
  border-end-end-radius: 4px;
  transition: inline-size 160ms ease-out;
}
.card:hover { box-shadow: var(--shadow-hover); }
.card:hover::before { inline-size: 12px; }
.card:focus-within { box-shadow: var(--shadow-hover); }
.card:focus-within::before { inline-size: 12px; }

/* A card that must let a popup escape it. `.card`'s `overflow: clip` keeps the
 * pigment chip inside the radius, and it would also amputate the Jalali date
 * picker the moment the popup is taller than the remaining card. The chip is
 * vertically centred where the card edge is straight, so dropping the clip on
 * this one card costs a 1px sliver nobody can see and no rounded corner. */
.card--overflow { overflow: visible; }

/* 480px, not 440: measured, not guessed. At 440 the hero sign-in label
 * «ورود با حساب مایکروسافت (Teams)» wrapped and left «(Teams)» alone on a
 * second line inside the pill, which reads as a mistake rather than as the
 * page's one confident action (EP-021). 480 puts it on one line at the
 * heading size; the phone breakpoint below drops the size instead. */
.login-card { max-inline-size: 480px; margin-inline: auto; }
/* Editorial page/card title — light 300 weight (careers hero-title feel). */
.card-title {
  margin-block: 0 0.5rem;
  font-size: var(--fs-h2);
  font-weight: 300;
  line-height: 1.4;
  color: var(--ink);
}
.section-title {
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.5;
  margin-block: 1.75rem 0.75rem;
  color: var(--ink);
}

/* ── Forms (RTL, logical props) ──────────────────────────────────────────*/
.form { display: flex; flex-direction: column; gap: 1rem; }
/* A form that is deliberately the SECOND choice on its page (the break-glass
 * login under the Microsoft button). Sunk into a surface panel so it reads as a
 * fallback rather than as a peer of the primary action. Scoped to a modifier on
 * purpose: `.form` itself is every form in the portal and must stay unboxed. */
.form-secondary {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.25rem;
}
.field { display: flex; flex-direction: column; gap: 0.35rem; }
.field-label { font-size: var(--fs-small); font-weight: 600; color: var(--ink); line-height: 1.6; }
.input, select.input, textarea.input {
  inline-size: 100%;
  padding-block: 0.7rem;
  padding-inline: 1rem;
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: 1.5;
  color: var(--ink);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-field);
  background: var(--card);
  text-align: start;
  min-block-size: 48px;         /* ≥48px tap target (§6.1) */
  font-feature-settings: "tnum";
  transition: border-color 160ms ease-out, box-shadow 160ms ease-out;
}
textarea.input { min-block-size: 96px; line-height: 1.8; }
.input::placeholder { color: var(--muted); opacity: 1; }
.input:focus { outline: 2px solid var(--primary); outline-offset: 2px; border-color: var(--primary); }
.amount { font-feature-settings: "tnum"; font-variant-numeric: tabular-nums; }

/* A Latin value inside an RTL page (username, e-mail address). `dir="ltr"` on
 * the element is what actually fixes the caret; this only stops the box itself
 * from being the odd one out, and keeps the tabular figures. */
.input-ltr { text-align: start; font-feature-settings: "tnum"; }

/* File pickers are the last raw-browser control in the portal: a grey system
 * button in the middle of a mulberry pill design. Style the button part only,
 * so the native file dialog and its accessibility are untouched. */
input[type="file"].input {
  padding-block: 0.55rem;
  padding-inline: 0.6rem;
  line-height: 1.6;
}
input[type="file"].input::file-selector-button {
  margin-inline-end: 0.75rem;
  padding-block: 0.35rem;
  padding-inline: 0.9rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
  transition: background-color 160ms ease-out, border-color 160ms ease-out;
}
input[type="file"].input::file-selector-button:hover {
  background: var(--ghost);
  border-color: var(--primary);
}

/* ── Grouped form sections (leave request: 3 fieldsets + responsive grid) ──
 * Each <fieldset class="form-section"> carries a legend and a 2-col responsive
 * grid; hairline rule separates sections. `[hidden]` is set by the reveal script
 * (progressive enhancement, hide-and-clear) — the explicit `.field[hidden]` rule
 * is REQUIRED because `.field{display:flex}` would otherwise beat the UA
 * `[hidden]{display:none}`. */
/* Breathing room between sections is what stops five stacked fieldsets reading
 * as one wall of boxes: the hairline alone was doing all the separating at 16px
 * of padding. The legend is the section's own title and was rendering SMALLER
 * than the labels underneath it, so it goes up to body size. */
.form-section {
  min-inline-size: 0;
  margin: 0;
  padding-block: var(--sp-5);
  padding-inline: 0;
  border: 0;
  border-block-start: 1px solid var(--border);
}
.form-section:first-of-type { border-block-start: 0; padding-block-start: 0; }
.form-section__legend {
  padding-inline: 0;
  margin-block-end: var(--sp-4);
  color: var(--primary);
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 1.6;
}
.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-4);
  align-items: start;
}
.field--span-2 { grid-column: 1 / -1; }
.field-help { color: var(--muted); font-size: var(--fs-small); line-height: 1.7; }
.form-section[hidden], .field[hidden] { display: none; }
@media (max-width: 640px) {
  .form-grid { grid-template-columns: 1fr; }
  .field--span-2 { grid-column: auto; }
}

/* ── Buttons — pill (999px), 600, mulberry primary ───────────────────────*/
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding-block: 0.6rem;
  padding-inline: 1.4rem;
  min-block-size: 48px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font: inherit;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
  transition: background-color 160ms ease-out, color 160ms ease-out,
              border-color 160ms ease-out, opacity 160ms ease-out;
}
.btn:hover { text-decoration: none; }
.btn:active { filter: brightness(0.96); }
.btn:disabled, .btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.btn-block { inline-size: 100%; }
.btn-primary { background: var(--primary); color: var(--primary-ink); }
.btn-primary:hover { background: var(--primary-deep); color: var(--primary-ink); }
.btn-secondary { background: var(--card); color: var(--ink); border-color: var(--border-strong); box-shadow: var(--shadow-rest); }
.btn-secondary:hover { background: var(--surface); color: var(--ink); border-color: var(--muted); box-shadow: var(--shadow-hover); }
.btn-ghost { background: transparent; border-color: var(--border-strong); color: var(--ink); }
.btn-ghost:hover { background: var(--surface); color: var(--ink); }
/* A DESTRUCTIVE ghost: cancelling your own leave request, rejecting someone
 * else's. Same quiet weight as `.btn-ghost` (these sit inside table rows and
 * must not shout over «تأیید»), but in the danger ink so the hand hesitates. */
.btn-danger-ghost { background: transparent; border-color: var(--border-strong); color: var(--danger-ink); }
.btn-danger-ghost:hover { background: var(--danger-bg); color: var(--danger-ink); border-color: var(--danger-ink); }

/* ── Alerts ──────────────────────────────────────────────────────────────
 * Flex row so a leading indicator can sit beside the text without the sentence
 * re-flowing around it. A 3px edge rule alone is easy to skim past on a phone,
 * which is exactly where these messages matter most. */
.alert {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding-block: 0.75rem;
  padding-inline: 1rem;
  border-radius: var(--radius-field);
  margin-block-end: 1rem;
  border-inline-start: 3px solid transparent;
  font-size: var(--fs-body);
  line-height: 1.8;
}
.alert-error { background: var(--danger-bg); color: var(--danger-ink); border-inline-start-color: var(--danger-ink); }
.alert-warn { background: var(--warn-bg); color: var(--warn-ink); border-inline-start-color: var(--warn-ink); }
/* A success must NOT borrow the warning colour: an accepted daily report shown
   in amber reads as a failure and invites a second submit. */
.alert-success { background: var(--ok-bg); color: var(--ok-ink); border-inline-start-color: var(--ok-ink); }

/* ── The EP-024 receipt, «چی ثبت شد» ────────────────────────────────────────
 * COO 2026-09-07. Rendered by `base.html` at the top of <main> on the ONE page
 * that follows a successful submission, from `web/templates/_receipt.html`.
 *
 * A SUCCESS ALERT WITH ROWS, not a second kind of card: it borrows the sage
 * `--ok-*` palette the success alert already owns, so «this worked» reads the
 * same everywhere in the portal. What it adds is the part the COO asked for,
 * which is the FACTS, laid out as a definition list somebody can check against
 * what they typed.
 *
 * `receipt--static` is the fallback contract and carries the whole reason this
 * needs no JavaScript. Two browsers to survive:
 *
 *   NO <dialog>   the tag is an unknown element and defaults to `display:
 *                 inline`, which would spill the heading, the rows and the
 *                 button into one run of text.
 *   WITH <dialog> the UA stylesheet gives a NON-MODAL open dialog
 *                 `position: absolute` with `inset: 0` and auto margins, which
 *                 centres it inside the nearest positioned ancestor and lifts
 *                 it clean out of the flow of the page.
 *
 * `display: block` + `position: static` + zeroed insets answers both, so the
 * panel sits in the document where it was written either way. Nothing here may
 * become `position: fixed`: a floating confirmation over an RTL page would land
 * on top of the content it is confirming.
 */
.receipt {
  margin-block: 0 1.25rem;
  padding-block: 1rem;
  padding-inline: 1.15rem;
  border: 1px solid var(--ok-ink);
  border-inline-start-width: 3px;
  border-radius: var(--radius-field);
  background: var(--ok-bg);
  color: var(--ok-ink);
  font-size: var(--fs-body);
  line-height: 1.9;
  max-inline-size: 100%;
}
.receipt--static {
  display: block;
  position: static;
  /* Logical, like everything else in this file: `inset: auto` would be the same
   * four values, spelled in physical sides. */
  inset-block: auto;
  inset-inline: auto;
  margin-inline: 0;
  inline-size: auto;
  max-block-size: none;
}
/* ── EP-043: closed means closed, and the popup the COO asked for ──────────
 * COO, 2026-09-22, on the retraction receipt: «دکمهٔ بستن کاری نمی‌کند» and
 * «رسید را وسط صفحه به صورت پاپ‌آپ نشان بده».
 *
 * THE DEFECT, and it lived in the two declarations directly above. A
 * `<form method="dialog">` DOES close the dialog: the button drops the `open`
 * attribute exactly as designed. What it cannot do is beat a stylesheet. The
 * UA rule that hides a closed dialog is `dialog:not([open]) { display: none }`,
 * one element plus one attribute; `.receipt--static { display: block }` is a
 * class from an author sheet, so it wins the cascade and the panel stays on the
 * page with the `open` attribute already gone. Measured in headless Chrome on
 * 2026-09-22: after `close()`, a bare dialog computes `display: none` while the
 * same dialog carrying `receipt--static` computes `display: block` and still
 * paints a box. The button was never dead; it was overruled.
 *
 * The rule below is the answer and it is deliberately the narrow one: it hides
 * the receipt only once the dialog is actually closed, so `receipt--static`
 * keeps doing its whole job while the window is open. `!important` because the
 * point is to be unbeatable by anything later in this file, which is the exact
 * mistake being repaired.
 *
 * The no-<dialog> path is UNTOUCHED. A browser that does not implement the tag
 * never sets or clears `open`; the attribute stays on the element from the
 * server render, `:not([open])` never matches, and «بستن» still submits its GET
 * to the current page, which re-renders WITHOUT the receipt because the flash
 * was already popped. Two routes, one outcome, exactly as before. */
.receipt:not([open]) { display: none !important; }

/* `receipt--modal` is what `static/js/receipt.js` swaps in for `receipt--static`
 * when the browser has a real `showModal()`. Nothing here may be
 * `position: fixed`: it does not need to be. A modal dialog is in the TOP LAYER
 * and the UA already centres it; declaring our own placement would only fight
 * that and would re-break the non-modal fallback the moment the class leaked
 * onto a static panel.
 *
 * `inline-size` rather than `max-inline-size` alone, so a two-row receipt and a
 * six-row one are the same width and the popup does not resize under the reader
 * between one submission and the next. `min()` keeps it off the edges at 320px.
 * The block cap plus `overflow: auto` is for the long window (a delivery
 * receipt with every field filled) on a short phone in landscape: without it
 * the bottom of the panel, «بستن» included, is unreachable. */
.receipt--modal {
  inline-size: min(34rem, calc(100vw - 2rem));
  max-block-size: calc(100vh - 4rem);
  overflow: auto;
  /* `margin: auto` on all four sides is not decoration, it IS the centring: the
   * UA gives a modal `inset: 0` and leaves the auto margins to place it. The
   * base `.receipt` sets `margin-block: 0 1.25rem` for the panel it normally
   * is, which pins the popup to the TOP of the screen - measured, 2026-09-22,
   * before this line existed. */
  margin-block: auto;
  margin-inline: auto;
  box-shadow: 0 18px 48px rgba(23, 18, 21, 0.28);
}
/* Same dim as the feedback dialog: one modal look in the portal, not two. */
.receipt--modal::backdrop { background: rgba(23, 18, 21, 0.45); }
.receipt__heading {
  margin: 0;
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.5;
  color: var(--ok-ink);
}
/* What was recorded, in one sentence, before the table of facts. */
.receipt__what { margin-block: 0.15rem 0.75rem; color: var(--ink); }
/* Two columns where there is room, one where there is not. `auto` on the label
 * keeps «مدت» and «جایگزین» on one line each instead of ragging the values. */
.receipt__list {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.15rem 1rem;
}
.receipt__row { display: contents; }
.receipt__label { color: var(--muted); font-size: var(--fs-small); font-weight: 600; }
.receipt__value { margin: 0; color: var(--ink); overflow-wrap: anywhere; }
.receipt__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  margin-block-start: 0.85rem;
}
.receipt__link { color: var(--ok-ink); font-weight: 600; }
/* The close control is a real <form method="dialog">; strip the block margin a
 * bare form would otherwise contribute to the row. */
.receipt__close { margin: 0; }
.receipt__btn { min-block-size: 40px; }
/* 320px: the label column stops earning its width once the value has to wrap
 * anyway, so the pair stacks. */
@media (max-width: 480px) {
  .brand-fa { display: none; }
  .receipt__list { grid-template-columns: 1fr; gap: 0 0; }
  .receipt__value { margin-block-end: 0.4rem; }
}

/* Screen-reader-only / layout-only. Used for the form's hidden default submit
   button, which is what makes Enter submit the report instead of clicking
   «افزودن ردیف» (the first submit in tree order otherwise). */
.visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.divider {
  display: flex;
  align-items: center;
  text-align: center;
  color: var(--muted);
  font-size: var(--fs-small);
  margin-block: 1.25rem;
}
.divider::before, .divider::after { content: ""; flex: 1; border-block-start: 1px solid var(--border); }
.divider span { padding-inline: 0.75rem; }

.hint { color: var(--muted); font-size: var(--fs-small); line-height: 1.7; }
.latin-id { unicode-bidi: isolate; }

/* «nothing here yet» deserves a shape, not a stray grey sentence adrift in an
 * otherwise empty card. A dashed panel says «this list exists and is empty»,
 * which is a different message from «the page failed to load». */
.empty-state {
  padding-block: 2.5rem;
  padding-inline: 1rem;
  text-align: center;
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: var(--fs-small);
  line-height: 1.9;
}

/* Status pill. Reading a column of identical grey sentences to find the rejected
 * row is the slowest thing anyone does on the archive; colour does it at a
 * glance. Colours come from the existing token pairs, which are already the
 * AA-checked bg/ink couples used by the alerts. */
.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-block: 0.1rem;
  padding-inline: 0.6rem;
  border-radius: var(--radius-pill);
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.7;
  white-space: nowrap;
}
.badge--pending { background: var(--warn-bg); color: var(--warn-ink); }
.badge--approved { background: var(--ok-bg); color: var(--ok-ink); }
.badge--rejected { background: var(--danger-bg); color: var(--danger-ink); }
/* Cancelled is neither a failure nor an approval: the employee withdrew it. */
.badge--neutral { background: var(--band); color: var(--muted); }

/* One way back. Every module page below the home screen was a dead end: the
 * only route out was the browser's back button, which in the Teams in-app
 * browser is not always where the thumb expects it. */
.back-link {
  display: inline-block;
  margin-block-end: 1rem;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--muted);
}
.back-link:hover, .back-link:focus { color: var(--primary); }
/* Was `style="color:#b00"` on the archive filter error. The edge CSP is
 * `style-src 'self'`, so that attribute was BLOCKED and the only Persian date
 * error the page can show was rendering in plain muted grey. */
.form-error { color: var(--danger-ink); font-weight: 600; }

/* ── Lists / key-value ───────────────────────────────────────────────────*/
.module-list { list-style: none; padding-inline: 0; }
.module-item {
  padding-block: 0.6rem;
  padding-inline: 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-field);
  margin-block-end: 0.5rem;
}
/* `max-content` alone let one long key («پاسخ پرسش امروز») set the width of the
 * key column for the whole list, which on a phone crushed every VALUE into a
 * ribbon. A floor keeps short keys aligned, and below 560px the pair simply
 * stacks, which is the only readable shape for a long Persian answer. */
.kv {
  display: grid;
  grid-template-columns: minmax(6.5rem, max-content) 1fr;
  gap: 0.4rem 1.5rem;
}
.kv dt { color: var(--muted); font-size: var(--fs-small); }
.kv dd { margin-inline-start: 0; font-weight: 600; }
.prewrap { white-space: pre-wrap; }
.thumb {
  max-inline-size: 100%;
  block-size: auto;
  border-radius: var(--radius-field);
}
@media (max-width: 560px) {
  .kv { grid-template-columns: 1fr; gap: 0; }
  .kv dt { margin-block-start: 0.75rem; }
}

/* ── Home module tiles (role-aware navigation) ───────────────────────────
 * The section title ("مرخصی") sits above a responsive grid of equal-size
 * tiles — one per permitted destination. Replaces the old cramped flex-wrap
 * pill row (5-in-a-row + 1 orphan) the COO flagged as under-scaled. Tiles are
 * uniform regardless of label length (auto-fill keeps aligned columns), read
 * as first-class "big cards", and carry the careers pigment-chip family cue.
 */
.module-card .card-title { margin-block-end: 1rem; }
.module-title { font-size: var(--fs-h2-sm); }

.module-tiles {
  /* Centered flex-wrap (was grid auto-fill). Broken flex-basis per breakpoint
   * makes a short last row CENTER instead of left-aligning, so the layout reads
   * symmetric: desktop 3+2 centered, tablet 2+2+1 centered, mobile 1-per-row.
   * All tiles stay equal height (see .module-tile__label 2-line reserve). */
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  /* 8px between two 64px-tall tap targets is a fat-finger trap on a phone; 12px
   * is the smallest gap that reliably separates them. The flex-basis formulas
   * below subtract THIS token, so the two must always change together. */
  gap: var(--sp-3);
}

/* Group header INSIDE the tile row. `flex: 0 0 100%` makes it a full-width
 * break in the wrap flow, so the tiles after it start a fresh row without a
 * second container. Small, taupe and quiet on purpose: it is a divider, not a
 * heading that competes with the card title above it. `text-align: start` keeps
 * it hard against the right edge in RTL, aligned with the tiles it labels,
 * despite the parent's `justify-content: center`.
 *
 * Set only by `_finance_links`, and only when that tile carries BOTH its groups
 * (see main.py). Every other module renders no group titles at all. */
.module-tiles__group-title {
  flex: 0 0 100%;
  margin-block: 0;
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.7;
  text-align: start;
}
/* Air ABOVE a second group, none above the first (the card title already sits
 * right there and a double gap reads as a broken card). */
.module-tiles__group-title:not(:first-child) { margin-block-start: var(--sp-2); }

/* Each tile echoes the card signature (white fill, hairline, rest shadow,
 * inline-start pigment chip) but at compact nav scale: a per-section glyph at
 * the start (right in RTL) is the primary identifier, then the label. */
.module-tile {
  position: relative;
  flex: 0 1 100%;                        /* mobile: 1 tile per row (basis broken per breakpoint below) */
  min-inline-size: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--sp-2);
  min-block-size: 64px;                  /* compact; whole tile is the tap target (≥44px) */
  padding-block: var(--sp-3);
  padding-inline: var(--sp-4) var(--sp-3);   /* start room clears the chip */
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-rest);
  color: var(--ink);
  text-decoration: none;
  overflow: clip;                        /* keep the chip inside the radius */
  transition: box-shadow 160ms ease-out, border-color 160ms ease-out,
              transform 160ms ease-out;
}
/* Broken flex-basis => centered symmetric rows (justify-content:center on the
 * flex parent). Tablet 2-up, desktop 3-up; the gap is subtracted so N tiles +
 * (N-1) gaps fit one row exactly. */
@media (min-width: 560px) { .module-tile { flex-basis: calc((100% - var(--sp-3)) / 2); } }
@media (min-width: 860px) { .module-tile { flex-basis: calc((100% - (2 * var(--sp-3))) / 3); } }
/* Pigment edge chip — grows in width on hover (careers parity with .card). */
.module-tile::before {
  content: "";
  position: absolute;
  inset-inline-start: -1px;
  inset-block-start: 50%;
  translate: 0 -50%;
  inline-size: 5px;
  block-size: 20px;
  background: var(--chip-grad);
  border-start-end-radius: 4px;
  border-end-end-radius: 4px;
  transition: inline-size 160ms ease-out;
}
/* KEYBOARD focus is unconditional: a Bluetooth keyboard on a tablet is still a
 * pointer-less device and the focus ring must never depend on a hover query. */
.module-tile:focus-visible {
  color: var(--ink);
  text-decoration: none;
  border-color: var(--border-strong);
  box-shadow: var(--shadow-hover);
  transform: translateY(-2px);           /* lift */
}
.module-tile:focus-visible::before { inline-size: 10px; }
/* HOVER is gated on an actual pointer. A touch browser (the Teams in-app one
 * included) emulates :hover on tap and then LEAVES IT STUCK: the tile stayed
 * lifted and shadowed after the finger left, which reads as a broken control. */
@media (hover: hover) {
  .module-tile:hover {
    color: var(--ink);
    text-decoration: none;
    border-color: var(--border-strong);
    box-shadow: var(--shadow-hover);
    transform: translateY(-2px);
  }
  .module-tile:hover::before { inline-size: 10px; }
}
.module-tile:active { transform: translateY(0); }

/* ── Tile weight by `kind` (main._finance_links) ─────────────────────────────
 * Nine equally-loud tiles on one card is the crowding this exists to undo. An
 * ARCHIVE is a back catalogue somebody opens once a month: it keeps its
 * hairline and its chip (still obviously the same family, still a ≥44px tap
 * target) but drops the fill and the shadow, so it reads as a footnote under
 * the forms rather than a peer of them. */
.module-tile--muted {
  min-block-size: 44px;
  box-shadow: none;
  background: transparent;
}
/* The 2-line label reserve is what makes every tile 64px tall, so without this
 * the min-block-size above would never be reached and the rule would be dead
 * CSS. One line is enough here: both archive labels are short. */
.module-tile--muted .module-tile__label { min-block-size: 0; }

/* A QUEUE is work waiting on this person, not a place they chose to go. The
 * existing pigment chip runs the FULL height of the tile instead of its 20px
 * stub: a marked row, in the palette the card already uses, with no new colour
 * token invented for it. Hover still widens it, exactly as everywhere else. */
.module-tile--queue::before { block-size: 100%; }

.module-tile__label {
  flex: 1 1 auto;
  min-inline-size: 0;                    /* allow long labels to wrap, not push */
  min-block-size: calc(2 * 1.35em);      /* reserve 2 lines so every tile is equal height */
  display: flex;
  align-items: center;
  font-weight: 600;
  font-size: 0.9375rem;                  /* 15px — a touch smaller than body */
  line-height: 1.35;
  color: var(--ink);
  text-align: start;
  text-wrap: balance;
  overflow-wrap: anywhere;
}
/* Per-section glyph (leading, mulberry). This is the tile's main identifier now,
 * so it sits fully opaque; the generic chevron is gone. */
.module-tile__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
  color: var(--primary);
  opacity: 0.9;
  transition: opacity 160ms ease-out;
}
.module-tile__icon .tile-glyph { display: block; }
.module-tile:focus-visible .module-tile__icon { opacity: 1; }
@media (hover: hover) {
  .module-tile:hover .module-tile__icon { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .module-tile:hover, .module-tile:focus-visible { transform: none; }
}

/* ── Leave module (tables, balance box, tags, inline fields) ─────────────*/
.row { display: flex; gap: 1rem; flex-wrap: wrap; }
.row > .field { flex: 1; min-inline-size: 9rem; }
.field-inline { display: flex; align-items: center; gap: 0.5rem; color: var(--ink); min-block-size: 48px; }
.field-inline input[type="checkbox"] { flex: 0 0 auto; inline-size: 20px; block-size: 20px; accent-color: var(--primary); }

/* Balance callout — warm surface + mulberry edge, tnum figures (no blue). */
.balance-box {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-inline-start: 3px solid var(--primary);
  border-radius: var(--radius-field);
  padding-block: 0.8rem;
  padding-inline: 1rem;
  margin-block-end: 1.25rem;
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
}
.balance-box strong { font-weight: 600; color: var(--primary); }
/* The three components of the balance were one long parenthetical sentence
 * («تخصیص X + انتقالی Y − استفاده‌شده Z»), which is arithmetic pretending to be
 * prose. Three labelled figures on their own row can be read instead of parsed. */
.balance-breakdown {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.5rem;
  margin-block-start: 0.4rem;
}
.balance-breakdown .hint { line-height: 1.9; }

/* ── Tables ───────────────────────────────────────────────────────────────
 * `.tbl-wrap` is the scroll container every `.tbl` lives inside, at EVERY
 * viewport width — not just on a phone. The card is `overflow: clip` (that
 * clip is load-bearing: it keeps the pigment edge chip inside the radius), so
 * a 9-column queue table plus an inline reject form is simply cut off at the
 * card's inline-start edge with no way to reach it. The wrapper takes that
 * overflow instead and turns it into a scroll, which makes the card's clip
 * harmless for wide content.
 */
.tbl-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.tbl { inline-size: 100%; border-collapse: collapse; font-feature-settings: "tnum"; font-variant-numeric: tabular-nums; }
.tbl th, .tbl td {
  text-align: start;             /* logical — never left/right */
  padding-block: 0.6rem;
  padding-inline: 0.7rem;
  border-block-end: 1px solid var(--border);
  font-size: var(--fs-small);
  line-height: 1.7;
}
.tbl th { color: var(--muted); font-weight: 600; }
.tbl tbody tr:hover { background: var(--surface); }

/* ── Sortable column headers (`templates/_sort_header.html`) ──────────────
 * Four classes, all emitted by that one macro: `.sort-th` on the `<th>`,
 * `.is-sorted` when this is the column the list is ordered by, `.sort-link` on
 * the anchor, `.sort-arrow` on the glyph. (`.visually-hidden`, the spoken
 * sentence the macro also emits, already exists above.)
 *
 * THE `<th>` STAYS A `<th>`. No `display` on it, deliberately: a header cell
 * given `display: flex` or `block` leaves table layout, stops sizing with its
 * column and stops being announced as the column header. The flex box goes on
 * the ANCHOR, which is an inline box inside the cell and is free to be one.
 *
 * THE ARROW'S POSITION IS THE BIDI ALGORITHM'S, NOT THIS FILE'S. The macro puts
 * the glyph in its own `dir="ltr"` span so it lands on the same side of the
 * label in every column; the portal is RTL, so that side is the label's LEFT.
 * The spacing is therefore a flex `gap` on the link and NOT a margin on the
 * arrow: a margin resolves against the ARROW SPAN's own direction (ltr, because
 * of that attribute) while the space it needs is on the side the RTL parent put
 * it, so `margin-inline-start` would open the gap on the wrong side of the
 * glyph. `gap` is laid out by the container and has no such ambiguity. Nothing
 * here sets `direction`, `float` or `position` on `.sort-arrow`.
 */
.sort-th .sort-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;                  /* see the note above: gap, never a margin */
  color: inherit;                /* a header row that reads as a header row */
  text-decoration: none;
  white-space: nowrap;           /* the label and its arrow never split */
  cursor: pointer;
  padding-block: 0.1rem;
  padding-inline: 0.15rem;
  border-radius: var(--radius-field);
}
.sort-th .sort-link:hover,
.sort-th .sort-link:focus-visible {
  color: var(--primary);
  text-decoration: underline;
  text-underline-offset: 3px;    /* clear of the Persian descenders */
}
/* The focus ring is written out rather than left to the global `:focus-visible`
 * rule, because this is a table people move through with the keyboard and the
 * ring is the only thing that says which header Enter would press. The tint is
 * a second, redundant carrier of the same fact for anyone who cannot see a
 * 2px mulberry line against a hairline border. */
.sort-th .sort-link:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  background: var(--ghost);
}
.sort-arrow {
  font-size: 0.9em;
  line-height: 1;
  opacity: 0.45;                 /* present, quiet: the affordance is visible */
  transition: opacity 160ms ease-out;
}
.sort-th .sort-link:hover .sort-arrow,
.sort-th .sort-link:focus-visible .sort-arrow { opacity: 1; }

/* THE ACTIVE COLUMN, WITHOUT RELYING ON COLOUR. Three signals, and two of them
 * survive greyscale, a colour-vision deficiency and a very dim screen:
 *   1. the arrow GLYPH itself changes (↕ offered -> ↑/↓ applied) and goes to
 *      full opacity, which is shape, not hue;
 *   2. the header goes from 600 to 700 and the column's bottom hairline becomes
 *      a 2px rule, which is weight, not hue;
 *   3. and only then the mulberry tint, which is the reinforcement rather than
 *      the message.
 * `aria-sort` on the `<th>` carries the same fact to assistive tech; the macro
 * owns that, this file must not contradict it. */
.sort-th.is-sorted {
  color: var(--primary);
  font-weight: 700;
  background: var(--ghost);
  border-block-end: 2px solid var(--primary);
}
.sort-th.is-sorted .sort-arrow { opacity: 1; }
/* In `.tbl-archive` the pinned «متقاضی» column keeps its opaque `--card` fill
 * (that rule is more specific and must stay: without an opaque cell the scrolled
 * content shows straight through it). Sorting on that column therefore loses
 * signal 3 and keeps signals 1 and 2, which is exactly why 1 and 2 exist. */
/* The action cell must stay a REAL table-cell. Giving `.actions` itself
 * `display: flex` takes the `<td>` out of table layout, so it no longer
 * aligns to its row and its buttons drift up over the header. Keep the td as
 * a cell and put the flex on the forms inside it, where it belongs. */
.actions form { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }
.actions form + form { margin-block-start: 0.5rem; }
.btn-sm { min-block-size: 36px; padding-block: 0.3rem; padding-inline: 0.9rem; font-size: var(--fs-small); }
.input-sm { min-block-size: 36px; padding-block: 0.3rem; padding-inline: 0.6rem; inline-size: 8rem; }

/* The flex lives on a DIV inside the cell, never on `.actions` itself — see the
 * note above: `display: flex` on the <td> takes it out of table layout and its
 * buttons drift up over the header row. */
.action-group { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: flex-start; }

/* ── Reject, folded away ──────────────────────────────────────────────────
 * The reject form is a <select> + a text box + a button, and it was rendered
 * OPEN on every row of every approval queue. One cell therefore set the width of
 * the whole table and the queue became a sideways scroll on a phone, where these
 * decisions are actually taken. Native <details> hides it behind one small
 * button with ZERO JavaScript, which the strict CSP (`script-src 'self'`)
 * rewards, and keyboard/screen-reader behaviour comes free with the element.
 *
 * The panel is IN FLOW, not an absolutely-positioned popover: `.tbl-wrap` is
 * `overflow-x: auto`, and a box with a non-visible overflow on one axis computes
 * the other axis to `auto` too, so a popover hanging below the last row would be
 * clipped by the scroll container. Growing the row instead cannot be clipped. */
/* EP-022 (COO report 2026-09-05): the expense detail page's «پرداخت‌ها و
 * فاکتورهای این هزینه» list. An expense is usually settled in a few
 * instalments, so the outer list is one entry per payment and the inner one is
 * that payment's invoices; the indent is what says «these files belong to that
 * payment» rather than to the expense. No new colour and no new box: this is a
 * list on a detail page, and `.hint` already carries the muted line under a
 * payment that has no invoice. */
.child-payments { margin-block: 0.5rem 0; padding-inline-start: 1.2rem; }
.child-payments > li { margin-block-end: 0.6rem; }
.child-payments > li > ul { margin-block: 0.25rem 0; padding-inline-start: 1.2rem; }
.child-payments .hint { margin-block: 0.25rem 0; }

/* EP-030 (COO request 2026-09-09): «شبای ذی‌نفع از پرداخت‌های قبلی». A radio
 * list of the beneficiary's known account numbers, all of them MASKED, plus a
 * «شبای جدید» row that reveals the ordinary input. No new colour and no new
 * component: it is a fieldset built from `.field-label`, `.hint` and
 * `.input-account`, which are the same three the queue rows already use for an
 * account number.
 *
 * The native radio stays visible and focusable, exactly as the WHO-5 scale and
 * the plan options argue: this is money leaving an account, and a control that
 * looks custom is a control a keyboard user has to guess at. */
.iban-options { border: 1px solid var(--border); border-radius: var(--radius-field); padding: 0.75rem; margin: 0; }
.iban-options > .field-label { padding-inline: 0.25rem; }
.iban-option { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.5rem; align-items: baseline; padding-block: 0.35rem; }
.iban-option > .hint { grid-column: 2; margin: 0; }
.iban-option + .iban-option { border-block-start: 1px solid var(--border); }
.iban-option__new { padding-block-start: 0.5rem; }

.reject-action { display: inline-block; }
.reject-action > summary {
  list-style: none;
  cursor: pointer;
  /* `display: inline-flex` from `.btn` also removes the default disclosure
   * triangle in Firefox; the WebKit pseudo-element below covers Safari. */
}
.reject-action > summary::-webkit-details-marker { display: none; }
.reject-action[open] > summary { background: var(--surface); border-color: var(--danger-ink); color: var(--danger-ink); }
.reject-panel {
  margin-block-start: 0.5rem;
  padding: 0.7rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-field);
  min-inline-size: 13rem;
}
/* Stacked, because a select + a free-text reason + a submit in one line is what
 * made the cell wide in the first place. */
.actions .reject-panel form { flex-direction: column; align-items: stretch; gap: 0.5rem; }
.reject-panel .input-sm { inline-size: 100%; }

/* ── Archive ledger ───────────────────────────────────────────────────────
 * `.tbl-archive` is the wide read-only history table. Cells must NOT wrap into
 * three-line paragraphs at desktop width or 16 columns become unscannable, so
 * the free-text column gets a fixed measure with an ellipsis and the full text
 * on the `title` tooltip (and, in full, on the detail page).
 */
.tbl-archive td, .tbl-archive th { white-space: nowrap; }

/* WHOSE ROW IS THIS. Sixteen columns means the wrapper scrolls, and by the time
 * «تاریخ ثبت» is on screen the applicant's name is long gone: every row looks
 * identical and the reader has to scroll back to place a single value.
 *
 * ONLY «متقاضی» is pinned, deliberately not the «#» column beside it. Pinning
 * two columns needs the second one's offset to equal the first one's rendered
 * width, and in an auto-layout table that width comes from the widest id in the
 * page — a hard-coded `inset-inline-start: 2.5rem` is a gap or an overlap the
 * day someone's request id reaches five digits. The name is the identifier a
 * human actually reads; the id scrolls away behind it.
 *
 * The opaque background is load-bearing: without it the scrolled-under cells
 * show straight through the pinned one. */
.tbl-archive th:nth-child(2),
.tbl-archive td:nth-child(2) {
  position: sticky;
  inset-inline-start: 0;
  z-index: 2;
  background: var(--card);
  border-inline-end: 2px solid var(--border-strong);
}
/* The row hover tint has to reach the pinned cell too, or the hovered row breaks
 * in half exactly where the eye is following it. */
.tbl-archive tbody tr:hover td:nth-child(2) { background: var(--surface); }
/* The header cell sits above the body cells AND above the scrolled content. */
.tbl-archive thead th:nth-child(2) { z-index: 3; background: var(--card); }
/* An inline-block, NOT the <td>: `text-overflow` is unreliable on a table cell
 * because the cell's width comes from table layout, not from `max-width`. The
 * text is already cut server-side (`|truncate`) with the full value on `title`;
 * this is the belt to that braces for a long unbroken token. */
.tbl-archive .cell-note {
  display: inline-block;
  vertical-align: bottom;
  max-inline-size: 22rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Secondary line inside a cell (the supervisor's decision date under the name). */
.cell-sub {
  display: block;
  color: var(--muted);
  font-size: 0.78rem;
  line-height: 1.5;
}
/* The archive filter row. It had no rule at all, so the six controls sat in
 * default inline flow and re-flowed into a ragged block on a narrow window. */
.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-block-end: 0.75rem;
}

/* ── Two-column detail tables (`<table class="tbl tbl-detail">`) ──────────
 * A key/value table is NOT a data grid: with no fixed measure the key column
 * took whatever width the longest label wanted and the boundary between «field»
 * and «value» dissolved. A fixed 30/70 split plus a tinted key column restores
 * it. Stacks below 560px, where 30% of a phone is not a column, it is a sliver. */
.tbl-detail { table-layout: fixed; }
.tbl-detail th {
  inline-size: 30%;
  background: var(--surface);
  color: var(--muted);
  border-inline-end: 1px solid var(--border);
  vertical-align: top;
  white-space: normal;
}
.tbl-detail td { inline-size: 70%; vertical-align: top; white-space: normal; }
@media (max-width: 560px) {
  .tbl-detail, .tbl-detail tbody, .tbl-detail tr { display: block; }
  .tbl-detail th, .tbl-detail td {
    display: block;
    inline-size: auto;
    border-inline-end: 0;
  }
  .tbl-detail th { border-block-end: 0; padding-block-end: 0; }
}

/* ── Pagination ───────────────────────────────────────────────────────────
 * «صفحهٔ ۲ از ۷» used to sit in a hint paragraph above the table while the two
 * arrows lived under it, so the reader's position and the controls that change
 * it were never in the same glance. */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem 1.5rem;
  margin-block-start: 1.5rem;
  padding-block-start: 1rem;
  border-block-start: 1px solid var(--border);
}
.pager-info {
  font-size: var(--fs-small);
  color: var(--muted);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
}

/* Social-security chip — pigment wash, pill (retonalised from amber). */
.tag {
  display: inline-block;
  background: var(--ghost);
  color: var(--primary);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding-block: 0.08rem;
  padding-inline: 0.55rem;
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: 1.5;
}

/* ── Responsive — narrow-screen table behaviour ──────────────────────────
 * Scrolling is no longer the table's job: `.tbl-wrap` owns it at every width.
 * The table must therefore stay `display: table` here, because `display:
 * block` on a phone would break the same row/cell alignment the wrapper was
 * added to preserve. All that is left for the narrow breakpoint is refusing
 * to wrap cell text, so columns keep their shape and the wrapper scrolls
 * instead of the rows growing tall.
 */
@media (max-width: 640px) {
  .tbl {
    white-space: nowrap;
  }
}

/* ── htmx in-flight feedback ──────────────────────────────────────────────
 * htmx puts `.htmx-request` on the element that issued the request for exactly
 * as long as it is in flight. Without a rule for it the portal gave NO sign
 * that «افزودن ردیف» or «حذف» had been pressed, and on a phone on a mobile
 * network the employee reasonably concludes the button is dead and taps it
 * three more times. Dimming plus pointer-events: none both shows the work and
 * makes the extra taps impossible.
 *
 * Scoped to buttons: the modules also use htmx on a bare keep-alive <div>
 * (`hx-trigger="every 10m"`), and a blanket rule would flicker the page every
 * ten minutes for a request the employee never made. */
.btn.htmx-request { opacity: 0.55; pointer-events: none; }

/* Money forms (finance module, council SA-9). The button rule above guards the
 * button; this guards the whole FORM, because on the payment queue the second
 * click that must not land is not always on the same control: the row carries a
 * transaction-reference box and a submit button, and «ثبت پرداخت» pressed twice
 * is a duplicated bank transfer, not a duplicated task row. htmx puts
 * `.htmx-request` on the element issuing the request, which for `hx-post` on a
 * <form> is the form itself, so the whole row goes inert for exactly as long as
 * the request is in flight. */
form.money-form.htmx-request { pointer-events: none; opacity: 0.7; }

/* Latin, digit-only fields inside an RTL document: شبا, کد ملی, شماره تراکنش
 * (council SA-9). `.input` is `text-align: start`, so without an explicit
 * direction an IBAN is laid out right-to-left and the caret jumps on every
 * keystroke. `dir="ltr"` on the element does the layout; this only pins the
 * numerals to a tabular face so a 26-character شبا can be read in groups. */
.input-account { font-feature-settings: "tnum"; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }

/* ── Reduced motion — freeze transitions; chip stays at rest size ─────────*/
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0s !important; animation: none !important; }
  .card:hover::before, .card:focus-within::before { inline-size: 8px; }
}

/* ── Daily report — task rows, static question, mood scale ───────────────
 * Logical properties only (the whole portal is RTL). No inline styles anywhere
 * in the module: the live CSP is `style-src 'self'`, which blocks a style=""
 * attribute exactly as surely as script-src blocks an inline <script>.
 *
 * .task-row__num is a pure CSS counter, so adding a row (htmx append) and
 * removing one (hx-swap="delete") both renumber correctly with ZERO JavaScript.
 */
.task-rows { counter-reset: taskrow; display: flex; flex-direction: column; gap: 0.6rem; }

/* GRID, not flex-wrap. The old row was a wrapping flex line, so as soon as the
   text box hit its 18rem basis the duration box wrapped onto a line of its own
   and the row stopped reading as one thing. A grid keeps description and
   duration side by side, and on a narrow phone it re-flows to a DELIBERATE
   two-line shape instead of whatever wrapping happened to produce. */
/* Description and duration stay SIDE BY SIDE at every width. The previous
   version dropped to a deliberate two-line shape below 560px, which is exactly
   the "duration is under the description" the COO rejected: he was looking at
   a phone. Losing the separate number column (the counter now lives inside the
   label) buys back the room that makes one line fit. */
.task-row {
  counter-increment: taskrow;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 7.5rem auto;
  grid-template-areas: "text hours remove";
  gap: 0.4rem 0.6rem;
  align-items: end;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2, #fbfaf9);
}
.task-row:focus-within { border-color: var(--primary); }
.task-row__text   { grid-area: text; }
.task-row__hours  { grid-area: hours; }
.task-row__remove { grid-area: remove; align-self: end; }

/* «۱. شرح کار» - the counter is part of the label, next to what it numbers. */
.task-row__num::before {
  content: counter(taskrow) ". ";
  color: var(--primary);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* Both labels must occupy the same height or the two inputs stop aligning:
   «مدت زمان انجام کار» wraps to two lines in a 7.5rem track and «شرح کار»
   never does. A fixed min height on the label row settles it without a
   media query. */
.task-row .field-label { min-block-size: 2.4em; display: flex; align-items: flex-end; }
.task-row__hours .input { inline-size: 100%; }

@media (max-width: 560px) {
  /* Only shrink, never stack. */
  .task-row { grid-template-columns: minmax(0, 1fr) 5.5rem auto; padding-inline: 0.5rem; gap: 0.35rem 0.4rem; }
  /* The 3.2em reserve was a hack around «مدت زمان انجام کار» wrapping to two
     lines in a 5.5rem track. The label is now «مدت زمان», which fits on one, so
     both labels are back on the same single-line reserve as the desktop rule
     and the alignment survives a font-size change or a browser zoom. */
  .task-row .field-label { font-size: 0.78rem; }
  /* 44px square: the platform minimum. This button sits one gap away from the
     duration box, so an undersized target does not just miss, it hits the field
     next to it and the employee edits a number they meant to delete a row. */
  .task-row__remove {
    min-block-size: 44px;
    min-inline-size: 44px;
    padding-inline: 0.5rem;
  }
}

/* Instruction that has to be READ, not skimmed past. Used above the task list
   because the same sentence sitting underneath was being read after the
   mistake it prevents. */
.callout {
  border-radius: 10px;
  padding: 0.7rem 0.9rem;
  margin-block-end: 0.8rem;
  line-height: 1.9;
  border-inline-start: 4px solid var(--primary);
  background: var(--surface-2, #fbfaf9);
  color: var(--ink);
}
.callout strong { color: var(--primary); }
/* A callout that INVITES rather than warns. `.callout`'s 4px mulberry edge is
 * the portal's «read this before you act» shape, and putting the monthly pulse's
 * friendly one-minute introduction inside it made a wellbeing check look like a
 * compliance notice. Same family, no edge rule, soft filled panel. */
.callout-soft {
  border-inline-start: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  text-align: center;
}

/* Row-cap affordance with ZERO JavaScript (the live CSP is script-src 'self'
   and this module ships no JS at all). Once the 20th .task-row exists, swap
   «افزودن ردیف» for the cap note, so the employee learns the limit BEFORE
   typing five more rows they then have to merge by hand on a phone. Browsers
   without :has() fall back to the previous behaviour; MAX_TASK_ROWS on the
   server is the actual enforcement either way. */
.task-rows-cap { display: none; }
.form-section:has(#task-rows .task-row:nth-child(20)) .task-rows-add { display: none; }
.form-section:has(#task-rows .task-row:nth-child(20)) .task-rows-cap { display: block; }

/* The spotlight question is STATIC TEXT, never an editable control (req 4).
 * It used to wear the same 3px mulberry edge rule as `.callout`, i.e. the
 * portal's «system notice» shape, and got skimmed past as one. A soft filled
 * panel says «this is content addressed to you» instead. */
.static-question {
  font-weight: 600;
  color: var(--ink);
  line-height: 1.9;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-field);
  padding-block: 0.6rem;
  padding-inline: 0.9rem;
  margin-block-end: 0.75rem;
}

/* ── Mood scale ───────────────────────────────────────────────────────────
 * Six equal pills across the row. The native radio STAYS: it is the accessible
 * control, it is what a keyboard's arrow keys drive, and it is the only thing
 * still visible where `:has()` is unsupported. What changed is the CHOSEN state,
 * which used to be a hairline border tint on a survey-form rectangle and now
 * fills mulberry, so «I answered, and this is my answer» reads across the room. */
.mood-scale { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.mood-opt {
  flex: 1 1 5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-block-size: 48px;
  padding-inline: 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  transition: background-color 160ms ease-out, color 160ms ease-out,
              border-color 160ms ease-out;
}
.mood-opt input { flex: 0 0 auto; inline-size: 20px; block-size: 20px; accent-color: var(--primary); }
.mood-opt:focus-within { border-color: var(--primary); outline: 2px solid var(--primary); outline-offset: 1px; }
.mood-opt:has(input:checked) {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-ink);
  font-weight: 600;
}
/* On the filled pill the mulberry accent would be invisible against mulberry. */
.mood-opt:has(input:checked) input { accent-color: var(--primary-ink); }

/* The old flex-era mobile block lived here and set
   `.task-row__hours { flex: 1 1 100% }`. Dead once the row became a grid, and
   deleting it removes a rule that would fight any future flex fallback. */

/* ── Daily report — «تعیین تکلیف برنامه‌های قبلی» ─────────────────────────
 * The plan-review section. Budget: under twenty seconds for the whole thing, so
 * it is deliberately light. One line per item, three pills on the same line
 * where the width allows, and the reason chips stay hidden until they mean
 * something.
 *
 * ZERO JavaScript, by construction. The reveal is the plain sibling combinator
 * `.plan-opt__input--not-done:checked ~ .plan-reasons`, which is why the radio
 * inputs are direct children of `.plan-item`: `~` only reaches siblings. The
 * live CSP is `script-src 'self'; style-src 'self'`, so an inline handler or a
 * style attribute would be blocked outright, and `:has()` is not needed here at
 * all. Without the stylesheet the chips are simply always visible and the native
 * radios still work.
 */
.plan-items { display: flex; flex-direction: column; gap: 0.6rem; }
.plan-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2, #fbfaf9);
}
.plan-item:focus-within { border-color: var(--primary); }
.plan-item__text { flex: 1 1 100%; margin: 0; line-height: 1.9; }
.plan-item__num { color: var(--primary); font-weight: 700; font-variant-numeric: tabular-nums; }

/* The native radio stays in the DOM and stays focusable and announced; it is
   only taken out of the visual flow so the label can BE the control. Never
   `display: none`, which removes it from the tab order and from the accessible
   tree, and never `visibility: hidden`, which does the same. */
.plan-opt__input,
.plan-chip__input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.plan-opt {
  min-block-size: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 1 1 6rem;
  padding-inline: 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  transition: background-color 160ms ease-out, color 160ms ease-out,
              border-color 160ms ease-out;
}
.plan-opt__input:checked + .plan-opt {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-ink);
  font-weight: 600;
}
/* Keyboard focus has to be visible on a control whose input is clipped, or the
   section becomes untraversable without a mouse. */
.plan-opt__input:focus-visible + .plan-opt,
.plan-chip__input:focus-visible + .plan-chip {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}

/* Hidden until «انجام نشد» is chosen. Optional either way: a report is never
   blocked on a reason. */
.plan-reasons { display: none; flex: 1 1 100%; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
.plan-opt__input--not-done:checked ~ .plan-reasons { display: flex; }
.plan-reasons__label { color: var(--muted, #6b6b6b); font-size: 0.85rem; margin-inline-end: 0.2rem; }
.plan-chip {
  min-block-size: 36px;
  display: inline-flex;
  align-items: center;
  padding-inline: 0.7rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: var(--ink);
  font-size: 0.9rem;
  cursor: pointer;
}
.plan-chip__input:checked + .plan-chip {
  border-style: solid;
  border-color: var(--primary);
  color: var(--primary);
  font-weight: 600;
}

@media (max-width: 560px) {
  /* Three pills on one line stop being tappable below this width. */
  .plan-opt { flex: 1 1 100%; }
}

/* ── Monthly pulse — WHO-5 radio scale ───────────────────────────────────
 * Logical properties only (the whole portal is RTL). No inline styles anywhere
 * in the module: the live CSP is `style-src 'self'`, which blocks a style=""
 * attribute exactly as surely as script-src blocks an inline <script>. The
 * module ships ZERO JavaScript; these are native radio inputs.
 *
 * DECISION 1 lives in this block. Six options per question, each a real 48px
 * tap target with the Persian anchor word next to the digit, laid out so that a
 * phone shows two readable columns rather than six squashed pills or six full
 * rows the employee has to scroll past five times.
 */
.pulse-key {
  margin-block-end: var(--sp-3);
  line-height: 2;
}

/* Each question is a fieldset. The default `.form-section` hairline is kept, so
   the five items read as five separated blocks, but the legend gets full-size
   body text: it is the QUESTION, not a section label, and at --fs-small the
   answers were louder than the thing being asked. */
.pulse-item__legend {
  font-size: var(--fs-body);
  color: var(--ink);
  font-weight: 600;
  line-height: 1.9;
}

/* auto-fit rather than a fixed column count: at 9rem minimum this is six
   columns on a desktop, three on a tablet and two on a phone, with no media
   query and no width at which an option is too narrow for its longest label
   («کمتر از نیمی از مواقع»). */
.pulse-scale {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 0.5rem;
}
/* One column on a phone. auto-fit produced TWO columns at 390px, and a two-column
 * ordinal scale is read in a zigzag: «هرگز، همیشه» side by side, then the next
 * pair below. A spectrum has to be scanned in a straight line or the reader has
 * to reconstruct its order before answering. */
@media (max-width: 560px) {
  .pulse-scale { grid-template-columns: 1fr; }
}
.pulse-opt {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-block-size: 48px;
  padding-block: 0.45rem;
  padding-inline: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2, #fbfaf9);
  color: var(--ink);
  line-height: 1.6;
  cursor: pointer;
}
.pulse-opt input {
  flex: 0 0 auto;
  inline-size: 20px;
  block-size: 20px;
  accent-color: var(--primary);
}
.pulse-opt__body { display: flex; align-items: baseline; gap: 0.35rem; min-inline-size: 0; }
.pulse-opt__num {
  color: var(--primary);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.pulse-opt__label { font-size: var(--fs-small); }
/* Keyboard reachability has to be VISIBLE, not just functional: the radio's own
   focus ring sits on a 20px dot inside a 48px box, so the box is outlined too. */
.pulse-opt:focus-within {
  border-color: var(--primary);
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
/* `:has()` is progressive enhancement — where it is unsupported the checked
   radio dot is still the answer, this only makes the whole chosen option read as
   chosen at a glance. */
.pulse-opt:has(input:checked) {
  border-color: var(--primary);
  background: var(--primary);
  color: var(--primary-ink);
  font-weight: 600;
}
/* The digit is mulberry by default, i.e. invisible on the filled pill. */
.pulse-opt:has(input:checked) .pulse-opt__num { color: var(--primary-ink); }
.pulse-opt:has(input:checked) input { accent-color: var(--primary-ink); }

/* DECISION 2: declining the month is a separate, clearly labelled action that
   lives OUTSIDE the questionnaire form, separated by a rule so it can never be
   mistaken for one more thing to fill in on the way to submitting. */
.pulse-optout {
  margin-block-start: var(--sp-4);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--border);
}
.pulse-optout .field-help { margin-block-end: 0.6rem; }

/* ── Jalali date picker (self-hosted, /static/js/jalali-picker.js) ────────
 * A popup Shamsi month grid for the two archive filter inputs, so a date can be
 * PICKED instead of typed. Progressive enhancement only: the input still
 * accepts a typed Jalali date and the server-side validation is unchanged, so
 * with JS off the filter behaves exactly as it did before.
 *
 * Logical properties throughout (the portal is RTL). No inline styles anywhere
 * in the component: the live CSP is `style-src 'self'`, which blocks a style=""
 * attribute exactly as it blocks an inline <script>. The script therefore sets
 * CLASSES, never `element.style`.
 */
/* The script wraps each picker input in this anchor so the popup can be
 * positioned against the input rather than against the page. Inline-block by
 * default, which is right in the archive's flex filter row where the inputs are
 * fixed-width `.input-sm`. */
.jdp-anchor { position: relative; display: inline-block; max-inline-size: 100%; }
/* On a real form field the anchor must NOT shrink-to-fit: `.field` is a column
 * flex and `.input` is `inline-size: 100%`, so an inline-block wrapper would
 * collapse «از تاریخ» to its intrinsic input width while every neighbouring box
 * stayed full width. Block restores the stretch the input had before wrapping. */
.field > .jdp-anchor { display: block; }

.jdp {
  position: absolute;
  z-index: 40;
  inset-block-start: calc(100% + 6px);
  inset-inline-start: 0;
  inline-size: 17.5rem;
  background: var(--card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-hover);
  padding: 0.6rem;
}
.jdp[hidden] { display: none; }

.jdp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.25rem;
  margin-block-end: 0.4rem;
}
/* Named nav buttons («ماه قبل» / «ماه بعد») instead of chevrons on purpose: a
 * ‹ / › glyph is bidi-mirrored in an RTL run, so "back" and "forward" swap
 * sides depending on the surrounding text and the reader has to guess. */
.jdp-title {
  flex: 1;
  text-align: center;
  font-weight: 600;
  font-size: var(--fs-small);
  color: var(--ink);
}
/* 36px tap targets — the same floor as .btn-sm, so this is usable on a phone. */
.jdp-nav {
  min-inline-size: 36px;
  min-block-size: 36px;
  padding-inline: 0.45rem;
  white-space: nowrap;
  border: 1px solid var(--border);
  border-radius: var(--radius-field);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
  cursor: pointer;
  line-height: 1;
}
.jdp-nav:hover { background: var(--ghost); border-color: var(--border-strong); }
.jdp-nav:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

.jdp-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.jdp-dow {
  text-align: center;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--muted);
  padding-block: 0.25rem;
}
.jdp-day {
  min-block-size: 34px;
  border: 1px solid transparent;
  border-radius: var(--radius-field);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  padding: 0;
}
.jdp-day:hover { background: var(--ghost); }
.jdp-day:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.jdp-day.is-today { border-color: var(--primary); font-weight: 600; }
.jdp-day.is-selected { background: var(--primary); color: var(--primary-ink); font-weight: 600; }
.jdp-blank { min-block-size: 34px; }

.jdp-foot {
  display: flex;
  gap: 0.4rem;
  margin-block-start: 0.5rem;
  padding-block-start: 0.5rem;
  border-block-start: 1px solid var(--border);
}
.jdp-foot .jdp-nav { flex: 1; }

/* ── Feedback: the floating «گزارش خطا یا پیشنهاد» control + its dialog ───────
 * COO 2026-08-18: a way to report a fault or suggest an improvement from EVERY
 * page. Rendered by `base.html` for every signed-in visitor, so these rules load
 * on every page in the portal and must not touch anything else.
 *
 * BOTTOM-LEFT, and in an RTL document that is `inset-inline-end`. The
 * inline-START edge (right) is where every line of Persian begins and where the
 * primary content sits, so a floating chip there would cover the beginning of
 * the text the reader is on. Logical properties only, as everywhere in this file.
 */
.fab {
  position: fixed;
  inset-block-end: max(1rem, env(safe-area-inset-bottom));
  inset-inline-end: 1rem;
  z-index: 60;                    /* above content, below the modal backdrop */
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  /* 44px is the floor for a touch target; 48px matches `.btn` so the control
     does not read as a smaller class of button than the ones on the page. */
  min-block-size: 48px;
  min-inline-size: 48px;
  padding-block: 0.6rem;
  padding-inline: 1.1rem;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: var(--primary);
  color: var(--primary-ink);
  font-weight: 600;
  font-size: var(--fs-small);
  line-height: 1;
  text-decoration: none;
  box-shadow: var(--shadow-hover);
  transition: background-color 160ms ease-out, box-shadow 160ms ease-out;
}
.fab:hover { background: var(--primary-deep); color: var(--primary-ink); text-decoration: none; }
/* Keyboard reachability has to be VISIBLE. The chip sits on the powder canvas,
   so the ring needs an offset to clear the mulberry fill. */
.fab:focus-visible {
  outline: 2px solid var(--ink-deep);
  outline-offset: 3px;
}
.fab__glyph { flex: 0 0 auto; }

/* PHONE: the label goes, the chip becomes a 56px circle. A pill wide enough for
 * «گزارش خطا یا پیشنهاد» covers a third of a 390px screen and lands on top of
 * the table cells and action buttons the module pages put near the bottom edge. */
@media (max-width: 640px) {
  .fab {
    inline-size: 56px;
    block-size: 56px;
    padding: 0;
    justify-content: center;
  }
  .fab__label {
    /* Same technique as `.visually-hidden`: gone from the layout, still read by
       a screen reader, so the control keeps its accessible name. */
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* The footer is the last thing on the page and the chip floats over it. Without
 * this, scrolling to the bottom on a phone puts the mulberry circle exactly on
 * top of «پورتال کارکنان پیپا». Extra block padding, so the colophon ends above
 * the chip rather than behind it. */
@media (max-width: 640px) {
  .footer { padding-block-end: 5rem; }
}

/* ── The dialog ──────────────────────────────────────────────────────────────
 * Native <dialog>. `feedback-widget.js` calls showModal(); with the script
 * blocked the element is simply never opened and the floating control stays an
 * ordinary link to /feedback/new, which carries the same form.
 */
.fb-dialog {
  inline-size: min(34rem, calc(100vw - 2rem));
  padding: var(--sp-5);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-hover);
  /* The UA stylesheet centres a modal <dialog> already; this only keeps a long
     report scrollable inside it rather than pushing the buttons off-screen. */
  max-block-size: min(90vh, 44rem);
  overflow-y: auto;
}
.fb-dialog::backdrop { background: rgba(23, 18, 21, 0.45); }
.fb-dialog__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
}
.fb-dialog__title {
  margin: 0;
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.6;
}
.fb-dialog__close {
  flex: 0 0 auto;
  min-inline-size: 44px;
  min-block-size: 44px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-h3);
  line-height: 1;
  cursor: pointer;
}
.fb-dialog__close:hover { background: var(--ghost); color: var(--ink); }
.fb-dialog__close:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.fb-dialog__intro { margin-block: 0.25rem 1rem; }

/* ── Category pills ──────────────────────────────────────────────────────────
 * The mood-scale pattern: the NATIVE radio stays visible, because it is the
 * accessible control, it is what a keyboard's arrow keys drive, and it is the
 * only thing still visible where `:has()` is unsupported. Its own class rather
 * than `.mood-opt` for the reason `.pulse-opt` has one: a shared class means one
 * module's spacing tweak silently restyles another module's form.
 */
.fb-scale { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.fb-opt {
  flex: 1 1 9rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-block-size: 48px;
  padding-inline: 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  transition: background-color 160ms ease-out, color 160ms ease-out,
              border-color 160ms ease-out;
}
.fb-opt input { flex: 0 0 auto; inline-size: 20px; block-size: 20px; accent-color: var(--primary); }
.fb-opt:focus-within { border-color: var(--primary); outline: 2px solid var(--primary); outline-offset: 1px; }
.fb-opt:has(input:checked) {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-ink);
  font-weight: 600;
}
/* On the filled pill the mulberry accent would be invisible against mulberry. */
.fb-opt:has(input:checked) input { accent-color: var(--primary-ink); }

.fb-body { min-block-size: 7rem; resize: vertical; }
.fb-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-block-start: var(--sp-4);
}
.fb-actions .btn { flex: 1 1 8rem; }
/* htmx marks the form `.htmx-request` while a submit is in flight. Same
 * double-click guard the money forms use; the server-side checks are what
 * actually hold. */
.fb-form.htmx-request { opacity: 0.6; pointer-events: none; }
.fb-done { margin-block-start: var(--sp-4); }

/* ── The reading queue (/feedback/) ──────────────────────────────────────────*/
/* A path is ASCII inside an RTL document: `dir="ltr"` on the cell puts the
 * slashes back where they belong, and this keeps it from wrapping mid-path. */
.fb-path { white-space: nowrap; font-size: var(--fs-small); color: var(--muted); }
.fb-body-cell { min-inline-size: 18rem; }
.fb-detail > summary { cursor: pointer; }
.fb-full { margin-block: 0.5rem 0; line-height: 1.9; }
.fb-ua { word-break: break-all; }
.fb-status { white-space: nowrap; }
.fb-reviewer { display: block; font-size: var(--fs-small); }
.fb-review-form { margin-block-start: 0.4rem; }

/* ── «مشاهده به‌عنوان» banner (view-as, 2026-08-19) ───────────────────────
 * Sticky, full-bleed, above the brand lockup, and the ONE element in this
 * stylesheet that is allowed to shout. The failure mode this guards against is
 * not ugliness, it is forgetting: in this mode the portal renders somebody
 * else's tiles and somebody else's 403s, so a quiet notice would let a strange
 * page read as a bug instead of as the mode.
 *
 * Warm sand (--warn-bg / --warn-ink), not danger red: nothing is wrong, and a
 * red bar on every page for an hour trains the eye to stop seeing it. Logical
 * properties only, per the file's rules.
 */
.viewas {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-2) var(--sp-4);
  background: var(--warn-bg);
  color: var(--warn-ink);
  border-block-end: 2px solid var(--warn-ink);
  font-size: var(--fs-small);
  line-height: 1.9;
}

.viewas__text {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-3);
}

.viewas__who { font-weight: 700; }

/* The attribution sentence is the half people skip, so it gets the weight the
 * name does rather than being demoted to a parenthetical. */
.viewas__rule { font-weight: 600; }

.viewas__clock { opacity: 0.8; }

.viewas__exit { margin: 0; }

.viewas__btn {
  padding-block: 2px;
  padding-inline: var(--sp-4);
  border: 1px solid var(--warn-ink);
  color: var(--warn-ink);
  font-size: var(--fs-small);
}

/* The one-shot «it ended» note. Sage, because returning to your own account is
 * a confirmation and not a warning, and NOT sticky: it says something that has
 * already finished. */
.viewas--ended {
  position: static;
  background: var(--ok-bg);
  color: var(--ok-ink);
  border-block-end-color: var(--ok-ink);
}

/* EP-020: the pre-submit file list rendered by file-input.js. Declared here
   rather than via CSSOM so the sheet stays the single source of layout. */
.file-list{list-style:none;padding-inline-start:0;margin-block:6px 0}
/* EP-039: the «چند ذی‌نفع با این نام» chooser in _iban_options.html. */
.name-choices{list-style:none;padding-inline-start:0;margin-block:6px;display:flex;flex-wrap:wrap;gap:6px}
.name-choices li{margin:0}

/* ── EP-021: the login page has ONE obvious way in (COO, 2026-09-02) ──────
 * «کادر ورود اضطراری خیلی تو چشمه و همه فکر می‌کنن باید از اون طریق وارد بشن.»
 *
 * The Microsoft button and the break-glass credentials box were rendered as
 * PEERS with a «یا» between them, so employees typed their Entra password into
 * a shared local admin form and were refused. Everything below is presentation
 * only: the route, the method and the field names of that form are untouched,
 * because nginx rate-limits `/auth/breakglass` by an exact-path location and
 * the fail2ban `portal-auth` jail reads the same path out of the access log.
 *
 * Logical properties only, per this file's rules.
 */

/* The hero. `.btn` is already 48px and `btn-primary` is already mulberry; what
 * this adds is the SIZE difference that survives a 320px screen, where the two
 * controls sit one under the other and only weight tells them apart.
 * `white-space: normal` and a real line-height because `.btn` sets
 * `line-height: 1` for single-line pills and this label wraps on a phone. */
.login-hero { margin-block-end: var(--sp-2); }
.login-hero__btn {
  min-block-size: 56px;
  padding-block: 0.85rem;
  font-size: var(--fs-h3);
  line-height: 1.5;
  white-space: normal;
  text-align: center;
}
/* The sentence that says out loud which door is theirs. Taupe and small: it
 * explains the button, it does not compete with it. */
.login-hero__note {
  margin-block: 0.75rem 0;
  color: var(--muted);
  font-size: var(--fs-small);
  line-height: 1.9;
}

/* The emergency door: present, reachable, and visibly not for you.
 *
 * A hairline rule and a quiet underlined line of taupe text — the shape of a
 * footnote, not of a control. The tap target is the summary's own box (~32px
 * with the padding below), deliberately NOT the 44px of a button: making it
 * comfortable to hit is the opposite of the requirement. */
.login-breakglass {
  margin-block-start: var(--sp-6);
  padding-block-start: var(--sp-4);
  border-block-start: 1px solid var(--border);
}
.login-breakglass__summary {
  display: inline-block;      /* also drops Firefox's disclosure triangle */
  list-style: none;
  padding-block: var(--sp-1);
  color: var(--muted);
  font-size: var(--fs-small);
  line-height: 1.8;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.login-breakglass__summary::-webkit-details-marker { display: none; }
.login-breakglass__summary:hover { color: var(--ink); }
.login-breakglass__panel { margin-block-start: var(--sp-3); }
.login-breakglass__scope { margin-block: 0 var(--sp-3); }

/* `.form-secondary` draws its own surface panel, which was right when the form
 * stood alone under a divider and is one box too many now that it is folded
 * into a disclosure that already sits below a rule. Cancelled by DESCENT rather
 * than by editing the form's class list, because that class list is part of the
 * contract this change promised not to move. */
.login-breakglass .form-secondary {
  background: transparent;
  border: 0;
  padding: 0;
}

/* Microsoft sign-in NOT CONFIGURED. The fold is then rendered `open` and this
 * form is the only way into the portal, so it stops being a footnote: the
 * summary reads as the card's own heading and the panel comes back. The summary
 * stays clickable on purpose — a `<details open>` whose handle is hidden can be
 * closed once and never reopened. */
.login-breakglass--sole {
  margin-block-start: var(--sp-4);
  padding-block-start: 0;
  border-block-start: 0;
}
.login-breakglass--sole .login-breakglass__summary {
  color: var(--ink);
  font-size: var(--fs-h3);
  font-weight: 600;
  text-decoration: none;
}
.login-breakglass--sole .form-secondary {
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 1.25rem;
}

/* 320px is the floor this page has to hold: the card is single-column already,
 * and at the heading size the hero label wraps to three lines inside it. */
@media (max-width: 560px) {
  .login-hero__btn { font-size: var(--fs-body); }
  .login-breakglass--sole .login-breakglass__summary { font-size: var(--fs-body); }
}

/* EP-023 */
/* The pay queue's decision-notes box (COO order, 2026-09-07). The payer must be
 * able to read the WHOLE note, so nothing here truncates: no max-height, no
 * ellipsis, no line clamp. `pre-wrap` keeps the approver's own line breaks,
 * because a note written as three bullets and rendered as one paragraph is a
 * different note. `overflow-wrap` is what stops one long unbroken token (a
 * pasted URL, an invoice number) from widening the row instead of wrapping. */
.notes-row > .notes-cell {
  background: var(--band);
  padding: 0.6rem 0.9rem 0.9rem;
}
.notes-box {
  border-inline-start: 3px solid var(--primary);
  padding-inline-start: 0.75rem;
}
.notes-box__title {
  font-size: var(--fs-small);
  font-weight: 600;
  margin: 0 0 0.4rem;
}
.notes-box .note + .note {
  margin-block-start: 0.6rem;
}
.notes-box .note__head {
  color: var(--muted);
  font-size: var(--fs-small);
  margin: 0 0 0.15rem;
}
.notes-box .note__body {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* The acknowledgement sits in the flow of the «ثبت پرداخت» form, on its own
 * line: a checkbox squeezed between two inputs on a wide row is a checkbox
 * people tick without reading the sentence beside it. */
.notes-ack {
  align-items: center;
  display: flex;
  flex-basis: 100%;
  font-size: var(--fs-small);
  gap: 0.35rem;
}
