/* ============================================
   Form Controls & Inputs
   ============================================ */

/* ===========================================
   Base Form Controls
   =========================================== */

/* background-COLOR, never the `background:` shorthand: the shorthand resets
   background-image, and on .form-select that image IS the dropdown chevron
   (Bootstrap draws it as an SVG background, positioned per direction by the
   LTR/RTL build). The shorthand sat here for a long time and silently
   stripped the arrow off every native select in the app. */
/* --border-strong, NOT --border. A control's border is the only thing that
   identifies it: .form-control is --card inside a --card modal, so with the
   decorative hairline it measured 1.47:1 against its own surroundings and the
   New Task modal read as a flat white sheet with faint rectangles rather than
   as a form. WCAG 1.4.11 asks 3:1 of a boundary doing that job; --border-strong
   is 2.95:1 in light. Disabled/readonly below deliberately stays on --border —
   a switched-off field SHOULD recede, and the softer edge is now what
   distinguishes it. */
.form-control,
.form-select {
    background-color: var(--card);
    border: 1px solid var(--border-strong);
    color: var(--text);
    /* Shared by every design (_variables.css); holds today's value. */
    border-radius: var(--radius-field);
    padding: 0.65rem 0.875rem;
    transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
}

/* The uniform padding above drops Bootstrap's arrow gutter — restore it on
   the inline END, which is where both builds park the chevron (right in LTR,
   left in RTL), so a long option never runs underneath it. */
.form-select {
    padding-inline-end: 2.25rem;
}

/* The two SIZE modifiers, re-declared AFTER the rule above so they win by
   source order again (2026-09-12, plans/button-scale-plan.md). Bootstrap's
   .form-control-sm / .form-select-sm set padding, font-size and radius at the
   same specificity as .form-control, and the flat `padding` above, loaded
   later, silently beat them: a -sm field drew the full 0.65rem box with 14px
   text — 43.1px instead of 30.3 — on 181 fields across 40 templates, which is
   why every register's filter row sat 3–4px off the buttons beside it and why
   45 of those buttons carried an inline `style="padding…"` to fake the match.
   Values are Bootstrap's own; the radius is the project's small token. The
   widgets follow on their own: date-field.js and searchable-select.js copy
   the host's classes onto the visible box. Every -sm in a TEMPLATE then went
   the same day (the owner wanted every surface control bigger), and the
   JS-built ROW EDITORS followed on the day's fourth cut («هل الـ inputtext
   حجمه مناسب», the invoice line editor circled): invoice lines, instalments,
   the settings rows — a repeat-row inside a form is a form. The class now
   lives only inside a dropdown panel: the search box of multi-select.js and
   searchable-select.js. */
.form-control-sm {
    min-height: calc(1.5em + 0.5rem + 2px);
    padding: 0.25rem 0.5rem;
    font-size: 0.875rem;
    border-radius: var(--radius-sm);
}

.form-select-sm {
    padding-block: 0.25rem;
    padding-inline-start: 0.5rem;
    padding-inline-end: 2rem;
    font-size: 0.875rem;
    border-radius: var(--radius-sm);
}

/* Bootstrap's chevron SVG has a hardcoded #343a40 stroke — a --bs-* value
   this app never themes (see Theme & FOUC), so on a dark card the arrow is
   invisible. Override the image through Bootstrap's own custom property:
   that keeps the second image slot (--bs-form-select-bg-icon, used by
   validation states) composing exactly as before. A data URI cannot read
   var(--text-secondary), so the stroke is the literal dark-theme gray. */
[data-theme="dark"] .form-select {
    --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}

.form-control:focus,
.form-select:focus {
    background-color: var(--card);
    border-color: var(--primary);
    color: var(--text);
    box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1);
    outline: none;
}

/* Disabled / read-only.
   Bootstrap fills these from --bs-secondary-bg, which does NOT follow this
   app's theme (we switch on data-theme, Bootstrap on data-bs-theme — never set
   here). Left alone, a disabled field keeps a pale grey box and light-grey text
   inside a dark modal: it reads as broken rather than as switched off. Same
   remap .form-check-input needed below, and the same reason bg-light is banned
   across the templates.

   --bg-secondary rather than --card, so "off" is visibly a shade away from the
   live fields around it in BOTH themes; not-allowed is what makes the state
   legible before a click. opacity stays 1 — Bootstrap's own value is already 1
   and fading the text would only make it harder to read a value the user is
   meant to see. */

.form-control:disabled,
.form-select:disabled,
.form-control[readonly],
.form-select[readonly] {
    background-color: var(--bg-secondary);   /* -color: keep the select's chevron image */
    border-color: var(--border);
    color: var(--text-secondary);
    cursor: not-allowed;
    opacity: 1;
}

.form-label {
    color: var(--text);
    font-weight: 500;
    margin-bottom: 0.5rem;
}

/* ===========================================
   Radio & Checkbox
   =========================================== */

/* Bootstrap colours .form-check-input from --bs-primary / --bs-body-bg, which
   do NOT follow this app's theme (we switch on data-theme, Bootstrap on
   data-bs-theme — never set here). Left alone, an unchecked radio keeps its
   light background and disappears against the dark modal card. Remap to
   project tokens, as .text-muted and .form-text already are. */

.form-check-input {
    background-color: var(--card);
    border: 1px solid var(--border);
}

.form-check-input:checked {
    background-color: var(--primary);
    border-color: var(--primary);
}

.form-check-input:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1);
    outline: none;
}

.form-check-label {
    color: var(--text);
}

/* Inline radio row. Deliberately not .btn-group or .form-check-inline: the
   options are tenant-editable lookup rows, so the row must stay readable when
   a tenant adds a type and when the modal is at 430px — both of which overflow
   a non-wrapping group. Logical gap/padding keeps it RTL-safe. */

.radio-inline-group {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.25rem;
    padding-block-start: 0.25rem;
}

.radio-inline-group .form-check {
    margin-bottom: 0;
}

.form-control-plaintext {
    color: var(--text) !important;
    background: transparent;
    border: none;
    padding: 0.375rem 0;
}

.form-control-plaintext:focus {
    outline: none;
}

[data-theme="dark"] .form-control-plaintext {
    color: var(--text) !important;
}

/* ===========================================
   Placeholder Styling
   =========================================== */

.form-control::placeholder {
    color: var(--text-muted);
    opacity: 0.7;
}

[data-theme="dark"] .form-control::placeholder {
    color: rgba(255, 255, 255, 0.5) !important;
    opacity: 1;
}

[data-theme="dark"] .form-control::-webkit-input-placeholder {
    color: rgba(255, 255, 255, 0.5) !important;
}

[data-theme="dark"] .form-control::-moz-placeholder {
    color: rgba(255, 255, 255, 0.5) !important;
}

[data-theme="dark"] .form-control:-ms-input-placeholder {
    color: rgba(255, 255, 255, 0.5) !important;
}

[data-theme="dark"] .form-control:focus::placeholder {
    color: rgba(255, 255, 255, 0.3) !important;
}

