/* STMS components — composes DS tokens (P1-FND-02). Do not invent colors/spacing:
   every value must reference a token var. Layout vars below are structural, not visual. */

/* shell */
.shell { display: flex; min-block-size: 100vh; }
.shell__main { flex: 1; display: flex; flex-direction: column; min-inline-size: 0; }
/* P1-FND-10: content stays inside --content-max (per-tier via responsive.css) —
   the ladder caps and centres the content column on very wide screens. */
.shell__content { padding: var(--space-5); flex: 1; inline-size: 100%;
  max-inline-size: var(--content-max); margin-inline: auto; }

/* sidebar (dark, per DS Chrome) */
.sidebar { inline-size: var(--sidebar-w); flex: 0 0 auto; background: var(--surface-inverse);
  display: flex; flex-direction: column; padding: var(--space-3); gap: var(--space-4); }
/* The sidebar is PINNED to the viewport, but that rule is breakpoint-scoped, so
   it lives in the ladder (tokens/adaptive.css, "sidebar pinned to the viewport")
   and not here: P1-FND-10's package law gives the ladder every media query, and
   test_editor_css_exists_with_no_media_queries_added holds this file to its
   baseline count. Noted here because the stretch this defeats originates in the
   `.shell` flex row directly above. */
/* overflow hidden (P1-FND-11): in rail mode the brand's first child (the
   Zeneon mark) survives inside a 64px box; everything after it is hidden by
   the adaptive.css rail rule (P1-FND-11 brand swap). */
/* P1-FND-15: the lockup is a FIXED-LTR coordinate space — the mark sits at
   x 2-50 of a 232-unit viewBox and the wordmark is anchored at x=66 — so the
   whole block is pinned ltr and never inherits the page direction. Under rtl,
   SVG text-anchor:start resolves to the RIGHT edge and the wordmark draws
   leftward over the mark (bbox x=-48 instead of 66), while the sub-label's
   padding-inline-start flips to the wrong side and the rail-clip rule below
   (which assumes the wordmark is right of the mark) inverts. Same idiom as
   .cell-ltr, which pins plates/times/coordinates inside rtl pages. */
.sidebar__brand { direction: ltr;
  display: flex; flex-direction: column; gap: 2px; padding: var(--space-2) var(--space-2) 0;
  max-inline-size: 100%; overflow: hidden; }
/* The lockup box is width-driven, never height-driven: at full sidebar width it
   shows the whole mark+wordmark lockup, and in the 64px rail the same 100% box
   resolves to the mark alone and clips the wordmark — no media query needed.
   STMS is indented to sit under the WORDMARK, not the mark: in the lockup's
   232-unit viewBox the "Z" starts at x=66, so the same ratio applied to the
   rendered width keeps the two aligned if --brand-logo-w ever changes. */
.sidebar__brand { --brand-logo-w: 172px; }
.sidebar__brand-logo { display: block; inline-size: 100%;
  max-inline-size: var(--brand-logo-w); overflow: hidden; }
/* max-inline-size:none defeats the ladder's blanket img,svg{max-width:100%}
   (adaptive.css) — without it the svg is capped at the box width, which
   SHRINKS the whole lockup to thumbnail size in the rail instead of letting
   the box crop it. Class-level specificity wins, so the vendored rule is
   overridden without editing it. */
.sidebar__brand-logo svg { display: block; block-size: auto;
  inline-size: var(--brand-logo-w); max-inline-size: none; }
/* In the rail the box is far narrower than the lockup, so a fixed-width svg
   would clip the mark itself in half. Scaling the svg to 232/52 of the box
   makes the mark's own 52-unit block exactly fill it — the wordmark falls
   outside and is clipped, which is the intent. Mirrors the ladder's rail
   breakpoint (adaptive.css) because the two must flip together. */
@media (max-width: 1023px) {
  .sidebar__brand-logo svg { inline-size: calc(100% * 232 / 52); }
}
.sidebar__brand-sub { font: var(--weight-medium) var(--text-2xs)/1 var(--font-mono);
  letter-spacing: .22em; color: var(--ink-400);
  padding-inline-start: calc(var(--brand-logo-w) * 66 / 232); }
.sidebar__nav { display: flex; flex-direction: column; gap: 2px; }
/* P1-FND-11: control token, not a hard-coded 34px (same value at baseline) —
   pointer:coarse lifts it to the 44px floor, so rail/bottom-bar icons stay
   hittable on touch; min-inline-size keeps the rail icon box from collapsing
   to the icon's own 16px when the label is hidden. */
.nav-item { display: flex; align-items: center; gap: var(--space-2); block-size: var(--control-h-md);
  min-inline-size: var(--control-h-md);
  padding: 0 var(--space-2); border: 0; border-radius: var(--radius-sm);
  font: var(--type-body-strong); text-align: start; text-decoration: none;
  color: var(--ink-400); background: transparent; }
.nav-item:hover { color: var(--ink-200); }
.nav-item--active { background: rgb(255 255 255 / .10); color: var(--white);
  box-shadow: inset 2px 0 0 var(--brand); }
[dir="rtl"] .nav-item--active { box-shadow: inset -2px 0 0 var(--brand); }
/* P1-FND-18: opt-in glyph mirroring for RTL (ar, ckb). This must stay
   opt-in via .icon--dir, NEVER a blanket `[dir="rtl"] .icon { transform:
   scaleX(-1); }` — most glyphs (bus, school, banknote, users, route,
   megaphone, ...) have no directional meaning and a global flip would
   mirror them into nonsense. Only glyphs whose meaning is a screen
   direction (arrow-left "back", and log-in once P1-FND-20 wires it) take
   this modifier at their call site. Do not "simplify" this to a global
   rule. */
[dir="rtl"] .icon--dir { transform: scaleX(-1); }
/* The old .sidebar__footer "Signed in as" block is retired (P1-FND-34): the
   user card (identity.css .usercard-slot) is the sidebar's footer now. */

/* topbar */
.topbar { block-size: var(--topbar-h); flex: 0 0 auto; background: var(--surface-card);
  border-block-end: 1px solid var(--border-subtle); display: flex; align-items: center;
  gap: var(--space-3); padding: 0 var(--space-5); }
/* P1-FND-11: min-inline-size + ellipsis so a long school name shrinks instead
   of overflowing the wrapped phone topbar (nowrap text ignores min-width). */
.topbar__titles { display: flex; flex-direction: column; white-space: nowrap; min-inline-size: 0; }
.topbar__titles > * { overflow: hidden; text-overflow: ellipsis; }
.topbar__title { font: var(--weight-semibold) var(--text-md)/1.2 var(--font-core); unicode-bidi: plaintext; }
.topbar__subtitle { font: var(--type-caption); color: var(--text-muted); }
.topbar__actions { margin-inline-start: auto; display: flex; align-items: center;
  gap: var(--space-2); min-inline-size: 0; }
/* P1-SET-01 review fix: the `height="28"` attribute alone only constrains
   block-size — a wide logo still renders at its native aspect ratio and
   shoves the school switcher along the topbar. Lives here, not settings.css,
   because the topbar is not page-scoped (it belongs to the shell). No
   breakpoint rules here — this file's media-query count is pinned by
   tests/test_routes_editor.py. */
.topbar__school-logo { max-inline-size: 120px; object-fit: contain; }

/* Search palette (P1-PLT-02 §5, review Finding 2): #platform-palette was an
   in-flow div, so a swapped-in .card grew the topbar instead of dropping
   below the input. .card itself already carries the elevated-surface look
   (border/radius/shadow, components.css above) — this is positioning only,
   following the .scope-switcher__menu overlay precedent (absolute, anchored
   to the relative wrapper, z-index above content). Logical properties only:
   ar/ckb are RTL and inset-inline-start follows the input's own edge in
   either direction. */
.topbar__search { position: relative; }
.topbar__search #platform-palette:not(:empty) {
  position: absolute; inset-inline-start: 0; top: calc(100% + 4px);
  z-index: 60; min-inline-size: 280px; max-inline-size: 420px; }

/* toolbar (P1-FND-11, #57): filter/action rows wrap instead of overflowing.
   Component-class twin of the DS data-toolbar hook (aliased below 768 by
   components.stms.css, where the controls also share the width). */
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  margin-block-end: var(--space-3); min-inline-size: 0; }

