/* ==========================================================================
   Brain-Stem components
   Built on tokens.css. One block per element. Load tokens.css first.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Base
   Document default font and ink. Type utilities and most components set their
   own family; this is the fallback so an element that sets type without a
   family (e.g. .card__title) inherits the brand sans, not the browser serif.
   -------------------------------------------------------------------------- */
/* box-sizing, once, for everything (added 2026-07-27).

   This file used to state box-sizing per component wherever a width or height
   met padding, and otherwise inherit whatever reset the consuming page brought.
   library.html carries `*{box-sizing:border-box}`, so the showcase always looked
   right; a page that loads tokens.css + components.css and nothing else — which
   is what vendoring produces — got content-box, and every such component
   rendered WIDER OR TALLER THAN ITS OWN CSS DECLARES.

   Three bugs came out of that gap before the cause was named: a .card 58px
   taller than the row holding it, a fill host 48px wider than its container,
   and a summary card declaring min-width:264px that measured 322px. Each was
   patched where it surfaced. This is the same fix at the root, so the fourth
   one does not happen.

   36 rules declared a size against real padding or a border without stating
   box-sizing; 8 components change measurably, the largest being .topbar and
   .statusbar, which stop overflowing their container by 80px, and
   .dtable__filters, which becomes the 222px the rule asks for instead of 255px.
   In every case the component renders at its DECLARED size.

   The leading `body` matters: the vendoring transform rewrites a top-level
   `body` selector to `&`, so this reaches the `.bs-v2` element itself and not
   only its descendants. Written as one selector list for that reason. */
body,*,*::before,*::after{box-sizing:border-box;}

body{font-family:var(--font-sans);color:var(--ink);}

/* [hidden] means hidden, everywhere, whatever the element's own display says.
   The browser's rule is a bare attribute selector, so ANY class beats it — and
   most components here set a display. That has been found and patched one
   component at a time (.panel-overlay, .modal-overlay, .statusbar, .amenu__panel,
   .tab-more, .dtable__detailrow, .field__error, .btn, .topbar__item — nine of
   them), and each was found the same way: the attribute set, the DOM reading
   exactly as intended, and the element still on screen.

   Specificity alone does not settle it. .topbar__item[hidden] and the collapsed
   bar's .topbar__right .topbar__item weigh the same, so the later one won and
   the account control painted inside the burger panel for a signed-out visitor
   — a per-component guard silently defeated by a rule written months apart.
   !important is what actually makes the attribute mean what it says.

   The per-component rules below are left where they are: each carries the story
   of how it was found, and they now agree with this one rather than carrying it
   alone. */
[hidden]{display:none !important;}

/* --------------------------------------------------------------------------
   Type   (locked 2026-06-24)
   Utility classes for the type scale. Apply to any element; they set size,
   line-height, weight, and the few tracking/colour rules from visual-identity.
   Sentence case everywhere; never italicise a heading; one H1 per view.
   -------------------------------------------------------------------------- */

.t-display-l{font:var(--fw-bold) var(--fs-display-l)/var(--lh-display-l) var(--font-sans);letter-spacing:var(--tracking-display);color:var(--ink);margin:0;}
.t-display-m{font:var(--fw-bold) var(--fs-display-m)/var(--lh-display-m) var(--font-sans);letter-spacing:var(--tracking-display);color:var(--ink);margin:0;}
.t-h1{font:var(--fw-bold) var(--fs-h1)/var(--lh-h1) var(--font-sans);color:var(--ink);margin:0;}
.t-h2{font:var(--fw-semibold) var(--fs-h2)/var(--lh-h2) var(--font-sans);color:var(--ink);margin:0;}
.t-h3{font:var(--fw-semibold) var(--fs-h3)/var(--lh-h3) var(--font-sans);color:var(--ink);margin:0;}
.t-body{font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--font-sans);color:var(--ink);margin:0;}
.t-body-sm{font:var(--fw-regular) var(--fs-body-sm)/var(--lh-body-sm) var(--font-sans);color:var(--ink);margin:0;}
.t-caption{font:var(--fw-regular) var(--fs-caption)/var(--lh-caption) var(--font-sans);color:var(--fe-text-2);margin:0;}
.t-eyebrow{font:var(--fw-semibold) var(--fs-eyebrow)/var(--lh-eyebrow) var(--font-sans);letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--green-text);margin:0;}
.t-label{font:var(--fw-semibold) var(--fs-label)/var(--lh-label) var(--font-sans);color:var(--ink);margin:0;}
.t-link{color:var(--green-text);text-decoration:underline;text-underline-offset:2px;}
.t-link:hover{color:var(--green-deep);}
.t-mono{font:var(--fw-regular) var(--fs-mono)/var(--lh-mono) var(--font-mono);color:var(--ink);}
/* Clamp a run of text to N rendered lines, ellipsised on the last. A
   server-side character truncation cannot do this: the same 180 characters is
   two lines in a wide table cell and five in a narrow one, so a row height the
   layout was designed around only holds at one viewport. Truncate on the server
   for payload, clamp here for shape. Composes with the type style that sets the
   size — .t-caption.t-clamp-2. */
.t-clamp-2,.t-clamp-3{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;}
.t-clamp-2{-webkit-line-clamp:2;}
.t-clamp-3{-webkit-line-clamp:3;}

/* Layout utility — cap a content block's width and centre it on wide screens.
   Opt-in: wrap page content (or place full-bleed chrome) in .bs-container so it
   doesn't stretch edge-to-edge on ultrawide monitors. See --bs-content-max. */
.bs-container{max-width:var(--bs-content-max);margin-inline:auto;}

/* --------------------------------------------------------------------------
   Icons   (locked 2026-06-24)
   Lucide line icons, vendored in icons/. Use inline <svg class="icon"> so the
   glyph inherits text colour (stroke=currentColor) and can take aria-hidden /
   aria-label. Default 20px; modifiers for 16 and 24. Decorative icons MUST be
   aria-hidden — the text label carries the meaning.
   -------------------------------------------------------------------------- */

.icon{display:inline-block;width:var(--icon-md);height:var(--icon-md);flex:none;vertical-align:middle;}
.icon--sm{width:var(--icon-sm);height:var(--icon-sm);}
.icon--lg{width:var(--icon-lg);height:var(--icon-lg);}

/* --------------------------------------------------------------------------
   Buttons   (locked 2026-06-24)
   Variants: primary (green) · secondary (white/outline) · tertiary (black)
   States:   default · hover · focus (keyboard) · disabled
   -------------------------------------------------------------------------- */

.btn{
  font-family:var(--font-sans);
  font-size:var(--btn-font-size);
  font-weight:var(--btn-font-weight);
  min-height:var(--btn-height);
  min-width:var(--btn-min-width);
  padding:0 var(--btn-pad-x);
  border-radius:var(--radius);
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:var(--btn-gap);
  border:1px solid transparent;
  /* Stated, not inherited from the UA: a <button> is border-box by default and
     an <a> is content-box, so without this the same .btn measures differently
     depending on the element under it — min-width:132 + 40 of padding + 2 of
     border made an <a class="btn"> 174 wide and 42 tall beside a 134x40
     <button>. It surfaced in the auth slot, whose whole contract is that Sign
     in and Account share a footprint (header.md), and where Sign in is an <a>
     because it navigates. */
  box-sizing:border-box;
  cursor:pointer;
  white-space:nowrap;
  text-decoration:none;   /* .btn carries no underline, incl. when used on <a> */
  transition:background .12s ease, border-color .12s ease,
             color .12s ease, box-shadow .12s ease;
}

/* A button a host swaps in and out — the auth slot's Sign in is the case this
   was written for (header.md, "The auth slot"). .btn sets display:inline-flex
   above, and a class outranks the UA's bare [hidden]{display:none}, so without
   this the attribute is set and the button stays on screen. */
.btn[hidden]{display:none;}

/* icon sits left of the label */
.btn .icon{width:16px;height:16px;flex:none;display:inline-block;}

/* focus — keyboard only. 2px white gap + 2px black ring; visible on every
   fill. Distinct from hover (which changes the fill). */
.btn:focus-visible{
  outline:none;
  box-shadow:0 0 0 2px var(--surface), 0 0 0 4px var(--ink);
}

