/* ============================================================================
   Z Brand UI kit — chrome.css  ·  v3.4.2  ·  2026-11-09  ·  THE STANDARD
   One light system, every app. Serif where the brand talks, grotesque where
   the interface works. SSOT: orchestrator/docs/BRAND_IDENTITY.md v2.1 + build
   spec §8 ("The Standard in apps") in
   orchestrator/shared-memory/reference_zbrand_standard_build_spec.md.
   Source of truth: 5202307/zbrand-ui.
   Vendored copy — do not edit in the app; run `npm run sync-ui` instead.
   Every class is prefixed `zb-` so it never collides with app-local CSS.
   v2.0.0 keeps the FULL v1.0.3 class inventory — same names, new tokens:
   radius 0 everywhere, no shadows, espresso the one accent, DM Mono retired.
   v2.0.1 styles `.zb-tip-b`, the one class chrome.js emits that this file had
   no rule for, so a multi-line `data-tip` paints one line per segment.
   v2.1.0 adds the Assistant rail (section 16): .zb-nav-assistant, .zb-rail,
   .zb-scrim and the .zb-rail-open state — rendered only when a host opts in.
   v2.2.0 adds "product grade" (section 17, build spec §8.12), behind ONE
   opt-in class: <body class="zb-app zb-product">. Sections 1–16 are exactly
   v2.1.1's, so an app that does not opt in renders as it did.
   v2.2.1 gives product mode its own placeholder color (--zb-placeholder,
   17.6): through --fg4 a placeholder was ink-2, and read as a typed value.
   Only section 17 changed.
   v2.2.2 changes chrome.js (a press outside a kit menu, the ⌘K palette's
   selection) and adds one rule to section 17: a split's menu can open
   upward ([data-zb-up], set by chrome.js when there is no room below).
   v2.3.0 adds the dark theme (section 18): light stays the default, and
   <html data-theme="dark"> re-points the tokens to §2's dark ramp. The
   kit's few hand-drawn colors now read tokens whose fallback is the old
   literal, so a light page renders exactly as on v2.2.2.
   v2.4.0 adds the English / 中文 switch (section 17b): the flag button,
   its menu, Chinese type stacks and the paint hold for a Chinese page.
   v2.6.0 changes the Assistant item's rules in section 16 — the first change
   since v2.1.1 to what sections 1–16 draw: a plain nav item pinned to the
   bottom of the sidebar, directly above its foot, drawn as selected only
   while the rail is open. A page without window.ZB_ASSISTANT has no such item.
   v2.7.0 takes the bottom pin out of section 16 again: the item sits right
   under the last nav item (chrome.js) and carries no margin of its own, so
   the foot's own margin-top: auto keeps the bottom. Only the two pin rules
   go; the open state's rule is v2.6.0's.
   v3.1.0 adds one rule to section 14, for every app: on a touch phone
   (no hover, a coarse pointer) the palette's ↑↓ / ↵ / esc hint line is
   hidden — there are no keys to press. Nothing else changes.
   v3.3.0 adds section 17e, for every app: the sortable table header
   (th.zb-sort, .zb-sort-btn, .zb-sort-arrow; ZB.sortHeader in chrome.js)
   and, in its comment, the table rule. Every rule names a .zb-sort class,
   so a page without one renders exactly as on v3.2.3.
   v3.4.0 changes ONE drawing, the sidebar group head (.zb-side-head), for
   every grade and frame: 11px caps in ink-2 with air above and a hairline
   over each group after the first. §17.4 and §17d.3 keep only the column's
   inset; §5 is the head. On a Chinese page it drops its tracking, as the
   eyebrow does (17b).
   v3.4.1 fixes ONE drawing for every grade and frame, the disabled tertiary
   button: it keeps its transparent border, so it stays text and only its
   text dims. The disabled rule gave every .zb-btn the hairline, so a
   disabled tertiary looked MORE like a button than an enabled one. One rule
   beside each disabled rule (§6, §17.5, §17d.10); primary, secondary and
   danger keep the hairline.
   v3.4.2 changes no rule (chrome.js only: Ziggy's step runner — the ring
   leaves with its control, a select is set by its option, an option that
   is not there yet gets a moment).
   v3.2.3 changes no rule (chrome.js only: Ziggy reads a form control
   inside a heading, ruling D12).
   v3.2.2 adds one rule (section 16): the kit's own landing on <main>
   (data-zb-landing, chrome.js) draws no focus ring.
   v3.2.1 changes no rule (chrome.js only: keys typed in the rail stay
   in the rail; the phone drawer's Escape is marked handled).
   v3.2.0: ⌘K opens above an app's own sheets and modals. The palette gets
   its own layer, --z-palette (section 1; it was --z-modal, under every app
   sheet), and stays above the open rail (section 16). Section 1 also
   declares --zb-rail-room, the width the open rail takes at the right edge
   (380px, 448px in product grade, above 900px; 0px otherwise — sections 16
   and 17.11 set it), so an app's right-hand drawer can stop at it:
   right: var(--zb-rail-room, 0px). The kit's own toasts do (section 11).
   ========================================================================== */

