/* ============================================================================
   CADdy-Hub shared styles — implements styles.md off palette.css tokens.
   RULE: no hardcoded colors here; every color references a palette.css var.
   (palette.css is the single source of color truth.)

   THE LOOK (2026-10 restyle): CADdy-Bid's structure in the Hub's colors. A light blue-gray canvas,
   raised rounded panels with a soft shadow, filled gray controls, pill tabs, bold uppercase table
   headers over a 2px rule, collapsible group bands, a slim header bar and a left icon rail.
   ============================================================================ */

:root {
  /* spacing (4px base; 9/10 for airy hero + section rhythm) */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;
  --sp-9: 96px; --sp-10: 128px;
  /* radius: md = inputs, ctl = filled buttons / tabs / icon buttons (Bid's 10px), lg = panels */
  --radius-sm: 4px; --radius-md: 8px; --radius-ctl: 10px; --radius-lg: 12px; --radius-pill: 999px;
  /* elevation — panels lift (Bid), popovers and modals lift more */
  --elev-1: var(--panel-shadow);
  --elev-2: var(--panel-shadow);
  --elev-3: var(--panel-shadow-lg);
  /* z-index layers. --z-floating: a non-modal window (the Updates panel), over the page's chrome but under
     every modal; opened from inside a modal it joins --z-modal-backdrop instead (project-updates.css).
     --z-search: the header while its search box is open (.ch-header:has(.ch-hsearch.is-focus)), so the
     search card lies over the chat dock and the Updates panel, and still under every modal. */
  --z-sticky: 100; --z-rail: 190; --z-header: 200; --z-dropdown: 300; --z-floating: 398; --z-search: 399;
  --z-modal-backdrop: 400; --z-modal: 401; --z-toast: 500; --z-popover: 600;
  /* type scale */
  --fs-display: clamp(2.25rem, 4.5vw, 3.5rem);
  --fs-h1: 2rem; --fs-h2: 1.5rem; --fs-h3: 1.25rem; --fs-h4: 1.125rem;
  --fs-h5: 1rem; --fs-h6: 0.8125rem; --fs-body: 1rem; --fs-small: 0.875rem; --fs-caption: 0.75rem;
  /* letter-spacing */
  --ls-tight: -0.01em; --ls-normal: 0; --ls-wide: 0.05em; --ls-wider: 0.14em;
  /* layout */
  --page-max: var(--stage-max-width, 1200px);
  --page-max-wide: 1440px;
  --header-h: 46px;            /* CADdy-Bid's header: a 30px logo + 8px top and bottom */
  /* the left rail (header.js renders it on every page with #ch-header) */
  --rail-w-collapsed: 62px;
  --rail-w-open: 220px;
  --rail-bar-h: 64px;          /* the bottom tab bar's height on touch / narrow screens */
  --ctl-h: 38px;               /* filled control height (buttons, inputs, pill tabs) */
}

/* ---- Base ---- */
*, *::before, *::after { box-sizing: border-box; }
/* `hidden` always hides. The browser's own [hidden] rule is display:none at the lowest weight, so any class
   that sets display (.ch-btn inline-flex, .ch-viewas, ...) silently overrode it: a department Admin saw the
   super-admin "Re-estimate testers" and "Departments" buttons, and everyone saw "View as" (A-B2 / S-5).
   Nothing in the Hub sets `hidden` on something it means to show. */
[hidden] { display: none !important; }
html { font-size: 100%; }
body {
  margin: 0;
  font-family: var(--font-family-base);
  font-weight: 400;
  color: var(--font-color);
  background: var(--canvas);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* The rail's width, for any page layout that must stay clear of it (full-bleed widths, position:fixed
     panels anchored to the left). 0 when there is no rail, and 0 in bottom-bar mode, where
     --rail-bottom-h carries the bar's height instead. Use `left: var(--rail-w)` /
     `width: calc(100vw - var(--rail-w))`, never a literal 62px. */
  --rail-w: 0px;
  --rail-bottom-h: 0px;
}
/* Headings — bold (Bid), not light. Weight stays high at every size. */
h1, h2, h3, h4, h5, h6 { margin: 0 0 var(--sp-3); line-height: 1.2; }
h1 { font-size: var(--fs-h1); font-weight: 700; letter-spacing: var(--ls-tight); }
h2 { font-size: var(--fs-h2); font-weight: 700; letter-spacing: var(--ls-tight); }
h3 { font-size: var(--fs-h3); font-weight: 700; }
h4 { font-size: var(--fs-h4); font-weight: 600; }
h5 { font-size: var(--fs-h5); font-weight: 600; }
p  { margin: 0 0 var(--sp-3); }
a  { color: var(--link-color); text-decoration: none; }
a:hover, a:focus-visible { color: var(--link-hover-color); text-decoration: underline; }
:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; }

/* numeric columns align */
.ch-num, td.ch-num, th.ch-num { font-variant-numeric: tabular-nums; text-align: right; }

/* ---- Type utilities ---- */
.ch-eyebrow { display: inline-block; font-size: var(--fs-caption); font-weight: 700; text-transform: uppercase; letter-spacing: var(--ls-wider); color: var(--eyebrow-color); }
.ch-display { font-size: var(--fs-display); font-weight: 800; line-height: 1.05; letter-spacing: var(--ls-tight); color: var(--brand-text); }
.ch-muted { color: var(--secondary-color); }
.ch-faint { color: var(--text-faint); }
/* The one way back to selectable text. palette.css sets `user-select: none` on :root so the Hub feels like an
   app (no accidental highlight on a double-click), which also made project numbers and job names impossible
   to copy (BUG-26-0068). Opt a value in where people paste it elsewhere; do not flip the whole app. */
.ch-selectable { user-select: text; -webkit-user-select: text; cursor: text; }
a.ch-selectable { cursor: pointer; }

/* ============================================================================
   APP SHELL — the header bar (brand + actions) and the left rail (navigation)
   ============================================================================ */
/* CADdy-Bid's header (header.css .header): a gray bar, `padding: 8px 16px`, the logo left and a row of
   icon tiles right (.button-container, 12px apart), no rule under it. --header-bar-bg is Bid's
   --sub-background-color role in Hub grays, light and dark. */
.ch-header {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  height: var(--header-h); padding: 8px 16px;
  background: var(--header-bar-bg); color: var(--font-color);
  position: sticky; top: 0; z-index: var(--z-header);
}
/* The search card is in the header's stacking context: while the box is open the whole header rises over the
   chat dock (--z-dropdown) and the Updates panel (--z-floating), still under the modal backdrops. */
.ch-header:has(.ch-hsearch.is-focus) { z-index: var(--z-search); }
/* The header spans the full window even though the body reserves the rail's gutter (Bid's layout: the
   logo sits in the top-left corner above the rail). */
body.ch-has-rail .ch-header { margin-left: calc(-1 * var(--rail-w)); width: calc(100% + var(--rail-w)); }
.ch-header__brand { display: flex; align-items: center; gap: var(--sp-2); font-weight: 800; font-size: var(--fs-h4); letter-spacing: var(--ls-tight); color: var(--brand-text); }
.ch-header__brand:hover { text-decoration: none; color: var(--brand-text); }
/* The skip link (header.js, WCAG 2.4.1): the page's first Tab stop, out of sight until it has focus, then a
   raised chip over the brand corner. It lands on <main id="main">, which gets the inset ring below so the
   ring is not clipped by the window edge. */
.ch-skip { position: absolute; left: var(--sp-3); top: var(--sp-2); z-index: var(--z-popover);
  transform: translateY(calc(-100% - var(--header-h))); padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-md);
  background: var(--panel); color: var(--font-color); font-weight: 700; box-shadow: var(--panel-shadow-lg); }
.ch-skip:focus, .ch-skip:focus-visible { transform: none; color: var(--font-color); text-decoration: none;
  outline: 2px solid var(--focus-ring-color); outline-offset: 2px; }
main#main:focus-visible { outline-offset: -2px; }
.ch-header__logo { height: 30px; width: auto; max-width: 100%; display: block; }   /* Bid #logo */
/* The brand wrapper, and the CADdy-Bid wordmark that drops out of the bar under it on hover (header.js
   wireBrand). The wrapper stretches to the bar's full height so the panel's top: 100% is the bar's
   bottom edge — no gap for the pointer to fall through on its way down. The panel is absolute, so
   opening it moves nothing; it hangs over the page below the logo. */
/* -8px top and bottom: the wrapper spans the bar's full height (padding included), so the panel's top
   is exactly the bar's bottom edge. */
.ch-brand { position: relative; align-self: stretch; display: flex; align-items: center; flex: 0 1 auto; min-width: 0; margin: -8px 0; }
.ch-brand__menu { position: absolute; top: 100%; left: -16px; z-index: var(--z-dropdown);
  padding: var(--sp-2); background: var(--header-bar-bg);
  border: 1px solid var(--menu-border); border-top: 0; border-radius: 0 0 6px 6px; box-shadow: var(--menu-shadow); }
.ch-brand__bid { display: block; padding: var(--sp-1) var(--sp-2); border-radius: var(--radius-md); }
.ch-brand__bid:hover, .ch-brand__bid:focus-visible { background: var(--control-bg); text-decoration: none; }
/* 24px, not the Hub logo's 30: CADdy-Hub.png carries its own transparent margin and the Bid art is
   cropped tight, so this is the height at which the two wordmarks' letters come out the same size. */
.ch-brand__bidlogo { height: 24px; width: auto; display: block; }
.ch-brand__bidlogo--dark { display: none; }   /* body.dark swaps the pair (palette.css) */
/* LEGACY: the header nav moved into the left rail (.ch-rail). .ch-nav is no longer rendered; the rule is
   kept so any markup still carrying it lays out. .ch-nav__link is now an EXTRA class on every rail link
   (code and the harness find a nav item by `.ch-nav__link[href="/x"]`), so it carries no look of its own. */
.ch-nav { display: flex; gap: var(--sp-5); flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.ch-nav::-webkit-scrollbar { display: none; }
.ch-nav__link.is-disabled { opacity: 0.4; pointer-events: none; }
/* The "needs attention" dot (header.js markReqDot / markApprovalsDot): a small danger dot on the rail
   icon's top-right corner, ringed in the rail color so it reads on the active fill too. */
.ch-nav__dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--danger-color); box-shadow: 0 0 0 2px var(--rail-bg); }
.ch-header__actions { display: flex; align-items: center; gap: 12px; margin-left: auto; flex: 0 1 auto; min-width: 0; }   /* Bid .button-container; the search slot gives when room runs short */
/* The signed-in name: Bid's header shows none, so it heads the options menu (header.js loadUser). */
.ch-header__user { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---- The left rail ----------------------------------------------------------------------------
   <nav class="ch-rail"> (header.js renderRail), fixed under the header: 62px of icons, opening to 220px
   with the labels on hover or keyboard focus. It OVERLAYS the page when open (the gutter stays 62px), so
   hovering it never reflows the content. It is the header bar's gray (--rail-bg = --header-bar), so the
   bar and the rail read as one L-shaped frame, as in Bid. Hover = a lighter wash; active page = Bid's
   brand border over a lighter brand wash; Logout pinned to the bottom. The body reserves the gutter via
   .ch-has-rail and exposes it as --rail-w. */
body.ch-has-rail { --rail-w: var(--rail-w-collapsed); padding-left: var(--rail-w); }
.ch-rail {
  position: fixed; left: 0; top: var(--header-h); bottom: 0; z-index: var(--z-rail);
  width: var(--rail-w-collapsed);
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-3) 7px;
  background: var(--rail-bg); border-right: 1px solid var(--rail-border);
  overflow-x: hidden; overflow-y: auto; scrollbar-width: none; overscroll-behavior: contain;
  transition: width .18s ease, box-shadow .18s ease;
}
.ch-rail::-webkit-scrollbar { display: none; }
.ch-rail:hover, .ch-rail:focus-within { width: var(--rail-w-open); box-shadow: var(--panel-shadow-lg); }
.ch-rail__link {
  position: relative; flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--sp-2);
  height: 42px; padding: 0; border-radius: var(--radius-ctl);
  border: 1px solid transparent;
  color: var(--rail-fg); white-space: nowrap; text-decoration: none;
  transition: background-color .12s ease, border-color .12s ease;
}
.ch-rail__link:hover, .ch-rail__link:focus-visible { background: var(--rail-hover-bg); color: var(--rail-hover-fg); text-decoration: none; }
/* Inside: the rail clips sideways, so an outside ring would be cut off. */
.ch-rail__link:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; }
.ch-rail__link.is-active { background: var(--rail-active-bg); border-color: var(--rail-active-border); color: var(--rail-active-fg); }
.ch-rail__icon { flex: 0 0 auto; width: 46px; height: 40px; display: grid; place-items: center; }
.ch-rail__icon svg, .ch-ico { width: 24px; height: 24px; display: block; flex: 0 0 auto; }
.ch-rail__label { font-size: 0.9375rem; font-weight: 700; opacity: 0; transition: opacity .12s ease; overflow: hidden; text-overflow: ellipsis; padding-right: var(--sp-2); }
.ch-rail:hover .ch-rail__label, .ch-rail:focus-within .ch-rail__label { opacity: 1; }
.ch-rail__link .ch-nav__dot { position: absolute; top: 7px; left: 30px; margin: 0; }
.ch-rail__logout { margin-top: auto; }
.ch-rail__sep { flex: 0 0 auto; height: 1px; margin: var(--sp-1) var(--sp-2); background: var(--hairline); }

