/* ============================================================================
   _admin.css — Admin Panel Styles
   ============================================================================ */

.admin-stat-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
    padding: 1.5rem;
    transition: background-color var(--transition-normal), border-color var(--transition-normal);
}

.admin-stat-card .stat-value {
    font-size: 2rem;
    font-weight: 700;
    color: var(--primary);
    line-height: 1.2;
}

.admin-stat-card .stat-label {
    color: var(--text-secondary);
    font-size: 0.875rem;
    margin-top: 0.25rem;
}

.admin-stat-card .stat-icon {
    font-size: 2.5rem;
    opacity: 0.15;
    position: absolute;
    top: 1rem;
    right: 1rem;
}

/* ----------------------------------------------------------------------------
   Inline stat tile — icon chip beside the label + value, rather than the
   corner watermark .stat-icon gives. Flex only, no absolute positioning, so
   it mirrors under RTL for free. (.stat-icon's physical top/right does NOT
   mirror; that is a pre-existing issue on the admin dashboard, left alone.)
   ---------------------------------------------------------------------------- */
.admin-stat-card.stat-card-inline {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1.25rem 1.5rem;
}

.admin-stat-card.stat-card-inline .stat-label {
    margin-top: 0;
}

.admin-stat-card.stat-card-inline .stat-value {
    font-size: 1.75rem;
    /* Tabular figures: during the count-up every digit is the same width, so
       the number does not wobble as it ticks (1 -> 4 -> 12 -> 128). */
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/* ----------------------------------------------------------------------------
   A tile that OPENS something is a real <button> — keyboard activation, focus
   ring and the button role come for free, where a div with a click handler
   would need all three written by hand and would still be invisible to a
   screen reader. It carries the SAME two classes, so every rule above already
   matches it; these declarations only undo the chrome a <button> brings
   (its own font, centred text, shrink-to-fit width).

   First host: «مصروفات لم تُفوتر على الموكل» on /cases/{id}, which opens the
   المصروفات sub-tab (2026-09-12). Reach for it only when the tile really does
   lead somewhere — a tile that merely reports a number must stay a <div>, or
   every card on the page starts inviting a click that does nothing.
   ------------------------------------------------------------------------- */
button.admin-stat-card {
    width: 100%;
    text-align: start;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

button.admin-stat-card:hover,
button.admin-stat-card:focus-visible {
    border-color: var(--primary);
}

/* ----------------------------------------------------------------------------
   A stat tile's Latin value (money, a date, "12 · 3,400.00") carries dir="ltr"
   so its digits and neutral separators order correctly. That dir is needed —
   but it also re-anchors the element's own start edge, and text-align: start
   resolves against the ELEMENT's direction, not the page's. In the Arabic
   locale the number therefore aligned LEFT while the label above it stayed on
   the page start edge, and the value visibly detached from its own label.

   It is intermittent, which is what makes it dangerous: the text wrapper is a
   flex item sized to max-content, so there is only slack to drift into when
   the LABEL is the wider of the two. Measured on /expenses at 430px —
   «الضريبة المدفوعة» / 1,850.25 sat flush (value as wide as the label) while
   «على الموكلين ولم تُفوتر» / 330.45 detached by 46px, in the same row. A
   shorter number or a longer label flips a tile that looks fine today.

   Pinned to html[dir] BY ATTRIBUTE, exactly as _forms.css pins
   .form-control[dir]: setting dir is precisely what causes the mismatch, so
   the fix rides along with the cause and cannot be forgotten on the next tile.
   Harmless on an inline <span dir="ltr"> — text-align does not apply to one.
   ---------------------------------------------------------------------------- */
html[dir="ltr"] .admin-stat-card [dir] { text-align: left; }
html[dir="rtl"] .admin-stat-card [dir] { text-align: right; }

/* ----------------------------------------------------------------------------
   The stats ROW sizes its tiles by the CONTENT area, not by a viewport
   breakpoint — because those two are not the same number here and no `col-*`
   can bridge them. The sidebar takes ~350px expanded and ~100px collapsed, so
   one viewport width yields two different content widths, and `col-lg-3` fires
   on the VIEWPORT (>=992px) either way. Measured with the sidebar open at a
   1076px viewport: /meetings got 165px columns and «بانتظار تسجيل النتيجة»
   wrapped to THREE lines; /consultations got 182px columns with 36-88px of
   text room, and `h-100` then stretched its one-line tiles to match the
   three-line one — 109px of empty card. Widening the breakpoint cannot fix
   this: collapsing the sidebar changes the answer without changing the
   viewport at all.

   auto-fit + minmax asks the actual container instead, and `1fr` hands out the
   surplus. The 270px floor is measured, not guessed, and it is set by the
   VALUE rather than the label: the longest label in the app wants 122-123px
   («استشارات غير مفوترة», «بانتظار تسجيل النتيجة») = a 235px tile, but the
   `count · money` composite on /invoices («17 · 34,164.20») wants 184px = 296px.
   A 240px floor covers every label and still let a 1344px row pack FIVE 256px
   tracks, which re-wrapped that one value; 270px gives four 324px tracks and it
   fits. `min(270px, 100%)` keeps the track from overflowing a container
   narrower than the floor (a phone, where one tile per row is correct anyway).

   The Bootstrap `col-*` classes STAY on the markup: they still say how many
   tiles the page wants per row, and they are what renders if :has() is ever
   unavailable — the fallback is exactly today's behaviour, never a broken page.
   Gutters move from Bootstrap's padding/negative-margin pair to a real `gap`
   so the 240px floor is the CARD's width and not the card plus its gutter.
   ---------------------------------------------------------------------------- */
.row:has(> [class*="col-"] > .admin-stat-card.stat-card-inline) {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(270px, 100%), 1fr));
    gap: var(--bs-gutter-x, 1rem);
    margin-inline: 0;
    margin-top: 0;
}

.row:has(> [class*="col-"] > .admin-stat-card.stat-card-inline) > [class*="col-"] {
    /* col-* sets width:25% etc; inside a grid that resolves against the CELL,
       which would re-create the squeeze one level down. */
    width: auto;
    max-width: none;
    padding: 0;
    margin-top: 0;
}

/* ----------------------------------------------------------------------------
   The VALUE is the only thing that can still outgrow a tile. A label tops out
   at ~123px, but the `count · money` composite («17 · 34,164.20» on /invoices,
   «0 · 0.00» on المتأخر) wants 184px at 1.75rem — a 296px tile. The grid floor
   is 270px, so a 1195px content row packs four 287px tracks and that one value
   split as «17 ·» / «34,164.20», stranding the separator at the end of a line.

   Raising the floor to 296 would fix it by costing a whole column at most
   desktop widths, on every panel, to suit two tiles. So the value steps down
   one size on the tiles that are actually too narrow for it instead: at 1.5rem
   the same string wants 158px, i.e. a 270px tile — the floor — so it fits
   anywhere the grid can place it.

   `container-type: inline-size` is safe on this card even though it applies
   inline-size containment: the card's width comes from its grid track (a fixed
   minmax, never min-content) or from its `col-*` percentage in the fallback, so
   nothing here was sizing itself from its contents. Where @container is
   unsupported the value simply keeps 1.75rem and wraps — today's behaviour.
   ---------------------------------------------------------------------------- */
.admin-stat-card.stat-card-inline {
    container-type: inline-size;
}

@container (max-width: 300px) {
    .admin-stat-card.stat-card-inline .stat-value {
        font-size: 1.5rem;
    }
}

/* ----------------------------------------------------------------------------
   FILTER BARS — the same content-vs-breakpoint fault as the stats row above,
   and the same remedy. Fixed 2026-09-09.

   A filter bar asks for `col-md-2`, which fires at a 768px VIEWPORT. The width
   it actually gets is the CONTENT area, and the sidebar takes ~270px of that,
   so one viewport yields two different answers. Measured on /statements at a
   842px viewport, changing nothing but the sidebar:

       sidebar expanded   content 524px   ->  col-md-2 = 72px
       sidebar collapsed  content 720px   ->  col-md-2 = 105px

   What that costs, measured:

     - A switch needs 184px to keep its label on one line («إظهار الحسابات
       الصفرية», the widest filter switch in the app, in either locale). At
       72px both labels on /statements wrapped to three and two lines and then
       OVERFLOWED their own columns by 29px and 26px onto the neighbouring
       control — «المتأخرة فقط» sat on top of the تطبيق button.
     - A date field's visible box needs 71px of text room for dd/MM/yyyy. On
       /fee-schedules a 700px card left it 46px and a 525px card left it 17px,
       so both date filters rendered as a clipped «yyy», and the selects beside
       them as a clipped «كل ا».

   Neither is a narrow-window-only fault: at a 1440px viewport with the sidebar
   open the column is 172px, still under 184, so a switch label wrapped to two
   lines on six pages. Only the OVERLAP needed a page as cramped as /statements.

   Widening the breakpoint cannot fix it, for the reason the stats note above
   gives: collapsing the sidebar changes the available width without changing
   the viewport at all. So the row asks the real container instead.

   The 190px floor is measured, not chosen: 184px for the widest switch, and
   190 is the largest floor that still packs FIVE tracks into the 1026px form
   of a 1440px viewport, i.e. that keeps a five-control bar on one row where it
   fits today. 200px cost a second row there; 170 and 180 behave identically to
   190 and leave less headroom for a future label. At the floor a date field
   gets ~143px of inner box against the 71px it needs.

   SCOPED by `g-2 align-items-end`, which is the filter-bar signature in this
   app — all 31 of them carry it, across 30 templates, and no other row does.
   The settings and modal switch rows are all `g-3`, and re-flowing those is
   not this rule's business; /employees, /lookups, /offices and the chart of
   accounts were checked and match nothing here.

   The `col-*` classes stay on the markup: they still say how many controls the
   page wants per row, and they are what a `mb-3` / `mt-3` / `border-top` on the
   same row still needs. Wider is not always taller — at a 1300px card
   /fee-schedules went from 158px to 129px, because six equal tracks pack better
   than Bootstrap wrapping 3+2+2+2+2+2+2 twelfths.
   ---------------------------------------------------------------------------- */
.row.g-2.align-items-end {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
    gap: 0.75rem;
    margin-inline: 0;
}

.row.g-2.align-items-end > [class*="col-"] {
    /* col-* sets width:16.66% etc; inside a grid that resolves against the
       CELL, which would re-create the squeeze one level down. */
    width: auto;
    max-width: none;
    margin-top: 0;
    padding: 0;
}

/* The 48px chip, SOLID and one shape in every design (D7). It was a 10-12%
   tint of the tone with the tone itself as the glyph colour — which cannot be
   measured for contrast, because the tint's opacity means the real background
   is whatever surface happens to be behind it. Each design now names a fill
   and a label for every tone, both measured against each other (violet:
   4.83:1 to 6.83:1), and the lime sheet's chip-less tiles are not carried
   over: one chip, everywhere. */
.admin-stat-card .stat-chip {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-chip);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.25rem;
    background: var(--chip-primary-bg);
    color: var(--chip-primary-fg);
    flex-shrink: 0;
}

.admin-stat-card .stat-chip.chip-success {
    background: var(--chip-success-bg);
    color: var(--chip-success-fg);
}

.admin-stat-card .stat-chip.chip-muted {
    background: var(--chip-muted-bg);
    color: var(--chip-muted-fg);
}

.admin-stat-card .stat-chip.chip-danger {
    background: var(--chip-danger-bg);
    color: var(--chip-danger-fg);
}

/* Amber takes DARK text — no amber passes under white. */
.admin-stat-card .stat-chip.chip-warning {
    background: var(--chip-warning-bg);
    color: var(--chip-warning-fg);
}

/* «المحصل» on تقرير ربح القضايا — cash received, which is neither profit
   (success) nor cost (danger). Every design defines the pair, so this needs
   no dark-mode override. */
.admin-stat-card .stat-chip.chip-info {
    background: var(--chip-info-bg);
    color: var(--chip-info-fg);
}

/* ----------------------------------------------------------------------------
   بوابة المحامي quick-create row — FLAT primary fill, gradient off.

   .btn-primary paints var(--primary-fill), a 135° two-stop gradient, and every
   button gets its own copy — fine when a page has ONE create button, but this
   row has five side by side, and five restarting gradients read as a striped
   band (the same five-in-a-row argument that gave these buttons per-register
   icons instead of "+"; see the comment in lawyer-portal/index.html).

   This is a gradient→flat change ONLY: the colour stays --primary, so the row
   still says "create" in the same voice as every other page, hover/shadow
   behaviour is untouched, and white-on---primary passes AA in both themes
   (5.27:1 light / 3.49:1 dark is the PLAIN --primary… careful — dark's flat
   --primary is exactly the fill the pagination fix just moved AWAY from, so
   dark keeps the gradient: its two deep stops are what white passes on). */
.lp-quick-create .btn-primary {
    background: var(--primary);
}

[data-theme="dark"] .lp-quick-create .btn-primary {
    background: var(--primary-fill);
}

/* ----------------------------------------------------------------------------
   بوابة المحامي — the date cells of every tab.

   In the TABLES the case sits under the row's title, inside the registers' own
   stacked .cell-clamped cell (lawyer-portal.js titleCell): the chip there is
   the plain `row-parent-chip small` and needs nothing from this block. Until
   2026-09-14 it was a column of its own (.lp-cell-case, max-width 12rem, a
   chip that clipped without an ellipsis); asked to match the /sessions
   register's cell instead — «العنوان وتحتها الرابط». On a CARD the chip is
   .portal-case-chip, with the portal card rules further down. */

.lp-when-date {
    display: block;
    font-weight: 500;
}

.lp-when-time {
    display: block;
    font-size: 0.8rem;
    color: var(--text-secondary);
}

/* Chevron tracks the collapse state via Bootstrap's aria-expanded.
 *
 * Selected by CLASS, not by #officeStatsToggle: /contacts, /employees and
 * /tasks all render .stats-chevron on their own toggle, and an id-scoped rule
 * left every one of them with a chevron that never turns. Anything carrying
 * the class is a collapse toggle by construction. */
[aria-expanded] .stats-chevron {
    transition: transform var(--transition-fast);
}

[aria-expanded="true"] .stats-chevron {
    transform: rotate(180deg);
}

.admin-nav-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    padding: 1.25rem 1.5rem;
    display: flex;
    align-items: center;
    gap: 1rem;
    text-decoration: none;
    color: var(--text);
    transition: background-color var(--transition-normal), border-color var(--transition-normal), transform var(--transition-fast);
}

.admin-nav-card:hover {
    border-color: var(--primary);
    transform: translateY(-2px);
    color: var(--text);
}

.admin-nav-card .nav-icon {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-md);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.25rem;
    background: color-mix(in srgb, var(--primary) 10%, transparent);
    color: var(--primary);
    flex-shrink: 0;
}

.admin-table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
}

/* NO text-transform and NO letter-spacing, and the file already argued why
   two sections further down: `uppercase` is a NO-OP in Arabic, which has no
   letter case, while `letter-spacing` pulls the cursive joins of a word like
   «الموكل» apart. The pair was tuned together for Latin headers and both go
   together; the sheets draw headers in sentence case. */