/* primary — deep green, one per view */
.btn--primary{background:var(--green-text);color:#fff;}
.btn--primary:hover{background:var(--green-hover);}

/* secondary — white fill, black outline */
.btn--secondary{background:var(--surface);color:var(--ink);border-color:var(--ink);}
.btn--secondary:hover{background:var(--surface-hover);}

/* tertiary — black fill, white text (strong neutral) */
.btn--tertiary{background:var(--ink);color:#fff;border-color:var(--ink);}
.btn--tertiary:hover{background:var(--ink-hover);}

/* disabled = locked — any variant. Hide the action icon and show a lock before
   the label, so a disabled button reads as locked like the input controls. */
.btn:disabled,.btn[aria-disabled="true"]{
  cursor:not-allowed;
  background:var(--fe-border)!important;
  color:var(--disabled-ink)!important;
  border-color:transparent!important;
  box-shadow:none!important;
}
.btn:disabled .icon,.btn[aria-disabled="true"] .icon{display:none;}
.btn:disabled::before,.btn[aria-disabled="true"]::before{
  content:"";width:16px;height:16px;flex:none;
  background:currentColor;
  -webkit-mask:var(--lock-disabled) center/contain no-repeat;
  mask:var(--lock-disabled) center/contain no-repeat;
}

/* group — buttons sit together; all equal width, matching the longest label.
   Keep labels short so the group stays compact. */
.btn-group{
  display:inline-grid;
  grid-auto-flow:column;
  grid-auto-columns:1fr;
  gap:12px;
}
.btn-group .btn{width:100%;}

/* loading   (added 2026-06-25) — an async action is running (Save / Submit /
   Run). A .spinner (first child) swaps in for the label; the button keeps its
   fill and width but goes inert. Mark it aria-busy, NOT disabled — disabled is
   the locked grey state, loading keeps the button's own colour. On the filled
   primary/tertiary add spinner--on so the mark reads white.
     <button class="btn btn--primary is-loading" aria-busy="true">
       <span class="spinner spinner--sm spinner--on" aria-hidden="true"></span>Saving…</button> */
.btn .spinner{display:none;}
.btn.is-loading{position:relative;color:transparent;pointer-events:none;cursor:progress;}
.btn.is-loading .icon{visibility:hidden;}
.btn.is-loading .spinner{display:block;position:absolute;top:50%;left:50%;margin:0;translate:-50% -50%;}  /* translate (not transform) so the spinner's rotate animation spins in place rather than orbiting */

/* --------------------------------------------------------------------------
   Form field   (locked 2026-06-24)
   Soft-filled text input — the atom every other input inherits. Top-aligned
   label, one 40px size. Fill clears to white with a green border on focus.
   States: default · focus · error · disabled · locked (gated, lock icon).
   The permanent system value is plain text (.field__value), never an input.
   Label carries the meaning; placeholder is a format hint only.
   -------------------------------------------------------------------------- */

.field{margin-bottom:var(--space-4);}
.field:last-child{margin-bottom:0;}

.field__label{
  display:block;
  font:var(--fw-semibold) var(--fs-label)/1.3 var(--font-sans);
  color:var(--ink);
  margin-bottom:var(--space-2);
}
.field__optional{font-weight:var(--fw-regular);color:var(--fe-text-2);}

.field__control{position:relative;}

.field__input,
.field__textarea{
  width:100%;box-sizing:border-box;
  font-family:var(--font-sans);
  font-size:var(--field-font-size);
  color:var(--ink);
  background:var(--field-fill);
  border:1px solid transparent;
  border-radius:var(--radius);
  outline:none;
  transition:border-color .12s ease, box-shadow .12s ease, background .12s ease;
}
.field__input{height:var(--field-height);padding:0 var(--field-pad-x);}
/* textarea — the multi-line field. 3-row default, vertical resize only. */
.field__textarea{
  min-height:var(--field-textarea-min-h);
  padding:10px var(--field-pad-x);
  line-height:1.5;
  resize:vertical;
  display:block;
}
.field__input::placeholder,
.field__textarea::placeholder{color:var(--field-placeholder);}

/* focus — fill clears to white, green border + soft ring. The grey→green
   border shift IS the focus indicator (clears 3:1); no black ring like buttons,
   whose hover already changes their fill. Mouse and keyboard alike. */
.field__input:focus,
.field__textarea:focus{
  background:var(--surface);
  border-color:var(--green);
  box-shadow:0 0 0 3px var(--field-focus-ring);
}

/* helper + error text under the field */
.field__help{font:var(--fw-regular) var(--fs-caption)/1.5 var(--font-sans);color:var(--fe-text-2);margin:var(--space-2) 0 0;}
.field__error{display:flex;align-items:center;gap:6px;font:var(--fw-regular) var(--fs-caption)/1.5 var(--font-sans);color:var(--status-error);margin:var(--space-2) 0 0;}
.field__error .icon{color:var(--status-error);}
/* An error message only exists some of the time, and [hidden] is how a host
   says so — but the UA's [hidden]{display:none} is a bare attribute selector
   and the rule above is a class, so the class wins and the message paints
   anyway. Measured on a live form: the field carried no .field--error, the
   attribute was set, the DOM read exactly as intended, and both messages were
   on screen under two clean inputs. Same guard the overlays already carry
   (.panel-overlay[hidden]) — an element that sets its own display has to say
   what [hidden] means. */
.field__error[hidden]{display:none;}

/* footer row beneath a textarea — helper/error on the left, counter on the right */
.field__foot{display:flex;justify-content:space-between;align-items:baseline;gap:var(--space-3);margin-top:var(--space-2);}
.field__foot .field__help,
.field__foot .field__error{margin-top:0;}
.field__count{margin-left:auto;font:var(--fw-regular) var(--fs-caption)/1.5 var(--font-sans);color:var(--fe-text-2);white-space:nowrap;font-variant-numeric:tabular-nums;}
.field__count--over{color:var(--status-error);font-weight:var(--fw-semibold);}

/* error — pink fill + red border. Driven by aria-invalid in markup. */
.field--error .field__input,
.field--error .field__textarea,
.field__input[aria-invalid="true"],
.field__textarea[aria-invalid="true"]{
  background:var(--field-error-fill);
  border-color:var(--status-error);
}
.field--error .field__input:focus,
.field--error .field__textarea:focus,
.field__input[aria-invalid="true"]:focus,
.field__textarea[aria-invalid="true"]:focus{
  box-shadow:0 0 0 3px var(--field-error-fill);
}

/* disabled = locked — not editable. A lock glyph sits on the right so it reads
   as locked without a hover. A field that will later open just adds an "unlocks
   once…" helper; the control looks the same either way. */
.field__input:disabled,
.field__textarea:disabled{
  background-color:var(--disabled-fill);
  color:var(--disabled-ink);
  border-color:var(--fe-border);
  cursor:not-allowed;
  box-shadow:none;
  padding-right:38px;
  background-image:var(--lock-disabled);
  background-repeat:no-repeat;
}
.field__input:disabled{background-position:right 11px center;}
.field__textarea:disabled{background-position:right 11px top 10px;resize:none;}

/* permanent system value — plain text, no box; never an <input>. */
.field__value{font:var(--fw-regular) var(--field-font-size)/1.4 var(--font-sans);color:var(--ink);margin:0;padding:9px 2px;}

/* async validation (checking…)   (added 2026-06-25) — a trailing spinner inside
   the control while an async check runs (e.g. "is this name available?"). The
   one input that spins rather than skeletons. Add .field--checking and a
   .field__spin as the last child of .field__control; pair with a "Checking…"
   helper, then resolve to the normal helper or the error / success state. */
.field__spin{position:absolute;top:50%;right:11px;translate:0 -50%;display:none;}
.field--checking .field__input{padding-right:38px;}
.field--checking .field__spin{display:block;}

/* password reveal   (added 2026-07-26, brain-stem.io login rebuild) — the
   show/hide control on a password field, and the only field adornment the user
   operates rather than reads. Same trailing idiom as .field__spin above:
   .field__control is the positioning context, the control hangs off its right
   edge, and the input reserves room so a long value never runs underneath it.

   The button IS the icon-button component (.ib .ib--28), not a new button
   style — this defines where it sits, not how it looks.

   Markup:
     <div class="field field--reveal">
       <label class="field__label" for="password">Password</label>
       <div class="field__control">
         <input class="field__input" type="password" id="password">
         <button class="ib ib--28 field__reveal" type="button"
                 aria-label="Show password">
           <svg class="icon eye-icon">…</svg>
           <svg class="icon eye-off-icon hidden">…</svg>
         </button>
       </div>
     </div>

   WHY .hidden IS SCOPED HERE RATHER THAN MADE A UTILITY. The library has no
   display utilities and should not grow one by the back door: a global .hidden
   is a class every surface would reach for, and it duplicates the [hidden]
   attribute that already carries this meaning. Scoped to the reveal button it
   states one fact about one control — which of two glyphs is showing — and it
   lets a host's existing toggle script drive the swap by class with no patch.

   The selector carries .field__control deliberately. `.ib .icon` sets
   display:inline-block and is defined LATER in this file, so a two-class rule
   here would tie on specificity and lose to it: the glyph would stay visible
   and the control would silently stop reading as a toggle. Three classes puts
   the result beyond source order. */
.field--reveal .field__input{padding-right:42px;}
.field__reveal{position:absolute;top:50%;right:6px;translate:0 -50%;}
.field__control .field__reveal .hidden{display:none;}

/* --------------------------------------------------------------------------
   Native select   (locked 2026-06-30, Inputs & forms group)
   A native <select> on the field shell — the lightweight alternative to the
   custom .combo dropdown (use the combo when you need filter / multi-select /
   rich rows; use this for a short, plain list). Soft-filled like the input, a
   muted chevron on the right, green border on focus, lock when disabled.
   -------------------------------------------------------------------------- */

.field__select{
  width:100%;height:var(--field-height);
  padding:0 36px 0 var(--field-pad-x);
  font:var(--fw-regular) var(--field-font-size)/1 var(--font-sans);
  color:var(--ink);
  background:var(--field-fill) var(--select-chevron) no-repeat right 12px center;
  background-size:18px;
  border:1px solid transparent;border-radius:var(--radius);
  appearance:none;-webkit-appearance:none;cursor:pointer;
  transition:background-color .12s ease, border-color .12s ease, box-shadow .12s ease;
}
.field__select:focus{outline:none;background-color:var(--surface);border-color:var(--green);box-shadow:0 0 0 3px var(--field-focus-ring);}
.field--error .field__select{background-color:var(--field-error-fill);border-color:var(--status-error);}
.field__select:disabled{color:var(--disabled-ink);background-color:var(--disabled-fill);cursor:not-allowed;background-image:var(--lock-disabled);background-size:18px;}

/* --------------------------------------------------------------------------
   Dropdown (select)   (locked 2026-06-24)
   Custom menu on the field shell. .combo__control looks like a field with a
   chevron; .menu is the popup (filter + scrolling list, capped at 7 rows).
   Single-select green-fills the chosen row and closes; multi-select uses a
   checkbox per row, stays open, and renders chosen items as removable chips.
   Behaviour — open/close, type-to-filter, keyboard, ARIA — lives in dropdown.js.
   -------------------------------------------------------------------------- */

.combo{position:relative;}
.combo__control{
  width:100%;box-sizing:border-box;min-height:var(--field-height);display:flex;align-items:center;gap:8px;
  font-family:var(--font-sans);font-size:var(--field-font-size);color:var(--ink);text-align:left;
  padding:0 var(--field-pad-x);background:var(--field-fill);border:1px solid transparent;
  border-radius:var(--radius);cursor:pointer;outline:none;
  transition:border-color .12s ease, box-shadow .12s ease, background .12s ease;
}
.combo__control:focus,
.combo.is-open .combo__control{
  background:var(--surface);border-color:var(--green);box-shadow:0 0 0 3px var(--field-focus-ring);
}
.combo__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.combo__value.is-placeholder{color:var(--field-placeholder);}
.combo__chev{width:18px;height:18px;color:var(--fe-text-2);flex:none;transition:transform .15s ease;}
.combo.is-open .combo__chev{transform:rotate(180deg);}
.combo--error .combo__control{background:var(--field-error-fill);border-color:var(--status-error);}
.combo__control[disabled],
.combo__control[aria-disabled="true"]{
  background-color:var(--disabled-fill);color:var(--disabled-ink);cursor:not-allowed;box-shadow:none;
  background-image:var(--lock-disabled);background-repeat:no-repeat;background-position:right 11px center;
}
.combo__control[disabled] .combo__chev,
.combo__control[aria-disabled="true"] .combo__chev{display:none;}  /* replaced by the lock glyph */

/* chips — the chosen set in a multi-select control; each clears via its ×.
   Same pill anatomy as the shared .tag/.tag--removable (Tag / chip); kept
   green here because the colour signals "live selection in a green control". */
.combo .chips{display:flex;flex-wrap:wrap;gap:5px;align-items:center;flex:1;min-width:0;padding:6px 0;}
.combo .chip{display:inline-flex;align-items:center;gap:3px;background:var(--green-light);color:#256B29;font-size:12px;font-weight:var(--fw-semibold);padding:3px 4px 3px 10px;border-radius:999px;white-space:nowrap;}/* darker green text — clears AA (4.5:1) on the pale green chip fill */
.combo .chip__x{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--green-text);cursor:pointer;position:relative;}
/* invisible 24px hit area — keeps the glyph small but clears the touch-target floor */
.combo .chip__x::before{content:"";position:absolute;left:50%;top:50%;width:24px;height:24px;transform:translate(-50%,-50%);}
.combo .chip__x:hover{background:rgba(46,125,50,.18);}
.combo .chip__x svg{width:11px;height:11px;}

/* popup */
.menu{
  position:absolute;top:calc(100% + 4px);left:0;width:100%;z-index:30;
  background:var(--surface);border:1px solid var(--fe-border);border-radius:6px;
  box-shadow:0 8px 24px rgba(0,0,0,.12);overflow:hidden;display:none;
}
/* Width comes from width:100% of the combo (the positioned ancestor), NOT a
   left:0 / right:0 stretch. Revealed from display:none inside a flex container,
   a left/right-stretched popup computes a shrink-to-fit (content) width and
   renders too narrow until a hover forces a relayout. An explicit width has no
   shrink-to-fit path, so it's full width on first open. (.combo is border-box,
   so width:100% + the 1px borders match the control exactly.) */
.combo.is-open .menu{display:block;}
/* dropdown.js adds .is-up when the menu would overflow the viewport bottom */
.combo.is-up .menu{top:auto;bottom:calc(100% + 4px);}
.menu__search{display:flex;align-items:center;gap:8px;padding:9px 11px;border-bottom:1px solid var(--fe-border);}
.menu__search .icon{color:var(--fe-text-2);flex:none;}
.menu__search input{flex:1;border:0;outline:none;font-family:var(--font-sans);font-size:14px;background:transparent;color:var(--ink);}
.menu__search input::placeholder{color:var(--field-placeholder);}
.menu__list{max-height:calc(7 * var(--combo-row-h));overflow-y:auto;padding:4px;}  /* 7 rows then scroll */
.menu__empty{padding:14px 12px;font-size:13px;color:var(--fe-text-2);text-align:center;}

/* option row — hover/keyboard = grey (.is-active); selected = green fill.
   Named .menu__row (not a generic .row) so no external grid or .row rule can
   leak into the options; still pins margin + flex-wrap defensively. */
.menu .menu__row{display:flex;flex-wrap:nowrap;align-items:center;gap:10px;margin:0;height:var(--combo-row-h);padding:0 10px;border-radius:var(--radius);cursor:pointer;font-size:14px;color:var(--ink);}
.menu .menu__row:hover,.menu .menu__row.is-active{background:var(--surface-hover);}
.menu .menu__row.is-selected{background:var(--green-light);}
.menu .menu__row.is-selected:hover,.menu .menu__row.is-selected.is-active{background:#B8DBBA;}  /* darker green over the selected row */
.menu .menu__label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* multi checkbox */
.menu .menu__box{width:16px;height:16px;border:1.5px solid var(--radio-ring);border-radius:3px;flex:none;display:flex;align-items:center;justify-content:center;background:var(--surface);}
.menu .menu__row.is-checked .menu__box{background:var(--green);border-color:var(--green);}
.menu .menu__box svg{width:12px;height:12px;color:#fff;display:none;}
.menu .menu__row.is-checked .menu__box svg{display:block;}
/* remote options loading   (added 2026-06-25) — a spinner row shown in place of
   the list while options are fetched (server-backed selects / typeahead).
   Mirrors .menu__empty's centring. Markup:
     <div class="menu__loading"><span class="spinner spinner--sm" aria-hidden="true"></span>Loading options…</div> */
.menu__loading{display:flex;align-items:center;justify-content:center;gap:9px;padding:16px 12px;font-size:13px;color:var(--fe-text-2);}

/* --------------------------------------------------------------------------
   Search   (locked 2026-06-26)
   A search input on the field shell — soft-fill, clearable. ONE component;
   every feature is a modifier you add or omit:
     .search--icon     leading magnifier (left)
     .search--button   attached green submit button (Enter always submits too)
     .search--suggest  live suggestions popup (reuses the dropdown .menu)
     .search--compact  36px height for tight toolbars / nav bars
   Two behaviours fall out of these: instant filter (no button) and submit
   search (button and/or Enter). This is the canonical search field — the data
   table's filter composes .search .search--icon (standard 40px — full size
   reads better in the table toolbar than compact). Behaviour —
   clear, filled state, Esc, suggest open/close + keyboard — lives in search.js.
   -------------------------------------------------------------------------- */
.search{position:relative;display:inline-flex;align-items:stretch;width:100%;max-width:380px;font-family:var(--font-sans);}
.search__field{position:relative;flex:1;min-width:0;display:flex;}

.search__input{
  width:100%;box-sizing:border-box;height:var(--field-height);
  font-family:var(--font-sans);font-size:var(--field-font-size);color:var(--ink);
  background:var(--field-fill);
  border:1px solid transparent;border-radius:var(--radius);outline:none;
  appearance:none;-webkit-appearance:none;
  padding:0 36px 0 var(--field-pad-x);     /* right room for clear/spinner; left grows with --icon */
  transition:border-color .12s ease, box-shadow .12s ease, background .12s ease;
}
.search__input::placeholder{color:var(--field-placeholder);}
/* drop the browser's native search clear/decoration — we draw our own × */
.search__input::-webkit-search-cancel-button,
.search__input::-webkit-search-decoration{-webkit-appearance:none;appearance:none;display:none;}
.search__input:focus{
  background:var(--surface);border-color:var(--green);
  box-shadow:0 0 0 3px var(--field-focus-ring);
}

/* leading magnifier (.search--icon) */
.search__icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--fe-text-2);pointer-events:none;display:none;}
.search--icon .search__icon{display:block;}
.search--icon .search__input{padding-left:38px;}

/* clear × — shown once there's text (search.js toggles .search--filled) */
.search__clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;display:none;align-items:center;justify-content:center;border:0;background:none;border-radius:50%;color:var(--fe-text-2);cursor:pointer;padding:0;}
.search__clear:hover{background:var(--fe-border);color:var(--ink);}
.search__clear svg{width:15px;height:15px;}
.search--filled .search__clear{display:inline-flex;}

/* loading — a trailing spinner replaces the clear while a query runs */
.search__spin{position:absolute;right:10px;top:50%;transform:translateY(-50%);display:none;}
.search--loading .search__clear{display:none;}
.search--loading .search__spin{display:block;}

/* attached submit button (.search--button) */
.search__btn{display:none;flex:none;align-items:center;gap:7px;height:var(--field-height);padding:0 16px;border:0;border-radius:0 var(--radius) var(--radius) 0;background:var(--green);color:#fff;font-family:var(--font-sans);font-size:14px;font-weight:var(--fw-semibold);cursor:pointer;}
.search__btn:hover{background:var(--green-deep);}
.search__btn svg{width:16px;height:16px;}
.search--button .search__btn{display:inline-flex;}
.search--button .search__input{border-radius:var(--radius) 0 0 var(--radius);}
/* with a button, focus wraps the WHOLE control (field + button): Enter searches,
   so the two read as one unit. The single deliberate break from the per-field
   focus rule. The input's own ring is suppressed; the ring goes on .search. */
.search--button{border-radius:var(--radius);}
.search--button .search__input:focus{box-shadow:none;border-color:transparent;}
.search--button:focus-within .search__input{background:var(--surface);}
.search--button:focus-within{box-shadow:0 0 0 1px var(--green), 0 0 0 4px var(--field-focus-ring);}

/* suggestions popup (.search--suggest) — reuses the dropdown .menu wholesale.
   The open state (.search--open) is owned by search.js. */
.search__menu{display:none;}
.search--suggest.search--open .search__menu{display:block;}
.search__menu .menu__head{padding:9px 12px 5px;font-size:11px;font-weight:var(--fw-semibold);letter-spacing:.06em;text-transform:uppercase;color:var(--fe-text-2);}
.search__menu .menu__row{cursor:pointer;}
.search__ricon{width:15px;height:15px;color:var(--fe-text-2);flex:none;}
.search__hit{font-weight:var(--fw-semibold);color:var(--ink);}   /* the matched substring in a suggestion */

/* compact (.search--compact) — 36px for tight toolbars / nav bars */
.search--compact .search__input{height:var(--field-height-compact);font-size:14px;}
.search--compact .search__btn{height:var(--field-height-compact);}
.search--compact.search--icon .search__input{padding-left:34px;}
.search--compact .search__icon{width:15px;height:15px;left:11px;}

/* --------------------------------------------------------------------------
   Radio   (locked 2026-06-24)
   Pick one from a defined set. Two controls on one group shell (legend/help/
   error/disabled-lock): the classic dot (.rad, vertical) and the selectable
   card (.rcard, horizontal). Selected = green dot / green-bordered tinted card.
   Keyboard model — every enabled option is Tab- and arrow-reachable, focus
   moves WITHOUT selecting, Space/Enter commits, locked options are skipped —
   lives in radio.js. Focus uses the input-family green ring, not the button ring.
   -------------------------------------------------------------------------- */

/* group shell — a fieldset; legend is the group label */
.rgroup{border:0;margin:0 0 var(--space-4);padding:0;min-inline-size:0;}
.rgroup:last-child{margin-bottom:0;}
.rgroup__legend{font:var(--fw-semibold) var(--fs-label)/1.3 var(--font-sans);color:var(--ink);padding:0;margin:0 0 var(--space-2);}
.rgroup__help{font:var(--fw-regular) var(--fs-caption)/1.5 var(--font-sans);color:var(--fe-text-2);margin:-2px 0 var(--space-3);}
.rgroup__error{display:flex;align-items:center;gap:6px;font:var(--fw-regular) var(--fs-caption)/1.5 var(--font-sans);color:var(--status-error);margin:var(--space-3) 0 0;}
.rgroup__error .icon{color:var(--status-error);}

/* the dot control — shared by .rad rows and .rcard tiles */
.rad__input{
  appearance:none;-webkit-appearance:none;margin:0;flex:none;
  width:var(--radio-size);height:var(--radio-size);border-radius:50%;
  border:2px solid var(--radio-ring);background:var(--surface);
  display:grid;place-content:center;cursor:pointer;
  transition:border-color .12s ease, box-shadow .12s ease;
}
.rad__input::after{content:"";width:var(--radio-dot);height:var(--radio-dot);border-radius:50%;background:var(--green);transform:scale(0);transition:transform .12s ease;}
.rad__input:checked{border-color:var(--green);}
.rad__input:checked::after{transform:scale(1);}
.rad__input:focus-visible{outline:none;box-shadow:0 0 0 3px var(--field-focus-ring);}

/* A · classic dot — vertical list */
.rlist{display:flex;flex-direction:column;gap:2px;}
.rad{display:flex;gap:11px;align-items:flex-start;cursor:pointer;padding:8px;border-radius:var(--radius);transition:background .12s ease;}
.rad:hover{background:var(--row-hover);}
.rad__text{display:flex;flex-direction:column;gap:2px;}
.rad__label{font:var(--fw-semibold) var(--field-font-size)/1.3 var(--font-sans);color:var(--ink);}
.rad__desc{font:var(--fw-regular) var(--fs-caption)/1.4 var(--font-sans);color:var(--fe-text-2);}

/* error — tint the rest-state rings red so it reads on the controls, not just text */
.rgroup--error .rad__input{border-color:var(--error-ring);}

/* disabled option (locked) — greyed, lock glyph at the row's right edge */
.rad--disabled,.rad--disabled .rad__input{cursor:not-allowed;}
.rad--disabled .rad__label{color:var(--disabled-ink);}
.rad--disabled .rad__input{border-color:var(--disabled-border);background:var(--disabled-fill);}
.rad__lock{width:18px;height:18px;flex:none;margin-left:auto;align-self:center;background:var(--lock-disabled) center/contain no-repeat;}

/* B · selectable card — horizontal */
/* wraps (added 2026-07-27): the cards are flex:1 but will not shrink past their
   own content, so a row of three on a phone spilled past the viewport instead of
   stacking. Measured at 375px: the row ran 71px past the edge and took the whole
   document with it. Wrapping is the only behaviour that keeps every option
   reachable — these are the choices in a form, not decoration. */
.rcards{display:flex;flex-wrap:wrap;gap:12px;}
.rcard{
  position:relative;flex:1;cursor:pointer;
  border:1px solid var(--fe-border);border-radius:8px;
  padding:16px;display:flex;gap:11px;align-items:flex-start;
  background:var(--surface);transition:border-color .12s ease, box-shadow .12s ease, background .12s ease;
}
.rcard:hover{border-color:var(--radio-ring);}
.rcard:has(.rad__input:checked){border-color:var(--green);box-shadow:inset 0 0 0 1px var(--green);background:var(--radio-card-fill);}
.rcard:has(.rad__input:focus-visible){box-shadow:0 0 0 3px var(--field-focus-ring);}
.rcard__text{display:flex;flex-direction:column;gap:3px;}
.rcard__label{font:var(--fw-semibold) var(--field-font-size)/1.3 var(--font-sans);color:var(--ink);}
.rcard__desc{font:var(--fw-regular) var(--fs-caption)/1.4 var(--font-sans);color:var(--fe-text-2);}
.rcard__badge{position:absolute;top:-9px;right:12px;background:var(--green);color:#fff;font-size:11px;font-weight:var(--fw-semibold);padding:2px 8px;border-radius:999px;}

/* disabled card (locked) — greyed, lock glyph in the corner */
.rcard--disabled{cursor:not-allowed;background:var(--disabled-fill);border-color:var(--fe-border);}
.rcard--disabled .rcard__label,.rcard--disabled .rcard__desc{color:var(--disabled-ink);}
.rcard--disabled .rad__input{border-color:var(--disabled-border);background:var(--disabled-fill);cursor:not-allowed;}
.rcard__lock{position:absolute;top:12px;right:12px;width:18px;height:18px;background:var(--lock-disabled) center/contain no-repeat;}

/* --------------------------------------------------------------------------
   Checkbox   (locked 2026-06-24)
   Any-of, on the same group shell as radio (.rgroup legend/help/error/lock).
   A square box that fills green with a white check. Optional description per
   option; works in a group or standalone (.chk--single, e.g. a consent box).
   Indeterminate (a dash) drives the select-all parent — roll-up logic + Enter-
   to-toggle parity live in checkbox.js. Focus uses the input-family green ring.
   -------------------------------------------------------------------------- */

.clist{display:flex;flex-direction:column;gap:2px;}
.chk{display:flex;gap:11px;align-items:flex-start;cursor:pointer;padding:8px;border-radius:var(--radius);transition:background .12s ease;}
.chk:hover{background:var(--row-hover);}
.chk--single{padding-left:0;}  /* standalone consent box — no legend, no row inset */

.chk__input{
  appearance:none;-webkit-appearance:none;margin:1px 0 0;flex:none;
  width:var(--check-size);height:var(--check-size);border-radius:4px;
  border:2px solid var(--radio-ring);background:var(--surface);
  display:grid;place-content:center;cursor:pointer;
  transition:background .12s ease, border-color .12s ease, box-shadow .12s ease;
}
.chk__input::after{
  content:"";width:13px;height:13px;background:#fff;transform:scale(0);transition:transform .12s ease;
  -webkit-mask:var(--check-glyph) center/contain no-repeat;mask:var(--check-glyph) center/contain no-repeat;
}
.chk__input:checked{background:var(--green);border-color:var(--green);}
.chk__input:checked::after{transform:scale(1);}
/* indeterminate — a dash, not a tick (select-all parent, partial state) */
.chk__input:indeterminate{background:var(--green);border-color:var(--green);}
.chk__input:indeterminate::after{transform:scale(1);-webkit-mask:none;mask:none;width:10px;height:2px;border-radius:1px;}
.chk__input:focus-visible{outline:none;box-shadow:0 0 0 3px var(--field-focus-ring);}

.chk__text{display:flex;flex-direction:column;gap:2px;}
.chk__label{font:var(--fw-semibold) var(--field-font-size)/1.3 var(--font-sans);color:var(--ink);}
.chk__desc{font:var(--fw-regular) var(--fs-caption)/1.4 var(--font-sans);color:var(--fe-text-2);}

/* select-all parent — children indented under a thin rule */
.clist--nested{margin-left:29px;border-left:1px solid var(--fe-border);padding-left:8px;}

/* error — tint the rest-state boxes red so it reads on the controls */
.rgroup--error .chk__input{border-color:var(--error-ring);}

/* disabled (locked) — greyed box + lock glyph at the row's right edge. A
   disabled-but-checked box (forced-on) keeps its check on a muted grey fill. */
.chk--disabled,.chk--disabled .chk__input{cursor:not-allowed;}
.chk--disabled .chk__label{color:var(--disabled-ink);}
.chk--disabled .chk__input{border-color:var(--disabled-border);background:var(--disabled-fill);}
.chk--disabled .chk__input:checked{background:var(--check-disabled-fill);border-color:var(--check-disabled-fill);}
.chk__lock{width:18px;height:18px;flex:none;margin-left:auto;align-self:center;background:var(--lock-disabled) center/contain no-repeat;}

/* Selectable card (multi-select) — the checkbox counterpart to the radio
   .rcard: a card you tick, any number at once. Reuses the .chk__input box as
   the control; selected = green border + faint green tint. Optional corner
   badge + description line. Lay out in .ccards (wrapping horizontal row). */
.ccards{display:flex;flex-wrap:wrap;gap:var(--space-4);}
.ccard{position:relative;display:flex;gap:11px;align-items:flex-start;flex:1 1 200px;min-width:0;padding:var(--space-4);border:1.5px solid var(--fe-border);border-radius:var(--card-radius);background:var(--fe-surface);cursor:pointer;transition:border-color .12s ease, background .12s ease;}
.ccard:hover{border-color:var(--radio-ring);}
.ccard:has(.chk__input:checked){border-color:var(--green);background:var(--radio-card-fill);}
.ccard:has(.chk__input:focus-visible){outline:none;box-shadow:0 0 0 3px var(--field-focus-ring);}
.ccard__text{display:flex;flex-direction:column;gap:2px;min-width:0;}
.ccard__label{font:var(--fw-semibold) var(--field-font-size)/1.3 var(--font-sans);color:var(--ink);}
.ccard__desc{font:var(--fw-regular) var(--fs-caption)/1.4 var(--font-sans);color:var(--fe-text-2);}
.ccard__badge{position:absolute;top:var(--space-3);right:var(--space-3);font-size:var(--fs-eyebrow);font-weight:var(--fw-semibold);color:var(--green-text);}
.ccard--disabled{cursor:not-allowed;}
.ccard--disabled .ccard__label{color:var(--disabled-ink);}

/* --------------------------------------------------------------------------
   Toggle / switch   (locked 2026-06-24)
   A single setting that takes effect immediately (no Save) — that's what
   separates it from the form-submitted single checkbox. On = green, knob right;
   off = grey, knob left, with a CSS-only On/Off readout. Two layouts: the
   settings row (.sw-row, label left / switch right) and a bare inline switch
   (.sw). Space (native) or Enter toggles — Enter parity from toggle.js.
   Use role="switch" on the input so the on/off state is announced.
   -------------------------------------------------------------------------- */

.sw__input{
  appearance:none;-webkit-appearance:none;margin:0;flex:none;cursor:pointer;position:relative;
  width:var(--switch-w);height:var(--switch-h);border-radius:999px;background:var(--switch-off);
  transition:background .15s ease, box-shadow .12s ease;
}
/* invisible hit area — lifts the 22px-tall track to a 24px touch target */
.sw__input::before{content:"";position:absolute;left:0;top:50%;width:100%;height:24px;transform:translateY(-50%);}
.sw__input::after{
  content:"";position:absolute;top:2px;left:2px;
  width:var(--switch-knob);height:var(--switch-knob);border-radius:50%;
  background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.28);transition:transform .15s ease;
}
.sw__input:checked{background:var(--green);}
.sw__input:checked::after{transform:translateX(calc(var(--switch-w) - var(--switch-knob) - 4px));}
.sw__input:focus-visible{outline:none;box-shadow:0 0 0 3px var(--field-focus-ring);}

/* On/Off readout — pure CSS, follows the input's state (decorative; role=switch
   carries the state for assistive tech). Must sit immediately after the input. */
.sw__state{font:var(--fw-regular) var(--fs-caption)/1.4 var(--font-sans);color:var(--fe-text-2);min-width:22px;}
.sw__state::before{content:"Off";}
.sw__input:checked + .sw__state::before{content:"On";color:var(--ink);}

/* A · settings row — label/description left, switch right; whole row toggles */
.sw-row{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:13px 0;margin:0;border-bottom:1px solid var(--fe-border);cursor:pointer;}
.sw-row:last-of-type{border-bottom:0;}
.sw-row__text{display:flex;flex-direction:column;gap:2px;}
.sw-row__label{font:var(--fw-semibold) var(--field-font-size)/1.3 var(--font-sans);color:var(--ink);}
.sw-row__desc{font:var(--fw-regular) var(--fs-caption)/1.4 var(--font-sans);color:var(--fe-text-2);}
.sw-row__control{display:flex;align-items:center;gap:10px;flex:none;}

/* B · bare inline switch — place the label yourself */
.sw{display:inline-flex;align-items:center;gap:10px;cursor:pointer;margin:0;}
.sw__txt{font-size:var(--field-font-size);}

/* disabled (locked) — greyed track, lock glyph in place of the On/Off readout.
   A forced-on locked switch stays muted green so "on" stays legible. */
.sw-row--disabled{cursor:not-allowed;}
.sw-row--disabled .sw-row__label{color:var(--disabled-ink);}
.sw__input:disabled{cursor:not-allowed;background:var(--switch-off-disabled);}
.sw__input:disabled:checked{background:var(--switch-on-disabled);}
.sw__lock{width:18px;height:18px;flex:none;background:var(--lock-disabled) center/contain no-repeat;}

/* --------------------------------------------------------------------------
   Date picker   (locked 2026-06-24)
   Custom calendar on the field shell. One control, three modes (data-mode):
     single   — one date (pick-only, commits on click)
     range    — start–end span (click start, hover-preview, click end)
     datetime — date + an analog clock (hour:minute, AM/PM), commits on OK
   Display "24 Jun 2026"; Monday week start; today ringed; min/max disables days.
   The .cal__* day grid is shared by the single/range popup (.cal) and the
   datetime popup's calendar column (.dtp__cal). Behaviour lives in date-picker.js.
   -------------------------------------------------------------------------- */

/* field control — the closed picker */
.date{position:relative;}
.date__control{
  width:100%;box-sizing:border-box;min-height:var(--field-height);display:flex;align-items:center;gap:8px;
  font-family:var(--font-sans);font-size:var(--field-font-size);color:var(--ink);text-align:left;
  padding:0 var(--field-pad-x);background:var(--field-fill);border:1px solid transparent;
  border-radius:var(--radius);cursor:pointer;outline:none;
  transition:border-color .12s ease, box-shadow .12s ease, background .12s ease;
}
.date__control:focus,.date.is-open .date__control{background:var(--surface);border-color:var(--green);box-shadow:0 0 0 3px var(--field-focus-ring);}
.date__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.date__value.is-placeholder{color:var(--field-placeholder);}
.date__icon{width:18px;height:18px;color:var(--fe-text-2);flex:none;}
.date--error .date__control{background:var(--field-error-fill);border-color:var(--status-error);}
.date__control[disabled]{
  background-color:var(--disabled-fill);color:var(--disabled-ink);cursor:not-allowed;box-shadow:none;
  background-image:var(--lock-disabled);background-repeat:no-repeat;background-position:right 11px center;
}
.date__control[disabled] .date__icon{display:none;}

/* calendar grid — shared by both popups */
.cal__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;}
.cal__title{font-weight:var(--fw-semibold);font-size:14px;}
.cal__nav{width:30px;height:30px;border:0;background:transparent;border-radius:var(--radius);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--ink);}
.cal__nav:hover{background:var(--surface-hover);}
.cal__nav:disabled{color:#D0D0CE;cursor:not-allowed;background:transparent;}
.cal__grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;}
/* role=row wrappers carry grid semantics for assistive tech without affecting
   layout — the day cells stay direct grid items of .cal__grid */
