/* ==========================================================================
   Shared Edit-modal widgets — modal shell, field primitives, chip rows, the
   Tags/Assigned-Users typeahead, a row action menu, a fake Select, and a
   DatePicker (screens/static/screens/js/shared-widgets.js). Prefix: sw-*
   ("shared widgets"). Modal-shell/field/chip property values are copied
   from details.css's .det-* rules — Details' Edit Details modal is the
   named style reference those were extracted from, so Directory's Entity
   Edit modal (previously .dir-modal-* and .dir-chip*, its own
   hand-duplicated copy with a heavier shadow and a bordered-pill chip
   style) now matches it exactly. Select/DatePicker are grounded in
   SHARED_COMPONENT_LIBRARY_PROPOSAL.md's canonical specs and directory.css's
   real .dir-calendar respectively — see their section comments below.
   Built on the shared tokens (--vds-*) loaded globally by
   base.html/app_base.html, same as every other screen's CSS.
   ========================================================================== */

/* App-wide fallback for the "gate every Edit/Add button below ADMIN_LEVEL"
   convention (see is-admin-json in base.html and each screen's own IS_ADMIN
   read) -- most button classes already carry their own :disabled/[disabled]
   rule (e.g. details.css's .det-btn, action-station.css's .vds-btn-primary),
   which wins over this on specificity alone; this only fills the gap for
   whatever doesn't. Not applied to the handful of classes that deliberately
   render disabled at full opacity for legibility (e.g. details.css's
   .det-statusbtn:disabled) -- those already out-specificity this too. */
button[disabled] { opacity: .5; cursor: not-allowed; }

.sw-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(17, 24, 39, 0.35);
}
.sw-modal { position: relative; z-index: 301; }
.sw-modal {
  width: 640px;
  max-width: calc(100vw - 40px);
  max-height: calc(100vh - 80px);
  background: var(--vds-white);
  border-radius: 12px;
  box-shadow: var(--vds-shadow-lg);
  display: flex;
  flex-direction: column;
}
/* Compact variant for a short confirm dialog (Directory's delete-confirm —
   Details has no small-modal today; folded in here since it's the same
   shell either way, just less padding/no head-foot borders). */
.sw-modal--sm { width: 420px; max-width: calc(100vw - 48px); padding: 28px; gap: 0; }
.sw-modal--sm .sw-modal-title { font: 700 17px/1.3 var(--vds-font-sans); margin-bottom: 12px; }
.sw-modal--sm .sw-modal-foot { padding: 0; border-top: none; }

/* Drawer variant (Library's Share/Revisions side panels, #383) -- stays a
   right-anchored side panel at EVERY width, including desktop, unlike the
   default .sw-modal (centered dialog at desktop, slide-over only below
   1024px). This is the vision-responsive-design skill's "secondary panel"
   shape (persistent side panel at desktop, slide-over at tablet, full-
   screen at phone) rather than the "primary dialog" shape .sw-modal
   implements by default -- Share/Revisions are supplementary info next to
   a file, not a primary action a user came here to perform. Below 1024px
   this falls through UNCHANGED to the existing .sw-modal:not(.sw-modal--sm)
   tablet/phone rules further down (this selector still matches them), so
   only the desktop case needs a new rule here. */
@media (min-width: 1024px) {
  .sw-modal--drawer {
    position: fixed;
    top: 0; right: 0; bottom: 0; left: auto;
    width: 360px; max-width: 80%; height: auto; max-height: 100%;
    margin: 0;
    border-radius: var(--vds-radius-lg) 0 0 var(--vds-radius-lg);
  }
}

.sw-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 28px 16px;
  border-bottom: 1px solid var(--vds-border);
  flex-shrink: 0;
}
.sw-modal-title { font: 700 17px/1 var(--vds-font-sans); color: var(--vds-text-primary); }
.sw-modal-body {
  flex: 1;
  overflow-y: auto;
  padding: 24px 28px;
  display: flex;
  flex-direction: column;
  gap: 24px;
  /* CSS Scroll Anchoring (2026-09, per explicit follow-up: "the page is
     going to the top whenever everytime after a few seconds") -- browsers
     silently adjust an overflow:auto container's scrollTop to compensate
     for layout shifts, entirely below the JS scrollTop setter (confirmed
     live: a scrollTop=400 assignment held for 10+s, then reset to 0 with
     zero further scrollTop= calls trapped via an Element.prototype
     override, and scrollHeight/clientHeight never changed either -- the
     signature of the browser's own internal anchoring correction, not
     application code). This whole modal is torn down and rebuilt by
     catalogs.js's render() on every unrelated periodic refresh (the
     Collector Agents screen's 20s auto-refresh, while this modal happens
     to be open on top of it) -- exactly the kind of global reflow that
     triggers a spurious anchoring correction here. */
  overflow-anchor: none;
}
.sw-modal-text { font: 400 14px/1.5 var(--vds-font-sans); color: var(--vds-text-secondary); margin-bottom: 24px; }

/* ---- Warning dialog (shared-widgets.js's warningDialog) -------------------
   A confirm for an action that reaches past the thing clicked into other
   records -- first used by the Form Template unlink/archive/edit warnings
   (Library) and Details' "back to the default form". Built on .sw-modal--sm
   (so it stays a centered dialog at every width; plain .sw-modal becomes a
   side sheet below 1024px) with that variant's single 28px padding swapped
   for three sections: head (tone badge + title + subject + X), body
   (message, affected-items list, note) and a tinted foot for the actions.
   --danger is red, --warning amber. */
.sw-modal--sm.sw-warn {
  width: 460px;
  padding: 0;
  overflow: hidden;
}
.sw-warn-head {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 22px 20px 0 24px;
}
.sw-warn-badge {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.sw-warn--danger .sw-warn-badge {
  background: var(--vds-red-50);
  color: var(--vds-red-600);
  box-shadow: 0 0 0 6px var(--vds-red-50);
}
.sw-warn--warning .sw-warn-badge {
  background: var(--vds-amber-50);
  color: var(--vds-amber-600);
  box-shadow: 0 0 0 6px var(--vds-amber-50);
}
.sw-warn-titles {
  flex: 1;
  min-width: 0;
  padding-top: 2px;
}
.sw-modal--sm.sw-warn .sw-warn-title {
  font: 700 16px/1.3 var(--vds-font-sans);
  margin: 0;
}
.sw-warn-subject {
  margin-top: 4px;
  font: 500 13px/1.3 var(--vds-font-sans);
  color: var(--vds-text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sw-warn-close {
  flex-shrink: 0;
  margin: -6px -4px 0 0;
}
.sw-warn-body {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 16px 24px 20px 78px; /* 24 + 40 badge + 14 gap: aligns under the title */
  min-height: 0;
  overflow-y: auto;
}
.sw-warn-text {
  margin: 0;
  font: 400 14px/1.55 var(--vds-font-sans);
  color: var(--vds-text-secondary);
}
.sw-warn-affected {
  border: 1px solid var(--vds-border);
  border-radius: var(--vds-radius-md);
  overflow: hidden;
}
.sw-warn-affected-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  background: var(--vds-neutral-50);
  border-bottom: 1px solid var(--vds-border);
}
.sw-warn-affected-label {
  font: 600 11px/1 var(--vds-font-sans);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--vds-text-secondary);
}
.sw-warn-count {
  min-width: 20px;
  padding: 2px 7px;
  border-radius: 999px;
  font: 600 11px/1.3 var(--vds-font-sans);
  text-align: center;
}
.sw-warn--danger .sw-warn-count {
  background: var(--vds-red-50);
  color: var(--vds-red-600);
}
.sw-warn--warning .sw-warn-count {
  background: var(--vds-amber-50);
  color: var(--vds-amber-600);
}
/* ~5 rows visible, the rest a scroll away -- a heavily-used template must
   not push the actions off the dialog. */
.sw-warn-list {
  max-height: 184px;
  overflow-y: auto;
}
.sw-warn-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
}
.sw-warn-item + .sw-warn-item {
  border-top: 1px solid var(--vds-border);
}
.sw-warn-item-type {
  flex-shrink: 0;
  padding: 2px 6px;
  border-radius: var(--vds-radius-sm);
  background: var(--vds-neutral-100);
  font: 600 10px/1.3 var(--vds-font-sans);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--vds-text-secondary);
}
.sw-warn-item-name {
  flex: 1;
  min-width: 0;
  font: 500 13px/1.4 var(--vds-font-sans);
  color: var(--vds-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sw-warn-empty {
  padding: 12px;
  font: 400 13px/1.4 var(--vds-font-sans);
  color: var(--vds-text-tertiary);
}
.sw-warn-note {
  padding: 10px 12px;
  border-radius: var(--vds-radius-md);
  background: var(--vds-neutral-50);
  border-left: 3px solid var(--vds-border-strong);
  font: 400 12.5px/1.5 var(--vds-font-sans);
  color: var(--vds-text-secondary);
}
.sw-warn-foot {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 14px 24px;
  background: var(--vds-neutral-50);
  border-top: 1px solid var(--vds-border);
}
@media (max-width: 599.98px) {
  .sw-warn-body {
    padding-left: 24px;
  }
  .sw-warn-foot > * {
    flex: 1;
  }
}
.sw-modal-foot {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 16px 28px 20px;
  border-top: 1px solid var(--vds-border);
  flex-shrink: 0;
}

.sw-field { display: flex; flex-direction: column; }
.sw-field-pair { display: flex; gap: 16px; }
.sw-field-pair .sw-field { flex: 1; }
.sw-fieldlabel {
  font: 600 11px/1 var(--vds-font-sans);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--vds-text-tertiary);
  margin-bottom: 8px;
}
.sw-input {
  width: 100%;
  padding: 10px 12px;
  border-radius: var(--vds-radius-md);
  border: 1.5px solid var(--vds-border);
  background: var(--vds-white);
  font: 400 14px/1.4 var(--vds-font-sans);
  color: var(--vds-text-primary);
  outline: none;
  box-sizing: border-box;
  transition: border-color 150ms, box-shadow 150ms;
}
/* Same locked-field convention as shell.css's .ws-oe-input[readonly] --
   a real .sw-input that can't be typed into (e.g. Catalogs' "Added By"
   field, 2026-10) reads as a plain, non-interactive fact instead of a
   normal editable box. */
.sw-input[readonly] {
  background: var(--vds-neutral-50);
  cursor: default;
}
/* Navy-blue glow on focus (2026-09, per explicit follow-up: "a navy blue
   glowy type of border just like in claude it is") -- --vds-focus-ring
   already existed as a token but had never actually been wired up
   anywhere in this file. Two stacked shadows: a tight, more opaque ring
   right at the border for definition, and a wider, softer one for the
   actual glow -- applies to every .sw-input (and .sw-textarea, which
   always carries the .sw-input class alongside its own) app-wide, not
   just this one screen. */
.sw-input:focus {
  border-color: var(--vds-focus-ring);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--vds-focus-ring) 30%, transparent),
              0 0 8px 0px color-mix(in srgb, var(--vds-focus-ring) 35%, transparent);
}
.sw-input--inline { flex: 1; }
.sw-textarea { min-height: 72px; resize: vertical; font-family: var(--vds-font-sans); }