/* File input — the browser's native "Choose Files" button ships un-themed
   (grey, square, ignores our tokens). Re-skin ::file-selector-button to the
   project's primary accent so it matches every other control, in both themes.
   Logical margin so it sits on the correct side under RTL. */
.form-control[type="file"] {
    padding-block: 0.35rem;
}

.form-control[type="file"]::file-selector-button {
    margin-inline-end: 0.75rem;
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--primary);
    border-radius: 8px;
    background: color-mix(in srgb, var(--primary) 12%, transparent);
    color: var(--primary);
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}

/* The hover sits on the INPUT, in Bootstrap's own selector shape. Bootstrap
   gives ::file-selector-button pointer-events: none, so the pointer always
   targets the input and a ::file-selector-button:hover rule never matched;
   what painted instead was Bootstrap's
   .form-control:hover:not(:disabled):not([readonly])::file-selector-button
   (0,4,1), its un-themed light grey behind the purple label, in both themes.
   One class more (0,5,1) wins the hover back. */
.form-control[type="file"]:hover:not(:disabled):not([readonly])::file-selector-button {
    background: var(--primary);
    color: var(--on-primary);
}

/* ===========================================
   Modal Form Controls
   =========================================== */

.modal .form-control {
    background-color: var(--bg-secondary);
    border-color: var(--border);
    color: var(--text);
}

.modal .form-control:focus {
    background-color: var(--bg-secondary);
    border-color: var(--primary);
    color: var(--text);
}

.modal .form-label {
    color: var(--text);
}

.modal .form-control::placeholder {
    color: var(--text-muted) !important;
    opacity: 0.7 !important;
}

.modal .form-control:focus::placeholder {
    opacity: 0.5 !important;
}

/* ===========================================
   Validation States
   =========================================== */

.form-control.is-invalid {
    border-color: var(--danger);
}

.form-control.is-invalid:focus {
    box-shadow: 0 0 0 3px rgba(var(--danger-rgb), 0.15);
}

/* --danger-TEXT: the message is body text on the card, where --danger is a
   FILL red at 3.57:1 (worst of the four cards; 6.13:1 as --danger-text). It is
   the one line on screen explaining why a save was refused. This does NOT touch the was-validated border/icon pair, which stays
   on Bootstrap's own tokens for the reason CLAUDE.md gives (the tick and cross
   are baked-in data URIs, so re-theming the border alone would put a themed
   ring around an unthemed glyph) — and the message was never on those tokens
   anyway. It also puts the text a shade DARKER than that #dc3545 border
   instead of lighter, which is the way round a caption and its frame read. */
.invalid-feedback {
    display: none;
    color: var(--danger-text);
    font-size: 0.8rem;
    margin-top: 0.25rem;
}

.form-control.is-invalid ~ .invalid-feedback {
    display: block;
}

/*
 * An ADVISORY note under a field — the non-blocking "not on the case" warning
 * on the meeting/task/admin-work assignee pickers. It deliberately does NOT
 * live inside .invalid-feedback: it must show while the field is :valid and
 * the save proceeds, so JS toggles d-none on it directly. Amber comes from the
 * project token (never Bootstrap's .text-warning — a --bs-* colour this app
 * does not theme), mixed toward --text-primary because the raw token is an
 * icon/badge amber with ~2:1 contrast on the light card.
 */
.field-warning-text {
    display: block;
    color: color-mix(in srgb, var(--warning) 60%, var(--text-primary));
    font-size: 0.8rem;
    margin-top: 0.25rem;
}

/*
 * The multi-select's invalid state needs its own pair of rules: the host is a
 * plain <div>, so `.form-control.is-invalid` never matches it and the sibling
 * rule above cannot reach its feedback. The flag goes on the HOST rather than
 * the generated toggle because multi-select.js rebuilds the toggle on every
 * init() — a class set on the toggle would be silently discarded the next time
 * the modal opened.
 */
.multi-select.is-invalid .multi-select-toggle {
    border-color: var(--danger);
}

.multi-select.is-invalid .multi-select-toggle:focus {
    box-shadow: 0 0 0 3px rgba(var(--danger-rgb), 0.15);
}

.multi-select.is-invalid ~ .invalid-feedback {
    display: block;
}

/*
 * The multi-select inside a `was-validated` form.
 *
 * The visible toggle is a <button>, which is BARRED from constraint validation
 * — it matches neither :valid nor :invalid, so it stayed plain while every
 * field around it was painted. multi-select.js therefore renders a hidden
 * native <select multiple> as the FIRST child of the host, and these rules lift
 * that select's state onto the toggle. Border only, no ✓/⚠ background icon:
 * the toggle already holds chips and a chevron, and an icon would land on top
 * of them.
 *
 * `~` reaches the toggle because the select is its earlier sibling; the
 * .invalid-feedback sits outside the host, so that one rule needs :has().
 */
.multi-select-native {
    /* Visually hidden but NOT display:none and NOT disabled — either would bar
       it from constraint validation and silently undo the whole mechanism. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    border: 0;
}

.was-validated .multi-select-native:invalid ~ .multi-select-toggle {
    border-color: var(--danger);
}

.was-validated .multi-select-native:invalid ~ .multi-select-toggle:focus {
    box-shadow: 0 0 0 3px rgba(var(--danger-rgb), 0.15);
}

.was-validated .multi-select:has(> .multi-select-native:invalid) ~ .invalid-feedback {
    display: block;
}

.was-validated .multi-select-native:valid ~ .multi-select-toggle {
    border-color: var(--success);
}

.was-validated .multi-select-native:valid ~ .multi-select-toggle:focus {
    box-shadow: 0 0 0 3px rgba(var(--success-rgb), 0.15);
}

/*
 * The ADVISORY state — .has-warning on the HOST (same placement reasoning as
 * .is-invalid above: init() rebuilds the toggle, so a class on the toggle would
 * be discarded). Set by the assignee/attendee membership warnings
 * (syncAssigneeCaseValidity / syncAttendeeCaseValidity) beside their
 * .field-warning-text message, so the field itself reads amber instead of the
 * green ✓ paint — which said "all good" over a warning the user was meant to
 * notice.
 *
 * The second selector of each pair exists for SPECIFICITY: inside a
 * was-validated form the green rule above is (0,4,0) and a plain
 * .multi-select.has-warning .multi-select-toggle (0,3,0) would lose to it —
 * the field would only warn until the first Save attempt, then flip green.
 */
.multi-select.has-warning .multi-select-toggle,
.was-validated .multi-select.has-warning > .multi-select-native:valid ~ .multi-select-toggle {
    border-color: var(--warning);
}

.multi-select.has-warning .multi-select-toggle:focus,
.was-validated .multi-select.has-warning > .multi-select-native:valid ~ .multi-select-toggle:focus {
    box-shadow: 0 0 0 3px rgba(var(--warning-rgb), 0.15);
}

/*
 * The same advisory on a PLAIN single select — المحامي المسؤول on the session
 * result modal (2026-08-14; sessions' roster rule joined the tasks warning
 * shape, and that modal's picker is not a multi-select). Same specificity
 * story: inside a was-validated form Bootstrap's `.form-select:valid` (0,3,0)
 * would out-paint a bare `.form-select.has-warning` (0,2,0) after the first
 * Save attempt, so the second selector of each pair re-states the rule at
 * (0,4,0)+. background-image: none there drops the green ✓ icon — an amber
 * border around a green check would say two things at once.
 */
.form-select.has-warning {
    border-color: var(--warning);
}

.was-validated .form-select.has-warning:valid {
    border-color: var(--warning);
    background-image: none;
}

.form-select.has-warning:focus,
.was-validated .form-select.has-warning:valid:focus {
    box-shadow: 0 0 0 3px rgba(var(--warning-rgb), 0.15);
}

/*
 * The searchable select inside a `was-validated` form — the same mechanism as
 * the multi-select above, and needed for the same reason: the visible control
 * is a <button>, which is barred from constraint validation and so matches
 * neither :valid nor :invalid. The ORIGINAL <select> is still in the DOM as the
 * first child of the wrapper, still carries `required`, and is still what
 * checkValidity() judges — these rules lift its state onto the toggle.
 *
 * Border only, no ✓/⚠ background icon: the toggle already carries a chevron at
 * the end edge and an icon would land on top of it.
 */
.was-validated .searchable-select-native:invalid ~ .searchable-select-toggle {
    border-color: var(--danger);
}

.was-validated .searchable-select-native:invalid ~ .searchable-select-toggle:focus {
    box-shadow: 0 0 0 3px rgba(var(--danger-rgb), 0.15);
}

.was-validated .searchable-select:has(> .searchable-select-native:invalid) ~ .invalid-feedback {
    display: block;
}

.was-validated .searchable-select-native:valid ~ .searchable-select-toggle {
    border-color: var(--success);
}

.was-validated .searchable-select-native:valid ~ .searchable-select-toggle:focus {
    box-shadow: 0 0 0 3px rgba(var(--success-rgb), 0.15);
}

/* ===========================================
   Copied Text Form
   =========================================== */

.copied-text-panel {
    padding: 0;
}

.copied-text-header {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1.5rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--border);
}

