/* ============================================================================
   JACRA Backoffice - components.css
   The shared component vocabulary. Everything in here is screen-agnostic and
   used by at least two screens; anything used by exactly one screen lives in
   assets/css/screens/<id>.css instead.

   Rules that apply to this whole file:
     - no !important, no hardcoded colours (every colour is a tokens.css token)
     - state classes are .is-active (router/JS owned), .is-on (UI toggle),
       .is-done / .is-selected / .is-disabled - never bare .active / .on
     - cursor:pointer only on real controls (.btn, .icon-btn, .mini-btn,
       .fchip, .seg button, button.chip) - nothing inert gets it
     - selectors are scoped to real child classes, never bare descendant
       element selectors that can leak onto a sibling well (the old `.kpi span`
       bug, which pushed a margin-top onto the icon plate)

   Motion rules (the "smooth" half of the retune):
     - every transition and animation is timed with a --t-* token and, where an
       easing is named on its own, an --ease-* token. No raw ms, no raw `ease`.
       That is what lets base.css collapse motion under prefers-reduced-motion
       by overriding the tokens rather than fighting specificity.
     - colour, border and opacity changes ride --t-fast; transforms, lifts and
       reveals ride --t-base; the only --t-slow here is the metric bar fill.
     - --ease-spring is used on hover lifts only, and only for the transform
       channel (set through transition-timing-function on the hover state, so
       the lift springs in and settles out on --ease-out).
     - entrance animations start on their own the moment the node renders and
       end on the element's resting values: nothing in this file is parked at
       opacity:0 waiting for a class JS might not add. Opacity-only entrances
       fill `both`; the one that moves (.kcard) fills `backwards`, because a
       held transform keyframe would outrank the hover lift for good.
   ============================================================================ */


/* ---------------------------------------------------------------------------
   0. Entrance keyframes
   Three small primitives shared by the components below. The distances are
   deliberately short (6px, a 4% scale) so an entrance reads as "settling",
   never as "flying in".
   --------------------------------------------------------------------------- */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Metric bars grow from the left on mount. scaleX rather than width so the
   entrance never triggers layout, and so it composes with the width transition
   that handles later --fill updates. */
@keyframes bar-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}


/* ---------------------------------------------------------------------------
   1. Surfaces - .card is the ONE surface primitive
   The prototype declared .card and used it zero times while four screens
   hand-rolled the same white/gray-200/radius/shadow block. Every panel-like
   surface now composes from this single group.
   --------------------------------------------------------------------------- */
.card,
.panel,
.kcard {
  background: var(--white);
  border: 1px solid var(--gray-200);
  border-radius: var(--r);
  box-shadow: var(--sh-sm);
  transition:
    transform var(--t-base),
    box-shadow var(--t-base),
    border-color var(--t-fast);
}

/* Only a card that goes somewhere lifts. Home's module launchers (.dmod) and
   the on-shift summary (.onshift) are <a class="card">; a static .card keeps
   its transition (so a JS-toggled border still eases) but never moves.
   The element prefix scores 0,1,1 on purpose: it outranks the single-class
   screen rules that re-declare a transition, so the timing stays one voice. */
a.card,
button.card {
  transition:
    transform var(--t-base),
    box-shadow var(--t-base),
    border-color var(--t-fast);
}

a.card:hover,
button.card:hover {
  transform: translateY(var(--lift));
  box-shadow: var(--sh-md);
  /* spring on the transform channel only; shadow and border keep --ease-out */
  transition-timing-function: var(--ease-spring), var(--ease-out), var(--ease-out);
}

/* base.css draws the focus ring as a box-shadow on a bare :focus-visible
   (0,1,0). The rule above is the same specificity and loads later, so on a
   focusable card - Home's .card.dmod module launchers and .card.onshift - it
   silently ate the ring, and base.css had already dropped the outline. Restate
   both marks together; the pseudo-class alone outscores it, no !important. */
.card:focus-visible,
.panel:focus-visible,
.kcard:focus-visible {
  box-shadow: var(--focus-ring), var(--sh-sm);
}

/* A lifted card that also holds keyboard focus keeps both marks. */
a.card:hover:focus-visible,
button.card:hover:focus-visible {
  box-shadow: var(--focus-ring), var(--sh-md);
}

/* .panel = card + a fixed header and one scrolling body.
   Replaces .acc-list and .epanel, which were byte-identical rule bodies
   47 lines apart in the prototype. */
.panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

.panel-h {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--gray-200);
}

/* Header top line: title on the left, count / action on the right. */
.panel-h-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}

.panel-t,
.panel-h > h2,
.panel-h-top > h2 {
  margin: 0;
  font-family: var(--f-ui);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--navy);
  line-height: 1.3;
}

/* The real inner scroller. Both Access panes and all three Email panes get
   their own; the prototype clipped them silently with overflow:hidden. */
.panel-b {
  flex: 1;
  min-height: 0;
  min-width: 0;
  overflow: auto;
}

.panel-b--pad {
  padding: var(--s-4);
}

.panel-f {
  flex: none;
  padding: var(--s-3) var(--s-4);
  border-top: 1px solid var(--gray-100);
}


/* ---------------------------------------------------------------------------
   2. Avatars
   --------------------------------------------------------------------------- */
.av {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  font-family: var(--f-ui);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1;
  /* visible default so a missing tint modifier can never render invisibly */
  background: var(--gray-200);
  color: var(--navy);
  overflow: hidden;
  transition: transform var(--t-base), box-shadow var(--t-base);
}

.av--xs { width: 22px; height: 22px; font-size: 10px; }
.av--sm { width: 28px; height: 28px; font-size: 11px; }
.av--md { width: 36px; height: 36px; font-size: 13px; }

.av--lg {
  width: 52px;
  height: 52px;
  border-radius: var(--r-lg);
  font-family: var(--f-disp);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -.01em;
}

/* tints - the only three the data layer emits (team.tint / properties.tint) */
.av--gold  { background: var(--gold);      color: var(--navy-900); }
.av--slate { background: var(--gray-200);  color: var(--navy); }
.av--navy  { background: var(--navy);      color: var(--white); }

/* One overlapping stack, used by Home's "on shift today" row and anywhere else
   a group of people is summarised. The prototype had two implementations of
   this trick (.avstack and .onshift .os-av) with different offsets. */
.avstack {
  display: flex;
  align-items: center;
}

.avstack > .av {
  /* position:relative so the hovered face can step in front of its siblings */
  position: relative;
  border: 2px solid var(--white);
  margin-inline-start: -8px;
}

.avstack > .av:first-child {
  margin-inline-start: 0;
}

/* The hovered face lifts, grows a touch and comes to the front of the pile. */
.avstack > .av:hover {
  z-index: 1;
  transform: translateY(var(--lift)) scale(1.06);
  box-shadow: var(--sh-md);
  transition-timing-function: var(--ease-spring), var(--ease-out);
}


/* ---------------------------------------------------------------------------
   3. Badges
   .bdg used to declare no background and no colour of its own, so a wrong
   modifier failed silently to invisible - which is exactly what happened to
   all 12 priority badges, written as `.b-b-high` instead of `.b-high`.
   .bdg now paints a visible default, and every modifier sets a background,
   a text colour and a dot colour.
   --------------------------------------------------------------------------- */
.bdg {
  --bdg-dot: var(--gray-500);
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 3px var(--s-2);
  border-radius: var(--r-sm);
  background: var(--gray-100);
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  line-height: 1.45;
  white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast);
}

/* the badge's leading dot picks up the modifier's colour */
.bdg > .dot,
.bdg > .d {
  background: var(--bdg-dot);
}

/* priority - case.priority: 'high' | 'med' | 'low' */
.b-high { background: var(--red-100);  color: var(--red-700);   --bdg-dot: var(--red); }
.b-med  { background: var(--gold-100); color: var(--gold-700);  --bdg-dot: var(--gold-600); }
.b-low  { background: var(--gray-100); color: var(--gray-600);  --bdg-dot: var(--gray-500); }

/* status - case.status: 'new' | 'progress' | 'done' */
.b-new  { background: var(--gray-100);  color: var(--navy);      --bdg-dot: var(--gray-500); }
.b-prog { background: var(--gold-100);  color: var(--gold-700);  --bdg-dot: var(--gold); }
.b-done { background: var(--green-100); color: var(--green-700); --bdg-dot: var(--green); }


/* ---------------------------------------------------------------------------
   4. Dots
   --------------------------------------------------------------------------- */
.dot {
  display: inline-block;
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  transition: background var(--t-fast);
}

.dot--lg { width: 8px; height: 8px; }

/* notification pip on an icon button (the old .dot-red) */
.dot--alert {
  position: absolute;
  top: 7px;
  right: 8px;
  width: 7px;
  height: 7px;
  background: var(--red);
  border: 1.5px solid var(--white);
}


/* ---------------------------------------------------------------------------
   5. Tags and pills
   --------------------------------------------------------------------------- */