.cal__row{display:contents;}
.cal__dow{font-size:11px;font-weight:var(--fw-semibold);color:var(--fe-text-2);text-align:center;padding:4px 0;}
.cal__day{height:var(--cal-day);border:0;background:transparent;border-radius:var(--radius);cursor:pointer;font-size:13px;color:var(--ink);font-family:var(--font-sans);}
.cal__day:hover{background:var(--surface-hover);}
.cal__day.is-empty{visibility:hidden;cursor:default;}
.cal__day.is-today{box-shadow:inset 0 0 0 1.5px var(--green);font-weight:var(--fw-bold);}
.cal__day.is-inrange{background:var(--green-light);color:var(--ink);border-radius:0;}
.cal__day.is-selected,.cal__day.is-start,.cal__day.is-end{background:var(--green-text);color:#fff;font-weight:var(--fw-semibold);box-shadow:none;}/* --green-text (darker) so the white day number clears AA */
.cal__day.is-start{border-radius:var(--radius) 0 0 var(--radius);}
.cal__day.is-end{border-radius:0 var(--radius) var(--radius) 0;}
.cal__day.is-selected:hover,.cal__day.is-start:hover,.cal__day.is-end:hover{background:var(--green-hover);}
.cal__day.is-disabled{color:#CBCBC9;cursor:not-allowed;}
.cal__day.is-disabled:hover{background:transparent;}
.cal__day:focus-visible{outline:none;box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--green);}

/* single/range popup */
.cal{position:absolute;top:calc(100% + 4px);left:0;z-index:30;width:var(--cal-popup-w);
  background:var(--surface);border:1px solid var(--fe-border);border-radius:8px;
  box-shadow:0 8px 24px rgba(0,0,0,.14);padding:12px;display:none;}
.date.is-open .cal{display:block;}
/* date-picker.js adds .is-up when the popup would overflow the viewport bottom */
.date.is-up .cal,.date.is-up .dtp{top:auto;bottom:calc(100% + 4px);}
.cal__foot{display:flex;justify-content:space-between;align-items:center;margin-top:10px;padding-top:10px;border-top:1px solid var(--fe-border);}
.cal__link{border:0;background:transparent;color:var(--green-text);font-size:13px;font-weight:var(--fw-semibold);cursor:pointer;padding:4px 2px;font-family:var(--font-sans);}
.cal__link:hover{text-decoration:underline;}

/* datetime popup — calendar | time, OK/Cancel below */
.dtp{position:absolute;top:calc(100% + 4px);left:0;z-index:30;display:none;
  grid-template-columns:auto auto;
  background:var(--surface);border:1px solid var(--fe-border);border-radius:8px;box-shadow:0 8px 28px rgba(0,0,0,.16);}
.date.is-open .dtp{display:grid;}
.dtp__cal{grid-column:1;padding:12px;border-right:1px solid var(--fe-border);width:var(--cal-popup-w);}
.dtp__time{grid-column:2;padding:12px 16px;width:252px;}
.dtp__foot{grid-column:1 / 3;display:flex;justify-content:flex-end;gap:10px;padding:10px 14px;border-top:1px solid var(--fe-border);}
.dtp__foot .btn{min-width:96px;min-height:38px;}

/* time head — hour : minute pills + AM/PM */
.tp-head{display:flex;align-items:center;gap:6px;justify-content:center;}
.tp-pill{min-width:52px;height:40px;border:1px solid var(--fe-border);border-radius:6px;background:var(--surface);font-size:20px;font-weight:var(--fw-semibold);color:var(--ink);cursor:pointer;font-family:var(--font-sans);}
.tp-pill.is-active{border-color:var(--green);color:var(--green);box-shadow:inset 0 0 0 1px var(--green);}
.tp-colon{font-size:20px;font-weight:var(--fw-semibold);}
.tp-ampm{display:inline-flex;flex-direction:column;border:1px solid var(--fe-border);border-radius:6px;overflow:hidden;margin-left:8px;}
.tp-ampm button{width:42px;height:20px;border:0;background:var(--surface);cursor:pointer;font-weight:var(--fw-semibold);font-size:12px;color:var(--ink);font-family:var(--font-sans);}
.tp-ampm button.is-active{background:var(--green-text);color:#fff;}/* darker green so the white AM/PM label clears AA */

/* analog clock */
.clock{position:relative;width:var(--clock-size);height:var(--clock-size);margin:18px auto 0;border-radius:50%;background:var(--field-fill);}
.clock__num{position:absolute;width:var(--clock-num);height:var(--clock-num);display:flex;align-items:center;justify-content:center;border-radius:50%;font-size:14px;color:var(--ink);cursor:pointer;transform:translate(-50%,-50%);user-select:none;}
.clock__num:hover{background:var(--surface-hover);}
.clock__num.is-sel{background:var(--green-text);color:#fff;font-weight:var(--fw-semibold);}/* darker green so the white number clears AA */
.clock__hand{position:absolute;left:calc(50% - 1px);bottom:50%;width:2px;background:var(--green);transform-origin:bottom center;z-index:0;}
.clock__hub{position:absolute;left:50%;top:50%;width:8px;height:8px;background:var(--green);border-radius:50%;transform:translate(-50%,-50%);z-index:1;}
.clock__label{position:absolute;left:50%;top:64%;transform:translateX(-50%);font-size:11px;color:var(--fe-text-2);}

/* --------------------------------------------------------------------------
   FILE UPLOAD — dropzone + browse, single/multiple, generic file rows with
   per-file progress + states. Behaviour: file-upload.js.
   -------------------------------------------------------------------------- */
.up{position:relative;}

/* dropzone */
.up__zone{
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;
  min-height:var(--up-min-h);padding:24px 20px;text-align:center;
  background:var(--field-fill);border:1.5px dashed var(--up-border);border-radius:var(--radius);
  cursor:pointer;transition:background .12s ease,border-color .12s ease;
}
.up__zone:hover{border-color:#9E9E9C;}
.up__zone:focus-visible{outline:none;border-color:var(--green);box-shadow:0 0 0 3px var(--field-focus-ring);}
.up.is-dragover .up__zone{background:#EAF5EA;border-color:var(--green);border-style:solid;}
.up__icon{width:30px;height:30px;color:var(--fe-text-2);}
.up.is-dragover .up__icon{color:var(--green);}
.up__primary{font-size:var(--field-font-size);color:var(--ink);}
.up__browse{color:var(--green-text);font-weight:var(--fw-semibold);text-decoration:underline;text-underline-offset:2px;}
.up__hint{font-size:var(--fs-caption);color:var(--fe-text-2);}
.up__input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}

/* compact button variant — hides the zone, shows a secondary-styled button */
.up--compact .up__zone{display:none;}
.up__btn{display:inline-flex;align-items:center;gap:8px;height:var(--field-height);padding:0 16px;
  border:1.5px solid var(--ink);border-radius:var(--radius);background:var(--surface);color:var(--ink);
  font:var(--fw-semibold) var(--field-font-size)/1 var(--font-sans);cursor:pointer;}
.up__btn:hover{background:var(--field-fill);}
.up__btn:focus-visible{outline:none;box-shadow:0 0 0 3px var(--field-focus-ring);}
.up__btn .icon{width:16px;height:16px;}
.up__hint-inline{font-size:var(--fs-caption);color:var(--fe-text-2);margin-left:12px;}

/* file list + rows */
.up__list{list-style:none;margin:14px 0 0;padding:0;display:flex;flex-direction:column;gap:8px;}
.up__list:empty{margin:0;}
.frow{display:flex;align-items:center;gap:12px;padding:10px 12px;background:var(--surface);border:1px solid var(--fe-border);border-radius:var(--radius);}
.frow__icon{width:22px;height:22px;color:var(--fe-text-2);flex:none;}
.frow__main{flex:1;min-width:0;}
.frow__name{font-size:var(--fs-body-sm);color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.frow__meta{font-size:12px;color:var(--fe-text-2);margin-top:1px;}
.frow__bar{height:4px;border-radius:2px;background:var(--up-track);overflow:hidden;margin-top:7px;}
.frow__fill{height:100%;width:0;background:var(--green);border-radius:2px;transition:width .2s linear;}
.frow__status{flex:none;display:inline-flex;align-items:center;}
.frow__status .icon{width:18px;height:18px;}
.frow__act{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;
  border:0;background:transparent;border-radius:50%;color:var(--fe-text-2);cursor:pointer;}
.frow__act:hover{background:var(--field-fill);color:var(--ink);}
.frow__act .icon{width:16px;height:16px;}

/* row states */
.frow.is-done .frow__status{color:var(--status-success);}
.frow.is-error{border-color:var(--error-ring);background:var(--field-error-fill);}
.frow.is-error .frow__icon,
.frow.is-error .frow__name,
.frow.is-error .frow__meta{color:var(--status-error);}

/* dropzone error (rejected on drop) + locked */
.up--error .up__zone{border-color:var(--status-error);background:var(--field-error-fill);}
.up__error{display:none;align-items:center;gap:6px;font:var(--fw-regular) var(--fs-caption)/1.5 var(--font-sans);color:var(--status-error);margin-top:8px;}
.up--error .up__error{display:flex;}
.up__error .icon{width:15px;height:15px;color:var(--status-error);}
.up--disabled .up__zone{cursor:not-allowed;background:#F2F1EE;border-style:solid;border-color:var(--fe-border);color:var(--fe-text-2);opacity:.85;}
.up--disabled .up__icon{display:none;}
.up--disabled .up__zone::before{content:"";width:24px;height:24px;background:var(--lock-disabled) center/contain no-repeat;}
.up--disabled .up__browse{color:var(--fe-text-2);text-decoration:none;}

/* ==========================================================================
   ICON BUTTON  —  text-free button, a single glyph. Circular only.
   Variants: ghost (default) · solid · danger · info. Sizes 40 / 32 / 28.
   No disabled state by design: never use an icon button for an action that
   can be disabled — use a labelled button instead (see icon-button.md).
   Markup: <button class="ib ib--info" aria-label="…" title="…"><svg class="icon" …></button>
   ========================================================================== */
.ib{
  display:inline-flex;align-items:center;justify-content:center;
  width:40px;height:40px;padding:0;flex:none;
  border:1px solid transparent;border-radius:50%;
  background:transparent;color:var(--fe-text);
  cursor:pointer;
  transition:background .12s ease, border-color .12s ease,
             color .12s ease, box-shadow .12s ease;
}
.ib .icon{width:20px;height:20px;flex:none;display:inline-block;}

/* sizes */
.ib--32{width:32px;height:32px;}
.ib--32 .icon{width:18px;height:18px;}
.ib--28{width:28px;height:28px;}
.ib--28 .icon{width:16px;height:16px;}

/* ghost (default) — ink glyph, faint grey fill on hover */
.ib:hover{background:var(--surface-hover);}

/* solid — green fill, white glyph (a prominent standalone add) */
.ib--solid{background:var(--green-deep);color:#fff;}
.ib--solid:hover{background:var(--green-hover);}

/* danger — red glyph, faint red fill on hover (destructive) */
.ib--danger{color:var(--status-error);}
.ib--danger:hover{background:var(--field-error-fill);}

/* info — blue (i), faint blue fill on hover (opens an info modal) */
.ib--info{color:var(--status-info);}
.ib--info:hover{background:var(--ib-info-fill);}

/* focus — same ring as .btn; renders on the circle, keyboard only */
.ib:focus-visible{
  outline:none;
  box-shadow:0 0 0 2px var(--surface), 0 0 0 4px var(--ink);
}

/* loading   (added 2026-06-25) — an async glyph action (refresh / sync) is
   running. A .spinner swaps in for the glyph, sized to match it (20/18/16 per
   size). The button goes inert; mark it aria-busy. On .ib--solid (green fill)
   add spinner--on so the mark is white. This is a temporary, self-clearing
   state — distinct from disabled, which the icon button still never carries.
     <button class="ib is-loading" aria-busy="true" aria-label="Refreshing">
       <span class="spinner" aria-hidden="true"></span><svg class="icon">…</svg></button> */
.ib .spinner{display:none;--_sp-size:20px;--_sp-bw:2.5px;}
.ib--32 .spinner{--_sp-size:18px;}
.ib--28 .spinner{--_sp-size:16px;--_sp-bw:2px;}
.ib.is-loading{pointer-events:none;cursor:progress;}
.ib.is-loading .icon{display:none;}
.ib.is-loading .spinner{display:block;}

/* ==========================================================================
   TEXT LINK  —  navigation, not action. Green-text, underlined always.
   Base type style is .t-link (type scale); .link is the interactive component
   that adds visited + focus and carries the standalone/arrow & external cases.
   ========================================================================== */
.link{
  color:var(--green-text);
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-underline-offset:2px;
  cursor:pointer;
  border-radius:2px;
}
.link:hover{color:var(--green-deep);}

/* visited — muted grey; hover (declared after) wins, so a visited link
   lights back to green on hover. :visited may only set colour, by design. */
.link:visited{color:var(--link-visited);}
.link:visited:hover{color:var(--green-deep);}

/* focus — same ring as .btn (keyboard only). On a wrapping link the ring
   follows the inline run; that's expected. */
.link:focus-visible{
  outline:none;
  box-shadow:0 0 0 2px var(--surface), 0 0 0 4px var(--ink);
}

/* arrow / external icon sits inline with the text, sized to the text */
.link .icon{width:1em;height:1em;flex:none;display:inline-block;vertical-align:-2px;}

/* visually-hidden text — present for screen readers, off-screen visually.
   Used for an external link's "(opens in new tab)"; reuse anywhere a control
   needs an accessible name the design doesn't show. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0,0,0,0);white-space:nowrap;border:0;}

/* skip link — first focusable element; off-screen until focused, then visible */
.skip-link{position:fixed;left:8px;top:-48px;z-index:100;background:var(--surface);color:var(--ink);
  padding:8px 14px;border-radius:var(--radius);text-decoration:none;font-weight:var(--fw-semibold);
  box-shadow:0 2px 10px rgba(0,0,0,.2);transition:top .15s ease;}
.skip-link:focus{top:8px;outline:none;box-shadow:0 0 0 2px var(--green),0 2px 10px rgba(0,0,0,.2);}

/* ==========================================================================
   ACTION MENU  —  a ⋯ icon-button trigger that opens a flat list of actions.
   Sibling of the dropdown popup (same panel look) but for commands, not values:
   each item runs and closes the menu. Flat, always-icons, actions-only.
   Destructive items are red and sit in the flow (no divider — a confirm modal
   carries the safety). Behaviour + keyboard in action-menu.js.
   Markup: .amenu > (.ib.amenu__trigger) + (.amenu__panel[role=menu] > .amenu__item[role=menuitem]).
   ========================================================================== */
.amenu{position:relative;display:inline-block;}
.amenu__panel{
  position:absolute;top:calc(100% + 6px);right:0;z-index:30;min-width:190px;
  background:var(--surface);border:1px solid var(--fe-border);border-radius:6px;
  box-shadow:0 8px 24px rgba(0,0,0,.12);padding:4px;
}
.amenu__panel[hidden]{display:none;}
.amenu--left .amenu__panel{right:auto;left:0;}              /* left-align to the trigger */
.amenu--up .amenu__panel{top:auto;bottom:calc(100% + 6px);} /* flips up (set by action-menu.js) */

.amenu__item{
  display:flex;align-items:center;gap:10px;width:100%;
  height:var(--combo-row-h);padding:0 10px;
  border:0;background:transparent;border-radius:var(--radius);
  font-family:var(--font-sans);font-size:14px;color:var(--ink);
  text-align:left;cursor:pointer;white-space:nowrap;
}
.amenu__item .icon{width:16px;height:16px;flex:none;color:var(--fe-text-2);}
.amenu__item:hover,.amenu__item.is-active,
.amenu__item:focus-visible{outline:none;background:var(--surface-hover);}

/* destructive — red text + icon, red-tinted hover. No divider, by decision. */
.amenu__item--danger{color:var(--status-error);}
.amenu__item--danger .icon{color:var(--status-error);}
.amenu__item--danger:hover,.amenu__item--danger.is-active{background:var(--field-error-fill);}

/* disabled item — greyed, not actionable (matches the disabled-control grey) */
.amenu__item[disabled]{color:var(--disabled-ink);cursor:not-allowed;}
.amenu__item[disabled] .icon{color:var(--disabled-ink);}
.amenu__item[disabled]:hover{background:transparent;}

/* --------------------------------------------------------------------------
   Rating slider   (locked 2026-06-30, Inputs & forms group)
   A labelled slider that snaps to the stops of a small discrete scale — a
   self-rating, a severity, a maturity level. A native range input gives the
   snapping (step), keyboard and drag for free; green fills the track to the
   left of the handle. The readout names the current stop; .is-unset greys the
   handle until the first move (so "not rated" reads apart from the first stop).
   Ships rating-slider.js (fill + readout + emits rating:change).
   -------------------------------------------------------------------------- */

.rating-slider{--pct:0%;padding:0 10px;}
.rating-slider__readout{display:flex;justify-content:flex-end;margin-bottom:var(--space-2);}
.rating-slider__value{font-size:var(--fs-body-sm);font-weight:var(--fw-semibold);color:var(--green-text);}
.rating-slider.is-unset .rating-slider__value{color:var(--fe-text-2);font-weight:var(--fw-regular);}

.rating-slider__input{-webkit-appearance:none;appearance:none;width:100%;height:22px;background:transparent;cursor:pointer;margin:0;}
.rating-slider__input:focus{outline:none;}
/* webkit — track fills green to the --pct point; thumb is a ringed circle */
.rating-slider__input::-webkit-slider-runnable-track{height:var(--slider-track);border-radius:3px;background:linear-gradient(to right,var(--green) var(--pct),var(--up-track) var(--pct));}
.rating-slider.is-unset .rating-slider__input::-webkit-slider-runnable-track{background:var(--up-track);}
.rating-slider__input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;margin-top:calc((var(--slider-track) - var(--slider-thumb)) / 2);width:var(--slider-thumb);height:var(--slider-thumb);border-radius:50%;background:var(--surface);border:2px solid var(--green);box-shadow:0 1px 2px rgba(33,33,33,.2);}
.rating-slider.is-unset .rating-slider__input::-webkit-slider-thumb{border-color:var(--radio-ring);}
.rating-slider__input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 4px var(--field-focus-ring);}
/* firefox — native progress paints the left fill */
.rating-slider__input::-moz-range-track{height:var(--slider-track);border-radius:3px;background:var(--up-track);}
.rating-slider__input::-moz-range-progress{height:var(--slider-track);border-radius:3px;background:var(--green);}
.rating-slider.is-unset .rating-slider__input::-moz-range-progress{background:var(--up-track);}
.rating-slider__input::-moz-range-thumb{width:var(--slider-thumb);height:var(--slider-thumb);border-radius:50%;background:var(--surface);border:2px solid var(--green);box-shadow:0 1px 2px rgba(33,33,33,.2);}
.rating-slider.is-unset .rating-slider__input::-moz-range-thumb{border-color:var(--radio-ring);}
.rating-slider__input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 4px var(--field-focus-ring);}

.rating-slider__labels{display:flex;justify-content:space-between;margin-top:var(--space-2);}
.rating-slider__label{flex:1;font-size:var(--fs-caption);color:var(--fe-text-2);}
.rating-slider__label:not(:first-child):not(:last-child){text-align:center;}
.rating-slider__label:last-child{text-align:right;}
.rating-slider__label.is-current{color:var(--green-text);font-weight:var(--fw-semibold);}

/* --------------------------------------------------------------------------
   Divider   (locked 2026-06-26)
   A quiet 1px hairline — the standalone version of the rule panel / stat /
   key-value / the table already draw inside themselves (1px solid --fe-border,
   so it can never drift from them). No new tokens.
   Base is margin-free: the parent owns the spacing, exactly like every other
   hairline in the system; --tight / --loose opt in for ad-hoc use.
   Markup:  <hr class="divider">                       (horizontal rule)
            <div class="divider divider--labelled" role="separator">or</div>
            <div class="divider divider--vertical" aria-hidden="true"></div>
   Modifiers compose: --tight/--loose (spacing) · --inset · --be (back-end
   surface colour) · --labelled (centred text) · --vertical.
   --inset reads --divider-inset (default 44px = a 32px icon column + 12px gap);
   set --divider-inset on the host to match a different leading column.
   -------------------------------------------------------------------------- */
.divider{border:0;border-top:1px solid var(--fe-border);height:0;margin:0;}

/* spacing helpers — opt-in; the base never bakes in a margin */
.divider--tight{margin-block:var(--space-3);}   /* 12px */
.divider--loose{margin-block:var(--space-5);}   /* 24px */

/* inset — indent past a leading icon / avatar column */
.divider--inset{margin-left:var(--divider-inset,44px);}

/* back-end surface — swaps to the operational border colour */
.divider--be{border-top-color:var(--be-border);}

/* labelled — a centred rule broken by text */
.divider--labelled{
  display:flex;align-items:center;gap:var(--space-3);border:0;height:auto;
  color:var(--fe-text-2);font-size:var(--fs-caption);font-weight:var(--fw-semibold);
  letter-spacing:.04em;text-transform:uppercase;
}
.divider--labelled::before,.divider--labelled::after{content:"";flex:1;height:1px;background:var(--fe-border);}
.divider--labelled.divider--be::before,.divider--labelled.divider--be::after{background:var(--be-border);}

/* vertical — short upright hairline between inline items; stretches to its row */
.divider--vertical{
  border-top:0;border-left:1px solid var(--fe-border);
  width:0;height:auto;align-self:stretch;min-height:1em;margin:0;
}
.divider--vertical.divider--be{border-left-color:var(--be-border);}

/* --------------------------------------------------------------------------
   Card   (locked 2026-06-26)
   Basic content card — the first of a card family. A white surface with an
   optional green eyebrow, an optional centred icon (above the body), a title,
   body, and an optional inline footer of buttons. Footer buttons fill the row:
   one = full width, two = 50/50 (cap at two — a third path is a text .link).
   A flex column with the footer pushed to the bottom (margin-top:auto), so
   cards in a row line up; equal height comes from the row container's
   align-items:stretch (use .card-grid), not a fixed height. --card-min-h is an
   opt-in floor the host can set; default none.
   Tokens: --card-pad / --card-radius / --card-shadow.
   Markup: <article class="card"> eyebrow? · title · icon? · body · footer? </article>
   -------------------------------------------------------------------------- */
.card{
  /* Redundant since the file-wide reset at the top (added 2026-07-27), and kept
     because .card is the component that exposed the gap: it carries BOTH
     height:100% and 28px of padding, which is the combination that breaks —
     give it a parent with a definite height and the 100% resolves to the
     CONTENT box, so the border box lands 58px taller and the card hangs out of
     the row holding it. MEASURED on /project-details, where a 340px identity
     row rendered a 398px card. Restating it costs nothing and keeps the card
     correct if it is ever lifted out of this file. */
  box-sizing:border-box;
  display:flex;flex-direction:column;height:100%;
  min-height:var(--card-min-h,auto);
  background:var(--fe-surface);
  border:1px solid transparent;        /* reserved so --flat adds a border without shifting layout */
  border-radius:var(--card-radius);
  box-shadow:var(--card-shadow);
  padding:var(--card-pad);
}
.card--flat{box-shadow:none;border-color:var(--fe-border);}   /* dense dashboards — border, no shadow */
.card--be{background:var(--be-surface);}                       /* back-end surface */

.card__eyebrow{
  font-size:var(--fs-eyebrow);line-height:var(--lh-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;font-weight:var(--fw-semibold);color:var(--green-text);margin:0 0 12px;
}
.card__title{font-size:var(--fs-h3);line-height:var(--lh-h3);font-weight:var(--fw-bold);color:var(--ink);margin:0 0 10px;}

/* icon — larger, centred, sits above the body */
.card__icon{color:var(--green-deep);display:flex;justify-content:center;margin:6px 0 18px;}
.card__icon svg{width:44px;height:44px;display:block;stroke-width:1.75;}

.card__body{font-size:var(--fs-body);line-height:var(--lh-body);color:var(--fe-text);margin:0;}

/* footer — inline; buttons fill the row (one = full width, two = 50/50) */
.card__footer{margin-top:auto;padding-top:var(--space-5);display:flex;gap:var(--space-3);}
.card__footer .btn{flex:1 1 0;min-width:0;}

/* card-grid — drop-in responsive row that equalises card heights via stretch */
.card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--space-5);align-items:stretch;}

/* --------------------------------------------------------------------------
   Accordion   (locked 2026-06-26)
   Seamless flush list of collapsible rows — hairline dividers BETWEEN rows
   only, no box and no fill, so it reads as part of the page. Built on native
   <details>/<summary>: keyboard + screen-reader support and the toggle are
   the browser's, no JavaScript. Multi-open by default (rows independent);
   --single makes them exclusive — give every <details> a shared name="" and
   the browser closes the others (no JS). Optional right-aligned slots before
   the chevron: a muted .accordion__meta count and/or a status .badge.
   No new tokens — reuses --fe-border, spacing, type, --green, the inks.
   Markup: <div class="accordion [accordion--single]">
             <details class="accordion__item" [open] [name="grp"]>
               <summary class="accordion__header">
                 <p class="accordion__title">…</p>
                 <span class="badge …">…</span>?  <span class="accordion__meta">…</span>?
                 <span class="accordion__chev"><svg …chevron-down…></span>
               </summary>
               <div class="accordion__body">…</div>
             </details> …
           </div>
   -------------------------------------------------------------------------- */
.accordion{interpolate-size:allow-keywords;}   /* lets the panel height animate to/from auto */

/* row — a hairline rule BETWEEN rows only (never top or bottom of the list) */
.accordion__item{border-top:1px solid var(--fe-border);}
.accordion__item:first-child{border-top:0;}

/* header — the native <summary>; default disclosure marker removed */
.accordion__header{
  list-style:none;
  display:flex;align-items:center;gap:var(--space-3);
  padding:18px 0;
  cursor:pointer;outline:none;
  transition:background .12s ease;
}
.accordion__header::-webkit-details-marker{display:none;}
.accordion__header:hover{background:rgba(33,33,33,.022);}
.accordion__header:focus-visible{box-shadow:0 0 0 2px var(--green);border-radius:3px;}

.accordion__title{
  flex:1 1 auto;min-width:0;
  font-size:var(--fs-body);line-height:1.35;font-weight:var(--fw-semibold);
  color:var(--ink);margin:0;
}
.accordion__meta{flex:none;font-size:var(--fs-body-sm);color:var(--fe-text-2);}   /* right-aligned count */

.accordion__chev{flex:none;width:20px;height:20px;color:var(--fe-text-2);transition:transform .2s ease;}
.accordion__chev svg{width:20px;height:20px;stroke-width:2;display:block;}
.accordion__item[open] > .accordion__header .accordion__chev{transform:rotate(180deg);}

/* panel — animates height in supporting browsers, snaps instantly elsewhere */
.accordion__item::details-content{
  block-size:0;overflow:hidden;
  transition:block-size .22s ease, content-visibility .22s allow-discrete;
}
.accordion__item[open]::details-content{block-size:auto;}
.accordion__body{padding:0 0 20px;margin-top:-2px;}

@media (prefers-reduced-motion: reduce){
  .accordion__chev,.accordion__item::details-content{transition:none;}
}

/* --------------------------------------------------------------------------
   Modal   (locked 2026-06-24)
   Centred dialog over a dimmed overlay. Two widths (sm 560 / lg 840). Header
   neutral by default; error / warning / info get a soft-tint band + status
   icon. X close shows only when there's no footer. Destructive confirms use
   the black .btn--tertiary (no red button variant). Behaviour in modal.js.
   -------------------------------------------------------------------------- */
.modal-overlay{
  position:fixed;inset:0;z-index:100;
  background:var(--modal-overlay);
  display:flex;align-items:center;justify-content:center;
  padding:var(--space-5);                 /* 24px gutter so the modal never touches the edge */
  opacity:0;transition:opacity .15s ease;  /* fade — 150ms ease */
}
.modal-overlay.is-open{opacity:1;}
.modal-overlay[hidden]{display:none;}

.modal{
  width:var(--modal-w-sm);max-width:100%;
  max-height:calc(100vh - var(--space-6));
  background:var(--surface);
  border-radius:var(--modal-radius);
  box-shadow:var(--modal-shadow);
  display:flex;flex-direction:column;overflow:hidden;
  opacity:0;transform:translateY(8px) scale(.985);
  transition:opacity .15s ease, transform .15s ease;  /* eases in/up — 150ms */
}
.modal-overlay.is-open .modal{opacity:1;transform:none;}
.modal--lg{width:var(--modal-w-lg);}

/* header — neutral default: white, plain, title only (+ X when no footer) */
.modal__header{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-5) var(--space-5) 0;}
.modal__title{flex:1;margin:0;font-size:var(--fs-h3);line-height:var(--lh-h3);font-weight:var(--fw-bold);
  text-transform:none;letter-spacing:normal;}  /* defend against host h2 styling (it's an <h2>) */
.modal__icon{display:none;width:24px;height:24px;flex:none;}  /* shown only on themed variants */
.modal__close{margin:-4px -8px -4px 0;}                       /* the .ib X; nudge to the corner */

/* themed bands — error / warning / info. Band fills the header; icon+title sit in it. */
.modal--error .modal__header,
.modal--warning .modal__header,
.modal--info .modal__header{padding:var(--space-4) var(--space-5);}
.modal--error .modal__icon,
.modal--warning .modal__icon,
.modal--info .modal__icon{display:block;}

.modal--error .modal__header{background:var(--field-error-fill);}
.modal--error .modal__icon,.modal--error .modal__title{color:var(--status-error);}
.modal--warning .modal__header{background:var(--modal-warn-fill);}
.modal--warning .modal__icon,.modal--warning .modal__title{color:var(--status-warning-ink);}
.modal--info .modal__header{background:var(--ib-info-fill);}
.modal--info .modal__icon,.modal--info .modal__title{color:var(--status-info);}

/* body — scrolls if content is tall; the header and footer stay put */
.modal__body{padding:var(--space-4) var(--space-5) var(--space-2);overflow-y:auto;}
.modal--error .modal__body,
.modal--warning .modal__body,
.modal--info .modal__body{padding-top:var(--space-5);}   /* a little air below the band */
.modal__subhead{margin:0 0 var(--space-2);font-size:var(--fs-body);font-weight:var(--fw-semibold);}
.modal__body p{margin:0 0 var(--space-3);font-size:15px;line-height:1.6;color:var(--fe-text);}
.modal__body p:last-child{margin-bottom:0;}

/* footer — 1–2 buttons, right-aligned. Secondary sits left of primary. */
.modal__footer{display:flex;justify-content:flex-end;gap:var(--space-3);padding:var(--space-4) var(--space-5) var(--space-5);}
.modal__footer .btn{min-width:104px;min-height:40px;}

/* page doesn't scroll behind an open modal (set by modal.js) */
body.modal-open{overflow:hidden;}

/* --------------------------------------------------------------------------
   Panel   (locked 2026-06-24)
   Right-sliding overlay surface — the interactive counterpart to the
   display-only modal. This is where inputs, forms, tabs and rich read content
   live (the modal defers all editing here). Full-height, hugs the right edge,
   over the same dimmed overlay. Two widths (md 400 / lg 720). Header is always
   title + X; footer (right-aligned buttons) is optional — a panel may carry
   both an X and a footer (Save + dismiss). Behaviour in panel.js.
   Markup: .panel-overlay > .panel[role=dialog] > .panel__header + __body + __footer.
   -------------------------------------------------------------------------- */
.panel-overlay{
  position:fixed;inset:0;z-index:100;
  background:var(--modal-overlay);          /* same dim as the modal */
  display:flex;justify-content:flex-end;     /* panel hugs the right edge, full height */
  opacity:0;transition:opacity .2s ease;
}
.panel-overlay.is-open{opacity:1;}
.panel-overlay[hidden]{display:none;}

.panel{
  width:var(--panel-w-md);max-width:100%;height:100vh;
  background:var(--surface);
  box-shadow:var(--panel-shadow);
  display:flex;flex-direction:column;overflow:hidden;
  transform:translateX(100%);               /* off-screen right when closed */
  transition:transform .2s ease;            /* slides in — 200ms (a touch slower than the modal fade) */
}
.panel-overlay.is-open .panel{transform:none;}
.panel--lg{width:var(--panel-w-lg);}

/* --- takeover (added 2026-07-27, the board's job overlay) ------------------
   A THIRD SIZE, not a fourth overlay component. Everything a takeover needs —
   the body-level host, role="dialog"/aria-modal, the focus trap, Escape, the
   background inert, the scroll lock, the stack, focus returning to the trigger
   — is what panel.js already does, and the only thing that differs is how wide
   the surface is. A separate component would have been a second copy of that
   engine, drifting from this one the first time either was touched. Same fold
   the drawer took into this component.

   WHAT IT IS FOR: showing a whole EXISTING page over the current one without
   leaving it — the board opens a job this way, so the reader keeps their
   filters, their scroll position and their place in the lane. The framed page
   stays the one source of truth; the overlay never re-implements it.

   IT STOPS SHORT OF THE EDGES, and that margin is the component's whole
   honesty (changed 2026-07-27, owner). A framed page at 100%/100dvh is
   pixel-for-pixel indistinguishable from having NAVIGATED to that page: the
   reader cannot tell they are still on the board, so the one thing the
   takeover exists to communicate — you have not left — is the one thing it
   failed to show. 90% x 90dvh leaves the dimmed board visible all the way
   around, which is how every other overlay in this library says "there is a
   page behind me".

   So it is CENTRED on its dim rather than hugging the right edge, and it
   borrows the modal's radius, shadow and 150ms rise: at this size it reads as
   a large modal, and it should move like one. The right-hand slide belongs to
   a surface that arrives from beside the page — this one arrives ON it.
   :has() rather than a modifier class on the overlay, so the variant stays a
   single class on the surface, where the other two sizes live.

   The dim itself needed no change: .panel-overlay has always carried
   --modal-overlay. At full width the surface simply covered every pixel of it.

   dvh, NOT the .panel base's vh. A right-hand panel at 100vh loses a little
   height under a mobile address bar and nobody notices; this one is measured
   against the viewport on purpose, so the same gap would eat its bottom
   margin. Same argument the app shell already makes.

   THE BODY IS A FRAME, so .panel__body's 24px padding would frame the page in
   a border it did not ask for — .panel__frame replaces it edge to edge. The
   header stays exactly as it is: title + X, the reader's way out. */
.panel-overlay:has(.panel--takeover){align-items:center;justify-content:center;}
.panel--takeover{
  width:90%;height:90dvh;
  border-radius:var(--modal-radius);box-shadow:var(--modal-shadow);
  /* Closed state. .panel-overlay.is-open .panel wins on specificity when open,
     so the open state needs no rule of its own. */
  transform:translateY(12px) scale(.99);
  transition:transform .15s ease;
}
.panel__frame{flex:1 1 auto;width:100%;border:0;display:block;background:var(--fe-canvas);}

/* header — title + X, divider beneath. (It's an <h2>; defend against host styling.) */
.panel__header{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--fe-border);}
.panel__title{flex:1;margin:0;font-size:var(--fs-h3);line-height:var(--lh-h3);font-weight:var(--fw-bold);
  text-transform:none;letter-spacing:normal;}
.panel__close{margin:-4px -8px -4px 0;}     /* the .ib X; nudge to the corner */

/* body — scrolls; header + footer stay put. Holds inputs / forms / rich content. */
.panel__body{flex:1;overflow-y:auto;padding:var(--space-5);}
.panel__body p{margin:0 0 var(--space-3);font-size:15px;line-height:1.6;color:var(--fe-text);}
.panel__body p:last-child{margin-bottom:0;}
.panel__subhead{margin:0 0 var(--space-2);font-size:var(--fs-body);font-weight:var(--fw-semibold);}

/* a simple key/value detail list for read panels */
.panel__facts{list-style:none;margin:0;padding:0;}
.panel__facts li{display:flex;justify-content:space-between;gap:var(--space-4);padding:10px 0;border-bottom:1px solid var(--fe-border);font-size:14px;}
.panel__facts li:last-child{border-bottom:0;}
.panel__facts dt{color:var(--fe-text-2);margin:0;}
.panel__facts dd{margin:0;font-weight:var(--fw-semibold);text-align:right;}

/* footer — right-aligned button row, divider above (optional) */
.panel__footer{display:flex;justify-content:flex-end;gap:var(--space-3);padding:var(--space-4) var(--space-5);border-top:1px solid var(--fe-border);}
.panel__footer .btn{min-height:40px;}

/* page doesn't scroll behind an open panel (set by panel.js) */
body.panel-open{overflow:hidden;}

/* ...and under .app-shell--fixed-chrome the page is not what scrolls. The
   background is .app-shell__main, and overflow:hidden on <body> says nothing
   about a different element. MEASURED, on a 5514px-deep table with a panel
   open: the content scrolled 2000px behind the panel with fixed chrome on, and
   0px with it off — the wheel hit-tests to the overlay, which is fixed and
   body-level, and the scroll falls through to the viewport-filling scroller
   behind it. So the lock has to name that scroller too.

   scrollbar-gutter:stable on the container is what keeps this from shifting the
   content sideways every time a panel opens: without it, hiding the overflow
   removes the scrollbar and the content jumps by its width. */
body.panel-open .app-shell--fixed-chrome .app-shell__main,
body.modal-open .app-shell--fixed-chrome .app-shell__main{overflow:hidden;}

/* ...and the same argument, one level further down (added 2026-07-27).
   .dtable--fill makes a table's rows region a THIRD viewport-filling scroller,
   and the rule above only ever names two. The lock is extended to it on the
   same reasoning: overflow:hidden on <body> and on .app-shell__main each say
   nothing about a different element, and the Work surface already opens a
   detail panel from a table row, so a fill table and an open panel will meet.

   HONEST STATUS: this is guarding by consistency, not a fix for a reproduced
   defect. Attempts to reproduce a fall-through into the table scroller all
   measured 0px — but so did an attempt to reproduce the ORIGINAL, measured
   .app-shell__main bug with its own lock removed, in the same rig. A test that
   cannot reproduce the known case cannot rule out the new one, so neither
   result means anything and no claim is made from them. It costs nothing
   verified: the overflow flips auto->hidden while a panel is open, the rows
   region does not shift sideways, and it scrolls normally again on close.
   Verification belongs on a real surface once the variant is applied to one.

   scrollbar-gutter:stable is on .dtable--fill .dtable__scroll for the same
   reason it is on the container above — without it, hiding the overflow removes
   the scrollbar and the rows jump sideways by its width every time a panel
   opens. Measured here: 0px shift with it. */
body.panel-open .dtable--fill .dtable__scroll,
body.modal-open .dtable--fill .dtable__scroll{overflow:hidden;}

/* --------------------------------------------------------------------------
   Status badge   (locked 2026-06-24)
   Small squared label that renders one of the 5 status colours as a soft-tint
   (pale fill + same-hue, AA-safe text). Text-only, non-interactive — the word
   carries the meaning, the tint reinforces it. Two sizes: default + .badge--sm
   for dense table rows. Opens the Data display group; tints come from the
   shared status-tint tokens (same as the modal/panel severity bands).
   Markup: <span class="badge badge--success">Grounded</span>
   -------------------------------------------------------------------------- */
.badge{
  display:inline-flex;align-items:center;justify-content:center;
  font-family:var(--font-sans);font-size:var(--fs-caption);font-weight:var(--fw-semibold);
  line-height:1;white-space:nowrap;
  padding:5px 8px;border-radius:var(--radius);
}
.badge--sm{font-size:var(--fs-eyebrow);padding:3px 6px;}

.badge--success{background:var(--status-success-fill);color:var(--status-success-ink);}
.badge--warning{background:var(--status-warning-fill);color:var(--status-warning-ink);}
.badge--error{background:var(--status-error-fill);color:var(--status-error-ink);}
.badge--info{background:var(--status-info-fill);color:var(--status-info-ink);}
.badge--neutral{background:var(--status-neutral-fill);color:var(--status-neutral-ink);}

/* --------------------------------------------------------------------------
   Tag / chip   (locked 2026-06-24)
   Small pill label for a category / keyword (.tag, read-only) or a clearable
   selection (.tag--removable, the same pill with an ×). Grey only — non-loud,
   rounded so it never reads as the squared status badge. Two sizes matching
   the badge. Fills are intentionally not offered yet; add when a use case
   earns one. The multi-select dropdown's chosen-item chips (.combo .chip)
   share this exact pill anatomy — kept green there because the colour signals
   "live selection in a green control".
   Markup: <span class="tag">Finance</span>
           <span class="tag tag--removable">Banking <button class="tag__x">…</button></span>
   -------------------------------------------------------------------------- */
.tag{
  display:inline-flex;align-items:center;gap:6px;
  font-family:var(--font-sans);font-size:var(--fs-caption);font-weight:var(--fw-semibold);
  line-height:1;white-space:nowrap;
  padding:5px 11px;border-radius:999px;
  background:var(--tag-fill);color:var(--tag-ink);
}
.tag--sm{font-size:var(--fs-eyebrow);padding:3px 9px;gap:4px;}

/* removable chip — a tag carrying an × that clears it */
.tag--removable{padding-right:5px;}
.tag__x{
  display:inline-flex;align-items:center;justify-content:center;
  width:16px;height:16px;padding:0;border:0;border-radius:50%;
  background:transparent;color:currentColor;cursor:pointer;opacity:.7;position:relative;
}
.tag__x:hover{opacity:1;background:rgba(0,0,0,.10);}
.tag__x svg{width:11px;height:11px;}
/* invisible 24px hit area — keeps the glyph small but clears the touch-target floor */
.tag__x::before{content:"";position:absolute;left:50%;top:50%;width:24px;height:24px;transform:translate(-50%,-50%);}

/* --------------------------------------------------------------------------
   Stat / metric   (locked 2026-06-24)
   One big number with a label — dashboard "scoreboard" tiles. Borderless,
   laid out as a horizontal strip divided by hairlines. Optional delta line:
   its colour means good vs bad change (green = welcome, red = not), NOT up vs
   down — a falling drift rate is green. The arrow shows direction; the colour
   shows whether that's welcome. No "(good)" text. Composes the H1 type size
   and the status inks.
   Markup: <div class="stat-strip">
             <div class="stat"><p class="stat__label">…</p>
               <div class="stat__value">1,284</div>
               <div class="stat__delta stat__delta--good"><svg…/>+126</div></div>
           </div>
   -------------------------------------------------------------------------- */
/* row-gap so a wrapped strip's rows don't collide — a strip wide enough to
   wrap is two rows of components, and components get space (foundations.md,
   steps 3–5 between components) */
.stat-strip{display:flex;flex-wrap:wrap;row-gap:var(--space-5);}
.stat{padding:0 28px;}
.stat:first-child{padding-left:0;}
.stat + .stat{border-left:1px solid var(--fe-border);}
.stat__label{font-size:var(--fs-caption);color:var(--fe-text-2);margin:0 0 8px;}
.stat__value{font-size:var(--fs-h1);font-weight:var(--fw-bold);line-height:1;letter-spacing:-.01em;}
.stat__value small{font-size:var(--fs-h3);font-weight:var(--fw-semibold);color:var(--fe-text-2);}
.stat__delta{display:inline-flex;align-items:center;gap:3px;font-size:var(--fs-caption);font-weight:var(--fw-semibold);margin-top:10px;}
.stat__delta--good{color:var(--status-success-ink);}   /* a welcome change — any direction */
.stat__delta--bad{color:var(--status-error-ink);}      /* an unwelcome change — any direction */
.stat__delta svg{width:13px;height:13px;}

/* --------------------------------------------------------------------------
   Key-value / record list   (locked 2026-06-24)
   A list of label → value pairs — the read-only details of one thing. Label
   left, value right, one per line, hairline between rows. This is the panel's
   Quick-facts pattern (.panel__facts) promoted to a standalone component; the
   panel keeps its own class but shares this anatomy. Values can be plain text,
   a mono date/id, a status .badge, or a .tag.
   Markup: <dl class="kv"><div class="kv__row"><dt>Owner</dt><dd>…</dd></div></dl>
           (a flat <dl> with <dt>/<dd> children also works)
   -------------------------------------------------------------------------- */
.kv{list-style:none;margin:0;padding:0;}
.kv > li,.kv__row{display:flex;justify-content:space-between;gap:var(--space-4);padding:11px 0;border-bottom:1px solid var(--fe-border);font-size:var(--fs-body-sm);}
.kv > li:last-child,.kv__row:last-child{border-bottom:0;}
.kv dt{color:var(--fe-text-2);margin:0;}
.kv dd{margin:0;font-weight:var(--fw-semibold);text-align:right;}
.kv dd.kv__mono{font-family:var(--font-mono);font-weight:var(--fw-regular);font-size:13px;}

/* ---- Code / log -------------------------------------------------------- *
   Three monospace surfaces. Inline <code> chip; a static code/config block
   with a header bar + copy (line-number gutter + copy built by code-log.js);
   and the audit/trace log — a read-only console of timestamped, level-tagged
   events whose level colours are the status inks. */

/* inline code — the <code> element */
code{font-family:var(--font-mono);font-size:.88em;background:var(--code-inline-fill);color:var(--code-inline-ink);padding:2px 6px;border-radius:var(--radius);}
.code-block code,pre code{background:none;padding:0;font-size:inherit;color:inherit;}

/* static code / config block */
.code-block{border:1px solid var(--fe-border);border-radius:6px;background:var(--code-fill);overflow:hidden;}
.code-block__bar{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:8px 12px;border-bottom:1px solid var(--fe-border);background:var(--code-bar);}
.code-block__name{font-family:var(--font-mono);font-size:12px;color:var(--fe-text-2);}
.code-block__copy{display:inline-flex;align-items:center;gap:5px;border:0;background:transparent;font-family:var(--font-sans);font-size:12px;font-weight:var(--fw-semibold);color:var(--fe-text-2);cursor:pointer;padding:2px 4px;border-radius:var(--radius);}
.code-block__copy svg{width:13px;height:13px;}
.code-block__copy:hover{color:var(--fe-text);}
.code-block__copy.is-copied{color:var(--status-success-ink);}
.code-block__body{display:flex;padding:14px 16px;overflow-x:auto;}
.code-block__gutter{flex:none;margin-right:14px;text-align:right;color:var(--code-ln);user-select:none;font-family:var(--font-mono);font-size:13px;line-height:1.65;white-space:pre;}
.code-block__code{margin:0;font-family:var(--font-mono);font-size:13px;line-height:1.65;color:var(--code-ink);white-space:pre;}
.tok-key{color:var(--code-key);}
.tok-str{color:var(--code-str);}
.tok-num{color:var(--code-num);}

/* audit / trace log — light console */
.log{list-style:none;margin:0;padding:6px 0;border:1px solid var(--fe-border);border-radius:6px;background:var(--fe-surface);overflow:hidden;font-family:var(--font-mono);font-size:12.5px;}
.log__row{display:flex;align-items:baseline;gap:14px;padding:5px 16px;line-height:1.6;}
.log__time{color:var(--fe-text-2);flex:none;}
.log__lvl{flex:none;font-weight:var(--fw-bold);width:46px;}
.log__msg{color:var(--fe-text);}
.log__lvl--ok{color:var(--status-success-ink);}
.log__lvl--warn{color:var(--status-warning-ink);}
.log__lvl--fail{color:var(--status-error-ink);}
.log__lvl--info{color:var(--status-info-ink);}

/* ---- Empty state ------------------------------------------------------- */
/* Centred placeholder a data container renders when it has no rows. Bare line
   icon (muted by default; tone modifier recolours it for error / done / info).
   Density is compositional — drop the body or the action to go lighter; no
   modifier needed. Composes existing tokens only (status inks, border, btn). */
.empty{
  --empty-ink:var(--fe-text-2);
  display:flex;flex-direction:column;align-items:center;text-align:center;
  padding:44px 24px;max-width:360px;margin:0 auto;
}
.empty__icon{margin-bottom:14px;color:var(--empty-ink);line-height:0;}
.empty__icon svg{width:30px;height:30px;}
.empty__title{font-size:16px;font-weight:var(--fw-semibold);color:var(--fe-text);margin:0 0 6px;}
.empty__body{font-size:14px;line-height:1.55;color:var(--fe-text-2);margin:0 0 20px;max-width:300px;}
.empty__actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;}
/* with no body, the actions still clear the title */
.empty__title + .empty__actions{margin-top:16px;}
/* compact buttons inside the empty state */
.empty__actions .btn{min-width:0;min-height:38px;padding:0 16px;font-size:14px;}
/* tone — neutral default; these recolour the icon to the status ink */
.empty--error{--empty-ink:var(--status-error-ink);}
.empty--success{--empty-ink:var(--status-success-ink);}
.empty--info{--empty-ink:var(--status-info-ink);}

