/*
 * Codeliance Design System — product component layer.
 * ============================================================================
 * Token-based visual vocabulary for an obligation's state (COD-232). Every
 * value here reads from a named token in design-system-tokens.css — no raw hex
 * or pixel numbers. Load order (see base.html): tokens.css FIRST, then this.
 *
 * Three components, built to the Lean naming model
 * (docs/PRODUCT_NAMING.md):
 *   .ds-status-pill  — Obligation Status (Open / Completed), + combined form.
 *   .ds-substate-tag — Status sub-state (Action Needed / In Progress).
 *   .ds-type-tag     — Obligation Type (immutable category axis).
 *
 * Namespaced `ds-` on purpose: core/static/css/main.css already owns a legacy
 * `.status-pill` (hardcoded radius, a different system). These do not touch it.
 *
 * Design intent: a status pill and a type tag must be tellable apart at a
 * glance. So status pills are SANS + fully-round (--r-pill) + a leading colour
 * dot; type tags are MONO + tight-radius (--r-xs) + neutral. Different shape,
 * different type family, different colour role.
 *
 * NOTE: --sans / --mono resolve to DM Sans / Roboto Mono, now self-hosted via
 * core/static/css/fonts.css (COD-233) and loaded on every page by the base
 * template — so these render in the real faces, not a system fallback.
 * ============================================================================
 */

/* ── Icon — inline stroke SVG (COD-308) ───────────────────────────────────────
 * Emitted only by {% icon %} / core/icons.py, which fixes the viewBox, the 1.3
 * stroke and the render size. Nothing here sets a size: the tag writes explicit
 * width/height so an icon cannot be silently rescaled by a container's font-size,
 * which is precisely the failure mode the icon font had.
 *
 * Colour comes from `currentColor`, so an icon is tinted by setting `color` on it
 * or on an ancestor — never by a fill/stroke override here.
 */
.ds-icon {
  display: inline-block;
  flex-shrink: 0;
  /* The app shell sets a global `* { box-sizing: border-box }`, under which any
   * padding a caller puts on an icon is subtracted from its 14px width rather
   * than added around it — a 0.5rem padding-left leaves ~6px of glyph. This bug
   * cannot happen with an icon font (a glyph is text, and box-sizing does not
   * apply), so it only surfaced once COD-308 swapped the font for inline SVG,
   * and it surfaces silently: the icon shrinks, it does not disappear.
   * Here width/height always describe the drawing area. */
  box-sizing: content-box;
  /* Optical baseline alignment when an icon sits inline in a run of text. Inside a
   * flex container (.ds-btn, .ds-panel__title) alignment comes from align-items and
   * this is inert. */
  vertical-align: -0.15em;
}

/* ── Status pill — Obligation Status (top level, binary) ──────────────────── */
.ds-status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.34rem;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  line-height: 1.2;
  padding: 0.2rem 0.6rem;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  white-space: nowrap;
}

/* Leading dot inherits the pill's text colour, so it tracks each variant. */
.ds-status-pill__dot {
  flex-shrink: 0;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* Open → amber (work outstanding). Matches --amber's "action needed" role. */
.ds-status-pill--open {
  background: var(--amber-bg);
  color: var(--amber-text);
  border-color: var(--amber-border);
}

/* Completed → teal (requirement satisfied). Border is teal at low alpha —
   the token layer ships --teal-bg but no --teal-border. */
.ds-status-pill--completed {
  background: var(--teal-bg);
  color: var(--teal);
  border-color: rgba(13, 122, 112, 0.28);
}

/* ── Sub-state tag — Status sub-state (rolls up to Open) ──────────────────── */
.ds-substate-tag {
  display: inline-flex;
  align-items: center;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  line-height: 1.2;
  padding: 0.16rem 0.55rem;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  white-space: nowrap;
}

/* Action Needed → amber (not started; owner must act). */
.ds-substate-tag--action-needed {
  background: var(--amber-bg);
  color: var(--amber-text);
  border-color: var(--amber-border);
}

/* In Progress → blue (work underway) — visibly distinct from amber's "act now". */
.ds-substate-tag--in-progress {
  background: var(--blue-pale);
  color: var(--blue);
  border-color: var(--blue-light);
}

/* ── Workflow state chip — one obligation's Lean state ────────────────────── */
/* Promoted from All Obligations' row chip (obl-chip) when the obligation page began
   rendering the same state: two screens showing one obligation's state need one chip, or
   they drift. Colours are the design's amber / blue / teal chips, on the same tokens as the
   sub-state tag and status pill above. The chip used main.css's --status-* palette while it
   lived on one screen; the component layer reads design-system tokens only.
   Class names in these comments carry no leading dot: check_template_classes reads selector
   text, and a dotted name in a comment counts as a definition. */
.ds-state-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.18rem 0.55rem;
  border-radius: var(--r-pill);
  border: 1px solid;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
  width: fit-content;
}
.ds-state-chip .ds-icon { flex-shrink: 0; }
.ds-state-chip--blue {
  background: var(--blue-pale);
  color: var(--blue);
  border-color: var(--blue-light);
}
.ds-state-chip--amber {
  background: var(--amber-bg);
  color: var(--amber-text);
  border-color: var(--amber-border);
}
.ds-state-chip--teal {
  background: var(--teal-bg);
  color: var(--teal);
  border-color: rgba(13, 122, 112, 0.28);
}
.ds-state-chip--muted {
  background: var(--gray-bg);
  color: var(--text-sub);
  border-color: var(--border-solid);
}

/* ── Priority — a dot and a label on the priority scale ───────────────────── */
/* One rule for every screen that lists obligations (COD-442): All Obligations rows and the
   article page's obligation footers. It was written three times, once per screen
   stylesheet. A dotted label rather than a filled badge, so it never competes with the
   status chip beside it; the prio fills are not AA under white text, so a solid chip could
   not carry this scale anyway. Bands are regulation_filters.PRIORITY_BANDS. The obligation
   page's Risk level row draws v2's triangle instead of the dot. */