/* ---------- 0 · Fonts (self-hosted since v2.4; SIL OFL — Source Serif 4 + Hanken Grotesk).
   Every app serves the faces itself from /ui/fonts/ (serveUi). Nothing loads from
   fonts.googleapis.com, which mainland China blocks (operator ruling 2026-10-01:
   the apps are read in China and the USA); an app drops its own Google Fonts
   <link>s when it re-pins. Latin subsets; Chinese falls back to the system's
   faces (section 17b). UI_HEAD preloads the two upright faces.
--------------------------------------------------------------------------- */
@font-face { font-family: "Source Serif 4"; src: url("/ui/fonts/source-serif-4-v1.woff2") format("woff2"); font-weight: 200 900; font-style: normal; font-display: swap; }
@font-face { font-family: "Source Serif 4"; src: url("/ui/fonts/source-serif-4-italic-v1.woff2") format("woff2"); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: "Hanken Grotesk"; src: url("/ui/fonts/hanken-grotesk-v1.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
/* Latin Extended for the interface face only, where people's names appear (Ł, č, ő …); fetched only by a page that shows one. */
@font-face { font-family: "Hanken Grotesk"; src: url("/ui/fonts/hanken-grotesk-ext-v1.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }

/* ---------- 1 · Tokens ----------------------------------------------------- */
:root {
  /* surfaces — paper ground, stone washes (never pure white) */
  --bg:  #FAF9F6;  --bg2: #FAF9F6;  --bg3: #ECE9E2;  --bg4: #E4E1D8;  --bg5: #DAD5C8;
  /* borders — hairline draws the page; border is control hardware */
  --border: #E2DED4;  --border2: #C9C4B8;  --border3: rgba(27,26,24,0.28);
  /* ink, 4 tiers (fg3 = taupe, the secondary-text voice) */
  --fg: #1B1A18;  --fg2: #57524A;  --fg3: #857E71;  --fg4: #A6A093;
  /* espresso = the ONE accent (eyebrows, hovers, active states). Ink = ACTION. */
  --accent: #4A3B2F;  --accent2: #3A2E24;
  --accent-tint: rgba(74,59,47,0.08);  --accent-tint2: rgba(74,59,47,0.16);
  /* semantic — tooling, not brand; text and thin washes only, never chrome */
  --danger: #B23A2E;   --danger-tint: rgba(178,58,46,0.10);
  --warn:   #B07A37;   --warn-tint:   rgba(176,122,55,0.12);
  --success:#4E7A52;   --success-tint:rgba(78,122,82,0.12);
  /* heatmap — the ONLY intensity scale for tables (espresso, 5 steps) */
  --heat-1: rgba(74,59,47,0.06); --heat-2: rgba(74,59,47,0.14);
  --heat-3: rgba(74,59,47,0.26); --heat-4: rgba(74,59,47,0.42); --heat-5: rgba(74,59,47,0.62);
  /* channel identity — chips only, never chrome */
  --ch-meta:#1877F2; --ch-google:#F4B400; --ch-tiktok:#EE1D52; --ch-pinterest:#E60023;
  --ch-klaviyo:#9333EA; --ch-email:#4A3B2F; --ch-organic:#4E7A52; --ch-geo:#5B8A72;
  /* studio mat — every product/model/scene thumbnail ground (§8.6) */
  --mat: #E0DFDB;
  /* type — serif is the brand voice (titles, sections, entity names, one aside);
     Hanken is EVERYTHING interactive. --fm survives as a variable so app-local
     rules keep resolving, but it now points at Hanken: DM Mono is retired —
     pair numeric uses with .zb-num / font-variant-numeric: tabular-nums. */
  --fp: "Source Serif 4", Georgia, "Times New Roman", serif; /* brand voice */
  --fs: "Hanken Grotesk", "Helvetica Neue", Arial, sans-serif; /* interface */
  --fm: "Hanken Grotesk", "Helvetica Neue", Arial, sans-serif; /* numbers: use tabular-nums */
  --fa: "Source Serif 4", Georgia, "Times New Roman", serif;   /* asides: italic 400 */
  /* radii — ONE scale, and the scale is zero. Variables kept so app-local
     rules referencing them resolve to The Standard's square hardware. */
  --r-xs: 0; --r-sm: 0; --r-md: 0; --r-lg: 0; --r-xl: 0; --r-2xl: 0;
  /* micro tracking */
  --track-micro: 0.16em;
  /* chrome dimensions — fixed across apps (§8.3: the bones survive) */
  --zb-top-h: 52px;  --zb-side-w: 220px;  --zb-content-max: 1400px;
  /* motion — §8.10: opacity and transform only, nothing springy */
  --zb-fast: 120ms; --zb-med: 200ms;
  /* z */
  --z-top: 30; --z-drop: 40; --z-modal: 50; --z-toast: 60; --z-tip: 70; --z-rail: 1000;
  /* v3.2.0: the ⌘K palette, above every app's own sheets and modals (the fleet's highest, 2026-10-21:
     Image Studio's 9999) and the rail; and the room the open rail takes at the right edge (section 16). */
  --z-palette: 10000;
  --zb-rail-room: 0px;
}

/* ---------- 2 · Base ------------------------------------------------------- */
.zb-app, .zb-app * { box-sizing: border-box; }
.zb-app {
  background: var(--bg); color: var(--fg);
  font-family: var(--fs); font-size: 13px; line-height: 1.5;
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
  font-variant-numeric: tabular-nums;
}
/* Links are opt-in: bare <a> inherits color (apps style anchors per component).
   Add .zb-link (or wrap prose in .zb-prose) for espresso text links. */
.zb-app :where(a) { text-decoration: none; }
.zb-app :where(a.zb-link, .zb-prose a) { color: var(--accent); }
.zb-app :where(a.zb-link, .zb-prose a):hover { color: var(--accent2); text-decoration: underline; text-underline-offset: 2px; }
/* Focus ring — 1px ink, offset 2px (§8.4). Never change radius on focus. */
.zb-app :where(:focus-visible) { outline: 1px solid var(--fg); outline-offset: 2px; }
/* Hardened ring for interactive elements. The :where() form above has ZERO specificity,
   so any app-local `outline: none` reset silently removed the ring — the QA crawler's
   focus-ring-missing class hit ~25 views on 2026-08-20. This rule outweighs those resets
   for keyboard focus. Form fields and .zb-info are excluded: they paint their own ring
   (border + outline below), and a second outline would double it. */
.zb-app :is(a, button, [role="button"], summary, [tabindex]:not(.zb-info)):not(.zb-input):not(.zb-select):not(.zb-textarea):focus-visible {
  outline: 1px solid var(--fg); outline-offset: 2px;
}
/* ZERO specificity on the whole selector — :where() must wrap .zb-app too. With a
   bare `.zb-app` prefix this reset is (0,1,0): it TIES with app-local component
   classes like `.btn-primary{color:...}` and wins on load order (chrome.css loads
   after app styles), painting ink-on-ink buttons. Found live on Loom's sign-in
   during the v2 re-skin. */
:where(.zb-app) :where(button, input, select, textarea) { font-family: inherit; font-size: inherit; color: inherit; }
.zb-app ::placeholder { color: var(--fg4); }
.zb-app ::selection { background: var(--accent-tint2); }
@media (prefers-reduced-motion: reduce) { .zb-app * { transition: none !important; animation: none !important; } }

/* Type roles — the §8.2 app ramp */
.zb-h1 { font-family: var(--fp); font-weight: 500; font-size: 27px; line-height: 1.15; letter-spacing: 0; color: var(--fg); margin: 0; text-wrap: balance; }
.zb-h2 { font-family: var(--fp); font-weight: 500; font-size: 19px; line-height: 1.25; color: var(--fg); margin: 0; }
.zb-h3 { font-family: var(--fs); font-weight: 600; font-size: 13px; color: var(--fg); margin: 0; }
.zb-eyebrow { font-family: var(--fs); font-size: 10px; font-weight: 600; letter-spacing: var(--track-micro); text-transform: uppercase; color: var(--accent); }
.zb-sub { font-size: 11.5px; color: var(--fg3); }
.zb-num { font-family: var(--fs); font-weight: 500; font-variant-numeric: tabular-nums; }
.zb-kpi { font-family: var(--fs); font-size: 24px; font-weight: 500; letter-spacing: -0.01em; color: var(--fg); line-height: 1.1; font-variant-numeric: tabular-nums; }
.zb-muted { color: var(--fg3); }
.zb-faint { color: var(--fg4); }

/* ---------- 3 · App frame -------------------------------------------------- */
.zb-frame { display: grid; grid-template-rows: var(--zb-top-h) 1fr; grid-template-columns: var(--zb-side-w) 1fr; min-height: 100vh; }
.zb-frame.zb-no-side { grid-template-columns: 1fr; }
.zb-main { min-width: 0; padding: 24px 28px 48px; }
.zb-main > .zb-container { max-width: var(--zb-content-max); }

/* ---------- 4 · Top bar ---------------------------------------------------- */
.zb-top {
  grid-column: 1 / -1; position: sticky; top: 0; z-index: var(--z-top);
  height: var(--zb-top-h); display: flex; align-items: center; gap: 12px;
  padding: 0 16px; background: var(--bg); border-bottom: 1px solid var(--border);
}
.zb-wordmark {
  display: inline-block; width: 123px; height: 20px; flex-shrink: 0;
  background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjQ3LjQ4IDYxLjI1NCA2OTguOTM0IDExMy41MjMiIGZpbGw9IiMyMzFGMjAiIGZpbGwtcnVsZT0ibm9uemVybyI+PHRpdGxlPlogQnJhbmQ8L3RpdGxlPjxwYXRoIGQ9Ik0gMzI5LjA3IDExMy44IEwgMzEzLjg0IDExMy43NiBDIDMxMi45NCAxMTMuNzYgMzEyLjExIDExMi43OSAzMTIuMTEgMTExLjgyIEwgMzEyLjExIDc3LjA4IEMgMzEyLjExIDc0LjU5IDMxMS45NSA3My44MyAzMTIuMTQgNzEuOTQgQyAzMTIuMyA3MC4zMSAzMTMuMDUgNjkuNTkgMzEzLjc2IDY4Ljg2IEMgMzE0LjQ2IDY4LjEyIDMxNS42OSA2Ny4zMSAzMTcuNiA2Ny4yOSBDIDMxOS40NSA2Ny4yNiAzMTkuOTkgNjcuMjkgMzIyLjA0IDY3LjI5IEwgMzI5LjQ1IDY3LjI5IEMgMzM1LjIgNjcuMjkgMzM5Ljc0IDY5LjE0IDM0My44NyA3My4xOCBDIDM0OC4yNiA3Ny41MiAzNTAuNDYgODMuMjQgMzUwLjQ2IDkwLjM1IEMgMzUwLjQ2IDk3LjEyIDM0OC40NCAxMDIuNjggMzQ0LjM5IDEwNy4wNCBDIDM0MC4zNSAxMTEuNCAzMzUuNDcgMTEzLjggMzI5LjA3IDExMy44IE0gMjczLjcyIDY3LjI5IEwgMjgzLjI5IDY3LjI5IEMgMjg0Ljg3IDY3LjI5IDI4Ni40NyA2Ny40MiAyODcuNjkgNjcuOTMgQyAyODguOTEgNjguNDUgMjg5LjY0IDY5LjE0IDI5MC4zOCA2OS45IEMgMjkxLjEgNzAuNjYgMjkxLjU3IDcxLjY1IDI5MS43NyA3Mi44OSBDIDI5MS45NyA3NC4xNCAyOTIuMDcgNzYuMTggMjkyLjA3IDc2LjE4IEwgMjkyLjA3IDE1Mi4yNCBDIDI5Mi4wNyAxNjAuMzYgMjkxLjkxIDE2Mi4wOSAyOTEuNTkgMTYzLjQxIEMgMjkxLjI3IDE2NC43MiAyOTAuODQgMTY2LjI3IDI4OS45MSAxNjcuMTcgQyAyODguOTggMTY4LjA3IDI4OS4xIDE2OC4wNCAyODguMTMgMTY4LjUgQyAyODYuNzMgMTY5LjE2IDI4NC45OCAxNjkuMTkgMjgzLjM2IDE2OS4xOSBMIDI3NC4xMyAxNjkuMTkgTCAyNzQuMTMgMTcyLjg1IEwgMzMwLjIzIDE3Mi44OSBMIDMzMC4yNSAxNjkuMjMgQyAzMzAuMjUgMTY5LjIzIDMxOS43IDE2OS4yMyAzMTguMTggMTY5LjE4IEMgMzE2LjIyIDE2OC45NCAzMTUuMDIgMTY4LjE5IDMxNC4wNCAxNjcuMTcgQyAzMTIuNzUgMTY1LjkgMzEyLjI2IDE2NC43IDMxMi4xNCAxNjMuMjcgQyAzMTIuMDMgMTYxLjkzIDMxMi4wMyAxNjAuNjMgMzEyLjAzIDE1OC42OCBMIDMxMi4wNyAxMTkuMzQgQyAzMTIuMDcgMTE4LjM3IDMxMi44NSAxMTcuNTggMzEzLjgxIDExNy41OCBMIDMyMy40MSAxMTcuNjEgQyAzMjkuNzYgMTE3LjU5IDMzNC40NyAxMTcuMzEgMzM2LjUgMTE5Ljg3IEMgMzM5Ljc0IDEyMi43NCAzMzkuMTQgMTQwLjA0IDM0MC42NiAxNTAuMTggQyAzNDIuMTEgMTU5Ljk1IDM0My4wNSAxNjEuNjkgMzQ0LjY4IDE2NC42MiBDIDM0Ny45OSAxNzAuNTggMzUzLjI1IDE3NC4wNSAzNjEuNTggMTc0LjA1IEMgMzY4LjI4IDE3NC4wNSAzNzAuOCAxNzEuMTQgMzc0IDE2NC4xOSBDIDM3NiAxNTkuODQgMzc2Ljk0IDE1NC42NSAzNzYuOTQgMTQ4LjA3IEMgMzc2Ljk0IDE0Ny40NCAzNzYuODkgMTQ2LjQ4IDM3Ni44IDE0NS4xOSBDIDM3Ni44IDE0NS4wOCAzNzYuNzUgMTQ0LjMzIDM3Ni41OSAxNDMuNzkgQyAzNzYuMzIgMTQyLjYyIDM3NS43MyAxNDAuOTYgMzczLjUyIDE0MS4xMSBDIDM3MS45MSAxNDEuMjEgMzcxLjEgMTQyLjU2IDM3MS4zNCAxNDMuOTIgQyAzNzEuMzQgMTQzLjk1IDM3MS4zNCAxNDMuOTYgMzcxLjM0IDE0My45OCBDIDM3MS4zNCAxNDMuOTkgMzcxLjQxIDE0NC4zNCAzNzEuNCAxNDQuMzUgQyAzNzEuNCAxNDQuMzUgMzcyLjE3IDE0Ny4wOSAzNzIuOCAxNTAuNjEgQyAzNzMuMzQgMTU1LjQzIDM3MS45NCAxNjAuOTcgMzcwLjUzIDE2My4yNyBDIDM2OS4wNyAxNjQuOTggMzY3Ljg5IDE2NS41NiAzNjYuMTYgMTY0LjkxIEMgMzY0LjIgMTY0LjE4IDM2My42MiAxNjAuMzkgMzYyLjg2IDE1Ni43NSBDIDM2Mi44NSAxNTYuNyAzNjIuODQgMTU2LjY0IDM2Mi44MyAxNTYuNTggQyAzNjIuMzEgMTUyLjU3IDM2MS44MiAxNDguNTUgMzYxLjM2IDE0NC41NCBDIDM2MC42NiAxMzkuMDQgMzYwLjQ1IDEzMy41NSAzNTYuNSAxMjMuMjYgQyAzNTQuODcgMTE5LjgzIDM1MC4zMyAxMTYuNjQgMzQ2LjE5IDExNS4xNCBDIDM0NC43IDExNC41MSAzNDMuODQgMTE0LjMzIDM0My42NiAxMTQuMTEgQyAzNDMuOTMgMTEzLjk1IDM0My45NCAxMTQuMDIgMzQ0Ljk0IDExMy43NCBDIDM1MS4xOCAxMTIuODcgMzU2LjE4IDExMS41NCAzNjAuOCAxMDcuODYgQyAzNjcuNDMgMTAyLjU1IDM2OS42MyA5OS4xMiAzNjkuNjMgODkuNzcgQyAzNjkuNjMgODEuNDkgMzY3LjQgNzMuODYgMzU4Ljk2IDY4LjcgQyAzNTEuNDUgNjQuMTIgMzQzLjg0IDYzLjY5IDMzMy4wNCA2My43MiBMIDI3My43MiA2My42OCBaIE0gMjczLjcyIDY3LjI5Ii8+PHBhdGggZD0iTSAxMzAuNjYgMTQ1LjkxIEMgMTI4LjU0IDE1MC44NSAxMjUuMjkgMTU2LjU1IDEyMi4zNiAxNTkuMzQgQyAxMTguNDUgMTYzLjA5IDExNS41NyAxNjUuMDUgMTEwLjEgMTY2Ljg4IEMgMTA0LjYzIDE2OC43MSA5NS44NiAxNjkuMjEgODkuMzIgMTY5LjIxIEwgNzQuMjYgMTY5LjIgTCAxMzYuNzMgNjcuOSBMIDEzOC43MyA2NC40NSBDIDEzOC45MyA2NC4xMSAxMzguNjggNjMuNjggMTM4LjI5IDYzLjY4IEwgNTYuMzEgNjMuNzEgTCA1NS40OSAxMDIuOTggTCA1OS4xNCAxMDIuOTggQyA1OS4xNCA5OC41IDYwLjMxIDkxLjQ2IDYxLjM2IDg3Ljc2IEMgNjIuMzkgODQuMDUgNjQuMzYgODAuMSA2Ni41NiA3Ny4zMSBDIDcxLjIgNzEuMzkgNzYuNjMgNjcuNTkgODQuNzcgNjcuMzMgQyA4OS4yIDY3LjE4IDExMi41MyA2Ny4zMSAxMTIuNTMgNjcuMzEgTCA0Ny42OSAxNzIuMTIgQyA0Ny40OCAxNzIuNDYgNDcuNzEgMTcyLjk1IDQ4LjIzIDE3Mi45NSBMIDEzNS4yMiAxNzIuOTEgTCAxMzcuOCAxMjkuODkgTCAxMzQuMDUgMTI5Ljg5IEMgMTM0LjA1IDEzNC41NiAxMzMuMDcgMTQwLjI4IDEzMC42NiAxNDUuOTEiLz48cGF0aCBkPSJNIDU3MC4wMyA2Ny4zMSBMIDU3Ny4wNSA2Ny4zMSBDIDU3OS4xNiA2Ny4zMSA1ODEuMjcgNjcuNDMgNTgyLjczIDY4LjA5IEMgNTgzLjggNjguNTcgNTg0LjIgNjguODIgNTg1LjE4IDY5LjYzIEMgNTg2LjI4IDcwLjUzIDU4Ny4wNyA3MS41OSA1ODcuNzcgNzMuNCBDIDU4OC40NiA3NS4yMSA1ODguNDYgNzcuNDEgNTg4LjQ2IDgwLjAxIEwgNTg3Ljk5IDEzNy44NyBMIDUzMC4xNCA2My42NyBMIDQ5NS43IDYzLjc0IEwgNDk1LjczIDY3LjMzIEwgNTAwLjkgNjcuMzMgQyA1MDIuNzQgNjcuMzMgNTAzLjk4IDY3LjMzIDUwNS44OSA2Ny44IEMgNTA3LjQyIDY4LjE3IDUwOS4xNiA2OS4wOCA1MTAuNTggNzAuNjUgQyA1MTEuNjEgNzEuNzggNTE0LjE1IDc0LjkxIDUxNC4xNSA3OS41OCBMIDUxNC4xNSAxNTcuNjMgQyA1MTQuMTUgMTU5LjggNTEzLjkxIDE2MS40NSA1MTMuNDQgMTYyLjU5IEMgNTEyLjk3IDE2My43MiA1MTIuMjUgMTY0Ljc4IDUxMS4yNyAxNjUuNzcgQyA1MTAuMyAxNjYuNzYgNTA5LjMyIDE2Ny41NCA1MDguMzIgMTY4LjEyIEMgNTA2LjgxIDE2OC45OSA1MDQuMzQgMTY5LjE4IDUwMi45IDE2OS4xOCBMIDQ5NS42NSAxNjkuMTggTCA0OTUuNjUgMTcyLjkyIEwgNTM5LjE1IDE3Mi45MiBMIDUzOS4xNSAxNjkuMTQgTCA1MzQuMTUgMTY5LjE0IEMgNTMyLjcgMTY5LjE0IDUyOS4yNSAxNjguODggNTI3Ljc4IDE2OC4zOCBDIDUyNi4zMSAxNjcuODcgNTI0Ljk5IDE2Ny4xMSA1MjMuODIgMTY2LjEgQyA1MjEuNjUgMTY0LjMyIDUyMC41NyAxNjEuNzggNTIwLjU3IDE1Ny45MiBMIDUyMC41NyA4My43NCBMIDU5My4zIDE3NC40IEMgNTkzLjYgMTc0Ljc4IDU5NC4yMSAxNzQuNTYgNTk0LjIxIDE3NC4wNyBMIDU5NC40NyA3Ny44OCBDIDU5NC40NyA3Ni4yOSA1OTQuNTMgNzQuNzggNTk1LjExIDczLjA5IEMgNTk1LjcgNzEuNDEgNTk2LjcxIDcwLjA1IDU5OC4xNiA2OS4wNCBDIDU5OS4xIDY4LjQxIDYwMC4xMiA2Ny45NSA2MDEuMiA2Ny42MyBDIDYwMi4yOSA2Ny4zMiA2MDMuNTYgNjcuMzEgNjA1LjEyIDY3LjMxIEwgNjEyLjc3IDY3LjMxIEwgNjEyLjc4IDYzLjcxIEwgNTcwLjAyIDYzLjcyIFogTSA1NzAuMDMgNjcuMzEiLz48cGF0aCBkPSJNIDY5Ny4xIDE1Mi45NiBDIDY5NC4zMiAxNTkuNSA2OTAuNTQgMTY0LjEyIDY4NS43NiAxNjYuODIgQyA2ODIuNyAxNjguNTUgNjgxLjg5IDE2OC42NSA2NzYuNzcgMTY5LjEgTCA2NjcuMDYgMTY5LjE5IEMgNjY1LjQ1IDE2OS4xOSA2NjQuMDggMTY5LjIxIDY2Mi41OCAxNjguOCBDIDY2MS4wOCAxNjguMzkgNjU5Ljg4IDE2Ny44NiA2NTguOTkgMTY3LjE4IEMgNjU3LjIxIDE2NS44OCA2NTYuMDYgMTY0LjIgNjU1LjU0IDE2Mi4xMyBDIDY1NSAxNjAuMDYgNjU0Ljc0IDE1Ny4zNiA2NTQuNzQgMTU0LjA0IEwgNjU0Ljc0IDc3Ljg2IEMgNjU0Ljc0IDc2LjAzIDY1NC45MiA3NC40OCA2NTUuMjkgNzMuMiBDIDY1NS45NiA3MS4wNSA2NTYuOTQgNjkuMTEgNjYwLjczIDY3LjggQyA2NjEuODcgNjcuNDMgNjYyLjg2IDY3LjMyIDY2NC41OCA2Ny4zMiBMIDY3NC4yNSA2Ny4zMiBDIDY4My40MiA2Ny4zMiA2ODkuNTEgNzEuMzYgNjk0LjUxIDc5LjQ1IEMgNjk5Ljc5IDg3LjkxIDcwMi40MyAxMDEuMTMgNzAyLjQzIDExNy41NCBDIDcwMi40MyAxMzEuODMgNzAwLjY1IDE0NC41NCA2OTcuMSAxNTIuOTYgTSA2NzMuNTcgNjMuNzEgTCA2MTYuMzYgNjMuNyBMIDYxNi4zOCA2Ny4zIEwgNjI1LjcxIDY3LjMgQyA2MjcuMDMgNjcuMzMgNjI4LjMyIDY3LjU2IDYyOS42NiA2OC4xMSBDIDYzMC45OSA2OC42NiA2MzIuMTMgNjkuNTIgNjMzLjA4IDcwLjY3IEMgNjMzLjY5IDcxLjQ0IDYzNC4xMiA3Mi4yOSA2MzQuMzcgNzMuMiBDIDYzNC42MiA3NC4xMSA2MzQuNzUgNzUuNTggNjM0Ljc1IDc3LjYgTCA2MzQuNzUgMTU5LjE0IEMgNjM0Ljc1IDE2MC4zOSA2MzQuNjIgMTYxLjU2IDYzNC4zNyAxNjIuNjQgQyA2MzQuMTIgMTYzLjcyIDYzMy42NiAxNjQuNzIgNjMzIDE2NS42NCBDIDYzMS45OSAxNjYuODQgNjMwLjYzIDE2Ny43NSA2MjguOTEgMTY4LjM4IEMgNjI3LjUyIDE2OC44NiA2MjYuMTQgMTY5LjE5IDYyNC42MyAxNjkuMTkgTCA2MTYuMzQgMTY5LjI2IEwgNjE2LjM0IDE3Mi45IEwgNjY1Ljk1IDE3Mi45NCBDIDY5My4yMyAxNzMuMTIgNjk5LjQ4IDE3MC4yIDcwOS4yNiAxNTkuMTMgQyA3MTguNTQgMTQ4LjU5IDcyMy4xOCAxMzMuMjMgNzIzLjE4IDExNy4zOSBDIDcyMy4xOCAxMDEuMzIgNzE4LjYxIDg2LjUzIDcwOC41IDc2IEMgNjk4LjgzIDY1Ljk0IDY4OC4yNyA2My43MSA2NzMuNTcgNjMuNzEiLz48cGF0aCBkPSJNIDIzOS41OCAxNjMuODYgQyAyMzQuMzggMTY4LjYzIDIzMS4xIDE2OS4xNCAyMjAuNTggMTY5LjE4IEwgMjE0LjI1IDE2OS4xOCBDIDIxMC43NSAxNjkuMTggMjA4LjAyIDE2OC40NyAyMDYuMDggMTY2LjMgQyAyMDUuMjQgMTY1LjI5IDIwNC42NCAxNjQuMTQgMjA0LjI5IDE2Mi44NCBDIDIwMy45MiAxNjEuNTQgMjAzLjc0IDE2MC4wNSAyMDMuNzQgMTU4LjM2IEwgMjAzLjc0IDExNy42MSBMIDIxNC4yNyAxMTcuNTkgQyAyMTcuMDIgMTE3LjYzIDIxOS4zOCAxMTcuNTQgMjIxLjcgMTE3LjU5IEMgMjI5LjQ5IDExNy43NyAyMzQuNCAxMTkuNDIgMjM4Ljc1IDEyMi41MSBDIDI0NC4yOCAxMjYuNDMgMjQ3LjA0IDEzNC42MyAyNDcuMDQgMTQzLjExIEMgMjQ3LjA0IDE1MS4xNiAyNDQuNSAxNTkuMzYgMjM5LjU4IDE2My44NiBNIDIwMy40MSA3OC42MyBMIDIwMy40MSA3NC44NCBDIDIwMy40MSA3My4xMSAyMDQuMDMgNzAuNiAyMDQuOTggNjkuNjQgQyAyMDUuNzkgNjguNDggMjA3LjMyIDY3LjUgMjA4LjcxIDY3LjM2IEMgMjEwLjg5IDY3LjE2IDIxMS42NSA2Ny4zNiAyMTMuMTggNjcuMzUgTCAyMjAuMSA2Ny4zNSBDIDIyNi44MiA2Ny4zNSAyMzEuNTIgNjkuNiAyMzUuODggNzMuNjIgQyAyNDAuMjQgNzcuNjQgMjQyLjQyIDgzLjg5IDI0Mi40MiA5MC4zOSBDIDI0Mi40MiA5Ny4wNyAyNDAuMTcgMTAzLjUgMjM1LjY3IDEwNy42NiBDIDIzMS4xNyAxMTEuODIgMjI2Ljg5IDExMy45IDIyMC4xNyAxMTMuOSBMIDIwMy40MSAxMTMuOSBaIE0gMTY0LjkxIDE3Mi45MSBMIDIyNy4yIDE3Mi45MSBDIDIzOC4zMiAxNzIuOTEgMjQ3LjUxIDE3MS40MiAyNTUuMDEgMTY2LjE4IEMgMjYzLjE4IDE2MC41IDI2Ny4yNyAxNTIuODIgMjY3LjI3IDE0My4xNSBDIDI2Ny4yNyAxMzQuNTQgMjYzLjcxIDEyNy41OCAyNTYuNTkgMTIyLjI5IEMgMjUwLjE1IDExNy40OCAyNDIuNDggMTE1LjA3IDIzMy41OSAxMTUuMDcgQyAyNDEuMDkgMTE0LjQ5IDI0Ny42OCAxMTEuNzkgMjUzLjM0IDEwNi45NSBDIDI1OS4wMSAxMDIuMTEgMjYxLjg1IDk2LjUgMjYxLjg1IDkwLjEgQyAyNjEuODUgODEuNTggMjU3LjkzIDc0Ljc5IDI1MC4wOSA2OS43NCBDIDI0Mi45OCA2NS4xNyAyMzQgNjMuNTggMjIzLjI4IDYzLjY5IEwgMTY0LjkyIDYzLjcxIEwgMTY0Ljk1IDY3LjMxIEwgMTc1LjU4IDY3LjI4IEMgMTc3Ljc5IDY3LjM1IDE3OS4wMSA2OCAxODAuMDEgNjguODMgQyAxODIuMTQgNzAuMTQgMTgyLjc5IDcyLjU3IDE4Mi45NiA3NC43OSBDIDE4My4wOSA3NS44NSAxODMuMTYgNzcuMjIgMTgzLjE2IDc4LjkxIEwgMTgzLjE2IDE1Ny4wOCBDIDE4My4wOCAxNTguOTUgMTgzIDE1OS45MSAxODIuNyAxNjEuNTEgQyAxODIuNDQgMTYyLjk0IDE4Mi4wNCAxNjQuNDUgMTgxLjQ1IDE2NS41IEMgMTgwLjY4IDE2Ni44MyAxNzkuNzUgMTY3LjcxIDE3OC40MyAxNjguNDEgQyAxNzcuMDQgMTY5LjE1IDE3NC4wMSAxNjkuMTggMTcyLjQyIDE2OS4xOCBMIDE2NC45IDE2OS4xOCBaIE0gMTY0LjkxIDE3Mi45MSIvPjxwYXRoIGQ9Ik0gNDQ4LjA2IDEyOS40MSBMIDQzMy45OSA4Ny4zOCBDIDQzMy45OSA4Ny4zNyA0MzMuOTcgODcuMzcgNDMzLjk3IDg3LjM4IEwgNDE2LjYxIDEyOS4zMyBDIDQxNi42MSAxMjkuMzQgNDE2LjYxIDEyOS4zNCA0MTYuNjIgMTI5LjM0IEwgNDQ4LjA1IDEyOS40MyBDIDQ0OC4wNiAxMjkuNDMgNDQ4LjA3IDEyOS40MiA0NDguMDYgMTI5LjQxIE0gMzc3LjgyIDE2OS4yMyBMIDM4My4yNSAxNjkuMjMgQyAzODQuOTIgMTY5LjIzIDM4Ny43MyAxNjguOTYgMzg5LjM5IDE2OC4xNCBDIDM5MC42NyAxNjcuNTIgMzkxLjkxIDE2Ni40OSAzOTMuMTkgMTY1LjMzIEMgMzk0LjA4IDE2NC41NiAzOTUuMDEgMTYzLjgxIDM5NS45OCAxNjIuMzkgQyAzOTYuOTYgMTYwLjk3IDM5Ny43NSAxNTkuNDQgMzk4LjM2IDE1Ny44IEwgNDM5LjA0IDYxLjY3IEMgNDM5LjIxIDYxLjI1IDQzOS44IDYxLjI1IDQzOS45OCA2MS42NyBMIDQ4MC4wNSAxNTYuMzcgQyA0ODEuMjEgMTU5LjMgNDgyLjQxIDE2MS42NSA0ODMuNjMgMTYzLjQgQyA0ODQuODUgMTY1LjE2IDQ4Ni40MSAxNjYuNjYgNDg4LjMgMTY3LjkxIEMgNDg5LjAyIDE2OC4zNSA0ODkuNTggMTY4LjU4IDQ5MC44NSAxNjguODcgQyA0OTEuOTIgMTY5LjExIDQ5My4zMyAxNjkuMTkgNDk0Ljg5IDE2OS4xOSBMIDUwMS4yNCAxNjkuMjIgQyA1MDEuMjUgMTY5LjIyIDUwMS4yNSAxNjkuMjIgNTAxLjI1IDE2OS4yMyBMIDUwMS4yOSAxNzIuODggQyA1MDEuMjkgMTcyLjg5IDUwMS4yOCAxNzIuODkgNTAxLjI3IDE3Mi44OSBMIDQ0MS4zMiAxNzIuODkgQyA0NDEuMzIgMTcyLjg5IDQ0MS4zMSAxNzIuODkgNDQxLjMxIDE3Mi44OCBMIDQ0MS4zMSAxNjkuMTcgQyA0NDEuMzEgMTY5LjE2IDQ0MS4zMiAxNjkuMTYgNDQxLjMyIDE2OS4xNiBMIDQ0OC42MyAxNjkuMTYgQyA0NTAuODYgMTY5LjE2IDQ1Mi42MiAxNjguOTMgNDUzLjkzIDE2OC40OCBDIDQ1NS4yMyAxNjguMDIgNDU2LjMzIDE2Ny4zMyA0NTcuMjIgMTY2LjQyIEMgNDU4LjM5IDE2NS4wNyA0NTkuMDMgMTYzLjU4IDQ1OS4wMyAxNjEuMjcgQyA0NTkuMDMgMTYwLjQ2IDQ1OC45NiAxNTkuOTEgNDU4Ljg4IDE1OS40NSBDIDQ1OC44MSAxNTkuMDQgNDU4LjYxIDE1OC40NSA0NTguMjUgMTU3LjY4IEwgNDQ5LjMyIDEzMy43NiBDIDQ0OS4zMiAxMzMuNzYgNDQ5LjMxIDEzMy43NSA0NDkuMyAxMzMuNzUgTCA0MTUuMTUgMTMzLjc4IEMgNDE1LjE0IDEzMy43OCA0MTUuMTQgMTMzLjc4IDQxNS4xNCAxMzMuNzkgTCA0MDQuNjEgMTU4LjcyIEMgNDA0LjI2IDE1OS41MiA0MDQuMTMgMTYwLjc3IDQwNC4wNyAxNjEuNzUgQyA0MDMuOTkgMTYzLjE5IDQwNC4zOCAxNjQuNDIgNDA0Ljk5IDE2NS40MyBDIDQwNS42IDE2Ni40NCA0MDYuMjQgMTY3LjIxIDQwNy4wMiAxNjcuNjkgQyA0MDguMTMgMTY4LjM2IDQwOS41IDE2OS4xMSA0MTAuOTcgMTY5LjE4IEMgNDEzLjQ2IDE2OS4yOSA0MjEuODYgMTY5LjE5IDQyMS44NiAxNjkuMTkgQyA0MjEuODcgMTY5LjE5IDQyMS44OCAxNjkuMiA0MjEuODggMTY5LjIgTCA0MjEuODggMTcyLjg1IEMgNDIxLjg4IDE3Mi44NSA0MjEuODcgMTcyLjg2IDQyMS44NiAxNzIuODYgTCAzNzcuODIgMTcyLjg5IFogTSAzNzcuODIgMTY5LjIzIi8+PHBhdGggZD0iTSA3NDQuNjkgMTcyLjg2IEwgNzQ0LjA5IDE2Mi43MyBDIDc0NC4wNSAxNjEuODUgNzQ0IDE2MC43NiA3NDMuOTYgMTU5LjU1IEwgNzQzLjg3IDE1OS41NSBDIDc0My41NSAxNjAuNDMgNzQzLjE4IDE2MS45NiA3NDIuODcgMTYzLjExIEwgNzQwIDE3Mi42NCBMIDczOC4xNCAxNzIuNjQgTCA3MzUuMjMgMTYyLjg0IEMgNzM1IDE2MS45MSA3MzQuNTQgMTYwLjQ4IDczNC4zMiAxNTkuNTUgTCA3MzQuMjMgMTU5LjU1IEMgNzM0LjIzIDE2MC41NCA3MzQuMTggMTYxLjU4IDczNC4xNCAxNjIuNzMgTCA3MzMuNTQgMTcyLjg2IEwgNzMxLjkxIDE3Mi44NiBMIDczMi45MSAxNTcuOTYgTCA3MzUuMzIgMTU3Ljk2IEwgNzM4LjIzIDE2Ny40OSBDIDczOC41OSAxNjguNTkgNzM4LjgyIDE2OS42MyA3MzkuMDkgMTcwLjYyIEwgNzM5LjE4IDE3MC42MiBDIDczOS40NiAxNjkuNTcgNzM5Ljc3IDE2OC4zNyA3MzkuOTYgMTY3LjYgTCA3NDIuODcgMTU3Ljk2IEwgNzQ1LjMyIDE1Ny45NiBMIDc0Ni40MSAxNzIuODYgWiBNIDczMS4yNyAxNTcuOTYgTCA3MzEuMjcgMTU5LjY2IEwgNzI3LjA5IDE1OS42NiBMIDcyNy4wOSAxNzIuODYgTCA3MjUuNDUgMTcyLjg2IEwgNzI1LjQ1IDE1OS42NiBMIDcyMS4yNyAxNTkuNjYgTCA3MjEuMjcgMTU3Ljk2IFogTSA3MzEuMjcgMTU3Ljk2Ii8+PC9zdmc+") left center / contain no-repeat;
  font-size: 0; line-height: 0; color: transparent;
}
a.zb-wordmark:hover { opacity: 0.8; text-decoration: none; }
.zb-crumb-sep { color: var(--fg4); font-size: 13px; padding: 0 2px; }
.zb-crumb { color: var(--fg2); font-size: 13px; font-weight: 500; white-space: nowrap; }
.zb-crumb.zb-active { color: var(--fg); }
.zb-crumb.zb-live { color: var(--accent); }
.zb-top-spacer { flex: 1; }
/* Top-bar tabs (small apps: ≤5 sections — Zinema, ZProfit). Tabs are caps-micro;
   active = ink + 2px espresso underline. No fills, no rounding. */
.zb-top-tabs { display: flex; gap: 4px; margin-left: 8px; align-self: stretch; }
.zb-top-tab { display: inline-flex; align-items: center; padding: 0 12px; border-bottom: 2px solid transparent; color: var(--fg2); font-size: 10px; font-weight: 600; letter-spacing: var(--track-micro); text-transform: uppercase; text-decoration: none; }
.zb-top-tab:hover { color: var(--fg); text-decoration: none; }
.zb-top-tab.zb-active { color: var(--fg); border-bottom-color: var(--accent); }
/* KPI strip in the top bar (Zephyr) */
.zb-top-stats { display: flex; gap: 18px; margin-left: 12px; }
.zb-top-stat { display: flex; flex-direction: column; line-height: 1.15; }
.zb-top-stat .zb-eyebrow { font-size: 9px; color: var(--fg3); }
.zb-top-stat .zb-num { font-size: 12px; color: var(--fg); }
/* The Loom button — icon + label, always. A 1px-bordered caps-micro pill (§8.3). */
.zb-loom {
  height: 30px; padding: 0 11px 0 9px; display: inline-flex; align-items: center; gap: 7px;
  background: transparent; border: 1px solid var(--border2); border-radius: 0;
  color: var(--fg2); font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  text-decoration: none; flex-shrink: 0;
  transition: border-color var(--zb-fast), color var(--zb-fast);
}
.zb-loom:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }
.zb-loom svg { width: 14px; height: 14px; }
/* Avatar — an ink SQUARE, serif initial. No circular avatars (§8.6). */
.zb-avatar {
  width: 28px; height: 28px; border-radius: 0; display: inline-flex; align-items: center; justify-content: center;
  background: var(--fg); color: var(--bg); font-family: var(--fp); font-size: 12px; font-weight: 600; flex-shrink: 0;
  border: 0; cursor: pointer;
}
.zb-tenant { display: inline-flex; align-items: center; gap: 7px; height: 30px; padding: 0 11px; border: 1px solid var(--border2); border-radius: 0; background: var(--bg); font-size: 12px; color: var(--fg2); }
.zb-tenant .zb-dot { width: 6px; height: 6px; border-radius: 0; background: var(--success); }

/* ---------- 5 · Sidebar ---------------------------------------------------- */
.zb-side {
  background: var(--bg); border-right: 1px solid var(--border);
  padding: 12px 0; display: flex; flex-direction: column; gap: 1px;
  position: sticky; top: var(--zb-top-h); height: calc(100vh - var(--zb-top-h)); overflow-y: auto;
}
/* The group head (v3.4.0, every grade and frame): 11px caps in ink-2, the
   product eyebrow's voice, with air above and a hairline over each group after
   the first, so a head reads as a label and never as an item. The head carries
   its own rule, so a flat sidebar and one that wraps each group (Inventory
   Planner's .nav-group) draw the same. The first head of the sidebar or the
   More sheet has no rule and no air, whether it is a direct child or the first
   child of a first group wrapper; right under a .zb-side-cta it keeps its air
   but draws no rule (the button is not a group). */
.zb-side-head { margin-top: 14px; padding: 16px 14px 6px; border-top: 1px solid var(--border); font-family: var(--fs); font-size: 11px; font-weight: 600; line-height: 1.2; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg3); }
:is(.zb-side, .zb-more) > .zb-side-head:first-child,
:is(.zb-side, .zb-more) > :first-child > .zb-side-head:first-child { margin-top: 0; padding-top: 2px; border-top: 0; }
:is(.zb-side, .zb-more) > .zb-side-cta:first-child + .zb-side-head { margin-top: 0; border-top: 0; }
/* Items 34px; active = stone wash + ink at 600 + a 2px espresso left rule (§8.3).
   The left rule rides every item (transparent) so activation never shifts text. */
.zb-nav {
  display: flex; align-items: center; gap: 10px; height: 34px; padding: 0 14px 0 12px;
  border-left: 2px solid transparent; border-radius: 0;
  color: var(--fg2); font-size: 13px; cursor: pointer; text-decoration: none; border-top: 0; border-right: 0; border-bottom: 0; background: transparent; width: 100%; text-align: left;
  transition: background var(--zb-fast), color var(--zb-fast);
}
.zb-nav:hover { background: var(--bg3); color: var(--fg); text-decoration: none; }
.zb-nav.zb-active { background: var(--bg3); color: var(--fg); font-weight: 600; border-left-color: var(--accent); }
.zb-nav svg { width: 16px; height: 16px; stroke-width: 2; flex-shrink: 0; color: currentColor; }
.zb-nav-badge { margin-left: auto; font-family: var(--fs); font-size: 11px; font-variant-numeric: tabular-nums; color: var(--fg3); background: var(--bg3); padding: 1px 6px; border-radius: 0; }
.zb-nav.zb-active .zb-nav-badge { background: var(--accent-tint2); color: var(--zb-badge-active, var(--accent)); }
.zb-side-foot { margin-top: auto; padding: 12px 14px 4px; font-family: var(--fs); font-size: 10px; color: var(--fg4); }
.zb-side-cta { margin: 4px 12px 8px; }
/* Collapsible sidebar group */
.zb-side-group[data-collapsed="true"] > .zb-nav { display: none; }
.zb-side-group > .zb-side-head { cursor: pointer; user-select: none; }
.zb-side-group > .zb-side-head::after { content: "▾"; float: right; opacity: 0.6; }
.zb-side-group[data-collapsed="true"] > .zb-side-head::after { content: "▸"; }

/* ---------- 6 · Buttons ---------------------------------------------------- */
/* Hierarchy: ONE primary per region (ruling 2026-10-29): one in the title row,
   one more in a separate region (a side panel, a dialog), never two in one
   region; a list's rows are one region. primary = ink fill. secondary = 1px ink
   outline. tertiary = text. hover = espresso. Labels are caps-micro verbs
   carrying the count when there is one ("Generate 4"). 44px; 40px floor in
   dense toolbars via .zb-btn-sm (§8.4). */
.zb-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 44px; padding: 0 20px; border-radius: 0;
  font-family: var(--fs); font-size: 11px; font-weight: 600; letter-spacing: var(--track-micro); text-transform: uppercase; line-height: 1; white-space: nowrap;
  border: 1px solid transparent; cursor: pointer; text-decoration: none; user-select: none;
  transition: background var(--zb-fast), border-color var(--zb-fast), color var(--zb-fast), opacity var(--zb-fast);
}
.zb-btn:hover { text-decoration: none; }
/* Disabled = hairline border + border-colored text — never an opacity fog
   over a live-looking control (§8.4). A tertiary is text with no border, so
   disabled it stays text: its border stays transparent and only its text
   dims (v3.4.1) — a hairline would make it look MORE like a button disabled
   than enabled. */
.zb-btn:disabled, .zb-btn[aria-disabled="true"] { background: transparent; color: var(--border2); border-color: var(--border); cursor: not-allowed; pointer-events: none; }
.zb-btn-tertiary:disabled, .zb-btn-tertiary[aria-disabled="true"] { border-color: transparent; }
.zb-btn svg { width: 14px; height: 14px; }
.zb-btn-primary { background: var(--fg); color: var(--bg); border-color: var(--fg); }
.zb-btn-primary:hover { background: var(--accent); border-color: var(--accent); }
.zb-btn-secondary { background: transparent; color: var(--fg); border-color: var(--fg); }
.zb-btn-secondary:hover { color: var(--accent); border-color: var(--accent); }
.zb-btn-tertiary { background: transparent; color: var(--fg2); border-color: transparent; padding: 0 10px; }
.zb-btn-tertiary:hover { color: var(--accent); }
.zb-btn-danger { background: transparent; color: var(--danger); border-color: var(--danger); }
.zb-btn-danger:hover { background: var(--danger-tint); }
.zb-btn-sm { height: 40px; padding: 0 14px; font-size: 10px; border-radius: 0; }
.zb-btn-sm.zb-btn-tertiary { padding: 0 8px; }
.zb-btn-lg { height: 44px; padding: 0 26px; }
.zb-btn-icon { width: 44px; padding: 0; }
.zb-btn-sm.zb-btn-icon { width: 40px; }
.zb-btn-block { width: 100%; }
/* Icon-only buttons MUST carry aria-label; kbd hint slot */
.zb-btn .zb-kbd { margin-left: 4px; }
.zb-btn-primary .zb-kbd { background: var(--zb-on-primary-wash, rgba(250,249,246,0.18)); border-color: transparent; color: var(--bg); }

/* ---------- 7 · Controls --------------------------------------------------- */
/* Inputs 38px, 1px border, transparent fill; focus ring 1px ink offset 2px (§8.4). */
.zb-input, .zb-select, .zb-textarea {
  height: 38px; padding: 0 12px; width: auto; min-width: 0;
  background: transparent; border: 1px solid var(--border2); border-radius: 0;
  color: var(--fg); font-family: var(--fs); font-size: 13px;
  transition: border-color var(--zb-fast);
}
.zb-textarea { height: auto; min-height: 84px; padding: 9px 12px; line-height: 1.5; resize: vertical; }
/* Controls size to their context: inline (toolbars, filter rows) they hug content;
   inside a .zb-field or .zb-search they fill the column. */
.zb-field > .zb-input, .zb-field > .zb-select, .zb-field > .zb-textarea, .zb-search > .zb-input, .zb-textarea { width: 100%; }
.zb-select { min-width: 96px; }
.zb-input:hover, .zb-select:hover, .zb-textarea:hover { border-color: var(--border3); }
.zb-input:focus, .zb-select:focus, .zb-textarea:focus { border-color: var(--fg); outline: 1px solid var(--fg); outline-offset: 2px; box-shadow: none; }
.zb-input.zb-mono { font-family: var(--fs); font-variant-numeric: tabular-nums; }
.zb-input.zb-sm, .zb-select.zb-sm { height: 32px; font-size: 12px; padding: 0 10px; }
.zb-select {
  appearance: none; -webkit-appearance: none; padding-right: 30px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23857E71' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center;
}
.zb-input[type="date"] { font-family: var(--fs); font-size: 12px; font-variant-numeric: tabular-nums; }
.zb-input[type="date"]::-webkit-calendar-picker-indicator { opacity: 0.55; cursor: pointer; }
.zb-label { display: block; font-size: 12px; font-weight: 500; color: var(--fg2); margin-bottom: 6px; }
.zb-hint { display: block; font-size: 11.5px; color: var(--fg3); margin-top: 5px; line-height: 1.5; }
.zb-field { display: flex; flex-direction: column; }
.zb-field + .zb-field { margin-top: 14px; }
.zb-check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--fg2); cursor: pointer; }
.zb-check input { width: 15px; height: 15px; accent-color: var(--accent); margin: 0; }
.zb-search { position: relative; }
.zb-search .zb-input { padding-left: 32px; }
.zb-search::before {
  content: ""; position: absolute; left: 11px; top: 50%; width: 13px; height: 13px; transform: translateY(-50%);
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23857E71' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* ---------- 8 · Chips · segmented · filter pills --------------------------- */
/* Chips and filters: 30px, 1px border, square; active = ink fill, paper text (§8.4).
   Small flat sets ONLY (≤6). More than that, or per-item metadata → rows / pills. */
.zb-chip {
  display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 11px;
  border-radius: 0; background: transparent; border: 1px solid var(--border2);
  color: var(--fg2); font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; white-space: nowrap; cursor: default;
}
.zb-chip.zb-mono { font-family: var(--fs); font-variant-numeric: tabular-nums; letter-spacing: 0.06em; }
button.zb-chip, a.zb-chip { cursor: pointer; transition: background var(--zb-fast), color var(--zb-fast), border-color var(--zb-fast); }
button.zb-chip:hover { color: var(--accent); border-color: var(--accent); }
.zb-chip.zb-on { background: var(--fg); border-color: var(--fg); color: var(--bg); }
.zb-chip.zb-success { background: var(--success-tint); border-color: transparent; color: var(--success); }
.zb-chip.zb-warn { background: var(--warn-tint); border-color: transparent; color: var(--warn); }
.zb-chip.zb-danger { background: var(--danger-tint); border-color: transparent; color: var(--danger); }
.zb-chip .zb-dot { width: 5px; height: 5px; border-radius: 0; background: currentColor; }
/* Segmented control — bordered squares; the on segment is ink (§8.4). */
.zb-seg { display: inline-flex; gap: 0; padding: 0; background: transparent; border: 1px solid var(--border2); border-radius: 0; }
.zb-seg-btn { height: 30px; padding: 0 12px; border: 0; border-radius: 0; background: transparent; color: var(--fg3); font-size: 12px; font-weight: 500; cursor: pointer; transition: background var(--zb-fast), color var(--zb-fast); }
.zb-seg-btn:hover { color: var(--fg); }
.zb-seg-btn.zb-on { background: var(--fg); color: var(--bg); box-shadow: none; }
/* Filter bar: one row of pills; each pill is a dropdown; "Clear" at the end */
.zb-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.zb-pill {
  display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 10px 0 12px;
  border-radius: 0; background: transparent; border: 1px solid var(--border2); color: var(--fg2);
  font-size: 12px; cursor: pointer; transition: border-color var(--zb-fast), background var(--zb-fast), color var(--zb-fast); position: relative;
}
.zb-pill:hover { border-color: var(--border3); color: var(--fg); }
.zb-pill .zb-pill-k { color: var(--fg3); }
.zb-pill .zb-pill-v { color: var(--fg); font-weight: 500; }
.zb-pill.zb-on { border-color: var(--fg); background: var(--fg); color: var(--bg); }
.zb-pill.zb-on .zb-pill-k { color: var(--zb-on-primary-2, rgba(250,249,246,0.65)); }
.zb-pill.zb-on .zb-pill-v { color: var(--bg); }
.zb-pill::after { content: "▾"; font-size: 10px; color: var(--fg4); }
.zb-pill.zb-on::after { color: var(--zb-on-primary-2, rgba(250,249,246,0.65)); }
.zb-pill-menu {
  position: absolute; top: calc(100% + 4px); left: 0; min-width: 160px; z-index: var(--z-drop);
  background: var(--bg); border: 1px solid var(--border2); border-radius: 0; padding: 4px;
  box-shadow: none;
}
.zb-pill-menu[hidden] { display: none; }
.zb-pill-item { display: flex; align-items: center; gap: 8px; padding: 7px 9px; border-radius: 0; font-size: 12px; color: var(--fg2); cursor: pointer; }
.zb-pill-item:hover { background: var(--bg3); color: var(--fg); }
.zb-pill-item.zb-on { background: var(--bg3); color: var(--fg); font-weight: 600; }
.zb-filters .zb-clear { margin-left: 2px; }

/* ---------- 9 · Cards · tables --------------------------------------------- */
/* Hairline rules draw the page, not boxed cards; stone washes group (§8.7). */
.zb-card { background: var(--bg); border: 1px solid var(--border); border-radius: 0; padding: 16px; }
.zb-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.zb-card-title { font-size: 13px; font-weight: 600; color: var(--fg); }
.zb-card.zb-inset { background: var(--bg3); border-color: transparent; }
.zb-divider { height: 1px; background: var(--border); border: 0; margin: 16px 0; }
.zb-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.zb-table th { text-align: left; padding: 8px 10px; font-family: var(--fs); font-size: 10px; font-weight: 600; letter-spacing: var(--track-micro); text-transform: uppercase; color: var(--fg3); border-bottom: 1px solid var(--border2); white-space: nowrap; }
.zb-table td { padding: 9px 10px; border-bottom: 1px solid var(--border); color: var(--fg2); vertical-align: middle; }
.zb-table tr:hover td { background: var(--bg3); }
.zb-table .zb-num, .zb-table td.zb-r, .zb-table th.zb-r { text-align: right; }
.zb-table td.zb-num { font-family: var(--fs); font-weight: 500; color: var(--fg); font-variant-numeric: tabular-nums; }
.zb-table-wrap { overflow-x: auto; }
.zb-up { color: var(--success); } .zb-down { color: var(--danger); } .zb-flat { color: var(--fg4); }
/* KPI tile */
.zb-kpi-tile { background: var(--bg); border: 1px solid var(--border); border-radius: 0; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; }
.zb-kpi-tile .zb-kpi-label { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--fg3); }
.zb-kpi-tile .zb-kpi-foot { font-family: var(--fs); font-size: 10.5px; color: var(--fg4); }

