/* ldu/components.css: the ldu_ui gem's component layer (slice 2 brings only the theme-split values the app chrome reads;
   the component recipes arrive with the slices that use them).

   Declared on :root and on every [data-theme] element (the places tokens.css declares its palette), because a custom
   property that reads another is resolved where it is declared: a subtree pinned dark (the top bar, the rail) must
   recompute these from its own tokens. Colours are all var() and color-mix() of fraction-ds tokens. */

:root, [data-theme] {
  --ldu-r-xl: calc(var(--r-2) + 4px);                                 /* the gem's rounded-xl (14px): menus, panels */
  --ldu-wash: color-mix(in srgb, var(--ink) 6%, transparent);         /* hover wash: ink-50 on light, ink-700 on dark */
  --ldu-on-hover: color-mix(in srgb, var(--accent) 24%, transparent); /* the current item, hovered: one step stronger than accent-soft */
}

/* the chosen segment of a segmented control (the theme switch): translucent lilac on dark, ink on light */
:root, [data-theme="dark"] {
  --ldu-seg-on: var(--accent-soft);
  --ldu-seg-on-ink: var(--accent);
  --ldu-hit: color-mix(in srgb, var(--accent) 34%, transparent);   /* a search hit (mark.ldu-hit): light lilac */
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --ldu-seg-on: color-mix(in oklab, var(--ink) 88%, var(--bg-2));
    --ldu-seg-on-ink: var(--bg);
    --ldu-hit: color-mix(in srgb, var(--accent) 22%, transparent);
  }
}
[data-theme="light"] {
  --ldu-seg-on: color-mix(in oklab, var(--ink) 88%, var(--bg-2));
  --ldu-seg-on-ink: var(--bg);
  --ldu-hit: color-mix(in srgb, var(--accent) 22%, transparent);
}

/* ── button (.btn) ──────────────────────────────────────────────
   THE button. Every page draws this one; no page restyles it (bin/ds-lint fails on a `<scope> .btn` rule that sets a colour,
   border, background, height or padding). Change a button here and it changes everywhere; /design-system#button shows every cell.
   Interface (classes compose; ui_button in app/helpers/ui_helper.rb writes them):
     variant  (none) secondary: outline · --primary: solid ink · --ghost: no border, wash on hover · --link: a text link
     tone     (none) neutral · --danger: red · --success: green. A tone on its own is the outline; add --solid (filled) or --ghost (text only).
     size     --sm 36px · (none) 40px · --lg 44px; under 720px every button is at least 44px. --icon makes it a square; --block fills the row.
     state    :hover, :focus-visible (the 2px ring), :active, :disabled / [aria-disabled="true"] / .is-locked (dimmed, no hover),
              --on (the chosen one of a set), [aria-busy="true"] or a submit button inside a submitting Turbo form (a spinner, no clicks).
   The look is set through --btn-* properties, so a variant only says what differs. Colours: tokens only. Never a shadow, a lift or a filter. */
:root, [data-theme] {
  --btn-h-sm: 36px;
  --btn-h-md: 40px;
  --btn-h-lg: 44px;
  --btn-radius: var(--r-2);
  --btn-primary-bg: var(--ink);
  --btn-primary-fg: var(--bg);
  --btn-primary-hover: color-mix(in oklab, var(--ink) 86%, var(--bg));
  --ok-soft: color-mix(in srgb, var(--ok) 14%, transparent);
}
.btn {
  --btn-bg: transparent; --btn-fg: var(--ink); --btn-border: var(--line);
  --btn-bg-hover: var(--ldu-wash); --btn-fg-hover: var(--btn-fg); --btn-border-hover: var(--ink-4);
  --btn-bg-active: color-mix(in srgb, var(--ink) 10%, transparent);
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  box-sizing: border-box; height: var(--btn-h-md); padding: 0 16px;
  border: 1px solid var(--btn-border); border-radius: var(--btn-radius);
  background: var(--btn-bg); color: var(--btn-fg); filter: none; box-shadow: none;
  font-family: var(--sans); font-size: var(--fs-body); font-weight: 500; line-height: 1.2; white-space: nowrap;
  text-decoration: none; cursor: pointer;
  transition: color var(--dur), background-color var(--dur), border-color var(--dur);
}
.btn:where(:hover, .is-hover):not(:disabled, [aria-disabled="true"], .is-locked, [aria-busy="true"]) { background: var(--btn-bg-hover); border-color: var(--btn-border-hover); color: var(--btn-fg-hover); filter: none; }
.btn:where(:active, .is-active):not(:disabled, [aria-disabled="true"], .is-locked) { background: var(--btn-bg-active); }
.btn:is(:focus-visible, .is-focus) { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }
.btn:is(:disabled, [aria-disabled="true"], .is-locked) { opacity: .5; cursor: not-allowed; }
.btn .icon { width: 16px; height: 16px; flex: none; }

.btn--primary { --btn-bg: var(--btn-primary-bg); --btn-fg: var(--btn-primary-fg); --btn-border: transparent; --btn-bg-hover: var(--btn-primary-hover); --btn-border-hover: transparent; --btn-bg-active: var(--btn-primary-hover); }
.btn--ghost { --btn-border: transparent; --btn-border-hover: transparent; }
.btn--link { --btn-fg: var(--accent); --btn-border: transparent; --btn-border-hover: transparent; --btn-bg-hover: transparent; --btn-bg-active: transparent; --btn-fg-hover: var(--ink); height: auto; padding: 0; text-decoration: underline; text-underline-offset: 3px; }

.btn--danger { --tone: var(--danger); --tone-soft: var(--danger-soft); }
.btn--success { --tone: var(--ok); --tone-soft: var(--ok-soft); }
/* Lilac (the accent): an outline, or .btn--solid for the fill (ink text on dark, paper on light). On dark it is the client portal's lilac-on-ink
   pair (Lia's welcome, portal_welcome/show); a hover tints, it never fills a container. */