.btn-back-to-sources {
    background: none;
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.5rem 1rem;
    border-radius: var(--radius-md);
    font-size: 0.875rem;
    cursor: pointer;
    transition: all var(--transition-normal);
    display: flex;
    align-items: center;
}

.btn-back-to-sources:hover {
    background: var(--bg-secondary);
}

.copied-text-title {
    font-size: 1rem;
    font-weight: 500;
    color: var(--text);
}

.copied-text-form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.copied-text-form .form-group {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.copied-text-form label {
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--text);
}

.copied-text-form .form-control {
    width: 100%;
    padding: 0.75rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    font-size: 0.9rem;
    background: var(--bg);
    color: var(--text);
    transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
}

.copied-text-form .form-control:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.15);
}

.copied-text-form textarea.form-control {
    resize: vertical;
    min-height: 150px;
    font-family: inherit;
    line-height: 1.6;
}

.copied-text-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.75rem;
    margin-top: 0.5rem;
}

.btn-cancel-text {
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.75rem 1.5rem;
    border-radius: var(--radius-md);
    font-size: 0.875rem;
    font-weight: 500;
    cursor: pointer;
    transition: all var(--transition-normal);
}

.btn-cancel-text:hover {
    background: var(--bg-secondary);
}

.btn-save-text {
    background: var(--primary);
    color: #ffffff;
    border: none;
    padding: 0.75rem 1.5rem;
    border-radius: var(--radius-md);
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
    transition: all var(--transition-normal);
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.btn-save-text:hover {
    background: var(--primary-dark);
}

.btn-save-text:disabled {
    opacity: 0.7;
    cursor: not-allowed;
}

/* Dark mode adjustments */
[data-theme="dark"] .copied-text-header {
    border-bottom-color: var(--border);
}

[data-theme="dark"] .btn-back-to-sources {
    border-color: var(--border);
}

[data-theme="dark"] .btn-back-to-sources:hover {
    background: var(--card);
}

[data-theme="dark"] .copied-text-form .form-control {
    background: var(--card);
    border-color: var(--border);
    color: var(--text);
}

[data-theme="dark"] .copied-text-form .form-control::placeholder {
    color: var(--text-muted);
}

[data-theme="dark"] .btn-cancel-text {
    background: var(--card);
    border-color: var(--border);
    color: var(--text);
}

[data-theme="dark"] .btn-cancel-text:hover {
    background: var(--card-hover);
}

/* ===========================================
   Note Edit Form
   =========================================== */

.note-edit-form {
    display: none;
    white-space: normal;
}

.note-edit-form.show {
    display: block;
}

.note-edit-form form {
    display: block;
}

.note-edit-form .mb-3 {
    margin-bottom: 1rem !important;
}

.note-edit-form .form-label {
    display: block;
    font-weight: 600;
    margin-bottom: 0.5rem;
    color: var(--text);
    font-size: 0.875rem;
}

.note-edit-form .form-control {
    display: block;
    width: 100%;
    padding: 0.75rem;
    border: 2px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-secondary);
    color: var(--text);
    font-size: 0.875rem;
    font-family: inherit;
    transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
    box-sizing: border-box;
}

.note-edit-form .form-control:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.15);
}

.note-edit-form textarea.form-control {
    resize: vertical;
    min-height: 120px;
}

.note-edit-form .d-flex {
    display: flex !important;
    margin-top: 1.5rem;
}

.note-edit-form .gap-2 {
    gap: 0.5rem !important;
}

.note-edit-form .justify-content-end {
    justify-content: flex-end !important;
}

.note-edit-form .btn-primary {
    background: var(--primary);
    border-color: var(--primary);
    color: #ffffff;
    font-weight: 600;
    box-shadow: var(--shadow-primary);
}

.note-edit-form .btn-primary:hover {
    background: var(--primary-dark);
    border-color: var(--primary-dark);
    transform: translateY(-1px);
    box-shadow: var(--shadow-primary-hover);
}

.note-edit-form .btn-primary:disabled {
    background: var(--primary);
    border-color: var(--primary);
    opacity: 0.7;
    transform: none;
}

.note-edit-form .btn-secondary {
    background: transparent;
    color: var(--text-secondary);
    border: 2px solid var(--border);
    font-weight: 600;
}

.note-edit-form .btn-secondary:hover {
    background: var(--bg-secondary);
    color: var(--text);
    border-color: var(--border);
}

/* ===========================================
   Quick Note Form (Dark Theme)
   =========================================== */

.dark-quick-note-form {
    display: none;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    margin-bottom: 1rem;
}

.dark-quick-note-form.show {
    display: flex;
}

.dark-quick-note-form input,
.dark-quick-note-form textarea {
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 0.6rem;
    border-radius: var(--radius-md);
    font-size: 0.875rem;
}

.dark-quick-note-form input::placeholder,
.dark-quick-note-form textarea::placeholder {
    color: var(--text-muted);
}

.dark-quick-note-form input:focus,
.dark-quick-note-form textarea:focus {
    outline: none;
    border-color: var(--primary);
}