.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-sm);
  background: var(--blue-100);
  color: var(--navy);
  font-family: var(--f-ui);
  font-size: 10.5px;
  font-weight: 600;
  /* an uppercase label: it carries the small-caps tracking, not the .02em of
     the mixed-case badge and pill */
  letter-spacing: .08em;
  text-transform: uppercase;
  line-height: 1.4;
  white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast);
}

.tag .ic {
  width: 13px;
  height: 13px;
  color: var(--blue);
}

.pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 20px;
  padding: 0 var(--s-2);
  border-radius: var(--r-sm);
  background: var(--gray-200);
  color: var(--navy);
  font-family: var(--f-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  line-height: 1;
  transition: background var(--t-fast), color var(--t-fast);
}

.pill--gold { background: var(--gold); color: var(--navy-900); }
.pill--soft { background: var(--gray-100); color: var(--gray-600); }


/* ---------------------------------------------------------------------------
   6. Buttons
   The base .btn is a solid navy button (the prototype's .btn-dark, which was
   its most used variant); the base therefore never renders as an invisible
   transparent rectangle when a modifier is missing or misspelled.

   Press model, shared by every button-shaped control in this file:
     rest -> hover : 1px lift + the resting card shadow, on --t-base
     hover -> press: back to 0 with no shadow, on --t-fast (the press must
                     feel instant, the release may breathe)
     disabled      : never moves
   --------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: var(--navy);
  color: var(--white);
  font-family: var(--f-ui);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .005em;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background var(--t-fast),
    border-color var(--t-fast),
    color var(--t-fast),
    box-shadow var(--t-base),
    transform var(--t-base);
}

.btn .ic { width: 16px; height: 16px; }

.btn:hover {
  background: var(--navy-600);
  transform: translateY(-1px);
  box-shadow: var(--sh-sm);
}

.btn:active {
  transform: translateY(0);
  box-shadow: none;
  transition:
    background var(--t-fast),
    border-color var(--t-fast),
    color var(--t-fast),
    box-shadow var(--t-fast),
    transform var(--t-fast);
}

.btn--gold {
  background: var(--gold);
  color: var(--navy-900);
}

/* Flat at rest, like .btn: a gold halo under the resting button read as a glow
   (DESIGN.md 13). The hover keeps the same lift shadow as every other button. */
.btn--gold:hover {
  background: var(--gold-600);
}

.btn--ghost {
  background: var(--white);
  border-color: var(--gray-300);
  color: var(--navy);
}

.btn--ghost:hover { background: var(--gray-50); }

.btn--sm { padding: var(--s-2) var(--s-3); font-size: 12px; }

.btn--block { width: 100%; }

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: .55;
  cursor: not-allowed;
}

/* An inert button neither lifts nor recolours. */
.btn:disabled:hover,
.btn[aria-disabled="true"]:hover {
  background: var(--navy);
  transform: none;
  box-shadow: none;
}

.btn--gold:disabled:hover,
.btn--gold[aria-disabled="true"]:hover {
  background: var(--gold);
}

.btn--ghost:disabled:hover,
.btn--ghost[aria-disabled="true"]:hover {
  background: var(--white);
}

/* icon-only square control - always needs an aria-label in the markup */
.icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 38px;
  height: 38px;
  border: 1px solid var(--gray-200);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--navy);
  cursor: pointer;
  transition:
    background var(--t-fast),
    border-color var(--t-fast),
    color var(--t-fast),
    box-shadow var(--t-base),
    transform var(--t-base);
}

.icon-btn .ic { width: 18px; height: 18px; }

.icon-btn:hover {
  background: var(--gray-50);
  border-color: var(--gray-300);
  box-shadow: var(--sh-sm);
}

/* A square control presses inward rather than lifting - it has no label to
   carry the lift, so a 1px rise would read as a wobble. */
.icon-btn:active {
  transform: scale(.96);
  box-shadow: none;
  transition: transform var(--t-fast), box-shadow var(--t-fast), background var(--t-fast);
}

/* An inert control must look inert. Four bells and the mobile "Add shift" ship
   disabled; without this they kept cursor:pointer and still lit on hover. */
.icon-btn:disabled,
.icon-btn[aria-disabled="true"] {
  opacity: .55;
  cursor: not-allowed;
}

.icon-btn:disabled:hover,
.icon-btn[aria-disabled="true"]:hover {
  background: var(--white);
  border-color: var(--gray-200);
  box-shadow: none;
  transform: none;
}

.icon-btn--sm { width: 34px; height: 34px; }

.icon-btn--sm .ic { width: 16px; height: 16px; }

/* small row action - the vault's copy / reveal / open buttons */
.mini-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--gray-200);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--gray-600);
  font-family: var(--f-ui);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition:
    background var(--t-fast),
    color var(--t-fast),
    border-color var(--t-fast),
    box-shadow var(--t-base),
    transform var(--t-base);
}

.mini-btn .ic { width: 15px; height: 15px; }

.mini-btn:hover {
  background: var(--gray-50);
  border-color: var(--gray-300);
  color: var(--navy);
}

.mini-btn:active {
  transform: scale(.94);
  transition: transform var(--t-fast), background var(--t-fast);
}

/* wider variant that carries a text label next to its icon (the old
   `.mini-btn.open`) */
.mini-btn--text {
  width: auto;
  gap: var(--s-1);
  padding: 0 var(--s-3);
  color: var(--navy);
}

/* toggled reveal button - driven by the real aria-pressed state, no extra class.
   Declared after :hover at the same score, so a pressed button keeps its gold
   ground under the pointer instead of flashing back to grey. */
.mini-btn[aria-pressed="true"] {
  background: var(--gold-100);
  border-color: var(--gold-300);
  color: var(--gold-700);
}


/* ---------------------------------------------------------------------------
   7. Chips and segmented controls
   --------------------------------------------------------------------------- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--gray-300);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--navy);
  font-family: var(--f-ui);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.3;
  white-space: nowrap;
  /* on the base, not only on button.chip: a <label class="chip"> that wraps a
     <select> (Cases' .cfilter) toggles its border too */
  transition:
    background var(--t-fast),
    border-color var(--t-fast),
    color var(--t-fast),
    box-shadow var(--t-fast);
}

.chip .ic {
  width: 14px;
  height: 14px;
  color: var(--gray-500);
  transition: color var(--t-fast);
}

.chip.is-on { background: var(--navy); border-color: var(--navy); color: var(--white); }
.chip.is-on .ic { color: var(--gold); }

/* only an actual <button class="chip"> is clickable; inert chips stay inert */
button.chip { cursor: pointer; }
button.chip:hover { background: var(--gray-50); }
button.chip.is-on:hover { background: var(--navy-600); border-color: var(--navy-600); }

/* the global filter chip. Named `.acc-chip` in the prototype even though the
   Cases toolbar used it too - the `acc-` prefix was misleading. */
.fchip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-2) var(--s-3);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: var(--gray-100);
  color: var(--gray-600);
  font-family: var(--f-ui);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background var(--t-fast),
    color var(--t-fast),
    border-color var(--t-fast);
}

.fchip .ic { width: 13px; height: 13px; }

.fchip:hover { background: var(--gray-200); }

.fchip.is-on { background: var(--navy); color: var(--white); }
.fchip.is-on:hover { background: var(--navy-600); }

/* The count inside a chip. It was declared three times - cases.css, access.css
   and email.css - all unscoped, so whichever loaded last won on every screen.
   One definition, one selected-state colour. --gray-600 clears 4.5:1 on the
   chip's --gray-100 ground; --gold-300 reads on the navy selected ground. */
.fchip-n {
  font-family: var(--f-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--gray-600);
  transition: color var(--t-fast);
}

.fchip.is-on .fchip-n { color: var(--gold-300); }

.fchips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

/* The segment's buttons are content-sized (Board is wider than List), so a
   pure-CSS sliding indicator would need equal tracks and a layout change.
   The selected pill instead fades in its white ground and shadow together,
   which at --t-fast reads as a single soft slide of emphasis. */
.seg {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border-radius: var(--r-sm);
  background: var(--gray-100);
}

.seg button {
  padding: var(--s-2) var(--s-3);
  border: 0;
  /* outer radius minus the track padding, so the pill sits concentric */
  border-radius: calc(var(--r-sm) - 3px);
  background: transparent;
  color: var(--gray-600);
  font-family: var(--f-ui);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background var(--t-fast),
    color var(--t-fast),
    box-shadow var(--t-fast);
}

.seg button:hover { color: var(--navy); }

.seg button.is-on {
  background: var(--white);
  color: var(--navy);
  box-shadow: var(--sh-sm);
}

/* This one scores 0,2,1 and would beat the ring whatever the load order, so
   the selected Board/List segment had no focus mark at all. */
.seg button.is-on:focus-visible {
  box-shadow: var(--focus-ring), var(--sh-sm);
}


/* ---------------------------------------------------------------------------
   8. Toolbar row
   --------------------------------------------------------------------------- */
.tbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}

.tbar-l,
.tbar-r {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
}


/* ---------------------------------------------------------------------------
   9. KPI strip
   Every part is a real child class. The prototype used a bare `.kpi span`,
   which also matched the icon plate's contents and leaked a margin-top onto it.
   --------------------------------------------------------------------------- */
.kpi {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3);
  border: 1px solid var(--gray-200);
  border-radius: var(--r-sm);
  background: var(--white);
}

.kpi .k-ic {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: var(--r-sm);
  background: var(--gold-100);
  color: var(--gold-600);
}

.kpi .k-ic .ic { width: 17px; height: 17px; }

.kpi .k-m { min-width: 0; }

.kpi .k-val,
.kpi .k-m > b {
  display: block;
  font-family: var(--f-disp);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--navy);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.kpi .k-lbl,
.kpi .k-m > span {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  color: var(--muted);
  line-height: 1.3;
}


/* ---------------------------------------------------------------------------
   10. Case card
   .kcard and .mcard were near-identical components maintained in parallel;
   there is now one card plus a --compact modifier for the mobile lists.
   Home and Cases both render this, so it lives here rather than in a screen.

   Motion: every card rises in on mount, which is also how a claimed card
   arrives in its new lane - Cases re-renders after store.claimCase(), so the
   moved card is a fresh node and plays the entrance on its own. Siblings
   stagger by --stagger; the stagger selectors are wrapped in :where() so they
   score 0,1,0 and the reduced-motion safety net in base.css (0,1,1) can zero
   the delays.
   --------------------------------------------------------------------------- */
.kcard {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-4);
  /* `backwards`, not `both`: the entrance ends on the card's resting values, so
     releasing the fill costs nothing visible, whereas holding the last keyframe
     would pin transform at 0 for good and the hover lift below could never
     move (animated values outrank normal declarations). The backwards fill is
     what keeps a staggered card invisible through its delay. */
  animation: rise-in var(--t-base) backwards;
}

.kcard:where(:nth-child(2))   { animation-delay: var(--stagger); }
.kcard:where(:nth-child(3))   { animation-delay: calc(var(--stagger) * 2); }
.kcard:where(:nth-child(4))   { animation-delay: calc(var(--stagger) * 3); }
.kcard:where(:nth-child(5))   { animation-delay: calc(var(--stagger) * 4); }
.kcard:where(:nth-child(6))   { animation-delay: calc(var(--stagger) * 5); }
.kcard:where(:nth-child(7))   { animation-delay: calc(var(--stagger) * 6); }
/* deep lanes cap the envelope: card eight and beyond arrive with card seven */
.kcard:where(:nth-child(n+8)) { animation-delay: calc(var(--stagger) * 6); }

/* A case card is an interactive container (it carries Claim), so it lifts. */
.kcard:hover {
  transform: translateY(var(--lift));
  box-shadow: var(--sh-md);
  transition-timing-function: var(--ease-spring), var(--ease-out), var(--ease-out);
}

.kcard:hover:focus-visible {
  box-shadow: var(--focus-ring), var(--sh-md);
}

.kcard-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}

.kcard-t,
.kcard > h3 {
  margin: 0;
  font-family: var(--f-ui);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--navy);
  line-height: 1.3;
  transition: color var(--t-fast);
}

/* the client line under the title */
.kcard-cl {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: 12px;
  color: var(--muted);
  min-width: 0;
}

.kcard-cl .ic { width: 14px; height: 14px; color: var(--gray-400); }

.kcard-ft {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  margin-top: 2px;
}

.kcard-meta {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: 11px;
  color: var(--muted);
  flex-wrap: wrap;
}

.kcard-meta .ic { width: 13px; height: 13px; }

.kcard-meta > span {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
}

/* Resolved. Carried by a soft green hairline, a receded title and a green
   check in the status badge - never by opacity, which used to dim the 11px
   meta line below the contrast floor. Every character stays full strength. */
.kcard.is-done {
  border-color: var(--green-100);
  box-shadow: none;
}

.kcard.is-done .kcard-t,
.kcard.is-done > h3 {
  color: var(--gray-600);
}

.kcard.is-done .b-done .ic {
  color: var(--green);
}

/* a resolved card still lifts, but only to the resting shadow */
.kcard.is-done:hover {
  box-shadow: var(--sh-sm);
}

/* mobile / dense list variant (the old .mcard) */
.kcard--compact {
  gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r);
}

.kcard--compact .kcard-t,
.kcard--compact > h3 { font-size: 14px; }

.kcard--compact .kcard-ft { margin-top: 0; }


/* ---------------------------------------------------------------------------
   11. Resource rows (the Access vault)
   --------------------------------------------------------------------------- */
.res {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  margin-bottom: var(--s-3);
  border: 1px solid var(--gray-200);
  border-radius: var(--r-sm);
  background: var(--gray-50);
  transition: border-color var(--t-fast), background var(--t-fast);
}

.res:last-child { margin-bottom: 0; }

.r-ic {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 36px;
  height: 36px;
  border: 1px solid var(--gray-200);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--navy);
}

.r-ic .ic { width: 18px; height: 18px; }

.r-body {
  flex: 1;
  min-width: 0;
}

.r-name,
.r-body > b {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--navy);
  line-height: 1.35;
}

.r-val {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  margin-top: 2px;
  font-family: var(--f-mono);
  font-size: 11.5px;
  color: var(--muted);
  word-break: break-word;
}

/* the masked value before reveal. The mask's wide tracking eases shut as the
   value lands, and the value itself fades in, so mask -> value cross-fades
   instead of snapping. */
.secret {
  letter-spacing: 2px;
  color: var(--muted);
  transition:
    letter-spacing var(--t-base),
    color var(--t-base),
    opacity var(--t-base);
}

/* the revealed demo value - never a real credential */
.secret.is-on {
  letter-spacing: 0;
  color: var(--navy);
  animation: fade-in var(--t-base) both;
}

.r-act {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex: none;
}

/* small explanatory line: "demo value", "Last accessed 2d ago by Karang" */
.note {
  font-size: 11px;
  line-height: 1.45;
  color: var(--muted);
}

.note--warn { color: var(--gold-700); }

.mono {
  font-family: var(--f-mono);
  font-size: 12px;
  letter-spacing: -.01em;
}

.muted { color: var(--muted); }


/* ---------------------------------------------------------------------------
   12. Metric bars (campaign stats)
   The fill is driven by the --fill custom property, set from JS with
   el.style.setProperty('--fill', '41%'). Never an inline width.

   Two motions, two jobs: the bar-grow animation draws the fill from the left
   when the screen mounts (no JS hook needed - it starts on its own and holds
   its end state), and the width transition eases any later --fill update.
   --------------------------------------------------------------------------- */
.stat {
  flex: 1;
  min-width: 0;
}

.stat-v,
.stat > b {
  display: block;
  font-family: var(--f-disp);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--navy);
  line-height: 1.1;
}

.stat-l,
.stat > span {
  display: block;
  font-size: 10.5px;
  color: var(--muted);
  line-height: 1.4;
}

.stat-bar {
  --fill: 0%;
  height: 4px;
  margin-top: var(--s-2);
  border-radius: 2px;
  background: var(--gray-200);
  overflow: hidden;
}

.stat-bar::after {
  content: "";
  display: block;
  width: var(--fill);
  height: 100%;
  border-radius: inherit;
  background: var(--gold);
  transform-origin: left center;
  transition: width var(--t-slow);
  animation: bar-grow var(--t-slow) both;
}

.stat-bar--navy::after { background: var(--navy); }


/* ---------------------------------------------------------------------------
   13. Empty state
   --------------------------------------------------------------------------- */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-7) var(--s-4);
  text-align: center;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.5;
  animation: fade-in var(--t-base) both;
}

.empty .ic {
  width: 24px;
  height: 24px;
  color: var(--gray-400);
}

.empty-t,
.empty > b {
  font-family: var(--f-ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--navy);
}

.empty--inline {
  padding: var(--s-4) var(--s-3);
}


/* ---------------------------------------------------------------------------
   14. Toast - the transient "Copied" confirmation
   Shown by JS with .is-on for roughly a second, then removed.
   Cleared above the mobile bottom bar with the shared metric, never a
   hardcoded 70px.

   In: rises and settles with a whisper of spring on --t-base.
   Out: the transition on the base rule is the one that plays when .is-on is
   removed, so the fade-out is the quicker --t-fast and never lingers.
   --------------------------------------------------------------------------- */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--bottombar-h) + var(--s-4));
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  max-width: calc(100% - var(--s-8));
  padding: var(--s-2) var(--s-4);
  border-radius: 999px;
  background: var(--navy-900);
  color: var(--white);
  box-shadow: var(--sh-lg);
  font-family: var(--f-ui);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.4;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, var(--s-2)) scale(.96);
  transition: opacity var(--t-fast), transform var(--t-fast);
}

.toast .ic {
  width: 15px;
  height: 15px;
  color: var(--gold);
}