/* ---------- 10 · Tooltip · info · help panel ------------------------------- */
/* Usage: <span data-tip="Plain-English definition">…</span> or <i class="zb-info" data-tip-key="iroas"></i>
   chrome.js renders the bubble on hover AND keyboard focus, and gives every .zb-info tabindex=0 + an aria-label. */
.zb-info {
  display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; border-radius: 0;
  border: 1px solid var(--fg4); color: var(--fg3); font-family: var(--fs); font-size: 9px; font-style: normal; font-weight: 600;
  cursor: help; vertical-align: -2px; margin-left: 4px; transition: border-color var(--zb-fast), color var(--zb-fast); flex-shrink: 0;
}
.zb-info::before { content: "i"; }
.zb-info:hover, .zb-info:focus-visible { border-color: var(--accent); color: var(--accent); outline: 1px solid var(--accent); outline-offset: 2px; box-shadow: none; }
[data-tip] { cursor: help; }
[data-tip].zb-tip-inline { text-decoration: underline dotted var(--fg4); text-underline-offset: 3px; }
.zb-tip {
  position: fixed; z-index: var(--z-tip); max-width: 280px; padding: 9px 11px;
  background: var(--fg); color: var(--bg); font-family: var(--fs); font-size: 12px; line-height: 1.45; font-weight: 400;
  border-radius: 0; box-shadow: none; pointer-events: none;
  opacity: 0; transform: translateY(2px); transition: opacity var(--zb-fast), transform var(--zb-fast);
}
.zb-tip.zb-show { opacity: 1; transform: none; }
.zb-tip .zb-tip-t { display: block; font-weight: 600; margin-bottom: 2px; }
/* The body honors the newlines the app wrote into `data-tip` — a two-row
   breakdown moved off `title=` (which renders them) must not collapse into one
   run-on sentence. `pre-line` and not `pre`: a tip authored inside an indented
   template literal must not paint its source indentation as leading spaces, and
   the bubble must still wrap at its 280px max-width. */