/* cards + stat blocks (DS StatBlock) */
.card { background: var(--surface-card); border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md); padding: var(--space-4); box-shadow: var(--shadow-sm); /* token gap: wanted --elevation-1, using --shadow-sm (DS sync) */ }
/* P4-FND-01 — the DS Card (Docs/DS/components/data/Card.jsx) puts padding on
   .card__body, NEVER on the surface: that is what lets a list card's rows and a
   table's header rule reach the card's own edge. The padding above stays for the
   many cards that are genuinely one padded box; a card that has grown a head, a
   body or a foot gives it up here, so THE PRESENCE OF THE PARTS IS THE MODIFIER
   and a converted card needs no second class to remember (the retired opt-in
   `.card--sectioned` is what this replaces). A template missed by the sweep
   therefore degrades to exactly how it looks today, not to content jammed
   against a border. `:has()` is already the idiom here — see .daypill below.
   `:not(.modal)` is load-bearing: <dialog class="modal card"> borrows this
   surface and sets its own padding at `dialog.modal`, specificity (0,1,1),
   which a bare `.card:has(...)` at (0,2,0) would silently defeat. */
.card:not(.modal):has(> .card__head),
.card:not(.modal):has(> .card__body),
.card:not(.modal):has(> .card__foot),
.card:not(.modal):has(> form > .card__head),
.card:not(.modal):has(> form > .card__body) { padding: 0; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-3); }
.stat__label { font: var(--type-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--text-muted); }
.stat__value { font: var(--weight-semibold) var(--text-2xl)/1 var(--font-core);
  letter-spacing: var(--tracking-snug); color: var(--text-primary); margin-block-start: 4px; }
.stat__value--empty { color: var(--text-muted); }
/* Money in a stat tile. Direction only — NOT .cell-ltr, whose mono 12px would
   shrink the tile's headline to body size. Without this the tile inherits
   dir=rtl and "37,800 AED" renders as "AED · 37,800", the unit jumping ahead
   of the number (browser round, ar/ckb). Money must read left-to-right
   everywhere, not only inside table cells. */
.stat__value--ltr { direction: ltr; unicode-bidi: isolate; }
.stat__caption { font: var(--type-caption); color: var(--text-muted); }
/* A stat's second line: the name behind the number (P1-PLT-03 — the Overdue
   stat says WHO is worst-lapsed, not just how many). Caption-sized and muted
   so the value keeps the tile's visual weight. */
.stat__sub { font: var(--type-caption); color: var(--text-muted); margin-block-start: 2px; unicode-bidi: plaintext; }
.card__title { font: var(--type-body-strong); color: var(--text-primary);
  margin-block-end: var(--space-3); }

/* controls */
/* P1-FND-18: BASE control styling is set by a control's own class or a
   semantic element rule, NEVER through a layout-wrapper descendant
   selector. The retired rule read `.form-row input, .form-row textarea,
   .input` — that coupling is how `.input` survived as a phantom class,
   how all 17 ChoiceFields shipped as raw OS dropdowns, and how
   `.input--mono` got defeated on specificity (the shorthand at (0,1,1)
   beat the bare modifier at (0,1,0), see system.css). Classes arrive via
   src/core/forms.py's DSFormMixin; the guard (which strips comments before
   matching, so this paragraph is safe) is tests/test_ds_control_convention.py.
   (`.toolbar .select` below IS a wrapper-scoped descendant selector — it is
   a deliberate, narrow exception: a width CAP, not base styling, so it
   does not reintroduce the coupling this paragraph bans.) */
.input, .select { padding: var(--space-2); border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm); background: var(--surface-card);
  font: var(--type-body); color: var(--text-primary); }

/* Disabled/readonly: the retired `.form-row input` rule never set
   background/color, so the user-agent's own disabled grey showed through
   by default. `.input, .select` above now sets both unconditionally, which
   erased that affordance product-wide — found live on fees/forms.py's
   TierForm.name, disabled for the Default tier (P1-SET-01 ruling: its name
   is not renameable) and, without this rule, rendering indistinguishable
   from an editable field. */
.input:disabled, .select:disabled, .input[readonly] {
  background: var(--surface-sunken); color: var(--text-muted); cursor: not-allowed; }

/* A dropdown inside a form matches the inputs beside it (ratified: the
   420px .form-grid cap governs). The old 220px cap was a TOPBAR metric —
   it lives on where compact contexts ask for it: */
.toolbar .select, .select--compact { max-inline-size: 220px; }

/* Checkboxes and radios: element rules, deliberately — Django renders the
   option inputs of a RadioSelect/CheckboxSelectMultiple itself and no form
   mixin can reach them, and hand-written checkboxes in templates (the
   duplicate-student confirm) get the same treatment for free. They must
   NOT inherit the text-control box. */
input[type="checkbox"], input[type="radio"] {
  accent-color: var(--brand); inline-size: 16px; block-size: 16px;
  padding: 0; border: 0; }

/* File inputs: no text-control box; the picker button joins .btn's own
   selector list below so there is exactly one definition of the button
   look. The SET-01 label-button (settings.css .settings__upload) remains
   the reference for a PROMINENT upload; this is the default treatment. */
.file { padding: 0; border: 0; background: transparent; font: var(--type-body); }

/* .file::file-selector-button rides the SAME list — one definition, no
   drift. Baseline since 2021 (Chrome 89/FF 82/Safari 14.1); in a browser
   without it this whole list would drop, which is inside our support
   window's risk tolerance and recorded in the spec (§4). The pseudo gets
   the BASE box only — .btn--* modifiers never apply to a file picker. */
.btn, .file::file-selector-button { display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3); border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm); background: var(--surface-card);
  font: var(--type-body-strong); color: var(--text-primary); cursor: pointer; }
.btn--primary { background: var(--brand); border-color: var(--brand); color: var(--white); }
.btn--ghost { background: transparent; border-color: transparent; color: var(--text-muted); }
/* Ported from the DS Button (Docs/DS/components/core/Button.jsx) — both were
   used in shipped templates (fees, organization) with NO rule behind them, so
   they rendered as a bare .btn. Found by this file's phantom-modifier guard
   during P1-PLT-02's browser round (2026-08-15). The DS sizes by `height`;
   this port sizes by padding like its own .btn base, so the sm rule keeps
   that idiom rather than mixing two sizing models in one stylesheet. */
.btn--sm { padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }
.btn--secondary { background: var(--surface-card); color: var(--text-primary);
  border-color: var(--border-default); box-shadow: var(--shadow-sm); }
.btn--danger { background: var(--action-danger); border-color: var(--action-danger); color: var(--white); }
.btn--danger:hover { background: var(--action-danger-hover); border-color: var(--action-danger-hover); }
/* P1-FEE-05 browser round: NOTHING in this stylesheet styled a disabled
   button, so every `disabled` button in the product rendered identical to a
   live one and stayed genuinely inert. Measured on the students list's new
   bulk action: `disabled === true` with `opacity: 1; cursor: pointer;
   background: rgb(245,171,27)` — full brand fill.

   That is the P1-FND-19 phantom-vs-DEFEATED trap in its worst direction. The
   control LOOKS pressable, so a manager presses it, nothing happens, and the
   product has explained nothing. It is not new to this slice either:
   settings-pin.js's address button and the geocoder buttons have always
   shipped the attribute and always looked live.

   Same treatment as .input:disabled above, so a dead control reads dead
   whichever kind it is. `:disabled` matches only <button>/<input>, so an
   <a class="btn"> is untouched — a link cannot be disabled and must not
   pretend to be.

   NOT grouped with `.file::file-selector-button:disabled`, and that is the
   whole reason this note exists. The first version of this rule listed both
   selectors together; Chrome rejects `:disabled` after that pseudo-element,
   and **one invalid selector in a comma group discards the ENTIRE rule** —
   so the file on disk read correctly, `curl` served it correctly, and the
   browser's CSSOM simply did not contain it. Caught only by measuring
   `getComputedStyle` on the real button (opacity still 1). A grep-the-file
   guard cannot see this class of failure; keep the selector alone. */
.btn:disabled {
  opacity: 0.55; cursor: not-allowed; box-shadow: none; }
.icon { inline-size: 16px; block-size: 16px; flex: 0 0 auto; }

/* data tables (DS Drivers.jsx) */
.table { inline-size: 100%; border-collapse: collapse; font: var(--type-small); }
.table th { text-align: start; padding: var(--space-2) var(--space-3);
  border-block-end: 1px solid var(--border-default); font: var(--type-label);
  letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-muted); }