/* Touch, or a narrow window: no hover to open the rail, so it becomes a bottom tab bar (Bid). 820px, not
   Bid's 1366: Hub users are on 1366px laptops with a mouse, and those keep the rail. The bar scrolls
   sideways when the items do not fit; `safe center` centres them only while they do. */
@media (hover: none) and (pointer: coarse), (max-width: 820px) {
  body.ch-has-rail {
    --rail-w: 0px;
    --rail-bottom-h: calc(var(--rail-bar-h) + env(safe-area-inset-bottom, 0px));
    padding-left: 0; padding-bottom: var(--rail-bottom-h);
  }
  .ch-rail {
    top: auto; bottom: 0; left: 0; right: 0; width: 100%; height: var(--rail-bottom-h);
    flex-direction: row; justify-content: safe center; align-items: stretch; gap: 2px;
    padding: 6px var(--sp-1) calc(6px + env(safe-area-inset-bottom, 0px));
    border-right: 0; border-top: 1px solid var(--rail-border);
    overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
  }
  .ch-rail:hover, .ch-rail:focus-within { width: 100%; box-shadow: none; }
  .ch-rail__link { flex-direction: column; justify-content: center; gap: 2px; height: auto; min-width: 64px; max-width: 92px; padding: 2px var(--sp-1); }
  .ch-rail__icon { width: 26px; height: 26px; }
  .ch-rail__icon svg { width: 22px; height: 22px; }
  .ch-rail__label { opacity: 1; font-size: 10px; line-height: 1.1; padding: 0; max-width: 100%; }
  .ch-rail__logout { margin-top: 0; }
  .ch-rail__sep { display: none; }
  .ch-rail__link .ch-nav__dot { top: 2px; left: calc(50% + 6px); }
  /* The floating chat and updates bubbles (chat.css / updates-dock.css) sit above the bar, not under it.
     `translate` composes with each file's own bottom offsets, including their phone breakpoint. */
  body.ch-has-rail .chat-bubble, body.ch-has-rail .chat-panel,
  body.ch-has-rail .upd-bubble, body.ch-has-rail .upd-panel { translate: 0 calc(-1 * var(--rail-bottom-h)); }
}

/* Paper has no navigation. */
@media print {
  .ch-rail, .ch-header, #ch-header, .ch-viewas-banner { display: none !important; }
  body.ch-has-rail { padding: 0 !important; --rail-w: 0px; }
}

/* ---- Header icon buttons + swappable icons ----
   .icon-btn is the generic small icon button: transparent until hover. In a modal head it is the close
   button and renders as the filled .ch-modal__x square (see the Modal section).
   In the header it is CADdy-Bid's icon TILE (header.css `.icon`): a 20px icon with 5px padding and a
   5px radius on the bar's own gray (so a tile shows only on hover), fading slowly (1s, Bid's) to
   --header-tile-hover-bg and snapping back. The icon is an inline Lucide line SVG (header.js svgIcon,
   class "ch-ico icon", stroke="currentColor"), so it takes the tile's text color in both themes with no
   per-theme image. The <button> around the tile only carries the click, the keyboard focus and the
   accessible name. */
.icon-btn {
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--radius-md);
  background: transparent; color: var(--secondary-color); cursor: pointer; padding: 0; font-family: inherit;
}
.icon-btn:hover { background: var(--control-bg); color: var(--font-color); }
.ch-header .icon-btn { width: auto; height: auto; border-radius: 5px; background: transparent; color: var(--font-color); }
.ch-header .icon-btn:hover { background: transparent; }
.ch-header .icon-btn .icon,
.ch-header .icon-btn > svg {
  width: 30px; height: 30px; padding: 5px; box-sizing: border-box; border-radius: 5px;
  background-color: var(--header-bar-bg); cursor: pointer; display: block;
}
.ch-header .icon-btn:hover .icon,
.ch-header .icon-btn:hover > svg { transition: 1s; background-color: var(--header-tile-hover-bg); }
.ch-header .icon-btn[aria-expanded="true"] .icon,
.ch-header .icon-btn.is-active .icon { background-color: var(--header-tile-hover-bg); }
.ch-header .icon-btn:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; }
/* The header's line icons are drawn at 20px (16px for search) from a 24px grid, which thins the 2px stroke;
   2.5 brings them back to about 2px on screen. CSS beats the svg's stroke-width attribute; the rail keeps 2. */
.ch-header .icon-btn svg,
.ch-hsearch__ico .ch-ico { stroke-width: 2.5; }
.icon { width: 20px; height: 20px; display: block; }

/* ---- Header search (header.js searchHtml, header-search.js) ---------------------------------------------
   GitHub's header search in Hub tokens. It is the first thing in the right-hand group (.ch-header__actions),
   12px from the icon tiles like they are from each other. At rest: a quiet 280px box ON the bar's gray (no
   fill), a 1px edge, 6px corners, a magnifier, the placeholder and a bordered "/" keycap, all in the rail's
   text color (≥ 6.9:1 on the bar). The box lies absolutely in a fixed slot, so focusing it widens it LEFTWARDS
   over the bar (--hs-open-w, set by header-search.js from the room to its left, at most 560px) into a form
   control on --panel, inside a raised card (.ch-hsearch__panel) that drops over the page: the header never
   moves and stays exactly --header-h (30px content). The card holds the suggestions: matches on this page,
   jobs, the page's active filters, and the keys. The filter tile beside it speaks the same border language
   (30px, 1px edge, 6px corners); a dot on its corner says a filter is on (Bid's .has-filters::after). Below
   640px the box folds to a magnifier tile and opens over the bar. */
.ch-hsearch { flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: var(--sp-2); }
.ch-hsearch__slot { position: relative; flex: 0 1 280px; width: 280px; min-width: 150px; height: 30px; }
.ch-hsearch__box { position: absolute; top: 0; right: 0; width: 100%; height: 30px; box-sizing: border-box;
  display: flex; align-items: center; cursor: text;
  background: transparent; color: var(--hsearch-fg); border: 1px solid var(--hsearch-border); border-radius: 6px;
  transition: border-color .12s ease, background-color .12s ease; }   /* the width snaps open, as GitHub's does */
.ch-hsearch__box:hover { border-color: var(--hsearch-border-hover); }
.ch-hsearch.is-focus .ch-hsearch__box { z-index: 2; width: var(--hs-open-w, 520px); background: var(--panel); color: var(--font-color);
  border-color: var(--input-color); box-shadow: 0 0 0 3px var(--brand-wash); }
.ch-hsearch__ico { flex: 0 0 auto; display: grid; place-items: center; width: 30px; color: inherit; pointer-events: none; }
.ch-hsearch.is-focus .ch-hsearch__ico { color: var(--secondary-color); }
.ch-hsearch__ico .ch-ico { width: 16px; height: 16px; }
.ch-hsearch__input { flex: 1 1 auto; min-width: 0; height: 100%; border: 0; outline: 0; /* a11y-focus: .ch-hsearch.is-focus .ch-hsearch__box */ background: transparent; padding: 0 var(--sp-2) 0 0;
  font-family: inherit; font-size: var(--fs-small); color: inherit; }
.ch-hsearch__input::placeholder { color: var(--hsearch-fg); opacity: 1; }
.ch-hsearch.is-focus .ch-hsearch__input::placeholder { color: var(--text-faint); }
/* GitHub's "/" keycap: a small outlined key, no fill, one even 1px edge (≥ 3:1 on the bar), in the box's own
   text color and weight. Two classes and the box, so the shared .ch-kbd (filled, raised, bold) never wins. */
.ch-hsearch__box .ch-kbd.ch-hsearch__hint { flex: 0 0 auto; margin-right: 6px; min-width: 0; padding: 0 5px; line-height: 16px; pointer-events: none;
  background: transparent; color: inherit; border: 1px solid var(--hsearch-key-border); border-radius: var(--radius-sm); font-weight: 400; }
.ch-hsearch.is-focus .ch-hsearch__hint, .ch-hsearch.has-text .ch-hsearch__hint { display: none; }
/* The count the box shows while its panel is folded aside for the current match (header-search.js reveal). */
.ch-hsearch__boxcount { flex: 0 0 auto; margin-right: var(--sp-2); font-size: var(--fs-caption); font-variant-numeric: tabular-nums;
  white-space: nowrap; color: var(--secondary-color); pointer-events: none; }
.ch-hsearch__boxcount[hidden] { display: none; }
.ch-hsearch__open { display: none !important; }
.ch-hsearch__open .ch-ico { width: 30px; height: 30px; padding: 6px; box-sizing: border-box; border-radius: 5px; color: var(--font-color); }
.ch-hsearch__filter { position: relative; flex: 0 0 auto; }
/* The filter tile: the header tile (its 1s hover fade included) with the box's edge and corners. */
.ch-header .ch-hsearch__filter .icon { padding: 4px; border: 1px solid var(--hsearch-border); border-radius: 6px; }
.ch-header .ch-hsearch__filter:hover .icon { border-color: var(--hsearch-border-hover); }
/* The active-filter dot: 8px, top-right, ringed in the bar's gray so it reads against the tile's hover fill. */
.ch-hsearch__filter.has-filters::after { content: ''; position: absolute; top: -2px; right: -2px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--filter-dot-bg); box-shadow: 0 0 0 2px var(--header-bar-bg); pointer-events: none; }

/* The card the focused box sits in: 8px round the box, its top on the bar's top edge, the suggestions below.
   Inside the header's stacking context (like the brand menu), so it hangs over the page and under dialogs. */
.ch-hsearch__panel { position: absolute; z-index: 1; top: -8px; right: -8px; width: calc(var(--hs-open-w, 520px) + 16px);
  max-height: min(72vh, 600px); box-sizing: border-box; display: flex; flex-direction: column; padding-top: 46px;
  background: var(--panel); color: var(--font-color); border: 1px solid var(--panel-border); border-radius: var(--radius-lg);
  box-shadow: var(--panel-shadow-lg); }
