/* GENERATED FROM _src/ — do not edit by hand (TZ-16). */
@layer components{
/* TZ-16 base — self-hosted @font-face + global box-sizing reset.
   Concatenated FIRST so faces register and the reset applies before any
   component rules. */
/* ── @font-face: IBM Plex Sans + Mono (self-hosted woff2 served by
   font_woff2() handler in src/main.rs). ───────────────────────────── */
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:optional;src:url('/fonts/ibm-plex-sans-latin-400-normal.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:optional;src:url('/fonts/ibm-plex-sans-latin-500-normal.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:optional;src:url('/fonts/ibm-plex-sans-latin-600-normal.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:700;font-display:optional;src:url('/fonts/ibm-plex-sans-latin-700-normal.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:optional;src:url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:optional;src:url('/fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:600;font-display:optional;src:url('/fonts/ibm-plex-mono-latin-600-normal.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:700;font-display:optional;src:url('/fonts/ibm-plex-mono-latin-700-normal.woff2') format('woff2')}

/* ── Box-sizing reset ───────────────────────────────────────────────── */
*,*::before,*::after{box-sizing:border-box}
/* TZ-16 atom · button — .wf-btn base + variants (--ghost/accent/danger/sm)
   + copied state. (:focus-visible and .is-loading live in utilities/.) */
.wf-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-1_5);border:1.5px solid var(--ink);background:var(--ink);color:var(--paper);font-family:var(--font-mono);font-size:var(--ds-eyebrow);font-weight:var(--font-semibold);text-transform:uppercase;letter-spacing:.08em;padding:var(--space-2) var(--space-3_5);min-height:32px;cursor:pointer;text-decoration:none;border-radius:0;transition:background .12s}
.wf-btn:hover{background:var(--ink-2)}
.wf-btn:disabled,.wf-btn.is-disabled,.wf-btn[aria-disabled="true"]{opacity:.5;cursor:default;pointer-events:none}
.wf-btn--ghost{background:var(--paper);color:var(--ink)}
.wf-btn--ghost:hover{background:var(--paper-2)}
/* TZ-14 A-1 — primary CTA contrast. Old ink-on-var(--accent) was 4.1:1 — fails
   WCAG AA for the 11px-bold label (needs 4.5:1). Darker amber-700 var(--color-amber-700)
   + paper text = 5.1:1 (pass). The --accent token (nav-active bg, focus
   ring, bonus chip — all large/UI surfaces) is left untouched; only the
   button surface changes. Hover → amber-800 var(--color-amber-800) (still ≥AA on paper). */
/* ТЗ-23 B-05: use --accent-btn token (= amber-700) instead of raw palette
   reference. Hover via color-mix so any future shift in --accent-btn
   automatically updates the hover state. */
.wf-btn--accent{background:var(--accent-btn);color:var(--paper);border-color:var(--ink)}
.wf-btn--accent:hover{background:color-mix(in oklab,var(--accent-btn) 85%,var(--ink))}
.wf-btn--danger{background:var(--color-red-700);color:var(--paper);border-color:var(--color-red-700)}
.wf-btn--danger:hover{background:var(--color-red-900);border-color:var(--color-red-900)}
.wf-btn--sm{padding:var(--space-1) var(--space-2_5);min-height:24px;font-size:var(--ds-eyebrow)}
.wf-btn.copied{background:var(--color-green-50);color:var(--color-green-950);border-color:var(--color-green-700)}
/* TZ-16 atom · eyebrow — .wf-eyebrow mono-caps label. (.wf-h / .wf-pagehead
   live in organisms/page-head.css.) */
.wf-eyebrow{font-family:var(--font-mono);font-size:var(--ds-eyebrow);font-weight:var(--font-semibold);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
/* TZ-16 atom · glyph — .icon (inline SVG, currentColor stroke) + the
   shared icon size scale (.icon-xs/sm/md/lg). */
.icon{width:16px;height:16px;display:inline-block;vertical-align:-3px;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;fill:none;flex-shrink:0}
/* TZ-14 I-2 — shared icon size scale (was a portal-only .icon-sm). One
   ladder so the same glyph renders predictably across both shells:
   xs in small buttons, sm in md-buttons/table cells, md in sidebar nav,
   lg for empty-state glyphs. */
.icon-xs{width:12px;height:12px;vertical-align:-2px}
.icon-sm{width:14px;height:14px;vertical-align:-2px}
.icon-md{width:16px;height:16px;vertical-align:-3px}
.icon-lg{width:20px;height:20px;vertical-align:-4px}
/* TZ-16 atom · pill — .wf-pill status badge.
   Per WCAG 2.1 §1.4.1 (Use of Color), state encoded in pill color must
   be consistent across the whole app. Modifier-to-state mapping:

     --ok     (green) Fully-on / approved / posted / published / active / awaiting reply
     --warn   (amber) Partial / pending / now / maintenance / "some on"
     --err    (red)   Denied / failed / expired / disabled-by-admin (destructive)
     --info   (blue)  Informational / scheduled / queued (not yet acting)
     --muted  (outline) Off / inactive / used / closed / draft / exhausted

   `--mute` (solid neutral) deprecated 2026-05-29 — alias on top of
   `--muted` so legacy template lookups still resolve. New code MUST use
   `--muted` for inactive/disabled outline states. Material 3 chip
   semantics: https://m3.material.io/components/chips/specs
*/
.wf-pill{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-1_5);border:1.5px solid var(--ink);padding:var(--space-0_5) var(--space-2);font-family:var(--font-mono);font-size:var(--ds-eyebrow);letter-spacing:.08em;text-transform:uppercase;background:var(--paper);height:20px;white-space:nowrap;border-radius:0}
.wf-pill--ok{background:var(--color-green-50);color:var(--color-green-950);border-color:var(--color-green-700)}
.wf-pill--warn{background:var(--color-amber-50);color:var(--color-amber-900);border-color:var(--color-amber-700)}
.wf-pill--err{background:var(--color-red-50);color:var(--color-red-950);border-color:var(--color-red-700)}
.wf-pill--info{background:var(--color-blue-50);color:#0c2a52;border-color:var(--color-blue-700)}
.wf-pill--muted{color:var(--ink-3);border-color:var(--ink-3);background:var(--paper)}
/* TZ-16 molecule · form-field — label + input/textarea/select layout for
   Settings cards (.adm-form-row / .adm-form-field) + the password column
   constraint. Composes input + label atoms via element selectors. */
/* ── TZ-12 — Form layout primitives for Settings cards.
   `.adm-form-row` → 2-col grid for paired fields (URL+Store, Key+Secret …).
   `.adm-form-field--half` keeps a singleton in the left half so it doesn't
   stretch full-width.
   At < 1024px collapses to one column. ────────────────────────────────── */
.adm-form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap:var(--space-3) var(--space-4);
  margin-bottom:var(--space-3);
}
.adm-form-row:last-child { margin-bottom:0; }
.adm-form-field { display: flex; flex-direction: column; gap:var(--space-1); }
.adm-form-field--half { grid-column: 1 / 2; }
.adm-form-field--full { grid-column: 1 / -1; }
.adm-form-field label {
  font-family: var(--font-mono);
  font-size: var(--ds-eyebrow);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.adm-form-field input,
.adm-form-field textarea,
.adm-form-field select {
  width: 100%;
  border: 1.5px solid var(--ink);
  background: var(--paper);
  padding:var(--space-1_5) var(--space-2_5);
  font-family: var(--font-mono);
  font-size: var(--ds-body-sm);
  color: var(--ink);
  outline: none;
}
.adm-form-field input:focus,
.adm-form-field textarea:focus,
.adm-form-field select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
@media (max-width: 1023px) {
  .adm-form-row { grid-template-columns: 1fr; }
  .adm-form-field--half { grid-column: 1; }
}

/* Password subtab — full-width input was wasteful for 3 short fields.
   Constrain the form column itself; child inputs stay 100% of that. */
.adm-card #form-password { max-width: 360px; }
/* TZ-16 molecule · switch — .adm-switch two-tone Off/On toggle (pure-CSS
   :checked ~ sibling state). Composes the mono type token + ink/paper. */
/* ── .adm-switch (designer two-tone Off/On pill — used in Users VIP +
   ApiRequests feature toggles). Pure CSS :checked ~ sibling drives the
   visual state, so optimistic UI update happens with zero JS — change
   handlers only post to server + revert input.checked on error.
   Structure required:
     <label class="adm-switch">
       <input type=checkbox>          ← MUST be first child (sibling target)
       <span class="adm-switch-half--off">Off</span>
       <span class="adm-switch-half--on">On</span>
     </label>
*/
.adm-switch{position:relative;display:inline-flex;border:1.5px solid var(--ink);background:var(--paper-2);height:24px;font-family:var(--font-mono);font-size:var(--ds-eyebrow);font-weight:var(--font-bold);text-transform:uppercase;letter-spacing:.12em;cursor:pointer;user-select:none}
.adm-switch-half{display:inline-flex;align-items:center;padding:0 var(--space-2);color:var(--ink-3)}
.adm-switch input:not(:checked) ~ .adm-switch-half--off{background:var(--ink);color:var(--paper)}
.adm-switch input:checked ~ .adm-switch-half--on{background:var(--accent);color:var(--ink)}
/* TZ-16 organism · interstitial — secure-context «use Tor Browser» overlay
   (TZ-Onion). Shown only when <html data-insecure> is set by the head
   script (non-secure context). */
/* ── TZ-Onion — secure-context interstitial ──────────────────────────────
   Shown only when the page is NOT a secure context — e.g. http://<addr>.onion
   opened in a NON-Tor browser (regular Firefox + SOCKS). There Web Crypto
   (crypto.subtle) is unavailable so login/encryption can't work. A head
   script sets `data-insecure` on <html> before body parse; this hides the
   app and shows the «use Tor Browser» prompt with no flash. Secure contexts
   (https clearnet, or .onion in Tor Browser) never trigger it. */
#insecure-ctx-stub { display: none; }
html[data-insecure] body > :not(#insecure-ctx-stub) { display: none !important; }
html[data-insecure] #insecure-ctx-stub {
  display: flex; position: fixed; inset: 0; z-index: 99999;
  align-items: center; justify-content: center;
  background: var(--paper); padding:var(--space-6); overflow: auto;
}
.ictx-card {
  max-width: 560px; border: 1.5px solid var(--ink); background: var(--paper);
  box-shadow: var(--shadow-lg); padding:var(--space-6) var(--space-7);
  font-family: var(--font-sans); color: var(--ink);
}
.ictx-eyebrow {
  font-family: var(--font-mono); font-size: var(--ds-eyebrow); font-weight:var(--font-semibold);
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); margin-bottom:var(--space-2);
}
.ictx-h { font-size: var(--ds-page); font-weight:var(--font-bold); letter-spacing: -.01em; margin:0 0 var(--space-3); }
.ictx-p { font-size: var(--ds-body); line-height: 1.5; color: var(--ink-2); margin:0 0 var(--space-3); }
.ictx-links { margin:0; padding-left:var(--space-5); display: flex; flex-direction: column; gap:var(--space-1_5); }
.ictx-links a { color: var(--ink); font-weight:var(--font-semibold); text-decoration: underline; }
/* TZ-16 organism · page-head — .wf-pagehead (H1 + eyebrow + right slot)
   and the TZ-9 .eb-bonus chip that swaps in for the eyebrow row. The
   .wf-eyebrow atom lives in atoms/eyebrow.css. */
/* ── .wf-eyebrow + .wf-h + .wf-pagehead (designer canonical page-head
   pattern). Used by portal pg-* in app_shell + admin views via this
   shared primitive. Shape per /tmp/redesign/wireframes-admin.css line 103. */
.wf-h{font-family:var(--font-sans);font-weight:var(--font-bold);font-size:var(--ds-page-lg);line-height:1;letter-spacing:-.01em;color:var(--ink);margin:0}
/* ТЗ-23 B-07 partial revert (regression — pulled tall right-slot content
   like history's filter-form + deposit btn ABOVE the eyebrow line).
   Designer's intent (BALANCE baseline-aligned with H1) only works when
   the right slot is the simple `.wf-balance` chip used on Search/Home
   /Support; History / Wallet / Wallet-Invoice have a multi-child right
   slot whose tallest baseline anchored above the eyebrow.
   Keeping the margin-bottom bump (14 → 24) from B-07; aligning back to
   flex-end which is what the original CSS used. */
.wf-pagehead{display:flex;justify-content:space-between;align-items:flex-end;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-6)}
.wf-pagehead .wf-h{margin-top:var(--space-1)}
.wf-pagehead-sub{font-size:var(--ds-helper);color:var(--ink-3);margin-top:var(--space-1)}
.wf-pagehead-r{display:flex;align-items:center;gap:var(--space-2_5);flex:none}