.dark-quick-note-form .form-actions {
    display: flex;
    gap: 0.5rem;
    justify-content: flex-end;
}

.dark-quick-note-form .btn-cancel {
    background: none;
    border: 1px solid var(--border);
    color: var(--text-muted);
    padding: 0.4rem 0.75rem;
    border-radius: var(--radius-sm);
    font-size: 0.8rem;
    cursor: pointer;
}

.dark-quick-note-form .btn-save {
    background: var(--primary);
    border: none;
    color: #fff;
    padding: 0.4rem 0.75rem;
    border-radius: var(--radius-sm);
    font-size: 0.8rem;
    cursor: pointer;
}

/* ===========================================
   Delete Title Input (Special Styling)
   =========================================== */

#deleteTitleInput::placeholder {
    color: var(--text-muted) !important;
    opacity: 0.8 !important;
}

#deleteTitleInput::-webkit-input-placeholder {
    color: var(--text-muted) !important;
    opacity: 0.8 !important;
}

#deleteTitleInput::-moz-placeholder {
    color: var(--text-muted) !important;
    opacity: 0.8 !important;
}

#deleteTitleInput:-ms-input-placeholder {
    color: var(--text-muted) !important;
    opacity: 0.8 !important;
}

#deleteTitleInput:focus::placeholder {
    opacity: 0.6 !important;
}

/* ===========================================
   Form Helper Text
   =========================================== */

/*
 * Bootstrap's .form-text resolves to var(--bs-secondary-color), which this
 * app never repoints: the theme switches on [data-theme], not [data-bs-theme],
 * so --bs-secondary-color keeps its LIGHT value (near-black) in dark mode and
 * every hint renders invisible against a dark card until you select the text.
 *
 * .text-muted was already remapped for exactly this reason (_base.css); this
 * is the same fix for the class Bootstrap uses on form hints. Global, not
 * per-page — the bug affected every .form-text in the app.
 *
 * --text-secondary, NOT --text-muted, and the difference is measured: on the
 * light card, --text-muted (#9ca3af) lands at 2.54:1, well under the 4.5:1 AA
 * floor and worse than the Bootstrap value it replaces. --text-secondary gives
 * 5.0:1 light and 10.9:1 dark. A form hint is instructional text a user has to
 * be able to read, not decoration.
 */
.form-text {
    color: var(--text-secondary);
}

/* ===========================================
   Direction-Independent Alignment
   =========================================== */

/*
 * Every form control aligns to the PAGE's start edge, whatever its own dir.
 *
 * A bilingual form has an Arabic input (dir="rtl") next to an English one
 * (dir="ltr"). Left alone, each aligns to ITS OWN direction's start, so the
 * two sit against opposite edges and the column reads ragged. text-align:start
 * cannot fix that — `start` is resolved against the element's own dir, which
 * is the very thing that differs.
 *
 * So the page direction has to be the anchor. dir stays on the input, because
 * it still governs caret movement and how punctuation orders while typing;
 * only the alignment is overridden.
 *
 * Applied by ATTRIBUTE rather than by a class you must remember to add:
 * setting dir on a control is precisely what causes the mismatch, so the fix
 * rides along with the cause and cannot be forgotten on the next form.
 *
 * .align-start-always stays for the controls that misalign with NO dir of
 * their own — <input type="date">, where the browser picks an edge for the
 * numeric value — and for one-off non-form elements.
 *
 * .form-control-plaintext is in the list because it is the READ-ONLY twin of
 * .form-control and sits in the same grid, under the same <label> — and it
 * was missing here until 2026-09-11, which is what detached the الإجمالي of
 * the expense and revenue vouchers: a computed total carries dir="ltr" for
 * the digits, so in the Arabic locale the number hugged the LEFT edge of its
 * column while its own label stayed on the right. Nothing declares
 * text-align on these, so they inherit `start` — and `start` is resolved
 * per element against ITS dir, which is the whole trap this block exists for.
 */
html[dir="ltr"] .form-control[dir],
html[dir="ltr"] .form-select[dir],
html[dir="ltr"] .form-control-plaintext[dir],
html[dir="ltr"] .align-start-always {
    text-align: left;
}

html[dir="rtl"] .form-control[dir],
html[dir="rtl"] .form-select[dir],
html[dir="rtl"] .form-control-plaintext[dir],
html[dir="rtl"] .align-start-always {
    text-align: right;
}


/* =========================================================================
 * DATE FIELD  (common/date-field.js)
 *
 * A native <input type="date"> draws its value in the BROWSER's locale, which
 * no app code can override — the same stored day reads 27/07/2026 for one user
 * and 07/27/2026 for another. So date-field.js puts a real text box in front of
 * it (formatted by common/dates.js) and keeps the native input purely as its
 * calendar button: still there, still holding the ISO value, but reduced to the
 * picker icon at the field's inline-end.
 *
 * The native input is NOT display:none — a hidden input cannot open a picker,
 * and `color: transparent` is what suppresses its locale-formatted text while
 * leaving the indicator clickable.
 * ========================================================================= */

.date-field {
    position: relative;
    display: block;
}

/* Room for the icon so a typed value never runs underneath it. */
.date-field-text {
    width: 100%;
    padding-inline-end: 2.5rem;
}

.date-field-native {
    position: absolute;
    inset-block-start: 0;
    inset-inline-end: 0;
    height: 100%;
    width: 2.5rem;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: transparent;          /* hides the browser's own locale text */
    cursor: pointer;
}

/* The indicator is the only part meant to be visible. */
.date-field-native::-webkit-calendar-picker-indicator {
    cursor: pointer;
    opacity: 0.55;
    width: 1rem;
    height: 1rem;
}

.date-field-native::-webkit-calendar-picker-indicator:hover {
    opacity: 1;
}

/* Chrome renders the indicator dark-on-dark otherwise. `color-scheme` is the
   supported lever for the picker panel itself; the filter handles the icon. */
html[data-theme="dark"] .date-field-native {
    color-scheme: dark;
}

html[data-theme="dark"] .date-field-native::-webkit-calendar-picker-indicator {
    filter: invert(1);
}

/* Focus belongs to the text box; the icon must not draw a second ring. */
.date-field-native:focus,
.date-field-native:focus-visible {
    outline: none;
    box-shadow: none;
}

/*
 * VALIDATION MESSAGE for an upgraded date field.
 *
 * Bootstrap reveals `.invalid-feedback` through a SIBLING combinator
 * (`.was-validated .form-control:invalid ~ .invalid-feedback`), and upgrading
 * breaks exactly that relationship: date-field.js wraps the control in
 * `.date-field`, so the message that was written as the input's sibling in the
 * template is now the WRAPPER's sibling and nothing matches it. The field then
 * blocks the save while showing no reason — which is how the consultations end
 * date rule looked before this rule existed.
 *
 * :has() re-crosses that boundary: the wrapper is treated as invalid when the
 * box inside it is. Both halves of Bootstrap's pair are mirrored — the
 * `was-validated` form and the imperative `.is-invalid` — so a date field
 * behaves like every other control in both flows.
 */