.ch-hsearch__panel[hidden] { display: none; }
.ch-hsearch__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; border-top: 1px solid var(--hairline); padding: var(--sp-1) var(--sp-2); }
.ch-hsearch__body:empty { display: none; }
.ch-hsearch__sec + .ch-hsearch__sec { margin-top: var(--sp-1); padding-top: var(--sp-1); border-top: 1px solid var(--hairline); }
.ch-hsearch__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-2) var(--sp-1); font-size: var(--fs-caption); font-weight: 700; color: var(--secondary-color); }
.ch-hsearch__count { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A row: a job (.ch-hsearch__opt, a link) or a match on this page (.ch-hsearch__hit). Up/Down move through both. */
.ch-hsearch__opt, .ch-hsearch__hit { display: flex; flex-direction: column; gap: 1px; padding: 6px var(--sp-2); border-radius: 6px;
  color: var(--font-color); text-decoration: none; cursor: pointer; }
.ch-hsearch__opt:hover, .ch-hsearch__hit:hover { text-decoration: none; background: var(--control-hover-bg); }
/* The active row (the one Enter acts on): the brand wash, a 3px brand bar on its left edge and its words in
   the brand text color, a job's name and a match's line alike, so it reads without the tint. */
.ch-hsearch__opt.is-active, .ch-hsearch__hit.is-active { background: var(--brand-wash); box-shadow: inset 3px 0 0 var(--primary-color); }
.ch-hsearch__name { font-size: var(--fs-small); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-hsearch__opt.is-active .ch-hsearch__name, .ch-hsearch__hit.is-active .ch-hsearch__snip { color: var(--brand-text); }
.ch-hsearch__hit.is-active .ch-hsearch__snip { font-weight: 600; }
.ch-hsearch__meta { font-size: var(--fs-caption); color: var(--secondary-color); }
.ch-hsearch__snip { font-size: var(--fs-small); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-hsearch__snip mark { background: var(--find-hit-bg); color: var(--find-hit-fg); border-radius: 2px; }
.ch-hsearch__hit.is-current .ch-hsearch__snip mark { background: var(--find-current-bg); color: var(--find-current-fg); }
.ch-hsearch__note { padding: var(--sp-2); font-size: var(--fs-small); color: var(--secondary-color); }
/* A registered page's filters, in the card: how many are on, and the two things to do about them. */
.ch-hsearch__filters { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); padding: var(--sp-2); font-size: var(--fs-small); }
.ch-hsearch__filters-text { flex: 1 1 auto; min-width: 0; color: var(--secondary-color); }
.ch-hsearch__foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-4); padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--hairline); font-size: var(--fs-caption); color: var(--text-faint); }
.ch-hsearch__foot .ch-kbd { padding: 0 5px; line-height: 16px; margin-right: 2px; }
/* "3 of 41 on this page" for a screen reader (role="status"); the head shows the same count. */
.ch-hsearch__status { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* Find on page: every match, and the one Enter is on. No padding, so marking text never moves it. */
mark.ch-find-hit { background: var(--find-hit-bg); color: var(--find-hit-fg); border-radius: 2px; padding: 0; }
mark.ch-find-hit.is-current { background: var(--find-current-bg); color: var(--find-current-fg); box-shadow: 0 0 0 2px var(--find-current-bg); }
@media (max-width: 640px) {
  .ch-hsearch__open { display: inline-flex !important; }
  /* The slot steps out of the row (no box, no gap); the box and its card lie over the whole bar. */
  .ch-hsearch__slot { display: contents; }
  .ch-hsearch__box { display: none; }
  .ch-hsearch.is-open .ch-hsearch__box, .ch-hsearch.is-focus .ch-hsearch__box { display: flex; left: var(--sp-3); right: var(--sp-3); top: 8px; width: auto; z-index: 2; }
  .ch-hsearch.is-open .ch-hsearch__hint { display: none; }
  .ch-hsearch__panel { left: 4px; right: 4px; top: 0; width: auto; }
}
/* The filter popover: a panel under the filter tile, the page's own controls in its body. */
.ch-hfilter { position: fixed; z-index: var(--z-dropdown); width: min(340px, calc(100vw - 16px)); display: flex; flex-direction: column;
  overflow: auto; background: var(--panel); color: var(--font-color); border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg); box-shadow: var(--panel-shadow-lg); }
.ch-hfilter__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-4); border-bottom: 1px solid var(--hairline); }
.ch-hfilter__title { font-weight: 800; font-size: var(--fs-small); }
.ch-hfilter__body { display: grid; gap: var(--sp-3); padding: var(--sp-4); }
.ch-hfilter__body .ch-field { margin: 0; }
/* The panel itself takes focus only as a fallback (nothing focusable left in it): no ring for a mouse user. */
.ch-hfilter:focus:not(:focus-visible) { outline: none; }
/* A checkbox filter in the popover ("Only mine", "Only not yet ordered"): the box, then its words, one
   line, the label's own text as the accessible name. Any page's popover uses it; a page class may add to it. */
.ch-fcheck { display: flex; align-items: center; gap: var(--sp-2); min-height: 24px; font-size: var(--fs-small); font-weight: 600;
  color: var(--font-color); cursor: pointer; }
.ch-fcheck input { flex: 0 0 auto; margin: 0; accent-color: var(--primary-color); cursor: pointer; }
.ch-fcheck input:disabled, .ch-fcheck input:disabled + * { cursor: not-allowed; }

/* ---- Keyboard keys and the shortcuts overlay (shortcuts.js openHelp; the scheduler's "?" opens it too) ----
   A key cap is .ch-kbd; a combination puts .ch-kbd-plus between caps and a sequence .ch-kbd-then ("G then P").
   The overlay is a .ch-modal holding .ch-shortcuts: one column per group, a bold header-blue title over a
   2px rule, rows of keys + what they do. */
.ch-kbd { display: inline-block; min-width: 18px; text-align: center; padding: 2px var(--sp-2);
  border: 1px solid var(--hairline-strong); border-bottom-width: 2px; border-radius: var(--radius-sm);
  background: var(--control-bg); color: var(--font-color); font-family: inherit; font-size: var(--fs-caption); font-weight: 700; }
.ch-kbd-plus { margin: 0 4px; color: var(--text-faint); font-size: var(--fs-caption); }
.ch-kbd-then { margin: 0 6px; color: var(--text-faint); font-size: var(--fs-caption); }
.ch-kbd-help { width: min(980px, 94vw); }
.ch-kbd-help .ch-shortcuts { grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); }
/* Keys at least 128px wide, so the descriptions line up; a wider combination ("Shift + Right-click") takes
   the room it needs and never breaks inside itself (.ch-kbd-chord), and when the column is too narrow for
   both, the description drops under the keys instead of squeezing them into a ragged wrap. */
.ch-kbd-help .ch-sc-row { flex-wrap: wrap; row-gap: 2px; }
.ch-kbd-help .ch-sc-keys { min-width: 128px; max-width: 100%; row-gap: 4px; }
.ch-kbd-help .ch-sc-desc { flex: 1 1 120px; min-width: 0; }
.ch-kbd-chord { display: inline-flex; align-items: center; white-space: nowrap; }
/* A key that is off right now (a `hint` registration whose when() is false): listed so it can be found,
   with dashed caps and the hint under its words. Both stay ≥ 4.5:1 (no opacity). */
.ch-sc-row.is-off .ch-kbd { background: transparent; border-style: dashed; color: var(--secondary-color); }
.ch-sc-hint { display: block; font-size: var(--fs-caption); font-style: italic; color: var(--text-faint); }
.ch-kbd-help__note { margin: var(--sp-5) 0 0; font-size: var(--fs-small); }
.ch-shortcuts { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-5); }
.ch-sc-title { font-weight: 800; color: var(--table-head-fg); margin-bottom: var(--sp-2); padding-bottom: var(--sp-2);
  border-bottom: 2px solid var(--table-head-rule); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--ls-wide); }
.ch-sc-row { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-1) 0; font-size: var(--fs-small); }
.ch-sc-keys { flex: 0 0 auto; display: inline-flex; align-items: center; flex-wrap: wrap; min-width: 96px; }
.ch-sc-desc { color: var(--secondary-color); }
/* The options menu's "Keyboard shortcuts ?" item: the cap sits on the menu's gray. */
.ch-options__item--kbd .ch-kbd { margin-left: var(--sp-1); }
/* A theme toggle drawn as two SVGs (the login page) shows the moon in light and the sun in dark. */
.ch-ico-sun { display: none !important; }
body.dark .ch-ico-sun { display: block !important; }
body.dark .ch-ico-moon { display: none !important; }

/* ---- Square icon button (.ch-iconbtn) — add / filter / delete / move, Bid's filled square ----
   <button class="ch-iconbtn" aria-label="Add"><svg …/></button>. Give it an accessible name (aria-label
   or title): it has no text. Variants: --primary (brand fill), --danger (red on hover), --ghost (no fill
   at rest), --sm (32px, for a toolbar inside a table panel). .is-active / [aria-pressed="true"] = on. */
.ch-iconbtn {
  width: 40px; height: 40px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent; border-radius: var(--radius-ctl); padding: 0;
  background: var(--control-bg); color: var(--font-color);
  font-family: inherit; font-size: var(--fs-h4); font-weight: 700; line-height: 1; cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease;
}
.ch-iconbtn:hover { background: var(--control-hover-bg); text-decoration: none; color: var(--font-color); }
.ch-iconbtn:active { background: var(--control-active-bg); }
.ch-iconbtn svg, .ch-iconbtn img { width: 20px; height: 20px; display: block; }
.ch-iconbtn.is-active, .ch-iconbtn[aria-pressed="true"] { background: var(--tab-active-bg); color: var(--brand-text); border-color: var(--tab-active-border); }
.ch-iconbtn:disabled, .ch-iconbtn.is-disabled { opacity: 0.45; pointer-events: none; }
.ch-iconbtn--primary { background: var(--btn-primary-bg); color: var(--on-brand-text); }
.ch-iconbtn--primary:hover { background: var(--btn-primary-hover-bg); color: var(--on-brand-text); }
.ch-iconbtn--danger:hover { background: var(--danger-bg); color: var(--danger-color); }
.ch-iconbtn--ghost { background: transparent; }
.ch-iconbtn--ghost:hover { background: var(--control-bg); }
.ch-iconbtn--sm { width: 32px; height: 32px; border-radius: var(--radius-md); }
.ch-iconbtn--sm svg, .ch-iconbtn--sm img { width: 16px; height: 16px; }

/* ---- Notifications bell + dropdown ---- */
.ch-notif { position: relative; display: inline-flex; }
.ch-notif__btn { position: relative; }
.ch-notif__bell { font-size: 17px; line-height: 1; display: inline-flex; }
.ch-notif__badge { position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; line-height: 1;
  color: var(--on-brand-text); background: var(--danger-color); border-radius: var(--radius-pill); box-shadow: 0 0 0 2px var(--header-bar-bg); }
/* Shared popover surface: the bell, View as and the options menu all drop one of these. */
/* Bid's .options-menu edge on all three: a --menu-border line, 6px corners, a small shadow. The bell and
   View as panels keep the panel surface (they hold reading text); the options menu is Bid's own, below. */
.ch-notif__panel, .ch-viewas__panel, .ch-options__panel {
  background: var(--panel); border: 1px solid var(--menu-border); border-radius: 6px; box-shadow: var(--menu-shadow); }
.ch-notif__panel { position: absolute; top: calc(100% + 7px); right: 0; z-index: var(--z-dropdown); width: min(360px, 92vw); overflow: hidden; }
.ch-notif__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--hairline); }
.ch-notif__head span { font-weight: 700; font-size: var(--fs-small); }
.ch-notif__readall { border: 0; background: transparent; color: var(--link-color); font-family: inherit; font-size: var(--fs-caption); font-weight: 700; cursor: pointer; padding: 0; }
.ch-notif__readall:hover { text-decoration: underline; }
.ch-notif__body { max-height: 60vh; overflow: auto; }
.ch-notif__empty { padding: var(--sp-5) var(--sp-4); text-align: center; color: var(--secondary-color); font-size: var(--fs-small); }
.ch-notif__item { display: grid; grid-template-columns: 1fr auto; gap: 2px var(--sp-3); width: 100%; text-align: left;
  border: 0; border-bottom: 1px solid var(--hairline); background: transparent; font-family: inherit; cursor: pointer; padding: var(--sp-3) var(--sp-4); }