.btn--accent { --tone: var(--accent); --tone-soft: color-mix(in srgb, var(--accent) 12%, transparent); }
.btn:is(.btn--danger, .btn--success, .btn--accent) { --btn-fg: var(--tone); --btn-border: var(--tone); --btn-bg-hover: var(--tone-soft); --btn-border-hover: var(--tone); --btn-bg-active: var(--tone-soft); }
.btn:is(.btn--danger, .btn--success, .btn--accent).btn--ghost { --btn-border: transparent; --btn-border-hover: transparent; }
.btn:is(.btn--danger, .btn--success, .btn--accent).btn--solid {
  --btn-bg: var(--tone); --btn-fg: var(--bg); --btn-border: transparent; --btn-fg-hover: var(--bg); --btn-border-hover: transparent;
  --btn-bg-hover: color-mix(in oklab, var(--tone) 84%, var(--ink)); --btn-bg-active: var(--btn-bg-hover);
}

.btn:is(.btn--on, [aria-pressed="true"]) { --btn-fg: var(--accent); --btn-border: var(--accent); --btn-border-hover: var(--accent); }
.btn--ghost:is(.btn--on, [aria-pressed="true"]) { --btn-border: transparent; --btn-border-hover: transparent; --btn-bg: var(--ldu-wash); }

.btn--sm { height: var(--btn-h-sm); padding: 0 12px; font-size: var(--fs-ui); }
.btn--lg { height: var(--btn-h-lg); padding: 0 24px; }
.btn--icon { width: var(--btn-h-md); padding: 0; }
.btn--icon.btn--sm { width: var(--btn-h-sm); }
.btn--icon.btn--lg { width: var(--btn-h-lg); }
.btn--block { display: flex; width: 100%; }
@media (max-width: 720px) {
  .btn:not(.btn--link) { min-height: 44px; }
  .btn--icon { min-width: 44px; }
}

/* loading: the label stays (the width does not jump), a spinner takes the icon's place, nothing can be clicked twice */
.btn:is([aria-busy="true"], form[aria-busy="true"] [type="submit"]:disabled) { pointer-events: none; }
.btn:is([aria-busy="true"], form[aria-busy="true"] [type="submit"]:disabled)::before {
  content: ""; flex: none; width: 14px; height: 14px; box-sizing: border-box;
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%;
  animation: ldu-spin 700ms linear infinite;
}
.btn:is([aria-busy="true"], form[aria-busy="true"] [type="submit"]:disabled) > .icon:first-child { display: none; }
@keyframes ldu-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .btn[aria-busy="true"]::before, form[aria-busy="true"] .btn::before { animation-duration: 2s; } }

/* ── surfaces every component below reads ───────────────────────── */
:root, [data-theme] {
  --ldu-recess: color-mix(in srgb, var(--board) 60%, transparent);  /* a table head, an empty state */
  --ldu-raise: color-mix(in oklab, var(--ink) 4%, var(--bg-2));     /* a clickable card or row, hovered */
}

/* ── chip (.chip) ───────────────────────────────────────────────
   A label in a pill: neutral surface, sentence case. Status is a 6px dot (--ok, --warn, --danger, --accent, --neutral), never a fill, so
   a list of twenty chips stays calm. --on is the chosen one of a set. A chip that is a link, button or label washes on hover; a chip that
   does something big is a button instead. An icon inside drops to 12px. No page restyles it. */
.chip {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  box-sizing: border-box; height: auto; min-height: 24px; padding: 2px 10px;
  border: 1px solid var(--line); border-radius: 999px; background: var(--ldu-wash); color: var(--ink-2);
  font-family: var(--sans); font-size: var(--fs-meta); font-weight: 500; line-height: 1.4; letter-spacing: 0; text-transform: none; white-space: nowrap;
  text-decoration: none;
}
.chip--on { border-color: var(--accent-fill); background: transparent; color: var(--accent); }
:is(a, button, summary, label).chip { cursor: pointer; transition: color var(--dur), background-color var(--dur), border-color var(--dur); }
:is(a, button, summary, label).chip:where(:hover, .is-hover) { border-color: var(--ink-4); background: color-mix(in srgb, var(--ink) 10%, transparent); color: var(--ink); }
:is(a, button, summary, label).chip:focus-visible { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }
.chip > .icon { width: 12px; height: 12px; flex: none; }
.chip--late::before { content: ""; width: 6px; height: 6px; flex: none; border-radius: 50%; background: var(--danger); }
/* a count beside a label (a tab, a bell, a button): the same pill, tabular */
.ldu-count {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  box-sizing: border-box; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px;
  background: var(--ldu-wash); color: var(--ink-2); font-size: var(--fs-meta); font-weight: 500; font-variant-numeric: tabular-nums; line-height: 1;
}
.ldu-count--on { background: var(--ink); color: var(--bg); }

/* ── alert (.ldu-alert) ─────────────────────────────────────────
   A message about the page or what just happened: an icon in the tone's colour, a bold first line, one line of detail. Neutral surface;
   the tone is the icon and a tinted hairline, never a coloured box. Tones: (none) info · --ok · --warn · --danger.
   Markup: components/ldu/_alert.html.erb. */
