/* ============================================================================
   Refocus X — Design Pack · components.css
   The visual component library. Load AFTER tokens.css:

     <link rel="stylesheet" href="/design-pack/tokens.css">
     <link rel="stylesheet" href="/design-pack/components.css">

   Everything here uses tokens — there is not one literal colour, radius or
   shadow below. If a component needs a value that isn't a token, that is a gap
   in tokens.css; fix it there, not here.

   Where a source app had several versions of the same component (three toggle
   switches, four badge styles, two stat bars), one canonical version is kept
   and the divergences are noted in DECISIONS.md.

   CONTENTS
     1  Buttons            8  Stats                15  Overlays & panels
     2  Cards              9  Badges & pills       16  Tabs
     3  Fields             10 Tables               17  Copy-to-clipboard
     4  Switches           11 Expandable rows      18  Success card
     5  Segmented control  12 Event / log list     19  PIN & OTP entry
     6  Info balloon       13 Empty/loading/error  20  Recipient-page bits
     7  Chips & tags       14 Pager                21  Dashboard cards
                                                   22  Stepper (engine-owned)
                                                   23  Activity timeline
                                                   24  Charts
   ============================================================================ */


/* v0.35.0 (D61): the HTML `hidden` attribute always hides - an author rule with `display` (flex, grid, inline-flex) would otherwise outrank the browser's own [hidden] rule. */
[hidden] { display: none !important; }


/* ══ 1 · BUTTONS ══════════════════════════════════════════════════════════ */

.rx-btn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 20px; border-radius: var(--rx-radius-sm);
  font-family: inherit; font-size: var(--rx-text-md); font-weight: 600;
  border: 1.5px solid transparent; cursor: pointer; text-decoration: none;
  transition: background .15s, border-color .15s, opacity .15s;
  white-space: nowrap;
}
.rx-btn:disabled, .rx-btn[aria-disabled="true"] { opacity: .4; cursor: not-allowed; }

.rx-btn-primary { background: var(--rx-ink); color: var(--rx-on-ink); border-color: var(--rx-ink); }
.rx-btn-primary:hover:not(:disabled) { background: var(--rx-ink-hover); }

.rx-btn-outline { background: var(--rx-surface-card); color: var(--rx-ink); border-color: var(--rx-border); }
.rx-btn-outline:hover:not(:disabled) { border-color: var(--rx-ink); }

.rx-btn-ghost { background: none; color: var(--rx-ink-muted); border-color: transparent; }
.rx-btn-ghost:hover:not(:disabled) { background: var(--rx-surface-sunken); color: var(--rx-ink); }

.rx-btn-danger { background: var(--rx-danger); color: var(--rx-on-danger); border-color: var(--rx-danger); }
.rx-btn-danger:hover:not(:disabled) { opacity: .9; }

/* The DESTRUCTIVE tier — unscoped, irreversible. Deliberately a different,
   darker red so it cannot be confused with an ordinary delete. */
.rx-btn-destroy { background: var(--rx-danger-strong); color: var(--rx-on-danger); border-color: var(--rx-danger-strong); }
.rx-btn-destroy:hover:not(:disabled) { opacity: .9; }

/* Quiet danger — for a dangerous action sitting among ordinary row actions.
   Grey and dashed at rest so it never invites a mis-click; red and solid on
   hover so it is unmistakable once you mean it. */
.rx-btn-danger-quiet {
  background: none; color: var(--rx-ink-subtle);
  border: 1px dashed var(--rx-border); padding: 5px 10px; font-size: calc(13.12px * var(--rx-scale)); font-weight: 500;
}
.rx-btn-danger-quiet:hover {
  color: var(--rx-danger); border-color: var(--rx-danger); border-style: solid;
}

.rx-btn-sm { padding: 6px 12px; font-size: var(--rx-text-sm); }


/* ══ 1b · PAGE HEADER ═════════════════════════════════════════════════════ */
/* Every screen names itself. The title size comes from the page-width
   modifier (see tokens.css) so a form and a list are visibly different kinds
   of page without anyone choosing a number. */

.rx-page-header {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 24px;
}
.rx-page-title {
  font-size: var(--rx-page-title, var(--rx-title));
  font-weight: 700; margin: 0 0 3px; color: var(--rx-ink); line-height: 1.2;
}
.rx-page-sub { font-size: var(--rx-text); color: var(--rx-ink-muted); margin: 0; }
/* Right-hand side: actions, a refresh control, a last-updated stamp. */
.rx-page-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }


/* ══ 2 · CARDS ════════════════════════════════════════════════════════════ */

.rx-card {
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-lg); box-shadow: var(--rx-shadow-sm);
  padding: 22px 24px;
}
.rx-card-title { font-size: calc(15px * var(--rx-scale)); font-weight: 600; margin: 0 0 4px; color: var(--rx-ink); }
.rx-card-sub   { font-size: var(--rx-text-sm); color: var(--rx-ink-muted); margin: 0 0 20px; line-height: 1.5; }

/* Sectioned card — one card, several groups divided by rules. */
.rx-card-sectioned { padding: 0; overflow: hidden; }
.rx-card-section   { padding: 22px 26px; border-bottom: 1px solid var(--rx-border); }
.rx-card-section:last-of-type { border-bottom: none; }
.rx-section-title {
  font-size: var(--rx-text-sm); font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
  color: var(--rx-ink-subtle); margin-bottom: 14px;
}

/* Severity edges — reads at a glance without reading the words. */
.rx-card-warn    { border-left: 3px solid var(--rx-warn-strong); }
.rx-card-danger  { border-left: 3px solid var(--rx-danger); }
.rx-card-destroy { border-left: 3px solid var(--rx-danger-strong); }

/* Picture card + catalogue grid — a card with a picture above its body, for a
   LIBRARY of things (lessons, apps, articles) rather than a single record.
   Lifted from LearnGuard's Library and Dashboard (the `.lg-pack-card` family),
   22 Sep 2026, the suite library's own GAP row. */
.rx-card-grid  { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.rx-card-stack { display: flex; flex-direction: column; gap: 12px; }
.rx-card-compact { padding: 14px 16px; }   /* denser than the 22/24 default — a catalogue card, not a form card */
.rx-card-pic {
  display: block; width: 100%; aspect-ratio: 16 / 9; max-height: 220px;
  border-radius: var(--rx-radius-sm); object-fit: cover;
  background: var(--rx-surface-sunken); flex-shrink: 0; margin-bottom: 12px;
}
.rx-card-excerpt {
  font-size: var(--rx-text-sm); color: var(--rx-ink-muted);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.rx-card-meta { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 8px; }
.rx-card-note { font-size: var(--rx-text-sm); color: var(--rx-ink-muted); margin-top: 10px; }
.rx-card-tag  { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); }
/* Card actions - the ONE place a card's buttons live, pinned to the bottom so
   every card in a grid or a stack shows its button in the same spot whatever
   the length of its text (LearnGuard, 23 Sep 2026, Pierre: "The placement of
   the button needs to be the same on all cards"). A card in a grid or a stack
   becomes a flex column for it; a locked or unavailable card keeps the SAME
   button, disabled, with its reason in an `.rx-card-note` above the row. */
.rx-card-grid > .rx-card, .rx-card-stack > .rx-card { display: flex; flex-direction: column; }
.rx-card-actions { margin-top: auto; padding-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

/* Danger zone — for a destructive block inside an otherwise ordinary page. */
.rx-danger-zone { border-color: var(--rx-danger); }
.rx-danger-zone .rx-section-title { color: var(--rx-danger-strong); }


/* ══ 3 · FIELDS ═══════════════════════════════════════════════════════════ */

.rx-fields { display: flex; flex-direction: column; gap: 16px; }
.rx-field  { margin-bottom: 18px; }
.rx-field:last-child { margin-bottom: 0; }

.rx-field label {
  display: block; font-size: var(--rx-text); font-weight: 500;
  color: var(--rx-ink-muted); margin-bottom: 6px;
}
.rx-field label .rx-muted { color: var(--rx-ink-subtle); font-weight: 400; }

/* Checkboxes and radios are EXCLUDED — otherwise a checkbox nested in a field
   gets stretched to the full form width, which looks broken. */
.rx-field input:not([type="checkbox"]):not([type="radio"]),
.rx-field textarea,
.rx-field select {
  width: 100%; padding: 10px 14px; font-family: inherit; font-size: var(--rx-text-md);
  background: var(--rx-surface-card); color: var(--rx-ink);
  border: 1px solid var(--rx-border); border-radius: var(--rx-radius);
  outline: none; transition: border-color .15s, box-shadow .15s; appearance: none;
}
.rx-field input:not([type="checkbox"]):not([type="radio"]):focus,
.rx-field textarea:focus,
.rx-field select:focus {
  border-color: var(--rx-ink); box-shadow: var(--rx-focus-ring);
}
.rx-field textarea { resize: vertical; min-height: 72px; }
.rx-field-hint {
  display: block; font-size: var(--rx-text-xs); color: var(--rx-ink-subtle);
  margin-top: 5px; line-height: 1.5; font-weight: 400;
}
.rx-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: start; }

/* FIELD HELP (v0.37.0, D63) - the small "i" beside a label, opening a short
   plain-words explanation in an RX.popover. Built by RX.fieldHelp
   (engines/rx-forms.js); every value is a token. */
.rx-fh-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.rx-field .rx-fh-row label { margin-bottom: 0; }
.rx-fh-btn {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  box-sizing: border-box; min-width: 24px; min-height: 24px; padding: 0;
  font-family: inherit; font-style: italic; font-weight: 700;
  font-size: var(--rx-text-xs); line-height: 1; cursor: pointer;
  color: var(--rx-ink-muted); background: var(--rx-surface-card);
  border: 1px solid var(--rx-border); border-radius: 50%;
}
.rx-fh-btn:hover, .rx-fh-btn[aria-expanded="true"] {
  color: var(--rx-ink); border-color: var(--rx-ink); background: var(--rx-surface-sunken);
}
.rx-fh-btn:focus-visible { outline: none; border-color: var(--rx-ink); box-shadow: var(--rx-focus-ring); }
.rx-pop.rx-fh-pop { padding: 12px 14px; font-size: var(--rx-text); line-height: 1.5; color: var(--rx-ink); }
.rx-fh-pop:focus { outline: none; }
.rx-fh-title { font-weight: 600; margin-bottom: 6px; }
.rx-fh-pop p { margin: 0 0 8px; }
.rx-fh-pop p:last-child { margin-bottom: 0; }
.rx-fh-k { display: block; font-size: var(--rx-text-xs); font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--rx-ink-subtle); }
.rx-fh-eg { display: block; padding: 6px 8px; background: var(--rx-surface-sunken);
  border-radius: var(--rx-radius-sm); color: var(--rx-ink-muted); }