.table td { padding: var(--space-2) var(--space-3); border-block-end: 1px solid var(--border-subtle); }
.table tbody tr:hover { background: var(--surface-hover); }

/* Platform audit view: impersonation sub-log (P1-PLT-05 Task 10, 12 §6.2). */
.audit-row--session-header td { background: var(--surface-sunken);
  font: var(--type-small); }
.audit-row--session-header .badge { margin-inline-end: var(--space-2); }
.audit-row__session-caption { font: var(--type-caption); color: var(--text-muted);
  margin-inline-start: var(--space-2); }
.audit-row--nested td:first-child { padding-inline-start: var(--space-6); }
.audit-row--nested { background: var(--surface-sunken); }
/* Search destination highlight (P1-PLT-02 §5.3, review Finding 3): the org
   row a search result focused (?focus=) and, inside its expansion, the
   branch row it named (&branch=) — same --surface-selected token as the
   .stops-row--selected / .scope-switcher__item--current precedents, so a
   landed search result reads as selected rather than merely pre-expanded.
   Background only (not those precedents' border-inline-start too): a <tr>
   under this table's border-collapse:collapse does not reliably paint its
   own border in every engine, so the row-level cue stays to what a <tr>
   actually renders. */
.is-focused td, .is-focused:hover td,
.is-focused-branch td, .is-focused-branch:hover td {
  background: var(--surface-selected); }
.cell-ltr { direction: ltr; unicode-bidi: isolate; font: var(--type-mono); font-size: 12px; }
.cell-ltr--danger { color: var(--text-danger); font-weight: var(--weight-semibold); }

/* status badges (DS Badge; tones per Drivers.jsx statusTone) */
.badge { display: inline-flex; align-items: center; padding: 2px var(--space-2);
  border-radius: var(--radius-pill); font: var(--type-label); text-transform: capitalize; }
.badge--active { background: var(--status-enroute-bg); color: var(--status-enroute-fg);
  /* token gap: wanted --positive-fg/bg, using --status-enroute-* (DS sync) */ }
.badge--suspended { background: var(--status-emergency-bg); color: var(--status-emergency-fg);
  /* token gap: wanted --negative-fg/bg, using --status-emergency-* (DS sync) */ }
.badge--inactive { background: var(--status-completed-bg); color: var(--status-completed-fg);
  /* token gap: wanted --muted-fg/bg, using --status-completed-* (DS sync) */ }

/* subscription-status chips (P1-PLT-03 spec §D). The six statuses reuse the
   existing tones rather than invent colours: `active` and `suspended` are the
   two above, `degraded` is the warning tone already defined for the health
   probes below, and the three here complete the set. Semantics, not decoration:
   pilot is informational (a trial is neither good nor bad news), overdue is an
   alert (money is late), cancelled is spent/neutral like any other end-state. */
.badge--pilot { background: var(--info-bg); color: var(--info-fg); }
.badge--overdue { background: var(--status-emergency-bg); color: var(--status-emergency-fg);
  /* token gap: wanted --negative-fg/bg, using --status-emergency-* (DS sync) */ }
.badge--cancelled { background: var(--status-completed-bg); color: var(--status-completed-fg);
  /* token gap: wanted --muted-fg/bg, using --status-completed-* (DS sync) */ }

/* invoice-status chips (P1-PLT-07 spec §D). Two states only, and they reuse
   the same tones as everything above rather than invent colours: `paid` is
   settled (the positive tone `active` already carries), `unpaid` is simply
   outstanding — informational, NOT an alert, because an unpaid invoice issued
   today is the normal case and the lateness signal already lives on the
   subscription's own overdue/degraded chip. Written out even though the
   template composes the modifier dynamically (`badge--{{ inv.status }}`, which
   the static-modifier guard cannot check): a phantom class renders as a bare
   `.badge` and nothing would have caught it — the P1-FND-19 lesson. */
.badge--paid { background: var(--status-enroute-bg); color: var(--status-enroute-fg);
  /* token gap: wanted --positive-fg/bg, using --status-enroute-* (DS sync) */ }
.badge--unpaid { background: var(--info-bg); color: var(--info-fg); }

/* fee-status chips. The DS has carried --fee-paid/-unpaid/-partial/-waived
   tokens since the token port, and three fee templates have composed
   `badge--fee-{{ record.status }}` since P1-FEE-02 — against rules that were
   never written. The P1-FND-19 phantom-class trap again, and again invisible:
   a missing modifier renders as a bare `.badge`, which looks like a plain
   chip rather than like a bug. Found while building P2-WEB-03, whose parent
   card shows the same chip. These are the only DS tokens on this page that
   needed no "token gap" note — the design system named all four. */
.badge--fee-paid { background: var(--fee-paid-bg); color: var(--fee-paid-fg); }
.badge--fee-unpaid { background: var(--fee-unpaid-bg); color: var(--fee-unpaid-fg); }
.badge--fee-partial { background: var(--fee-partial-bg); color: var(--fee-partial-fg); }
.badge--fee-waived { background: var(--fee-waived-bg); color: var(--fee-waived-fg); }

/* system-health probe states (P1-PLT-02 §4). Same token mapping as the
   sibling status chips above — the health page had been rendering these as
   bare text with no visual signal at all (browser round 2026-08-15).
   `note` is the third state that is neither up nor absent: the ws worker is
   deployed with an empty routing table until P2, and both a green tick and
   a red cross would be lies in opposite directions. */
.badge--up { background: var(--status-enroute-bg); color: var(--status-enroute-fg);
  /* token gap: wanted --positive-fg/bg, using --status-enroute-* (DS sync) */ }
.badge--degraded { background: var(--status-boarding-bg); color: var(--status-boarding-fg);
  /* token gap: wanted --warning-fg/bg, using --status-boarding-* (DS sync) */ }
.badge--down { background: var(--status-emergency-bg); color: var(--status-emergency-fg);
  /* token gap: wanted --negative-fg/bg, using --status-emergency-* (DS sync) */ }
.badge--note { background: var(--info-bg); color: var(--info-fg); }
/* P4-ANN-02: the announcement class. The SAME status-emergency pair the
   driver's incident wears, because they are the same class of thing to the
   reader — one declared by a driver, one by the office — and a list that
   coloured them differently would be teaching two vocabularies for one
   idea. Only the emergency class gets a badge; a normal announcement is
   the unmarked default, so the mark means something. */
.badge--emergency { background: var(--status-emergency-bg); color: var(--status-emergency-fg); }

/* document expiry-state chips (12 §1.2, P1-DRV-03) */
.badge--valid { background: var(--status-enroute-bg); color: var(--status-enroute-fg);
  /* token gap: wanted --positive-fg/bg, using --status-enroute-* (DS sync) */ }
.badge--expiring { background: var(--status-boarding-bg); color: var(--status-boarding-fg);
  /* token gap: wanted --warning-fg/bg, using --status-boarding-* (DS sync) */ }
.badge--expired { background: var(--status-emergency-bg); color: var(--status-emergency-fg);
  /* token gap: wanted --negative-fg/bg, using --status-emergency-* (DS sync) */ }

/* announcement lifecycle chips (P1-ANN-01). Scheduled reuses the info tone
   (a future publish is a plan, not a warning); Expired reuses .badge--expired
   above rather than inventing a second "past its date" colour. */
.badge--scheduled { background: var(--info-bg); color: var(--info-fg); }

/* incident severity + status chips (12 §1.3, P1-DRV-04) */
.badge--critical { background: var(--status-emergency-bg); color: var(--status-emergency-fg);
  /* token gap: wanted --negative-fg/bg, using --status-emergency-* (DS sync) */ }
.badge--high { background: var(--status-emergency-bg); color: var(--status-emergency-fg);
  /* token gap: wanted --negative-fg/bg, using --status-emergency-* (DS sync) */ }
.badge--medium { background: var(--status-boarding-bg); color: var(--status-boarding-fg);
  /* token gap: wanted --warning-fg/bg, using --status-boarding-* (DS sync) */ }
.badge--low { background: var(--status-completed-bg); color: var(--status-completed-fg);
  /* token gap: wanted --muted-fg/bg, using --status-completed-* (DS sync) */ }
.badge--open { background: var(--status-boarding-bg); color: var(--status-boarding-fg);
  /* token gap: wanted --warning-fg/bg, using --status-boarding-* (DS sync) */ }