.admin-table thead th {
    background: var(--bg-secondary);
    color: var(--text-secondary);
    font-weight: 600;
    font-size: 0.8125rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

.admin-table tbody td {
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

/* ----------------------------------------------------------------------------
   Free-text cells — two lines, then an ellipsis.

   A register column holding user prose (سبب الجلسة on the lawyer portal,
   عنوان القضية on /cases) has no natural width: one long value stretches its
   column and squeezes every other one, and a single truncated line throws away
   the half of the sentence that identifies the record. Two lines is the
   compromise — enough to tell two cases apart, bounded enough that no row is
   twice the height of its neighbours. The full text goes in the cell's title.

   TWO classes, because they do different jobs and sit on different elements:
   the cap goes on the <td> (a clamp with no bound never reaches a second line —
   the cell just widens), the clamp on a block INSIDE it. Both are needed.

   `width`, NOT `max-width` — and this is the whole rule, not a style choice.
   CSS 2.1 §17.5.2.2 leaves the effect of max-width on a table cell UNDEFINED,
   and Blink duly ignores it while distributing surplus width across an
   auto-layout `width: 100%` table: the cap caps the column's max-CONTENT
   contribution, then the leftover space is handed back out in proportion to it
   and the column sails straight past the cap. This rule said `max-width` from
   the day it was written and never once bound. Measured on /opponents at a
   1600px viewport, cap 22rem = 352px:

       max-width:  الوصف 506  القضية 506   (64% of the table between them),
                   الاسم 134  الهاتف 96  الهوية 125  صفة الخصم 144
       width:      الوصف 352  القضية 352,
                   الاسم 224  الهاتف 145  الهوية 189  صفة الخصم 218

   The clamp still "worked" the whole time, which is what hid this — it just
   took a ~1000px value to reach a second line. Do NOT put the cap on
   .cell-clamp-2 instead: an inner block honours max-width, but the CELL keeps
   the space, so the text narrows and the column stays wide. Only a cell width
   feeds the table algorithm.

   In auto layout `width` is a PREFERENCE, not a hard bound — a column whose
   min-content exceeds it still grows, so a narrow viewport degrades by
   shrinking these columns rather than by overflowing (verified: no horizontal
   scroll at 1024px), and the min-width floor below stops that degrading into
   a column too narrow to read.

   Scoped through `.admin-table tbody td`, not written as a bare class: the base
   cell rule is (0,1,2) and a lone class is (0,1,0) — the same specificity trap
   the detail-row rules below are scoped for. The width is a custom property so
   a table with different proportions can retune it without a second rule. */
.admin-table tbody td.cell-clamped {
    width: var(--cell-clamp-width, 22rem);
}

/* The SECOND half of the cap, and it is not optional — `width` on the cell
   alone leaves two holes, both measured:

   1. A `text-truncate` child sets `white-space: nowrap`, so ITS min-content is
      the entire string. A column can never be narrower than its min-content, so
      the cell's `width` loses outright and the column takes the full title —
      564px against a 352px cap in the stacked tasks/sessions/meetings cells.
      That is also why the inline max-widths those cells used to carry never
      ellipsised anything: the truncation had no definite width to truncate to.
   2. Where the table has surplus space (few columns, short content), it is
      handed out in proportion to max-content, and a cell `width` is a
      preference the surplus pass will overshoot.

   `max-width` on a normal block IS honoured, and it clamps that block's
   intrinsic CONTRIBUTION — so capping the children caps what the column can
   demand, and the two rules together give a column that neither overshoots nor
   is forced wide. Measured on the stacked cell: 564px -> 384px (352 + the
   cell's 2rem of padding), and the truncating lines start ellipsising.

   Applies to `> *` rather than to .cell-clamp-2, deliberately: the cap must
   reach the `text-truncate` lines of a stacked cell, which are not .cell-clamp-2
   and never can be. Chips and badges under the text are also children and are
   unaffected — they are far narrower than any cap. */
.admin-table tbody td.cell-clamped > * {
    max-width: var(--cell-clamp-width, 22rem);
}

/* THE FLOOR, AND WHICH COLUMN ABSORBS. A cap alone is only half a column:
   `width` is a preference, so under pressure the table takes the shortfall out
   of whichever column will give it up — and left to itself that is the WRONG
   one. On the الجلسات register المكتب fell to 70px and broke المكتب across two
   lines mid-word, while سبب الجلسة beside it never moved off its full 384px.

   The column that should stretch and shrink is the BIG free-text one. It has
   the most to give and loses the least by giving it: a prose cell is already
   clamped to two lines with the rest on hover, so 90px off its width costs a
   few words of preview. The same 90px off a name column costs the name.

   So the floors are TIERED — and what makes prose yield FIRST is the GAP
   between its cap and its floor (22rem down to 10rem, against the label
   tier's 14rem down to 9rem), not a lower floor. Measured at a
   1250px table: سبب الجلسة gives up 95px, المكتب gives up 54 and stays legible,
   and the horizontal scrollbar goes away.

   The prose floor is the HIGHER of the two even though prose is the tier that
   yields, and that is not a contradiction: yielding is about the ORDER columns
   give up width, the floor is about where giving up stops. Set prose below the
   label tier and a squeezed register ends with موضوع/سبب — the column the row is
   identified by — narrower than the branch name beside it, which is the
   hierarchy upside down. Measured at a 1082px table before this was corrected:
   سبب الجلسة 138px against المكتب 160px.

   Both are legibility minimums, not second caps — they apply only when the
   table is over budget — and both are FLAT rather than a fraction of
   --cell-clamp-width, because how readable a column has to be does not scale
   with how wide it would like to be. */
.admin-table tbody td.cell-clamped {
    min-width: 10rem;
}

.admin-table tbody td.cell-clamped-sm {
    min-width: 9rem;
}

/* ...and the thing that was actually pinning the column, which is NOT the
   clamp and not the text: the CHIP.

   `.row-parent-chip` is `white-space: nowrap` with `text-overflow: ellipsis`,
   and its own `max-width: 100%` does nothing to stop this — a PERCENTAGE
   max-width is treated as `auto` when computing intrinsic sizes, so it never
   clamps the min-content contribution. A column can never be narrower than its
   min-content, so one chip carrying a long case label ("Opponent search probe -
   Opponent search probe — 2026-7") is an incompressible ~300px block that pins
   the whole column at its cap while every neighbour on the row is starved. The
   ellipsis never even fires: the cell is as wide as the chip demanded.

   Letting it WRAP is what unpins it — min-content drops to the longest word.
   Measured on the الجلسات register at 1250px: the scrollbar disappears and
   المكتب goes 96px -> 170px. The chip grows to a second line only when the
   label genuinely exceeds the column, which is the case it was silently
   clipping before.

   Scoped to clamped cells, so status/priority badges in their own columns keep
   their pill shape. `.badge` rides along because Bootstrap's is nowrap too, and
   a chip built from one would have the identical problem. */
.admin-table tbody td.cell-clamped .row-parent-chip,
.admin-table tbody td.cell-clamped .badge {
    white-space: normal;
    overflow-wrap: break-word;
    text-align: start;
}

/* ...but wrapping ALONE trades one bug for another: a chip free to wrap grew to
   SIX lines on a seeded case label, 149px tall in a 222px row. Clamp it like
   every other run of text in this table.

   The clamp goes on the chip's inner <span>, not on the chip: `.row-parent-chip`
   is `inline-flex` so its icon and text sit on one line, and `-webkit-box` with
   a vertical orient would stack the icon ABOVE the text. Every chip in the app
   is built icon-then-span (12 call sites, 7 modules), so the span is the text.
   `align-items: flex-start` keeps the icon on the first line rather than
   floating in the middle of a two-line chip, and `min-width: 0` is what lets a
   flex item shrink below its content at all.

   Measured: chip 149px -> 44px, row 222px -> 119px, with the column widths and
   the absence of a scrollbar both unchanged — the wrap is what frees the
   column, this only bounds how tall the freed chip gets. */
.admin-table tbody td.cell-clamped .row-parent-chip {
    align-items: flex-start;
}

.admin-table tbody td.cell-clamped .row-parent-chip > span {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    min-width: 0;
}

/* THE LAST THING THAT PINS A PROSE COLUMN: its own truncating lines.

   `text-truncate` is `white-space: nowrap`, so each line's min-content is the
   WHOLE string — clamped by the `> *` max-width above, which turns that cap
   into a hard FLOOR of exactly one cap-width. Measured on the running
   /sessions page: the widest سبب الجلسة line wants 508px, clamps to 352, and
   the column then refused to move off 384px (352 + padding) no matter how
   tight the table got. Every other column paid for it and the register scrolled
   sideways.

   Switching those lines to a ONE-line clamp fixes it with no visual change at
   all: -webkit-box + line-clamp: 1 renders exactly like text-truncate — a
   single line, ellipsised — but `white-space` goes back to `normal`, so the
   min-content contribution drops to the longest WORD while the max-width above
   still caps how wide the line will ever ask to be. The prose column can then
   yield, which is the whole point of the tiered floors.

   Measured on /sessions at a 1282px table: سبب الجلسة 384 -> 251 (gives up
   133px), المكتب 160 -> 194, total demand lands exactly on the available width
   and the horizontal scrollbar disappears.

   Scoped to clamped cells. A `.text-truncate` anywhere else in the app keeps
   Bootstrap's behaviour untouched. */
.admin-table tbody td.cell-clamped .text-truncate {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    line-clamp: 1;
    white-space: normal;
    overflow: hidden;
    overflow-wrap: break-word;
}

/* THE STACKED VARIANT. A cell that already renders a bold title over a muted
   description (the tasks / sessions / meetings / adminworks / notifications
   registers) keeps `text-truncate` on EACH inner line — which the rule directly
   above turns into a one-line clamp, visually identical and compressible — and takes only
   .cell-clamped on the <td>. Do not collapse those into a single .cell-clamp-2
   — a two-line clamp spanning both would let a long title eat the description's
   line and destroy the distinction the stacked cell exists to make. Chips under
   the text stay outside the truncating divs for the same reason: a chip inside
   a text-truncate line is ellipsised along with the text beside it.

   Narrower cap, for a column holding a short LABEL rather than prose: a
   person's or company's name wants ~14rem, where a case subject wants 22.
   Retunes the rule above through its custom property instead of authoring a
   second width — so there is still exactly one place the cap is written.

   A bare class is safe here where it would not be above: nothing else in the
   app declares --cell-clamp-width, so there is no competing declaration for
   specificity to arbitrate. */
.cell-clamped-sm {
    --cell-clamp-width: 14rem;
}

/* Same shape as .attachment-card-name and .cp-case-card-subject.

   `break-word`, NOT `anywhere`: the two look identical until the column is under
   pressure, and then they differ in exactly the way that matters. Per CSS Text
   3, the break opportunities `anywhere` introduces ARE counted when computing
   min-content, so an `anywhere` cell can be squeezed to ONE CHARACTER and its
   text broken mid-word — المكت / ب. `break-word` breaks a token that would
   otherwise overflow, but its opportunities are NOT counted, so the column keeps
   a min-content of one whole word. Widening past the cap is already prevented by
   the `> *` max-width above, which is the job `anywhere` used to be doing here
   before that rule existed. */
.cell-clamp-2 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    overflow-wrap: break-word;
    line-height: 1.45;
}

.admin-table tbody tr:hover {
    background: color-mix(in srgb, var(--primary) 3%, transparent);
}

/*
 * Expandable detail row — the cases register's overflow columns.
 *
 * A wide table can either scroll sideways or hand its secondary columns to a
 * row that opens on demand. The cases table does the second: seven columns
 * stay, nine live here, opened by the chevron button in the actions cell.
 *
 * The panel is a quiet recessed card, inset from the row edges so it reads as
 * belonging to the case above it rather than as the next record. NO hover tint
 * — it is not independently hoverable, and inheriting the row hover would make
 * it look like a separate record.
 */
/* Scoped through .admin-table tbody on purpose, not written as a bare
   .case-detail-row: the base cell rule is `.admin-table tbody td` (0,1,2),
   which outranks a lone class + element (0,1,1) and would keep its own
   padding here. */
.admin-table tbody .case-detail-row > td,
.admin-table tbody .session-detail-row > td,
.admin-table tbody .meeting-detail-row > td,
.admin-table tbody .admin-work-detail-row > td,
.admin-table tbody .task-detail-row > td,
.admin-table tbody .consultation-detail-row > td,
.admin-table tbody .client-detail-row > td,
.admin-table tbody .poa-detail-row > td,
.admin-table tbody .template-detail-row > td,
.admin-table tbody .staff-notification-detail-row > td,
.admin-table tbody .client-notification-detail-row > td,
.admin-table tbody .case-number-detail-row > td,
.admin-table tbody .office-detail-row > td,
.admin-table tbody .employee-detail-row > td,
.admin-table tbody .journal-detail-row > td,
.admin-table tbody .invoice-detail-row > td,
.admin-table tbody .expense-detail-row > td,
.admin-table tbody .revenue-detail-row > td,
.admin-table tbody .cp-invoice-detail-row > td,
.admin-table tbody .support-detail-row > td {
    /* Both zeroed so the PANEL inside owns the whole visual. A background or a
       padding here would survive the close tween as a full-bleed band for its
       whole 0.2s, because neither one shrinks with the inner div's height. */
    background: none;
    padding: 0;
}

/* Killed on the ROW, not masked at the cell. The cell used to paint itself
   opaque to hide `.admin-table tbody tr:hover`, which only worked while it had
   a background of its own to hide behind. (0,3,1) outranks that rule's (0,2,2)
   on class count. */
.admin-table tbody .case-detail-row:hover,
.admin-table tbody .session-detail-row:hover,
.admin-table tbody .meeting-detail-row:hover,
.admin-table tbody .admin-work-detail-row:hover,
.admin-table tbody .task-detail-row:hover,
.admin-table tbody .consultation-detail-row:hover,
.admin-table tbody .client-detail-row:hover,
.admin-table tbody .poa-detail-row:hover,
.admin-table tbody .template-detail-row:hover,
.admin-table tbody .staff-notification-detail-row:hover,
.admin-table tbody .client-notification-detail-row:hover,
.admin-table tbody .case-number-detail-row:hover,
.admin-table tbody .office-detail-row:hover,
.admin-table tbody .employee-detail-row:hover,
.admin-table tbody .journal-detail-row:hover,
.admin-table tbody .invoice-detail-row:hover,
.admin-table tbody .expense-detail-row:hover,
.admin-table tbody .revenue-detail-row:hover,
.admin-table tbody .cp-invoice-detail-row:hover,
.admin-table tbody .support-detail-row:hover {
    background: none;
}

/*
 * overflow: hidden is what makes the GSAP height tween read as a reveal
 * rather than a clip-less slide. Padding sits INSIDE this element so it
 * collapses with the height — on the <td> it would linger as a visible
 * band for the length of the close tween.
 */
.case-detail-body {
    overflow: hidden;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 0.5rem 1.5rem;
    padding: 1rem 1.25rem;
}

.case-detail-item {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.875rem;
    min-width: 0;
}

/* --text-secondary, not --text-muted: the latter is only 2.5:1 on the light
   card, below the AA floor. */
.case-detail-label {
    color: var(--text-secondary);
    font-weight: 600;
    white-space: nowrap;
}

.case-detail-value {
    color: var(--text);
    min-width: 0;
    overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
 * The LIST row's panel chrome.
 *
 * Everything above is shared with the /cases/{id} page (see the block header
 * below). Everything in this sub-section is scoped to the expandable-row
 * classes — .case-detail-row (/cases), .session-detail-row (/sessions),
 * .meeting-detail-row (/meetings), .task-detail-row (/tasks),
 * .consultation-detail-row (/consultations), .client-detail-row (/clients),
 * .poa-detail-row (/powers-of-attorney),
 * .template-detail-row (/templates),
 * .case-number-detail-row (the أرقام القضية tab on /cases/{id}),
 * .staff-notification-detail-row (/staff-notifications),
 * .client-notification-detail-row (/client-notifications),
 * .office-detail-row (/offices), .employee-detail-row (/employees),
 * .support-detail-row (/support) and
 * .admin-work-detail-row (/administrative-works AND the الأعمال الإدارية tab on
 * /cases/{id}), which are fifteen row classes over sixteen tables using ONE
 * pattern:
 * the selector lists carry all of them rather than the block being copied,
 * because a second copy is how they drift. The
 * inner .case-detail-* class names stay as they are on all fifteen; renaming
 * them per page would fork the shared grid above for nothing.
 *
 * Note that .admin-work-detail-row now serves TWO tables with different column
 * counts — ten on the register, seven on the case tab. Nothing here cares,
 * because the colspan is read off each table's own thead at render time and
 * never hardcoded; that is exactly the property that lets one class cover both.
 *
 * A NEW list page adopting this pattern must be added to SEVEN selector lists,
 * not one: the two above (cell reset + hover kill) and the five below. Miss the
 * chrome ones and the panel still opens — it just renders as bare text in a
 * table cell instead of a recessed card, which is the only symptom you get.
 * Miss the last (the label colon) and it renders correctly but punctuated
 * differently from every other panel in the app.
 *
 * On the detail page these fields already sit inside
 * an .admin-section-card, and a second tinted, bordered surface in there would
 * be a box inside a box.
 *
 * Deliberately NEUTRAL — no primary tint and no accent bar. This is a quiet
 * recessed surface inside an already-busy register: the table is the loud thing
 * on the page, and a coloured panel under one row competed with it. Shape does
 * the work instead (inset, radius, hairline, a barely-there shadow).
 *
 * NOT .form-callout (_forms.css) for the same reason, on top of the fact that
 * the class means "a distinct step/group inside a FORM" and this is read-only
 * data in a table.
 *
 * --bg-secondary over the card's --card is the project's recessed-surface pair
 * and is defined in BOTH themes, so this needs no dark-mode override.
 * ------------------------------------------------------------------------- */
.case-detail-row .case-detail-body,
.session-detail-row .case-detail-body,
.meeting-detail-row .case-detail-body,
.admin-work-detail-row .case-detail-body,
.task-detail-row .case-detail-body,
.consultation-detail-row .case-detail-body,
.client-detail-row .case-detail-body,
.poa-detail-row .case-detail-body,
.template-detail-row .case-detail-body,
.staff-notification-detail-row .case-detail-body,
.client-notification-detail-row .case-detail-body,
.case-number-detail-row .case-detail-body,
.office-detail-row .case-detail-body,
.employee-detail-row .case-detail-body,
.journal-detail-row .case-detail-body,
.invoice-detail-row .case-detail-body,
.expense-detail-row .case-detail-body,
.revenue-detail-row .case-detail-body,
.cp-invoice-detail-row .case-detail-body,
.support-detail-row .case-detail-body {
    /* The inset: the panel sits inside the row rather than running edge to
       edge, which is what separates it from the case above it. */
    margin: 0.5rem 0.75rem 1rem;
    /* Generous on purpose, and the horizontal side more so than the vertical:
       every value in here is start-aligned, so the inline padding is the only
       thing standing between the longest one and the border. */
    padding: 1.5rem 1.85rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--bg-secondary);
    box-shadow: var(--shadow-sm);
    /* Stacked pairs need less width than inline ones, so more of them fit per
       row; the gap grows to keep the columns visually separate without a rule
       between them. */
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 1.15rem 2rem;
}

/*
 * Label ABOVE value, not beside it. Inline pairs ragged-align their values at
 * whatever width each label happens to be, so eight of them in a grid read as
 * scattered text; stacking puts every value on the same start edge.
 */
.case-detail-row .case-detail-item,
.session-detail-row .case-detail-item,
.meeting-detail-row .case-detail-item,
.admin-work-detail-row .case-detail-item,
.task-detail-row .case-detail-item,
.consultation-detail-row .case-detail-item,
.client-detail-row .case-detail-item,
.poa-detail-row .case-detail-item,
.template-detail-row .case-detail-item,
.staff-notification-detail-row .case-detail-item,
.client-notification-detail-row .case-detail-item,
.case-number-detail-row .case-detail-item,
.office-detail-row .case-detail-item,
.employee-detail-row .case-detail-item,
.journal-detail-row .case-detail-item,
.invoice-detail-row .case-detail-item,
.expense-detail-row .case-detail-item,
.revenue-detail-row .case-detail-item,
.cp-invoice-detail-row .case-detail-item,
.support-detail-row .case-detail-item {
    flex-direction: column;
    align-items: stretch;
    gap: 0.2rem;
}

.case-detail-row .case-detail-label,
.session-detail-row .case-detail-label,
.meeting-detail-row .case-detail-label,
.admin-work-detail-row .case-detail-label,
.task-detail-row .case-detail-label,
.consultation-detail-row .case-detail-label,
.client-detail-row .case-detail-label,
.poa-detail-row .case-detail-label,
.template-detail-row .case-detail-label,
.staff-notification-detail-row .case-detail-label,
.client-notification-detail-row .case-detail-label,
.case-number-detail-row .case-detail-label,
.office-detail-row .case-detail-label,
.employee-detail-row .case-detail-label,
.journal-detail-row .case-detail-label,
.invoice-detail-row .case-detail-label,
.expense-detail-row .case-detail-label,
.revenue-detail-row .case-detail-label,
.cp-invoice-detail-row .case-detail-label,
.support-detail-row .case-detail-label {
    font-size: 0.75rem;
    /* NO letter-spacing and NO text-transform, which is where the usual
       "small caps label" treatment would go: Arabic has no letter case, so
       uppercase is a no-op, and letter-spacing pulls apart the cursive joins
       that make Arabic words legible. */
    white-space: normal;
}

.case-detail-row .case-detail-value,
.session-detail-row .case-detail-value,
.meeting-detail-row .case-detail-value,
.admin-work-detail-row .case-detail-value,
.task-detail-row .case-detail-value,
.consultation-detail-row .case-detail-value,
.client-detail-row .case-detail-value,
.poa-detail-row .case-detail-value,
.template-detail-row .case-detail-value,
.staff-notification-detail-row .case-detail-value,
.client-notification-detail-row .case-detail-value,
.case-number-detail-row .case-detail-value,
.office-detail-row .case-detail-value,
.employee-detail-row .case-detail-value,
.journal-detail-row .case-detail-value,
.invoice-detail-row .case-detail-value,
.expense-detail-row .case-detail-value,
.revenue-detail-row .case-detail-value,
.cp-invoice-detail-row .case-detail-value,
.support-detail-row .case-detail-value {
    font-size: 0.9rem;
    font-weight: 500;
}

/*
 * The colon after every label in the expandable panel — the same rule tab
 * المعلومات الأساسية gets further down (`#tabInfo .case-detail-label::after`),
 * for the same reasons: the labels are t('...form.*') keys shared with each
 * page's own form, so the colon cannot live in the message strings, and
 * ::after puts it at the label's logical end so it flips with the locale.
 *
 * Scoped to the fourteen ROW classes rather than added to the base
 * .case-detail-label, because that class is also worn bare — with no
 * -detail-row ancestor — by three PAGE-level surfaces: tab المعلومات الأساسية
 * on /cases/{id} (#tabInfo), سجل الموكل (#clientDetailPage) and the client
 * hub's بيانات القضية grid (#cpSubInfo). Each of those colons itself through
 * its own scoped rule, so a bare .case-detail-label::after would happen to
 * produce the same output today — the scoping is what keeps each surface's
 * punctuation its own decision rather than a side effect of this one.
 * (The /support ticket panel used to be the colon-LESS example here. It is a
 * row-class panel now, so that exception is gone.)
 *
 * Fifteen tables, not fourteen: .admin-work-detail-row serves the
 * /administrative-works register AND the الأعمال الإدارية tab on /cases/{id},
 * so that tab's panels take the colon too. That is the point of the shared
 * class — the two are the same panel reached from two URLs, and colon-ing only
 * one of them would be the drift this block exists to prevent.
 */
.case-detail-row .case-detail-label::after,
.session-detail-row .case-detail-label::after,
.meeting-detail-row .case-detail-label::after,
.admin-work-detail-row .case-detail-label::after,
.task-detail-row .case-detail-label::after,
.consultation-detail-row .case-detail-label::after,
.client-detail-row .case-detail-label::after,
.poa-detail-row .case-detail-label::after,
.template-detail-row .case-detail-label::after,
.staff-notification-detail-row .case-detail-label::after,
.client-notification-detail-row .case-detail-label::after,
.case-number-detail-row .case-detail-label::after,
.office-detail-row .case-detail-label::after,
.employee-detail-row .case-detail-label::after,
.journal-detail-row .case-detail-label::after,
.invoice-detail-row .case-detail-label::after,
.expense-detail-row .case-detail-label::after,
.revenue-detail-row .case-detail-label::after,
.cp-invoice-detail-row .case-detail-label::after,
.support-detail-row .case-detail-label::after {
    content: ':';
}

/*
 * A PARAGRAPH in the panel, rather than a value.
 *
 * Worn ALONGSIDE .case-detail-item, so the pair still stacks label-over-content
 * like every field beside it and still takes the colon above — this adds only
 * the span. The grid's minmax(190px, 1fr) track is right for a name, a date or
 * a status and wrong for a textarea body: /support's وصف المشكلة is a
 * 10,000-character field, and in one track it renders as a ribbon down the side
 * of the panel with every other field's worth of empty space beside it.
 *
 * The content element inside it is .case-activity-details, NOT
 * .case-detail-value: that is where the pre-wrap lives, and the writer's own
 * newlines are content — collapsing them merges a numbered list into one
 * paragraph. This is renderLongText() in case-detail.js, which gives the two
 * contract bodies on /cases/{id} exactly this treatment for exactly this
 * reason; the only difference is that this one stays INSIDE .case-detail-body,
 * so it sits within the recessed card rather than under it.
 *
 * The ONE rule here that is deliberately not scoped to the row classes. The
 * four inner .case-detail-* names are scoped because they are shared with
 * page-level surfaces (#tabInfo, #clientDetailPage, #cpSubInfo) that lay their
 * own long text out; this class is new and worn by nothing else, so there is
 * nothing for a bare rule to reach. Spanning every track is also what a page
 * surface would want if it ever adopted it.
 *
 * NOT a new inner class in the seven selector lists — those stay seven. A page
 * with no paragraph field simply never emits this.
 */
.case-detail-longtext {
    grid-column: 1 / -1;
}

/*
 * .case-detail-wide -- a SECTION that spans every track: a table, a card grid.
 * The client portal's invoice row (2026-09-09) is the one host: its body is
 * the stacked-pairs grid above, and its blocks (the lines, the schedule, the
 * receipts, the attachments) are not pairs. Dropped in bare, each one became a
 * 190px cell: the lines table scrolled inside a sliver showing # and the
 * description alone, and the receipts and attachments texts sat beside it.
 * The same span as .case-detail-longtext, kept as a second name because a
 * table is not long text and must not inherit that rule's pre-wrap contract.
 * min-width: 0 is load-bearing: a grid item's automatic minimum is its
 * content's min-content, so a six-column table wider than the panel would
 * widen the row instead of scrolling inside its own .table-responsive -- and
 * the page must never scroll sideways. Like the rule above it is NOT an
 * eighth selector list: a row with no section simply never emits it.
 */
.case-detail-wide {
    grid-column: 1 / -1;
    min-width: 0;
}

/* ===========================================================================
   CASE DETAIL PAGE — /cases/{id}
   ===========================================================================
   The four .case-detail-* rules above are SHARED with the list's expandable
   row: one grid definition serves both the peek and the record. What the list
   row adds on top of them — the recessed panel and the stacked label/value
   pairs — is scoped to .case-detail-row and does NOT reach here,
   because these fields already sit inside an .admin-section-card and a second
   surface inside it would be a box in a box. Everything below is only the page.
   =========================================================================== */

/*
 * The grid above ships padding of its own. Inside an .admin-section-card (which
 * already pads 1.5rem) that reads as a double indent, so it is zeroed HERE
 * rather than removed from the base rule — the list row still needs it.
 *
 * `>` IS LOAD-BEARING, not tidiness. On this page .case-detail-body is a direct
 * child of the section card; on the cases LIST it is nested far below one
 * (card > .table-responsive > table > tbody > tr > td > div), so a descendant
 * selector here matched the list row too. It ties (0,2,0) with
 * `.case-detail-row .case-detail-body` above and, coming later in the file,
 * won — silently stripping the list panel's padding and leaving its text flush
 * against the border. The child combinator drops this to the page only.
 *
 * overflow: hidden comes along too. It exists for the GSAP height tween and is
 * inert outside it, so it is left alone rather than unset.
 */
.admin-section-card > .case-detail-body {
    padding: 0;
}

/*
 * The colon after every field label on tab المعلومات الأساسية.
 *
 * In CSS rather than in the label text, and that is not a style preference:
 * those labels are t('cases.form.*') — the SAME keys the create/edit form
 * renders, reused deliberately so the two screens say the same word for the
 * same field (see renderInfo in case-detail.js). A colon added to the message
 * would follow them onto the form, where each label already carries a `*`.
 *
 * Scoped to #tabInfo because .case-detail-label is shared with every
 * expandable detail row in the app (/cases, /sessions, /meetings, /tasks,
 * /consultations, /clients, /powers-of-attorney, /offices, /employees,
 * /support, both notification registers and /administrative-works), the
 * الأعمال الإدارية and أرقام القضية tabs of THIS page, سجل الموكل and the
 * client hub. A bare .case-detail-label::after would colon all of them.
 *
 * #tabInfo is NOT unique across the app — the employees modal names its first
 * pane the same thing, and so does the client record page (/clients/{id}).
 * Harmless for the modal because that pane renders form controls and never a
 * .case-detail-label; if it ever does, this rule reaches it too, and the fix is
 * a page-scoped ancestor (#caseDetailPage), not a rename. The client record page
 * DOES render labels in its #tabInfo, and it took that advice rather than the
 * free ride — see the rule directly below.
 *
 * ::after, not a colon prepended to the value: the pseudo-element sits at the
 * label's logical end, so it renders on the LEFT of the Arabic text and flips
 * to the right by itself in the English locale — no per-direction rule. It is
 * also outside the esc()'d value, so it cannot collide with user data.
 *
 * Field labels only. The .section-title headings above each group
 * (البيانات الأساسية, التصنيف, ...) are headings, not labels, and take none.
 */
#tabInfo .case-detail-label::after {
    content: ':';
}