.ldu-alert {
  --alert-tone: var(--ink-2);
  display: flex; align-items: flex-start; gap: 12px; box-sizing: border-box; margin: 0; padding: 12px 16px;
  border: 1px solid color-mix(in srgb, var(--alert-tone) 35%, var(--line)); border-radius: var(--r-2); background: var(--bg-2); color: var(--ink);
  font-size: var(--fs-ui); line-height: 1.5;
}
.ldu-alert--ok { --alert-tone: var(--ok); }
.ldu-alert--warn { --alert-tone: var(--warn); }
.ldu-alert--danger { --alert-tone: var(--danger); }
.ldu-alert > .icon { flex: none; width: 18px; height: 18px; margin-top: 1px; color: var(--alert-tone); }
.ldu-alert__body { display: grid; gap: 2px; min-width: 0; flex: 1 1 auto; }
.ldu-alert__body > * { margin: 0; }
.ldu-alert__title { font-weight: 600; }
.ldu-alert__text { color: var(--ink-2); }
.ldu-alert__action { flex: none; align-self: center; }

/* ── card (.card) ───────────────────────────────────────────────
   A hairline frame on the card surface, 14px corners, 24px inside (16px on a phone and with --tight). It never lifts and never casts a
   shadow. A card that is a link (--link, or any a.card) steps its border and washes on hover. Content stacks; the page sets the gaps. */
.card {
  box-sizing: border-box; display: flex; flex-direction: column; gap: 0; padding: 16px;
  border: 1px solid var(--line); border-radius: var(--ldu-r-xl); background: var(--bg-2); color: var(--ink);
  font-size: var(--fs-body); text-decoration: none; box-shadow: none;
  transition: border-color var(--dur), background-color var(--dur);
}
.card:hover { border-color: var(--line); }
@media (min-width: 640px) { .card { padding: 24px; } }
.card.card--tight { padding: 16px; }
.card > p { margin: 0; }
.card > p + p { margin-top: 4px; }
.card__title { margin: 0; font-family: var(--display); font-size: var(--fs-h3); font-weight: 600; line-height: 1.4; color: var(--ink); }
.card__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: var(--fs-meta); color: var(--ink-2); }
:is(a.card, .card--link) { cursor: pointer; }
:is(a.card, .card--link):where(:hover, .is-hover) { border-color: var(--ink-4); background: var(--ldu-raise); }
:is(a.card, .card--link):focus-visible { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }
.card--on { border-color: var(--accent-fill); }

/* ── stat (.stat) ───────────────────────────────────────────────
   One number that matters: what it counts (12/500), the figure (DM Sans, tabular), one line of how. --lg for the first one on a dashboard.
   Markup: components/ldu/_stat.html.erb (a card around it, or a link card when it opens the full view). */