.toast.is-on {
  opacity: 1;
  transform: translate(-50%, 0) scale(1);
  transition: opacity var(--t-base), transform var(--t-base);
  transition-timing-function: var(--ease-out), var(--ease-spring);
}


/* ---------------------------------------------------------------------------
   15. Text inputs
   The shell's search fields carry their border on the wrapping <label>
   (.tb-search in layout.css, .eml-search / .acc-search in the screens), and
   those wrappers ease their own ring. This base rule covers every field that
   carries its own border - the subject line, the compose textarea, the filter
   <select> - so the focus ring fades in rather than snapping on. Element
   selectors only, so any screen rule outranks it.
   --------------------------------------------------------------------------- */
input[type="search"],
input[type="text"],
input[type="email"],
input[type="url"],
textarea,
select {
  transition:
    border-color var(--t-fast),
    box-shadow var(--t-fast),
    background var(--t-fast);
}


/* ===========================================================================
   THE ui.js COMPONENT LIBRARY
   ---------------------------------------------------------------------------
   Sections 16 to 27 below are the stylesheet half of assets/js/ui.js. Every
   class here is written by that module and by nothing else, which is why they
   all carry the `ui-` prefix: a screen stylesheet can safely style anything
   without a prefix, and this block can be reasoned about on its own.

   Three rules on top of the ones at the top of this file:

     - Dynamic values arrive as custom properties, never as inline styles:
       --drag-y (how far a sheet has been pulled down), --veil-progress (how
       solid the backdrop is mid drag), --meter-fill, --ink-x / --ink-w (the
       tab underline), --col-w (a table column) and --skel-delay.
     - Anything ui.js toggles with `el.hidden` states its display through
       `:not([hidden])`. base.css declares `[hidden]{display:none}` at 0,1,0 and
       loads first, so a bare `display:flex` here would win and resurrect the
       node. This is the one trap in the whole block.
     - Touch targets are 44px from 860px down. Above that the same controls
       relax, because a mouse does not need the room and a 44px row of chips
       reads as a toolbar rather than as a filter.
   =========================================================================== */


/* ---------------------------------------------------------------------------
   16. Overlay keyframes
   Four pairs, one per shape. The exits are quicker than the entrances: a thing
   arriving deserves to be watched, a thing leaving should get out of the way.

   ui-sheet-out reads --drag-y for its start frame, so a sheet flicked away
   carries on from where the finger let go instead of snapping back to rest and
   then sliding down.
   --------------------------------------------------------------------------- */
@keyframes ui-veil-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes ui-veil-out { from { opacity: 1; } to { opacity: 0; } }

@keyframes ui-dialog-in {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes ui-dialog-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(6px) scale(.99); }
}

@keyframes ui-sheet-in  { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes ui-sheet-out { from { transform: translateY(var(--drag-y, 0px)); } to { transform: translateY(100%); } }

@keyframes ui-drawer-in-right  { from { transform: translateX(100%); }  to { transform: translateX(0); } }
@keyframes ui-drawer-out-right { from { transform: translateX(0); }     to { transform: translateX(100%); } }
@keyframes ui-drawer-in-left   { from { transform: translateX(-100%); } to { transform: translateX(0); } }
@keyframes ui-drawer-out-left  { from { transform: translateX(0); }     to { transform: translateX(-100%); } }

@keyframes ui-toast-in {
  from { opacity: 0; transform: translateY(10px) scale(.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes ui-toast-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(6px); }
}

/* The skeleton sweep. A loop, not a state change, and slower than the busy
   sweep's --t-loop, so it keeps a component-local duration: --skel-cycle. */
@keyframes ui-shimmer {
  from { background-position: 150% 0; }
  to   { background-position: -50% 0; }
}


/* ---------------------------------------------------------------------------
   17. Overlays - modal, bottom sheet, drawer
   ui.js puts .has-overlay on <html> for as long as at least one is open. Both
   the root and the body are frozen, because which of the two scrolls changes
   at the 860px shell switch: on desktop .content is the scroller inside a
   locked body, on mobile layout.css hands scrolling back to the window.
   --------------------------------------------------------------------------- */
.has-overlay,
.has-overlay body {
  overflow: hidden;
  overscroll-behavior: none;
}

.has-overlay .content,
.has-overlay .pbody {
  overflow: hidden;
}

.ui-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  min-width: 0;
}

.ui-backdrop {
  position: absolute;
  inset: 0;
  background: var(--veil);
  backdrop-filter: blur(var(--blur-veil));
  -webkit-backdrop-filter: blur(var(--blur-veil));
  /* --veil-progress falls with the drag, so veil and sheet leave together. */
  opacity: var(--veil-progress, 1);
  /* The veil is a scroll blocker as much as a wash: without this a drag on it
     pans the page underneath on a phone. */
  touch-action: none;
  animation: ui-veil-in var(--t-base) backwards;
}

.ui-overlay.is-closing .ui-backdrop {
  animation: ui-veil-out var(--t-fast) forwards;
}

/* Alignment belongs to the overlay, not to the dialog: the box that centres a
   dialog is not the box that pins a sheet to the bottom edge. ui.js re-applies
   these when the viewport crosses 720px, so a form opened on a desktop window
   and finished on a narrow one changes shape without losing its state. */
.ui-overlay.ui-as-center {
  align-items: center;
  justify-content: center;
  padding:
    calc(var(--s-6) + var(--safe-t))
    calc(var(--s-5) + var(--safe-r))
    calc(var(--s-6) + var(--safe-b))
    calc(var(--s-5) + var(--safe-l));
}

.ui-overlay.ui-as-sheet {
  align-items: flex-end;
  justify-content: center;
  padding-top: calc(var(--s-7) + var(--safe-t));
}

.ui-overlay.ui-as-drawer { align-items: stretch; }
.ui-overlay.ui-as-drawer.ui-side-right { justify-content: flex-end; }
.ui-overlay.ui-as-drawer.ui-side-left  { justify-content: flex-start; }

.ui-dialog {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  min-height: 0;
  min-width: 0;
  background: var(--white);
  box-shadow: var(--sh-lg);
  overflow: hidden;
}

/* Focus lands on the dialog itself on open - that is what makes a screen
   reader announce the title instead of a random first field, and what keeps
   the on-screen keyboard down until the owner asks for it. Keep the ring. */
.ui-dialog:focus-visible {
  box-shadow: var(--focus-ring), var(--sh-lg);
}

.ui-dialog--center {
  max-height: calc(100dvh - var(--s-8) - var(--safe-t) - var(--safe-b));
  border-radius: var(--r-lg);
  animation: ui-dialog-in var(--t-base) backwards;
}

.ui-as-center .ui-dialog--sm   { max-width: 420px; }
.ui-as-center .ui-dialog--md   { max-width: 560px; }
.ui-as-center .ui-dialog--lg   { max-width: 820px; }
.ui-as-center .ui-dialog--full { max-width: 1120px; }

.ui-overlay.is-closing .ui-dialog--center {
  animation: ui-dialog-out var(--t-fast) forwards;
}

/* The sheet. `backwards`, not `both`: the entrance ends on the resting value,
   and holding the last keyframe would pin transform for good, so the drag
   below could never move the sheet an inch. Same trap as .kcard above. */
.ui-dialog--sheet {
  max-width: 640px;
  max-height: 100%;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  transform: translateY(var(--drag-y, 0px));
  transition: transform var(--t-base);
  animation: ui-sheet-in var(--t-base) backwards;
}

/* While a finger owns the sheet there is no easing: the surface has to track
   the finger exactly, or the gesture feels like it is being argued with. */
.ui-dialog--sheet.is-dragging {
  transition: none;
}

.ui-overlay.is-closing .ui-dialog--sheet {
  animation: ui-sheet-out var(--t-base) forwards;
}

.ui-dialog--drawer {
  max-width: min(480px, 100%);
  height: 100%;
  border-radius: 0;
  padding-top: var(--safe-t);
  animation: ui-drawer-in-right var(--t-base) backwards;
}

.ui-dialog--drawer.ui-dialog--lg   { max-width: min(680px, 100%); }
.ui-dialog--drawer.ui-dialog--full { max-width: 100%; }
.ui-dialog--drawer.ui-dialog--left { animation-name: ui-drawer-in-left; }

.ui-overlay.is-closing .ui-dialog--drawer { animation: ui-drawer-out-right var(--t-base) forwards; }
.ui-overlay.is-closing .ui-dialog--left   { animation: ui-drawer-out-left  var(--t-base) forwards; }

/* The grab handle. Only a sheet has one, and it is also one of the two drag
   surfaces, hence touch-action: none - otherwise the browser claims the
   vertical gesture before the pointermove listener ever sees it. */
.ui-grab { display: none; }

.ui-dialog--sheet .ui-grab {
  display: flex;
  flex: none;
  justify-content: center;
  padding: var(--s-3) 0 var(--s-1);
  touch-action: none;
  cursor: grab;
}

.ui-dialog--sheet.is-dragging .ui-grab { cursor: grabbing; }

.ui-grab-bar {
  width: 40px;
  height: 4px;
  border-radius: 999px;
  background: var(--gray-300);
  transition: background var(--t-fast);
}

.ui-dialog--sheet.is-dragging .ui-grab-bar { background: var(--gray-400); }

.ui-dialog-head {
  flex: none;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--gray-200);
}