.badge--under_review { background: var(--info-bg); color: var(--info-fg); }
.badge--resolved { background: var(--status-enroute-bg); color: var(--status-enroute-fg);
  /* token gap: wanted --positive-fg/bg, using --status-enroute-* (DS sync) */ }
.badge--dismissed { background: var(--status-completed-bg); color: var(--status-completed-fg);
  /* token gap: wanted --muted-fg/bg, using --status-completed-* (DS sync) */ }
.badge--platform { background: var(--info-bg); color: var(--info-fg); }

/* driver flag chips (12 §1.3, P1-DRV-05) */
.badge--watch { background: var(--status-boarding-bg); color: var(--status-boarding-fg);
  /* token gap: wanted --warning-fg/bg, using --status-boarding-* (DS sync) */ }
.badge--no_solo_trips { background: var(--status-emergency-bg); color: var(--status-emergency-fg);
  /* token gap: wanted --negative-fg/bg, using --status-emergency-* (DS sync) */ }

/* bus condition chips (12 §1.3, P1-BUS-01) */
.badge--excellent { background: var(--status-enroute-bg); color: var(--status-enroute-fg);
  /* token gap: wanted --positive-fg/bg, using --status-enroute-* (DS sync) */ }
.badge--good { background: var(--status-enroute-bg); color: var(--status-enroute-fg);
  /* token gap: wanted --positive-fg/bg, using --status-enroute-* (DS sync) */ }
.badge--needs_service { background: var(--status-boarding-bg); color: var(--status-boarding-fg);
  /* token gap: wanted --warning-fg/bg, using --status-boarding-* (DS sync) */ }
.badge--out_of_service { background: var(--status-emergency-bg); color: var(--status-emergency-fg);
  /* token gap: wanted --negative-fg/bg, using --status-emergency-* (DS sync) */ }

/* tabs (ported from DS components/data/Tabs.jsx, underline variant) */
.tabs { display: flex; gap: var(--space-1);
  border-block-end: 1px solid var(--border-default); }
.tab { display: inline-flex; align-items: center; gap: var(--space-15);
  block-size: 36px; padding: 0 var(--space-3); border: 0; background: transparent;
  font: var(--type-body-strong); color: var(--text-muted); text-decoration: none;
  cursor: pointer; position: relative; }
.tab:hover { color: var(--text-primary); }
.tab--active { color: var(--text-primary); }
.tab--active::after { content: ""; position: absolute; inset-inline: 0;
  inset-block-end: -1px; block-size: 2px; background: var(--brand); }
.tab__count { font: var(--type-caption); color: var(--text-muted);
  background: var(--surface-active); border-radius: var(--radius-pill);
  padding: 1px var(--space-15); }

/* identity KV card (driver profile Overview) */
.kv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.kv__k { font: var(--type-caption); color: var(--text-muted); }
.kv__v { font: var(--type-body); color: var(--text-primary); margin-block-start: 2px; }

/* form rows (driver create/edit) */
.form-grid { display: flex; flex-direction: column; gap: var(--space-3); max-inline-size: 420px; }
.form-row { display: flex; flex-direction: column; gap: var(--space-1); }
/* [hidden] guard (P1-ANN-01 browser round defect A) — .form-row sets
   display:flex, which wins the specificity race against the UA
   [hidden]{display:none} rule, so JS setting the `hidden` attribute on a
   .form-row (e.g. the announcement composer's audience rows) has zero
   visual effect without this. Fourth occurrence of this exact defect class
   in this codebase — see route-editor.css's "[hidden] guard (defect D1)",
   .upload__error[hidden] and .scope-switcher__menu[hidden] above. */
.form-row[hidden] { display: none; }
.form-row label { font: var(--type-body-strong); }
.form-error { color: var(--text-danger); font: var(--type-caption); }

/* rider picker inside the stop modal (P1-STU-05 Task 8) — logical properties
   only: `ar` and `ckb` are both RTL. */
.rider-current { list-style: none; margin: 0; padding: 0;
                 display: flex; flex-direction: column; gap: var(--space-1); }
.rider-current__item { display: flex; align-items: center; gap: var(--space-2);
                       font: var(--type-body); color: var(--text-primary); }
.rider-list { max-block-size: 220px; overflow-y: auto;
              border: 1px solid var(--border-subtle);
              border-radius: var(--radius-sm);
              padding: var(--space-2);
              display: flex; flex-direction: column; gap: var(--space-1); }
.rider-row { display: flex; align-items: center; gap: var(--space-2);
             font: var(--type-body); }
/* FIFTH occurrence of the [hidden] defect class (see the .form-row guard
   above). .rider-row and .rider-list__empty both set their own `display`,
   which beats the UA [hidden]{display:none} rule — without these two lines
   the filter would tick every box in the DOM and hide nothing on screen. */
.rider-row[hidden], .rider-list__empty[hidden] { display: none; }
.rider-row__name { flex: 1 1 auto; unicode-bidi: plaintext; }
.rider-row__tag { font: var(--type-caption); color: var(--text-muted); }
.rider-list__empty { display: block; }

/* inline form notices (P1-STU-01 Task 8: guardian phone-lookup reuse
   affordance) — no notice/hint class existed yet, so this composes the
   existing --info-bg/--info-fg semantic tokens (already used by
   .badge--under_review) rather than inventing a new color. */
/* unicode-bidi:plaintext (P1-ANN-01 browser round defect B): a hint string
   can be plain Latin text (untranslated nouns + digits, or a full English
   sentence) sitting inside an RTL (ar/ckb) paragraph. Without this, the run
   inherits the parent's RTL base direction and the UBA reorders weak
   characters at its edges — a leading digit or a trailing full stop jumps
   to the wrong side. plaintext resolves direction from the run's OWN first
   strong character instead, so it self-corrects once these strings are
   translated (Arabic first-strong-char -> RTL) rather than needing a
   second fix. Same idiom as the DS's Toast.jsx .zs-toast__body.

   THIRD occurrence, P1-STU-06 browser round: the fix above was copied
   per-component and never shared, so a new slice had nothing to reach for and
   reintroduced the defect on the vehicle detail page ("of 30 seats on this
   date · 1 attendant 29", and a full stop jumping to the head of the
   attendants card's sentence). `.bidi-plaintext` below is now THE place to
   reach for: put it on any element whose text can be a whole Latin sentence
   or can begin with a number. .form__hint and .toast__body predate the
   utility and keep their own declaration — they work, and rewriting them
   would widen a fix diff for no behaviour change. */
.bidi-plaintext { unicode-bidi: plaintext; }

/* P1-FND-40 — the two user-panel runs, declared HERE rather than in
   identity.css. That file is vendored VERBATIM from the brand package
   (tests/test_theme_dark.py::test_identity_css_is_vendored_verbatim asserts
   it byte-for-byte), so editing it in place is not a style preference — it
   breaks a guard, and it silently forks a file the designer re-delivers.

   No cascade fight: identity.css declares neither property on either
   selector, so there is nothing here to lose to it. And when it comes first
   in the load order (components.css, then identity.css), a future delivery
   that DOES declare them wins — which is the right way round, because the
   vendored file is the authority on its own component.

   .userpanel__name gets `plaintext`: it is a person's name, which stays Latin
   for a Latin-named user and becomes RTL for a Kurdish-named one.
   .userpanel__code is a two-letter language code — always LTR, like
   .cell-ltr, never "whatever its first character suggests". */
.userpanel__name { unicode-bidi: plaintext; }
.userpanel__code { direction: ltr; unicode-bidi: isolate; }
/* P4-SEC-04: the privacy row is an <a> (a link, not a POST); base.css
   underlines links. Here for the same vendored-verbatim reason as above. */
a.userpanel__row { text-decoration: none; border-bottom: 0; }