.stat { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.stat__label { margin: 0; font-size: var(--fs-ui); font-weight: 500; color: var(--ink-2); }
.stat__value {
  margin: 0; font-family: var(--display); font-size: 2rem; font-weight: 600; line-height: 1.2; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; color: var(--ink); overflow-wrap: anywhere;
}
.stat--lg .stat__value { font-size: 2.5rem; }
.stat--sm .stat__value { font-size: 1.5rem; }
.stat__note { margin: 0; font-size: var(--fs-meta); color: var(--ink-2); }

/* ── table (.ldu-table) ─────────────────────────────────────────
   A framed table: the frame is the wrapper (it scrolls sideways when the table is wider), the head is a 40px recessed stripe in muted 12px,
   cells have 12px inside and a hairline between rows; a row that is a link washes on hover (--hover on the table, or tr.is-link).
   Markup: <div class="ldu-table"><table>…</table></div>. A number column takes .num (right aligned, tabular); a row header is <th scope="row">.
   --menus drops the sideways scroll for a table whose rows open a menu. */
.ldu-table { box-sizing: border-box; max-width: 100%; overflow-x: auto; border: 1px solid var(--line); border-radius: var(--ldu-r-xl); background: var(--bg-2); }
.ldu-table > table { width: 100%; border-collapse: collapse; font-size: var(--fs-ui); color: var(--ink); }
.ldu-table thead th {
  height: 40px; padding: 0 12px; border-bottom: 1px solid var(--line); background: var(--ldu-recess); color: var(--ink-2);
  font-size: var(--fs-meta); font-weight: 500; text-align: left; text-transform: none; letter-spacing: 0; white-space: nowrap;
}
.ldu-table :is(tbody th, td) { padding: 12px; text-align: left; font-weight: 500; border-top: 1px solid var(--line); vertical-align: middle; }
.ldu-table td { font-weight: 400; }
.ldu-table tbody tr:first-child :is(th, td) { border-top: 0; }
.ldu-table--menus { overflow: visible; }   /* a table whose rows open a menu or dropdown: no sideways scroll, so the panel is not cut off */
.ldu-table :is(th, td):first-child { padding-left: 16px; }
.ldu-table :is(th, td):last-child { padding-right: 16px; }
.ldu-table :is(th, td).num { text-align: right; font-variant-numeric: tabular-nums; }
.ldu-table td small { font-size: var(--fs-meta); color: var(--ink-2); }
.ldu-table td a:not(.btn, .chip) { color: var(--ink); text-decoration-line: underline; text-decoration-color: var(--ink-4); text-underline-offset: 3px; transition: color var(--dur), text-decoration-color var(--dur); }
.ldu-table td a:not(.btn, .chip):hover { color: var(--accent); text-decoration-color: var(--accent); }
.ldu-table tbody tr { transition: background-color var(--dur); }
.ldu-table--hover tbody tr:where(:hover, .is-hover), .ldu-table tr.is-link:hover { background: var(--ldu-wash); }
.ldu-table tr.is-link { cursor: pointer; }

/* ── fields (.input .select .textarea, .field, .check, .radio, .switch) ──
   Controls are 40px (--sm 36px; 44px under 720px), a hairline that steps darker on hover, the 2px ring on focus, dimmed when disabled,
   a red hairline when invalid (aria-invalid). Form controls: one hover, the border to --ink-4 (also .ldu-dd__trigger), never a fill, shadow or move. .field stacks a label (14/500), the control, then a hint or an error. .check is a box with an
   ink square inside when ticked; .radio the same as a circle; .switch slides to ink. */
.input, .select, .textarea {
  box-sizing: border-box; width: 100%; border: 1px solid var(--line); border-radius: var(--r-2); background: var(--bg); color: var(--ink);
  font-family: var(--sans); font-size: var(--fs-body); box-shadow: none; transition: border-color var(--dur);
}
.input, .select { height: var(--btn-h-md); padding: 0 12px; }
.select { padding-right: 32px; }
.textarea { min-height: 96px; padding: 8px 12px; line-height: 1.5; }
.input--sm, .select.input--sm { height: var(--btn-h-sm); font-size: var(--fs-ui); }
:is(.input, .select, .textarea):where(:hover, .is-hover):not(:disabled) { border-color: var(--ink-4); }
:is(.input, .select, .textarea):is(:focus, :focus-visible, .is-focus) { outline: var(--ring-w) solid var(--ring); outline-offset: 0; border-color: var(--ink-3); box-shadow: none; }
:is(.input, .select, .textarea):disabled { opacity: .5; cursor: not-allowed; }
:is(.input, .select, .textarea)[aria-invalid="true"] { border-color: var(--danger); }
:is(.input, .textarea)::placeholder { color: var(--ink-3); }
.field .is-hover:is(.input, .select, .textarea), .field.is-hover :is(.input, .select, .textarea) { border-color: var(--ink-4); }
.field.is-focus :is(.input, .select, .textarea) { outline: var(--ring-w) solid var(--ring); outline-offset: 0; border-color: var(--ink-3); }
@media (max-width: 720px) { .input, .select { min-height: 44px; } }

.field { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.field__label { font-size: var(--fs-ui); font-weight: 500; color: var(--ink); }
.field__hint { font-size: var(--fs-meta); line-height: 1.5; color: var(--ink-2); }
.field__error { font-size: var(--fs-meta); line-height: 1.5; color: var(--danger); }
.field--inline { flex-direction: row; align-items: center; gap: 8px; cursor: pointer; }

.check, .radio {
  appearance: none; position: relative; display: inline-block; flex: none; box-sizing: border-box; width: 18px; height: 18px; margin: 0;
  border: 1px solid var(--line-3); border-radius: 5px; background: var(--bg); box-shadow: none; cursor: pointer; transition: border-color var(--dur);
}
.radio { border-radius: 50%; }
.check::after, .radio::after {
  content: ""; position: absolute; inset: 3px; width: auto; height: auto; border: 0; border-radius: 2px; background: var(--ink);
  opacity: 0; transform: scale(.5); transition: opacity var(--dur), transform var(--dur);
}
.radio::after { border-radius: 50%; }
:is(.check, .radio):where(:hover, .is-hover):not(:checked, :disabled), :where(label:hover) > :is(.check, .radio):not(:checked, :disabled) { border-color: var(--ink-3); }
:is(.check, .radio):where(:hover, .is-hover):not(:checked, :disabled)::after, :where(label:hover) > :is(.check, .radio):not(:checked, :disabled)::after { opacity: .22; transform: scale(1); }
:is(.check, .radio):checked { border-color: var(--ink); background: var(--bg); box-shadow: none; }
:is(.check, .radio):checked::after { inset: 3px; width: auto; height: auto; opacity: 1; transform: scale(1); border: 0; }
:is(.check, .radio):is(:focus-visible, .is-focus) { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }
:is(.check, .radio):disabled { opacity: .45; cursor: not-allowed; }

.switch { width: 36px; height: 20px; background: var(--ink-4); transition: background-color var(--dur); }
.switch::after { transition: transform var(--dur); }
.switch:checked { background: var(--ink); }
.switch:checked::after { transform: translateX(16px); }
.switch:is(:focus-visible, .is-focus) { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }
.switch:disabled { opacity: .45; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) { .check, .check::after, .radio, .radio::after, .switch, .switch::after { transition: none; } }

/* ── avatar (.avatar) ───────────────────────────────────────────
   Everything round with a person in it: a photo filling the circle (.avatar__img), else initials on the neutral wash. Sizes: --xs 20 ·
   --sm 24 · (none) 32 · --md 36 · --lg 96. An AI's circle carries its provider logo (--model). .avatars overlaps a group by 6px.
   As a button (.avatar-btn) the border turns accent on hover: never a scale. */
.avatar {
  position: relative; display: inline-flex; align-items: center; justify-content: center; flex: none; box-sizing: border-box; overflow: hidden;
  width: 32px; height: 32px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-2); color: var(--ink);
  font-family: var(--sans); font-size: 11px; font-weight: 600; line-height: 1; letter-spacing: 0; vertical-align: middle;
}
.avatar--xs { width: 20px; height: 20px; font-size: 9px; }
.avatar--sm { width: 24px; height: 24px; font-size: 10px; }
.avatar--md { width: 36px; height: 36px; font-size: 13px; }
.avatar--lg { width: 96px; height: 96px; font-size: 30px; border-width: 2px; }
.avatar--model { background: var(--bg-2); color: var(--ink); }
.avatar--model .modelicon { width: 18px; height: 18px; }
.avatar__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
.avatars { display: inline-flex; align-items: center; }
.avatars > .avatar { outline: 2px solid var(--bg); }
.avatars > .avatar + .avatar { margin-left: -6px; }
.avatar-btn { appearance: none; display: inline-flex; padding: 0; border: 0; border-radius: 999px; background: none; cursor: pointer; }
.avatar-btn .avatar { transition: border-color var(--dur); }
.avatar-btn:where(:hover, .is-hover) .avatar { border-color: var(--accent); }
.avatar-btn:focus-visible { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }

/* ── empty state (.ldu-empty) ───────────────────────────────────
   For a container people expect to hold something: an icon, what goes here (a title), one line, and the one way to fill it. A recessed
   dashed frame, centred. --page is the big one for a whole page; (none) sits inside a card or a pane. Markup: components/ldu/_empty.html.erb. */
.ldu-empty {
  display: grid; justify-items: center; align-content: center; gap: 8px; box-sizing: border-box; padding: 32px 24px; text-align: center;
  border: 1px dashed var(--line-3); border-radius: var(--ldu-r-xl); background: var(--ldu-recess); color: var(--ink-2); font-size: var(--fs-ui);
}
.ldu-empty > * { margin: 0; }
.ldu-empty > .icon { width: 24px; height: 24px; color: var(--ink-3); }
.ldu-empty__title { font-family: var(--display); font-size: var(--fs-h3); font-weight: 600; line-height: 1.4; color: var(--ink); }
.ldu-empty__text { max-width: 40ch; line-height: 1.5; }
.ldu-empty__action { margin-top: 8px; }
.ldu-empty--page { padding: 64px 24px; }
.ldu-empty--page > .icon { width: 32px; height: 32px; }
.ldu-empty--page .ldu-empty__title { font-size: var(--fs-h2); }
.ldu-empty--bare { border: 0; background: none; }

/* ── progress (.ldu-progress) ───────────────────────────────────
   A rounded 6px track (--lg 12px) that fills with the accent; --ok, --warn, --danger recolour the fill. Determinate only: the width is the
   value. Markup: <span class="ldu-progress" role="progressbar" aria-valuenow="40" …><span class="ldu-progress__fill" style="width: 40%"></span></span> */
.ldu-progress { display: block; box-sizing: border-box; width: 100%; height: 6px; overflow: hidden; border-radius: 999px; background: var(--ldu-wash); }
.ldu-progress--lg { height: 12px; }
.ldu-progress__fill { display: block; height: 100%; border-radius: inherit; background: var(--accent-fill); transition: width var(--dur); }
.ldu-progress--ok .ldu-progress__fill { background: var(--ok); }
.ldu-progress--warn .ldu-progress__fill { background: var(--warn); }
.ldu-progress--danger .ldu-progress__fill { background: var(--danger); }

/* ── segmented control (.ldu-seg) ───────────────────────────────
   Two to five choices that filter one thing in place (light / dark / auto, week / month). A hairline track; the chosen segment is solid
   (ink on light, lilac on dark: --ldu-seg-on); the others wash on hover. Segments are buttons or links with aria-pressed / --on.
   --lg: 40px segments (44px on a phone), for a switch that leads a page. */
.ldu-seg { display: inline-flex; align-items: center; gap: 2px; box-sizing: border-box; padding: 2px; border: 1px solid var(--line); border-radius: var(--r-2); background: var(--bg); }
.ldu-seg__opt {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px; box-sizing: border-box; height: 32px; padding: 0 12px;
  border: 0; border-radius: calc(var(--r-2) - 2px); background: transparent; color: var(--ink-2);
  font-family: var(--sans); font-size: var(--fs-ui); font-weight: 500; white-space: nowrap; text-decoration: none; cursor: pointer;
  transition: color var(--dur), background-color var(--dur);
}
.ldu-seg__opt:where(:hover, .is-hover):not(.ldu-seg__opt--on, [aria-pressed="true"]) { background: var(--ldu-wash); color: var(--ink); }
.ldu-seg__opt:is(.ldu-seg__opt--on, [aria-pressed="true"], [aria-current]) { background: var(--ldu-seg-on); color: var(--ldu-seg-on-ink); }
.ldu-seg__opt:focus-visible { outline: var(--ring-w) solid var(--ring); outline-offset: 0; }
.ldu-seg__opt .icon { width: 16px; height: 16px; }
.ldu-seg--lg .ldu-seg__opt { height: 40px; padding: 0 16px; font-size: var(--fs-body); }
@media (max-width: 720px) { .ldu-seg--lg .ldu-seg__opt { height: 44px; } }

/* ── menu (.menu) ───────────────────────────────────────────────
   A floating card of actions under its trigger: 14px corners, the card surface, the one shadow overlays may have. Rows are 36px with 8×12
   inside and wash on hover; the current row has a trailing check; a destructive row is last, after a .menu__sep, and washes red. */
.menu {
  box-sizing: border-box; min-width: 200px; padding: 4px;
  border: 1px solid var(--line); border-radius: var(--ldu-r-xl); background: var(--bg-2); box-shadow: var(--shadow-menu);
}
.menu__item {
  display: flex; align-items: center; gap: 8px; box-sizing: border-box; width: 100%; min-height: 36px; padding: 8px 12px;
  border: 0; border-radius: var(--r-1); background: none; color: var(--ink);
  font-family: var(--sans); font-size: var(--fs-ui); line-height: 1.4; text-align: left; text-decoration: none; cursor: pointer;
  transition: background-color var(--dur), color var(--dur);
}
.menu__item:where(:hover, .is-hover) { background: var(--ldu-wash); }
.menu__item:focus-visible { outline: var(--ring-w) solid var(--ring); outline-offset: -2px; }
.menu__item .icon { width: 16px; height: 16px; flex: none; color: var(--ink-2); }
.menu__item--on { font-weight: 500; }
.menu__item--on > .icon:last-child { margin-left: auto; color: var(--ink); }
.menu__item--danger, .menu__item--danger .icon { color: var(--danger); }
.menu__item--danger:where(:hover, .is-hover) { background: var(--danger-soft); }
.menu__sep { height: 1px; margin: 4px 0; background: var(--line); }
.menu__label { padding: 8px 12px 4px; font-size: var(--fs-meta); font-weight: 500; color: var(--ink-2); text-transform: none; letter-spacing: 0; }

/* ── spinner (.ldu-spinner) ─────────────────────────────────────
   A wait you cannot see the shape of (a request in flight): a 16px ring that turns (--lg 24px), in the text colour. Content that has a
   shape waits as a .skeleton instead. A button that is busy has its own (aria-busy, above). */
.ldu-spinner {
  display: inline-block; flex: none; box-sizing: border-box; width: 16px; height: 16px; vertical-align: -3px;
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; color: var(--ink-3);
  animation: ldu-spin 700ms linear infinite;
}
.ldu-spinner--lg { width: 24px; height: 24px; border-width: 3px; }
@media (prefers-reduced-motion: reduce) { .ldu-spinner { animation-duration: 2s; } }

/* ── toast (.toast) ─────────────────────────────────────────────
   fraction-ds's ink bubble at the bottom of the window, for what just happened. --ok and --danger add a dot in the tone's colour
   before the words (the bubble itself stays ink). */
.toast:is(.toast--ok, .toast--danger) { display: inline-flex; align-items: center; gap: 8px; }
.toast:is(.toast--ok, .toast--danger)::before { content: ""; flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--ok); }
.toast--danger::before { background: var(--danger); }