.ch-notif__item:hover { background: var(--panel-2); }
.ch-notif__item.is-unread { background: var(--brand-wash); }
.ch-notif__item.is-unread:hover { background: var(--panel-2); }
.ch-notif__item-title { grid-column: 1; font-size: var(--fs-small); font-weight: 700; color: var(--font-color); }
.ch-notif__item-job { grid-column: 1; font-size: var(--fs-caption); color: var(--font-color); }
.ch-notif__item-meta { grid-column: 1; font-size: var(--fs-caption); color: var(--secondary-color); }
.ch-notif__item-time { grid-column: 2; grid-row: 1; font-size: var(--fs-caption); color: var(--secondary-color); white-space: nowrap; }

/* ---- "View as" (super admin previews the Hub as a role, a person, or a company) ---- */
.ch-viewas { position: relative; display: inline-flex; }
/* header.js renders View as and the bell `hidden` and reveals them for the people they are for
   (canViewAs; internal users). The display rule above would otherwise beat the attribute and show
   View as to everyone. */
.ch-viewas[hidden], .ch-notif[hidden] { display: none; }
/* View as is Bid's role-picker tile (the friends icon); while previewing it stays lit and the banner
   under the header says what is being previewed. The panel is a column: a title, the Role / Person /
   Company pill tabs, the chosen mode's body (scrolls when tall), and a foot with the log note and Apply. */
.ch-viewas__panel { position: absolute; top: calc(100% + 7px); right: 0; z-index: var(--z-dropdown);
  width: min(384px, calc(100vw - 2 * var(--sp-4))); max-height: calc(100vh - var(--header-h) - var(--rail-bottom-h) - var(--sp-6));
  display: flex; flex-direction: column; overflow: hidden; }
.ch-viewas__empty { color: var(--secondary-color); font-size: var(--fs-small); padding: var(--sp-3) 0; }
.ch-viewas__panel > .ch-viewas__empty { padding: var(--sp-4); }
.ch-viewas__top { padding: var(--sp-4) var(--sp-4) var(--sp-3); }
.ch-viewas__title { font-weight: 700; font-size: var(--fs-body); }
.ch-viewas__sub { font-size: var(--fs-caption); color: var(--text-faint); margin-top: 2px; }
/* Two classes: .ch-tabs (later in this file) would otherwise win back its own padding and margin. */
.ch-viewas__panel .ch-viewas__tabs { margin: 0; padding: 2px var(--sp-4) var(--sp-1); flex: 0 0 auto; }
.ch-viewas__tabs .ch-tab { flex: 1 1 0; justify-content: center; min-height: 34px; padding: var(--sp-1) var(--sp-3); }
.ch-viewas__body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: var(--sp-3) var(--sp-4) var(--sp-2); }
.ch-viewas__field { display: flex; flex-direction: column; gap: var(--sp-1); margin-bottom: var(--sp-3); }
.ch-viewas__field--flush { margin-bottom: var(--sp-1); }
.ch-viewas__field > span { font-size: var(--fs-caption); font-weight: 700; color: var(--secondary-color); }
.ch-viewas__field .ch-select { width: 100%; }
.ch-viewas__hint { margin: 0 0 var(--sp-3); font-size: var(--fs-caption); color: var(--text-faint); }
/* The person typeahead: a search box over a listbox. The options are not focusable themselves; the box
   keeps focus and names the active one (aria-activedescendant), which is drawn like a row hover. */
.ch-viewas__search { width: 100%; }
.ch-viewas__list { list-style: none; margin: var(--sp-2) 0 0; padding: var(--sp-1); max-height: 264px; overflow: auto;
  background: var(--panel); border: 1px solid var(--hairline); border-radius: var(--radius-md); }
.ch-viewas__opt { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2); border-radius: var(--radius-sm); cursor: pointer; }
.ch-viewas__opt:hover, .ch-viewas__opt.is-active { background: var(--row-hover-bg); }
.ch-viewas__opt.is-selected { background: var(--row-selected-bg); box-shadow: inset 3px 0 0 var(--tab-active-border); }
.ch-viewas__avatar { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px;
  border-radius: var(--radius-pill); background: var(--brand-wash); color: var(--brand-text); font-size: var(--fs-caption); font-weight: 800; }
.ch-viewas__opt-text { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.ch-viewas__opt-name, .ch-viewas__opt-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-viewas__opt-name { font-size: var(--fs-small); font-weight: 600; color: var(--font-color); }
.ch-viewas__opt-meta { font-size: var(--fs-caption); color: var(--secondary-color); }
.ch-viewas__none { padding: var(--sp-3) var(--sp-2); font-size: var(--fs-small); color: var(--text-faint); }
.ch-viewas__count { margin-top: var(--sp-1); font-size: var(--fs-caption); color: var(--text-faint); }
/* Company: the picked company with its Test / External badge, then "A role" / "A person" chips. */
.ch-viewas__picked { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); margin: calc(-1 * var(--sp-1)) 0 var(--sp-1); }
.ch-viewas__picked-name { font-weight: 700; font-size: var(--fs-small); }
.ch-viewas__picked-meta { font-size: var(--fs-caption); color: var(--secondary-color); }
.ch-viewas__kind { margin-bottom: var(--sp-3); }
.ch-viewas__error { margin: 0 var(--sp-4) var(--sp-2); padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-md);
  background: var(--danger-bg); color: var(--danger-color); font-size: var(--fs-small); font-weight: 600; }
.ch-viewas__error[hidden] { display: none; }
.ch-viewas__foot { flex: 0 0 auto; border-top: 1px solid var(--hairline); background: var(--panel-2); padding: var(--sp-3) var(--sp-4); }
.ch-viewas__note { margin: 0 0 var(--sp-2); font-size: var(--fs-caption); color: var(--secondary-color); }
.ch-viewas__actions { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
/* The banner under the header while previewing: who, the "acting as" note, and the way back. The brand
   tone for a person, a role or an external company; the warning tone for a test-company sandbox. */
.ch-viewas-banner { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4);
  background: var(--brand-active); color: var(--on-brand-text); font-size: var(--fs-small); font-weight: 600; padding: var(--sp-2) var(--sp-4); text-align: center; }
.ch-viewas-banner b { font-weight: 800; }
.ch-viewas-banner__note { font-size: var(--fs-caption); font-weight: 400; }
.ch-viewas-banner__tag { font-weight: 700; }
.ch-viewas-banner button { border: 1px solid currentColor; background: transparent; color: inherit;
  font-family: inherit; font-size: var(--fs-caption); font-weight: 700; border-radius: var(--radius-md); padding: 2px var(--sp-3); cursor: pointer; }
.ch-viewas-banner button:hover { background: var(--on-brand-text); color: var(--brand-active); }
.ch-viewas-banner button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.ch-viewas-banner--sandbox { background: var(--warning-bg); color: var(--warning-color); box-shadow: inset 0 -1px 0 var(--warning-color); }
.ch-viewas-banner--sandbox button:hover { background: var(--warning-color); color: var(--panel); }

/* ---- Options menu (Catalog, admin pages, Company logo, Send Feedback) — far-right dropdown ---- */
.ch-options { position: relative; display: inline-flex; }
/* Bid's .options-menu: the bar's gray, a --menu-border edge, 6px corners, centered 13px items at 8px 12px,
   hovering to --menu-hover-bg. */
.ch-options__panel { position: absolute; top: calc(100% + 7px); right: -1px; z-index: var(--z-dropdown); min-width: 170px; max-width: min(280px, 92vw);
  background: var(--header-bar-bg); text-align: center; overflow: hidden; padding: 0; }
/* Both <a> and <button> live in this menu — an item that navigates is a link, one that opens a dialog
   is a button. The resets are what keep the two indistinguishable. */
.ch-options__item { display: block; width: 100%; text-align: center; padding: 8px 12px; border-radius: 6px; white-space: nowrap;
  font-size: 13px; font-weight: 400; color: var(--font-color); line-height: 1.4;
  border: 0; background: transparent; font-family: inherit; cursor: pointer; text-decoration: none; }
.ch-options__item:hover, .ch-options__item:focus-visible { text-decoration: none; background: var(--menu-hover-bg); color: var(--font-color); }
.ch-options__item:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; }
/* The signed-in person, at the head of the menu. */
.ch-options__who { padding: 8px 12px; font-size: 13px; color: var(--font-color); border-bottom: 1px solid var(--menu-border); }

/* ---- Standard delete / remove control — trash-can icon ----
   The icon is a <span class="ch-del-ico" aria-hidden="true"> INSIDE the button so per-page button rules
   (which use the `background:` shorthand) can't clobber it: Lucide's trash-2 line icon (--ico-trash) as a
   mask, painted in a color token, so it follows the theme with no per-theme swap. In a .ch-del-btn it is
   the muted text color and turns danger red on hover; inside a labelled .ch-btn it takes the button's text
   color. .ch-del-btn is a bare button reset for controls that had no prior styling; buttons that already
   have their own class keep it (transparent bg + hover). */
.ch-del-btn { display: inline-flex; align-items: center; justify-content: center; border: 0; background: transparent;
  cursor: pointer; padding: 2px var(--sp-2); border-radius: var(--radius-md); flex: 0 0 auto; line-height: 0; }
.ch-del-btn:hover { background: var(--danger-bg); }
.ch-del-btn:disabled { opacity: 0.4; cursor: default; }
.ch-del-btn:disabled:hover { background: transparent; }
.ch-del-ico { width: 15px; height: 15px; display: inline-block; vertical-align: middle; flex: 0 0 auto; background-color: currentColor;
  -webkit-mask: var(--ico-trash) no-repeat center / contain; mask: var(--ico-trash) no-repeat center / contain; }
.ch-del-btn .ch-del-ico { background-color: var(--secondary-color); }
.ch-del-btn:hover .ch-del-ico { background-color: var(--danger-color); }
.ch-del-btn:disabled .ch-del-ico { background-color: var(--secondary-color); }
/* When the icon sits before a text label (e.g. a "Delete" danger button), space them apart. */
.ch-btn .ch-del-ico { margin-right: var(--sp-1); vertical-align: -2px; }

/* ---- Page container ---- */
/* The bottom padding clears the floating chat and updates bubbles (50px, 24px from the bottom: 90px with
   a gap), so the last row of a page can always scroll out from under them. */
.ch-page { margin: 0 auto; padding: var(--sp-5) var(--sp-5) var(--fab-clear, 90px); }
.ch-page--wide { max-width: none }
.ch-page--full { max-width: none; } /* edge-to-edge (procurement Master BOM & Orders) */
.ch-page__title { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); margin-bottom: var(--sp-5); }
.ch-page__titletext { display: flex; flex-direction: column; gap: var(--sp-1); }
.ch-page__titletext h1 { margin: 0; }
/* The right-hand half of a page title. It had no rule at all, so two buttons in one of these butted
   straight into each other; the class was always part of the shell object and this is its home. */
.ch-page__actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; justify-content: flex-end; }
.ch-page__actions:empty { display: none; }
/* A page's toolbar row (search box left, icon buttons right), Bid's projects-page head. */
.ch-toolbar { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-4); }
.ch-toolbar__spacer { flex: 1 1 auto; }

/* ---- Buttons — filled, rounded, bold (Bid) ----
   Neutral = filled gray that darkens on hover (a bare .ch-btn and --secondary look the same);
   --primary = the Hub blue; --ghost = text-only until hover; --danger = red fill. */
