/*
 * Django's admin markup, wearing Edumin's look.
 *
 * Two vocabularies that do not meet: Django emits #changelist, .form-row,
 * fieldset.module, .submit-row; Edumin styles .card, .table, .form-control.
 * CLAUDE.md 42 records a re-skin failing precisely because this file did not
 * exist and a shell was swapped underneath markup nothing had adapted.
 *
 * Scoped to the new shell only. /admin/ never loads this.
 */

/* Light is the default; dark is the same tokens with different values.
   `data-theme-version` is Edumin's own switch -- the stylesheet we ship carries
   344 rules keyed off it -- so setting that one attribute restyles Edumin's
   components and this bridge together, instead of maintaining two palettes. */
:root {
    --brand: #BE1325;
    --brand-ink: #fff;
    --ink: #252525;
    --ink-muted: #595959;
    --ink-dim: #9aa0a6;
    --line: #e6e6e8;
    --field: #f3f3f4;      /* the ground behind cards */
    --surface: #fff;       /* cards, header, sidebar */
    --surface-2: #faf9f9;  /* table headings, filter panel */
    --input-bg: #fff;
    --dropdown-bg: #fff;   /* the panel a dropdown opens OVER the page */
    --shadow: rgba(37, 37, 37, .06);
    --line-strong: #C3C3C9; /* a border meant to be seen, not merely to divide */
    --danger: #B3261E;      /* the app's semantic red -- NOT --brand, which is
                               the school's and is not per-school here */
    /* A switch that is OFF. Its own token rather than `--surface-2`, which is
       #faf9f9 -- a hair off the white card it sits on, so an off switch was a
       control you could not see (owner, 2026-09-03). Chosen to clear 3:1
       against `--surface`, which is what WCAG 1.4.11 asks of the boundary of a
       control that carries no text: it is the ONLY thing saying the switch is
       there at all when it is off. */
    --switch-off: #93939B;
    --switch-knob: #fff;
    /* The edge of an OUTLINED control, and the reason it is not
       `--line-strong`: that token's own comment says «a border meant to be
       seen» and it measures 1.76:1 against this card, so a button drawn with
       it reads as disabled beside a filled one. Same value as `--switch-off`
       and for the same reason -- CLAUDE.md 78 picked that against the 3:1 of
       WCAG 1.4.11 -- but a different ROLE: that one is a fill, this is a
       border. `--line` divides, `--line-strong` separates, this one is
       ANSWERED BY A PERSON. */
    --control-edge: #93939B;
    /* The product's blue, promoted out of the dashboard so a changelist can
       reach it. It was `--viz-info` on `.dash-root` and nowhere else, which
       made it invisible to everything that is not a chart -- and it is the one
       blue here that has already been validated against `--brand` for colour
       vision (CLAUDE.md 67: dE 26.4 light / 24.5 dark, both PASS). Picking a
       fresh blue would have been picking an unvalidated one. 4.41:1 against
       the white card, so it clears 3:1 as an indicator that is the only thing
       saying a state exists. */
    --info: #2a78d6;
}

[data-theme-version="dark"] {
    --brand: #D8394A;      /* lifted: #BE1325 on a dark ground fails contrast */
    --brand-ink: #fff;
    --ink: #E6E6E9;
    --ink-muted: #A8AAB3;
    --ink-dim: #7C7F89;
    --line: #2E2E33;
    --field: #171719;
    --surface: #1F1F23;
    --surface-2: #26262B;
    --input-bg: #1A1A1E;
    --dropdown-bg: #17171E;  /* owner, 2026-09-03 */
    --shadow: rgba(0, 0, 0, .45);
    --line-strong: #3B3B42;
    --danger: #E79890;      /* lifted: #B3261E on this ground fails contrast */
    /* Same rule against the dark card (#1F1F23), which needs a LIGHTER grey to
       clear it -- `--surface-2` (#26262B) was 1.1:1 here, and the knob was
       `--surface`, i.e. the card colour, so both halves vanished at once. */
    --switch-off: #6B6B75;
    --switch-knob: #E6E6E9;
    --control-edge: #6B6B75;
    --info: #3987e5;   /* 4.51:1 against #1F1F23 */
}

body { font-family: 'alexandria', sans-serif; background: var(--field); color: var(--ink); margin: 0; }

/* ------------------------------------------------------------------ layout */
.nav-header { background: var(--brand); position: fixed; top: 0; right: 0; width: 17.1875rem; height: 74px;
              z-index: 6; display: flex; align-items: center; justify-content: center; }
.nav-header .brand-logo { color: #fff; font-weight: 700; font-size: 1.02rem; text-decoration: none; text-align: center; }
.header { position: fixed; top: 0; right: 17.1875rem; left: 0; height: 74px; z-index: 5; display: flex;
          align-items: center; padding: 0 26px; background: var(--surface); border-bottom: 1px solid var(--line); }
.header .header-content { width: 100%; display: flex; align-items: center; justify-content: space-between; }
.dashboard_bar { font-size: 1.1rem; font-weight: 700; color: var(--ink); }
.header-right { display: flex; align-items: center; gap: 16px; list-style: none; margin: 0; padding: 0; }
.ui-switch { border: 1px solid var(--line); border-radius: 999px; padding: .3rem .8rem; font-size: .8rem;
             color: var(--ink-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
.ui-switch:hover { background: var(--surface-2); color: var(--ink); }
.deznav { position: fixed; top: 74px; right: 0; width: 17.1875rem; height: calc(100vh - 74px); overflow-y: auto;
          background: var(--surface); border-left: 1px solid var(--line); z-index: 5; padding-top: 8px; }
.deznav ul { padding: 0; margin: 0; list-style: none; }
.deznav a { display: flex; align-items: center; gap: 12px; padding: 11px 24px; color: var(--ink-muted);
            text-decoration: none; font-size: .93rem; }
.deznav a i { font-size: 19px; width: 22px; text-align: center; color: var(--ink-dim); }
.deznav a:hover { background: var(--surface-2); }
/* The current page: a filled bar plus an edge marker. The fill alone is one
   colour among thirty rows the reader has to find by scanning; the marker gives
   the eye an edge to land on, and survives being scrolled past in peripheral
   vision. */
.deznav a.active { background: var(--brand); color: #fff; font-weight: 700; position: relative; }
.deznav a.active i { color: #fff; }
.deznav a.active::before { content: ''; position: absolute; top: 0; bottom: 0; right: 0; width: 4px;
    background: #fff; opacity: .85; }
[dir="ltr"] .deznav a.active::before { right: auto; left: 0; }
/* The group heading above the current item, so "where am I" answers with the
   section as well as the row. */
.deznav .menu-title.is-current { color: var(--brand); font-weight: 700; }
/* A break inside a group, for a block that belongs under the same heading but
   is not the same kind of thing. Quieter than the heading above it on purpose:
   it separates without claiming to be a section of its own, and a rule as loud
   as a title would read as one. Inset to the text, not the full width, so the
   sidebar keeps one left edge. */
.deznav .menu-divider { height: 1px; background: var(--line); margin: 8px 24px; }
.menu-title { color: var(--ink-dim); font-size: .72rem; letter-spacing: .04em; padding: 14px 24px 6px; display: block; }
.content-body { margin-right: 17.1875rem; margin-top: 74px; min-height: calc(100vh - 74px); padding: 1.5rem 0; }
.container-fluid { padding: 0 1.75rem; }
.card { background: var(--surface); color: var(--ink); border: 0; border-radius: 14px; box-shadow: 0 1px 2px var(--shadow); margin-bottom: 1.5rem; }
.card-body { padding: 1.5rem; }

/* ------------------------------------------------- Django admin, bridged */
#content h1, .card-body > h1 { font-size: 1.15rem; font-weight: 700; color: var(--ink); margin: 0 0 1rem; }

/* the add button and other object tools */
.object-tools { list-style: none; padding: 0; margin: 0 0 1rem; display: flex; gap: .5rem; justify-content: flex-start; }
.object-tools a, .object-tools a:link, .object-tools a:visited {
    background: var(--brand); color: #fff; border-radius: 8px; padding: .5rem 1rem;
    font-size: .85rem; text-decoration: none; display: inline-block; }

/* search + filters row */
/* No card around the search. It was a full-width bordered box with a 450px form
   floating inside it, so most of the loudest element on the page was empty. */
#toolbar { flex: 3 1 20rem; min-width: 0; }
#toolbar #searchbar { flex: 1 1 auto; min-width: 0; }
#toolbar form, #toolbar form > div { display: flex; align-items: center; gap: .5rem; width: 100%; }
#toolbar label img { display: block; width: 16px; opacity: .45; }
#toolbar input[type="text"], #toolbar select { border: 1px solid var(--line); border-radius: 8px;
           padding: .45rem .7rem; font-family: inherit; font-size: .88rem;
           background-color: var(--input-bg); color: var(--ink); }
/* --- the action ladder ---------------------------------------------------
   Three rungs, and they are told apart by FILL, BORDER and INK rather than by
   hue -- so the hierarchy survives greyscale and every form of colour
   blindness, which is what "do not say it with colour alone" means for a
   control that carries no icon.

     primary    filled `--brand`   أضف · احفظ · ارسال -- ONE per screen
     secondary  outlined           ابحث -- a real action that changes nothing
     tertiary   quiet fill         مسح الفلاتر · الغاء (`.ec-clear-filters`)

   ابحث was byte-identical to «أضف امتحان»: two filled brand buttons in one
   row, one of which creates a record (owner, 2026-09-05). Giving it the quiet
   fill instead would have collided with «مسح الفلاتر» one control further
   along, which is tertiary and undoes a filter -- so it takes the rung
   between them rather than either end.

   It is built the SAME WAY as the field beside it -- .45rem of padding, a 1px
   border, .88rem of type -- which is what makes the two the same height
   (33.1px, measured on a render in both themes). The filled tier gets away
   with inheriting the body's 14px because it has no border to line up with;
   compensating for the border in the padding instead, which was the first
   attempt here, leaves this two pixels short. */
#toolbar input[type="submit"] { background: transparent; color: var(--ink); font-weight: 600;
           border: 1px solid var(--control-edge); border-radius: 8px;
           padding: .45rem 1.1rem;
           font-family: inherit; font-size: .88rem; cursor: pointer;
           transition: background-color .15s ease, border-color .15s ease, color .15s ease; }
#toolbar input[type="submit"]:hover { background: var(--surface-2); border-color: var(--brand);
           color: var(--brand); }
#toolbar input[type="submit"]:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { #toolbar input[type="submit"] { transition: none; } }

/* the results table */
#result_list { width: 100%; border-collapse: collapse; background: var(--surface); }
#result_list thead th { background: var(--surface-2); color: var(--ink-muted); font-size: .8rem; font-weight: 600;
           text-align: start; padding: .8rem .75rem; border: 0; position: sticky; top: 0; z-index: 2;
           box-shadow: inset 0 -1px 0 var(--line); }
#result_list thead th a { color: var(--ink-muted); text-decoration: none; }
#result_list td, #result_list th { padding: .75rem; font-size: .9rem; color: var(--ink-muted);
           border-bottom: 1px solid var(--line); vertical-align: middle; }
#result_list tbody tr:hover > td, #result_list tbody tr:hover > th { background: var(--surface-2); }
/* The whole row opens the record (changelist-header.js). The pointer is the
   only thing that says so before the click lands. */
#result_list tbody tr.ec-row-link { cursor: pointer; }
#result_list a { color: var(--ink); text-decoration: none; }
#result_list a:hover { color: var(--brand); }

.paginator { padding: 1rem 0 0; color: var(--ink-muted); font-size: .85rem; }
.paginator a, .paginator .this-page { padding: .3rem .6rem; border-radius: 6px; text-decoration: none; }
.paginator .this-page { background: var(--brand); color: #fff; }
.actions { padding: .75rem 0; display: flex; gap: .5rem; align-items: center; font-size: .88rem; }
.actions select { border: 1px solid var(--line); border-radius: 8px; padding: .4rem .6rem; font-family: inherit; }
.actions button { background: var(--brand); color: #fff; border: 0; border-radius: 8px; padding: .4rem 1rem; cursor: pointer; }

/* ------------------------------------------------------------- change form
 *
 * Django stacks every field in a single column, and this bridge then capped
 * each one at 460px. On a 1440px window that is a narrow ribbon of inputs with
 * half the card empty beside it, and a teacher -- twenty fields -- is a
 * 2,600px scroll to reach the save button.
 *
 * The fieldset is a grid instead: two columns where there is room, one where
 * there is not, and no fixed breakpoint anywhere -- `auto-fill` with a 26rem
 * minimum means the column count follows the space the form actually has,
 * including inside a narrowed window or beside the open sidebar.
 *
 * Fields that genuinely need the width opt out and span the row: a textarea, a
 * multi-select, a file input. Squeezing a five-row list of subjects into half
 * a column is how you get the clipped boxes this replaces.
 */
fieldset.module { border: 0; padding: 0; margin: 0 0 2rem; }
fieldset.module.aligned {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 26rem), 1fr));
    gap: 0 2.5rem;
    align-items: start;
    max-width: 74rem; }

/* The section heading names the group, so it owns the whole row and carries a
   rule under it. It was the same weight as the field labels below it, which
   left the form with no hierarchy at all. */
fieldset.module > h2 {
    grid-column: 1 / -1;
    font-size: 1rem; font-weight: 700; color: var(--ink);
    margin: 0 0 1.1rem; padding-bottom: .6rem; border-bottom: 1px solid var(--line); }

/* Inline forms keep the single column they have always had.
 *
 * nested_admin and Django's own StackedInline both emit `fieldset.module
 * aligned` inside `.inline-related`, so the grid above would reach them --
 * a parent's nested student blocks would silently become two-column. That
 * might well be an improvement; it is not one I have looked at, and a layout
 * change nobody has seen is exactly what CLAUDE.md 45 is a record of. Once
 * the top-level forms are accepted, delete this rule and look at the result.
 */
.inline-related fieldset.module.aligned { display: block; max-width: none; }

/* --- a student and its admission, side by side ---------------------------
 *
 * `nested_admin` nests the admission group INSIDE the student's own fieldset
 * and marks that fieldset `has-inlines`, so the two read as one long column:
 * الاسم, الحالة, السائق, الجنس, and then قبول الطالب somewhere below the fold.
 * They are one record seen from two sides -- who the student is, and where the
 * school has put them -- and the second is what you check while typing the
 * first (owner, 2026-09-03).
 *
 * `has-inlines` is nested_admin's own class and matches only a fieldset that
 * actually contains a nested group, so nothing else on the site moves. The
 * group spans every row of the grid rather than sitting in one cell, or it
 * would align with whichever field happened to be beside it.
 */
@media (min-width: 1100px) {
    /* The grid goes on the student BLOCK, not on its fieldset: nested_admin
       marks the fieldset `has-inlines` but CLOSES it before the nested group,
       which is therefore a SIBLING of the fields rather than a child of them.
       Read from the markup, after a first version keyed on the fieldset laid
       out a grid with nothing in its second column. */
    .inline-related:has(> .inline-group.djn-group-nested) {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
        gap: 0 2rem;
        align-items: start;
    }
    /* The row's own heading -- name, drag handle, delete -- belongs to the
       whole record, so it spans both. */
    .inline-related:has(> .inline-group.djn-group-nested) > h3 { grid-column: 1 / -1; }
    .inline-related:has(> .inline-group.djn-group-nested) > fieldset.module { grid-column: 1; }
    .inline-related:has(> .inline-group.djn-group-nested) > .inline-group {
        grid-column: 2; align-self: start; margin: 0;
    }
}

/* The nested group is a panel, not a continuation of the column above it: a
   step down the surface ramp, its own edge, and its heading at the weight of a
   section rather than of a field label. */
.inline-related .inline-group.djn-group-nested {
    background: var(--surface-2);
    border: 1px solid var(--line); border-radius: 10px;
    padding: .2rem .9rem .9rem;
}
.inline-related .inline-group.djn-group-nested > fieldset.module { margin: 0; }
.inline-related .inline-group.djn-group-nested h2.inline-heading {
    font-size: .88rem; font-weight: 700; color: var(--ink);
    margin: .1rem 0 .5rem; padding: .55rem 0 .5rem;
    border-bottom: 1px solid var(--line);
}
/* The nested rows sit ON that panel, so they take no second background and no
   second border -- one edge per group, not one per row. */
.inline-related .inline-group.djn-group-nested .djn-item.inline-related {
    background: transparent; border: 0; padding: 0; margin: 0 0 .4rem;
}

/* The template row every inline formset carries for "add another".
 *
 * `display: none` on it lives in Django's `admin/css/forms.css`, which jet
 * ships as a ZERO-BYTE file (CLAUDE.md 40) -- deliberate, and fine for /admin/
 * because jet's own base.css re-declares the rule. This shell loads no jet CSS,
 * so the blank template row was VISIBLE under every inline on /admin-new/: an
 * empty row that looks like a real one and saves nothing. Same class of silent
 * breakage as the nine custom properties in CLAUDE.md 53.
 */
.empty-form { display: none; }

/* The grade/order table on المواد and الفترات.
 *
 * One row per grade, and the only control is the order. The template does the
 * structure -- no dropdown, no delete column, no heading -- so this is width,
 * weight and colour only.
 */
/* Two columns of real content do not need the width of the page between them:
   uncapped, الترتيب ends up a hand's width from the grade it belongs to. */
.offering-order table { width: 100%; max-width: 34rem; }
/* The same header treatment as a changelist, so the two read as one system.
   Without it these fall through to Edumin's default `th`, which is brand-red --
   a column heading that looks like an error message. */
.offering-order thead th { background: var(--surface-2); color: var(--ink-muted);
    font-size: .8rem; font-weight: 600; text-align: start; padding: .5rem .7rem; }
.offering-order tbody th.offering-grade { font-weight: 500; color: var(--ink);
    text-align: start; padding: .45rem .7rem; white-space: nowrap; }
.offering-order tbody td { padding: .35rem .7rem; vertical-align: middle; }
/* Specificity, not taste: the generic `.form-row input[type=number]` rule above
   is (0,2,1) and sets width:100%, so anything shorter has to outrank it. */