.was-validated .date-field:has(> .form-control:invalid) ~ .invalid-feedback,
.date-field:has(> .form-control.is-invalid) ~ .invalid-feedback {
    display: block;
}

/* Bootstrap's ✓/✗ icon is drawn at the field's end edge — precisely where the
   calendar button already sits, so the two overlap into an unreadable smudge.
   The border colour still carries the state, and the message above says what is
   wrong; the padding reservation stays for the picker icon. */
.date-field-text {
    background-image: none !important;
}

/* Firefox has no picker-indicator pseudo-element and paints its own control
   chrome, so give the transparent overlay a glyph of its own to sit under. */
@supports (-moz-appearance: none) {
    .date-field::after {
        content: "\1F4C5";                  /* 📅 */
        position: absolute;
        inset-block-start: 50%;
        inset-inline-end: 0.75rem;
        transform: translateY(-50%);
        font-size: 0.9rem;
        opacity: 0.55;
        pointer-events: none;
    }
}


/* =========================================================================
 * MULTI-SELECT  (common/multi-select.js)
 *
 * A form control, so it lives here rather than in _components.css. Colours
 * come from project tokens only — never a --bs-* variable, which follows
 * data-bs-theme (a thing this app never sets) and would keep its LIGHT value
 * forever on a dark card.
 *
 * Every inline offset is a LOGICAL property (inset-inline-*, margin-inline-*,
 * padding-inline-*). The page is RTL by default, so a physical left/right here
 * would put the chevron and the chip's × on the wrong edge in Arabic.
 * ========================================================================= */

.multi-select {
    position: relative;
}

/* The toggle borrows .form-control for its border/height/focus tokens, then
   overrides the bits a button needs. */
.multi-select-toggle {
    display: flex;
    /*
     * flex-start, not center. Once the control can overflow (below), a centred
     * flex container pushes the first row past its own top edge and no amount
     * of scrolling brings it back — overflow above the start edge is
     * unreachable. This is the standard centred-flex overflow trap.
     */
    align-items: flex-start;
    align-content: flex-start;
    gap: 0.375rem;
    flex-wrap: wrap;
    width: 100%;
    /*
     * Match a plain .form-control sitting beside it, EXACTLY — this control is
     * almost always half of a two-column row, and a toggle 8px shorter than the
     * input next to it reads as a broken field rather than a different one.
     *
     * The formula is the input's own box: one line of text (1.5em) + the block
     * padding below (0.65rem × 2) + both borders. Written as a calc rather than
     * a px literal so it tracks the padding token above instead of drifting the
     * next time .form-control is retuned.
     *
     * A chip row is SHORTER than a text line (0.8125rem × 1.4 + its own padding
     * ≈ 22px vs 24px), so with the same padding the one-row case lands under
     * that floor and min-height lifts it to flush. Multi-row still grows.
     */
    min-height: calc(1.5em + 1.3rem + 2px);
    /*
     * Capped at roughly three rows of chips, then the control scrolls itself.
     * Without this it grows without bound as clients are added: a power of
     * attorney covering a dozen clients pushed the rest of the form off screen
     * and turned the modal into one long scroll. Three rows keeps the common
     * case fully visible; past that the FIELD scrolls, not the whole form.
     *
     * Raised from 6.5rem when the block padding grew to match .form-control:
     * the padding eats into the same box, so the old cap had quietly become
     * ~2.9 rows.
     */
    max-height: 7rem;
    overflow-y: auto;
    /* Do not chain to .modal-form-scroll once the chips run out. */
    overscroll-behavior: contain;
    /* 0.65rem, NOT a tighter value: it must equal .form-control's block padding
       or the chips sit at a different height from the text in the input beside
       them, which is the same mismatch min-height above fixes for the box. */
    padding-block: 0.65rem;
    padding-inline-end: 2rem;   /* room for the chevron */
    text-align: start;
    cursor: pointer;
    background-color: var(--card);
    /* Scrollbar skin comes from the global rules in _base.css. */
}

.multi-select-chevron {
    position: absolute;
    inset-inline-end: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none;
    font-size: 0.75rem;
    color: var(--text-secondary);
    transition: transform 0.2s ease;
}

.multi-select.is-open .multi-select-chevron {
    transform: translateY(-50%) rotate(180deg);
}

.multi-select-placeholder {
    color: var(--text-secondary);
}

.multi-select-chips {
    display: contents;
}

.multi-select-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    max-width: 100%;
    padding: 0.125rem 0.5rem;
    border-radius: var(--radius-pill);
    background-color: rgba(var(--primary-rgb), 0.12);
    color: var(--primary);
    font-size: 0.8125rem;
    line-height: 1.4;
}

.multi-select-chip-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.multi-select-chip-remove {
    display: inline-flex;
    align-items: center;
    margin-inline-start: 0.125rem;
    font-size: 0.6875rem;
    cursor: pointer;
    opacity: 0.7;
}

.multi-select-chip-remove:hover,
.multi-select-chip-remove:focus-visible {
    opacity: 1;
}

/*
 * FIXED, not absolute — and positioned from JS (multi-select.js positionPanel).
 *
 * Every consumer mounts this inside `.modal-form-scroll`, which is
 * `overflow-y: auto`, and an absolutely positioned child is clipped by any
 * scrolling ancestor. A field low in the form used to open its options into a
 * strip the user could not reach without scrolling the form first. `fixed`
 * escapes every overflow ancestor.
 *
 * left/top/width/max-height are all set inline by positionPanel; the values
 * here are the pre-JS fallback and the shared cap. `max-height: 260px` MIRRORS
 * PANEL_MAX_H in multi-select.js — change both together.
 *
 * --z-popover (1060) sits above --z-modal (1050), which it must: a fixed panel
 * is no longer inside the modal's stacking context. Toasts (9999) still win.
 */
.multi-select-panel {
    position: fixed;
    z-index: var(--z-popover);
    max-height: 260px;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background-color: var(--card);
    box-shadow: var(--shadow-lg);
}

.multi-select-search-row {
    margin-bottom: 0.5rem;
}

.multi-select-option {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.375rem 0.5rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
}

.multi-select-option:hover,
.multi-select-option:focus-visible {
    background-color: var(--bg-secondary);
}

/* --bg-secondary is theme-aware, but in dark it is DARKER than --card, so the
   hover would read as a dent rather than a lift. --card-hover is the
   token that exists for exactly this. */
[data-theme="dark"] .multi-select-option:hover,
[data-theme="dark"] .multi-select-option:focus-visible {
    background-color: var(--card-hover);
}

/* The checkbox is drawn rather than native so it can carry the token colours
   in both themes; the row itself owns aria-selected. */
.multi-select-check {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    border: 1px solid var(--border);
    border-radius: var(--radius-xs);
    font-size: 0.625rem;
    color: transparent;
}

.multi-select-option.is-selected .multi-select-check {
    border-color: var(--primary);
    background-color: var(--primary);
    color: var(--on-primary);
}

.multi-select-option-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.multi-select-empty {
    padding: 0.5rem;
    font-size: 0.875rem;
    text-align: center;
}