/* ── tooltip ([data-tip]) ───────────────────────────────────────
   A short name for an icon-only control, on hover and keyboard focus: an ink bubble, 12px, above the control. The control still
   carries aria-label; the tip is for eyes. Never holds anything a person must read to finish a task. */
[data-tip] { position: relative; }
[data-tip]:is(:hover, :focus-visible, .is-hover)::after {
  content: attr(data-tip); position: absolute; z-index: 130; top: auto; bottom: 100%; left: 50%; transform: translateX(-50%); margin: 0 0 6px;
  width: max-content; max-width: 240px; padding: 4px 8px; border-radius: var(--r-1); background: var(--ink); color: var(--bg);
  font-family: var(--sans); font-size: var(--fs-meta); font-weight: 500; line-height: 1.4; text-align: center; white-space: pre-wrap; pointer-events: none;
  opacity: 0; animation: ds-tip-in 120ms 300ms forwards;
}
[data-tip].is-hover::after { opacity: 1; animation: none; }
/* Where it sits, for a control near an edge or inside a box that clips (the side chat): data-tip-side="bottom" (a top bar, a header row), "left" or
   "right" (a rail, a divider); data-tip-align="start" or "end" lines it up with the control's left or right edge instead of its centre. */
[data-tip][data-tip-side="bottom"]:is(:hover, :focus-visible, .is-hover)::after { top: 100%; bottom: auto; margin: 6px 0 0; }
[data-tip][data-tip-side="left"]:is(:hover, :focus-visible, .is-hover)::after { top: 50%; right: 100%; bottom: auto; left: auto; transform: translateY(-50%); margin: 0 6px 0 0; }
[data-tip][data-tip-side="right"]:is(:hover, :focus-visible, .is-hover)::after { top: 50%; bottom: auto; left: 100%; transform: translateY(-50%); margin: 0 0 0 6px; }
[data-tip][data-tip-align="start"]:is(:hover, :focus-visible, .is-hover)::after { left: 0; transform: none; }
[data-tip][data-tip-align="end"]:is(:hover, :focus-visible, .is-hover)::after { right: 0; left: auto; transform: none; }
/* Not on a touch screen (a tap leaves :hover stuck on), and not over the menu its own control has opened. Keyboard focus still shows it. */
@media (hover: none) { [data-tip]:hover:not(:focus-visible, .is-hover)::after { content: none; } }
details[open] > summary[data-tip]::after { content: none; }