/* P1-FND-40, the repo-wide sweep. The utility above is for one-off runs in
   markup. Where a CLASS guarantees the kind of content it holds, the
   declaration belongs on the class itself, so a new call site inherits the fix
   instead of needing an author who already knows about the trap. That is the
   difference between this pass and the four private copies that preceded it.

   ⚠️ WHAT IT IS APPLIED TO, AND WHY THE LINE IS DRAWN THERE.
   Only classes holding a run that STAYS Latin after translation — a person's
   name, an organization or school name, a vehicle code, an address, a date:

     .topbar__title      every {% block page_title %} in the product
     .kv__v              the value half of every key/value row
     .stat__sub          P1-PLT-03: the NAME behind the number
     .rider-row__name · .org-chip__name · .scope-switcher__item-name/-sub
     .userpanel__name (identity.css) · .reassign-group__title (route-editor.css)

   NOT applied to translatable PROSE — .card__title, .modal__title,
   .topbar__subtitle, .form-error, .tab__count, the .sys-* system-page
   sentences, .set-row__hint, .settings__subtitle. The atlas item's own
   sequencing says to prioritise the runs that stay Latin, and a browser round
   in ckb showed exactly why: `plaintext` resolves the element's DIRECTION from
   its first strong character, and `text-align: start` follows direction — so
   an English card heading in an RTL page stops hugging the page's start edge
   and jumps to the other side. Every card title in the product moved. The
   misplaced full stop those strings show today is the smaller defect, and
   P1-FND-31 removes it at the root by translating the string, after which the
   heading resolves RTL on its own and needs nothing from us.

   NOT touched either: .cell-ltr, .userpanel__phone, .userpanel__code and
   .stat__value--ltr already carry `direction: ltr` + `unicode-bidi: isolate`,
   which is the STRONGER and correct answer — a plate, a time, a coordinate, a
   phone and a language code are ALWAYS left-to-right, never "whatever their
   first character suggests".

   .table td gets nothing. A cell that also holds badge chrome would resolve
   its direction from whatever strong character a badge label began with (the
   P1-STU-06 worked example), so table cells are wrapped per column in the
   template with .bidi-plaintext on an INNER element. */

.form__hint { padding: var(--space-2); border-radius: var(--radius-sm); font: var(--type-caption);
  unicode-bidi: plaintext; }
.form__hint--info { background: var(--info-bg); color: var(--info-fg); }

/* P1-FND-33: the DS Toast (Docs/DS/components/feedback/Toast.jsx), ported
   token-for-token onto the existing .toast/.toasts names — the vendored
   components.stms.css phone overrides ("stop being a fixed 340px") and
   ui.js's selectors bind to these names, so the names stay and only the
   rules change. Tone drives the MARK colour only; the surface is always
   inverse. `emergency` is reserved (Toast.d.ts): styled here, emitted by
   nothing until P4 — `error` keeps its own red circle-alert, never siren. */
.toast { display: flex; align-items: flex-start; gap: var(--space-3);
  width: 340px; max-width: 100%;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
  background: var(--surface-inverse); color: var(--text-inverse);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
  animation: toast-in var(--dur-base) var(--ease-out); }
.toast__mark { flex: 0 0 auto; margin-block-start: 1px; color: var(--ink-300); }
.toast__mark .icon { inline-size: 18px; block-size: 18px; }
.toast__content { flex: 1 1 auto; min-inline-size: 0; }
.toast__title { font: var(--type-body-strong); }
.toast__body { font: var(--type-small); color: var(--text-inverse-muted);
  margin-block-start: 1px; unicode-bidi: plaintext; }
.toast__actions { margin-block-start: var(--space-2); display: flex; gap: var(--space-3); }
.toast__link { font: var(--type-small); font-weight: var(--weight-medium);
  color: var(--amber-300); }
.toast__link:hover { color: var(--amber-200); }
.toast__close { flex: 0 0 auto; background: none; border: 0; padding: 2px;
  cursor: pointer; border-radius: var(--radius-sm);
  color: var(--text-inverse-muted); }
.toast__close:hover { background: rgb(255 255 255 / .10);
  color: var(--text-inverse); }
.toast--success .toast__mark { color: var(--green-500); }
.toast--error .toast__mark { color: var(--red-500); }
.toast--warning .toast__mark { color: var(--amber-300); }
.toast--emergency .toast__mark { color: var(--red-500); }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; } }

/* toast region — body-level so out-of-band toasts survive content swaps.
   Logical properties throughout: ar and ckb are RTL (P1-FND-05). The DS
   stack: space-5 insets, space-2 gap, z-index 90 (above sticky chrome;
   native <dialog> sits in the top layer regardless). */
.toasts { position: fixed; inset-block-end: var(--space-5); inset-inline-end: var(--space-5);
  z-index: 90; display: flex; flex-direction: column; gap: var(--space-2); }
.toasts:empty { display: none; }

/* upload field affordances (P1-DRV-03, upload-field.js). Client-side only —
   the server independently re-validates whatever arrives; these are just the
   visible progress/retry states while an XHR is in flight or has failed. */
.upload__progress { inline-size: 100%; block-size: var(--space-2); border-radius: var(--radius-pill);
  accent-color: var(--brand); }
.upload__error { display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3); border: 1px solid var(--action-danger);
  border-radius: var(--radius-sm); color: var(--text-danger); font: var(--type-caption); }
.upload__error[hidden] { display: none; }

/* crop dialog (P1-DRV-03). Layers above the form dialog; logical properties
   throughout so ar/ckb (RTL) render correctly (P1-FND-05). */
dialog.crop-modal { border: none; padding: var(--space-4);
  inline-size: min(480px, calc(100vw - 2 * var(--space-4)));
  max-block-size: calc(100vh - 2 * var(--space-4)); overflow-y: auto; }
dialog.crop-modal::backdrop { background: rgb(0 0 0 / 45%); }
.crop-modal__stage { max-inline-size: 100%; max-block-size: 320px; overflow: hidden; }
.crop-modal__stage img { max-inline-size: 100%; display: block; }
.crop-modal__controls { display: flex; gap: var(--space-2); margin-block-start: var(--space-3);
  align-items: center; }
.crop-modal__controls .btn { padding-inline: var(--space-2); }
.crop-modal__actions { display: flex; gap: var(--space-2); margin-block-start: var(--space-4);
  justify-content: flex-end; }

/* modal forms (P1-FND-05). Native <dialog>: ESC, focus trap and inertness
   come from the platform. Simple single-object forms only — formsets and
   multi-step flows stay full-page (15 §1 threshold rule). */
dialog.modal { border: none; padding: var(--space-4); inline-size: min(420px, calc(100vw - 2 * var(--space-4)));
  max-block-size: calc(100vh - 2 * var(--space-4)); overflow-y: auto; }
dialog.modal::backdrop { background: rgb(0 0 0 / 45%); }
.modal__title { font: var(--type-h3); margin-block-end: var(--space-3); }
/* P4-FND-01: a BOUNDING CONTAINER owns the measure — the 420px cap below is
   for a form that IS the page, with nothing else shaping its width. Written
   positively (naming the containers) rather than as a growing list of opt-outs,
   which is how the sectioned-card case came to be missed until P4-ANN-03. */
.modal .form-grid, .card .form-grid { max-inline-size: none; }
.modal__actions { display: flex; gap: var(--space-2); margin-block-start: var(--space-4); }

/* --- P1-ORG-01: Alert (kit components/feedback/Alert) — ALWAYS tinted;
   solid blue-700 is reserved for the org scope marker (fill law). --- */
.alert { display: flex; align-items: flex-start; gap: var(--space-3);
  padding: var(--space-3) var(--space-4); border: 1px solid; border-radius: var(--radius-md); }
.alert--info { background: var(--info-bg-soft); border-color: var(--blue-100); color: var(--info-fg); }
.alert--warning { background: var(--orange-050); border-color: var(--orange-100); color: var(--orange-700); }
.alert--danger { background: var(--red-050); border-color: var(--red-100); color: var(--red-700); }
.alert--success { background: var(--green-050); border-color: var(--green-100); color: var(--green-700); }
.alert__title { font: var(--type-body-strong); color: var(--text-primary); }
.alert__body { font: var(--type-small); color: var(--text-secondary); margin-top: 1px; }

/* Badge role tones — outline modifier, hairline no fill, so a role never
   reads as a state (kit Badge). */
.badge--outline { background: transparent;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 45%, transparent); }
.badge--role-orgadmin { color: var(--scope-org); }
.badge--role-manager { color: var(--ink-700); }
.badge--role-member { color: var(--ink-500); }
/* Solid org scope badge — org-mode chrome only. */
.badge--org { background: var(--scope-org); color: var(--scope-org-on); }

/* ScopeSwitcher (kit Chrome.jsx §4.1) */
.scope-switcher { position: relative; display: flex; align-items: center; }
.scope-switcher__btn { display: flex; align-items: center; gap: var(--space-2); height: 32px;
  padding-inline: var(--space-3) var(--space-2); cursor: pointer; border-radius: var(--radius-md);
  font: var(--type-small); font-weight: var(--weight-medium); max-width: 280px;
  border: 1px solid var(--border-default); background: var(--surface-card); color: var(--text-primary); }