/* Field group (D25) — several fields under ONE heading inside a form, when the
   fields belong to different KINDS of thing and the reader must not confuse
   them (PageCast: settings for every device vs desk phones only vs amplifiers
   only). A bare `.rx-section-title` above `.rx-field`s is not enough: a field
   label is also bold-ish text on its own line, so the heading reads as another
   label and the grouping disappears (Pierre, 22 Aug 2026: "the separation is
   not clear enough"). The group therefore gets a real container — an inset
   surface with a hairline — so membership is visible, not implied. */
.rx-field-group {
  border: 1px solid var(--rx-border); border-radius: var(--rx-radius-lg);
  background: var(--rx-surface-sunken); padding: 14px 16px 16px; margin-bottom: 18px;
}
.rx-field-group:last-child { margin-bottom: 0; }
.rx-field-group > .rx-field-group-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  margin: 0 0 14px; padding-bottom: 10px; border-bottom: 1px solid var(--rx-border);
}
.rx-field-group > .rx-field-group-head .rx-field-group-title {
  font-size: var(--rx-text-sm); font-weight: 700; letter-spacing: .4px;
  text-transform: uppercase; color: var(--rx-ink);
}
.rx-field-group > .rx-field-group-head .rx-field-group-sub {
  font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); font-weight: 400;
}
/* The fields inside sit on the inset, so their controls keep the card surface
   to stay legible against it. */
.rx-field-group .rx-field input:not([type="checkbox"]):not([type="radio"]),
.rx-field-group .rx-field textarea,
.rx-field-group .rx-field select { background: var(--rx-surface-card); }
@media (max-width: 560px) { .rx-field-row { grid-template-columns: 1fr; } }

/* Native select arrow removed above — supply our own. */
.rx-field select {
  background-image: url("brand/select-arrow.svg");
  background-repeat: no-repeat; background-position: right 14px center; padding-right: 36px;
}

/* Save status — one element, three states, beside the button.
   Never a toast: put it where the user is already looking. */
.rx-save-row    { display: flex; align-items: center; gap: 12px; margin-top: 24px; }
.rx-save-status { font-size: var(--rx-text-sm); color: var(--rx-ink-subtle); }
.rx-save-status.ok  { color: var(--rx-success-strong); }
.rx-save-status.err { color: var(--rx-danger); }


/* ══ 4 · SWITCHES ═════════════════════════════════════════════════════════ */
/* ONE canonical toggle. The source app had three (38×22 ink-filled, 40×22
   ink-filled, 38×22 green-filled) — this is the green one, because "on" reading
   as green is a stronger signal than "on" reading as dark. */

.rx-switch { position: relative; display: inline-block; width: 38px; height: 22px; flex-shrink: 0; vertical-align: middle; }
.rx-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.rx-switch-slider {
  position: absolute; inset: 0; background: var(--rx-switch-off);
  border-radius: var(--rx-radius-pill); cursor: pointer; transition: background .2s;
}
.rx-switch-slider::before {
  content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px;
  background: var(--rx-surface-card); border-radius: 50%;
  transition: transform .2s; box-shadow: 0 1px 3px rgba(0,0,0,.25);
}
.rx-switch input:checked + .rx-switch-slider { background: var(--rx-success); }
.rx-switch input:checked + .rx-switch-slider::before { transform: translateX(16px); }
.rx-switch input:focus-visible + .rx-switch-slider { box-shadow: var(--rx-focus-ring); }

/* DISABLED (PACK-011, v0.56.0). A disabled switch must LOOK disabled, on or off: the same
   .4 opacity and not-allowed cursor as .rx-btn:disabled, and a muted track (--rx-ink-subtle,
   the "disabled" token) in BOTH states, so a locked-on switch never reads as a live green
   one - the knob position still says on or off. Three ways in: the input is :disabled (the
   browser also stops the toggle), or aria-disabled="true" on the input or on the .rx-switch
   (look only: the pack cannot stop a click that aria-disabled leaves live; the host must).
   No :has() - the markup is label > input + slider, so sibling selectors reach it. Written
   after the :checked rule at equal specificity so it wins. A disabled switch has no hover
   change (there is none to remove) and no focus ring. */
.rx-switch input:disabled + .rx-switch-slider,
.rx-switch input[aria-disabled="true"] + .rx-switch-slider,
.rx-switch[aria-disabled="true"] input + .rx-switch-slider {
  background: var(--rx-ink-subtle); opacity: .4; cursor: not-allowed;
}
.rx-switch input:disabled:focus-visible + .rx-switch-slider,
.rx-switch input[aria-disabled="true"]:focus-visible + .rx-switch-slider,
.rx-switch[aria-disabled="true"] input:focus-visible + .rx-switch-slider { box-shadow: none; }

/* Label + description on the left, switch on the right. */
.rx-switch-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0; }
.rx-switch-row:not(:last-child) { border-bottom: 1px solid var(--rx-surface-sunken); }
.rx-switch-label { font-size: var(--rx-text-md); font-weight: 500; color: var(--rx-ink); }
.rx-switch-desc  { font-size: calc(12.5px * var(--rx-scale)); color: var(--rx-ink-subtle); margin-top: 2px; max-width: 460px; }


/* ══ 5 · SEGMENTED CONTROL ════════════════════════════════════════════════ */
/* Two-or-three-way mode selector. Each option carries a title AND a one-line
   explanation — a mode the user cannot distinguish is a mode they will pick
   wrong. */

.rx-segmented { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; }
.rx-segment {
  display: flex; align-items: center; gap: 10px; padding: 12px 14px;
  border: 1.5px solid var(--rx-border); border-radius: var(--rx-radius);
  background: var(--rx-surface-card); cursor: pointer; font-family: inherit; text-align: left;
  transition: border-color .15s, background .15s;
}
.rx-segment:hover { border-color: var(--rx-ink-subtle); }
.rx-segment.on    { border-color: var(--rx-ink); background: var(--rx-surface-sunken); }
.rx-segment-icon  { font-size: var(--rx-title-sm); flex-shrink: 0; }
.rx-segment strong { display: block; font-size: var(--rx-text); color: var(--rx-ink); }
.rx-segment small  { display: block; font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); margin-top: 2px; font-weight: 400; }