/*
 * The same colon, for سجل الموكل (/clients/{id}).
 *
 * Scoped on the PAGE, not on #tabInfo, even though that page names its profile
 * pane #tabInfo and would therefore be covered by the rule above. Riding it
 * would make this page's punctuation depend on an id two other surfaces also
 * use — exactly what the comment above warns about — so the page owns its own
 * rule instead. The overlap is one identical declaration and costs nothing;
 * renaming that pane later leaves the colons working.
 *
 * Same reasoning as the rule itself: those labels are t('clients.form.*') /
 * t('clients.field.*'), the SAME keys the client form renders, so a ':' in the
 * message bundle would follow them onto the form beside the required marker.
 *
 * The page's six data panes are tables and carry no .case-detail-label, so
 * scoping on the page rather than the pane reaches nothing extra today.
 */
#clientDetailPage .case-detail-label::after {
    content: ':';
}

/*
 * A tab pane. Nothing but spacing — d-none does the actual switching, so this
 * class must never set `display`, or removing d-none would not reveal it.
 */
.case-detail-pane {
    margin-top: 1.5rem;
}

/* One entry in the تحديثات وملاحظات feed. */
.case-activity-item {
    padding: 1rem 1.25rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--card);
    margin-bottom: 0.75rem;
}

.case-activity-item:last-child {
    margin-bottom: 0;
}

/*
 * A comment body is typed into a <textarea>, so its newlines are content —
 * collapsing them would merge a list into one paragraph. overflow-wrap keeps a
 * pasted URL from widening the card past the viewport.
 */
.case-activity-details {
    color: var(--text);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    margin-bottom: 0;
}

.case-activity-meta {
    color: var(--text-secondary);
    font-size: 0.8125rem;
}

/*
 * Feed filter — a SEGMENTED CONTROL: one recessed track holding three pills,
 * exactly one of them raised. The shape carries the "pick one of these" meaning
 * on its own, which a row of three separately-outlined buttons does not.
 *
 * Built from project tokens, not Bootstrap's .btn-group + .btn-outline-*: those
 * are coloured through --bs-* variables this app never sets (we switch on
 * data-theme), so the borders would stay light-theme forever on a dark card,
 * and btn-group's negative margins fight the pill radius.
 */
.case-activity-filters {
    display: inline-flex;
    gap: 0.125rem;
    padding: 0.25rem;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: 999px;
}

.case-activity-filters .filter-pill {
    border: none;                       /* the track draws the only border */
    background: transparent;
    color: var(--text-secondary);
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
    padding: 0.4rem 1rem;
    border-radius: 999px;
    cursor: pointer;
    transition: all var(--transition-normal);
}

.case-activity-filters .filter-pill:hover:not(.active) {
    color: var(--text);
    background: var(--card);
}

/*
 * The raised one. A real shadow rather than only a fill: on the dark theme the
 * track and the primary fill sit close enough in value that fill alone reads
 * flat, and the lift is what makes the selection obvious at a glance.
 */
.case-activity-filters .filter-pill.active {
    background: var(--primary);
    color: var(--on-primary);
    box-shadow: var(--shadow-sm);
}

/* Keyboard focus must stay visible on a borderless control — without this the
   ring would be the only affordance and it is easy to lose against the fill. */
.case-activity-filters .filter-pill:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/*
 * Every table wrapper in the app is Bootstrap's .table-responsive. The
 * scrollbar SKIN is global (_base.css); the one behaviour rule here stops a
 * trackpad swipe at the end of the table from triggering the browser's
 * back-navigation gesture.
 */
.table-responsive {
    overscroll-behavior-x: contain;
}

/*
 * Wide data table — pair with Bootstrap's .table-responsive on the wrapper
 * (which also brings the thin scrollbar skin above; this class adds no
 * scrollbar styling of its own).
 *
 * .admin-table is width: 100%, so a many-column table does NOT overflow its
 * wrapper: it fits by crushing every column instead. .table-responsive alone
 * therefore never shows a scrollbar, and the columns squeeze until each cell
 * wraps to three or four lines. The min-width is what converts that squeeze
 * into a scroll — without it .table-responsive has nothing to scroll.
 *
 * The default 1800px suits roughly sixteen columns at ~112px each, which holds
 * a date or a short lookup name on one line. Override --table-min-width per
 * page rather than adding another class.
 *
 * NOTE: no page uses this today. The cases table was its only consumer and
 * dropped to seven columns (its overflow moved into .case-detail-row above),
 * so it now fits without scrolling. Kept because it is the answer for the next
 * genuinely wide table — but before reaching for it, check whether that table
 * wants a detail row instead. Scrolling sideways loses the identifying column
 * off the start edge, which is what pushed cases off it.
 */
.admin-table-scroll {
    --table-min-width: 1800px;
}

.admin-table-scroll > .admin-table {
    min-width: var(--table-min-width);
}

/* Permissions catalog — group header row */
/* Same audit as the table header above — these headings are Arabic area
   names («المحاسبة», «إدارة القضايا»). */
.perm-group-row td {
    background: var(--bg-secondary);
    color: var(--text-secondary);
    font-size: 0.8125rem;
    padding: 0.5rem 1rem;
    border-bottom: 1px solid var(--border);
}