.ui-dialog--sheet .ui-dialog-head {
  padding-top: var(--s-2);
  touch-action: none;
}

.ui-dialog-heads {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ui-dialog-t {
  margin: 0;
  font-family: var(--f-disp);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.01em;
  line-height: 1.25;
  color: var(--ink);
}

.ui-dialog-desc {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--muted);
}

.ui-close { flex: none; }

/* The sprite has no cross, and adding one is another agent's file. A plus
   turned 45 degrees is the same glyph, and it inherits every icon-btn state. */
.ui-x { transform: rotate(45deg); }

.ui-dialog-body {
  flex: 1;
  min-height: 0;
  min-width: 0;
  overflow: auto;
  overscroll-behavior: contain;
}

.ui-dialog-body--fixed { overflow: hidden; }

.ui-dialog-inner { padding: var(--s-5); }

.ui-dialog-foot:not([hidden]) { display: flex; }

.ui-dialog-foot {
  flex: none;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-2);
  flex-wrap: wrap;
  padding: var(--s-3) var(--s-5);
  border-top: 1px solid var(--gray-200);
  background: var(--white);
}

.ui-dialog--sheet .ui-dialog-foot,
.ui-dialog--drawer .ui-dialog-foot {
  padding-bottom: calc(var(--s-3) + var(--safe-b));
}

/* Confirm dialog: one short question, so the icon, the sentence and nothing
   else. */
.ui-confirm {
  display: flex;
  align-items: flex-start;
  gap: var(--s-4);
}

.ui-confirm-ic {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: var(--r-sm);
  background: var(--gold-100);
  color: var(--gold-600);
}

.ui-confirm-ic--danger { background: var(--red-100); color: var(--red-700); }

.ui-confirm-ic .ic { width: 20px; height: 20px; }

.ui-confirm-msg {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink);
}


/* ---------------------------------------------------------------------------
   18. Toasts
   Bottom right on desktop, a full width column above the tab bar on mobile,
   clear of the iPhone home indicator on both. z-index sits ABOVE the overlay
   on purpose: a "Case saved" line, and its Undo, must stay readable and
   pressable while the next dialog is already open.
   --------------------------------------------------------------------------- */
.ui-toasts {
  position: fixed;
  z-index: 80;
  right: calc(var(--s-4) + var(--safe-r));
  bottom: calc(var(--s-4) + var(--safe-b));
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  width: min(380px, calc(100vw - var(--s-8)));
  pointer-events: none;
}

.ui-toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-3) var(--s-3) var(--s-4);
  border-radius: var(--r);
  background: var(--navy-900);
  color: var(--white);
  box-shadow: var(--sh-lg);
  font-family: var(--f-ui);
  font-size: 13px;
  line-height: 1.45;
  animation: ui-toast-in var(--t-base) backwards;
}

.ui-toast.is-closing { animation: ui-toast-out var(--t-fast) forwards; }

.ui-toast-ic {
  display: inline-flex;
  flex: none;
  color: var(--gold-300);
}

.ui-toast-ic .ic { width: 17px; height: 17px; }

.ui-toast--success .ui-toast-ic { color: var(--green); }
.ui-toast--warn    .ui-toast-ic { color: var(--gold); }
.ui-toast--error   .ui-toast-ic { color: var(--red); }

.ui-toast-msg {
  margin: 0;
  flex: 1;
  min-width: 0;
}

.ui-toast-act {
  flex: none;
  min-height: 32px;
  padding: var(--s-1) var(--s-2);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--gold);
  font-family: var(--f-ui);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}

.ui-toast-act:hover { background: var(--navy-800); }

.ui-toast-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--gray-400);
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}

.ui-toast-x .ic { width: 15px; height: 15px; }

.ui-toast-x:hover { background: var(--navy-800); color: var(--white); }


/* ---------------------------------------------------------------------------
   19. Form fields
   Every control is a real control with a real <label for>. The error line is
   permanently in the DOM and toggled with `hidden`, so aria-describedby can
   point at it from the moment the field mounts rather than being rewired at
   the exact moment the owner most needs it read out.
   --------------------------------------------------------------------------- */
.ui-form { display: block; }

.ui-form-desc {
  margin: 0 0 var(--s-4);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}

.ui-form-alert:not([hidden]) { display: block; }

.ui-form-alert {
  margin: 0 0 var(--s-4);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--red-100);
  border-radius: var(--r-sm);
  background: var(--red-100);
  color: var(--red-700);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.45;
  animation: rise-in var(--t-base) backwards;
}

.ui-form-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-4);
  min-width: 0;
}

.ui-form-foot:not([hidden]) { display: flex; }

.ui-form-foot {
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-2);
  flex-wrap: wrap;
  margin-top: var(--s-5);
}

/* A busy form dims but does not collapse: ui.js disables the controls, so the
   only job left here is to say so. */
.ui-form.is-busy .ui-form-grid {
  opacity: .7;
  transition: opacity var(--t-fast);
}

.ui-field {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  min-width: 0;
}

/* A checkbox or a switch carries its own label inside the control, so the
   field wrapper has nothing to space away from it. */
.ui-field--inline { gap: var(--s-1); }

.ui-label {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-family: var(--f-ui);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1.3;
  color: var(--navy);
}

.ui-req { color: var(--red); }

.ui-help {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--muted);
}

.ui-err:not([hidden]) { display: block; }

.ui-err {
  margin: 0;
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.45;
  color: var(--red-700);
  animation: rise-in var(--t-fast) backwards;
}

.ui-input {
  width: 100%;
  min-height: 40px;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--gray-300);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: 14px;
  line-height: 1.4;
  transition:
    border-color var(--t-fast),
    box-shadow var(--t-fast),
    background var(--t-fast);
}

.ui-input::placeholder { color: var(--gray-500); }

.ui-input:hover:not(:disabled):not([readonly]) { border-color: var(--gray-400); }

.ui-input:focus { border-color: var(--gold-600); }

.ui-input:disabled {
  background: var(--gray-50);
  color: var(--muted);
  cursor: not-allowed;
}

.ui-input[readonly] { background: var(--gray-50); }

.ui-textarea {
  min-height: 96px;
  resize: vertical;
  line-height: 1.5;
}

.ui-input--money {
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
}

/* The invalid state paints the control, not only the message under it. */
.ui-field.is-invalid .ui-input,
.ui-field.is-invalid .ui-select-el {
  border-color: var(--red);
  background: var(--red-100);
}

.ui-select { position: relative; display: block; }

.ui-select-el {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--s-7);
  cursor: pointer;
}

.ui-select-ic {
  position: absolute;
  top: 50%;
  right: var(--s-3);
  display: flex;
  transform: translateY(-50%);
  pointer-events: none;
  color: var(--gray-500);
}

.ui-select-ic .ic { width: 16px; height: 16px; }

/* Prefixed and suffixed inputs: the Rp on a money field, a unit on a number. */
.ui-affix {
  display: flex;
  align-items: stretch;
  min-width: 0;
}

.ui-affix-pre,
.ui-affix-post {
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: 0 var(--s-3);
  border: 1px solid var(--gray-300);
  background: var(--gray-50);
  color: var(--muted);
  font-family: var(--f-ui);
  font-size: 12.5px;
  font-weight: 600;
}

.ui-affix-pre  { border-right: 0; border-radius: var(--r-sm) 0 0 var(--r-sm); }
.ui-affix-post { border-left: 0;  border-radius: 0 var(--r-sm) var(--r-sm) 0; }

.ui-affix > .ui-input { border-radius: 0; }
.ui-affix > .ui-input:first-child { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.ui-affix > .ui-input:last-child  { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
.ui-affix > .ui-input:only-child  { border-radius: var(--r-sm); }

/* Checkbox. A 20px box inside a 44px row: the target is the row, the mark is
   the box. The radius is derived from --r-sm rather than typed, so a change to
   the radius scale still lands here. */
/* A <label for> that wraps its own input: the whole 44px row is the target,
   and the association is still explicit rather than implied by nesting. */
.ui-check {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-height: 44px;
  cursor: pointer;
}

.ui-checkbox {
  position: relative;
  flex: none;
  width: 20px;
  height: 20px;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid var(--gray-300);
  border-radius: calc(var(--r-sm) - 4px);
  background: var(--white);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}

.ui-checkbox:checked {
  background: var(--navy);
  border-color: var(--navy);
}

.ui-checkbox:checked::after {
  content: "";
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 10px;
  border: solid var(--white);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.ui-checkbox:disabled { background: var(--gray-100); cursor: not-allowed; }

.ui-check-l {
  font-family: var(--f-ui);
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--ink);
  cursor: pointer;
}

/* Switch. A button with role="switch", so its accessible name is its own
   label text and there is no <label for> pointing at a thing that is not an
   input. */
.ui-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  min-height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
}

.ui-switch-track {
  position: relative;
  flex: none;
  width: 44px;
  height: 26px;
  border-radius: 999px;
  background: var(--gray-300);
  transition: background var(--t-fast);
}

.ui-switch-thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--white);
  box-shadow: var(--sh-sm);
  transition: transform var(--t-base);
}