.ds-prio {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 600;
  white-space: nowrap;
}
.ds-prio::before {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.ds-prio--high { color: var(--prio-high-text); }
.ds-prio--medium { color: var(--prio-med-text); }
.ds-prio--low { color: var(--prio-low-text); }

/* ── Obligation Type tag — immutable category axis ────────────────────────── */
/* Einat's `.type-label` / `.type-label-icon`, 2026-09-07 export (COD-445): a flat
   icon + text label — no fill, no border, sans, muted icon against --dark text.

   It still reads as a category rather than a status, but by CONTRAST rather than by
   shape: it is the only unfilled label on the obligation row, where .ds-status-pill
   and .ds-chip are both round and tinted. This replaced a mono/tight-radius chip that
   COD-232 had reasoned its way to independently; the export does not draw that, and
   the design is the spec. Check the current design (docs/UI_STYLE_GUIDE.md, "Design
   references") before "restoring consistency".

   One icon per type (in _OBLIGATION_TYPE), one colour for all four — the per-type
   modifier hooks stay empty on purpose, because the design defines a single
   `color:var(--dark)` and varies only the glyph. */
.ds-type-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.38rem;
  font-size: var(--fs-meta);
  font-weight: 500;
  line-height: 1.2;
  color: var(--dark);
  white-space: nowrap;
}

.ds-type-tag__icon {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--text-muted);
}

/* ── Chip — the generic tinted label (COD-308, batch 2) ───────────────────────
 * The three components above each carry a fixed *vocabulary*: .ds-status-pill
 * prints Open / Completed, .ds-substate-tag prints Action Needed / In Progress,
 * .ds-type-tag prints one of the four obligation types. That is deliberate —
 * they exist so those words are defined once.
 *
 * A chip is what you reach for when the word is NOT from those vocabularies: an
 * orchestrator job's status (queued / running / cancelled), a container state, a
 * confidence level, a "Current" marker on a version. Batch 1 hit this and rolled
 * a screen-local `.pf-chip`; rather than let every batch grow its own, the two
 * tones it needed (teal affirmative, neutral) are generalised here and it was
 * retired onto this.
 *
 * Tone is chosen by the caller, because only the caller knows what the word
 * means. Shape follows .ds-status-pill (sans, fully round) so a chip reads as
 * state rather than category — a category is .ds-type-tag, which since COD-445 is
 * the flat, unfilled icon+text label above. The distinction is now fill-vs-none
 * rather than sans-vs-mono; do not re-mono .ds-type-tag to restore the old pairing.
 */
.ds-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-family: var(--sans);
  font-size: var(--fs-label);
  font-weight: 700;
  line-height: 1.2;
  padding: 0.12rem 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  white-space: nowrap;
}

/* Leading dot inherits the chip's text colour, so it tracks the tone. Optional —
   most chips read fine without one; use it where chips of different tones sit in
   one column and the shape needs to help the colour. */