.ch-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: var(--ctl-h); padding: var(--sp-2) var(--sp-4);
  border: 1px solid transparent; border-radius: var(--radius-ctl); cursor: pointer;
  font-family: inherit; font-weight: 700; font-size: var(--fs-small); line-height: 1.2;
  background: var(--control-bg); color: var(--font-color);
  text-decoration: none; white-space: nowrap;
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.ch-btn:hover { text-decoration: none; background: var(--control-hover-bg); color: var(--font-color); }
.ch-btn:active { background: var(--control-active-bg); }
/* A row-level action inside a table, where a full-height button would set the row height. */
.ch-btn--sm { min-height: 30px; padding: var(--sp-1) var(--sp-3); font-size: var(--fs-caption); border-radius: var(--radius-md); }

.ch-btn:disabled, .ch-btn.is-disabled { opacity: 0.5; pointer-events: none; }
.ch-btn--primary { background: var(--btn-primary-bg); color: var(--on-brand-text); }
.ch-btn--primary:hover { background: var(--btn-primary-hover-bg); color: var(--on-brand-text); }
.ch-btn--primary:active { background: var(--btn-primary-hover-bg); }
.ch-btn--secondary { background: var(--control-bg); color: var(--font-color); }
.ch-btn--secondary:hover { background: var(--control-hover-bg); color: var(--font-color); }
.ch-btn--ghost { background: transparent; color: var(--brand-text); }
.ch-btn--ghost:hover { background: var(--control-bg); color: var(--brand-text); }
.ch-btn--danger { background: var(--btn-danger-bg); color: var(--on-brand-text); }
.ch-btn--danger:hover { background: var(--btn-danger-hover-bg); color: var(--on-brand-text); }
/* Dark mode: half opacity fades a filled blue / red button toward the near-black panel, which reads as its
   darker hover (a live button), not as off. Off there is the muted control fill with faint text instead,
   so it reads as off the way light mode's pale wash does. */
body.dark .ch-btn--primary:disabled, body.dark .ch-btn--primary.is-disabled,
body.dark .ch-btn--danger:disabled, body.dark .ch-btn--danger.is-disabled { background: var(--control-bg); color: var(--text-faint); opacity: 1; }
.ch-btn--lg { min-height: 44px; padding: var(--sp-3) var(--sp-5); font-size: var(--fs-body); }
/* A pressed / selected neutral button (a toggle that is on). */
.ch-btn.is-active, .ch-btn[aria-pressed="true"] { background: var(--tab-active-bg); color: var(--brand-text); border-color: var(--tab-active-border); }

/* ---- Cards & tiles — raised rounded panels ---- */
.ch-card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow); padding: var(--sp-5); margin-bottom: var(--sp-5); }
/* A plain raised panel with no padding, for content that brings its own (a table, a list). */
.ch-panel { background: var(--panel); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow); }
/* A section heading inside or above a panel (Bid's "Devices" / "Controllers"). */
.ch-section-title { font-size: var(--fs-h4); font-weight: 700; margin: 0 0 var(--sp-3); color: var(--font-color); }

.ch-tile-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--sp-4); }
.ch-tile {
  display: flex; flex-direction: column; gap: var(--sp-2);
  background: var(--card-bg); border: 1px solid var(--card-border);
  border-radius: var(--radius-lg); box-shadow: var(--panel-shadow);
  padding: var(--sp-5); color: var(--font-color); position: relative;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.ch-tile:hover { border-color: var(--primary-color); box-shadow: var(--panel-shadow-lg); transform: translateY(-1px); text-decoration: none; color: var(--font-color); }
.ch-tile__icon { width: 44px; height: 44px; border-radius: var(--radius-ctl); background: var(--brand-wash); display: flex; align-items: center; justify-content: center; color: var(--brand-text); font-size: 22px; }
.ch-tile__icon svg { width: 24px; height: 24px; }
.ch-tile__title { font-size: var(--fs-h4); font-weight: 700; }
.ch-tile__sub { font-size: var(--fs-small); color: var(--secondary-color); }
.ch-tile__badge { position: absolute; top: var(--sp-4); right: var(--sp-4); background: var(--info-bg); color: var(--info-color); border-radius: var(--radius-pill); padding: 2px var(--sp-2); font-size: var(--fs-caption); font-weight: 700; }
.ch-tile--disabled { opacity: 0.6; pointer-events: none; }
.ch-tile--disabled .ch-tile__badge { background: var(--status-stock-bg); color: var(--status-stock-fg); }

/* ---- Status badges (line + rollup) ---- */
.ch-badge { display: inline-block; padding: 3px var(--sp-2); border-radius: var(--radius-pill); font-size: var(--fs-caption); font-weight: 700; line-height: 1.3; text-transform: uppercase; letter-spacing: var(--ls-wide); white-space: nowrap; }
.ch-badge--stock     { background: var(--status-stock-bg);     color: var(--status-stock-fg); }
.ch-badge--pending   { background: var(--status-pending-bg);   color: var(--status-pending-fg); }
.ch-badge--ordered   { background: var(--status-ordered-bg);   color: var(--status-ordered-fg); }
.ch-badge--received  { background: var(--status-received-bg);  color: var(--status-received-fg); }
.ch-badge--delivered { background: var(--status-delivered-bg); color: var(--status-delivered-fg); }
.ch-badge--cancelled { background: var(--status-cancelled-bg); color: var(--status-cancelled-fg); }
.ch-badge--freetext  { background: var(--info-bg);             color: var(--info-color); }
/* Semantic badges for anything that is not a requisition status. */
.ch-badge--success { background: var(--success-bg); color: var(--success-color); }
.ch-badge--warning { background: var(--warning-bg); color: var(--warning-color); }
.ch-badge--danger  { background: var(--danger-bg);  color: var(--danger-color); }
.ch-badge--info    { background: var(--info-bg);    color: var(--info-color); }
.ch-badge--neutral { background: var(--status-stock-bg); color: var(--status-stock-fg); }

/* ============================================================================
   DATA TABLE — inside a rounded panel; bold uppercase tracked headers in the header color over a 2px
   rule; thin column dividers and row rules; hover. Alignment follows the Hub's rules (styles.md §4),
   which beat Bid's centered cells: text left, numbers right, header follows its cells.
   ============================================================================ */
/* ---- table scroll box + sticky header ----
   The header row is sticky, and this is what makes it actually work.

   The trap: `overflow-x: auto` does NOT leave the other axis alone. Per spec, when one axis is
   non-visible the other computes to `auto` — so this box was already a scroll container, just an
   unbounded one. A sticky <th> sticks to its nearest scrollport, and an unbounded box never scrolls, so
   the header sat at the top of the table and rode the PAGE out of view. Measured before this change:
   after scrolling 900px the header was 783px above the top of the screen.

   So the box is only made a scroll container when the table genuinely does not fit — chUI.stickyTables
   toggles .is-xscroll from a ResizeObserver. In the ordinary case the box does not clip, and the header
   sticks to the VIEWPORT, just under the app header, which is what "scroll down and still see the
   column" actually means. When a table IS too wide, the box scrolls in both axes and the header sticks
   to the top of the box instead — less good, but the alternative is scrolling the whole page sideways.

   THE PANEL. The wrap is the rounded panel. It cannot clip (that would break the sticky header above), so
   it carries 4px of padding instead: at a 12px radius the corner arc sits inside 3.5px, so the header
   cells' square corners never poke out of the rounded ones. Inside a .ch-card or a modal (already a
   panel) the wrap goes flat again. */
.ch-table-wrap { overflow: visible; background: var(--panel); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow); padding: var(--sp-1); }
.ch-table-wrap.is-xscroll { overflow: auto; max-height: calc(100vh - var(--header-h) - 6rem);
  overscroll-behavior: contain; padding: 0; }
/* A scrolling wrap clips to its own rounded corners, so it needs no inner padding (which showed as a
   see-through strip beside sticky columns). Its scrollbar is on tokens in both themes. */
.ch-table-wrap { scrollbar-color: var(--scroll-color) transparent; }
.ch-card .ch-table-wrap, .ch-panel .ch-table-wrap, .ch-modal .ch-table-wrap, .ch-group__body .ch-table-wrap, .ch-table-wrap .ch-table-wrap {
  background: transparent; border: 0; border-radius: 0; box-shadow: none; padding: 0; }
.ch-table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); background: transparent; }
/* The rule under the header is drawn with an inset shadow rather than border-bottom: with
   border-collapse the border belongs to the table grid, not the cell, so it stays behind while a stuck
   header floats over the rows. The opaque background is load-bearing for the same reason. The column
   divider on a header cell is a second inset shadow for the same reason. */
.ch-table th { position: sticky; top: var(--header-h); background: var(--table-head-bg); color: var(--table-head-fg); text-align: left; font-size: var(--fs-caption); font-weight: 800; text-transform: uppercase; letter-spacing: var(--ls-wide); padding: var(--sp-3); box-shadow: inset 0 -2px 0 var(--table-head-rule); z-index: var(--z-sticky); }
.ch-table th + th { box-shadow: inset 0 -2px 0 var(--table-head-rule), inset 1px 0 0 var(--hairline); }
.ch-table td { padding: var(--sp-3); border-bottom: 1px solid var(--hairline); }
.ch-table td + td { border-left: 1px solid var(--hairline); }
/* The last row sits on the panel's own edge; a rule there reads as a stray line. */
.ch-table > tbody:last-child > tr:last-child > td { border-bottom: 0; }
/* Inside a scroll box the header sticks to the box, not to the page, so the app-header offset would
   push it down by that much again. */
.ch-table-wrap.is-xscroll .ch-table th { top: 0; }
/* ---- Column tools: drag-to-resize handle + the checkbox popover (chUI.checkMenu / columnMenu) ----
   Shared, because three grids already wanted them and each had grown its own copy. The handle is absolute
   inside its `th`; `.ch-table th` is position:sticky, which is a positioned ancestor, so it anchors there
   without any extra rule. It stays invisible until hovered so a header row doesn't read as a row of pipes. */
.col-resize { position: absolute; top: 0; right: 0; width: 8px; height: 100%; cursor: col-resize; user-select: none; }
/* Finding the handle used to mean hunting for an invisible 8px strip. Hovering ANYWHERE on the header now
   draws a faint divider on its right edge — that is the grab point, shown before you have to guess — and
   hovering the handle itself promotes it to the brand color so the target is unmistakable. A 2px rule
   inside the 8px hit area, so the mark is precise while the target stays easy to hit. */
th:hover > .col-resize::before,
.col-resize::before { content: ''; position: absolute; top: 15%; bottom: 15%; right: 3px; width: 2px; background: transparent; border-radius: 1px; transition: background 90ms linear; }
th:hover > .col-resize::before { background: var(--hairline-strong); }
.col-resize:hover::before { top: 0; bottom: 0; background: var(--primary-color); }
body.col-resizing { cursor: col-resize; user-select: none; }
body.col-resizing * { pointer-events: none; }   /* a drag must not hover-highlight rows underneath it */

.ch-checkmenu { position: fixed; z-index: var(--z-popover); display: flex; flex-direction: column; max-height: 320px;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow-lg); overflow: hidden; }
.ch-checkmenu__head { padding: var(--sp-2) var(--sp-3); font-size: var(--fs-caption); font-weight: 800; text-transform: uppercase;
  letter-spacing: var(--ls-wide); color: var(--table-head-fg); border-bottom: 1px solid var(--hairline); }
.ch-checkmenu__list { overflow-y: auto; padding: var(--sp-1); }
.ch-checkmenu__row { display: flex; align-items: center; gap: var(--sp-2); padding: 6px var(--sp-2); border-radius: var(--radius-md); font-size: var(--fs-small); cursor: pointer; white-space: nowrap; }
.ch-checkmenu__row:hover { background: var(--control-bg); }
.ch-checkmenu__row input { cursor: pointer; margin: 0; accent-color: var(--primary-color); }
.ch-checkmenu__foot { padding: var(--sp-2); border-top: 1px solid var(--hairline); text-align: right; }
.ch-checkmenu__empty { padding: var(--sp-2) var(--sp-3); font-size: var(--fs-small); color: var(--secondary-color); }