.ui-switch[aria-checked="true"] .ui-switch-track { background: var(--navy); }
.ui-switch[aria-checked="true"] .ui-switch-thumb { transform: translateX(18px); }

.ui-switch:disabled { opacity: .55; cursor: not-allowed; }

.ui-switch-l { font-weight: 500; }

/* Radios: whole-row targets, because a 20px dot is not a tap target. */
.ui-radios {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.ui-radio-row {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  min-height: 44px;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--gray-200);
  border-radius: var(--r-sm);
  transition: border-color var(--t-fast), background var(--t-fast);
}

.ui-radio {
  position: relative;
  flex: none;
  width: 20px;
  height: 20px;
  margin: 3px 0 0;
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid var(--gray-300);
  border-radius: 50%;
  background: var(--white);
  cursor: pointer;
  transition: border-color var(--t-fast), border-width var(--t-fast);
}

.ui-radio:checked {
  border-color: var(--navy);
  border-width: 6px;
}

.ui-radio-row:has(.ui-radio:checked) {
  border-color: var(--navy);
  background: var(--gray-50);
}

.ui-radio-l {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  cursor: pointer;
}

.ui-radio-t { font-size: 13.5px; font-weight: 500; color: var(--ink); }
.ui-radio-s { font-size: 11.5px; color: var(--muted); }

/* Combobox: search-select and the user / client pickers. */
.ui-combo { position: relative; min-width: 0; }

.ui-combo-input { padding-right: var(--s-7); }

.ui-combo-toggle {
  position: absolute;
  top: 50%;
  right: var(--s-1);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--gray-500);
  cursor: pointer;
  transform: translateY(-50%);
  transition: color var(--t-fast), transform var(--t-base);
}

.ui-combo-toggle .ic { width: 16px; height: 16px; }

.ui-combo.is-open .ui-combo-toggle { transform: translateY(-50%) rotate(180deg); }

.ui-combo-list:not([hidden]) { display: block; }

.ui-combo-list {
  position: absolute;
  z-index: 5;
  top: calc(100% + var(--s-1));
  left: 0;
  right: 0;
  max-height: 260px;
  padding: var(--s-1);
  overflow: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--gray-200);
  border-radius: var(--r);
  background: var(--white);
  box-shadow: var(--sh-lg);
  animation: rise-in var(--t-fast) backwards;
}

/* Flipped upward when the field sits near the bottom of a sheet. */
.ui-combo--up .ui-combo-list {
  top: auto;
  bottom: calc(100% + var(--s-1));
}

.ui-opt {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-height: 44px;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background var(--t-fast);
}

.ui-opt.is-active,
.ui-opt:hover { background: var(--gray-50); }

.ui-opt[aria-selected="true"] { background: var(--gold-a05); }

.ui-opt.is-disabled { opacity: .5; cursor: not-allowed; }

.ui-opt--note {
  min-height: 36px;
  color: var(--muted);
  font-size: 12.5px;
  cursor: default;
}

.ui-opt--note:hover { background: transparent; }

.ui-opt-ic { display: inline-flex; flex: none; color: var(--gray-500); }
.ui-opt-ic .ic { width: 16px; height: 16px; }

.ui-opt-b {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex: 1;
  min-width: 0;
}

.ui-opt-l {
  font-size: 13.5px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-opt-s {
  font-size: 11.5px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-opt-check { display: inline-flex; flex: none; color: var(--gold-600); }
.ui-opt-check .ic { width: 16px; height: 16px; }


/* ---------------------------------------------------------------------------
   20. Empty state and skeletons
   --------------------------------------------------------------------------- */
.ui-empty-ic {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--r-lg);
  background: var(--gray-100);
  color: var(--gray-500);
}

.ui-empty-ic .ic { width: 20px; height: 20px; }

.ui-empty-msg {
  margin: 0;
  max-width: 44ch;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}

.ui-empty .btn { margin-top: var(--s-2); }

.ui-skel-list {
  /* Component-local motion token: a shimmer is a loop, and an unhurried one,
     slower than --t-loop in tokens.css. base.css still neutralises it under
     prefers-reduced-motion through the global animation-duration safety net,
     which runs it once and leaves the plain gradient at rest. That holds only
     while .ui-skel scores under the net's 0,1,1: a selector that ties it or
     scores more has to stop the loop itself, the way layout.css stops the
     busy bar. */
  --skel-cycle: 1400ms;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.ui-skel-list[data-kind="tile"] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}

.ui-skel-item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4);
  border: 1px solid var(--gray-200);
  border-radius: var(--r);
  background: var(--white);
  animation: fade-in var(--t-base) backwards;
  animation-delay: var(--skel-delay, 0ms);
}

.ui-skel-list[data-kind="card"] .ui-skel-item {
  flex-direction: column;
  align-items: stretch;
  gap: var(--s-3);
}

.ui-skel {
  display: block;
  height: 12px;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--gray-100) 0%, var(--gray-200) 50%, var(--gray-100) 100%);
  background-size: 200% 100%;
  animation: ui-shimmer var(--skel-cycle) linear infinite;
}

.ui-skel-lines {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  flex: 1;
  min-width: 0;
}

.ui-skel-lines--row { flex-direction: row; gap: var(--s-2); }

.ui-skel--av    { flex: none; width: 36px; height: 36px; border-radius: 50%; }
.ui-skel--plate { flex: none; width: 40px; height: 40px; border-radius: var(--r-sm); }
.ui-skel--num   { width: 40%; height: 22px; border-radius: var(--r-sm); }
.ui-skel--l1    { width: 62%; }
.ui-skel--l2    { width: 38%; height: 10px; }
.ui-skel--chip  { width: 68px; height: 20px; border-radius: var(--r-sm); }


/* ---------------------------------------------------------------------------
   21. Data table
   Both views are always rendered and the stylesheet shows exactly one. That is
   deliberate: a resize listener that re-renders can be wrong for a frame, and
   the hidden half is display:none, which also takes its buttons out of the tab
   order. Below 720px the table is gone and the cards are the interface.
   --------------------------------------------------------------------------- */
.ui-dt { min-width: 0; }

.ui-dt-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.ui-dt-cards { display: none; }

.ui-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-family: var(--f-ui);
  font-size: 13px;
}

.ui-table col { width: var(--col-w, auto); }

.ui-table th {
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--gray-200);
  background: var(--white);
  color: var(--gray-600);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
}

.ui-table td {
  padding: var(--s-3);
  border-bottom: 1px solid var(--gray-100);
  color: var(--ink);
  vertical-align: middle;
  /* Columns of numbers have to line up; a standalone figure does not, which is
     why the stat tile below deliberately does not do this. */
  font-variant-numeric: tabular-nums;
}

.ui-table tbody tr:last-child td { border-bottom: 0; }

.ui-table [data-align="end"]    { text-align: right; }
.ui-table [data-align="center"] { text-align: center; }

.ui-table tbody tr { transition: background var(--t-fast); }

.ui-dt--rows tbody tr { cursor: pointer; }

.ui-dt--dense td { padding: var(--s-2) var(--s-3); }

.ui-dt--sticky thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  box-shadow: inset 0 -1px 0 var(--gray-200);
}

/* The keyboard route into a row: the title cell is a real button, so a row is
   reachable without a mouse and without inventing tabindex on a <tr>. */
.ui-dt-open {
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--navy);
  font-family: inherit;
  font-size: inherit;
  font-weight: 600;
  text-align: inherit;
  cursor: pointer;
  transition: color var(--t-fast);
}

.ui-dt-open:hover { color: var(--gold-600); }

.ui-dt-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--gray-200);
  border-radius: var(--r);
  background: var(--white);
  box-shadow: var(--sh-sm);
  overflow: hidden;
  animation: rise-in var(--t-base) backwards;
}

.ui-dt-card:where(:nth-child(2))   { animation-delay: var(--stagger); }
.ui-dt-card:where(:nth-child(3))   { animation-delay: calc(var(--stagger) * 2); }
.ui-dt-card:where(:nth-child(4))   { animation-delay: calc(var(--stagger) * 3); }
.ui-dt-card:where(:nth-child(5))   { animation-delay: calc(var(--stagger) * 4); }
.ui-dt-card:where(:nth-child(n+6)) { animation-delay: calc(var(--stagger) * 5); }