.ds-chip__dot {
  flex-shrink: 0;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* Neutral is the default tone and the right one whenever the word carries no
   judgement — "cancelled", "queued", a phase name. Reaching for a colour that
   the state does not earn is how a screen ends up shouting. */
.ds-chip--neutral {
  background: var(--gray-bg);
  color: var(--text-muted);
  border-color: var(--border-solid);
}
/* Affirmative, but NOT compliance-completed: "Current", "Configured", "Connected".
   Teal is the design's satisfied-requirement colour; there is no --teal-border
   token, so the hairline is teal at low alpha, as .ds-status-pill--completed does. */
.ds-chip--teal {
  background: var(--teal-bg);
  color: var(--teal);
  border-color: rgba(13, 122, 112, 0.28);
}
/* A test/evaluation that passed. Distinct from teal on purpose: green is an
   outcome the system measured, teal is a configuration state it was told. */
.ds-chip--green {
  background: var(--green-bg);
  color: var(--green);
  border-color: var(--green-border);
}
.ds-chip--amber {
  background: var(--amber-bg);
  color: var(--amber-text);
  border-color: var(--amber-border);
}
.ds-chip--red {
  background: var(--red-bg);
  color: var(--red-text);
  border-color: var(--red-border);
}
/* Work underway — same blue as .ds-substate-tag--in-progress, same meaning. */
.ds-chip--blue {
  background: var(--blue-pale);
  color: var(--blue);
  border-color: var(--blue-light);
}
.ds-chip--purple {
  background: var(--purple-bg);
  color: var(--purple-text);
  border-color: rgba(107, 91, 214, 0.28);
}

/* Solid tier — white text on a filled background, for a chip that must carry a
   *ranking* rather than a state. The pale tones above are deliberately quiet and
   all read as roughly equal weight, which is wrong for an ordered ladder.

   It has no callers at the moment. Its only one was the sky-blue priority ladder,
   which COD-442 removed in favour of the --prio-* scale drawn as a dot and a label.
   Kept because it is a component tier from COD-231 rather than a product palette —
   the next ordered chip wants it — but nothing renders it today, so don't read its
   presence as evidence that solid chips are in use somewhere.

   Note before reaching for it with priority: the --prio-* fills are NOT AA under
   white text, which is why priority did not simply move onto this tier. */
.ds-chip--solid {
  color: var(--white);
  border-color: transparent;
}

/* ════════════════════════════════════════════════════════════════════════════
 * CORE COMPONENT LIBRARY (COD-231)
 * ----------------------------------------------------------------------------
 * The shared, non-product UI primitives every screen reuses: buttons, form
 * fields, the toggle switch, and the modal / drawer shells. Namespaced .ds-*
 * for the same reason as the product components above — Bootstrap and main.css
 * own bare .btn / .form-* / .modal, and these must not collide with them, so
 * the handoff's bare class names map to .ds-* here (mapping noted per block).
 *
 * Colour / type / radius / spacing / shadow / motion values read from a token
 * in design-system-tokens.css; geometry (px sizes, knob offsets) stays literal,
 * verbatim from the handoff — same convention as the product layer above.
 * Source: Einat's Design System handoff (COD-230/231), in git history.
 *
 * SCOPE (COD-231, "specified half"): only components with a complete visual
 * spec in the handoff are built. Deliberately NOT built here — no design spec,
 * see the COD-231 spec-request comment:
 *   · button DISABLED state — the handoff defines none; a conservative derived
 *     treatment is included below, flagged PROVISIONAL pending Einat's sign-off.
 *   · modal / drawer OVERLAY, slide-in animation and focus-trap — described in
 *     prose only. The shells are complete; the interaction layer is deferred,
 *     and the gallery shows both shells inline exactly as the handoff does.
 * ════════════════════════════════════════════════════════════════════════════ */

/* ── Buttons ──────────────────────────────────────────────────────────────── */
/* Handoff: .btn / .btn-primary / .btn-ghost / .btn-xs(→ --compact) / .btn-micro
   / .btn-row(.green|.amber|.muted). "Five roles, two size tiers." primary &
   ghost are .ds-btn + a role modifier; micro and row are standalone roles. */
.ds-btn {
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 600;
  border-radius: var(--r-sm);
  padding: 0.5rem 1rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  transition: all var(--dur-base);
  white-space: nowrap;
  /* A button is a button whether it is rendered as <button> or <a>. Without this,
   * an <a class="ds-btn"> inherits the global link underline and reads as text —
   * which it did on the obligations list and the auth screens until this reset
   * replaced a per-screen override on obligation-detail.css. */
  text-decoration: none;
}
.ds-btn--primary {
  background: var(--blue);
  color: #fff;
  border: 1px solid var(--blue);
}
.ds-btn--primary:hover {
  background: var(--blue-dark);
}
.ds-btn--ghost {
  background: var(--white);
  color: var(--dark);
  border: 1px solid var(--border-solid);
}
.ds-btn--ghost:hover {
  border-color: rgba(11, 69, 255, 0.3);
  color: var(--blue);
}

/* Selected link in a row of filter links (COD-308 batch 3), set by
   {% ds_button is_active=True %} together with aria-current="page".
   The pale-blue fill and blue border read as "you are here" without spending the
   solid brand blue, which belongs to the page's one primary action; a filter row
   where six links compete at full strength has no primary at all. The border
   colour is repeated on :hover so moving the pointer over the already-selected
   link does not make it look unselected. */
.ds-btn--ghost.is-active,
.ds-btn--ghost.is-active:hover {
  background: var(--blue-pale);
  border-color: var(--blue);
  color: var(--blue);
}

/* Compact size tier — combine with a role: .ds-btn.ds-btn--ghost.ds-btn--compact */
.ds-btn--compact {
  font-size: var(--fs-meta);
  padding: 0.36rem 0.7rem;
}

/* Micro — standalone smallest button (in-table actions). No .ds-btn base. */
.ds-btn-micro {
  font-family: var(--sans);
  font-weight: 600;
  font-size: var(--fs-label);
  padding: 0.24rem 0.58rem;
  border-radius: var(--r-xs);
  cursor: pointer;
  border: 1px solid var(--border-solid);
  background: var(--white);
  color: var(--dark);
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  white-space: nowrap;
  /* Same reset as .ds-btn — these are standalone tiers, so they don't inherit
   * it. f1d02b79 fixed the base class only, leaving <a class="ds-btn-row"> and
   * <a class="ds-btn-micro"> still underlined in the obligation table. */
  text-decoration: none;
}

/* Row — in-list tinted action. Standalone; semantic tint modifiers. */
.ds-btn-row {
  font-family: var(--sans);
  font-weight: 600;
  font-size: var(--fs-meta);
  padding: 0.28rem 0.68rem;
  border-radius: var(--r-xs);
  cursor: pointer;
  border: 1px solid var(--blue-light);
  background: var(--blue-pale);
  color: var(--blue);
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  white-space: nowrap;
  text-decoration: none;
}
.ds-btn-row--green {
  border-color: var(--green-border);
  background: var(--green-bg);
  color: var(--green);
}
.ds-btn-row--amber {
  border-color: var(--amber-border);
  background: var(--amber-bg);
  color: var(--amber-text);
}
.ds-btn-row--muted {
  border-color: var(--border-solid);
  background: var(--white);
  color: var(--text-muted);
}

/* Disabled — PROVISIONAL. The handoff specifies no button disabled state; this
   is a conservative, non-inventive fallback (dim + not-allowed, hover reset to
   the resting look) so disabled buttons are usable now. Pending Einat's spec. */
.ds-btn:disabled,
.ds-btn[disabled],
.ds-btn-micro:disabled,
.ds-btn-micro[disabled],
.ds-btn-row:disabled,
.ds-btn-row[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}
.ds-btn--primary:disabled:hover,
.ds-btn--primary[disabled]:hover {
  background: var(--blue);
}
.ds-btn--ghost:disabled:hover,
.ds-btn--ghost[disabled]:hover {
  border-color: var(--border-solid);
  color: var(--dark);
}

/* Keyboard focus ring — matches the handoff's global :focus-visible (2px blue,
   2px offset). Scoped to the .ds-* controls so focus is correct regardless of
   whatever global rule main.css does or doesn't ship. */
.ds-btn:focus-visible,
.ds-btn-micro:focus-visible,
.ds-btn-row:focus-visible,
.ds-modal__close:focus-visible,
.ds-drawer__close:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* ── Form field ───────────────────────────────────────────────────────────── */
/* Handoff: .form-group / .form-label / .form-input / .form-hint → .ds-field /
   .ds-label / .ds-input / .ds-hint. "One standard: blue uppercase" labels; the
   input shows focus with a blue border (no ring). select/checkbox/radio/
   textarea/error states are NOT specified — omitted, pending Einat. */
.ds-field {
  display: flex;
  flex-direction: column;
  gap: 0.32rem;
}
.ds-label {
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--blue);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.ds-input {
  font-family: var(--sans);
  font-size: var(--fs-body);
  color: var(--dark);
  background: var(--white);
  border: 1px solid var(--border-solid);
  border-radius: var(--r-sm);
  padding: 0.45rem 0.75rem;
  outline: none;
  transition: border-color var(--dur-fast);
  width: 100%;
}
.ds-input:focus {
  border-color: var(--blue);
}
.ds-input::placeholder {
  color: var(--text-sub);
}
.ds-input:disabled {
  background: var(--gray-bg);
  color: var(--text-muted);
  cursor: not-allowed;
}
/* The file picker's own button, as a compact ghost button. Keyed on the element, not on
   a class, so a file input drawn by any form gets it: the browser's default (a grey
   outset button) is otherwise the one control on a page the design system never reached. */
input[type="file"] {
  font-family: var(--sans);
  font-size: var(--fs-meta);
  color: var(--text-sub);
}
input[type="file"]::file-selector-button {
  font-family: var(--sans);
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--dark);
  background: var(--white);
  border: 1px solid var(--border-solid);
  border-radius: var(--r-sm);
  padding: 0.36rem 0.7rem;
  margin-right: 0.6rem;
  cursor: pointer;
  transition: all var(--dur-base);
}
input[type="file"]::file-selector-button:hover {
  border-color: rgba(11, 69, 255, 0.3);
  color: var(--blue);
}
input[type="file"]:disabled::file-selector-button {
  color: var(--text-muted);
  cursor: not-allowed;
}
/* A boxed file input would put a button inside a field inside a card. The button is the control. */
.ds-input[type="file"] {
  padding: 0;
  border: none;
  background: none;
}
/* Required marker. Amber, not red: an unfilled required field is not an error
   until it is submitted, and red here would put every form in a failed state. */
.ds-label__req { color: var(--amber); }
.ds-hint {
  font-size: var(--fs-meta);
  color: var(--text-muted);
}
/* Checkbox field — the box sits beside its label, and any error/hint below both. */
.ds-check-field {
  display: flex;
  flex-direction: column;
  gap: 0.32rem;
}