/* ══ 6 · INFO BALLOON ═════════════════════════════════════════════════════ */
/* The grey explanatory block that sits under a control and changes with the
   selection. Use it wherever a choice has consequences the user cannot infer
   from the label — it is the cheapest way to prevent a wrong choice. */

.rx-balloon {
  margin-top: 8px; padding: 10px 14px;
  background: var(--rx-surface-sunken); border-radius: var(--rx-radius);
  font-size: var(--rx-text-sm); color: var(--rx-ink-muted); line-height: 1.6;
}
.rx-balloon strong { color: var(--rx-ink); }

.rx-balloon-info   { background: var(--rx-surface-sunken); border-left: 3px solid var(--rx-info); }
.rx-balloon-warn   { background: var(--rx-surface-sunken); border-left: 3px solid var(--rx-warn); }
.rx-balloon-danger { background: var(--rx-danger-soft);    border-left: 3px solid var(--rx-danger); }


/* ══ 7 · CHIPS & TAGS ═════════════════════════════════════════════════════ */

/* Filter chip — selection is inversion. */
.rx-chip {
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-pill); padding: 5px 14px;
  font-family: inherit; font-size: var(--rx-text-sm); font-weight: 500;
  color: var(--rx-ink-muted); cursor: pointer; transition: all .15s;
}
.rx-chip:hover { border-color: var(--rx-ink-subtle); color: var(--rx-ink); }
.rx-chip.on    { background: var(--rx-ink); color: var(--rx-on-ink); border-color: var(--rx-ink); }
.rx-chip-group { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.rx-chip-group-label {
  font-size: var(--rx-text-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: .5px; color: var(--rx-ink-subtle); margin-right: 4px;
}

/* Tag input — a bordered box that behaves like one field but holds many
   values (recipients, domains, extensions). Click anywhere to focus. */
.rx-tags {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  padding: 8px 10px; min-height: 42px; cursor: text;
  background: var(--rx-surface-card);
  border: 1px solid var(--rx-border); border-radius: var(--rx-radius);
  transition: border-color .15s, box-shadow .15s;
}
.rx-tags:focus-within { border-color: var(--rx-ink); box-shadow: var(--rx-focus-ring); }
.rx-tags input {
  border: none; outline: none; background: transparent; flex: 1; min-width: 140px;
  padding: 2px 4px; font-family: inherit; font-size: var(--rx-text-md); color: var(--rx-ink);
}
.rx-tag {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--rx-surface-sunken); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-pill); padding: 3px 8px 3px 11px;
  font-size: var(--rx-text-sm); font-weight: 500; color: var(--rx-ink); white-space: nowrap;
}
.rx-tag-x { background: none; border: none; cursor: pointer; color: var(--rx-ink-subtle); font-size: var(--rx-text-md); line-height: 1; padding: 0; }
.rx-tag-x:hover { color: var(--rx-danger); }
.rx-tag.invalid { border-color: var(--rx-danger); color: var(--rx-danger); }

/* Preset blocks — pick-one from a small fixed set (5 / 10 / 20 / Unlimited). */
.rx-presets { display: flex; gap: 8px; flex-wrap: wrap; }
.rx-preset {
  flex: 1; min-width: 64px; padding: 10px 12px; text-align: center;
  font-family: inherit; font-size: var(--rx-text); font-weight: 600;
  background: var(--rx-surface-card); color: var(--rx-ink-muted);
  border: 1.5px solid var(--rx-border); border-radius: var(--rx-radius-sm);
  cursor: pointer; transition: all .12s;
}
.rx-preset:hover { border-color: var(--rx-ink-subtle); color: var(--rx-ink); }
.rx-preset.on    { background: var(--rx-ink); color: var(--rx-on-ink); border-color: var(--rx-ink); }


/* ══ 8 · STATS ════════════════════════════════════════════════════════════ */

.rx-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
.rx-stat {
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius); padding: 16px 18px;
}
.rx-stat-value { font-size: var(--rx-title-lg); font-weight: 700; line-height: 1; color: var(--rx-ink); }
.rx-stat-label { font-size: calc(12.5px * var(--rx-scale)); color: var(--rx-ink-subtle); margin-top: 6px; }
.rx-stat-sub   { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); margin-top: 3px; }
.rx-stat-value.good { color: var(--rx-success-strong); }
.rx-stat-value.warn { color: var(--rx-warn); }
.rx-stat-value.bad  { color: var(--rx-danger); }

/* Compact inline variant, for a filter bar or a header. */
.rx-stat-pills { display: flex; gap: 16px; flex-wrap: wrap; }
.rx-stat-pill {
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-sm); padding: 10px 16px;
  display: flex; align-items: center; gap: 8px; font-size: var(--rx-text);
}
.rx-stat-pill .rx-stat-value { font-size: var(--rx-title-sm); }
.rx-stat-pill .rx-stat-label { margin-top: 0; color: var(--rx-ink-muted); }


/* ══ 9 · BADGES & PILLS ═══════════════════════════════════════════════════ */

.rx-badge {
  display: inline-flex; align-items: center; padding: 2px 8px;
  border-radius: 4px; font-size: var(--rx-text-xs); font-weight: 600; white-space: nowrap;
}
.rx-badge-mono { font-family: var(--rx-mono); }

.rx-badge-neutral { background: var(--rx-surface-sunken); color: var(--rx-ink-muted); }
.rx-badge-active  { background: var(--rx-tint-green);  color: var(--rx-tint-green-ink); }
.rx-badge-info    { background: var(--rx-tint-blue);   color: var(--rx-tint-blue-ink); }
.rx-badge-purple  { background: var(--rx-tint-purple); color: var(--rx-tint-purple-ink); }
.rx-badge-warn    { background: var(--rx-tint-orange); color: var(--rx-tint-orange-ink); }
.rx-badge-pending { background: var(--rx-tint-amber);  color: var(--rx-tint-amber-ink); }
.rx-badge-danger  { background: var(--rx-danger-soft); color: var(--rx-danger-strong); }

/* Role / provenance pill — a quieter, outlined variant for metadata. */
.rx-pill {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--rx-surface-sunken); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-pill); padding: 3px 10px;
  font-size: calc(13.6px * var(--rx-scale)); color: var(--rx-ink);
}
.rx-pill-x { background: none; border: none; cursor: pointer; color: var(--rx-ink-muted); font-size: calc(15px * var(--rx-scale)); line-height: 1; padding: 0; }
.rx-pill-x:hover { color: var(--rx-danger); }


/* ══ 10 · TABLES ══════════════════════════════════════════════════════════ */
/* Grid-based list table, for rows that expand. (For a plain sortable data
   table use the rx-table engine instead — it does sort/paginate/resize/CSV.) */

.rx-table-wrap {
  border: 1px solid var(--rx-border); border-radius: var(--rx-radius);
  overflow: hidden; background: var(--rx-surface-card);
}
.rx-table-head {
  background: var(--rx-surface-sunken); font-weight: 600;
  color: var(--rx-ink-subtle); text-transform: uppercase;
  font-size: var(--rx-text-xs); letter-spacing: .4px;
}
.rx-table-head, .rx-table-row { display: grid; gap: 10px; padding: 10px 14px; align-items: center; }
.rx-table-row { border-top: 1px solid var(--rx-border); font-size: var(--rx-text); }
.rx-table-row:hover { background: var(--rx-surface-sunken); }
.rx-table-row.selected { background: var(--rx-surface-sunken); }

/* Sticky header for long lists. */
.rx-table-head.sticky { position: sticky; top: var(--rx-header-h); z-index: var(--rx-z-sticky); }


/* ══ 11 · EXPANDABLE ROWS ═════════════════════════════════════════════════ */

.rx-exp-row { border-top: 1px solid var(--rx-border); }
.rx-exp-main { display: grid; gap: 10px; padding: 12px 14px; align-items: center; cursor: pointer; }
.rx-exp-main:hover { background: var(--rx-surface-sunken); }
.rx-exp-row.open > .rx-exp-main { background: var(--rx-surface-sunken); }
.rx-exp-chevron { color: var(--rx-ink-subtle); transition: transform .18s; display: inline-block; }
.rx-exp-row.open .rx-exp-chevron { transform: rotate(90deg); }
.rx-exp-body { display: none; padding: 14px 18px 18px; background: var(--rx-surface-sunken); }
.rx-exp-row.open .rx-exp-body { display: block; }
.rx-exp-title {
  font-size: var(--rx-text-xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--rx-ink-subtle); margin: 0 0 8px;
}
/* Left edge marks a row whose children matched the current filter — filtering
   a nested list without showing WHICH child matched is disorienting. */