/* ── tabs ───────────────────────────────────────────────────────
   One flat strip, a lilac label over a lilac underline; no track, no
   fill. Hover: the label turns lilac and a faint underline shows, so a
   hovered tab never looks current. --bar is the header's version: the
   strip fills the bar's height and the underline sits on its hairline. */
:where(.ldu-tabs) {
  display: flex;
  align-items: stretch;
  gap: 4px;
  width: 100%;
  overflow-x: auto;
  border-bottom: 1px solid var(--line);
  scrollbar-width: none;
}
/* A row longer than its box scrolls, and fades where it is cut so a phone shows there is more (QA, 4 Nov): the fade follows the scroll (a
   scroll-driven animation of two registered lengths), so the first tab is crisp at the start and the last at the end. A row that fits has no
   scroll timeline, so no fade. Browsers without scroll timelines scroll as before. */
@property --tabs-fade-l { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --tabs-fade-r { syntax: "<length>"; inherits: false; initial-value: 0px; }
@keyframes ldu-tabs-fade {
  0% { --tabs-fade-l: 0px; --tabs-fade-r: 32px; }
  10%, 90% { --tabs-fade-l: 32px; --tabs-fade-r: 32px; }
  100% { --tabs-fade-l: 32px; --tabs-fade-r: 0px; }
}
@supports (animation-timeline: scroll()) {
  :where(.ldu-tabs) {
    -webkit-mask-image: linear-gradient(to right, transparent, var(--ink) var(--tabs-fade-l), var(--ink) calc(100% - var(--tabs-fade-r)), transparent);
    mask-image: linear-gradient(to right, transparent, var(--ink) var(--tabs-fade-l), var(--ink) calc(100% - var(--tabs-fade-r)), transparent);
    animation: ldu-tabs-fade linear both;
    animation-timeline: scroll(self inline);
  }
}
/* A strip that must show every tab on a phone wraps to a second row instead of scrolling (My Company, with its two Coming soon chips; Milan, 10 Nov). */
.ldu-tabs--wrap { flex-wrap: wrap; overflow: visible; -webkit-mask-image: none; mask-image: none; animation: none; }
.ldu-tabs__tab {
  position: relative;
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 12px 16px;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--fs-ui);
  font-weight: 500;
  line-height: 1.5;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--dur);
}
.ldu-tabs__tab::after { content: ""; position: absolute; right: 0; bottom: -1px; left: 0; height: 2px; background: transparent; transition: background-color var(--dur); }
.ldu-tabs__tab:hover { color: var(--accent); }
.ldu-tabs__tab:hover::after { background: color-mix(in srgb, var(--accent-fill) 35%, transparent); }
.ldu-tabs__tab:focus-visible { outline-offset: -2px; border-radius: var(--r-1); }
.ldu-tabs__tab .icon { width: 16px; height: 16px; }
.ldu-tabs__tab--on, .ldu-tabs__tab--on:hover { color: var(--accent); }
.ldu-tabs__tab--on::after, .ldu-tabs__tab--on:hover::after { background: var(--accent-fill); }
.ldu-tabs__tab--soon, .ldu-tabs__tab--soon:hover { gap: 8px; color: var(--ink-3); cursor: default; }
.ldu-tabs__tab--soon:hover::after { background: transparent; }
.ldu-tabs__soon { font-size: var(--fs-meta); }
:where(.ldu-tabs__count) {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 4px; box-sizing: border-box;
  border-radius: 999px; background: var(--ldu-wash); color: var(--ink-2);
  font-size: var(--fs-meta); font-variant-numeric: tabular-nums;
}
:where(.ldu-tabs__count--on) { background: var(--accent-soft); color: var(--accent); }
:where(.ldu-tabs--bar) { align-self: stretch; width: auto; gap: 0; border-bottom: 0; }
:where(.ldu-tabs--bar) .ldu-tabs__tab { padding: 0 14px; }