/* TZ-9 — page-head bonus chip. Replaces .wf-eyebrow (row 1, 22px high)
   when an active bonus exists, so H1 and sub never shift Y-position.
   Per `redesign/banner-in-pagehead.html` variant C. */
.eb-bonus{
  display:inline-flex;align-items:center;gap:var(--space-2);
  padding:var(--space-1) var(--space-2_5);height:22px;box-sizing:border-box;
  border:1.5px solid var(--color-green-700);
  background:var(--color-green-50);
  color:var(--color-green-900);
  font-family:var(--font-mono);font-size:var(--ds-eyebrow);font-weight:var(--font-semibold);
  letter-spacing:.04em;line-height:1
}
.eb-bonus-tag{
  font-size:var(--ds-eyebrow);text-transform:uppercase;letter-spacing:.14em;
  background:var(--color-green-700);color:var(--paper);
  padding:var(--space-0_5) var(--space-1_5);border-radius:0
}
.eb-bonus-msg{font-weight:var(--font-medium);color:var(--color-green-900)}
.eb-bonus-cta{
  background:var(--ink);color:var(--paper);
  padding:var(--space-0_5) var(--space-2);font-size:var(--ds-eyebrow);letter-spacing:.12em;
  text-decoration:none;text-transform:uppercase;font-weight:var(--font-bold)
}
.eb-bonus-cta:hover{background:var(--ink-2)}
.eb-bonus-x{
  margin-left:var(--space-0_5);color:var(--color-green-700);
  font-family:var(--font-mono);font-size:var(--ds-body);line-height:1;
  cursor:pointer;background:none;border:0;padding:0 var(--space-1)
}
.eb-bonus-x:hover{color:var(--ink)}
.eb-bonus-x:disabled{opacity:.5;cursor:default}
/* Profile card uses .sk-card / .sk-head / .sk-body / .sk-left /
   .sk-right from app_shell.html (skeleton-mirror). Only rules needed
   by admin live here (admin_shell.html doesn't include portal's inline
   styles) — `.profile-card .wf-label` / `.aka-list` / `.ssn-*`, plus
   the base ink-border on .profile-card in case skeleton CSS is missing. */