.rx-exp-row.matched > .rx-exp-main { border-left: 3px solid var(--rx-info); }


/* ══ 12 · EVENT / LOG LIST — defined in engines/rx-events.js ══════════════ */
/* The flat "here is a list of things that happened, or that need doing" object
   — an audit log, an activity feed, a dashboard "needs attention" card. Its
   rules are NOT here on purpose, and this note exists so nobody adds a second
   copy (D24 §2: never both).

   It moved on 28 Aug 2026, when FileDrop asked for the object for its Dashboard
   attention feed. Up to then every caller hand-wrote the markup, which is what
   put the rules here; now an ENGINE builds the row — and it has to, because the
   row is an <a> when it navigates and a <button> when it acts, and each needs a
   reset that every caller was writing for itself. D24 §1: who builds the markup
   owns the CSS. The deciding evidence is the same as the stepper's: FileDrop
   links tokens.css + compat/filedrop.css and NOT this file on any of its 15
   pages, so a section here would have shipped an unstyled feed to the caller
   that asked for the object.

   Every value in the engine sheet is a var(--rx-*) with that token's own value
   as the fallback. The pill inside a row carries a SCOPED copy of §9's badge
   rules (D24 §5), values identical, so a page that HAS this sheet sees no
   difference. Classes: .rx-events / .rx-event (.active .is-warn .is-danger) /
   -icon / -body / -title / -meta / -time / .rx-events-empty (-icon -title -sub)
   / .rx-events-more. Catalogue: "Data display".

   CALLERS: any page loading engines/rx-admin.js must now load
   engines/rx-events.js BEFORE it — rx-admin renders .rx-event rows for its
   audit log and no longer finds the rules here. It says so in the console. */


/* ══ 13 · EMPTY / LOADING / ERROR ═════════════════════════════════════════ */
/* Same geometry, different icon and copy — so the page never jumps between
   states. (rx-table.js has these built in.) */

.rx-state {
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius); padding: 48px 24px; text-align: center;
}
.rx-state-icon  { font-size: calc(36px * var(--rx-scale)); margin-bottom: 12px; }
.rx-state-title { font-size: var(--rx-text-lg); font-weight: 600; margin-bottom: 6px; color: var(--rx-ink); }
.rx-state-sub   { font-size: var(--rx-text); color: var(--rx-ink-muted); }
.rx-state-detail { margin-top: 8px; }   /* RX.noRole's optional `detail` line (PACK-048) */

.rx-spinner {
  width: 24px; height: 24px; margin: 0 auto 12px;
  border: 2.5px solid var(--rx-border); border-top-color: var(--rx-ink);
  border-radius: 50%; animation: rxspin .7s linear infinite;
}
.rx-spinner-lg { width: 40px; height: 40px; border-width: 3px; }
@keyframes rxspin { to { transform: rotate(360deg); } }


/* ══ 14 · PAGER ═══════════════════════════════════════════════════════════ */

.rx-pager { display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px 0 4px; }
.rx-pager-btn {
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-sm); padding: 6px 14px;
  font-family: inherit; font-size: var(--rx-text); color: var(--rx-ink); cursor: pointer;
}
.rx-pager-btn:hover:not(:disabled) { background: var(--rx-surface-sunken); }
.rx-pager-btn:disabled { opacity: .4; cursor: not-allowed; }
.rx-pager-info { font-size: var(--rx-text); color: var(--rx-ink-subtle); }


/* ══ 15 · OVERLAYS & PANELS ═══════════════════════════════════════════════ */

/* Centred modal. */
.rx-backdrop {
  position: fixed; inset: 0; z-index: var(--rx-z-modal);
  background: var(--rx-scrim);
  display: none; align-items: center; justify-content: center; padding: 20px;
}
.rx-backdrop.open { display: flex; }
.rx-dialog {
  background: var(--rx-surface-card); border-radius: var(--rx-radius-lg);
  box-shadow: var(--rx-shadow-modal); max-width: 520px; width: 100%;
  max-height: 90vh; display: flex; flex-direction: column; overflow: hidden;
}
.rx-dialog-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 20px 24px 16px; border-bottom: 1px solid var(--rx-border); flex: 0 0 auto;
}
.rx-dialog-title { font-size: var(--rx-text-lg); font-weight: 700; }
.rx-dialog-sub   { font-size: var(--rx-text-sm); color: var(--rx-ink-muted); margin-top: 2px; }
.rx-dialog-body  { padding: 18px 24px; overflow-y: auto; flex: 1 1 auto; }
.rx-dialog-foot  {
  padding: 14px 24px 20px; border-top: 1px solid var(--rx-border);
  display: flex; justify-content: flex-end; gap: 10px; flex: 0 0 auto;
}
.rx-dialog-x {
  width: 32px; height: 32px; border-radius: var(--rx-radius-sm);
  border: 1.5px solid var(--rx-border); background: none; cursor: pointer;
  font-size: var(--rx-text-lg); color: var(--rx-ink-muted); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.rx-dialog-x:hover { border-color: var(--rx-ink); color: var(--rx-ink); }

/* Slide-in detail panel — the list-plus-detail pattern. The overlay is a
   SEPARATE element that cross-fades, so it never intercepts clicks while
   hidden. */
.rx-panel-overlay {
  position: fixed; inset: 0; z-index: var(--rx-z-modal);
  background: var(--rx-scrim-soft); opacity: 0; pointer-events: none; transition: opacity .2s;
}
.rx-panel-overlay.open { opacity: 1; pointer-events: all; }
.rx-panel {
  position: fixed; top: 0; right: 0; bottom: 0; width: 420px; max-width: 100vw;
  background: var(--rx-surface-card); border-left: 1px solid var(--rx-border);
  box-shadow: -4px 0 24px rgba(0,0,0,.08);
  transform: translateX(100%); transition: transform .22s cubic-bezier(.4,0,.2,1);
  display: flex; flex-direction: column; z-index: calc(var(--rx-z-modal) + 1);
}
.rx-panel.open { transform: translateX(0); }
.rx-panel-wide { width: 560px; }
.rx-panel-head { display: flex; align-items: center; justify-content: space-between; padding: 20px 20px 16px; border-bottom: 1px solid var(--rx-border); flex-shrink: 0; }
.rx-panel-body { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.rx-panel-foot { padding: 16px 20px; border-top: 1px solid var(--rx-border); flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; }

/* Key/value rows inside a detail panel. mono for identifiers and timestamps. */
.rx-kv        { display: flex; flex-direction: column; gap: 2px; }
.rx-kv-label  { font-size: var(--rx-text-xs); color: var(--rx-ink-muted); }
.rx-kv-value  { font-size: var(--rx-text); font-weight: 500; word-break: break-all; }
.rx-kv-value.mono { font-family: var(--rx-mono); }

/* Edge tab that opens a drawer — a persistent affordance that costs no
   layout space. */
.rx-drawer-tab {
  position: fixed; right: 0; top: 50%; transform: translateY(-50%);
  z-index: calc(var(--rx-z-header) - 1);
  background: var(--rx-ink); color: var(--rx-on-ink);
  border: none; border-radius: var(--rx-radius) 0 0 var(--rx-radius);
  padding: 14px 8px; cursor: pointer; font-family: inherit; font-size: var(--rx-text-sm);
  font-weight: 600; writing-mode: vertical-rl; display: flex; align-items: center; gap: 8px;
}
.rx-drawer-tab:hover { background: var(--rx-ink-hover); }


/* ══ 16 · TABS ════════════════════════════════════════════════════════════ */

.rx-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--rx-border); margin-bottom: 28px; overflow-x: auto; }
.rx-tab {
  padding: 8px 16px; font-family: inherit; font-size: var(--rx-text); font-weight: 500;
  background: none; border: none; border-bottom: 2px solid transparent;
  color: var(--rx-ink-subtle); cursor: pointer; margin-bottom: -1px;
  transition: color .15s; white-space: nowrap;
}
.rx-tab:hover  { color: var(--rx-ink-muted); }
.rx-tab.active { color: var(--rx-ink); border-bottom-color: var(--rx-ink); }
.rx-tab-panel { display: none; }
.rx-tab-panel.active { display: block; }