/* ── form controls: one hover ───────────────────────────────────
   Every field (input, search, select, textarea, the dropdown trigger) hovers the same way: the BORDER changes (--ink-4: darker in light,
   lighter in dark) with a smooth colour transition, and nothing else (no background, shadow or transform). bin/ds-lint fails a control
   hover rule that sets those. Page files may repeat this selector at higher specificity but never add to it. */
.input, .select, .textarea, .ldu-dd__trigger { transition: border-color var(--dur); }
.input:hover, .select:hover, .textarea:hover, .ldu-dd__trigger:hover { border-color: var(--ink-4); }

/* ── dropdown (.ldu-dd) ─────────────────────────────────────────
   A trigger that shows the current value and a panel of options: a <details> with radio inputs, no native <select>. Trigger and panel
   take the input's look (line, --bg-2, radius); an option washes under the pointer and the chosen one is accent with a check.
   A trigger whose filter narrows the list (--on) has the accent border and wash. Markup: components/ldu/_dropdown.html.erb. */
.ldu-dd { position: relative; display: inline-block; min-width: 0; }
.ldu-dd__trigger {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  box-sizing: border-box; min-height: 40px; width: 100%; padding: 0 12px;
  border: 1px solid var(--line); border-radius: var(--r-2); background: var(--bg-2); color: var(--ink);
  font-family: var(--sans); font-size: var(--fs-ui); line-height: 1.5; cursor: pointer; list-style: none;
  transition: border-color var(--dur);
}
.ldu-dd__trigger::-webkit-details-marker { display: none; }
.ldu-dd__trigger .icon { flex: none; color: var(--ink-2); transition: transform var(--dur); }
.ldu-dd[open] > .ldu-dd__trigger { border-color: var(--ink-3); }
.ldu-dd[open] > .ldu-dd__trigger .icon { transform: rotate(180deg); }
.ldu-dd--on > .ldu-dd__trigger { border-color: var(--ink-3); }   /* a narrowing filter: a firmer border only, nothing that changes the width or the hue */
.ldu-dd__value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ldu-dd__panel {
  position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; min-width: 100%; width: max-content; max-width: min(20rem, 90vw); max-height: 18rem; overflow-y: auto;
  display: grid; gap: 2px; padding: 4px; border: 1px solid var(--line); border-radius: var(--r-2); background: var(--bg-2);
}
.ldu-dd__option {
  position: relative; display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 12px; border-radius: var(--r-1); color: var(--ink); font-size: var(--fs-ui); line-height: 1.4; cursor: pointer;
  transition: background-color var(--dur), color var(--dur);
}
.ldu-dd__option:hover { background: color-mix(in srgb, var(--ink) 9%, transparent); }
.ldu-dd__option .icon { flex: none; visibility: hidden; }
.ldu-dd__option:has(.ldu-dd__input:checked) { background: var(--ldu-wash); font-weight: 500; }
.ldu-dd__option:has(.ldu-dd__input:checked) .icon { visibility: visible; }
.ldu-dd__option:has(.ldu-dd__input:focus-visible) { outline: var(--ring-w) solid var(--ring); outline-offset: -2px; }
.ldu-dd__input { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
.ldu-dd__trigger:focus-visible { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }
.ldu-dd--block { display: block; }

/* ── the LDU modal ──────────────────────────────────────────────
   One shape for every modal (the Filters modal's box, ui-cleanup/milan, grown into three bars):
     .modal-scrim.ldu-modal > .modal.ldu-modal__box
       .ldu-modal__head   the title, and a Close (X) button
       .ldu-modal__body   all the content; scrolls by itself when the box would pass the window
       .ldu-modal__foot   two buttons only: the outline one, then the solid one
   A card in the middle; a sheet from the bottom on a phone. Doubled classes beat the older .room .modal rules. */
.modal-scrim.ldu-modal { z-index: 200; padding: 24px 16px; box-sizing: border-box; } /* above the phone menu bar (z 100) */
:is(.modal-scrim, dialog.popup-dialog, .ds-overlay) .modal.ldu-modal__box {
  display: flex; flex-direction: column; box-sizing: border-box; width: min(480px, calc(100vw - 32px)); max-height: calc(100dvh - 48px);
  padding: 0; overflow: hidden; border: 1px solid var(--line); border-radius: var(--ldu-r-xl); background: var(--bg-2);
}
.ldu-modal__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex: none; padding: 16px 16px 16px 24px; border-bottom: 1px solid var(--line); }
.ldu-modal__head .modal__title { margin: 0; min-width: 0; font-family: var(--display); font-size: var(--fs-h2); font-weight: 600; line-height: 1.4; letter-spacing: -0.01em; }
:is(.modal-scrim, dialog.popup-dialog, .ds-overlay) .ldu-modal__close .icon { flex: none; width: 18px; height: 18px; min-width: 18px; max-width: none; }
.ldu-modal__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 20px 24px; display: flex; flex-direction: column; gap: 16px; font-size: var(--fs-body); color: var(--ink); }
.ldu-modal__body > p { margin: 0; line-height: 1.5; }
/* a dropdown inside a modal opens in place under its trigger: the body is a scroll box, so a floating panel would be cut off at its edge */
.ldu-modal__body .ldu-dd__panel { position: static; width: auto; max-width: none; margin-top: 4px; }
.ldu-modal__text { color: var(--ink-2); }
.ldu-modal__note { font-size: var(--fs-ui); color: var(--ink-3); }
.ldu-modal__foot { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex: none; padding: 16px 24px; border-top: 1px solid var(--line); }
.ldu-modal__foot form { margin: 0; }
/* the two buttons share the bar's width equally (a button_to's form takes the share, its button fills the form) */
.ldu-modal__foot > * { flex: 1 1 0; min-width: 0; }
.ldu-modal__foot .btn { width: 100%; justify-content: center; }
@media (max-width: 720px) {
  .modal-scrim.ldu-modal { align-items: flex-end; padding: 0; }
  /* :root lifts it over comms/app.css's 480px popup width, which loads after this file and left the sheet 32px short of the right edge */
  :root dialog.popup-dialog { width: 100%; max-width: 100vw; margin: auto 0 0; }
  :is(.modal-scrim, dialog.popup-dialog, .ds-overlay) .modal.ldu-modal__box { width: 100%; max-height: 90dvh; border-width: 1px 0 0; border-radius: var(--ldu-r-xl) var(--ldu-r-xl) 0 0; }
  .ldu-modal__head { padding: 12px 12px 12px 16px; }
  .ldu-modal__body { padding: 16px; }
  .ldu-modal__foot { padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); }
  /* the sheet rises from the bottom edge as it opens (Milan, 11 Nov); [open] restarts it on every showModal() */
  dialog.popup-dialog[open] { animation: ldu-sheet-up 240ms var(--ease-out-quint); }
}
@keyframes ldu-sheet-up { from { transform: translateY(100%); } }
@media (prefers-reduced-motion: reduce) { dialog.popup-dialog[open] { animation: none; } }
/* the frame's own parts a modal may hold: a turbo-frame or a panel passes the box's flex column through */
.ldu-modal__box > turbo-frame { display: contents; }
/* detail on demand inside a modal: a plain disclosure, neutral */
.ldu-modal__more > summary { cursor: pointer; font-size: var(--fs-ui); font-weight: 500; color: var(--ink-2); }
.ldu-modal__more > summary:hover { color: var(--ink); }
.ldu-modal__more[open] > summary { margin-bottom: 12px; }
/* a modal whose head, body and foot sit inside one form: the form takes no box of its own */
.ldu-modal__form { display: contents; }
.ldu-modal__box { position: relative; }   /* above a .popup__backdrop button behind it */
.modal-scrim > .popup__backdrop { background: transparent; }   /* the scrim already dims the page */
/* a file field drawn as a button (my_agents/_form): the input is hidden inside the label; the chosen file's name sits beside it */
.filepick { display: inline-flex; align-items: center; gap: 12px; min-width: 0; }
.filepick__button { flex: none; cursor: pointer; }
.filepick__button:focus-within { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }
.filepick__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-ui); color: var(--ink-2); }
.agentpage__picture { display: flex; align-items: center; gap: 12px; min-width: 0; }