.profile-card{font-size:var(--ds-helper);position:relative}

.pcard-addr-list{display:block}
.pcard-addr{display:flex;gap:var(--space-2);align-items:center;padding:var(--space-0_5) 0;border-bottom:1px dashed var(--ink-3);font-size:var(--ds-helper)}
.pcard-addr:last-child{border-bottom:none}
.pcard-addr-pin{color:var(--ink-3);display:inline-flex;flex-shrink:0}
.pcard-addr-pin svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;fill:none}
.pcard-addr-text{flex:1;color:var(--ink-2);min-width:0;font-weight:var(--font-medium)}
.pcard-current{height:16px;font-size:var(--ds-eyebrow);padding:0 var(--space-1_5)}
.pcard-id{font-family:var(--font-mono);font-size:var(--ds-eyebrow);color:var(--ink-3)}

/* Local fallbacks for primitives the portal defines inline in app_shell.html
   but the admin shell does not. Scoped under .profile-card so they can't
   collide with admin's own .wf-label / .aka-list / .ssn-* overrides. */
.profile-card .wf-label{font-family:var(--font-mono);font-size:var(--ds-eyebrow);color:var(--ink-3);letter-spacing:.08em;text-transform:uppercase}
.profile-card .aka-list{list-style:none;padding-left:0;margin:var(--space-0_5) 0 0;color:var(--ink-2)}
.profile-card .aka-list li{padding:var(--space-0_5) 0;font-size:var(--ds-helper)}
.profile-card .ssn-val{font-family:var(--font-mono);letter-spacing:.04em}
.profile-card .ssn-text{color:var(--ink)}
.profile-card .ssn-not-found{display:inline-flex;align-items:center;gap:var(--space-1);color:var(--ink-3);font-family:var(--font-mono);font-size:var(--ds-helper);font-weight:var(--font-medium);letter-spacing:.04em}