/* ── Toggle switch ────────────────────────────────────────────────────────── */
/* Handoff: .toggle / .toggle-track → .ds-toggle / .ds-toggle__track. 36×20px,
   off-track is --border-solid, blue when the hidden checkbox is :checked; the
   knob is a ::before that translates 16px on check. */
.ds-toggle {
  position: relative;
  display: inline-block;
  width: 36px;
  height: 20px;
  flex-shrink: 0;
}
.ds-toggle input {
  opacity: 0;
  width: 0;
  height: 0;
}
.ds-toggle__track {
  position: absolute;
  inset: 0;
  background: var(--border-solid);
  border-radius: var(--r-pill);
  transition: background var(--dur-base);
  cursor: pointer;
}
.ds-toggle__track::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 3px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  transition: transform var(--dur-base);
}
.ds-toggle input:checked + .ds-toggle__track {
  background: var(--blue);
}
.ds-toggle input:checked + .ds-toggle__track::before {
  transform: translateX(16px);
}
.ds-toggle input:focus-visible + .ds-toggle__track {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* ── Modal (centered dialog shell) ────────────────────────────────────────── */
/* Handoff: .modal-demo / .panel-top / .panel-title-lg / .panel-close → .ds-modal*.
   Shadow uses the --shadow-modal token (the same value the handoff hard-codes).
   Overlay / entrance animation / focus-trap are prose-only — deferred (see the
   scope note); .ds-modal__body and __footer promote the handoff's inline demo
   styles to reusable classes. */
.ds-modal {
  background: var(--white);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-modal);
  width: 100%;
  max-width: min(560px, 95vw);
  overflow: hidden;
}
/* Width comes from the shell when .ds-modal is dropped inside a container that
   already sizes the dialog — Bootstrap's .modal-dialog(.modal-lg/.modal-xl),
   which the evidence lightboxes keep for its focus trap, scrim and Esc handling
   (COD-308: re-skin the surface, don't reimplement the behaviour). Without this
   the 560px cap above wins and an evidence screenshot opens letterboxed. */
.ds-modal--fluid { max-width: none; }
.ds-modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1.35rem;
  border-bottom: 1px solid var(--border-solid);
}
.ds-modal__title {
  font-size: var(--fs-section);
  font-weight: 800;
  color: var(--dark);
}
.ds-modal__subtitle {
  font-size: var(--fs-meta);
  color: var(--text-muted);
  margin-top: 0.15rem;
}
.ds-modal__close {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--border-solid);
  background: var(--gray-bg);
  color: var(--text-muted);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
}
.ds-modal__body {
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}
.ds-modal__footer {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  padding-top: 0.25rem;
  border-top: 1px solid var(--border-solid);
}

/* The confirmation dialog (includes/_confirm_dialog.html): a .ds-modal on a native <dialog>,
   which supplies the overlay and focus trap the shell above defers. The UA's dialog padding and
   border go, so the form fills the box and a click landing on the <dialog> itself is a click
   on the backdrop. */
.ds-confirm {
  max-width: min(420px, calc(100vw - 2rem));
  padding: 0;
  border: none;
}
.ds-confirm::backdrop { background: rgba(0, 0, 0, 0.5); }
.ds-confirm .ds-modal__footer { padding-top: 0.85rem; }
.ds-confirm__message {
  margin: 0;
  color: var(--dark);
  white-space: pre-line;
}

/* "Message us" (includes/_message_dialog.html): the same shell, wide enough to write in. */
.ds-message {
  width: min(520px, calc(100vw - 2rem));
  max-width: none;
  padding: 0;
  border: none;
}
.ds-message::backdrop { background: rgba(0, 0, 0, 0.5); }
.ds-message .ds-modal__footer { padding-top: 0.85rem; }

/* A button that reads as the link around it: an action that opens something on the page
   rather than going anywhere ({% message_us %}). A color utility beside it, such
   as the footer's .text-muted, wins over this blue. */
.ds-link-button {
  display: inline;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: var(--blue);
  text-decoration: underline;
  cursor: pointer;
}

/* ── Drawer (right-edge side panel shell) ─────────────────────────────────── */
/* Handoff: .side-panel-demo / .ssp-* → .ds-drawer*. Same deferral of the
   overlay / slide-in / focus-trap layer as the modal; the shell is complete. */