/* ---- Table (.dtable) --------------------------------------------------- */
/* One table component, two configurations. Shared base: a left filter rail
   (multi-select dropdowns, open by default, collapse on the left), a
   title/count/search bar, a semantic <table> with sortable headers, and a
   pagination foot. Filter (facets + search) → sort → paginate runs over the
   WHOLE dataset (all rows live in the table), then a page is shown. Optional
   features by markup: row SELECTION (checkbox column), EXPANDABLE rows (a
   detail row per group — the "logs" config), and EXPORT. Columns, data,
   facets and the expanded detail are supplied per use; behaviour is in
   dtable.js. Composes the badge / checkbox / dropdown / key-value / code
   block. Recessed surfaces reuse --fe-canvas; no new token values. */
.dtable{border:1px solid var(--fe-border);border-radius:8px;background:var(--fe-surface);}
.dtable__layout{display:flex;align-items:stretch;}

/* filter rail (left, open by default) */
.dtable__filters{width:222px;flex:none;border-right:1px solid var(--fe-border);background:var(--fe-canvas);padding:14px 16px 18px;border-radius:8px 0 0 8px;}
.dtable__filters-head{display:flex;align-items:center;gap:8px;margin-bottom:16px;}
.dtable__filters-title{font-size:var(--fs-eyebrow);font-weight:var(--fw-semibold);letter-spacing:.08em;text-transform:uppercase;color:var(--fe-text-2);}
.dtable__facet{margin-bottom:14px;}
.dtable__facet:last-child{margin-bottom:0;}
.dtable__facet-label{font-size:11px;font-weight:var(--fw-semibold);letter-spacing:.07em;text-transform:uppercase;color:var(--fe-text-2);margin:0 0 6px;}
/* collapsed: hide the panel, show a narrow rail with the reopen button */
.dtable__rail{display:none;width:48px;flex:none;border-right:1px solid var(--fe-border);background:var(--fe-canvas);padding-top:15px;justify-content:center;border-radius:8px 0 0 8px;}
.dtable.is-collapsed .dtable__filters{display:none;}
.dtable.is-collapsed .dtable__rail{display:flex;}
.dtable__ib{width:30px;height:30px;flex:none;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--fe-border);background:var(--fe-surface);border-radius:var(--radius);color:var(--fe-text-2);cursor:pointer;padding:0;}
.dtable__ib:hover{color:var(--fe-text);border-color:#C8C8C4;}
.dtable__ib svg{width:16px;height:16px;}

/* main (right) */
.dtable__main{position:relative;flex:1;min-width:0;display:flex;flex-direction:column;border-radius:0 8px 8px 0;overflow:hidden;}
/* scroll wrapper around the table only: a table wider than its container scrolls
   horizontally instead of being clipped by .dtable__main's overflow:hidden. The
   bar and foot sit outside it, so they never scroll sideways. */
.dtable__scroll{overflow-x:auto;min-width:0;}
/* flex-wrap: the bar holds a heading cluster plus a tools cluster (select +
   search) that together outgrow a phone viewport — wrapping keeps the table
   inside its page instead of forcing a horizontal scroll on the whole body */
.dtable__bar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--fe-border);}
.dtable__heading{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;}
.dtable__title{font-size:15px;font-weight:var(--fw-semibold);color:var(--fe-text);}
.dtable__count{font-size:var(--fs-caption);color:var(--fe-text-2);}
/* selection toolbar — swaps in for the heading while rows are selected */
.dtable__selinfo{display:none;align-items:center;gap:12px;font-size:14px;font-weight:var(--fw-semibold);color:var(--green-text);}
.dtable.is-selecting .dtable__heading{display:none;}
.dtable.is-selecting .dtable__selinfo{display:flex;}
.dtable__sel-clear{font:inherit;font-weight:var(--fw-regular);border:0;background:none;color:var(--fe-text-2);cursor:pointer;text-decoration:underline;}
.dtable__tools{margin-left:auto;display:flex;flex-wrap:wrap;align-items:center;gap:10px;}
/* the table search IS the Search component (.search .search--icon, standard 40px);
   this only sizes it for the toolbar. See the Search section above. */
.dtable__tools .search{width:260px;max-width:46%;}

/* export (optional) — black-outline button + a small menu */
.dtable__export{position:relative;}
.dtable__export-btn{display:inline-flex;align-items:center;gap:7px;height:var(--field-height);padding:0 14px;border:1px solid var(--ink);background:var(--fe-surface);color:var(--ink);border-radius:var(--radius);font-family:var(--font-sans);font-size:14px;font-weight:var(--fw-semibold);cursor:pointer;}
.dtable__export-btn:hover{background:var(--surface-hover);}
.dtable__export-btn svg{width:15px;height:15px;}
.dtable__export-menu{position:absolute;top:calc(100% + 4px);right:0;min-width:150px;z-index:30;background:var(--surface);border:1px solid var(--fe-border);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);padding:4px;display:none;transform:translateZ(0);}
.dtable__export.is-open .dtable__export-menu{display:block;}
.dtable__export-item{display:block;width:100%;text-align:left;border:0;background:none;font-family:var(--font-sans);font-size:14px;color:var(--ink);padding:8px 10px;border-radius:var(--radius);cursor:pointer;}
.dtable__export-item:hover{background:var(--surface-hover);}

/* the table */
.dtable__table{width:100%;border-collapse:collapse;}
.dtable__th{text-align:left;font-size:10.5px;font-weight:var(--fw-semibold);letter-spacing:.06em;text-transform:uppercase;color:var(--fe-text-2);padding:10px 12px;background:var(--fe-canvas);border-bottom:1px solid var(--fe-border);white-space:nowrap;}
.dtable__th--num{text-align:right;}
.dtable__th--check{width:44px;}
.dtable__th--sortable{cursor:pointer;user-select:none;}
.dtable__th--sortable:hover{color:var(--fe-text);}
.dtable__sort{display:inline-block;width:12px;height:12px;margin-left:5px;vertical-align:-2px;opacity:.32;transition:opacity .12s,transform .15s;}
.dtable__sort svg{width:12px;height:12px;}
.dtable__th.is-asc .dtable__sort{opacity:1;transform:rotate(180deg);}
.dtable__th.is-desc .dtable__sort{opacity:1;}
/* dtable.js upgrades each sortable header's content into this button so sorting
   is keyboard-operable; it inherits the header's type and carries the focus ring */
.dtable__sortbtn{display:inline-flex;align-items:center;width:100%;margin:0;padding:0;border:0;background:none;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;text-align:inherit;cursor:pointer;}
.dtable__th--num .dtable__sortbtn{justify-content:flex-end;}
.dtable__sortbtn:focus-visible{outline:none;box-shadow:0 0 0 2px var(--green);border-radius:3px;}