.sw-chiprow { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.sw-chip {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 5px 10px;
  background: var(--vds-blue-50);
  border-radius: var(--vds-radius-sm);
  font: 500 13px/1 var(--vds-font-sans);
  color: var(--vds-accent);
}
.sw-chip-x {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0 2px;
  color: var(--vds-text-tertiary);
  font-size: 15px;
  line-height: 1;
}

/* Chosen-item chips (shared-widgets.js's tagChip/personChip) -- the
   canonical look for a typeaheadField's `chosen` list, ported from the
   Dialog component mockup. Distinct from .sw-chip above (Catalogs' own
   tag/people chips now use tagChip/personChip too, as of the Catalogs
   rollout pass -- .sw-chip itself is still used directly by Subs, not
   migrated onto these yet). */
.sw-tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  background: var(--vds-action);
  border-radius: var(--vds-radius-sm);
  font: 500 12px/1 var(--vds-font-sans);
  color: var(--vds-text-on-dark);
}
.sw-tag-chip .sw-chip-x { color: rgba(255, 255, 255, 0.65); }
.sw-tag-chip .sw-chip-x:hover { color: var(--vds-text-on-dark); }

/* A tag staged by the inline create flow (shared-widgets.js's tagChip
   opts.pending): it looks like a chip, but no poc_tags row exists for it
   until the surrounding overlay is saved. Dashed outline + the app's amber
   "pending" treatment (the same pairing .lib-archived-badge uses) rather
   than the solid dark fill a real, already-linked tag gets -- the whole
   point is that the two must not look alike. */
.sw-tag-chip--pending {
  background: var(--vds-amber-50);
  color: var(--vds-amber-700, var(--vds-amber-600));
  border: 1px dashed var(--vds-amber-600);
}
.sw-tag-chip--pending .sw-chip-x { color: var(--vds-amber-600); }
.sw-tag-chip--pending .sw-chip-x:hover { color: var(--vds-amber-700, var(--vds-amber-600)); }

/* Catalog step of the inline create flow -- takes over the same popover slot
   the suggestion list uses (they're mutually exclusive; see typeaheadField's
   own pendingCreate branch), so it inherits .sw-suggest's positioning and
   only adds its own padding/rows. */
.sw-suggest--createcat {
  padding: 10px;
  gap: 8px;
}
.sw-createcat-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.sw-createcat-name {
  font: 600 12px/1.2 var(--vds-font-sans);
  color: var(--vds-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sw-createcat-hint {
  flex-shrink: 0;
  font: 400 11px/1.2 var(--vds-font-sans);
  color: var(--vds-text-tertiary);
}
.sw-createcat-select {
  width: 100%;
}
.sw-createcat-cancel {
  align-self: flex-start;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font: 500 11px/1 var(--vds-font-sans);
  color: var(--vds-text-tertiary);
}
.sw-createcat-cancel:hover { color: var(--vds-text-primary); }
.sw-person-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 4px;
  background: var(--vds-neutral-100);
  border-radius: 999px;
  font: 500 12px/1 var(--vds-font-sans);
  color: var(--vds-text-primary);
}

.sw-addrow { display: flex; gap: 8px; position: relative; }
/* Inline clear "x" (shared-widgets.js's typeaheadField, isOpenKey callers
   only) -- positioned against .sw-addrow rather than .sw-addrow-wrap below,
   since .sw-addrow's own box is exactly the input's height (the wrap also
   contains the absolutely-positioned suggestion list/error, which don't
   contribute to its height, but would make a naive vertical-center guess
   wrong if anchored there instead). */
.sw-input--clearable { padding-right: 34px; }
.sw-addrow-clear {
  position: absolute; top: 50%; right: 8px; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; border: none; border-radius: 50%;
  background: none; cursor: pointer; color: var(--vds-text-tertiary);
}
.sw-addrow-clear:hover { background: var(--vds-surface); color: var(--vds-text-secondary); }
/* Suggestions/error float in a popover anchored under the input row instead
   of sitting in normal flow, so typing/opening them never resizes the
   modal or shifts anything below it (see shared-widgets.js's typeaheadField). */
.sw-addrow-wrap { position: relative; }
.sw-suggest {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  border: 1.5px solid var(--vds-border);
  border-radius: var(--vds-radius-md);
  background: var(--vds-white);
  overflow-y: auto;
  max-height: 220px;
  box-shadow: var(--vds-shadow-md, 0 8px 24px rgba(0, 0, 0, 0.12));
}
/* The catalog-picker step (per explicit report: it was floating over the
   next field down, same as a plain suggestion list) sits in normal flow
   instead -- it's a short, deliberate step the admin has to complete, not
   a transient browse list, so pushing the fields below it down is the
   right trade, not a bug to route around. Combined-class selector, not
   just .sw-suggest--createcat, so this beats .sw-suggest's own
   position: absolute regardless of declaration order. */
.sw-suggest.sw-suggest--createcat {
  position: static;
  margin-top: 6px;
}
.sw-suggest-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 12px;
  border: none;
  background: none;
  cursor: pointer;
  font: 400 13px/1.4 var(--vds-font-sans);
  color: var(--vds-text-primary);
}
.sw-suggest-item:hover { background: var(--vds-blue-50); }
/* Arrow-key highlight (keyboard equivalent of :hover -- see typeaheadField's
   own highlightKey/highlighted in shared-widgets.js). */
.sw-suggest-item.is-active { background: var(--vds-blue-50); }
.sw-fielderr {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 20;
  font: 500 12px/1.4 var(--vds-font-sans);
  color: var(--vds-danger, #d33);
  background: var(--vds-white);
  padding: 4px 0;
}

/* Person-suggestion row (Assigned Users / Actors / a Relationship's
   counterparty picker) — same inline .sw-suggest dropdown Tags uses (no
   popover, no backdrop: an earlier version had a full checkbox popover with
   a fixed full-viewport backdrop, which blocked the modal's own scroll
   while open; this is just a richer row inside the same lightweight
   dropdown). */
.sw-ppl-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-weight: 700;
  font-family: var(--vds-font-sans);
  flex-shrink: 0;
}
.sw-ppl-avatar-wrap { position: relative; display: inline-flex; flex-shrink: 0; }
.sw-ppl-status-dot {
  position: absolute;
  bottom: -1px;
  right: -1px;
  border-radius: 50%;
  border: 2px solid var(--vds-white);
}
.sw-suggest-person { display: flex; align-items: center; gap: 8px; text-align: left; }
.sw-suggest-person-name { font: 600 13px/1.3 var(--vds-font-sans); color: var(--vds-text-primary); }
.sw-suggest-person-sub { font: 400 11px/1.3 var(--vds-font-sans); color: var(--vds-text-tertiary); margin-top: 2px; }

/* Search + jump field (shared-widgets.js's searchJumpField()) -- the
   toolbar search box's own bordered-box look (matches the shape every
   screen's search box already converged on independently: .cat-search-box/
   .dir-searchbox/.lib-rail-searchbox/etc, 36px height, 1px border, radius
   md, 12px horizontal padding), with a .sw-suggest-shaped dropdown of
   matches underneath instead of that box narrowing anything itself. */
.sw-searchjump-wrap { position: relative; }
.sw-searchjump-box {
  display: flex;
  align-items: center;
  gap: var(--vds-space-10, 10px);
  border: 1px solid var(--vds-border);
  border-radius: var(--vds-radius-md);
  padding: 0 12px;
  height: 36px;
  background: var(--vds-bg, var(--vds-white));
  transition: border-color 150ms;
}
.sw-searchjump-box:focus-within { border-color: var(--vds-accent); }
.sw-searchjump-box input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  font: 400 13px/1 var(--vds-font-sans);
  color: var(--vds-text-primary);
}
.sw-searchjump-box input::placeholder { color: var(--vds-text-tertiary); }
.sw-searchjump-box > .sw-icon { color: var(--vds-text-tertiary); flex-shrink: 0; }
/* Same shape as .sw-suggest above (kept as a separate class rather than a
   shared one -- typeaheadField's own .sw-suggest is anchored against
   .sw-addrow-wrap's own compact add-row box, this against the wider
   searchjump box; the two have never needed to vary independently so far,
   but keeping them separate means they safely can later). */
.sw-searchjump-list {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  border: 1.5px solid var(--vds-border);
  border-radius: var(--vds-radius-md);
  background: var(--vds-white);
  box-shadow: var(--vds-shadow-lg);
  overflow-y: auto;
  max-height: 320px;
}
.sw-searchjump-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 12px;
  border: none;
  background: none;
  cursor: pointer;
  font: 400 13px/1.4 var(--vds-font-sans);
  color: var(--vds-text-primary);
}
.sw-searchjump-item:hover { background: var(--vds-blue-50); }

/* Button / IconButton (shared-widgets.js's button()/iconButton()) --
   canonical replacement for the ~10 independently-maintained button-class
   families audited across the app (audit-findings.md #61,
   SHARED_COMPONENT_LIBRARY_PROPOSAL.md's Buttons section). Ported from the
   design system's Button.jsx (_ds_bundle.js) as a DESIGN, not its mechanics
   -- hover is plain CSS :hover here (no JS state), matching every other
   sw-* component. :focus-visible is NOT redeclared here -- base.css's
   global rule (see :focus-visible there) already covers every focusable
   element in the app, including this one; don't add outline:none anywhere
   below or it'll silently opt back out of that. */