/* A numeric header has to sit over its numbers. `th.ch-num` (line ~70) and `.ch-table th` above have
   the SAME specificity, so the later left-align was silently winning and every numeric column in the
   app had a left header over right-aligned figures. This re-asserts it with the table scope. */
.ch-table th.ch-num { text-align: right; }
.ch-table tbody tr:hover { background: var(--panel-2); }
.ch-table tbody tr.is-selected { background: var(--row-selected-bg); }

/* Sortable tables (opt in with `ch-sortable` on the table; see styles.md). A header cell that can sort gets
   `ch-sort-th`; the active column shows an ▲/▼ arrow. Wired generically by chUI.sortable(). */
.ch-sort-th { cursor: pointer; user-select: none; }
.ch-sort-th:hover { color: var(--font-color); }
.th-sort { margin-left: var(--sp-1); font-size: .75em; color: var(--brand-text); }

/* ---- Collapsible group band (Bid's status band) ----------------------------------------------
   <section class="ch-group">                                 (.is-collapsed hides the body)
     <button class="ch-group__head" aria-expanded="true">     a tinted rounded bar
       <span class="ch-group__chev"></span>                   chevron: right when closed, down when open
       <span class="ch-group__title">Ready for pricing</span> bold uppercase
       <span class="ch-group__count">(1)</span>
       <span class="ch-group__aside">…</span>                 optional, pushed right (a total, a button)
     </button>
     <div class="ch-group__body"> … a .ch-table … </div>
   </section>
   The band is tinted with --band-bg / --band-fg when a page sets them (a status: e.g.
   `style="--band-bg: var(--project-status-pricing-bg); --band-fg: var(--project-status-header-text)"`),
   else the neutral blue-gray --group-band-bg. The panel under an open band joins it (square top corners).
   Wire the toggle in page JS (flip aria-expanded and .is-collapsed); this is only the look. */
.ch-group { margin-bottom: var(--sp-4); background: var(--panel); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow); }
.ch-group__head {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  padding: var(--sp-3) var(--sp-4); margin: 0;
  background: var(--band-bg, var(--group-band-bg)); color: var(--band-fg, var(--group-band-fg));
  border: 0; border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
  font-family: inherit; font-size: 0.95rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em;
  text-align: left; cursor: pointer;
}
.ch-group.is-collapsed > .ch-group__head, .ch-group__head[aria-expanded="false"] { border-radius: calc(var(--radius-lg) - 1px); }
.ch-group__head:hover { filter: brightness(0.97); }
.ch-group__head:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; }
.ch-group__chev { flex: 0 0 auto; width: 14px; height: 14px; display: inline-grid; place-items: center; transition: transform .12s ease; transform: rotate(90deg); }
.ch-group__chev::before { content: ''; width: 7px; height: 7px; border-right: 2.5px solid currentColor; border-bottom: 2.5px solid currentColor; transform: translateX(-1px) rotate(-45deg); }
.ch-group.is-collapsed .ch-group__chev, .ch-group__head[aria-expanded="false"] .ch-group__chev { transform: rotate(0deg); }
.ch-group__title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-group__count { font-weight: 700; }
.ch-group__aside { margin-left: auto; text-transform: none; letter-spacing: normal; font-weight: 700; }
.ch-group__body { padding: var(--sp-1); }
.ch-group.is-collapsed > .ch-group__body { display: none; }
/* A band with no panel under it (a stack of bands, Bid's projects list): the band alone. */
.ch-group--bare { background: transparent; border: 0; box-shadow: none; }
.ch-group--bare > .ch-group__head { border-radius: calc(var(--radius-lg) - 1px); }

/* Shared category catalog picker (window.chCatalogPicker) — search + category filter, results grouped
   under sticky category headers. Used by the Master BOM and the change-order item search. */
.chcat-modal { width: min(680px, 96vw); max-height: 88vh; display: flex; flex-direction: column; }
.chcat-modal .chcat-body { display: flex; flex-direction: column; gap: var(--sp-3); min-height: 0; }
.chcat-filters { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.chcat-filters .chcat-search { flex: 1 1 220px; min-width: 0; }
.chcat-filters .chcat-cat { flex: 0 0 auto; min-width: 200px; }
.chcat-results { overflow-y: auto; display: flex; flex-direction: column; gap: 1px; min-height: 240px; max-height: 56vh;
  border: 1px solid var(--hairline); border-radius: var(--radius-md); padding: var(--sp-2); }
.chcat-band { position: sticky; top: 0; z-index: 1; background: var(--group-band-bg); color: var(--group-band-fg);
  font-size: var(--fs-caption); font-weight: 800; text-transform: uppercase; letter-spacing: var(--ls-wide);
  padding: var(--sp-2); border-radius: var(--radius-md); margin-top: var(--sp-1); }
.chcat-band:first-child { margin-top: 0; }
.chcat-opt { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; text-align: left; width: 100%;
  border: 0; background: transparent; font-family: inherit; cursor: pointer; padding: var(--sp-2); border-radius: var(--radius-md); }
.chcat-opt:hover { background: var(--brand-wash); }
.chcat-opt__name { font-size: var(--fs-small); font-weight: 600; color: var(--font-color); }
.chcat-opt__meta { font-size: var(--fs-caption); color: var(--secondary-color); }
.chcat-msg, .chcat-hint { padding: var(--sp-2); font-size: var(--fs-caption); color: var(--secondary-color); }

/* ---- Form controls — FILLED (Bid): gray at rest, no border, radius 8px, a brand outline on focus ---- */
.ch-field { display: flex; flex-direction: column; gap: var(--sp-1); margin-bottom: var(--sp-3); }
.ch-label { font-size: var(--fs-small); font-weight: 600; }
.ch-label .ch-req { color: var(--danger-color); }
.ch-input, .ch-select, .ch-textarea {
  font-family: inherit; font-size: var(--fs-body); color: var(--font-color);
  background: var(--control-bg); border: 1px solid transparent;
  border-radius: var(--radius-md); padding: 8px var(--sp-3); min-height: var(--ctl-h);
  transition: border-color 0.12s ease, box-shadow 0.12s ease, background-color 0.12s ease;
}
.ch-textarea { min-height: 80px; }
/* Full opacity: --secondary-color is 5.1:1 on the filled gray, and still reads well under an entered value. */
.ch-input::placeholder, .ch-textarea::placeholder { color: var(--secondary-color); opacity: 1; }
.ch-input:hover, .ch-select:hover, .ch-textarea:hover { background: var(--control-hover-bg); }
.ch-input:focus, .ch-select:focus, .ch-textarea:focus { outline: none; background: var(--panel); border-color: var(--input-color); box-shadow: 0 0 0 3px var(--brand-wash); }
.ch-input:disabled, .ch-select:disabled, .ch-textarea:disabled { opacity: 0.6; cursor: default; }
.ch-input.is-invalid, .ch-select.is-invalid, .ch-textarea.is-invalid,
.ch-field.is-invalid .ch-input { border-color: var(--danger-color); background: var(--danger-bg); }
.ch-field__error { color: var(--danger-color); font-size: var(--fs-caption); }
/* A search box (Bid's projects search): a filled input with room for a leading glyph if a page adds one. */
.ch-search { min-width: 220px; }

/* ---- Modal — panel color, radius 12px, a head row with a bottom rule, buttons right-aligned ---- */
.ch-modal-backdrop { position: fixed; inset: 0; background: var(--modal-backdrop); z-index: var(--z-modal-backdrop); display: flex; align-items: center; justify-content: center; }
.ch-modal { background: var(--panel); color: var(--font-color); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow-lg); z-index: var(--z-modal); max-width: 90vw; max-height: 90vh; overflow: auto; }
.ch-modal__head { padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--hairline); font-weight: 700; font-size: var(--fs-h5); }
/* A head that is a title plus a ✕ (the common `<span>Title</span><button class="icon-btn">✕</button>`)
   lays out as a row with the ✕ at the right. Only when a button is a direct child, so a head that is
   text with an inline tag in it (`Title <span class="bj-co">CO</span>`) keeps flowing as text. */
.ch-modal__head:has(> button) { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.ch-modal__head > h3, .ch-modal__head > h2 { margin: 0; font-size: inherit; }
.ch-modal__body { padding: var(--sp-5); }
/* A small dialog (a confirm-sized form). */
.ch-modal--narrow { width: min(440px, 94vw); }
.ch-modal__foot { padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--hairline); display: flex; justify-content: flex-end; gap: var(--sp-2); }

/* PANE MODAL — the header stays put and only the body scrolls. A plain .ch-modal scrolls as one piece,
   so on a long record the title, the ✕ and the ways out scroll away with it and the only exit left is
   hunting for the backdrop. Here the header (title left; actions and ✕ right) and any footer are pinned
   and the body is the scroll pane. Opt-in with .ch-modal--pane rather than a change to .ch-modal, because
   some modals put content outside __body and would lose it to overflow: hidden.
   Used by the WIP job peek and the Projects detail modal. */
.ch-modal--pane { display: flex; flex-direction: column; overflow: hidden; }
.ch-modal--pane > .ch-modal__head,
.ch-modal--pane > .ch-modal__foot { flex: 0 0 auto; }
.ch-modal--pane > .ch-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; }
.ch-modal__bar { display: flex; align-items: center; gap: var(--sp-3); }
/* `.ch-modal__bar > .ch-modal__title`, two classes, on purpose: pages restyle their own headings —
   wip.css has `body[data-active='wip'] h3 { margin: ... }` — and one class loses to that, which wipes the
   auto margin that pushes the actions and the ✕ to the right edge. Two classes beat it. */
.ch-modal__bar > .ch-modal__title { margin: 0 auto 0 0; min-width: 0; font-size: var(--fs-h4); font-weight: 700; letter-spacing: normal;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-modal__actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); flex: 0 0 auto; }
.ch-modal__actions .ch-btn { white-space: nowrap; }
/* The modal's ✕: a filled 36px square. Dialogs built before the pane modal put a plain `.icon-btn` in the
   head instead (Invite user, Pick a job, the Inventory item view, New project, …); it takes the same look
   wherever it sits in the head, so every dialog closes from the same control. Three classes, so a page's
   own `.x-close` rule cannot flatten it again. */
.ch-modal__x,
.ch-modal .ch-modal__head .icon-btn { width: 36px; height: 36px; flex: 0 0 auto; margin-left: var(--sp-1); padding: 0;
  display: inline-grid; place-items: center;
  border: 0; border-radius: var(--radius-ctl); background: var(--control-bg); color: var(--font-color);
  font-family: inherit; font-size: var(--fs-h5); font-weight: 400; line-height: 1; cursor: pointer; }
.ch-modal__x:hover,
.ch-modal .ch-modal__head .icon-btn:hover { background: var(--control-hover-bg); color: var(--font-color); }
.ch-modal__x:focus-visible,
.ch-modal .ch-modal__head .icon-btn:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; }
/* The dialog itself takes focus on open (so Esc and Tab start inside it) without drawing a ring around
   the whole panel. */
.ch-modal--pane:focus:not(:focus-visible) { outline: none; }
.ch-modal--pane:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; }
@media (max-width: 640px) {
  .ch-modal__bar { flex-wrap: wrap; }
  .ch-modal__bar > .ch-modal__title { flex: 1 1 0; }   /* shares the first line with the ✕; the actions wrap under both */
  .ch-modal__actions { order: 3; flex: 1 1 100%; }
}