/* one <tbody> per logical row (the "group") — holds the row + an optional
   detail row, so sorting/paginating keeps a detail with its parent */
.dtable__group{border-bottom:1px solid var(--fe-border);}
.dtable__group:last-child{border-bottom:0;}
.dtable__row:hover{background:#FAFAF8;}
.dtable__group.is-open .dtable__row,.dtable__row.is-selected{background:var(--radio-card-fill);}
.dtable__row--expandable{cursor:pointer;}
.dtable__td{padding:11px 12px;font-size:14px;color:var(--fe-text);vertical-align:middle;}
.dtable__td--check{width:44px;}
.dtable__td--chev{width:30px;padding-right:0;}
.dtable__chev{color:var(--fe-text-2);display:inline-flex;transition:transform .15s ease;}
.dtable__chev svg{width:15px;height:15px;}
.dtable__group.is-open .dtable__chev{transform:rotate(90deg);}
/* dtable.js upgrades the chevron <span> into a button for keyboard expand */
button.dtable__chev{margin:0;padding:0;border:0;background:none;cursor:pointer;}
button.dtable__chev:focus-visible{outline:none;box-shadow:0 0 0 2px var(--green);border-radius:3px;}

/* grouped rows   (added 2026-07-24) — a tbody flagged --head is a collapsible
   group heading inside the same table, so columns stay aligned across every
   group and a subtotal sits under the figures it summarises. The heading reads
   as structure, not as a record: recessed fill, semibold, and it keeps that
   fill when open, because the green .is-open wash means "expanded/selected
   row" and a heading is neither. dtable.js owns the collapse; every figure in
   the heading is the server's. */
.dtable__group--head .dtable__row{background:var(--fe-canvas);cursor:pointer;}
.dtable__group--head.is-open .dtable__row{background:var(--fe-canvas);}
.dtable__group--head .dtable__row:hover,.dtable__group--head.is-open .dtable__row:hover{background:var(--surface-hover);}
.dtable__group--head .dtable__td{font-weight:var(--fw-semibold);}
.dtable__group--head .dtable__count{font-weight:var(--fw-regular);}
.dtable__group--head .dtable__partialnote{font-size:var(--fs-caption);color:var(--fe-text-2);font-weight:var(--fw-regular);margin-left:var(--space-2);}
/* is-partial = a search/facet is hiding some of this group's rows: the note above
   says how many, and the figure cells dim because their arithmetic covers rows
   that are not on screen (dtable.js never recomputes a server subtotal). */
.dtable__group--head.is-partial .dtable__td--num{opacity:.55;}

.dtable__td--name{font-weight:var(--fw-semibold);}
.dtable__td--mono{font-family:var(--font-mono);font-size:12px;color:var(--fe-text-2);white-space:nowrap;}
.dtable__td--src{font-family:var(--font-mono);font-size:12.5px;color:var(--fe-text);white-space:nowrap;}
.dtable__td--num{text-align:right;font-family:var(--font-mono);font-size:13px;color:var(--fe-text);white-space:nowrap;}
.dtable__td--date{font-family:var(--font-mono);font-size:12.5px;color:var(--fe-text-2);white-space:nowrap;}

/* expandable detail (a row spanning all columns) */
.dtable__detailrow[hidden]{display:none;}
.dtable__detail{padding:18px 16px 22px 42px;background:var(--fe-canvas);border-top:1px solid var(--fe-border);display:grid;grid-template-columns:minmax(240px,300px) minmax(0,1fr);gap:28px;align-items:start;}
.dtable__detail-h{font-size:11px;font-weight:var(--fw-semibold);letter-spacing:.07em;text-transform:uppercase;color:var(--fe-text-2);margin:0 0 8px;}
.dtable__detail .kv{margin-top:2px;}

/* footer / pagination */
/* flex-wrap for the same reason .dtable__bar carries it: a page-info cluster
   plus a pager pushed to the far edge outgrow a phone viewport, and without a
   wrap they break the card rather than stacking. Inert at desktop, where it
   fits on one line. */
.dtable__foot{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:11px 16px;border-top:1px solid var(--fe-border);}
.dtable__pageinfo{font-size:var(--fs-caption);color:var(--fe-text-2);}
.dtable__pager{margin-left:auto;display:flex;gap:8px;}
.dtable__pgbtn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border:1px solid var(--fe-border);background:var(--fe-surface);color:var(--fe-text);border-radius:var(--radius);font-family:var(--font-sans);font-size:13px;font-weight:var(--fw-semibold);cursor:pointer;}
.dtable__pgbtn:hover:not(:disabled){background:var(--surface-hover);}
.dtable__pgbtn:disabled{color:var(--pager-disabled);cursor:not-allowed;}
.dtable__pgbtn svg{width:14px;height:14px;}

/* loading   (added 2026-06-25) — a server-side re-fetch (filter / sort /
   paginate round-trip) is in flight. A translucent scrim + centred spinner
   over the main column; the current rows stay faintly visible so the table
   doesn't collapse. FIRST load, when there are no rows yet, uses skeleton rows
   in the tbody instead — not this. Add .dtable__loader as the last child of
   .dtable__main, toggle .is-loading, and set aria-busy on the table.
   dtable.js owns this around the dtable:change fetch. */
.dtable__loader{position:absolute;inset:0;z-index:5;display:none;align-items:center;justify-content:center;background:rgba(250,250,248,.62);}
.dtable.is-loading .dtable__loader{display:flex;}

/* viewport fill (.dtable--fill)   (added 2026-07-27) — the ROWS scroll, not the
   page. For a table whose row count is not something pagination can bound — a
   grouped one, where expanding a heading or a lazy detail changes how many rows
   are visible — the page otherwise grows without limit and the filters, the
   search and the count scroll off the top exactly when a long table makes them
   worth having.

   THE HEIGHT COMES FROM LAYOUT, NOT FROM A MEASUREMENT. There is no JS here and
   no offset to keep in sync: .app-shell--fixed-chrome is height:100dvh and its
   .app-shell__main is flex:1 1 auto, so main's height is already definite. A
   flex column from there down to .dtable__scroll resolves the remaining space on
   its own — and re-resolves for free when anything above the table changes
   height, which a per-page calc() would not. A title that wraps to two lines
   just makes the rows region one line shorter.

   THE CHAIN IS THE CONTRACT. Every element between .app-shell__main and the
   table has to pass the fill down, because one static-height ancestor stops it
   dead. Put .dtable-fill-host on each of them (and on main itself). They are
   usually two or three wrappers — a page region, a fragment root, a tab panel.

   IT DEGRADES TO TODAY. If the chain is incomplete the table simply keeps its
   natural height and the page scrolls as it always did: nothing clips, nothing
   overlaps, no rows become unreachable. A contract that fails soft is the only
   kind worth putting across seven surfaces.

   min-height:0 everywhere is load-bearing, not defensive. A flex item's default
   min-height is auto — it refuses to shrink below its content — so without it
   every one of these boxes reports the full height of all its rows and the
   scroller never has a smaller box to scroll inside. */
.app-shell--fixed-chrome .app-shell__main.dtable-fill-host{display:flex;flex-direction:column;}
/* EVERYTHING ABOVE THE TABLE HOLDS ITS NATURAL HEIGHT. A flex item's default
   flex-shrink is 1, so the page's own chrome — a title row, a summary card, a
   tab strip — was being compressed to make room for the table, and compressed
   silently: MEASURED on /project-details, a 398px identity row rendered in a
   340px box with the card's last 58px hanging out of it, at every viewport
   height including 1440 where there was space to spare. Nothing above the table
   is allowed to give; the table is the part that flexes.

   Declared BEFORE the two rules under it so a nested host and the table itself
   take their flex-shrink:1 back — equal specificity, later wins. */
.dtable-fill-host > *{flex-shrink:0;}
/* min-width:0 alongside min-height:0, and for the same reason on the other axis:
   a flex item's automatic minimum size is its CONTENT, so a host wrapping a wide
   table refused to narrow below that table's min-content width and burst out of
   the column sideways. MEASURED at a 900px viewport: .bs-container rendered
   1228px wide inside an 885px .app-shell__main, scrolling the shell 343px
   horizontally — a table that already had its own overflow-x, pushing the page
   because the box around it would not shrink. */
/* width:100% makes the cross size DEFINITE. A column flex container stretches
   its items to its own width only while their width stays auto and nothing else
   claims it; a centred, max-width page container inside one was sized by its
   CONTENT instead — measured at a 900px viewport, 1228px wide inside an 885px
   scroller, moving the shell 343px sideways. Stating the width fixes it without
   costing the cap or the centring: max-width still bites where the container is
   wider than the cap, and the auto margins still centre what is left. */
/* box-sizing restated for the same reason .card restates it, though the
   file-wide reset at the top now covers it: width:100% on a content-box host
   with side padding produces a border box wider than its container — measured,
   933px inside 885px, 48px of shell scroll at every width. */
.dtable-fill-host{box-sizing:border-box;display:flex;flex-direction:column;min-height:0;min-width:0;width:100%;flex:1 1 auto;}
.dtable--fill{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;}
.dtable--fill .dtable__layout{flex:1 1 auto;min-height:0;}
.dtable--fill .dtable__main{min-height:0;}
/* the rows region becomes the scroller in BOTH axes. Horizontal scroll was
   always .dtable__scroll's job and still is — it now nests inside the vertical
   one rather than being replaced by it. */
/* min-height is the floor that stops "fills the remaining space" from meaning
   "gets whatever is left, including nothing" (added 2026-07-27). MEASURED on
   /project-details, whose chrome above the table — breadcrumb, a 340px identity
   row carrying the description and the summary card, and the 40px tab strip —
   comes to 570px: at a 1440-tall viewport the rows region was 667px and showed
   11 rows, at 1080 it showed 5, at 900 it showed TWO, and at 800 and below it
   showed none at all. A table that renders zero rows is worse than one that
   scrolls the page, so below the floor the page scrolls again — .app-shell__main
   is overflow-y:auto and takes it back. The pinned chrome is the thing given up,
   and it is the right thing to give up: the promise is worth nothing over an
   empty table.

   min(360px, 50dvh): about six rows on a laptop, and proportional rather than
   fixed on a short viewport, where 360px would be most of the screen. */
.dtable--fill .dtable__scroll{flex:1 1 auto;min-height:min(360px,50dvh);overflow:auto;scrollbar-gutter:stable;}
/* the bar and the foot stay OUT of the scroller — they already are, structurally,
   which is why this variant needs no markup change beyond the class. */
.dtable--fill .dtable__bar,.dtable--fill .dtable__foot{flex:none;}
/* the rail scrolls independently once it is taller than the table it sits beside,
   rather than stretching the card and taking the rows region's height with it */
.dtable--fill .dtable__filters{overflow-y:auto;}
/* column headers stay put while the rows move under them. Without this the one
   thing that says what a column MEANS is the first thing to leave the screen,
   and 400 rows down a numeric column is unreadable. .dtable__th already carries
   an opaque --fe-canvas fill, so the rows pass behind it rather than through. */
.dtable--fill .dtable__th{position:sticky;top:0;z-index:2;}

/* narrow viewports (added 2026-07-26) — STACK the rail instead of seating it
   beside the table. Side-by-side, the 222px filter panel is a fixed track taken
   off the top of whatever width there is: measured at a 390px viewport it left
   .dtable__main 24px, so a 699px table was scrolling inside a 24-pixel window.
   .dtable__scroll was doing its job; there was simply nothing left to scroll in.

   Both states stack, which is what keeps the collapse toggle honest — a media
   query that force-hid the panel would leave the "Show filters" button below
   the breakpoint with nothing to do. Open, the panel is a full-width block
   above the table; collapsed, it is a full-width strip carrying the same
   button. The rounded corners move with it: the left-edge pair the two shared
   horizontally becomes a top pair on the panel and a bottom pair on the table,
   so the outer radius still reads as one card either way. */
@media (max-width:700px){
  .dtable__layout{display:block;}
  .dtable__filters{width:auto;border-right:0;border-bottom:1px solid var(--fe-border);border-radius:8px 8px 0 0;}
  /* full-width strip, so the reopen button keeps a comfortable target and the
     table below it is not pushed into a 48px-narrower column */
  .dtable__rail{width:auto;border-right:0;border-bottom:1px solid var(--fe-border);border-radius:8px 8px 0 0;padding:10px 16px;justify-content:flex-start;}
  .dtable__main{border-radius:0 0 8px 8px;}
  /* the bar and the foot already wrap; what stops them fitting once wrapped is
     the pair of margin-left:auto clusters, which hold a full line each and then
     size to their own content. Give each the whole row and let the search fill
     it — 260px (or 46% of a cluster that is itself sized by content) is a
     desktop measurement and overflows the card here. */
  .dtable__tools{margin-left:0;width:100%;}
  .dtable__tools .search{width:100%;max-width:none;}
  .dtable__pager{margin-left:0;}

  /* .dtable--fill TURNS OFF HERE, deliberately (added 2026-07-27). The variant
     buys one thing — chrome that stays put while the rows move — and below this
     breakpoint the price stops being worth it. The rail has just stacked into a
     full-width block ABOVE the table, so the fixed chrome is now the bar plus
     that whole panel; on a 640px-tall phone that leaves a rows region barely
     taller than the header it sits under, and a scroller inside a scroller on a
     touch screen is a fight over which one the drag belongs to. The page scroll
     is the right interaction at this width, and it is the one the reader's thumb
     already expects. Everything above the table is a short scroll away rather
     than pinned, which on a phone is what "reachable" means anyway. */
  .dtable--fill,.dtable-fill-host{display:block;min-height:auto;}
  .dtable--fill .dtable__layout{min-height:auto;}
  .dtable--fill .dtable__scroll{overflow-x:auto;overflow-y:visible;scrollbar-gutter:auto;}
  .dtable--fill .dtable__filters{overflow-y:visible;}
  .dtable--fill .dtable__th{position:static;}
}

/* capped height (.dtable--capped)   (added 2026-07-27) — the TABLE stops
   growing, and the page carries on scrolling.

   THE OTHER ANSWER TO A LONG TABLE, and the one to reach for far more often.
   .dtable--fill above hands the rows every pixel the viewport has left, which
   requires the table to BE the page's content — the page stops scrolling and the
   table becomes the scroller. That premise holds on a tab panel and almost
   nowhere else. A detail page is breadcrumb, then a head, then a table, then
   more sections; there is no "remaining viewport" to give away, and taking it
   would push whatever follows below a page that can no longer scroll to reach
   it. Three surfaces hit exactly that (feature-details Jobs, job-details Tasks
   and Sub-jobs), which is what this variant is for.

   So: a ceiling instead of a claim. The rows region scrolls once it passes
   --dtable-cap and the document keeps its own scroll, which is the interaction
   a reader already has. Under the cap nothing happens at all — max-height binds
   only when the content exceeds it, so a 19-row table renders at its natural
   height with no dead space and no scrollbar.

   WHY THIS REPLACES PAGINATION ON THOSE SURFACES rather than joining it. A pager
   forces filtering and search to the server, because a client-side search over
   one page reports matches from fifty rows and says nothing about the other
   forty-six. Cap the height instead and EVERY ROW IS IN THE DOM, so the
   library's own data-dtable-search is telling the truth again and comes for
   free. Pagination earns its cost when the set is genuinely too large to ship —
   entity tables at 1,705 rows still page, and should.

   THE CAP IS A FIXED HEIGHT, NOT A SHARE OF THE VIEWPORT (changed 2026-07-27,
   owner, after the dvh version shipped and was used). A viewport share reads as
   the obvious unit and is the wrong one: it measures the WHOLE screen, while the
   table starts wherever its page leaves it — under a breadcrumb, a head and a
   heading, several hundred pixels down. 44dvh of a 900px viewport is 396px of
   table beginning 500px down a 900px screen, so the reader sees a sliver and the
   cap has no idea it has already been spent. The page above the table is exactly
   what a viewport unit cannot see.

   480px is a table's worth of table: the header row plus ten rows at the default
   44px. It is the same on every surface and every screen, which is the point —
   a reader learns one shape. --dtable-cap stays a custom property for the
   surface that genuinely differs, but reach for it rarely; a page that needs a
   different table height usually needs less above the table.

   NO ANCESTOR CONTRACT, unlike --fill. This needs nothing of its parents — no
   definite height, no flex chain, no host class. Put the class on the .dtable
   and it works, which is why it also stays ON below 700px where --fill switches
   itself off: there is no viewport arithmetic to get wrong, the nested scroller
   chains to the page at its ends the way a reader's thumb expects, and a phone
   is where an unbounded 96-row table is worst.

   Deliberately NOT here: the body.panel-open scroll lock --fill carries. That
   rule guards a case never reproduced (see the note above it), and the page
   behind this variant still scrolls anyway, so a second speculative lock would
   be guessing twice. */
.dtable--capped .dtable__scroll{
  max-height:var(--dtable-cap,480px);
  overflow:auto;
  scrollbar-gutter:stable;
}
/* Headers stay put against the rows moving under them. Same reasoning as
   --fill's: a column header that scrolls away turns row 60 into six unlabelled
   values, and this variant exists precisely to put rows under a fold. */
.dtable--capped .dtable__th{position:sticky;top:0;z-index:2;}

/* ---- Board (.board)   (locked 2026-07-27) -------------------------------
   A status board: one lane per stage of a workflow, each holding a card per
   thing sitting at that stage. Reading down a lane answers "what is in this
   state"; reading across answers "where is the work piling up". The other data
   surface for the same rows is the [table](data-table.md) — reach for the
   table when the question is about the rows themselves (sort, compare, export)
   and the board when the question is about their distribution across stages.

   READ-ONLY BY DECISION. Cards do not drag between lanes. The board is a
   window onto a status field, not the control that sets it, so nothing here
   draws a grab handle, a lift shadow, or any other affordance that would
   promise a drag this component does not implement. A board that looks
   draggable and isn't is worse than one that plainly isn't.

   THE COUNT IN THE HEAD IS THE TRUE TOTAL, NOT THE NUMBER OF CARDS BELOW IT.
   A real lane runs to thousands of cards and rendering them all is the payload
   mistake this component is shaped to avoid: the head states how many there
   ARE, the body holds a bounded first page, and .board__more fetches the next.
   A head reading 2,684 over 50 cards is the CORRECT state — never "fix" the
   count by counting the DOM, and never drop the count because it disagrees
   with what is on screen. It is the only place the full size is stated.

   SIZING IS THE HOST'S JOB and the host must have a DEFINITE HEIGHT. The lanes
   scroll internally, which cannot happen inside a box that grows with its
   content — give the board an auto-height host and every lane grows to fit its
   cards, the internal scrollbars never appear, and the whole board scrolls as
   one long page instead. .board-region is that host: a full-height flex column
   carrying the page padding, so a toolbar (filters, search) sits above the
   board and the board takes the rest. On a signed-in surface it goes inside
   .app-shell--fixed-chrome's .app-shell__main, which is already exactly the
   viewport. min-height:0 on the region, the board and the lane is load-bearing
   in the same way the app shell's is NOT: there the scroll container is the
   flex item itself, so its automatic minimum doesn't apply — here the scroller
   is .board__body, a CHILD of the flex item, which is precisely the case that
   still bites (see the app-shell Don't).

   LANES CARRY NO STATUS COLOUR. Five tinted heads would spend the status
   palette on labels that are already words, and five severities is not what
   five stages mean — "Blocked" is a stage a thing sits in, not an error the
   reader must act on. The lane is neutral; the column title is the signal.
   A card may still carry a status badge for something that IS a severity.

   Markup:
     <div class="board-region">
       <div class="board__toolbar">…project filter, search…</div>
       <div class="board">
         <section class="board__col" aria-labelledby="col-ready">
           <header class="board__head">
             <h3 class="board__title" id="col-ready">Ready</h3>
             <span class="board__count">2,684</span>
           </header>
           <div class="board__body">
             <a class="board__card" href="/job-details.html?job=JOB-AUS1-0001lh">
               <span class="board__name">Vendor the brand chrome into the v2 pages</span>
               <span class="tag tag--sm">Core Platform</span>
               <div class="board__metrics">
                 <span class="board__frac">3/7 tasks</span>
                 <div class="progress" role="progressbar" aria-valuenow="43" aria-valuemin="0" aria-valuemax="100">
                   <i class="progress__fill" style="width:43%"></i>
                 </div>
               </div>
               <span class="badge badge--sm badge--neutral">3 sub-jobs</span>
               <span class="board__id">JOB-AUS1-0001lh</span>
             </a>
             …
             <button class="btn btn--secondary board__more">Show more</button>
           </div>
         </section>
       </div>
     </div>

   The card is an <a>, so the whole card is ONE keyboard stop and one hit area.
   Nothing inside it is separately focusable — a link inside a link is invalid
   and a button inside one swallows the card's own activation. Interactivity is
   scoped to .board__card:is(a,button) so the same class also dresses a plain
   <article>: that is the loading skeleton (sk__line shapes inside a card-shaped
   box, aria-hidden) and it must not offer a pointer or a focus ring.

   An empty lane holds the [empty state](empty-state.md) at minimal density,
   tightened here — its 44px/24px padding is a page-width measurement and looks
   abandoned in a 288px lane. */
.board-region{display:flex;flex-direction:column;gap:var(--space-4);height:100%;min-height:0;box-sizing:border-box;padding:var(--space-5);}
.board__toolbar{flex:none;display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;}
.board{flex:1 1 auto;min-height:0;display:flex;gap:var(--space-4);overflow-x:auto;overflow-y:hidden;}

/* the lane. flex:1 1 var(--board-col-w) with the same value as min-width means
   "share the spare room, but never go under the floor" — five lanes fill a wide
   screen and the board scrolls sideways on a narrow one, with no breakpoint. */
.board__col{flex:1 1 var(--board-col-w);min-width:var(--board-col-w);min-height:0;
  display:flex;flex-direction:column;
  background:var(--board-lane);border:1px solid var(--fe-border);border-radius:var(--card-radius);}
.board__head{flex:none;display:flex;align-items:baseline;gap:var(--space-2);
  padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--fe-border);}
.board__title{margin:0;font:var(--fw-semibold) var(--fs-body-sm)/1.3 var(--font-sans);color:var(--ink);}
.board__count{margin-left:auto;font-size:var(--fs-caption);color:var(--fe-text-2);font-variant-numeric:tabular-nums;}
/* the scroller. The head stays put because it is a SIBLING of this box, not a
   sticky child of it — same move the fixed-chrome shell makes with the topbar. */
.board__body{flex:1 1 auto;min-height:0;overflow-y:auto;
  display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-3);}
/* centred in the lane, and tight enough that an EMPTY lane does not scroll:
   at the stock 44px/24px the block stands 3px taller than a 340px lane's body
   and hangs a scrollbar on a column with nothing in it. */
.board__body > .empty{margin:auto;padding:var(--space-4) var(--space-3);}

/* the card. align-items:flex-start lets the tag and badge hug their own text
   (a stretched pill reads as a bar); the two children that must span the card
   say so themselves. */
.board__card{display:flex;flex-direction:column;align-items:flex-start;gap:var(--space-2);
  padding:var(--space-3);background:var(--fe-surface);
  border:1px solid var(--fe-border);border-radius:var(--radius);color:var(--ink);text-decoration:none;}
.board__card:is(a,button){cursor:pointer;width:100%;box-sizing:border-box;text-align:left;font:inherit;
  transition:border-color .12s ease;}
.board__card:is(a,button):hover{border-color:var(--green-deep);}
.board__card:is(a,button):focus-visible{outline:2px solid var(--green);outline-offset:2px;}
/* the clamp is baked in rather than left to .t-clamp-2 on the caller: two lines
   is the card's shape, and a lane of cards whose height depends on remembering
   a utility class is a lane that loses its rhythm on the first long name. */
.board__name{width:100%;font-weight:var(--fw-semibold);font-size:var(--fs-body-sm);line-height:1.4;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;}
.board__metrics{width:100%;display:flex;flex-direction:column;gap:6px;}
.board__frac{font-size:var(--fs-caption);color:var(--fe-text-2);font-variant-numeric:tabular-nums;}
.board__id{font-family:var(--font-mono);font-size:12px;color:var(--fe-text-2);}
/* the pager sits with the cards, not pinned: it is the end of the loaded run,
   and pinning it would claim the lane has an end it hasn't reached. */
.board__more{width:100%;min-width:0;}

/* ==========================================================================
   FEEDBACK & STATUS
   Severity is one shared spec — success / warning / error / info — applied
   across the whole group so a banner, a toast, a progress state all speak the
   same colour language. The tints are the locked --status-*-fill / -ink set
   (defined for the badge); this section adds no new colour tokens.
   ========================================================================== */