.sw-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  border: none; border-radius: var(--vds-radius-md);
  font: var(--vds-weight-semibold) 13px/1 var(--vds-font-sans);
  padding: var(--vds-space-8) var(--vds-space-16);
  /* Fixed to match .sw-searchjump-box's 36px exactly -- box-sizing:border-box
     is global (base.css), so this includes the padding above rather than
     adding to it. Without this, each button's rendered height was set by
     whichever child was tallest (its text vs. its icon), and different
     toolbar call sites pass different icon sizes (13/15/16px via
     BUTTON_ICON_SIZE below) purely incidentally -- not as a deliberate size
     choice -- so buttons ended up at visibly different heights from each
     other and from the search box they sit next to. Declared once here (not
     per size-variant below) so sm/default/lg all inherit the same fixed
     height uniformly. */
  height: 36px;
  white-space: nowrap; cursor: pointer;
  transition: background 120ms ease, box-shadow 120ms ease;
}
.sw-btn:disabled { opacity: .5; cursor: not-allowed; }

.sw-btn--primary { background: var(--vds-action); color: var(--vds-text-on-dark); }
.sw-btn--primary:hover:not(:disabled) { background: var(--vds-action-hover); }
.sw-btn--primaryBlue { background: var(--vds-accent); color: var(--vds-white); }
.sw-btn--primaryBlue:hover:not(:disabled) { background: var(--vds-accent-hover); }
.sw-btn--secondary { background: var(--vds-white); color: var(--vds-text-primary); box-shadow: var(--vds-hairline-strong); }
.sw-btn--secondary:hover:not(:disabled) { background: var(--vds-surface); }
.sw-btn--ghost { background: transparent; color: var(--vds-text-secondary); }
.sw-btn--ghost:hover:not(:disabled) { background: var(--vds-neutral-100); }
.sw-btn--destructive { background: var(--vds-danger); color: var(--vds-white); }
.sw-btn--destructive:hover:not(:disabled) { background: var(--vds-danger-strong); }
.sw-btn--success { background: var(--vds-success); color: var(--vds-white); }
.sw-btn--success:hover:not(:disabled) { background: var(--vds-green-700); }

.sw-btn--sm { padding: 6px 12px; font-size: 12px; }
.sw-btn--lg { padding: 10px 20px; font-size: 14px; }
/* button()'s anchor mode (opts.href) -- an <a> doesn't get a UA button
   look for free, so it needs its own reset. */
a.sw-btn { text-decoration: none; display: inline-flex; }
a.sw-btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; pointer-events: none; }
/* "+ Add X" dashed-border affordance -- layered via opts.className on top
   of a canonical variant (ghost, in practice) rather than a 7th canonical
   variant, per KNOWN_MISTAKES.md's "don't invent a new button variant"
   rule -- this exact recipe (dashed border, accent text, lighter weight,
   tighter padding) recurs across several screens' "+ Add X" affordances
   (originally .det-btn--dashed) and is worth keeping available generally. */
.sw-btn--dashed-add {
  background: none; border-style: dashed; border-color: var(--vds-border-strong);
  color: var(--vds-accent); font-weight: var(--vds-weight-medium);
  padding: 6px 12px;
}
.sw-btn--dashed-add:hover:not(:disabled) { background: var(--vds-blue-50); }
.sw-icon { display: inline-flex; }

/* Filter-toggle button (a funnel/sliders icon, optionally with a numeric
   active-filter-count badge) -- was hand-copied identically 4x across
   Action Station, Directory (x2), and Feed (audit-findings.md #310/#342/#450)
   before converging here. Plain CSS class, not a JS builder -- each caller
   still builds its own <button> (icon + onClick + is-open/has-active class
   toggle already computed from its own state), just applies this class name
   instead of a local one. Action Station later forked its own local
   .as-filter-toggle pill for an unrelated reason (see action-station.css);
   this is the shape Directory/Feed should carry forward.
   2026-08 visual-consistency unification pass, I3 (follow-up): upgraded from
   a plain icon-only ghost button to the 26px pill+embedded-count shape that
   Catalogs/Subs/Library/Details/Action-Station each already built locally
   for the same decision, so Directory/Feed (the two remaining screens still
   consuming this shared class) get the same look without forking again. */
.sw-filter-toggle {
  display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0;
  height: 26px; padding: 0 8px; border-radius: var(--vds-radius-sm);
  background: transparent; border: none; cursor: pointer;
  color: var(--vds-text-secondary);
  transition: all 120ms ease;
}
.sw-filter-toggle.is-open { background: var(--vds-neutral-100); }
.sw-filter-toggle.has-active { background: var(--vds-neutral-800); color: var(--vds-white); }
.sw-filter-count { font: 600 11px/1 var(--vds-font-sans); }

/* Screen toolbar (shared-widgets.js's screenToolbar()) -- the row every
   screen builds directly below app_base.html's own .ws-subheader (top nav
   tabs + Task Control Pill), holding that screen's own internal tab list on
   the left and search/filter/primary-actions/"···" overflow on the right.
   This is the shape every prior per-screen toolbar row already converged on
   independently (.cat-subbar, .dir-topbar, .lib-topbar, .det-tabbar,
   .td-toolbar, .feed-toolbar) -- flex row, space-between, a bottom hairline,
   ~12px/32px padding -- codified here as one shared class instead of a 7th
   near-identical copy. */
.sw-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; flex-shrink: 0;
  padding: 12px 32px; border-bottom: 1px solid var(--vds-border);
  background: var(--vds-white);
}
/* Always rendered even when opts.left is null (an empty div) -- with
   justify-content:space-between, a single flex child packs to the START,
   not the end, so this empty-but-present wrapper is what keeps
   .sw-toolbar-right pinned to the right edge on a screen with no internal
   tabs (ToDos, Feed). Do not remove this wrapper as a "simplification". */
.sw-toolbar-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.sw-toolbar-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
/* screenToolbar()'s own "···" overflow trigger (rowMenuTrigger() ->
   iconButton(), no size passed -> default .sw-iconbtn, 32x32) sits right
   next to the now-36px .sw-btn/.sw-searchjump-box in this row and read
   visibly smaller. rowMenuTrigger() is also used directly for several
   genuine per-row/dense kebab menus outside any toolbar (Sheets/Ledger row
   menus, feed item rows, the Gantt density-menu trigger) that must stay
   compact at 32px, so the fix is scoped to this row via descendant
   selector rather than changing iconButton()'s default or rowMenuTrigger()
   itself. Reuses .sw-iconbtn--lg's own values (36x36 box, -4px hit-slop
   inset for the same 44px real tap target) without requiring every
   screenToolbar() overflow call site to opt into size:'lg'. */
.sw-toolbar-right .sw-more-wrap .sw-iconbtn { width: 36px; height: 36px; }
.sw-toolbar-right .sw-more-wrap .sw-iconbtn::before { inset: -4px; }

/* Row action menu (kebab "···" trigger + fixed-position Edit/Archive/Delete
   dropdown) -- shared-widgets.js's rowMenuTrigger/rowMenuDropdown. Used by
   Directory's Ledger tab and Details' Sheets tab (Needs/Targets) so both
   behave identically. Values copied from directory.css's .dir-iconbtn/
   -backdrop/-more-* (the original, single-screen version this was
   extracted from). .sw-iconbtn is now iconButton()'s canonical output --
   visible box stays compact (unchanged) so it doesn't blow out row height
   in dense contexts; real >=44px hit-slop comes from the invisible
   ::before pseudo-element below, not a bigger visible button, per the
   "never shrink the tappable area, never grow the visible glyph" contract.
   NOTE: on a row of closely-spaced icon buttons (e.g. Library's rich-text
   toolbar), adjacent hit-slop zones can overlap and steal clicks -- verify
   with a real click test wherever icon buttons sit closer than ~12px apart,
   a screenshot cannot reveal an invisible hit-zone collision. */
.sw-backdrop { position: fixed; inset: 0; z-index: 150; background: transparent; border: none; padding: 0; cursor: default; }
.sw-iconbtn {
  position: relative;
  width: 32px; height: 32px; border-radius: var(--vds-radius-md);
  border: 1px solid transparent; background: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--vds-text-tertiary); transition: background 120ms ease;
}
.sw-iconbtn::before { content: ''; position: absolute; inset: -6px; } /* 32+6+6 = 44px hit box */
.sw-iconbtn:hover, .sw-iconbtn.is-active { background: var(--vds-surface); border-color: var(--vds-border); }
.sw-iconbtn:disabled { opacity: .5; cursor: not-allowed; }
.sw-iconbtn--sm { width: 28px; height: 28px; }
.sw-iconbtn--sm::before { inset: -8px; } /* 28+8+8 = 44 */
.sw-iconbtn--lg { width: 36px; height: 36px; }
.sw-iconbtn--lg::before { inset: -4px; } /* 36+4+4 = 44 */
/* Permanent destructive-color modifier (this button always deletes/removes
   -- not a pressed/toggled state), applied via opts.className. */
.sw-iconbtn.is-danger { color: var(--vds-red-600); }
.sw-iconbtn.is-danger:hover { background: var(--vds-red-50); }
.sw-more-wrap { position: relative; }

/* shared-widgets.js's colorSwatch() -- a solid-fill circle for "pick one of
   a small set of named colors" (e.g. Action Station's create-project Color
   field). Same 44px real hit-slop via an invisible ::before as .sw-iconbtn
   above, same reasoning. The ring is a border PLUS a same-color box-shadow
   gap (not just a border) so it reads as "selected" against a swatch of any
   color, including ones close to the ring color itself. */
.sw-colorswatch {
  position: relative;
  width: 24px; height: 24px; border-radius: var(--vds-radius-full);
  border: 2px solid transparent; cursor: pointer; padding: 0;
  display: inline-flex; flex-shrink: 0;
}
.sw-colorswatch::before { content: ''; position: absolute; inset: -10px; } /* 24+10+10 = 44px hit box */
.sw-colorswatch.is-active { border-color: var(--vds-text-primary); box-shadow: 0 0 0 2px var(--vds-surface); }
.sw-colorswatch:disabled { opacity: .5; cursor: not-allowed; }
.sw-more-menu {
  /* position/top/left are set inline per-open (fixed, viewport-relative --
     see rowMenuPosFromRect) -- z-index/width/chrome stay here. */
  z-index: 151; width: 200px; background: var(--vds-white); border-radius: var(--vds-radius-md);
  box-shadow: var(--vds-shadow-lg); border: 1px solid var(--vds-border); padding: 4px 0;
}
.sw-more-item {
  width: 100%; display: flex; align-items: center; gap: 10px; padding: 8px 14px;
  border: none; background: transparent; cursor: pointer; text-align: left;
  font: 400 13px/1 var(--vds-font-sans); color: var(--vds-text-primary); transition: background 100ms ease;
}
.sw-more-item:hover { background: var(--vds-surface); }
.sw-more-item.is-danger { color: var(--vds-danger-strong); }
.sw-more-item:disabled { opacity: .5; cursor: not-allowed; }
.sw-more-item:disabled:hover { background: transparent; }
.sw-more-item.is-danger:hover { background: var(--vds-red-50); }