.zb-tip .zb-tip-b { white-space: pre-line; }
.zb-tip .zb-tip-m { display: block; font-size: 10.5px; opacity: 0.7; margin-top: 4px; }
/* "How this works" — collapsible, remembered per app+key in localStorage by chrome.js */
.zb-help { background: var(--bg); border: 1px solid var(--border); border-radius: 0; padding: 14px 16px; }
.zb-help[data-collapsed="true"] .zb-help-body { display: none; }
.zb-help-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.zb-help-title { font-size: 13px; font-weight: 600; color: var(--fg); }
.zb-help-body { margin-top: 8px; font-size: 13px; color: var(--fg2); line-height: 1.55; }
.zb-help-body ol { margin: 6px 0 0; padding-left: 0; list-style: none; counter-reset: zbh; }
.zb-help-body ol li { position: relative; padding-left: 26px; margin: 6px 0; counter-increment: zbh; }
.zb-help-body ol li::before { content: counter(zbh); position: absolute; left: 0; top: 1px; width: 17px; height: 17px; border-radius: 0; background: var(--bg3); border: 1px solid var(--border2); font-family: var(--fs); font-size: 10px; display: flex; align-items: center; justify-content: center; color: var(--fg3); }
.zb-help-body b { color: var(--fg); font-weight: 600; }

/* ---------- 11 · States: skeleton · empty · error · toast ------------------ */
@keyframes zb-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
.zb-skel { position: relative; overflow: hidden; border-radius: 0; background: var(--bg3); color: transparent !important; pointer-events: none; }
.zb-skel::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent 0%, var(--zb-shimmer, rgba(250,249,246,0.65)) 50%, transparent 100%); background-size: 200% 100%; animation: zb-shimmer 1.4s ease-in-out infinite; }
.zb-skel-line { height: 12px; width: 60%; margin: 6px 0; }
.zb-skel-line.zb-w40 { width: 40%; } .zb-skel-line.zb-w80 { width: 80%; } .zb-skel-line.zb-w100 { width: 100%; }
.zb-skel-kpi { height: 24px; width: 72px; }
.zb-skel-row { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 12px; padding: 9px 10px; border-bottom: 1px solid var(--border); }
.zb-skel-row .zb-skel { height: 11px; margin: 0; }
.zb-skel-card { height: 120px; border-radius: 0; }
/* Empty states: one plain sentence plus at most one action, meta type (§8.8). */
.zb-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 44px 24px; gap: 6px; }
.zb-empty .zb-empty-title { font-size: 13px; font-weight: 500; color: var(--fg2); }
.zb-empty .zb-empty-body { font-size: 11.5px; color: var(--fg3); max-width: 46ch; line-height: 1.5; }
.zb-empty .zb-empty-actions { display: flex; gap: 8px; margin-top: 10px; }
.zb-empty.zb-quiet { padding: 24px 16px; }
.zb-empty.zb-quiet .zb-empty-title { font-size: 12px; font-weight: 500; color: var(--fg3); }
.zb-alert { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border-radius: 0; border: 1px solid var(--border); background: var(--bg); font-size: 12px; color: var(--fg2); line-height: 1.5; }
.zb-alert.zb-warn { background: var(--warn-tint); border-color: transparent; color: var(--warn); }
.zb-alert.zb-danger { background: var(--danger-tint); border-color: transparent; color: var(--danger); }
.zb-alert.zb-success { background: var(--success-tint); border-color: transparent; color: var(--success); }
.zb-alert b { font-weight: 600; }
.zb-alert .zb-alert-fix { margin-left: auto; white-space: nowrap; }
.zb-toasts { position: fixed; right: calc(16px + var(--zb-rail-room, 0px)); bottom: 16px; z-index: var(--z-toast); display: flex; flex-direction: column; gap: 8px; }
.zb-toast { min-width: 220px; max-width: 360px; padding: 11px 13px; background: var(--fg); color: var(--bg); border-radius: 0; font-size: 12px; box-shadow: none; display: flex; align-items: center; gap: 10px; animation: zb-toast-in var(--zb-med) ease-out; }
.zb-toast.zb-danger { background: var(--danger); }
.zb-toast.zb-success { background: var(--success); }
@keyframes zb-toast-in { from { opacity: 0; transform: translateY(6px); } }
.zb-status { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--fg3); }
.zb-status .zb-dot { width: 6px; height: 6px; border-radius: 0; background: var(--fg4); }
.zb-status.zb-live .zb-dot { background: var(--success); } .zb-status.zb-beta .zb-dot { background: var(--warn); } .zb-status.zb-dev .zb-dot { background: var(--fg4); }

/* ---------- 12 · Image frame ---------------------------------------------- */
/* Always reserve the box; never let a card collapse while its image loads.
   Every thumbnail sits on the studio mat, square corners — including people (§8.6). */
.zb-img { position: relative; display: block; overflow: hidden; background: var(--mat); border-radius: 0; aspect-ratio: 4 / 5; }
.zb-img.zb-square { aspect-ratio: 1 / 1; } .zb-img.zb-wide { aspect-ratio: 16 / 10; } .zb-img.zb-tall { aspect-ratio: 3 / 4; }
.zb-img > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity var(--zb-med); }
.zb-img > img.zb-loaded, .zb-img > img[data-loaded="1"] { opacity: 1; }
.zb-img::before { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent 0%, var(--zb-shimmer-img, rgba(250,249,246,0.5)) 50%, transparent 100%); background-size: 200% 100%; animation: zb-shimmer 1.6s ease-in-out infinite; }
.zb-img.zb-done::before, .zb-img.zb-none::before { animation: none; background: none; }
.zb-img.zb-none::after { content: attr(data-empty); position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; padding: 12px; font-family: var(--fs); font-size: 10px; font-weight: 600; letter-spacing: var(--track-micro); text-transform: uppercase; color: var(--fg4); }
.zb-img > .zb-img-badge { position: absolute; top: 8px; left: 8px; }
.zb-img > .zb-img-check { position: absolute; top: 8px; right: 8px; }

/* ---------- 13 · Modal · menu · kbd ---------------------------------------- */
.zb-modal-backdrop { position: fixed; inset: 0; z-index: var(--z-modal); background: var(--zb-backdrop, rgba(27,26,24,0.28)); display: flex; align-items: center; justify-content: center; padding: 24px; }
.zb-modal { width: min(560px, 100%); max-height: 90vh; overflow: auto; background: var(--bg); border: 1px solid var(--border2); border-radius: 0; box-shadow: none; }
.zb-modal-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px 0; }
.zb-modal-body { padding: 14px 20px 20px; font-size: 13px; color: var(--fg2); }
.zb-modal-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 0 20px 18px; }
.zb-menu { min-width: 180px; background: var(--bg); border: 1px solid var(--border2); border-radius: 0; padding: 4px; box-shadow: none; z-index: var(--z-drop); }
.zb-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 9px; border: 0; background: transparent; border-radius: 0; font-size: 12px; color: var(--fg2); cursor: pointer; text-align: left; text-decoration: none; }
.zb-menu-item:hover { background: var(--bg3); color: var(--fg); text-decoration: none; }
.zb-menu-item.zb-danger { color: var(--danger); }
.zb-menu-div { height: 1px; background: var(--border); margin: 4px 0; }
.zb-kbd { display: inline-block; padding: 1px 5px; border-radius: 0; background: var(--bg3); border: 1px solid var(--border2); font-family: var(--fs); font-size: 10px; color: var(--fg2); line-height: 1.4; }

/* ---------- 14 · Command palette (⌘K) ------------------------------------- */
.zb-pal-backdrop { position: fixed; inset: 0; z-index: var(--z-palette); background: var(--zb-backdrop-pal, rgba(27,26,24,0.22)); display: flex; align-items: flex-start; justify-content: center; padding-top: 12vh; }
.zb-pal { width: min(560px, 92vw); background: var(--bg); border: 1px solid var(--border2); border-radius: 0; box-shadow: none; overflow: hidden; }
.zb-pal-input { width: 100%; height: 46px; padding: 0 16px; border: 0; border-bottom: 1px solid var(--border); background: transparent; font-size: 14px; color: var(--fg); outline: none; }
.zb-pal-list { max-height: 50vh; overflow: auto; padding: 6px; }
.zb-pal-group { padding: 8px 10px 3px; font-family: var(--fs); font-size: 10px; font-weight: 600; letter-spacing: var(--track-micro); text-transform: uppercase; color: var(--fg3); }
.zb-pal-item { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 0; font-size: 13px; color: var(--fg2); cursor: pointer; }
.zb-pal-item .zb-kbd { margin-left: auto; }
.zb-pal-item.zb-on, .zb-pal-item:hover { background: var(--bg3); color: var(--fg); }
.zb-pal-empty { padding: 20px; text-align: center; font-size: 12px; color: var(--fg3); }
.zb-pal-foot { display: flex; gap: 14px; padding: 8px 14px; border-top: 1px solid var(--border); font-size: 11px; color: var(--fg4); }
@media (hover: none) and (pointer: coarse) { .zb-pal-foot { display: none; } }   /* v3.1: no keys on a touch phone */