/* ── search (components/ldu/_search) ────────────────────────────
   The icon inside the field on the left, a clear (x) on the right once something is typed (the placeholder is gone), the browser's own
   clear hidden. .input gives the field its look and states; the wrapper only places things, and a page sets its width through its own class. */
.ldu-search { position: relative; display: flex; align-items: center; flex: 1 1 14rem; min-width: 0; }
.ldu-search > .icon { position: absolute; left: 12px; color: var(--ink-3); pointer-events: none; }
/* [type=search] lifts it over the pages' own `.page.ldu-page .input` padding rules */
.ldu-search .input.ldu-search__input[type="search"] { width: 100%; padding-left: 36px; padding-right: 36px; }
.ldu-search__input::-webkit-search-cancel-button, .ldu-search__input::-webkit-search-decoration { appearance: none; }
.ldu-search__clear {
  position: absolute; right: 4px; display: grid; place-items: center; width: 28px; height: 28px;
  border: 0; border-radius: var(--r-1); background: transparent; color: var(--ink-3); cursor: pointer;
  transition: background-color .15s ease, color .15s ease;
}
.ldu-search__clear:hover { background: var(--ldu-wash); color: var(--ink); }
.ldu-search__clear:focus-visible { outline: var(--ring-w) solid var(--ring); outline-offset: var(--ring-offset); }
.ldu-search__input:placeholder-shown + .ldu-search__clear { display: none; }

/* What a search found, marked in its results (SearchHelper#highlight_query, search_hits.js): a light lilac wash under the letters (Milan, 19 Oct 2026),
   the text keeps its colour. */
mark.ldu-hit { padding: 0; border-radius: 2px; background: var(--ldu-hit); color: inherit; font-weight: inherit; }

/* Remove filters (components/ldu/_filter_reset): the last thing in a filter bar, a ghost button shown only while a filter is on (filter_reset_controller.js) */
.filter-reset { flex: none; }
.filter-reset[hidden] { display: none; }