/* The level above the area rows: one row per PermissionCategory (the module
   atlas' six groups — «القضايا والأعمال القانونية», «المالية والمحاسبة», …), which the
   role editor draws as fieldsets. A shade stronger than the area row under
   it, and a rule above it that closes the previous group. */
.perm-category-row td {
    background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary));
    color: var(--text);
    font-size: 0.875rem;
    font-weight: 600;
    padding-block: 0.6rem;
    border-top: 2px solid var(--border);
}

/* Permissions catalog — description cell */
.perm-desc {
    color: var(--text-secondary);
}

/* Permissions catalog — theme-aware badges */
.perm-badge-roles {
    background: color-mix(in srgb, var(--primary) 15%, transparent);
    color: var(--primary);
}

.perm-badge-users {
    background: var(--bg-secondary);
    color: var(--text-secondary);
    border: 1px solid var(--border);
}

.admin-search {
    max-width: 320px;
}

/* ============================================
   BREADCRUMB — the page-location trail
   ============================================

   Bootstrap ships .breadcrumb with a "/" divider and its own --bs-breadcrumb-*
   colours, and this app sets NONE of those variables. So the trail rendered in
   Bootstrap link-blue on both themes — the one piece of chrome on the page that
   did not follow data-theme — separated by a slash the rest of the UI never
   uses. Every token below is ours.

   On the bare .breadcrumb class deliberately, NOT scoped to a page. Three trails
   exist and they are the same object: /cases/{id} and /cases/new|{id}/edit (page
   location, an <a> crumb) and the attachments tab's folder trail on /cases/{id},
   whose first crumb is a <button data-action="attachmentHome"> because it filters
   a grid instead of navigating. That is why .btn-link appears beside a in every
   selector list here — style the anchor alone and the folder trail is the odd one
   out on the same page. */
.breadcrumb {
    /* Bootstrap spaces crumbs with padding-left on the item and padding-right on
       the divider (mirrored in the RTL build). Zeroed: the chevron carries its
       own margin-inline below, which needs no direction rule. */
    --bs-breadcrumb-item-padding-x: 0;
    --bs-breadcrumb-margin-bottom: 0;
    align-items: center;
    font-size: 0.8125rem;
}

/* Flex so the divider — a sized ::before, which would be a zero-height inline
   box otherwise — becomes a flex item and centres against the crumb text. */
.breadcrumb-item {
    display: flex;
    align-items: center;
}

/*
 * The divider: a chevron drawn from two borders, pointing the way the trail
 * reads. Borders rather than a content string so it takes --text-muted and
 * follows the theme; a glyph would be stuck at whatever the font gives.
 *
 * float: none is REQUIRED, not tidying. Bootstrap floats this ::before (left in
 * the LTR build, right in the RTL one); a float inside the <li> takes it out of
 * that flex line and drops the chevron off the baseline.
 *
 * The rotation is the ONE physical thing in this block — borders can be logical,
 * a transform cannot — so it is mirrored by a single html[dir="rtl"] rule in
 * app-rtl.css. If the chevron ever points backwards in Arabic, that rule is what
 * went missing.
 */
.breadcrumb-item + .breadcrumb-item::before {
    content: '';
    float: none;
    flex: 0 0 auto;
    width: 0.4em;
    height: 0.4em;
    margin-inline: 0.55rem 0.5rem;
    border-top: 1.5px solid var(--text-muted);
    border-right: 1.5px solid var(--text-muted);
    transform: rotate(45deg);
}

/* The clickable crumbs. inline-flex + gap so a leading icon sits on the text
   baseline without a me-* utility on every page that renders one. */
.breadcrumb-item > a,
.breadcrumb-item > .btn-link {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.15rem 0.45rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-secondary);
    font: inherit;
    text-decoration: none;
    transition: color var(--transition-fast), background var(--transition-fast);
}

/* Pulls the padding back out of the layout so the first crumb starts on the same
   inline edge as the <h2> below it. Without this the trail sits 0.45rem inboard
   of its own page title, which reads as a misalignment rather than as padding. */
.breadcrumb-item:first-child > a,
.breadcrumb-item:first-child > .btn-link {
    margin-inline-start: -0.45rem;
}

.breadcrumb-item > a:hover,
.breadcrumb-item > .btn-link:hover {
    color: var(--primary);
    background: color-mix(in srgb, var(--primary) 10%, transparent);
    text-decoration: none;
}

/* The crumbs are small text with no underline and no button chrome, so the ring
   is the only thing that can show keyboard focus here. */
.breadcrumb-item > a:focus-visible,
.breadcrumb-item > .btn-link:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
    color: var(--primary);
    text-decoration: none;
}

/* The current page. Full --text at 600 rather than Bootstrap's muted grey: it is
   where you ARE, which is the one crumb that is not a place you can go. */
.breadcrumb-item.active {
    color: var(--text);
    font-weight: 600;
}

.admin-page-header {
    margin-bottom: 1.5rem;
}

.admin-page-header h2 {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--text);
}

.admin-page-header p {
    color: var(--text-secondary);
    margin-bottom: 0;
}

/* Tags beside the case title (its stage and type). Sized in rem, not the
   h2's em, so they read as labels rather than a second heading; they wrap
   under the title on a phone. Empty until case-detail.js fills them. */
.case-header-tags {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-inline-start: 0.75rem;
    vertical-align: middle;
}

.case-header-tags:empty {
    display: none;
}

.case-header-tags .badge {
    font-size: 0.75rem;
    font-weight: 600;
}

.admin-section-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-card);
    padding: 1.5rem;
    margin-bottom: 1.5rem;
}

.admin-section-card .section-title {
    font-size: 1.125rem;
    font-weight: 600;
    color: var(--text);
    margin-bottom: 1rem;
    padding-bottom: 0.75rem;
    border-bottom: 1px solid var(--border);
}

.admin-config-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--border);
}

.admin-config-row:last-child {
    border-bottom: none;
}

.admin-config-row .config-key {
    font-weight: 500;
    color: var(--text);
    font-size: 0.9rem;
}

.admin-config-row .config-desc {
    color: var(--text-muted);
    font-size: 0.8125rem;
}

.admin-config-row .config-value-cell {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.admin-badge {
    display: inline-flex;
    align-items: center;
    padding: 0.2em 0.6em;
    border-radius: var(--radius-pill);
    font-size: 0.75rem;
    font-weight: 500;
}

.admin-badge-tenant {
    background: color-mix(in srgb, var(--primary) 10%, transparent);
    color: var(--primary);
}

.admin-badge-global {
    background: color-mix(in srgb, var(--text-muted) 10%, transparent);
    color: var(--text-muted);
}

/* /admin/roles — the permission grid is one <fieldset> per PermissionCategory
   (the module atlas' six groups, القضايا والأعمال القانونية first), its <legend> the
   group's name with an icon, and under it each area's heading over that
   area's chip grid (2026-09-25). The legend undoes reboot's heading-sized
   legend; the area heading is what the 26 bare headings used to be, on
   --text-secondary rather than --text-muted (2.5:1 on the light card). */
.admin-perm-category {
    margin-top: 1.5rem;
}

.admin-perm-category:first-child {
    margin-top: 0.25rem;
}

.admin-perm-category-title {
    display: block;
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.4;
    color: var(--text);
    padding-bottom: 0.5rem;
    margin-bottom: 0;
    border-bottom: 1px solid var(--border);
}

/* The legend IS the group's open/close control: a full-width button carrying
   the icon, the name, the «granted of total» count and a chevron that says
   which way the body will move (down = closed, up = open — the detail-row
   rule). A button so the keyboard and a screen reader get it for free; the
   chrome a <button> brings is undone here, like button.admin-stat-card. */
.admin-perm-category-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: start;
    cursor: pointer;
}

.admin-perm-category-toggle:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 3px;
}

.admin-perm-category-toggle > i:first-child {
    color: var(--primary);
}

.admin-perm-category-count {
    font-weight: 500;
}

.admin-perm-category-chevron {
    margin-inline-start: auto;
    color: var(--text-secondary);
    font-size: 0.8125rem;
}

/* A standalone control, so it takes the touch floor the buttons take. */
@media (pointer: coarse) {
    .admin-perm-category-toggle {
        min-height: 44px;
    }
}

/* The body is a formatting-context root, and that is what keeps the first
   area heading off the legend's rule. Reboot FLOATS every <legend> and gives
   `legend + *` a `clear`, so the body is a cleared box — and clearance places
   its border edge on the float's bottom edge whatever top margin it carries.
   As a plain block the body has no padding or border, so the first heading's
   0.85rem margin collapsed INTO it and the clearance swallowed it: the heading
   sat flush under the rule in every group, and the tween's overflow:hidden
   (a root while it ran) brought the gap back for 0.25s and dropped it again
   on clearProps, so the chips jumped up as each group finished opening. A
   root contains the margin, so it stays where it was designed to be; while
   the group is closed, [hidden]'s display:none still wins. */
.admin-perm-category-body {
    display: flow-root;
}

.admin-perm-area-title {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-secondary);
    margin: 0.85rem 0 0.5rem;
}

.admin-perm-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 0.75rem;
}

.admin-perm-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
    background: var(--bg-secondary);
}

.form-switch .form-check-input {
    width: 3em;
    height: 1.5em;
    cursor: pointer;
}

.btn-admin-action {
    padding: 0.25rem 0.5rem;
    font-size: 0.8125rem;
}

/* ── Audit list: respect theme variables ── */
.admin-section-card .list-group-item {
    background-color: transparent;
    color: var(--text);
    border-color: var(--border);
}

.admin-section-card .list-group-item .text-muted {
    color: var(--text-secondary) !important;
}

.admin-section-card .list-group-item .fw-medium {
    color: var(--text);
}

@media (max-width: 768px) {
    .admin-stat-card .stat-value {
        font-size: 1.5rem;
    }

    .admin-config-row {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.5rem;
    }
}

/* ── Theme-aware tables in admin pages ────────────────────────────────────── */
.admin-section-card .table,
.modal-content .table {
    color: var(--text);
    background-color: transparent;
    --bs-table-bg: transparent;
    --bs-table-color: var(--text);
    --bs-table-border-color: var(--border);
    --bs-table-striped-bg: color-mix(in srgb, var(--text) 4%, transparent);
    --bs-table-striped-color: var(--text);
    --bs-table-hover-bg: color-mix(in srgb, var(--primary) 8%, transparent);
    --bs-table-hover-color: var(--text);
    border-color: var(--border);
}

.admin-section-card .table > :not(caption) > * > *,
.modal-content .table > :not(caption) > * > * {
    background-color: transparent;
    color: var(--text);
    border-bottom-color: var(--border);
}

.admin-section-card .table > thead,
.modal-content .table > thead {
    background-color: var(--bg-secondary);
}

.admin-section-card .table > thead th,
.modal-content .table > thead th {
    color: var(--text-secondary);
    background-color: var(--bg-secondary);
    border-bottom-color: var(--border);
    font-weight: 600;
}

/* Bootstrap's .table-light header → use theme surface instead */
.admin-section-card .table-light,
.admin-section-card .table > :not(caption) > .table-light > *,
.modal-content .table-light,
.modal-content .table > :not(caption) > .table-light > * {
    --bs-table-bg: var(--bg-secondary);
    --bs-table-color: var(--text-secondary);
    background-color: var(--bg-secondary);
    color: var(--text-secondary);
    border-color: var(--border);
}

/* ── Theme-aware list-group inside modals ─────────────────────────────────── */
.modal-content .list-group {
    --bs-list-group-bg: transparent;
    --bs-list-group-color: var(--text);
    --bs-list-group-border-color: var(--border);
    --bs-list-group-action-color: var(--text);
    --bs-list-group-action-hover-color: var(--text);
    --bs-list-group-action-hover-bg: color-mix(in srgb, var(--primary) 8%, transparent);
}

.modal-content .list-group-item {
    background-color: transparent;
    color: var(--text);
    border-color: var(--border);
}

.modal-content .list-group-item .text-muted,
.modal-content .list-group-item .small.text-muted {
    color: var(--text-secondary) !important;
}

/* ── Theme-aware nav-tabs in admin pages ─────────────────────────────────── */
.admin-tabs {
    --bs-nav-tabs-border-color: var(--border);
    --bs-nav-tabs-link-active-color: var(--text);
    --bs-nav-tabs-link-active-bg: var(--card);
    --bs-nav-tabs-link-active-border-color: var(--border) var(--border) var(--card);
    --bs-nav-tabs-link-hover-border-color: var(--border);
    border-bottom-color: var(--border);
}

.admin-tabs .nav-link {
    color: var(--text-secondary);
    border: 1px solid transparent;
    background: transparent;
    margin-bottom: -1px;
}

.admin-tabs .nav-link:hover,
.admin-tabs .nav-link:focus {
    color: var(--text);
    border-color: var(--border) var(--border) transparent;
    background: color-mix(in srgb, var(--primary) 5%, transparent);
}

.admin-tabs .nav-link.active {
    color: var(--text);
    background: var(--card);
    border-color: var(--border) var(--border) var(--card);
}

/* Tab pane card sits flush against the tab strip (no double top border). */
.admin-tab-content > .tab-pane > .admin-section-card {
    border-top-left-radius: 0;
    /* keep the right corner rounded; visually anchor to the active tab */
}
/* ===========================================
   LOOKUPS — source panel (/lookups page)
   =========================================== */

/*
 * The scroll container. scrollbar-gutter reserves the track so the list does
 * not shift sideways the moment it becomes scrollable, and overscroll-behavior
 * stops a scroll that reaches the end from chaining to the page behind it.
 */
.lookup-source-panel {
    max-height: 62vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
    padding-inline-end: 0.25rem;
    /* Scrollbar skin comes from the global rules in _base.css. */
}

/*
 * Category group (lookup_sources.category, from the DB) — collapsible, using
 * the same open/caret mechanics as the sidebar nav groups in _sidebar.css:
 * children hidden until .open, caret rotates 180deg. Groups start open.
 */
/*
 * TYPOGRAPHY, and why it is not the usual "section eyebrow" (0.7rem + 600 +
 * letter-spacing + uppercase + --text-muted) this file uses for table headers,
 * the permission group rows and .dropdown-header.
 *
 * `text-transform: uppercase` is a NO-OP in Arabic — the script has no letter
 * case — so the eyebrow only ever rendered in English, where it turned the
 * bundle's `Technical Support` into TECHNICAL SUPPORT and cost real width in a
 * narrow rail on the two longest labels. A treatment that is invisible in the
 * default locale and disruptive in the other is not carrying its weight, so it
 * went, and `letter-spacing` went with it: the two were tuned together and
 * spacing without caps just loosens a lowercase word.
 *
 * That leaves size and weight to keep the heading subordinate to the source
 * buttons under it. 0.8125rem against their 0.875rem does that while staying
 * readable; 0.7rem was legible only BECAUSE it was uppercase, and lowercase at
 * that size reads as smaller than its own children.
 *
 * --text-secondary, not --text-muted: muted is 2.5:1 on the light rail, under
 * the 4.5:1 AA floor, and this is a heading rather than decoration. Same call
 * the form-hint rules in _forms.css make.
 */
.lookup-category-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    margin: 0.875rem 0 0.25rem;
    padding: 0.25rem 0.5rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    font-size: 0.8125rem;
    font-weight: 600;
    text-align: start;
    color: var(--text-secondary);
    transition: color var(--transition-fast);
}

.lookup-category-toggle:hover {
    color: var(--text);
}

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

.lookup-category:first-child .lookup-category-toggle {
    margin-top: 0;
}

.lookup-category-label {
    flex: 1 1 auto;
    min-width: 0;
}

.lookup-category-count {
    flex: 0 0 auto;
    font-weight: 500;
    opacity: 0.7;
}

.lookup-category-caret {
    flex: 0 0 auto;
    font-size: 0.85em;
    transition: transform var(--transition-fast);
}

.lookup-category.open > .lookup-category-toggle .lookup-category-caret {
    transform: rotate(180deg);
}

.lookup-category-items {
    display: none;
}

.lookup-category.open > .lookup-category-items {
    display: block;
}

/*
 * A source row.
 *
 * white-space: normal is the fix for the clipped-label bug: source names run to
 * 40 characters ("أنواع الصحف والمذكرات") in a col-lg-3 rail, and a button's
 * default nowrap pushed the tail outside the scroll container, where overflow-y
 * clipped it. In RTL that hid the START of the name, so the item read as blank.
 *
 * text-align: start rather than Bootstrap's .text-start — a logical property is
 * correct in both directions no matter which Bootstrap build is loaded, and
 * CLAUDE.md prefers logical properties in new CSS.
 */
.lookup-source-btn {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.5rem 0.625rem;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--text);
    font-size: 0.875rem;
    line-height: 1.35;
    text-align: start;
    white-space: normal;
    overflow-wrap: anywhere;
    transition: background var(--transition-normal), color var(--transition-normal);
}