.offering-order .form-row .field-order input[type="number"] { width: 7rem; text-align: center; }
.offering-order thead th.column-order { width: 8rem; }
.offering-order .offering-help { padding: .6rem .7rem 0; }

.form-row { padding: .5rem 0 .9rem; border: 0; min-width: 0; }
.form-row > div { display: flex; flex-direction: column; gap: 6px; min-width: 0; }

/* Wide by nature. `:has()` is the whole mechanism; a browser without it simply
   leaves these in one column, which is the old behaviour rather than a break. */
.form-row:has(textarea),
.form-row:has(select[multiple]) { grid-column: 1 / -1; }

/* Two fields that are one question -- Django wraps each in a `.field-box` when
   a fieldset row names more than one (the class is `field-box` in 4.2; the
   `fieldBox` spelling belongs to older Django and to grappelli, and matching
   it silently matched nothing). See `PAIRED` in forms/validation.py.
 *
 * The row spans the fieldset's whole line and then repeats the fieldset's OWN
 * column template inside itself, so the pair lands in exactly the tracks every
 * other field uses: aligned with the rows above and below, at the same widths,
 * with no number to keep in step. At one column it stacks, which is the same
 * thing the fieldset does. */
.form-row:has(> .field-box) {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 26rem), 1fr));
    gap: 0 2.5rem;
    align-items: start;
}
.form-row > div.field-box { min-width: 0; }

/* The label is the quiet half of the pair. It read at the same weight and
   colour as the value, so nothing on the form was louder than anything else. */
.form-row label { font-weight: 600; color: var(--ink-muted); font-size: .82rem;
    float: none; width: auto; padding: 0; letter-spacing: .01em; }
.form-row .help, .form-row p.help { font-size: .78rem; color: var(--ink-dim); margin: 0; padding: 0; max-width: 46em; }

/* ONE text-input treatment for the whole shell.
 *
 * There were four. The form's own inputs, the composer's title and content, the
 * student search, the long-set filter and the single-select filter had each been
 * written where they were needed, so a text box's padding, size, corner radius
 * and focus behaviour depended on which feature had added it -- `.5rem/.9rem/8px`
 * beside `.6rem/.9rem/9px` beside `.4rem/.85rem/8px`, and a focus that was a ring
 * on one and a border-colour change on the next. Nobody chose that; it is what
 * five separate rules add up to (owner: "why is this text field different from
 * the others", 2026-09-03).
 *
 * Anything that takes typed text is in this selector list. Local rules below may
 * still set width, margin or height -- what they may not do is redecide the
 * decoration. */
.form-row input[type="text"], .form-row input[type="password"], .form-row input[type="email"],
.form-row input[type="number"], .form-row input[type="url"], .form-row input[type="date"],
.form-row input[type="time"], .form-row input[type="search"],
.form-row textarea, .form-row select,
.ec-compose-field input[type="text"], .ec-compose-field textarea,
.ec-thread-field textarea,
.ec-student-search, .ec-checkset-filter, .ec-combo-search, .ec-roster-filter,
/* The years table is not inside a `.form-row`, so it was getting the calendar
   glyph from the bare element selector and NONE of the padding, border, size or
   focus below -- a date box that did not look like any other date box on the
   panel (owner, 2026-09-05). Joining the list rather than styling it locally is
   the whole point of there being a list. */
.ec-years-table input[type="date"] {
    width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: 8px;
    padding: .5rem .7rem; font-family: inherit; font-size: .9rem;
    /* Arabic needs the room. Alexandria puts the two dots of ي BELOW the
       baseline, and a control whose content box is only as tall as the font's
       own default line box clips them -- «عربي» renders as «عربى», which is a
       different letter, not a cosmetic difference (owner, 2026-09-03).
       Stated once here, with the rest of the one text-input treatment, so a
       sixth control cannot be added without it. */
    line-height: 1.6;
    background-color: var(--input-bg); color: var(--ink); }
.form-row textarea { min-height: 7rem; }

/* A multi-select was showing four and a half rows, clipped mid-line -- the one
   widget on the page where you have to SEE the options to use it. The height
   is NOT set here: change-form.js gives each one a `size` matching how many
   options it actually has, so a list of one is one row tall and a list of
   forty stops at ten. A fixed min-height solves the clipping and replaces it
   with an empty box, which is the same mistake facing the other way.

   `:not(.admin-autocomplete)` because Django's autocomplete fields are
   select2-ified, and sizing the underlying element brings it back into view
   beside its own replacement -- the mistake CLAUDE.md 45 records. */
.form-row select[multiple]:not(.admin-autocomplete) { padding: .35rem; max-height: 16rem; }

/* Every interactive thing gets a visible ring. There was none. */
.form-row input:focus-visible, .form-row select:focus-visible, .form-row textarea:focus-visible,
.ec-compose-field input[type="text"]:focus-visible, .ec-compose-field textarea:focus-visible,
.ec-thread-field textarea:focus-visible, .ec-thread-send:focus-visible,
.ec-student-search:focus-visible, .ec-checkset-filter:focus-visible, .ec-combo-search:focus-visible,
.submit-row input:focus-visible, .submit-row a:focus-visible,
.related-widget-wrapper-link:focus-visible, .object-tools a:focus-visible {
    outline: 2px solid var(--brand); outline-offset: 2px; }

/* A read-only value is a <label> followed by a bare <p>, which reads as an
   input that failed to render. Dashed, on the muted surface: data, not a
   field you are failing to type into.

   `p:not([class])` and not `p:not(.help)`, which was wrong and shipped: Django
   wraps date and time inputs in `<p class="date">`, `<p class="time">` and
   `<p class="datetime">`, so every date field on the site was being painted as
   a read-only chip around a live control. A read-only value is the only <p>
   here with no class at all. */
.form-row > div > p:not([class]), .form-row > div > div.readonly {
    margin: 0; padding: .5rem .7rem; border-radius: 8px; font-size: .9rem;
    background: var(--surface-2); color: var(--ink); border: 1px dashed var(--line); }

/* Date and time controls.
 *
 * Django wraps each input in its own <p>, and a split datetime nests one <p>
 * inside another -- which is invalid, so the browser auto-closes it and the
 * parts arrive as siblings with the "التاريخ:"/"الوقت:" text between them. Laid
 * out inline rather than fought: the labels stay attached to their controls,
 * and Django's <br> puts the time on the next line, which is legible.
 *
 * The controls size to their content instead of filling the column -- a date
 * is ten characters wide however wide the form is. */
.form-row p.date, .form-row p.time, .form-row p.datetime {
    margin: 0; display: inline-flex; align-items: center; gap: .4rem; }
.form-row p.date input, .form-row p.time input { width: auto; min-width: 11rem; }
.form-row p.datetime { color: var(--ink-dim); font-size: .82rem; gap: .5rem; }

/* A calendar on a date field and a clock on a time field.
 *
 * The browser draws an indicator already, but it is a faint 12px monochrome
 * glyph that reads as decoration -- the field still looks like a text box you
 * are expected to type a format into, which is the state CLAUDE.md 53 was
 * fixing when it moved these to native controls in the first place.
 *
 * The glyph is painted as a BACKGROUND on the input, and the native indicator
 * is made transparent rather than removed -- it keeps its hit area, so the
 * click target is unchanged and the picker still opens from it. An earlier
 * version styled `::-webkit-calendar-picker-indicator` directly with a mask,
 * which is the tidier idea and did not render: nothing appeared in the field,
 * and `getComputedStyle(el, '::-webkit-calendar-picker-indicator')` answers
 * with the INPUT's own style rather than the pseudo-element's, so the probe
 * cannot tell you it failed. CLAUDE.md 69, once more: the render is the answer.
 *
 * The stroke colour is baked into each data URI because a background-image
 * cannot inherit `currentColor` -- hence one pair of variables per theme,
 * rather than the mask-plus-token pattern the message strip uses.
 */
:root {
    --date-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23595959' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4.5' width='18' height='16' rx='2.5'/%3E%3Cpath d='M3 9.5h18M8 2.8v3.4M16 2.8v3.4'/%3E%3C/svg%3E");
    --time-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23595959' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5.2l3.3 2'/%3E%3C/svg%3E");
}
[data-theme-version="dark"] {
    --date-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23A8AAB3' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4.5' width='18' height='16' rx='2.5'/%3E%3Cpath d='M3 9.5h18M8 2.8v3.4M16 2.8v3.4'/%3E%3C/svg%3E");
    --time-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23A8AAB3' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5.2l3.3 2'/%3E%3C/svg%3E");
}
input[type="date"], input[type="time"],
/* The `.form-row` copies are not duplication: `.form-row input[type="date"]`
   above sets `padding`, and a bare element selector loses to it -- which put
   the glyph underneath the `dd/mm/yyyy` text instead of beside it. Same
   specificity, declared later, so this wins. */
.form-row input[type="date"], .form-row input[type="time"],
.ec-range-leg input[type="date"], .ec-range-leg input[type="time"],
.ec-years-table input[type="date"] {
    background-image: var(--date-icon);
    background-repeat: no-repeat;
    /* `left`, not an inline keyword: the native control lays its own segments
       out LTR whatever the page direction, and its indicator sits at the
       PHYSICAL left. The glyph has to sit where the click already does. */
    background-position: left .55rem center;
    background-size: 1.05rem 1.05rem;
    padding-left: 2.1rem;
}
input[type="time"], .form-row input[type="time"],
.ec-range-leg input[type="time"] { background-image: var(--time-icon); }
/* Tighter inside a filter chip: the group's border is already the frame, and
   2.1rem of padding there is most of the control. */
.ec-range-leg input[type="date"], .ec-range-leg input[type="time"] {
    padding-left: 1.8rem; background-position: left .4rem center; }
/* Transparent, not `display: none`: hiding it removes the hit area, and the
   click that opens the picker is the whole point of drawing an icon there. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator { opacity: 0; cursor: pointer; }
/* Checkbox and its label share one line and one hit target. */
.checkbox-row { display: grid; grid-template-columns: auto 1fr; gap: .25rem .6rem; align-items: center; }
.checkbox-row input[type="checkbox"] { inline-size: 1.1rem; block-size: 1.1rem; margin: 0; accent-color: var(--brand); }
.checkbox-row label { grid-column: 2; margin: 0; cursor: pointer; color: var(--ink); font-size: .9rem; }
.checkbox-row .help { grid-column: 2; }

/* The green `+` sat under the field on a line of its own. It belongs beside
   the control it adds to. */
.related-widget-wrapper { display: flex; align-items: flex-start; gap: .5rem; min-width: 0; }
.related-widget-wrapper > select, .related-widget-wrapper > .select2 { flex: 1 1 auto; min-width: 0; }
.related-widget-wrapper-link {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line);
    background: var(--surface-2); text-decoration: none; }
.related-widget-wrapper-link img { width: 15px; height: 15px; }
/* The same measure as every other control. Without the cap an autocomplete
   field ran to the full width of its row while the `.ec-combo` beside it
   stopped at 32rem, so الصف and المادة were different WIDTHS on one form even
   once they were the same shape. Scoped to the form: the panel select2 appends
   to <body> carries this same class and is sized from its control. */
.select2-container { min-width: 260px; }
.form-row .select2-container { max-width: 32rem; }

/* The autocomplete dropdown was painting its highlighted row PERIWINKLE, a
   colour that appears nowhere else in this product.

   Django's `admin/css/autocomplete.css` draws the whole widget from eight of
   its own custom properties -- --body-bg, --body-fg, --body-quiet-color,
   --border-color, --darkened-bg, --selected-bg, --primary, --primary-fg -- and
   defines all eight in `admin/css/base.css`, which jet ships as a ZERO BYTE
   file (CLAUDE.md 40). So on this shell every one of them is undefined and the
   declarations that read them are invalid: that is why the box had no visible
   border (CLAUDE.md 53) and why the already-chosen row had no background at
   all. Seven of the eight simply did nothing.

   The eighth did something worse. `--primary` is ALSO edumin's own token, and
   `edumin/css/style-rtl.css` declares `:root { --primary: #6a73fa }` -- the
   ThemeForest template's demo accent, loaded one line earlier in
   admin_new/base.html. Django's rule picked that up, so the widget was wearing
   a colour nobody chose and no palette here contains.

   Aliased onto the tokens this file already carries, so the widget follows the
   theme with the rest of the panel and introduces no colour of its own. The
   dark values come for free: `--brand` and friends are redeclared under
   [data-theme-version="dark"] and `var()` resolves at use.

   Scoped to the container rather than declared on :root, deliberately.
   `--primary` is edumin's name too, with --primary-hover/-light/-dark and nine
   --rgba-primary-* beside it; redefining one of that family on :root leaves the
   other twelve disagreeing with it, on 276 rules. Django's widget is the only
   thing here that reads these names, so it is the only thing that gets them. */
.select2-container--admin-autocomplete {
    --body-bg: var(--input-bg);
    --body-fg: var(--ink);
    --body-quiet-color: var(--ink-dim);
    --border-color: var(--line);
    --darkened-bg: var(--surface-2);
    /* The row already chosen, and the row the cursor is on, must not look
       alike: one is state, the other is position. Neutral step vs brand. */
    --selected-bg: var(--surface-2);
    --primary: var(--brand);
    --primary-fg: var(--brand-ink);
}

/* --- one ground for every dropdown panel --------------------------------
   The panel was edumin's `--card`, which is #212130 in dark mode -- a NAVY,
   and the only navy left on a panel whose every other surface is neutral
   (#1F1F23 / #26262B / #1A1A1E). It read as a dropdown borrowed from another
   product, because it was: `style-rtl.css` paints `.select2-dropdown` and
   `.select2-container--open .select2-dropdown` with `var(--card)`, and that
   beats Django's own `var(--body-bg)` rule above.

   `--dropdown-bg`, so the value is in the token block with the rest of the
   palette rather than spelled as a literal at three selectors. Set for every
   container variant, not only `--admin-autocomplete`: this shell also carries
   jet's `--jet` select2 on some change forms (CLAUDE.md 46), and a dropdown
   should not depend on which widget happened to build it -- which is the same
   mistake §73 removed from the checkbox sets.

   A panel that opens OVER the page is not a field on it, so it is a step
   DARKER than `--input-bg` rather than lighter: it has the page behind it, not
   a card. The search box inside keeps the shell's one text-input treatment
   (§71), which puts a field on that panel exactly as it sits on a card.

   A native `<select>`'s own popup is drawn by the OS and cannot be styled by
   any of this -- which is why there is no native one left to style. Every
   dropdown on this shell is either select2 or `.ec-combo` (form-controls.js),
   and BOTH are painted by this one rule, so the two shapes the owner saw on one
   form cannot come back by one of them being restyled alone. */
.select2-dropdown,
.select2-container.select2-container--open .select2-dropdown,
.select2-container.select2-container--open .select2-dropdown--above,
.select2-container.select2-container--open .select2-dropdown--below,
.ec-combo-panel {
    background-color: var(--dropdown-bg);
    /* The whole border, not just its colour. `select2.min.css` arrives from the
       widget's own media -- i.e. in `extrastyle`, AFTER this file -- carrying
       `.select2-dropdown { border: 1px solid #aaa }`, and a `border` shorthand
       resets the colour: a lone `border-color` here was measured coming out as
       currentColor, so the panel wore a #E6E6E9 hairline, the text colour, in
       dark mode. Hence also the doubled class: one more class than anything in
       that file, so load order stops mattering. */
    border: 1px solid var(--line);
    /* Corner and shadow here too, for the same reason the ground is: these are
       what say "this floats above the page", and a square panel beside a
       rounded one is the two shapes again in miniature. */
    border-radius: 10px;
    box-shadow: 0 10px 30px var(--shadow);
}
/* The rows are transparent, so the panel's colour is the one that shows.
   Left opaque, the highlighted and already-chosen states are the only rows
   that would follow the token and every other row would keep edumin's navy.

   `.select2-results__options li`, not `.select2-results__option`: edumin paints
   the rows with the former, which is (0,1,1) and beats the class alone. Written
   at its own specificity rather than with `!important`, so the highlighted and
   selected states -- which are (0,2,0) and are meant to show -- still win. */
.select2-results__options li,
.select2-results__option { background-color: transparent; }
.select2-search--dropdown { background-color: transparent; }
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field,
.select2-container--default .select2-search--dropdown .select2-search__field {
    box-sizing: border-box; border: 1px solid var(--line); border-radius: 8px;
    padding: .5rem .7rem; font-family: inherit; font-size: .9rem;
    background-color: var(--input-bg); color: var(--ink); }

/* The history link was a filled brand-red pill -- the loudest control on a
   page whose primary action is 2,600px below it. Red is for saving. */
#content-main > .object-tools { justify-content: flex-end; }
.object-tools a.historylink, .object-tools a.historylink:link, .object-tools a.historylink:visited {
    background: var(--surface-2); color: var(--ink-muted); border: 1px solid var(--line); }
.object-tools a.historylink:hover { color: var(--ink); }

/* The change form's two whole-record actions, as square icons: تاريخ, and احذف
   after change-form.js lifts it out of the save row. Same size and same seam as
   the changelist's row icons, so "act on this record" looks the same wherever
   it appears. The label survives as `title`/`aria-label`, never only as a
   shape. */
.object-tools a.ec-tool-icon, .object-tools a.ec-tool-icon:link,
.object-tools a.ec-tool-icon:visited {
    display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; padding: 0; border-radius: 8px;
    background: var(--surface-2); border: 1px solid var(--line);
    color: var(--ink-muted); font-size: 1.05rem; }
.object-tools a.ec-tool-icon:hover { color: var(--ink); background: var(--surface); }
/* Destructive, and only on approach: red at rest beside a neutral twin reads as
   an error on the record rather than an action available to it. */