/* Fake Select — canonical replacement for every native-<select> styling in
   the app (shared-widgets.js's selectTrigger/selectMenu). Open-state ring
   matches SHARED_COMPONENT_LIBRARY_PROPOSAL.md's canonical spec exactly
   (Action Station's now-deleted .as-project-select-btn, #150). Menu
   position/top/left/width are set inline per-open (fixed, viewport-relative
   — see anchoredBelow) — z-index/chrome stay here, same split as
   .sw-more-menu above. */
.sw-select {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: 10px 12px; box-sizing: border-box;
  font: 400 14px/1.4 var(--vds-font-sans); color: var(--vds-text-primary);
  background: var(--vds-white); border: 1.5px solid var(--vds-border);
  border-radius: var(--vds-radius-md); cursor: pointer;
}
.sw-select:hover { border-color: var(--vds-border-strong); }
.sw-select.is-open { border-color: var(--vds-accent); box-shadow: inset 0 0 0 1.5px var(--vds-accent); }
.sw-select-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-select-chevron { flex-shrink: 0; color: var(--vds-text-secondary); display: inline-flex; }
.sw-select-menu {
  z-index: 151; border: 1px solid var(--vds-border); border-radius: var(--vds-radius-md);
  background: var(--vds-white); box-shadow: var(--vds-shadow-lg);
  max-height: 240px; overflow-y: auto; padding: 4px 0;
}
.sw-select-item {
  display: block; width: 100%; text-align: left;
  padding: 8px 12px; border: none; background: none; cursor: pointer;
  font: 400 13px/1.4 var(--vds-font-sans); color: var(--vds-text-primary);
}
.sw-select-item:hover, .sw-select-item:focus-visible { background: var(--vds-surface); outline: none; }
.sw-select-item.is-selected { background: var(--vds-blue-50); color: var(--vds-accent); font-weight: 600; }
/* Hierarchical option (2026-09 addition, e.g. Catalogs' Parent Catalog
   picker) -- real depth-based padding-left is set inline per-option (see
   selectMenu's own `depth` opt); a root-level option (no `.has-depth`)
   stays the plain primary-text weight above, a nested one reads slightly
   quieter so the hierarchy is visible at a glance, same "children read
   secondary" convention library.css's own .lib-rail-item--child uses. */
.sw-select-item.has-depth { color: var(--vds-text-secondary); }
.sw-select-item.has-depth.is-selected { color: var(--vds-accent); }

/* DatePicker calendar popover (shared-widgets.js's datePickerPopover) —
   values lifted from directory.css's real .dir-calendar (miniCalendar()'s
   shell), simplified to single-date. Top/left set inline per-open (fixed,
   viewport-relative — see anchoredRightAligned); width/chrome stay here. */
.sw-datepicker {
  z-index: 151; width: 280px; padding: 12px; box-sizing: border-box;
  border: 1px solid var(--vds-border); border-radius: var(--vds-radius-md);
  background: var(--vds-white); box-shadow: var(--vds-shadow-lg); user-select: none;
}
.sw-datepicker-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.sw-datepicker-navbtn {
  background: none; border: none; cursor: pointer; padding: 4px; border-radius: var(--vds-radius-sm);
  color: var(--vds-text-secondary); display: inline-flex;
}
.sw-datepicker-navbtn:hover { background: var(--vds-surface); }

/* Generic client-side pager (see shared-widgets.js's paginate()/
   pagerControls()) -- same icon-button look as .sw-datepicker-navbtn
   above, the convention Catalogs' own Collector-Agents pager already
   converged onto. */
.sw-pager { display: flex; align-items: center; justify-content: center; gap: 14px; padding-top: 12px; }
.sw-pager-btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 4px; border-radius: var(--vds-radius-sm);
  color: var(--vds-text-secondary); cursor: pointer;
}
.sw-pager-btn:hover:not(:disabled) { background: var(--vds-surface); }
.sw-pager-btn:disabled { opacity: 0.4; cursor: default; }
.sw-pager-label { font: 400 12px/1 var(--vds-font-sans); color: var(--vds-text-tertiary); }
.sw-datepicker-monthlabel { font: 600 13px/1 var(--vds-font-sans); color: var(--vds-text-primary); }
.sw-datepicker-dayheaders, .sw-datepicker-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.sw-datepicker-dayheaders { margin-bottom: 4px; }
.sw-datepicker-dayheader { font: 600 10px/1 var(--vds-font-sans); color: var(--vds-text-tertiary); text-align: center; padding: 4px 0; }
.sw-datepicker-day, .sw-datepicker-day-empty { aspect-ratio: 1; }
.sw-datepicker-day {
  width: 100%; border: none; border-radius: var(--vds-radius-sm); background: transparent;
  color: var(--vds-text-primary); font: 400 12px/1 var(--vds-font-sans); cursor: pointer;
  display: flex; align-items: center; justify-content: center; transition: background 100ms ease;
}
.sw-datepicker-day:hover { background: var(--vds-surface); }
.sw-datepicker-day.is-today { color: var(--vds-accent); font-weight: 600; }
.sw-datepicker-day.is-selected { background: var(--vds-accent); color: var(--vds-white); font-weight: 600; }
/* Range-select mode (opt-in via datePickerCells/datePickerPopover's new
   opts.rangeStart/rangeEnd/disableFuture) -- additive, same token
   directory.css's own .dir-cal-day.is-inrange already uses. */
.sw-datepicker-day.is-inrange { background: var(--vds-blue-50); }
.sw-datepicker-day:disabled, .sw-datepicker-day.is-disabled { color: var(--vds-text-tertiary); opacity: .35; cursor: not-allowed; }
.sw-datepicker-day:disabled:hover { background: transparent; }
/* Handle bar + Done button -- always in the DOM (shared-widgets.js's
   datePickerPopover renders them unconditionally), hidden here and shown
   only at phone width below, where the popover becomes a bottom sheet. */
.sw-datepicker-handle { display: none; width: 36px; height: 4px; border-radius: 2px; background: var(--vds-border-strong); margin: 0 auto 16px; }
.sw-datepicker-done {
  display: none; width: 100%; margin-top: 12px; padding: 13px; border-radius: var(--vds-radius-md);
  border: 1px solid transparent; background: var(--vds-action); color: var(--vds-text-on-dark);
  font: 600 15px/1 var(--vds-font-sans); cursor: pointer;
}

/* ---- Bucket Config modal + Template Builder modal (folded in whole from
   the former standalone bucket-config-modal.css/template-builder-modal.css,
   dev-branch merge -- their JS is now shared-widgets.js's own
   SharedWidgets.BucketConfigModal/SharedWidgets.TemplateBuilderModal, see
   that file's own matching comment). Prefixes (as-bucketcfg-*, unchanged;
   tplbuild-overlay) kept byte-for-byte -- as-bucketcfg-* is independently
   reused by details.js's and subs.js's own criteria rows (see their own
   "Deliberately reuses bucket-config-modal.css's own .as-bucketcfg-*
   classes" comments), and Template Builder's own DOM below .tplbuild-
   overlay is otherwise all library.css's existing lib-fb-* / lib-fbp-* /
   lib-modal-* / lib-ai-* / lib-tpv-* classes, not duplicated here -- any page
   using SharedWidgets.TemplateBuilderModal must still load library.css
   itself (see shared-widgets.js's own note on that factory). ---- */
/* Shared "Create/Configure Bucket" modal (window.BucketConfigModal, see
   screens/static/screens/js/bucket-config-modal.js) -- extracted from
   action-station.css once a second screen (details.js's Sequence Flow
   "Add Buckets" picker) needed the exact same modal, rather than
   duplicating this a second time or depending on the whole rest of
   action-station.css's own board/timeline styles just for these few
   rules (see the conversation this was built in). Any page using the JS
   module must also link this stylesheet -- action_station.html and
   details.html both do.

   Wider than action-station.css's own .as-addbucket-modal/.as-runagent-
   modal: this one has several stacked fields plus a criteria builder and
   preview, not a single input. */