.ui-dt-cardbtn {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  width: 100%;
  min-height: 44px;
  padding: var(--s-3) var(--s-4);
  border: 0;
  background: none;
  color: inherit;
  font-family: var(--f-ui);
  text-align: left;
  cursor: pointer;
  transition: background var(--t-fast);
}

.ui-dt-cardbtn--static { cursor: default; }

.ui-dt-cardbtn:active { background: var(--gold-a05); }

.ui-dt-card-t {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--navy);
}

.ui-dt-card-s {
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--muted);
}

.ui-dt-card-m {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  margin-top: var(--s-1);
}

.ui-dt-cell {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.ui-dt-k {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--gray-600);
}

.ui-dt-v {
  font-size: 12.5px;
  line-height: 1.35;
  color: var(--ink);
}

.ui-dt-card-act {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4) var(--s-3);
  border-top: 1px solid var(--gray-100);
}


/* ---------------------------------------------------------------------------
   22. Pager
   --------------------------------------------------------------------------- */
.ui-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-4) 0;
}

.ui-pager-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 38px;
  height: 38px;
  border: 1px solid var(--gray-200);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--navy);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}

.ui-pager-arrow .ic { width: 17px; height: 17px; }

.ui-pager-arrow:hover:not(:disabled) { background: var(--gray-50); border-color: var(--gray-300); }
.ui-pager-arrow:active:not(:disabled) { transform: scale(.94); }

.ui-pager-arrow:disabled { opacity: .45; cursor: not-allowed; }

.ui-pager-pages {
  display: flex;
  align-items: center;
  gap: var(--s-1);
}

.ui-pager-n {
  min-width: 36px;
  height: 36px;
  padding: 0 var(--s-2);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--gray-600);
  font-family: var(--f-ui);
  font-size: 12.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}

/* A page number is a real target, and 36px is under what a thumb hits reliably.
   Raised only where the pointer is coarse: a mouse is precise enough for 36px,
   and a desktop pager that jumped to 44px would push the table down for nothing. */
@media (pointer: coarse) {
  .ui-pager-n {
    min-width: 44px;
    height: 44px;
  }
}

.ui-pager-n:hover { background: var(--gray-100); color: var(--navy); }

.ui-pager-n.is-on {
  background: var(--navy);
  border-color: var(--navy);
  color: var(--white);
}

.ui-pager-gap {
  padding: 0 var(--s-1);
  color: var(--gray-400);
  font-size: 12.5px;
}

.ui-pager-status {
  display: none;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}


/* ---------------------------------------------------------------------------
   23. Tabs and segmented control
   The tab underline is one element moved with --ink-x and --ink-w, so the
   emphasis glides from tab to tab rather than blinking out of one and into the
   next. It lives inside the scrolling track, which is what keeps it aligned
   when the strip is scrolled sideways on a phone.
   --------------------------------------------------------------------------- */
.ui-tabs {
  position: relative;
  border-bottom: 1px solid var(--gray-200);
  min-width: 0;
}

.ui-tabs-track {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: var(--s-1);
  overflow-x: auto;
  scrollbar-width: none;
}

.ui-tabs-track::-webkit-scrollbar { display: none; }

.ui-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  flex: none;
  min-height: 44px;
  padding: var(--s-2) var(--s-3);
  border: 0;
  background: none;
  color: var(--gray-600);
  font-family: var(--f-ui);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--t-fast);
}

.ui-tab .ic { width: 16px; height: 16px; }

.ui-tab:hover { color: var(--navy); }

.ui-tab.is-on { color: var(--navy); }

.ui-tab:disabled { opacity: .5; cursor: not-allowed; }

.ui-tab-n { margin-left: var(--s-1); }

.ui-tabs-ink {
  position: absolute;
  left: 0;
  bottom: 0;
  width: var(--ink-w, 0px);
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--gold);
  transform: translateX(var(--ink-x, 0px));
  opacity: 0;
  transition:
    transform var(--t-base),
    width var(--t-base),
    opacity var(--t-fast);
}

.ui-tabs.is-inked .ui-tabs-ink { opacity: 1; }

/* The segmented control reuses the .seg track from section 7, so the Board /
   List switch the Cases screen already ships and a segmented() built here are
   the same control. Only the flex box for an icon plus a label is new. */
.ui-seg-b {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 34px;
}

.ui-seg-b .ic { width: 15px; height: 15px; }


/* ---------------------------------------------------------------------------
   24. Filter bar
   The search field leads on its own row; the chips and selects live in a track
   that wraps on desktop and scrolls sideways on a phone. That track is the one
   place in this file that scrolls horizontally, and it is its own box, so the
   page body still never moves sideways.
   --------------------------------------------------------------------------- */
.ui-fb {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-width: 0;
}

.ui-fb-lead:not([hidden]) { display: flex; }

.ui-fb-lead {
  flex: 0 1 280px;
  min-width: 0;
}

.ui-fb-track {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex: 1;
  flex-wrap: wrap;
  min-width: 0;
}

.ui-fb-search {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  min-height: 40px;
  padding: 0 var(--s-3);
  border: 1px solid var(--gray-300);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--gray-500);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}

.ui-fb-search:focus-within {
  border-color: var(--gold-600);
  box-shadow: var(--focus-ring);
}

.ui-fb-search .ic { width: 16px; height: 16px; flex: none; }

.ui-fb-input {
  flex: 1;
  min-width: 0;
  width: 100%;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: 13.5px;
  line-height: 1.4;
}

.ui-fb-input:focus-visible { outline: none; box-shadow: none; }

.ui-fb-input::placeholder { color: var(--gray-500); }

.ui-fb-date { flex: none; width: auto; }

.ui-fb-input--date { width: auto; font-variant-numeric: tabular-nums; }

.ui-fb-select {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: none;
}

.ui-fb-select-el {
  appearance: none;
  -webkit-appearance: none;
  min-height: 36px;
  padding: 0 var(--s-7) 0 var(--s-3);
  border: 1px solid var(--gray-300);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--navy);
  font-family: var(--f-ui);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}

.ui-fb-select-el:hover { background: var(--gray-50); }

.ui-fb-caret {
  position: absolute;
  top: 50%;
  right: var(--s-2);
  display: flex;
  transform: translateY(-50%);
  pointer-events: none;
  color: var(--gray-500);
}

.ui-fb-caret .ic { width: 14px; height: 14px; }

.ui-fb-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  min-width: 0;
}

.ui-fb-reset:not([hidden]) { display: inline-flex; }

.ui-fb-reset .ui-x { width: 12px; height: 12px; }


/* ---------------------------------------------------------------------------
   25. Figures - stat tile and progress meter
   Direction is never carried by colour alone: a delta ships an arrow and a
   signed number too, so it still reads in greyscale, in print, and to someone
   who cannot separate the red from the green.
   --------------------------------------------------------------------------- */
.ui-stat {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  width: 100%;
  /* A grid item defaults to min-width:auto, which refuses to shrink below the
     widest unbreakable word inside it. On a 320px phone that pushed the right
     hand column of the dashboard six pixels past the edge of the screen. The
     tile is always allowed to be narrower than its content wants; what the
     content does inside is the tile's own business. */
  min-width: 0;
  padding: var(--s-4);
  font-family: var(--f-ui);
  text-align: left;
}

button.ui-stat,
a.ui-stat { cursor: pointer; }

.ui-stat-ic {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: var(--r-sm);
  background: var(--gray-100);
  color: var(--gray-600);
}

.ui-stat-ic .ic { width: 19px; height: 19px; }

/* The tone map. Every tint is a status pair from tokens.css, and the -700 text
   shade is the one tuned to its own tint. */
.ui-tone--gold  { background: var(--gold-100);  color: var(--gold-600); }
.ui-tone--green { background: var(--green-100); color: var(--green-700); }
.ui-tone--red   { background: var(--red-100);   color: var(--red-700); }
.ui-tone--blue  { background: var(--blue-100);  color: var(--blue); }
.ui-tone--navy  { background: var(--navy);      color: var(--gold); }
.ui-tone--slate { background: var(--gray-100);  color: var(--gray-600); }

.ui-stat-b {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.ui-stat-l {
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--muted);
}

.ui-stat-v {
  font-family: var(--f-disp);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.15;
  color: var(--navy);
  font-variant-numeric: tabular-nums;
}

.ui-stat-d {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  margin-top: 2px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--muted);
}

.ui-stat-d.is-good { color: var(--green-700); }
.ui-stat-d.is-bad  { color: var(--red-700); }

.ui-delta-ar { width: 13px; height: 13px; }

/* The sprite carries chevron-down only; up is the same glyph turned over. */
.ui-delta--up .ui-delta-ar { transform: rotate(180deg); }

.ui-stat-dl { font-weight: 500; color: var(--muted); }

.ui-stat-h {
  font-size: 11px;
  line-height: 1.4;
  color: var(--muted);
}

/* The meter track is a light step of the fill's own ramp rather than plain
   grey, so the state reads across the whole bar and not only across the part
   that is filled. */