/* ---------- 15 · Utilities ------------------------------------------------ */
.zb-row { display: flex; align-items: center; gap: 8px; }
.zb-row.zb-between { justify-content: space-between; }
.zb-stack { display: flex; flex-direction: column; gap: 8px; }
.zb-grid { display: grid; gap: 16px; }
.zb-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } .zb-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .zb-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } .zb-grid-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.zb-page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
.zb-page-head .zb-actions { display: flex; gap: 8px; align-items: center; }
.zb-section { margin-top: 28px; }
.zb-section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.zb-hide { display: none !important; }
.zb-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.zb-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.zb-version { font-family: var(--fs); font-size: 10px; color: var(--fg4); }
@media (max-width: 900px) {
  .zb-frame { grid-template-columns: 1fr; }
  .zb-side { display: none; }
  .zb-main { padding: 16px; }
  .zb-grid-3, .zb-grid-4, .zb-grid-6 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------- 16 · Assistant rail (v2.1 · merchant ROADMAP §4.14) -------------
   chrome.js renders this ONLY when the host page sets window.ZB_ASSISTANT, so
   an app that has not opted in is unchanged by the bump. Three parts: the
   Assistant nav item; the rail, a 380px right-hand column beside .zb-main above
   900px and a full-height drawer over .zb-scrim below it; and .zb-rail-open,
   the state class chrome.js puts on <html> while the rail is open. The kit
   draws the frame only — what is inside the rail is the rail's own (merchant
   rail.js).
   v2.7.0 — the item is a plain nav item directly under the sidebar's last
   nav item, inside that item's own group (chrome.js places it, and marks it
   [data-zb-assistant]): it takes the group's gap and nothing else — no
   margin, no flex rule — so it is spaced, focused, hovered and laid out
   (an app's phone-width row included) exactly as the item above it. The
   free space stays where the foot's own `margin-top: auto` puts it: above
   the foot. Operator, 2026-10-04: "i thought we moved assistant up so it's
   below audit log. it is hidden at bottom". v2.6.0 pinned it to the bottom,
   directly above the foot, with `margin-top: auto` (and traded the foot's
   own auto margin for a fixed 8px, !important); both rules are gone, so a
   foot computes as it did before v2.6.
   Nothing marks it selected until the rail is open — a fresh visit starts
   closed (chrome.js) — and open is the hover's stone wash, held: no weight
   and no espresso rule, which mean "the page you are on", and the rail
   opens beside that page (v2.6.0, unchanged).
   `.zb-rail[hidden]` is spelled out because `.zb-rail` sets display and would
   otherwise beat the equal-specificity `[hidden]` reset above it. The scrim is
   one layer BELOW the rail: chrome.js appends it to <body>, after the rail, so
   at an equal z-index it would paint over the drawer and take its taps. */
.zb-nav.zb-nav-assistant[aria-expanded="true"] { background: var(--bg3); color: var(--fg); }
.zb-rail {
  position: fixed; top: var(--zb-top-h); right: 0; bottom: 0; z-index: var(--z-rail);
  width: 380px; max-width: 92vw; min-height: 0; display: flex; flex-direction: column;
  background: var(--bg); border-left: 1px solid var(--border);
}
.zb-scrim { position: fixed; inset: 0; z-index: calc(var(--z-rail) - 1); background: var(--zb-backdrop, rgba(27,26,24,0.28)); }
.zb-rail[hidden], .zb-scrim[hidden] { display: none; }
/* v3.2.2 — when Ziggy's rail closes and nothing else on the page can take focus, chrome.js lands it on the page's <main>
   with a tabindex="-1" of its own, marked data-zb-landing, for that moment only: a place to start from, not a control, so
   no ring around the whole page (review LOW-3). A tabindex the app wrote is never marked and keeps its ring. */
[data-zb-landing]:focus { outline: none !important; }
/* v2.8.1 — the open rail sits above the page's own overlays. With Merchant's hands (v2.8) the person
   works in the rail while an app's modal or drawer is open (Content Engine's Edit ad window, Helix's New
   test form), and at --z-drop (40) every modal backdrop (the kit's 50, an app's 60) covered it: a click
   on Apply landed on the backdrop and closed the window. --z-rail is above any app layer; the kit's own
   toasts, tips and ⌘K palette stay above the rail while it is open. A right-hand app drawer must make
   room for the column itself (html.zb-rail-open).
   v3.2.0 — the room is published: --zb-rail-room is the open rail's column (380px; 448px in product grade,
   17.11) above 900px and 0px otherwise (below, the rail is a drawer over its scrim), inherited from <html>,
   so a right-hand drawer, its scrim and a toast write right: var(--zb-rail-room, 0px) (the kit's toasts do, section
   11: left of the open rail, above it only where it is a drawer). The palette opens on
   its own layer (--z-palette) and never drops under the rail: the larger of the two wins. */
.zb-rail-open .zb-toasts { z-index: calc(var(--z-rail) + 1); }
.zb-rail-open .zb-pal-backdrop { z-index: max(var(--z-palette), calc(var(--z-rail) + 1)); }
.zb-rail-open .zb-tip { z-index: calc(var(--z-rail) + 2); }
@media (min-width: 901px) {
  .zb-rail-open { --zb-rail-room: 380px; }
  .zb-rail-open .zb-frame { grid-template-columns: var(--zb-side-w) 1fr 380px; }
  .zb-rail-open .zb-frame.zb-no-side { grid-template-columns: 1fr 380px; }
  .zb-frame > .zb-rail { position: sticky; top: var(--zb-top-h); grid-row: 2; grid-column: -2 / -1; width: auto; max-width: none; height: calc(100vh - var(--zb-top-h)); }
  .zb-scrim { display: none; }
}
@media (max-width: 900px) {
  .zb-rail { top: 0; width: min(380px, 92vw); max-width: none; }
}

/* ---------- 17 · Product grade (v2.2 · opt-in · build spec §8.12) --------
   An app opts in with ONE class — <body class="zb-app zb-product"> — and every
   rule in this section is scoped under it. Nothing above this section changed
   from v2.1.1 but the Assistant item's rules (section 16, v2.6.0/v2.7.0), so an app
   that has not opted in renders exactly as before; the suite pins both halves
   (test/product-grade.test.mjs). The rulings, approved
   by the operator on 2026-09-29:
   D1  sentence-case controls; tracked capitals survive only on the 11px eyebrow
       and, since v3.4.0, the sidebar group head in the eyebrow's voice (§5).
   D2  the product's mark + serif name lead the top bar; the Z BRAND wordmark
       moves to a foot endorsement (.zb-endorse).
   D3  ONE soft shadow (--zb-float), floating layers only: menus, the kit's
       option list, the ⌘K palette, dialogs and the tooltip. Pages, cards,
       panels and rails stay flat.
   D4  a 14px base and nothing under 12px; all secondary text is ink-2
       (#57524A), taupe (#857E71) is for icons and rules, #A6A093 leaves text.
   Specificity: a rule here is `.zb-product` + the selector of the base rule it
   overrides, so it outranks that rule by exactly one class and an app's own
   rule at the same weight still wins. Where a base COMPOUND selector
   (.zb-nav.zb-active, .zb-chip.zb-on, .zb-btn-sm.zb-btn-icon, ...) would
   otherwise outrank the simple product rule, that compound gets its own
   product counterpart, marked "compound" below. */

/* 17.1 Tokens, base type, the ramp. Re-pointing --fg3/--fg4 to ink-2 is D4 —
   and it would merge any two states the kit told apart only by fg2 vs fg3/fg4,
   so every such pair is re-drawn below with a second cue (a fill, a weight, a
   ring or ink): (i) idle vs hover, the nav badge idle vs active, crumb vs its
   separator, the empty title vs body, the quiet empty title, table head vs
   cell, KPI label vs foot, segment idle vs hover. .zb-muted and .zb-faint
   meeting at ink-2 is D4 itself: "all secondary text is ink-2". */
.zb-product {
  --zb-top-h: 56px; --zb-side-w: 232px;
  --side: #F4F2ED;            /* sidebar and rail ground (the one new tint) */
  --nav-active: #E6E2D8;      /* the active nav item's fill */
  --taupe: #857E71;           /* icons, rules, text 24px and up — never small text */
  --zb-float: 0 16px 36px -10px rgba(27,26,24,0.28), 0 2px 6px rgba(27,26,24,0.06);
  --zb-placeholder: #6E685E;  /* a field's placeholder, lighter than any value (v2.2.1, 17.6) */
  --fg3: #57524A; --fg4: #57524A;
  font-size: 14px; line-height: 1.55;
}
.zb-product .zb-h1 { font-size: 34px; line-height: 1.1; letter-spacing: -0.01em; }
.zb-product .zb-h1-lg { font-size: 40px; }   /* <h1 class="zb-h1 zb-h1-lg"> on a home screen */
.zb-product .zb-h2 { font-size: 22px; line-height: 1.25; }
.zb-product .zb-h3 { font-size: 15px; }
.zb-product .zb-eyebrow { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg2); }
.zb-product .zb-sub { font-size: 13px; color: var(--fg2); }
.zb-product .zb-kpi { font-size: 32px; line-height: 1.05; }
.zb-product .zb-card-title, .zb-product .zb-help-title { font-size: 15px; }

/* 17.1b `hidden` means hidden. Several rules below set display (.zb-rows > *,
   .zb-panel, .zb-tag, .zb-affix, ...), and an author display beats the
   browser's own [hidden] rule, so a hidden row or panel would still render.
   The kit's own toggles (menus, the rail, its scrim) all mean display: none.
   hidden="until-found" keeps the browser's find-in-page behavior. */
.zb-product [hidden]:not([hidden="until-found"]) { display: none !important; }

/* 17.2 Focus: the 1px ink ring, 2px out, on KEYBOARD focus only (the base
   :focus-visible rules above still draw it). A mouse click draws nothing —
   not even an app's own class-level `:focus` outline around a section. Text
   fields are the one exception to the ring itself: see 17.6.
   v3.0.1: a PICK is not a focus ring. An element marked picked
   (aria-pressed / aria-checked / aria-selected = "true", or aria-current
   other than "false") keeps whatever outline the app draws for that state.
   A click focuses the button it lands on, so blanking it hid the pick the user
   had just made; the ring then turned up on the PREVIOUS pick once focus
   moved ("it picks the wrong one": Image Studio's cast row, 10-19). The
   exclusion sits in :where(), so the rule stays 0-3-0 and an app rule
   that won before still wins. An app's own `:focus` ring on a picked element
   would now draw on a click too; none does today (every app `:focus` outline
   is on a text field). A pick drawn by a CLASS alone gets no help here: mark
   it with its ARIA state, or restate it for :focus:not(:focus-visible). */
.zb-product :focus:not(:focus-visible):where(:not([aria-pressed="true"], [aria-checked="true"], [aria-selected="true"], [aria-current]:not([aria-current="false"]))) { outline: none; }

/* 17.3 Top bar (D2): [mark + serif name] / crumbs · tabs · spacer · search ·
   the app's controls · Z Brand apps · account. 56px; uiTopBar({product}) in
   the generated kit.ts writes exactly this markup.
   It does not push the page wider than the window. As room runs out: the
   current crumb (a style's long name) ellipsizes first — the crumbs before it
   keep their width — then search narrows from 240px to 160px, then at 900px
   and under search folds to its icon (still a ⌘K button, with its
   aria-label) and the gaps tighten, and at 640px and under the crumbs and
   their tags step out and the product name may ellipsize — never above
   640px, where the lockup does not shrink at all (a flex share of even a
   fraction of a pixel draws "Zeb…"). The app's own controls (`right`, an
   html crumb) do not shrink: keep `right` to one control at phone width and
   let an html crumb compact itself.
   min-width: 0 lets the bar, a grid item of .zb-frame, be narrower than its
   content — without it the page grows to the bar's width. */