/* ---- Inline notification / banner   (locked 2026-06-25) ------------------
   A page- or section-level message keyed to a status colour. Direction B:
   a 4px left accent bar in the solid status hue over the soft-tint fill.
   One block, four severities; anatomy is composable — icon + title and/or
   body + optional actions + optional dismiss. Actions reuse the real .link /
   .btn (no banner-specific button). Dismiss is wired in banner.js. The
   severity wiring below is what a future toast reuses.

   Markup:
     <div class="banner banner--warning" role="alert">
       <span class="banner__icon"><svg…></span>
       <div class="banner__body">
         <p class="banner__title">3 sources need review.</p>
         <p class="banner__text">They grounded with low confidence — check before handoff.</p>
         <div class="banner__actions"><a class="link" href="#">Review now</a></div>
       </div>
       <button class="banner__dismiss" aria-label="Dismiss"><svg…></button>
     </div>
   role: "status" for info/success (polite), "alert" for warning/error (assertive).
*/
.banner{
  position:relative;overflow:hidden;
  display:flex;align-items:flex-start;gap:12px;
  padding:14px 16px 14px 18px;
  border-radius:var(--radius);
  background:var(--_bn-fill);
  font-size:var(--fs-body-sm);line-height:var(--lh-body-sm);color:var(--ink);
}
.banner::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--_bn-edge);}
.banner__icon{flex:0 0 auto;width:var(--icon-md);height:var(--icon-md);color:var(--_bn-ink);margin-top:1px;}
.banner__icon svg{width:100%;height:100%;display:block;}
.banner__body{flex:1 1 auto;min-width:0;}
.banner__title{margin:0;font-weight:var(--fw-semibold);color:var(--_bn-ink);}
.banner__text{margin:0;color:var(--ink);}
.banner__title + .banner__text{margin-top:2px;}
.banner__actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;margin-top:10px;}
.banner__dismiss{flex:0 0 auto;width:24px;height:24px;margin:-2px -4px 0 0;padding:0;
  border:0;background:transparent;color:var(--fe-text-2);border-radius:var(--radius);cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;}
.banner__dismiss svg{width:16px;height:16px;}
.banner__dismiss:hover{color:var(--ink);background:#00000010;}
.banner__dismiss:focus-visible{outline:none;box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--ink);}

/* severity → tint wiring (the shared severity spec; reused by toast) */
.banner--success{--_bn-fill:var(--status-success-fill);--_bn-ink:var(--status-success-ink);--_bn-edge:var(--status-success);}
.banner--warning{--_bn-fill:var(--status-warning-fill);--_bn-ink:var(--status-warning-ink);--_bn-edge:var(--status-warning);}
.banner--error  {--_bn-fill:var(--status-error-fill);  --_bn-ink:var(--status-error-ink);  --_bn-edge:var(--status-error);}
.banner--info   {--_bn-fill:var(--status-info-fill);   --_bn-ink:var(--status-info-ink);   --_bn-edge:var(--status-info);}

/* single-line — centre the icon/dismiss against one line of text (no title) */
.banner--inline{align-items:center;}
.banner--inline .banner__icon{margin-top:0;}
.banner--inline .banner__dismiss{margin-top:0;}

/* ---- Snackbar / toast   (locked 2026-06-25) -----------------------------
   A toast IS a .banner — same severity wiring, anatomy and a11y — lifted out
   of the page flow into a fixed bottom-left region, given a shadow + slide-in,
   and auto-dismissing. A 3px countdown bar (.toast__timer) in the solid status
   hue (same as the left accent bar) depletes left→right to show time remaining;
   hover pauses it. Spawned by toast.js (BrandToast.show); the × reuses banner.js.
   Markup is produced by toast.js:
     <div class="banner toast banner--success" role="status"> … .toast__timer </div>
*/
.toast-region{
  position:fixed;left:20px;bottom:20px;z-index:1000;
  display:flex;flex-direction:column;gap:12px;
  width:min(380px,calc(100vw - 40px));
  pointer-events:none;                /* region ignores clicks; toasts re-enable */
}
.toast{
  pointer-events:auto;
  box-shadow:0 6px 24px rgba(33,33,33,.14),0 1px 3px rgba(33,33,33,.10);
  animation:toast-in .22s ease both;
}
@keyframes toast-in{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
.toast__timer{
  position:absolute;left:0;right:0;bottom:0;height:3px;
  background:var(--_bn-edge);transform-origin:left;
  animation:toast-timer linear both;  /* duration set inline per toast */
}
.toast:hover .toast__timer{animation-play-state:paused;}   /* pause countdown while read */
@keyframes toast-timer{from{transform:scaleX(1);}to{transform:scaleX(0);}}

@media (prefers-reduced-motion:reduce){
  .toast{animation:none;}
  .toast__timer{animation:none;transform:scaleX(1);}   /* JS still times out; no shrink */
}

/* ---- Spinner   (locked 2026-06-25) --------------------------------------
   The shared loading *mark* — indeterminate ("working, no shape yet"). One
   animation, three sizes; every component's loading state reuses this rather
   than rolling its own, so they never drift. A2 "comet" ring: a conic arc
   masked to a ring, tapering to a tail. Pure CSS, no JS. Placement (in a
   button, beside a label, centred over a region) is the consuming component's
   business — see the usage page. The companion idiom is the skeleton loader.
     <span class="spinner spinner--md" role="status" aria-label="Loading"></span>   */
.spinner{
  --_sp-size:24px; --_sp-bw:2.5px;          /* set by the size modifiers below */
  display:inline-block;width:var(--_sp-size);height:var(--_sp-size);border-radius:50%;
  background:conic-gradient(from 90deg, transparent 0%, var(--green) 360deg);
  -webkit-mask:radial-gradient(farthest-side, transparent calc(100% - var(--_sp-bw)), #000 0);
          mask:radial-gradient(farthest-side, transparent calc(100% - var(--_sp-bw)), #000 0);
  animation:spin .8s linear infinite;
}
@keyframes spin{to{rotate:360deg;}}   /* rotate (not transform) so a host's positioning transform — button/field centring — composes instead of being overwritten */
.spinner--sm{--_sp-size:16px;--_sp-bw:2px;}    /* inline / in a button   */
.spinner--md{--_sp-size:24px;--_sp-bw:2.5px;}  /* beside a label         */
.spinner--lg{--_sp-size:40px;--_sp-bw:3.5px;}  /* centred in a region    */
/* on a coloured/dark surface (e.g. inside a primary button) */
.spinner--on{background:conic-gradient(from 90deg, transparent 0%, #fff 360deg);}
@media (prefers-reduced-motion:reduce){
  .spinner{animation-duration:1.6s;}          /* keep a slow turn; never freeze a "still working" mark */
}

/* ---- Progress bar   (locked 2026-06-25) ---------------------------------
   The determinate counterpart — we know the %. Track + green fill; the
   labelled variant adds a label + % header above it (the canonical form for a
   visible task). The fill width carries the value; set it inline and mirror it
   on aria-valuenow for screen readers. Generalises the per-row bar that
   file-upload already ships.
     <div class="progress-field">
       <div class="progress__head"><b>Exporting</b><span class="progress__pct">62%</span></div>
       <div class="progress" role="progressbar" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100">
         <i class="progress__fill" style="width:62%"></i>
       </div>
     </div>   */
.progress{height:6px;border-radius:3px;background:var(--up-track);overflow:hidden;}
.progress__fill{display:block;height:100%;width:0;background:var(--green);border-radius:3px;transition:width .3s ease;}
.progress__head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:7px;font-size:var(--fs-body-sm);color:var(--ink);}
.progress__head b{font-weight:var(--fw-semibold);}
.progress__pct{color:var(--fe-text-2);font-variant-numeric:tabular-nums;}

/* ---- Skeleton loader   (locked 2026-06-25) ------------------------------
   The other loading idiom — placeholders shaped like the content that's
   coming, so the page doesn't reflow when it arrives. Use when you know the
   *layout*; use .spinner when you don't. Compose the four shape primitives to
   mirror a real layout's footprint. Shimmer: a sheen sweeps across the resting
   grey. Mark the region aria-busy and hide the shapes (they carry no meaning).
     <div class="sk" aria-busy="true">
       <span class="sk__circle" style="width:40px;height:40px"></span>
       <span class="sk__line" style="width:60%"></span>
     </div>
   Each shape is sized by the caller (width/height inline); shapes inherit the
   shimmer from the .sk wrapper. */
.sk__line,.sk__circle,.sk__rect{
  display:block;background:var(--skeleton-base);border-radius:5px;
  background-image:linear-gradient(100deg, var(--skeleton-base) 30%, var(--skeleton-sheen) 50%, var(--skeleton-base) 70%);
  background-size:220% 100%;animation:sk-sweep 1.4s linear infinite;
}
@keyframes sk-sweep{from{background-position:200% 0;}to{background-position:-200% 0;}}
.sk__line{height:11px;}
.sk__line--sm{height:9px;}
.sk__circle{border-radius:50%;flex:none;}
.sk__rect{border-radius:7px;}
@media (prefers-reduced-motion:reduce){
  .sk__line,.sk__circle,.sk__rect{background-image:none;animation:none;}   /* fall back to flat grey */
}

/* ==========================================================================
   TABS   (locked 2026-06-25, Navigation group) ---------------------------
   Underline tabs that switch between sibling views. ONE component: each .tab
   is a label with an OPTIONAL leading icon and an OPTIONAL trailing count chip
   — turn either off (Day/Week/Month) or on (Activity 8) per use. Active = green
   text + 2px green underline; inactive muted, ink on hover. When the strip runs
   out of room, trailing tabs collapse into a "More ▾" menu that REUSES the
   dropdown popup (.menu / .menu__row) — only the position differs; if the active
   tab is hidden there, More shows the active state. Keyboard (roving ←/→, Home/
   End, automatic activation) + overflow measurement live in tabs.js.
   Markup: .tabs[role=tablist] > button.tab[role=tab][aria-selected]
     optional overflow:  + .tab-more[data-tab-more][hidden]
       > (button.tab.tab-more__btn) + (.menu.tab-menu[role=menu] > .menu__list)
   ========================================================================== */
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--fe-border);align-items:stretch;}
/* A strip WITHOUT the More menu SCROLLS rather than pushing the page
   (added 2026-07-27). The More menu is opt-in — tabs.md: "only include it when
   you want overflow handling" — and tabs.js only measures a strip that has one.
   But the un-opted-in state was not "no overflow handling", it was "breaks the
   page": .tabs is a flex row whose buttons will not shrink below their content,
   so a strip too wide for its container spilled, and visible overflow counts
   toward the viewport's scrollable area. MEASURED on this library's own page at
   a 375px viewport: a four-tab strip reached 612px and dragged the whole
   document 237px sideways — every section on the page scrollable off-axis
   because of one component that fit fine at desktop.

   Scrolling, not clipping: every tab stays reachable, which .tabs cannot promise
   any other way without the menu. And scoped with :not(:has(.tab-more)) rather
   than applied to every strip, because on a strip that HAS the menu tabs.js has
   already parked whatever did not fit — there is nothing to scroll — and making
   it a scroll container there would clip the More popup, which is positioned
   inside the strip. */
.tabs:not(:has(.tab-more)){overflow-x:auto;}
.tab{
  appearance:none;background:none;border:0;cursor:pointer;font-family:var(--font-sans);
  font-size:15px;font-weight:var(--fw-semibold);color:var(--fe-text-2);
  padding:10px 14px 11px;display:inline-flex;align-items:center;gap:7px;line-height:1.2;
  border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap;transition:color .12s;
}
.tab:hover{color:var(--ink);}
.tab[aria-selected="true"]{color:var(--green-text);border-bottom-color:var(--green-deep);}
.tab:focus-visible{outline:none;box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--ink);border-radius:3px;}
.tab .icon{width:16px;height:16px;flex:none;}

/* optional trailing count chip — reuses the tag tokens; green on the active tab */
.tab__count{
  font-size:11px;font-weight:var(--fw-semibold);line-height:1;padding:3px 6px;
  border-radius:10px;background:var(--tag-fill);color:var(--tag-ink);
}
.tab[aria-selected="true"] .tab__count{background:var(--green-light);color:var(--green-text);}

/* overflow — the "More ▾" trigger is itself a .tab; its menu reuses the dropdown
   popup (.menu / .menu__row), repositioned to hug the trigger and size to content. */
.tab-more{position:relative;display:flex;margin-left:auto;}
.tab-more[hidden]{display:none;}                       /* beats .tab-more's display:flex */
.tab-more__btn .tab__chev{transition:transform .12s;}
.tab-more__btn[aria-expanded="true"] .tab__chev{transform:rotate(180deg);}
.tab-more .menu{left:auto;right:0;width:auto;min-width:184px;top:calc(100% + 6px);}
.tab-more .menu.is-open{display:block;}
/* the rows are <button>s (focusable) — neutralise native button chrome so the
   shared .menu__row look applies: full width, no border/fill, left-aligned. */
.tab-more .menu__row{appearance:none;width:100%;border:0;background:transparent;font-family:var(--font-sans);text-align:left;}
.tab-more .menu__row .icon{width:16px;height:16px;flex:none;color:var(--fe-text-2);}
.tab-more .menu__row.is-selected .icon{color:var(--green-deep);}

/* ==========================================================================
   PAGINATION   (locked 2026-06-25, Navigation group) ---------------------
   Universal page navigator, generalised from the table footer pager. A centred
   run of numbered pages (1 2 3 …) flanked by four edge controls — « first and
   ‹ previous on the left, › next and » last on the right. The current page is
   the green button (matches the primary action fill); first/prev disable on
   page 1, next/last on the last page. Long ranges collapse to a windowed set
   with … gaps (1 … 4 5 [6] 7 8 … 20). pagination.js builds the buttons, clamps
   the window, restores keyboard focus, and emits page:change; add
   .pagination--compact to drop the numbers for tight footers (arrows + a
   "Page X of Y" readout). Every control reuses the table pager's chrome.
   Markup: nav.pagination[data-pagination][data-total][data-page] (built empty)
   ========================================================================== */
.pagination{display:flex;align-items:center;justify-content:center;gap:6px;flex-wrap:wrap;}
/* wraps for the same reason .pagination does (added 2026-07-27): the outer row
   already wrapped, but the number run inside it was one unbreakable line, so a
   deep pager did not fit a phone and pushed the page instead. Measured at 375px
   on a 24-page example: 15px past the edge. */
.pagination__list{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:6px;}        /* the centred number run */

/* every control shares the table pager's chrome: 32px, hairline, soft surface */
.pagination__btn{
  display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;
  min-width:32px;height:32px;padding:0 9px;
  border:1px solid var(--fe-border);background:var(--fe-surface);color:var(--fe-text);
  border-radius:var(--radius);font-family:var(--font-sans);font-size:13px;
  font-weight:var(--fw-semibold);cursor:pointer;transition:background .12s,border-color .12s,color .12s;
}
.pagination__btn:hover:not(:disabled):not([aria-current="page"]){background:var(--surface-hover);}
.pagination__btn:focus-visible{outline:none;box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--ink);}
.pagination__btn:disabled{color:var(--pager-disabled);cursor:not-allowed;}
.pagination__btn svg{width:14px;height:14px;}

.pagination__btn--edge{padding:0;}                                  /* icon-only, square */

/* current page — the green button (matches .btn--primary) */
.pagination__btn[aria-current="page"]{
  background:var(--green-deep);border-color:var(--green-deep);color:#fff;cursor:default;
}

/* the … gap between windows — not a button */
.pagination__gap{
  display:inline-flex;align-items:flex-end;justify-content:center;
  min-width:24px;height:32px;color:var(--fe-text-2);font-size:13px;line-height:1;
  padding-bottom:6px;user-select:none;
}

/* compact — hide the numbered run, show just the four arrows + a readout */
.pagination__info{font-size:var(--fs-caption);color:var(--fe-text-2);padding:0 6px;white-space:nowrap;}
.pagination--compact .pagination__list{display:none;}
.pagination:not(.pagination--compact) .pagination__info{display:none;}

/* ==========================================================================
   BREADCRUMB   (locked 2026-06-25, Navigation group) ---------------------
   The wayfinding trail to the current page: Home › Accounts › Acme › Contacts.
   A quiet, secondary nav — crumb links sit muted grey (--fe-text-2) and only
   light to green + underline on hover/focus, so the trail orients without
   competing with the page. The last crumb is the current page: ink, semibold,
   not a link (aria-current="page"). Separators are a masked chevron-right drawn
   by CSS (no DOM nodes, hidden from screen readers). Semantic <ol>/<li>.
   Long trails optionally collapse the middle into an … menu (breadcrumb.js,
   reusing the dropdown popup) — set data-collapse="N" to cap the visible crumbs.
   No new tokens. CSS-only core; breadcrumb.js is progressive enhancement.
   Markup: nav.breadcrumb > ol.breadcrumb__list > li.breadcrumb__item
   ========================================================================== */
.breadcrumb__list{
  display:flex;align-items:center;flex-wrap:wrap;
  list-style:none;margin:0;padding:0;
  font-size:var(--fs-caption);line-height:1.4;
}
.breadcrumb__item{display:inline-flex;align-items:center;}

/* chevron separator before every item except the first — masked SVG so it
   inherits the muted colour and stays out of the accessibility tree */
.breadcrumb__item + .breadcrumb__item::before{
  content:"";flex:none;width:14px;height:14px;margin:0 2px;
  background:var(--fe-text-2);
  -webkit-mask:url("icons/chevron-right.svg") center/14px no-repeat;
          mask:url("icons/chevron-right.svg") center/14px no-repeat;
}

/* crumb link — quiet grey at rest, brand green + underline on hover/focus */
.breadcrumb__link{
  color:var(--fe-text-2);text-decoration:none;white-space:nowrap;
  padding:1px 3px;border-radius:2px;
}
.breadcrumb__link:hover{color:var(--green-deep);text-decoration:underline;text-underline-offset:2px;}
.breadcrumb__link:focus-visible{outline:none;box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--ink);}

/* optional leading icon on a crumb (e.g. a root) — sized to the text */
.breadcrumb__link .icon,.breadcrumb__current .icon{
  width:14px;height:14px;flex:none;vertical-align:-2px;margin-right:4px;
}

/* current page — ink, semibold, not interactive */
.breadcrumb__current{
  color:var(--fe-text);font-weight:var(--fw-semibold);white-space:nowrap;padding:1px 3px;
}

/* ---- optional overflow: … button opening the dropdown menu of hidden crumbs */
.breadcrumb__overflow{position:relative;}
.breadcrumb__more{
  appearance:none;border:0;background:transparent;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;
  min-width:24px;height:24px;padding:0 4px;border-radius:3px;
  color:var(--fe-text-2);font-family:var(--font-sans);font-size:15px;line-height:1;
}
.breadcrumb__more:hover{color:var(--green-deep);background:var(--surface-hover);}
.breadcrumb__more:focus-visible{outline:none;box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--ink);}
.breadcrumb__menu{left:0;width:auto;min-width:160px;top:calc(100% + 6px);}
.breadcrumb__menu.is-open{display:block;}
/* the menu reuses .menu__row (authored for divs) on an <a> — reset link chrome */
.breadcrumb__menu .menu__row{
  appearance:none;width:100%;border:0;background:transparent;
  font-family:var(--font-sans);text-align:left;text-decoration:none;color:var(--fe-text);
}
.breadcrumb__menu .menu__row:hover,.breadcrumb__menu .menu__row.is-active{background:var(--surface-hover);}

/* ==========================================================================
   STEPPER   (locked 2026-06-30, Navigation group) ------------------------
   The progress rail for a multi-step flow (a wizard, a guided setup). Numbered
   markers down a vertical list (default) or across a horizontal row; each step
   is upcoming (muted), .is-active (green marker + ink label) or .is-complete
   (green marker, the number swapped for a check). A step may be a <button>
   (clickable, to jump) or a plain <li>. The host owns the state classes and any
   navigation — CSS-only, no script.
   ========================================================================== */

.stepper{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--space-2);}
.stepper__step{display:flex;gap:var(--space-3);align-items:flex-start;padding:var(--space-3);border-radius:var(--radius);background:none;border:0;width:100%;text-align:left;font:inherit;color:inherit;}
button.stepper__step{cursor:pointer;}
button.stepper__step:hover{background:var(--row-hover);}
button.stepper__step:focus-visible{outline:none;box-shadow:0 0 0 2px var(--green);}
.stepper__marker{position:relative;flex:none;width:var(--step-marker);height:var(--step-marker);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:var(--fs-body-sm);font-weight:var(--fw-semibold);border:1.5px solid var(--radio-ring);color:var(--fe-text-2);background:var(--surface);}
.stepper__label{display:flex;flex-direction:column;gap:2px;padding-top:3px;}
.stepper__title{font-size:var(--fs-body-sm);font-weight:var(--fw-semibold);color:var(--fe-text-2);}
.stepper__sub{font-size:var(--fs-caption);line-height:1.35;color:var(--fe-text-2);}