.ds-drawer {
  background: var(--white);
  border: 1px solid var(--border-solid);
  border-radius: var(--r-lg);
  overflow: hidden;
  width: 100%;
  max-width: 370px;
}
.ds-drawer__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.85rem 1.1rem;
  border-bottom: 1px solid var(--border-solid);
}
.ds-drawer__title {
  font-size: var(--fs-body);
  font-weight: 700;
  color: var(--dark);
}
.ds-drawer__close {
  width: 24px;
  height: 24px;
  border-radius: var(--r-xs);
  border: 1px solid var(--border-solid);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-size: var(--fs-body);
  display: flex;
  align-items: center;
  justify-content: center;
}
.ds-drawer__body {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
.ds-drawer__section {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.ds-drawer__label {
  font-family: var(--mono);
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.ds-drawer__text {
  font-size: var(--fs-body);
  color: var(--dark);
  line-height: 1.55;
}

/* ════════════════════════════════════════════════════════════════════════════
 * CORE COMPONENT LIBRARY — DEFERRED HALF (COD-231)
 * ----------------------------------------------------------------------------
 * The eleven components that had no visual spec when the first half shipped, now
 * built from Einat's approved additions spec (2026-07-22):
 *   codeliance-additions.css (the source of these values; in git history under design/).
 * Same conventions as the specified half above: colour / type / radius /
 * spacing / shadow / motion read from a token in design-system-tokens.css;
 * geometry (px sizes) stays literal, verbatim from the spec. The spec's bare
 * class names (.select, .checkbox, .tooltip, .pagination…) are mapped to .ds-*
 * for the same reason as the specified half — Bootstrap and main.css own bare
 * .select / .checkbox / .tooltip / .pagination, and these must not collide.
 *
 * These are custom, JS-driven controls (a div-based select, a div-based
 * checkbox), so visual state rides on .is-* classes the app toggles — not the
 * native :checked/:disabled the specified half uses on real <input>s.
 * ════════════════════════════════════════════════════════════════════════════ */

/* ── Select / dropdown ────────────────────────────────────────────────────── */
/* Spec: .select / .select-chevron / .select-menu / .select-option. The trigger
   reuses the input geometry; the open menu and options are div-based rows. */
.ds-select {
  font-family: var(--sans);
  font-size: var(--fs-body);
  color: var(--dark);
  background: var(--white);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 0.45rem 0.75rem;
  width: 100%;
  cursor: pointer;
  outline: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-select { transition: border-color var(--dur-base) var(--ease-ui); }
}
.ds-select__chevron { color: var(--text-sub); flex-shrink: 0; }
.ds-select.is-open,
.ds-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-light); }
.ds-select.is-open .ds-select__chevron { color: var(--blue); }
.ds-select.is-error { border-color: var(--border-strong); }  /* border unchanged; error shown via .ds-form-error */
.ds-select.is-placeholder { color: var(--text-sub); }
.ds-select:disabled,
.ds-select.is-disabled { background: var(--gray-bg); color: var(--text-muted); cursor: not-allowed; }

.ds-select-menu {
  background: var(--white);
  border: 1px solid var(--border-solid);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-modal);
  padding: 0.25rem;
  margin-top: 4px;
  z-index: var(--z-overlay);
  overflow: hidden;
}
.ds-select-option {
  font-size: var(--fs-body);
  color: var(--dark);
  padding: 0.45rem 0.6rem;
  border-radius: var(--r-xs);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.ds-select-option:hover { background: var(--blue-pale); }
.ds-select-option.is-selected { background: var(--blue-pale); color: var(--blue); font-weight: 600; }

/* Native <select>. The control above is div-based — it needs a menu element and
   JS, which a server-rendered Django ChoiceField does not have. This variant
   gives a real <select> the same geometry, border and focus treatment and lets
   the platform draw its own arrow: a custom caret means suppressing `appearance`,
   and a suppressed native control is the kind of thing that breaks on one
   platform and is never noticed on the others. */
.ds-select--native {
  display: block;
  cursor: pointer;
}

/* ── Checkbox ─────────────────────────────────────────────────────────────── */
/* Spec: .checkbox with an inline 12px check svg (.checkbox-glyph) and a 9×2 bar
   (.checkbox-bar) for the indeterminate state. */
.ds-checkbox {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  box-sizing: border-box;
  border: 1px solid var(--border-solid);
  border-radius: var(--r-xs);
  background: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  position: relative;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-checkbox { transition: background var(--dur-fast) var(--ease-ui), border-color var(--dur-fast) var(--ease-ui); }
}
.ds-checkbox.is-checked,
.ds-checkbox.is-indeterminate { background: var(--blue); border-color: var(--blue); }
.ds-checkbox__glyph { color: #fff; display: none; }
.ds-checkbox.is-checked .ds-checkbox__glyph { display: inline-flex; }
.ds-checkbox__bar { display: none; width: 9px; height: 2px; border-radius: var(--r-bar); background: #fff; }
.ds-checkbox.is-indeterminate .ds-checkbox__bar { display: block; }
.ds-checkbox.is-focused,
.ds-checkbox:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--blue-light); }
.ds-checkbox.is-disabled { background: var(--gray-bg); border-color: var(--border-solid); opacity: 0.6; cursor: not-allowed; }
.ds-checkbox-label { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-body); color: var(--dark); cursor: pointer; }

/* Native <input type="checkbox">. Deliberately NOT a modifier of .ds-checkbox
   above: that control is div-based and driven by an .is-checked class, so on a
   real input it would draw an 18px box that never checks. This one keeps the
   browser's own control — and its keyboard and screen-reader behaviour — and
   only recolours it, which `accent-color` does in one line. */
.ds-checkbox-native {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  cursor: pointer;
  accent-color: var(--blue);
}
.ds-checkbox-native:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--blue-light); }
.ds-checkbox-native:disabled { cursor: not-allowed; opacity: 0.6; }

/* Native <input type="radio">. Same reasoning as .ds-checkbox-native above, and
   kept separate from it so a radio never inherits a square. */
.ds-radio-native {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  cursor: pointer;
  accent-color: var(--blue);
}
.ds-radio-native:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--blue-light); }
.ds-radio-native:disabled { cursor: not-allowed; opacity: 0.6; }

/* ── Radio ────────────────────────────────────────────────────────────────── */
.ds-radio {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  box-sizing: border-box;
  border: 1px solid var(--border-solid);
  border-radius: 50%;
  background: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-radio { transition: border-color var(--dur-fast) var(--ease-ui); }
}
.ds-radio::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--blue); transform: scale(0); }
@media (prefers-reduced-motion: no-preference) {
  .ds-radio::after { transition: transform var(--dur-fast) var(--ease-ui); }
}
.ds-radio.is-selected { border-color: var(--blue); }
.ds-radio.is-selected::after { transform: scale(1); }
.ds-radio.is-focused,
.ds-radio:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--blue-light); }
.ds-radio.is-disabled { background: var(--gray-bg); border-color: var(--border-solid); opacity: 0.6; cursor: not-allowed; }
.ds-radio-label { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-body); color: var(--dark); cursor: pointer; }
.ds-radio-group { display: flex; flex-direction: column; gap: var(--sp-3); }

/* ── Textarea ─────────────────────────────────────────────────────────────── */
.ds-textarea {
  font-family: var(--sans);
  font-size: var(--fs-body);
  color: var(--dark);
  background: var(--white);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 0.45rem 0.75rem;
  width: 100%;
  min-height: 84px;
  line-height: var(--lh-base);
  outline: none;
  resize: vertical;  /* vertical only — protects dense columns */
}
@media (prefers-reduced-motion: no-preference) {
  .ds-textarea { transition: border-color var(--dur-base) var(--ease-ui); }
}
.ds-textarea::placeholder { color: var(--text-sub); }
.ds-textarea:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-light); }
.ds-textarea:disabled { background: var(--gray-bg); color: var(--text-muted); cursor: not-allowed; resize: none; }

/* ── Form error — shared across .ds-input / .ds-select / .ds-textarea ─────────
   Field border stays default; the message carries the signal. No icon. */
.ds-form-error {
  font-size: var(--fs-meta);
  color: var(--red-text);
  line-height: var(--lh-base);
  margin-top: var(--sp-2);
}

/* ── Toast / notification ─────────────────────────────────────────────────── */
/* .ds-toast-stack is the fixed bottom-right rail; each .ds-toast is a normal
   block, so a toast can also be rendered inline (as the gallery does). */