/* RX.tabs on its side (v0.65.0, D87; lifted from ServicePortal's report menu, SP-129).
   The frame is the caller's wrapper: the menu column, then the panels. Tokens only. */
.rx-tabs-frame { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 28px; align-items: start; }
.rx-tabs-menu { position: sticky; top: calc(var(--rx-header-h) + 16px);
                max-height: calc(100vh - var(--rx-header-h) - 32px); overflow-y: auto; }
.rx-tabs-find { margin-bottom: 12px; }
.rx-tabs-find input { width: 100%; box-sizing: border-box; padding: 8px 10px;
                      font-family: inherit; font-size: var(--rx-text-sm);
                      color: var(--rx-ink); background: var(--rx-surface-card);
                      border: 1px solid var(--rx-border); border-radius: var(--rx-radius-sm); }
.rx-tabs-find input:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--rx-accent-ui); }
.rx-tabs.rx-tabs-side { flex-direction: column; gap: 2px; border-bottom: 0; margin-bottom: 0; overflow: visible; }
.rx-tabs-side .rx-tab { text-align: left; white-space: normal; margin-bottom: 0;
                        padding: 7px 12px; border-bottom: 0; border-left: 3px solid transparent;
                        font-size: var(--rx-text-sm); border-radius: var(--rx-radius-sm); }
.rx-tabs-side .rx-tab:hover { background: var(--rx-surface-sunken); }
.rx-tabs-side .rx-tab.active { border-left-color: var(--rx-ink); background: var(--rx-surface-sunken); }
.rx-tabs-side .rx-tab:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--rx-accent-ui); }
.rx-tabs-group { font-size: var(--rx-text-xs); font-weight: 600; color: var(--rx-ink-subtle);
                 text-transform: uppercase; letter-spacing: .04em; padding: 12px 12px 4px; }
.rx-tabs-group:first-child { padding-top: 0; }
.rx-tabs-nomatch { font-size: var(--rx-text-sm); color: var(--rx-ink-subtle); padding: 8px 12px; }
/* On a phone the menu sits ABOVE the panels and runs along one scrolling row. */
@media (max-width: 720px) {
  .rx-tabs-frame { grid-template-columns: minmax(0, 1fr); gap: 16px; }
  .rx-tabs-menu { position: static; max-height: none; overflow: visible; }
  .rx-tabs.rx-tabs-side { flex-direction: row; flex-wrap: nowrap; align-items: center; overflow-x: auto; }
  .rx-tabs-side .rx-tab { white-space: nowrap; border-left: 0; border-bottom: 2px solid transparent; }
  .rx-tabs-side .rx-tab.active { border-bottom-color: var(--rx-ink); }
  .rx-tabs-group { white-space: nowrap; padding: 0 4px 0 12px; }
  .rx-tabs-group:first-child { padding-left: 0; }
}

/* RX.drafts (v0.65.0, D88; lifted from ServicePortal's drafts box, SP-127): the "unsent" flag and the Restored line. */
.rx-draft-flag { white-space: nowrap; }
.rx-draft-restored { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px;
                     font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); margin: 6px 0; }


/* ══ 17 · COPY-TO-CLIPBOARD ═══════════════════════════════════════════════ */

.rx-copy-box {
  display: flex; align-items: center; gap: 8px;
  background: var(--rx-surface-page); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius); padding: 10px 14px;
}
.rx-copy-text {
  flex: 1; font-family: var(--rx-mono); font-size: var(--rx-text);
  color: var(--rx-ink); word-break: break-all; text-align: left;
}
.rx-copy-btn {
  background: var(--rx-ink); color: var(--rx-on-ink); border: none;
  border-radius: var(--rx-radius-sm); padding: 7px 14px;
  font-family: inherit; font-size: var(--rx-text-sm); font-weight: 600;
  cursor: pointer; white-space: nowrap;
}
.rx-copy-btn:hover { background: var(--rx-ink-hover); }
.rx-copy-btn.copied { background: var(--rx-success); }


/* ══ 18 · SUCCESS CARD ════════════════════════════════════════════════════ */

.rx-success {
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-lg); box-shadow: var(--rx-shadow);
  padding: 40px 32px; text-align: center;
}
.rx-success-icon  { font-size: var(--rx-display); margin-bottom: 16px; }
.rx-success-title { font-size: var(--rx-title); font-weight: 700; margin-bottom: 8px; }
.rx-success-sub   { font-size: var(--rx-text-md); color: var(--rx-ink-muted); margin-bottom: 28px; }


/* ══ 19 · PIN & OTP ENTRY ═════════════════════════════════════════════════ */
/* One box per digit. Reads as a code rather than a password, and gives the
   user a visible sense of how many characters are expected. */

.rx-pin { display: flex; gap: 10px; justify-content: center; }
.rx-pin-digit {
  width: 46px; height: 56px; text-align: center;
  font-family: var(--rx-mono); font-size: calc(24px * var(--rx-scale)); font-weight: 500;
  color: var(--rx-ink); background: var(--rx-surface-card);
  border: 1.5px solid var(--rx-border); border-radius: var(--rx-radius);
  outline: none; transition: border-color .15s, box-shadow .15s;
  -moz-appearance: textfield;
}
.rx-pin-digit::-webkit-outer-spin-button,
.rx-pin-digit::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.rx-pin-digit:focus  { border-color: var(--rx-ink); box-shadow: var(--rx-focus-ring); }
.rx-pin-digit.filled { border-color: var(--rx-ink); }
.rx-pin.error .rx-pin-digit { border-color: var(--rx-danger); }
@media (max-width: 420px) {
  .rx-pin { gap: 6px; }
  .rx-pin-digit { width: 38px; height: 48px; font-size: calc(20px * var(--rx-scale)); }
}


/* ══ 20 · RECIPIENT-PAGE BITS ═════════════════════════════════════════════ */
/* For public pages that load no shell. Deliberately minimal. */

.rx-step { display: none; }
.rx-step.active { display: block; }

.rx-msg { padding: 12px 14px; border-radius: var(--rx-radius); font-size: var(--rx-text); line-height: 1.55; }
.rx-msg-info   { background: var(--rx-surface-sunken); color: var(--rx-ink-muted); border-left: 3px solid var(--rx-info); }
.rx-msg-error  { background: var(--rx-danger-soft); color: var(--rx-danger-strong); border-left: 3px solid var(--rx-danger); }
.rx-msg-note   { background: var(--rx-surface-sunken); color: var(--rx-ink); border-left: 3px solid var(--rx-accent-ui); }

/* File row — one file with icon, name, size and an action. */
.rx-file-row {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px;
  background: var(--rx-surface-page); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-sm);
}
.rx-file-icon { font-size: var(--rx-title-sm); flex-shrink: 0; }
.rx-file-info { flex: 1; min-width: 0; }
.rx-file-name { font-size: var(--rx-text); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rx-file-size { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); }
/* A person's name with the quiet "left" marker (RX.nameHtml, rx-names.js, D84). */
.rx-name-left { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); }

/* Agreement box — a scrollable terms block with a checkbox gate. */
.rx-agree {
  border: 1px solid var(--rx-border); border-radius: var(--rx-radius);
  padding: 14px 16px; background: var(--rx-surface-page);
  max-height: 180px; overflow-y: auto; font-size: calc(12.5px * var(--rx-scale));
  color: var(--rx-ink-muted); line-height: 1.6;
}
.rx-agree-check { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; margin-top: 12px; }
.rx-agree-check input { margin-top: 2px; width: 16px; height: 16px; flex-shrink: 0; accent-color: var(--rx-ink); cursor: pointer; }
.rx-agree-check span { font-size: var(--rx-text); color: var(--rx-ink); line-height: 1.5; }


/* ══ 21 · DASHBOARD CARDS ═════════════════════════════════════════════════ */
/* Chrome for a movable/resizable widget. The rx-dashboard engine supplies the
   markup; this is what it looks like. */