.lookup-source-btn:hover {
    background: var(--bg-secondary);
}

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

.lookup-source-btn.active {
    background: var(--primary);
    border-color: var(--primary);
    color: var(--on-primary);
}

/* min-width:0 lets the name shrink inside the flex row so it wraps instead of
   forcing the badge out of the button. */
.lookup-source-name {
    flex: 1 1 auto;
    min-width: 0;
}

.lookup-source-btn.active .badge {
    background: color-mix(in srgb, var(--on-primary) 25%, transparent) !important;
    color: var(--on-primary) !important;
}


/* =========================================================================
 * TASKS — progress cell and calendar legend (/tasks)
 * ========================================================================= */

/* The progress bar in the table's التقدم column. Inline-block beside its
   percentage label so the pair stays on one line in a narrow cell. */
.task-progress {
    display: inline-block;
    vertical-align: middle;
    width: 60px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--text-secondary) 20%, transparent);
    overflow: hidden;
}

.task-progress-bar {
    height: 100%;
    border-radius: var(--radius-pill);
    background: var(--primary);
}

/* The WIDTH is the only thing set from data, and it is a clamped 0-100 number
   built in JS — never a colour, and never a value that came from the database.
   Task colours all travel as palette tokens through a whitelist. */

.task-progress-label {
    margin-inline-start: 0.375rem;
    font-size: 0.8125rem;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}

/* The three .task-legend-* swatches that stood here are GONE. They named palette
   TOKENS, and the /tasks calendar legend is now a FILTER built from the
   task_priority catalog and keyed on the lookup id — a token cannot key it,
   because two priorities share `danger` and three had no swatch at all. Its
   dots reuse .agenda-legend-dot + .task-priority-* (_calendar.css), so the
   legend and the grid can never disagree about a colour. */

/* ═══════════════════════════════════════════════════════════════════════════
   المرفقات — the case attachments tab (case-detail.js).
   Token-based throughout, so it follows data-theme in both directions; logical
   properties only. The grid collapses to one column on a phone.
   ═══════════════════════════════════════════════════════════════════════════ */

.attachment-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 1rem;
}

.attachment-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--card);
    min-width: 0;
}

/* The folder is the interactive card (click / Enter opens it), so it alone
   carries the affordance: a subtle fill and a hover/focus lift. */
.attachment-card-folder {
    background: var(--bg-secondary);
    cursor: pointer;
}

.attachment-card-folder:hover,
.attachment-card-folder:focus-visible {
    border-color: var(--primary);
    box-shadow: var(--shadow-sm);
}

.attachment-card-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.5rem;
}

.attachment-card-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.375rem;
    font-weight: 600;
    color: var(--text);
    min-width: 0;
    overflow-wrap: anywhere;
}

/* A folder or file name long enough to wrap three or four times pushed the
   visibility badge, notes and date down and left the head ragged. Clamped to
   two lines with an ellipsis; the full text is in the element's title, so it is
   hidden rather than lost. The 🔒 system marker is rendered OUTSIDE this span
   on purpose — inside, a long name would push it into the hidden overflow and
   the card would silently stop explaining why it has no delete button.
   min-width:0 is what lets it shrink inside the flex title row at all. */
.attachment-card-name {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    overflow-wrap: anywhere;
    min-width: 0;
}

.attachment-card-actions {
    display: flex;
    gap: 0.25rem;
    flex-shrink: 0;
}

/* How many files are inside a folder, so the count is readable without opening
   it. It carries .btn.btn-sm.btn-outline-secondary in the markup and takes its
   border, radius, padding and colours from there — it sits in the same actions
   row as the edit/delete buttons, and anything shaped differently reads as a
   stray tag rather than part of that toolbar. NOT .badge.text-bg-*: those ride
   --bs-* variables this app never sets, so they would keep their light values
   on a dark card, while .btn-outline-secondary IS remapped onto project tokens
   in _components.css.

   Only what .btn cannot give it lives here: a flex line for the icon + number
   (.btn is inline-block), and dir:ltr on the digits, a Latin run inside an RTL
   row. It is a read-only chip, but a click still bubbles to the card and opens
   the folder, so the button hover is honest and stays. */
.attachment-count-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    flex-shrink: 0;
}

.attachment-count-badge > span {
    direction: ltr;
    unicode-bidi: isolate;
}

.attachment-icon-folder { color: var(--primary); }
.attachment-icon-file   { color: var(--text-secondary); }
/* Lock marker on an auto-provisioned system folder (V40). */
.attachment-system-lock {
    color: var(--text-secondary);
    font-size: 0.72em;
    margin-inline-start: 0.15rem;
    vertical-align: baseline;
}

/* Per-session attachments panel — one file per row. */
.session-attachment-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.5rem 0.25rem;
    border-block-end: 1px solid var(--border);
}
.session-attachment-row:last-child { border-block-end: 0; }
.session-attachment-main {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
    flex-wrap: wrap;
}
.session-attachment-name { word-break: break-word; }

/* متعلق بـ — the chip under a row's title saying what the record is ABOUT (a
   case or a consultation). Rendered ONLY on linked rows, so a register of
   unlinked records looks exactly as it did before.

   A chip and not a column: the pair is null on most rows, so a column would be
   empty nearly everywhere. It was in the expandable detail row alone at first,
   which hid it so thoroughly that a linked record was indistinguishable from an
   unlinked one without opening rows at random — this is the fix for that, and
   the detail row keeps the labelled version.

   Born as .meeting-parent-chip on /meetings (V57) and RENAMED rather than
   copied when /tasks needed the same chip — it is one component, and a second
   colour/radius/hover block for an identical widget is how the two drift apart
   (same story as .row-count-btn below).

   Both colours come from --primary through color-mix, so the chip tracks BOTH
   themes with no dark-mode override — the same reason .people-* does it. */
.row-parent-chip {
    display: inline-flex;
    align-items: center;
    max-width: 100%;
    margin-top: 0.25rem;
    padding: 0.05rem 0.4rem;
    border-radius: var(--radius-pill, 24px);
    background: color-mix(in srgb, var(--primary) 10%, transparent);
    /* --primary-dark, not --primary: the chip's own 10% wash lifts the
       background toward the very colour of the text, and --primary lands at
       4.38:1 here in BOTH themes. Fourth instance of this pattern (sidebar
       active, tab-strip active, dark pagination were the others) —
       --primary-dark is the higher-contrast member of the pair in each theme,
       so one token fixes both with no [data-theme] override. */
    color: var(--primary-dark);
    text-decoration: none;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

a.row-parent-chip:hover,
button.row-parent-chip:hover {
    background: color-mix(in srgb, var(--primary) 18%, transparent);
    text-decoration: underline;
}

/* The same chip as a BUTTON — the الجلسات and الاجتماعات tabs of بوابة الموكل
   (2026-09-14), where a row's case opens INSIDE the page, in the القضايا tab's
   hub: a client has no /cases/{id}, and the portal's hash is tab-only on
   purpose, so there is no URL for the chip to hold. A button brings a border
   and centred text the chip must not show; Bootstrap's reboot already makes it
   inherit the font and gives it the pointer. */
button.row-parent-chip {
    border: 0;
    text-align: start;
}

/* The muted variant is the consultation one — no per-record page exists to link
   to, so it is deliberately not a link and must not look like one. */
.row-parent-chip.text-muted {
    background: color-mix(in srgb, var(--border) 35%, transparent);
}

/* A ROUTE link drawn as the chip — common/link-chip.js (2026-09-14, first on
   /invoices and /invoices/{id}, then every finance screen). It stands on its
   own: alone in a cell, as a panel value, inside a line of text. The top
   margin above exists to separate a chip from a title over it, and here it
   would only push the chip off its line.

   A document NUMBER (its text carries dir="ltr") is one unbroken run, and
   ends in «…» only when its box is narrower than it: text-overflow does
   nothing on the inline-flex <a>, so it goes on the span, with min-width: 0 so
   the flex item may shrink. The chip's title holds the full text either way. */
.row-parent-chip.link-chip {
    margin-top: 0;
}

.row-parent-chip.link-chip > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A chip holding a NAME wraps the way the plain name did — two lines at most,
   then «…». Left nowrap it is as wide as the whole name at every width:
   measured at 1100px it took the trial balance's account column from 136px
   of min-content to 283px, and in the chart it is the floor of a row that
   must wrap (.coa-row). Wrapping INSIDE the inline-flex chip is no better:
   its glyph keeps a column of its own beside both lines, which on /invoices
   at 1600px cut six of ten case names that the plain link had shown whole.
   So the CHIP is the two-line clamp box and its glyph flows with the text, at
   the start of the first line only.

   A number is left out on purpose: "JV-2026-000004" broken at its hyphens is
   not readable, so it keeps its one line and pays for it in width. The
   .cell-clamped selector is not redundant: the registers' own parent-chip
   rules there clamp the SPAN, and only its weight outranks them.

   vertical-align: middle, because a clipped inline box has its baseline at
   its BOTTOM edge: on the text baseline, the chip stood 6px above a badge or
   a line of text beside it (the ledger's source chip, the invoice header). */
.row-parent-chip.link-chip:not(:has(> span[dir="ltr"])),
.admin-table tbody td.cell-clamped .row-parent-chip.link-chip {
    display: -webkit-inline-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    white-space: normal;
    overflow: hidden;
    vertical-align: middle;
}

.row-parent-chip.link-chip:not(:has(> span[dir="ltr"])) > span,
.admin-table tbody td.cell-clamped .row-parent-chip.link-chip > span {
    display: inline;
    overflow: visible;
    -webkit-line-clamp: unset;
}

/* Row-action button carrying a COUNT — a notification-style pill on the icon
   when the row owns some collection, plus a tinted icon.

   Stays btn-outline-secondary (the row-action colour rule); only the marker is
   loud. That is the whole point: the actions column keeps reading as a toolbar,
   and the one thing that varies per row — "this row has N of something" — is
   the only thing that draws the eye.

   Generic on purpose. Born as .session-attach-btn on the sessions tab
   (مرفقات), now also the التوكيلات button on the clients directory. Renamed
   rather than copied: it is one component, and a second colour/offset/shadow
   block for an identical widget is how the two drift apart.

   The active tint targets `> i` rather than a specific glyph, so it follows
   whichever icon the caller uses (fa-paperclip, fa-file-signature, …). */
.row-count-btn {
    position: relative;
    overflow: visible;
}
.row-count-btn--active {
    border-color: color-mix(in srgb, var(--primary) 55%, var(--border));
    background: color-mix(in srgb, var(--primary) 8%, transparent);
}
.row-count-btn--active > i {
    color: var(--primary);
}
.row-count-badge {
    position: absolute;
    inset-block-start: -6px;
    inset-inline-end: -6px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--primary);
    /* White in light (5.27:1), dark ink in dark (5.31:1): the dark --primary
       is a light purple, and at 10px bold this is small text held to 4.5:1. */
    color: var(--on-primary);
    font-size: 0.65rem;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
    box-shadow: 0 0 0 2px var(--card);
}

/* A list of PEOPLE as overlapping initials — every table column in the app
   that lists people, built by common/avatar-stack.js (2026-09-17). A count
   that is not people (المرفقات, التوكيلات) keeps the .row-count-btn above.

   ONE <button> for the whole stack, so a circle and the «+N» are the same
   click. The 2px ring is --card because it is what separates two overlapping
   circles, and a table row's surface is the card. The FIRST circle sits on
   top, so reading from the start edge the first person is whole and each next
   one tucks under it; isolation keeps those z-indexes inside the button. */
.avatar-stack {
    display: inline-flex;
    align-items: center;
    isolation: isolate;
    padding: 2px;
    border: 0;
    /* A geometric pill around circles, not a design radius — the same literal
       .row-count-badge uses. */
    border-radius: 999px;
    background: none;
    color: inherit;
    cursor: pointer;
    vertical-align: middle;
}
.avatar-stack:hover {
    background: color-mix(in srgb, var(--primary) 10%, transparent);
}
.avatar-stack:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
.avatar-stack-item {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    inline-size: 32px;
    block-size: 32px;
    border-radius: 50%;
    /* The clip is the circle: a very wide pair («صص» measures 28px here) is
       cut at its own edge instead of painting over the circle beside it. */
    overflow: hidden;
    /* Two bold letters. MEASURED, in the app's font at this size: the median
       pair is 15px wide and nine in ten fit in 21px — which is what sets the
       circle and the overlap below. Each colour pair is held to 4.5:1 in
       _variables.css, because this is small text. */
    font-size: 0.66rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0;
    white-space: nowrap;
    box-shadow: 0 0 0 2px var(--card);
}
/* A following circle sits partly UNDER the one before it — 6px of overlap plus
   that circle's 2px ring — so its letters are centred in the 24px that shows,
   not in the whole circle: centred on all of it, the covered side cut a letter
   in half («سق» read «ں ق»), and with 8px of overlap the 20px left over was
   narrower than most pairs. The covered side is the inline-START in both
   directions, because the earlier circle is always there. */
.avatar-stack-item + .avatar-stack-item {
    margin-inline-start: -6px;
    padding-inline-start: 8px;
}
.avatar-stack-item:nth-child(1) { z-index: 3; }
.avatar-stack-item:nth-child(2) { z-index: 2; }
.avatar-stack-item:nth-child(3) { z-index: 1; }
.avatar-stack-c1 { background: var(--avatar-1-bg); color: var(--avatar-1-fg); }
.avatar-stack-c2 { background: var(--avatar-2-bg); color: var(--avatar-2-fg); }
.avatar-stack-c3 { background: var(--avatar-3-bg); color: var(--avatar-3-fg); }
.avatar-stack-c4 { background: var(--avatar-4-bg); color: var(--avatar-4-fg); }
.avatar-stack-c5 { background: var(--avatar-5-bg); color: var(--avatar-5-fg); }
.avatar-stack-c6 { background: var(--avatar-6-bg); color: var(--avatar-6-fg); }
/* «+N» — the people past the third. Neutral on purpose: it counts, it is not a
   person, so it takes the design's MUTED chip rather than one of its six people
   hues (designs/<id>.css keeps those clear of it). A SOLID circle like the
   people beside it, under the chip's own measured label. It was a --bg-secondary
   tint with a 1px inset rule, which on a dark card sits a step or two from the
   card itself: only the rule showed, and the «+N» read as a hollow outline
   beside solid circles. */
.avatar-stack-more {
    background: var(--chip-muted-bg);
    color: var(--chip-muted-fg);
}

/* ظهور للموكل — the COLUMN's cell: one soft icon chip wherever the client-
   visibility flag is a column of its own (the /cases, /consultations and
   /meetings registers, and the numbers / sessions / meetings / admin-works /
   attachments tables on /cases/{id}), built by common/visibility-chip.js
   (2026-09-13).

   It replaced a text badge («ظاهر للموكل» / «غير ظاهر للموكل») that was the
   widest thing in the column — 80–100px of ink per row pinned it at
   112–132px. The header asks the question, so the cell answers with a glyph:
   a person with a check (the client sees the row) or with a slash (hidden
   from them). A badge beside a NAME — the activity feed, the attachment
   cards — keeps its words, since no header explains an icon there.

   HIDDEN IS GREY, NEVER RED: keeping a row from the client is a normal choice,
   and a column of red marks reads as a column of errors. Every colour derives
   from theme tokens, so the chip tracks both themes with no override. */
.visibility-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    border-radius: 50%;
    font-size: 0.8rem;
    line-height: 1;
    vertical-align: middle;
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--text-secondary) 12%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-secondary) 28%, transparent);
}
.visibility-chip.is-visible {
    color: var(--success-text);
    background: rgba(var(--success-rgb), 0.16);
    box-shadow: inset 0 0 0 1px rgba(var(--success-rgb), 0.35);
}

/* The chip's header wraps onto two lines («ظهور» over «للموكل»): with the
   cell down to a 30px chip, the one-line HEADER (71px) became the widest
   thing in the column. width: 1px is a preference the auto layout widens to
   the column's min-content — the longer word — so the column settles near
   70px and leaves the surplus to the columns that can use it. */
.admin-table thead th.col-visibility {
    white-space: normal;
    width: 1px;
}

/* The hover PREVIEW of what a .row-count-btn opens — one shared element that
   js/common/row-preview.js floats beside whichever button is hovered/focused
   (fine pointers only; click still opens the full panel).

   pointer-events: none is load-bearing: the card must never capture the
   pointer, or it wedges itself between the cursor and the button and the
   mouseleave that closes it never fires.

   max-height mirrors PANEL_MAX_H in row-preview.js — change both together.
   The overflow scrollbar takes the global thin skin (_base.css). */
.row-count-preview {
    position: fixed;
    z-index: var(--z-popover);
    min-width: 180px;
    max-width: 300px;
    max-height: 260px;
    overflow-y: auto;
    padding: 0.5rem 0.75rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    pointer-events: none;
    font-size: 0.8125rem;
    color: var(--text);
}