.ds-toast-stack {
  position: fixed;
  right: var(--sp-8);
  bottom: var(--sp-8);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.ds-toast {
  width: 360px;
  max-width: calc(100vw - 2 * var(--sp-5));
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  background: var(--white);
  border: 1px solid var(--border-solid);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-modal);
  padding: 0.85rem 0.9rem;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-toast { animation: ds-toast-in var(--dur-slow) var(--ease-ui); }
}
@keyframes ds-toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.ds-toast__icon { width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; }
.ds-toast__body { flex: 1; min-width: 0; }
.ds-toast__title { font-size: var(--fs-body); font-weight: 700; color: var(--dark); }
.ds-toast__text { font-size: var(--fs-meta); color: var(--text-muted); line-height: var(--lh-base); margin-top: 0.1rem; }
.ds-toast__close { width: 22px; height: 22px; flex-shrink: 0; border: none; background: none; color: var(--text-sub); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.ds-toast--success .ds-toast__icon { background: var(--green-bg); color: var(--green); }
.ds-toast--error   .ds-toast__icon { background: var(--red-bg);   color: var(--red); }
.ds-toast--info    .ds-toast__icon { background: var(--blue-pale); color: var(--blue); }
.ds-toast--warning .ds-toast__icon { background: var(--amber-bg); color: var(--amber); }

/* ── Confirmation dialog — narrow variant of the modal ────────────────────── */
.ds-confirm-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  background: rgba(13, 30, 58, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-5);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-confirm-scrim { animation: ds-scrim-in var(--dur-base) var(--ease-ui); }
}
@keyframes ds-scrim-in { from { opacity: 0; } to { opacity: 1; } }
.ds-confirm-dialog {
  width: 400px;
  max-width: 100%;
  z-index: var(--z-modal);
  background: var(--white);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-modal);
  overflow: hidden;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-confirm-dialog { animation: ds-dialog-in var(--dur-slow) var(--ease-ui); }
}
@keyframes ds-dialog-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}
.ds-confirm__head { padding: var(--sp-6) var(--sp-6) var(--sp-4); display: flex; gap: var(--sp-3); }
.ds-confirm__icon { width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; background: var(--red-bg); color: var(--red); }
.ds-confirm__title { font-size: var(--fs-section); font-weight: 800; color: var(--dark); }
.ds-confirm__text { font-size: var(--fs-read); color: var(--text-muted); line-height: var(--lh-read); margin-top: var(--sp-3); }
.ds-confirm__foot { display: flex; justify-content: flex-end; gap: var(--sp-3); padding: var(--sp-4) var(--sp-6); border-top: 1px solid var(--border-solid); }

/* Destructive action button — pairs with .ds-btn (spec: .btn-danger). The .ds-btn
   base carries no border of its own (unlike the handoff's .btn), so set the full
   border here rather than only its colour. */
.ds-btn--danger { background: var(--red); color: #fff; border: 1px solid var(--red); }
.ds-btn--danger:hover { background: var(--red-dark); border-color: var(--red-dark); }

/* ── Callout — inline, in-flow notice ─────────────────────────────────────────
 * The static sibling of .ds-toast. A toast is fixed, transient and dismissible;
 * a callout is part of the page and stays — the consequences block on a delete
 * confirmation, a form-wide error, a standing caveat.
 *
 * Tinted background + matching hairline, no icon chip: the colour carries the
 * severity, and at this size an icon chip competes with the text. Default is
 * informational; --warn and --danger raise it.
 */
.ds-callout {
  display: flex;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-md);
  font-size: var(--fs-body);
  line-height: var(--lh-base);
  background: var(--blue-pale);
  border: 1px solid var(--blue-light);
  color: var(--dark);
}
.ds-callout__icon { flex-shrink: 0; color: var(--blue); margin-top: 0.1rem; }
.ds-callout__body { min-width: 0; }
.ds-callout__title { font-weight: 700; margin-bottom: var(--sp-2); }
.ds-callout__body > :last-child { margin-bottom: 0; }
/* Dismiss affordance (COD-308 batch 6) — replaces Bootstrap's .btn-close, whose
   glyph is a background-image SVG that inherits nothing and ignores the callout's
   tone. Pushed to the right edge so it never crowds the body text. */
.ds-callout__close {
  flex-shrink: 0;
  margin-left: auto;
  align-self: flex-start;
  padding: 0.15rem;
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  color: inherit;
  opacity: 0.55;
  cursor: pointer;
}
.ds-callout__close:hover { opacity: 1; background: rgba(0, 0, 0, 0.05); }
/* Affirmative tone. The base callout is informational blue; success needs its own
   green because a "saved successfully" message in blue reads as a hint. */
.ds-callout--success {
  background: var(--green-bg);
  border-color: var(--green-border);
  color: var(--green);
}
.ds-callout--success .ds-callout__icon { color: var(--green); }
.ds-callout--warn {
  background: var(--amber-bg);
  border-color: var(--amber-border);
  color: var(--amber-text);
}
.ds-callout--warn .ds-callout__icon { color: var(--amber); }
.ds-callout--danger {
  background: var(--red-bg);
  border-color: var(--red-border);
  color: var(--red-text);
}
.ds-callout--danger .ds-callout__icon { color: var(--red); }

/* ── Tooltip — light gray-blue with a caret ───────────────────────────────── */
.ds-tooltip {
  position: relative;
  max-width: 240px;
  z-index: var(--z-overlay);
  background: var(--blue-pale);
  color: var(--dark);
  border: 1px solid var(--blue-light);
  font-size: var(--fs-meta);
  line-height: var(--lh-base);
  padding: 0.4rem 0.6rem;
  border-radius: var(--r-xs);
  box-shadow: 0 4px 14px rgba(11, 69, 255, 0.12);
  /* A bubble sits inside whatever it explains — an uppercase mono section label, a column
     header, a chip — and would inherit its type treatment. It reads as body text everywhere. */
  font-family: var(--sans);
  font-weight: 400;
  font-style: normal;
  text-transform: none;
  letter-spacing: normal;
  text-align: left;
  white-space: normal;
  /* Where the caret points along the bubble; main.js moves it when it pins the bubble. */
  --ds-tooltip-caret-x: 50%;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-tooltip { animation: ds-tooltip-in var(--dur-fast) var(--ease-ui); }
}
@keyframes ds-tooltip-in { from { opacity: 0; } to { opacity: 1; } }
.ds-tooltip::after { content: ''; position: absolute; width: 8px; height: 8px; background: var(--blue-pale); }
.ds-tooltip--top::after    { bottom: -5px; left: 50%; transform: translateX(-50%) rotate(45deg); border-right: 1px solid var(--blue-light); border-bottom: 1px solid var(--blue-light); }
.ds-tooltip--bottom::after { top: -5px;    left: 50%; transform: translateX(-50%) rotate(45deg); border-left: 1px solid var(--blue-light); border-top: 1px solid var(--blue-light); }
.ds-tooltip--right::after  { left: -5px;   top: 50%;  transform: translateY(-50%) rotate(45deg); border-left: 1px solid var(--blue-light); border-bottom: 1px solid var(--blue-light); }
.ds-tooltip--left::after   { right: -5px;  top: 50%;  transform: translateY(-50%) rotate(45deg); border-right: 1px solid var(--blue-light); border-top: 1px solid var(--blue-light); }