.scope-switcher__btn--org { border-color: transparent; background: var(--scope-org); color: var(--scope-org-on); }
.scope-switcher__btn--icon { width: 32px; height: 32px; padding: 0; display: grid;
  place-items: center; color: var(--scope-org); }
.scope-switcher__btn--icon.scope-switcher__btn--org { color: var(--scope-org-on); }
.scope-switcher__menu { position: absolute; top: calc(100% + 4px); inset-inline-end: 0;
  width: 320px; max-width: calc(100vw - 24px); z-index: 60; background: var(--surface-card);
  border: 1px solid var(--border-subtle); border-radius: var(--radius-md);
  box-shadow: var(--shadow-md); padding: var(--space-2);
  display: flex; flex-direction: column; gap: 2px; }
.scope-switcher__menu[hidden] { display: none; }
.scope-switcher__group { font: var(--type-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--text-muted); padding: var(--space-1) var(--space-2); }
.scope-switcher__item { display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2); border: 0; cursor: pointer; border-radius: var(--radius-sm);
  text-align: start; background: transparent; width: 100%; }
.scope-switcher__item:hover:not(:disabled) { background: var(--surface-hover); }
.scope-switcher__item--current { background: var(--surface-selected); }
.scope-switcher__item:disabled { cursor: not-allowed; }
.scope-switcher__item:disabled .scope-switcher__item-name { color: var(--text-muted); }
.scope-switcher__org-tile { width: 28px; height: 28px; flex: 0 0 auto; border-radius: var(--radius-sm);
  background: var(--scope-org); color: var(--scope-org-on); display: grid; place-items: center; }
.scope-switcher__item-name { font: var(--type-small); font-weight: var(--weight-medium); unicode-bidi: plaintext; }
.scope-switcher__item-sub { font: var(--type-caption); color: var(--text-muted); unicode-bidi: plaintext; }

/* The P1-FND-16 topbar language switcher (.lang-switcher) is retired
   (P1-FND-34): language now lives in the user panel (identity.css
   .userpanel rows) and on the system shell (.syslang). Its
   never-share-the-scope-switcher's-class-names lesson carries over: the
   identity classes are unique to that component family, guard-tested. */

/* P1-FND-34 port adaptations for identity.css — the kit's card is a <button>
   with aria-expanded + React state; our port is a <details>, same as the
   scope switcher, so open/close costs no JS. These rules only re-target the
   kit's own visual states (identity.css .usercard:hover /
   [aria-expanded="true"] / __chev transform) at the details mechanism —
   no new visual style is invented here. */
.usercard-slot > summary { list-style: none; }
.usercard-slot > summary::-webkit-details-marker { display: none; }
.usercard-slot[open] > .usercard { background: color-mix(in srgb, var(--white) 8%, transparent); }
.usercard-slot[open] .usercard__chev { transform: none; }

/* Org sidebar chip + scope callout (kit Chrome.jsx Sidebar inOrg) */
.org-chip { margin: 0 var(--space-1); padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm); background: var(--scope-org); color: var(--scope-org-on);
  display: flex; flex-direction: column; gap: 2px; overflow: hidden; }
.org-chip__label { font: var(--type-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; display: flex; align-items: center; gap: var(--space-15); }
.org-chip__name { font: var(--type-body-strong); unicode-bidi: plaintext; }
.org-callout { padding: var(--space-3); border-radius: var(--radius-sm);
  background: var(--scope-org-soft); border: 1px solid var(--scope-org-soft-border); }
.org-callout__label { font: var(--type-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--blue-100); }
.org-callout__body { font: var(--type-caption); color: var(--ink-300); margin-top: 4px; }

/* P1-PLT-05: the impersonation banner — always-on, sticky, solid danger
   fill (an admin acting as someone else is the one state in this app that
   must never be missable). Logical properties only (inset-inline, not
   left/right): both ar and ckb are RTL locales and this banner sits above
   every page, including RTL ones. */
/* P1-PLT-08: the school's own money state. A notice, never solid (semantic.css
   rule) — the solid-danger treatment belongs to impersonation, which is a
   security state, not a bill. Payment tones reuse the fee tokens so "overdue"
   looks the same here as it does on a fee row. */
.billing-banner { display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-4); border-block-end: 1px solid var(--border-subtle); }
.billing-banner__text { font: var(--type-small); font-weight: var(--weight-medium); }
.billing-banner--warning { background: var(--fee-partial-bg); color: var(--fee-partial-fg); }
.billing-banner--danger { background: var(--fee-unpaid-bg); color: var(--fee-unpaid-fg); }
.billing-banner--info { background: var(--info-bg); color: var(--info-fg); }
.impersonation-banner { position: sticky; inset-block-start: 0; z-index: 100;
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-2) var(--space-4); background: var(--action-danger);
  color: var(--text-inverse); }
.impersonation-banner__text { font: var(--type-small); font-weight: var(--weight-medium); }
.impersonation-banner .btn--danger { background: var(--status-emergency-bg);
  border-color: var(--status-emergency-bg); color: var(--status-emergency-fg); }
.impersonation-banner .btn--danger:hover { background: var(--white); border-color: var(--white); }

/* ===========================================================================
   P1-FND-38 — rules for classes the templates had always cited and no
   stylesheet had ever defined.

   These are PHANTOM classes, not defeated ones: there was no rule anywhere to
   lose a specificity fight with, so every one of these elements has rendered
   as plain unstyled text since the day it was written, and nobody noticed
   because unstyled text still reads. `form-help` alone stood at fourteen call
   sites across nine templates. The repo's own guard
   (tests/test_ds_control_convention.py) only checked that `btn--` and
   `badge--` MODIFIER prefixes resolve, so a class using neither prefix was
   invisible to every control the repo owned; tests/test_ds_classes.py is the
   widened guard that makes this a control rather than a one-time sweep.
   =========================================================================== */

/* Field help text: the muted sibling of .form-error, same single-dash family.
   NOT .form__hint — that is the boxed, padded callout with an --info variant,
   a different thing from the caption under a control. Carries the bidi fix
   directly (P1-FND-40) because help text is full-sentence prose. */
.form-help { font: var(--type-caption); color: var(--text-muted);
  unicode-bidi: plaintext; }
/* A <span> standing in for a label above a control GROUP, where no single
   <input> owns the text and a <label for> would therefore be a lie. */
.form-label { font: var(--type-body-strong); }

/* The lead paragraph of a modal body — the sentence naming the object the
   dialog acts on, above the form. Eighteen call sites, eleven templates. */
.modal__lead { font: var(--type-body); color: var(--text-secondary);
  margin: 0 0 var(--space-3); unicode-bidi: plaintext; }

/* A muted caption paragraph outside a form (a status line, a no-matches note). */
.muted { font: var(--type-caption); color: var(--text-muted);
  unicode-bidi: plaintext; }

/* An empty-state line inside a card. */
.empty { font: var(--type-caption); color: var(--text-muted);
  margin: var(--space-3) 0 0; unicode-bidi: plaintext; }

/* An empty list's one door (P1-FND-49): the fact, then the act that fills
   the list. Lives inside the table's single cell, so it is a row, not a card. */
.empty-state { display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: wrap; padding: var(--space-3) 0; }

/* An unadorned vertical list: no marker, no indent, rows spaced by the scale. */
.list { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--space-1); }

/* The text run inside .upload__error. The box already owns the colour and the
   border; this owns nothing but the run's own direction, because a filename
   or a scanner message is Latin inside an RTL page (P1-FND-40). */
.upload__error-text { unicode-bidi: plaintext; }

/* Card header: the card's NAME and the controls that change what it shows —
   never the act that changes the object, which belongs in the foot (P4-FND-01
   §3). .card__title carries a bottom margin for the headline-then-body case,
   which is wrong once it sits in a flex row. The subtitle sits UNDER the title
   inside the same block, as the DS does it, not beside it. */
.card__head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); flex-wrap: wrap; padding: var(--space-3) var(--space-4);
  border-block-end: 1px solid var(--border-subtle); }
.card__head .card__title { margin-block-end: 0; display: flex; align-items: center;
  gap: var(--space-2); }
.card__sub { font: var(--type-caption); color: var(--text-muted); }
.card__actions { margin-inline-start: auto; display: flex; align-items: center;
  gap: var(--space-1); }