/* For admin (no portal inline CSS) we still want the sk-card look on the
   admin Profile-search result cards. Minimal polyfill — wf-box look.
   The full sk-card / sk-head / sk-body / sk-left / sk-right CSS lives in
   app_shell.html inline and admin won't get it without this block. */
.profile-card.sk-card{background:var(--paper);border:1.5px solid var(--ink);box-shadow:none}
.profile-card .sk-head{padding:var(--space-3) var(--space-5);border-bottom:1.5px solid var(--ink);background:var(--paper-2);display:flex;justify-content:space-between;align-items:center;gap:var(--space-3)}
.profile-card .sk-head-l{display:flex;flex-direction:column;gap:var(--space-0_5);min-width:0}
.profile-card .sk-head-r{display:flex;align-items:center;gap:var(--space-2);flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}
.profile-card .sk-right{position:relative}
.profile-card .pcard-id-row{position:absolute;top:0;right:0;font-family:var(--font-mono);font-size:var(--ds-eyebrow);color:var(--ink-3);padding:var(--space-1) var(--space-2);border-left:1px solid var(--ink);border-bottom:1px solid var(--ink);background:var(--paper-2);white-space:nowrap}
.profile-card .sk-head .card-eyebrow{font-family:var(--font-mono);font-size:var(--ds-eyebrow);font-weight:var(--font-semibold);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);margin-bottom:0}
.profile-card .card-name{font-size:var(--ds-body-md);font-weight:var(--font-bold);text-transform:none;color:var(--ink);letter-spacing:-.01em;margin:0}
.profile-card .sk-head-r .pcard-id{margin-top:0;text-transform:none;letter-spacing:.04em;white-space:nowrap}
.profile-card .sk-body{display:flex}
.profile-card .sk-left{padding:var(--space-4) var(--space-5);min-width:260px;flex-shrink:0;border-right:1.5px solid rgba(21,20,15,.15);display:flex;flex-direction:column;gap:var(--space-3);font-size:var(--ds-helper);color:var(--ink-2)}
.profile-card .sk-right{padding:var(--space-4) var(--space-5);flex:1;display:flex;flex-direction:column;gap:var(--space-3);font-size:var(--ds-helper);color:var(--ink-2);min-width:0}
.profile-card .sk-right .wf-label{display:block;margin-bottom:var(--space-0_5)}
.profile-card .pcard-age .wf-label{margin-right:var(--space-1_5)}
.profile-card .pcard-age b{color:var(--ink)}
.profile-card .pcard-dob{color:var(--ink-3);font-family:var(--font-mono);font-size:var(--ds-eyebrow);margin-left:var(--space-1)}
@media(max-width:639px){.profile-card .sk-body{flex-direction:column}.profile-card .sk-left{min-width:0;width:100%;border-right:none;border-bottom:1.5px solid rgba(21,20,15,.15)}}
/* TZ-16 organism · region-frame — skeleton/empty container frame. */
/* ── .region-frame (Polaris SkeletonPage + web.dev CLS): outer card
   frame visible from first paint while empty; child swap removes the
   :empty paint automatically. Each call-site adds inline min-height
   matching the expected loaded content (NN/g skeleton-screens). */
.region-frame{min-height:200px;width:100%}
.region-frame:empty{background:var(--paper);border:1.5px solid var(--ink);width:100%}
/* TZ-16 template · settings-layout — 2-col card grids
   (.adm-settings-grid / .adm-grid-2 / .adm-col-pair) with equal-height
   stretch (TZ-15) + the TLS 3-card responsive collapse. */
/* ── TZ-11 + TZ-15 — Settings cards in 2 flex-columns inside one grid.
   `.adm-col` columns stack independently (gap-only). TZ-15 makes both
   columns end on the same Y line: the grid stretches its column tracks
   (align-items:stretch) and the LAST card in each `.adm-col` flex-grows
   to absorb the remaining height — so the shorter column's bottom card
   fills down to match the taller column. Content inside stays natural
   size (only the card box grows, the button/form is not inflated). At
   <1024px both columns collapse to one. ─────────────────────────────── */
.adm-settings-grid,
.adm-grid-2,
.adm-col-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap:var(--space-3);
  align-items: stretch;
}
.adm-settings-grid .adm-col,
.adm-grid-2 .adm-col,
.adm-col-pair .adm-col {
  display: flex;
  flex-direction: column;
  gap:var(--space-3);
}
/* Only the last card stretches — keeps a single-card column flush to the
   row height, and multi-card columns pin their earlier cards top-aligned. */
.adm-col > .adm-card:last-child { flex: 1; }
/* Escape hatch (TZ-15 rollback): opt a specific grid out of stretch. */
.adm-grid-2--no-stretch,
.adm-settings-grid--no-stretch { align-items: start; }
@media (max-width: 1023px) {
  .adm-settings-grid,
  .adm-grid-2,
  .adm-col-pair { grid-template-columns: 1fr; }
}

/* TLS — 3 per-host cards side-by-side (Admin / BTCPay / Client) each
   containing the toggle + domain input + cert PEM textarea + key PEM
   textarea. Collapses to one column under 1200px because each card
   needs ~380px to keep the PEM textarea readable without horizontal
   scroll on the cert content. */