.stepper__step.is-active .stepper__marker{background:var(--green);border-color:var(--green);color:#fff;}
.stepper__step.is-active .stepper__title{color:var(--ink);}
.stepper__step.is-complete .stepper__marker{background:var(--green);border-color:var(--green);color:transparent;}
.stepper__step.is-complete .stepper__marker::after{content:"";position:absolute;inset:0;background:#fff;-webkit-mask:var(--check-glyph) center/15px no-repeat;mask:var(--check-glyph) center/15px no-repeat;}
.stepper__step.is-complete .stepper__title{color:var(--ink);}

/* horizontal — markers and labels centred in a row (no connector line) */
.stepper--horizontal{flex-direction:row;gap:var(--space-2);}
/* min-width:0 (added 2026-07-27): flex:1 grows a step but does not let it shrink
   past its own label, so on a narrow phone the row ran past the viewport instead
   of the labels wrapping. Chosen over flex-wrap on the row — both cleared the
   overflow by the same amount, and a stepper that keeps its steps on one line
   still reads as an ordered sequence. */
.stepper--horizontal .stepper__step{flex:1;min-width:0;flex-direction:column;align-items:center;text-align:center;gap:var(--space-2);}
.stepper--horizontal .stepper__label{align-items:center;padding-top:0;}

/* ==========================================================================
   LAYOUT — APP SHELL   (locked 2026-06-26, Navigation group) --------------
   The page wrapper that makes the header and footer bookend the viewport: a
   full-height flex column where the content grows and pushes the footer down,
   so on a short page the footer sits flush to the viewport bottom and on a
   long one it's pushed below the fold. The shell owns the full height; the
   .topbar and .footer stay natural-height flex children and need ZERO changes
   — "where the footer sits" is a page-layout concern, not a footer one. One
   per page (mirrors header/footer: one each, bookending the shell). 100dvh —
   the dynamic viewport unit — avoids the mobile address-bar jump that 100vh
   leaves as a gap under the footer. Padding stays on the content: .app-shell__main
   is a bare layout primitive; pages add their own section padding inside it.
   CSS-only, no new tokens.
   Markup: body.app-shell > header.topbar + main.app-shell__main + footer.footer
   margin:0 neutralises the UA default body margin so the topbar/footer sit flush
   to the viewport edges (the shell is applied to <body>); without it an 8px
   gutter frames the whole shell.
   ========================================================================== */
.app-shell{margin:0;min-height:100dvh;display:flex;flex-direction:column;background:var(--fe-canvas);}
.app-shell__main{flex:1 0 auto;}   /* content grows; header/footer stay natural height */

/* --- fixed chrome (added 2026-07-26, brain-stem.io signed-in surfaces) -----
   A MODIFIER, not a replacement. The default shell above is still correct and
   still the one to reach for: the document scrolls, and the chrome scrolls
   with it. This variant is for a signed-in APP surface, where the reader works
   a long table for minutes at a time and the chrome — the header's nav and the
   status bar's live state — has to stay reachable without scrolling back for it.

   The shell stops being AT LEAST the viewport (min-height) and becomes EXACTLY
   the viewport (height), so the document has nothing left to scroll. The
   content region takes the scrolling instead.

   NO min-height:0 HERE, AND THAT IS DELIBERATE. A flex item's automatic
   minimum size is its CONTENT size, not zero, which is why this pattern is
   usually written with min-height:0 — but the exception is that the automatic
   minimum does not apply to a SCROLL CONTAINER, and overflow-y:auto already
   makes this one. Measured: removing min-height:0 changes nothing (Chromium
   142, container still scrolls its full 1682px). It would be a no-op shipped
   as a safeguard, and the next reader to measure it would delete it. It DOES
   bite the moment the overflow moves to a child — see the Don't in
   app-shell.md, which is where that warning is actually useful.

   The container must be FOCUSABLE, and that is markup, not CSS: tabindex="0"
   on .app-shell__main. With the document no longer scrolling, Space and
   PageDown act on the nearest scrollable ancestor of the FOCUSED element, and
   focus starts on <body>. Measured on a real page: focus on <body> -> PageDown
   moves 0px; container focused -> 751px; a link inside it -> 751px. So it
   self-heals the moment the reader Tabs in, and the dead zone is precisely the
   first thing a keyboard reader does. See app-shell.md for why tabindex="0"
   rather than tabindex="-1" plus a focus-on-load script.

   DO NOT ADD .topbar--sticky. The header is a flex SIBLING of the scroll
   container here, so it never scrolls in the first place — measured at top:0,
   visible, with the container scrolled to its end and the class absent. Sticky
   is the answer to a scrolling document, which this no longer is.

   COMPOSES WITH .app-shell--statusbar, and deliberately adds no bottom offset
   of its own — that would double-count. The statusbar modifier's padding-bottom
   now sits INSIDE the scroll container, so it is scrolled-to rather than
   reserved, and the last row still clears the fixed bar at the scroll end.

   The focus ring is inset so it draws inside the scroll container rather than
   under the header or clipped by the overflow. */
.app-shell--fixed-chrome{height:100dvh;}
.app-shell--fixed-chrome .app-shell__main{flex:1 1 auto;overflow-y:auto;scrollbar-gutter:stable;}
.app-shell--fixed-chrome .app-shell__main:focus-visible{outline:2px solid var(--green);outline-offset:-2px;}

/* THE CHROME MUST NOT SHRINK. .app-shell__main is the only child meant to flex,
   but the bar keeps the default flex-shrink:1 — so the moment the burger
   expands it into three wrapped rows (below 1024px) the column outgrows 100dvh
   and the bar is squeezed straight back onto its min-height:68px floor. The
   rows do not disappear with it: overflow is visible, so they still PAINT,
   outside the bar's box and across .app-shell__main — and UNDERNEATH it, since
   .dtable__main is position:relative and .topbar is static, and a positioned
   element wins over a static one whatever the DOM order.

   The failure is silent and total: the collapsed panel looks right in a
   screenshot and rejects every click. Measured at 900px on a live table — bar
   69px tall with its own rows laid out down to y=284, and all three controls
   in it (nav link, view switch, account button) hit-testing to the content
   beneath. With flex:none: bar 301px, main 599px, all three reachable.

   This restores what app-shell.md already claims — that the chrome is a
   natural-height flex child and needs no changes of its own. Desktop is
   untouched: above the breakpoint the bar never wraps, so there is nothing to
   shrink (measured identical at 1280px, bar 69px). */
.app-shell--fixed-chrome .topbar{flex:none;}

/* ==========================================================================
   HEADER   (locked 2026-06-26, Navigation group) ------------------------
   The site / app top bar: brand mark · primary nav · [gap] · search · auth.
   White surface with a bottom hairline, sitting on the warm canvas. Reuses
   the existing search, button and icon-button untouched — the only new thing
   is the bar layout. The right cluster shares ONE height: inside the bar a
   .btn drops to the field height (40px) so the search, its Search button and
   the auth button all line up — the same trick .search__btn already uses;
   .btn elsewhere keeps its standalone 44px. The auth slot swaps Sign in for an
   equal-size Account button once signed in, so nothing shifts on sign-in.
   Primary nav links are NOT underlined (that idiom belongs to body links) —
   they sit muted and go ink on hover / active. No new tokens. CSS-only.
   Markup: header.topbar > a.topbar__logo + nav.topbar__nav
                         + div.topbar__spacer + div.topbar__right
   ========================================================================== */
.topbar{
  display:flex;align-items:center;gap:var(--space-6);
  /* side padding grows on ultrawide so content caps at --bs-content-max and
     centres, while the bar background still spans the full viewport width */
  height:68px;padding:0 max(var(--space-6), calc((100% - var(--bs-content-max)) / 2));
  background:var(--surface);
  border-bottom:1px solid var(--fe-border);
  font-family:var(--font-sans);
}
.topbar__logo{flex:none;display:inline-flex;align-items:center;}
.topbar__logo img{height:30px;width:auto;display:block;}

/* primary nav — text links, never underlined (that's the body-link idiom) */
.topbar__nav{display:flex;align-items:center;gap:var(--space-5);flex:none;}
.topbar__link{
  font-size:var(--fs-body-sm);font-weight:var(--fw-semibold);
  color:var(--fe-text-2);text-decoration:none;
  padding:6px 2px;border-radius:3px;
  transition:color .12s ease;
}
.topbar__link:hover{color:var(--ink);}
.topbar__link.is-active{color:var(--ink);}
.topbar__link:focus-visible{outline:none;box-shadow:0 0 0 2px var(--green);}

/* the gap — everything after it hugs the right edge */
.topbar__spacer{flex:1 1 auto;}

/* right cluster: search + auth, all on one 40px baseline */
.topbar__right{display:flex;align-items:center;gap:var(--space-4);flex:0 1 auto;min-width:0;}
.topbar__search{flex:0 1 300px;min-width:160px;}

/* in the bar the button takes the field height (40px) so it lines up with the
   search — the same approach .search__btn uses; standalone .btn keeps 44px */
.topbar .btn{min-height:0;height:var(--field-height);}

/* nav dropdown — a trigger that reveals a rich link panel on hover and pins it
   open on click. Plain .topbar__link and dropdown items live in one .topbar__nav.
   Behaviour (hover-open, click-pin, keyboard, close) is header.js.
   Markup: .topbar__item > (button.topbar__link.topbar__link--menu + .topbar__menu) */
.topbar__item{position:relative;display:inline-flex;align-items:center;}
/* The account anchor is half the auth slot and is hidden while the other half
   shows, so the same [hidden] guard .btn needs applies here — display:inline-flex
   above would otherwise keep it on screen with the attribute set. */
.topbar__item[hidden]{display:none;}

/* the trigger is a <button> wearing .topbar__link, plus a chevron that flips */
.topbar__link--menu{
  appearance:none;border:0;background:transparent;cursor:pointer;
  display:inline-flex;align-items:center;gap:5px;font-family:var(--font-sans);
}
.topbar__chev{width:14px;height:14px;flex:none;color:currentColor;transition:transform .14s ease;}
.topbar__item.is-open > .topbar__link--menu{color:var(--ink);}
.topbar__item.is-open .topbar__chev{transform:rotate(180deg);}

/* the panel — same chrome as the other popups (.menu / .amenu__panel) */
.topbar__menu{
  position:absolute;top:calc(100% + 8px);left:0;z-index:40;min-width:268px;
  background:var(--surface);border:1px solid var(--fe-border);border-radius:8px;
  box-shadow:0 12px 28px rgba(0,0,0,.14);padding:6px;
  opacity:0;visibility:hidden;transform:translateY(-4px);
  /* visibility gets a 0s change DELAYED to the end of the fade, not a .12s
     transition. Transitioning it meant that on OPEN the panel stayed computed
     `hidden` for a frame — long enough that the script's .focus() on the first
     item, running in the same tick as .is-open, landed on an unfocusable
     element and silently did nothing (measured: hidden after a forced reflow
     and after one rAF; focusable only after two). Delaying instead flips it to
     visible immediately on open and still holds it visible through the fade on
     close, which is all the transition was ever buying. */
  transition:opacity .12s ease,transform .12s ease,visibility 0s linear .12s;
}
/* invisible bridge across the 8px gap so the cursor can cross without closing */
.topbar__menu::before{content:"";position:absolute;top:-8px;left:0;right:0;height:8px;}
.topbar__item.is-open .topbar__menu{
  opacity:1;visibility:visible;transform:translateY(0);
  transition:opacity .12s ease,transform .12s ease,visibility 0s;
}
@media (prefers-reduced-motion:reduce){
  /* collapse the big entrance movements (modal rise/scale, panel slide-in, menu
     drop) to a plain opacity fade — no translate or scale — for users who've
     asked for reduced motion. The overlay opacity fade still eases them in. */
  .modal{transition:opacity .15s ease;transform:none;}
  .modal-overlay.is-open .modal{transform:none;}
  .panel{transition:none;transform:none;}
  .panel-overlay.is-open .panel{transform:none;}
  .topbar__menu,.topbar__item.is-open .topbar__menu{transform:none;}
}

/* a menu item — bold label over a one-line description; no icon (by decision —
   the labels carry the meaning, so a glyph would be ornament; see icons.md).
   The description is optional: a title on its own is a plain single-line item,
   which is what the account menu below uses. */
.topbar__mi{display:flex;flex-direction:column;gap:2px;padding:9px 10px;border-radius:var(--radius);text-decoration:none;}
.topbar__mi:hover,.topbar__mi.is-active{background:var(--surface-hover);outline:none;}
.topbar__mi:focus-visible{outline:none;box-shadow:0 0 0 2px var(--green);}
.topbar__mi-title{font-size:var(--fs-body-sm);font-weight:var(--fw-semibold);color:var(--ink);}
.topbar__mi-desc{font-size:var(--fs-caption);line-height:1.4;color:var(--fe-text-2);}
/* an item may be a real <button> when it RUNS something instead of navigating
   (Sign out, Report a bug). Only the UA button chrome needs neutralising — the
   layout, padding and radius already come from .topbar__mi, and :hover /
   .is-active outrank this on specificity so the highlight still lands. */
button.topbar__mi{appearance:none;border:0;background:transparent;width:100%;text-align:left;font-family:var(--font-sans);cursor:pointer;}

/* account menu — the signed-in auth control opens the same .topbar__menu popup
   as a nav dropdown, off the same .topbar__item anchor, so one-at-a-time,
   click-outside and Esc all behave identically. It differs in one way that
   matters: it opens on CLICK only (header.js) — an account control is not a
   destination you might be walking past, so there is nothing to preview.
   Markup: .topbar__item > button.btn.topbar__account + .topbar__menu[role=menu]
   The trigger keeps its plain .btn footprint (--btn-min-width) so the auth slot
   still doesn't move on sign-in; no chevron, for the same reason. */
.topbar__right .topbar__menu{left:auto;right:0;}   /* hug the bar's right edge, not overflow it */
.topbar__item.is-open .topbar__account{background:var(--surface-hover);}
/* the anchor has to compress exactly like the bare button it wraps, or the auth
   slot changes width on sign-in — the one thing it must never do. Account's
   content runs ~2px past the 132px floor and a bare .btn absorbs that by
   shrinking to its own min-width; a flex item's automatic minimum size is its
   min-content, so an unqualified wrapper refuses to shrink at all. Taking the
   button's own floor gives the anchor the same stopping point — leave it at 0
   and it shrinks straight past the button it is supposed to be anchoring. */
.topbar__right .topbar__item{min-width:var(--btn-min-width);}

/* Header burger + mobile nav (locked 2026-07-16, Navigation group) ---------
   Below 1024px the row can't hold logo + full nav + the right cluster (Sign in
   + the persistent CTA together need ~960px of row before they push past the
   viewport), so the nav and the right cluster collapse behind a burger and stack
   as full-width rows that open under the bar. Desktop is untouched: the burger is
   display:none and every mobile rule lives inside the media block. header.js
   toggles .topbar.is-menu-open on the bar; the existing .is-open state on each
   .topbar__item drives the inline accordion. No new tokens. */
.topbar__burger{
  display:none;appearance:none;border:0;background:transparent;cursor:pointer;
  flex:none;width:48px;height:68px;align-items:center;justify-content:center;
  color:var(--fe-text-2);
}
.topbar__burger:hover{color:var(--ink);}
.topbar__burger:focus-visible{outline:none;box-shadow:0 0 0 2px var(--green);}
.topbar__burger svg{width:24px;height:24px;flex:none;}
.topbar__burger .topbar__burger-ico--close{display:none;}
.topbar.is-menu-open .topbar__burger .topbar__burger-ico--open{display:none;}
.topbar.is-menu-open .topbar__burger .topbar__burger-ico--close{display:block;}

@media (max-width:1024px){
  .topbar{flex-wrap:wrap;height:auto;min-height:68px;gap:0;padding:0 var(--space-5);}
  .topbar__logo{min-height:68px;}
  .topbar__spacer{display:none;}
  .topbar__burger{display:inline-flex;margin-left:auto;}
  /* nav + right cluster collapse to full-width stacked rows below the bar */
  .topbar__nav,.topbar__right{flex-basis:100%;display:none;}
  /* ...and they must wrap BELOW the burger, not above it. DOM order in .topbar
     is logo, nav, spacer, burger, right, so once .topbar__nav takes a full
     basis it claims line 1 and pushes everything after it — the burger
     included — onto later lines. Closed, that is invisible: nav and right are
     display:none, so the burger still shares line 1 with the logo. Open, the
     burger dropped to line 3 (measured 129-197px down a 301px bar at 900px),
     sandwiched between the nav links and the right cluster, with a dead band
     above it. The control you press to open the panel moved away from the
     place you pressed it — and this component's own contract is that the
     burger "sits at the right of the bar" and the panel drops "beneath the
     bar" (header.md, Collapsed).

     `order` rather than a DOM change because the burger is desktop-hidden and
     "the host does no responsive work" (header.md) — a DOM fix would make
     every host edit its markup, and any host that did not would keep the bug.

     KNOWN CONSEQUENCE, stated because this file rejects `order:` in the status
     bar for exactly this reason: order never moves tab order, so with the
     common DOM the open panel tabs logo, nav, burger, right while it READS
     logo, burger, nav, right — the close control is reached mid-panel. Accepted
     here as the smaller fault: the misplaced burger affects everyone and moves
     under the pointer, where this costs a keyboard reader one out-of-sequence
     stop. A host that wants both can put .topbar__burger before .topbar__nav
     in its own markup; these rules give the same layout either way. */
  .topbar__nav{order:1;}
  .topbar__right{order:2;}
  .topbar.is-menu-open .topbar__nav,
  .topbar.is-menu-open .topbar__right{display:flex;flex-direction:column;align-items:stretch;}
  .topbar.is-menu-open .topbar__nav{gap:0;border-top:1px solid var(--fe-border);padding:var(--space-2) 0;}
  .topbar.is-menu-open .topbar__right{padding:var(--space-2) 0 var(--space-4);}
  .topbar__nav .topbar__item{display:block;width:100%;}
  /* the account menu's anchor stacks the same way; its panel then falls through
     to the accordion reset below like any other .topbar__menu */
  .topbar__right .topbar__item{display:block;width:100%;}
  .topbar__nav>.topbar__link,
  .topbar__link--menu{width:100%;min-height:44px;box-sizing:border-box;padding:6px 4px;}
  .topbar__link--menu{justify-content:space-between;}
  /* dropdown panels become inline accordions, not floating popups */
  .topbar__menu{
    position:static;z-index:auto;min-width:0;background:transparent;
    border:0;border-radius:0;box-shadow:none;padding:0 0 var(--space-2) var(--space-4);
    opacity:1;visibility:visible;transform:none;transition:none;display:none;
  }
  .topbar__menu::before{display:none;}
  .topbar__item.is-open .topbar__menu{display:block;}
  .topbar__mi{padding:8px 6px;}
  /* the CTA fills the width with a proper 44px touch target (border-box so the
     button's own padding stays inside 100% — there is no global box reset) */
  .topbar__right .btn{box-sizing:border-box;width:100%;height:44px;}
}


/* ==========================================================================
   FOOTER   (locked 2026-06-26, Navigation group) ------------------------
   The site / app footer — the one dark-inverted surface in the system (warm
   near-black, --footer-* tokens). One component, two configs on a shared base:
   the FULL marketing footer (brand + social · link columns · newsletter ·
   bottom bar) and --slim (the bottom bar alone, for app surfaces). Every block
   below the columns is optional — drop the newsletter, the social row, or the
   whole top half independently. Reuses link / form-field / button / icon-button
   untouched, re-tinted for the dark surface; the bottom bar carries the © line,
   the ABN and the legal links. CSS-only, no script.
   ========================================================================== */
.footer{
  background:var(--footer-surface);
  color:var(--footer-text-2);
  font-family:var(--font-sans);
}
.footer__inner{max-width:1180px;margin:0 auto;padding:var(--space-7) var(--space-6) var(--space-5);}

/* body row: brand | link columns | newsletter */
.footer__body{display:flex;gap:var(--space-6);justify-content:space-between;align-items:flex-start;flex-wrap:wrap;}

.footer__brand{flex:0 1 240px;}
.footer__brand img{height:26px;width:auto;display:block;}  /* logo-footer.svg — green mark, white wordmark, green .ai */
.footer__tagline{margin:var(--space-3) 0 0;font-size:var(--fs-body-sm);line-height:1.5;color:var(--footer-text-2);max-width:240px;}
.footer__social{display:flex;gap:6px;margin-top:var(--space-4);}

.footer__cols{display:flex;gap:var(--space-6);flex:1 1 auto;flex-wrap:wrap;justify-content:space-between;max-width:560px;}
.footer__col{min-width:104px;}
.footer__coltitle{
  font-size:var(--fs-caption);font-weight:var(--fw-semibold);
  letter-spacing:.07em;text-transform:uppercase;
  color:var(--footer-text);margin:0 0 var(--space-3);
}
.footer__col ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px;}
.footer__link{color:var(--footer-text-2);text-decoration:none;font-size:var(--fs-body-sm);transition:color .12s ease;}
.footer__link:hover{color:var(--footer-text);}
.footer__link:focus-visible{outline:none;box-shadow:0 0 0 2px var(--green-bright);border-radius:2px;}

/* newsletter — email field full-width on top, Subscribe right-aligned below.
   Stacked (not inline) on purpose: sidesteps the 40px-field / 44px-button
   height mismatch and stays clean at any width. */
.footer__news{flex:0 1 280px;}
.footer__news .footer__coltitle{margin-bottom:6px;}
.footer__newscopy{font-size:var(--fs-body-sm);line-height:1.5;margin:0 0 var(--space-3);color:var(--footer-text-2);}
.footer__newsform{display:flex;flex-direction:column;gap:var(--space-2);}
.footer__newsform .field{width:100%;margin-bottom:0;}
.footer__newsform .field__control{width:100%;}
/* the field input is content-box with 12px padding and the system has no global
   border-box reset — width:100% must include the padding or the input overflows */
.footer__newsform .field__input{box-sizing:border-box;width:100%;}
.footer__newsform .btn{align-self:flex-end;}

/* bottom bar — © + ABN on the left, legal links on the right */
.footer__bar{
  display:flex;justify-content:space-between;align-items:center;gap:var(--space-4);flex-wrap:wrap;
  border-top:1px solid var(--footer-border);
  margin-top:var(--space-6);padding-top:var(--space-4);
}
.footer__copy{font-size:var(--fs-caption);line-height:1.5;color:var(--footer-text-2);}
.footer__copy strong{color:var(--footer-text);font-weight:var(--fw-semibold);}
.footer__legal{display:flex;gap:var(--space-4);flex-wrap:wrap;}
.footer__legal a{color:var(--footer-text-2);text-decoration:none;font-size:var(--fs-caption);}
.footer__legal a:hover{color:var(--footer-text);}

/* slim — the bottom bar alone, for app / signed-in surfaces */
.footer--slim .footer__inner{padding:var(--space-5) var(--space-6);}
.footer--slim .footer__bar{border-top:0;margin-top:0;padding-top:0;}

/* scoped reuse: form-field label + the social icon-buttons adapt to the dark surface */
.footer .field__label{color:var(--footer-text);}
.footer .ib{color:var(--footer-text-2);}
.footer .ib:hover{background:rgba(255,255,255,.08);color:var(--footer-text);}
.footer .ib .icon{width:18px;height:18px;}

/* --------------------------------------------------------------------------
   Status bar   (locked 2026-07-26)
   The signed-in platform status bar: a full-width strip fixed to the viewport
   bottom carrying a worst-of health dot, a state word, four content slots, and
   a drop-up of per-connection detail. Sits on the footer's dark surface — the
   system's only inverted surface — and uses the on-dark status colours, since
   the light-canvas ones fail AA as text there.

   Colour is never the only signal: the dot and the state word always travel
   together, and the word carries the meaning on its own.

   State is set by the host with data-state="healthy|degraded|down" on
   .statusbar (the aggregate) and on each .statusbar__row (that connection).
   Behaviour: components/status-bar.js.
   -------------------------------------------------------------------------- */
.statusbar{
  position:fixed;left:0;right:0;bottom:0;z-index:60;
  height:var(--statusbar-h);
  display:flex;align-items:center;gap:var(--space-4);
  padding:0 var(--space-4);
  background:var(--footer-surface);
  border-top:1px solid var(--footer-border);
  color:var(--footer-text-2);
  font-family:var(--font-sans);
  font-size:var(--fs-caption);
  line-height:1;
}
/* The host hides the whole bar with `hidden` — a signed-out reader must not
   see it at all, and the host cannot know that until its session check
   answers. display:flex above outranks the UA's own [hidden]{display:none},
   so without restating it here the attribute is set and silently does
   nothing. The panel already carries the same guard for the same reason. */
.statusbar[hidden]{display:none;}

/* The dot. One glyph shared by the bar and the drop-up rows. */
.statusbar__dot{
  width:var(--statusbar-dot);height:var(--statusbar-dot);
  border-radius:50%;flex:none;
  background:var(--fe-text-2);
}
/* The aggregate dot lives in the trigger; each row carries its own. Scoping
   them separately keeps a row's state from being overridden by the aggregate. */
.statusbar[data-state="healthy"]  .statusbar__trigger .statusbar__dot{background:var(--status-success-on-dark);}
.statusbar[data-state="degraded"] .statusbar__trigger .statusbar__dot{background:var(--status-warning-on-dark);}
.statusbar[data-state="down"]     .statusbar__trigger .statusbar__dot{background:var(--status-error-on-dark);}
.statusbar__row[data-state="healthy"]  .statusbar__dot{background:var(--status-success-on-dark);}
.statusbar__row[data-state="degraded"] .statusbar__dot{background:var(--status-warning-on-dark);}
.statusbar__row[data-state="down"]     .statusbar__dot{background:var(--status-error-on-dark);}

/* The status control — the trigger and the drop-up it opens, as one box.
   Everything after .statusbar__slots forms the right-hand cluster, in markup
   order: status, updated, clock. Markup order, not `order:` — the tab order
   and the reading order have to keep agreeing with each other.

   The wrapper exists for one reason: the panel aligns to the TRIGGER, not to
   the bar, and the trigger is no longer the bar's last child. CSS cannot say
   "align to a sibling", so the panel is positioned against a box that hugs
   the trigger. Computing the panel's `right` from the width of the clock and
   the updated line would work too, and would need JS to stay correct as their
   content changes — the wrapper is the version that cannot drift.

   The negative margins are the trigger's 8px padding paid back, so the
   trigger's TEXT — not its hover background — sits on the same 16px rhythm as
   the slots on one side and the separator on the other. They live here rather
   than on the trigger because the wrapper is what the bar lays out now. */
.statusbar__status{
  position:relative;
  display:flex;align-items:center;
  height:var(--statusbar-h);flex:none;
  margin:0 calc(var(--space-2) * -1);
}

/* Trigger — a real button. The bar is 32px, below the 44px touch minimum, so
   the hit area is extended upward by a pseudo-element: the target measures
   44px while the bar still draws 32px. The wrapper must never clip it, which
   is why .statusbar__status sets no overflow. */
.statusbar__trigger{
  position:relative;
  display:flex;align-items:center;gap:var(--space-2);
  height:var(--statusbar-h);
  padding:0 var(--space-2);
  border:0;background:transparent;
  color:var(--footer-text);
  font-family:inherit;font-size:inherit;line-height:1;
  cursor:pointer;border-radius:var(--radius);
}
.statusbar__trigger::before{
  content:'';position:absolute;left:0;right:0;bottom:0;
  top:calc((44px - var(--statusbar-h)) * -1);
}
.statusbar__trigger:hover{background:rgba(255,255,255,.08);}
.statusbar__trigger:focus-visible{outline:2px solid var(--footer-text);outline-offset:1px;}
.statusbar__state{white-space:nowrap;}
.statusbar[data-state="healthy"]  .statusbar__trigger .statusbar__state{color:var(--status-success-on-dark);}
.statusbar[data-state="degraded"] .statusbar__trigger .statusbar__state{color:var(--status-warning-on-dark);}
.statusbar[data-state="down"]     .statusbar__trigger .statusbar__state{color:var(--status-error-on-dark);}
.statusbar__chev{width:14px;height:14px;flex:none;transition:transform .12s ease;}
.statusbar__trigger[aria-expanded="true"] .statusbar__chev{transform:rotate(180deg);}

/* Slots — the four content categories, condensed. Hidden progressively as the
   bar narrows; the drop-up always carries them in full, so nothing is lost. */
.statusbar__slots{display:flex;align-items:center;gap:var(--space-4);flex:1;min-width:0;overflow:hidden;}
.statusbar__slot{display:flex;align-items:center;gap:var(--space-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.statusbar__slot strong{color:var(--footer-text);font-weight:var(--fw-semibold);}

/* Updated and clock — the rest of the right-hand cluster, in that order, with
   the clock hard right on the bar's padding edge. Both are the host's to fill
   and both disappear when it leaves them empty, so a host that wants neither
   gets the same bar it had before rather than two stray separators.

   The separator is the component's, not the host's: these are the bar's own
   children, so the bar owns the punctuation between them. `var(--space-4)`
   after the dot matches the flex gap before it, and because .statusbar__status
   pays the trigger's padding back, that gap is measured from the trigger's
   text — 16px either side of every dot, the rhythm the slots already use.

   Tabular figures on the clock: it reruns every minute, and proportional
   digits would shift the one element pinned to the bar's right edge. */
.statusbar__updated,
.statusbar__clock{white-space:nowrap;flex:none;}
.statusbar__clock{font-variant-numeric:tabular-nums;}
.statusbar__updated::before,
.statusbar__clock::before{content:"·";margin-right:var(--space-4);}
.statusbar__updated:empty,
.statusbar__clock:empty{display:none;}

/* Drop-up. Opens upward from a bar already at the viewport's bottom edge, and
   is capped to the space above it so four connections with detail can never
   run past the top of the viewport. Anchored to the trigger's right TEXT edge
   — a disclosure opens under the control that opened it, and the trigger no
   longer holds the bar's edge for the panel to borrow. The wrapper's right edge
   is the trigger's BORDER edge, so the inset here is the trigger's own padding
   again: 8px in from the hover box lands exactly on the text. */
.statusbar__panel{
  position:absolute;right:var(--space-2);bottom:calc(var(--statusbar-h) + 6px);
  min-width:320px;max-width:min(480px, calc(100vw - var(--space-6)));
  max-height:calc(100dvh - var(--statusbar-h) - var(--space-6));
  overflow-y:auto;
  background:var(--footer-surface);
  border:1px solid var(--footer-border);
  border-radius:var(--radius);
  box-shadow:0 -6px 24px rgba(0,0,0,.36);
  padding:var(--space-3);
}
.statusbar__panel[hidden]{display:none;}
.statusbar__panelhead{
  font-size:var(--fs-caption);color:var(--footer-text-2);
  padding:0 var(--space-2) var(--space-2);
  border-bottom:1px solid var(--footer-border);margin-bottom:var(--space-2);
}

/* One monitored connection. */
.statusbar__row{
  display:flex;align-items:center;gap:var(--space-3);
  padding:var(--space-2);border-radius:var(--radius);
  font-size:var(--fs-caption);line-height:1.4;
}
.statusbar__conn{flex:1;min-width:0;color:var(--footer-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.statusbar__rowstate{flex:none;}
.statusbar__row[data-state="healthy"]  .statusbar__rowstate{color:var(--status-success-on-dark);}
.statusbar__row[data-state="degraded"] .statusbar__rowstate{color:var(--status-warning-on-dark);}
.statusbar__row[data-state="down"]     .statusbar__rowstate{color:var(--status-error-on-dark);}
.statusbar__latency{flex:none;color:var(--footer-text-2);font-variant-numeric:tabular-nums;min-width:56px;text-align:right;}

/* The four categories in full — what the bar drops at narrow widths. */
.statusbar__detail{
  border-top:1px solid var(--footer-border);
  margin-top:var(--space-2);padding-top:var(--space-2);
}
.statusbar__detailrow{display:flex;gap:var(--space-3);padding:var(--space-2);font-size:var(--fs-caption);line-height:1.4;}
.statusbar__detaillabel{flex:none;min-width:92px;color:var(--footer-text-2);}
.statusbar__detailvalue{flex:1;min-width:0;color:var(--footer-text);}

/* Shell offset — the bar is fixed, so it overlays content. Without this the
   last row of every table hides underneath it. */
.app-shell--statusbar .app-shell__main{padding-bottom:var(--statusbar-h);}

/* ---- Embedded mode   (added 2026-07-27) ---------------------------------
   A page being shown INSIDE another page — the [takeover](panel.md)'s frame —
   drops its own chrome, so the reader sees one topbar and one way out instead
   of two of each. The host page's header is the chrome; the framed page is the
   content.

   IT HIDES, IT DOES NOT REMOVE. The markup stays exactly as it is, which is
   what keeps this from forking the page: one document serves both the direct
   visit and the framed one, no server branch, no second template, and every
   spec rule that asserts a topbar or a trail exists still passes. Turning the
   mode off is one class.

   ONLY THE CHROME GOES. Not the content, not the panels, not the links —
   a framed page must stay a working page, because the reader will click in it.

   The class lands on the SAME element as .modal-open / .panel-open (the <body>
   of a v2 page), so it is set the same way and scopes the same way. The host
   turns it on from its own URL flag — the library has no opinion on the flag's
   name, only on what the mode does. */
body.is-embedded .topbar,
body.is-embedded .breadcrumb,
body.is-embedded .footer,
body.is-embedded .statusbar{display:none;}
/* The bar is gone, so the room reserved for it would be a dead strip at the
   bottom of the frame. */
body.is-embedded .app-shell--statusbar .app-shell__main{padding-bottom:0;}

/* Narrow widths. Everything drops in order of importance, least important
   first, and everything dropped is still in the drop-up:

     clock      — the only thing in the bar the reader's own OS already shows
                  them, in the same corner of the same screen. First to go.
     updated    — data freshness. Useful, but the drop-up's Activity row
                  carries the same timestamp in full.
     activity   — live request state, and silent when idle anyway.
     context    — what you are looking at, which the page itself also says.
     environment
     alerts     — never dropped. It is the one slot that reports a problem.

   The status control is never dropped either: it is the component.

   The two widths are measured, not chosen. Against a real signed-in bar — the
   long identity string in the environment slot, the right-hand cluster at its
   full 477px — 1080px is the last width at which nothing is ellipsised; at
   1040 the environment and context slots start losing characters. Dropping the
   clock buys back ~156px, which carries the bar to just past 900, where they
   begin clipping again. So `updated` goes at 900 as well: by then it is that or
   ellipsised text in two slots, and the drop-up still has the timestamp.

   That both go at 900 is where the space ran out, not a tie in importance —
   the priority order is what decides which one leaves first, and the clock
   already has.

   This used to read "right-to-left", which was true of the old layout only
   because alerts happened to sit at the far right. The right edge belongs to
   the clock now; the priority is what was actually decided, so the priority is
   what the rule states. */
@media (max-width:1080px){ .statusbar__clock{display:none;} }
@media (max-width:900px){ .statusbar__slot--activity,.statusbar__updated{display:none;} }
@media (max-width:720px){ .statusbar__slot--context{display:none;} }
@media (max-width:480px){
  .statusbar__slot--env{display:none;}
  .statusbar{gap:var(--space-3);padding:0 var(--space-3);}
  /* Full-bar-width, re-derived. The panel lives inside .statusbar__status now,
     so `left`/`right` alone would inset from a box that hugs the trigger — the
     rule would go on rendering and quietly stop doing anything. Dropping the
     wrapper out of the positioning chain hands the panel back to .statusbar,
     which is what these insets have always meant.

     There is nothing left to align to down here anyway: the clock and the
     updated line are gone by this width, so the trigger holds the bar's edge
     on its own and the wrapper has no job.

     max-width has to go with it. min(480px, 100vw - 32px) is 8px narrower than
     the insets ask for, so it silently won the cascade and the panel has been
     landing 8px short of its right inset. Alignment is the whole point of this
     rule, so the cap comes off. */
  .statusbar__status{position:static;}
  .statusbar__panel{left:var(--space-3);right:var(--space-3);min-width:0;max-width:none;}
}

/* --------------------------------------------------------------------------
   Page sections   (locked 2026-06-29)
   Marketing page-composition patterns: the Hero, the Section band, and the
   Platform network (scrollytelling). Built on the tokens + existing primitives
   (card, card-grid, buttons, links). Seeded by the Home page (issue 0005).
   -------------------------------------------------------------------------- */

/* Hero — centred marketing hero: eyebrow + display title + lede + CTA row.
   Optional scroll cue for a scrollytelling page. */
.hero{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;
  text-align:center;padding:var(--space-7) var(--space-6) var(--space-8);}
.hero__inner{max-width:760px;}
.hero .t-eyebrow{margin-bottom:var(--space-4);}
.hero__title{margin:0 auto;max-width:600px;text-wrap:balance;}
.hero__lede{margin:var(--space-5) auto 0;max-width:56ch;color:var(--fe-text-2);}
.hero .cta-row{margin-top:var(--space-6);}
.hero__scrollcue{margin-top:var(--space-8);color:var(--fe-text-2);font-size:var(--fs-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;font-weight:var(--fw-semibold);
  display:flex;flex-direction:column;align-items:center;gap:var(--space-2);}
.hero__scrollcue svg{width:20px;height:20px;animation:hero-bob 1.8s ease-in-out infinite;}
@keyframes hero-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(4px)}}

/* a shared centred action row (hero + section CTA) */
.cta-row{display:flex;gap:var(--space-3);justify-content:center;flex-wrap:wrap;}
/* --start: left-aligned, for an action row under left-aligned copy (a wizard
   screen card, a form card) rather than a centred hero/section block */
.cta-row--start{justify-content:flex-start;margin-top:var(--space-6);}

/* Section band — a marketing section with page rhythm and a centred heading
   block. --alt swaps to the white surface for the light/dark section cadence. */
.section{padding:var(--space-9) var(--space-6);}
.section--alt{background:var(--fe-surface);}
.section__inner{max-width:1080px;margin-inline:auto;}
.section__head{max-width:60ch;margin:0 auto var(--space-7);text-align:center;}
.section__head .t-eyebrow{margin-bottom:var(--space-3);}
.section__head .t-h2{margin-bottom:var(--space-4);}
.section__head .lede{color:var(--fe-text-2);}
.section__center{text-align:center;margin-top:var(--space-7);}
/* centred prose block (e.g. an on-spine credibility slice) */
.section__prose{max-width:62ch;margin-inline:auto;text-align:center;}
.section__prose .t-eyebrow{margin-bottom:var(--space-3);}
.section__prose p{color:var(--fe-text-2);margin:var(--space-3) 0 var(--space-5);}
/* CTA section — centred, narrow */
.section--cta{text-align:center;}
.section--cta .section__inner{max-width:680px;}
.section--cta p{color:var(--fe-text-2);max-width:54ch;margin:0 auto var(--space-6);}
.section--cta .cta-row{margin-top:var(--space-5);}

/* 4-up card row. The card ships height:100%, which under-sizes an auto grid row
   and lets the cards overflow it — let the track size them; stretch still
   equalises heights. */
.card-grid--4{grid-template-columns:repeat(4,minmax(0,1fr));}
.card-grid--4 .card{height:auto;}
@media (max-width:900px){.card-grid--4{grid-template-columns:repeat(2,minmax(0,1fr));}}
@media (max-width:520px){.card-grid--4{grid-template-columns:1fr;}}

/* card-grid--2 — two-up grid (a fixed 2x2 for four cards), centred and width-capped
   so cards stay readable; collapses to a single column on narrow screens.
   height:auto (as in --4) stops cards overflowing their row track and overlapping
   the row below when columns have unequal content. */
.card-grid--2{grid-template-columns:repeat(2,minmax(0,1fr));max-width:860px;margin-inline:auto;}
.card-grid--2 .card{height:auto;}
@media (max-width:640px){.card-grid--2{grid-template-columns:1fr;}}

/* link list — a tidy 2-up grid of self-select destination rows */
.linklist{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-4);max-width:760px;margin:0 auto;}
@media (max-width:560px){.linklist{grid-template-columns:1fr;}}
.linklist__item{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);
  padding:var(--space-4) var(--space-5);background:var(--fe-canvas);border:1px solid var(--fe-border);
  border-radius:var(--card-radius);color:var(--ink);text-decoration:none;transition:border-color .12s ease;}
.section--alt .linklist__item{background:var(--fe-surface);}
.linklist__item:hover{border-color:var(--green-deep);}
.linklist__label{font-weight:var(--fw-semibold);}
.linklist__arrow{color:var(--green-text);}

/* statements — a framed hairline matrix of the deliberate omissions. No fill;
   thin rules inside one bordered frame (line-art, infrastructure feel); the
   leading "No" set in brand green as the one repeated accent. */
.statements{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1px;
  background:var(--fe-border);border:1px solid var(--fe-border);border-radius:var(--card-radius);overflow:hidden;}
.statement{padding:var(--space-6);background:var(--fe-surface);}
.statement__head{font-weight:var(--fw-bold);color:var(--ink);margin:0 0 var(--space-3);font-size:var(--fs-h3);}
.statement__no{color:var(--green-deep);}
.statement__body{color:var(--fe-text-2);margin:0;}

/* Platform network (scrollytelling) — a pinned stage that IS the Brain-Stem mark.
   Each product owns a region of the mark; the mark assembles region by region as
   the chapters scroll, and the active region is annotated as a dashed, labelled
   frame. Thin-stroke line-art, one accent, no glow. Behaviour:
   platform-network.js (auto-inits .scrolly). */
.scrolly{padding:0 var(--space-6);}
.scrolly__inner{max-width:1140px;margin-inline:auto;display:grid;grid-template-columns:1fr 1fr;gap:var(--space-8);align-items:start;}
.scrolly__stage{position:sticky;top:88px;align-self:start;}
.scrolly .chapter{min-height:56vh;display:flex;flex-direction:column;justify-content:center;opacity:.5;transition:opacity .4s ease;}
.scrolly .chapter:first-child{min-height:46vh;}
.scrolly .chapter.is-active{opacity:1;}
.scrolly .chapter .t-eyebrow{margin-bottom:var(--space-3);text-transform:none;}
.scrolly .chapter h2{margin:0 0 var(--space-4);}
.scrolly .chapter p{color:var(--fe-text-2);margin:0 0 var(--space-4);max-width:46ch;}

.platnet{margin:0;background:var(--fe-surface);border:1px solid var(--fe-border);border-radius:var(--card-radius);box-shadow:var(--card-shadow);overflow:hidden;width:100%;}  /* margin:0 resets the UA <figure> 40px inline margin, which overflowed the single-column mobile spine */
.platnet__head{display:flex;align-items:center;justify-content:flex-end;gap:var(--space-2);padding:var(--space-4) var(--space-5) 0;}
.platnet__steps{display:flex;gap:6px;}
.platnet__steps i{width:7px;height:7px;border-radius:50%;background:var(--fe-border);display:block;transition:background .3s ease,transform .3s ease;}
.platnet__steps i.is-on{background:var(--green-deep);transform:scale(1.25);}
.platnet__viz{padding:var(--space-5) var(--space-5) var(--space-2);}
.platnet__viz svg{width:100%;height:auto;display:block;}
.platnet__caption{padding:var(--space-3) var(--space-5) var(--space-5);font-size:var(--fs-caption);color:var(--fe-text-2);}
/* the mark — thin-stroke blueprint register; idle in --net-idle, green when lit */
.platnet__link{stroke:var(--net-idle);stroke-width:10;stroke-linecap:round;transition:stroke .4s ease;}
.platnet__link.is-on{stroke:var(--green-deep);}
.platnet__node circle{fill:var(--fe-surface);stroke:var(--net-idle);stroke-width:10;transition:stroke .35s ease,stroke-width .35s ease;}
.platnet__node.is-on circle{stroke:var(--green-deep);stroke-width:12;}
/* annotation frame — dashed bracket + label on the active region */
.platnet__frame{opacity:0;}
.platnet__frame.is-shown{opacity:1;transition:opacity .45s ease;}
.platnet__frame rect{fill:none;stroke:var(--green-bright);stroke-width:3;stroke-dasharray:11 13;}
.platnet__tag{fill:var(--ink);font:var(--fw-bold) 28px var(--font-sans);paint-order:stroke;stroke:var(--fe-surface);stroke-width:7px;stroke-linejoin:round;}

@media (prefers-reduced-motion:reduce){
  .hero__scrollcue svg{animation:none;}
  .platnet__frame,.platnet__link,.platnet__node circle,.platnet__steps i{transition:none;}
}
@media (max-width:860px){
  .scrolly__inner{grid-template-columns:1fr;gap:var(--space-5);}
  .scrolly__stage{position:sticky;top:64px;background:var(--fe-canvas);padding:var(--space-3) 0;z-index:1;}
  .scrolly .chapter{min-height:auto;opacity:1;padding:var(--space-6) 0;}
}
@media (max-width:640px){.section{padding:var(--space-8) var(--space-5);}}

/* Feature list — labelled groups of two-up media rows (icon + name + outcome +
   one line + link). Groups a related set (products, capabilities) under a ruled
   label so a reader can scan a pairing before the parts. Built on the tokens,
   the type scale, and .t-link. Seeded by the Platform overview (issue 0006). */
.featurelist{max-width:960px;margin-inline:auto;}
.featurelist__group + .featurelist__group{margin-top:var(--space-8);}
.featurelist__head{display:flex;align-items:baseline;gap:var(--space-3);flex-wrap:wrap;
  margin-bottom:var(--space-5);padding-bottom:var(--space-3);border-bottom:1px solid var(--fe-border);}
.featurelist__head .t-eyebrow{color:var(--green-text);}
.featurelist__note{color:var(--fe-text-2);}
.featurelist__row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-6);}
.featurelist__item{display:flex;gap:var(--space-4);}
.featurelist__icon{flex:0 0 auto;color:var(--green-deep);}
.featurelist__icon svg{width:24px;height:24px;}
.featurelist__name{display:flex;align-items:baseline;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-2);}
.featurelist__name .t-eyebrow{color:var(--fe-text-2);}
.featurelist__item p{color:var(--fe-text-2);margin:0 0 var(--space-3);}
@media (max-width:640px){.featurelist__row{grid-template-columns:1fr;gap:var(--space-5);}}