/* =========================================================================
 * SEARCHABLE SELECT  (common/searchable-select.js)
 *
 * A native <select> with a search box in front of its option list. The original
 * element is kept as the value holder and the validation carrier (see the
 * was-validated rules further up); everything visible here is the replacement.
 * EVERY <select> in the app is one since 2026-09-12 (owner: «في كل القوائم
 * نستطيع أن نبحث فيها مثل قائمة القضية»), the box hidden by the widget on a
 * CATALOG list under four real options (a list of records keeps it) — so these
 * rules are what every dropdown on every page looks like, not a special case.
 *
 * Colours are project tokens only — never a --bs-* variable, which follows
 * data-bs-theme (a thing this app never sets) and would keep its LIGHT value on
 * a dark card. Every inline offset is a LOGICAL property, because the page is
 * RTL by default.
 * ========================================================================= */

.searchable-select {
    position: relative;
}

/* Visually hidden but NOT display:none and NOT disabled — either one bars it
   from constraint validation, and `required` on it would then silently stop
   working. Same clip-path pattern as .multi-select-native, same reason.

   Tag-qualified, and the two dimensions !important: the select KEEPS the
   classes it was written with (.form-select, .dial-code-select's 5.25rem, a
   `w-auto` utility — which is itself !important), because the row builders
   find it by a class; this rule has to outrank every one of them, or the
   clipped-away element grows a real box beside the toggle. */
select.searchable-select-native {
    position: absolute;
    width: 1px !important;
    height: 1px !important;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    border: 0;
}

/*
 * The toggle carries the select's OWN classes (.form-select, and
 * .form-select-sm where the field had it), so its border, height, padding and
 * focus ring all come from the same rules as the plain selects beside it. Only
 * the bits a <button> needs are overridden here.
 *
 * text-align: start rather than the button default of center. There is no
 * `html[dir]` pin needed: the toggle carries a `dir` only when the select did,
 * and in that case the .form-select[dir] rules above already reach it.
 */
.searchable-select-toggle {
    display: block;
    width: 100%;
    padding-inline-end: 2rem;   /* room for the chevron */
    text-align: start;
    cursor: pointer;
    background-color: var(--card);
    /* This button carries .form-select, but it draws its OWN chevron (the FA
       icon below, which rotates on open and follows the theme) — without this
       it would show Bootstrap's restored SVG arrow behind it as a second one. */
    background-image: none;
}

.searchable-select-label {
    display: block;
    /* One line's worth, so an unfilled field is exactly as tall as a filled one
       — these selects are empty until their page's reference feed lands. */
    min-height: 1.5em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* "Nothing chosen" — an empty-valued option (كل القضايا / اختر القضية) is a
   placeholder, not a selection, so it reads muted exactly like a native
   select's placeholder does. --text-secondary, not --text-muted: the latter is
   only 2.5:1 on the light card. */
.searchable-select-toggle.is-placeholder .searchable-select-label {
    color: var(--text-secondary);
}

.searchable-select-chevron {
    position: absolute;
    inset-inline-end: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none;
    font-size: 0.75rem;
    color: var(--text-secondary);
    transition: transform 0.2s ease;
}

.searchable-select.is-open .searchable-select-chevron {
    transform: translateY(-50%) rotate(180deg);
}

/*
 * FIXED, not absolute — and positioned from JS (searchable-select.js
 * positionPanel). Two of the four consumers sit inside `.modal-form-scroll`
 * (`overflow-y: auto`), and an absolutely positioned child is clipped by any
 * scrolling ancestor: a field low in the form would open its options into a
 * strip the user cannot reach.
 *
 * left/top/width/max-height are set inline by positionPanel; the values here
 * are the pre-JS fallback and the shared cap. `max-height: 280px` MIRRORS
 * PANEL_MAX_H in searchable-select.js — change both together.
 *
 * --z-popover (1060) sits above --z-modal (1050), which it must: a fixed panel
 * is no longer inside the modal's stacking context.
 */
.searchable-select-panel {
    position: fixed;
    z-index: var(--z-popover);
    display: flex;
    flex-direction: column;
    max-height: 280px;
    padding: 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background-color: var(--card);
    box-shadow: var(--shadow-lg);
}

/* The search row is pinned and only the OPTIONS scroll — the whole point of
   this control is to keep typing while the list narrows, and a search box that
   scrolls away with the results defeats it. */
.searchable-select-search-row {
    flex: 0 0 auto;
    margin-bottom: 0.5rem;
}

.searchable-select-options {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.searchable-select-option {
    padding: 0.375rem 0.5rem;
    border-radius: var(--radius-sm);
    cursor: pointer;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.searchable-select-option:hover,
.searchable-select-option:focus-visible {
    background-color: var(--bg-secondary);
}

/* --bg-secondary is theme-aware, but in dark it is DARKER than --card, so the
   hover would read as a dent rather than a lift. --card-hover exists for
   exactly this. */
[data-theme="dark"] .searchable-select-option:hover,
[data-theme="dark"] .searchable-select-option:focus-visible {
    background-color: var(--card-hover);
}

.searchable-select-option.is-placeholder {
    color: var(--text-secondary);
}

.searchable-select-option.is-selected {
    background-color: rgba(var(--primary-rgb), 0.12);
    color: var(--primary);
    font-weight: 500;
}

.searchable-select-empty {
    padding: 0.5rem;
    font-size: 0.875rem;
    text-align: center;
}

/* An <optgroup> keeps its heading in the panel (the party filter on /meetings:
   clients under one, employees under the other). The heading is not a row —
   no hover, no tabindex — and the widget drops it with its rows when the
   filter leaves it none. */
.searchable-select-group-label {
    padding: 0.5rem 0.5rem 0.125rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-secondary);
}

.searchable-select-group + .searchable-select-group,
.searchable-select-option + .searchable-select-group {
    margin-top: 0.25rem;
}

/*
 * Inside an .input-group — the dial code beside رقم الجوال (employees,
 * clients), a type select beside its «+» button (expenses, revenues), the
 * case form's client beside its «+». Bootstrap sizes and seams its DIRECT
 * children, and after the upgrade the direct child is the wrapper: the select
 * that carried .form-select is clipped away and the toggle sits one level
 * down. So the wrapper takes the select's flex share and focus lift, and the
 * toggle takes the seam its position earns — LOGICAL corners, so the RTL build
 * needs no mirror. Only the corners: the -1px overlap Bootstrap gives a
 * non-first child already lands on the wrapper. The has-validation form
 * counts from the end past the feedback element, as Bootstrap's own rule does.
 */
.input-group > .searchable-select {
    flex: 1 1 auto;
    width: 1%;
    min-width: 0;
}

.input-group > .searchable-select:focus-within {
    z-index: 5;
}

.input-group:not(.has-validation) > .searchable-select:not(:last-child) > .searchable-select-toggle,
.input-group.has-validation > .searchable-select:nth-last-child(n+3) > .searchable-select-toggle {
    border-start-end-radius: 0;
    border-end-end-radius: 0;
}

.input-group > .searchable-select:not(:first-child) > .searchable-select-toggle {
    border-start-start-radius: 0;
    border-end-start-radius: 0;
}

/* The dial-code select keeps its 5.25rem: `flex: 0 0 auto` used to sit on the
   select itself, and the flex item is the wrapper now. */
.input-group > .searchable-select:has(> .dial-code-select) {
    flex: 0 0 auto;
    width: auto;
}

/*
 * Repeatable child rows — الموكلون / الخصوم on the case form page.
 *
 * The first add/remove-row primitive in this project: nothing else here builds
 * a collection of rows a user can grow and shrink. Written generically (no
 * case- prefix) so the next such collection reuses it rather than cloning it.
 *
 * Grid, not a Bootstrap .row of columns: the remove button has to sit at a
 * fixed width at the row's END edge while the fields share what is left, and
 * `1fr` columns plus `auto` expresses that directly. Logical properties
 * throughout — `inset-inline`, `margin-inline` — never left/right, or the RTL
 * build puts the remove button on the wrong side of the row.
 */
.repeat-row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0.75rem;
    align-items: start;
    padding: 1rem;
    margin-bottom: 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    /* TRANSPARENT, not a filled panel. These rows sit inside an
       .admin-section-card, which is already a raised surface — a second fill on
       top of it reads as a box inside a box and muddies both. The border alone
       is enough to group a row's fields, and it keeps the inputs (which have
       their own background) the only filled things in the card. */
    background: transparent;
    transition: border-color var(--transition-normal), background-color var(--transition-normal);
}

/* A whisper of tint on hover, so it is obvious which row a remove button
   belongs to when several are stacked. */
.repeat-row:hover {
    border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 4%, transparent);
}