.ui-meter {
  --meter-ink: var(--gold);
  --meter-track: var(--gold-100);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  min-width: 0;
}

.ui-meter[data-tone="green"] { --meter-ink: var(--green);     --meter-track: var(--green-100); }
.ui-meter[data-tone="red"]   { --meter-ink: var(--red);       --meter-track: var(--red-100); }
.ui-meter[data-tone="blue"]  { --meter-ink: var(--blue);      --meter-track: var(--blue-100); }
.ui-meter[data-tone="navy"]  { --meter-ink: var(--navy);      --meter-track: var(--gray-200); }
.ui-meter[data-tone="slate"] { --meter-ink: var(--gray-500);  --meter-track: var(--gray-200); }

.ui-meter-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
}

.ui-meter-l {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--navy);
}

.ui-meter-v {
  font-family: var(--f-mono);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.ui-meter-track {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: var(--meter-track);
  overflow: hidden;
}

.ui-meter-fill {
  display: block;
  width: var(--meter-fill, 0%);
  height: 100%;
  border-radius: inherit;
  background: var(--meter-ink);
  transform-origin: left center;
  transition: width var(--t-slow), background var(--t-fast);
  animation: bar-grow var(--t-slow) both;
}

.ui-meter-hint {
  margin: 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--muted);
}


/* ---------------------------------------------------------------------------
   26. Two button variants the library needs
   .btn--danger is the destructive confirm; .btn--plain is a footer action that
   should not compete with the primary. Both restate their disabled hover,
   because section 6 parks a disabled .btn back on the navy ground.
   --------------------------------------------------------------------------- */
.btn--danger { background: var(--red); color: var(--white); }
.btn--danger:hover { background: var(--red-700); }

.btn--plain { background: transparent; color: var(--navy); }
.btn--plain:hover { background: var(--gray-100); }

.btn--danger:disabled:hover,
.btn--danger[aria-disabled="true"]:hover { background: var(--red); }

.btn--plain:disabled:hover,
.btn--plain[aria-disabled="true"]:hover { background: transparent; }


/* ---------------------------------------------------------------------------
   27. Narrow viewports
   Contract section 7 and section 4.5: 44px targets, cards instead of tables,
   and the tab bar plus the home indicator paid back at the bottom of anything
   that floats.
   --------------------------------------------------------------------------- */
@media (max-width: 719.98px) {

  /* The table is gone and the cards are the interface. display:none, not
     visibility, so the table's buttons leave the tab order with it. */
  .ui-dt-scroll { display: none; }

  /* Unless there is nothing to show: the empty state is rendered into the
     scroller, and it still has to be readable on a phone. */
  .ui-dt.is-empty .ui-dt-scroll { display: block; }

  .ui-dt-cards {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
  }

  /* Numbered pages do not fit at 390px; the arrows plus a count do. */
  .ui-pager-pages { display: none; }
  .ui-pager-status { display: block; }
  .ui-pager { justify-content: space-between; }

  /* The search field takes its own row, the rest scroll under it. */
  .ui-fb {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-2);
  }

  .ui-fb-lead { flex: none; }

  /* The filters WRAP on a phone. They used to sit in one horizontally
     scrolling strip, which looked tidy and broke the whole app: Android Chrome
     sizes the layout viewport to the minimum width the page needs, and a
     scrolling strip of flex:none children still reports the width of every
     child to that sum. Two date fields and a select came to 528px, the
     viewport grew to 528px, and every screen was drawn zoomed out, with the
     camera sheet cut off at the right edge. Seen on a real phone on
     12 September 2026, and only after the Indonesian labels made the strip
     longer than the English ones had been.

     Wrapping makes the minimum width the widest single control, which always
     fits, and it also stops hiding a date field off the right edge where a
     thumb never finds it. Measured, not assumed: a strip with width 0 and
     min-width 100% collapsed to nothing, and shrinkable children squashed the
     date inputs unreadable. This is the variant that kept everything visible. */
  .ui-fb-track {
    flex: none;
    flex-wrap: wrap;
    min-width: 0;
    padding-bottom: var(--s-1);
  }

  .ui-fb-track > * { flex: none; }

  .ui-dialog-inner { padding: var(--s-4); }
}

@media (max-width: 860px) {

  /* Contract section 4.5: nothing tappable is smaller than 44 by 44. */
  .ui-close,
  .ui-pager-arrow {
    width: 44px;
    height: 44px;
  }

  .ui-dialog-foot .btn,
  .ui-form-foot .btn {
    flex: 1 1 auto;
    min-height: 44px;
  }

  .ui-input,
  .ui-select-el,
  .ui-fb-search,
  .ui-fb-select-el {
    min-height: 44px;
  }

  /* The same floor, applied to the controls the original list missed. Measured
     on 11 September 2026 across all nine screens at 320, 390 and 430px: filter
     chips came out 34px tall, small icon buttons 34 by 34, mini buttons 30 by
     30, and native date inputs 36px. Every one of them is a primary control on
     a phone, not an afterthought: the chips ARE the filter on mobile.
     Height is what matters for a thumb, so a chip with a short label may stay
     narrower than 44px, but none of them may stay shorter. */
  .fchip,
  .btn,
  .btn--sm,
  .ui-fb-reset {
    min-height: 44px;
  }

  .icon-btn--sm,
  .mini-btn {
    width: 44px;
    height: 44px;
  }

  /* The labelled variant keeps its own width: squeezed into the square above,
     "Mark all as read" broke over four lines and spilled out of its box. */
  .mini-btn--text {
    width: auto;
    min-width: 44px;
    height: auto;
    min-height: 44px;
  }

  /* 16px is the floor iOS needs to leave the page unzoomed when a field takes
     focus; commandbar.css and finance.css already say so for their own fields. */
  .ui-input,
  .ui-select-el,
  .ui-fb-input,
  .tb-search input {
    font-size: 16px;
  }

  input[type="date"],
  input[type="time"],
  input[type="datetime-local"] {
    min-height: 44px;
  }

  /* The search field in every mobile header. It is a <label>, so the whole box
     is the target and the bare input inside it needs no floor of its own, but
     the box itself came out 39px. One pixel under is still under. */
  .tb-search {
    min-height: 44px;
  }

  .ui-combo-toggle { width: 44px; height: 44px; }
  .ui-combo-input { padding-right: var(--s-8); }

  .ui-seg-b { min-height: 40px; }

  .ui-tab { min-height: 48px; }

  .ui-toast-x,
  .ui-toast-act {
    min-width: 44px;
    min-height: 44px;
  }

  /* Above the tab bar and above the home indicator. --bottombar-safe is
     layout.css arithmetic on --bottombar-h plus the inset; the fallback keeps
     the toast off the bar even if this file is ever loaded on its own. */
  .ui-toasts {
    left: calc(var(--s-3) + var(--safe-l));
    right: calc(var(--s-3) + var(--safe-r));
    width: auto;
    bottom: calc(var(--bottombar-safe, 72px) + var(--s-2));
  }

  /* While a sheet owns the bottom of the screen, the stack moves to the top:
     a toast that covers the sheet's own Save button is worse than no toast.
     Desktop keeps the bottom right corner, which no centred dialog reaches. */
  .has-overlay .ui-toasts {
    top: calc(var(--s-3) + var(--safe-t));
    bottom: auto;
  }
}

@media (min-width: 720px) {

  /* Two column forms, but only where there is room for two columns. */
  .ui-form-grid[data-cols="2"] { grid-template-columns: 1fr 1fr; }
  .ui-form-grid[data-cols="2"] .ui-field[data-span="2"] { grid-column: 1 / -1; }
}

/* Hover lifts are for pointers. On a touch screen the same rule leaves a
   phantom hover behind after every tap. */
@media (hover: hover) {
  .ui-dt--rows tbody tr:hover { background: var(--gold-a05); }
  .ui-dt-cardbtn:hover { background: var(--gold-a05); }
}

/* ------------------------------------------------------------------ *
 * SLA countdown tones (.sla on Cases, .dsla on Home)
 *
 * store.slaState() and home.js set the tone classes; until 28 September
 * 2026 no rule coloured them, so a case past its promise read the same as
 * one with hours to go. Text colour only, the same tint pairs as badges.
 * ------------------------------------------------------------------ */
.sla,
.dsla{
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.sla--ok,   .dsla--ok{   color: var(--green-700); }
.sla--warn, .dsla--warn{ color: var(--gold-700); font-weight: 600; }
.sla--late, .dsla--late{ color: var(--red-700); font-weight: 700; }
.sla--none, .dsla--none{ color: var(--gray-500); }

/* ------------------------------------------------------------------ *
 * [hidden] on components that set their own display
 *
 * base.css hides [hidden], but .fchip and .ui-field set display at the same
 * specificity later in the cascade, so the Clear chip never hid and the
 * vault entry form showed every field whatever its type. Restated here, after
 * both rules, without !important.
 * ------------------------------------------------------------------ */
.fchip[hidden],
.ui-field[hidden]{ display: none; }