.as-bucketcfg-overlay {
  position: fixed;
  inset: 0;
  /* Must stay above bucket-picker.css's .bp-overlay (z-index: 200) --
     same nesting action-station.css's own .as-addbucket-overlay/
     .as-runagent-overlay use, and details.css's .det-modal-overlay
     needs the same treatment here too (this nests ON TOP of the "Add
     Buckets" picker, not beside it). */
  /* On the Details page that "Add Buckets" picker is a .sw-modal-overlay
     (z-index: 300, its .sw-modal 301), which sat ABOVE this modal and
     covered its fields and buttons -- raised above 301 so this nests on top
     of it there too. Was: z-index: 210; */
  z-index: 310;
  background: rgba(17, 24, 39, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
.as-bucketcfg-modal {
  width: 480px;
  max-width: 90vw;
  max-height: 85vh;
  background: var(--vds-bg);
  border-radius: var(--vds-radius-lg);
  box-shadow: var(--vds-shadow-lg);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.as-bucketcfg-head {
  padding: 20px 24px 16px;
  border-bottom: 1px solid var(--vds-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: none;
}
.as-bucketcfg-title { font: var(--vds-weight-semibold) 16px/1.2 var(--vds-font-sans); color: var(--vds-text-primary); }
/* 2026-09 fix -- was a solid near-black circle (var(--vds-neutral-800))
   with the "X" rendered in its default (dark) color on top of it, since
   Icon(name, size) has no color param to honor the onClick markup's own
   (silently-ignored) 3rd-arg color request -- a dark icon on a near-black
   circle read as a broken/blank blob rather than a close button. Restyled
   to match the Catalogs page's own plain modal close button
   (catalogs.css's .cat-modal-x): transparent, muted icon color via
   `color` (VisionIcons' svg output uses currentColor), a light hover
   circle instead of a permanent dark one. */
.as-bucketcfg-close {
  display: flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: var(--vds-radius-md);
  background: none; color: var(--vds-text-tertiary); cursor: pointer;
  transition: background 120ms ease;
}
.as-bucketcfg-close:hover { background: var(--vds-neutral-100); }
.as-bucketcfg-body {
  padding: 16px 24px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.as-bucketcfg-field { display: flex; flex-direction: column; gap: 6px; }
.as-bucketcfg-label { font: var(--vds-weight-semibold) 12px/1.2 var(--vds-font-sans); color: var(--vds-text-secondary); text-transform: uppercase; letter-spacing: 0.02em; }
.as-bucketcfg-hint { font: 400 12px/1.4 var(--vds-font-sans); color: var(--vds-text-tertiary); }
/* .as-bucketcfg-input/.as-bucketcfg-select no longer carry their own visual
   styling (2026-08, audit-findings.md #556) -- every field in this modal
   now renders as a real .sw-input or selectTrigger()'s .sw-select, which
   already have their own border/padding/font/focus-state CSS. These two
   class names are kept ONLY as layout markers for the flex-sizing rules
   below (.as-bucketcfg-critrow's "flex: 2"/"flex: 1" children) -- do not
   add visual properties back onto them; style a new field via .sw-input/
   selectTrigger like everything else in this file does. */
.as-bucketcfg-err { margin-top: 4px; font: 500 12px/1.4 var(--vds-font-sans); color: var(--vds-danger, #d33); }
.as-bucketcfg-critrows { display: flex; flex-direction: column; gap: 8px; }
.as-bucketcfg-critrow { display: flex; align-items: center; gap: 6px; }
.as-bucketcfg-critrow .as-bucketcfg-select { flex: 2; }
.as-bucketcfg-critrow .as-bucketcfg-select-op { flex: 1; }
.as-bucketcfg-critrow .as-bucketcfg-input { flex: 2; }
.as-bucketcfg-critrow-remove { flex: none; }
/* Per explicit follow-up ("add the ability to use custom named fields for
   filters") -- replaces the Field <select> in this same flex slot
   (as-bucketcfg-critrow's own "flex: 2" on .as-bucketcfg-select above)
   once a row switches to a hand-typed key name; its own child input still
   needs the same flex-grow, the "back to the dropdown" × just sits
   beside it rather than stretching too. */
.as-bucketcfg-critrow-customfield { display: flex; align-items: center; gap: 4px; flex: 2; }
.as-bucketcfg-critrow-customfield .as-bucketcfg-input { flex: 1; }
/* AV (sort + take) editor -- see SharedWidgets.avEditor. Only NEW classes; it sits in an
   .as-bucketcfg-critrow whose "flex: 2" child rules would otherwise stretch its number boxes,
   so those are restated at equal specificity AFTER them. */
.as-bucketcfg-critrow--av { flex-wrap: wrap; align-items: flex-start; }
.as-bucketcfg-critrow .sw-av-editor { display: flex; flex-direction: column; gap: 6px; flex: 1 1 100%; min-width: 0; }
.sw-av-editor .sw-av-key, .sw-av-editor .sw-av-take { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.sw-av-editor .sw-av-label { font: 400 12px/1.3 var(--vds-font-sans); color: var(--vds-text-secondary); min-width: 54px; }
.sw-av-editor .sw-av-key-field { flex: 2 1 120px; min-width: 0; }
.sw-av-editor .sw-av-key-dir { flex: 1 1 90px; min-width: 0; }
.sw-av-editor .sw-av-custom { flex: 2 1 120px; min-width: 0; }
.sw-av-editor .sw-av-number { flex: none; width: 64px; }
.as-bucketcfg-preview {
  margin-top: 4px;
  padding: 10px 12px;
  border: 1px solid var(--vds-border);
  border-radius: var(--vds-radius-md);
  background: var(--vds-neutral-50, var(--vds-neutral-800));
}
.as-bucketcfg-preview-count { font: var(--vds-weight-semibold) 12px/1.3 var(--vds-font-sans); color: var(--vds-text-primary); }
.as-bucketcfg-preview-list { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; }
.as-bucketcfg-preview-item { font: 400 12px/1.4 var(--vds-font-sans); color: var(--vds-text-secondary); }
.as-bucketcfg-preview-more { font: 400 12px/1.4 var(--vds-font-sans); color: var(--vds-text-tertiary); font-style: italic; }

/* Re-enabled per explicit follow-up ("here as well") -- see bucket-config-
   modal.js's own bucketConfigMatchingBuckets comment. */
.as-bucketcfg-criteria-match {
  margin-top: 4px;
  padding: 8px 12px;
  border: 1px solid var(--vds-warning);
  border-radius: var(--vds-radius-md);
  background: var(--vds-warning-soft);
}
.as-bucketcfg-criteria-match-label { font: var(--vds-weight-semibold) 12px/1.3 var(--vds-font-sans); color: var(--vds-text-primary); }
.as-bucketcfg-criteria-match-item { margin-top: 2px; font: 400 12px/1.4 var(--vds-font-sans); color: var(--vds-text-secondary); }

/* Criteria's own sub-fields (Form Template picker, Tags groups) -- nested
   one level inside .as-bucketcfg-field's own "Criteria" label, so a
   lighter/smaller label style than the top-level .as-bucketcfg-label
   reads correctly as belonging to it rather than a sibling field. */
.as-bucketcfg-subfield { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
.as-bucketcfg-sublabel { font: var(--vds-weight-semibold) 11px/1.2 var(--vds-font-sans); color: var(--vds-text-tertiary); text-transform: uppercase; letter-spacing: 0.02em; }
.as-bucketcfg-taggroups { display: flex; flex-direction: column; gap: 10px; }
.as-bucketcfg-taggroup { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; border: 1px solid var(--vds-border); border-radius: var(--vds-radius-md); }
.as-bucketcfg-taggroup-or { font: var(--vds-weight-semibold) 11px/1.2 var(--vds-font-sans); color: var(--vds-text-tertiary); text-transform: uppercase; }
.as-bucketcfg-comboin { flex: 2; display: flex; flex-direction: column; gap: 6px; }
.as-bucketcfg-foot {
  padding: 14px 24px;
  border-top: 1px solid var(--vds-border);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--vds-space-8);
  flex: none;
}

/* Parent Bucket picker's own dropdown -- only visible while its input has
   real DOM focus (see renderBucketConfigParentPicker's own comment for
   why this is plain CSS, not a JS focus/blur-driven re-render). Scoped to
   this one field, not shared-widgets.css's own .sw-suggest generally --
   every OTHER typeaheadField-based dropdown in this app keeps its
   existing always-visible-once-matched behavior. */
.as-bucketcfg-parentpicker .sw-suggest { display: none; }
.as-bucketcfg-parentpicker:focus-within .sw-suggest { display: block; }

/* Shared Create/Edit Template builder's own overlay wrapper (see
   template-builder-modal.js's own header comment for the full extraction
   writeup) -- .lib-fbp itself (Library's own original full-width flex
   layout, unchanged, from library.css) sits INSIDE this as its one child,
   so it fills the overlay exactly the same way it used to fill the Files
   tab's own content area. Opaque, not a dimmed backdrop over the host
   screen -- this is a full takeover of the view, same as Library's own
   original intent, just now achievable from any screen via position:fixed
   instead of depending on that screen's own tab-content layout to make
   room for it.

   It takes over the SCREEN, not the whole viewport: it used to be
   `inset: 0`, which also swallowed the shell's own .ws-header and
   .ws-subheader (and the KB rail), leaving no breadcrumb, nav tabs or Task
   Control Pill while the builder was open. ToDos' own in-page Template
   Maker (todos.js's renderMaker, rendered straight into the screen root)
   already read the right way, so this now matches it: pinned to
   .ws-content's own box via the two variables shell.js publishes for
   exactly this (see its own "Shell content geometry" block for why they're
   measured rather than a hardcoded height). The 0px fallbacks are what
   keeps it correct on a page with no shell at all (Action Station, Login --
   they extend base.html, not app_base.html, so nothing sets these). */
.tplbuild-overlay {
  position: fixed;
  top: var(--ws-content-top, 0px);
  left: var(--ws-content-left, 0px);
  right: 0;
  bottom: 0;
  z-index: 220;
  background: var(--vds-white);
  display: flex;
  flex-direction: column;
  overflow: auto;
  /* Re-casts .ws-subheader's own drop shadow (shell.css:
     `0 2px 8px rgba(17,24,39,0.06)`) onto this surface. That shadow falls
     DOWNWARD, into the content area -- which this opaque overlay now sits
     on top of, so the sub-header above read as a flat, unseparated edge
     while the builder was open. Drawn from the receiving surface as an
     inset rather than by raising .ws-subheader's own stacking above
     z-index 220, which would also lift it over the KB panel
     (.ws-kb-open.is-panel, z-index 50) that is deliberately meant to cover
     the chrome. -2px spread keeps the same inset from bleeding visibly
     down the left/right/bottom edges, where there's nothing casting. */
  box-shadow: inset 0 2px 8px -2px rgba(17, 24, 39, 0.06);
}

/* ---- Toast (shared-widgets.js's showToast) -- consolidates action-station.js's
   old .as-toast-host/.as-toast/.as-toast-msg/.as-toast-jump (the box model,
   including the optional icon + action-button layout) and feed.js's old
   .feed-toast-host/.feed-toast (the fade/slide-in keyframe, which
   action-station's own version never had) into one shape. Directory/Details/
   Library/Subs keep their own local toast styling for now -- out of scope. */
.sw-toast-host {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 999;
}

.sw-toast {
  display: flex;
  align-items: center;
  gap: var(--vds-space-10);
  padding: var(--vds-space-8) var(--vds-space-10) var(--vds-space-8) var(--vds-space-14);
  background: var(--vds-neutral-800);
  color: var(--vds-white);
  border-radius: var(--vds-radius-default);
  font: 500 13px/1 var(--vds-font-sans);
  box-shadow: 0 8px 24px rgba(0,0,0,0.18);
  animation: swToastIn 200ms ease;
}
@keyframes swToastIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

.sw-toast-msg {
  white-space: nowrap;
}

.sw-toast-action {
  flex-shrink: 0;
  padding: var(--vds-space-4) var(--vds-space-10);
  background: rgba(255,255,255,0.12);
  color: var(--vds-white);
  border: none;
  border-radius: var(--vds-radius-sm);
  font: var(--vds-weight-semibold) 12px/1 var(--vds-font-sans);
  cursor: pointer;
}

.sw-toast-action:hover {
  background: rgba(255,255,255,0.2);
}

/* ---- Profile dropdown (shared-widgets.js's renderProfileDropdown) --
   avatar button + Time-today/Streak/Desk-No stats + Portals + View-Profile/
   Settings/Log-out menu. Canonical extraction of what used to be 3
   independently hand-copied .ws-profile* blocks -- app_base.html-based
   screens (shell.css) and Action Station (action-station.css, a
   byte-identical copy of shell.css's own block) both had their own; every
   screen that needs this now loads shared-widgets.css instead. ---- */
.ws-profile { position: relative; flex-shrink: 0; }
.ws-profile-btn {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 8px 4px 4px; border: 1.5px solid var(--vds-border); border-radius: var(--vds-radius-full);
  background: var(--vds-white); cursor: pointer;
}
.ws-profile-btn:hover { background: var(--vds-surface); }
.ws-profile-avatar {
  width: 32px; height: 32px; border-radius: var(--vds-radius-full);
  background: var(--vds-neutral-800); color: var(--vds-text-on-dark);
  display: flex; align-items: center; justify-content: center;
  font: var(--vds-weight-semibold) 11px/1 var(--vds-font-sans); flex-shrink: 0;
}
/* Uploaded profile picture (SharedWidgets.renderProfileDropdown's person.avatarUrl). */
.ws-profile-avatar--img { overflow: hidden; background: var(--vds-neutral-100); }
.ws-profile-avatar--img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ws-profile-caret { color: var(--vds-text-tertiary); display: flex; }
.ws-profile-dd {
  position: absolute; top: calc(100% + 8px); right: 0; width: 240px; z-index: 200;
  background: var(--vds-white); border: 1px solid var(--vds-border); border-radius: var(--vds-radius-md);
  box-shadow: var(--vds-shadow-lg); overflow: hidden;
}
.ws-profile-dd-head { display: flex; align-items: center; gap: 10px; padding: 16px; border-bottom: 1px solid var(--vds-border); }
.ws-profile-dd-name { font: var(--vds-weight-semibold) 14px/1.2 var(--vds-font-sans); color: var(--vds-text-primary); }
.ws-profile-dd-mail { font: 400 12px/1.3 var(--vds-font-sans); color: var(--vds-text-tertiary); margin-top: 2px; }
.ws-profile-dd-stats { display: flex; padding: 12px 16px; gap: 8px; border-bottom: 1px solid var(--vds-border); }
.ws-profile-dd-stat { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.ws-profile-dd-stat-label { display: flex; align-items: center; gap: 4px; font: 400 11px/1 var(--vds-font-sans); color: var(--vds-text-tertiary); }
.ws-profile-dd-stat-val { font: var(--vds-weight-semibold) 14px/1 var(--vds-font-sans); color: var(--vds-text-primary); }
.ws-profile-dd-menu { padding: 6px; }
.ws-profile-dd-item {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 14px; border: none; border-radius: 8px;
  background: none; cursor: pointer; text-align: left; text-decoration: none;
  font: 400 13px/1 var(--vds-font-sans); color: var(--vds-text-primary);
}
.ws-profile-dd-item:hover { background: var(--vds-surface); }
.ws-profile-dd-item svg { color: var(--vds-text-tertiary); flex-shrink: 0; }
.ws-profile-dd-item--danger { color: var(--vds-danger-strong); }
.ws-profile-dd-item--danger svg { color: var(--vds-danger-strong); }
.ws-profile-dd-item--danger:hover { background: var(--vds-red-50); }
/* Availability row + its fold-open status list (renderProfileDropdown's
   profileAvailabilityItems). The dot sits in a 16px slot so it lines up with
   the other rows' 16px icons. */
.ws-profile-dd-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; margin: 0 4px; }
.ws-profile-dd-status-label { flex: 1; }
.ws-profile-dd-status-value { color: var(--vds-text-secondary); font-size: 12px; }
.ws-profile-dd-status-chev { display: inline-flex; color: var(--vds-text-tertiary); }
.ws-profile-dd-status-list { display: flex; flex-direction: column; margin: 2px 0 4px 22px; padding-left: 6px; border-left: 1px solid var(--vds-border); }
.ws-profile-dd-status-opt {
  display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 10px; border: none; border-radius: 6px;
  background: none; cursor: pointer; text-align: left; font: 400 13px/1 var(--vds-font-sans); color: var(--vds-text-primary);
}
.ws-profile-dd-status-opt .ws-profile-dd-dot { margin: 0; }
/* Same dot, pinned to the avatar's bottom-right corner (headerAvatarWithStatus)
   -- on the header trigger and the dropdown head. The white ring keeps it
   readable over a photo. */
.ws-profile-avatar-wrap { position: relative; display: inline-flex; flex-shrink: 0; }
.ws-profile-avatar-dot {
  position: absolute; right: -1px; bottom: -1px; margin: 0;
  width: 9px; height: 9px; box-shadow: 0 0 0 2px var(--vds-white);
}
.ws-profile-dd-status-opt:hover { background: var(--vds-surface); }
.ws-profile-dd-status-opt.is-selected { font-weight: 600; }
.ws-profile-dd-status-check { margin-left: auto; display: inline-flex; color: var(--vds-accent); }
.ws-profile-dd-status-error { padding: 4px 14px 6px; font: 400 12px/1.3 var(--vds-font-sans); color: var(--vds-red-600); }

/* Portals section (new) -- dummy list only, no real portal integration
   exists yet (see renderProfileDropdown's own PROFILE_DD_PORTALS array);
   same mock shape as Actor Profile's own Portals tab. Sits between the
   stats row and the View-Profile/Settings/Log-out menu; its own bottom
   border keeps it visually separate from the menu below, same as the head/
   stats rows above it. */
.ws-profile-dd-portals { padding: 10px 16px 6px; border-bottom: 1px solid var(--vds-border); }
.ws-profile-dd-portals-label { font: 600 10px/1 var(--vds-font-sans); color: var(--vds-text-tertiary); letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 6px; }
.ws-profile-dd-portal {
  display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%;
  padding: 7px 2px; margin: 0 -2px; border: none; border-radius: 6px;
  background: none; cursor: pointer; text-align: left;
  font: 400 13px/1.2 var(--vds-font-sans); color: var(--vds-text-primary);
}
.ws-profile-dd-portal-name { overflow: visible; white-space: normal; word-break: break-word; }
.ws-profile-dd-portal-open { display: flex; flex-shrink: 0; color: var(--vds-text-tertiary); }
.ws-profile-dd-portal:hover .ws-profile-dd-portal-name,
.ws-profile-dd-portal:hover .ws-profile-dd-portal-open { color: var(--vds-accent); }
/* The profile dropdown no longer renders .ws-profile-dd-portals itself --
   the list moved to the header's Portals popover (shared-widgets.js's
   initPortalsPopover), which reuses the -portals-label/-portal row classes
   above as-is. Its own panel box, same look as .ws-nst-popover; z-index
   151 sits over withBackdrop's .sw-backdrop (150).
.ws-portals-pop {
  position: fixed; z-index: 151; box-sizing: border-box;
  padding: 12px 16px 8px;
  background: var(--vds-white); border: 1px solid var(--vds-border); border-radius: var(--vds-radius-md);
  box-shadow: var(--vds-shadow-lg);
}
*/
/* Header pill popovers (shared-widgets.js's wireHeaderPopover) -- Portals
   and Notifications share this one panel box (was .ws-portals-pop above,
   before Notifications needed the same thing). Same look as
   .ws-nst-popover; z-index 151 sits over withBackdrop's .sw-backdrop (150).
   Both reuse .ws-profile-dd-portals-label for their section heading. */
.ws-header-pop {
  position: fixed; z-index: 151; box-sizing: border-box;
  padding: 12px 16px 8px;
  background: var(--vds-white); border: 1px solid var(--vds-border); border-radius: var(--vds-radius-md);
  box-shadow: var(--vds-shadow-lg);
}
/* Retired (per feedback -- Admin Dashboard is now a plain first row of the
   portals list, styled like the rest, see initPortalsPopover):
   Admin Dashboard -- first row of the Portals popover (admin-only), moved
   from the profile dropdown's menu. A .ws-profile-dd-portal row with a
   leading icon, split from the portals list below by a hairline.
.ws-header-pop-admin { justify-content: flex-start; }
.ws-header-pop-admin-label { display: flex; align-items: center; gap: 8px; }
.ws-header-pop-admin-icon { display: flex; flex-shrink: 0; color: var(--vds-text-tertiary); }
.ws-header-pop-admin:hover .ws-header-pop-admin-icon { color: var(--vds-accent); }
.ws-header-pop-sep { height: 1px; margin: 6px -16px 10px; background: var(--vds-border); }
*/
/* Notifications -- empty until a real feed exists (setNotifications). */
.ws-notif-empty { padding: 14px 0 12px; font: 400 13px/1.3 var(--vds-font-sans); color: var(--vds-text-tertiary); text-align: center; }
.ws-notif-row { padding: 8px 0; border-top: 1px solid var(--vds-border); }
.ws-notif-row:first-of-type { border-top: none; }
.ws-notif-row-title { font: var(--vds-weight-medium) 13px/1.3 var(--vds-font-sans); color: var(--vds-text-primary); }
.ws-notif-row-body { margin-top: 2px; font: 400 12px/1.35 var(--vds-font-sans); color: var(--vds-text-secondary); }

/* ==========================================================================
   Responsive breakpoints — vision-responsive-design skill's convention
   (<600px phone / 600-1023px tablet / >=1024px desktop, matching Material 3/
   Tailwind/Bootstrap), grounded in the Dialog component mockup this whole
   shared library was ported from (the one Claude Design canvas the user
   reviewed and approved before any of this was built). Nothing above this
   point changes at >=1024px -- these rules only ever narrow the viewport
   further than what's already written above.
   ========================================================================== */

/* ---- Tablet and phone (<1024px): modal becomes a right-anchored slide-over
   sheet, full height, instead of a centered floating dialog -- matches the
   mockup's Tablet artboard exactly. Switching the MODAL itself to
   position:fixed (rather than changing .sw-modal-overlay's flex alignment)
   takes it out of the overlay's centering layout entirely, so .sw-modal--sm
   (delete/confirm dialogs, which stay centered at every width -- the mockup
   never showed these becoming a sheet, and a tiny confirm as a full-height
   edge sheet would be an odd, wasteful shape) is naturally unaffected by
   this rule without needing a separate override. ---- */
@media (max-width: 1023.98px) {
  .sw-modal:not(.sw-modal--sm) {
    position: fixed;
    top: 0; right: 0; bottom: 0; left: auto;
    width: 460px; max-width: 100%; height: auto; max-height: 100%;
    margin: 0;
    border-radius: var(--vds-radius-lg) 0 0 var(--vds-radius-lg);
  }
}

/* ---- Phone (<600px): the sheet goes full-screen (matches the mockup's
   Phone artboard -- .frame occupies the full device width), form fields get
   a dedicated mobile type scale (16px floor on inputs is not a stylistic
   choice -- anything smaller triggers Safari's auto-zoom-on-focus on a real
   iPhone), and .sw-modal-body's field gap widens for readability (this was
   an explicit ask when reviewing the mockup: "more spacing between the
   different fields to separate it more" on mobile). ---- */
@media (max-width: 599.98px) {
  .sw-modal:not(.sw-modal--sm) {
    width: 100%;
    border-radius: 0;
  }
  .sw-modal--sm { width: calc(100% - 32px); max-width: calc(100% - 32px); }

  .sw-modal-title { font: 700 18px/1.2 var(--vds-font-sans); }
  .sw-modal-body { gap: 28px; padding: 20px 20px calc(24px + env(safe-area-inset-bottom, 0px)); }
  .sw-fieldlabel { font: 600 13px/18px var(--vds-font-sans); letter-spacing: 0.01em; }
  .sw-input, .sw-textarea { font-size: 16px; padding: 12px; }

  /* Touch targets: every menu/suggestion/chip-remove row >=44px tall, per
     SHARED_COMPONENT_LIBRARY_PROPOSAL.md's Dropdowns/Menus acceptance
     criteria -- these rows are 8-13px padding (~30-34px total) above. */
  .sw-select { padding: 12px; font-size: 16px; }
  .sw-select-item, .sw-suggest-item, .sw-more-item, .sw-searchjump-item {
    min-height: 44px; padding-top: 12px; padding-bottom: 12px; box-sizing: border-box;
  }
  .sw-searchjump-box input { font-size: 16px; }
  .sw-tag-chip, .sw-person-chip { padding: 8px 11px; font-size: 14px; }
  .sw-person-chip { padding: 6px 11px 6px 6px; }

  /* DatePicker becomes a bottom sheet instead of a floating popover -- a
     month grid needs real touch-target room, and cramming the desktop/
     tablet popover shape onto a 375px-wide screen is exactly the
     "shrink-the-desktop-shape-to-fit" anti-pattern the vision-responsive-
     design skill flags. !important overrides the JS-computed inline
     top/left (position:fixed, viewport-relative -- see anchoredRightAligned)
     that anchoredRightAligned/anchoredBelow set per-open; at this width the
     sheet always pins to the bottom instead. Picking a day here does NOT
     auto-close (see directory.js's dateField onPick) -- only the explicit
     Done button (below) does, so the sheet has to stay open long enough to
     actually reach it. */
  .sw-datepicker {
    position: fixed !important; top: auto !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
    width: 100% !important; border-radius: 16px 16px 0 0;
    padding: 8px 20px calc(20px + env(safe-area-inset-bottom, 0px));
  }
  .sw-datepicker-handle { display: block; }
  .sw-datepicker-done { display: block; }
  .sw-datepicker-day { min-height: 40px; font-size: 15px; }
}

/* ══════════════════════════════════════════════════════════════════════
   "Next Suggested Task" widget (shell.js's fetchNst/renderNst, and now
   action-station.js's own port of that same widget -- see its header
   comment for why Action Station needed its own JS copy rather than
   loading shell.js directly). Moved here from shell.css (not duplicated)
   once a second screen needed it, same "byte-identical copy... moved to
   shared-widgets.css" treatment this file's own .ws-profile* block
   already got (see action-station.css's comment on that) -- both
   app_base.html and action_station.html already load this stylesheet.
   Persistent chip left of the profile button (app_base.html's
   .ws-header .ws-spacer / action_station.html's .as-topbar-right) --
   ambient "what's next" answer. Sized/shaped to read as a peer of
   .ws-profile-btn beside it -- same rounded-full pill/border/~32px
   height -- rather than an unrelated insert next to it.
   ══════════════════════════════════════════════════════════════════════ */
.ws-nst {
  display: flex; align-items: center; gap: 6px;
  height: 32px; max-width: 260px; padding: 0 12px; box-sizing: border-box;
  border: 1.5px solid var(--vds-border); border-radius: var(--vds-radius-full);
  background: var(--vds-white); cursor: pointer; flex-shrink: 0;
}
.ws-nst:hover, .ws-nst.is-open { background: var(--vds-surface); }
.ws-nst-icon { display: inline-flex; flex-shrink: 0; }
.ws-nst-label {
  font: 600 11px/1 var(--vds-font-sans); letter-spacing: .05em; text-transform: uppercase;
  color: var(--vds-text-tertiary); flex-shrink: 0;
}
.ws-nst-title {
  font: 500 13px/1 var(--vds-font-sans); color: var(--vds-text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Loading -- neutral pulsing skeleton (nothing resolved yet to show a real
   icon/title). Mounted immediately at boot so the header row doesn't
   visibly resize once the first fetch actually lands. */
.ws-nst--loading { cursor: default; }
.ws-nst-skeleton {
  display: block; width: 120px; height: 12px; border-radius: var(--vds-radius-sm);
  background: var(--vds-neutral-100); animation: wsNstPulse 1.4s ease-in-out infinite;
}
@keyframes wsNstPulse { 0%, 100% { opacity: 1; } 50% { opacity: .4; } }

/* Nothing to suggest -- an affirmative state (see renderNst's own comment
   on why this isn't a silent gap), and "could not load" -- a muted,
   manual-retry-only error state (audit-findings.md #59 already flags zero
   Supabase-outage UI anywhere else in the Shell; this doesn't repeat it).
   Both read as informational, not actionable the way a real suggestion is. */
.ws-nst--empty { cursor: default; color: var(--vds-text-tertiary); }
.ws-nst--empty .ws-nst-icon { color: var(--vds-green-600); }
.ws-nst--empty .ws-nst-title { color: var(--vds-text-tertiary); font-weight: 400; }
.ws-nst--error .ws-nst-icon, .ws-nst--error .ws-nst-title { color: var(--vds-text-tertiary); }
.ws-nst--error:hover { background: var(--vds-red-50); }

/* Expanded popover -- top/right set inline per-open (position:fixed,
   computed from the real trigger rect -- same convention this file's own
   selectMenu/datePickerPopover already use). Action buttons use
   .ws-nst-actions right below (own dedicated rule rather than reusing
   shell.css's .ws-oe-next-actions, which action-station.js's port has no
   access to); the card itself is its own .ws-nst-card* family below, not
   a reuse of shell.css's .ws-oe-next-card -- see shell.js's own comment
   for why.
   z-index 231, was 151: the popover hangs down out of the header into the
   content area, where the Template builder's .tplbuild-overlay (220, above)
   was covering everything below the header's bottom edge. Its own
   withBackdrop .sw-backdrop is lifted to 230 alongside it (scoped to the two
   NST mounts only), so a click on the builder still closes the popover
   instead of landing on the overlay. Both stay under the modal layer (300+)
   so a modal opened on top still covers them.
.ws-nst-popover {
  position: fixed; z-index: 151; width: 280px; padding: 14px; */
#ws-nst-root > .sw-backdrop, #as-nst-root > .sw-backdrop { z-index: 230; }
.ws-nst-popover {
  position: fixed; z-index: 231; width: 280px; padding: 14px;
  border: 1px solid var(--vds-border); border-radius: var(--vds-radius-md);
  background: var(--vds-white); box-shadow: var(--vds-shadow-lg);
  display: flex; flex-direction: column; gap: 10px; box-sizing: border-box;
}
.ws-nst-popover-label {
  font: 600 11px/1 var(--vds-font-sans); letter-spacing: .05em; text-transform: uppercase;
  color: var(--vds-text-tertiary);
}
.ws-nst-actions { display: flex; gap: 10px; }
.ws-nst-actions .sw-btn { flex: 1; justify-content: center; }

/* Card -- deliberately modeled on Action Station's own board card look
   (action-station.css's .as-card-top/-title/-sub/-side/-date, .as-priority)
   rather than the Outcome Eval modal's accent-bar/project-chip look this
   used to reuse. Its own dedicated classes rather than sharing .as-card's
   directly, same reasoning as that file's own .as-pool-card comment: this
   is a static preview with no drag/select affordances to inherit. */
.ws-nst-card {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 12px; border-radius: var(--vds-radius-md);
  background: var(--vds-surface); box-sizing: border-box;
  transition: opacity 150ms ease;
}
.ws-nst-card-main { min-width: 0; flex: 1; }
.ws-nst-card-title {
  font: var(--vds-weight-semibold) 14px/18px var(--vds-font-sans);
  color: var(--vds-text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ws-nst-card-sub { margin-top: 3px; font: 400 12px/16px var(--vds-font-sans); color: var(--vds-text-secondary); }
.ws-nst-card-side { text-align: right; flex-shrink: 0; }
.ws-nst-card-date { margin-top: 8px; font: 400 12px/16px var(--vds-font-sans); color: var(--vds-text-tertiary); }
.ws-nst-priority {
  display: inline-flex; align-items: center; padding: 3px 10px;
  border-radius: var(--vds-radius-full); font: var(--vds-weight-semibold) 11px/1.3 var(--vds-font-sans);
}
/* Low is green here too (2026-09) -- this card deliberately mirrors
   .as-priority, so leaving it grey would have made the same ToDo show a
   green pill on the board and a grey one in this widget. */
.ws-nst-priority.is-high { background: var(--vds-red-50); color: var(--vds-danger-strong); }
.ws-nst-priority.is-medium { background: var(--vds-amber-50); color: var(--vds-amber-700); }
.ws-nst-priority.is-low { background: var(--vds-green-50); color: var(--vds-green-700); }

.ws-nst-reason { margin-top: -4px; font: 400 12px/1.3 var(--vds-font-sans); color: var(--vds-text-tertiary); }

/* "Show another" clicked -- dim the stale card in place (still legible,
   clearly non-final) while the refetch is in flight, rather than the
   popover going visibly inert with no feedback at all for however long the
   unscheduled-ranking path's several real Supabase queries take. The
   button's own loading label (renderNst's `loading`/`loadingLabel`) is the
   other half of this feedback. */
.ws-nst-popover.is-refreshing .ws-nst-card,
.ws-nst-popover.is-refreshing .ws-nst-reason { opacity: 0.45; pointer-events: none; }

/* ── Responsive -- same desktop-first cascade this file's own breakpoints
   use (<600px phone / 600-1023px tablet / >=1024px desktop). Deliberately
   its own media queries here (not folded into this file's earlier
   Responsive breakpoints section above), since that section is scoped to
   the Dialog-mockup-derived .sw-modal/.sw-select/.sw-datepicker family --
   see its own header comment -- and this widget isn't part of that
   mockup. ── */
@media (max-width: 1023.98px) {
  /* Tablet: drop the title from the collapsed trigger (icon + micro-label
     only) -- the header row this sits in has no other tablet-specific gap
     on record, but the title's own space is the one easy thing to give
     back at this width; the full title still shows inside the popover on
     tap. */
  .ws-nst-title { display: none; }
  .ws-nst { max-width: none; padding: 0 10px; }
}

@media (max-width: 599.98px) {
  /* Phone: icon-only trigger, sized to a real >=44px touch target (the
     Task Controls Pill's own Play/Pause button is the smallest hit target
     anywhere in this audit at 20x20px, audit-findings.md #469 --
     deliberately not repeating that here). The label also drops; "Next"
     alone at icon size would misread as a nav arrow. */
  .ws-nst { width: 44px; height: 44px; padding: 0; justify-content: center; border-radius: var(--vds-radius-full); }
  .ws-nst-label { display: none; }

  /* Popover becomes a full-width bottom sheet -- same conversion this
     file's own .sw-datepicker already applies at this width, for the same
     reason (a fixed-position floating panel this wide has nowhere good to
     anchor on a 375px-wide screen). */
  .ws-nst-popover {
    top: auto !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
    width: 100%; border-radius: 16px 16px 0 0;
    padding: 14px 20px calc(20px + env(safe-area-inset-bottom, 0px));
  }
}

/* ══════════════════════════════════════════════════════════════════════
   Header cluster (header redesign): globe · NST pill · bell · profile, all
   one shared 36px height with the same 1.5px border so they read as a
   single row of peers. Scoped to the two top bars that carry it --
   app_base.html's .ws-header and action_station.html's .as-topbar -- so
   the base .ws-nst/.ws-profile-btn rules above stay as they were anywhere
   else. The globe/bell buttons are placeholders (no handlers yet, static
   bell dot -- see app_base.html's own comment).
   ══════════════════════════════════════════════════════════════════════ */
.ws-header-iconbtn {
  position: relative; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0; box-sizing: border-box;
  border: 1.5px solid var(--vds-border); border-radius: var(--vds-radius-full);
  background: var(--vds-white); color: var(--vds-neutral-600); cursor: pointer;
}
.ws-header-iconbtn svg { width: 17px; height: 17px; }
/* Text variant (the "Portals" button) -- same pill, sized to its label. */
.ws-header-iconbtn--text {
  width: auto; padding: 0 14px;
  font: var(--vds-weight-medium) 13px/1 var(--vds-font-sans); color: var(--vds-text-primary);
  white-space: nowrap;
}
.ws-header-iconbtn:hover { background: var(--vds-surface); }
.ws-header-iconbtn-dot {
  position: absolute; top: 7px; right: 8px;
  width: 7px; height: 7px; border-radius: var(--vds-radius-full);
  background: var(--vds-neutral-600); box-shadow: 0 0 0 2px var(--vds-white);
}
.ws-header-iconbtn:hover .ws-header-iconbtn-dot { box-shadow: 0 0 0 2px var(--vds-surface); }

/* Merged pill: Portals · NST · bell in ONE bordered 36px pill with thin
   vertical dividers between segments (app_base.html/action_station.html's
   .ws-header-group). The group owns the border; each segment is borderless
   and fills the full height, so a segment's hover tint reads as part of
   one control. overflow:hidden clips those tints to the rounded ends --
   safe for the NST popover, which is position:fixed (not clipped). */
.ws-header-group {
  display: flex; align-items: stretch; flex-shrink: 0; min-width: 0;
  height: 36px; box-sizing: border-box; overflow: hidden;
  border: 1.5px solid var(--vds-border); border-radius: var(--vds-radius-full);
  background: var(--vds-white);
}
.ws-header-group-btn {
  position: relative; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 0 14px; border: none; background: none; cursor: pointer;
  font: var(--vds-weight-medium) 13px/1 var(--vds-font-sans); color: var(--vds-text-primary);
  white-space: nowrap;
}
.ws-header-group-btn:hover, .ws-header-group-btn.is-open { background: var(--vds-surface); }
.ws-header-group-btn--icon { width: 40px; padding: 0; color: var(--vds-neutral-600); }
.ws-header-group-btn--icon svg { width: 17px; height: 17px; }
.ws-header-group-btn--icon .ws-header-iconbtn-dot { top: 6px; right: 10px; }
/* Bell's unseen-notification dot -- hidden unless shared-widgets.js's
   refreshNotifDots finds something newer than the last time the popover
   was opened; red when shown (was an always-on grey placeholder). */
.ws-notif-dot { display: none; background: var(--vds-red-600); }
.ws-notif-dot.is-visible { display: block; }
.ws-header-group-btn:hover .ws-header-iconbtn-dot { box-shadow: 0 0 0 2px var(--vds-surface); }
.ws-header-group-divider { width: 1px; align-self: center; height: 18px; background: var(--vds-border); flex-shrink: 0; }
/* NST mount + its pill (all states) become a borderless middle segment. */
.ws-header-group #ws-nst-root, .ws-header-group #as-nst-root { display: flex; min-width: 0; }
.ws-header .ws-header-group .ws-nst, .as-topbar .ws-header-group .ws-nst {
  height: 100%; border: none; border-radius: 0; background: none;
}
.ws-header .ws-header-group .ws-nst:hover, .ws-header .ws-header-group .ws-nst.is-open,
.as-topbar .ws-header-group .ws-nst:hover, .as-topbar .ws-header-group .ws-nst.is-open { background: var(--vds-surface); }
.ws-header .ws-header-group .ws-nst--loading:hover, .ws-header .ws-header-group .ws-nst--empty:hover,
.as-topbar .ws-header-group .ws-nst--loading:hover, .as-topbar .ws-header-group .ws-nst--empty:hover { background: none; }
.ws-header .ws-header-group .ws-nst--error:hover, .as-topbar .ws-header-group .ws-nst--error:hover { background: var(--vds-red-50); }
/* Retired (per feedback -- Portals goes back to plain vertical centering):
   Line "Portals" up with the NST title, not the pill's midline: the NST's
   label-over-title stack is centered as a whole, which puts the title
   (label + gap) / 2 below center -- so Portals is pushed down by the same
   amount via padding-top = label size (9px) + stack gap (2px), see
   .ws-nst-label/.ws-nst-text below. Only while the stacked trigger is
   actually showing (:has) -- the loading/empty/error states are a single
   centered line, and so is the tablet/phone trigger (reset in the media
   queries at the end of this block).
.ws-header-group:has(.ws-nst-text) .ws-header-group-btn:not(.ws-header-group-btn--icon) { padding-top: calc(9px + 2px); }
*/

/* NST pill -- taller, with the "UP NEXT" micro-label stacked over the title
   (shell.js's/action-station.js's renderNst wrap both in .ws-nst-text).
   Fixed width (not content-sized up to a max) so the header doesn't shift
   as the suggestion changes -- a long title ellipsizes, and the loading/
   empty/error states occupy the same footprint. */
/* .ws-header .ws-nst, .as-topbar .ws-nst { height: 36px; max-width: 240px; padding: 0 14px; gap: 6px; } */
.ws-header .ws-nst, .as-topbar .ws-nst { height: 36px; width: 170px; max-width: none; padding: 0 12px; gap: 6px; }
/* text-align:left -- the trigger is a <button>, which centers its text by
   default, so the shorter "UP NEXT" label was sitting centered over the
   title instead of flush-left with it. */
.ws-header .ws-nst-text, .as-topbar .ws-nst-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; text-align: left; }
.ws-header .ws-nst-label, .as-topbar .ws-nst-label { font: 600 9px/1 var(--vds-font-sans); letter-spacing: .08em; }
.ws-header .ws-nst-title, .as-topbar .ws-nst-title { font: var(--vds-weight-medium) 13px/1.15 var(--vds-font-sans); }
.ws-header .ws-nst-icon--phone, .as-topbar .ws-nst-icon--phone { display: none; }

/* Profile pill -- same 36px height. Scoped to the trigger button only, so
   the dropdown head's own avatar keeps its size. */
.ws-header .ws-profile-btn, .as-topbar .ws-profile-btn { height: 36px; box-sizing: border-box; gap: 8px; padding: 3px 10px 3px 3px; }
.ws-header .ws-profile-btn .ws-profile-avatar, .as-topbar .ws-profile-btn .ws-profile-avatar { width: 27px; height: 27px; font-size: 11px; }

@media (max-width: 1023.98px) {
  /* .ws-header .ws-nst, .as-topbar .ws-nst { max-width: none; padding: 0 12px; } */
  /* Title is hidden at this width (label only), so the fixed desktop width
     would just be empty space -- size to the label instead. */
  .ws-header .ws-nst, .as-topbar .ws-nst { width: auto; max-width: none; padding: 0 12px; }
  /* Title is hidden at this width (label only) -- nothing to line up with.
  .ws-header-group:has(.ws-nst-text) .ws-header-group-btn:not(.ws-header-group-btn--icon) { padding-top: 0; }
  */
}
@media (max-width: 599.98px) {
  /* Same icon-only circle the base .ws-nst phone rule above collapses to --
     restated at this selector's specificity so the rules above don't win. */
  .ws-header .ws-nst, .as-topbar .ws-nst { width: 36px; padding: 0; justify-content: center; }
  .ws-header .ws-nst-text, .as-topbar .ws-nst-text { display: none; }
  .ws-header .ws-nst-icon--phone, .as-topbar .ws-nst-icon--phone { display: inline-flex; }
}