.rx-dash-bar  { display: flex; justify-content: flex-end; gap: 8px; margin-bottom: 10px; }
.rx-dash-card {
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  border-radius: var(--rx-radius-lg); box-shadow: var(--rx-shadow-sm);
  height: 100%; display: flex; flex-direction: column; overflow: hidden;
}
.rx-dash-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 11px 16px; border-bottom: 1px solid var(--rx-border);
  cursor: move; flex: none;              /* drag handle — body text stays selectable */
}
.rx-dash-title { font-size: var(--rx-text-md); font-weight: 600; color: var(--rx-ink); }
.rx-dash-grip  { color: var(--rx-ink-subtle); font-size: var(--rx-text-md); letter-spacing: -2px; user-select: none; }
.rx-dash-body  { padding: 16px; flex: 1; overflow: auto; }
/* fill:true (rx-dashboard.js, v0.23.0) — the body becomes a full-height flex
   column instead of a plain padded block, so content the card's render()
   mounts into it (e.g. RX.calendar's timeline, timelineMaxHeight:'fill')
   can stretch to the grid item's real height, and keeps stretching when a
   drag makes the widget taller or shorter — no resize listener anywhere,
   it is the same CSS flex chain GridStack's own item height already drives.
   A card without `fill` is unaffected: this rule only fires with the class. */
.rx-dash-body.rx-dash-fill { display: flex; flex-direction: column; min-height: 0; padding: 0; }
/* Auto height (rx-dashboard.js, v0.45.0, D71) — the body of a card that sizes itself to
   its content is as tall as the content, whatever height the grid item has, so the engine can
   measure it (and so a widget can shrink as well as grow). The engine sets max-height on it
   at the cap, where the inner scroll returns (overflow:auto above). fill:true cards are not
   auto unless they say so; this rule only fires with the class. */
.rx-dash-card.rx-dash-auto .rx-dash-body { flex: 0 0 auto; }
/* Collapse (rx-dashboard.js, v0.61.0, D81) — the header's own button (a real <button>, so Enter and
   Space work and aria-expanded is honest), and what a collapsed card looks like. The engine sets the
   grid item to ONE row; the card itself stops filling the item and shrinks to its header, so no empty
   card shows under the title. The body is `hidden` in the markup (the pack's own [hidden] rule above
   always wins, so a fill:true card's display:flex cannot keep it open). */
.rx-dash-title { flex: 1 1 auto; }
.rx-dash-toggle {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  min-width: 28px; min-height: 28px; padding: 0 6px; margin-left: 6px;
  background: transparent; border: 1px solid transparent; border-radius: var(--rx-radius-sm);
  color: var(--rx-ink-muted); font: inherit; cursor: pointer;
}
.rx-dash-toggle:hover { background: var(--rx-surface-sunken); color: var(--rx-ink); }
.rx-dash-toggle:focus-visible { outline: none; border-color: var(--rx-ink); box-shadow: var(--rx-focus-ring); }
.rx-dash-toggle-icon { display: inline-block; line-height: 1; }
.rx-dash-card.is-collapsed .rx-dash-toggle-icon { transform: rotate(-90deg); }
.rx-dash-card.is-collapsed { height: auto; }
.rx-dash-card.is-collapsed > .rx-dash-head { border-bottom-color: transparent; }
@media print { .rx-dash-toggle { display: none; } }
/* Override GridStack's default so a popover opened in a card isn't clipped. */
.grid-stack-item-content { overflow: visible !important; }
/* PACK-035 (v0.52.0, was FileDrop FD-054): gridstack.min.css puts a card in motion at z-index 10000
   (= --rx-z-popover), which painted it OVER the open phone menu and every shell dropdown. A card being
   dragged or resized lives at the MENU layer: above its sibling cards, below the fixed header
   (--rx-z-header) and every shell menu (--rx-z-dropdown). The resize handles carry an inline
   z-index:100 set by the library (= the header's layer), so they are pulled down to the sticky
   layer: still above the card's own content, never over the header. !important is only for the
   handles (an inline style); the card rule wins on specificity (two classes against the vendor's
   one), so the load order of the two sheets does not matter. Never edit the vendored files. */
.grid-stack .ui-draggable-dragging,
.grid-stack .ui-resizable-resizing { z-index: var(--rx-z-menu); }
.grid-stack .ui-resizable-handle { z-index: var(--rx-z-sticky) !important; }
/* Fallback when the grid library is absent — still a usable page. */
.rx-dash-fallback { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }


/* ══ 22 · STEPPER — defined in engines/rx-stepper.js ══════════════════════ */
/* The guided "next › next › finish" object (rail, panels, foot, review list,
   More-options disclosure). Its rules are NOT here on purpose, and this note
   exists so nobody adds a second copy.

   The stepper is an ENGINE-rendered object, like rx-shell / rx-table / rx-ui /
   rx-help / rx-admin — every one of those injects its own stylesheet, and this
   one follows them. The deciding evidence (measured 22 Aug 2026, when FileDrop
   asked for the object): FileDrop links tokens.css + compat/filedrop.css and
   NOT components.css on any of its 15 pages, and its deploy does not even
   vendor this file — so a section here would have shipped an unstyled stepper
   to the object's own first caller.

   Every value in the engine sheet is a var(--rx-*) with that token's own value
   as the fallback, so it is token-complete either way; the foot's rx-btn rules
   are a scoped copy of §1's values, so a page that HAS this sheet sees no
   difference. Classes: .rx-stepper / -item (.is-current .is-done .is-error
   .is-review) / -num / -label / -progress / -bar / -panel / -head / -title /
   -lead / -body / -error / -more / -review / -group / -kv / -foot /
   -foot-spacer. Catalogue: "Flows". */


/* ══ 23 · ACTIVITY TIMELINE ═══════════════════════════════════════════════ */
/* Split out of §12 on 28 Aug 2026 when the event list became an engine. These
   two shapes were filed together because they answer related questions — what
   happened across everything, and what happened to THIS one thing — but they
   are different objects and only one of them moved. rx-activity.js renders
   these classes and still depends on this file; that is a known gap, recorded
   in CHANGELOG v0.9.3, not something this section pretends away. */

/* Vertical timeline — for a single item's history. */
.rx-timeline-item { position: relative; padding-left: 32px; padding-bottom: 18px; }
.rx-timeline-item:last-child { padding-bottom: 0; }
.rx-timeline-item::before {
  content: ''; position: absolute; left: 10px; top: 22px; bottom: 0;
  width: 2px; background: var(--rx-border);
}
.rx-timeline-item:last-child::before { display: none; }
.rx-timeline-dot {
  position: absolute; left: 0; top: 0; width: 22px; height: 22px;
  border-radius: 50%; background: var(--rx-surface-sunken);
  display: flex; align-items: center; justify-content: center; font-size: var(--rx-text-sm);
}
.rx-timeline-label { font-size: var(--rx-text); font-weight: 600; }
.rx-timeline-desc  { font-size: var(--rx-text-sm); color: var(--rx-ink-muted); line-height: 1.45; margin-top: 2px; }
.rx-timeline-time  { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); margin-top: 3px; }


/* ══ 24 · CHARTS — defined in engines/rx-charts.js ═════════════════════════ */
/* RX.charts — a stacked-bar chart and a small column chart, plain flex markup
   (no SVG), token-only. Lifted from LearnGuard's Reports > Overview,
   22 Sep 2026 (js/charts.js, LG.charts) — the pack, every engine and the
   suite library carried no chart object until now (the suite library's own
   GAP row). Every fill reuses the pack's own severity/state FILL tier
   (--rx-success / --rx-danger / --rx-warn-strong / --rx-info), the same
   five-tone vocabulary tokens.css already names for "fills, bars, dots,
   switches" — a badge is the quiet tint tier of that family, a chart segment
   is this one - and, since v0.60.0, --rx-magenta for the sixth series. No colour
   literal anywhere below. */

.rx-chart-card { margin-bottom: 18px; }
.rx-chart-rows { display: flex; flex-direction: column; gap: 14px; }
.rx-chart-row { display: flex; align-items: center; gap: 12px; }
.rx-chart-row-label { flex: 0 0 170px; font-size: var(--rx-text-sm); color: var(--rx-ink); overflow-wrap: anywhere; }
.rx-chart-row-total { flex: 0 0 34px; text-align: right; font-family: var(--rx-mono); font-size: var(--rx-text-sm); color: var(--rx-ink-muted); }
.rx-chart-track {
  flex: 1; display: flex; min-height: 24px; border-radius: var(--rx-radius-sm);
  overflow: hidden; background: var(--rx-surface-sunken); border: 1px solid var(--rx-border);
}
.rx-chart-seg {
  border: 0; padding: 0 4px; font-family: inherit; font-size: var(--rx-text-xs); font-weight: 600;
  display: flex; align-items: center; justify-content: center; cursor: pointer; min-width: 0;
}
.rx-chart-seg:focus-visible { outline: 2px solid var(--rx-ink); outline-offset: -2px; }
.rx-chart-track-empty { flex: 1; display: flex; align-items: center; justify-content: center; font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); }