.tls-host-grid { grid-template-columns: 1fr 1fr; }
@media (max-width: 1199px) {
  /* No !important needed since the columns moved off the inline style
     attribute and into the class above — plain cascade wins here. */
  .tls-host-grid { grid-template-columns: 1fr; }
}
}
@layer utilities{
/* TZ-16 utility · focus-visible (TZ-14 A-2) — global keyboard-focus ring.
   https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button:focus-visible,
.wf-btn:focus-visible,
a:focus-visible,
[role="button"]:focus-visible { outline-offset: 1px; }
/* TZ-17 T-11 (AC#2) · font-size utility classes — move inline
   font-size onto classes (zero inline style="...font-size:..."). Each
   maps 1:1 to a --ds-* token; values identical to before. */
.fs-ds-eyebrow{font-size:var(--ds-eyebrow)}
.fs-ds-helper{font-size:var(--ds-helper)}
.fs-ds-body-sm{font-size:var(--ds-body-sm)}
.fs-ds-body{font-size:var(--ds-body)}
.fs-ds-body-md{font-size:var(--ds-body-md)}
.fs-ds-section{font-size:var(--ds-section)}
/* TZ-18 T-05 · font-weight utility classes — move inline font-weight
   onto classes (zero inline style="...font-weight:..."). 1:1 with the
   --font-* tokens; values identical. */
.fw-normal{font-weight:var(--font-normal)}
.fw-medium{font-weight:var(--font-medium)}
.fw-semibold{font-weight:var(--font-semibold)}
.fw-bold{font-weight:var(--font-bold)}
/* TZ-16 utility · loading — button spinner (TZ-14 M-2) + refresh dimmer
   (TZ-14 S-3). Both share the wf-spin keyframes defined here. */
/* M-2 · Button loading state — prevents double-submit by disabling the
   button + showing an inline spinner after click. CSS spinner via
   ::after + @keyframes per MDN CSS animations:
   https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations */
.wf-btn.is-loading { pointer-events: none; opacity: .7; position: relative; }
.wf-btn.is-loading::after {
  content: '';
  width: 12px; height: 12px;
  border: 1.5px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: wf-spin .6s linear infinite;
  margin-left:var(--space-2);
}
@keyframes wf-spin { to { transform: rotate(360deg); } }
/* Respect reduced-motion: drop the spin, keep the dimmed/disabled cue. */
@media (prefers-reduced-motion: reduce) { .wf-btn.is-loading::after { animation: none; } }

/* S-3 · Refresh dimmer. First render uses the skeleton; re-fetches
   (filter / pagination / search) instead dim the EXISTING content + float
   a spinner, so the table doesn't blink back to skeleton. Add `.is-refreshing`
   to the region before fetch, remove on settle. Spinner reuses @keyframes
   wf-spin above. NN/g — keep context visible during in-place updates:
   https://www.nngroup.com/articles/progress-indicators/ */
.is-refreshing { position: relative; pointer-events: none; }
.is-refreshing > * { opacity: .45; transition: opacity .1s; }
.is-refreshing::after {
  content: '';
  position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
  width: 22px; height: 22px;
  border: 2px solid var(--ink); border-right-color: transparent;
  border-radius: 50%;
  animation: wf-spin .7s linear infinite;
  z-index: 5;
}
@media (prefers-reduced-motion: reduce) { .is-refreshing::after { animation: none; } }
/* TZ-16 utility · skeleton — .sk shimmer loading placeholder (NN/g). */
.sk{background:linear-gradient(90deg,var(--paper-2) 0%,var(--paper-3) 50%,var(--paper-2) 100%);background-size:200% 100%;animation:sk-shimmer 1.6s infinite}
.sk-cell{height:12px;display:inline-block}
@keyframes sk-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
@media(prefers-reduced-motion:reduce){.sk{animation:none;background:var(--paper-2)}}
/* TZ-19 spacing utilities — token-mapped classes for padding / margin / gap.
   Each maps 1:1 to a --space-* token; used to move inline style="padding:…"
   onto classes (zero inline spacing). Mirrors font-size.css / font-weight.css. */

/* Padding — all sides */
.p-0 { padding:var(--space-0) }
.p-px { padding:var(--space-px) }
.p-0-5 { padding:var(--space-0_5) }
.p-1 { padding:var(--space-1) }
.p-1-5 { padding:var(--space-1_5) }
.p-2 { padding:var(--space-2) }
.p-2-5 { padding:var(--space-2_5) }
.p-3 { padding:var(--space-3) }
.p-3-5 { padding:var(--space-3_5) }
.p-4 { padding:var(--space-4) }
.p-5 { padding:var(--space-5) }
.p-6 { padding:var(--space-6) }
.p-8 { padding:var(--space-8) }

/* Padding — horizontal (x) */
.px-1 { padding-left:var(--space-1); padding-right:var(--space-1) }
.px-2 { padding-left:var(--space-2); padding-right:var(--space-2) }
.px-2-5 { padding-left:var(--space-2_5); padding-right:var(--space-2_5) }
.px-3 { padding-left:var(--space-3); padding-right:var(--space-3) }
.px-3-5 { padding-left:var(--space-3_5); padding-right:var(--space-3_5) }
.px-4 { padding-left:var(--space-4); padding-right:var(--space-4) }
.px-5 { padding-left:var(--space-5); padding-right:var(--space-5) }
.px-6 { padding-left:var(--space-6); padding-right:var(--space-6) }

/* Padding — vertical (y) */
.py-1 { padding-top:var(--space-1); padding-bottom:var(--space-1) }
.py-1-5 { padding-top:var(--space-1_5); padding-bottom:var(--space-1_5) }
.py-2 { padding-top:var(--space-2); padding-bottom:var(--space-2) }
.py-2-5 { padding-top:var(--space-2_5); padding-bottom:var(--space-2_5) }
.py-3 { padding-top:var(--space-3); padding-bottom:var(--space-3) }
.py-3-5 { padding-top:var(--space-3_5); padding-bottom:var(--space-3_5) }
.py-4 { padding-top:var(--space-4); padding-bottom:var(--space-4) }
.py-5 { padding-top:var(--space-5); padding-bottom:var(--space-5) }
.py-6 { padding-top:var(--space-6); padding-bottom:var(--space-6) }

/* Margin */
.m-0 { margin:var(--space-0) }
.mb-0 { margin-bottom:var(--space-0) }
.mt-1 { margin-top:var(--space-1) }
.mt-1-5 { margin-top:var(--space-1_5) }
.mt-2 { margin-top:var(--space-2) }
.mt-3 { margin-top:var(--space-3) }
.mt-3-5 { margin-top:var(--space-3_5) }
.mt-4 { margin-top:var(--space-4) }
.mb-1 { margin-bottom:var(--space-1) }
.mb-1-5 { margin-bottom:var(--space-1_5) }
.mb-2 { margin-bottom:var(--space-2) }
.mb-3 { margin-bottom:var(--space-3) }
.mb-3-5 { margin-bottom:var(--space-3_5) }
.mb-4 { margin-bottom:var(--space-4) }

/* Gap */
.gap-px { gap:var(--space-px) }
.gap-0-5 { gap:var(--space-0_5) }
.gap-1 { gap:var(--space-1) }
.gap-1-5 { gap:var(--space-1_5) }
.gap-2 { gap:var(--space-2) }
.gap-2-5 { gap:var(--space-2_5) }
.gap-3 { gap:var(--space-3) }
.gap-3-5 { gap:var(--space-3_5) }
.gap-4 { gap:var(--space-4) }
.gap-5 { gap:var(--space-5) }
.gap-6 { gap:var(--space-6) }
/* TZ-19 T-06 · generated spacing classes — exact 1:1 replicas of the
   inline padding/margin that used to sit in style="". Faithful (same
   shorthand) so zero render change; just moves spacing out of HTML. */
.u-m-0{margin:0}
.u-m-0-0-2_5{margin:0 0 var(--space-2_5)}
.u-m-0-0-3{margin:0 0 var(--space-3)}
.u-m-0-0-3_5{margin:0 0 var(--space-3_5)}
.u-m-0-0-5rem{margin:0 0 var(--space-2)}
.u-m-2-0{margin:var(--space-2) 0}
.u-m-2-0-0{margin:var(--space-2) 0 0}
.u-m-2_5-0-0{margin:var(--space-2_5) 0 0}
.u-m-4rem-0-6rem-11rem{margin:var(--space-1_5) 0 var(--space-2_5) var(--space-4)}
.u-m-85rem-0-0{margin:var(--space-3_5) 0 0}
.u-m-px-0_5-px-0{margin:var(--space-px) var(--space-0_5) var(--space-px) 0}
.u-mb-1{margin-bottom:var(--space-1)}
.u-mb-1_5{margin-bottom:var(--space-1_5)}
.u-mb-2_5{margin-bottom:var(--space-2_5)}
.u-mb-3{margin-bottom:var(--space-3)}
.u-mb-3_5{margin-bottom:var(--space-3_5)}
.u-mb-5{margin-bottom:var(--space-5)}
.u-mb-5rem{margin-bottom:var(--space-2)}
.u-mb-75rem{margin-bottom:var(--space-3)}
.u-ml-2_5{margin-left:var(--space-2_5)}
.u-ml-auto{margin-left:auto}
.u-mt-0{margin-top:0}
.u-mt-0_5{margin-top:var(--space-0_5)}
.u-mt-1{margin-top:var(--space-1)}
.u-mt-1_5{margin-top:var(--space-1_5)}
.u-mt-2{margin-top:var(--space-2)}
.u-mt-25rem{margin-top:var(--space-1)}
.u-mt-2_5{margin-top:var(--space-2_5)}
.u-mt-3{margin-top:var(--space-3)}
.u-mt-3_5{margin-top:var(--space-3_5)}
.u-mt-4{margin-top:var(--space-4)}
.u-mt-5rem{margin-top:var(--space-2)}
.u-mt-6rem{margin-top:var(--space-2_5)}
.u-mt-85rem{margin-top:var(--space-3_5)}
.u-p-0{padding:0}
.u-p-0-1_5{padding:0 var(--space-1_5)}
.u-p-1-2_5{padding:var(--space-1) var(--space-2_5)}
.u-p-1_5-2_5{padding:var(--space-1_5) var(--space-2_5)}
.u-p-2-0{padding:var(--space-2) 0}
.u-p-2-2_5{padding:var(--space-2) var(--space-2_5)}
.u-p-2_5-3{padding:var(--space-2_5) var(--space-3)}
.u-p-2_5-3_5{padding:var(--space-2_5) var(--space-3_5)}
.u-p-2rem{padding:2rem}
.u-p-3{padding:var(--space-3)}
.u-p-3_5{padding:var(--space-3_5)}
.u-p-3_5-5{padding:var(--space-3_5) var(--space-5)}
.u-p-5{padding:var(--space-5)}
.u-p-6{padding:var(--space-6)}
.u-p-8{padding:var(--space-8)}
.u-p-px-1_5{padding:var(--space-px) var(--space-1_5)}
.u-pb-1_5{padding-bottom:var(--space-1_5)}
.u-pr-10{padding-right:var(--space-10)}
/* TZ-22 utilities · layout — display + flex/grid + composite stack/row.
   Composition layer: 1 property = 1 class. Gaps map to --space-* tokens.
   .stack-N / .row-N are the only allowed composites (vertical / horizontal
   flex+gap) — do NOT add more, or utilities become a component library. */

/* Display */
.flex   { display:flex }
.iflex  { display:inline-flex }
.grid   { display:grid }
.block  { display:block }
.iblock { display:inline-block }
.hidden { display:none }

/* Flex direction & wrap */
.flex-row    { flex-direction:row }
.flex-col    { flex-direction:column }
.flex-wrap   { flex-wrap:wrap }
.flex-nowrap { flex-wrap:nowrap }

/* Align items (cross-axis) */
.items-start    { align-items:flex-start }
.items-center   { align-items:center }
.items-end      { align-items:flex-end }
.items-baseline { align-items:baseline }
.items-stretch  { align-items:stretch }

/* Justify content (main-axis) */
.justify-start   { justify-content:flex-start }
.justify-center  { justify-content:center }
.justify-end     { justify-content:flex-end }
.justify-between { justify-content:space-between }
.justify-around  { justify-content:space-around }

/* Align self */
.self-baseline { align-self:baseline }
.self-center   { align-self:center }
.self-end      { align-self:flex-end }
.self-start    { align-self:flex-start }

/* Flex grow/shrink */
.flex-1    { flex:1 1 0% }
.flex-auto { flex:1 1 auto }
.flex-none { flex:none }

/* Grid templates */
.grid-2 { grid-template-columns:1fr 1fr }
.grid-3 { grid-template-columns:1fr 1fr 1fr }
.grid-4 { grid-template-columns:repeat(4, 1fr) }
.grid-auto-fit-180 { grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)) }
.grid-auto-fit-220 { grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)) }