.row-count-preview-title {
    font-size: 0.75rem;
    color: var(--text-secondary);
    margin-block-end: 0.35rem;
}

.row-count-preview-item {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    padding-block: 0.15rem;
}

.row-count-preview-item > i {
    font-size: 0.75rem;
    color: var(--text-secondary);
}

.row-count-preview-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.row-count-preview-name {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.row-count-preview-sub {
    font-size: 0.75rem;
    color: var(--text-secondary);
}


/* The ROW ACTIONS menu — the ⋮ button that collapses a register row's
   action buttons into one list (js/common/row-actions-menu.js).

   position: fixed, and that is structural rather than cosmetic: every
   register table sits in .table-responsive, which is `overflow: auto` on both
   axes, so an absolutely positioned menu is clipped by it — a row near the
   bottom would open into a strip nobody can see. The panel is placed from the
   toggle's viewport rect by DropdownPanel.place (matchWidth: false), which is
   also what keeps it inside the viewport and flips it upward when it must.

   max-height mirrors PANEL_MAX_H in row-actions-menu.js — change both
   together. The overflow scrollbar takes the global thin skin (_base.css). */
.row-actions {
    display: inline-flex;
}

/* EVERY icon button in an actions cell that carries the ⋮ shares one icon
   advance, so the group reads as one control rather than buttons of three
   widths. Font Awesome glyphs differ enough to show: measured at .btn-sm,
   ⋮ is 3.5px, fa-file-invoice 10.5, fa-chevron-down 14, fa-eye 15.8 — which
   rendered buttons of 20.8 / 27.8 / 31.3 / 33.1px side by side, and the ⋮
   read as a different, smaller control.

   1.15em = 16.1px at .btn-sm's 0.875rem: wide enough that the broadest of
   them does not overflow its box, and expressed in em so it tracks the button
   rather than drifting from Bootstrap's padding. Scoped by :has() to cells
   that actually hold a menu, so no other table is touched, and it needs no
   class on the buttons — which is what keeps it from being forgotten on the
   next register converted. */
td:has(> div > .row-actions) .btn > i {
    display: inline-block;
    width: 1.15em;
    text-align: center;
}

/* An icon button in an actions cell takes the ICON-BUTTON radius, as the
   sheets draw them — not .btn-sm's. The two resolve to the same step today,
   which is exactly why this has to be written down: they are two separate
   decisions (how round a SMALL button is, and how round an ICON button is),
   and without the rule, retuning --radius-icon-btn would move the topbar's
   .btn-navbar and the rail's brand mark and silently miss every actions cell
   it was named for.

   The first selector already covers the ⋮ — it is a .btn inside .row-actions —
   and the second names it anyway, so the rule reads as its own sentence.
   .row-count-btn needs its own: it sits in a column of its own (موجه إلى,
   المرفقات, المحامون), never in the actions cell. Specificity: the :has()
   form outranks .btn-sm on its own, and the two bare classes tie with it and
   win on source order, because _admin.css loads after _components.css. */
td:has(> div > .row-actions) .btn,
.row-actions-toggle,
.row-count-btn {
    --bs-btn-border-radius: var(--radius-icon-btn);
}

.row-actions-menu {
    position: fixed;
    z-index: var(--z-dropdown);
    min-width: 12rem;
    max-width: 18rem;
    max-height: 320px;
    overflow-y: auto;
    padding: 0.25rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
}

/* text-align: start is correct here, unlike on a field: these rows carry no
   dir of their own, so `start` resolves against the PAGE direction. */
.row-actions-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.45rem 0.65rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text);
    font-size: 0.8125rem;
    text-align: start;
    text-decoration: none;
    white-space: nowrap;
}

.row-actions-item > i {
    flex: 0 0 1rem;
    text-align: center;
    font-size: 0.8125rem;
    color: var(--text-secondary);
}

.row-actions-item:hover,
.row-actions-item:focus-visible {
    background: var(--bg-secondary);
    color: var(--text);
}

/* One destructive row, visible only while the menu is open and one click from
   committing — which is the point of commitment the actions-column rule sends
   colour to, rather than the repeating stripe it forbids.
   --danger-TEXT, because this is a LABEL: the base token measured 3.57:1 on
   the tightest light card and 4.42:1 on ليموني's dark one, so the one row in
   the menu that colour is meant to single out was the hardest row in it to
   read. It is 6.13:1 and 5.70:1 there now. */
.row-actions-item-danger,
.row-actions-item-danger > i {
    color: var(--danger-text);
}

.row-actions-item-danger:hover,
.row-actions-item-danger:focus-visible {
    background: rgba(var(--danger-rgb), 0.12);
    color: var(--danger-text);
}

.row-actions-sep {
    height: 1px;
    margin-block: 0.25rem;
    background: var(--border);
}

/* ---------------------------------------------------------------------------
 * People list — the panel a .row-count-btn OPENS.
 *
 * .row-count-btn is the button; this is what is behind it. Born for the two
 * meetings panels (الطرف and أعضاء المكتب), which list the same thing in the
 * same shape, so they share one component rather than two copies — exactly the
 * reasoning that made .row-count-btn generic in the first place.
 *
 * A bare <ul class="list-unstyled"> with an icon per line was what these
 * started as, and at three or four names it read as a paragraph rather than a
 * list of people: nothing separated one name from the next, and the icon was
 * the only thing carrying "these are individuals". Each name now sits on its
 * own recessed card with a round icon chip, which is the same
 * quiet-surface-inside-a-surface treatment the expandable detail row uses.
 *
 * Every colour derives from --primary / --border / --bg-secondary through
 * color-mix, so both themes are covered with no dark-mode override.
 * ------------------------------------------------------------------------- */
.people-list {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.people-row {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.5rem 0.65rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-secondary);
    /* Not hoverable and not clickable — these rows are read, not operated. No
       cursor and no hover tint, which would promise an action that is not
       there. */
}

/* The round icon chip. flex 0 0 auto + fixed box so a long name never squeezes
   it into an oval. */
.people-avatar {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--primary) 12%, transparent);
    color: var(--primary);
    font-size: 0.8rem;
}

.people-main {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    /* min-width: 0 is what lets overflow-wrap actually fire inside a flex
       child — without it the item refuses to shrink below its content. */
    min-width: 0;
}

.people-name {
    color: var(--text);
    font-weight: 500;
    overflow-wrap: anywhere;
}

/* --text-secondary, not --text-muted: the latter is 2.5:1 on the light card,
   below the AA floor. */
.people-sub {
    color: var(--text-secondary);
    font-size: 0.8rem;
    overflow-wrap: anywhere;
}

/* The empty state, as a row of the same list rather than a stray <p>: it keeps
   the panel the same height and shape whether or not there is anything in it. */
.people-empty {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    justify-content: center;
    padding: 1.25rem 0.65rem;
    border: 1px dashed var(--border);
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    font-size: 0.875rem;
    text-align: center;
}

/* The count beside a panel heading. Not a Bootstrap .text-bg-* class, which is
   driven by --bs-* variables this app never sets and would keep its light-theme
   colour on a dark card. */
.people-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5rem;
    padding: 0 0.4rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--primary) 14%, transparent);
    color: var(--primary);
    font-size: 0.8rem;
    font-weight: 700;
    line-height: 1.5rem;
}

/* Read-only info block in the session-attachment info/edit modal — a label /
   value grid. Logical alignment so it mirrors under RTL. */
.attachment-info-grid {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.4rem 1rem;
    margin: 0 0 0.5rem;
    padding-block-end: 1rem;
    border-block-end: 1px solid var(--border);
}
.attachment-info-grid dt {
    color: var(--text-secondary);
    font-weight: 600;
}
.attachment-info-grid dd {
    margin: 0;
    word-break: break-word;
}

.attachment-card-notes {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--text-secondary);
    overflow-wrap: anywhere;
}

.attachment-notes-label {
    font-weight: 600;
    color: var(--text);
}

.attachment-card-foot {
    margin-top: auto;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border);
    font-size: 0.75rem;
    color: var(--text-secondary);
}

/* A folder row in the list view opens on click, like its card. */
.attachment-row { cursor: pointer; }
.attachment-row:hover { background: var(--bg-secondary); }

@media (max-width: 30rem) {
    .attachment-grid { grid-template-columns: 1fr; }
}

/* ─── الجلسات tab ─────────────────────────────────────────────────────────
   The table reuses .admin-table; only the result badge and the adjournment
   sub-form need their own rules. Project tokens only, checked in both themes. */
.session-result-badge {
    font-weight: 600;
}

/* The "create next session" sub-form inside the نتيجة modal. Its modern framed
   "callout" background now comes from the shared .form-callout class (_forms.css)
   — this rule only carries the heading tweak specific to the sub-form. */
.session-next-group .section-title {
    margin-block-start: 0;
    margin-block-end: 0.85rem;
    color: var(--primary);
    font-weight: 600;
}

/* ─── الخط الزمني tab ─────────────────────────────────────────────────────
   The case's whole history as one date-ordered feed: a filter chip row, a
   count line, month separators and a card per event.

   Everything here is FLEX with logical properties and no absolute positioning,
   so the whole feed mirrors under RTL for free — the .stat-icon lesson. The
   one physical property that would break it, `border-left` for the connecting
   rail, is deliberately `border-inline-start` below.

   Colours come only from project tokens that are defined in BOTH themes, so
   there is no dark-mode override anywhere in this block. No --bs-* variable is
   used: this app switches on data-theme and Bootstrap switches on
   data-bs-theme, which nothing here ever sets. */

.timeline-chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-block-end: 1rem;
}

.timeline-chip {
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text-secondary);
    border-radius: var(--radius-pill);
    padding: 0.35rem 0.85rem;
    font-size: 0.85rem;
    line-height: 1.4;
    transition: background var(--transition-fast), color var(--transition-fast),
                border-color var(--transition-fast);
}

.timeline-chip:hover {
    border-color: var(--primary);
    color: var(--primary);
}

/* Active state on the project primary, NOT a .btn-primary / --bs-* class —
   those do not follow data-theme. */
.timeline-chip.active {
    background: color-mix(in srgb, var(--primary) 12%, transparent);
    border-color: var(--primary);
    color: var(--primary);
    font-weight: 600;
}

.timeline-count {
    color: var(--text-secondary);
    font-size: 0.875rem;
    margin-block-end: 0.75rem;
}

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

/* The centred month separator. `::before`/`::after` rules on a flex parent draw
   the two rules either side without any width calculation, and flex order is
   direction-aware, so this needs no RTL variant. */
.timeline-month {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-block: 1.25rem 0.75rem;
    color: var(--text-secondary);
    font-size: 0.8rem;
    font-weight: 600;
}

.timeline-month::before,
.timeline-month::after {
    content: "";
    flex: 1;
    block-size: 1px;
    background: var(--border);
}

.timeline-item {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.75rem 0.85rem;
    margin-block-end: 0.5rem;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    /* The connecting rail. border-inline-start, NEVER border-left — a physical
       property would put the accent on the wrong edge in one of the two
       directions, and the page defaults to RTL. */
    border-inline-start: 3px solid var(--border);
    border-radius: var(--radius-md);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.timeline-item:hover {
    box-shadow: var(--shadow-sm);
}

.timeline-dot {
    /* The per-type colour arrives as a custom property (see the eight rules
       below) and this ONE rule spends it, so a new event type is a single
       declaration rather than a copied block. --dot also gives every unmapped
       type a working neutral default rather than an unstyled dot. */
    --dot: var(--text-secondary);
    inline-size: 2rem;
    block-size: 2rem;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-circle);
    font-size: 0.8rem;
    background: color-mix(in srgb, var(--dot) 15%, transparent);
    color: var(--dot);
}

/* Eight types, eight tints — every one a project token, so all eight follow
   data-theme with no override. The two mixed hues exist because there are only
   six semantic tokens and eight branches; mixing two tokens keeps them
   theme-aware, which a literal hex would not be. */
.timeline-dot-case       { --dot: var(--primary); }
.timeline-dot-session    { --dot: var(--accent); }
.timeline-dot-activity   { --dot: var(--info); }
.timeline-dot-number     { --dot: color-mix(in srgb, var(--info), var(--success)); }
.timeline-dot-task       { --dot: var(--warning); }
.timeline-dot-meeting    { --dot: var(--success); }
.timeline-dot-admin_work { --dot: color-mix(in srgb, var(--warning), var(--danger)); }
.timeline-dot-attachment { --dot: var(--text-secondary); }

.timeline-item-body {
    flex: 1;
    min-inline-size: 0;      /* lets the two lines below actually truncate */
}

.timeline-item-title {
    font-weight: 600;
    color: var(--text);
    word-break: break-word;
}

.timeline-item-subtitle {
    /* --text-secondary, not --text-muted: muted is 2.5:1 on the light card,
       below the 4.5:1 AA floor. See the contrast note in CLAUDE.md. */
    color: var(--text-secondary);
    font-size: 0.85rem;
    word-break: break-word;
}

.timeline-item-date {
    flex-shrink: 0;
    color: var(--text-secondary);
    font-size: 0.8rem;
    white-space: nowrap;
}

/* ===========================================
   ROLES ADMIN — role list rail (/admin/roles)
   =========================================== */

/* A <button> per role: full-width, start-aligned, themed off project tokens
   (never Bootstrap list-group — its colors ride --bs-* vars that ignore
   data-theme). .active mirrors the lookups source-rail selection look. */
.role-list-item {
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.55rem 0.75rem;
    margin-bottom: 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-secondary);
    color: var(--text);
    text-align: start;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease;
}

.role-list-item:hover {
    background: color-mix(in srgb, var(--primary) 6%, var(--bg-secondary));
}

.role-list-item.active {
    border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 10%, var(--bg-secondary));
}

.role-list-item .role-list-name {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    min-width: 0;
    font-weight: 500;
}

/* ===========================================================================
   CLIENT PORTAL — /client-portal (بوابة الموكل)
   ===========================================================================
   The page reuses existing components everywhere it can: .admin-page-header,
   .admin-section-card, .modal-form-tabs (for BOTH the outer strip and the hub's
   sub-strip), .admin-table, .admin-stat-card. Only two things are new — the
   case-selector card, and a scoped colon rule for the hub's field grid.
   =========================================================================== */

/*
 * The case SELECTOR card in the القضايا pane.
 *
 * A card grid rather than a table because it is a NAVIGATION control: a client
 * has no /cases/{id} page to be sent to, so the case opens in the hub below
 * this grid and these are the things that open it. A row list would read as a
 * register whose rows do nothing.
 *
 * A real <button>, so it is keyboard-operable and focusable for free.
 * Tokens only and logical properties only, so it mirrors under RTL and tracks
 * both themes with no dark-mode override — every colour derives from
 * --primary / --border / --card via color-mix.
 */
.cp-case-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: 100%;
    height: 100%;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    background: var(--card);
    color: var(--text);
    text-align: start;
    cursor: pointer;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}

.cp-case-card:hover {
    border-color: color-mix(in srgb, var(--primary) 40%, var(--border));
    box-shadow: var(--shadow-md);
}

/*
 * :focus-visible, not :focus — a mouse click must not leave a ring behind on
 * the card the user just opened, but keyboard traversal must show one.
 */
.cp-case-card:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.cp-case-card.selected {
    border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 8%, var(--card));
}

.cp-case-card-number {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--text-secondary);
}

/*
 * The subject is free text of unbounded length. Clamped to two lines so a long
 * one cannot make its card twice the height of its neighbours and break the
 * grid's rhythm; the full text is on the button's title attribute.
 */
.cp-case-card-subject {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-weight: 500;
    line-height: 1.4;
    min-height: 2.8em;
}

.cp-case-card-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-block-start: auto;
}

.cp-case-card-court {
    font-size: 0.8125rem;
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/*
 * The colon after every field label in the hub's بيانات القضية grid.
 *
 * SCOPED to #cpSubInfo, never a bare .case-detail-label::after — that class is
 * worn by every expandable detail row plus tab المعلومات الأساسية on
 * /cases/{id} and سجل الموكل, each of which punctuates itself through a rule
 * of its own. The #tabInfo rule above is the precedent this copies, including the
 * reason the colon cannot live in the message string: these labels are
 * clientPortal.info.* keys, and ::after puts the colon at the label's logical
 * end so it flips with the locale on its own.
 */
#cpSubInfo .case-detail-label::after {
    content: ':';
}

/*
 * The hub's field grid sits inside an .admin-section-card, so it takes the same
 * padding reset the case detail page's does — a second padded surface inside a
 * padded card reads as a double indent. Scoped rather than relying on the
 * `.admin-section-card > .case-detail-body` rule above, because here the grid is
 * nested one level deeper (card > .cp-sub-pane > .case-detail-body) and the
 * child combinator in that rule deliberately does not reach it.
 */
#cpSubInfo > .case-detail-body {
    padding: 0;
}