/* ---- chUI dialogs (confirm / alert / success / prompt) ---- */
.ch-dialog { width: min(440px, 94vw); padding: var(--sp-5); }
.ch-dialog__main { display: flex; gap: var(--sp-4); align-items: flex-start; }
.ch-dialog__icon { flex: 0 0 auto; width: 40px; height: 40px; border-radius: var(--radius-ctl); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: var(--fs-h4); }
.ch-dialog__icon--success { background: var(--success-bg); color: var(--success-color); }
.ch-dialog__icon--danger { background: var(--danger-bg); color: var(--danger-color); }
.ch-dialog__icon--info { background: var(--info-bg); color: var(--info-color); }
.ch-dialog__icon--question { background: var(--brand-wash); color: var(--brand-text); }
.ch-dialog__body { flex: 1; min-width: 0; }
.ch-dialog__title { font-size: var(--fs-h4); font-weight: 700; margin-bottom: var(--sp-2); }
.ch-dialog__msg { color: var(--secondary-color); font-size: var(--fs-small); line-height: 1.55; }
.ch-dialog__input { width: 100%; margin-top: var(--sp-4); }
.ch-dialog__list { margin: var(--sp-2) 0 0; padding-left: var(--sp-5); color: var(--secondary-color); font-size: var(--fs-small); line-height: 1.7; }
.ch-dialog__list b { color: var(--font-color); }
/* A summary line that reports something still needing attention (e.g. import lines with no cost code). */
.ch-dialog__list .ch-dialog__warn, .ch-dialog__list li.ch-dialog__warn { color: var(--danger-color); }
.ch-dialog__list li.ch-dialog__warn b { color: var(--danger-color); }
.ch-dialog__list li.ch-dialog__warn ul { margin: var(--sp-1) 0 0; padding-left: var(--sp-4); }
.ch-dialog__foot { display: flex; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-5); }

/* ---- chUI loading overlay ---- */
.ch-loading__box { display: flex; flex-direction: column; align-items: center; gap: var(--sp-4); background: var(--panel); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); padding: var(--sp-7) var(--sp-8); box-shadow: var(--panel-shadow-lg); }
.ch-loading__msg { color: var(--secondary-color); font-size: var(--fs-small); }
/* The brand kit's own loading art (public/logos/loading, from the CADdy-Hub brand guide): the blocking
   overlay gets the loader (the C pulling apart and snapping back), inline and small spots get the spinner.
   The motion and its reduced-motion fallback live inside the SVGs; body.dark swaps both in palette.css. */
.ch-loader { width: 56px; height: 56px; background: url('/CADdy-Hub/logos/loading/CADdy-Hub-loader.svg') center / contain no-repeat; }
.ch-spinner { display: inline-block; width: 36px; height: 36px; border: 0; background: url('/CADdy-Hub/logos/loading/CADdy-Hub-spinner.svg') center / contain no-repeat; vertical-align: middle; }
@keyframes ch-spin { to { transform: rotate(360deg); } }

/* ---- chUI custom dropdown (replaces <select>) ---- */
/* ---------- .cell-in — a control INSIDE a table cell ----------
   The spreadsheet look: no border until you hover, a hairline then, a focus ring when you are in it. Used
   for text inputs and for <select>s, which chUI.enhance upgrades to a .ch-dropdown — enhance copies the
   select's className onto the trigger button, which is what makes `.ch-dropdown__trigger.cell-in` below
   able to strip the trigger's own background. THAT COUPLING IS THE WHOLE POINT: a select in a grid cell
   must carry `cell-in` or the enhanced trigger keeps its standalone-form look (an opaque, unpadded box)
   and reads as a raw browser control sitting among styled ones.

   THIS LIVES IN THE SHARED SHEET ON PURPOSE. It used to be copy-pasted into requisition.css,
   projects.css and users.css, scoped to each page's own table and identical but for a min-width — so the
   fourth page to want an in-cell dropdown (the approval board's Approve / Deny column) inherited nothing
   and rendered unstyled. users.css still carries the note from the time `.usr-caddy-table` hit the same
   thing. Pages now override only what genuinely differs, which in practice is min-width. */
.cell-in {
  width: 100%; min-width: 70px; box-sizing: border-box;
  border: 1px solid transparent; background: transparent; border-radius: var(--radius-md);
  padding: 6px 8px; font-family: inherit; font-size: var(--fs-small); color: var(--font-color);
}
.cell-in:hover { border-color: var(--hairline-strong); }
.cell-in:focus { outline: none; border-color: var(--input-color); background: var(--panel); box-shadow: 0 0 0 2px var(--brand-wash); }
/* An empty cell's placeholder is a STATE, not content — faint, so a blank field's em dash cannot be
   mistaken for a typed value. Louder placeholders (an add-row, a missing cost code) out-specify this. */
.cell-in::placeholder { color: var(--text-faint); opacity: 1; }
select.cell-in { cursor: pointer; }
/* A cell the current role may not edit reads as plain text, not as an input that refuses to work. */
.cell-in:disabled { color: var(--font-color); -webkit-text-fill-color: var(--font-color); opacity: 1; cursor: default; background: transparent; }
.cell-in:disabled:hover { border-color: transparent; }
.cell-in.ch-num { text-align: right; }

.ch-dropdown { position: relative; display: inline-block; width: 100%; }
.ch-dropdown__trigger { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); width: 100%; cursor: pointer; font-family: inherit; text-align: left; color: var(--font-color); background: var(--control-bg); border-radius: var(--radius-md); }
.ch-dropdown__trigger:not(.cell-in):hover { background: var(--control-hover-bg); }
.ch-dropdown__trigger.cell-in { background: transparent; }
.ch-dropdown__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-dropdown__label.is-placeholder { color: var(--secondary-color); }
.ch-dropdown__chev { color: var(--secondary-color); font-size: 10px; flex: 0 0 auto; }
/* 360px, not 280: the role list has grown to a dozen entries and 280 showed five and a half of them
   over a scrollbar. placePopover still clamps this down to whatever the window actually has room for. */
.ch-dropdown__menu { position: fixed; z-index: var(--z-popover); display: flex; flex-direction: column; max-height: 360px; background: var(--panel); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow-lg); overflow: hidden; }
.ch-dropdown__search { margin: var(--sp-2); padding: 6px var(--sp-2); font-family: inherit; font-size: var(--fs-small); color: var(--font-color); background: var(--control-bg); border: 1px solid transparent; border-radius: var(--radius-md); }
.ch-dropdown__search:focus { outline: none; background: var(--panel); border-color: var(--input-color); box-shadow: 0 0 0 2px var(--brand-wash); }
.ch-dropdown__list { overflow-y: auto; padding: 0 var(--sp-1) var(--sp-1); }
.ch-dropdown__opt { padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-md); cursor: pointer; font-size: var(--fs-small); white-space: nowrap; }
.ch-dropdown__opt.is-active { background: var(--control-bg); }
.ch-dropdown__opt.is-selected { color: var(--brand-text); font-weight: 700; }
/* chUI.multiSelect: the same trigger and menu, a checkbox per row, its count at the right. The trigger's
   words go brand-colored while anything is picked, so a narrowed field reads apart from an "All" one. The
   trigger also carries `.ch-select`, so it is the same box as an enhanced select beside it. */
.ch-mselect__trigger.is-on .ch-dropdown__label { color: var(--brand-text); font-weight: 700; }
.ch-mselect__opt { display: flex; align-items: center; gap: var(--sp-2); padding: 6px var(--sp-2); border-radius: var(--radius-md);
  font-size: var(--fs-small); cursor: pointer; white-space: nowrap; }
.ch-mselect__opt:hover, .ch-mselect__opt:focus-within { background: var(--control-bg); }
.ch-mselect__opt input { cursor: pointer; margin: 0; accent-color: var(--primary-color); }
.ch-mselect__name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ch-mselect__n { flex: 0 0 auto; margin-left: var(--sp-3); font-variant-numeric: tabular-nums; font-weight: 600; color: var(--secondary-color); }
.ch-mselect__all { font-weight: 700; border-bottom: 1px solid var(--hairline); border-radius: var(--radius-md) var(--radius-md) 0 0; margin-bottom: 2px; }
.ch-mselect__empty { padding: var(--sp-2) var(--sp-3); font-size: var(--fs-small); color: var(--secondary-color); }

/* ---- chUI custom date picker (replaces <input type=date>) ---- */
.ch-datefield-wrap { width: 100%; }
.ch-datefield { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); width: 100%; font-family: inherit; text-align: left; color: var(--font-color); background: var(--control-bg); }
.ch-datefield.cell-in { background: transparent; }
.ch-datefield:focus-within { background: var(--panel); border-color: var(--input-color); box-shadow: 0 0 0 3px var(--brand-wash); }
.ch-datefield.cell-in:focus-within { background: var(--panel); }
/* Typeable field: borderless input fills the box; the box itself carries the .ch-input/.cell-in chrome. */
.ch-datefield__input { flex: 1 1 auto; min-width: 0; border: 0; background: transparent; padding: 0; margin: 0; font: inherit; color: inherit; outline: none; /* a11y-focus: .ch-datefield:focus-within */ }
.ch-datefield__input::placeholder { color: var(--secondary-color); }
.ch-datefield__icon { border: 0; background: transparent; cursor: pointer; color: var(--secondary-color); font-size: 10px; line-height: 1; flex: 0 0 auto; padding: 0 2px; }
.ch-datefield__icon:hover { color: var(--brand-text); }
.ch-datepicker { position: fixed; z-index: var(--z-popover); width: 272px; background: var(--panel); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow-lg); padding: var(--sp-3); }
.ch-dp__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-2); }
.ch-dp__title { font-weight: 700; font-size: var(--fs-small); }
.ch-dp__nav { border: 0; background: var(--control-bg); cursor: pointer; font-size: 18px; line-height: 1; color: var(--font-color); width: 30px; height: 30px; border-radius: var(--radius-md); }
.ch-dp__nav:hover { background: var(--control-hover-bg); }
.ch-dp__dow { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 2px; }
.ch-dp__dow span { text-align: center; font-size: var(--fs-caption); font-weight: 700; color: var(--secondary-color); text-transform: uppercase; letter-spacing: var(--ls-wide); }
.ch-dp__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.ch-dp__day { border: 0; background: transparent; cursor: pointer; font-family: inherit; font-size: var(--fs-small); padding: 6px 0; border-radius: var(--radius-md); color: var(--font-color); }
.ch-dp__day.is-empty { visibility: hidden; }
.ch-dp__day:hover { background: var(--brand-wash); }
.ch-dp__day.is-today { color: var(--brand-text); font-weight: 800; }
.ch-dp__day.is-selected { background: var(--btn-primary-bg); color: var(--on-brand-text); }
.ch-dp__foot { display: flex; justify-content: space-between; margin-top: var(--sp-2); border-top: 1px solid var(--hairline); padding-top: var(--sp-2); }
.ch-dp__btn { border: 0; background: transparent; cursor: pointer; font-family: inherit; font-size: var(--fs-small); font-weight: 700; color: var(--brand-text); padding: 4px var(--sp-2); border-radius: var(--radius-md); }
.ch-dp__btn:hover { background: var(--control-bg); }

/* ---- Utility ---- */
.ch-empty-state { text-align: center; padding: var(--sp-7) var(--sp-5); color: var(--secondary-color); font-weight: 600; }
.ch-alert { padding: var(--sp-3) var(--sp-4); border-radius: var(--radius-ctl); border: 1px solid transparent; font-size: var(--fs-small); font-weight: 500; margin-bottom: var(--sp-4); }
.ch-alert--danger { background: var(--danger-bg); color: var(--danger-color); }
.ch-alert--info { background: var(--info-bg); color: var(--info-color); }
.ch-alert--success { background: var(--success-bg); color: var(--success-color); }
.ch-alert--warning { background: var(--warning-bg); color: var(--warning-color); }
.ch-hidden { display: none !important; }
/* The hidden attribute always wins over the shared components' own display (inline-flex / inline-block),
   so `el.hidden = true` hides a button, tab or chip the way it hides anything else. */
.ch-btn[hidden], .ch-iconbtn[hidden], .ch-chip[hidden], .ch-tab[hidden] { display: none !important; }