/* The six series tones - a segment's `tone` selects one, defaulting to
   neutral. Generic tone names, not LearnGuard's own state words (done /
   overdue / paused / excused / not_started), because these segments have to
   colour ANY product's states - open/closed, pass/fail, done/overdue - not
   only a training record. Six is the whole palette (v0.60.0, PACK-036): the
   four state fills, magenta, and neutral. A seventh series must be given a
   tone by name; the engine and lint.js refuse to reuse one silently.
   Neutral carries a hairline edge: it is the track's own colour, so without
   one a neutral segment is invisible against the empty track. */
.rx-chart-seg--success { background: var(--rx-success);         color: var(--rx-on-success); }
.rx-chart-seg--neutral { background: var(--rx-surface-sunken);  color: var(--rx-ink-muted); box-shadow: inset 0 0 0 1px var(--rx-ink-subtle); }
.rx-chart-seg--magenta { background: var(--rx-magenta);         color: var(--rx-on-magenta); }
.rx-chart-seg--danger  { background: var(--rx-danger);          color: var(--rx-on-danger); }
.rx-chart-seg--warn    { background: var(--rx-warn-strong);     color: var(--rx-on-warn); }
.rx-chart-seg--info    { background: var(--rx-info);            color: var(--rx-on-info); }

.rx-chart-row-note { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); margin-left: 182px; }
.rx-chart-legend { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 12px; font-size: var(--rx-text-xs); color: var(--rx-ink-muted); }
.rx-chart-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.rx-chart-swatch { width: 11px; height: 11px; border-radius: var(--rx-radius-sm); display: inline-block; }
.rx-chart-cols { display: flex; align-items: flex-end; gap: 4px; height: 120px; }
.rx-chart-col-wrap { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; height: 100%; justify-content: flex-end; }
.rx-chart-col { width: 100%; min-height: 2px; background: var(--rx-info); border-radius: var(--rx-radius-sm); }
.rx-chart-col-label { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); white-space: nowrap; }

/* The accessible mirror — a real <table> carrying the same figures as the
   chart above, visually hidden but never removed from the accessibility
   tree, for anything (a screen reader, a reader who prefers a table) that
   wants the numbers rather than a row of buttons. */
.rx-chart-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* The hiding lives on the wrapper (.rx-chart-sr), not the <table>: a table ignores
   width/height/overflow/clip, so a wide one (six series = eight columns) made the
   whole page scroll sideways at phone width (found building v0.60.0). */
.rx-chart-table { border-collapse: collapse; }

@media (max-width: 360px) {
  .rx-chart-row { flex-direction: column; align-items: stretch; gap: 4px; }
  .rx-chart-row-label, .rx-chart-row-total { flex: none; text-align: left; }
  .rx-chart-row-note { margin-left: 0; }
}


/* ── Group label (D74) — RX.groupLabel: the name is primary, the opaque id is
   small and muted beneath it (inline: after it, on one line). Tokens only. */
.rx-glabel { display: block; min-width: 0; }
.rx-glabel-name { display: block; color: var(--rx-ink); overflow-wrap: anywhere; }
.rx-glabel-id { display: block; margin-top: 2px; font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); overflow-wrap: anywhere; }
.rx-glabel-inline, .rx-glabel-inline .rx-glabel-name, .rx-glabel-inline .rx-glabel-id { display: inline; }
.rx-glabel-inline .rx-glabel-id { margin: 0 0 0 4px; }

/* ============================================================================
   25. WORKFLOW CANVAS - RX.workflow (D75, v0.49.0)
   The React Flow island (D48), lifted from ServicePortal's workflow.html. React
   renders inside .rx-wf-island and nowhere else; everything around it is plain
   DOM. THE ISLAND'S LOOK IS SET HERE AND ONLY HERE (D48 condition 2): React Flow
   publishes every colour as a --xy-NAME custom property with a -default
   fallback, so each --xy-NAME is declared on the island's own element, which
   outranks the library's fallback. vendor/xyflow-react-*.css is never edited and
   must be linked BEFORE this sheet so these rules win the cascade. Tokens only,
   not one literal colour.
   ============================================================================ */
.rx-wf { display: flex; flex-direction: column; gap: var(--rx-gap); min-width: 0; }
.rx-wf-bar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between; }
.rx-wf-bar-left, .rx-wf-bar-right { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.rx-wf-title { font-size: var(--rx-text); font-weight: 600; color: var(--rx-ink); }
.rx-wf-note { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); max-width: 44ch; line-height: 1.45; }
.rx-wf-draft-chip {
  font-size: var(--rx-text-xs); font-weight: 600; padding: 2px 9px;
  border-radius: var(--rx-radius-pill); background: var(--rx-warn-soft);
  color: var(--rx-warn); border: 1px solid var(--rx-warn-border);
}
.rx-wf-draft-chip[hidden] { display: none; }

.rx-wf-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--rx-gap); align-items: start; }
.rx-wf-cols.has-palette { grid-template-columns: minmax(140px, 170px) minmax(0, 2.4fr) minmax(280px, 1fr); }
.rx-wf-cols.no-palette  { grid-template-columns: minmax(0, 2.2fr) minmax(300px, 1fr); }
@media (max-width: 900px) { .rx-wf-cols.has-palette, .rx-wf-cols.no-palette { grid-template-columns: minmax(0, 1fr); } }

.rx-wf-palette h3 { font-size: var(--rx-text-sm); font-weight: 600; margin: 14px 0 6px; color: var(--rx-ink); }
.rx-wf-palette h3:first-child { margin-top: 0; }
.rx-wf-pal-item {
  display: block; width: 100%; text-align: left; margin: 0 0 6px; padding: 7px 9px;
  border: 1px solid var(--rx-border); border-radius: var(--rx-radius);
  background: var(--rx-surface-card); color: var(--rx-ink-muted);
  font-family: inherit; font-size: var(--rx-text-sm); line-height: 1.4; cursor: pointer;
}
.rx-wf-pal-item:hover:not(:disabled) { border-color: var(--rx-ink-subtle); color: var(--rx-ink); }
.rx-wf-pal-item:disabled { cursor: default; }
.rx-wf-pal-prod { display: block; font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); }

/* THE ISLAND. React renders inside this element and nowhere else. */
.rx-wf-island {
  height: 70vh; min-height: 420px; padding: 0; overflow: hidden;
  background: var(--rx-surface-sunken);

  --xy-background-color: var(--rx-surface-sunken);
  --xy-background-pattern-color: var(--rx-border-strong);
  --xy-edge-stroke: var(--rx-ink-subtle);
  --xy-edge-stroke-selected: var(--rx-ink);
  --xy-edge-stroke-width: 1.6;
  --xy-edge-label-background-color: var(--rx-surface-card);
  --xy-edge-label-color: var(--rx-ink-muted);
  --xy-connectionline-stroke: var(--rx-ink-subtle);
  --xy-node-color: var(--rx-ink);
  --xy-node-background-color: var(--rx-surface-card);
  --xy-node-border: 1px solid var(--rx-border);
  --xy-node-border-radius: var(--rx-radius);
  --xy-node-boxshadow-hover: var(--rx-shadow-sm);
  --xy-node-boxshadow-selected: var(--rx-shadow);
  --xy-handle-background-color: var(--rx-ink-subtle);
  --xy-handle-border-color: var(--rx-surface-card);
  --xy-selection-background-color: var(--rx-tint-purple);
  --xy-selection-border: 1px dotted var(--rx-accent);
  --xy-controls-button-background-color: var(--rx-surface-card);
  --xy-controls-button-background-color-hover: var(--rx-surface-sunken);
  --xy-controls-button-color: var(--rx-ink-muted);
  --xy-controls-button-color-hover: var(--rx-ink);
  --xy-controls-button-border-color: var(--rx-border);
  --xy-controls-box-shadow: var(--rx-shadow-sm);
  --xy-minimap-background-color: var(--rx-surface-card);
  --xy-minimap-mask-background-color: var(--rx-scrim-soft);
  --xy-minimap-node-background-color: var(--rx-border-strong);
  --xy-attribution-background-color: var(--rx-surface-card);
}
.rx-wf-island .react-flow__attribution a { color: var(--rx-ink-subtle); }
/* The arrowheads: React Flow renders every marker with stroke:currentColor inline,
   so `color` here is what paints them (an inline style cannot be beaten by a class). */