/*
 * أرقام القضية — the hub's SECOND sub-tab, and client-portal only. /cases/{id}
 * keeps its editable table there: it carries per-row edit and delete buttons,
 * and an actions toolbar is exactly what a card with no actions must not grow.
 *
 * Translated from the Lovable prototype (Case Management Hub,
 * client-portal.tsx, sub-tab "numbers"): one recessed rounded panel per number,
 * its label above a monospace value. Tokens only — the prototype's literal
 * #111520 is --bg-secondary here, the SAME recessed surface .case-detail-body
 * uses one sub-tab over, so بيانات القضية and أرقام القضية read as siblings
 * rather than as two unrelated panels that happen to share a tab strip.
 *
 * Every value is neutral. The prototype tints one purple, but that tint is on
 * its single HARDCODED demo string, and case_numbers carries no field that
 * separates one row from another — `name` is free text — so a per-row colour
 * could only come from a hardcoded map over typed Arabic, which is the thing
 * the "never hardcode a per-value colour map in JS" rule forbids. Colour on
 * this page stays reserved for state.
 */
.cp-number-grid {
    display: grid;
    gap: 0.75rem;
    /* minmax(0, 1fr), never a bare 1fr: a case number is an unbroken Latin run,
       and a track that may not shrink below its own min-content gets sized to
       the whole string. The .portal-card-meta min-width:0 lesson, one layer up
       — there a nowrap chip pushed 60px through the card's end edge. */
    grid-template-columns: minmax(0, 1fr);
}

/* 1 -> 2 -> 3 across. The prototype jumps straight to three at sm, which puts
   three ~180px cards on a phone held sideways; the extra step keeps a long
   reference number on one line at every width, and three is still the shape
   the design asks for on a desktop. */
@media (min-width: 576px) {
    .cp-number-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 992px) {
    .cp-number-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.cp-number-card {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    padding: 0.75rem 0.9rem;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

/* No letter-spacing and no text-transform — the .case-detail-label note:
   Arabic has no letter case, so uppercase is a no-op, and tracking pulls apart
   the cursive joins that make an Arabic word legible. */
.cp-number-label {
    font-size: 0.75rem;
    color: var(--text-secondary);
    /* Free text from the same form as the value below, so it gets the same
       guard: a name with no space in it is rare, but it escapes the card
       entirely without this — measured at 382px of text in a 256px box. */
    overflow-wrap: anywhere;
}

/*
 * The colon lives in CSS, as it does for every other label in this app:
 * ::after lands at the label's LOGICAL end, so it renders to the left of the
 * Arabic and flips to the right in the English locale with no direction rule.
 *
 * The usual second reason does not apply here — this label is row DATA
 * (case_numbers.name), not a message key shared with a form — so what remains
 * is only a tenant who types their own trailing colon into the name.
 */
.cp-number-label::after {
    content: ':';
}

.cp-number-value {
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 0.95rem;
    color: var(--text);
    /* A reference number offers no break opportunity, so without this a long
       one runs out through the card's edge instead of wrapping inside it. */
    overflow-wrap: anywhere;
}

/* السنة / التفاصيل. Rendered only when one of them is set — see numberCard(). */
.cp-number-meta {
    font-size: 0.75rem;
    color: var(--text-secondary);
    overflow-wrap: anywhere;
}

/*
 * Case selector, LIST view (/client-portal → القضايا). The whole <tr> carries
 * data-action, so it takes the pointer.
 *
 * The .selected rule is written as `.admin-table tbody tr.cp-case-row.selected`
 * (0,3,2) ON PURPOSE: `.admin-table tbody tr:hover` is (0,2,2), so the short
 * form (0,2,0) would LOSE to hover and the selected row would fade to the 3%
 * hover tint exactly when the pointer was on it — the selection appearing to
 * come undone under the cursor. Same 8% primary as .cp-case-card.selected, so
 * the two views read as one component.
 */
.cp-case-row {
    cursor: pointer;
}

.admin-table tbody tr.cp-case-row.selected {
    background: color-mix(in srgb, var(--primary) 8%, transparent);
}

/*
 * Where the hub LANDS when a case is picked (client-portal.js scrollToHub).
 *
 * The navbar is .sticky-top, so scrollIntoView({block:'start'}) would put the
 * hub's heading directly underneath it. scroll-margin is the declarative fix —
 * the browser applies it during the scroll, so the JS stays a plain
 * scrollIntoView with no measuring and no offset arithmetic.
 *
 * env() is in the sum because the bar grows by the safe-area inset on a
 * notched device (see the iOS PWA rule in _layout.css); --topbar-height alone
 * would come up short there by exactly the notch.
 */
#cpCaseHub {
    scroll-margin-block-start: calc(var(--topbar-height) + env(safe-area-inset-top, 0px) + 1rem);
}

/* ===========================================================================
   PORTAL RECORD CARDS — the shared card reading mode for BOTH portals
   ===========================================================================
   /lawyer-portal and /client-portal render the same registers, and half of
   those registers are narrative: a meeting IS its وصف, a consultation IS its
   ملخص, a hearing IS its نتيجة, a وكالة IS its نطاق. A table cell can only
   truncate a sentence, so every tab now offers a CARD view beside its table
   and the narrative field gets read instead of clipped.

   ONE block for both pages, prefixed .portal-* rather than lp-/cp-: the card
   is the same component on both, and two copies would drift the moment one
   page gained a tweak. The pages differ only in which fields they pass.

   Everything derives from --primary / --border / --card / --text-* through
   color-mix, so BOTH themes track with no dark-mode override anywhere in this
   block, and no --bs-* variable is touched (this app switches on data-theme,
   Bootstrap switches on data-bs-theme, which nothing here ever sets).

   Logical properties throughout — the start-edge rail must mirror under RTL,
   and no direction rule may set the same property (the .show lesson).
   =========================================================================== */

/* minmax(0, 1fr), never a bare 1fr: 1fr's minimum is the content's, so a card
   holding one unbreakable run — a case chip, which never wraps — widened the
   one column past a 430px screen and scrolled the whole page 352px sideways
   (measured on بوابة الموكل, 2026-09-14). The two-column rule below always had
   the zero minimum. */
.portal-card-grid {
    display: grid;
    gap: 0.75rem;
    grid-template-columns: minmax(0, 1fr);
}

/* Two across from md up. Deliberately never three: these cards carry a
   paragraph, and a third column drops the callout under ~30 characters a line,
   which is the shape the table already failed at. */
@media (min-width: 768px) {
    .portal-card-grid.portal-card-grid-2 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.portal-card {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
    padding: 0.9rem 1rem;
    background: var(--card);
    border: 1px solid var(--border);
    /* The rail default. A value whose lookup source carries no 'color' keeps
       this neutral edge rather than being tinted a meaning it does not have —
       see RAIL_FALLBACK in lookups.js. */
    border-inline-start: 3px solid var(--border-strong);
    border-radius: var(--radius-lg);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.portal-card:hover {
    box-shadow: var(--shadow-md);
}

/* Rail tones — driven by Lookups.railClass(), i.e. by the SAME lookup_metadata
   'color' token the badge reads. Only the inline-start edge changes; the other
   three keep --border so the card does not read as an alert. */
.portal-rail-info    { border-inline-start-color: var(--info); }
.portal-rail-success { border-inline-start-color: var(--success); }
.portal-rail-primary { border-inline-start-color: var(--primary); }
.portal-rail-warning { border-inline-start-color: var(--warning); }
.portal-rail-danger  { border-inline-start-color: var(--danger); }
.portal-rail-muted   { border-inline-start-color: var(--text-muted); }

/* A finished record reads quieter, but never below AA — 0.75 opacity on
   --text (14.68:1 light / 12.6:1 dark) still clears it comfortably, which a
   lighter token would not. */
.portal-card.is-done { opacity: 0.75; }

.portal-card-top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.6rem;
}

.portal-card-title {
    font-weight: 600;
    line-height: 1.45;
    color: var(--text);
    /* Free text of unbounded length. Two lines keeps a long subject from making
       its card twice its neighbour height and breaking the grid rhythm; the
       full string stays in the title attribute. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.portal-card-title.is-struck {
    text-decoration: line-through;
    color: var(--text-muted);
}

/* The badges corner. Wraps rather than shrinking the title, and never wraps a
   single badge own text. */
.portal-card-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    justify-content: flex-end;
    flex-shrink: 0;
}

.portal-card-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem 0.85rem;
    font-size: 0.8125rem;
    color: var(--text-secondary);
    /* Pairs with the min-width:0 below: once an item may narrow, a value with
       no break opportunity in it would overflow instead of stretching the row. */
    overflow-wrap: anywhere;
}

.portal-card-meta .portal-meta-strong {
    color: var(--text);
    font-weight: 600;
}

/* Overdue. --danger is a STATE derived from a date here, not a lookup colour,
   so it is a literal token rather than a railClass() token — the rule against
   hardcoded per-value maps is about lookup VALUES, and lateness is arithmetic. */
.portal-card-meta .portal-meta-late {
    color: var(--danger-text);
    font-weight: 600;
}

.portal-card-meta i {
    margin-inline-end: 0.25rem;
    opacity: 0.8;
}

/* A meta item must be allowed to SHRINK.
 *
 * A flex item defaults to min-width:auto, i.e. it never narrows below its own
 * min-content — and .row-parent-chip is white-space:nowrap, so ITS min-content
 * is the entire case name. On the sessions tab that sized the <span> around the
 * chip to the full string and pushed it 60px out through the card's end edge,
 * over the neighbouring column. The chip's own max-width:100% never bit,
 * because the span it resolves against had already been sized to the string.
 * min-width:0 lets the item narrow, which is what finally hands the chip the
 * ellipsis it was always styled for. */
.portal-card-meta > * {
    min-width: 0;
}

/* No max-width override for .row-parent-chip here, deliberately. The card gives
   it a meta row of its own (caseChipRow, in both portals) — shared at most with
   the lawyer portal's marker chip, which wraps below a name that needs the
   whole line — so the base
   rule's max-width:100% is exactly right — and with min-width:0 above it now
   BITES, which is the whole fix. A narrower cap was tried and only wasted the
   line; a flat `max-width: 20rem` was worse still, since this selector outranks
   the base one (0,2,0 vs 0,1,0) and so REPLACES the 100% rather than tightening
   it — the chip escaped by 59px again on any card under 320px wide. */

/* The case chip on a portal CARD, in both portals. Normally the chip is the
   second line of a stacked cell, where its top margin separates it from the
   title above; on a card it sits alone in a meta row the card already spaces,
   and the offset would only push it off that row's centre. It was .lp-case-chip
   until بوابة الموكل drew the same chip (2026-09-14) — declared ABOVE
   .row-parent-chip at the same weight, so that margin won and the rule never
   applied. Here, after it, it does.

   Its text ends in «…» when the card is narrower than the name: the chip is
   inline-flex, so its own text-overflow never reaches the span inside it —
   the .link-chip > span rule's reason. Before this a long name was cut at the
   card's edge mid-letter. */
.portal-case-chip {
    margin-top: 0;
}

.portal-case-chip > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*
 * The narrative block — .form-callout does the gradient, the start-edge accent
 * and the shadow, and this class adds ONLY what a card needs on top of it:
 * tighter padding (the form value is tuned for a modal) and the label type.
 *
 * Never redeclare the gradient/border/shadow here. _forms.css owns that look
 * and a second copy is exactly what the "add .form-callout to the element"
 * rule exists to prevent.
 */
.portal-card .form-callout.portal-callout {
    padding: 0.6rem 0.75rem;
    font-size: 0.85rem;
    line-height: 1.6;
    color: var(--text);
    /* Long free text with no spaces (a pasted reference, a URL) must not push
       the card wider than its grid column. */
    overflow-wrap: anywhere;
    /* A ملخص / رأي قانوني is typed with paragraph breaks, and this is the whole
       reason the field moved out of a table cell — collapsing them here would
       hand back half of what the card view is for. pre-line, not pre: it keeps
       the newlines the author typed while still wrapping long lines to the card
       width. Safe because the text is escaped before it is inserted, so this
       changes rendering only, never parsing. */
    white-space: pre-line;
}

.portal-callout-label {
    display: block;
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--primary);
    margin-block-end: 0.15rem;
}

/* Actions sit at the card start edge, after everything else. margin-block-start
   auto pins them to the bottom so two cards in one grid row line their buttons
   up even when one carries a callout and the other does not. */
.portal-card-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
    margin-block-start: auto;
    padding-block-start: 0.15rem;
}

/* ---------------------------------------------------------------------------
   Progress — نسبة الإنجاز on a task card.

   A <progress> element would be the semantic choice and is deliberately not
   used: its bar and value are painted by UA-specific pseudo-elements
   (::-webkit-progress-value / ::-moz-progress-bar) that cannot take a CSS
   custom property for the fill in every engine, so the themed --primary would
   silently fall back to the platform accent in at least one browser. The div
   pair below carries the same information with role/aria attributes.
   --------------------------------------------------------------------------- */
.portal-progress {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.75rem;
    color: var(--text-secondary);
}

.portal-progress-track {
    flex: 1;
    height: 5px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--text-secondary) 20%, transparent);
    overflow: hidden;
}

.portal-progress-fill {
    display: block;
    height: 100%;
    border-radius: var(--radius-pill);
    background: var(--primary);
}

.portal-progress-value {
    color: var(--text);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/* ---------------------------------------------------------------------------
   File row — المرفقات on /client-portal.
   --------------------------------------------------------------------------- */
.portal-file {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding: 0.6rem 0.8rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

.portal-file-icon {
    width: 36px;
    height: 36px;
    border-radius: var(--radius-md);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: 1rem;
    background: color-mix(in srgb, var(--primary) 10%, transparent);
    color: var(--primary);
}

/* min-width:0 is load-bearing: without it a flex item refuses to shrink below
   its content and a long file name pushes the download button off the row. */
.portal-file-body {
    flex: 1;
    min-width: 0;
}

.portal-file-name {
    display: block;
    font-weight: 600;
    font-size: 0.9rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.portal-file-meta {
    display: block;
    font-size: 0.775rem;
    color: var(--text-muted);
}

/* ---------------------------------------------------------------------------
   The hub header band — /client-portal, the selected case.

   A client first question is "when is my next hearing", and before this the
   answer lived one sub-tab away. The band answers it beside the case own
   identity, and costs no request: it reads the earliest future row out of the
   الجلسات sub-tab cache the hub already fetches.
   --------------------------------------------------------------------------- */
.portal-band {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.1rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
}

.portal-band-main { flex: 1 1 18rem; min-width: 0; }

.portal-band-title {
    font-size: 1.05rem;
    font-weight: 700;
    line-height: 1.4;
    margin-block-end: 0.35rem;
    color: var(--text);
}

.portal-band-facts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.9rem;
    font-size: 0.8125rem;
    color: var(--text-secondary);
}

.portal-band-facts b {
    color: var(--text);
    font-weight: 600;
}

/* The one highlighted fact. Same accent grammar as .form-callout (start-edge
   bar + primary wash) without pulling that class in — this is a standalone
   panel rather than a grouped sub-section, and it needs no gradient. */
.portal-next {
    flex: 0 1 auto;
    min-width: 11rem;
    padding: 0.6rem 0.85rem;
    border: 1px solid color-mix(in srgb, var(--primary) 25%, var(--border));
    border-inline-start: 3px solid var(--primary);
    border-radius: var(--radius-lg);
    background: color-mix(in srgb, var(--primary) 6%, transparent);
}

.portal-next-label {
    display: block;
    font-size: 0.7rem;
    font-weight: 700;
    color: var(--primary);
    margin-block-end: 0.1rem;
}

.portal-next-date {
    display: block;
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--text);
}

.portal-next-sub {
    display: block;
    font-size: 0.775rem;
    color: var(--text-secondary);
}

/* ---------------------------------------------------------------------------
   The pane head — search on one side, the table/cards toggle on the other.

   .view-toggle itself is the shared /employees control (_components.css); this
   only lays the row out, so the two portals cannot drift apart on spacing.
   --------------------------------------------------------------------------- */
.portal-pane-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-block-end: 1rem;
}

.portal-pane-search {
    flex-grow: 1;
    max-width: 24rem;
}

/* ---------------------------------------------------------------------------
   Statistics tiles as the tab switcher (/lawyer-portal).

   The six tiles were already one-to-one with the six tabs and did nothing when
   clicked. They are real <button>s now, so :focus-visible and keyboard
   operation come free; this only removes the UA button chrome and adds the
   affordance. .admin-stat-card keeps ownership of the surface itself.
   --------------------------------------------------------------------------- */
.admin-stat-card.stat-card-button {
    width: 100%;
    text-align: start;
    font: inherit;
    color: inherit;
    cursor: pointer;
    /* .admin-stat-card animates background/border only; adding box-shadow here
       rather than editing that rule keeps the plain tiles on other pages
       untouched. */
    transition: background-color var(--transition-normal),
                border-color var(--transition-normal),
                box-shadow var(--transition-normal);
}