/* ---- Toast — a short-lived note in the bottom-right corner (page JS adds / removes it) ----
   <div class="ch-toast ch-toast--success" role="status">Saved</div>. Stack several in a .ch-toasts. */
.ch-toasts { position: fixed; right: var(--sp-5); bottom: calc(var(--sp-5) + var(--rail-bottom-h)); z-index: var(--z-toast); display: flex; flex-direction: column; gap: var(--sp-2); align-items: flex-end; pointer-events: none; }
.ch-toast { pointer-events: auto; max-width: min(420px, 92vw); padding: var(--sp-3) var(--sp-4); border-radius: var(--radius-ctl);
  background: var(--panel); color: var(--font-color); border: 1px solid var(--panel-border); box-shadow: var(--panel-shadow-lg);
  font-size: var(--fs-small); font-weight: 600; border-left: 4px solid var(--primary-color); }
.ch-toast--success { border-left-color: var(--success-color); }
.ch-toast--warning { border-left-color: var(--warning-color); }
.ch-toast--danger  { border-left-color: var(--danger-color); }

/* ---- What's new pop-up (header.js, every page with the header) --------------------------------------------
   A toast-card, not a modal: bottom-LEFT, clear of the rail and the bottom bar, because the chat and Updates
   bubbles own the bottom-right. Never takes focus. The options menu's "What's new" item carries the version in
   muted text and a .ch-nav__dot while something is unseen. */
.ch-wn-pop { position: fixed; z-index: var(--z-toast); left: calc(var(--rail-w) + var(--sp-5)); bottom: calc(var(--sp-5) + var(--rail-bottom-h));
  width: min(380px, calc(100vw - var(--rail-w) - 2 * var(--sp-5))); max-height: min(70vh, 560px); display: flex; flex-direction: column;
  padding: var(--sp-4); background: var(--panel); color: var(--font-color); border: 1px solid var(--panel-border);
  border-left: 4px solid var(--primary-color); border-radius: var(--radius-lg); box-shadow: var(--panel-shadow-lg); }
.ch-wn-pop__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.ch-wn-pop__title { margin: 0; font-size: var(--fs-h5, var(--fs-body)); font-weight: 700; line-height: 1.3; padding-top: var(--sp-1); }
.ch-wn-pop__list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-3); }
.ch-wn-pop__name { font-size: var(--fs-small); }
.ch-wn-pop__hl { margin: var(--sp-1) 0 0; padding-left: var(--sp-5); font-size: var(--fs-small); color: var(--secondary-color); }
.ch-wn-pop__hl li + li { margin-top: 2px; }
.ch-wn-pop__more { margin: var(--sp-2) 0 0; font-size: var(--fs-small); color: var(--text-faint); }
.ch-wn-pop__foot { display: flex; gap: var(--sp-2); justify-content: flex-end; flex-wrap: wrap; margin-top: var(--sp-4); }
.ch-wn-pop__say { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.ch-options__ver { margin-left: var(--sp-1); color: var(--secondary-color); font-variant-numeric: tabular-nums; }
.ch-options__item--wn .ch-nav__dot { margin-left: var(--sp-1); vertical-align: middle; box-shadow: 0 0 0 2px var(--header-bar-bg); }
@media print { .ch-wn-pop { display: none; } }

/* ---- company logo dialog (header options menu) -------------------------------------------------
   Shared chrome, so it lives here rather than on a page stylesheet: the item is in the options menu
   and opens from anywhere. */
.ch-logo-modal { width: min(520px, 94vw); }
.ch-logo-modal .ch-modal__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.ch-logo-note { margin: 0 0 var(--sp-3); color: var(--secondary-color); font-size: var(--fs-caption); }
/* The preview band is dark on purpose — it is where the logo actually lands in the PDF header, and a
   white logo on a white card looks like a broken image. */
.ch-logo-prev { display: flex; align-items: center; justify-content: center; height: 110px;
  border: 1px solid var(--hairline); border-radius: var(--radius-md); background: var(--panel-2); padding: var(--sp-3); }
.ch-logo-prev.is-empty { color: var(--secondary-color); font-size: var(--fs-small); }
.ch-logo-prev img { max-width: 100%; max-height: 86px; object-fit: contain; }
.ch-logo-acts { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; margin-top: var(--sp-3); }

/* ---- Pill tabs (.ch-tabs / .ch-tabstrip > .ch-tab) — Bid's tab row ----------------------------
   A row of filled rounded pills; the active one lifts to the panel color with brand text and a brand
   border. `.ch-tabs` is the container name for new markup; `.ch-tabstrip` (Users, Assets, Catalog,
   Projects) is the same component under its older name. A tab is `.is-active` or
   `[aria-selected="true"]`; `.ch-tab__n` is its count. Pages with their own tab classes (pj-tab, wip-tab,
   appr-tab, inv-tab, …) map onto this look rather than inventing another.

   Counts belong on the tab, not the chip: a tab is a partition of everything and the number tells you
   whether it is worth opening, while chips are usually a small named set where the count is noise. */
.ch-tabs, .ch-tabstrip { display: flex; align-items: center; gap: var(--sp-2);
  overflow-x: auto; overflow-y: hidden; padding: 2px 2px var(--sp-3);
  border-bottom: 1px solid var(--hairline); margin-bottom: var(--sp-4); scrollbar-width: thin; }
.ch-tab { flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--sp-2); white-space: nowrap;
  min-height: 36px; padding: var(--sp-2) var(--sp-4);
  border: 1px solid transparent; border-radius: var(--radius-ctl); background: var(--tab-bg); cursor: pointer;
  font-family: inherit; font-weight: 700; font-size: var(--fs-small); color: var(--font-color); text-decoration: none;
  transition: background-color .12s ease, border-color .12s ease; }
.ch-tab:hover { background: var(--tab-hover-bg); color: var(--font-color); text-decoration: none; }
.ch-tab.is-active, .ch-tab[aria-selected="true"] { background: var(--tab-active-bg); color: var(--tab-active-fg); border-color: var(--tab-active-border); box-shadow: var(--panel-shadow); }
.ch-tab__n { font-weight: 600; color: var(--secondary-color); }
.ch-tab.is-active .ch-tab__n, .ch-tab[aria-selected="true"] .ch-tab__n { color: var(--tab-active-fg); }
/* A tab row whose bottom rule would double up with a panel edge right under it. */
.ch-tabs--flush { border-bottom: 0; padding-bottom: 2px; }

/* ---- Page sub-tabs (.ch-tabs.ch-subnav > a.ch-tab.ch-subnav__tab) ------------------------------
   The switcher between two pages that share one rail item (Calendar | Look Ahead, Users | Companies),
   drawn by header.js into #ch-subnav right under the page title. It is the level ABOVE the page's own
   pill rows (Month / Week / Day, Look Ahead's views, the Users departments), so it must not read as one
   more of them: these are page tabs standing on the strip's rule, not pills. Each is a link with the
   rail's icon and body-size text, unfilled at rest; the current page is filled with the rail's active
   wash and underlined in the brand color along the rule. */
.ch-subnav { gap: var(--sp-1); padding-bottom: 0; margin-bottom: var(--sp-5); }
.ch-subnav .ch-tab { min-height: 42px; padding: var(--sp-2) var(--sp-4) var(--sp-2) var(--sp-3);
  border: 0; border-radius: var(--radius-ctl) var(--radius-ctl) 0 0;
  font-size: var(--fs-body); background: transparent; color: var(--font-color); }
.ch-subnav .ch-tab:hover { background: var(--tab-hover-bg); }
.ch-subnav .ch-tab.is-active { background: var(--rail-active-bg); color: var(--rail-active-fg);
  box-shadow: inset 0 -3px 0 var(--rail-active-border); }
/* Inset: the strip clips anything below its rule. */
.ch-subnav .ch-tab:focus-visible { outline-offset: -2px; text-decoration: none; }
.ch-subnav__icon { width: 18px; height: 18px; flex: 0 0 auto; }

/* overflow-y is pinned deliberately: setting overflow-x alone makes overflow-y compute to `auto`. The
   2px padding keeps the active pill's border and shadow inside the padding box so they are not clipped. */

/* ---- Filter chips — the secondary axis under a tab row ---- */
.ch-chips { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-5); }
.ch-chip { white-space: nowrap; cursor: pointer; font-family: inherit;
  font-size: var(--fs-caption); font-weight: 700; padding: 4px var(--sp-3);
  border-radius: var(--radius-pill); border: 1px solid transparent;
  background: var(--control-bg); color: var(--font-color); }
.ch-chip:hover { background: var(--control-hover-bg); color: var(--font-color); }
.ch-chip.is-active { background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); color: var(--on-brand-text); }
/* Pushes a boolean narrowing away from the named set it would otherwise read as a member of. */
.ch-chips__spacer { flex: 1 1 auto; }

/* The sentence under a table that explains what the numbers in it mean. */
.ch-tablenote { margin-top: var(--sp-4); max-width: 640px; font-size: var(--fs-caption); color: var(--secondary-color); }

/* Sortable header cell + its direction arrow. */
.ch-th-sort { cursor: pointer; user-select: none; white-space: nowrap; }
.ch-th-sort__arrow { margin-left: var(--sp-1); font-size: .75em; color: var(--brand-text); }

/* ---- person avatar ------------------------------------------------------------------------------
   Initials in a brand-wash circle. One definition, because Users, Assets and the Scheduler were each
   growing their own and they had already drifted — the scheduler's was a smaller pill tinted a
   different color per person, which read as a fourth thing rather than the same person you had just
   seen on the Users page.

   Uniform rather than per-person color on purpose: the color was decorative (a hash of the name, no
   meaning), and where color DOES carry meaning here — a task's own color, a status — a second
   color system next to it is noise. */
.ch-avatar { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
  background: var(--brand-wash); color: var(--brand-text);
  font-size: .7rem; font-weight: 700; flex: 0 0 auto; }
.ch-avatar--sm { width: 22px; height: 22px; }
/* Nobody assigned: a quiet placeholder rather than initials that mean nothing. */
.ch-avatar.is-empty { background: var(--control-bg); color: var(--secondary-color); }

/* ---- skeletons and load motion (motion.js, window.chMotion; styles.md §4) ------------------------
   A dashboard draws its layout at once with .ch-skel blocks where the figures will go, then rolls the
   figures up and grows the bars when the data lands. The sweep is a band of --hairline-strong, which is
   darker than the block on light and lighter on dark, so it reads in both themes with one rule. */
.ch-skel { position: relative; display: block; height: .9em; max-width: 100%; overflow: hidden;
  border-radius: var(--radius-sm); background: var(--surface-2); }
.ch-skel + .ch-skel { margin-top: var(--sp-2); }
.ch-skel--figure { height: 1.6em; border-radius: var(--radius-md); }
.ch-skel--bar { height: 10px; border-radius: var(--radius-pill); }
.ch-skel::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--hairline-strong) 55%, transparent), transparent);
  animation: ch-skel-sweep 1.5s ease-in-out infinite; }
@keyframes ch-skel-sweep { to { transform: translateX(100%); } }
/* A skeleton card holds its lines apart like a real card's text. */
.ch-skel-card { display: grid; gap: var(--sp-2); }
.ch-skel-card .ch-skel + .ch-skel { margin-top: 0; }
/* Words for a screen reader only ("Loading the projects…" beside the blocks). */
.ch-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* A rolling figure keeps its digits one width, so the number does not shimmy while it counts. */
.ch-rolling { font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) { .ch-skel::after { animation: none; } }
/* Reduced motion, everywhere (a11y): somebody who asks the OS for less motion gets no sweeps, slides or
   spins on any page. styles.css is linked on every page, so this one block covers every page's own CSS;
   motion.js (chMotion) already skips its animations for the same setting. A transition still ends (and
   fires transitionend), it just takes no time. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important; scroll-behavior: auto !important;
  }
}