/* Composite "stack" — vertical flex + gap (reads cleaner than .flex .flex-col .gap-N) */
.stack-1   { display:flex; flex-direction:column; gap:var(--space-1) }
.stack-1-5 { display:flex; flex-direction:column; gap:var(--space-1_5) }
.stack-2   { display:flex; flex-direction:column; gap:var(--space-2) }
.stack-2-5 { display:flex; flex-direction:column; gap:var(--space-2_5) }
.stack-3   { display:flex; flex-direction:column; gap:var(--space-3) }
.stack-3-5 { display:flex; flex-direction:column; gap:var(--space-3_5) }
.stack-4   { display:flex; flex-direction:column; gap:var(--space-4) }
.stack-5   { display:flex; flex-direction:column; gap:var(--space-5) }
.stack-6   { display:flex; flex-direction:column; gap:var(--space-6) }

/* Composite "row" — horizontal flex + gap + center */
.row-1   { display:flex; gap:var(--space-1); align-items:center }
.row-1-5 { display:flex; gap:var(--space-1_5); align-items:center }
.row-2   { display:flex; gap:var(--space-2); align-items:center }
.row-2-5 { display:flex; gap:var(--space-2_5); align-items:center }
.row-3   { display:flex; gap:var(--space-3); align-items:center }
.row-3-5 { display:flex; gap:var(--space-3_5); align-items:center }
.row-4   { display:flex; gap:var(--space-4); align-items:center }
/* TZ-22 utilities · surface — backgrounds + borders + shadow + radius.
   All values via tokens. NOTE (deviation from TZ-22 §3.2): the spec's
   .shadow-paper referenced a non-existent --ink-12 token and a soft blurred
   shadow; the design system is brutalist offset (radius:0, --shadow-*
   = "Npx Npx 0 var(--ink)"). Shadows here map to the real --shadow-*
   tokens; .rounded-1 (2px) dropped — system radius is 0. */