/* Trigger + placement for the bubble above. The bubble was shipped without one — the
   gallery says "positioning is the app's job" — so every caller had to invent both, and
   the first three callers instead left their content sitting in the page (COD-154: the
   DSA article citations under each classification option, which Inbal asked to move into
   tooltips three times).

   Keyboard and touch reach it through `tabindex="0"` on the host plus :focus-within, so
   it is not hover-only. Put the trigger's accessible name on the host and give the bubble
   role="tooltip"; the bubble is display-toggled by visibility/opacity rather than
   `display:none` so the fade still runs. */
.ds-tooltip-host {
  position: relative;
  display: inline-flex;
  align-items: center;
  cursor: help;
  color: var(--text-muted);
  vertical-align: middle;
}
.ds-tooltip-host > .ds-tooltip {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-ui);
}
.ds-tooltip-host:hover > .ds-tooltip,
.ds-tooltip-host:focus > .ds-tooltip,
.ds-tooltip-host:focus-within > .ds-tooltip {
  visibility: visible;
  opacity: 1;
}
/* Near the left edge a centred bubble would be clipped by the panel; this modifier
   anchors it to the trigger instead. */
.ds-tooltip-host--start > .ds-tooltip { left: 0; transform: none; }
.ds-tooltip-host--start > .ds-tooltip::after { left: 12px; transform: rotate(45deg); }
/* While open, main.js lifts the bubble to the viewport ("Tooltip placement") so that no
   clipping ancestor can cut it off, and sets its coordinates and the caret's offset. After
   the --start rules on purpose: same specificity, so this wins while the bubble is fixed. */
.ds-tooltip-host > .ds-tooltip--fixed {
  position: fixed;
  bottom: auto;
  transform: none;
  max-width: min(240px, calc(100vw - 16px));
}
.ds-tooltip-host > .ds-tooltip--fixed::after {
  left: var(--ds-tooltip-caret-x);
  transform: translateX(-50%) rotate(45deg);
}

/* ── Data table ───────────────────────────────────────────────────────────── */
/* The handoff's `.dt` base table (decoded handoff lines 601–605), mapped to
   .ds-table; Einat's additions layer the sortable-header affordance on top. */
.ds-table { width: 100%; border-collapse: collapse; }

/* Horizontal scroller for a table wider than its panel. Wrap the <table>, never
   the panel: a .ds-panel clips to its own rounded corners, so an unwrapped wide
   table is not scrolled but *truncated* — at 375px that hid the last three
   columns of the API-key table, action buttons included.
   Promoted here in COD-308 batch 5. Four apps had each defined this identical
   one-liner as .pf-scroll / .rg-scroll / .ev-scroll / .op-scroll; a fifth copy
   for accounts is what prompted the merge. */
.ds-scroll { overflow-x: auto; }
.ds-table th {
  text-align: left;
  font-size: var(--fs-label);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border-solid);
  white-space: nowrap;
  background: var(--gray-bg);
}
.ds-table td {
  padding: 0.55rem 0.6rem;
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-meta);
  vertical-align: middle;
  color: var(--dark);
}
.ds-table tbody tr:last-child td { border-bottom: none; }
.ds-table tbody tr:hover td { background: var(--gray-bg); }

/* Column sort — the sortable-header affordance (spec: .dt th.sortable). */
.ds-table th.ds-sortable { cursor: pointer; }
.ds-table th.ds-sortable .ds-sort-label { display: inline-flex; align-items: center; gap: 0.3rem; }
.ds-table th.ds-sortable .ds-sort-icon { color: var(--text-sub); flex-shrink: 0; }
.ds-table th.ds-sortable:hover .ds-sort-icon { color: var(--dark); }
.ds-table th.ds-sortable.is-sorted { color: var(--blue); }
.ds-table th.ds-sortable.is-sorted .ds-sort-icon { color: var(--blue); }

/* ── Panel — the content surface (spec: .panel / .panel-head / .panel-body) ───
 * White card on the gray app background, hairline border, --r-lg, no shadow at
 * rest — cards lean on the tinted border, not elevation.
 *
 * This is the surface rule the whole design rests on: content lives on white
 * cards and --gray-bg is only ever seen *between* them. A screen whose content
 * sits directly on the app background reads as unstyled, which is exactly how
 * the first pass of COD-306 went wrong. Reach for this before anything else
 * when re-skinning a screen.
 *
 * The head is optional — a panel with only a body is a plain card.
 */
.ds-panel {
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 1px solid var(--border-solid);
  border-radius: var(--r-lg);
}
.ds-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
  padding: 0.8rem 1.1rem;
  border-bottom: 1px solid var(--border-solid);
}
.ds-panel__title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-section);
  font-weight: 800;
  color: var(--dark);
  min-width: 0;
}
/* Counts and other numerics beside a title — mono, per the type rule. */
.ds-panel__count {
  font-family: var(--mono);
  font-size: var(--fs-body);
  font-weight: 500;
  color: var(--text-muted);
}
/* Secondary line under a title — the spec's .panel-sub. */
.ds-panel__sub {
  font-size: var(--fs-meta);
  color: var(--text-muted);
  margin-top: 0.12rem;
}
.ds-panel__body { padding: 1.1rem; }
/* Flush variant — for a body that is itself a full-bleed surface (a .ds-table),
   where the panel's own padding would inset it from the border. */
.ds-panel__body--flush { padding: 0; }

/* ── Tabs (spec: .tabs / .tab / .tabpane) ─────────────────────────────────────
 * The page-level tab bar: it sits on the app background *above* the panels it
 * switches between, so a pane can hold several panels. (The handoff also draws a
 * tab strip inside a single card's head; that variant is not built until a screen
 * needs it — one tab idiom is enough to keep them consistent.)
 *
 * Appearance only. Behaviour stays on Bootstrap's tab plugin, which toggles
 * `.active` on the trigger and its target — so a trigger keeps
 * data-bs-toggle="tab" / data-bs-target and needs no bespoke JS. The panel
 * visibility rules below are what let the Bootstrap `.nav-tabs` / `.tab-pane`
 * *styling* classes go away while the plugin keeps working.
 */