.card__body { padding: var(--space-4); }
/* A table or a row list: no body padding, so the rules span the card and the
   rows stop paying for the box's inset twice. */
.card__body--tight { padding: 0; }
.card__foot { padding: var(--space-3) var(--space-4);
  border-block-start: 1px solid var(--border-subtle);
  display: flex; align-items: center; gap: var(--space-2); }
/* The CAPTION claims the free space, so "caption … action" reads the same in
   ltr and rtl without either being positioned by hand — and a foot holding
   only a caption still starts at the start. The shorter `> :last-child
   { margin-inline-start: auto }` looks equivalent and is not: it pushes a lone
   caption to the far end, which is where the live bus card's "No position"
   fault line would have landed. */
.card__foot > .card__caption, .card__foot > .form__hint { margin-inline-end: auto; }
/* A foot carrying no caption: its buttons group at the end, as the kit's
   settings cards do with Save changes. */
.card__foot--end { justify-content: flex-end; }
.card__caption { font: var(--type-caption); color: var(--text-muted); }
/* SIXTH occurrence of the [hidden] defect class (see the .form-row guard and
   .rider-row above). .card__head/.card__body/.card__foot each declare `display`,
   which silently beats the user agent's own [hidden]{display:none} — so a card
   part a script hides by setting the attribute would stay on screen. It is not
   hypothetical here: the home-pin card's Save foot is toggled exactly that way,
   and without this line it would have shipped permanently visible. */
.card__head[hidden], .card__body[hidden], .card__foot[hidden],
.card__actions[hidden], .card__row[hidden] { display: none; }
/* Rows inside a tight body (the announcement list, the fee list): one hairline
   BETWEEN rows, never above the first — the head's rule is already there, and
   `+` gets that right without the template counting `forloop.first`. */
.card__row { padding: var(--space-3) var(--space-4); }
.card__row + .card__row { border-block-start: 1px solid var(--border-subtle); }
/* A row that puts its content in a line: the announcement's title block beside
   its meta, the fee row's name beside its amount. */
.card__row--split { display: flex; gap: var(--space-3); align-items: flex-start; }

/* The spelled-out consequences of a confirm-only dialog (students archive). */
.dialog__consequences { margin: var(--space-2) 0 0;
  padding-inline-start: var(--space-4);
  font: var(--type-caption); color: var(--text-secondary); }
.dialog__consequences li { margin-block-start: var(--space-1); }

/* The audit row's diff disclosure (P1-SEC-01). A bare <details> summary shows
   a text cursor and no affordance that it opens. */
.audit-diff > summary { cursor: pointer; font: var(--type-caption);
  color: var(--text-muted); }
.audit-diff[open] > summary { margin-block-end: var(--space-1); }

/* Fee cards (P1-FEE-02 / P1-FEE-03). The whole row component was written with
   names and never given rules, so a student's fee history rendered as an
   undifferentiated run of text with the payments indistinguishable from the
   fees they belong to. */
/* P4-FND-01: the inline inset used to be BORROWED from the card box's padding,
   which is why these rows could not live in a tight body — their hairlines
   stopped short of the card's edge, the exact defect the sweep exists to fix.
   The row owns both axes now, so the fee cards are list cards like the rest. */
.fee-row { border-block-start: 1px solid var(--border-subtle);
  padding-block: var(--space-3); padding-inline: var(--space-4); }
.fee-row:first-of-type { border-block-start: 0; }
.fee-row__head { display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-2); }
.fee-row__actions { display: inline-flex; flex-wrap: wrap; gap: var(--space-2);
  margin-inline-start: auto; }
.fee-row__note { margin: var(--space-1) 0 0; font: var(--type-caption);
  color: var(--text-muted); unicode-bidi: plaintext; }
/* A payment belongs to the fee above it, so it is indented behind a rule
   rather than floated as a sibling. */
.fee-payment { display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-2); margin-block-start: var(--space-2);
  padding-inline-start: var(--space-3);
  border-inline-start: 2px solid var(--border-subtle);
  font: var(--type-caption); }
.fee-payment--voided { color: var(--text-muted); text-decoration: line-through; }

/* Feature-flag matrix (P1-PLT-04). `.flag-toggle` is the load-bearing one:
   the markup is an EMPTY <button>, so with no rule it had no content, no size
   and no colour — an un-clickable nothing where the switch should be. The
   matrix shipped, and its central control has never been visible. */
.flags-matrix th { vertical-align: top; }
.flag-key { font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--text-primary); }
.flag-gates { display: block; font: var(--type-caption); color: var(--text-muted); }
/* P1-PLT-04's pending-marker law: a flag that is defined but not yet enforced
   must SAY so. Amber, not red — it is a state of the roadmap, not a fault. */
.flag-pending { display: block; font: var(--type-caption);
  color: var(--amber-700); }
.flags-org-row > td { background: var(--surface-sunken);
  font: var(--type-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--text-muted); }
.flag-cell { display: flex; align-items: center; gap: var(--space-2); }
.flag-toggle { inline-size: 36px; block-size: 20px; padding: 0;
  border: 1px solid var(--border-default); border-radius: var(--radius-pill);
  background: var(--surface-sunken); cursor: pointer; position: relative;
  transition: background var(--dur-fast) var(--ease-out); }
.flag-toggle::after { content: ""; position: absolute; inset-block-start: 2px;
  inset-inline-start: 2px; inline-size: 14px; block-size: 14px;
  border-radius: 50%; background: var(--surface-card);
  box-shadow: var(--shadow-sm);
  transition: inset-inline-start var(--dur-fast) var(--ease-out); }
.flag-toggle--on { background: var(--brand); border-color: var(--brand); }
.flag-toggle--on::after { inset-inline-start: 18px; }
.flag-toggle:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset); }
/* The override pin: this school's value is set by hand and does NOT follow the
   default. A button carrying one emoji, so it needs no box of its own. */
.flag-pin { background: none; border: 0; padding: 0; cursor: pointer;
  font-size: var(--text-xs); line-height: 1; }

/* The expandable branch drawer under an organization row (P1-PLT-02 /
   P1-PLT-07). Flush, because whatever it holds brings its own padding, and
   sunken so the drawer reads as inside the row above it rather than as a
   sibling row of the table. */
.org-branches, .sub-branches { padding: 0; background: var(--surface-sunken); }
.org-branches:empty, .sub-branches:empty { display: none; }

/* The weekday multi-select (P2-SET-01, from the 2026-08-22 handoff). A new
   control shape: the DS control layer had no multi-select of this kind.

   Content-sized pills that WRAP. Never a fixed seven-across grid and never
   truncation — Kurdish day names are long (چوارشەممە), and a row that fits in
   English does not fit in ckb. Wrapping is the overflow strategy.

   The row is a plain flex line with no explicit direction, so it flips with
   `dir` for ar/ckb without a second rule. The week still starts on Sunday in
   all three languages; that order is the template's, not the CSS's. */
.daypills { display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin-block-start: var(--space-3); }
.daypill { display: inline-flex; align-items: center; cursor: pointer;
  padding: 6px 14px; border-radius: var(--radius-pill);
  border: 1px solid var(--border-default); background: var(--surface-card);
  color: var(--text-secondary); font: var(--type-body-strong);
  user-select: none; }
/* The checkbox itself is the accessible control and the state; it is taken out
   of the flow rather than display:none so it stays focusable and reachable by
   a screen reader. The visible pill is the label around it. */
.daypill input { position: absolute; opacity: 0; inline-size: 1px;
  block-size: 1px; }
.daypill--on { border-color: var(--brand); background: var(--surface-selected);
  color: var(--brand); }
.daypill:has(input:focus-visible) { outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: var(--focus-ring-offset); }
.daypill:has(input:disabled) { cursor: default; opacity: .6; }


/* ===========================================================================
   The Today board (P2-TRP-06, from the 2026-08-22 handoff — TodayBoard.jsx).
   =========================================================================== */

/* Pill tabs. The DS Tabs component has two variants and only the underline one
   was ever ported; the handoff uses `variant="pill"` for both switchers on this
   page (board/overview, and today/next service day). A pill group is a
   different affordance from the underline rail, not a restyle of it: the
   underline says "sections of this page", the pill says "two views of the same
   thing", which is exactly the board-vs-map distinction. */