.zb-product .zb-top { gap: 12px; min-width: 0; padding: 0 20px; }
.zb-product .zb-product-lockup { display: inline-flex; align-items: center; gap: 10px; flex: 0 0 auto; color: var(--fg); }   /* shrinks only at 640px and under */
.zb-product .zb-mark { display: inline-flex; width: 24px; height: 24px; flex-shrink: 0; }
.zb-product .zb-mark > svg { display: block; width: 100%; height: 100%; }
.zb-product .zb-product-name { min-width: 0; overflow: hidden; font-family: var(--fp); font-size: 19px; font-weight: 600; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
.zb-product .zb-crumb-sep { padding: 0 2px; color: var(--border2); font-size: 18px; font-weight: 300; line-height: 1; }
.zb-product .zb-crumb { color: var(--fg2); font-size: 14px; font-weight: 400; }
.zb-product a.zb-crumb:hover { color: var(--fg); }
.zb-product .zb-crumb.zb-active { color: var(--fg); font-weight: 500; }   /* compound */
.zb-product .zb-crumb.zb-live { color: var(--accent); }                  /* compound */
/* The current crumb (a style's full name) ellipsizes before anything else gives. */
.zb-product .zb-top > .zb-crumb.zb-active { flex: 0 1000 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.zb-product .zb-top-tabs { min-width: 0; overflow-x: auto; }
/* The strip scrolls, and a scroll container clips a ring drawn outside a tab,
   so product tabs draw their keyboard ring 3px inside instead. The selector
   matches the weight of the base hardened ring (section 2, 0,7,0) — later, so
   it wins, as the (i)'s does. */
.zb-product .zb-top-tab:not(.zb-input):not(.zb-select):not(.zb-textarea):not(.zb-info):focus-visible { outline: 1px solid var(--fg); outline-offset: -3px; }
.zb-product .zb-top-tab { font-size: 14px; font-weight: 500; letter-spacing: 0; text-transform: none; }
.zb-product .zb-top-stat .zb-eyebrow { font-size: 11px; color: var(--fg2); }
.zb-product .zb-top-stat .zb-num { font-size: 13px; }
.zb-product .zb-top .zb-tag { height: 22px; padding: 0 7px; }
/* Search opens the ⌘K palette: chrome.js wires every [data-zb-palette-open]. */
.zb-product .zb-searchbtn {
  display: inline-flex; align-items: center; gap: 8px; flex: 0 10 240px;
  min-width: 160px; height: 36px; padding: 0 8px 0 12px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 0;
  color: var(--fg2); font-size: 14px; cursor: pointer;
  transition: border-color var(--zb-fast), color var(--zb-fast);
}
.zb-product .zb-searchbtn:hover { border-color: var(--border3); color: var(--fg); }
.zb-product .zb-searchbtn > svg { width: 16px; height: 16px; flex-shrink: 0; }
.zb-product .zb-searchbtn > span:not(.zb-kbd) { flex: 1; min-width: 0; overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
/* The Z Brand apps switcher (a 2×2 grid → Loom) replaces the Loom button. */
.zb-product .zb-apps {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 36px; height: 36px; border: 1px solid transparent; color: var(--fg2);
  transition: background var(--zb-fast), color var(--zb-fast);
}
.zb-product .zb-apps:hover { background: var(--bg3); color: var(--fg); }
.zb-product .zb-apps > svg { width: 18px; height: 18px; }
.zb-product .zb-loom { height: 36px; gap: 8px; padding: 0 12px 0 10px; font-size: 13px; font-weight: 500; letter-spacing: 0; text-transform: none; }
.zb-product .zb-avatar { width: 32px; height: 32px; font-family: var(--fs); font-size: 13px; }
/* The endorsement: the wordmark, never altered, at its 96px minimum, signing
   the product at the foot of the page or sidebar — uiEndorse() writes it. */
.zb-product .zb-endorse { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding-top: 14px; border-top: 1px solid var(--border); color: var(--fg2); font-size: 12.5px; }
.zb-product .zb-endorse .zb-wordmark { width: 96px; height: 16px; }
.zb-product .zb-side > .zb-endorse { margin-top: auto; padding: 14px 10px 4px; }

/* 17.4 Sidebar: the side tint, 36px items in ink, hover stone, active a
   --nav-active fill at 600 with NO left rule — the transparent 2px rule stays
   on every item so activation never shifts the text. The group head is
   section 5's (v3.4.0); product grade sets only its inset. */
.zb-product .zb-side { gap: 2px; padding: 16px 12px; background: var(--side); }
.zb-product .zb-side-head { padding-left: 10px; padding-right: 10px; }
.zb-product .zb-side-cta { margin: 0 0 8px; }
.zb-product .zb-nav { height: 36px; gap: 10px; padding: 0 10px 0 8px; color: var(--fg); font-size: 14px; }
.zb-product .zb-nav:hover { background: var(--bg3); color: var(--fg); }
.zb-product .zb-nav.zb-active { background: var(--nav-active); color: var(--fg); font-weight: 600; border-left-color: transparent; }   /* compound */
.zb-product .zb-nav[aria-disabled="true"] { color: var(--fg2); cursor: default; }
.zb-product .zb-nav[aria-disabled="true"]:hover { background: transparent; color: var(--fg2); }
.zb-product .zb-nav svg { width: 17px; height: 17px; stroke-width: 1.5; }
.zb-product .zb-nav .zb-tag { margin-left: auto; }
.zb-product .zb-nav-badge { padding: 1px 6px; background: var(--bg3); color: var(--fg2); font-size: 12px; }
.zb-product .zb-nav.zb-active .zb-nav-badge { background: var(--bg); color: var(--fg); }   /* compound */
.zb-product .zb-side-foot { padding: 14px 10px 4px; color: var(--fg2); font-size: 12px; }
/* A label for what is not there yet: "Soon", "Pre-launch". Outlined, never filled. */
.zb-product .zb-tag {
  display: inline-flex; align-items: center; flex-shrink: 0; height: 20px; padding: 0 6px;
  border: 1px solid var(--border2); color: var(--fg2);
  font-size: 12px; font-weight: 500; line-height: 1; white-space: nowrap;
}
/* The Assistant item is an ordinary nav item, directly under the last nav
   item (v2.7.0; v2.6.0 pinned it to the bottom above the foot, and before
   that this block undid the base outline box and set margin: 0). It is
   drawn as selected only while the rail is open — the person opened it — with
   a 1px inset ink ring (a border drawn inside, not a shadow — D3 is
   untouched), not the --nav-active fill: that fill names the page you are on,
   and the rail opens beside it, so a second fill would name two pages. */
.zb-product .zb-nav.zb-nav-assistant[aria-expanded="true"] { background: transparent; color: var(--fg); box-shadow: inset 0 0 0 1px var(--fg); }
.zb-product .zb-nav.zb-nav-assistant[aria-expanded="true"]:hover { background: var(--bg3); }

/* 17.5 Buttons (D1 · S4): sentence case, Hanken 500. 36px default (toolbars,
   the top bar), 44px .zb-btn-lg for a region's ONE primary, 30px .zb-btn-sm.
   Primary keeps the base recipe (ink → espresso). Secondary is a paper fill in
   the field border; tertiary washes stone. Disabled keeps the kit's hairline;
   a disabled tertiary stays text (v3.4.1, as section 6). */
.zb-product .zb-btn { height: 36px; padding: 0 14px; font-size: 14px; font-weight: 500; letter-spacing: 0; text-transform: none; }
.zb-product .zb-btn svg { width: 16px; height: 16px; }
.zb-product .zb-btn-secondary { background: var(--bg); color: var(--fg); border-color: var(--border2); }
.zb-product .zb-btn-secondary:hover { color: var(--fg); border-color: var(--fg); }
.zb-product .zb-btn-tertiary { padding: 0 12px; color: var(--fg2); }
.zb-product .zb-btn-tertiary:hover { background: var(--bg3); color: var(--fg); }
.zb-product .zb-btn-sm { height: 30px; gap: 6px; padding: 0 10px; font-size: 13px; }
.zb-product .zb-btn-sm svg { width: 14px; height: 14px; }
.zb-product .zb-btn-lg { height: 44px; padding: 0 18px; }
.zb-product .zb-btn-icon { width: 36px; padding: 0; }
.zb-product .zb-btn-icon svg { width: 18px; height: 18px; }
.zb-product .zb-btn-sm.zb-btn-tertiary { padding: 0 8px; }                /* compound */
.zb-product .zb-btn-sm.zb-btn-icon { width: 30px; padding: 0; }           /* compound */
.zb-product .zb-btn-sm.zb-btn-icon svg { width: 16px; height: 16px; }
.zb-product .zb-btn-lg.zb-btn-icon { width: 44px; padding: 0; }           /* compound */
.zb-product .zb-btn:disabled, .zb-product .zb-btn[aria-disabled="true"] { background: transparent; color: var(--border2); border-color: var(--border); }   /* compound */
.zb-product .zb-btn-tertiary:disabled, .zb-product .zb-btn-tertiary[aria-disabled="true"] { border-color: transparent; }   /* compound */
/* kbd hints: 20px, hairline, 12px (D4's floor, not the board's 11.5). */
.zb-product .zb-kbd { display: inline-flex; align-items: center; height: 20px; padding: 0 5px; background: transparent; border-color: var(--border); color: var(--fg2); font-size: 12px; font-weight: 500; line-height: 1; vertical-align: middle; }
.zb-product .zb-btn-primary .zb-kbd { border-color: var(--zb-on-primary-line, rgba(250,249,246,0.28)); color: var(--bg); }   /* compound */
/* Split button: a region's one primary when it has options (both halves
   count as one). The toggle is a data-zb-menu button (chrome.js) whose menu
   the split anchors below it. */
.zb-product .zb-split { position: relative; display: inline-flex; vertical-align: middle; }
.zb-product .zb-split-toggle { width: 32px; padding: 0; border-left-color: var(--zb-on-primary-line, rgba(250,249,246,0.28)); }
.zb-product .zb-btn-sm.zb-split-toggle { width: 28px; }                    /* compound */
.zb-product .zb-btn-lg.zb-split-toggle { width: 36px; }                    /* compound */
.zb-product .zb-split-toggle[aria-expanded="true"] { background: var(--accent); }
.zb-product .zb-split > .zb-menu { position: absolute; top: calc(100% + 6px); right: 0; z-index: var(--z-drop); width: max-content; max-width: min(352px, 92vw); }
.zb-product .zb-split > .zb-menu[data-zb-up] { top: auto; bottom: calc(100% + 6px); }   /* v2.2.2: chrome.js opens it upward when there is no room below */

/* 17.6 Fields (S5): labeled above, 44px, 15px text, the #C9C4B8 field border
   on a paper fill. The select's chevron is a background IMAGE, so every rule
   here uses background-color / background-image longhands — the `background`
   shorthand would erase it. */
.zb-product .zb-input, .zb-product .zb-select, .zb-product .zb-textarea { height: 44px; padding: 0 14px; background-color: var(--bg); font-size: 15px; }
.zb-product .zb-textarea { height: auto; min-height: 96px; padding: 12px 14px; line-height: 1.5; }
.zb-product .zb-select {
  padding-right: 40px; background-position: right 12px center; background-size: 16px 16px;
  background-image: var(--zb-chevron, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2357524A' stroke-width='1.5' stroke-linecap='square'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"));
}
/* A <button class="zb-select"> is the trigger for a list the app draws (a
   colorway with its swatch); it lays its parts out in a row. */
.zb-product button.zb-select { display: inline-flex; align-items: center; gap: 10px; text-align: left; }
.zb-product .zb-select[aria-expanded="true"] { border-color: var(--fg); }
.zb-product .zb-select > .zb-swatch { width: 24px; height: 24px; margin-left: -4px; }
.zb-product .zb-input.zb-sm, .zb-product .zb-select.zb-sm { height: 32px; padding: 0 10px; font-size: 13px; }   /* compound */
.zb-product .zb-select.zb-sm { padding-right: 32px; background-position: right 8px center; background-size: 14px 14px; }   /* compound */
.zb-product .zb-input[type="date"] { font-size: 15px; }                   /* compound */
.zb-product .zb-label { margin-bottom: 6px; color: var(--fg); font-size: 13px; }
/* v2.2.1: a placeholder is a hint, and must not read as a value already
   typed. The base rule paints it --fg4, which D4 re-points to ink-2 — the
   color of secondary text — so "Name it later" read as filled. It takes its
   own token instead: #6E685E, 5.2:1 on paper and 4.55:1 on stone, between
   ink-2 and taupe. opacity: 1 because Firefox dims ::placeholder by default.
   Every other use of --fg4 is unchanged. */
.zb-product ::placeholder { color: var(--zb-placeholder); opacity: 1; }
.zb-product .zb-hint { margin-top: 6px; color: var(--fg2); font-size: 13px; }
.zb-product .zb-check { font-size: 14px; }
.zb-product .zb-search .zb-input { padding-left: 38px; }                  /* compound */
.zb-product .zb-search::before { left: 14px; width: 16px; height: 16px; }
.zb-product :is(.zb-input, .zb-select, .zb-textarea):focus { border-color: var(--fg); outline: none; }
/* A browser counts EVERY focus of a text field as :focus-visible, a mouse
   click included, so a ring there is a ring after a click. Text fields show
   focus with the ink border above; the ring stays keyboard-only for the rest,
   the select among them. */
.zb-product .zb-select:focus-visible { outline: 1px solid var(--fg); outline-offset: 2px; }
/* An affix: a field that says what it holds — "$" before a price, a unit
   after. The wrapper draws the field; the input inside goes borderless. */
.zb-product .zb-affix { display: flex; align-items: center; height: 44px; background: var(--bg); border: 1px solid var(--border2); transition: border-color var(--zb-fast); }
.zb-product .zb-affix:hover { border-color: var(--border3); }
.zb-product .zb-affix:focus-within { border-color: var(--fg); }
.zb-product .zb-affix > span { flex-shrink: 0; padding: 0 4px 0 14px; color: var(--fg2); font-size: 15px; }
.zb-product .zb-affix > .zb-input + span { padding: 0 14px 0 4px; }
.zb-product .zb-affix > .zb-input { flex: 1; width: auto; min-width: 0; height: 42px; padding: 0 14px 0 0; background-color: transparent; border: 0; outline: none; }
.zb-product .zb-affix > .zb-input:first-child { padding-left: 14px; }
/* A color, shown: a square chip for a colorway in a field, a menu or a row.
   The app sets its background inline; the hairline ring keeps white visible. */
.zb-product .zb-swatch { display: inline-block; flex-shrink: 0; width: 20px; height: 20px; background: var(--mat); outline: 1px solid var(--zb-swatch-line, rgba(27,26,24,0.1)); outline-offset: -1px; vertical-align: middle; }

/* 17.7 Chips, pills, segments, status (S6): chips 28px square, sentence case,
   the hairline border; on = ink. Status color lives in an 8px square beside
   the word and the word stays ink (D4) — so the semantic chips trade their
   tint and colored text for the square. */
.zb-product .zb-chip { height: 28px; gap: 7px; padding: 0 10px; border-color: var(--border); color: var(--fg); font-size: 13px; font-weight: 500; letter-spacing: 0; text-transform: none; }
.zb-product .zb-chip.zb-mono { letter-spacing: 0; }                       /* compound */
.zb-product .zb-chip .zb-dot { width: 8px; height: 8px; }
.zb-product button.zb-chip:hover { color: var(--fg); border-color: var(--fg); }
.zb-product .zb-chip.zb-on { background: var(--fg); border-color: var(--fg); color: var(--bg); }   /* compound */
.zb-product button.zb-chip.zb-on:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); }
.zb-product .zb-chip.zb-success, .zb-product .zb-chip.zb-warn, .zb-product .zb-chip.zb-danger { background: transparent; border-color: var(--border); color: var(--fg); }   /* compound */
.zb-product .zb-chip.zb-success .zb-dot, .zb-product .zb-chip.zb-warn .zb-dot, .zb-product .zb-chip.zb-danger .zb-dot { display: none; }
.zb-product .zb-chip.zb-success::before, .zb-product .zb-chip.zb-warn::before, .zb-product .zb-chip.zb-danger::before { content: ""; flex-shrink: 0; width: 8px; height: 8px; }
.zb-product .zb-chip.zb-success::before { background: var(--success); }
.zb-product .zb-chip.zb-warn::before { background: var(--warn); }
.zb-product .zb-chip.zb-danger::before { background: var(--danger); }
/* The status square — aria-hidden; the word beside it carries the meaning. */
.zb-product .zb-sq { display: inline-block; flex-shrink: 0; width: 8px; height: 8px; background: var(--taupe); vertical-align: middle; }
.zb-product .zb-sq-good { background: var(--success); }
.zb-product .zb-sq-steady { background: var(--taupe); }
.zb-product .zb-sq-warn { background: var(--warn); }
.zb-product .zb-sq-bad { background: var(--danger); }
.zb-product .zb-status { gap: 8px; color: var(--fg); font-size: 13px; }
.zb-product .zb-status .zb-dot { width: 8px; height: 8px; background: var(--taupe); }
.zb-product .zb-status.zb-live .zb-dot { background: var(--success); }    /* compound */
.zb-product .zb-status.zb-beta .zb-dot { background: var(--warn); }       /* compound */
.zb-product .zb-status.zb-dev .zb-dot { background: var(--taupe); }       /* compound */
.zb-product .zb-pill { font-size: 13px; }
.zb-product .zb-pill::after { color: var(--taupe); font-size: 12px; }
.zb-product .zb-pill.zb-on::after { color: var(--zb-on-primary-2, rgba(250,249,246,0.65)); }     /* compound */
.zb-product .zb-pill-item { padding: 8px 10px; font-size: 14px; }
.zb-product .zb-seg-btn { height: 34px; padding: 0 14px; color: var(--fg2); font-size: 14px; }
.zb-product .zb-seg-btn + .zb-seg-btn { border-left: 1px solid var(--border2); }
.zb-product .zb-seg-btn:not(.zb-on):hover { background: var(--bg3); color: var(--fg); }
.zb-product .zb-seg-btn.zb-on { background: var(--fg); color: var(--bg); font-weight: 500; }   /* compound */

/* 17.8 Tables, KPIs, the (i), help. Heads in sentence case at 12.5/600 ink-2
   over ink cells; the (i) draws its box in taupe and turns espresso on hover. */
.zb-product .zb-table { font-size: 14px; }
.zb-product .zb-table th { color: var(--fg2); font-size: 12.5px; letter-spacing: 0; text-transform: none; }
.zb-product .zb-table td { color: var(--fg); }
.zb-product .zb-kpi-tile .zb-kpi-label { color: var(--fg2); font-size: 13px; font-weight: 500; }
.zb-product .zb-kpi-tile .zb-kpi-foot { font-size: 12.5px; }            /* a step under the 13px label */
.zb-product .zb-skel-kpi { height: 32px; }
.zb-product .zb-info { width: 16px; height: 16px; border-color: var(--taupe); color: var(--fg2); font-size: 12px; }
/* The third selector mirrors the base hardened ring (section 2), which reaches
   .zb-info through [role="button"] (chrome.js sets it) at (0,7,0): matching
   that weight, later, is what lets the espresso ring show on keyboard focus. */
.zb-product .zb-info:hover, .zb-product .zb-info:focus-visible,
.zb-product .zb-info[role="button"]:not(.zb-input):not(.zb-select):not(.zb-textarea):focus-visible { border-color: var(--accent); color: var(--accent); outline: 1px solid var(--accent); outline-offset: 2px; }
.zb-product [data-tip].zb-tip-inline { text-decoration-color: var(--taupe); }
.zb-product .zb-help-body { font-size: 14px; }
.zb-product .zb-help-body ol li { padding-left: 30px; }
.zb-product .zb-help-body ol li::before { width: 20px; height: 20px; font-size: 12px; }

/* 17.9 Floating layers (D3 · S5): these five, and only these, take the one
   shadow. A menu: a title, then icon + label + one-line hint per item,
   unavailable items last with a "Soon" tag, an optional foot. Put the hint
   INSIDE the label: <span class="zb-menu-label">Fork<span class="zb-menu-hint">…</span></span>. */
.zb-product .zb-menu { min-width: 220px; padding: 6px; box-shadow: var(--zb-float); }
.zb-product .zb-pill-menu { padding: 6px; box-shadow: var(--zb-float); }
.zb-product .zb-pal { box-shadow: var(--zb-float); }
.zb-product .zb-modal { box-shadow: var(--zb-float); }
.zb-product .zb-tip { font-size: 13px; box-shadow: var(--zb-float); }
.zb-product .zb-tip .zb-tip-m { font-size: 12px; }
.zb-product .zb-menu-title { padding: 10px 12px 8px; color: var(--fg2); font-size: 12.5px; font-weight: 600; }
.zb-product .zb-menu-item { gap: 12px; padding: 10px 12px; color: var(--fg); font-size: 14px; line-height: 1.45; }
.zb-product .zb-menu-item:has(.zb-menu-hint) { align-items: flex-start; }
.zb-product .zb-menu-item:hover, .zb-product .zb-menu-item:focus-visible { background: var(--bg3); color: var(--fg); }
.zb-product .zb-menu-item.zb-danger { color: var(--danger); }             /* compound */
.zb-product .zb-menu-item[aria-disabled="true"] { color: var(--fg2); cursor: default; }
.zb-product .zb-menu-item[aria-disabled="true"]:hover, .zb-product .zb-menu-item[aria-disabled="true"]:focus-visible { background: transparent; color: var(--fg2); }
/* The icon and the label take their item's color — ink, danger on a .zb-danger
   item, ink-2 on a disabled one — so neither sets a color of its own. */
.zb-product .zb-menu-ico { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 34px; height: 34px; background: var(--bg3); }
.zb-product .zb-menu-ico > svg { width: 18px; height: 18px; }
.zb-product .zb-menu-item:hover .zb-menu-ico, .zb-product .zb-menu-item:focus-visible .zb-menu-ico { background: var(--bg); }
.zb-product .zb-menu-item[aria-disabled="true"] .zb-menu-ico { background: transparent; border: 1px solid var(--border); color: var(--taupe); }
.zb-product .zb-menu-label { display: block; flex: 1; min-width: 0; font-size: 14px; font-weight: 600; }
.zb-product .zb-menu-label .zb-tag { margin-left: 8px; }
.zb-product .zb-menu-hint { display: block; margin-top: 2px; color: var(--fg2); font-size: 13px; font-weight: 400; }
.zb-product .zb-menu-foot { padding: 8px 12px; color: var(--fg2); font-size: 12.5px; }
.zb-product .zb-menu-div { margin: 6px 0; }
.zb-product .zb-modal-body { font-size: 14px; }
.zb-product .zb-pal-input { height: 50px; font-size: 15px; }
.zb-product .zb-pal-group { padding: 10px 10px 4px; color: var(--fg2); font-size: 12.5px; letter-spacing: 0; text-transform: none; }
.zb-product .zb-pal-item { color: var(--fg); font-size: 14px; }
.zb-product .zb-pal-empty { font-size: 14px; }
.zb-product .zb-pal-foot { color: var(--fg2); font-size: 12.5px; }

/* 17.10 Surfaces and states (S7): ONE stone answer panel per screen, rows
   ruled by hairlines instead of boxes, an empty state that is one sentence
   and one action (left-aligned, never dashed), a skeleton that stays visible
   on stone, and ink words on the semantic alerts (warn text on its tint was
   3.1:1). */
.zb-product .zb-panel { display: flex; flex-direction: column; gap: 12px; padding: 24px; background: var(--bg3); }
.zb-product .zb-rows > * { display: flex; align-items: center; gap: 14px; padding: 14px 0; border-top: 1px solid var(--border); }
.zb-product .zb-rows > :last-child { border-bottom: 1px solid var(--border); }
.zb-product .zb-rows > a { color: inherit; }
.zb-product .zb-panel .zb-rows > * { padding: 10px 0; border-color: var(--bg5); }
.zb-product .zb-panel .zb-divider { background: var(--bg5); }
.zb-product .zb-empty { align-items: flex-start; justify-content: flex-start; gap: 4px; padding: 24px 0; text-align: left; }
.zb-product .zb-empty .zb-empty-title { color: var(--fg); font-size: 14px; }
.zb-product .zb-empty .zb-empty-body { max-width: 60ch; color: var(--fg2); font-size: 14px; }
.zb-product .zb-empty .zb-empty-actions { margin-top: 12px; }
.zb-product .zb-empty.zb-quiet { padding: 12px 0; }
.zb-product .zb-empty.zb-quiet .zb-empty-title { color: var(--fg2); font-size: 13px; }   /* compound; a step under the 14px body */
.zb-product .zb-rail > .zb-empty { padding: 24px 20px; }
.zb-product .zb-panel .zb-skel, .zb-product .zb-card.zb-inset .zb-skel { background: var(--zb-skel-inset, #DDD8CD); }
.zb-product .zb-alert { font-size: 14px; }
.zb-product .zb-alert.zb-warn, .zb-product .zb-alert.zb-danger, .zb-product .zb-alert.zb-success { color: var(--fg); }   /* compound */
.zb-product .zb-toast { font-size: 14px; }
.zb-product .zb-img.zb-none::after { color: var(--fg2); font-size: 13px; font-weight: 500; letter-spacing: 0; text-transform: none; }
.zb-product .zb-version { font-size: 12px; }

/* 17.11 The Assistant rail (v2.1) on the side tint, 448px, flat. The base
   rules size it in three places (the fixed rail, the grid column above 900px,
   the drawer below it); each gets its product counterpart — and so does the
   room it takes, --zb-rail-room (v3.2.0). */
.zb-product .zb-rail { width: 448px; background: var(--side); }
@media (min-width: 901px) {
  .zb-rail-open .zb-product { --zb-rail-room: 448px; }
  .zb-rail-open .zb-product .zb-frame { grid-template-columns: var(--zb-side-w) minmax(0, 1fr) 448px; }
  .zb-rail-open .zb-product .zb-frame.zb-no-side { grid-template-columns: minmax(0, 1fr) 448px; }
  .zb-product .zb-frame > .zb-rail { width: auto; }
}
@media (max-width: 900px) {
  .zb-product .zb-rail { width: min(448px, 92vw); }
}

/* 17.12 A narrower top bar (see 17.3). At 900px and under, search folds to
   its icon — the button keeps its aria-label and still opens ⌘K — and the
   gaps tighten. At 640px and under the label crumbs step out (the page's
   title names the page), and so does every crumb's tag (.zb-crumb-tag, which
   uiTopBar writes); an html crumb, and a tag the app puts in `right`, are
   the app's own and stay. Only here may the product name ellipsize. */
@media (max-width: 900px) {
  .zb-product .zb-top { gap: 8px; padding: 0 16px; }
  .zb-product .zb-searchbtn { flex: 0 0 36px; justify-content: center; min-width: 0; padding: 0; }
  .zb-product .zb-searchbtn > span { display: none; }
}
@media (max-width: 640px) {
  .zb-product .zb-top { gap: 6px; padding: 0 12px; }
  .zb-product .zb-product-lockup { flex-shrink: 1; min-width: 0; }
  .zb-product .zb-top > .zb-crumb, .zb-product .zb-top > .zb-crumb-sep, .zb-product .zb-top > .zb-crumb-tag { display: none; }
}

/* ---------- 17b · Language (v2.4 · English default, 简体中文 by choice) ------
   Operator ruling 2026-10-01: every app's top bar carries an English / 中文
   switch; English is the default and the choice is one for every Z Brand app.
   The switch is the flag (the woven side-seam tag: the true flag, a stone fold
   with its stitch, a fine weave) and a word beside it, EN or 中文; on a phone
   the word drops. Flipping turns the flag over. <html data-zb-lang="zh"
   lang="zh-Hans"> is set before paint by UI_LANG_BOOT; chrome.js translates
   the page in place. The brand faces carry no Chinese letters, so Chinese
   falls back to the system's Chinese faces (never Google's CJK fonts:
   fonts.googleapis.com does not load in mainland China), the serif voice
   skipping SimSun, and tracked capital labels lose their tracking. */
/* opacity, not visibility, so a field can still take its autofocus while the page waits for its words;
   and no pointer, so nothing invisible can be clicked meanwhile */
html.zb-i18n-wait body { opacity: 0; pointer-events: none; }
:root[data-zb-lang="zh"] {
  --fs: "Hanken Grotesk", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "Helvetica Neue", Arial, sans-serif;
  --fp: "Source Serif 4", "Songti SC", "STSong", "Noto Serif CJK SC", "Noto Serif SC", "Source Han Serif SC", "PingFang SC", "Microsoft YaHei", Georgia, serif;
  --track-micro: 0.04em;
}
:root[data-zb-lang="zh"] body { font-synthesis-style: none; }
[data-zb-lang="zh"] .zb-product .zb-eyebrow { letter-spacing: 0.04em; }
[data-zb-lang="zh"] .zb-side-head { letter-spacing: 0.04em; }
.zb-lang { position: relative; display: inline-flex; flex-shrink: 0; }
.zb-lang-btn {
  display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 8px 0 9px;
  border: 1px solid transparent; border-radius: 0; background: transparent; color: var(--fg);
  font-family: var(--fs); font-size: 14px; font-weight: 500; line-height: 1; white-space: nowrap; cursor: pointer;
  transition: background var(--zb-fast);
}
.zb-lang-btn:hover, .zb-lang-btn[aria-expanded="true"] { background: var(--bg3); }
.zb-lang-chev { width: 10px; height: 10px; flex-shrink: 0; color: var(--fg2); }
.zb-flags { position: absolute; width: 0; height: 0; overflow: hidden; }
.zb-flag { position: relative; display: inline-block; flex-shrink: 0; width: 26px; height: 16px; }
.zb-flag > svg { display: block; width: 100%; height: 100%; }
.zb-flag::after { content: ""; position: absolute; inset: 0; border: 1px solid var(--zb-flag-edge, rgba(27,26,24,0.24)); pointer-events: none; }
.zb-lang-flip { position: relative; display: inline-block; flex-shrink: 0; width: 26px; height: 16px; perspective: 140px; }
.zb-lang-flip > span { position: absolute; inset: 0; transform-style: preserve-3d; transition: transform 0.5s cubic-bezier(0.3, 0.7, 0.2, 1); }
.zb-lang-flip .zb-flag { position: absolute; inset: 0; -webkit-backface-visibility: hidden; backface-visibility: hidden; }
.zb-lang-flip .zb-flag + .zb-flag { transform: rotateY(180deg); }
[data-zb-lang="zh"] .zb-lang-flip > span { transform: rotateY(180deg); }
@media (prefers-reduced-motion: reduce) { .zb-lang-flip > span { transition: none; } }
.zb-lang > .zb-menu { position: absolute; top: calc(100% + 6px); right: 0; z-index: var(--z-drop); width: max-content; min-width: 260px; max-width: min(320px, 92vw); }
/* The top bar is its own stacking layer (sticky, --z-top), so a menu inside it can rise no higher
   than the bar: while one is open the bar steps above the Assistant rail and its scrim. The bar spans the
   rail's column, so a menu from its right end drops over the open rail (v2.8.2: above --z-rail, not --z-drop). */
.zb-top:has(.zb-menu:not([hidden])) { z-index: calc(var(--z-rail) + 1); }
/* Its label and hint, for a bar without product grade (which styles both itself). */
:where(.zb-lang) .zb-menu-label { display: flex; flex-direction: column; }
:where(.zb-lang) .zb-menu-hint { color: var(--fg2); font-size: 12px; }
.zb-lang .zb-menu-item { text-decoration: none; }
.zb-lang .zb-menu-item > .zb-flag { margin-top: 2px; }
.zb-lang-check { width: 16px; height: 16px; flex-shrink: 0; margin-left: auto; visibility: hidden; }
.zb-lang .zb-menu-item[aria-checked="true"] .zb-lang-check { visibility: visible; }
.zb-lang-foot { margin: 0; padding: 4px 12px 8px; color: var(--fg2); font-size: 12.5px; line-height: 1.45; max-width: 30ch; }
/* The switch for a page without script (uiLangSwitchForm): a <details> menu of two forms. */
.zb-lang-form > summary { list-style: none; }
.zb-lang-form > summary::-webkit-details-marker { display: none; }
.zb-lang-form:not([open]) > .zb-menu { display: none; }
.zb-lang-form form { margin: 0; }
.zb-lang-form .zb-menu-item { font: inherit; }
/* Off the top bar (a sign-in door), the switch sits in a corner of the page. */
.zb-lang-corner { position: absolute; top: 14px; right: 16px; z-index: var(--z-top); }
@media (max-width: 640px) {
  .zb-lang-code { display: none; }
  .zb-lang-btn { height: 40px; padding: 0 7px; }
  .zb-product .zb-top .zb-lang { position: static; }
  .zb-product .zb-top .zb-lang > .zb-menu { right: 8px; }
}

/* ---------- 17c · Ziggy does it (v2.12) -------------------------------------
/* The ring around the control Ziggy is about to press or is pressing, with its mark
   and what it is doing. Above the page and the rail; it never takes a click. It pulses where the person's
   answer is awaited: a priced step (asks again) or a button that is theirs to press. */
.zb-act-ring { position: fixed; z-index: calc(var(--z-rail) + 2); pointer-events: none; outline: 2px solid var(--accent); outline-offset: 3px; transition: left var(--zb-med), top var(--zb-med), width var(--zb-med), height var(--zb-med); }
.zb-act-ring[hidden] { display: none; }
.zb-act-tag { position: absolute; left: -5px; top: -30px; height: 22px; padding: 0 8px 0 5px; display: flex; align-items: center; gap: 5px; background: var(--accent); color: var(--bg); font: 600 11.5px/1 var(--fs); white-space: nowrap; }
.zb-act-tag svg { width: 14px; height: 14px; }
.zb-act-pulse { animation: zb-act-pulse 1.2s ease-in-out infinite; }
@keyframes zb-act-pulse { 0%, 100% { outline-offset: 3px; } 50% { outline-offset: 7px; } }
@media (prefers-reduced-motion: reduce) { .zb-act-ring { transition: none; } .zb-act-pulse { animation: none; } }

/* ---------- 17d · Frame B (v3.0 · opt-in · drawing docs/frame-b.html) ----------
   Frame B "Masthead" with A's phone tab bar, approved by the operator on
   2026-10-13. A page opts in with ONE more class beside product grade:
   <body class="zb-app zb-product zb-frame-b">. Every rule here is scoped
   `.zb-product.zb-frame-b …`, so it outranks the matching section-17 rule by
   exactly one class; without zb-frame-b a page renders exactly as v2.12.1.
   Every number is the drawing's (docs/frame-b.html, at 1440 unless stated);
   colors come only through tokens, and section 18 gives the new ones their
   dark values. The suite pins this section (test/frame-b.test.mjs). */

/* 17d.1 Tokens. A 48px bar; quiet text that still reads (the disabled text
   was #C9C4B8, 1.6:1 on paper; #6E685E is 5.3:1); the More sheet's scrim. */
.zb-product.zb-frame-b {
  --zb-top-h: 48px;
  --zb-disabled-text: #6E685E;
  --zb-more-scrim: rgba(27,26,24,0.32);
}

/* 17d.2 Top bar: [mark + serif name] · tabs · search, centered in the space
   left · the right group (at most one app control, the apps switcher, the
   account square). No crumbs in the bar: apps move them to the place line
   (17d.5); hiding them here is the safety net. */
.zb-product.zb-frame-b .zb-top { gap: 12px; padding: 0 20px; }
.zb-product.zb-frame-b .zb-mark { width: 22px; height: 22px; }
.zb-product.zb-frame-b .zb-product-name { font-size: 17px; }
.zb-product.zb-frame-b .zb-top > .zb-crumb, .zb-product.zb-frame-b .zb-top > .zb-crumb-sep, .zb-product.zb-frame-b .zb-top > .zb-crumb-tag { display: none; }
/* Search is 420px, narrowing to 260px, between auto margins — so the spacer
   before it steps out (with no search, the spacer still pushes the right group). */
.zb-product.zb-frame-b .zb-top-spacer:has(+ .zb-searchbtn) { display: none; }
.zb-product.zb-frame-b .zb-searchbtn { flex: 0 1 420px; min-width: 260px; height: 32px; margin: 0 auto; }
.zb-product.zb-frame-b .zb-apps { width: 32px; height: 32px; }
.zb-product.zb-frame-b .zb-avatar { width: 28px; height: 28px; font-family: var(--fs); font-size: 12px; font-weight: 600; }
/* Top tabs (apps with five sections or fewer, no sidebar): 16px after the
   name, sentence case, ink-2; the current tab ink with a 2px ink rule on the
   bar's bottom edge. */
.zb-product.zb-frame-b .zb-top-tabs { gap: 0; margin-left: 16px; }
.zb-product.zb-frame-b .zb-top-tab { color: var(--fg2); font-size: 14px; font-weight: 500; }
.zb-product.zb-frame-b .zb-top-tab:hover { color: var(--fg); }
.zb-product.zb-frame-b .zb-top-tab.zb-active, .zb-product.zb-frame-b .zb-top-tab[aria-current="page"] { color: var(--fg); border-bottom-color: var(--fg); }

/* 17d.3 Sidebar: 232px on paper (no --side tint), a 1px rule on its right,
   words only (icons live in the tab bar). Items 34px in ink; hover stone; the
   current item 600 with a 2px ink rule inside its left edge — an inset
   shadow, never a border, so nothing shifts — and no fill. The same look
   draws the More sheet (17d.7). The group head is section 5's (v3.4.0);
   Frame B sets only its inset. */
.zb-product.zb-frame-b .zb-side { gap: 2px; padding: 20px 12px 16px; background: var(--bg); border-right: 1px solid var(--border); }
.zb-product.zb-frame-b .zb-side-head { padding-left: 12px; padding-right: 12px; }
.zb-product.zb-frame-b .zb-nav { height: 34px; padding: 0 10px 0 12px; border-left: 0; color: var(--fg); font-size: 14px; }
.zb-product.zb-frame-b .zb-nav > svg { display: none; }
.zb-product.zb-frame-b .zb-nav:hover { background: var(--bg3); color: var(--fg); }
.zb-product.zb-frame-b .zb-nav.zb-active { background: transparent; color: var(--fg); font-weight: 600; box-shadow: inset 2px 0 0 var(--fg); }
/* Not yet: ink-2 with an outlined "Soon" .zb-tag, and it does not open. */
.zb-product.zb-frame-b .zb-nav[aria-disabled="true"] { color: var(--fg2); cursor: default; pointer-events: none; }
.zb-product.zb-frame-b .zb-nav[aria-disabled="true"]:hover { background: transparent; color: var(--fg2); }

/* 17d.4 The main frame: 32 / 48 / 64px, 32px sides at 901–1100px; content
   left-aligned and capped at --zb-content-max (1400px). */
.zb-product.zb-frame-b .zb-main { padding: 32px 48px 64px; }
.zb-product.zb-frame-b .zb-main > * { max-width: var(--zb-content-max); }
@media (min-width: 901px) and (max-width: 1100px) {
  .zb-product.zb-frame-b .zb-main { padding: 28px 32px 56px; }
}

/* 17d.5 The masthead: the place line (section · company · scope, 13px ink-2,
   each a link where it has a page, " · " between), the title row (Source
   Serif 600 36px, 44px on a home, the actions on its baseline 8px apart, 36px
   tall: tertiary, secondary, then the one primary), an optional aside (one
   sentence, Source Serif italic 17px ink-2, at most 68ch), then a hairline. */
.zb-product.zb-frame-b .zb-masthead { display: grid; gap: 8px; max-width: var(--zb-content-max); padding-bottom: 20px; border-bottom: 1px solid var(--border); }
.zb-product.zb-frame-b .zb-place { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; color: var(--fg2); font-size: 13px; }
.zb-product.zb-frame-b .zb-place a { color: var(--fg2); text-decoration: none; }
.zb-product.zb-frame-b .zb-place a:hover { color: var(--fg); }
.zb-product.zb-frame-b .zb-mast-row { display: flex; align-items: flex-end; gap: 8px; }
.zb-product.zb-frame-b .zb-mast-title { flex: 1; min-width: 0; margin: 0; color: var(--fg); font-family: var(--fp); font-size: 36px; font-weight: 600; line-height: 1.1; letter-spacing: -0.01em; }
.zb-product.zb-frame-b .zb-masthead.zb-home .zb-mast-title { font-size: 44px; }
.zb-product.zb-frame-b .zb-mast-actions { display: inline-flex; flex-shrink: 0; align-items: center; gap: 8px; }
.zb-product.zb-frame-b .zb-mast-actions > .zb-btn { height: 36px; }
.zb-product.zb-frame-b .zb-mast-aside { max-width: 68ch; margin: 2px 0 0; color: var(--fg2); font-family: var(--fa); font-size: 17px; font-style: italic; font-weight: 400; }
/* D1: uiMasthead({ asideId }) with no aside writes an empty aside for the app
   to fill in the browser; until it does, it takes no row (nor the grid's gap). */
.zb-product.zb-frame-b .zb-mast-aside:empty { display: none; }

/* 17d.6 The toolbar under the masthead: 16px under its hairline; chips
   first, a 36px field may sit at the end. */
.zb-product.zb-frame-b .zb-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.zb-product.zb-frame-b .zb-masthead + .zb-toolbar { margin-top: 16px; }
.zb-product.zb-frame-b .zb-toolbar-end { margin-left: auto; }
.zb-product.zb-frame-b .zb-toolbar-end:is(.zb-input, .zb-select), .zb-product.zb-frame-b .zb-toolbar-end :is(.zb-input, .zb-select) { height: 36px; }

/* 17d.7 The tab bar and the More sheet, at 900px and under (where the
   sidebar hides): fixed to the bottom, 56px plus the safe area, paper, a
   hairline on top; equal slots, an icon 20px over a 12px label (D4's floor);
   idle ink-2, current ink 600 with a 2px ink rule on the slot's top edge.
   More raises the sheet over the page — the rest of the nav, the Assistant,
   the endorsement — with the one floating shadow (D3) over a 32% ink scrim
   between the bars. While the sheet is open More is the current slot and
   the page's own tab drops to idle (Board 3's second phone). The page
   reserves the bar's height at its foot, and toasts rise above the bar. */
.zb-product.zb-frame-b .zb-tabbar { display: none; }
.zb-product.zb-frame-b .zb-tab { display: grid; justify-items: center; align-content: center; gap: 3px; min-width: 0; padding: 0; border: 0; background: transparent; color: var(--fg2); font-family: var(--fs); font-size: 12px; font-weight: 500; line-height: 1.2; text-decoration: none; cursor: pointer; }
.zb-product.zb-frame-b .zb-tab > svg { width: 20px; height: 20px; }
.zb-product.zb-frame-b .zb-tab.zb-active, .zb-product.zb-frame-b .zb-tab[aria-current="page"], .zb-product.zb-frame-b .zb-tab[data-zb-more][aria-expanded="true"] { color: var(--fg); font-weight: 600; box-shadow: inset 0 2px 0 var(--fg); }
.zb-product.zb-frame-b .zb-tabbar:has([data-zb-more][aria-expanded="true"]) .zb-tab:is(.zb-active, [aria-current="page"]):not([data-zb-more]) { color: var(--fg2); font-weight: 500; box-shadow: none; }
.zb-product.zb-frame-b .zb-more { position: fixed; left: 0; right: 0; bottom: calc(56px + env(safe-area-inset-bottom)); z-index: calc(var(--z-top) + 1); display: flex; flex-direction: column; gap: 2px; max-height: calc(100vh - var(--zb-top-h) - 56px - env(safe-area-inset-bottom)); overflow-y: auto; padding: 10px 8px 12px; background: var(--bg); border-top: 1px solid var(--border); box-shadow: var(--zb-float); }
.zb-product.zb-frame-b .zb-more > .zb-endorse { margin-top: 8px; padding: 14px 12px 2px; }
.zb-product.zb-frame-b .zb-more-scrim { position: fixed; top: var(--zb-top-h); left: 0; right: 0; bottom: calc(56px + env(safe-area-inset-bottom)); z-index: var(--z-top); background: var(--zb-more-scrim); }
@media (min-width: 901px) {
  .zb-product.zb-frame-b .zb-more, .zb-product.zb-frame-b .zb-more-scrim { display: none; }
}
@media (max-width: 900px) {
  .zb-product.zb-frame-b .zb-tabbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-top); display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); height: calc(56px + env(safe-area-inset-bottom)); padding-bottom: env(safe-area-inset-bottom); background: var(--bg); border-top: 1px solid var(--border); }
  .zb-product.zb-frame-b .zb-main { padding: 28px 32px calc(56px + env(safe-area-inset-bottom) + 24px); }
  .zb-product.zb-frame-b .zb-toasts { bottom: calc(56px + env(safe-area-inset-bottom) + 16px); }
  /* The bar narrows as 17.12 does — search folds to its 36px icon, the gaps
     tighten — and its tabs step out: they are in the tab bar. */
  .zb-product.zb-frame-b .zb-top { gap: 8px; padding: 0 16px; }
  .zb-product.zb-frame-b .zb-searchbtn { flex: 0 0 36px; justify-content: center; width: 36px; min-width: 0; height: 36px; margin: 0 0 0 auto; padding: 0; }
  .zb-product.zb-frame-b .zb-top-tabs { display: none; }
}

/* 17d.8 The phone (640px and under). The bar: lockup (only here may the
   name ellipsize), the search icon, the account square; no tabs, no crumbs,
   no app controls. The masthead keeps its voice at a 16px gutter: place
   12px, title 28px (home 32px), aside 15px, and the actions drop under the
   aside in one row, the primary first and taking the width. */
@media (max-width: 640px) {
  .zb-product.zb-frame-b .zb-top { gap: 10px; padding: 0 12px 0 16px; }
  .zb-product.zb-frame-b .zb-product-name { font-size: 16px; }
  .zb-product.zb-frame-b .zb-top > :not(.zb-product-lockup, .zb-searchbtn, .zb-avatar, .zb-account-menu) { display: none; }
  .zb-product.zb-frame-b .zb-main { padding: 20px 16px calc(56px + env(safe-area-inset-bottom) + 24px); }
  .zb-product.zb-frame-b .zb-masthead { gap: 6px; padding-bottom: 16px; }
  .zb-product.zb-frame-b .zb-place { font-size: 12px; }
  .zb-product.zb-frame-b .zb-mast-row { display: contents; }
  .zb-product.zb-frame-b .zb-mast-title { font-size: 28px; }
  .zb-product.zb-frame-b .zb-masthead.zb-home .zb-mast-title { font-size: 32px; }
  .zb-product.zb-frame-b .zb-mast-aside { font-size: 15px; }
  .zb-product.zb-frame-b .zb-mast-actions { order: 1; display: flex; margin-top: 2px; }
  .zb-product.zb-frame-b .zb-mast-actions > .zb-btn-primary { order: -1; flex: 1 1 auto; }
}

/* 17d.9 The account menu: the account square is a button[data-zb-menu]
   (chrome.js opens it, and Esc closes it and returns focus) controlling a
   .zb-menu.zb-account-menu, 272px with the one floating shadow (17.9),
   anchored under the square at the bar's right edge (a direct child of the
   sticky bar, its containing block). Name 14px 600 and email 13px ink-2; a
   rule; Theme as Light | Dark and Language as English | 中文, segments whose
   checked button (a menuitemradio) is ink; a rule; the app's own items and
   a rule (where it passes any); Help; Sign out. */
.zb-product.zb-frame-b .zb-account-menu { width: 272px; min-width: 0; max-width: calc(100vw - 24px); padding: 8px; border-color: var(--border); }
.zb-product.zb-frame-b .zb-top > .zb-account-menu { position: absolute; top: calc(100% + 6px); right: 12px; z-index: var(--z-drop); }
.zb-product.zb-frame-b .zb-account-name { display: block; padding: 8px 10px 0; color: var(--fg); font-size: 14px; font-weight: 600; }
.zb-product.zb-frame-b .zb-account-email { display: block; padding: 2px 10px 10px; color: var(--fg2); font-size: 13px; overflow-wrap: anywhere; }
.zb-product.zb-frame-b .zb-account-menu .zb-menu-div { margin: 6px 2px; }
.zb-product.zb-frame-b .zb-account-row { display: flex; align-items: center; gap: 12px; height: 36px; padding: 0 10px; color: var(--fg); font-size: 14px; }
.zb-product.zb-frame-b .zb-account-row > .zb-seg { margin-left: auto; }
.zb-product.zb-frame-b .zb-account-menu .zb-seg-btn { height: 28px; padding: 0 10px; color: var(--fg2); font-size: 13px; }
.zb-product.zb-frame-b .zb-account-menu .zb-seg-btn[aria-checked="true"] { background: var(--fg); color: var(--bg); font-weight: 500; }
.zb-product.zb-frame-b .zb-account-menu .zb-menu-item { height: 36px; padding: 0 10px; }
/* D1: an app's own items (uiAccountMenu({ items })) sit between Language and
   Help, as Help and Sign out are drawn; an item's icon is an 18px glyph in a
   bare 20px box, never product grade's 34px tile, which a 36px row cannot hold. */
.zb-product.zb-frame-b .zb-account-menu .zb-menu-ico { width: 20px; height: 20px; background: transparent; }

/* 17d.10 Quiet text that reads. A disabled button keeps the hairline and the
   transparent fill, in --zb-disabled-text; a disabled tertiary keeps its
   transparent border, so only its text dims (v3.4.1). A placeholder takes
   --zb-placeholder in EVERY field: apps paint their own (Loom's
   `.field input::placeholder`, Zephyr's `.zy-search .search-input::placeholder`,
   Image Studio's `.cg-comment::placeholder`), and those outranked the kit's
   (0,1,1). :is() weighs as its heaviest argument, so the second :is() counts
   one class even on a bare <input>: (0,3,2) beats each of them, and each of
   them with a .zb-product in front. The language flag swaps at once. */
.zb-product.zb-frame-b .zb-btn:disabled, .zb-product.zb-frame-b .zb-btn[aria-disabled="true"] { background: transparent; color: var(--zb-disabled-text); border-color: var(--border); }
.zb-product.zb-frame-b .zb-btn-tertiary:disabled, .zb-product.zb-frame-b .zb-btn-tertiary[aria-disabled="true"] { border-color: transparent; }
.zb-product.zb-frame-b :is(input, textarea):is(.zb-input, .zb-textarea, *)::placeholder { color: var(--zb-placeholder); opacity: 1; }
.zb-product.zb-frame-b .zb-lang-flip > span { transition: none; }

/* ---------- 17e · Sortable table header (v3.3 · every app) ------------------
   THE TABLE RULE (README "Tables"): words left, numbers right; a header sits
   exactly over its column with the same alignment, so a number column's <th>
   carries its cells' right-align class (.zb-num, or .zb-r — section 9);
   charts start at the left of their column; row actions sit at the right
   edge; numbers use tabular figures (section 2 sets them on .zb-app).
   A header opts in to sorting as th.zb-sort holding
   <button type="button" class="zb-sort-btn">Label<svg class="zb-sort-arrow">
   (three paths: .zb-sort-stem, .zb-sort-up, .zb-sort-dn); ZB.sortHeader
   (chrome.js) writes the button and the arrow when the app left them out and
   keeps aria-sort. The arrow sits after a left header's label and before a
   right header's, so the label's edge stays on its column's edge, and it
   keeps its room in every state, so nothing moves on hover or on a sort.
   Unsorted: no arrow, a faint two-way chevron on hover and keyboard focus.
   Sorted: an ink label and a thin up (ascending) or down (descending) arrow.
   The keyboard ring is the kit's own (section 2). Tokens only, so dark
   follows. Every rule names a .zb-sort class, which no app carried before
   v3.3, so nothing that exists changes. */
th.zb-sort:is(.zb-num, .zb-r) { text-align: right; }
.zb-sort-btn {
  display: inline-flex; align-items: center; gap: 5px; margin: 0; padding: 0; border: 0; border-radius: 0; background: none;
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; white-space: nowrap; cursor: pointer;
  transition: color var(--zb-fast);
}
.zb-sort-btn:hover { color: var(--fg); }
th.zb-sort:is([aria-sort="ascending"], [aria-sort="descending"]),
th.zb-sort:is([aria-sort="ascending"], [aria-sort="descending"]) .zb-sort-btn { color: var(--fg); }
.zb-sort-arrow { flex: none; width: 9px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.3; visibility: hidden; }
th.zb-sort:is(.zb-num, .zb-r) .zb-sort-arrow { order: -1; }
th.zb-sort:is([aria-sort="ascending"], [aria-sort="descending"]) .zb-sort-arrow { visibility: visible; }
th.zb-sort[aria-sort="ascending"] .zb-sort-dn, th.zb-sort[aria-sort="descending"] .zb-sort-up { display: none; }
th.zb-sort:not([aria-sort="ascending"], [aria-sort="descending"]) .zb-sort-stem { display: none; }
th.zb-sort:not([aria-sort="ascending"], [aria-sort="descending"]) .zb-sort-btn:is(:hover, :focus-visible) .zb-sort-arrow { visibility: visible; color: var(--fg4); }
@media (prefers-reduced-motion: reduce) { .zb-sort-btn { transition: none; } }

/* ---------- 18 · Dark theme (v2.3 · build spec §2 dark ramp, §8.9) ---------
   Operator ruling 2026-10-01: every app works in dark mode — light is the
   default, each person can switch to dark. <html data-theme="dark"> (set
   before paint by the boot in UI_HEAD, from the person's choice; ZB.theme in
   chrome.js keeps it) re-points the same tokens to §2's dark ramp, so every
   rule written against the tokens follows: ground #1B1A18, surface #24221E,
   text #FAF9F6, secondary rgba(250,249,246,.60), hairline .15, Espresso Lift
   (#A68C74, raised one step to #AD937B below), paper-fill primaries; the
   steps §2 does not name (raised surfaces, quieter text, stronger rules) are
   drawn from the same ink and paper. Never the default, never from the OS
   preference. This section re-points tokens only; the two rules after the
   token blocks paint what no token reaches (the wordmark image, drawn marks). */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #1B1A18; --bg2: #1B1A18; --bg3: #24221E; --bg4: #2E2B27; --bg5: #3A3631;
  --border: rgba(250,249,246,0.15); --border2: rgba(250,249,246,0.28); --border3: rgba(250,249,246,0.42);
  --fg: #FAF9F6; --fg2: rgba(250,249,246,0.60); --fg3: rgba(250,249,246,0.50); --fg4: rgba(250,249,246,0.40);
  /* §2's Espresso Lift (#A68C74) was raised to pass 4.5:1 on the ground; one step more (#AD937B)
     keeps accent text at 4.5:1 on the surface, on --nav-active and on its own tint too. */
  --accent: #AD937B; --accent2: #C2A992;
  --accent-tint: rgba(173,147,123,0.10); --accent-tint2: rgba(173,147,123,0.20);
  --danger: #E07467; --danger-tint: rgba(224,116,103,0.10);
  --warn: #D6A05C;   --warn-tint: rgba(214,160,92,0.14);
  --success: #8DBB90; --success-tint: rgba(141,187,144,0.14);
  --heat-1: rgba(173,147,123,0.08); --heat-2: rgba(173,147,123,0.16);
  --heat-3: rgba(173,147,123,0.28); --heat-4: rgba(173,147,123,0.44); --heat-5: rgba(173,147,123,0.64);
  --mat: #2A2824;
  /* the kit's own hand-drawn colors (light keeps each literal as its fallback) */
  --zb-on-primary-wash: rgba(27,26,24,0.10); --zb-on-primary-2: rgba(27,26,24,0.62); --zb-on-primary-line: rgba(27,26,24,0.28);
  --zb-shimmer: rgba(250,249,246,0.06); --zb-shimmer-img: rgba(250,249,246,0.05);
  --zb-backdrop: rgba(0,0,0,0.60); --zb-backdrop-pal: rgba(0,0,0,0.50);
  --zb-swatch-line: rgba(250,249,246,0.16); --zb-skel-inset: #2E2B27;
  /* the active nav item's count sits on the accent tint: paper reads there, accent does not (3.7:1) */
  --zb-badge-active: #FAF9F6;
  /* the language switch: the flag's hairline edge, and its woven fold, a little dimmer on the dark */
  --zb-flag-edge: rgba(250,249,246,0.24); --zb-flag-fold: #CFC8BB;
  /* product grade's select chevron is an ink-2 drawing (2.3:1 on the ground); in the dark it is drawn light */
  --zb-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23B5AFA4' stroke-width='1.5' stroke-linecap='square'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}
[data-theme="dark"] .zb-product {
  /* the side is the ground, so a hover (--bg3) and the active item (--nav-active) stand off it */
  --side: #1B1A18; --nav-active: #2E2B27; --taupe: #9C968B;
  --zb-float: 0 16px 36px -10px rgba(0,0,0,0.6), 0 2px 6px rgba(0,0,0,0.3);
  --zb-placeholder: #8F897E;
  --fg3: rgba(250,249,246,0.60); --fg4: rgba(250,249,246,0.60);
}
/* Frame B (17d): disabled text at 45% paper. The More sheet's scrim keeps its 32% ink, as the drawing's dark mock does. */
[data-theme="dark"] .zb-product.zb-frame-b {
  --zb-disabled-text: rgba(250,249,246,0.45);
}
/* The Z BRAND wordmark is an ink image; dark draws it white. */
[data-theme="dark"] .zb-wordmark { filter: brightness(0) invert(1); }
/* Marks are ink squares carrying paper shapes (§8.12 D2); in the dark they turn
   over — paper squares, ink shapes — wherever an app draws them inline. */
[data-theme="dark"] svg [fill="#1B1A18" i] { fill: #FAF9F6; }
[data-theme="dark"] svg [fill="#FAF9F6" i] { fill: #1B1A18; }
[data-theme="dark"] svg [stroke="#1B1A18" i] { stroke: #FAF9F6; }
[data-theme="dark"] svg [stroke="#FAF9F6" i] { stroke: #1B1A18; }