.ds-tabs {
  display: flex;
  gap: 0.25rem;
  border-bottom: 1px solid var(--border-solid);
  margin-bottom: var(--sp-5);
}
.ds-tab {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0.6rem 1rem 0.7rem;
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text-muted);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  /* Overlaps the strip's own border so the active underline replaces it. */
  margin-bottom: -1px;
  cursor: pointer;
  white-space: nowrap;
}
.ds-tab:hover { color: var(--dark); }
.ds-tab.active {
  color: var(--blue);
  border-bottom-color: var(--blue);
}
/* Count beside a tab label — mono, and picks up the brand tint when active. */
.ds-tab__badge {
  font-family: var(--mono);
  font-size: var(--fs-label);
  font-weight: 700;
  padding: 0.08rem 0.38rem;
  border-radius: var(--r-pill);
  background: var(--gray-bg);
  color: var(--text-muted);
  border: 1px solid var(--border-solid);
}
.ds-tab.active .ds-tab__badge {
  background: var(--blue-pale);
  color: var(--blue);
  border-color: var(--blue-light);
}
.ds-tabpanel { display: none; }
.ds-tabpanel.active { display: block; }

/* ── Spinner — in-progress indicator ──────────────────────────────────────────
 * A ring with one quadrant in the brand blue, sized to sit inline with body
 * text. Replaces Bootstrap's .spinner-border, whose 0.25em border and currentColor
 * ring read as chrome from the old system.
 */
.ds-spinner {
  display: inline-block;
  flex-shrink: 0;
  width: 0.85em;
  height: 0.85em;
  border: 2px solid var(--border-solid);
  border-top-color: var(--blue);
  border-radius: 50%;
  animation: ds-spin 0.7s linear infinite;
}
@keyframes ds-spin {
  to { transform: rotate(360deg); }
}
/* Respect a reduced-motion preference — the ring still reads as "busy" static. */
@media (prefers-reduced-motion: reduce) {
  .ds-spinner { animation: none; }
}

/* ── Empty state ──────────────────────────────────────────────────────────── */
.ds-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-4);
  padding: var(--sp-8);
  background: var(--gray-bg);
  border: 1px dashed var(--border-solid);
  border-radius: var(--r-md);
}
.ds-empty-state__icon { width: 52px; height: 52px; border-radius: 50%; background: var(--blue-pale); border: 1px solid var(--blue-light); color: var(--blue); display: inline-flex; align-items: center; justify-content: center; }
.ds-empty-state__title { font-size: var(--fs-section); font-weight: 800; color: var(--dark); }
.ds-empty-state__text { font-size: var(--fs-body); color: var(--text-muted); line-height: var(--lh-base); max-width: 340px; }
.ds-empty-state--compact { padding: var(--sp-6); }
.ds-empty-state--compact .ds-empty-state__icon { display: none; }

/* ── Pagination ───────────────────────────────────────────────────────────── */
.ds-pagination { display: flex; align-items: center; gap: var(--sp-2); }
.ds-page-btn {
  min-width: 32px;
  height: 32px;
  padding: 0 0.5rem;
  box-sizing: border-box;
  border: 1px solid var(--border-solid);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--dark);
  font-family: var(--mono);
  font-size: var(--fs-meta);
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-page-btn { transition: border-color var(--dur-fast) var(--ease-ui), background var(--dur-fast) var(--ease-ui); }
}
.ds-page-btn:hover { border-color: rgba(11, 69, 255, 0.3); background: var(--gray-bg); }
.ds-page-btn.is-current { background: var(--blue); border-color: var(--blue); color: #fff; font-weight: 700; }
.ds-page-btn:disabled { color: var(--text-sub); opacity: 0.55; cursor: not-allowed; background: var(--white); }
.ds-page-btn:disabled:hover { border-color: var(--border-solid); }
.ds-page-ellipsis { font-family: var(--mono); font-size: var(--fs-meta); color: var(--text-sub); padding: 0 0.2rem; }
/* Centred variant — the shared _pagination.html sits under a full-width list. */
.ds-pagination--centered { justify-content: center; margin-top: var(--sp-6); }
/* Double chevrons (first/last page) sit tight rather than gapped, so the pair
   reads as one "jump to the end" mark instead of two separate steps. */
.ds-page-btn .ds-icon + .ds-icon { margin-left: -5px; }
.ds-page-summary { font-size: var(--fs-meta); color: var(--text-muted); }
.ds-page-summary .ds-num { font-family: var(--mono); color: var(--dark); }

/* ── Detached content fragment ────────────────────────────────────────────
 * For markup served on its own (an AJAX partial injected into a container the
 * template cannot see). Deliberately UNSCOPED — a `.some-view .x-*` rule would
 * silently not apply once the host drops the fragment somewhere else, which is
 * the whole failure mode this set exists to avoid. Added in COD-308 batch 5 for
 * compliance/_obligation_detail_partial.html.
 */
.ds-frag {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  font-size: var(--fs-meta);
  line-height: var(--lh-base);
  color: var(--dark);
}
.ds-frag__section { display: flex; flex-direction: column; gap: var(--sp-2); align-items: flex-start; }
.ds-frag__head {
  margin: 0;
  font-size: var(--fs-body);
  font-weight: 700;
  color: var(--dark);
}
.ds-frag__ref {
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--blue);
  text-decoration: none;
}
.ds-frag__ref:hover { text-decoration: underline; }
.ds-frag__text { margin: 0; }
.ds-frag__note { font-size: var(--fs-label); color: var(--text-sub); }
.ds-frag__row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.ds-frag__summary {
  align-self: stretch;
  padding: var(--sp-3);
  background: var(--gray-bg);
  border-radius: var(--r-sm);
}
.ds-frag__summary > :last-child { margin-bottom: 0; }
/* Recommendation rule — neutral by default, tinted to match the verdict. */
.ds-frag__rec {
  align-self: stretch;
  padding-left: var(--sp-3);
  border-left: 3px solid var(--border-strong);
}
.ds-frag__rec--warn { border-left-color: var(--amber); }
.ds-frag__rec--ok { border-left-color: var(--green); }
.ds-frag__rec > :last-child { margin-bottom: 0; }

/* ── Page header ─────────────────────────────────────────────────────────────
 * The shared includes/_page_header.html frame. Unscoped because the include is
 * used by every app; a per-app copy is how four apps ended up with four
 * identical .xx-scroll rules (see .ds-scroll above). Added COD-308 batch 6.
 */
.ds-page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-5);
  flex-wrap: wrap;
  margin-bottom: var(--sp-6);
}
.ds-page-head__title {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0;
  font-size: var(--fs-stat);
  font-weight: 800;
  color: var(--dark);
  line-height: var(--lh-tight);
}
.ds-page-head__sub {
  margin: var(--sp-2) 0 0;
  max-width: 80ch;
  font-size: var(--fs-meta);
  color: var(--text-muted);
  line-height: var(--lh-base);
}
.ds-page-head__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