/* Engagement model — the reusable "how an engagement works" module: an intro
   column beside a vertical numbered rail of phases. The canonical instance is
   the fixed-price-delivery -> managed-services-retainer shape (ADR 0002),
   repeated on every Solutions page; it describes the shape of the relationship,
   never the numbers. No new tokens. Seeded by the Platform overview (issue 0006). */
.engagement{display:grid;grid-template-columns:0.9fr 1.1fr;gap:var(--space-8);
  max-width:1000px;margin-inline:auto;align-items:start;}
.engagement__intro .t-eyebrow{color:var(--green-text);margin-bottom:var(--space-3);}
.engagement__intro h2{margin-bottom:var(--space-4);}
.engagement__intro p{color:var(--fe-text-2);}
.engagement__intro p + p{margin-top:var(--space-5);}
.engagement__rail{display:grid;gap:var(--space-6);}
.engagement__step{display:grid;grid-template-columns:auto 1fr;gap:var(--space-4);}
.engagement__marker{display:flex;flex-direction:column;align-items:center;}
.engagement__dot{width:36px;height:36px;border-radius:999px;background:var(--green-deep);color:var(--surface);
  display:flex;align-items:center;justify-content:center;font-weight:var(--fw-bold);flex:0 0 auto;}
.engagement__line{width:2px;flex:1;background:var(--green-light);margin-top:var(--space-2);min-height:24px;}
.engagement__step:last-child .engagement__line{display:none;}
.engagement__phase{font-size:var(--fs-eyebrow);letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;
  font-weight:var(--fw-semibold);color:var(--green-text);margin-bottom:var(--space-2);}
.engagement__step h3{margin-bottom:var(--space-2);}
.engagement__step p{color:var(--fe-text-2);}
@media (max-width:760px){.engagement{grid-template-columns:1fr;gap:var(--space-7);}}

/* Header — sticky modifier: keep the top bar visible while the page scrolls.
   Composes with .app-shell (the sticky child sticks to the viewport top). */
.topbar--sticky{position:sticky;top:0;z-index:50;}

/* Demo embed — a product demonstration on a Platform/product page: a 16:9 poster
   frame with a play affordance and a caption that frames the clip as a
   demonstration ("is this real"), never as proof (CONTEXT.md Trust; ADR 0003).
   The frame uses --footer-surface (the system's only dark surface) so a stubbed
   demo reads as honest "coming" rather than a fabricated screenshot; drop a real
   poster image into .demo__poster when a recording exists. Seeded by CortexOne
   (issue 0007). No new tokens. */
.demo{max-width:880px;margin-inline:auto;}
.demo__frame{position:relative;aspect-ratio:16/9;border-radius:var(--card-radius);overflow:hidden;
  border:1px solid var(--fe-border);box-shadow:var(--card-shadow);background:var(--footer-surface);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-4);}
.demo__poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}
.demo__play{position:relative;display:inline-flex;align-items:center;justify-content:center;
  width:72px;height:72px;border-radius:999px;border:0;cursor:pointer;color:#fff;
  background:var(--green-text);box-shadow:var(--card-shadow);transition:background .15s ease,transform .15s ease;}
.demo__play:hover{background:var(--green-hover);transform:scale(1.04);}
.demo__play:focus-visible{outline:none;box-shadow:0 0 0 2px var(--footer-surface),0 0 0 4px var(--surface);}
.demo__play svg{width:30px;height:30px;margin-left:3px;}
.demo__label{position:relative;color:var(--footer-text-2);font-size:var(--fs-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;}
.demo__caption{margin-top:var(--space-4);text-align:center;color:var(--fe-text-2);max-width:60ch;margin-inline:auto;}
@media (max-width:640px){.demo__play{width:60px;height:60px;}.demo__play svg{width:26px;height:26px;}}

/* Booking / contact split — a high-intent form page layout: a persuasion aside
   (eyebrow + title + lede + point list + step-down) beside a sticky form card.
   The form card is a plain .card; the page wires the field / btn / banner
   primitives and the submit behaviour. Pairs with .confirm for the post-submit
   state. Reusable by Contact. Seeded by Book a briefing (issue 0014). No new
   tokens. */
.bookform{display:grid;grid-template-columns:1.05fr .95fr;gap:var(--space-8);
  max-width:1080px;margin-inline:auto;align-items:start;}
.bookform__aside{max-width:48ch;}
.bookform__aside .lede{color:var(--fe-text-2);margin-top:var(--space-4);}
.bookform__form{position:sticky;top:96px;}
.bookform__form .card > h2{margin-bottom:var(--space-5);}
.bookform__form .btn{width:100%;}
.bookform__fineprint{margin-top:var(--space-3);}
.bookform__stepdown{margin-top:var(--space-7);padding-top:var(--space-6);border-top:1px solid var(--fe-border);}
.bookform__stepdown p{color:var(--fe-text-2);margin:0 0 var(--space-4);max-width:44ch;}
@media (max-width:860px){
  .bookform{grid-template-columns:1fr;gap:var(--space-7);}
  .bookform__form{position:static;}
}

/* Point list — icon + title + one-line body rows; a compact reassurance list
   (e.g. "30 minutes / prepared / no obligation"). Built on the green accent and
   the type scale. Seeded by Book a briefing (issue 0014). No new tokens. */
.pointlist{list-style:none;margin:var(--space-6) 0 0;padding:0;display:grid;gap:var(--space-5);}
.pointlist__item{display:flex;gap:var(--space-3);}
.pointlist__item > svg{flex:0 0 auto;width:22px;height:22px;color:var(--green-deep);margin-top:3px;}
.pointlist__title{font-weight:var(--fw-semibold);color:var(--ink);margin:0 0 2px;}
.pointlist__body{margin:0;color:var(--fe-text-2);font-size:var(--fs-body-sm);line-height:var(--lh-body-sm);}

/* Confirmation panel — the centred post-submit success state of a form page: a
   green check, eyebrow, heading, lede, and a forward CTA row (never a dead end).
   The page toggles it in place of the form on success; reusable by every form
   page (assessment, contact). Seeded by Book a briefing (issue 0014). No new
   tokens. */
.confirm{max-width:620px;margin-inline:auto;text-align:center;}
.confirm__icon{color:var(--green-deep);display:flex;justify-content:center;margin-bottom:var(--space-4);}
.confirm__icon svg{width:56px;height:56px;}
.confirm .lede{color:var(--fe-text-2);margin-top:var(--space-4);}
.confirm .cta-row{margin-top:var(--space-6);}

/* Team / person — a person card: a circular avatar (photo, or an initials
   fallback when no photo exists) above the card's role / name / bio, with a
   links row pinned to the foot so cards align in a grid. Composes the .card
   primitive; use inside a .card-grid. Seeded by the About page (issue 0016).
   No new tokens. */
.person{align-items:flex-start;text-align:left;}
.person__avatar{width:88px;height:88px;border-radius:50%;object-fit:cover;display:block;
  margin:0 0 var(--space-4);background:var(--fe-canvas);}
.person__avatar--initials{display:flex;align-items:center;justify-content:center;
  background:var(--green-light);color:var(--green-text);
  font-weight:var(--fw-bold);font-size:var(--fs-h2);letter-spacing:.02em;}
.person__links{margin-top:auto;padding-top:var(--space-5);display:flex;gap:var(--space-4);}

/* --------------------------------------------------------------------------
   Wizard layout   (locked 2026-07-30)
   Page sections — the frame for a multi-step flow: a sticky stepper rail
   beside the screen area. Composes the stepper (progress rail) and the card
   primitive; the host swaps screens inside .app-content and owns all step
   state. --solo drops the rail (a delegate / scoped run has no overview).
   Seeded by the Assessment page shell.
   -------------------------------------------------------------------------- */
.app-layout{display:grid;grid-template-columns:260px minmax(0,1fr);gap:var(--space-7);
  align-items:start;padding:var(--space-7) var(--space-6) var(--space-9);}
.app-layout--solo{grid-template-columns:minmax(0,1fr);}
.stepper-rail{position:sticky;top:var(--space-7);}
.stepper-rail__heading{font:var(--fw-semibold) var(--fs-eyebrow)/var(--lh-eyebrow) var(--font-sans);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--fe-text-2);
  margin:0 0 var(--space-3) var(--space-3);}
.app-content{min-width:0;}
/* narrow: the rail stacks above the screen and the steps flow as a wrapped row
   of title-only chips (the base .stepper__step is width:100%, so it must shrink
   here or every step wraps to its own line) */
@media (max-width:720px){
  .app-layout{grid-template-columns:1fr;gap:var(--space-5);}
  .stepper-rail{position:static;}
  .stepper-rail .stepper{flex-direction:row;flex-wrap:wrap;gap:var(--space-1);}
  .stepper-rail .stepper__step{width:auto;flex:0 1 auto;padding:var(--space-2);}
  .stepper-rail .stepper__sub{display:none;}
}