/* Background surfaces */
.surface      { background:var(--paper) }
.surface-2    { background:var(--paper-2) }
.surface-3    { background:var(--paper-3) }
.surface-ink  { background:var(--ink); color:var(--paper) }
.surface-none { background:transparent }

/* Borders — all via tokens */
.border               { border:1px solid var(--ink) }
.border-2             { border:1.5px solid var(--ink) }
.border-hairline      { border:1px solid var(--ink-4) }
.border-top           { border-top:1px solid var(--ink-4) }
.border-bottom        { border-bottom:1px solid var(--ink-4) }
.border-top-strong    { border-top:1.5px solid var(--ink) }
.border-bottom-strong { border-bottom:1.5px solid var(--ink) }
.border-bottom-dashed { border-bottom:1px dashed var(--ink-3) }
.border-left          { border-left:1px solid var(--ink-4) }
.border-right         { border-right:1px solid var(--ink-4) }
.no-border            { border:0 }

/* Shadow — brutalist offset tokens (--shadow-*) */
.shadow-sm { box-shadow:var(--shadow-sm) }
.shadow    { box-shadow:var(--shadow) }
.shadow-md { box-shadow:var(--shadow-md) }
.shadow-lg { box-shadow:var(--shadow-lg) }
.no-shadow { box-shadow:none }

/* Radius — system is sharp (--radius-none = 0) */
.rounded-0    { border-radius:var(--radius-none) }
.rounded-full { border-radius:var(--radius-full) }
/* TZ-22 utilities · sizing — width + height + max/min.
   Named widths map to the --width-* content-width scale. */

/* Width — fractions */
.w-full { width:100% }
.w-auto { width:auto }
.w-1-2  { width:50% }
.w-1-3  { width:33.333% }
.w-2-3  { width:66.667% }
.w-1-4  { width:25% }
.w-3-4  { width:75% }