.rx-wf-island .react-flow__marker { color: var(--rx-ink-subtle); }
.rx-wf-island .react-flow__edge.rx-wf-edge--requester .react-flow__edge-path { stroke: var(--rx-info); }
.rx-wf-island .react-flow__edge.rx-wf-edge--resolver  .react-flow__edge-path { stroke: var(--rx-accent); }
.rx-wf-island .react-flow__edge.rx-wf-edge--system    .react-flow__edge-path { stroke: var(--rx-ink-subtle); }
.rx-wf-island .react-flow__edge.rx-wf-edge--mixed     .react-flow__edge-path { stroke: var(--rx-ink-muted); }
.rx-wf-island .react-flow__edge.rx-wf-edge--auto      .react-flow__edge-path { stroke-dasharray: 6 4; }
.rx-wf-island .react-flow__edge.rx-wf-dim { opacity: .12; }
.rx-wf-island .react-flow__edge.rx-wf-edge--picked .react-flow__edge-path { stroke-width: 3; }
.rx-wf-island .react-flow__edge-text { font-family: var(--rx-font); font-size: var(--rx-text-xs); fill: var(--rx-ink-muted); }
.rx-wf-island .react-flow__edge-textbg { fill: var(--rx-surface-card); }

/* A status node */
.rx-wf-node {
  width: 196px; padding: 10px 12px; border-radius: var(--rx-radius);
  background: var(--rx-surface-card); border: 1px solid var(--rx-border);
  font-family: var(--rx-font); color: var(--rx-ink); line-height: 1.35;
}
.rx-wf-node--open    { border-left: 4px solid var(--rx-info); }
.rx-wf-node--closed  { border-left: 4px solid var(--rx-success); }
.rx-wf-node--end     { box-shadow: 0 0 0 3px var(--rx-border-strong); }
.rx-wf-node--error   { border-color: var(--rx-danger); }
.rx-wf-node--warn    { border-color: var(--rx-warn-strong); }
.rx-wf-node--current { box-shadow: 0 0 0 3px var(--rx-accent); }
/* A retired status is quiet but never faded with opacity (faded text on a card fails 4.5:1). */
.rx-wf-node--retired { border-style: dashed; background: var(--rx-surface-sunken); }
.rx-wf-node--retired .rx-wf-node-title { color: var(--rx-ink-muted); }
.rx-wf-node--dim { opacity: .16; }
.rx-wf-node-title { font-size: var(--rx-text-md); font-weight: 600; }
.rx-wf-node-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.rx-wf-tag {
  font-size: var(--rx-text-xs); padding: 1px 7px; border-radius: var(--rx-radius-pill);
  background: var(--rx-surface-sunken); color: var(--rx-ink-muted); border: 1px solid var(--rx-border);
}
.rx-wf-tag--open   { background: var(--rx-info-soft); color: var(--rx-info-strong); border-color: var(--rx-info-border); }
.rx-wf-tag--closed { background: var(--rx-success-soft); color: var(--rx-success-strong); border-color: var(--rx-success-border); }
.rx-wf-tag--pause  { background: var(--rx-warn-soft); color: var(--rx-warn); border-color: var(--rx-warn-border); }
.rx-wf-hub {
  padding: 8px 14px; border-radius: var(--rx-radius-pill);
  background: var(--rx-surface-sunken); border: 1px dashed var(--rx-border-strong);
  font-family: var(--rx-font); font-size: var(--rx-text-sm); color: var(--rx-ink-muted); white-space: nowrap;
}
.rx-wf-hub--dim { opacity: .16; }

/* The side panel (plain DOM, never React) */
.rx-wf-panel { position: sticky; top: calc(var(--rx-header-h) + 16px); min-width: 0; }
.rx-wf-panel h3 { font-size: var(--rx-text-md); font-weight: 600; margin: 16px 0 6px; color: var(--rx-ink); }
.rx-wf-panel h3:first-child { margin-top: 0; }
.rx-wf-panel p  { font-size: var(--rx-text); color: var(--rx-ink-muted); margin: 0 0 8px; line-height: 1.55; }
.rx-wf-panel ul { margin: 0 0 8px; padding-left: 18px; }
.rx-wf-panel li { font-size: var(--rx-text); color: var(--rx-ink-muted); line-height: 1.55; margin-bottom: 3px; }
.rx-wf-panel code { font-family: var(--rx-mono); font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); }
.rx-wf-ed .rx-field { margin-bottom: 10px; }
.rx-wf-ed-line { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; padding: 8px 0; border-top: 1px solid var(--rx-border); }
.rx-wf-ed-line > .rx-field { flex: 1 1 110px; margin-bottom: 0; }
.rx-wf-ed-checks { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0 0 10px; }
.rx-wf-ed-check { display: flex; align-items: center; gap: 6px; margin: 0; font-size: var(--rx-text); color: var(--rx-ink-muted); }
.rx-wf-ed-check input { width: auto; }
.rx-wf-ed-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.rx-wf-ed-hint { font-size: var(--rx-text-xs); color: var(--rx-ink-subtle); margin: 0 0 8px; line-height: 1.5; }

/* The checks list */
.rx-wf-check {
  display: flex; gap: 10px; align-items: flex-start; width: 100%; text-align: left;
  padding: 9px 10px; border: 1px solid var(--rx-border); border-radius: var(--rx-radius);
  background: var(--rx-surface-card); font-family: inherit; font-size: var(--rx-text);
  color: var(--rx-ink-muted); cursor: pointer; margin-bottom: 6px; line-height: 1.5;
}
.rx-wf-check:hover { border-color: var(--rx-ink-subtle); }
.rx-wf-sev { flex: none; font-size: var(--rx-text-xs); font-weight: 700; padding: 1px 8px; border-radius: var(--rx-radius-pill); }
.rx-wf-sev--error   { background: var(--rx-danger-soft); color: var(--rx-danger); border: 1px solid var(--rx-danger-border); }
.rx-wf-sev--warning { background: var(--rx-warn-soft); color: var(--rx-warn); border: 1px solid var(--rx-warn-border); }
.rx-wf-clean { font-size: var(--rx-text); color: var(--rx-success-strong); margin: 0; }
.rx-wf-missing { padding: 24px; font-size: var(--rx-text); color: var(--rx-ink-muted); }

/* ============================================================================
   26. UPDATE BANNER - RX.updateBanner (PACK-045, v0.78.0; lifted from PageCast PC-048)
   "<App> was updated - Reload". The engine draws this inside the mount it is given,
   in a role="status" region that is empty (no height) until a newer page is served.
   Info family tokens only; the buttons are the pack's own rx-btn.
   ============================================================================ */
.rx-update-banner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px;
  padding: 12px 14px; margin-bottom: 12px; border-radius: var(--rx-radius); background: var(--rx-info-soft);
  border: 1px solid var(--rx-info-border); border-left: 4px solid var(--rx-info); color: var(--rx-ink);
  font-size: var(--rx-text); line-height: 1.5; }
.rx-update-actions { display: flex; gap: 8px; flex: none; }

/* ============================================================================
   22. ATTRIBUTION - "a [Refocus X logo] product" (D67, v0.41.0)
   The static form for pages with no JS; RX.attribution() (rx-esc.js) renders the
   same markup and injects these same rules (selftest pins the two equal).
   <span class="rx-attribution">a <a href="https://refocusx.com" target="_blank" rel="noopener">
     <img class="rx-attr-light" src="/design-pack/brand/refocusx-wordmark.svg" alt="Refocus X">
     <img class="rx-attr-dark" src="/design-pack/brand/refocusx-wordmark-on-dark.svg" alt="Refocus X"></a> product</span>
   ============================================================================ */
.rx-attribution { --attr-l: inline-block; --attr-d: none; white-space: nowrap; }
.rx-attribution a { color: inherit; text-decoration: none; }
.rx-attribution a:hover { text-decoration: underline; }
.rx-attribution img { height: 1.05em; width: auto; vertical-align: -.2em; border: 0; }
.rx-attribution .rx-attr-light { display: var(--attr-l); }
.rx-attribution .rx-attr-dark { display: var(--attr-d); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .rx-attribution { --attr-l: none; --attr-d: inline-block; } }
[data-theme="dark"] .rx-attribution { --attr-l: none; --attr-d: inline-block; }
:root .rx-attribution.rx-attribution[data-surface="dark"] { --attr-l: none; --attr-d: inline-block; }
:root .rx-attribution.rx-attribution[data-surface="light"] { --attr-l: inline-block; --attr-d: none; }