/* The fields themselves, so a row can hold two or three side by side and still
   stack on a phone. */
.repeat-row-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 0.75rem;
}

/* A field that must occupy the full row width regardless of the auto-fit above
   (معلومات إضافية — a textarea beside a 180px select reads as an accident). */
.repeat-row-fields .repeat-row-wide {
    grid-column: 1 / -1;
}

/* الخصوم: force the four top fields — اسم الخصم · الصفة · رقم الهوية ·
   هاتف الخصم — onto ONE stretched row of equal quarters, instead of letting the
   auto-fit above wrap them to a second line at narrower card widths.
   minmax(0, 1fr) lets the <select> shrink rather than overflow. The notes
   textarea below still spans the full width via the .repeat-row-wide rule (it
   stays a descendant of .repeat-row-fields). Comes AFTER the base rule so it
   wins on source order. */
.repeat-row-fields--quad {
    grid-template-columns: repeat(4, minmax(0, 1fr));
}

/* Four columns inside an .admin-section-card is ~150px each on a tablet, which
   is below the auto-fit minimum the base rule picked for a reason — an Arabic
   opponent name stops being readable. Two-by-two first, then fully stacked in
   the phone block below. */
@media (max-width: 991.98px) {
    .repeat-row-fields--quad {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/*
 * Aligned with the INPUTS, not the row box: the fields carry a .form-label
 * above them, so a button at the row's top edge would sit beside the label
 * rather than the control it removes. The offset is that label's outer box —
 * one body line (1.6 × --base-font-size, _base.css) plus .form-label's .5rem
 * margin — 32px, which the old flat 1.75rem missed by 4px. A collection whose
 * titles sit in a HEAD row instead (invoice lines, instalments, journal
 * lines) zeroes it beside its grid in _admin.css.
 *
 * The box is the FIELD's (2026-09-12, plans/button-scale-plan.md §3.5): the
 * same 0.65rem block padding and 1px border as .form-control around a 1.5rem
 * line, so the button and the input beside it land on the same pixel
 * (46.8px), and at least as wide as it is tall so it reads as a square peer
 * of the fields rather than a chip parked between two lines of the row — it
 * used to be a 29px pill 28px below the inputs. The three head rows in
 * _admin.css size their last column to this same 2.8rem + 2px.
 */
.repeat-row-remove {
    margin-block-start: calc(1.6 * var(--base-font-size, 15px) + 0.5rem);
    min-inline-size: calc(2.8rem + 2px);
    padding: 0.65rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: transparent;
    /* The outline-danger pair from _variables.css, not the base --danger: the
       base token is 3.76:1 as text on the light card and behind white on
       hover — the two cases rule 36 forbids. --danger-text is the readable
       shade at rest, --danger-fill the one white passes on. */
    color: var(--danger-text);
    line-height: 1.5rem;
    text-align: center;
    cursor: pointer;
}

.repeat-row-remove:hover {
    border-color: var(--danger-fill);
    background: var(--danger-fill);
    color: #fff;
}

.repeat-row-add {
    margin-block-start: 0.25rem;
}

/* Placeholder shown when a collection has no rows at all. */
.repeat-row-empty {
    padding: 1rem;
    margin-bottom: 0.75rem;
    border: 1px dashed var(--border);
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    font-size: 0.875rem;
    text-align: center;
}

/*
 * .form-callout — the modern "framed step" background for a grouped sub-section
 * that must read as distinct from the fields around it (a repeatable child
 * collection, an adjournment sub-form). A soft primary-tinted gradient plus a
 * primary accent bar on the START edge say "a group begins here" without the
 * heavy box-in-a-box fill a solid --bg-secondary panel gives inside a card.
 *
 * Every colour derives from --primary / --border via color-mix, so it tracks
 * BOTH themes with no dark-mode override. Logical properties throughout, so the
 * accent bar mirrors under RTL. Reused by .session-next-group (detail.html) and
 * #clientRows / #opponentRows (cases/form.html).
 */
.form-callout {
    border: 1px solid color-mix(in srgb, var(--primary) 22%, var(--border));
    border-inline-start: 3px solid var(--primary);
    border-radius: var(--radius-lg);
    padding: 1.15rem 1.25rem;
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--primary) 7%, transparent) 0%,
        color-mix(in srgb, var(--primary) 2%, transparent) 100%);
    box-shadow: var(--shadow-sm);
}

/* An empty JS-filled container (a collection with no rows yet) must not paint a
   floating tinted bar — the separate .repeat-row-empty placeholder covers that
   state. */
.form-callout:empty {
    display: none;
}

/* The rows carry their own bottom margin; drop it on the last one so it does not
   push a gap between the final row and the callout's padding. */
.form-callout > .repeat-row:last-child {
    margin-bottom: 0;
}

@media (max-width: 575.98px) {
    /*
     * Stack the remove button under the fields rather than beside them: at
     * 430px a fixed-width button next to a full-width select leaves the select
     * too narrow to read an Arabic contact name in.
     */
    .repeat-row {
        grid-template-columns: 1fr;
    }

    .repeat-row-remove {
        margin-block-start: 0;
        justify-self: end;
    }

    /* Four number/select columns are unreadable on a phone — stack them. */
    .repeat-row-fields--quad {
        grid-template-columns: 1fr;
    }
}