.admin-stat-card.stat-card-button:hover {
    border-color: color-mix(in srgb, var(--primary) 40%, var(--border));
    box-shadow: var(--shadow-md);
}

/* :focus-visible, not :focus — a mouse click must not leave a ring on the tile
   the user just pressed, but keyboard traversal must show one. The
   .cp-case-card precedent. */
.admin-stat-card.stat-card-button:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* The tile whose tab is open. Same 8% primary wash as .cp-case-card.selected,
   so "this one is current" reads identically across the two portals. */
.admin-stat-card.stat-card-button.is-active {
    border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 8%, var(--card));
}

/* ============================================================================
   ACCOUNTING (batch A, 2026-09) — chart tree, journal lines, trial balance
   ============================================================================ */

/* ── شجرة الحسابات — /accounting/accounts ──────────────────────────────────
   Nested <ul>s; every level indents on the INLINE start so the tree mirrors
   under RTL. The row is a flex line: toggle · code · name · badges · actions. */
.coa-tree,
.coa-tree ul {
    list-style: none;
    margin: 0;
    padding: 0;
}
.coa-tree .coa-children {
    padding-inline-start: 1.75rem;
    border-inline-start: 1px dashed var(--border);
    margin-inline-start: 0.9rem;
}
.coa-row {
    display: flex;
    /* Wrapping is the safety net at EVERY width, not only on a phone: the row
       carries five items and each level of depth takes 2.65rem off the width
       they share, so a deep enough tree overruns the row at any viewport. It
       must never overflow — .coa-children is clipped while the open/close
       tween runs, so an overflowing row would lose its action buttons rather
       than merely scrolling. The phone block below makes the wrap deterministic. */
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    padding: 0.45rem 0.5rem;
    border-radius: var(--radius-sm);
    min-height: 44px;
}
.coa-row:hover {
    background: var(--bg-secondary);
}
.coa-toggle {
    flex: 0 0 1.75rem;
    width: 1.75rem;
    height: 1.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--card);
    color: var(--text-secondary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    cursor: pointer;
}
.coa-toggle:hover {
    color: var(--primary);
    border-color: var(--primary);
}
.coa-toggle-spacer {
    border-color: transparent;
    background: transparent;
    cursor: default;
}
.coa-code {
    font-variant-numeric: tabular-nums;
    color: var(--text-secondary);
    min-width: 3.5rem;
}
.coa-group > .coa-row .coa-code,
.coa-group > .coa-row .coa-name {
    font-weight: 600;
}
.coa-name {
    color: var(--text);
    text-decoration: none;
}
/* A LEAF's name links to its ledger, and a route link is the chip
   (common/link-chip.js), drawn INSIDE this box so the row keeps its item. It
   wraps like the plain name did, by the name-chip rule beside .link-chip. */
.coa-meta {
    display: inline-flex;
    gap: 0.25rem;
    flex-wrap: wrap;
}
.coa-actions {
    margin-inline-start: auto;
}
.coa-inactive > .coa-row .coa-name,
.coa-inactive > .coa-row .coa-name .row-parent-chip,
.coa-inactive > .coa-row .coa-code {
    color: var(--text-muted);
    text-decoration: line-through;
}

/* Phone — the tree runs out of room long before the row does. At 390px the
   card leaves 261px of row at level 1 and 175px at level 3, for five items
   that want 337px; the type badge alone is 71px. Two levers, in this order:
   the indent gives way first (it is decoration, and the dashed guide still
   marks the level), then the row breaks into exactly two lines — identity
   (toggle · code · name) above, badges and actions below.

   The break is FORCED with a zero-height flex item of full basis rather than
   left to the content, because the natural wrap point moves with the name's
   length: a row whose buttons sit under its name and a row whose buttons sit
   beside its badges read as two different components in one list. Ordering
   the pseudo-element between the two halves is what fixes it in place. */
@media (max-width: 767.98px) {
    .coa-tree .coa-children {
        padding-inline-start: 0.85rem;
        margin-inline-start: 0.45rem;
    }
    .coa-row {
        row-gap: 0.3rem;
        column-gap: 0.5rem;
    }
    /* A flex item's min-width is auto, so without this the longest word in an
       account name is the row's floor and the whole name jumps to a line of its
       own. break-word, never anywhere: it adds no break opportunity to the
       min-content size, so an ordinary name still wraps between words and only
       a single word too long for the row is broken at all. */
    .coa-row .coa-name {
        min-width: 0;
        overflow-wrap: break-word;
    }
    /* 3.5rem aligns the codes into a column, which is worth 22px of a 1440px
       row and not of a 148px one — at 320px it is the difference between the
       code sharing a line with its name and sitting alone above it. */
    .coa-row .coa-code {
        min-width: 2.75rem;
    }
    .coa-row::after {
        content: '';
        flex: 0 0 100%;
        height: 0;
        order: 1;
    }
    /* flex-basis 0 keeps the badges and the buttons on ONE line: line breaking
       measures the basis, not the shrunk size, so a meta box sized by its own
       content pushes the buttons onto a third line of their own. */
    .coa-row .coa-meta {
        order: 2;
        flex: 1 1 0;
    }
    .coa-row .coa-actions {
        order: 3;
    }
}

/* ── Manual journal entry lines — /accounting/journal/new ───────────────── */
.journal-lines-head,
.journal-line-fields {
    display: grid;
    grid-template-columns: 2.2fr 2fr 1.4fr 1fr 1fr;
    gap: 0.5rem;
    align-items: center;
}
/* The head gets the remove button's track as a sixth column and the inset
   every head row shares (the .invoice-lines-head block further down) — it
   used to carry a PHYSICAL `padding-right: 3rem` for that button, which sits
   on the LEFT under RTL, so every title stood 21px off its column. */
.journal-lines-head {
    grid-template-columns: 2.2fr 2fr 1.4fr 1fr 1fr calc(2.8rem + 2px + 0.25rem);
}
/* The repeat-row grid is generic; the line grid above replaces it here. */
.journal-line .repeat-row-fields.journal-line-fields {
    display: grid;
}
/* No label above these fields — the head carries the titles — so the label
   offset .repeat-row-remove reserves (_forms.css) would park the button a
   full line below the inputs it removes. */
.journal-line > .repeat-row-remove {
    margin-block-start: 0;
}
@media (max-width: 767.98px) {
    .journal-lines-head { display: none; }
    .journal-line-fields { grid-template-columns: 1fr 1fr; }
    .journal-col-account,
    .journal-col-description { grid-column: 1 / -1; }
}
.journal-totals {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    justify-content: flex-end;
    font-variant-numeric: tabular-nums;
}
.journal-lines-table td,
.journal-lines-table th {
    font-size: 0.9rem;
}

/* The bare detail grid on /accounting/journal/{id} punctuates itself — the
   same scoping rule as #tabInfo / #clientDetailPage. */
#journalDetailPage .case-detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 1rem 1.5rem;
}
#journalDetailPage .case-detail-item {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}
#journalDetailPage .case-detail-label {
    font-size: 0.8rem;
    color: var(--text-muted);
}
#journalDetailPage .case-detail-label::after {
    content: ':';
}

/* ── Trial balance — /accounting/trial-balance ─────────────────────────── */
/* The tree-shaped account tables. Two of them wear this vocabulary — ميزان
   المراجعة (six amount columns, from column 3) and قائمة الدخل on
   /reports/net-profit (one, column 3) — so the numeric rule reads "column 3
   onwards" and covers both without a per-table variant. The tb- row classes
   stay as they are: they name a tree row, not the trial balance. */
.trial-balance-table td:nth-child(n+3),
.trial-balance-table th:nth-child(n+3),
.net-profit-table td:nth-child(n+3),
.net-profit-table th:nth-child(n+3) {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.trial-balance-table .tb-group-row td,
.net-profit-table .tb-group-row td {
    font-weight: 600;
    background: var(--bg-secondary);
}
.trial-balance-table .tb-level-1 td,
.net-profit-table .tb-level-1 td {
    font-weight: 700;
}
.trial-balance-table .tb-name,
.net-profit-table .tb-name {
    padding-inline-start: calc(0.75rem + var(--tb-indent, 0) * 1.1rem);
}

/* The office logo preview on /admin/settings. Bounded like the printed one
   (print-finance.css .seller-logo) so an admin sees the sheet's proportions
   rather than a 600px banner across the card, and on a light plate because a
   transparent PNG cut for white paper vanishes on the dark theme. */
.seller-logo-preview {
    max-height: 64px;
    max-width: 200px;
    object-fit: contain;
    background: #fff;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm, 6px);
    padding: 0.35rem 0.5rem;
}

/* Print: the reports print from their own page — hide the chrome. */
@media print {
    .app-sidebar, .navbar, .main-footer, .d-print-none { display: none !important; }
    .main-content { margin: 0 !important; width: 100% !important; }
    .admin-section-card { box-shadow: none; border: 0; }
}

/* Mirrors an icon that points "back": right under RTL, left under LTR. */
html[dir="ltr"] .rtl-flip { transform: scaleX(-1); }


/* ── Invoices — الفواتير (finance plan, batch B) ─────────────────────────
   The line grid of the invoice form / credit-note modal, the instalment
   grid, the totals strip and the small summary of the receipt modal. All
   logical properties.

   The LINE widget switches layout on its CONTAINER, not on the viewport,
   and .invoice-lines-wrap exists for exactly that. Its two hosts are given
   very different room at the SAME viewport — the full-width invoice form
   hands it ~1289px while the credit-note modal hands it ~797px — so a
   `@media (min-width: 768px)` rule cannot tell them apart: the modal was
   handed the seven-column grid it has no space for at EVERY viewport,
   which is what put a horizontal scrollbar inside the dialog and clipped
   البيان. So the stacked labelled layout is now the DEFAULT and the grid
   is the enhancement — a container too narrow for seven columns simply
   never gets them, in the modal and on the page alike, which is also why
   the two heads no longer carry `d-none d-md-grid`.

   60rem is measured, not guessed: the seven columns floor at 824px
   (8+10+5+7+6.5+5+7 = 48.5rem, plus six .5rem gaps), .repeat-row adds its
   padding, gap and remove button (~79px) and .form-callout another 40px —
   943px — so the grid is claimed just above that.

   The INSTALMENT grid keeps its viewport media query below: it has one
   host, a full-width page, so it has no second container to tell apart. */
.invoice-lines-wrap {
    container-type: inline-size;
    container-name: invoice-lines;
}
/* Stacked by default. The header row is hidden here rather than in the
   markup, because in this layout its column titles are on the fields
   themselves (.invoice-line-label). */
.invoice-lines-head { display: none; }
.invoice-line-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .5rem;
    align-items: center;
}
/* Bootstrap's .form-label is inline-block, which is invisible above a block
   .form-control and wrong above the ONE field whose control is an inline
   span: the total rendered as «الإجمالي627.90», label and value glued into
   one token. */
.invoice-line-label { display: block; }
/* Stacked, each field wears a .small label with mb-1, so the remove button's
   offset is THAT label's box, not .form-label's (see .repeat-row-remove).
   min-width 576px because below it .repeat-row stacks the button under the
   fields and _forms.css zeroes the offset there — this (0,2,0) rule would
   otherwise outrank that (0,1,0) one. */
@media (min-width: 576px) {
    .invoice-line > .repeat-row-remove {
        margin-block-start: calc(1.6 * 0.875 * var(--base-font-size, 15px) + 0.25rem);
    }
}
@container invoice-lines (min-width: 60rem) {
    .invoice-lines-head,
    .invoice-line-fields {
        display: grid;
        grid-template-columns: 8rem minmax(10rem, 2fr) 5rem 7rem 6.5rem 5rem 7rem;
        gap: .5rem;
        align-items: center;
    }
    .invoice-lines-head {
        grid-template-columns: 8rem minmax(10rem, 2fr) 5rem 7rem 6.5rem 5rem 7rem calc(2.8rem + 2px + 0.25rem);
    }
    /* The head now carries the column titles, so a per-field label would
       repeat every one of them on every row — and the remove button aligns
       with the inputs, not with a label that is no longer there. */
    .invoice-line-label { display: none; }
    .invoice-line > .repeat-row-remove { margin-block-start: 0; }
}
.d-md-grid { display: none; }
@media (min-width: 768px) {
    .d-md-grid { display: grid !important; }
}
.invoice-line .repeat-row-fields.invoice-line-fields { flex: 1 1 auto; }
.invoice-line-total { min-width: 6rem; text-align: end; display: inline-block; }
.invoice-line-account { grid-column: 1 / -1; display: flex; align-items: center; gap: .5rem; }
.invoice-line-account .form-select { max-width: 24rem; }
.invoice-installments-head,
.invoice-installment-fields {
    display: grid;
    grid-template-columns: 2.5rem minmax(10rem, 2fr) 11rem 9rem;
    gap: .5rem;
    align-items: center;
}
/* AFTER the base rule, or it never applies: same specificity, and the base
   rule used to come second — so the four fixed tracks (544px) overflowed a
   501px viewport, with the remove button pushed 157px off the start edge,
   for as long as the editor existed (found 2026-09-12). */
@media (max-width: 767.98px) {
    .invoice-installment-fields { grid-template-columns: 1fr 1fr; }
}
.invoice-installments-head {
    grid-template-columns: 2.5rem minmax(10rem, 2fr) 11rem 9rem calc(2.8rem + 2px + 0.25rem);
}
.invoice-installment .repeat-row-fields.invoice-installment-fields { flex: 1 1 auto; }
/* Never a label above an instalment's fields (the head, or nothing at all on
   a phone), so the button aligns with the inputs at every width. */
.invoice-installment > .repeat-row-remove { margin-block-start: 0; }

/* The three head rows. A head sits OUTSIDE the .form-callout its rows live
   in, so its tracks line up with the fields' only if it is inset by exactly
   what the rows are — .form-callout's 3px accent bar + 1.25rem padding +
   .repeat-row's 1px border + 1rem padding on the start side (a 1px border,
   not the accent, on the end side) — and its last track is the remove
   button (2.8rem + 2px, .repeat-row-remove in _forms.css) plus the .25rem by
   which .repeat-row's .75rem gap exceeds these grids' .5rem. Before this
   every title on /invoices/new stood 36px off its column and البيان was 70px
   wider in the head than on the row (measured 2026-09-12). */
.invoice-lines-head,
.invoice-installments-head,
.journal-lines-head {
    padding-block: 0 .35rem;
    padding-inline: calc(2.25rem + 4px) calc(2.25rem + 2px);
    font-size: .8rem;
    color: var(--text-secondary);
}
.invoice-totals {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: .5rem 1.5rem;
}
.invoice-total-item { display: flex; gap: .5rem; align-items: baseline; }
.invoice-total-label { color: var(--text-secondary); font-size: .85rem; }
.invoice-total-value { font-weight: 600; }
.invoice-total-grand .invoice-total-value { font-size: 1.1rem; color: var(--primary); }
.receipt-summary { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.billing-client-check { padding-block-start: .45rem; }
/* The two tables inside an /invoices row panel (البنود, جدول السداد). Each is an
   .admin-table nested inside the register's own .admin-table, so it inherits
   that table's 0.75rem/1rem cells — generous for a peek that is already inset
   twice. One notch down on both axes keeps a section reading as part of the
   panel rather than as a second register inside it. The class is worn by
   nothing else. */
.invoice-lines-table td,
.invoice-lines-table th {
    font-size: .85rem;
    padding: .5rem .75rem;
}

/* The section reads as a BLOCK inside the panel, not as loose rows on it: the
   panel surface is --bg-secondary, so a bare table there is a flat field of one
   colour with only a hairline under each row. --card lifts it, and the header
   band (already --bg-secondary) then has something to sit against. The last
   row drops its own border — it would double with the card's. */
.invoice-lines-table {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.invoice-lines-table tbody tr:last-child td {
    border-bottom: 0;
}

/* ── «حقول القالب» — the field rows of the /templates modal (the 2026-09 V48) ──
   Two lines per row: المصدر · التسمية (عربي) · التسمية (إنجليزي), then
   الاسم في النص · النوع · القيمة الافتراضية. Three equal tracks, so the two lines
   share their columns; a system row hides its type and default (d-none on their
   wrappers) and keeps its key in the first track. minmax(0, 1fr) lets a select
   shrink instead of overflowing the modal; one column on a phone. The remove
   button keeps .repeat-row-remove's label offset: these fields carry labels. */
.template-field-row .repeat-row-fields.template-field-fields {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 767.98px) {
    .template-field-row .repeat-row-fields.template-field-fields {
        grid-template-columns: minmax(0, 1fr);
    }
}
/* The fields table inside a /templates row panel — the .invoice-lines-table
   density above, for its reason: a second register inside a panel should read
   as part of the panel. Worn by nothing else. */
.template-fields-table {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}
.template-fields-table td,
.template-fields-table th {
    font-size: .85rem;
    padding: .5rem .75rem;
}
.template-fields-table tbody tr:last-child td {
    border-bottom: 0;
}