/* Width — named (--width-* tokens) */
.w-narrow  { width:var(--width-narrow) }   /* 480px */
.w-reading { width:var(--width-reading) }  /* 720px */
.w-content { width:var(--width-content) }  /* 1140px */

/* Max-width */
.w-max-narrow  { max-width:var(--width-narrow) }
.w-max-reading { max-width:var(--width-reading) }
.w-max-content { max-width:var(--width-content) }
.w-max-full    { max-width:100% }

/* Height */
.h-full   { height:100% }
.h-auto   { height:auto }
.h-screen { height:100vh }

/* Min */
.min-w-0 { min-width:0 }
.min-h-0 { min-height:0 }
/* TZ-22 utilities · text — family + color + transform + align + tracking
   + line-height + whitespace + decoration. Colors via --ink/--paper/--accent
   + semantic status tokens. (font-size → font-size.css, weight →
   font-weight.css — kept separate, pre-existing.) */

/* Font family */
.font-sans { font-family:var(--font-sans) }
.font-mono { font-family:var(--font-mono) }

/* Text color */
.text-ink     { color:var(--ink) }
.text-ink-2   { color:var(--ink-2) }
.text-ink-3   { color:var(--ink-3) }
.text-ink-4   { color:var(--ink-4) }
.text-paper   { color:var(--paper) }
.text-accent  { color:var(--color-amber-700) }
.text-success { color:var(--color-green-700) }
.text-error   { color:var(--color-red-700) }
.text-link    { color:var(--color-blue-700) }

/* Text transform */
.uppercase  { text-transform:uppercase }
.lowercase  { text-transform:lowercase }
.capitalize { text-transform:capitalize }
.no-case    { text-transform:none }

/* Text alignment */
.text-left   { text-align:left }
.text-center { text-align:center }
.text-right  { text-align:right }

/* Letter-spacing */
.tracking-eyebrow { letter-spacing:0.14em }
.tracking-caps    { letter-spacing:0.08em }
.tracking-tight   { letter-spacing:var(--tracking-tight) }
.tracking-tighter { letter-spacing:var(--tracking-tighter) }
.tracking-normal  { letter-spacing:0 }
.tracking-body    { letter-spacing:-0.02em }

/* Line-height */
.lh-tight   { line-height:1 }
.lh-snug    { line-height:1.15 }
.lh-base    { line-height:1.45 }
.lh-relaxed { line-height:1.55 }

/* Whitespace + wrapping */
.nowrap         { white-space:nowrap }
.whitespace-pre { white-space:pre }
.break-all      { word-break:break-all }
.break-words    { overflow-wrap:break-word }
.truncate       { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.measure        { max-width:62ch }

/* Decoration */
.underline    { text-decoration:underline }
.no-underline { text-decoration:none }

/* Italic */
.italic     { font-style:italic }
.not-italic { font-style:normal }
/* TZ-22 utilities · position — position + inset + z-index. */

.static   { position:static }
.relative { position:relative }
.absolute { position:absolute }
.fixed    { position:fixed }
.sticky   { position:sticky }

.inset-0  { inset:0 }
.top-0    { top:0 }
.right-0  { right:0 }
.bottom-0 { bottom:0 }
.left-0   { left:0 }

.z-0   { z-index:0 }
.z-10  { z-index:10 }
.z-50  { z-index:50 }
.z-100 { z-index:100 }
/* TZ-22 utilities · overflow — overflow + visibility. */

.overflow-hidden  { overflow:hidden }
.overflow-auto    { overflow:auto }
.overflow-x-auto  { overflow-x:auto }
.overflow-y-auto  { overflow-y:auto }
.overflow-visible { overflow:visible }

.visible   { visibility:visible }
.invisible { visibility:hidden }
/* TZ-22 utilities · interaction — cursor + user-select + pointer-events. */

.cursor-pointer     { cursor:pointer }
.cursor-default     { cursor:default }

.select-none { user-select:none }
.select-all  { user-select:all }

.pointer-events-none { pointer-events:none }
/* (cursor-not-allowed / cursor-text / select-text / pointer-events-auto
   removed 2026-06-09 — zero consumers per the README "reuse >= 2" rule.) */
/* TZ-22 utilities · aspect — aspect-ratio proportions. */

/* (aspect-square / aspect-video removed 2026-06-09 — zero consumers;
   re-add per the README "reuse >= 2" rule when a second user appears.) */
/* TZ-16 utility · sr-only — visually-hidden accessibility helper. */
.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}
/* TZ-16 utility · tabular-nums (TZ-14 T-1) — fixed-width figures on data
   surfaces so digit columns stay aligned (OpenType tnum).
   https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-numeric */
.adm-tbl, .wf-tbl, .data-table, .adm-mini-tbl,
.adm-stat-val { font-variant-numeric: tabular-nums; }
/* TZ-16 utility · text — mono font helper + slashed-zero (TZ-14 T-3). */
/* ── .wf-mono (utility — force mono font-family). ─────────────────── */
.wf-mono{font-family:var(--font-mono)}

/* T-3 · Slashed zero on monospaced text so the numeral 0 is unmistakable
   from the letter O in IDs / codes / amounts. Verified the official
   IBM Plex Mono OTF (IBM/plex repo) ships the OpenType `zero` feature;
   uses the standard `slashed-zero` keyword (MDN recommends it over
   font-feature-settings) + tabular-nums to keep column alignment. Degrades
   to a plain 0 if a subset ever lacks the feature.
   https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-numeric */
.mono, .wf-mono, code, pre, kbd, samp { font-variant-numeric: slashed-zero tabular-nums; }
}