/*
 * A switch whose label sits BESIDE it with real breathing room.
 *
 * Bootstrap's .form-switch puts `padding-inline-start: 2.5em` on the wrapper
 * and `margin-inline-start: -2.5em` on the input; the two cancel, and the label
 * then butts straight up against the toggle with zero gap. That is the whole
 * complaint this class fixes.
 *
 * A SEPARATE class rather than a global `.form-switch .form-check-label` rule:
 * the employees and POA pages use .form-switch too, and silently re-spacing
 * their forms is not this page's business.
 *
 * The wrapper also gets a fixed min height so a switch lines up with the
 * regular inputs sitting next to it in the same .row — without it the switch
 * floats a few pixels high and the row looks unaligned.
 */
.form-switch-labelled {
    display: flex;
    align-items: center;
    min-block-size: 42px;
}

.form-switch-labelled .form-check-input {
    flex: none;
    margin-block: 0;
    cursor: pointer;
}

.form-switch-labelled .form-check-label {
    margin-inline-start: 0.625rem;
    color: var(--text-secondary);
    line-height: 1.3;
    cursor: pointer;
}

/* =========================================================================
   DIAL-CODE SELECT — the رمز الدولة half of a phone .input-group
   =========================================================================
   A prefix, not a field in its own right: the number beside it is the subject,
   so this must stay narrow enough to read as a label on it.

   An EXPLICIT width, and that is the whole point of the rule. A native <select>
   at width:auto sizes itself to its WIDEST OPTION, not to the selected one — so
   even while collapsed to "+966" it reserved room for "+966 - السعودية" and ate
   roughly a third of the group. Bootstrap's .w-auto did exactly that.

   The width fits the longest dial code (+### plus a digit or two) at the
   inline padding below. padding-inline-end keeps clear of .form-select's arrow,
   which Bootstrap positions 0.75rem from the end edge and is 16px wide —
   anything under ~1.75rem here and the code runs beneath the chevron. */
.dial-code-select {
    flex: 0 0 auto;
    width: 5.25rem;
    padding-inline: 0.5rem 1.75rem;
}

/* =========================================================================
   PASSWORD EYE TOGGLE — common/password-toggle.js
   =========================================================================
   A real <button> styled as an .input-group-text, sitting against the field it
   reveals. The input carries .has-toggle so the seam between the two is
   border-free.

   Unscoped, deliberately. These rules lived in login.css under .login-card
   until the /account/security change modal grew the same button — a
   page-scoped rule would have left that one a bare bordered box inside the
   modal, and a second copy is how two rules drift. Nothing here was ever
   login-specific.

   The BUTTON's rule stays logical (border-inline-*): it inherits the page
   direction, so its inline-start is always the side facing the field. The
   INPUT's rule is pinned to html[dir] instead — every .has-toggle input in
   the app carries dir="ltr" (Latin password content), and a logical property
   resolves against the ELEMENT's own dir, not the page's. In the Arabic
   locale border-inline-end therefore removed the input's RIGHT border: the
   icon seam on /login, the OUTER edge in the /account/security modal (whose
   group has no leading icon) — never the toggle seam it was aimed at. Same
   trap as text-align: start, documented in this file's header. */
html[dir="ltr"] .input-group .form-control.has-toggle { border-right: none; }
html[dir="rtl"] .input-group .form-control.has-toggle { border-left: none; }

.password-toggle {
    border-inline-start: none;
    cursor: pointer;
    transition: background-color var(--transition-normal, 0.2s ease);
}

.password-toggle:hover {
    background: var(--card-hover);
}

/* =========================================================================
   PASSWORD POLICY CHECKLIST — fragments/auth/password-policy.html
   =========================================================================
   The live requirements list under every new-password field (the /login
   reset step, the /account/security change modal). password-policy.js adds
   .is-met / .is-unmet per row as the user types; an untouched field carries
   neither, so the list reads as a statement of the rules rather than as five
   failures the user has not had a chance to answer yet.

   Colour is the ONLY thing that changes between the three states — the tick
   glyph stays put, so no row ever reflows and the block cannot shift the
   fields under it mid-typing. --text-secondary (not --text-muted) for the
   neutral state: --text-muted is 2.5:1 on the light card, below the AA floor
   for text that instructs. */
.password-policy {
    margin-top: 0.5rem;
}

.password-policy-title {
    margin-bottom: 0.25rem;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-secondary);
}

.password-policy-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 0.15rem;
}

.password-policy-list li {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.75rem;
    line-height: 1.4;
    color: var(--text-secondary);
}

.password-policy-list li i {
    /* Fixed box so the glyph column stays aligned whatever the state. */
    width: 0.85rem;
    flex-shrink: 0;
    font-size: 0.7rem;
    text-align: center;
    opacity: 0.45;
    transition: opacity 0.15s ease, color 0.15s ease;
}

/* The readable-as-text shades: a policy line is prose, and --success measured
   2.41–2.54:1 on the light cards — the list that tells you why a password was
   rejected, in the colour hardest to read on it. 7.29:1 and up now. */
.password-policy-list li.is-met {
    color: var(--success-text);
}

.password-policy-list li.is-met i {
    color: var(--success-text);
    opacity: 1;
}

.password-policy-list li.is-unmet {
    color: var(--danger-text);
}

.password-policy-list li.is-unmet i {
    color: var(--danger-text);
    opacity: 0.8;
}

/* ============================================================================
   TOUCH — every text control is at least 16px
   ============================================================================
   iOS Safari zooms the whole page in when a control smaller than 16px takes
   focus, and does NOT zoom back out when the field blurs. The user is left on a
   magnified page they have to pinch their way out of, once per field.

   16px is the threshold iOS tests, so this is a floor, not a preference — the
   app's own scale (0.875rem controls, 0.8rem on the -sm variants) is unchanged
   on every pointer device.

   THIS RULE REPLACES `maximum-scale=1.0` in fragments/head.html, and the two
   must not both exist. That meta stopped the zoom by forbidding zoom entirely,
   which Android Chrome honours — so it disabled pinch-zoom for every Android
   user (WCAG 1.4.4) to fix an iOS annoyance, while modern iOS ignores the hint
   in any case. Fixing the font size instead fixes the actual cause, on both
   platforms, and costs nobody their ability to zoom.

   Scoped to `pointer: coarse`, not a width breakpoint: the on-screen keyboard
   is a property of the input device, not of the viewport. A 1024px iPad zooms
   exactly like a 390px iPhone and would sail past a max-width query.

   Last in the file so it beats the equal-specificity rules above it. A few
   selectors here are more specific than a bare `.form-control` for that reason
   and no other — they are answering a more specific rule elsewhere, not
   describing anything different. */
@media (pointer: coarse) {

    .form-control,
    .form-select,
    .form-control-sm,
    .form-select-sm,
    input.form-control,
    select.form-select,
    textarea.form-control,
    .input-group .form-control,
    .input-group .form-select,
    .app-topbar .form-control,
    .multi-select-search,
    .searchable-select-search,
    /* The picker button date-field.js leaves behind. It is `color: transparent`
       inside a fixed 2.5rem box, so 16px changes nothing you can see — but it is
       still an <input type="date">, and tapping it is still a focus iOS would
       zoom for. The visible text box beside it is covered by .form-control. */
    .date-field-native {
        font-size: 16px;
    }
}