.object-tools a.deletelink.ec-tool-icon, .object-tools a.deletelink.ec-tool-icon:link,
.object-tools a.deletelink.ec-tool-icon:visited { color: #B3261E; }
.object-tools a.deletelink.ec-tool-icon:hover { background: #B3261E; border-color: #B3261E; color: #fff; }

/* Sticky at every width, not only on a phone: the save button was at the
   bottom of a very long form, and `save_on_top` is the alternative -- a
   per-ModelAdmin flag on 33 classes that fixes it by drawing three more
   buttons, so "which احفظ did I press" becomes a question. */
.submit-row { display: flex; gap: .5rem; justify-content: flex-start; align-items: center; flex-wrap: wrap;
    position: sticky; bottom: 0; z-index: 3;
    background: var(--surface); border-top: 1px solid var(--line); padding: .85rem 0; margin-top: 1.5rem; }
.submit-row input[type="submit"] { background: var(--surface-2); color: var(--ink-muted); border: 0; border-radius: 8px;
    padding: .55rem 1.1rem; font-family: inherit; cursor: pointer; }
.submit-row input[name="_save"] { background: var(--brand); color: #fff; }
/* Destructive actions do not get the same weight as the safe default. */
.submit-row a.deletelink, .submit-row a.deletelink:link, .submit-row a.deletelink:visited {
    background: transparent; color: #B3261E; border: 1px solid #B3261E; border-radius: 8px;
    padding: .5rem 1rem; text-decoration: none; }
.submit-row a.deletelink:hover { background: #B3261E; color: #fff; }

/* An inline row's remove control, at the scale of the row it removes.
   Same square destructive shape as the record's own احذف, one step smaller, so
   the two read as one kind of action rather than as a link and a button. Red
   only on approach, for the reason given above it. */
.inline-related a.ec-inline-remove, .inline-related a.ec-inline-remove:link,
.inline-related a.ec-inline-remove:visited {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0; border-radius: 7px;
    background: var(--surface-2); border: 1px solid var(--line);
    color: var(--ink-muted); font-size: .95rem; text-decoration: none; }
.inline-related a.ec-inline-remove:hover {
    background: #B3261E; border-color: #B3261E; color: #fff; }

.messagelist { list-style: none; padding: 0; margin: 0 0 1rem; display: grid; gap: .5rem; }

/* Django's messages, as a real alert.
 *
 * These rules were dead for as long as they have existed: MESSAGE_TAGS maps
 * every level onto Bootstrap utilities, so a message carried `bg-success` and
 * never `success`, and Edumin's `.bg-success{...!important}` painted a raw
 * #7ED321 slab under white text -- 1.87:1 against a 4.5 floor. The setting is
 * jet's and stays untouched; this shell's template emits the canonical level
 * name instead (message_tags.message_level), which is what these rules meet.
 *
 * Tinted surface with a dark ink rather than a saturated slab with white text:
 * it clears 6.5:1 at every level, and it stops a routine "saved" confirmation
 * shouting louder than anything else on the page. The level is carried by the
 * accent -- bar, icon -- which is what a colour-blind reader still separates by
 * SHAPE, since each level draws a different glyph.
 *
 * Semantic colour is NOT per-school. Same rule the app follows: the brand moves
 * per deployment (ADMIN_PALETTE), red-means-error does not.
 */
.messagelist li {
    --msg-tint: var(--surface-2);
    --msg-line: var(--line);
    --msg-accent: var(--ink-muted);
    --msg-ink: var(--ink);
    --msg-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4M12 8h.01'/%3E%3C/svg%3E");

    position: relative;
    display: flex;
    align-items: flex-start;
    gap: .65rem;
    padding: .8rem 1rem;
    border: 1px solid var(--msg-line);
    border-inline-start: 3px solid var(--msg-accent);
    border-radius: 10px;
    background: var(--msg-tint);
    color: var(--msg-ink);
    font-size: .9rem;
    line-height: 1.65;
    /* A long Arabic sentence has to wrap, and the icon must not shrink with it. */
    overflow-wrap: anywhere;
    animation: ec-msg-in 180ms ease-out both;
}

/* The glyph is a mask, not a font: it inherits --msg-accent, needs no icon
   set, and cannot render as a tofu box before Line Awesome has loaded. */
.messagelist li::before {
    content: "";
    flex: 0 0 auto;
    width: 20px; height: 20px;
    margin-top: .18rem;
    background-color: var(--msg-accent);
    -webkit-mask: var(--msg-icon) center / 20px 20px no-repeat;
    mask: var(--msg-icon) center / 20px 20px no-repeat;
}

.messagelist li.success {
    --msg-tint: #E9F4EE; --msg-line: #C4E2D1; --msg-accent: #2E6B45; --msg-ink: #1E5233;
    --msg-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 11.08V12a10 10 0 1 1-5.93-9.14'/%3E%3Cpath d='m9 11 3 3L22 4'/%3E%3C/svg%3E");
}
.messagelist li.error {
    --msg-tint: #FCEDEB; --msg-line: #F1C9C4; --msg-accent: #B3261E; --msg-ink: #8C1D18;
    --msg-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m15 9-6 6M9 9l6 6'/%3E%3C/svg%3E");
}
.messagelist li.warning {
    --msg-tint: #FDF3E3; --msg-line: #EDD6A8; --msg-accent: #8A5A00; --msg-ink: #7A4F00;
    --msg-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z'/%3E%3Cpath d='M12 9v4M12 17h.01'/%3E%3C/svg%3E");
}
.messagelist li.info {
    --msg-tint: #EAF1FA; --msg-line: #C3D6EE; --msg-accent: #1B4F8A; --msg-ink: #17457A;
}

/* Dismiss. Built by edumin/js/messages.js -- this shell's own copy, so /admin/
   keeps the handler it has always had -- and a message still reads correctly
   with no JavaScript: it simply cannot be closed, which is the safe half. */
.messagelist .ec-msg-close {
    position: relative;
    flex: 0 0 auto;
    margin-inline-start: auto;
    width: 30px; height: 30px;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0; border: 0; border-radius: 8px;
    background: transparent; color: var(--msg-accent);
    cursor: pointer; opacity: .6;
    transition: opacity 160ms ease, background-color 160ms ease;
}
/* 30px is the drawn button; the hit area is 44x44, which is the number that
   matters on a phone and the one a bare 30px button fails. */
.messagelist .ec-msg-close::after {
    content: ""; position: absolute; inset: -7px;
}
.messagelist .ec-msg-close svg { width: 16px; height: 16px; display: block; pointer-events: none; }
.messagelist .ec-msg-close:hover { opacity: 1; background: var(--msg-line); }
.messagelist .ec-msg-close:focus-visible {
    opacity: 1; outline: 2px solid var(--msg-accent); outline-offset: 2px;
}

.messagelist li.is-leaving {
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity 160ms ease, transform 160ms ease;
}

@keyframes ec-msg-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
    .messagelist li { animation: none; }
    .messagelist li.is-leaving { transition: none; }
    .messagelist .ec-msg-close { transition: none; }
}
.errorlist { color: #B3261E; font-size: .82rem; list-style: none; padding: 0; margin: .25rem 0 0; }
.inline-group { margin: 1rem 0; }
.inline-group h2 { font-size: .9rem; color: var(--ink-muted); }

/* --- a tabular inline is a small table, and had no treatment at all -------
 *
 * `.inline-related > h3` below dresses the STACKED shape; the tabular one fell
 * through to Edumin's default `th`, which is brand red -- a column heading that
 * reads as an error message, the same trap CLAUDE.md 66 records for the
 * offering table. It takes the changelist's header treatment so the two read as
 * one system.
 *
 * Django puts `.form-row` on the `<tr>` here, not on a div, so the row rules
 * above reach a table row: the padding does nothing to it and is reset rather
 * than left to look intentional.
 */
.inline-group .tabular table { width: 100%; max-width: 52rem; border-collapse: collapse; }
.inline-group .tabular thead th {
    background: var(--surface-2); color: var(--ink-muted);
    font-size: .8rem; font-weight: 600; text-align: start;
    padding: .5rem .7rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
.inline-group .tabular tbody td { padding: .55rem .7rem; vertical-align: middle;
    border-bottom: 1px solid var(--line); }
.inline-group .tabular tr.form-row { padding: 0; }
/* Django's own spacer column: it holds the row's hidden inputs and nothing a
   reader needs, so it takes no width rather than an empty heading's worth. */
.inline-group .tabular th.original,
.inline-group .tabular td.original { width: 0; padding: 0; }
.inline-group .tabular td .select2-container,
.inline-group .tabular td select,
.inline-group .tabular td input[type="text"] { min-width: 13rem; }
.inline-group .tabular td.delete { text-align: center; }
.inline-group .tabular td.delete input[type="checkbox"] { accent-color: var(--brand); }
.inline-group .add-row { padding: .6rem .7rem 0; }
.inline-group .add-row a { font-size: .82rem; }

/* The filter panel. Django renders #changelist-filter AFTER the results in the
   DOM, so without a layout it lands underneath the table as a wall of headings
   -- which is exactly how it looked before this rule. */
/* No side-by-side split any more: filters-as-dropdowns.js lifts the filter
   panel into a row above the table, so the results get the full width back. The
   panel is kept in the DOM, hidden, as a fallback if the script does not run. */
#changelist-filter[hidden] { display: none; }
#changelist-filter { background: var(--surface-2); border: 1px solid var(--line);
    border-radius: 10px; padding: 1rem; font-size: .85rem; margin-top: 1rem; }
#changelist-filter h2 { font-size: .8rem; text-transform: none; color: var(--ink-muted);
    margin: 0 0 .75rem; font-weight: 700; }
#changelist-filter h3 { font-size: .8rem; color: var(--ink); margin: .9rem 0 .35rem; font-weight: 700; }
#changelist-filter ul { list-style: none; padding: 0; margin: 0; }
#changelist-filter li { padding: .18rem 0; }
#changelist-filter a { color: var(--ink-muted); text-decoration: none; font-size: .82rem; }
#changelist-filter li.selected a { color: var(--brand); font-weight: 700; }
.results { overflow-x: auto; }


/* The filter row, above the table and beside the search. */
.ec-filter-row { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; }
/* Labels inline rather than stacked -- half the height, and four dropdowns all
   reading "الكل" say nothing about what they filter without them. */
.ec-filter { display: flex; align-items: center; gap: .4rem; }
.ec-filter > span { font-size: .78rem; color: var(--ink-dim); white-space: nowrap; }
.ec-filter select { border: 1px solid var(--line); border-radius: 8px; padding: .42rem 2rem .42rem .7rem;
    font-family: inherit; font-size: .85rem; color: var(--ink); background-color: var(--surface); min-width: 11rem; }
.ec-filter select:focus { outline: none; border-color: var(--brand); }

/* «من تاريخ / الى تاريخ». The two legs are grouped in one bordered shell so the
   pair reads as ONE control -- two loose date boxes on a row of dropdowns look
   like two unrelated filters, which is the opposite of what a range is. The
   inner inputs carry no border of their own; the group owns it, and the focus
   ring moves to the group when either leg has focus. */
.ec-filter-range { align-items: center; }
.ec-range-pair { display: flex; align-items: center; gap: 0;
    border: 1px solid var(--line); border-radius: 8px; background: var(--surface); overflow: hidden; }
.ec-range-pair:focus-within { border-color: var(--brand); }
.ec-range-leg { display: flex; align-items: center; gap: .3rem; padding: .18rem .5rem; }
/* The separator sits between the legs and nowhere else, so it cannot appear on
   a wrapped single leg. */
.ec-range-leg + .ec-range-leg { border-inline-start: 1px solid var(--line); }
.ec-range-leg > span { font-size: .72rem; color: var(--ink-dim); }
.ec-range-leg input[type="date"] { border: 0; background: transparent; color: var(--ink);
    font-family: inherit; font-size: .82rem; padding: .22rem 0; min-width: 6.6rem; }
.ec-range-leg input[type="date"]:focus { outline: none; }
/* The native picker glyph is drawn dark, so on the dark theme it is a black
   icon on a near-black field: present, clickable, invisible. Same fix as the
   change form's date inputs (CLAUDE.md 53). */
[data-theme-version="dark"] .ec-range-leg input[type="date"]::-webkit-calendar-picker-indicator {
    filter: invert(1) opacity(.7); }

/* ------------------------------------------------------------------- dark */
/* Edumin's own 344 dark rules cover its components; these cover Django's
   markup, which Edumin has never seen. */
[data-theme-version="dark"] #result_list a { color: var(--ink); }
[data-theme-version="dark"] #result_list a:hover { color: var(--brand); }
[data-theme-version="dark"] .messagelist li {
    --msg-tint: #26262B; --msg-line: #35353C; --msg-accent: #A8AAB3; --msg-ink: #C9CBD1; }
[data-theme-version="dark"] .messagelist li.success {
    --msg-tint: #15271C; --msg-line: #23422F; --msg-accent: #6FBF8C; --msg-ink: #8FD3A8; }
[data-theme-version="dark"] .messagelist li.error {
    --msg-tint: #2A1512; --msg-line: #44231F; --msg-accent: #E79890; --msg-ink: #F2B8B5; }
[data-theme-version="dark"] .messagelist li.warning {
    --msg-tint: #2A2011; --msg-line: #453721; --msg-accent: #DDB271; --msg-ink: #F3CC8A; }
[data-theme-version="dark"] .messagelist li.info {
    --msg-tint: #141F2C; --msg-line: #24374C; --msg-accent: #83AEE0; --msg-ink: #9EC5F0; }
[data-theme-version="dark"] .ui-switch { border-color: var(--line); color: var(--ink-muted); }
[data-theme-version="dark"] select option { background: var(--input-bg); color: var(--ink); }

/* «تفعيل إشعارات المتصفح».
 *
 * It had NO rule anywhere, so it rendered as the browser's default button -- a
 * raw grey box in a row of glyphs, which is why it read as pasted on. It takes
 * the theme toggle's treatment because it is the same kind of thing: a small,
 * stateful, once-in-a-while control that is not an action on the page.
 *
 * Refused is the one state that has to be visible rather than merely readable,
 * because it is the one the person cannot undo from here. */
.ec-web-push-btn { border: 1px solid var(--line); background: transparent; border-radius: 999px;
    width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer; color: var(--ink-muted); font-size: 17px; padding: 0; }
.ec-web-push-btn:hover { background: var(--surface-2); color: var(--ink); }
/* Dimmed, so «not available yet» and «press me» are not the same picture.
   The button ships disabled and is enabled only once the SDK has said this
   browser can still be asked -- see web-push.js. */
.ec-web-push-btn:disabled { cursor: default; opacity: .45; }
.ec-web-push-btn.is-blocked { color: var(--danger); border-color: var(--danger); }

/* The ask itself, on «الرسائل». A tinted strip rather than a card: it sits above
   the table it is about and must not read as the first row of it. `--brand` at
   6% is a ground, not a fill -- the only saturated thing here is the one button,
   which is the one thing to press. */
.ec-push-invite { display: flex; align-items: center; gap: .75rem; margin: 0 0 1rem;
    padding: .7rem .9rem; border: 1px solid var(--line); border-radius: 10px;
    background: color-mix(in srgb, var(--brand) 6%, var(--surface)); color: var(--ink); }
.ec-push-invite > .la { font-size: 1.3rem; color: var(--brand); flex: none; }
.ec-push-invite-say { margin: 0; flex: 1; font-size: .92rem; line-height: 1.5; }
.ec-push-invite-go { background: var(--brand); color: var(--brand-ink); border: 0; border-radius: 8px;
    padding: .45rem .95rem; font: inherit; font-weight: 700; cursor: pointer; flex: none; }
.ec-push-invite-go:disabled { opacity: .6; cursor: default; }
.ec-push-invite-hide { background: transparent; border: 0; color: var(--ink-dim); cursor: pointer;
    font-size: 1.05rem; line-height: 1; padding: .25rem; flex: none; }
.ec-push-invite-hide:hover { color: var(--ink); }
@media (max-width: 700px) {
    .ec-push-invite { flex-wrap: wrap; }
    .ec-push-invite-say { flex-basis: 100%; order: 2; }
}

.ec-theme-toggle { border: 1px solid var(--line); background: transparent; border-radius: 999px;
    width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer; color: var(--ink-muted); font-size: 17px; padding: 0; }
.ec-theme-toggle:hover { background: var(--surface-2); color: var(--ink); }
.ec-theme-toggle .la-sun { display: none; }
[data-theme-version="dark"] .ec-theme-toggle .la-sun { display: inline; }
[data-theme-version="dark"] .ec-theme-toggle .la-moon { display: none; }

/* ------------------------------------------------------------- responsive */
/* The sidebar is a fixed 275px column, which is right on a desk and unusable on
   a phone -- it covered most of the screen and the table lost its columns behind
   it. Below 1024px it becomes off-canvas behind a hamburger. Edumin's own
   responsive sidebar is driven by layout JS this shell deliberately does not
   load, so the behaviour is here instead. */
.ec-menu-toggle { display: none; background: transparent; border: 0; color: var(--ink-muted);
    font-size: 24px; cursor: pointer; padding: 0 4px; }
.ec-scrim { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 4; }
.ec-scrim.is-open { display: block; }

@media (max-width: 1024px) {
    .ec-menu-toggle { display: inline-flex; }

    /* The brand block travels WITH the drawer instead of sitting on the page.
       It is fixed at right:0 with a higher z-index than .header, so leaving it
       in place parked a solid slab exactly where the hamburger is -- the button
       was rendered, laid out and completely unreachable underneath it. Sliding
       it out with the drawer frees the header and gives the open drawer the
       heading it otherwise lacks. */
    .nav-header { width: 17.1875rem; right: 0; left: auto; padding: 0 16px; z-index: 8;
        transform: translateX(100%); transition: transform .22s ease-out; }
    [dir="ltr"] .nav-header { transform: translateX(-100%); }
    .nav-header.is-open { transform: translateX(0); }

    .header { right: 0; padding: 0 14px; gap: 10px; }
    .dashboard_bar { font-size: 1rem; }

    .deznav { transform: translateX(100%); transition: transform .22s ease-out; z-index: 7; }
    [dir="ltr"] .deznav { transform: translateX(-100%); }
    .deznav.is-open { transform: translateX(0); box-shadow: 0 0 24px rgba(0,0,0,.2); }

    .content-body { margin-right: 0; }
    .container-fluid { padding: 0 1rem; }
    .card-body { padding: 1rem; }

    /* `.results` keeps its own overflow as a last resort, but the table no
       longer carries a 640px floor: the columns fold (below) instead of
       forcing a width the window does not have. */
    .results { -webkit-overflow-scrolling: touch; }
    .ec-filter select { min-width: 0; width: 100%; }
    .ec-filter { flex: 1 1 9rem; }
    .ec-filter-range { flex: 1 1 100%; }
    .ec-range-pair { width: 100%; }
    .ec-range-leg { flex: 1 1 50%; }
    .ec-range-leg input[type="date"] { width: 100%; min-width: 0; }
    /* the fieldset grid drops to one column on its own, and .submit-row is
       sticky at every width now -- neither needs a breakpoint any more */
}

@media (max-width: 560px) {
    /* Drop the LABELS, not the controls. The previous rule kept only the last two
       items, which threw away the theme toggle and the interface switch -- the two
       things a person is most likely to reach for on a phone -- and kept the
       username, which is the one thing they already know. */
    .header-right { gap: 10px; }
    .header-right .ec-user { display: none; }
    .ui-switch-label { display: none; }
    .ui-switch { padding: .3rem .45rem; }
    .nav-header .brand-logo { font-size: .9rem; }
}

/* Nothing wider than the window.
 *
 * The page scrolled sideways on a phone because #result_list carries a 640px
 * minimum and the chain above it -- #changelist-form, .changelist-form-container,
 * #changelist -- had no width constraint, so the box grew instead of the table
 * scrolling inside .results. Constraining that chain is what makes the min-width
 * do its job rather than push the layout. */
#changelist, .changelist-form-container, #changelist-form, #content, #content-main {
    min-width: 0;
    max-width: 100%;
}

#toolbar form, #toolbar input[type="text"] { max-width: 100%; min-width: 0; }
#toolbar { flex-wrap: wrap; }
#toolbar #searchbar { flex: 1 1 12rem; }

/* Deliberately NO `html, body { overflow-x: hidden }` here.
   It was added as a safety net against an overflow that turned out not to exist:
   the page measured docScrollWidth === innerWidth all along, and the clipping in
   the screenshot was Chrome headless enforcing a 500px minimum window and then
   cropping the image to the requested 390. Worse, that rule silently disables
   `position: sticky` on descendants -- which is what keeps the table headings in
   view. Constraining the width chain above is the actual fix. */


/* ------------------------------------------------------- changelist header */
/* Two rows instead of five: what this list is and how to add to it, then
   everything that narrows it. Assembled by changelist-header.js from the nodes
   Django already renders. */
/* One line: the count at the start, the add button at the end. `.object-tools`
   is a <ul> and keeps Django's block layout unless told otherwise, which is what
   made the two stack. */
.ec-list-head { display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
/* The count no longer takes the slack -- search does. Left growing, the count
   pushed the search box and the add button apart to opposite edges of a 1440px
   window, which is the layout this row was merged to stop. */
.ec-list-head .ec-count { flex: 0 0 auto; font-size: .9rem; color: var(--ink-muted); font-weight: 600; }
.ec-list-head #toolbar { flex: 1 1 18rem; min-width: 0; }
.ec-list-head .object-tools { display: flex; flex: 0 0 auto; margin: 0; padding: 0; }

/* «مسح الفلاتر» is present only while the list is narrowed, so it is allowed to
   sit in the primary row without becoming permanent furniture. Quiet at rest --
   it undoes a choice, it does not destroy anything. */
.ec-clear-filters, .ec-clear-filters:link, .ec-clear-filters:visited {
    display: inline-flex; align-items: center; gap: .35rem; flex: 0 0 auto;
    padding: .45rem .75rem; border-radius: 8px; font-size: .85rem;
    background: var(--surface-2); border: 1px solid var(--line);
    color: var(--ink-muted); text-decoration: none; white-space: nowrap; }
.ec-clear-filters:hover { color: var(--ink); background: var(--surface); }

.ec-controls { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin-bottom: 1.1rem; }

/* The instalment totals, immediately above the rows they total.
   `auto-fit` rather than five fixed columns: at 1440px they are one row, at a
   phone width they stack, and no breakpoint decides which -- the same reasoning
   the change-form grid uses. */
.ec-stats { display: grid; gap: .75rem; margin-bottom: 1rem;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); }
.ec-stat { background: var(--surface-2); border: 1px solid var(--line); border-radius: 10px;
    padding: .7rem .85rem; display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.ec-stat-label { font-size: .78rem; color: var(--ink-muted); }
.ec-stat-value { font-size: 1.15rem; font-weight: 700; color: var(--ink);
    font-variant-numeric: tabular-nums; }
.ec-stat-note { font-size: .72rem; color: var(--ink-muted); }
/* Only «متأخر» is toned, and only on its own line: money that is late is the
   one figure here that is BAD rather than merely large. Colouring the others
   would spend the signal on numbers nobody needs to act on. */
.ec-stat-bad .ec-stat-value { color: #B3261E; }

/* The bulk-action bar and the row checkboxes go together: without a way to select
   rows the bar has nothing to act on, and with per-row edit/delete icons at the
   end of each row there is no reason to select first. Both go.

   NOTE this also removes bulk delete and "تصدير المستخدمين بصيغة PDF". The row
   icons replace the first; the second has no replacement yet. */
#changelist .actions { display: none; }
#result_list .action-checkbox-column,
#result_list td.action-checkbox { display: none; }

/* The paginator keeps the page links; the total has moved up beside the title. */
.paginator { border-top: 1px solid var(--line); margin-top: .5rem; }

@media (max-width: 1024px) {
    .ec-list-head { gap: .5rem; }
    .ec-controls { gap: .5rem; }
    #toolbar { flex: 1 1 100%; }
    .ec-filter { flex: 1 1 10rem; }
    .ec-filter select { width: 100%; }
}


/* The paginator keeps its page links; its copy of the total is removed by
   changelist-header.js, which has moved that text up beside the title. An
   otherwise empty paginator is hidden rather than left as a stray rule. */
.paginator:empty, .paginator.is-empty { display: none; border: 0; }


/* Controls the earlier rules missed.
 *
 * The action <select> kept a white background with dark text on a dark page --
 * it is emitted by Django's changelist, not by anything the bridge had named, so
 * it inherited the browser default. Cover every form control in one place rather
 * than chasing them one screenshot at a time. */
select, input, textarea, .vTextField {
    background-color: var(--input-bg);
    color: var(--ink);
    border-color: var(--line);
}
select option { background: var(--input-bg); color: var(--ink); }
.actions label, .actions .action-counter { color: var(--ink-muted); }
.actions { background: var(--surface-2); border-radius: 10px; padding: .6rem .8rem; align-items: center; gap: .6rem; }

/* Header controls, identical on every page. */
.ec-bell, .ec-logout { color: var(--ink-muted); font-size: 20px; display: inline-flex; text-decoration: none; }
.ec-bell:hover, .ec-logout:hover { color: var(--brand); }
.ec-user { color: var(--ink-muted); font-size: .85rem; }

/* jet's notification widget is a background-image sized for jet's own bar; it has
   no business in this shell, which draws its own bell. */
.notification-container, #user-tools { display: none; }


/* ---------------------------------------------- header and table, revised */

/* Search and filters share a row. The search was claiming a 20rem basis and
   pushing the filters onto their own line; it flexes down now instead. */
#toolbar { flex: 1 1 14rem; }
.ec-filter-row { flex: 2 1 auto; justify-content: flex-end; }
.ec-filter select { min-width: 6.5rem; }

/* Per-row actions: sized to the content so the data columns keep the width
 * (`width: 1%` is the table-layout idiom for that), and pinned to the end of
 * the row at every width.
 *
 * These icons ARE the row's affordance -- there is no checkbox and no action
 * bar -- so the one thing they must never be is the part that scrolled off.
 * The columns below fold rather than scroll, which normally makes this moot;
 * `position: sticky` is the guarantee for the case they do not (a long name,
 * a window between breakpoints). The seam is an inset shadow, not a border: a
 * sticky cell under `border-collapse` loses its own border once it detaches.
 */
#result_list th.ec-actions-col, #result_list td.ec-row-actions {
    width: 1%; white-space: nowrap; text-align: center;
    position: sticky; inset-inline-end: 0;
    background: var(--surface); box-shadow: inset 1px 0 0 var(--line); }
[dir="ltr"] #result_list th.ec-actions-col,
[dir="ltr"] #result_list td.ec-row-actions { box-shadow: inset -1px 0 0 var(--line); }
#result_list thead th.ec-actions-col { background: var(--surface-2); }
#result_list tbody tr:hover > td.ec-row-actions { background: var(--surface-2); }
.ec-row-actions a { display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; border-radius: 8px; color: var(--ink-dim); text-decoration: none; font-size: 17px; }
.ec-row-actions a:hover { background: var(--surface-2); color: var(--ink); }
.ec-row-actions a.ec-delete:hover { background: rgba(179, 38, 30, .12); color: #B3261E; }


/* --------------------------------------------------- columns that fold
 *
 * Each column is numbered by importance and written twice by
 * changelist-header.js: once as a cell, once as a labelled line under the
 * row's first cell. Exactly one of the two is displayed at any width, so a
 * column leaving the table is not a column leaving the page.
 *
 * The breakpoints are per COLUMN, not per device: a three-column list never
 * folds anything, an eight-column one starts folding on a laptop. That is why
 * they are written as a ladder rather than one phone rule.
 */
.ec-row-meta { margin-top: .35rem; display: flex; flex-wrap: wrap; gap: .15rem 1rem; }
.ec-meta-item { display: none; font-size: .82rem; font-weight: 400; color: var(--ink-dim); }
.ec-meta-item img { vertical-align: -2px; }
.ec-meta-label { color: var(--ink-muted); opacity: .75; }
.ec-meta-label::after { content: ': '; }

@media (max-width: 1280px) {
    #result_list .ec-col-8 { display: none; }
    .ec-meta-8 { display: inline; }
}
@media (max-width: 1120px) {
    #result_list .ec-col-7 { display: none; }
    .ec-meta-7 { display: inline; }
}
@media (max-width: 1000px) {
    #result_list .ec-col-6 { display: none; }
    .ec-meta-6 { display: inline; }
}
@media (max-width: 880px) {
    #result_list .ec-col-5 { display: none; }
    .ec-meta-5 { display: inline; }
}
@media (max-width: 760px) {
    #result_list .ec-col-4 { display: none; }
    .ec-meta-4 { display: inline; }
}
@media (max-width: 640px) {
    #result_list .ec-col-3 { display: none; }
    .ec-meta-3 { display: inline; }
}
@media (max-width: 520px) {
    /* One column and the actions. Everything else is under the name. */
    #result_list .ec-col-2 { display: none; }
    .ec-meta-2 { display: inline; }
    .ec-row-meta { flex-direction: column; gap: .1rem; }
    #result_list td, #result_list th { padding: .6rem .5rem; }
}

/* ------------------------------------------------------------------ *
 * The academic-year selector, and the banner that stops you editing
 * last year by mistake.
 *
 * The banner is deliberately the loudest thing on the page. The whole
 * hazard of a year selector is forgetting which year the header says and
 * editing history; a quiet notice would be worse than no selector.
 * ------------------------------------------------------------------ */

.ec-year form {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
}

.ec-year select {
  border: 1px solid var(--line, #e5e7eb);
  background: var(--surface, #fff);
  color: inherit;
  border-radius: 6px;
  padding: 4px 8px;
  font: inherit;
  max-width: 11rem;
}

/* A past year is stated in the header too, not only in the banner: the
   banner scrolls away and the header does not. */
.ec-year-past select {
  border-color: var(--warning, #b45309);
  color: var(--warning, #b45309);
  font-weight: 600;
}

.ec-year-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 16px;
  padding: 12px 16px;
  border-radius: 8px;
  border: 1px solid var(--warning, #b45309);
  background: color-mix(in srgb, var(--warning, #b45309) 10%, transparent);
  color: var(--warning-ink, #7c2d12);
}

/* Unlocked is a different state, not a louder one -- red for "you can now
   change history", amber for "you are only looking". */
.ec-year-banner.is-unlocked {
  border-color: var(--danger, #b3261e);
  background: color-mix(in srgb, var(--danger, #b3261e) 12%, transparent);
  color: var(--danger, #b3261e);
}

.ec-year-banner form { margin: 0 0 0 auto; }

.ec-year-banner button {
  border: 1px solid currentColor;
  background: transparent;
  color: inherit;
  border-radius: 6px;
  padding: 5px 12px;
  font: inherit;
  cursor: pointer;
}

.ec-year-banner button:hover { background: color-mix(in srgb, currentColor 12%, transparent); }

@media (max-width: 720px) {
  /* The dropdown is the control; its icon is decoration and goes first. */
  .ec-year .la-calendar { display: none; }
  .ec-year select { max-width: 8rem; }
}

/* ------------------------------------------------------------------ *
 * The student archive, and the promotion page.
 * ------------------------------------------------------------------ */

.ec-archive {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}

.ec-archive th,
.ec-archive td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--line, #e5e7eb);
  text-align: start;
}

.ec-archive th { font-weight: 600; opacity: .75; }

/* A gap year is stated, not skipped: a student who leaves and returns is one
   student, and a timeline that closes the hole claims they never left. */
/* The instalments panel on a student's page reuses `.ec-archive`'s table and
   the changelist's own status pills; only these two lines are its own. */
.ec-panel-summary { margin: 0 0 .6rem; font-size: .9rem; color: var(--ink-muted); }
.ec-panel-link { margin: .6rem 0 0; font-size: .85rem; }
/* A footnote UNDER a panel, where `.ec-panel-summary` sits above one. The
   «آخر اطّلاع» table needs it: an empty cell there means "the app has not told
   us", and a reader who is not told that will read it as "the parent never
   looked". Capped at 70ch, like the promotion page's intro -- a sentence
   running the width of a change form is a sentence nobody finishes. */
.ec-panel-note { margin: .6rem 0 0; max-width: 70ch; font-size: .85rem;
    line-height: 1.6; color: var(--ink-muted); }
.ec-archive td.ec-num { font-variant-numeric: tabular-nums; }
/* A device that has not told us what it is, said as a sentence rather than as a
   dash -- a dash in a «الجهاز» column reads as a fact about the phone. */
.ec-archive .ec-muted { color: var(--ink-muted); font-style: italic; }
.ec-archive td.ec-device-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: .8rem; color: var(--ink-muted); }

.ec-archive-gap td {
  color: var(--warning, #b45309);
  font-style: italic;
  background: color-mix(in srgb, var(--warning, #b45309) 7%, transparent);
}

.ec-promote-intro { max-width: 70ch; opacity: .8; }

.ec-promote-years {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 16px 0;
  padding: 12px 16px;
  border: 1px solid var(--line, #e5e7eb);
  border-radius: 8px;
}

.ec-promote-arrow { opacity: .5; }
.ec-promote-target { font-weight: 600; }

.ec-promote-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
.ec-promote-table th,
.ec-promote-table td { padding: 8px 10px; border-bottom: 1px solid var(--line, #e5e7eb); text-align: start; }

.ec-promote-actions { display: flex; gap: 10px; margin: 16px 0 24px; }

.ec-promote-actions button {
  border: 1px solid var(--line, #e5e7eb);
  background: var(--surface, #fff);
  color: inherit;
  border-radius: 6px;
  padding: 8px 16px;
  font: inherit;
  cursor: pointer;
}

/* The write is the loud one. Preview is the quiet default, and there is no way
   to reach this button without having been shown the counts first. */
.ec-promote-actions .ec-promote-run {
  border-color: var(--brand, #b3261e);
  background: var(--brand, #b3261e);
  color: var(--on-brand, #fff);
  font-weight: 600;
}

.ec-promote-plan {
  border: 1px solid var(--line, #e5e7eb);
  border-radius: 8px;
  padding: 12px 16px;
  margin-bottom: 12px;
}

.ec-promote-plan h3 { margin: 0 0 6px; font-size: 15px; }
.ec-promote-plan ul { margin: 6px 0 0; padding-inline-start: 20px; opacity: .85; }
.ec-promote-skip { color: var(--warning, #b45309); }
.ec-promote-empty { opacity: .7; }

/* The subject-merge screen. Shares the promotion page's action buttons and
   plan blocks -- both are "preview, then perform" and should not look like two
   different features. */

.ec-merge-hint {
  border: 1px solid var(--line, #e5e7eb);
  border-radius: 8px;
  padding: 12px 16px;
  margin: 16px 0;
}

.ec-merge-hint p { margin: 8px 0 0; opacity: .75; font-size: 13px; }

.ec-merge-chip {
  display: inline-block;
  border: 1px solid var(--line, #e5e7eb);
  border-radius: 999px;
  padding: 2px 10px;
  margin: 2px;
  font-size: 13px;
}

.ec-merge-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
.ec-merge-table th,
.ec-merge-table td { padding: 6px 10px; border-bottom: 1px solid var(--line, #e5e7eb); text-align: start; }
.ec-merge-table th:first-child,
.ec-merge-table th:nth-child(2),
.ec-merge-table td:first-child,
.ec-merge-table td:nth-child(2) { width: 4rem; text-align: center; }

/* ---------------------------------------------------------------------------
   The notification composer. New UI only -- /admin/ keeps its own form.

   Two columns on a desk, one on a phone, and the counter always last: it is the
   thing you read immediately before pressing an irreversible button, so it sits
   next to the button rather than at the top where it would be read first and
   forgotten.

   The brand colour is spent on exactly two things here -- the chosen segment
   and the send button -- because on a page whose one dangerous action is a send
   to every family at once, a red that also paints headings and chips stops
   meaning anything.
   ------------------------------------------------------------------------- */

.ec-compose-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
  gap: 1.5rem;
  align-items: start;
}

/* `height: auto` is the load-bearing half, and `align-self` alone did nothing.

   Edumin ships `.card { height: calc(100% - 0.938rem) }`. An explicit height
   beats `align-self: start`: the item stops stretching, then immediately
   re-fills the row because 100% resolves against the grid AREA, whose height is
   set by the tallest item. So the audience card kept the message card's height
   and its tree floated in a third of a screen of nothing.

   Diagnosed by outlining the boxes and re-rendering -- three wrong guesses came
   first, and none of them was visible in the markup. CLAUDE.md 51: probe, do
   not reason about what the CSS ought to be doing. */
.ec-compose-grid > .card { align-self: start; height: auto; }

/* The school's colour on the native controls instead of the browser's blue.
   It matters most in the tree: `indeterminate` is painted by the browser and
   cannot be restyled without replacing the control, so the only way the
   tri-state dash belongs to this palette is `accent-color`. */
.ec-compose input[type="checkbox"],
.ec-compose input[type="radio"] { accent-color: var(--brand); width: 1rem; height: 1rem; flex: none; margin: 0; }

/* --- the step header ---------------------------------------------------- */
/* Numbered because the order is a rule, not a suggestion: the send button is
   dead until step 3 has been run, and a page that does not say so reads as
   broken rather than as guarded. */
.ec-step { display: flex; align-items: flex-start; gap: .75rem; margin-bottom: 1.25rem; }
.ec-step-n {
  flex: none; width: 1.65rem; height: 1.65rem; border-radius: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); background: var(--surface-2);
  color: var(--ink-muted); font-size: .82rem; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.ec-step-text { min-width: 0; }
.ec-step h1 { margin: 0; font-size: 1.05rem; font-weight: 700; color: var(--ink); line-height: 1.35; }
.ec-step-hint { margin: .2rem 0 0; font-size: .8rem; color: var(--ink-dim); line-height: 1.6; }

/* --- fields ------------------------------------------------------------- */
.ec-field-label,
.ec-compose-field > span {
  display: block; font-size: .78rem; color: var(--ink-muted);
  margin-bottom: .35rem; font-weight: 600;
}
.ec-compose-field { display: block; margin-bottom: 1.1rem; }
.ec-compose-field input[type="text"],
.ec-compose-field textarea,
.ec-student-search { transition: border-color .15s ease, box-shadow .15s ease; }
/* Django's Textarea ships `rows=10`, and a row count beats `min-height` --
   which drew a quarter-screen of empty field for a medium whose own preview
   is clamped at two lines. An explicit `height` is what overrides it, and
   `resize` keeps the long case reachable. */
.ec-compose-field textarea { min-height: 8rem; height: 9.5rem; line-height: 1.7; resize: vertical; }
/* The soft glow stays as the RESTING focus look for the composer's own fields;
   the ring above is what a keyboard user gets. Both, not one instead of the
   other -- `:focus` fires on a click too, and `:focus-visible` is the one that
   has to be unmissable. */
.ec-compose-field input[type="text"]:focus,
.ec-compose-field textarea:focus,
.ec-student-search:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}

.ec-compose-error {
  color: var(--danger, #b3261e); margin-bottom: 1rem;
  border: 1px solid color-mix(in srgb, var(--danger, #b3261e) 40%, var(--line));
  border-radius: 10px; padding: .7rem .9rem; font-size: .88rem;
}
.ec-compose-empty {
  color: var(--ink-dim); font-size: .88rem; border: 1px dashed var(--line);
  border-radius: 10px; padding: 1.25rem; text-align: center; margin: 0;
}

/* --- what a parent sees ------------------------------------------------- */
/* Drawn as a lock-screen card, and the body is clamped to two lines, because
   the phone clamps it too: a length warning nobody reads is replaced by the
   truncation itself, visible while you type. The heading is a SAMPLE and says
   so below it -- it is computed per family at send time, so any single value
   shown here is wrong for some of the recipients. */
.ec-compose-preview { margin-top: 1.5rem; }
.ec-compose-preview-label {
  font-size: .72rem; color: var(--ink-dim); margin-bottom: .5rem;
  font-weight: 600; letter-spacing: .02em;
}
.ec-compose-phone {
  border: 1px solid var(--line); border-radius: 14px; padding: .75rem .85rem;
  background: var(--surface-2); max-width: 23rem;
}
.ec-phone-top { display: flex; align-items: center; gap: .45rem; margin-bottom: .55rem; }
.ec-phone-app {
  width: 20px; height: 20px; border-radius: 6px; flex: none;
  background: var(--brand); color: var(--brand-ink, #fff);
  display: inline-flex; align-items: center; justify-content: center; font-size: 11px;
}
.ec-phone-name { font-size: .72rem; color: var(--ink-muted); font-weight: 600; }
.ec-phone-time { margin-inline-start: auto; font-size: .7rem; color: var(--ink-dim); }
.ec-compose-phone-heading { font-size: .72rem; color: var(--ink-dim); }
.ec-compose-phone-title {
  font-weight: 700; font-size: .92rem; margin-top: .2rem; line-height: 1.6;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ec-compose-phone-body {
  font-size: .85rem; color: var(--ink-muted); margin-top: .25rem; line-height: 1.6;
  white-space: pre-wrap; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.ec-compose-phone .is-placeholder { color: var(--ink-dim); font-weight: 400; }
.ec-compose-preview-note { margin: .5rem 0 0; font-size: .72rem; color: var(--ink-dim); }

/* --- the segmented control ---------------------------------------------- */
/* A radio pair with two options is a switch, so it is drawn as one. The native
   input is kept -- it is the accessible control and the thing the form posts --
   and covered by its own label, with the SPAN carrying the paint.

   The state is carried by `input:checked ~ span`, not by `:has()` on the label.
   Both work; the sibling selector is the older, wider-supported one and needs
   no `:has()` at all, so the switch keeps working on a browser that has none.

   A warning about verifying this, paid for once: `getComputedStyle` under
   Chrome's `--dump-dom` reported the OLD background after a real click, for
   `:has()` and for `~` alike, and looked exactly like a broken selector. It is
   the harness -- that mode does not run the rendering step, so a pseudo-class
   change never settles into the computed style. A `--screenshot` of the same
   interaction shows both spellings repainting correctly. CLAUDE.md 51: probe,
   but when the probe and the render disagree, the render is the answer. */
.ec-seg {
  display: inline-flex; gap: 3px; padding: 3px; margin-bottom: 1rem;
  border: 1px solid var(--line); border-radius: 11px; background: var(--surface-2);
}
.ec-seg-option { position: relative; display: inline-flex; cursor: pointer; }
.ec-seg-option input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; margin: 0; cursor: pointer;
}
.ec-seg-option span {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 34px; padding: 0 1.1rem; border-radius: 8px;
  font-size: .85rem; font-weight: 600; color: var(--ink-muted);
  transition: background .15s ease, color .15s ease;
}
.ec-seg-option:hover span { color: var(--ink); }
.ec-seg-option input:checked ~ span { background: var(--brand); color: var(--brand-ink, #fff); }
/* «غير محدد» is the ABSENCE of an answer, not one of them. Painted with the
   brand it reads as a choice somebody made -- which is exactly what the news
   attachment row must not say, since «nothing selected» is its resting state
   and the whole point is that neither control is offered yet. Marked, so the
   control still says where it stands, but quietly. `segment()` gives the blank
   option an empty value, so no extra attribute is needed. */
.ec-seg-option input[value=""]:checked ~ span {
    background: var(--surface-2); color: var(--ink-muted); box-shadow: inset 0 0 0 1px var(--line);
}
.ec-seg-option input:focus-visible ~ span { outline: 2px solid var(--brand); outline-offset: 2px; }

/* --- the tree ----------------------------------------------------------- */
/* Indentation is logical (`padding-inline-start` and a `border-inline-start`
   rail), so it reads the right way round in both directions without a second
   rule. Grades collapse because a school with nine of them puts thirty rows in
   a scroll box, and the thing you are looking for is a grade, not a row. */
.ec-tree-wrap {
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  background: var(--surface);
}
.ec-tree-tools {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  padding: .5rem .6rem; background: var(--surface-2); border-bottom: 1px solid var(--line);
}
.ec-tree-summary {
  flex: 1 1 8rem; min-width: 0; font-size: .78rem; color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.ec-tree-tool {
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-muted);
  border-radius: 7px; padding: .3rem .7rem; font: inherit; font-size: .75rem;
  cursor: pointer; transition: background .15s ease, color .15s ease;
}
.ec-tree-tool:hover { background: var(--field); color: var(--ink); }
.ec-tree-tool:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.ec-tree { max-height: 24rem; overflow: auto; overscroll-behavior: contain; padding: .35rem; }
.ec-tree-row {
  display: flex; align-items: center; gap: .6rem;
  min-height: 38px; padding: .25rem .55rem; border-radius: 8px; cursor: pointer;
  transition: background .12s ease;
}
.ec-tree-row:hover { background: var(--surface-2); }
.ec-tree-row input:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.ec-tree-name { flex: 1 1 auto; min-width: 0; overflow: hidden; line-height: 1.6;
    text-overflow: ellipsis; white-space: nowrap; }
.ec-tree-count {
  flex: none; font-size: .7rem; color: var(--ink-dim); font-variant-numeric: tabular-nums;
  border: 1px solid var(--line); border-radius: 999px; padding: .08rem .5rem;
}
.ec-tree-level-1 { font-weight: 700; font-size: .95rem; }
.ec-tree-level-2 { font-weight: 600; font-size: .9rem; }
.ec-tree-level-3 { color: var(--ink-muted); font-size: .87rem; }

/* The rail. `border-inline-start` puts it on the reading edge in either
   direction, which is what the old `padding-inline-start: 3rem` was only
   approximating. */
.ec-tree-grade {
  margin-inline-start: 1.1rem;
  padding-inline-start: .45rem;
  border-inline-start: 1px solid var(--line);
}
.ec-tree-children { padding-inline-start: 1.6rem; }
.ec-tree-grade[data-collapsed] .ec-tree-children { display: none; }

.ec-tree-rowline { display: flex; align-items: center; gap: .1rem; }
.ec-tree-rowline .ec-tree-row { flex: 1 1 auto; min-width: 0; }
.ec-tree-toggle {
  flex: none; width: 26px; height: 26px; padding: 0; border: 0; border-radius: 7px;
  background: none; color: var(--ink-dim); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .12s ease, color .12s ease;
}
.ec-tree-toggle:hover { background: var(--surface-2); color: var(--ink); }
.ec-tree-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.ec-tree-toggle i { display: block; font-size: 13px; transition: transform .15s ease; }
/* Collapsed points toward the start of the line, which is the right edge here
   and the left edge under `dir="ltr"`. */
.ec-tree-toggle[aria-expanded="false"] i { transform: rotate(90deg); }
[dir="ltr"] .ec-tree-toggle[aria-expanded="false"] i { transform: rotate(-90deg); }

/* --- named students ----------------------------------------------------- */
.ec-student-results, .ec-student-chosen { list-style: none; margin: .6rem 0 0; padding: 0; }
.ec-student-results {
  border: 1px solid var(--line); border-radius: 10px; padding: .25rem;
  max-height: 16rem; overflow: auto; background: var(--surface);
}
.ec-student-results li button {
  display: block; width: 100%; text-align: start; border: 0; background: none;
  color: inherit; font: inherit; font-size: .88rem; padding: .45rem .6rem;
  border-radius: 7px; cursor: pointer;
}
.ec-student-results li button {
  display: flex; align-items: baseline; gap: .5rem; justify-content: space-between;
}
/* The placement, on the same line as the name and quieter than it: it is what
 * tells two «محمد» apart, not what you are reading the list for. */
.ec-student-hint { color: var(--ink-dim); font-size: .78rem; white-space: nowrap; }
.ec-student-none { color: var(--ink-dim); font-size: .82rem; padding: .45rem .6rem; }
.ec-student-results li button:hover { background: var(--surface-2); }
.ec-student-results li button:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.ec-student-chosen { display: flex; flex-wrap: wrap; gap: .4rem; }
.ec-student-chosen li {
  display: inline-flex; align-items: center; gap: .35rem;
  border: 1px solid var(--line); border-radius: 999px;
  padding: .25rem .35rem .25rem .75rem; font-size: .82rem; background: var(--surface-2);
}
.ec-student-chosen li button {
  border: 0; background: none; color: var(--ink-dim); cursor: pointer; font: inherit;
  width: 20px; height: 20px; border-radius: 999px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
.ec-student-chosen li button:hover { background: var(--line); color: var(--ink); }

/* --- step 3: the numbers, and the button -------------------------------- */
.ec-send-body {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(15rem, auto);
  gap: 1.5rem; align-items: start;
}
@media (max-width: 62rem) { .ec-send-body { grid-template-columns: minmax(0, 1fr); } }

/* Capped rather than stretched to the column. Three tiles at `1fr` each
   became three wide slabs holding one em-dash, which reads as an empty state
   rather than as a figure waiting to be filled. */
.ec-count-numbers {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .6rem; max-width: 25rem;
}
.ec-count {
  border: 1px solid var(--line); border-radius: 12px;
  padding: .7rem .9rem; background: var(--surface-2);
}
/* Devices is the figure that decides whether a phone rings at all -- but it is
   NOT given the accent. An empty tile with a red edge reads as a validation
   error, and the one red thing on this page has to stay the send button. */
.ec-count b {
  display: block; font-size: 1.75rem; line-height: 1.15; font-weight: 700;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.ec-count span { font-size: .74rem; color: var(--ink-dim); }
.ec-count-note { margin: .9rem 0 0; color: var(--ink-muted); font-size: .85rem; line-height: 1.7; }
/* "This will send, and no phone will ring." Amber rather than red: the send is
   allowed and the numbers are correct -- what is unusual is the outcome, not
   the request. Its own token, because there is no warning colour in the shared
   palette and one colour that fails in dark mode is worse than none. */
.ec-compose { --ec-warn: #8A5300; --ec-warn-bg: #FBF3DB; }
[data-theme-version="dark"] .ec-compose { --ec-warn: #E0A458; --ec-warn-bg: #2B2318; }
.ec-count-note.is-warning {
  color: var(--ec-warn); background: var(--ec-warn-bg);
  border: 1px solid color-mix(in srgb, var(--ec-warn) 35%, transparent);
  border-radius: 10px; padding: .6rem .8rem;
}

/* What the server says it will actually address, rolled up -- the safety read.
   Chips rather than bullets: it is a set, and a short set reads faster in a row
   than in a column. */
.ec-count-scopes {
  list-style: none; margin: .75rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .4rem;
}
.ec-count-scopes:empty { display: none; }
.ec-count-scopes li {
  border: 1px solid var(--line); border-radius: 999px; padding: .22rem .7rem;
  font-size: .78rem; color: var(--ink-muted); background: var(--surface-2);
}

.ec-send-controls {
  border-inline-start: 1px solid var(--line); padding-inline-start: 1.5rem;
}
@media (max-width: 62rem) {
  .ec-send-controls {
    border-inline-start: 0; border-top: 1px solid var(--line);
    padding-inline-start: 0; padding-top: 1.25rem;
  }
}

/* Scheduling: the control only appears when «في وقت لاحق» is chosen, so an
   immediate send is not asked to dismiss a date picker it will never use. */
.ec-compose-schedule { margin-bottom: 1.1rem; }
.ec-compose-schedule .ec-seg { margin-bottom: 0; }
.ec-compose-when { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }
.ec-compose-when input {
  border: 1px solid var(--line); border-radius: 8px; padding: .5rem .65rem;
  background: var(--input-bg); color: var(--ink); font: inherit; font-size: .88rem;
}
.ec-compose-when input:focus {
  outline: none; border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}

.ec-compose-actions { display: flex; gap: .6rem; flex-wrap: wrap; }
.ec-btn, .ec-compose-actions button {
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  border-radius: 9px; padding: .55rem 1.4rem; min-height: 40px;
  font: inherit; font-size: .9rem; font-weight: 600; cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease, transform .1s ease;
}
.ec-btn:hover { background: var(--surface-2); }
.ec-btn:active { transform: translateY(1px); }
.ec-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.ec-compose-actions .ec-compose-send {
  border-color: var(--brand); background: var(--brand); color: var(--brand-ink, #fff);
}
.ec-compose-actions .ec-compose-send:hover:not(:disabled) {
  background: color-mix(in srgb, var(--brand) 88%, #000);
  border-color: color-mix(in srgb, var(--brand) 88%, #000);
}
/* Disabled is a STATE, not a dead end: the note beside it says what to press
   first, and `aria-describedby` ties the two together for a reader that cannot
   see they are next to each other. */
.ec-compose-actions .ec-compose-send:disabled {
  background: var(--surface-2); border-color: var(--line); color: var(--ink-dim);
  cursor: not-allowed; font-weight: 500; transform: none;
}

.ec-scope-list { margin: 0; padding-inline-start: 1.2rem; }

/* The delivery report on a sent notification. */
.ec-delivery { border-collapse: collapse; }
.ec-delivery th { text-align: start; padding: .2rem 1rem .2rem 0; color: var(--ink-dim); font-weight: 600; white-space: nowrap; }
.ec-delivery td { padding: .2rem 0; }
.ec-delivery-refresh { display: flex; align-items: center; gap: .75rem; margin: 1rem 0 2rem; }
.ec-delivery-refresh button {
  border: 1px solid var(--line); background: var(--surface); color: inherit;
  border-radius: 8px; padding: .45rem 1rem; font: inherit; cursor: pointer;
}
.ec-delivery-when { font-size: .8rem; color: var(--ink-dim); }
.ec-delivery-refresh form { display: contents; }
.ec-send-now {
  border-color: var(--brand) !important; background: var(--brand) !important;
  color: var(--brand-ink, #fff) !important; font-weight: 700;
}

@media (prefers-reduced-motion: reduce) {
  .ec-compose *, .ec-compose { transition: none !important; }
  .ec-btn:active { transform: none; }
}


/* ------------------------------------------------------- the delete dialog
   A question, not an inventory. Django's delete page lists every cascaded
   object -- 240 <li> for one classroom -- which is the right information for a
   HARD delete and the wrong information here twice over: this delete is soft,
   and nobody reads 240 lines before pressing a button they have already
   decided to press. The page is still there behind this and is the
   no-JavaScript path; see delete-confirm.js. */
dialog.ec-confirm {
    border: 1px solid var(--line);
    border-radius: 14px;
    background: var(--surface);
    color: var(--ink);
    padding: 0;
    width: min(24rem, calc(100vw - 2rem));
    box-shadow: 0 18px 48px var(--shadow);
}
dialog.ec-confirm::backdrop { background: rgba(15, 15, 18, .48); }
.ec-confirm-body { padding: 1.5rem 1.4rem 1.1rem; text-align: center; font-family: inherit; }
/* Round, tinted, and NOT brand red: the brand is the primary action on every
   other screen in this shell, and a red circle at the top of a dialog whose
   whole question is "are you sure" would be the loudest thing on it. Danger
   red belongs on the button you press, once. */
.ec-confirm-icon {
    width: 3rem; height: 3rem; margin: 0 auto .9rem;
    border-radius: 50%;
    display: grid; place-items: center;
    background: #FCEDEB; color: #B3261E; font-size: 1.4rem;
}
[data-theme-version="dark"] .ec-confirm-icon { background: #2A1512; color: #E79890; }
.ec-confirm h2 { margin: 0 0 .45rem; font-size: 1.05rem; font-weight: 700; color: var(--ink); }
/* The record's own name, quoted by weight rather than by punctuation -- an
   Arabic name inside «» beside a question mark is three kinds of mark in one
   line. */
.ec-confirm-what { margin: 0; font-size: .95rem; font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.ec-confirm-extra { margin: .5rem 0 0; font-size: .84rem; color: var(--ink-muted); }
/* The promise, and the reason this dialog can be short at all. */
.ec-confirm-safe {
    margin: .9rem 0 0; padding: .5rem .7rem;
    display: flex; align-items: center; justify-content: center; gap: .4rem;
    border-radius: 9px; background: var(--surface-2);
    font-size: .8rem; color: var(--ink-muted);
}
.ec-confirm-actions { display: flex; gap: .5rem; margin-top: 1.2rem; }
.ec-confirm-actions .ec-btn { flex: 1 1 50%; padding: .55rem .8rem; border-radius: 9px;
    font-family: inherit; font-size: .88rem; font-weight: 600; cursor: pointer; border: 1px solid transparent; }
.ec-btn-quiet { background: var(--surface); border-color: var(--line); color: var(--ink-muted); }
.ec-btn-quiet:hover { background: var(--surface-2); color: var(--ink); }
.ec-btn-danger { background: #B3261E; color: #fff; }
.ec-btn-danger:hover { background: #8C1D18; }
.ec-btn-danger[disabled] { opacity: .6; cursor: default; }
@media (prefers-reduced-motion: no-preference) {
    dialog.ec-confirm[open] { animation: ec-confirm-in 140ms ease-out; }
    @keyframes ec-confirm-in { from { opacity: 0; transform: translateY(-6px) scale(.98); } }
}


/* ------------------------------------------------------------- status pills
   Colour by MEANING only -- see backend/OneEdu/admin_badges.py for which value
   wears which tone and why. A tinted ground with its own darker ink, never a
   saturated block: these repeat on every row of a 500-row table, and a solid
   fill at that density turns the table into the badge. */
.ec-badge {
    display: inline-block;
    padding: .16rem .55rem;
    border-radius: 999px;
    font-size: .78rem;
    font-weight: 600;
    line-height: 1.6;
    white-space: nowrap;
    border: 1px solid transparent;
}
/* The tint is what separates the pill from the cell; the border is what keeps
   it legible for anyone who cannot separate the two hues, so neither channel
   is carrying the meaning alone. */
.ec-badge-bad     { background: #FCEDEB; color: #8C1D18; border-color: #F1C9C4; }
.ec-badge-warn    { background: #FDF3E3; color: #7A4F00; border-color: #EDD6A8; }
.ec-badge-ok      { background: #EDF4EF; color: #2E6B45; border-color: #CFE3D7; }
.ec-badge-neutral { background: var(--surface-2); color: var(--ink-muted); border-color: var(--line); }

[data-theme-version="dark"] .ec-badge-bad     { background: #2A1512; color: #F2B8B5; border-color: #44231F; }
[data-theme-version="dark"] .ec-badge-warn    { background: #2A2011; color: #F3CC8A; border-color: #453721; }
[data-theme-version="dark"] .ec-badge-ok      { background: #15271C; color: #8FD3A8; border-color: #23422F; }
[data-theme-version="dark"] .ec-badge-neutral { background: var(--surface-2); color: var(--ink-muted); border-color: var(--line); }


/* The composer's "choose a type first" prompt. Quiet, because it is guidance
   rather than an error: nothing has gone wrong, the form simply has not been
   answered yet -- and painting it as a warning would teach staff to read the
   first state of every page as a problem. */
.ec-compose-choose {
    margin: 1rem 0 0;
    padding: .9rem 1rem;
    display: flex; align-items: center; gap: .55rem;
    border: 1px dashed var(--line);
    border-radius: 10px;
    background: var(--surface-2);
    color: var(--ink-muted);
    font-size: .86rem;
    line-height: 1.7;
}
.ec-compose-choose i { color: var(--ink-dim); font-size: 1.05rem; }


/* ===================================================== the control policy ===
   The right control for the meaning. See form-controls.js for which control
   each field gets and why; this only dresses them. Every one of these sits
   BESIDE its original <select>, which stays in the DOM and stays what the form
   posts -- so `ec-visually-hidden` has to hide it without `display:none`,
   which would take it out of the form's own layout calculations and, on some
   browsers, out of constraint validation. */
.ec-visually-hidden {
    position: absolute !important;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%);
    overflow: hidden; white-space: nowrap;
}

/* --- a flag is a switch, not a checkbox ---------------------------------
   الاعدادات is a page of settings, and a column of ticked boxes reads as a
   form that has been filled in rather than as a set of things that are ON.
   The switch says its state by POSITION as well as by colour, which is what
   keeps it legible when the colour is not available. */
/* Sized at ~150% of what it was (owner, 2026-09-03). The old 2.1x1.2rem track
   was smaller than the 40px row every other control on this shell gives its
   primary target, and a switch IS the target -- there is nothing else on the
   row to press. The knob and both offsets scale with it; `checked` puts the
   knob the same distance from the far edge as `.2rem` puts it from the near
   one, so the two states are symmetric and a change to one number is visible
   as an asymmetry rather than as nothing. */
.checkbox-row input[type="checkbox"] {
    appearance: none; -webkit-appearance: none;
    position: relative;
    inline-size: 3.1rem; block-size: 1.75rem;
    border-radius: 999px;
    /* The border matches the track rather than `--line`: with the track now a
       real grey, a paler hairline around it drew a second, lighter edge and
       made the pill read as two shapes. */
    border: 1px solid var(--switch-off);
    background: var(--switch-off);
    cursor: pointer;
    transition: background .16s ease, border-color .16s ease;
    flex: 0 0 auto;
}
.checkbox-row input[type="checkbox"]::after {
    content: "";
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: .2rem;
    inline-size: 1.35rem; block-size: 1.35rem;
    border-radius: 50%;
    background: var(--switch-knob);
    box-shadow: 0 1px 2px rgba(0,0,0,.25);
    transform: translateY(-50%);
    transition: inset-inline-start .16s ease;
}
.checkbox-row input[type="checkbox"]:checked { background: var(--brand); border-color: var(--brand); }
.checkbox-row input[type="checkbox"]:checked::after { inset-inline-start: calc(100% - 1.55rem); }
.checkbox-row input[type="checkbox"]:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.checkbox-row input[type="checkbox"]:disabled { opacity: .5; cursor: default; }
@media (prefers-reduced-motion: reduce) {
    .checkbox-row input[type="checkbox"],
    .checkbox-row input[type="checkbox"]::after { transition: none; }
}

/* --- 2-4 meanings are segments ------------------------------------------
   The composer's control, reused verbatim -- one vocabulary across the admin,
   so the thing that looks the same behaves the same. `ec-seg-field` only says
   "this one is inside a form row", which changes the margins and nothing else. */
/* `align-self`, because `.form-row > div` is a column flex container and
   stretches its children: without it the track ran the full width of the field
   with the four segments huddled at one end, which reads as a control that
   failed to fill rather than as a set of four choices. */
.ec-seg-field { margin-bottom: 0; flex-wrap: wrap; max-width: 100%; align-self: flex-start; }
.ec-seg-field .ec-seg-option span { padding: 0 .9rem; min-height: 32px; font-weight: 600; }

/* --- a set is checkboxes -------------------------------------------------
   Independent yes/no answers, laid out so the whole set is visible at once:
   the question "which grades hide marks" is answered by reading all of them,
   which a ctrl-click list of the same options cannot do. */
.ec-checkset {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    gap: .3rem .8rem;
    padding: .6rem .7rem;
    border: 1px solid var(--line); border-radius: 10px;
    background: var(--surface);
}
.ec-checkset-item { display: flex; align-items: center; gap: .45rem; cursor: pointer;
    /* A 40px row, not a 20px one: this is the primary target on the field, and
       a checkbox label that is only as tall as its box is a control you aim at
       rather than press. */
    padding: .45rem .35rem; min-block-size: 2.35rem; font-size: .86rem; color: var(--ink);
    border-radius: 7px; }
.ec-checkset-item input { inline-size: 1.05rem; block-size: 1.05rem; margin: 0; accent-color: var(--brand); flex: 0 0 auto; }
.ec-checkset-item:hover { color: var(--brand); background: var(--surface-2); }
.ec-checkset-item:focus-within { outline: 2px solid var(--brand); outline-offset: -2px; }

/* «الكل» is the statement that every row below is chosen, not a fifth row
   beside them -- so it takes the full width, comes first, and is separated by
   the same hairline the rest of this admin uses for "everything under here". */
.ec-checkset-all { grid-column: 1 / -1; font-weight: 600;
    border-block-end: 1px solid var(--line); border-radius: 7px 7px 0 0; margin-block-end: .15rem; }

.ec-checkfield { display: flex; flex-direction: column; }
/* A long set is a panel, not a field. 288 permissions unbounded is a page you
   scroll past rather than a control you use; capped, it is a thing with edges.
   Below that it is NOT capped, and the whole set shows: it is laid out in as
   many columns as the width allows, so 27 rows are six lines rather than 27,
   and a scrollbar over six lines hides a set for no reason. Owner, 2026-09-03.
   The threshold is on the JS side (`TALL_THRESHOLD`) because only it knows how
   many options there are; CSS cannot count them. */
.ec-checkset-tall { max-block-size: 26rem; overflow-y: auto; }
.ec-checkset-filter { margin-block-end: .35rem; max-inline-size: 22rem; }
.ec-checkset-filter[hidden] { display: none; }
.ec-checkset-item[hidden] { display: none; }
.ec-checkfield .ec-multi-tools { margin-bottom: .3rem; }

/* --- ONE dropdown shape ---------------------------------------------------
   There were two, on one form: الصف is an `autocomplete_field`, so Django gives
   it select2 -- a panel on this shell's ground, with a search box and a
   highlighted row -- while المادة sits behind the grade cascade, cannot be an
   autocomplete (CLAUDE.md 64), and was therefore left as a bare <select>, whose
   popup is drawn by the OPERATING SYSTEM in the OS's own colours. Which of the
   two shapes a field got depended on nothing a user can see (owner,
   2026-09-03) -- the same finding as §73's «الكل», one page further in.

   The panel itself is NOT painted here: `.ec-combo-panel` is named in the
   "one ground for every dropdown panel" rule above, beside `.select2-dropdown`,
   so the two cannot drift back apart. What is here is the closed control, the
   rows, and the geometry.

   `.ec-combo` is `position: relative` because the hidden <select> inside it is
   `position: absolute` (see `.ec-visually-hidden`): without a containing block
   it would be positioned against the page and could drag a scrollbar out of a
   card. The PANEL is not positioned against it -- it is a child of <body>. */
.ec-combo { position: relative; display: block; max-width: 32rem; }
.ec-combo-control {
    display: flex; align-items: center; gap: .5rem; width: 100%; box-sizing: border-box;
    border: 1px solid var(--line); border-radius: 8px;
    padding: .5rem .7rem; font-family: inherit; font-size: .9rem; text-align: start;
    line-height: 1.6;
    background-color: var(--input-bg); color: var(--ink); cursor: pointer; }
.ec-combo-control:hover { border-color: var(--line-strong); }
.ec-combo.is-open .ec-combo-control { border-color: var(--brand); }
.ec-combo-control:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.ec-combo-control[disabled] { cursor: default; opacity: .6; }
/* The value takes the room and the arrow keeps its own, so a long subject
   ellipsizes instead of pushing the caret off the end of the control. */
/* `overflow: hidden` is what clips a descender, so every element carrying it
   states its own line-height rather than inheriting a tighter one. */
.ec-combo-value { flex: 1 1 auto; min-width: 0; overflow: hidden;
    line-height: 1.6; text-overflow: ellipsis; white-space: nowrap; }
/* «غير محدد» is the ABSENCE of an answer, not one of them (CLAUDE.md 72), and
   an empty timetable cell must not read at the weight of a lesson. */
.ec-combo.is-empty .ec-combo-value { color: var(--ink-dim); }
.ec-combo-arrow { flex: 0 0 auto; width: 0; height: 0; border-inline: 4px solid transparent;
    border-top: 5px solid var(--ink-dim); transition: transform .12s ease; }
.ec-combo.is-open .ec-combo-arrow { transform: rotate(180deg); }

/* The panel. Fixed, and a child of <body>: the timetable is 48 of these inside
   `.ec-grid-wrap { overflow-x: auto }`, and a panel positioned inside a scroll
   container is CLIPPED by it. `left`/`top` are written by the script from the
   control's own rect, so the physical properties here are deliberate. */
.ec-combo-panel {
    position: fixed; z-index: 1200; display: flex; flex-direction: column;
    box-sizing: border-box; padding: .3rem; overflow: hidden; }
.ec-combo-panel[hidden] { display: none; }
.ec-combo-panel .ec-combo-search { margin: .1rem .1rem .3rem; width: calc(100% - .2rem); }
.ec-combo-panel .ec-combo-search[hidden] { display: none; }
.ec-combo-list { flex: 1 1 auto; margin: 0; padding: 0; list-style: none; overflow-y: auto; }
/* The same cap the checkbox set uses, so a long list is a panel with edges
   in both shapes rather than two different heights. */
.ec-combo-list-tall { max-height: 19rem; }
.ec-combo-option { padding: .45rem .6rem; border-radius: 7px; font-size: .88rem;
    line-height: 1.6;
    color: var(--ink); cursor: pointer; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; }
/* The row already chosen and the row the cursor is on must not look alike: one
   is state, the other is position. The same split select2 is given above. */
.ec-combo-option[aria-selected="true"] { background: var(--surface-2); font-weight: 600; }
.ec-combo-option.is-active, .ec-combo-option:hover {
    background: var(--brand); color: var(--brand-ink); }
.ec-combo-option[aria-disabled="true"] { color: var(--ink-dim); cursor: default; background: none; }
.ec-combo-group { padding: .5rem .6rem .25rem; font-size: .74rem; font-weight: 700;
    color: var(--ink-dim); }
.ec-combo-search:focus { border-color: var(--brand); }
.ec-combo-empty { margin: 0; padding: .4rem .6rem; font-size: .8rem; color: var(--ink-dim); }
.ec-combo-empty[hidden] { display: none; }

/* Where the shell puts a dropdown that is not a form field. Each of these had
   a width of its own on the <select>; the control inherits it rather than
   restating it, so a filter stays compact and a form field stays full-width. */
.related-widget-wrapper > .ec-combo { flex: 1 1 auto; min-width: 0; }
/* And the checkbox set, which was the one shape left off that list.
 *
 * `.related-widget-wrapper` is a ROW flex container (it used to hold the
 * related-field icons beside the control), so a child that is not told to grow
 * shrinks to its own content -- and a grid whose tracks are `auto-fill` then
 * resolves to ONE column. Every M2M on the shell was therefore a 176px column
 * of checkboxes with a scrollbar, at any window width, which is why 27
 * permissions read as a list you scroll rather than a set you take in (owner,
 * 2026-09-03).
 *
 * The same rule already existed for `> select`, `> .select2` and `> .ec-combo`.
 * A list of shapes by name is exactly what CLAUDE.md 75 records going wrong:
 * the newest one is the one missing from it. */
.related-widget-wrapper > .ec-checkfield { flex: 1 1 auto; min-width: 0; }
.ec-filter .ec-combo { max-width: none; min-width: 11rem; }
.ec-filter .ec-combo-control { padding: .42rem .7rem; font-size: .85rem;
    background-color: var(--surface); }
.ec-year .ec-combo { max-width: 11rem; }
.ec-year .ec-combo-control { padding: 4px 8px; border-radius: 6px; font: inherit;
    background: var(--surface); }
.ec-year-past .ec-combo-control { border-color: var(--warning, #b45309);
    color: var(--warning, #b45309); font-weight: 600; }
/* Borderless until touched, exactly as the 48 native cells were: a wall of
   boxed dropdowns is chrome, and the grid's own lines already say where each
   cell is. */
.ec-grid td .ec-combo { max-width: none; min-width: 6.5rem; }
.ec-grid td .ec-combo-control { padding: .35rem .45rem; font-size: .82rem;
    border-color: transparent; background: transparent; }
.ec-grid td .ec-combo-control:hover, .ec-grid td .ec-combo-control:focus-visible,
.ec-grid td .ec-combo.is-open .ec-combo-control {
    border-color: var(--line); background: var(--input-bg); }
/* The caret goes with the border, and for the same reason: 48 of them at rest
   is the wall of chrome the borderless cell exists to avoid, and the grid
   already says every cell is a control. */
.ec-grid td .ec-combo-arrow { opacity: 0; transition: opacity .12s ease; }
.ec-grid td .ec-combo-control:hover .ec-combo-arrow,
.ec-grid td .ec-combo-control:focus-visible .ec-combo-arrow,
.ec-grid td .ec-combo.is-open .ec-combo-arrow { opacity: 1; }

/* --- the line above a set ------------------------------------------------
   Only the count now. The تحديد الكل / مسح buttons that used to sit here were
   the SECOND way this admin offered "select all", and which of the two a field
   got depended on whether its form happened to ship an «الكل» option -- an
   implementation detail, visible on screen. «الكل» is a row in the set itself
   on every field, so this line says what is chosen and nothing else. */
.ec-multi-tools { display: flex; align-items: center; gap: .4rem; margin-bottom: .35rem; flex-wrap: wrap; }
.ec-multi-count { font-size: .76rem; color: var(--ink-dim); font-variant-numeric: tabular-nums; }


/* The switch and its label share a LINE.
   `.form-row > div` (line ~211) stacks every field's parts in a column, and it
   outranks a bare `.checkbox-row` -- one class plus a type beats one class --
   so the switch sat above its own label, centred, reading as a control with no
   name. Matched at the same specificity to put it back on one row; the help
   text keeps its own line by taking the full basis. */
.form-row > div.checkbox-row {
    flex-direction: row; flex-wrap: wrap; align-items: center; gap: .1rem .55rem;
}
.form-row > div.checkbox-row > label { flex: 0 1 auto; }
.form-row > div.checkbox-row > .help,
.form-row > div.checkbox-row > p.help { flex: 1 0 100%; }

/* --- «اعدادات المدارس»: a set of flags is a LIST, not a grid ---------------
 *
 * The page was one `fieldset.module.aligned` -- `auto-fill` at a 26rem minimum
 * -- holding four different kinds of question at once, so which switch landed
 * beside which was decided by how many cells came before it (owner,
 * 2026-09-05). Seven independent toggles in two columns also force a zig-zag
 * read: right, left, right, left. A set of unrelated yes/no answers is scanned
 * DOWN one column, which is why every settings screen anybody has ever used is
 * a list.
 *
 * Keyed on the class `GROUPED` puts on that fieldset, so a checkbox anywhere
 * else on the shell is not swept in.
 *
 * The label goes FIRST and takes the rest of the row, with the switch pinned to
 * the far edge: that is the conventional order, and it means the whole strip
 * toggles rather than the 28px pill alone. It costs nothing -- Django already
 * renders `<label for>`, so the browser does the work. */
fieldset.module.aligned.ec-switch-list { grid-template-columns: 1fr; gap: 0; }
.ec-switch-list .form-row { padding: 0; }
/* A rule between rows, not a card each: seven cards is seven boxes for seven
   words. `+` so the first row has no line above it and the group's own heading
   is what opens it. */
.ec-switch-list .form-row + .form-row { border-top: 1px solid var(--line); }
/* Matched at the specificity of the base rule this extends (one class + one
   type on `> div`), plus the list's own class, so it wins without !important. */
.ec-switch-list .form-row > div.checkbox-row {
    min-block-size: 44px; padding-block: .3rem; column-gap: .75rem; }
.ec-switch-list .form-row > div.checkbox-row > label {
    order: -1; flex: 1 1 auto;
    display: flex; align-items: center; min-block-size: 44px;
    font-size: .9rem; font-weight: 600; color: var(--ink); cursor: pointer; }
/* Pushed to the trailing edge by the label's own growth, not by a width. */
.ec-switch-list .form-row > div.checkbox-row > input[type="checkbox"] {
    margin-inline-start: auto; }
/* After both, on its own line -- `order` decides which line, `flex-basis`
   decided that there is one. */
.ec-switch-list .form-row > div.checkbox-row > .help,
.ec-switch-list .form-row > div.checkbox-row > p.help {
    order: 1; margin-block-end: .35rem; }
/* The row is the target, so it answers to the pointer like one. */
.ec-switch-list .form-row:hover { background: var(--surface-2); }
.ec-switch-list .form-row:has(input:focus-visible) { background: var(--surface-2); }

/* Django attaches the ctrl-click sentence to every M2M. Where the control it
   describes has been replaced by a checkbox set, the sentence is instructions
   for a widget that is no longer on the page -- worse than noise, because
   somebody will try to follow it. change-form.js hides the DUPLICATES; this
   hides the one that is now wrong. */
/* The ctrl-click sentence is removed in JS, per help paragraph, so the rest of
   a field's help survives -- «صلاحيات خاصة بهذا المستخدم» is real information
   and the blanket rule that used to live here threw it away with the sentence. */


/* ============================================================ the timetable
   Lessons down, days across -- the shape the parents' app draws and the shape
   pinned to the classroom wall. See backend/OneEdu/admin_timetable.py. */
.ec-grid-card { margin-top: 1.2rem; padding: 1rem 1.1rem 1.2rem;
    border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
.ec-grid-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem 1rem; margin-bottom: .9rem; }
.ec-grid-head h2 { margin: 0; font-size: 1rem; font-weight: 700; color: var(--ink); }
.ec-grid-note { font-size: .78rem; color: var(--ink-dim); }
.ec-grid-empty { margin: 0; padding: .9rem 1rem; border: 1px dashed var(--line); border-radius: 10px;
    background: var(--surface-2); color: var(--ink-muted); font-size: .86rem; }

/* The one place in this admin where horizontal scroll is right: a week is six
   columns wide and folding one away (the changelist ladder, CLAUDE.md 52)
   would remove a DAY from the timetable. The lesson column is sticky instead,
   so whatever is scrolled into view still says which lesson it is. */
.ec-grid-wrap { overflow-x: auto; }
.ec-grid { border-collapse: separate; border-spacing: 0; width: 100%; min-width: 44rem; }
.ec-grid th, .ec-grid td { border-bottom: 1px solid var(--line); padding: .35rem .4rem; }
.ec-grid thead th {
    position: sticky; top: 0; z-index: 2;
    background: var(--surface-2); color: var(--ink);
    font-size: .82rem; font-weight: 700; text-align: center;
    padding: .55rem .4rem;
}
.ec-grid tbody th {
    position: sticky; inset-inline-start: 0; z-index: 1;
    background: var(--surface); color: var(--ink-muted);
    font-size: .8rem; font-weight: 600; text-align: start; white-space: nowrap;
    /* A sticky cell under border-collapse loses its own border once it
       detaches, so the seam is an inset shadow -- the same fix the changelist's
       sticky header needed. */
    box-shadow: inset -1px 0 0 var(--line);
    min-width: 8rem;
}
.ec-grid-corner { inset-inline-start: 0; z-index: 3; }
.ec-grid tbody tr:hover td { background: var(--surface-2); }
.ec-grid td select {
    width: 100%; min-width: 6.5rem;
    border: 1px solid transparent; border-radius: 7px;
    padding: .35rem .45rem;
    background: transparent; color: var(--ink);
    font-family: inherit; font-size: .82rem;
}
/* Borderless until touched: 48 boxed dropdowns is a wall of chrome, and the
   grid's own lines already say where each cell is. The border appears where
   the pointer or the keyboard is, which is the only place it is information. */
.ec-grid td select:hover { border-color: var(--line); background: var(--input-bg); }
.ec-grid td select:focus { outline: none; border-color: var(--brand); background: var(--input-bg); }
/* An empty cell is quiet; a filled one is the content. Without this the «—»
   placeholders read at the same weight as the lessons and the eye cannot tell
   a full week from an empty one at a glance. */
.ec-grid td select:has(option[value=""]:checked) { color: var(--ink-dim); }


/* الغاء. A link, styled as the quiet sibling of احفظ -- it leaves without
   saving, so it must not look like a third thing that might write something.
   Placed at the end of the row, away from احذف, because the two are opposite
   in consequence and adjacent in a row is how they get confused. */
.submit-row .ec-cancel {
    order: 99;
    display: inline-flex; align-items: center;
    padding: .5rem 1.1rem; border-radius: 9px;
    border: 1px solid var(--line); background: var(--surface);
    color: var(--ink-muted); font-size: .88rem; font-weight: 600;
    text-decoration: none;
}
.submit-row .ec-cancel:hover { background: var(--surface-2); color: var(--ink); }
.submit-row .ec-cancel:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }


/* ============================================ files, pictures, and when they
 * apply.  The behaviour, and why each half is shaped this way, is in
 * edumin/js/media-fields.js and backend/OneEdu/admin_media.py. */

/* A field whose condition is not met. `!important` because a `.form-row` here
   is `display: grid` and a `<td>` is `display: table-cell`, and either beats a
   plain `display: none` from a later rule of the same specificity. */
.ec-when-off { display: none !important; }

/* What Django rendered for a file that already has a value: the «Currently /
   Change» paragraph, and the standalone <a><img> that CustomAdminFileWidget
   emits. Both stay in the DOM -- the clear checkbox inside them is what the
   remove button ticks, and a hidden checkbox still posts. */
.ec-file-legacy { display: none !important; }

.ec-file { display: flex; flex-direction: column; gap: .6rem; align-items: flex-start; }

/* A row of squares, wrapping. `auto-fill` rather than a fixed count so the
   strip follows the space it is given -- an inline block on a phone and a full
   card on a laptop -- with no breakpoint to keep in step. */
.ec-file-grid {
    display: grid; gap: .55rem; width: 100%; max-width: 640px;
    grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
}
.ec-file-item {
    position: relative; margin: 0; min-width: 0;
    border: 1px solid var(--line); border-radius: 10px;
    background: var(--surface-2); overflow: hidden;
}
.ec-file-thumb {
    display: block; width: 100%; aspect-ratio: 1; object-fit: cover;
    background: var(--field);
}
/* Not a picture. The extension, large, instead of a generic paperclip that
   tells you the same thing about every file. */
.ec-file-doc {
    display: flex; align-items: center; justify-content: center;
    font-size: .8rem; font-weight: 700; letter-spacing: .04em;
    color: var(--ink-dim);
}
.ec-file-item figcaption {
    padding: .3rem .4rem; font-size: .72rem; line-height: 1.6; color: var(--ink-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Saved and about to be replaced by a new pick. Still shown: it is what the
   record holds until you press احفظ. */
.ec-file-item.is-stale .ec-file-thumb { opacity: .45; }
/* Marked for deletion. Struck rather than removed, because the act is undoable
   right up to the save and a row that disappears reads as already gone. */
.ec-file-item.is-removed { border-color: var(--danger); }
.ec-file-item.is-removed .ec-file-thumb { opacity: .3; }
.ec-file-item.is-removed figcaption { color: var(--danger); }

.ec-file-remove {
    position: absolute; inset-block-start: .3rem; inset-inline-end: .3rem;
    width: 24px; height: 24px; padding: 0; border-radius: 50%;
    border: 1px solid var(--line); background: var(--surface); color: var(--ink-muted);
    font-size: .78rem; line-height: 1; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
}
.ec-file-remove:hover { color: var(--danger); border-color: var(--danger); }
.ec-file-remove.is-undo:hover { color: var(--ink); border-color: var(--line); }
.ec-file-remove:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The picker. A dashed field while empty -- the shape that says "put something
   here" -- and a quiet button once there is something to add to. */
.ec-file-drop {
    display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
    padding: .55rem .7rem; border-radius: 10px;
    border: 1px solid var(--line); background: var(--surface-2);
}
.ec-file-empty .ec-file-drop {
    border-style: dashed; border-color: var(--line-strong);
    padding: .9rem 1rem; width: 100%; max-width: 640px; cursor: pointer;
}
.ec-file-drop.is-over { border-color: var(--brand); background: var(--surface); }
.ec-file-pick {
    padding: .4rem .9rem; border-radius: 8px;
    border: 1px solid var(--line); background: var(--surface); color: var(--ink);
    font-family: inherit; font-size: .84rem; font-weight: 600; cursor: pointer;
}
.ec-file-pick:hover { border-color: var(--brand); color: var(--brand); }
.ec-file-pick:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.ec-file-hint { font-size: .78rem; color: var(--ink-dim); }
/* Dragging is a pointer affordance and nothing else -- the button is the way
   in for a keyboard, and on a touch screen there is nothing to drag. */
@media (hover: none) { .ec-file-hint { display: none; } }

/* A news attachment is a block on this shell, not a table row (admin_media.py).
   Its heading carries the delete checkbox, which Django styles for a table. */
.inline-related > h3 {
    display: flex; align-items: center; gap: .8rem; flex-wrap: wrap;
    margin: 0 0 .6rem; padding: .5rem .75rem; border-radius: 9px;
    background: var(--surface-2); color: var(--ink-muted);
    font-size: .82rem; font-weight: 700;
}
.inline-related > h3 .inline_label { color: var(--ink); }
.inline-related > h3 .delete { margin-inline-start: auto; display: flex; align-items: center; gap: .35rem; }
.inline-related > h3 .delete label { margin: 0; font-weight: 600; }
.inline-related > h3 .delete input[type="checkbox"] { accent-color: var(--brand); margin: 0; }

/* ===================================================================== *
 * الرسائل -- the transcript                                             *
 * ===================================================================== *
 *
 * A conversation is not a table of rows, and the inline that drew it was
 * carrying its own light-theme stylesheet: `#fff` bubbles, `#eee` bubbles, a
 * `#f1f5fc` border and a `#3c8dbc` author name -- a blue that is in no palette
 * this product has. On the dark shell that renders as white slabs with grey
 * text on them. Everything below is the shell's own tokens, so the screen
 * follows the theme switch and a school's brand like every other screen.
 *
 * Identity is carried three ways -- the SIDE, the bubble's GROUND, and a named
 * AUTHOR -- never by colour alone.
 *
 * `--brand` is NOT used as a bubble fill. CLAUDE.md 69: on a page whose one
 * consequential action reaches a family, a red that also paints twenty message
 * bubbles stops meaning anything. The office's bubble is the brand mixed 10%
 * into the muted surface -- enough to read as "us" at a glance, quiet enough
 * that «أرسل» is still the only saturated thing on the page.
 */
.ec-thread {
    /* Three grounds, all mixed INTO the card rather than borrowed from tokens
       whose relationship to it differs by theme. `--surface-2` was the obvious
       pick for the family's bubble and is #faf9f9 on a #fff card -- a hair off
       it, so in light mode the bubble was its border and nothing else. That is
       the same trap CLAUDE.md 78 records for the switch, and it is invisible on
       a dark laptop, which is every shot this project's harness had ever taken.

       A percentage of `--ink` steps the same way in both palettes -- toward the
       text and away from the card -- so one expression is correct in both, and
       a school re-branding moves the office's bubble with everything else. */
    --bub-them-bg: color-mix(in srgb, var(--ink) 6%, var(--surface));
    --bub-them-line: var(--line);
    --bub-us-bg: color-mix(in srgb, var(--brand) 12%, var(--surface));
    --bub-us-line: color-mix(in srgb, var(--brand) 30%, var(--line));
    /* The teacher is a third voice, and deliberately NOT the school's colour:
       in a teacher thread the administration is reading two OTHER people, and
       painting the teacher in the school's brand would claim the office said
       it. A further step of the same grey carries it, plus a border meant to be
       seen. Two borders on one ground told nobody anything -- only visible in a
       shot with both bubbles in it. */
    --bub-third-bg: color-mix(in srgb, var(--ink) 13%, var(--surface));
    --bub-third-line: var(--line-strong);

    /* The unread tick. See `.ec-msg-tick` below for why it is not
       `--ink-muted` and why the two themes need separate values. */
    --tick-idle: #8A8A90;

    background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
    padding: 1.1rem 1.25rem 1.25rem; margin-block-start: 1.25rem;
}
[data-theme-version="dark"] .ec-thread { --tick-idle: var(--ink-muted); }

.ec-thread-title { font-size: .95rem; font-weight: 700; color: var(--ink); margin: 0 0 .5rem;
                   padding-block-end: .7rem; border-block-end: 1px solid var(--line); }

.ec-thread-log { list-style: none; margin: 0; padding: .9rem .15rem; display: flex;
                 flex-direction: column; gap: .5rem;
                 /* Capped and scrolled rather than uncapped: a long thread would
                    otherwise push the composer a screen and a half below the
                    last message you just read. */
                 max-height: min(60vh, 38rem); overflow-y: auto; overscroll-behavior: contain; }

/* A thread that runs for weeks needs its days named. The one structural device
   here, and it encodes something true rather than decorating. */
.ec-thread-day { align-self: center; margin: .7rem 0 .35rem; }
.ec-thread-day span { display: inline-block; font-size: .72rem; color: var(--ink-muted);
                      background: var(--field); border: 1px solid var(--line);
                      border-radius: 999px; padding: .18rem .7rem; }

.ec-msg { display: flex; }
/* Logical properties, so RTL mirrors without a second stylesheet: the family
   sits at the inline START, everyone speaking for the school at the inline END
   -- which in this panel's RTL is right and left respectively. */
.ec-msg.is-parent { justify-content: flex-start; }
.ec-msg.is-office, .ec-msg.is-teacher { justify-content: flex-end; }
/* Neither side, because we do not know which. Centred and dashed, like every
   other "this is not a fact" surface on the shell. */
.ec-msg.is-unknown { justify-content: center; }
.ec-msg.is-unknown .ec-msg-bubble { background: transparent; border-style: dashed;
                                    border-color: var(--line-strong); max-width: 60%; }

.ec-msg-bubble { max-width: min(46rem, 78%); border: 1px solid var(--bub-them-line);
                 background: var(--bub-them-bg); border-radius: 14px;
                 padding: .6rem .8rem .45rem; }
.ec-msg.is-parent  .ec-msg-bubble { border-start-start-radius: 4px; }
.ec-msg.is-office  .ec-msg-bubble { background: var(--bub-us-bg); border-color: var(--bub-us-line);
                                    border-start-end-radius: 4px; }
.ec-msg.is-teacher .ec-msg-bubble { background: var(--bub-third-bg); border-color: var(--bub-third-line);
                                    border-start-end-radius: 4px; }

.ec-msg-who { margin: 0 0 .2rem; display: flex; align-items: baseline; gap: .45rem; flex-wrap: wrap; }
.ec-msg-name { font-size: .78rem; font-weight: 700; color: var(--ink); }
/* `--ink-muted`, not `--ink-dim`: this names WHO is speaking, so it is not
   decoration and has to clear 4.5:1 on the bubble it sits on. */
.ec-msg-role { font-size: .7rem; color: var(--ink-muted); }
.ec-msg-text { font-size: .93rem; line-height: 1.75; color: var(--ink); overflow-wrap: anywhere; }
.ec-msg-media { margin-block-start: .45rem; }
/* The model's `image_tag` writes `height="200px"` as an attribute; a
   presentational attribute loses to any rule, so this is where the size is
   decided rather than in Python. */
.ec-msg-media img { height: auto; width: auto; max-width: 100%; max-height: 18rem;
                    border-radius: 9px; display: block; }
/* A document is a CHIP, not an inline preview: there is nothing to look at
   until it is opened, and a full-width slab for a filename reads as an image
   that failed to load. Sized as a target rather than as text -- it is the one
   thing in a bubble anybody presses. */
.ec-msg-doc { margin-block-start: .45rem; }
.ec-msg-file { display: inline-flex; align-items: center; gap: .4rem; max-width: 100%;
               padding: .4rem .6rem; border-radius: 9px; font-size: .82rem;
               background: var(--surface-2); border: 1px solid var(--line-strong);
               color: var(--ink); text-decoration: none; overflow-wrap: anywhere; }
.ec-msg-file::before { content: "\1F4CE"; font-size: .9em; }
.ec-msg-file:hover, .ec-msg-file:focus-visible { border-color: var(--brand); color: var(--ink); }

/* The time, and how far the family has got with this message. One line, at the
   bubble's end, the way every messaging app puts them. */
.ec-msg-meta { margin: .25rem 0 0; display: flex; align-items: center; gap: .3rem;
               justify-content: flex-end; }
.ec-msg-at { font-size: .68rem; color: var(--ink-muted);
             font-variant-numeric: tabular-nums; }

/* ONE glyph for every state -- Material's `done_all`, tinted by `currentColor`,
   the same mark the app draws. «أُرسلت» carried a single tick until 2026-09-07,
   so that shape rather than hue told the states apart; the owner dropped it once
   «وصلت» was shown to be unreachable in practice (§111), which left the single
   tick separating two states nobody can see apart.

   So colour is now the only difference, and `--ink-muted` could not be it: at
   #595959 against the brand's #BE1325 the two are 1.10 IN GREYSCALE -- a
   red-blind reader, or anyone on a poor screen, sees one mark. `--tick-idle` is
   lighter, which is a real lightness difference (1.85) and still clears the 3:1
   a non-text indicator is asked for (3.43 on the card) -- the rule §78 used for
   the switch and §98 for `--control-edge`.

   TWO values rather than one `color-mix`, because the themes want opposite
   things: in light the read mark is darker than the page and the idle must be
   lighter, in dark the read mark is already lifted (#D8394A) and mixing toward
   the surface would make the idle DARKER and collapse it back to 1.04. Measured,
   not assumed. `--ink-muted` is right in dark on its own (1.97). */
.ec-msg-tick { display: inline-flex; align-items: center; color: var(--tick-idle); }
.ec-msg-tick svg { display: block; }
.ec-msg-tick.is-read { color: var(--brand); }

.ec-thread-empty, .ec-thread-locked {
    margin: .4rem 0 0; padding: .8rem .9rem; border-radius: 10px; font-size: .85rem;
    line-height: 1.75; color: var(--ink-muted); background: var(--surface-2);
    border: 1px dashed var(--line-strong); }

/* -- the composer ---------------------------------------------------------
 * Only rendered where writing is allowed, so there is no disabled state to
 * explain: on a teacher thread this whole block is absent and `.ec-thread-locked`
 * says why. */
.ec-thread-composer { margin-block-start: 1rem; padding-block-start: 1rem;
                      border-block-start: 1px solid var(--line); }
.ec-thread-composer h3 { margin: 0 0 .55rem; font-size: .82rem; font-weight: 700; color: var(--ink-muted); }
.ec-thread-field { margin-block-end: .6rem; }
/* Height only. The border, radius, padding, size, line-height, ground and focus
   ring all come from the shell's ONE text-input treatment, which this selector
   was added to rather than restated here. */
.ec-thread-field textarea { min-height: 5.5rem; height: 6.5rem; resize: vertical; }
.ec-thread-actions { display: flex; justify-content: flex-end; }
.ec-thread-send { background: var(--brand); color: var(--brand-ink); border: 0; border-radius: 9px;
                  padding: .55rem 1.6rem; font-family: inherit; font-size: .9rem; font-weight: 700;
                  cursor: pointer; transition: filter .14s ease; }
.ec-thread-send:hover { filter: brightness(1.08); }

/* The page's own «احفظ» is hidden on this screen: with every other field on the
   form read-only, the only thing saving does is send the message -- so it is
   said once, in the composer, where the message is written. الغاء stays, which
   keeps CLAUDE.md 70's "one action + الغاء". */
body.model-conversation .submit-row input[name="_save"] { display: none; }
/* Four read-only facts about the thread, not four full-width form fields: the
   header was taking the top third of the screen to say two names three times. */
body.model-conversation fieldset.module.aligned { grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); }

@media (max-width: 700px) {
    .ec-msg-bubble { max-width: 92%; }
    .ec-thread { padding: .85rem .8rem 1rem; }
}

/* «بانتظار رد», at the far end of «الرسائل» in the sidebar.
 *
 * It sat in the header, beside a comments icon -- which made it a THIRD way to
 * reach a screen that is already a sidebar tab and a breadcrumb, and put the
 * number a whole header away from the word it is about. On the entry itself it
 * is read as part of «الرسائل ٣» rather than as a lone red dot somewhere else.
 *
 * The one place on this shell where `--brand` is spent on something that is not
 * an action -- and it earns it: this is the only control here whose whole job is
 * to be noticed from across a desk while somebody is doing something else. It is
 * also drawn only when the count is non-zero, so it is never a red dot reporting
 * that nothing is happening.
 *
 * `--brand-ink` on `--brand` is the same pair the send button uses, measured at
 * 6.35:1 light and 4.56:1 dark. */
.deznav .ec-menu-row { position: relative; }
/* Room for the pill, so a longer label never runs under it. `padding-inline-end`
   after the shorthand on `.deznav a`, which sets all four sides. */
.deznav .ec-menu-row > a { padding-inline-end: 3.4rem; }
/* Absolutely positioned rather than a flex sibling: `.deznav a` is the row, and
   a second box in the flow would push the label off its own left edge.
   Named `.ec-badge` and not `.ec-waiting`: it counted «بانتظار رد» until
   2026-09-05 and counts «غير مقروءة» now, and a class whose name states the
   older meaning is the same trap as a column whose name does (CLAUDE.md 64). */
.deznav .ec-badge {
    position: absolute; inset-inline-end: 18px; top: 50%; transform: translateY(-50%);
    display: flex; padding: 4px; border-radius: 999px; text-decoration: none; }
.deznav .ec-badge:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.ec-badge-count {
    min-width: 1.25rem; height: 1.25rem; padding: 0 .32rem; box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--brand); color: var(--brand-ink);
    border-radius: 999px; font-size: .72rem; font-weight: 700; line-height: 1;
    /* Western digits and even columns, like every other number on this shell. */
    font-variant-numeric: tabular-nums; }
/* On the CURRENT row the ground IS `--brand`, so a brand pill would disappear
   into it -- the count inverts rather than vanishing on the one page where the
   reader is most likely to be looking at it. */
.deznav a.active ~ .ec-badge .ec-badge-count { background: #fff; color: var(--brand); }

/* The same red, on the row the count is about.
 *
 * The badge said 1 and «الرسائل» gave no way to tell which of seven rows it
 * meant, so following the number meant opening threads until one of them was
 * the one (owner, 2026-09-05). This is that row, marked with the colour of the
 * pill that sent you looking -- the two are one signal, and the dot is what
 * makes the pill's number followable.
 *
 * A DOT and not a pill with words: it sits in the widest column on the table,
 * and «بانتظار رد من الإدارة» there would push the name it belongs to off the
 * screen. The words are on the `title`, in hidden text for a screen reader, and
 * on the «الحالة» filter directly above the table. */
/* Two facts, and they nest: unread (by me) is a subset of unanswered (by
   anyone). So ONE marker with two states rather than two dots.

     ●  filled --brand   a message I have not read -- what the badge counts
     ◯  ring in --info   nobody has replied, but I have looked

   FILLED against HOLLOW, not red against blue. Two solid dots differing only
   in hue is the one accessibility rule that fails outright here: a red-blind
   or blue-blind reader sees one indistinguishable dot standing for two
   different things. Fill is a second channel, it survives greyscale, and it
   costs nothing. Each also carries a `title` and a visually-hidden label.

   Slightly larger than the single dot it replaces, because a 2px ring on an
   8px circle leaves a hole too small to read as hollow. */
.ec-row-mark {
    display: inline-block; inline-size: .6rem; block-size: .6rem;
    margin-inline-end: .5rem; vertical-align: .05em;
    border-radius: 50%; flex: none; box-sizing: border-box; }
/* The cell is inside the row's own link, which is `--ink` -- so each state
   states its colour rather than inheriting one, exactly like the sidebar pill. */
.ec-row-mark.is-unread,
#result_list td .ec-row-mark.is-unread, #result_list th .ec-row-mark.is-unread {
    background: var(--brand); border: 0; }
.ec-row-mark.is-unanswered,
#result_list td .ec-row-mark.is-unanswered, #result_list th .ec-row-mark.is-unanswered {
    background: transparent; border: 2px solid var(--info); }

/* --- مطابقة الاعوام: what no longer sits in the year its date falls in ------
 *
 * Two tables that answer different questions and must not read as one: what the
 * years ARE, then what would move. The count between them is the sentence the
 * page exists to say, so it carries the weight rather than a heading above it.
 *
 * `--brand` is spent on «طبّق» alone. It is the only irreversible control here,
 * and a red that also tinted the counts or the table headings would stop
 * meaning anything on the one page whose whole job is to be read before a press
 * -- the same reason the notification composer keeps its brand for the send
 * button and nothing else.
 */
.ec-mismatch-intro { max-width: 70ch; opacity: .8; margin-bottom: 20px; }

.ec-mismatch-years,
.ec-mismatch-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
.ec-mismatch-years th, .ec-mismatch-years td,
.ec-mismatch-table th, .ec-mismatch-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--line, #e5e7eb);
  text-align: start;
}
.ec-mismatch-years th,
.ec-mismatch-table th { background: var(--surface-2); font-weight: 600; }

/* A row with nothing to move is context, not news. */
.ec-mismatch-clean { opacity: .55; }

.ec-mismatch-count { font-size: 1.05rem; font-weight: 600; margin: 4px 0 14px; }

/* Each (from -> to) as its own chip: several per row, and run together they
 * read as one long sentence rather than as separate movements. */
.ec-mismatch-move {
  display: inline-block;
  padding: 2px 8px;
  margin: 0 0 4px 6px;
  border: 1px solid var(--line, #e5e7eb);
  border-radius: 999px;
  font-size: .85rem;
  white-space: nowrap;
}

.ec-mismatch-apply { margin-top: 8px; }
.ec-mismatch-warning { max-width: 70ch; opacity: .85; margin-bottom: 12px; }

.ec-mismatch-button {
  background: var(--brand);
  color: var(--on-brand, #fff);
  border: 0;
  border-radius: 8px;
  padding: .6rem 1.6rem;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.ec-mismatch-button:hover { filter: brightness(1.06); }

.ec-mismatch-clean-note { opacity: .8; }

/* --- الاعوام الدراسية, on the school's own page ---------------------------
 *
 * The end column is deliberately not an input. A year ends the day before the
 * next begins, so it is derived, and rendering it as a field would invite
 * somebody to type a date that the next year's start then contradicts -- two
 * editable ends of one boundary, which is the gap-or-overlap this whole design
 * exists to make impossible.
 */
.ec-years-card {
  background: var(--surface);
  border: 1px solid var(--line, #e5e7eb);
  border-radius: 12px;
  padding: 16px 18px;
  margin: 20px 0;
}
.ec-years-head { margin-bottom: 12px; }
.ec-years-head h2 { margin: 0 0 4px; font-size: 1.05rem; }
.ec-years-note { display: block; max-width: 70ch; opacity: .75; font-size: .88rem; }

.ec-years-shared {
  margin: 0 0 12px;
  padding: 8px 12px;
  border-inline-start: 3px solid var(--brand);
  background: var(--surface-2);
  border-radius: 6px;
  font-size: .88rem;
}

.ec-years-table { width: 100%; border-collapse: collapse; }
/* The input takes the shell's treatment, which is `width: 100%` -- right in a
   form column, wrong in a table cell where it stretches the column to whatever
   is left over. Capped, and the cell holds it to its own width. */
.ec-years-table td.ec-years-start { width: 1%; white-space: nowrap; }
.ec-years-table input[type="date"] { width: auto; min-width: 11rem; }
.ec-years-table th,
.ec-years-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--line, #e5e7eb);
  text-align: start;
  font-weight: 400;
}
.ec-years-table thead th { background: var(--surface-2); font-weight: 600; }
.ec-years-table tbody th { font-weight: 600; white-space: nowrap; }

/* Derived, not entered -- so it reads as a statement, not a blank to fill. */
.ec-years-end { font-variant-numeric: tabular-nums; opacity: .75; }

.ec-years-hint { margin-inline-start: 8px; font-size: .8rem; opacity: .6; }
.ec-years-empty { opacity: .8; margin: 0; }

/* «اضافة العام القادم». One action, at the end of the rows it adds to, in the
   secondary treatment: adding next year is routine and reversible, and the only
   thing on this page wearing the brand is «احفظ». */
.ec-years-add {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 14px;
  flex-wrap: wrap;
}
.ec-years-add button {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  /* 44px, the one control here that is a target rather than a field. The date
     boxes keep the shell's own height on purpose -- making them taller than
     every other input on the panel is the inconsistency being fixed, inverted. */
  min-height: 44px;
  padding: 0 1.1rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: .9rem;
  cursor: pointer;
  transition: background-color .18s, border-color .18s;
}
.ec-years-add button:hover { background: var(--surface); border-color: var(--line-strong, var(--line)); }
.ec-years-add button:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.ec-years-add button svg { inline-size: 1rem; block-size: 1rem; }
.ec-years-add-note { opacity: .7; font-size: .85rem; }

@media (prefers-reduced-motion: reduce) {
  .ec-years-add button { transition: none; }
}

/* «لون لوحة التحكم» -- the swatch and the preset chips -----------------------
 *
 * Every one of these carries a VISIBLE border rather than relying on its own
 * fill to have an edge. A colour control whose whole point is arbitrary fills
 * will eventually be handed white, or a pale tint, on a white card -- which is
 * CLAUDE.md 78's switch (1.05:1 against its own ground) in a control that
 * cannot avoid it by choosing a better token. `--control-edge` is the border
 * picked against the 3:1 of WCAG 1.4.11 for exactly this.
 */
.ec-brand-row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-top: .5rem; }

.ec-brand-swatch {
    inline-size: 2.2rem; block-size: 2.2rem; border-radius: 8px;
    border: 1px solid var(--control-edge); background: var(--brand);
    flex: 0 0 auto;
}
/* Empty is a STATE, not a colour: a blank field means "the default", and
 * painting the swatch the default red would say a colour had been chosen. */
.ec-brand-swatch.is-empty {
    background: repeating-linear-gradient(45deg, var(--surface-2) 0 6px, var(--surface) 6px 12px);
}

.ec-brand-chip {
    inline-size: 1.75rem; block-size: 1.75rem; border-radius: 50%;
    border: 1px solid var(--control-edge); padding: 0; cursor: pointer;
    flex: 0 0 auto; transition: transform .12s ease;
}
.ec-brand-chip:hover { transform: scale(1.12); }
.ec-brand-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The field itself is LTR (a hex value is not Arabic text), so its help text
 * and error read correctly beside it only if the row starts where the input
 * does. */
.form-row input[name="admin_brand_color"] { max-inline-size: 12rem; }

/* ------------------------------------------------------------ «من قرأها»
   The per-parent roster of a sent notification, on its own page
   (admin_composer.readers_view). `.ec-archive` draws the table, the
   changelist's pills draw the state, and the toolbar borrows the changelist's
   own `.ec-filter-row`, so a filter here looks like a filter there. */
.ec-readers-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; margin: .25rem 0 1rem; }
.ec-readers-toolbar .ec-roster-filter { flex: 1 1 16rem; max-width: 26rem; }
.ec-readers-table td { vertical-align: top; }
.ec-roster-student { display: block; }
.ec-roster-none { margin: .6rem 0 0; }
.ec-readers-shown { color: var(--ink-dim); }
/* Quiet -- it undoes a filter (the action ladder, CLAUDE.md 98). */
.ec-readers-clear { border: 1px solid var(--line); background: var(--surface-2); color: var(--ink-muted);
    border-radius: 8px; padding: .42rem .8rem; font: inherit; font-size: .85rem; cursor: pointer; }
.ec-readers-clear:hover { color: var(--ink); }
/* The link into the reader page, beside تاريخ in the object-tools row: a
   destination with a label, in the history link's quiet treatment -- red is
   for saving, and this only opens a list. */
.object-tools a.readerslink, .object-tools a.readerslink:link, .object-tools a.readerslink:visited {
    background: var(--surface-2); color: var(--ink-muted); border: 1px solid var(--line); }
.object-tools a.readerslink:hover { color: var(--ink); background: var(--surface); }
/* Level with the first field: tools-beside.js moves the row into the fieldset
   as the cell beside it. `align-self: end` sits the buttons on the field's
   own baseline rather than its label's; the padding mirrors `.form-row`'s so
   the two cells share a bottom edge. When the grid drops to one column the
   row simply follows the field, as any cell does. */
fieldset.module.aligned > .object-tools.ec-tools-beside {
    align-self: end; justify-content: flex-end;
    margin: 0; padding: .5rem 0 .9rem; }