.tabs--pill { border-block-end: none; gap: var(--space-1);
  background: var(--surface-sunken); padding: var(--space-1);
  border-radius: var(--radius-pill); inline-size: fit-content; }
.tab--pill { padding: var(--space-15) var(--space-3);
  border-radius: var(--radius-pill); font: var(--type-small); }
/* The underline variant paints its active state with an ::after bar. A pill
   must not inherit it — the bar would sit across the pill's rounded bottom. */
.tab--pill.tab--active::after { content: none; }
.tab--pill.tab--active { background: var(--surface-card);
  color: var(--text-primary); box-shadow: var(--shadow-sm); }

/* A small inline noun-chip — the direction on the run detail header. Distinct
   from .badge, which carries a status colour; a Tag is neutral by definition. */
.tag { display: inline-flex; align-items: center; padding: 2px var(--space-2);
  border-radius: var(--radius-sm); border: 1px solid var(--border-default);
  background: var(--surface-sunken); color: var(--text-secondary);
  font: var(--type-caption); }

/* Trip-state badges. Deliberately `badge--trip-*` rather than reusing the bare
   `badge--scheduled` that already exists: that one is the announcement state
   and is painted info-blue, while a scheduled RUN is neutral — the handoff is
   explicit that only boarding and en-route read as live. Two different meanings
   behind one class name is how a colour starts lying. */
.badge--trip-scheduled { background: var(--surface-sunken);
  color: var(--text-secondary); }
.badge--trip-boarding { background: var(--status-boarding-bg);
  color: var(--status-boarding-fg); }
.badge--trip-en_route { background: var(--status-enroute-bg);
  color: var(--status-enroute-fg); }
.badge--trip-arrived { background: var(--status-arrived-bg);
  color: var(--status-arrived-fg); }
.badge--trip-completed { background: var(--status-completed-bg);
  color: var(--status-completed-fg); }
.badge--trip-aborted { background: var(--status-emergency-bg);
  color: var(--status-emergency-fg); }
/* Preview rows: a run that does not exist yet. Outlined, never filled — a
   filled badge on a row nobody has generated reads as a real state. */
.badge--preview { background: transparent; color: var(--text-muted);
  border: 1px dashed var(--border-default); }

/* The live pulse. Only boarding and en-route get it; a decorative animation on
   a completed run would say "something is happening" about a bus in its yard. */
/* `--pulse-live` is the DS's own live-data animation and motion.css already
   sets it to `none` under prefers-reduced-motion — a hand-rolled keyframe
   here would need its own guard, which components.css is not allowed to
   carry (P1-FND-10: the ladder owns every media query). */
.badge__pulse { inline-size: 6px; block-size: 6px; border-radius: 50%;
  background: currentColor; margin-inline-end: var(--space-15);
  animation: var(--pulse-live); }

/* The runs table. A grid rather than a <table> because each row is a BUTTON
   (Enter opens the run) and a focusable row inside a table cell fights the
   table's own semantics. The column ladder is the handoff's, verbatim. */
.board__head, .board__row { display: grid;
  grid-template-columns: minmax(160px, 1.4fr) 64px minmax(120px, 1fr) 64px 70px 90px 130px 28px;
  gap: var(--space-3); align-items: center;
  padding: var(--space-2) var(--space-3); }
.board__head { font: var(--type-label);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--text-muted); }
.board__row { border-block-start: 1px solid var(--border-subtle);
  font: var(--type-small); text-align: start; inline-size: 100%;
  background: none; border-inline: none; border-block-end: none;
  color: inherit; cursor: pointer; }
.board__row:hover { background: var(--surface-hover); }
.board__row:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: calc(var(--focus-ring-offset) * -1); }
/* Below the ladder the eight-column grid stops being a table and becomes a
   smear. The one-column collapse lives in components.stms.css with the rest
   of the port's component adaptations — components.css carries no media
   query of its own. */
.board__route { min-inline-size: 0; }
.board__route-name { display: block; font-weight: var(--weight-medium);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The abort reason and the stale-guard note. Different colours on purpose:
   an aborted run is a failure, an auto-completed one is a run nobody closed —
   "needs review", not "went wrong". */
.board__reason { font: var(--type-caption);
  color: var(--status-emergency-fg); unicode-bidi: plaintext; }
/* token gap: wanted --warning-fg, using --status-boarding-fg (DS sync) */
.board__auto { font: var(--type-caption); color: var(--status-boarding-fg);
  unicode-bidi: plaintext; }
.board__driver { color: var(--text-secondary); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.board__driver--none { color: var(--text-muted); }
/* Plate, clock and rider count are Latin/mono runs inside a page that may be
   RTL — isolate each so a plate does not reorder against the name beside it
   (P1-FND-40). */
.board__num { font: var(--type-mono); direction: ltr; unicode-bidi: isolate;
  color: var(--text-secondary); }
.board__bus { font: var(--type-mono); direction: ltr; unicode-bidi: isolate;
  font-weight: var(--weight-semibold); }
/* token gap: wanted --warning-fg, using --status-boarding-fg (DS sync) */
.board__away { color: var(--status-boarding-fg);
  font-weight: var(--weight-medium); }
.board__away--none { color: var(--text-muted); }
.board__state { display: flex; gap: var(--space-2); align-items: center; }
.board__chev { color: var(--text-muted); justify-self: end; }

/* The day switcher line: pills, the weekend caption, and the generation
   timestamp pushed to the far end. */
.board__bar { display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: wrap; margin-block-end: var(--space-4); }
.board__bar-note { font: var(--type-caption); color: var(--text-muted);
  unicode-bidi: plaintext; }
.board__bar-note--end { margin-inline-start: auto; }
.board__cards { display: flex; flex-direction: column; gap: var(--space-4); }
/* The two empty states are narrow prose cards, not full-width tables. */
.board__empty { display: flex; flex-direction: column; gap: var(--space-4);
  max-inline-size: 640px; }

/* The run detail header: back button, route, direction tag, state. */
.run__head { display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: wrap; margin-block-end: var(--space-4); }
.run__title { font: var(--type-h1); margin: 0; unicode-bidi: plaintext; }
.run__live { margin-inline-start: auto; }
/* A roster row for a child who is away. Struck through AND muted AND faded —
   the same treatment DriverApp.jsx uses, because a driver glancing at a phone
   in daylight must not have to read a badge to see it. */
.roster__away { opacity: .75; }
.roster__name { font-weight: var(--weight-medium); unicode-bidi: plaintext; }
.roster__away .roster__name { text-decoration: line-through;
  color: var(--text-muted); }
.roster__grade { font: var(--type-caption); color: var(--text-muted); }
.roster__reason { font: var(--type-caption); color: var(--text-muted);
  margin-inline-start: var(--space-2); unicode-bidi: plaintext; }
/* Stat values that carry a tone. `.board__reason` and `.board__away` cannot be
   reused here: they set a caption font, which would shrink the number they
   were meant to colour. */
.stat__value--warn { color: var(--status-boarding-fg); }
.stat__value--danger { color: var(--status-emergency-fg); }

/* P4-ANN-03: the announcement composer's To: line — an email-style chip
   field. The menu reuses the scope-switcher item vocabulary the platform
   search palette already borrows, so nothing new is invented below the
   input; only the chip itself is new (a .badge in shape, --info in tone,
   with a removal button inside it). */
.to-field { position: relative; display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2); }
.to-field .input { flex: 1 1 200px; min-inline-size: 0; }
.to-field__chips { display: contents; }
.chip { display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 2px var(--space-1) 2px var(--space-2); border-radius: var(--radius-pill);
  background: var(--info-bg); color: var(--info-fg); font: var(--type-label);
  max-inline-size: 100%; }
.chip__remove { display: grid; place-items: center; inline-size: 18px; block-size: 18px;
  border: 0; border-radius: var(--radius-pill); background: transparent; color: inherit;
  cursor: pointer; padding: 0; }
.chip__remove:hover, .chip__remove:focus-visible { background: var(--surface-card); }
.chip__remove .icon { inline-size: 12px; block-size: 12px; }
.to-field__menu:not(:empty) { position: absolute; inset-inline-start: 0; top: calc(100% + 4px);
  z-index: 60; min-inline-size: 280px; max-inline-size: 420px; max-block-size: 320px;
  overflow-y: auto; }
.to-field__menu .scope-switcher__item { inline-size: 100%; border: 0; background: none;
  text-align: start; cursor: pointer; font: inherit; color: inherit; }

