/* ============================================================================
   Cortex DS v4 — component layer

   THIS FILE IS THE POINT. The v3 design system shipped tokens and no
   components: shared/ds/styles.css is a 20-line @import manifest with zero
   rules, which is why spacing/weight/motion had ZERO consumers while colour
   had ~700 (a lint required colour and nothing required the rest).

   Rule for this layer: a DS component NEVER carries a Tailwind utility, and a
   Tailwind utility never sets a value a token owns. Tailwind stays legal for
   layout one-offs OUTSIDE these components.

   Everything here reads tokens.css. No raw hex, no raw px for anything a
   token owns.
   ========================================================================= */

/* ── base ──────────────────────────────────────────────────────────────── */
.cx-root {
  margin: 0; min-height: 100vh;               /* a recipe puts this on <body>: no UA margin, the ground fills the canvas */
  font-family: var(--cx-font);
  background: var(--cx-ground);
  color: var(--cx-ink);
  font-size: var(--cx-body);
  line-height: var(--cx-lh-normal);
  font-variant-numeric: tabular-nums;   /* every figure aligns, no mono needed */
}
.cx-icon { width: 20px; height: 20px; stroke: currentColor; stroke-width: 1.5;
  fill: none; stroke-linecap: round; stroke-linejoin: round; flex: none; }
.cx-icon--sm { width: 15px; height: 15px; }
.cx-icon--lg { width: 26px; height: 26px; }
:where(.cx-root) :focus-visible { outline: none; box-shadow: var(--cx-focus-ring); }

/* ── shell ─────────────────────────────────────────────────────────────── */
.cx-app { display: grid; grid-template-columns: 264px 1fr; min-height: 100vh; }
.cx-app[data-rail="collapsed"] { grid-template-columns: 64px 1fr; }
.cx-stage { padding: var(--cx-s3) var(--cx-s4) var(--cx-s8); display: grid;
  gap: var(--cx-s3); align-content: start; min-width: 0; }
/* the panel row. The third panel is CONTEXTUAL — absent until an item opens. */
.cx-stage[data-panel="open"] { grid-template-columns: minmax(0, 1fr) 372px; align-items: start; }
.cx-stage:not([data-panel="open"]) > .cx-panel--sticky { display: none; }
/* REGION SEPARATION IS STRICTLY GREATER THAN SEPARATION INSIDE ONE. This was s3 (12px)
   while a panel pads at s4 (16) and its inner blocks space at 12-16 — so two whole panels
   sat closer together than two blocks inside one of them, and the only thing that made a
   region was the eyebrow's 46px margin. Where consecutive panels carry no eyebrow between
   them there was no hierarchy of breath at all. s4 is the obvious fix and it FAILS the rule
   it is meant to serve: 16 against .cx-subgroup's 16 is equal, not greater. So s5.
   .cx-stage's own gap stays at s3 — that one is the column gutter to the 372px panel, and a
   gutter is not a stack rhythm; they share a number today by accident, not by rule. */
.cx-stage-main { display: grid; gap: var(--cx-s5); min-width: 0; }

/* ── rail ──────────────────────────────────────────────────────────────── */
.cx-rail { display: flex; flex-direction: column; padding: var(--cx-s3) var(--cx-s3);
  position: sticky; top: 0; height: 100vh; }
.cx-rail-top { display: flex; align-items: center; gap: var(--cx-s2);
  padding: var(--cx-s1) var(--cx-s2) var(--cx-s3); }
.cx-rail-mark { width: 26px; height: 26px; border-radius: var(--cx-radius-sm);
  background: var(--cx-accent); color: var(--cx-ink-on-accent); display: grid;
  place-items: center; font-weight: var(--cx-w-bold); font-size: var(--cx-sm); flex: none; }
.cx-rail-name { font-size: var(--cx-h3); font-weight: var(--cx-w-semi); }
.cx-rail-name span { color: var(--cx-ink-3); font-weight: var(--cx-w-regular); }
.cx-rail-collapse { margin-left: auto; color: var(--cx-ink-3); background: none;
  border: 0; cursor: pointer; display: flex; padding: 2px; border-radius: var(--cx-radius-sm); }
.cx-rail-search { display: flex; align-items: center; gap: var(--cx-s2);
  padding: var(--cx-s2) 11px; border-radius: var(--cx-radius-md); background: var(--cx-panel);
  border: 1px solid var(--cx-line); color: var(--cx-ink-3); font-size: var(--cx-body); cursor: text; }
.cx-rail-group { font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-ink-3);
  padding: 0 var(--cx-s2); margin: var(--cx-s4) 0 var(--cx-s1); }
/* 20px from the rail's outer edge — s3 (the rail's own padding) + s2 — which is where
   .cx-rail-top's mark and .cx-rail-group's label already start. Was 9px, putting this glyph
   1px right of the label above it. Five rows that read as one column were starting on four
   different left edges: 20, 21, 22 and 24. */
.cx-rail-item { display: flex; align-items: center; gap: var(--cx-s2) ;
  padding: var(--cx-s2) var(--cx-s2); border-radius: var(--cx-radius-md); color: var(--cx-ink-2);
  text-decoration: none; font-size: var(--cx-body); }
.cx-rail-item:hover { background: var(--cx-panel); }
.cx-rail-item[aria-current] { background: var(--cx-accent-wash);
  color: var(--cx-accent-quiet); font-weight: var(--cx-w-semi); }
.cx-rail-item__count { margin-left: auto; font-size: var(--cx-sm);
  font-weight: var(--cx-w-semi); color: var(--cx-accent); }
/* in-production: present, not hidden, not fake. Never opens onto a stub. */
.cx-rail-item[aria-disabled="true"] { color: var(--cx-ink-3); cursor: default; }
.cx-rail-item[aria-disabled="true"]:hover { background: none; }
.cx-rail-item[aria-disabled="true"] .cx-icon { opacity: .55; }
.cx-rail-soon { margin-left: auto; font-size: 10px; font-weight: var(--cx-w-semi);
  letter-spacing: .05em; text-transform: uppercase; color: var(--cx-ink-3);
  border: 1px solid var(--cx-line); padding: 1px 6px; border-radius: var(--cx-radius-sm); }
/* collapsed rail — 64px, icons only. Authored 2026-08-26: until this release the
   collapsed state only narrowed the grid column and left every label painting
   over the stage; the screenshot that "verified" it had never been read.
   Labels are bare text nodes and stay in the markup so the accessible name
   survives; they are hidden by zeroing the item's font size while icons keep
   their pixel size. A consumer adds `title` on items when it collapses.
   In-production items stay present and dimmed — the Soon badge is a label, and
   the dimming carries the state on its own at this width. */
.cx-app[data-rail="collapsed"] .cx-rail { padding-left: var(--cx-s2); padding-right: var(--cx-s2); }
.cx-app[data-rail="collapsed"] .cx-rail-top { flex-direction: column; gap: var(--cx-s2);
  padding: var(--cx-s1) 0 var(--cx-s3); }
.cx-app[data-rail="collapsed"] .cx-rail-name,
.cx-app[data-rail="collapsed"] .cx-rail-soon,
.cx-app[data-rail="collapsed"] .cx-rail-stack { display: none; }
.cx-app[data-rail="collapsed"] .cx-rail-collapse { margin-left: 0; }
.cx-app[data-rail="collapsed"] .cx-rail-collapse .cx-icon { transform: scaleX(-1); }
.cx-app[data-rail="collapsed"] .cx-rail-search { font-size: 0; gap: 0; justify-content: center;
  padding: var(--cx-s2) 0; }
.cx-app[data-rail="collapsed"] .cx-rail-group { font-size: 0; height: 1px; padding: 0;
  margin: var(--cx-s3) var(--cx-s2) var(--cx-s2); background: var(--cx-line-2); }
.cx-app[data-rail="collapsed"] .cx-rail-item { font-size: 0; gap: 0; justify-content: center;
  padding: 10px 0; position: relative; }
.cx-app[data-rail="collapsed"] .cx-rail-item__count { position: absolute; top: 3px; right: 7px;
  margin: 0; font-size: 10px; line-height: 1; }
.cx-app[data-rail="collapsed"] .cx-rail-foot { padding-top: var(--cx-s2); }

/* Connections is NOT a department — it is the other half of Inputs. */
.cx-rail-foot { margin-top: auto; padding-top: var(--cx-s3); border-top: 1px solid var(--cx-line-2); }
.cx-rail-stack { display: flex; margin-left: auto; }
.cx-rail-stack > * { width: 20px; height: 20px; border-radius: var(--cx-radius-sm);
  border: 1.5px solid var(--cx-ground); margin-left: -6px; display: grid; place-items: center;
  font-size: 8.5px; font-weight: var(--cx-w-bold); background: var(--cx-panel); color: var(--cx-ink-2); }
.cx-rail-stack > *:first-child { margin-left: 0; }

/* ── panel ─────────────────────────────────────────────────────────────── */
.cx-panel { background: var(--cx-panel); border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-panel); min-width: 0; }
.cx-panel__hd { display: flex; align-items: center; gap: var(--cx-s2);
  padding: 13px var(--cx-s4); border-bottom: 1px solid var(--cx-line-2); }
.cx-panel__hd h2 { margin: 0; font-size: var(--cx-h3); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-heading); }
/* every panel carries its OWN affordance — collapse, refresh, scope, more */
.cx-panel__aff { margin-left: auto; display: flex; align-items: center; gap: var(--cx-s3);
  color: var(--cx-ink-3); font-size: var(--cx-sm); }
.cx-panel__bd { padding: var(--cx-s4); }
.cx-panel__kicker { padding: var(--cx-s3) var(--cx-s4) 0; margin: 0;
  font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-panel__kicker + .cx-panel__bd { padding-top: var(--cx-s3); }
/* icon button — a panel's own affordance as a real button: show or close the
   detail panel, refresh, more. 28px hit box inside a 44px row; the target rule
   is met by the header's height, not by the glyph. */
.cx-iconbtn { appearance: none; border: 0; background: none; color: var(--cx-ink-3);
  width: 28px; height: 28px; display: grid; place-items: center; cursor: pointer;
  border-radius: var(--cx-radius-sm); transition: background var(--cx-dur-fast) var(--cx-ease), color var(--cx-dur-fast) var(--cx-ease); }
.cx-iconbtn:hover { background: var(--cx-ground); color: var(--cx-ink); }
.cx-iconbtn[aria-pressed="true"] { color: var(--cx-accent); }
/* The desktop sticky panel needs its own ceiling: it is top-pinned and never moves, so a
   panel taller than the viewport had its lower third cut off with no way to reach it.
   Same ceiling the 901-1240px float already carried; the 900px sheet resets both.
   Owner ruling 2026-08-29. */
.cx-panel--sticky { position: sticky; top: var(--cx-s3);
  max-height: calc(100vh - var(--cx-s3) * 2); overflow: auto; }

/* ── buttons ───────────────────────────────────────────────────────────── */
.cx-btn { font: inherit; font-size: var(--cx-body); font-weight: var(--cx-w-semi);
  padding: 7px 15px; border-radius: var(--cx-radius-md); border: 1px solid var(--cx-line);
  background: var(--cx-panel); color: var(--cx-ink); cursor: pointer;
  transition: background-color var(--cx-dur) var(--cx-ease), border-color var(--cx-dur) var(--cx-ease); }
.cx-btn:hover { border-color: var(--cx-line-strong); }
.cx-btn--primary { background: var(--cx-accent); border-color: var(--cx-accent);
  color: var(--cx-ink-on-accent); }
.cx-btn--primary:hover { background: var(--cx-accent-hover); border-color: var(--cx-accent-hover); }
.cx-btn--quiet { font-size: var(--cx-sm); padding: 3px 9px; border-radius: var(--cx-radius-sm);
  color: var(--cx-ink-2); background: var(--cx-ground); }
.cx-btn[disabled] { opacity: .5; cursor: not-allowed; }

/* ── input ───────────────────────────────────────────────────────────────
   THE PACKAGE HAS NO BORDER-BOX RESET, by design — so a control that declares both
   `width: 100%` and padding must set box-sizing itself, or it overflows its container by
   exactly its own padding plus border. This one did not, and shipped 24px over
   (11 + 11 + 1 + 1) at every width above 900px; below 900px the touch block happened to
   set it, which is why it was only ever visibly wrong on desktop. Measured 2026-08-29 in
   the gallery's Inputs specimen: three fields at width 505px inside a 481px box. */
.cx-input { width: 100%; box-sizing: border-box; padding: var(--cx-s2) 11px; border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-md); font: inherit; font-size: var(--cx-body);
  color: var(--cx-ink); background: var(--cx-panel); }
.cx-input::placeholder { color: var(--cx-ink-3); }
.cx-input[aria-invalid="true"] { border-color: var(--cx-error); }

/* ── segmented control ─────────────────────────────────────────────────── */
.cx-seg { display: inline-flex; background: var(--cx-ground); border-radius: var(--cx-radius-md); padding: 2px; }
.cx-seg button { font: inherit; font-size: var(--cx-sm); padding: 4px 12px; border: 0;
  background: none; color: var(--cx-ink-2); border-radius: var(--cx-radius-sm); cursor: pointer; }
.cx-seg button[aria-pressed="true"] { background: var(--cx-panel); color: var(--cx-ink);
  font-weight: var(--cx-w-semi); }

/* ── status pill — ALWAYS mark + word, never colour alone ────────────────
   ROUNDED, NOT PILLED (owner ruling 2026-09-02: "we're not using pills at all
   unless absolutely necessary" — for the CTAs and the other containers). The
   999px radius left this family and every other CONTAINER in the package on the
   same sweep: eighteen sites moved to --cx-radius-sm, and .cx-btn to
   --cx-radius-md so a button and the input beside it agree on one corner.

   THE NAME STAYS. A "pill" here has never meant a shape — it means MARK PLUS
   WORD, never colour alone, which is the rule the class exists to carry and the
   one thing that survives greyscale. Renaming the family would cost every call
   site in every consumer to rename a corner.

   FOUR SITES KEPT THE 999px AND THEY ARE THE "absolutely necessary": the stage
   mark (.cx-step__mark / .cx-chain__mark, a 22px ring), the companion launcher
   (.cx-chat-fab, a 48px circle), today's ring on a calendar numeral, and
   .cx-person__mono — where round against square is a STATED SEMANTIC, "a role is
   square, a person is round", and flattening it would erase a distinction the
   file makes out loud. A circle is a shape; a capsule was a decoration. */
.cx-pill { display: inline-flex; align-items: center; gap: 5px; font-size: var(--cx-sm);
  font-weight: var(--cx-w-medium); padding: 3px 9px; border-radius: var(--cx-radius-sm); }
.cx-pill .cx-icon { width: 13px; height: 13px; }
.cx-pill--canon   { background: var(--cx-canon-wash);   color: var(--cx-canon); }
.cx-pill--working { background: var(--cx-working-wash); color: var(--cx-ink-2); }
.cx-pill--ok      { background: var(--cx-ok-wash);      color: var(--cx-ok); }
.cx-pill--warn    { background: var(--cx-warn-wash);    color: var(--cx-warn); }
.cx-pill--error   { background: var(--cx-error-wash);   color: var(--cx-error); }

/* ── reading — machine output ────────────────────────────────────────────
   THE AI-SIGNAL RULE:
     panel-level (cross-item) readings live INSIDE the panel they describe;
     item-level readings live in the contextual third panel with the item.
   Chat is NOT here. Chat stays in the pop-out companion. */
.cx-reading { background: var(--cx-ai); border: 1px solid var(--cx-ai-line);
  border-radius: var(--cx-radius-lg); padding: 13px 15px; }
.cx-reading__hd { display: flex; align-items: center; gap: 7px; font-size: var(--cx-sm);
  font-weight: var(--cx-w-semi); color: var(--cx-ai-ink); margin-bottom: 6px; }
/* THE BLOCK IS OPTIONAL — a container is a claim (D5, 2026-08-29), applied to the machine's
   voice. Bare drops the container so a reading blends into whatever ground it sits on. Its
   identity survives because the identity was never the tint: it is the mark plus the word,
   which is how every state in this package reads and the reason none of them is colour
   alone. Use it wherever the surrounding panel already makes it obvious the machine is
   speaking; keep the block where a reading has to separate itself from what is around it. */
.cx-reading--bare { background: none; border: 0; padding: 0; }
.cx-reading__hd .cx-icon { width: 14px; height: 14px; }
.cx-reading p { margin: 0; font-size: var(--cx-h3); line-height: var(--cx-lh-normal); }
.cx-reading__chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 11px; }
.cx-reading__chips > * { font-size: var(--cx-sm); padding: 4px 10px;
  border-radius: var(--cx-radius-sm); background: var(--cx-panel);
  border: 1px solid var(--cx-ai-line); color: var(--cx-ai-ink); }

/* ── honest absence — the product's best writing gets its own component ── */
.cx-absent { border: 1px dashed var(--cx-line); border-radius: var(--cx-radius-lg);
  padding: 13px 15px; }
.cx-absent h4 { margin: 0 0 5px; font-size: var(--cx-body); font-weight: var(--cx-w-semi); }
.cx-absent p { margin: 0; font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-2); }

/* ── the reading — hero figure ─────────────────────────────────────────── */
.cx-read { display: flex; align-items: baseline; gap: var(--cx-s4); flex-wrap: wrap; }
.cx-read__value { font-family: var(--cx-font-figure); font-size: 58px; font-weight: 400; line-height: 1;
  letter-spacing: 0; font-variant-numeric: tabular-nums; }
.cx-read__value small { font-family: var(--cx-font); }
.cx-read__value small { font-size: 22px; font-weight: var(--cx-w-medium); color: var(--cx-ink-3); margin-left: 5px; }
.cx-read__delta { display: flex; align-items: baseline; gap: 7px; }
.cx-read__delta b { font-size: 22px; font-weight: var(--cx-w-semi);
  color: var(--cx-accent); letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.cx-read__delta span { font-size: var(--cx-sm); color: var(--cx-ink-3); }
/* the tare line — the baseline every figure is measured against */
.cx-read + .cx-tare { margin-top: var(--cx-s2); }
.cx-tare { display: flex; align-items: center; gap: 9px; font-size: var(--cx-sm); color: var(--cx-ink-3); }

/* ── stat tile — when the answer is a number, not a chart ─────────────── */
.cx-tile { border: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-lg); padding: 14px 16px; }
.cx-tile dt { font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-tile dd { margin: 3px 0 0; font-family: var(--cx-font-figure); font-size: 27px; font-weight: 400; letter-spacing: 0; }
.cx-tile dd em { font-style: normal; font-size: var(--cx-h3); font-weight: var(--cx-w-semi);
  color: var(--cx-accent); margin-left: var(--cx-s2); font-family: var(--cx-font); }
.cx-tile small { display: block; font-size: var(--cx-sm); color: var(--cx-ink-3); margin-top: 3px; }

/* ── data row — the product's dominant primitive. There is no table. ───── */
.cx-rows { display: grid; }
.cx-row { display: grid; grid-template-columns: 14px 1fr 92px 120px 72px; gap: var(--cx-s3);
  /* derived, not hand-computed: 11px was (44 - 22) / 2 written out, which meant
     [data-density="compact"] remapped --cx-row and nothing read it — a live toggle
     wired to zero pixels. Comfortable still resolves to exactly 11px. */
  align-items: center; padding: calc((var(--cx-row) - 22px) / 2) var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1);
  border-bottom: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-md); cursor: pointer; }
.cx-row:last-child { border-bottom: 0; }
.cx-row:hover { background: var(--cx-ground); }
/* A WASHED SURFACE LIFTS ITS QUIET INK ONE STEP. --cx-ink-3 is validated against the two
   surfaces it was designed for — 5.06:1 on panel, 4.54:1 on ground — and the package then
   uses it on the tinted WASHES, where it lands at 4.33 on info-wash and 4.48 on
   accent-wash, both under the 4.5 floor. Check 8 could not see it because there is no
   ink-3-on-a-wash pair in its list; audit check 9 measures it as painted and does.

   These are the washed surfaces that CONTAIN sub-text. The pills and state chips are not
   here on purpose: each carries one word in its own semantic ink, already measured. The
   token is re-pointed on the container so every descendant reading var(--cx-ink-3) steps
   up without a descendant selector per component. Measured 2026-08-29. */
:is(.cx-rail-item[aria-current],
    .cx-row[aria-selected="true"],
    .cx-row--verb[aria-current="true"],
    .cx-search-hit[aria-selected="true"],
    .cx-scope__opt[data-state="current"],
    .cx-scope__chip[data-state="syncing"],
    .cx-scope__chip[data-state="restricted"],
    .cx-cal__strip .cx-cal__day[data-today],
    .cx-match[data-state="proposed"],
    .cx-geo__zone[aria-selected="true"],
    .cx-queue__to--proposed,
    .cx-docrow__read,
    .cx-timeline__needs > div[data-state="now"],
    .cx-mini > a) { --cx-ink-3: var(--cx-ink-2); }

.cx-row[aria-selected="true"],
.cx-row--verb[aria-current="true"] { background: var(--cx-accent-wash);
  box-shadow: inset 2px 0 0 var(--cx-accent); }
/* THE SELECTED ROW SAYS SO IN A WORD (v1.3.4, owner-ruled 2026-09-02). The wash and
   the inset bar are chrome: neither survives greyscale, and neither reaches a screen
   reader — measured in v1.3.3, where both this row and the verb row came back
   `role: generic` with an empty accessible name. So the selected row now leads its
   subject with the word "Showing", in the eyebrow grammar, accent-quiet on the wash
   (the pair the surface-step rule already reserves for selection). It is generated
   content, and that is deliberate: the accessibility tree includes ::before text in
   the name, so the row reads "Showing Ethiopia Guji" with no ARIA and no markup at
   any call site — every register in the package gains the word at once.

   NOT A TICK. The check glyph is spent on done, verified, go and holds up, and
   selection is chrome, not data: a mark that means "true" on the row beside it
   cannot also mean "this is the one the peek is showing". A word can.
   NO CONTAINER, by container discipline: a box around "Showing" would claim a state,
   and selection is not a state of the record.

   IT SITS ON THE SUBJECT, not in the meta line and not in the dot track, because the
   subject is the one part every row variant has: .cx-row__name on the base row, the
   status row and the verb row; .cx-doc__title's bold on the doc row. A meta line is
   optional and the dot track is 14px. The subject is nowrap-ellipsis, so a very long
   name gives up seven characters to the word; that is the trade, and the peek has the
   full name.
   AUTHORING RULE: ONE PER LIST. The word means "this one"; two of them mean nothing. */
.cx-row[aria-selected="true"] > .cx-row__name::before,
.cx-row--verb[aria-current="true"] > .cx-row__name::before,
.cx-row[aria-selected="true"] > .cx-doc__title > b::before { content: "Showing";
  display: inline-block; margin-right: 7px; vertical-align: 1px;
  font-family: var(--cx-font); font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-accent-quiet); }
.cx-row > * { min-width: 0; }
.cx-row__name { font-size: var(--cx-h3); font-weight: var(--cx-w-medium);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cx-row__name small { display: block; font-size: var(--cx-sm); font-weight: var(--cx-w-regular);
  color: var(--cx-ink-3); margin-top: 1px; }
.cx-row__qty { font-size: var(--cx-body); text-align: right; color: var(--cx-ink-2); }
.cx-row__move { font-size: var(--cx-sm); font-weight: var(--cx-w-semi); text-align: right; color: var(--cx-accent); }
.cx-row__move--flat { color: var(--cx-ink-3); font-weight: var(--cx-w-medium); }
/* the dot: an item has a reading waiting behind the click.
   OPEN RULE — the dot only signals while readings are scarce. If a normal day
   marks most rows it becomes texture. Cap it or let the legend count carry it. */
.cx-row__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--cx-ai-ink); margin-left: 3px; }
.cx-row__dot--none { background: transparent; }
.cx-rows__legend { padding: 11px 0 0; font-size: var(--cx-sm); color: var(--cx-ink-3);
  display: flex; align-items: center; gap: 7px; }

/* ── attention row — a flagged line with its one action ────────────────── */
.cx-attn__group, .cx-attn__row { list-style: none; margin: 0; padding-left: 0; }
.cx-attn__row > * { min-width: 0; }   /* the flex form can shrink its subject too, not only the queue grid */
.cx-attn__group { padding-top: var(--cx-s3); }
.cx-attn__group:first-child { padding-top: 0; }
.cx-attn__hd { display: flex; align-items: baseline; font-size: var(--cx-body); font-weight: var(--cx-w-semi); }
.cx-attn__count { margin-left: auto; font-size: var(--cx-sm); font-weight: var(--cx-w-regular); color: var(--cx-ink-3); }
.cx-attn__why { font-size: var(--cx-sm); color: var(--cx-ink-3); margin: 1px 0 4px; }
.cx-attn__row { display: flex; align-items: center; gap: var(--cx-s2); padding: var(--cx-s2) 0;
  border-bottom: 1px solid var(--cx-line-2); }
.cx-attn__row:last-child { border-bottom: 0; }
.cx-attn__dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--cx-warn); }
.cx-attn__dot--err { background: var(--cx-error); }
.cx-attn__line { flex: 1; font-size: var(--cx-body); }
.cx-attn__act { font-size: var(--cx-sm); color: var(--cx-accent); background: none; border: 0;
  cursor: pointer; padding: 0; font-weight: var(--cx-w-medium); }
.cx-attn__act:hover { text-decoration: underline; }

/* ── quiet note — an honest-absence or degraded line inside a panel ─────── */
.cx-note { font-size: var(--cx-body); color: var(--cx-ink-3); padding: var(--cx-s2) 0; }

/* ── labelled field — plain-case label over its input ───────────────────── */
.cx-field { display: block; margin-top: var(--cx-s3); font-size: var(--cx-sm); color: var(--cx-ink-2); }
.cx-field > .cx-input { margin-top: var(--cx-s1); width: 100%; }

/* ── inline checkbox control ─────────────────────────────────────────────── */
.cx-check { display: inline-flex; align-items: center; gap: 6px; font-size: var(--cx-sm);
  color: var(--cx-ink-3); cursor: pointer; user-select: none; margin-left: auto; }
/* The control itself, which was a bare native input until 2026-08-29 — so it painted in
   the OPERATING SYSTEM's accent (blue on macOS), which is both a system default the owner
   has ruled against and a hue this palette does not contain. It also measured 13x13.
   Drawn from the package now: the accent fills it, --cx-ink-on-accent draws the mark, and
   the mark is a real glyph rather than a tint, so the checked state survives greyscale and
   forced-colors like every other state in this file. The label is the hit target and
   already clears the floor; the box is sized optically against --cx-sm text. */
.cx-check input[type="checkbox"], .cx-check input[type="radio"] {
  appearance: none; -webkit-appearance: none; flex: none; margin: 0;
  width: 15px; height: 15px; box-sizing: border-box;
  display: grid; place-content: center; cursor: pointer;
  border: 1px solid var(--cx-line-strong); border-radius: 4px;
  background: var(--cx-panel);
  transition: background var(--cx-dur-fast) var(--cx-ease), border-color var(--cx-dur-fast) var(--cx-ease); }
.cx-check input[type="radio"] { border-radius: 50%; }
.cx-check input:checked { background: var(--cx-accent); border-color: var(--cx-accent); }
.cx-check input[type="checkbox"]:checked::after {
  content: ""; width: 8px; height: 4px; margin-top: -2px;
  border-left: 1.5px solid var(--cx-ink-on-accent);
  border-bottom: 1.5px solid var(--cx-ink-on-accent);
  transform: rotate(-45deg); }
.cx-check input[type="radio"]:checked::after {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--cx-ink-on-accent); }
.cx-check input:disabled { opacity: .5; cursor: not-allowed; }

/* ── subgroup — a plain-titled group inside a panel body ────────────────── */
.cx-subgroup { margin-top: var(--cx-s4); }
.cx-subgroup:first-child { margin-top: 0; }
.cx-subgroup__bd { margin-top: var(--cx-s2); }

.cx-drop__name { font-size: var(--cx-sm); color: var(--cx-ink-3); margin-top: var(--cx-s1); }

/* ── gauge — value against its target ──────────────────────────────────── */
/* 3px on a 5px bar was the token written out as a literal, which is how a family drifts —
   the same defect --cx-chart-radius was introduced to fix on .cx-progress. At 5px tall the
   radius clamps itself, so the token is safe here and the family now agrees end to end. */
.cx-gauge { height: 5px; border-radius: 0 var(--cx-chart-tip) var(--cx-chart-tip) 0; background: var(--cx-line-2); position: relative; overflow: hidden; }
.cx-gauge > i { position: absolute; inset: 0 auto 0 0; background: var(--cx-accent);
  border-radius: 0 var(--cx-chart-tip) var(--cx-chart-tip) 0; }
/* geometry by custom property, like .cx-progress and the clock track: a consumer sets --cx-bar-v
   (0..1) and --cx-bar-t on the gauge and writes no width or left */
.cx-gauge > i { width: calc(var(--cx-bar-v, 0) * 100%); }
.cx-gauge > b { left: calc(var(--cx-bar-t, 0) * 100%); }
.cx-gauge--ok > i { background: var(--cx-line-strong); }
.cx-gauge > b { position: absolute; top: -3px; bottom: -3px; width: 2px;
  background: var(--cx-ink); border-radius: 1px; }   /* the target mark */

/* ── stage chain, base — an ordered set of named stages ──────────────────
   Was "progress timeline — Capture's ingestion, stage by stage". GENERALISED
   2026-08-28 (COVERAGE.md shape 6): an ingestion run is one case of a record
   moving through named stages in order, so the two SHARE this base instead of
   the chain forking a second copy of it. Every selector below carries both
   names; .cx-steps / .cx-step keep every declaration they had and render
   unchanged. What the chain adds — six states, the evidence line, the header
   stepper, the register form, the absence grammar — is in the stage chain
   block at the end of this file. */
.cx-steps, .cx-chain { display: grid; }
.cx-step, .cx-chain__stage { display: grid; grid-template-columns: 22px 1fr auto; gap: 11px;
  align-items: start; padding: var(--cx-s2) 0; font-size: var(--cx-body); }
.cx-step__mark, .cx-chain__mark { display: grid; place-items: center; width: 22px; height: 22px;
  border-radius: var(--cx-radius-pill); border: 1.5px solid var(--cx-line);
  color: var(--cx-ink-3); font-size: 11px; }
/* --cx-ok, not --cx-canon. `done` on a chain means a PROCESS finished; canon teal is spent
   only on a human's verified stamp, and it is the most load-bearing colour in the product.
   The 2026-08-29 red-team moved this state's WORD and its mini bar to ok and stopped at the
   ring, so one state printed two hues two inches apart. Same sweep, same reasoning as the
   calendar's confirmed glyph. */
.cx-step[data-state="done"] .cx-step__mark,
.cx-chain__stage[data-state="done"] .cx-chain__mark { border-color: var(--cx-ok); color: var(--cx-ok); }
.cx-step[data-state="now"]  .cx-step__mark,
.cx-chain__stage[data-state="current"] .cx-chain__mark { border-color: var(--cx-accent); color: var(--cx-accent); }
.cx-step small, .cx-chain__name small { display: block; color: var(--cx-ink-3); font-size: var(--cx-sm);
  margin-top: 1px; line-height: var(--cx-lh-snug); }
.cx-step time, .cx-chain__when { font-size: var(--cx-sm); color: var(--cx-ink-3); }

/* ── skeleton ──────────────────────────────────────────────────────────── */
.cx-skeleton { display: grid; gap: 9px; }
.cx-skeleton > i { display: block; height: 11px; border-radius: var(--cx-radius-sm);
  background: linear-gradient(90deg, var(--cx-line-2) 25%, var(--cx-line) 37%, var(--cx-line-2) 63%);
  background-size: 340% 100%; animation: cx-shimmer 1.5s var(--cx-ease-in-out) infinite; }
@keyframes cx-shimmer { from { background-position: 120% 0 } to { background-position: -120% 0 } }
@media (prefers-reduced-motion: reduce) { .cx-skeleton > i { animation: none } }
/* A SKELETON IS THE CONTAINER IT REPLACES WITH ITS TEXT SWAPPED FOR BARS. The package drew
   four skeletons and every one was a stack of 11px bars BESIDE the shape it stood in for, so
   the layout jumped when content landed — the exact thing a skeleton exists to prevent.
   Carry `cx-skeleton` ON the real container instead of next to it and the existing `> i`
   rule, its shimmer and its reduced-motion guard all apply untouched, while the container
   supplies its own template. That matters most for .cx-row, which has FIVE templates across
   the responsive and modifier space — any hardcoded skeleton grid would be wrong at four of
   them within a release. Usage: <div class="cx-row cx-skeleton"><i></i>...</div> */
.cx-row.cx-skeleton, .cx-tile.cx-skeleton, .cx-doc__fields.cx-skeleton { gap: var(--cx-s3); }
.cx-row.cx-skeleton > i { height: 11px; }
.cx-doc__fields.cx-skeleton > i { height: 11px; }

/* ── advisory — the authored error model ─────────────────────────────────
   Neither Cortex nor ANY of the 13 corpus references authored error or
   permission-denied. These are invented. The rule that matters:
   ON A PARTIAL FAILURE THE PRIMARY ACTION STAYS ENABLED. */
.cx-advisory { border-radius: var(--cx-radius-lg); padding: 13px 15px;
  display: grid; grid-template-columns: 20px 1fr; gap: 11px; }
.cx-advisory .cx-icon { width: 18px; height: 18px; margin-top: 1px; }
.cx-advisory b { display: block; font-size: var(--cx-h3); font-weight: var(--cx-w-semi); margin-bottom: 3px; }
.cx-advisory p { margin: 0; font-size: var(--cx-body); line-height: var(--cx-lh-snug); color: var(--cx-ink-2); }
/* THE NOTE — a fact, not a state, so it carries no container. This form was already in use
   four times across two files and existed only by the ABSENCE of a modifier: no name, no
   comment, no specimen, never rendered under the audit. A variant nobody can find is one the
   next author reaches past — they take --warn and paint a fact as a caution, which is
   "urgency is a budget" failing in its purest form. The paint is unchanged and correct under
   the container ruling; only the name is new. */
.cx-advisory--note { color: var(--cx-ink); }
.cx-advisory--warn  { background: var(--cx-warn-wash);
  border: 1px solid color-mix(in oklab, var(--cx-warn) 25%, transparent); color: var(--cx-warn); }
.cx-advisory--error { background: var(--cx-error-wash);
  border: 1px solid color-mix(in oklab, var(--cx-error) 25%, transparent); color: var(--cx-error); }

/* ── empty — ONE register, held everywhere: name what is missing and why ──
   THE SENTENCE DECIDES THE COMPONENT. Four states of an operations list are four different
   sentences and the package could write two of them:
     nothing yet ......... .cx-empty          "go connect something"
     failed to load ...... .cx-advisory--error
     still loading ....... .cx-skeleton
     filter matched none . .cx-empty--filtered   <- was not expressible
     nothing selected .... .cx-empty--awaiting   <- was .cx-empty, wrongly
     does not apply here . .cx-absent--na        <- was BOTH .cx-empty and .cx-absent,
                                                    on one page, for one fact
   None of the three additions needs a hue, a token or a glyph the sprite does not carry. */
.cx-empty--filtered .cx-icon { color: var(--cx-ink-3); }
/* the reader is not blocked, they simply have not picked yet — so it is quieter than an
   empty, and it does not offer a primary verb */
.cx-empty--awaiting { color: var(--cx-ink-3); }
.cx-empty--awaiting b { font-weight: var(--cx-w-medium); color: var(--cx-ink-2); }
/* "not applicable here" is still an ABSENCE, so it keeps .cx-absent's dashed grammar; it is
   simply an absence with a reason rather than a gap to be filled */
.cx-absent--na { border-style: dashed; }
.cx-absent--na h4 { color: var(--cx-ink-2); }

/* ── empty — ONE register, held everywhere: name what is missing and why ── */
.cx-empty { padding: var(--cx-s7) var(--cx-s5); text-align: center; }
.cx-empty .cx-icon { color: var(--cx-ink-3); margin-bottom: var(--cx-s2); }
.cx-empty b { display: block; font-size: var(--cx-h3); font-weight: var(--cx-w-semi); margin-bottom: 5px; }
.cx-empty p { margin: 0 auto var(--cx-s3); font-size: var(--cx-body);
  color: var(--cx-ink-2); line-height: var(--cx-lh-snug); max-width: 400px; }

/* ── drop target ───────────────────────────────────────────────────────── */
.cx-drop { border: 1.5px dashed var(--cx-line); border-radius: 14px;
  padding: 30px var(--cx-s5); text-align: center; }
/* block-display svg resets (Tailwind preflight) would pin it left */
.cx-drop > .cx-icon { margin: 0 auto var(--cx-s1); }
.cx-drop[data-dragover="true"] { border-color: var(--cx-accent); background: var(--cx-accent-wash); }
.cx-drop b { display: block; font-size: 15px; font-weight: var(--cx-w-semi); margin-bottom: 4px; }
.cx-drop p { margin: 0 auto var(--cx-s3); font-size: var(--cx-body);
  color: var(--cx-ink-2); max-width: 400px; line-height: var(--cx-lh-snug); }
/* A FILE INPUT MUST EXIST. Import was paste-only in v3 — zero type="file"
   anywhere in the repo — against a demo that opens on a POS upload. */
.cx-drop__alts { display: flex; gap: 9px; justify-content: center; align-items: center; }

/* ── modal — blurred scrim, commit bar OUTSIDE the scrolling body ──────── */
.cx-scrim { position: fixed; inset: 0; background: var(--cx-scrim);
  backdrop-filter: blur(var(--cx-scrim-blur)); z-index: 40; }
.cx-modal { position: fixed; z-index: 41; top: 56px; left: 50%; transform: translateX(-50%);
  width: 620px; max-width: calc(100vw - 40px); max-height: calc(100vh - 112px);
  background: var(--cx-panel); border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-modal); box-shadow: var(--cx-shadow-modal);
  display: flex; flex-direction: column; overflow: hidden; }
.cx-modal__hd { padding: var(--cx-s4) var(--cx-s5) var(--cx-s3); }
.cx-modal__hd h3 { margin: 0; font-size: 17px; font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-heading); }
.cx-modal__hd p { margin: 4px 0 0; font-size: var(--cx-body); color: var(--cx-ink-3); }
.cx-modal__bd { padding: 4px var(--cx-s5) var(--cx-s4); overflow: auto; }
.cx-modal__ft { padding: 13px var(--cx-s5); border-top: 1px solid var(--cx-line-2);
  display: flex; align-items: center; gap: var(--cx-s2); background: var(--cx-panel); }
.cx-modal__ft .cx-modal__note { font-size: var(--cx-sm); color: var(--cx-ink-3); margin-right: auto; }

/* ── home ──────────────────────────────────────────────────────────────── */
.cx-home { max-width: 780px; margin: 0 auto; padding: 64px var(--cx-s4) var(--cx-s8); text-align: center; }
/* the ONLY use of the display face anywhere in the product */
.cx-wordmark { font-family: var(--cx-font-display); font-variation-settings: "wdth" 100, "opsz" 72;
  font-weight: var(--cx-w-bold); font-size: var(--cx-display); line-height: var(--cx-lh-display);
  letter-spacing: var(--cx-tr-display); color: var(--cx-accent); margin: 0; }
.cx-home__logo { display: block; width: min(20rem, 70%); height: auto; margin: 0 auto; }
.cx-home__greet { margin: var(--cx-s3) 0 0; font-size: 15px; color: var(--cx-ink-2); }
.cx-eyebrow { font-size: var(--cx-xs); font-weight: var(--cx-w-semi); letter-spacing: var(--cx-tr-wide);
  text-transform: uppercase; color: var(--cx-ink-3); margin: 46px 0 var(--cx-s2); text-align: left; }
.cx-ctas { display: grid; grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
  gap: var(--cx-s3); text-align: left; }
.cx-cta { background: var(--cx-panel); border: 1px solid var(--cx-line); border-radius: 14px;
  padding: 15px 16px; cursor: pointer; text-align: left; font: inherit; color: var(--cx-ink);
  transition: border-color var(--cx-dur) var(--cx-ease), box-shadow var(--cx-dur) var(--cx-ease); }
.cx-cta:hover { border-color: var(--cx-accent); box-shadow: var(--cx-shadow-hover); }
.cx-cta__v { font-size: 25px; font-weight: var(--cx-w-semi); letter-spacing: -.03em;; font-family: var(--cx-font-figure); font-weight: 400; letter-spacing: 0; }
.cx-cta__v em { font-style: normal; font-size: var(--cx-body); font-weight: var(--cx-w-semi);
  color: var(--cx-accent); margin-left: 7px; }
.cx-cta__l { font-size: var(--cx-h3); margin-top: 2px; }
.cx-cta__s { font-size: var(--cx-sm); color: var(--cx-ink-3); margin-top: 3px; }
/* Home's readings — the same readings, shrunk. Home decides nothing. */
.cx-mini { display: grid; gap: 9px; text-align: left; }
.cx-mini > a { display: grid; grid-template-columns: 16px 1fr auto; gap: 11px; align-items: start;
  padding: var(--cx-s3) 14px; border-radius: var(--cx-radius-lg); background: var(--cx-ai);
  border: 1px solid var(--cx-ai-line); text-decoration: none; color: var(--cx-ink); }
.cx-mini > a .cx-icon { width: 15px; height: 15px; color: var(--cx-ai-ink); margin-top: 1px; }
.cx-mini p { margin: 0; font-size: var(--cx-h3); line-height: var(--cx-lh-snug); }
.cx-mini span { font-size: var(--cx-sm); color: var(--cx-ink-3); white-space: nowrap; padding-top: 2px; }

/* ── connection tile ───────────────────────────────────────────────────── */
.cx-conns { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: var(--cx-s2); }
.cx-conn { display: flex; align-items: center; gap: var(--cx-s3); padding: 13px 14px;
  border: 1px solid var(--cx-line); border-radius: 13px; background: var(--cx-panel); }
button.cx-conn { font: inherit; color: inherit; text-align: left; width: 100%; cursor: pointer; }
.cx-conn__logo { width: 34px; height: 34px; border-radius: var(--cx-radius-md);
  display: grid; place-items: center; font-weight: var(--cx-w-bold); font-size: var(--cx-sm);
  background: var(--cx-ground); color: var(--cx-ink-2); border: 1px solid var(--cx-line); flex: none; }
/* monogram marks — complete by default (2026-08-26, owner: "complete it"). A mark
   with no vendor colour is a quiet counter-surface chip with ink-2 letters (a
   panel chip in the rail, a ground chip on a panel) — vendor logos are never
   redrawn, so this is the honest placeholder. A call site that supplies a vivid
   brand fill adds `cx-mark--vivid` so the ink goes white in both themes; the
   call site owns that fill's contrast. */
.cx-mark--vivid { color: var(--cx-ink-on-vivid); }
.cx-conn b { display: block; font-size: var(--cx-h3); font-weight: var(--cx-w-semi); }
.cx-conn small { display: block; font-size: var(--cx-sm); color: var(--cx-ink-3); margin-top: 1px; }
.cx-conn--off { border-style: dashed; }
.cx-conn--off .cx-conn__logo { filter: grayscale(1); opacity: .5; }

/* ── chat launcher — the persistent companion affordance ────────────────
   Owner ruling 2026-08-03 (round 3, R7; supersedes the round-2 removal): a
   floating launcher on EVERY surface, accent-derived — the client accent,
   never violet — DS radius/elevation, sparkle mark; opens the pop-out
   companion. One obvious click opens chat. */
.cx-chat-fab { position: fixed; right: 20px; bottom: 20px; z-index: 50;
  width: 48px; height: 48px; border-radius: var(--cx-radius-pill);
  background: var(--cx-accent); color: var(--cx-ink-on-accent);
  border: 0; cursor: pointer; display: flex; align-items: center; justify-content: center;
  box-shadow: var(--cx-shadow-popover);
  transition: transform var(--cx-dur) var(--cx-ease), box-shadow var(--cx-dur) var(--cx-ease); }
.cx-chat-fab:hover { transform: translateY(-1px); box-shadow: var(--cx-shadow-hover), var(--cx-shadow-popover); }

/* ── icon hover ───────────────────────────────────────────────────────────
   THE RULE: an icon may move on hover only when the motion REHEARSES ITS OWN VERB — the
   movement previews what the click will do. Not feedback, not delight. Three consequences
   fall out of it, and they are why this section is one rule and not thirty:

     - a NOUN cannot be previewed, so it never moves (i-document, i-supplier, i-item,
       i-inventory, the channel glyphs);
     - NAVIGATION already carries state motion and does not get hover motion on top
       (i-chevron, i-collapse-rail, i-toggle-panel, i-close, i-more — the chevrons already
       rotate on aria-expanded). State beats hover;
     - a STATE MARK must never move, ever (i-check, i-verified, i-warn, i-error, i-pending,
       i-locked). Motion on a state mark implies the state is changing, and canon teal marks
       verified truth — a verified fact that twitches under the pointer is a lie about the
       data. Same reasoning as "depth groups, it never means".

   Censused every <use> in the package against that rule (2026-08-29). i-import and i-export
   have ZERO interactive instances, so a rule for them would ship as dead CSS; i-capture's
   eight are all rail navigation. Exactly one glyph qualifies today — i-refresh inside
   .cx-iconbtn, 11 sites — and the scoping is load-bearing: the same glyph is a STATUS mark
   ("Syncing") in .cx-scope__state, .cx-doc__state and .cx-pill--working 12 more times, and a
   looser selector would have animated those, saying that hovering a row refreshes it.

   Guarded on (hover: hover) because the package had no such guard anywhere and a hover
   transform sticks on touch. Routed through --cx-dur-fast, which tokens.css already zeroes
   under prefers-reduced-motion, so no new @media block is needed. */
@media (hover: hover) {
  .cx-iconbtn .cx-icon { transition: transform var(--cx-dur-fast) var(--cx-ease); }
  /* the wind-up, not the spin: a full turn reads as "already running", which is the busy
     state's job, not hover's */
  .cx-iconbtn:hover .cx-icon:has(use[href$="i-refresh"]) { transform: rotate(-38deg); }
  /* the pre-existing FAB lift, now guarded too — it stuck on touch before */
  .cx-chat-fab { transition: transform var(--cx-dur) var(--cx-ease), box-shadow var(--cx-dur) var(--cx-ease); }
}
.cx-chat-fab .cx-icon { width: 22px; height: 22px; }
.cx-chat-fab--busy { animation: cx-fab-breathe 1.6s ease-in-out infinite; }
@keyframes cx-fab-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) {
  .cx-chat-fab { transition: none; }
  .cx-chat-fab--busy { animation: none; }
}

/* ── advisory footer — stays on every advisory surface ─────────────────── */
.cx-footnote { text-align: center; font-size: var(--cx-sm); color: var(--cx-ink-3); padding-top: var(--cx-s1); }

/* ── section tabs — a department's sub-navigation ────────────────────────
   Lives in the DS rather than beside its first consumer because it is shared
   by construction: every department grows a sub-nav, and a copy per department
   is how four sub-navs end up looking like four different products.
   Ported 2026-08-26 from another instance's copy of this file (which wrote it
   against three v3 token names; they are v4 names here, and the hover fill
   follows the v4 convention — an element on a panel hovers to the ground).

   TWO STATES, ONE OF WHICH IS THE POINT. A `--soon` tab is a screen that will
   exist and does not yet. It is deliberately still legible — dimmed, not
   ghosted — because its whole job is to be read. Hiding unbuilt screens makes a
   half-finished department look finished, which is the more expensive lie. */
.cx-tabs { display: flex; align-items: stretch; gap: 2px; flex-wrap: wrap;
  border-bottom: 1px solid var(--cx-line); margin-bottom: var(--cx-s4); }
.cx-tab { appearance: none; border: 0; background: none; font: inherit;
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); color: var(--cx-ink-2);
  padding: 10px 12px; min-height: 44px; cursor: pointer; position: relative;
  border-radius: var(--cx-radius-sm) var(--cx-radius-sm) 0 0; white-space: nowrap;
  transition: color var(--cx-dur-fast) var(--cx-ease), background var(--cx-dur-fast) var(--cx-ease); }
.cx-tab:hover { color: var(--cx-ink); background: var(--cx-ground); }
/* The selected rail sits ON the container's bottom border rather than above it,
   so the tab reads as attached to the panel it controls. */
.cx-tab[aria-selected="true"] { color: var(--cx-accent); font-weight: var(--cx-w-semi); }
.cx-tab[aria-selected="true"]::after { content: ""; position: absolute;
  left: 8px; right: 8px; bottom: -1px; height: 2px; background: var(--cx-accent);
  border-radius: 2px 2px 0 0; }
.cx-tab:focus-visible { outline: 2px solid var(--cx-accent); outline-offset: -2px; }
/* not-allowed, not `disabled`: the element stays in the accessibility tree and
   in the Tab order so the screen it names can still be discovered. */
.cx-tab--soon { color: var(--cx-ink-3); cursor: not-allowed; }
.cx-tab--soon:hover { background: none; color: var(--cx-ink-3); }
.cx-tab__soon { margin-left: 6px; font-size: 10px; font-weight: var(--cx-w-semi);
  letter-spacing: .06em; text-transform: uppercase; color: var(--cx-ink-3);
  border: 1px solid var(--cx-line-2); border-radius: 3px; padding: 1px 4px;
  vertical-align: 1px; }
@media (max-width: 900px) {
  .cx-tabs { flex-wrap: nowrap; overflow-x: auto; }
}

/* ── chart layer — the ByeWind register, ruled the DEFAULT 2026-08-28 ────
   Authored 2026-08-26 from the chart spec (runs/cortex 08-26 DS log); RE-CUT 2026-08-28 when
   the owner ruled the ByeWind register — until now `cx-chart--bw` — the default chart form.
   `.cx-chart` with no modifier IS the register. The two earlier forms survive, opt-in and
   still rendering: `cx-chart--plain` is the 08-26 form (hairline frame, straight 2px lines,
   the words on the plot) and `cx-chart--grid` is the r19 fusion (the plain form plus a dotted
   grid and a numbered value scale). `cx-chart--bw` and `cx-chart--card` are ALIASES that
   paint nothing — kept for ONE RELEASE so today's pages keep rendering, then deleted.

   THE SIX BENDS, ruled 2026-08-28. Each was a DS rule the register broke on purpose; each is
   now a rule itself, and each is named again at the block that carries it.

     1  SMOOTHING.     The line is a smoothed path — every segment a cubic, no corners. The
                       plot is the shape; the TABLE TWIN carries the true values. That is why
                       the twin is not optional in this register: smoothing puts ink where no
                       reading was taken, so the figures have to be reachable somewhere exact.
     2  3PX STROKE.    The series line is 3px, not the 2px `--cx-chart-stroke` the plain form
                       keeps. A small-multiple cell steps down to 2px so the family holds at
                       cell scale. The two weights are literals here on purpose: the tokens
                       stay the plain form's contract and the reference template's.
     3  GRADIENT WASH. A vertical fade under a SINGLE-series line, and never under two — two
                       washes overlap and the overlap reads as a third value. It is the one
                       gradient in the system and it is chrome, not a mark.
     4  SHADOWED CARD. Radius 24, padding 24, gap 24, and a half-pixel shadow. CHARTS ARE THE
                       ONE SHADOWED THING IN THE PANEL WORLD. Everything else separates by
                       ground contrast, radius and a hairline, because everything else sits IN
                       the plane — a row, a panel, a field is the surface. A plot is a picture
                       of something that is not in the room, so it is allowed to float. And
                       because only charts float, the shadow stays a signal instead of a style:
                       the moment a second component takes it, it means nothing.
     5  CAPSULES.      Bars and columns are capsules, both ends soft, quiet grey by default
                       with the subject in ink. The 08-26 rule — 2px on the data end, square at
                       the baseline — is still right for a dense data row and moves intact to
                       `cx-chart--plain`.
     6  THE WORD IN THE CAPTION. The plot carries no words. The end label, the target label,
                       the flag word and the band label all move to `.cx-chart__cap`, which
                       carries the unit and the noun. Axis ticks stay — they name positions,
                       not values. Status is still MARK PLUS WORD: the word is one line lower,
                       not gone, and the delta pill keeps its glyph and its word.

   Added with the ruling: a LAST-LOOK timestamp in the header of every card (`.cx-chart__last`,
   small, ink-3, the same place on every card). It is the header half of the since-marker —
   the rule on the plot says where, the header says when — and it is what makes the surface
   answer "what changed since I last looked" without a sentence.

   What did NOT bend, still binding from lane.md: two series is a COMPUTED ceiling (beyond
   two, facet); series ink is house-owned and never client-themed; no third hue — the radial
   forms' extra slices step through the neutral ramp (ink-3, line, line-2); a semantic hue is
   never a series; one axis, never two; and NO Y AXIS is drawn, so left padding is zero.
   Copper and amber never appear. Departure Mono is display figures only (`--cx-font-figure`).
   Dashing still means NOT MEASURED — the register's dotted target is the one exception and it
   is chrome, not a mark, which is why it is a hairline in `--cx-line` and never in series ink.
   The panel world is unchanged outside the chart card: hairline panels on a tinted ground.

   The package ships no runtime. The app fills the reference SVG (templates/) and binds pointer
   events to `.cx-chart__hit`. Nothing draws itself in — the only motion is an opacity fade
   whose duration is a token, so reduced motion switches it off. No SMIL, ever.
   ByeWind's file is all rights reserved: described and re-derived, never copied. */

/* ── RULE: STATUS HUES NEVER PAINT CHART GEOMETRY (owner ruling 2026-09-02) ─────────
   Two colour registers live in this package and they do not mix on a plotted shape.
   The CHART register (--cx-series-1, --cx-series-2, the outline) encodes QUANTITY — a
   segment, a bar, a slice, a line. The STATUS register (--cx-ok, --cx-warn, --cx-error,
   --cx-canon, --cx-info) encodes a WORD — done, due, wrong, verified — and it lives on
   pills, chips, chain marks, clock ticks and the words beside them. A status hue on a
   bar or a slice reads as a third series and fights the two real ones (the settled money
   track, 2026-09-02, was the case). So: geometry wears series paint only; state is said
   in a word beside it, and the geometry shows the state by SHAPE — a bar that is all one
   segment, a slice that is the whole ring, a dashed empty track for an absence. */
/* ── the frame ───────────────────────────────────────────────────────────
   BEND 4. One number repeated: 24 of padding, 24 of gap, 24 of radius. The blocks stack
   header (32) · figure (40) · plot (196) and the card's height is the sum, so a row of cards
   lines up without a grid telling them to. */
.cx-chart { margin: 0; display: grid; gap: var(--cx-s5); min-width: 0;
  background: var(--cx-panel); border-radius: 24px; padding: var(--cx-s5);
  box-shadow: var(--cx-shadow-card); }
.cx-chart__hd { display: flex; align-items: baseline; gap: var(--cx-s3); flex-wrap: wrap; min-height: 32px; }
.cx-chart__title { margin: 0; font-size: var(--cx-body); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-heading); }
/* the last-look stamp — same place on every card, and the only thing allowed to the right of
   the title. Not a status, so no mark: it is a timestamp, and it is quiet. */
.cx-chart__last { margin-left: auto; font-size: var(--cx-sm); color: var(--cx-ink-3);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
/* the unit eyebrow is the plain form's; in the register the unit rides the caption (BEND 6) */
.cx-chart__unit { display: none; font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-ink-3); }
.cx-chart__legend { margin-left: auto; display: flex; gap: var(--cx-s3); font-size: var(--cx-sm); color: var(--cx-ink-2); }
.cx-chart__legend i { display: inline-block; width: 12px; height: 2px; vertical-align: middle; margin-right: 6px; border-radius: 1px; }
.cx-chart__legend i[data-series="1"] { background: var(--cx-series-1); }
.cx-chart__legend i[data-series="2"] { background: var(--cx-series-2); }

/* ── the figure block — the reading, before the plot ─────────────────────
   The register puts the answer above the picture: one display figure in Departure Mono and a
   delta pill that carries a glyph AND a word, because status is never colour alone. */
.cx-chart__figure { display: flex; align-items: baseline; gap: var(--cx-s3); min-height: 40px; flex-wrap: wrap; }
.cx-chart__figure b { font-family: var(--cx-font-figure); font-size: 34px; font-weight: 400; line-height: 1; letter-spacing: 0; }
.cx-chart__figure small { display: none; }
.cx-chart__delta { display: inline-flex; align-items: center; gap: 4px; font-size: var(--cx-sm); font-weight: var(--cx-w-semi);
  padding: 3px 9px; border-radius: var(--cx-radius-sm); font-variant-numeric: tabular-nums; }
/* up and down are STATES and carry mark + word + wash, which is the full grammar. --flat is
   the ABSENCE of a direction: no mark, so under "mark and word, never colour alone" it was
   the one variant reading as colour and a container only. A container is a claim, and
   "day 27 of 31" claims nothing — it is a fact. It loses the box and keeps the geometry so
   a figure row does not shift when a delta turns directional. */
.cx-chart__delta--up   { background: var(--cx-ok-wash); color: var(--cx-ok); }
.cx-chart__delta--down { background: var(--cx-error-wash); color: var(--cx-error); }
.cx-chart__delta--flat { background: none; color: var(--cx-ink-3);
  font-weight: var(--cx-w-regular); padding-left: 0; padding-right: 0; }

/* ── the plot ────────────────────────────────────────────────────────── */
.cx-chart__plot { display: block; width: 100%; height: auto; overflow: visible; font-family: inherit; }
.cx-chart__g { animation: cx-chart-in var(--cx-dur) var(--cx-ease) both; }
@keyframes cx-chart-in { from { opacity: 0; } to { opacity: 1; } }
.cx-chart[data-loading="refetch"] .cx-chart__plot { opacity: .55; transition: opacity var(--cx-dur-slow) var(--cx-ease); }
/* marks — every one paints from a class; a template carries no fill or stroke attribute */
.cx-chart__band { fill: var(--cx-line-2); }
.cx-chart__grid { stroke: var(--cx-line-2); stroke-width: var(--cx-chart-hair); fill: none; }
/* no floor rule in the register: the capsule bars sit on nothing and the line runs edge to edge */
.cx-chart__base { display: none; stroke: var(--cx-line-2); stroke-width: var(--cx-chart-hair); fill: none; }
/* the target is a dotted hairline in --cx-line-strong — chrome weight, not data weight, and
   never in series ink, so the dash cannot be read as "not measured" (that rule governs MARKS).
   line-strong, not line: at 1px under a wash, --cx-line disappears, and a reference level the
   caption names but the reader cannot find is worse than no reference level. */
.cx-chart__target { stroke: var(--cx-line-strong); stroke-width: var(--cx-chart-hair); stroke-dasharray: 3 4; fill: none; }
.cx-chart__target-tick { display: none; fill: var(--cx-ink); }
.cx-chart__since { stroke: var(--cx-line-strong); stroke-width: var(--cx-chart-hair); fill: none; }
/* BEND 2 — 3px, and 2px in a small-multiple cell */
.cx-chart__series { fill: none; stroke-width: 3px; stroke-linejoin: round; stroke-linecap: round; }
.cx-chart__series--1 { stroke: var(--cx-series-1); }
.cx-chart__series--2 { stroke: var(--cx-series-2); }
.cx-chart__series--sm { stroke-width: 2px; }
.cx-chart__series--partial { stroke-dasharray: 2 4; }   /* an incomplete tail: not measured */
/* BEND 3 — the one gradient in the system, and it is chrome. Defined ONCE per document as
   `#cx-area-fade` (`.cx-chart__defs`, the same consumer-owned coupling the icon sprite already
   has) and referenced from CSS, so no mark carries a fill attribute. The flat colour after the
   url() is the SVG paint fallback: a page that forgets the defs block gets a quiet tint, never
   an invisible fill — which is exactly the defect that shipped once and must not ship twice. */
.cx-chart__defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.cx-chart__area { fill: url(#cx-area-fade) color-mix(in oklab, var(--cx-series-1) 14%, transparent); }
.cx-chart__area--fade { fill: url(#cx-area-fade) color-mix(in oklab, var(--cx-series-1) 14%, transparent); }
/* never under two series: two washes overlap and the overlap reads as a third value */
.cx-chart[data-series="2"] .cx-chart__area { display: none; }
.cx-chart__dot { fill: var(--cx-series-1); stroke: var(--cx-panel); stroke-width: var(--cx-chart-ring); }
.cx-chart__flag { r: 5; stroke: var(--cx-panel); stroke-width: var(--cx-chart-ring); }
.cx-chart__flag--warn { fill: var(--cx-warn); }
.cx-chart__flag--error { fill: var(--cx-error); }
/* BEND 6 — no words on the plot. These classes stay defined so `--plain` and `--grid` can turn
   them back on and so a consumer's template keeps one vocabulary across all three forms. */
.cx-chart__label { display: none; fill: var(--cx-ink); font-size: var(--cx-sm); font-weight: var(--cx-w-semi); font-variant-numeric: tabular-nums; }
.cx-chart__note { display: none; fill: var(--cx-ink-2); font-size: var(--cx-sm); font-weight: var(--cx-w-medium); font-variant-numeric: tabular-nums; }
/* ticks name positions, not values, so they stay — set with air, sentence case, no tracking */
.cx-chart__tick { fill: var(--cx-ink-3); font-size: 11px; font-weight: var(--cx-w-medium);
  letter-spacing: 0; text-transform: none; font-variant-numeric: tabular-nums; }
.cx-chart__tickchip { fill: var(--cx-ink); }
.cx-chart__tick--chip { fill: var(--cx-panel); font-weight: var(--cx-w-semi); }
.cx-chart__hit { fill: transparent; }
.cx-chart__hit:focus-visible { outline: none; fill: color-mix(in oklab, var(--cx-accent) 12%, transparent); }
/* the caption is where the words went (BEND 6): the unit, the noun, the target's value, the
   flag's sentence, the illustrative marker */
.cx-chart__cap { font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-3); margin-top: -6px; }
.cx-chart__cap em { font-style: normal; color: var(--cx-ink-3); }

/* ── the table twin — mandatory, and load-bearing under BEND 1 ───────────
   Semantic table markup dressed as the data row. `<details>` is the toggle: a real button in
   the accessibility tree, closed by default, in the DOM either way. A smoothed line is an
   approximation of the readings; this is where the readings actually are. */
/* the summary is .cx-fold's — see DISCLOSURE at the foot of this file. It carries the
   chevron, the -90 rest, and the COUNT, which this construction had none of across its
   34 instances: "Show the figures" could have been four rows or four hundred. */
.cx-chart__table table { width: 100%; border-collapse: collapse; font-size: var(--cx-sm); font-variant-numeric: tabular-nums; margin-top: var(--cx-s1); }
.cx-chart__table th { text-align: left; font-size: var(--cx-xs); font-weight: var(--cx-w-semi); letter-spacing: var(--cx-tr-wide);
  text-transform: uppercase; color: var(--cx-ink-3); padding: 6px 0; border-bottom: 1px solid var(--cx-line-2); }
.cx-chart__table td { padding: 6px 0; border-bottom: 1px solid var(--cx-line-2); color: var(--cx-ink-2); }
.cx-chart__table td:last-child, .cx-chart__table th:last-child { text-align: right; }

/* ── states — the frame holds its height so the card does not jump ────── */
/* THE ROW LIST'S STATE WELL. .cx-chart__state and .cx-geo__state both had one; .cx-rows —
   "the product's dominant primitive, there is no table" — had nowhere to put a state, so a
   list that lands empty, fails or filters to nothing collapsed the panel under the reader's
   finger. Declared as .cx-chart__state's twin at a row-list height. */
.cx-rows__state { min-height: 132px; display: grid; align-content: center; }
.cx-rows__state .cx-empty { padding: var(--cx-s4) var(--cx-s3); }

.cx-chart__state { min-height: 196px; display: grid; align-content: center; }
.cx-chart__state .cx-empty { padding: var(--cx-s4) var(--cx-s3); }
.cx-chart--loading .cx-chart__skel { height: 180px; border-radius: var(--cx-radius-md); }

/* ── bar plot — horizontal by default; a data row that carries a bar ──────
   Built 2026-08-28 against the Figma read of three kits (vault run log the same day).
   The categories are named units, so the name sits flush left and the VALUE sits flush right
   in a fixed column — never at the bar's tip, so values align on one edge and cannot collide.
   The bar's value axis always includes zero. The HIT TARGET and the hover wash are the whole
   band, never the mark. Emphasis is SUBTRACTION: the subject is ink, every other bar and its
   value stay quiet; nothing is brightened. A target shared by every row spans the plot as a
   hairline; a target that differs per row is a tick inside that row's band — a rule drawn
   across bars with different targets asserts a level that does not exist.
   BEND 5 — the fill is a capsule at 24px, both ends soft, in --cx-line-2 with the subject in
   --cx-ink. Grey-by-default is what makes subtraction free: the others are already quiet. */
.cx-bars { display: grid; }
.cx-bar { display: grid; grid-template-columns: minmax(96px, 150px) 1fr 72px; align-items: center;
  gap: var(--cx-s3); min-height: 40px; padding: 0 var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1);
  border-radius: var(--cx-radius-md); font: inherit; text-align: left; background: none; border: 0;
  /* NO WIDTH. The padding-plus-negative-margin bleed idiom has six sites in this file and
     five carry none — a grid item stretches on its own. `calc(100% + var(--cx-s4))` under
     content-box (the package sets no border-box reset) painted 8px of bleed on the left and
     24px on the right instead of 8 and 8. */
  color: var(--cx-ink); cursor: default; position: relative; }
.cx-bar[tabindex], button.cx-bar { cursor: pointer; }
.cx-bar:hover, .cx-bar:focus-visible { background: var(--cx-panel-sunken); outline: none; }
.cx-bar__name { font-size: var(--cx-body); color: var(--cx-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cx-bar__track { position: relative; height: 24px; }
/* THE ANCHORED END IS SQUARE. A bar grows from a zero baseline, so the end it is anchored
   to must meet that baseline flat; only the free end is capped. A fully pilled bar detaches
   from its own axis — at small values it stops reading as a quantity and starts reading as
   a dot, which is exactly what "House Blend 0.9 kg" did. Owner, 2026-08-29, on the chart
   set: "insane how we overlooked the inconsistencies with these charts."

   D4 IS RETIRED, same day, by the register ruling. Every end is now square at
   --cx-chart-radius, so there is no capsule left to detach from the axis and nothing for
   the anchored/free distinction to carry. The DEFECT D4 named is still fixed — it is fixed
   by the radius being 2px rather than by which corners get it. Kept here as the reason the
   family is square, because a rule deleted without its reason grows back. */
.cx-bar__fill { position: absolute; left: 0; top: 0; height: 24px; width: calc(var(--cx-bar-v) * 100%);
  background: var(--cx-line-2);
  border-radius: 0 var(--cx-chart-tip) var(--cx-chart-tip) 0;
  border: var(--cx-chart-outline) solid var(--cx-chart-outline-ink); box-sizing: border-box;
  min-width: 4px; }
.cx-bar--subject .cx-bar__fill { background: var(--cx-ink); }
/* per-row target: hidden in the register — a per-row level is a sentence in the caption here.
   `--plain` and `--grid` draw it as a 2x12 tick, which is where a dense data row wants it. */
.cx-bar__tick { display: none; position: absolute; top: 4px; width: 2px; height: 12px; background: var(--cx-ink);
  left: calc(var(--cx-bar-t) * 100%); }
/* PROGRESS TO TARGET IS A RANKED BAR WITH ITS TICK SHOWING, not a shape of its own. The
   package already had every part: .cx-bar's three-column grid, .cx-bar__fill, and
   .cx-bar__tick reading --cx-bar-t, which was authored then hidden because a per-row level
   is usually a sentence in the caption. When the target IS the subject it stops being a
   footnote, so one modifier turns it back on. This retires the bespoke .cx-progress__row +
   thin .cx-progress construction for this shape: a 12px bar with its label stacked above it
   was a second bar family living beside the first, which is what made it read as foreign
   the moment the register landed. Owner, 2026-08-30: "it should look like green on hand's
   bars but with the progress indicator." */
/* ONE marker for one target. The first pass drew a dashed line as well as the tick, which is
   two devices saying the same thing — the drift this package keeps catching elsewhere. The
   tick is the authored device, so it stays and simply gets tall enough to read as a target
   line instead of a 12px stub inside a 24px track. */
.cx-bars--target .cx-bar__tick { display: block; top: -3px; bottom: -3px; height: auto; }
/* the fill carries the accent here, as the thin gauge did — a progress bar has no ranking to
   express, so there is no subject to promote and nothing to keep quiet. */
.cx-bars--target .cx-bar__fill { background: var(--cx-accent); }
.cx-bar__value { font-size: var(--cx-sm); font-weight: var(--cx-w-medium); text-align: right;
  font-variant-numeric: tabular-nums; color: var(--cx-ink-3); }
.cx-bar__value small { font-weight: var(--cx-w-regular); color: var(--cx-ink-3); margin-left: 4px; }
.cx-bar--subject .cx-bar__value { color: var(--cx-ink); font-weight: var(--cx-w-semi); }
/* emphasis is subtraction, and in this register the others are already quiet */
/* A QUIET BAR LOSES THE OUTLINE, and only the quiet bar. At 24px with a --cx-line-2 fill,
   an outlined bar stops reading as a quantity and starts reading as an empty input — the
   outline becomes the loudest thing in a row whose whole job is to be background. The
   subject keeps its outline because it has a real fill to be drawn around. Colour is kept
   transparent rather than removing the border, so nothing shifts by a pixel. */
.cx-bars[data-subject] .cx-bar:not(.cx-bar--subject) .cx-bar__fill { background: var(--cx-line-2);
  border-color: transparent; }
.cx-bars[data-subject] .cx-bar:not(.cx-bar--subject) .cx-bar__value { color: var(--cx-ink-3); }
/* a shared target spans the plot: one hairline through the track column, named once in the caption */
.cx-bars__shared { position: relative; height: 0; }
.cx-bars__shared > i { position: absolute; top: 0; width: 1px; height: 100%; background: var(--cx-ink-3); }
.cx-bars--shared .cx-bar__track::before { content: ""; position: absolute; top: -9px; bottom: -9px;
  left: calc(var(--cx-bars-t) * 100%); width: 1px; background: var(--cx-ink-3); }
.cx-bars--shared .cx-bar:first-child .cx-bar__track::after { content: ""; position: absolute; top: -9px;
  left: calc(var(--cx-bars-t) * 100% - 1px); width: 2px; height: 12px; background: var(--cx-ink); }
/* stacked — a stack of TWO is a ratio, and the remainder is the track. Never three series. */
.cx-bar--stack .cx-bar__track { display: flex; overflow: hidden;
  border-radius: 0 var(--cx-chart-tip) var(--cx-chart-tip) 0;
  background: var(--cx-line-2);
  border: var(--cx-chart-outline) solid var(--cx-chart-outline-ink); box-sizing: border-box; }
.cx-bar--stack .cx-bar__fill { display: block; position: static; transform: none; width: calc(var(--cx-bar-v) * 100%);
  border-radius: 0; background: var(--cx-series-1); min-width: 0; }
.cx-bar--stack .cx-bar__fill--2 { width: calc(var(--cx-bar-v2) * 100%); background: var(--cx-series-2); border-radius: 0; }
/* the internal join: a divider on the LEADING edge only, so it is drawn once and meets the
   track's own outline flush. Never a border on each fill — that doubles every shared edge. */
.cx-bar--stack .cx-bar__fill + .cx-bar__fill {
  border-left: var(--cx-chart-outline) solid var(--cx-chart-outline-ink); }

/* ── columns — the vertical bar, legal only when the category IS time ────
   BEND 5 again: capsules at 44% of the pitch, both ends soft, no floor rule to sit on.
   The bar is bottom-anchored inside a cell of DEFINITE height: a percentage height on a grid
   item in a 1fr row resolves in Chrome and collapses in Safari (found 2026-08-28). The grid
   tracks carry minmax(0, 1fr) for the same reason a long tick label used to push the card
   sideways — an auto track sizes to its widest child and a twelve-week row overflows. */
.cx-cols { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); align-items: end;
  gap: 0; height: 196px; }
.cx-col { position: relative; height: 100%; min-width: 0; }
/* anchored at the bottom, so square there and capped at the top — the same rule as
   .cx-bar__fill. This is also what .cx-col--pair inherits, which is the form the owner
   flagged: two capsules floating clear of the baseline they are measured from. */
.cx-col__bar { position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); width: 44%;
  height: calc(var(--cx-bar-v) * 100%); background: var(--cx-line-2);
  border-radius: var(--cx-chart-tip) var(--cx-chart-tip) 0 0;
  border: var(--cx-chart-outline) solid var(--cx-chart-outline-ink); box-sizing: border-box;
  min-height: 4px; }
.cx-col--subject .cx-col__bar { background: var(--cx-ink); }
.cx-cols[data-subject] .cx-col:not(.cx-col--subject) .cx-col__bar { background: var(--cx-line-2);
  border-color: transparent; }
.cx-col__value { position: absolute; left: 50%; transform: translateX(-50%); bottom: calc(var(--cx-bar-v) * 100% + 6px);
  font-size: var(--cx-sm); font-weight: var(--cx-w-semi); font-variant-numeric: tabular-nums; color: var(--cx-ink); white-space: nowrap; }
/* grouped — two series side by side in one time slot, and never a third */
.cx-col--pair { display: flex; align-items: flex-end; justify-content: center; gap: 4px; }
.cx-col--pair .cx-col__bar { display: block; position: static; transform: none; width: 28%;
  background: var(--cx-series-1); }
.cx-col--pair .cx-col__bar--2 { background: var(--cx-series-2); }
/* the tick band, and the subject's tick becomes an ink chip — the LABEL carries the emphasis,
   which is the register's whole instinct: everything unusual happens off the data */
.cx-cols__ticks { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); justify-items: center;
  padding-top: var(--cx-s2); }
/* the tick band. Padding is 5px 4px on an ordinary tick and 5px 8px only on the chip, because
   the chip is the one that has to read as a CONTAINER and the others only have to read (it
   read as a pill until the 2026-09-02 de-pill; the padding difference is unchanged). Even so a
   TWELVE-category column plot needs a full-panel card: at twelve equal cells in a 340px card a
   three-character label does not fit its cell and ellipsis is not a tick. Below that width the
   category count comes down, or the plot is a line. */
.cx-cols__ticks span { font-size: 11px; font-weight: var(--cx-w-medium); letter-spacing: 0; text-transform: none;
  color: var(--cx-ink-3); padding: 5px 4px; border-radius: var(--cx-radius-sm);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.cx-cols__ticks span.is-subject { background: var(--cx-ink); color: var(--cx-panel); padding: 5px 8px; }

/* ── the radial forms (owner, 2026-08-28: "complete the set — donut charts, pie charts,
   everything"). Refused by the 08-26 spec; the owner asked for them by name, so they exist
   with the DS's honesty inside them: a donut carries ONE figure in its hole, at most five
   slices, every slice named with a word and a value in the legend, no 3D, no exploded slices;
   a gauge is a single ratio against a limit. Fills beyond the two series inks step through the
   NEUTRAL RAMP (ink-3, line, line-2) so nothing invents a hue. Wedges are real annular-sector
   paths, not dashes on a wide stroke — a dash gap on a 74px stroke is not a radial line and
   rendered as slanted slivers when it was tried. */
.cx-ring { display: grid; grid-template-columns: auto 1fr; gap: var(--cx-s5); align-items: center; }
.cx-ring__svg { width: 148px; height: 148px; display: block; transform: rotate(-90deg); }
.cx-ring__seg { fill: none; stroke-width: 22; stroke-linecap: butt; }
.cx-ring--pie .cx-ring__seg { stroke-width: 74; }
/* THE RING STEPS THE CATEGORICAL RAMP, not the series ink. A donut's slices are CATEGORIES,
   not two series over time, so the two-series ceiling never applied to it — it was simply
   the only ramp available. Three hues then two neutrals, owner-ruled 2026-08-30. */
.cx-ring__seg--1 { stroke: var(--cx-cat-1); } .cx-ring__seg--2 { stroke: var(--cx-cat-2); }
.cx-ring__seg--3 { stroke: var(--cx-cat-3); } .cx-ring__seg--4 { stroke: var(--cx-cat-4); } .cx-ring__seg--5 { stroke: var(--cx-cat-5); }
.cx-ring__track { fill: none; stroke: var(--cx-line-2); stroke-width: 22; }
.cx-ring__slice { stroke: var(--cx-chart-outline-ink); stroke-width: 2; stroke-linejoin: round; }
.cx-ring__slice--1 { fill: var(--cx-cat-1); } .cx-ring__slice--2 { fill: var(--cx-cat-2); }
.cx-ring__slice--3 { fill: var(--cx-cat-3); } .cx-ring__slice--4 { fill: var(--cx-cat-4); } .cx-ring__slice--5 { fill: var(--cx-cat-5); }
.cx-ring__svg--wedge { transform: none; }
.cx-ring__wrap { position: relative; width: 148px; height: 148px; }
.cx-ring__centre { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.cx-ring__centre b { font-family: var(--cx-font-figure); font-size: 26px; font-weight: 400; line-height: 1; }
.cx-ring__centre small { font-size: var(--cx-xs); color: var(--cx-ink-3); margin-top: 4px; }
.cx-legend { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.cx-legend li { display: grid; grid-template-columns: 10px 1fr auto; gap: 10px; align-items: center; font-size: var(--cx-sm); color: var(--cx-ink-2); }
.cx-legend i { width: 10px; height: 10px; border-radius: 50%; box-shadow: inset 0 0 0 1px var(--cx-line-strong); }   /* the ring is why a neutral-ramp swatch still reads */
.cx-legend b { font-weight: var(--cx-w-semi); color: var(--cx-ink); font-variant-numeric: tabular-nums; }
.cx-legend--row { grid-auto-flow: column; justify-content: start; gap: var(--cx-s4); }
.cx-legend--row li { grid-template-columns: 10px auto; }
/* semicircle gauge — one ratio against one limit, and the limit is an ink tick with a word
   in the caption. Never a second arc, never a third hue. */
.cx-gaugearc { position: relative; width: 200px; height: 120px; margin: 8px auto 0; }
.cx-gaugearc svg { width: 200px; height: 112px; display: block; overflow: visible; }
/* THE RADIAL, brought into the register 2026-08-30. It was the last round-capped shape in
   the package — a pill in a family that had gone square, which the owner called correctly as
   an obvious bug once everything around it changed.

   IT ALSO TAKES THE INK, and my first answer that it should not was built on a misread.
   I had read the pack's Circular Progress as a stroked arc carrying no ink. Re-checked: every
   stroke colour in it also appears as a FILL, so it is filled annular sectors with a stroke in
   their own hue — the same family as Donut and Pie, which use INK for that stroke. There was
   never a stroked-arc exception; I invented one from a bad read and dressed it as a rule.

   The real obstacle was ours. This gauge was drawn as a wide stroke with a dasharray, and a
   stroke has no interior to outline. THE PACKAGE ALREADY SOLVED THIS ONCE — see the ring's
   own note: "Wedges are real annular-sector paths, not dashes on a wide stroke." The arc
   gauge was simply left behind on that migration. It is now real geometry too: outer radius
   97, inner 79, so it takes the ink exactly as the wedges do.

   CONSUMER NOTE: the value now lives in the path, not in a stroke-dasharray, which is the
   same contract .cx-ring__slice already has. */
.cx-gaugearc__track { fill: var(--cx-line-2); stroke: var(--cx-chart-outline-ink);
  stroke-width: var(--cx-chart-outline); stroke-linejoin: round; }
.cx-gaugearc__fill { fill: var(--cx-series-1); stroke: var(--cx-chart-outline-ink);
  stroke-width: var(--cx-chart-outline); stroke-linejoin: round; }
.cx-gaugearc__limit { stroke: var(--cx-ink); stroke-width: 2; }
.cx-gaugearc__centre { position: absolute; left: 0; right: 0; bottom: -2px; text-align: center; }
.cx-gaugearc__centre b { font-family: var(--cx-font-figure); font-size: 30px; font-weight: 400; line-height: 1; display: block; }
.cx-gaugearc__centre small { font-size: var(--cx-xs); color: var(--cx-ink-3); }

/* ── progress — a single ratio against a limit, in a row ───────────────── */
/* 6px on a 12px bar IS --cx-radius-pill; it was the token written out as a literal, which
   is how a family drifts. The fill anchors left, so it follows the anchored-end rule. */
.cx-progress { position: relative; height: 12px;
  border-radius: 0 var(--cx-chart-tip) var(--cx-chart-tip) 0; background: var(--cx-line-2); overflow: visible;
  border: var(--cx-chart-outline) solid var(--cx-chart-outline-ink); box-sizing: border-box; }
/* The fill is the ACCENT, not --cx-series-1 (corrected 2026-08-29). This shape's own
   caption in the gallery says a single ratio against a limit "stays a gauge and never
   becomes a chart" — and --cx-series-1 is the CHART's series ink, which exists to tell
   one series from a second. A gauge has no second series, so the hue encoded nothing
   and simply arrived as the loudest thing in the row; on a client page with no chart
   beside it, an orphan blue. The package's other gauge, .cx-gauge > i, already paints
   in accent; these two now agree. The ink limit tick still outranks the fill. */
/* THE FILL IS THE TRACK'S INSIDE, not a second 12px shape. Once the track took a 1px
   outline its content box became 10px, and a hard-coded 12px fill on a track with
   overflow:visible spilled a pixel past its own outline top and bottom — the shape drawn
   OUTSIDE the line meant to bound it. Owner caught it as an obvious bug.
   height:100% resolves against the track's content box, so it follows the outline whatever
   the outline is set to; the inner radius is the outer radius less the line, which is what
   keeps the two curves concentric instead of merely similar. */
.cx-progress__fill { display: block; height: 100%;
  border-radius: 0 calc(var(--cx-chart-tip) - var(--cx-chart-outline))
                   calc(var(--cx-chart-tip) - var(--cx-chart-outline)) 0;
  width: calc(var(--cx-bar-v) * 100%); background: var(--cx-accent); }
.cx-progress__limit { position: absolute; top: -3px; width: 2px; height: 18px; background: var(--cx-ink); left: calc(var(--cx-bar-t) * 100%); }
.cx-progress__row { display: grid; grid-template-columns: 1fr auto; gap: var(--cx-s3); align-items: center; font-size: var(--cx-sm); color: var(--cx-ink-2); margin-bottom: 6px; }
.cx-progress__row b { font-weight: var(--cx-w-semi); color: var(--cx-ink); font-variant-numeric: tabular-nums; }

/* ── small multiples — the answer to a third series, and to a third unit ──
   ONE SCALE ACROSS EVERY CELL, ALWAYS. Per-cell scales are the dual-axis lie in a different
   costume: the reader compares shapes and the shapes are not comparable. Colour is not the
   identity channel here — position and the name are — so every cell carries --cx-series-1 and
   a named subject keeps it while the rest drop to --cx-ink-3. Emphasis is subtraction.

   CEILING: EIGHT CELLS PER FRAME (owner, 2026-08-28). Past eight, the comparison a facet
   exists to enable stops working — the eye can hold eight small shapes at once and not
   twenty-five. The ninth unit is NOT hidden by CSS: a silently dropped row is a lie about the
   data. The frame ships eight, ranked by movement since the last look, and the caption carries
   a real "Show the remaining N" affordance (`.cx-charts__more`, an ordinary .cx-btn--quiet).
   A frame that needs every unit at once is a register, not a facet — route it to .cx-rows. */
.cx-charts { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--cx-s3); }
/* 150, not the 08-26 form's 196: the register's cell carries a name, a value and a 2px line and
   nothing else, so eight of them fit a full-panel card instead of eight of them being a scroll. */
.cx-charts__cell { min-width: 0; }
.cx-charts__hd { display: flex; align-items: baseline; justify-content: space-between; gap: var(--cx-s2); margin-bottom: 4px; }
.cx-charts__name { font-size: var(--cx-h3); font-weight: var(--cx-w-medium); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-charts__val { font-size: var(--cx-sm); font-weight: var(--cx-w-semi); font-variant-numeric: tabular-nums; }
.cx-charts__cell .cx-chart__plot { height: 72px; }
.cx-charts[data-subject] .cx-charts__cell:not(.cx-charts__cell--subject) .cx-chart__series--1 { stroke: var(--cx-ink-3); }
.cx-charts[data-subject] .cx-charts__cell:not(.cx-charts__cell--subject) .cx-chart__dot { fill: var(--cx-ink-3); }
.cx-charts[data-subject] .cx-charts__cell:not(.cx-charts__cell--subject) .cx-charts__val { color: var(--cx-ink-3); }
.cx-charts__more { justify-self: start; }
/* the hit-target groups the reference templates name: they paint nothing; the app binds pointer
   behaviour on the __hit children */
.cx-chart__hits, .cx-geo__hits { fill: transparent; }
.cx-rows__more { margin-top: var(--cx-s3); }

/* ── sparkline — glyph-scale trend inside a tile. No series ink: a row of tiles is not a chart. */
.cx-spark { display: block; width: 100%; height: 22px; margin-top: var(--cx-s2); overflow: visible; }
.cx-spark__line { fill: none; stroke: var(--cx-ink-3); stroke-width: var(--cx-chart-stroke-sm); stroke-linejoin: round; stroke-linecap: round; }
.cx-spark__dot { fill: var(--cx-ink); }
/* dot-field sparkline — SPIKE 2026-08-28 (retro sweep, mark idea B). One dot per sample on
   a fixed 12-column grid, no connecting stroke: a sample with no value draws no dot, so a
   hole in the ledger is a hole in the field and interpolation is not expressible in the
   mark. HTML, not SVG, so a dot stays a dot at any tile width. Same pixel logic as the
   Departure Mono figure directly above it. Each cell carries --cx-spark-y (0..1) for its value, set inline.
   Render-checked at 22px in the gallery beside the stroked form; owner rules which ships. */
.cx-spark--dots { display: grid; grid-template-columns: repeat(12, 1fr); height: 22px; align-items: end;
  margin-top: var(--cx-s2); }
.cx-spark--dots > i { display: block; justify-self: center; width: 2px; height: 2px; border-radius: 1px;
  background: var(--cx-ink-3); margin-bottom: calc(var(--cx-spark-y) * 18px); }
.cx-spark--dots > i:empty:not([style]) { background: transparent; }   /* a sample with no value */
.cx-spark--dots > i.cx-spark__dot { width: 4px; height: 4px; border-radius: 2px; background: var(--cx-ink);
  margin-bottom: calc(var(--cx-spark-y) * 18px - 1px); }

/* ── opt-in: cx-chart--plain — the 08-26 form, unchanged ─────────────────
   The dense form: a hairline frame in the plane, straight 2px lines, the words ON the plot
   (end label, target label, flag word), a 14px bar in a 32px band with a 2px data end and a
   square baseline, uppercase tracked ticks. It is still the right form for a contextual panel
   at 372px and for a surface that must print — everything the register moves to the caption,
   this form keeps beside the mark. Every rule below is an explicit return, never a reset
   shorthand, so a reader can see exactly what the register changed. */
.cx-chart--plain, .cx-chart--grid { gap: var(--cx-s2); background: none; border-radius: 0; padding: 0; box-shadow: none; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__hd { min-height: 0; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__title { font-size: var(--cx-h3); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__unit { display: inline; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__figure { min-height: 56px; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__figure b { font-size: 40px; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__figure small { display: inline; font-size: var(--cx-h3); color: var(--cx-ink-3); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__series { stroke-width: var(--cx-chart-stroke); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__series--sm { stroke-width: var(--cx-chart-stroke-sm); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__base { display: block; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__target { stroke: var(--cx-ink-3); stroke-dasharray: none; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__target-tick { display: block; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__label,
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__note { display: block; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__flag { r: 4; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__area { fill: color-mix(in oklab, var(--cx-series-1) 10%, transparent); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__tick { font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__cap { margin-top: 0; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-chart__state { min-height: 202px; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-charts { grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-legend i { box-shadow: none; }
/* the dense data row: 14 in 32, 2px on the data end, square at the baseline, the value flush
   right in a fixed column, and emphasis by dropping everything that is not the subject */
:is(.cx-chart--plain, .cx-chart--grid) .cx-bar { grid-template-columns: minmax(96px, 168px) 1fr 92px; min-height: var(--cx-row-compact); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-bar__track { height: 14px; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-bar__fill { height: 14px; background: var(--cx-series-1);
  border-radius: 0 min(2px, 50%) min(2px, 50%) 0; min-width: 0; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-bar--subject .cx-bar__fill { background: var(--cx-series-1); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-bars[data-subject] .cx-bar:not(.cx-bar--subject) .cx-bar__fill { background: var(--cx-ink-3); opacity: .55; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-bar[data-target] .cx-bar__tick { display: block; top: 1px; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-bar__value { font-weight: var(--cx-w-semi); color: var(--cx-ink); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-bar--stack .cx-bar__track { border-radius: 0 min(2px, 50%) min(2px, 50%) 0; }
/* columns: 56% of the pitch, 4px top radius, square on a hairline floor */
:is(.cx-chart--plain, .cx-chart--grid) .cx-cols { border-bottom: 1px solid var(--cx-line-2); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-col__bar { width: 56%; border-radius: 4px 4px 0 0;
  background: var(--cx-line); min-height: 2px; }
:is(.cx-chart--plain, .cx-chart--grid) .cx-col--subject .cx-col__bar { background: var(--cx-series-1); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-cols[data-subject] .cx-col:not(.cx-col--subject) .cx-col__bar { background: var(--cx-line); }
:is(.cx-chart--plain, .cx-chart--grid) .cx-cols__ticks span { font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; padding: 3px 8px; }

/* ── opt-in: cx-chart--grid — the r19 fusion (comp 2026-08-28) ───────────
   Everything `--plain` is, plus r19's plot grammar: a quiet DOTTED grid on both axes in
   line-2, a numbered value scale at the left in ink-3, small tick marks on the x band, a
   hairline under the chart header, thicker bars against a value axis. It is the form for a
   surface that has to be read as a measurement rather than as a shape. */
.cx-chart--grid .cx-chart__hd { padding-bottom: var(--cx-s2); border-bottom: 1px solid var(--cx-line-2); }
.cx-chart--grid .cx-chart__unit { display: block; width: 100%; margin-top: 2px; text-transform: none;
  letter-spacing: 0; font-weight: var(--cx-w-regular); font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-chart__grid--dot { stroke: var(--cx-line); stroke-width: var(--cx-chart-hair); stroke-dasharray: 1 3; fill: none; }
.cx-chart__ytick { fill: var(--cx-ink-3); font-size: var(--cx-xs); font-variant-numeric: tabular-nums; }
.cx-chart__xtick { stroke: var(--cx-line-strong); stroke-width: var(--cx-chart-hair); }
.cx-chart__axis { stroke: var(--cx-line-strong); stroke-width: var(--cx-chart-hair); }
.cx-chart--grid .cx-bar__track { height: 20px; }
.cx-chart--grid .cx-bar__fill { height: 20px; border-radius: 0 min(2px, 50%) min(2px, 50%) 0; }
.cx-chart--grid .cx-bar__tick { top: 4px; }
.cx-chart--grid .cx-bars { background-image: repeating-linear-gradient(to right, transparent 0 calc(25% - 1px), var(--cx-line) calc(25% - 1px) 25%); }
.cx-bars__axis { display: grid; grid-template-columns: minmax(96px, 168px) 1fr 92px; gap: var(--cx-s3); margin-top: 4px; }
.cx-bars__axis > span:nth-child(2) { display: flex; justify-content: space-between; font-size: var(--cx-xs); color: var(--cx-ink-3);
  font-variant-numeric: tabular-nums; border-top: 1px solid var(--cx-line-strong); padding-top: 4px; }
.cx-bars__axis > span > * { font-style: normal; }

/* ── aliases, and they paint nothing ─────────────────────────────────────
   `cx-chart--bw` was the register's name while it was a proposal; `cx-chart--card` was the
   first fusion it replaced. Both are folded into `.cx-chart` above. They are kept as inert
   names for ONE RELEASE so a page or an app that already writes them keeps rendering the
   register, and they are DELETED at the next one. Nothing new should be written with them.
   Declared here as empty rules on purpose: a grep for the name finds this note. */
.cx-chart--bw { /* alias of the default register — remove after one release */ }
.cx-chart--card { /* alias of the default register — remove after one release */ }

/* ── responsive ──────────────────────────────────────────────────────────
   Below 900px a facet is one column, and every hit target clears 44px. */
@media (max-width: 900px) {
  .cx-charts { grid-template-columns: 1fr; }
  .cx-bar { min-height: 44px; }
  :is(.cx-chart--plain, .cx-chart--grid) .cx-bar { min-height: 44px; }
  /* the twin's toggle and the facet's affordance are real controls and clear the touch floor
     here, in this block rather than in the shell's 900px list, so the chart layer stays one
     cut-out region of this file */
  .cx-fold > summary { min-height: 44px; }
  .cx-charts__more { min-height: 44px; }
  .cx-chart { padding: var(--cx-s4); }
}

/* ── ported from the instances, 2026-08-26 ─────────────────────────────
   The "42 inline components" the README kept out of the package (the real
   number was 44 distinct classes across 77 rules; the README's figure was
   wrong when written). Lifted from a client instance's inline <style> block,
   re-tokened to v4 on the role-based surface mapping, consumer id-scoping and
   department names dropped, Tailwind dependencies discharged so each stands
   alone. Two refinements the port surfaced, now part of the mapping: an
   element that is already panel-filled at rest hovers by BORDER (line-strong),
   not by fill; and a nested fill on the GROUND takes --cx-panel — sunken is
   only for a nested fill on a panel. Instance-only rules stayed out. */
/* ── rail · the collapsed rail's search stand-in ─────────────────────────
   The expanded rail owns a real search input. At 64px there is no room for
   one, so this button takes its place: hidden while the rail is expanded,
   shown when it collapses, and its job is to re-expand the rail and focus the
   real input. One search behaviour, one thing to keep honest — a second live
   search field at a second width is two things to keep honest.

   Ported changes: the source scoped this to #dept-nav and drove it from
   html[data-rail], both of which are the host app's. It is unscoped here and
   reads the package's own .cx-app[data-rail="collapsed"] state. The source
   also inherited its box from a shared rule with the department buttons; the
   box is written out here so the component does not depend on a sibling.
   Fills follow the v4 rail convention: the rail sits on the ground, so a
   filled control in it takes --cx-panel (v3 --cx-surface-1, same role). */
.cx-rail-searchbtn { display: none; align-items: center; gap: var(--cx-s2);
  width: 100%; text-align: left; font: inherit; font-size: var(--cx-body);
  padding: var(--cx-s2) 9px; margin-bottom: var(--cx-s1);
  border-radius: var(--cx-radius-md); cursor: pointer;
  background: var(--cx-panel); border: 1px solid var(--cx-line);
  color: var(--cx-ink-3); }
.cx-app[data-rail="collapsed"] .cx-rail-searchbtn { display: flex;
  justify-content: center; padding-left: 0; padding-right: 0; }
.cx-rail-searchbtn:hover { border-color: var(--cx-line-strong); color: var(--cx-ink); }

/* ── rail · connections entry ────────────────────────────────────────────
   Connections is the other half of Inputs, not a department, so it lives in
   the rail's foot below the divider rather than in the department list. Same
   box as a rail item; it is a button rather than a link because it opens a
   surface rather than navigating to one.

   Ported change: v3 --cx-surface-2 on :hover mapped to the counter-surface.
   In the source app the rail was itself a panel-coloured column, so its
   counter-surface was the sunken step; in the panel world the rail sits ON the
   ground, so an element in it hovers to --cx-panel. This is the same fill
   .cx-rail-item:hover already uses, which is the point — the two controls sit
   in the same column and must not hover to two different colours. */
.cx-rail-conn { display: flex; align-items: center; gap: var(--cx-s2); width: 100%;
  font: inherit; font-size: var(--cx-body); text-align: left; cursor: pointer;
  /* 7px, not 9: this row carries a 1px transparent border for its hover, and a control's
     border is absorbed by its padding, never added to it. 12 + 1 + 7 = 20, the same edge. */
  padding: var(--cx-s2) 7px; border-radius: var(--cx-radius-md);
  border: 1px solid transparent; background: transparent; color: var(--cx-ink-2); }
.cx-rail-conn:hover { background: var(--cx-panel); color: var(--cx-ink); }
.cx-app[data-rail="collapsed"] .cx-rail-conn { justify-content: center;
  padding-left: 0; padding-right: 0; }

/* ── jump search ─────────────────────────────────────────────────────────
   The rail's one search, and its result list. NOT a global index: it jumps
   between pages and over whatever list the surface serves, and the empty state
   says so rather than implying a document search that does not exist.

   Ported changes:
   · .cx-search is authored here. It carries no rule in the source — the
     positioning context for the icon and the dropdown came from a Tailwind
     `relative` on the same element, which is exactly the dependency a DS
     component may not have.
   · The label span's truncation came from a Tailwind `truncate`; it is
     authored on the first child here for the same reason.
   · :hover and [aria-selected] were one rule in the source, both painting the
     accent wash. Split: hover is a preview and takes the counter-surface
     (the list is a panel, so it hovers to the ground); aria-selected is the
     active option and keeps the wash with its quiet ink. */
.cx-search { position: relative; display: block; }
.cx-search .cx-input { padding-left: 32px; box-sizing: border-box; }
.cx-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  color: var(--cx-ink-3); pointer-events: none; }
.cx-search-results { position: absolute; top: calc(100% + var(--cx-s1)); left: 0; right: 0;
  z-index: 50; max-height: 60vh; overflow-y: auto; padding: var(--cx-s1);
  background: var(--cx-panel); border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-md); box-shadow: var(--cx-shadow-popover); }
.cx-search-hit { display: flex; align-items: baseline; gap: var(--cx-s2);
  width: 100%; min-height: 44px; text-align: left; padding: 6px var(--cx-s2);
  border-radius: var(--cx-radius-sm); font-size: var(--cx-body); color: var(--cx-ink);
  background: transparent; border: 0; cursor: pointer;
  transition: background var(--cx-dur-fast) var(--cx-ease); }
.cx-search-hit > span:first-child { min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.cx-search-hit:hover { background: var(--cx-ground); }
.cx-search-hit[aria-selected="true"] { background: var(--cx-accent-wash);
  color: var(--cx-accent-quiet); }
.cx-search-kind { margin-left: auto; font-size: var(--cx-xs);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-ink-3); }
/* the empty state is the product's writing, so it gets room to be read */
.cx-search-empty { padding: var(--cx-s2); font-size: var(--cx-body); color: var(--cx-ink-3); }

/* ── section heading ─────────────────────────────────────────────────────
   A department's own heading, carrying the department's mark. The mark is the
   sprite glyph, not a config-supplied text icon: the rail and the heading must
   name the same department with the same mark. */
.cx-dept-title { display: flex; align-items: center; gap: var(--cx-s2);
  font-size: var(--cx-h1); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-heading); line-height: var(--cx-lh-tight); }
.cx-dept-mark { color: var(--cx-accent); }

/* ── stat tiles ──────────────────────────────────────────────────────────
   The grid the package's .cx-tile was missing, plus the fill and the pressable
   state. Not everything is a chart: when the answer is a headline figure it is
   a tile, and the figure sits in INK — the accent marks the delta, never every
   number, or four saturated figures compete for the same eye.

   Note on the fill: .cx-tile ships with a border and no background because it
   is authored for a panel body. These tiles sit on the STAGE, on the ground,
   where a fill-less card does not read as a card at all, so the fill is scoped
   to the grid: a tile inside a panel keeps the borderline-only form.

   Note on the margin: a block component inside a panel body spaces DOWN, and
   .cx-actions below follows the same idiom. Kept from the source deliberately
   — it is the one thing that makes these stack without a wrapper. */
.cx-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--cx-s3); margin-bottom: var(--cx-s3); }
.cx-tiles > .cx-tile { background: var(--cx-panel); }
/* a tile with records behind it becomes pressable; one without stays visibly
   static, because a false affordance is worse than none */
.cx-tile--go { cursor: pointer;
  transition: border-color var(--cx-dur) var(--cx-ease); }
.cx-tile--go:hover { border-color: var(--cx-accent); }

/* ── filter row ──────────────────────────────────────────────────────────
   A panel's own filter row: a search field that grows and any number of
   scopes that do not. The max-width is a reading measure, not a token value —
   a search input that grows to a full panel width stops looking like a field. */
.cx-grid-head { display: flex; align-items: center; gap: var(--cx-s2);
  margin-bottom: var(--cx-s3); flex-wrap: wrap; }
.cx-grid-head .cx-input { flex: 1 1 200px; max-width: 22rem; }
/* .cx-input--select is the NATIVE select, kept only for a form a consumer has not
   converted yet. It is not the DS's dropdown: a native select paints the operating
   system's menu — a dark macOS sheet with its own type, radius and check mark — over
   a surface built to be quiet, and none of the panel world's rules reach inside it.
   Prefer .cx-select below on every product surface. (Owner ruling 2026-08-29.) */
.cx-input--select { flex: 0 0 auto; width: auto; }

/* ── select — the DS's own dropdown ──────────────────────────────────────
   Built on <details>/<summary>, so it opens and closes with no script and stays
   keyboard-operable: the package still imports nothing and still ships no runtime.
   The LIST and the OPTIONS are .cx-scope's, not a second popover grammar — the
   shared rules are declared once, at .cx-scope__list and .cx-scope__opt, with this
   component's classes named alongside them. What is added here is only the closed
   control, which wears .cx-input's geometry exactly so a filter row of a field and
   a dropdown sits on one baseline. */
/* Sized by its content at rest, but able to GIVE WAY when the track is short: a long
   value must ellipsise, never push past the stage edge, which ?diag=1 exists to forbid.
   min-width: 0 alone is not enough and that is the trap — it only removes the automatic
   minimum. With flex-shrink still 0 the control cannot shrink at all, so the overflow
   survives; measured at 820px, four elements past the right edge and scrollWidth 970
   against clientWidth 820. flex-shrink must be 1 as well. The .cx-scope__id triple at
   :1915 is the precedent for the ellipsis half. */
.cx-select { position: relative; flex: 0 1 auto; min-width: 0; max-width: 100%; }
.cx-select > summary { list-style: none; display: inline-flex; align-items: center;
  box-sizing: border-box; min-width: 0; max-width: 100%;
  gap: var(--cx-s2); padding: var(--cx-s2) 11px; border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-md); font-size: var(--cx-body); color: var(--cx-ink);
  background: var(--cx-panel); cursor: pointer; white-space: nowrap;
  transition: border-color var(--cx-dur) var(--cx-ease); }
.cx-select > summary::-webkit-details-marker { display: none; }
.cx-select > summary:hover { border-color: var(--cx-line-strong); }
.cx-select__label { color: var(--cx-ink-3); }
.cx-select__value { font-weight: var(--cx-w-medium); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; }
/* the chevron is .cx-scope__chev's, turning on the disclosure's own open state */
.cx-select__chev { flex: none; margin-left: auto; color: var(--cx-ink-3);
  transition: transform var(--cx-dur) var(--cx-ease); }
.cx-select[open] > summary .cx-select__chev { transform: rotate(180deg); }
.cx-select[open] > summary { border-color: var(--cx-line-strong); }
/* the selected option carries a CHECK, the package's own mark, never a bare tint:
   mark and word, the same rule every state in this file obeys */
.cx-select__opt[aria-current="true"] { color: var(--cx-ink); font-weight: var(--cx-w-medium); }
.cx-select__opt .cx-select__tick { flex: none; color: var(--cx-accent); visibility: hidden; }
.cx-select__opt[aria-current="true"] .cx-select__tick { visibility: visible; }
/* A dropdown at the END of a row MUST carry this, and it is not cosmetic: the list is
   absolutely positioned from the select's left edge, so an un-flagged one near the stage's
   right edge extends past it and pushes a horizontal scroll onto the page — the one thing
   README > ?diag=1 forbids outright. Measured 2026-08-29 on the gallery's Inventory filter
   row: the list ran to 1297 in a 1280 viewport, scrollWidth 17px over.

   The package ships no runtime, so nothing can flip this automatically at open time; the
   call site owns it. Every .cx-select that is the last element of a .cx-grid-head carries
   --end for that reason. */
.cx-select--end .cx-select__list { left: auto; right: 0; }

/* ── action row ──────────────────────────────────────────────────────────
   A surface's verbs, first one primary. --gap-top is for the row that sits
   BELOW fields rather than above a list: it spaces down from what it follows
   instead of down onto what it precedes. */
.cx-actions { display: flex; flex-wrap: wrap; gap: var(--cx-s2); margin-bottom: var(--cx-s3); }
.cx-actions--gap-top { margin-top: var(--cx-s3); margin-bottom: 0; align-items: center; }

/* ── segmented row, and its nested variant ───────────────────────────────
   The row that carries a .cx-seg used as section navigation, and the answer to
   what happens when a section has sections of its own.

   Two segmented controls of equal weight stacked on top of each other read as
   one confused row. The nested one drops the raised track and marks its active
   item with a rule instead — clearly subordinate to the row above it, the same
   component underneath. That subordination is the whole point of the variant;
   without it a consumer builds a second, different-looking sub-nav.

   Ported change: named for the department that first needed it in the source
   (.cx-inv-tabs / .cx-inv-subtabs). Renamed — a package class must not carry a
   consumer's department name. */
.cx-segrow { margin-bottom: var(--cx-s3); }
.cx-segrow--nested { margin-top: calc(-1 * var(--cx-s2)); }
.cx-segrow--nested .cx-seg { background: none; padding: 0; gap: 2px;
  border-bottom: 1px solid var(--cx-line-2); border-radius: 0; }
.cx-segrow--nested .cx-seg button { border-radius: 0; padding: 6px 12px;
  border-bottom: 2px solid transparent; margin-bottom: -1px; }
.cx-segrow--nested .cx-seg button[aria-pressed="true"] { background: none;
  color: var(--cx-ink); border-bottom-color: var(--cx-accent); }

/* ── data row · the status variant ───────────────────────────────────────
   The package's .cx-row is dot | name | qty | gauge | movement. This variant
   is for a surface that derives no per-item movement figure: the fifth track
   carries a status pill rather than a number nobody computed, and a sixth
   track carries the row's own actions.

   The track widths are the CONSUMER'S to tune — they are the shape of that
   surface's data, not a system value. What the package owns is the shape:
   six tracks, the pill before the actions, the actions last.

   The actions button is a 32px track. It meets the target rule the way
   .cx-iconbtn does — through the row's height, not through the glyph. */
.cx-row--status { grid-template-columns: 14px 1fr 158px 120px 132px 32px; }
/* the accountable number, as a button: the figure IS the way into its history.
   NO MONO — tabular numerals on the working face do the aligning, which is the
   whole no-mono rule. */
.cx-row__figure { font: inherit; font-size: var(--cx-body); font-weight: var(--cx-w-medium);
  font-variant-numeric: tabular-nums; color: var(--cx-ink);
  background: none; border: 0; padding: 0; cursor: pointer; text-align: right; width: 100%; }
.cx-row__figure:hover { color: var(--cx-accent); text-decoration: underline; }
.cx-row__qty small { display: block; font-size: var(--cx-sm); color: var(--cx-ink-3);
  margin-top: 1px; text-align: right; white-space: nowrap; }
/* a freshness line that has lapsed says so in the warn hue, still as a WORD */
.cx-row__qty small.cx-row__stale { color: var(--cx-warn); }
.cx-row__status { display: flex; justify-content: flex-start; }
/* honest absence, in the row: no threshold means no target to measure against,
   so the gauge track carries a dash rather than an empty or a zeroed bar */
.cx-gauge-none { color: var(--cx-ink-3); font-size: var(--cx-sm); text-align: center; }
.cx-row__more { font: inherit; font-size: var(--cx-body); line-height: 1;
  color: var(--cx-ink-3); background: none; border: 0; cursor: pointer;
  border-radius: var(--cx-radius-sm); padding: 4px;
  transition: background var(--cx-dur-fast) var(--cx-ease), color var(--cx-dur-fast) var(--cx-ease); }
/* an element on a panel hovers to the ground */
.cx-row__more:hover { color: var(--cx-ink); background: var(--cx-ground); }

/* ── home · projection panels ────────────────────────────────────────────
   Home decides nothing, so its department panels project rather than report:
   attention-first rows in a value / label / sub grammar, and a quiet line in
   the department's own voice when there is nothing to project. The row is a
   button — the value is what you click to go where it came from.

   46px above the group matches .cx-eyebrow's own top margin; it is the rhythm
   that separates the wordmark from the first group, and no spacing token owns
   it (s7 is 48px). Kept for consistency with .cx-eyebrow rather than snapped. */
.cx-home .cx-mini { margin-top: var(--cx-s3); }
.cx-home-depts { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--cx-s3); margin-top: 46px; text-align: left; }
.cx-proj .cx-panel__bd { padding-top: var(--cx-s2); }
.cx-proj__open { font: inherit; font-size: var(--cx-sm); font-weight: var(--cx-w-medium);
  background: none; border: 0; cursor: pointer; color: var(--cx-accent);
  padding: 2px 4px; border-radius: var(--cx-radius-sm); }
.cx-proj__open:hover { text-decoration: underline; }
.cx-proj__row { display: flex; align-items: baseline; gap: var(--cx-s3); width: 100%;
  text-align: left; font: inherit; color: var(--cx-ink); background: none; border: 0;
  border-bottom: 1px solid var(--cx-line-2); padding: var(--cx-s2) 0; cursor: pointer; }
.cx-proj__row:last-child { border-bottom: 0; }
.cx-proj__row:hover .cx-proj__l { color: var(--cx-accent-quiet); }
.cx-proj__v { font-family: var(--cx-font-figure); font-size: 18px; font-weight: 400; letter-spacing: 0;
  min-width: 1.4em; }
.cx-proj__l { flex: 1; font-size: var(--cx-body); }
.cx-proj__l small { display: block; font-size: var(--cx-sm); color: var(--cx-ink-3);
  margin-top: 1px; }

/* ── the status row's own breakpoint, 1101px and below ───────────────────
   Six tracks need more width than five. The two widest tracks go before the
   row wraps, because a wrapped data row stops being a row.

   Below 900px the package's narrow block carries its own .cx-row--status line,
   because the base .cx-row template there has equal specificity and comes later. */
@media (max-width: 1100px) {
  .cx-row--status { grid-template-columns: 14px 1fr 128px 132px 32px; }
  .cx-row--status .cx-gauge, .cx-row--status .cx-gauge-none { display: none; }
}

/* ── motion slots ─────────────────────────────────────────────────────────
   Owner ruling 2026-08-26: assets BAKE INTO THE DS ("otherwise I'd have to
   prompt the agent building the UI for the project to do it each time"). Eight
   slots, each with the owner's REGISTER picks (2026-08-28: the cube world
   throughout) from the motion-assets library under motion/<slot>/, rebuilt by
   motion/bake.mjs — the pick list lives there. Every asset ships twice: the Lottie JSON, and its RESTING
   FRAME as inline SVG — the complete state a reduced-motion reader sees, and
   the state every slot has with no JavaScript at all. The player
   (motion/lottie.min.js + motion/cx-motion.js) is vendored and OPTIONAL; the
   CSS layers still import nothing.

   COLOUR IS BY ROLE, NOT BAKED. Each asset's palette was rewritten to seven
   sentinel values at import (manifest.json lists them), and the rules below
   map a sentinel to a token, so every asset theme-flips and takes the client
   accent. A cube's shaded and lit faces are two more sentinels, painted as
   mixes of the slot's accent toward black and white — the same hue in three
   values, never a second hue, and the shading stays physical in both themes
   (the two literal keywords below are deliberate: a face mixed toward --cx-ink
   would go LIGHTER in dark mode). A slot can re-route a role: success turns
   the accent into ok, error into error, the chart-absent illustration into
   muted ink — an illustration of an absent chart must not wear the client's
   colour like a live one. The faces follow the re-route. */
.cx-motion { display: block; width: var(--cx-motion-w); max-width: 100%; line-height: 0; }
.cx-motion svg { display: block; width: 100%; height: auto; }
.cx-motion--wide { --cx-motion-w: 100%; }
.cx-motion--inline { display: inline-block; --cx-motion-w: 1.1em; vertical-align: -.2em; }
.cx-motion--center { margin-inline: auto; }
.cx-motion__rest { display: block; }   /* the baked resting frame; its own inline sizing is the asset's, not the page's */
/* sentinel → token. lottie-web paints rgb(r,g,b) attributes; CSS beats attributes. */
.cx-motion [fill="rgb(16,16,16)"]   { fill: var(--cx-ink); }
.cx-motion [stroke="rgb(16,16,16)"] { stroke: var(--cx-ink); }
.cx-motion [fill="rgb(48,48,48)"]   { fill: var(--cx-ink-3); }
.cx-motion [stroke="rgb(48,48,48)"] { stroke: var(--cx-ink-3); }
.cx-motion stop[stop-color="rgb(48,48,48)"] { stop-color: var(--cx-ink-3); }
.cx-motion [fill="rgb(200,200,201)"]   { fill: var(--cx-line); }
.cx-motion [stroke="rgb(200,200,201)"] { stroke: var(--cx-line); }
.cx-motion [fill="rgb(254,254,254)"]   { fill: var(--cx-panel); }
.cx-motion [stroke="rgb(254,254,254)"] { stroke: var(--cx-panel); }
.cx-motion [fill="rgb(160,0,160)"]   { fill: var(--cx-accent); }
.cx-motion [stroke="rgb(160,0,160)"] { stroke: var(--cx-accent); }
/* the object palette — an asset with real hues keeps them instead of being flattened onto
   the accent's shading ramp. House-owned, never themed, never an encoding. */
.cx-motion [fill="rgb(255,90,0)"]     { fill: var(--cx-obj-orange); }
.cx-motion [stroke="rgb(255,90,0)"]   { stroke: var(--cx-obj-orange); }
.cx-motion [fill="rgb(43,149,255)"]   { fill: var(--cx-obj-blue); }
.cx-motion [stroke="rgb(43,149,255)"] { stroke: var(--cx-obj-blue); }
.cx-motion [fill="rgb(255,204,51)"]   { fill: var(--cx-obj-yellow); }
.cx-motion [stroke="rgb(255,204,51)"] { stroke: var(--cx-obj-yellow); }
.cx-motion [fill="rgb(255,143,176)"]   { fill: var(--cx-obj-pink); }
.cx-motion [stroke="rgb(255,143,176)"] { stroke: var(--cx-obj-pink); }
/* the second value in each family — the pack's own light-and-dark face of one hue */
.cx-motion [fill="rgb(199,70,0)"]     { fill: var(--cx-obj-orange-deep); }
.cx-motion [stroke="rgb(199,70,0)"]   { stroke: var(--cx-obj-orange-deep); }
.cx-motion [fill="rgb(31,111,191)"]   { fill: var(--cx-obj-blue-deep); }
.cx-motion [stroke="rgb(31,111,191)"] { stroke: var(--cx-obj-blue-deep); }
.cx-motion [fill="rgb(199,154,38)"]   { fill: var(--cx-obj-yellow-deep); }
.cx-motion [stroke="rgb(199,154,38)"] { stroke: var(--cx-obj-yellow-deep); }
.cx-motion [fill="rgb(199,107,134)"]   { fill: var(--cx-obj-pink-deep); }
.cx-motion [stroke="rgb(199,107,134)"] { stroke: var(--cx-obj-pink-deep); }
/* slot re-routes */
.cx-motion[data-slot="success"] [fill="rgb(160,0,160)"]   { fill: var(--cx-ok); }
.cx-motion[data-slot="success"] [stroke="rgb(160,0,160)"] { stroke: var(--cx-ok); }
.cx-motion[data-slot="error"] [fill="rgb(160,0,160)"]     { fill: var(--cx-error); }
.cx-motion[data-slot="error"] [stroke="rgb(160,0,160)"]   { stroke: var(--cx-error); }
.cx-motion[data-slot="chart-absent"] [fill="rgb(160,0,160)"]   { fill: var(--cx-ink-3); }
.cx-motion[data-slot="chart-absent"] [stroke="rgb(160,0,160)"] { stroke: var(--cx-ink-3); }
.cx-motion[data-slot="reading"] [fill="rgb(160,0,160)"]   { fill: var(--cx-ai-ink); }
.cx-motion[data-slot="reading"] [stroke="rgb(160,0,160)"] { stroke: var(--cx-ai-ink); }
.cx-motion[data-slot="loading"] { opacity: .9; }
/* the faces: deep = rgb(120,0,120), light = rgb(200,120,200) */
.cx-motion[data-slot="success"]      { --cx-motion-face: var(--cx-ok); }
.cx-motion[data-slot="error"]        { --cx-motion-face: var(--cx-error); }
.cx-motion[data-slot="chart-absent"] { --cx-motion-face: var(--cx-ink-3); }
.cx-motion[data-slot="reading"]      { --cx-motion-face: var(--cx-ai-ink); }
.cx-motion [fill="rgb(120,0,120)"]   { fill:   color-mix(in oklab, var(--cx-motion-face) 72%, black); }
.cx-motion [stroke="rgb(120,0,120)"] { stroke: color-mix(in oklab, var(--cx-motion-face) 72%, black); }
.cx-motion [fill="rgb(200,120,200)"]   { fill:   color-mix(in oklab, var(--cx-motion-face) 55%, white); }
.cx-motion [stroke="rgb(200,120,200)"] { stroke: color-mix(in oklab, var(--cx-motion-face) 55%, white); }

/* ── mid width, 901–1240px. Authored 2026-08-26 after the detail panel was seen
   painting over the register at a gallery-frame width: rail 264 + panel 372 leaves
   the main track under 400px, and re-proportioning the row has no room to work.
   Here the contextual panel FLOATS over the stage instead — same width, same
   sticky top, a popover shadow so it reads as above rather than beside. The rail
   stays as the user set it; collapsing it recovers the column layout. */
@media (max-width: 1240px) {
  .cx-app:not([data-rail="collapsed"]) .cx-stage[data-panel="open"] { grid-template-columns: minmax(0, 1fr); }
  .cx-app:not([data-rail="collapsed"]) .cx-stage[data-panel="open"] .cx-panel--sticky {
    position: fixed; right: var(--cx-s4); top: var(--cx-s3); width: 372px;
    max-height: calc(100vh - var(--cx-s3) * 2); overflow: auto; z-index: 30;
    box-shadow: var(--cx-shadow-popover); }
}

/* ── narrow width. The corpus has NO answer below ~900px; this is authored.
   Rail becomes an overlay; the contextual panel becomes a full-width sheet. */
@media (max-width: 900px) {
  .cx-app, .cx-app[data-rail="collapsed"] { grid-template-columns: 1fr; }
  .cx-rail { position: fixed; inset: 0 auto 0 0; width: 264px; z-index: 42;
    background: var(--cx-ground); border-right: 1px solid var(--cx-line);
    transform: translateX(-100%); transition: transform var(--cx-dur-slow) var(--cx-ease); }
  .cx-app[data-rail="open"] .cx-rail { transform: none; }
  .cx-stage[data-panel="open"] { grid-template-columns: 1fr; }
  /* the sheet: written at the mid-band rule's own specificity so it wins here (the 1240px
     float used to outrank this line and the detail stayed a 372px overlay at 390px) */
  .cx-panel--sticky, .cx-app:not([data-rail="collapsed"]) .cx-stage[data-panel="open"] .cx-panel--sticky {
    position: static; width: auto; max-height: none; overflow: visible; box-shadow: none; }
  .cx-panel__hd, .cx-panel__aff { flex-wrap: wrap; }
  .cx-attn__row { flex-wrap: wrap; }
  .cx-read__value { font-size: 46px; }
  .cx-row { grid-template-columns: 14px 1fr 84px; }
  .cx-row .cx-gauge, .cx-row__qty { display: none; }
  .cx-row--status { grid-template-columns: 14px 1fr 132px 32px; }
  .cx-row--status .cx-gauge-none, .cx-row--status .cx-row__qty { display: none; }
}
/* a register inside the 372px contextual panel takes the same fold at any viewport: three tracks,
   the quantity and the gauge dropped (the second proof page had 346px of fixed track against a
   338px panel body) */
.cx-panel--sticky .cx-row { grid-template-columns: 14px 1fr 84px; }
.cx-panel--sticky .cx-row .cx-gauge, .cx-panel--sticky .cx-row__qty { display: none; }
@media (max-width: 900px) {
  /* touch: every interactive row and control clears 44px here; desktop stays denser */
  .cx-rail-item, .cx-rail-conn, .cx-row, .cx-proj__row, .cx-search-hit, .cx-seg button, .cx-tab, .cx-iconbtn,
  .cx-rail-collapse, .cx-btn, .cx-btn--quiet, .cx-input, .cx-input--select, .cx-attn__act, .cx-rows__more,
  .cx-cal__more, .cx-doc__sup :is(a, button), .cx-select > summary,
  .cx-select__opt, .cx-scope__opt { min-height: 44px; }
  /* verbs side by side must be 44px WIDE too, or a mis-tap lands on the neighbour */
  .cx-btn, .cx-btn--quiet, .cx-attn__act, .cx-doc__sup :is(a, button) { min-width: 44px; display: inline-flex; align-items: center; justify-content: center; }
  /* min-height applies to the CONTENT box, so a control with padding overshoots the floor
     it was just given. Every selector in the 44px list above that declares padding needs
     this in the same block: .cx-select > summary measured 62px against .cx-input's 44px on
     the same filter row. Measured 2026-08-29. */
  /* .cx-input and .cx-select > summary now carry border-box at their base rule; these
     three only need it here, where the 44px floor is what makes their padding overshoot. */
  .cx-select__opt, .cx-scope__opt, .cx-btn, .cx-btn--quiet { box-sizing: border-box; }
  /* the width half of the floor: an icon button's 28px box grows to 44 here; the rail's collapse
     chevron and a row's overflow dots likewise */
  .cx-iconbtn, .cx-rail-collapse, .cx-row__more { min-width: 44px; min-height: 44px; }
  .cx-seg button { min-width: 44px; }
  .cx-wordmark { font-size: 40px; }
}
/* ── clock / deadline — a date that bites ─────────────────────────────────
   COVERAGE.md shape 4, built 2026-08-28. Seven of eight pipeline clients need a
   clock and the package had none. A clock is not a chart: it is a LABEL, a DATE
   and a COUNT, and only the count wears the display face — the chart card's figure
   treatment carried out of the card into the panel world (hairline, no shadow).
   ONE markup, three forms: the panel tile, the inline chip in a register row's
   gauge track (a bounded clock IS a value against its target, so it needs no new
   row class), and the "what bites next" list, ordered by bite date by the consumer.
   FIVE states on one attribute, `data-state` on .cx-clock, so a state is declared
   once and every part reads it — mark, word, track and border together, never
   colour alone. The fifth is NO CLOCK: a clock whose start condition has not
   happened is not an empty cell but .cx-absent's dashed grammar, with a drawn,
   empty track (the bound exists; nothing runs on it) and the reason written out.
   The track reads --cx-bar-v and --cx-bar-t inline — the same 0..1 pair
   .cx-progress already borrows — so the shape adds no token. The fill runs on
   --cx-dur-slow, which the reduced-motion query zeroes: the track lands filled and
   nothing moves. No SMIL. */
.cx-clock { display: grid; gap: var(--cx-s2); min-width: 0;
  border: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-lg); padding: 14px 16px; }
/* the chip wraps under the label rather than crushing it — a 190px tile is the
   contextual panel's two-up, and a two-line label there is not a defect */
.cx-clock__hd { display: flex; align-items: baseline; gap: var(--cx-s3); flex-wrap: wrap; }
/* what the clock is ON. Never the display face — labels and dates stay on the working face. */
.cx-clock__what { margin: 0; min-width: 0; font-size: var(--cx-body); font-weight: var(--cx-w-semi); }
.cx-clock__what small { display: block; margin-top: 1px; font-size: var(--cx-sm);
  font-weight: var(--cx-w-regular); color: var(--cx-ink-3); }
/* the count — the one display figure. 34px is the chart card's figure size. */
.cx-clock__figure { margin: 0; display: flex; align-items: baseline; gap: 7px; }
.cx-clock__figure b { font-family: var(--cx-font-figure); font-size: 34px; font-weight: 400;
  line-height: 1; letter-spacing: 0; font-variant-numeric: tabular-nums; }
.cx-clock__figure small { font-size: var(--cx-h3); color: var(--cx-ink-3); }
/* the state chip. It is .cx-pill's box written out rather than reused, because a
   clock's state is not a record's status: it is driven by the parent's data-state
   (one source of truth, the .cx-step idiom) and its calm state is the sunken
   counter-surface, which the semantic pill family has no variant for. */
.cx-clock__state { margin-left: auto; flex: none; display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); white-space: nowrap;
  background: var(--cx-panel-sunken); color: var(--cx-ink-2); }
.cx-clock__state .cx-icon { width: 13px; height: 13px; }
/* the date it bites, and what it was counted from */
.cx-clock__when { margin: 0; display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-clock__when time { font-weight: var(--cx-w-semi); color: var(--cx-ink-2);
  font-variant-numeric: tabular-nums; }
/* the caption carries the word the mark cannot — same grammar as a chart caption */
.cx-clock__cap { margin: 0; font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-3); }
.cx-clock__cap em { font-style: normal; color: var(--cx-ink-2); }
/* the track — bounded clocks only (day N of M). An unbounded clock ships without one
   rather than with a track that guesses at a start. */
.cx-clock__track { position: relative; height: 5px; border-radius: 3px; background: var(--cx-line-2); }
.cx-clock__track > i { position: absolute; inset: 0 auto 0 0; border-radius: 3px;
  width: calc(var(--cx-bar-v) * 100%); background: var(--cx-ink-3);
  transform-origin: left center; animation: cx-clock-fill var(--cx-dur-slow) var(--cx-ease) both; }
@keyframes cx-clock-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
/* a marked date inside the span — certification, a countersignature. Absent unless declared. */
.cx-clock__track > b { position: absolute; top: -3px; bottom: -3px; width: 2px; border-radius: 1px;
  background: var(--cx-ink); left: calc(var(--cx-bar-t) * 100%); display: none; }
.cx-clock[data-mark] .cx-clock__track > b { display: block; }

/* states — mark and word live in the chip; the track and the border only reinforce */
.cx-clock[data-state="soon"] .cx-clock__state { background: var(--cx-warn-wash); color: var(--cx-warn); }
.cx-clock[data-state="over"] .cx-clock__state { background: var(--cx-error-wash); color: var(--cx-error); }
.cx-clock[data-state="done"] .cx-clock__state { background: var(--cx-ok-wash); color: var(--cx-ok); }
.cx-clock[data-state="soon"] .cx-clock__track > i { background: var(--cx-warn); }
.cx-clock[data-state="over"] .cx-clock__track > i { background: var(--cx-error); }
.cx-clock[data-state="done"] .cx-clock__track > i { background: var(--cx-ok); }
.cx-clock[data-state="over"] .cx-clock__when time { color: var(--cx-error); }
.cx-clock[data-state="done"] .cx-clock__figure b { color: var(--cx-ink-2); }
/* the inline form has no chip of its own — the row's chip carries the word — so its figure carries the state */
.cx-clock--inline[data-state="over"] .cx-clock__figure b { color: var(--cx-error); }
.cx-clock--inline[data-state="soon"] .cx-clock__figure b { color: var(--cx-warn); }
/* NO CLOCK — the absence state, in .cx-absent's dashed grammar */
.cx-clock[data-state="none"] { border-style: dashed; border-color: var(--cx-line); }
.cx-clock[data-state="none"] .cx-clock__figure b { color: var(--cx-ink-3); }
.cx-clock[data-state="none"] .cx-clock__state { background: none; color: var(--cx-ink-3);
  border-color: var(--cx-line); padding: 2px 8px; }
.cx-clock[data-state="none"] .cx-clock__track { background: none; border: 1px dashed var(--cx-line); }
.cx-clock[data-state="none"] .cx-clock__track > i { display: none; }

/* ── the list form — "what bites next", ordered by bite date ───────────────
   Rows, not tiles: the container is bare like .cx-rows and each clock carries its
   own divider, its own hover and (below 900px) its own touch target. Two columns —
   what over when on the left, count over state on the right — so a 372px contextual
   panel and a full panel run the same three lines. */
.cx-clocks { display: grid; }
.cx-clock--row { grid-template-columns: minmax(0, 1fr) auto; align-items: baseline;
  column-gap: var(--cx-s3); row-gap: 2px;
  border-width: 0 0 1px; border-style: solid; border-radius: var(--cx-radius-md);
  padding: var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1); }
.cx-clock--row:last-child { border-bottom-width: 0; }
/* the absence state paints a dashed BOX; in the list it is carried by the empty
   dashed track and the chip instead, so the divider stays the register's own */
.cx-clock--row[data-state="none"] { border-bottom-style: solid; border-color: var(--cx-line-2); }
.cx-clock--row .cx-clock__what   { grid-column: 1; grid-row: 1; }
.cx-clock--row .cx-clock__figure { grid-column: 2; grid-row: 1; justify-self: end; }
.cx-clock--row .cx-clock__figure b { font-size: 20px; }
.cx-clock--row .cx-clock__figure small { font-size: var(--cx-sm); }
.cx-clock--row .cx-clock__when   { grid-column: 1; grid-row: 2; }
.cx-clock--row .cx-clock__state  { grid-column: 2; grid-row: 2; justify-self: end; margin-left: 0; }
.cx-clock--row .cx-clock__track  { grid-column: 1 / -1; margin-top: 5px; }
.cx-clock--row .cx-clock__cap    { grid-column: 1 / -1; }
/* a row with records behind it becomes a button; one without stays visibly static */
button.cx-clock--row, .cx-clock--row[tabindex] { width: 100%; font: inherit; text-align: left;
  color: var(--cx-ink); background: none; cursor: pointer; }
button.cx-clock--row:hover, .cx-clock--row[tabindex]:hover { background: var(--cx-ground); }

/* ── the inline form — a register row's clock, in the gauge track ──────────
   No new row class: .cx-row's fourth track already holds "value against its
   target", which is what a bounded clock is. The chip loses its box and stacks
   count over state, right-aligned, so it reads as a cell rather than a card. */
.cx-clock--inline { display: inline-grid; justify-items: end; gap: 1px;
  border: 0; border-radius: 0; padding: 0; }
.cx-clock--inline .cx-clock__figure { gap: 4px; }
.cx-clock--inline .cx-clock__figure b { font-size: var(--cx-h3); }
.cx-clock--inline .cx-clock__figure small { font-size: var(--cx-sm); }
.cx-clock--inline .cx-clock__state { margin-left: 0; padding: 2px 7px; }
/* THE BASE STATE OF A SET IS NOT PRINTED. `running` is the clock's default — nearly half of
   the inline clocks in the package said it — so it asserted nothing while spending a chip,
   a mark and a word next to a figure that already carries the meaning. Clipped, not
   display:none, so the word stays in the accessibility tree; clip-path rather than
   position:absolute because .cx-clock--inline sets no position and an absolutely positioned
   chip would escape to an unknown containing block. Scoped to --inline only: in the tile
   and the row forms the clock IS the whole object and there is no adjacent figure to carry
   it. The .cx-cal__entry[data-state="confirmed"] .cx-cal__word idiom. */
.cx-clock--inline[data-state="running"] .cx-clock__state {
  width: 1px; height: 1px; padding: 0; overflow: hidden;
  white-space: nowrap; clip-path: inset(50%); }
.cx-clock--inline .cx-clock__state .cx-icon { width: 12px; height: 12px; }
.cx-clock--inline[data-state="none"] { border: 0; }

@media (max-width: 900px) {
  /* the sheet band: every clock in a list is a 44px target */
  .cx-clock--row { min-height: 44px; align-content: center; }
  /* the narrow row keeps three tracks and the clock takes the last one, so the
     movement figure folds — a 44px row has room for one thing at its edge, and
     the date that bites is that thing. */
  .cx-clock--inline ~ .cx-row__move, .cx-clock--inline ~ .cx-row__more { display: none; }
  .cx-clock--inline .cx-clock__state { padding: 2px 5px; gap: 4px; }
}

/* ── stage chain — a record moving through named stages, in order ──────────
   COVERAGE.md shape 6, built 2026-08-28. A chain is not a progress bar. A progress
   bar says how far along; a chain says WHICH RULE the record is being held to —
   the stage names ARE the client's own procedure, written down where the record
   lives. So every stage carries four things and never fewer: a NAME, a DATE, the
   EVIDENCE that closed it (what proved it — a countersignature, a PO number), and
   a STATE. Take the evidence away and the chain becomes decoration; the date on
   its own never says who agreed to it.

   The base is the ingestion timeline's own. `.cx-steps` / `.cx-step` was authored
   for Capture's run and is now one CASE of this shape, sharing the rule heads
   above (search "stage chain, base") rather than being forked into a second copy.
   The ingestion run keeps every declaration it had and renders unchanged; the
   chain adds what a record needs that a run does not — states, evidence, the
   header stepper, the register form and the absence grammar.

   SIX STATES on one attribute, `data-state`, mark + word together in every one:
     done      a canon ring with a tick, and the date it happened
     current   an accent ring; the stage the record is AT
     awaiting  a quiet ring carrying the stage's ORDINAL — a future stage
     blocked   an error ring; cannot advance, AND THE REASON IS A LINE. A red ring
               says something is wrong and nothing about what, which on a stage
               that costs money is not a state, it is a shrug.
     skipped   drawn, never removed. A chain with a stage deleted out of it reads
               as a shorter rule, and a shorter rule is a different rule.
     absent    the sixth, and the one this shape exists for: a stage whose
               PRECONDITION NEVER HAPPENED has no date to carry and cannot invent
               one. .cx-absent's dashed grammar, the reason written out, and
               nothing downstream may claim a date either — a billing packet
               returned incomplete has no certification, so no clock runs.
   The attribute sits on the STAGE, except in --mini where the chain itself is the
   thing with a state (the row is reporting on the record, not on one stage).

   A stage whose date BITES composes .cx-clock--inline. The certified stage runs
   "day N of 45" and that is the clock's job, already built and already stateful —
   a chain that grew its own countdown would be a second clock with a second set
   of thresholds to keep honest.

   THREE FORMS, one markup, one class set:
     the RAIL (default)  the record-detail form for the 372px contextual panel —
                         one stage per row, each with its date and its evidence.
     --across            the stepper across a record header: marks and names, the
                         current one OPEN (its date, its evidence, its clock). It
                         is authored at 901px and up only, so below that it is the
                         rail again with nothing to undo — five named stages side
                         by side at 390px is a row of truncated words.
     --mini              a register row's "where is this record": 3 of 5 ·
                         certified. The marks carry ORDER and POSITION by SHAPE —
                         filled, hollow, pill, dot, hairline — never by colour, and
                         the stage word carries the state, which is why the word is
                         not optional in this form.

   The rail's connector is drawn by the stage's own ::before, so the chain needs no
   wrapper element and no token; it stops at the last stage, and it goes DASHED
   through a skipped or absent stage — nothing flowed there. Zero new tokens: the
   chain reads the ink, line and semantic ramps that were already here.

   No animation of its own. The only motion in a chain is the clock chip a current
   stage may carry, on --cx-dur-slow, which the reduced-motion query zeroes. No SMIL. */
.cx-chain__stage { position: relative; }
.cx-chain__name { margin: 0; font-weight: var(--cx-w-medium); }
.cx-chain__when { margin: 0; }
.cx-chain__when time { font-weight: var(--cx-w-semi); color: var(--cx-ink-2);
  font-variant-numeric: tabular-nums; }
/* the right column: the date over the state word, both hard right */
.cx-chain__meta { display: grid; justify-items: end; gap: 2px; text-align: right; }
/* the WORD. The mark is the ring's glyph in the same row; neither ships alone. */
.cx-chain__state { display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); color: var(--cx-ink-3);
  white-space: nowrap; }
/* the connector — this stage's mark to the next one's */
.cx-chain > .cx-chain__stage::before { content: ""; position: absolute;
  left: 10.25px; top: 32px; bottom: -6px; width: 1.5px;
  background: var(--cx-line); border-radius: 1px; }
.cx-chain > .cx-chain__stage:last-child::before { display: none; }

/* states — the ring and the word together; done and current take their ring from
   the shared base above, which the ingestion timeline already defined */
.cx-chain__stage[data-state="blocked"] .cx-chain__mark { border-color: var(--cx-error);
  color: var(--cx-error); }
.cx-chain__stage[data-state="skipped"] .cx-chain__mark,
.cx-chain__stage[data-state="absent"] .cx-chain__mark { border-style: dashed;
  border-color: var(--cx-line); color: var(--cx-ink-3); }
.cx-chain__stage[data-state="done"]    .cx-chain__state { color: var(--cx-ok); }      /* a stage closing is a process finished — ok, not canon (red-team 2026-08-29) */
.cx-chain__stage[data-state="current"] .cx-chain__state { color: var(--cx-accent-quiet); }
.cx-chain__stage[data-state="blocked"] .cx-chain__state { color: var(--cx-error); }
.cx-chain__stage[data-state="skipped"] .cx-chain__name { color: var(--cx-ink-3); }
/* nothing flowed through a stage that was skipped or never started */
.cx-chain > .cx-chain__stage[data-state="skipped"]::before,
.cx-chain > .cx-chain__stage[data-state="absent"]::before { background: none;
  border-left: 1.5px dashed var(--cx-line); }

/* the reason — a LINE, not a colour. Dashed for an absence (.cx-absent's grammar),
   washed for a block (.cx-advisory's), because one is a hole and the other is a wall. */
.cx-chain__why { grid-column: 2 / -1; margin: 7px 0 0; padding: 9px 11px;
  border: 1px dashed var(--cx-line); border-radius: var(--cx-radius-md);
  font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-2); }
.cx-chain__stage[data-state="blocked"] .cx-chain__why { border-style: solid;
  border-color: color-mix(in oklab, var(--cx-error) 25%, transparent);
  background: var(--cx-error-wash); color: var(--cx-error); }
/* the clock chip a current stage carries, under its state word */
.cx-chain__meta .cx-clock--inline { margin-top: 3px; }

/* ── --across, the record header's stepper ─────────────────────────────────
   901px and up ONLY: below it, the base rail is already the right answer and there
   is nothing to undo. Every stage keeps its word; the OPEN one adds its date, its
   evidence line and its clock. */
@media (min-width: 901px) {
  .cx-chain--across { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
    column-gap: var(--cx-s3); align-items: start; }
  .cx-chain--across > .cx-chain__stage { grid-template-columns: minmax(0, 1fr);
    gap: 0; padding: 0; }
  /* name and state resolve to the same size and weight in this form, and the gallery
     ships "Certified" over "Certified" 2px apart. The name is the label; it outranks. */
  .cx-chain--across .cx-chain__name { margin-top: var(--cx-s2); font-size: var(--cx-sm);
    font-weight: var(--cx-w-semi); }
  .cx-chain--across .cx-chain__meta { justify-items: start; text-align: left; margin-top: 2px; }
  .cx-chain--across .cx-chain__why { grid-column: 1; }
  .cx-chain--across .cx-chain__meta .cx-clock--inline { justify-items: start; }
  .cx-chain--across > .cx-chain__stage::before { top: 10.25px; bottom: auto;
    left: 26px; right: calc(var(--cx-s3) * -1); width: auto; height: 1.5px; }
  /* closed by default; the record is AT one stage and that is the one that opens */
  .cx-chain--across .cx-chain__when,
  .cx-chain--across .cx-chain__name small { display: none; }
  .cx-chain--across .cx-chain__stage[data-state="current"] .cx-chain__when,
  .cx-chain--across .cx-chain__stage[data-state="current"] .cx-chain__name small,
  .cx-chain--across .cx-chain__stage[data-state="blocked"] .cx-chain__when,
  .cx-chain--across .cx-chain__stage[data-state="blocked"] .cx-chain__name small { display: block; }
}

/* ── --mini, the register row's chain ──────────────────────────────────────
   Sits in .cx-row's fourth track — the one that already holds "value against its
   target" — so it needs no new row class, the same reason .cx-clock--inline needed
   none. State is on the CHAIN here: the row reports where the record is, not what
   one stage is doing. */
.cx-chain--mini { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px;
  justify-content: flex-end; min-width: 0; }
.cx-chain--mini > .cx-chain__stage { display: block; padding: 0; width: 7px; height: 7px;
  border-radius: 2px; background: var(--cx-line-strong); }
.cx-chain--mini > .cx-chain__stage::before { display: none; }
.cx-chain--mini > .cx-chain__stage[data-state="done"]     { background: var(--cx-ok); }
.cx-chain--mini > .cx-chain__stage[data-state="current"]  { width: 12px; border-radius: 4px;
  background: var(--cx-accent); }
/* the quiet three take INK-3, not a line token: at 7px a hairline-coloured mark is
   gone in dark, and a mark that cannot be seen is not a mark */
.cx-chain--mini > .cx-chain__stage[data-state="awaiting"] { background: none;
  box-shadow: inset 0 0 0 1.5px var(--cx-ink-3); }
.cx-chain--mini > .cx-chain__stage[data-state="blocked"]  { border-radius: 50%;
  background: var(--cx-error); }
.cx-chain--mini > .cx-chain__stage[data-state="skipped"]  { height: 1.5px; border-radius: 1px;
  background: var(--cx-ink-3); }
.cx-chain--mini > .cx-chain__stage[data-state="absent"]   { border-radius: 50%; background: none;
  border: 1.5px dashed var(--cx-ink-3); box-sizing: border-box; }
/* the word takes its own line under the marks. A 120px track holds five marks OR a
   stage word, never both side by side — the clock's inline chip stacks for the same
   reason, and a cell that overruns its track paints over the figure beside it. */
.cx-chain--mini .cx-chain__state { flex: 0 0 100%; margin-top: 2px; justify-content: flex-end;
  text-align: right; white-space: normal; color: var(--cx-ink-2); }
.cx-chain--mini[data-state="blocked"] .cx-chain__state { color: var(--cx-error); }
.cx-chain--mini[data-state="absent"]  .cx-chain__state { color: var(--cx-ink-3); }

@media (max-width: 900px) {
  /* the sheet band: a stage is a tap area for its own evidence */
  .cx-chain:not(.cx-chain--mini) > .cx-chain__stage { min-height: 44px; }
  /* the register keeps three tracks and the chain takes the last one, so the
     movement figure folds — the same trade .cx-clock--inline makes, and for the
     same reason: an 84px track holds one thing, and where the record is IS it.
     The word wraps under the marks rather than being dropped: at this width the
     marks are position and the word is the state. */
  .cx-chain--mini ~ .cx-row__move, .cx-chain--mini ~ .cx-row__more { display: none; }
}
/* ── queue — what arrived, and who owes it an answer ───────────────────────
   COVERAGE.md shape 3, completed 2026-08-28. The package already had the
   ATTENTION ROW — .cx-attn__group, its header and count, the why line, the dot,
   the line and its one action — and COVERAGE recorded it as PARTIAL for the
   queue: no assignee, no age, no routing. This block completes it rather than
   forking it. Everything the attention row already owns is reused unchanged;
   the unread dot is .cx-row__dot; a wait that has an SLA is .cx-clock--inline;
   and only the four facts a queue adds get selectors of their own. .cx-queue__rows
   on the list re-lays .cx-attn__row as a grid, so an attention list with no
   .cx-queue around it is untouched — which is why the package's existing
   .cx-attn specimens still render exactly as they did.

   THE FOUR FACTS a queue adds to a flagged line: what CHANNEL it arrived on,
   which ENTITY it was routed to, WHOM it is waiting on, and how long it has
   WAITED. Plus a fifth thing that is not a fact — the machine's reading, which
   on a queue is a PROPOSAL (whose this is, what it is about) and is marked as
   one under the AI-signal rule: the tinted surface, the sparkle, and the word
   "proposed" written out. It stays a proposal until a person confirms it.

   SIX STATES on one attribute, `data-state` on the row, so the state is declared
   once and the chip, the subject's weight and the age all read it — mark AND
   word in every one, never colour alone. The sixth is the ABSENCE: UNROUTED is
   not an empty cell but .cx-absent's dashed grammar in the routed-to and
   awaiting tracks, with the machine's best guess demoted to the reading, where a
   guess belongs. "We could not tell whose this is" is something the system
   knows, and a queue that hides it hands the operator a silent backlog.

   AGE IS A CLOCK, and the package has one. A plain wait is .cx-queue__age on the
   working face with tabular numerals — a row's age is not a display figure, so it
   never takes --cx-font-figure; only the header's counts do. A wait with an SLA
   drops .cx-clock--inline into the same track, verbatim, with its own data-state.
   The clock's state chip is left out there: the row already carries one, and two
   chips saying the same thing is not two facts.

   Nothing here animates. The one transition is the row's hover, on --cx-dur-fast,
   which the reduced-motion query zeroes. No SMIL, no keyframes, no new token. */
.cx-queue { display: grid; gap: var(--cx-s3); min-width: 0; container-type: inline-size; }

/* the header — a queue's counts ARE display figures (the one place this shape
   wears --cx-font-figure), and each one carries its word, and its mark when it
   carries a hue */
.cx-queue__hd { display: flex; align-items: baseline; gap: var(--cx-s3); flex-wrap: wrap;
  padding-bottom: var(--cx-s2); border-bottom: 1px solid var(--cx-line-2); }
.cx-queue__what { margin: 0; min-width: 0; font-size: var(--cx-h3); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-heading); }
.cx-queue__what small { display: block; margin-top: 1px; font-size: var(--cx-sm);
  font-weight: var(--cx-w-regular); letter-spacing: var(--cx-tr-normal); color: var(--cx-ink-3); }
.cx-queue__counts { margin: 0 0 0 auto; display: flex; align-items: baseline; gap: var(--cx-s4);
  flex-wrap: wrap; }
.cx-queue__count { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap;
  font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-queue__count b { font-family: var(--cx-font-figure); font-size: 20px; font-weight: 400;
  line-height: 1; letter-spacing: 0; font-variant-numeric: tabular-nums; color: var(--cx-ink); }
.cx-queue__count .cx-icon { width: 13px; height: 13px; align-self: center; }
.cx-queue__count[data-state="stale"], .cx-queue__count[data-state="stale"] b { color: var(--cx-warn); }
.cx-queue__count[data-state="unrouted"] b { color: var(--cx-ink-3); }

/* the rows. The list is still .cx-attn__group — its header, count and why line
   are the attention row's and are not re-authored — so .cx-queue__rows only
   un-indents the list box and re-lays each row on seven tracks. EVERY CELL
   DECLARES ITS OWN COLUMN: a row that ships no actions must leave a gap at the
   edge, not slide its neighbours left. And the actions track is a WIDTH, not
   `auto`: every row is its own grid, so an auto last track lets the 1fr subject
   differ row to row and the routed-to chips stop lining up — caught in the first
   render. Track widths are the consumer's to tune; the shape is the package's. */
.cx-queue__rows { margin: 0; padding-inline-start: 0; }
.cx-queue__rows > .cx-attn__row { display: grid;
  grid-template-columns: 14px minmax(0, 1fr) 132px 150px 78px 118px 124px;
  gap: var(--cx-s3); align-items: center;
  padding: var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1);
  border-radius: var(--cx-radius-md); row-gap: 0;
  transition: background var(--cx-dur-fast) var(--cx-ease); }
.cx-queue__rows > .cx-attn__row:hover { background: var(--cx-ground); }
.cx-queue__rows > .cx-attn__row > * { min-width: 0; }
.cx-queue__rows > .cx-attn__row > .cx-row__dot { grid-column: 1; grid-row: 1; }

/* WHAT ARRIVED. The subject truncates on one line the way .cx-row__name does;
   the meta line and the reading below it wrap, because they are sentences. */
.cx-queue__subject { grid-column: 2; grid-row: 1; min-width: 0; font-size: var(--cx-h3); font-weight: var(--cx-w-medium); }
.cx-queue__subject > b { display: block; font-weight: inherit;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cx-queue__subject > small { display: flex; align-items: center; gap: var(--cx-s2); flex-wrap: wrap;
  margin-top: 1px; font-size: var(--cx-sm); font-weight: var(--cx-w-regular); color: var(--cx-ink-3); }
/* the channel it came in on — a silhouette from the sprite and a GENERIC word.
   Never a brand lockup and never a brand name: the shape names the rail, the
   word names the kind, and the word is what survives greyscale. */
/* CONTAINER DISCIPLINE (2026-08-29). A box or a fill around a small piece of text
   is a CLAIM: it says this is a state, or you can act on it. The queue row was
   putting one around every FACT too — the rail it arrived on, the entity that owns
   it, the desk it waits on — so four containers rode every row and the one that
   carried a state had nothing to stand out from. The rule the file already states
   at .cx-scope__state ("eight branches wearing eight loud chips is texture, not
   information") is extended here: facts are a mark and a word; only state and hit
   targets are painted. Geometry is unchanged — the border stays as transparent so
   a cell that DOES carry state paints in place without moving the row. */
.cx-queue__chan { display: inline-flex; align-items: center; gap: 5px; padding: 1px 0;
  border-radius: var(--cx-radius-sm); background: none; color: var(--cx-ink-3); }
.cx-queue__chan .cx-icon { width: 13px; height: 13px; }
/* the machine's reading at row scale — the AI-signal rule's tinted surface and
   sparkle, shrunk to a line. It is a PROPOSAL and the tail says so in words.
   It is a CELL of the row, not a child of the subject, so that when the row folds
   it can take the whole width instead of wrapping six times inside half of it. */
.cx-queue__read { grid-column: 2; grid-row: 2; justify-self: start;
  display: flex; align-items: flex-start; gap: 6px; margin-top: 5px;
  padding: 3px 9px; border-radius: var(--cx-radius-sm);
  background: var(--cx-ai); border: 1px solid var(--cx-ai-line); color: var(--cx-ai-ink);
  font-size: var(--cx-sm); font-weight: var(--cx-w-regular); line-height: var(--cx-lh-snug); }
.cx-queue__read .cx-icon { width: 13px; height: 13px; margin-top: 1px; }
.cx-queue__read > span { min-width: 0; }
.cx-queue__read em { font-style: normal; color: var(--cx-ink-2); }

/* ROUTED TO — which entity owns this: a group company, a branch, a floor, a
   team. A SCOPE chip, not a status pill: it names a place, so it takes the
   chip-in-cell radius rather than the pill's, and it never wears a semantic hue. */
.cx-queue__to { grid-column: 3; grid-row: 1; justify-self: start; max-width: 100%;
  display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px;
  border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  background: none; color: var(--cx-ink-2); font-size: var(--cx-sm); }
/* AUTHORING RULE, not a declaration: an ordinary routed-to cell carries NO glyph. It was
   the same #i-store-ops on 23 of the 24 ordinary cells in the package — a mark identical on
   every instance is not a mark, it is a texture, and it made the two cells that DO differ
   (--proposed's #i-reading, --none's #i-empty) work harder to be seen. A fact cell leads
   with its word and reserves the glyph for the states that diverge from it. The sizing rule
   below still serves those two. */
.cx-queue__to .cx-icon { width: 13px; height: 13px; }
.cx-queue__to span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the system INFERRED the routing. Still a proposal, so the cell wears the
   machine's tint and the sparkle rather than passing itself off as a fact. */
.cx-queue__to--proposed { background: var(--cx-ai); border-color: var(--cx-ai-line);
  color: var(--cx-ai-ink); }
/* UNROUTED, in the cell — .cx-absent's dashed grammar. Nothing is guessed INTO
   the cell; the guess lives in the reading above, where a proposal belongs. */
.cx-queue__to--none { background: none; border-style: dashed; border-color: var(--cx-line);
  color: var(--cx-ink-3); }

/* AWAITING WHOM — a role or a desk. The monogram is the neutral mark ratified
   2026-08-26: a counter-surface chip with ink-2 letters (a role code, never a
   person's initials), or the sprite's people glyph when the queue waits on a
   role rather than on a named desk. */
.cx-queue__who { grid-column: 4; grid-row: 1; display: flex; align-items: center; gap: var(--cx-s2); min-width: 0; }
.cx-queue__mono { flex: none; width: 22px; height: 22px; display: grid; place-items: center;
  border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  background: none; color: var(--cx-ink-2);
  font-size: 9.5px; font-weight: var(--cx-w-bold); letter-spacing: .02em; }
.cx-queue__mono .cx-icon { width: 13px; height: 13px; }
.cx-queue__who span { min-width: 0; font-size: var(--cx-sm); color: var(--cx-ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* nobody yet — the same dashed absence the routed-to cell uses, so the two read
   as one condition rather than two defects */
.cx-queue__who--none .cx-queue__mono { background: none; border-style: dashed;
  border-color: var(--cx-line); color: var(--cx-ink-3); }
.cx-queue__who--none span { color: var(--cx-ink-3); }

/* AGE — how long it has waited. Tabular on the WORKING face: a row's age is not
   a display figure. A wait with an SLA takes .cx-clock--inline in this same
   track instead, unchanged, and the clock's data-state does the painting. */
.cx-queue__age { grid-column: 5; grid-row: 1; justify-self: end; display: grid; justify-items: end; gap: 1px;
  text-align: right; }
.cx-queue__age b { font-size: var(--cx-h3); font-weight: var(--cx-w-semi); line-height: 1.15;
  font-variant-numeric: tabular-nums; }
.cx-queue__age small { font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-queue__rows .cx-clock--inline { grid-column: 5; grid-row: 1; justify-self: end; }
.cx-queue__rows .cx-clock--inline[data-state="soon"] .cx-clock__figure b { color: var(--cx-warn); }
.cx-queue__rows .cx-clock--inline[data-state="over"] .cx-clock__figure b { color: var(--cx-error); }
/* ONE PILL PER ROW. Overdue (the promised date) and Stale (nobody has touched it) are
   two different facts and both keep their word — but two filled pills side by side is
   the noise. The row's own state stays the pill; the clock's state keeps its mark, its
   word and its semantic ink, and drops the box. */
.cx-attn__row .cx-clock--inline .cx-clock__state { background: none; padding-left: 0; padding-right: 0; }

/* the state chip — .cx-clock__state's box written out for the same reason it was
   written out there: it is driven by the PARENT's data-state, one source of
   truth, and its calm state is the sunken counter-surface, which the semantic
   pill family has no variant for. */
.cx-queue__state { grid-column: 6; grid-row: 1; justify-self: start; flex: none;
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  padding: 3px 9px; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium);
  background: var(--cx-panel-sunken); color: var(--cx-ink-2); }
.cx-queue__state .cx-icon { width: 13px; height: 13px; }

/* the row's verbs. Optional, static, few, and .cx-attn__act unchanged — a queue
   row's action IS the attention row's action. No menu: three verbs that fit are
   three verbs, and a menu is where the one that matters goes to hide. */
.cx-queue__acts { grid-column: 7; grid-row: 1; display: flex; align-items: center; gap: var(--cx-s3);
  justify-content: flex-end; }

/* states — mark and word live in the chip; the ink and the weight only reinforce */
.cx-attn__row[data-state="new"] .cx-queue__state { background: var(--cx-accent-wash); color: var(--cx-accent-quiet); }
.cx-attn__row[data-state="stale"] .cx-queue__state { background: var(--cx-warn-wash); color: var(--cx-warn); }
.cx-attn__row[data-state="routed"] .cx-queue__state { background: var(--cx-working-wash); color: var(--cx-ink-2); }
.cx-attn__row[data-state="done"] .cx-queue__state { background: var(--cx-ok-wash); color: var(--cx-ok); }
.cx-attn__row[data-state="new"] .cx-queue__subject > b { font-weight: var(--cx-w-semi); }
.cx-attn__row[data-state="stale"] .cx-queue__age b { color: var(--cx-warn); }
/* what has left the queue reads quieter than what has not */
.cx-attn__row[data-state="routed"] .cx-queue__subject,
.cx-attn__row[data-state="done"] .cx-queue__subject { color: var(--cx-ink-2); }
/* UNROUTED — the absence state, in .cx-absent's dashed grammar */
.cx-attn__row[data-state="unrouted"] .cx-queue__state { background: none;
  border-color: var(--cx-line); color: var(--cx-ink-3); }

@media (max-width: 900px) {
  /* the fold. Routed-to and awaiting drop under the subject; the age stays at the
     edge with the state under it, because at a 44px band the edge holds one thing
     and how long this has waited is that thing. */
  .cx-queue__rows > .cx-attn__row { grid-template-columns: 14px minmax(0, 1fr) auto;
    column-gap: var(--cx-s2); row-gap: 6px; align-items: start; align-content: center;
    min-height: 44px; }
  .cx-queue__rows > .cx-attn__row > .cx-row__dot { grid-row: 1; margin-top: 7px; }
  .cx-queue__subject { grid-row: 1; }
  .cx-queue__age, .cx-queue__rows .cx-clock--inline { grid-column: 3; grid-row: 1; }
  .cx-queue__read  { grid-column: 2 / -1; grid-row: 2; }
  .cx-queue__to    { grid-column: 2; grid-row: 3; }
  .cx-queue__state { grid-column: 3; grid-row: 3; justify-self: end; }
  .cx-queue__who   { grid-column: 2 / -1; grid-row: 4; }
  .cx-queue__acts  { grid-column: 2 / -1; grid-row: 5; justify-content: flex-start; }
  /* every verb is a 44px target here; on the desktop band the row's height carries it */
  .cx-queue__acts .cx-attn__act { min-height: 44px; display: inline-flex; align-items: center; }
  .cx-queue__counts { gap: var(--cx-s3); }
}
/* the same fold by CONTAINER: the seven tracks need ~690px before the subject, which a stage
   beside an open contextual panel does not have at 1400px (the first proof page found the subject
   track computing to 0px). So the queue folds when ITS OWN width is short, viewport aside. */
@container (max-width: 780px) {
  .cx-queue__rows > .cx-attn__row { grid-template-columns: 14px minmax(0, 1fr) auto;
    column-gap: var(--cx-s2); row-gap: 6px; align-items: start; align-content: center; min-height: 44px; }
  .cx-queue__rows > .cx-attn__row > .cx-row__dot { grid-row: 1; margin-top: 7px; }
  .cx-queue__subject { grid-row: 1; }
  .cx-queue__age, .cx-queue__rows .cx-clock--inline { grid-column: 3; grid-row: 1; }
  .cx-queue__read  { grid-column: 2 / -1; grid-row: 2; }
  .cx-queue__to    { grid-column: 2; grid-row: 3; }
  .cx-queue__state { grid-column: 3; grid-row: 3; justify-self: end; }
  .cx-queue__who   { grid-column: 2 / -1; grid-row: 4; }
  .cx-queue__acts  { grid-column: 2 / -1; grid-row: 5; justify-content: flex-start; }
  .cx-queue__acts .cx-attn__act { min-height: 44px; display: inline-flex; align-items: center; }
}
/* ── scope switcher — which single view is open ───────────────────────────
   COVERAGE.md shape 11, built 2026-08-28. Four of eight pipeline clients run
   more than one entity and the shell could only ever name one. Note what this
   is NOT: the scope limit at the head of COVERAGE.md holds — the DS serves ONE
   operator's view, so this control changes WHICH single view is open. It never
   fuses two scopes into one surface and it is not an estate console.

   WHERE IT LIVES — the rail's top, in place of the mark and the name. That row
   is already the identity block: monogram, wordmark, collapse. The switcher is
   that block made answerable, and putting it anywhere else would give one
   question — whose data is this? — two answers inside one frame. The rest falls
   out of the placement for free: the collapsed rail already stacks .cx-rail-top
   and drops its labels, so the switcher becomes a monogram button with no new
   breakpoint; below 900px the rail IS the drawer, so the switcher is already in
   the drawer header. No shell rule was amended — this block reads
   .cx-app[data-rail="collapsed"] the way .cx-rail-searchbtn already does. An
   instance with ONE scope keeps .cx-rail-mark and .cx-rail-name; the switcher
   appears when there is something to switch to, and never as decoration.

   ONE MARKUP, THREE DEPTHS. One level (a group's companies), two levels
   (corporation → branch) and the BROKER TIER (one operator acting for several
   client profiles) are the same elements. __group names the level, __id's small
   line carries the level above — or the words "acting for", which is what the
   level above IS when the operator stands outside the entity entirely. The
   depth is data; nothing in the markup knows how deep it is.

   FIVE STATES on one attribute, `data-state`, every one carrying a mark AND a
   word: current, available, restricted, syncing, absent. RESTRICTED IS NEVER
   HIDDEN — an operator who cannot open a corporation still has to know it
   exists, or a five-corporation group renders as a one-corporation group, which
   is the more expensive lie (the .cx-tab--soon argument, applied to data rather
   than to screens). ABSENT is a scope with no data yet — a branch added this
   morning — in .cx-absent's dashed grammar with the reason written out and no
   faked zero.

   THE ECHO. Every panel whose data is scoped carries __chip in its header, so a
   screenshot of a single panel is never ambiguous about which branch it shows.
   It is NOT .cx-pill: that family is the semantic status set, and a scope is not
   a status — painting it canon or warn would claim a meaning it does not have,
   and the neutral pill has no variant. The same reasoning gave .cx-clock its own
   chip. It is quiet by construction and goes loud only when the scope's own
   state is loud.

   No JavaScript is required: the open list is markup, and every state the CSS
   paints is declared. A consumer binds aria-expanded, roving focus and
   type-ahead on the real control. The one motion is an opacity-and-lift on
   --cx-dur, which the reduced-motion query zeroes — the list simply is there.
   No SMIL. */
.cx-scope { position: relative; display: block; min-width: 0; flex: 1 1 auto; }

/* the trigger — the rail's mark-and-name block, made answerable. Its negative
   left margin puts the monogram exactly where .cx-rail-mark stood, so adding a
   switcher does not move the rail's own left edge. */
.cx-scope__btn { display: flex; align-items: center; gap: var(--cx-s2); width: 100%;
  font: inherit; font-size: var(--cx-body); text-align: left; color: var(--cx-ink);
  cursor: pointer; background: none; border: 1px solid transparent;
  border-radius: var(--cx-radius-md); padding: var(--cx-s1) var(--cx-s2);
  margin-left: calc(var(--cx-s2) * -1); }
/* the rail sits ON the ground, so a control in it hovers to the panel — the same
   fill .cx-rail-item and .cx-rail-conn already take */
.cx-scope__btn:hover { background: var(--cx-panel); border-color: var(--cx-line); }

/* the monogram. The 26px box of .cx-rail-mark, because it is what the switcher
   replaces; a scope you are NOT in drops to the package's neutral monogram. */
.cx-scope__mark { width: 26px; height: 26px; flex: none; display: grid; place-items: center;
  border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  background: var(--cx-accent); color: var(--cx-ink-on-accent);
  font-weight: var(--cx-w-bold); font-size: var(--cx-sm); }

/* the identity — the operative scope over the level above it */
.cx-scope__id { min-width: 0; flex: 1 1 auto; display: grid; line-height: var(--cx-lh-tight); }
.cx-scope__id b { font-size: var(--cx-h3); font-weight: var(--cx-w-semi);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-scope__id small { font-size: var(--cx-sm); font-weight: var(--cx-w-regular);
  color: var(--cx-ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-scope__chev { flex: none; color: var(--cx-ink-3);
  transition: transform var(--cx-dur) var(--cx-ease); }
.cx-scope__btn[aria-expanded="true"] .cx-scope__chev { transform: rotate(180deg); }

/* the list. It genuinely floats above the plane, so it takes the popover shadow
   — one of the three places the panel world admits elevation at all. */
.cx-scope__list, .cx-select__list { position: absolute; z-index: 50; top: calc(100% + var(--cx-s1)); left: 0;
  min-width: 100%; width: 264px; max-height: 60vh; overflow-y: auto; padding: var(--cx-s1);
  background: var(--cx-panel); border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-md); box-shadow: var(--cx-shadow-popover);
  animation: cx-scope-open var(--cx-dur) var(--cx-ease) both; }
@keyframes cx-scope-open { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
/* the quiet search, when the list is long enough to need one. .cx-search
   verbatim — a second search box that looks like the first is two things to
   keep honest. Only the inset moves, because the field sits inside a padded
   popover rather than in a column of its own. */
.cx-scope .cx-search { padding: var(--cx-s1) var(--cx-s1) var(--cx-s2); }
.cx-scope .cx-search .cx-input { padding-left: 30px; }
.cx-scope .cx-search-icon { left: 13px; }
/* the level heading — entities, then their branches; recently used first */
.cx-scope__group, .cx-select__group { font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-ink-3);
  padding: var(--cx-s2) var(--cx-s2) var(--cx-s1); }
.cx-scope__opt, .cx-select__opt { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--cx-s2); align-items: center; width: 100%; text-align: left; font: inherit;
  font-size: var(--cx-body); color: var(--cx-ink); background: none; border: 0;
  border-radius: var(--cx-radius-sm); padding: 6px var(--cx-s2); cursor: pointer; }
/* an element on a panel hovers to the ground */
.cx-scope__opt:hover, .cx-select__opt:hover { background: var(--cx-ground); }
/* a scope you are not in: the package's neutral monogram, a nested fill on a panel */
.cx-scope__opt .cx-scope__mark { width: 22px; height: 22px; font-size: 10px;
  background: var(--cx-panel-sunken); border-color: var(--cx-line); color: var(--cx-ink-2); }

/* the state chip — .cx-clock__state's box, for the same reason it was written
   there: a scope's state is not a record's status, so the semantic pill family
   is the wrong family. AVAILABLE is the base and stays the quietest thing on
   the surface — eight branches wearing eight loud chips is texture, not
   information — but it still carries its mark and its word. */
.cx-scope__state { display: inline-flex; align-items: center; gap: 5px; flex: none;
  padding: 3px 9px; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); white-space: nowrap;
  background: none; color: var(--cx-ink-3); }
.cx-scope__btn .cx-scope__state { margin-left: var(--cx-s2); }
/* "Available" is not a state, it is the absence of one, and it was printed on a third of
   the scope states. Same rule as the inline clock above. position:absolute IS safe here —
   .cx-scope sets position: relative — and scoping to __opt leaves the trigger button's own
   chip alone, where the state is the only thing the collapsed control can say. */
.cx-scope__opt[data-state="available"] .cx-scope__state {
  position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden;
  white-space: nowrap; clip-path: inset(50%); }
.cx-scope__state .cx-icon, .cx-scope__chip .cx-icon { width: 13px; height: 13px; }

/* ── the five states, one attribute, mark and word in every one ────────────
   CURRENT is a SELECTION, not a verification: it takes the package's selected
   treatment (accent wash, quiet accent ink) and never canon teal, which is
   reserved for verified truth. SYNCING borrows info, the one semantic that
   means "in flight and nothing is wrong". RESTRICTED is dimmed, not ghosted,
   and keeps its lock and its word. ABSENT is the dashed grammar of .cx-absent
   with the reason on the option's own second line. */
.cx-scope__opt[data-state="current"] { background: var(--cx-accent-wash); }
.cx-scope__opt[data-state="current"] .cx-scope__id b { color: var(--cx-accent-quiet); }
.cx-scope [data-state="current"] .cx-scope__state,
.cx-scope__chip[data-state="current"] { color: var(--cx-accent-quiet); }
.cx-scope [data-state="syncing"] .cx-scope__state,
.cx-scope__chip[data-state="syncing"] { background: var(--cx-info-wash); color: var(--cx-info); }
.cx-scope [data-state="restricted"] .cx-scope__state,
.cx-scope__chip[data-state="restricted"] { background: var(--cx-working-wash); color: var(--cx-ink-2); }
.cx-scope [data-state="absent"] .cx-scope__state,
.cx-scope__chip[data-state="absent"] { background: none; border-color: var(--cx-line);
  border-style: dashed; color: var(--cx-ink-3); }
.cx-scope__opt[data-state="restricted"] { cursor: not-allowed; }
.cx-scope__opt[data-state="restricted"] .cx-scope__id b { color: var(--cx-ink-2);
  font-weight: var(--cx-w-medium); }
.cx-scope__opt[data-state="restricted"] .cx-scope__mark { opacity: .6; }
.cx-scope__opt[data-state="absent"] .cx-scope__mark,
.cx-scope__btn[data-state="absent"] .cx-scope__mark { background: none; border-style: dashed;
  border-color: var(--cx-line); color: var(--cx-ink-3); }

/* ── the echo — the scope chip in a panel header ───────────────────────────
   Quiet by construction: the sunken counter-surface, no border, ink-2 at sm,
   never the accent. It is a label on the panel's data, not a control. A nested
   fill inside a sunken chip is border-only by the package's own surface rule,
   so the chip's monogram is an outline rather than a second filled box. */
.cx-scope__chip { display: inline-flex; align-items: center; gap: 6px; min-width: 0;
  max-width: 100%; padding: 2px 10px 2px 3px; border: 1px solid transparent;
  /* NO FILL. This chip's own block comment promises it is "quiet by construction and goes
     loud only when the scope's own state is loud" — and then the base rule painted
     panel-sunken on all of it, so 27 of 29 instances were a container around a pure fact.
     The transparent border stays, so a chip that DOES carry a state paints in place without
     moving the header: the .cx-queue__chan trick. */
  border-radius: var(--cx-radius-sm); background: none;
  color: var(--cx-ink-2); font-size: var(--cx-sm); font-weight: var(--cx-w-medium); }
.cx-scope__chip .cx-scope__mark { width: 18px; height: 18px; font-size: 9px;
  background: none; border-color: var(--cx-line); color: var(--cx-ink-3); }
.cx-scope__chip > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the level ABOVE the operative scope, inside the chip. It is the part a narrow
   header can afford to lose — the monogram and the branch still name the scope —
   so it is marked rather than concatenated into one string. */
.cx-scope__chip small { font-size: inherit; color: var(--cx-ink-3); }

/* ── the collapsed rail — the switcher survives as its monogram ────────────
   No new breakpoint. The collapsed rail already stacks .cx-rail-top and drops
   its labels; the switcher drops its identity block and its chevron and what is
   left is the mark, which is the one thing a 64px column has room for. The list
   keeps its own width and opens over the stage the way a popover does — a 64px
   column of branch names would be unreadable. */
.cx-app[data-rail="collapsed"] .cx-scope { flex: none; width: 100%; }
.cx-app[data-rail="collapsed"] .cx-scope__btn { justify-content: center; margin-left: 0;
  padding-left: 0; padding-right: 0; }
/* the chevron goes; the NAME stays in the tree. display:none made the switcher
   announce as its monogram ("C"), which contradicts the rail rule above that a
   collapsed item keeps its name. Same clip idiom .cx-cal__word already uses. */
.cx-app[data-rail="collapsed"] .cx-scope__chev { display: none; }
.cx-app[data-rail="collapsed"] .cx-scope__id { position: absolute; clip-path: inset(50%);
  width: 1px; height: 1px; overflow: hidden; white-space: nowrap; }

@media (max-width: 900px) {
  /* the drawer header. The rail IS the drawer here, so the switcher is already
     in it and nothing moves; what changes is the touch floor, and a list that
     takes the trigger's width instead of carrying one of its own. */
  .cx-scope__btn, .cx-scope__opt { min-height: 44px; }
  /* border-box locally: .cx-input is content-box in this package, so a bare
     min-height would pay for its padding twice and land at 62px */
  .cx-scope .cx-search .cx-input { min-height: 44px; box-sizing: border-box; }
  .cx-scope__list { left: 0; right: 0; width: auto; max-height: 50vh; }
  /* the echo drops the level above rather than ellipsing the branch away: a
     header 390px wide cannot hold a full path, and the half worth keeping is
     the scope the panel is actually showing */
  .cx-scope__chip small { display: none; }
}
/* ── calendar / schedule — one entry model, three ways to look at it ───────
   COVERAGE.md shape 5, built 2026-08-28. The clock answered "a date that bites";
   this answers "what is on, and when". ONE ENTRY — what, when, who or where, and
   a state — rendered as MONTH (the overview), WEEK (the day columns) and AGENDA
   (the list). They are VIEWS, not three components: the same chip carries an
   entry in the month and in the week's all-day lane, and where a row is a row
   the agenda IS .cx-row on a calendar template, so the register's container, its
   hover, its selection and its 44px band arrive already built.

   THE MONTH IS NOT A GRID OF CELLS. lane.md kills the ruled-cell grid twice, on
   independent grounds — the product has no table anywhere, and the family's own
   brief files "glanceable metrics dashboards, where rules add structure nobody
   reads" under Does not fit. So a month here is a RHYTHM OF DAY COLUMNS: seven
   tracks, no vertical rules, no boxed cell, no per-day fill, and one hairline
   BETWEEN weeks in --cx-line-2 — the divider a panel already draws inside
   itself. What separates Tuesday from Wednesday is the gap, the same way what
   separates two panels is the ground.

   FOUR STATES on one attribute, `data-state` on the entry, so chip, bar and row
   all read one declaration — confirmed, held, cancelled (STRUCK, never removed:
   a cancelled tasting is still a fact about that Thursday) and NO DATE, an entry
   the system cannot place at all. The fourth is why a month carries a footer:
   something awaiting the client's date has nowhere to sit in a month, so it is
   NAMED under one in .cx-absent's dashed grammar rather than quietly dropped.

   COLOUR IS THE CATEGORY, NEVER THE STATE. A chip is series-1, series-2 or the
   neutral counter-surface and that is the whole palette (two series is a computed
   ceiling; beyond two a calendar facets into lanes, it does not invent a hue).
   State rides on the MARK, whose shape survives greyscale, and on a WORD placed
   in FRONT of the entry so a narrow column truncates the booking and never its
   state. The one place the word is spent rather than printed is a confirmed chip
   in a month cell: confirmed asserts nothing that colour is carrying, the busy
   week is the whole job of that view, and the agenda row — the form that ships
   below 900px and in the 372px panel — carries mark and word in all four.

   Geometry reads --cx-bar-v and --cx-bar-t inline, the same 0..1 pair .cx-progress
   and .cx-clock already borrow, here as a timed bar's start and end through the
   day; the hour band is --cx-row-comfortable, because an hour is exactly one
   comfortable row. So the shape adds no token. The only motion is a hover
   transition on --cx-dur-fast, which the reduced-motion query zeroes. No SMIL. */
.cx-cal { display: grid; gap: var(--cx-s3); min-width: 0; }

/* the chrome, and it invents nothing: .cx-iconbtn is a surface's own affordance,
   .cx-btn--quiet is its quiet verb and .cx-seg is the shell's switch. A calendar
   that ships a second segmented control is how a product ends up with two. */
.cx-cal__nav { display: flex; align-items: center; gap: var(--cx-s2); flex-wrap: wrap; }
.cx-cal__period { margin: 0; font-size: var(--cx-h3); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-heading); font-variant-numeric: tabular-nums; }
.cx-cal__nav .cx-seg { margin-left: auto; }
.cx-cal__nav [data-nav="prev"] .cx-icon { transform: rotate(90deg); }
.cx-cal__nav [data-nav="next"] .cx-icon { transform: rotate(-90deg); }

/* ── month — a rhythm of day columns ──────────────────────────────────────
   Seven equal tracks, gap-separated. The weekday labels are the file's uppercase
   eyebrow; the day number is a quiet LABEL and never a display figure — Departure
   Mono is for figures that are read as quantities, and the 14th is not a quantity. */
.cx-cal__month { display: grid; min-width: 0; }
.cx-cal__dow, .cx-cal__week, .cx-cal__days {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--cx-s2); min-width: 0; }
.cx-cal__dow > * { font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-ink-3);
  padding-bottom: var(--cx-s1); }
/* THE ONE RULE IN A MONTH, and it is the divider a panel already draws */
.cx-cal__week { padding: var(--cx-s2) 0; border-top: 1px solid var(--cx-line-2); }
.cx-cal__dow + .cx-cal__week { border-top: 0; padding-top: 0; }
.cx-cal__day { display: grid; gap: 3px; align-content: start; min-width: 0; }
.cx-cal__num { display: flex; align-items: center; gap: 5px; min-height: 20px;
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); color: var(--cx-ink-2);
  font-variant-numeric: tabular-nums; }
.cx-cal__num em { font-style: normal; font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-ink-3); }
/* TODAY — a quiet ring AND the word. Neither is asked to carry it alone, and the
   ring is a border on the number rather than a fill on the day: filling a day is
   how a rhythm of columns turns back into a grid of cells. */
.cx-cal__num > span { display: inline-grid; place-items: center; min-width: 20px; height: 20px;
  padding: 0 5px; border-radius: var(--cx-radius-pill); border: 1px solid transparent; }
.cx-cal__day[data-today] .cx-cal__num { color: var(--cx-accent-quiet); font-weight: var(--cx-w-semi); }
.cx-cal__day[data-today] .cx-cal__num > span { border-color: var(--cx-accent);
  background: var(--cx-accent-wash); }
.cx-cal__num b { font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; }
/* outside the month: dimmed, never removed — a month that opens on a Wednesday
   still has a Monday, and its bookings did not stop existing */
.cx-cal__day[data-out] .cx-cal__num { color: var(--cx-ink-3); font-weight: var(--cx-w-regular); }
.cx-cal__day[data-out] .cx-cal__entry { opacity: .6; }

/* ── the entry chip — one line, three fills and no fourth ─────────────────
   Was "a capsule" until the 2026-09-02 de-pill; it is a rounded chip now and the
   three fills are still the whole vocabulary. */
.cx-cal__entry { display: flex; align-items: center; gap: 5px; width: 100%; min-width: 0;
  padding: 3px 8px; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  background: var(--cx-panel-sunken); font: inherit; font-size: var(--cx-sm);
  color: var(--cx-ink); text-align: left; cursor: pointer;
  transition: border-color var(--cx-dur-fast) var(--cx-ease); }
/* the name truncates; the mark and the state word never do */
.cx-cal__entry > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-cal__entry .cx-icon { width: 12px; height: 12px; color: var(--cx-ink-3); }
.cx-cal__word { flex: none; font-weight: var(--cx-w-semi); }
/* a confirmed chip keeps its word for assistive tech and the agenda; the month and week hide it visually so the name survives a 140px column */
.cx-cal__entry[data-state="confirmed"] .cx-cal__word { position: absolute; clip-path: inset(50%); width: 1px; height: 1px; overflow: hidden; white-space: nowrap; }
/* a chip is panel-filled at rest, so it hovers by BORDER — the settled rule */
.cx-cal__entry:hover { border-color: var(--cx-line-strong); }
.cx-cal__entry[data-series="1"] { background: color-mix(in oklab, var(--cx-series-1) 14%, transparent); }
.cx-cal__entry[data-series="2"] { background: color-mix(in oklab, var(--cx-series-2) 14%, transparent); }
/* states — the fill stays the CATEGORY's; the border, the rule and the word are
   the state, so a held tasting is still visibly a tasting */
.cx-cal__entry[data-state="held"] { border-style: dashed; border-color: var(--cx-line-strong); }
.cx-cal__entry[data-state="cancelled"] { color: var(--cx-ink-3); }
.cx-cal__entry[data-state="cancelled"] > span { text-decoration: line-through; }
.cx-cal__entry[data-state="cancelled"] .cx-icon,
.cx-cal__entry[data-state="cancelled"] .cx-cal__word { color: var(--cx-error); }
.cx-cal__entry[data-state="none"] { background: none; border-style: dashed;
  border-color: var(--cx-line); color: var(--cx-ink-3); }
/* the overflow line. A day with more entries than fit says so in words and in a
   count, and the count is the way into the agenda — never a silent truncation. */
.cx-cal__more { font: inherit; font-size: var(--cx-sm); color: var(--cx-ink-3);
  background: none; border: 0; padding: 2px 8px; border-radius: var(--cx-radius-sm);
  text-align: left; cursor: pointer;
  transition: color var(--cx-dur-fast) var(--cx-ease), background var(--cx-dur-fast) var(--cx-ease); }
.cx-cal__more:hover { color: var(--cx-ink); background: var(--cx-ground); }

/* ── week — seven day columns, and the hour guides are DASHED, never ruled ──
   The month's refusal holds: no vertical rule between days, no boxed hour cell.
   A timed day does need a reference, so the body carries one DASHED hairline in
   --cx-line-2 an hour, behind everything — dashing already means "a guide, not a
   measured thing" in the chart layer, and with no verticals crossing it nothing
   is ever enclosed. A day-granular domain — renewals, cut-offs, cheque dates —
   has no hours at all: it ships the ALL-DAY LANE and no body, same markup. */
.cx-cal__wk { display: grid; grid-template-columns: 52px minmax(0, 1fr);
  gap: var(--cx-s2) var(--cx-s3); align-items: start; min-width: 0; }
.cx-cal__lab { font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-ink-3);
  text-align: right; padding-top: 3px; }
.cx-cal__allday { display: grid; gap: 3px; align-content: start; min-width: 0; }
.cx-cal__hours { display: grid; text-align: right; font-size: var(--cx-xs);
  color: var(--cx-ink-3); font-variant-numeric: tabular-nums; }
.cx-cal__hours > span { height: var(--cx-row-comfortable); line-height: 1; }
.cx-cal__col { position: relative; min-width: 0; }
/* the guides are real elements, one per hour: a dashed hairline is trivially a
   border and impossible to mistake for a cell wall */
.cx-cal__col > i { display: block; height: var(--cx-row-comfortable);
  border-top: 1px dashed var(--cx-line-2); }
/* the timed bar. Start and end are --cx-bar-v and --cx-bar-t, 0..1 through the
   COLUMN'S DISPLAYED BAND (not the 24-hour day — a 9am-2pm column of six bands
   puts a 10am start at .1667), set inline — the pair the progress capsule and the clock already read.
   Radius is lg, not pill: a 999px radius on a two-hour bar eats its own length,
   and the file already rules that a radius which eats a bar is error, not
   decoration. The all-day lane's entries stay true capsules; they are one line. */
.cx-cal__entry--bar { position: absolute; left: 0; right: 0; width: auto;
  top: calc(var(--cx-bar-v) * 100%); height: calc((var(--cx-bar-t) - var(--cx-bar-v)) * 100%);
  align-items: flex-start; padding: 4px 8px; border-radius: var(--cx-radius-lg); overflow: hidden; }
.cx-cal__entry--bar .cx-icon { margin-top: 2px; }
.cx-cal__entry--bar > span { white-space: normal; line-height: var(--cx-lh-snug); }

/* ── agenda — where a row is a row, it is .cx-row ──────────────────────────
   No new row primitive. The agenda is .cx-rows holding .cx-row on a calendar
   template — when / what / who or where / state — grouped by day, and the group
   holding what has no date at all sits at the foot with the rest rather than in
   a drawer somewhere. This is also the sheet form below 900px and the form that
   fits the 372px contextual panel, so it is the one that must carry every word. */
.cx-cal__agenda { display: grid; }
.cx-cal__group { padding-top: var(--cx-s4); }
.cx-cal__group:first-child { padding-top: 0; }
.cx-cal__group-hd { display: flex; align-items: baseline; gap: var(--cx-s2);
  padding-bottom: var(--cx-s1); font-size: var(--cx-body); font-weight: var(--cx-w-semi); }
.cx-cal__group-hd time { font-size: var(--cx-sm); font-weight: var(--cx-w-regular); color: var(--cx-ink-3); }
.cx-cal__group-hd span { margin-left: auto; font-size: var(--cx-sm);
  font-weight: var(--cx-w-regular); color: var(--cx-ink-3); }
.cx-cal__row { grid-template-columns: 84px minmax(0, 1fr) 132px 120px; min-height: 44px; }
/* a row with a record behind it is a button; the register's own box is kept and
   only the UA's button chrome is discharged */
button.cx-cal__row { width: 100%; font: inherit; text-align: left; color: var(--cx-ink);
  background: none; border-width: 0 0 1px; border-style: solid; border-color: var(--cx-line-2); }
.cx-cal__time { font-size: var(--cx-sm); color: var(--cx-ink-2); font-variant-numeric: tabular-nums; }
.cx-cal__what { font-size: var(--cx-h3); font-weight: var(--cx-w-medium);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cx-cal__what small { display: block; margin-top: 1px; font-size: var(--cx-sm);
  font-weight: var(--cx-w-regular); color: var(--cx-ink-3); text-decoration: none; }
.cx-cal__where { font-size: var(--cx-sm); color: var(--cx-ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* the state chip. Written out rather than reusing .cx-pill for the reason
   .cx-clock__state was: it is driven by the row's own data-state (one source of
   truth), its calm step is the sunken counter-surface and its absence step is not
   a pill at all — neither has a variant in the semantic pill family. */
.cx-cal__state { display: inline-flex; align-items: center; gap: 5px; justify-self: start;
  padding: 3px 9px; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); white-space: nowrap;
  background: var(--cx-panel-sunken); color: var(--cx-ink-2); }
.cx-cal__state .cx-icon { width: 13px; height: 13px; }
.cx-cal__row[data-state="confirmed"] .cx-cal__state { background: var(--cx-ok-wash); color: var(--cx-ok); }
.cx-cal__row[data-state="held"] .cx-cal__state { background: var(--cx-working-wash); color: var(--cx-ink-2); }
.cx-cal__row[data-state="cancelled"] .cx-cal__state { background: var(--cx-error-wash); color: var(--cx-error); }
/* cancelled is STRUCK and kept: removing it loses the fact that the day was booked */
.cx-cal__row[data-state="cancelled"] .cx-cal__what { color: var(--cx-ink-3); text-decoration-line: line-through; }
.cx-cal__row[data-state="cancelled"] .cx-cal__time { color: var(--cx-ink-3); }
/* NO DATE — a first-class absence, in .cx-absent's dashed grammar */
.cx-cal__row[data-state="none"] .cx-cal__state { background: none; border-style: dashed;
  border-color: var(--cx-line); color: var(--cx-ink-3); }
.cx-cal__row[data-state="none"] .cx-cal__time { color: var(--cx-ink-3); }

/* a day whose date BITES composes the clock; it does not grow a second one. The
   inline clock stacks its count over its state right-aligned for a register's
   last track, so a day column re-hangs it left and nothing else changes. */
.cx-cal__day .cx-clock--inline, .cx-cal__allday .cx-clock--inline,
.cx-cal__row > .cx-clock--inline { justify-items: start; }

/* ── the week strip — the month's narrow form ──────────────────────────────
   Seven days across, the letter over the number, and a COUNT rather than chips:
   at 390px a chip is a truncation, and a count with the agenda directly under it
   is the same information without the lie. */
.cx-cal__strip { display: none; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--cx-s1); }
.cx-cal__strip .cx-cal__day { justify-items: center; text-align: center; gap: 2px;
  min-height: 44px; padding: var(--cx-s2) 0; border-radius: var(--cx-radius-md); }
.cx-cal__strip .cx-cal__day[data-today] { background: var(--cx-accent-wash); }
.cx-cal__strip .cx-cal__num { flex-direction: column; gap: 2px; min-height: 0; }
.cx-cal__strip .cx-cal__count { font-size: var(--cx-xs); color: var(--cx-ink-3);
  font-variant-numeric: tabular-nums; }

/* ── the 372px contextual panel form ───────────────────────────────────────
   Beside a record the agenda is what fits. The where column folds into the what's
   second line and the state keeps its own track, because the state is usually the
   reason the panel was opened at all. */
.cx-cal--panel .cx-cal__row { grid-template-columns: 72px minmax(0, 1fr) auto; }
.cx-cal--panel .cx-cal__where { display: none; }
.cx-cal--panel .cx-cal__state { padding: 2px 7px; }
.cx-cal--panel .cx-cal__what { font-size: var(--cx-body); }

@media (max-width: 900px) {
  /* ONE calendar in the sheet band: the week strip over the agenda. A month of
     seven ~48px columns is not a month, and a timed week body at that width is a
     plot nobody can hit — so both desk views hand over and the agenda becomes the
     body. The switch stays and keeps its 44px targets from the package; what it
     changes here is the span the strip covers, not the grammar under it. */
  .cx-cal__month, .cx-cal__wk { display: none; }
  .cx-cal__strip { display: grid; }
  .cx-cal__row { grid-template-columns: 64px minmax(0, 1fr) auto; }
  .cx-cal__where, .cx-cal__row > .cx-clock--inline { display: none; }
  .cx-cal__state { padding: 2px 7px; }
  .cx-cal__nav .cx-seg { margin-left: 0; }
}
/* ── reconciliation / match — two sides that should agree ─────────────────
   COVERAGE.md shape 12, built 2026-08-28. Three pipeline clients reconcile for a
   living — a restaurant chain across five sets of books, a branch manager's POS
   against the cash drawer, a forwarder's receivables against payments in tranches
   — and the package had nothing for it. A match row is NOT a register row: a
   register row is one record wearing a status, and this is TWO RECORDS AND THE
   GAP BETWEEN THEM. The gap is the only new information on the line, so the gap
   is the only thing that gets size and weight. Both amounts stay at working-face
   body scale, and that is also why the display face is absent from the row:
   `--cx-font-figure` is for DISPLAY figures, and the summary header's two totals
   are the only display figures this shape has.

   NOT A RULED-CELL GRID (lane.md, killed with reasons). Two sides, ONE hairline
   between them, the difference in a third track, and everything else held by
   rhythm and tabular numerals. The seam is the only rule drawn, and it is drawn
   because the two columns are two different books — it is a seam, not a cell
   wall. It takes --cx-line, the panel EDGE, not --cx-line-2, the divider: the
   dividers in this list run horizontally and the seam runs against them, so at
   1px the two must not be the same weight or the seam disappears into the grain.
   The left side's amount sits flush RIGHT against the seam and the right side's
   flush LEFT against it, so the two figures meet across the hairline and the eye
   compares them without travelling. Below 900px the seam rotates to a border-top
   and both amounts fall to the stack's right edge instead.

   SIX STATES on one attribute, `data-state`, so the state is declared once and
   the chip, the difference and the seam all read it — mark plus word in every
   one, never colour alone:
     matched    the two sides agree. CANON TEAL, the reserved verified-truth hue,
                because that is precisely the claim a reconciled row makes. Green
                ok stays for "a process finished", which is a different sentence.
     partial    part of the amount is explained and the rest is not. Amber caution.
     unmatched  the other side was read and had nothing, so the WHOLE amount is
                the gap. The one state that tints the difference figure itself.
     held       a person has looked at it and stopped it. The neutral working hue
                on purpose: a hold is a decision, not a fault, and it must not
                read like the automatic states around it.
     proposed   THE AI-SIGNAL RULE. The machine thinks these two go together. The
                row is tinted --cx-ai, sparkle-marked, and it is a PROPOSAL until
                a person confirms it — so it carries confirm and reject as real
                44px buttons AT EVERY WIDTH, not only under 900. This is the one
                control in the system that moves money between two books; a 28px
                target for it is a defect at any pointer. A machine match that
                applies itself silently is the single thing this shape must never
                do, which is why nothing here changes state without a click.
     absent     a side that cannot be read AT ALL — the accounting package that is
                "available, not connected", the bank export that stops on the
                24th. .cx-absent's dashed grammar at row scale, with the reason
                written out and a word where the number would be. It is NOT
                unmatched: unmatched means the other side was read and was empty;
                absent means the other side was never read. An empty cell says
                neither, and looks like zero.

   ONE-TO-MANY is the same row plus a disclosure — a real button carrying
   aria-expanded and a list that indents into the RIGHT side's column, so each
   tranche's amount lands under the total it is part of and the seam simply
   continues down past them. That alignment is the entire explanation; nothing
   draws a bracket. Part amounts are right-set in a 10ch tabular column so the
   decimals stack, which is the one place this shape asks for a fixed measure.

   Every target clears 44px under 900px. The one motion is an opacity fade on an
   arriving proposal, on --cx-dur, which the reduced-motion query zeroes; the row
   lands opaque and nothing moves. No SMIL. The shape adds no token and no hue. */
.cx-matches { display: grid; }
.cx-match { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 132px;
  column-gap: var(--cx-s4); row-gap: var(--cx-s2); align-items: start;
  padding: var(--cx-s3) var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1);
  border-bottom: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-md); }
.cx-match:last-child { border-bottom: 0; }
/* a side is a ledger line: who says so, how much, and what it sits on underneath */
.cx-match__side { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--cx-s3); align-items: baseline; row-gap: 1px; min-width: 0; }
/* the source WRAPS rather than ellipsing. Which book a figure came from is the
   half of the line a reader cannot reconstruct, so it never gets truncated — the
   same call .cx-clock__hd makes when a two-line label meets a 190px tile. */
.cx-match__src { grid-column: 1; grid-row: 1; font-size: var(--cx-sm); color: var(--cx-ink-3);
  line-height: var(--cx-lh-snug); min-width: 0; }
.cx-match__amt { grid-column: 2; grid-row: 1; text-align: right; font-size: var(--cx-h3);
  font-weight: var(--cx-w-semi); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* a WORD where a number goes — "none", "not read". It drops to metadata ink so it
   cannot be read as a figure, the .cx-tile dd em idiom. One mechanism, both cases. */
.cx-match__amt em { font-style: normal; font-weight: var(--cx-w-medium); color: var(--cx-ink-3); }
.cx-match__what { grid-column: 1 / -1; grid-row: 2; font-size: var(--cx-sm);
  line-height: var(--cx-lh-snug); color: var(--cx-ink-3); }
/* THE SEAM. The row's column gap is s4 and the padding here is s4, so the hairline
   sits centred in the channel between two equal columns rather than hard against
   one of them. The right side mirrors: its amount leads, flush against the seam. */
.cx-match__side--b { border-left: 1px solid var(--cx-line); padding-left: var(--cx-s4);
  grid-template-columns: auto minmax(0, 1fr); }
.cx-match__side--b .cx-match__amt { grid-column: 1; text-align: left; }
.cx-match__side--b .cx-match__src { grid-column: 2; }
/* a side that cannot be read — .cx-absent's dashed grammar at row scale. The dashed
   box IS that row's seam; a hairline beside it would be two rules for one edge. */
.cx-match__side--absent { border: 1px dashed var(--cx-line); border-radius: var(--cx-radius-md);
  padding: var(--cx-s2) var(--cx-s3); }

/* the third track — the gap, and the state under it, both flush to the row's edge */
.cx-match__diff { display: grid; justify-items: end; gap: 5px; text-align: right; }
.cx-match__diff b { font-size: var(--cx-h2); font-weight: var(--cx-w-semi); line-height: 1.15;
  font-variant-numeric: tabular-nums; }
.cx-match__diff small { font-size: var(--cx-sm); color: var(--cx-ink-3); }
/* the state chip. .cx-pill's box written out rather than reused, for the reason the
   clock's chip is: it is driven by the PARENT's data-state (one source of truth, the
   .cx-step idiom) and its calm state is the sunken counter-surface, which the
   semantic pill family has no variant for. */
.cx-match__state { display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); white-space: nowrap;
  background: var(--cx-panel-sunken); color: var(--cx-ink-2); }
.cx-match__state .cx-icon { width: 13px; height: 13px; }

/* ── states — the chip carries mark and word; the figure and the seam reinforce ──
   Written against `[data-state]` on either the row or a summary count, so one
   declaration paints the chip wherever the state is declared. */
:is(.cx-match, .cx-match-count)[data-state="matched"]   .cx-match__state { background: var(--cx-ok-wash); color: var(--cx-ok); }   /* computed agreement is ok, not canon; canon stays for a human's verified stamp (red-team 2026-08-29) */
:is(.cx-match, .cx-match-count)[data-state="partial"]   .cx-match__state { background: var(--cx-warn-wash); color: var(--cx-warn); }
:is(.cx-match, .cx-match-count)[data-state="unmatched"] .cx-match__state { background: var(--cx-error-wash); color: var(--cx-error); }
:is(.cx-match, .cx-match-count)[data-state="held"]      .cx-match__state { background: var(--cx-working-wash); color: var(--cx-ink-2); }
:is(.cx-match, .cx-match-count)[data-state="proposed"]  .cx-match__state { background: var(--cx-panel);
  color: var(--cx-ai-ink); border-color: var(--cx-ai-line); }
:is(.cx-match, .cx-match-count)[data-state="absent"]    .cx-match__state { background: none;
  color: var(--cx-ink-3); border-color: var(--cx-line); padding: 2px 8px; }
/* zero reads quietly; the whole amount reads loudest; a gap that cannot be computed
   is a dash and is not allowed to look like a small number */
.cx-match[data-state="matched"]   .cx-match__diff b { color: var(--cx-ink-3); font-weight: var(--cx-w-medium); }
.cx-match[data-state="unmatched"] .cx-match__diff b { color: var(--cx-error); }
.cx-match[data-state="absent"]    .cx-match__diff b { color: var(--cx-ink-3); font-weight: var(--cx-w-regular); }
.cx-match[data-state="held"]      .cx-match__what em { font-style: normal; color: var(--cx-ink-2); }

/* ── the proposal — tinted, sparkle-marked, and undecided until someone clicks ── */
.cx-match[data-state="proposed"] { background: var(--cx-ai); border-bottom-color: var(--cx-ai-line);
  border-radius: var(--cx-radius-lg); animation: cx-match-in var(--cx-dur) var(--cx-ease) both; }
@keyframes cx-match-in { from { opacity: 0; } to { opacity: 1; } }
.cx-match[data-state="proposed"] .cx-match__side--b { border-left-color: var(--cx-ai-line); }
.cx-match[data-state="proposed"] .cx-match__diff b { color: var(--cx-ai-ink); }
/* THE RECORD IS THE WAY IN. Its parts now live in the peek, so the record needs to open
   one — rendered as a button, or given a tabindex, exactly as .cx-clock--row and
   .cx-geo__zone already are. aria-selected marks the record the peek is currently
   showing, which is the idiom .cx-row already uses for the same relationship. */
button.cx-match, .cx-match[tabindex] { width: 100%; font: inherit; text-align: left;
  cursor: pointer; }
button.cx-match:hover, .cx-match[tabindex]:hover { background: var(--cx-ground); }
.cx-match[aria-selected="true"] { background: var(--cx-ground); }

/* the reason on the left, the two verbs on the right. The reason is the machine's
   own sentence and wears the AI ink, so the tint and the words agree. */
.cx-match__ask { grid-column: 1 / -1; display: flex; align-items: center; gap: var(--cx-s2);
  flex-wrap: wrap; }
.cx-match__ask p { margin: 0 auto 0 0; max-width: 62ch; font-size: var(--cx-sm);
  line-height: var(--cx-lh-snug); color: var(--cx-ai-ink); }
.cx-match__ask .cx-btn { min-height: 44px; }

/* ── one-to-many — WHAT EXPLAINS THE RECORD, and it lives in the peek ─────
   The record itself carries two sides and a delta and stops there. Its constituent
   lines are the 10%: they belong to the detail surface, not to the row, because a
   list that grows under the reader's cursor costs them their place. .cx-match__parts
   is therefore a STANDALONE block now — no grid column, no assumed parent — so a peek
   can render it directly. The chevron toggle that used to sit above it is retired
   with .cx-more; see DISCLOSURE at the foot of this file. */
.cx-match__parts { margin: 0; padding: 0 0 0 var(--cx-s4); list-style: none;
  border-left: 1px solid var(--cx-line); }
.cx-match__part { display: flex; align-items: baseline; gap: var(--cx-s3); padding: 5px 0; }
.cx-match__part-amt { min-width: 10ch; text-align: right; flex: none; font-size: var(--cx-h3);
  font-weight: var(--cx-w-medium); font-variant-numeric: tabular-nums; }
.cx-match__part-what { min-width: 0; font-size: var(--cx-sm); color: var(--cx-ink-3);
  line-height: var(--cx-lh-snug); }

/* ── the summary header — what is reconciled, what is not, and how many ────
   The two money totals are the shape's ONLY display figures: a header total is a
   reading and `--cx-font-figure` is for readings, while a row amount is body text.
   The tiles are .cx-tiles / .cx-tile unchanged and the period selector is .cx-seg
   unchanged — a summary that invents its own tile is a second tile. Counts per
   state reuse the row's chip, so the legend and the rows cannot drift apart. */
.cx-match-sum { display: grid; gap: var(--cx-s3); margin-bottom: var(--cx-s4); }
.cx-match-sum .cx-tiles { margin-bottom: 0; }
.cx-match-sum__hd { display: flex; align-items: center; gap: var(--cx-s3); flex-wrap: wrap; }
.cx-match-sum__hd h4 { margin: 0; font-size: var(--cx-h3); font-weight: var(--cx-w-semi); }
.cx-match-sum__hd .cx-seg { margin-left: auto; }
.cx-match-sum__counts { display: flex; flex-wrap: wrap; gap: var(--cx-s2) var(--cx-s4);
  padding-top: var(--cx-s3); border-top: 1px solid var(--cx-line-2); }
.cx-match-count { display: inline-flex; align-items: baseline; gap: var(--cx-s2); }
/* THE SUMMARY IS COUNTS, NOT STATES (2026-08-29, owner: can this be cleaner still).
   The summary reused .cx-match__state — the ROW's chip — as a count label, so six
   filled pills wrapped onto two lines above a list whose every row already carries
   its own state. A count is not a record's status: it is a fact about how many.
   .cx-queue__counts had already settled this exact shape on the queue — a figure and
   a word, quiet, with the semantic reserved for the ones that need attention — so the
   summary adopts it rather than inventing a third treatment. Mark and word survive;
   only the pill goes, and the eye now lands on the three unmatched instead of on six
   chips of equal weight. */
/* the [data-state] in the selector is load-bearing: the six state rules above are
   :is(.cx-match, .cx-match-count)[data-state="…"] .cx-match__state, which outranks a
   plain descendant pair, so a de-pill written without it silently loses. */
.cx-match-sum__counts .cx-match-count[data-state] .cx-match__state {
  background: none; border-color: transparent; padding-left: 0; padding-right: 0;
  color: var(--cx-ink-3); font-weight: var(--cx-w-regular); }
/* the two that mean someone has to do something keep their ink, on the FIGURE too —
   the same place .cx-queue__count puts it */
.cx-match-sum__counts .cx-match-count[data-state="unmatched"] :is(b, .cx-match__state) { color: var(--cx-error); }
.cx-match-sum__counts .cx-match-count[data-state="absent"]    :is(b, .cx-match__state) { color: var(--cx-ink-3); }
.cx-match-sum__counts .cx-match-count[data-state="partial"]   .cx-match__state { color: var(--cx-warn); }
.cx-match-count b { font-family: var(--cx-font-figure); font-size: 20px; font-weight: 400;
  line-height: 1; letter-spacing: 0; font-variant-numeric: tabular-nums; }

@media (max-width: 900px) {
  /* the sides stack, left over right, and the seam rotates with them; the
     difference keeps the row's edge because at 390px it is the one thing that
     has to be findable without reading */
  .cx-match { grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--cx-s3);
    min-height: 44px; }
  .cx-match__side, .cx-match__side--b { grid-column: 1; }
  .cx-match__side--b { grid-template-columns: minmax(0, 1fr) auto;
    border-left: 0; border-top: 1px solid var(--cx-line-2);
    padding-left: 0; padding-top: var(--cx-s2); }
  .cx-match__side--b .cx-match__amt { grid-column: 2; text-align: right; }
  .cx-match__side--b .cx-match__src { grid-column: 1; }
  .cx-match__side--absent, .cx-match__side--b.cx-match__side--absent {
    border: 1px dashed var(--cx-line); padding: var(--cx-s2) var(--cx-s3); }
  .cx-match__diff { grid-column: 2; grid-row: 1 / span 2; align-content: start; }
  .cx-match__diff b { font-size: var(--cx-h3); }
  .cx-match__parts { grid-column: 1 / -1; padding-left: var(--cx-s3); }
  .cx-match__part { min-height: 44px; flex-wrap: wrap; row-gap: 0; }
  .cx-match__part-amt { min-width: 0; text-align: left; }
  /* two verbs on one line at 390px is two 150px targets; they take the width instead */
  .cx-match__ask .cx-btn { flex: 1 1 40%; }
}
/* ── document / record card — a document as a record, not an attachment ────
   COVERAGE.md shape 10, built 2026-08-28. Only one pipeline client needs it, and it is
   that client's whole product: the procedure canon — which version is current, who owns
   it, when it was last verified — plus the scanned packets the registers hang off. So a
   document here is a RECORD with a version, an owner role and a stamp, never a file chip
   at the foot of a form.

   NO THUMBNAIL, EVER. The package ships no renderer and fetches nothing, so a page
   preview is a promise the DS cannot keep inside a consumer. The type is a sprite glyph
   — the sprite has two document-shaped glyphs and this shape adds none: `i-document` for
   something written and `i-capture` for something scanned — and the SOURCE line carries
   the rest of the distinction. Source is the .cx-conn grammar written as a LINE rather
   than a tile, because where a document came from is one fact about the record, not a
   surface of its own.

   THE RESERVED TEAL EARNS ITS KEEP HERE. --cx-canon has meant "verified truth" since the
   token file was written and had almost no consumer. The verified stamp is what it was
   reserved FOR: a date, a ROLE — never a person, because the person leaves and the role
   still owns the document — and the word "verified" beside the canon mark. In this whole
   block the teal appears in exactly two rules, and both of them are that stamp.

   SIX states on one attribute: `data-state`, carried by .cx-doc in the card and the
   detail and by .cx-row--doc in the register, so the card, the row, the chip and the
   stamp all read one declaration. Mark AND word in every one, never colour alone.
     verified    the current, signed-off version — canon
     draft       written, not yet verified — working
     superseded  struck and KEPT, with the current version linked; a replaced SOP is how
                 you read what the procedure said last quarter, so it is never deleted
     stale       past its review date — warn, and the stamp itself goes warn and says so
     read        fields the system pulled off a scan — the AI-signal rule, so they are
                 tinted, sparkle-marked and PROPOSED until a person confirms them
     absent      the document that SHOULD exist and does not: a packet the index lists
                 and no one ever scanned, a certificate the chain requires and no branch
                 filed. .cx-absent's dashed grammar with the reason written out, because
                 an absence a person can act on beats a row that is merely missing.

   THREE forms, one markup: the card in a board, the row in a register (the .cx-row
   template with its own track widths — the .cx-row--status precedent), and the detail in
   the 372px contextual panel (--detail drops the box; the panel is the box). A CANON
   grouping is the board holding only current verified versions with the superseded ones
   folded behind a summary that counts them.

   A verification cycle that bites is a CLOCK and the package already ships one, so the
   foot COMPOSES .cx-clock--inline rather than growing a second date-that-bites. Nothing
   in this block animates but the fold's chevron, on --cx-dur, which the reduced-motion
   query zeroes. No SMIL. */
.cx-doc { display: grid; gap: var(--cx-s2); min-width: 0; text-align: left;
  border: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-lg); padding: 14px 16px; }
.cx-docs { display: grid; grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); gap: var(--cx-s3); }
/* a card with the record behind it becomes a button; one without stays visibly static,
   because a false affordance is worse than none. An element on a panel hovers to the ground. */
button.cx-doc { width: 100%; font: inherit; color: var(--cx-ink); background: none; cursor: pointer; }
button.cx-doc:hover { background: var(--cx-ground); }

/* the header — the type, what the document is, the record it belongs to, and the state.
   The chip wraps under the title rather than crushing it: a 268px card with a two-line
   procedure name is not a defect. What WAS a defect is where it landed when it wrapped.
   Under flex + `margin-left: auto` it flew to the card's right edge and sat on a line of
   its own, aligned to nothing, in every card at 268px — the state reading as an orphan
   rather than as part of the record it describes. Owner, 2026-08-29: "why do the chips
   have their own lines?"

   A two-column grid instead, with the state placed in the title's own column. The block
   now reads title, then the record it belongs to, then its state — three lines, one left
   edge, no ragged right. The row gap is 3px, not --cx-s3: the state belongs to the title
   block, so it is set close to it, and the 12px column gap still holds the glyph off. */
/* ONE LEFT EDGE. The header used to open with a 32px glyph column, so the title started
   44px in while every block below it — facts, source, stamp — started at the card's own
   padding. Two left edges inside a 224px card, and the 44px under the glyph was empty for
   the whole height of the card. Owner, 2026-08-29: "the space still isn't being used
   efficiently; something is a bit off."

   The glyph was not the problem, its COLUMN was. It moved into the record line (the
   title's <small>), where it names the kind exactly as before, costs no vertical space and
   no left edge, and sits beside the words that say the same thing — "a scan", "a
   procedure". The header is one column now and the title measures 224px instead of 180,
   which is the difference between "Fire safety inspection certificate" wrapping and not. */
.cx-doc__hd { display: grid; grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "title" "state"; gap: 3px 0; align-items: start; }
.cx-doc__hd > .cx-doc__title { grid-area: title; }
.cx-doc__hd > .cx-doc__state { grid-area: state; justify-self: start; }
/* a fact, per the container discipline at .cx-queue__chan: the glyph names the kind.
   The absent state keeps its dashed box, which is the only paint left in this slot. */
/* an inline mark on the record line, sized to that line's text rather than to a box */
.cx-doc__type { display: inline-flex; align-items: center; flex: none;
  margin-right: 5px; vertical-align: -2px; color: var(--cx-ink-3); }
.cx-doc__type .cx-icon { width: 13px; height: 13px; }
.cx-doc__title { margin: 0; min-width: 0; flex: 1 1 120px; font-size: var(--cx-body);
  font-weight: var(--cx-w-semi); line-height: var(--cx-lh-snug); }
/* The name is wrapped in its own <b>, which looks redundant against a semibold heading and is
   not: a text-decoration propagates into in-flow descendants and CANNOT be switched off by them,
   so a superseded record's strike would otherwise run through the record it belongs to and through
   the link to its current version — the two lines you are there to read. The strike needs an
   element of its own, so the name gets one. */
.cx-doc__title > b { font-weight: inherit; }
/* the record it belongs to. A document that belongs to nothing is a file, not a record. */
.cx-doc__title small { display: block; margin-top: 1px; font-size: var(--cx-sm);
  font-weight: var(--cx-w-regular); color: var(--cx-ink-3); }
/* the state chip. .cx-clock__state's box written out again, and for the same reason: it
   is driven by the PARENT's data-state — one source of truth, the .cx-step idiom — and
   its calm state is the sunken counter-surface, which the .cx-pill family has no variant
   for. A .cx-pill--canon would also have to be chosen per call site, which is exactly the
   thing that lets a row and its card disagree about a document. */
/* A CONTAINER IS A CLAIM (owner ruling, 2026-08-29), and this chip was claiming on every
   state including the four that are plain facts about the record. Default is now mark and
   word: no fill, no visible edge, geometry held by a transparent border so a state that
   DOES keep its box paints in place without moving the card. The .cx-queue__chan precedent
   at :1721 is the same edit for the same reason.

   Two states keep their container, which is the ruling's own carve-out — absence, and the
   machine's guess. They stay legible precisely because they became rare. */
.cx-doc__state { flex: none; display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 0; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); white-space: nowrap;
  background: none; color: var(--cx-ink-2); }
.cx-doc__state .cx-icon { width: 13px; height: 13px; }

/* the facts line — version, what it supersedes, and the owner ROLE */
.cx-doc__facts { margin: 0; display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-doc__v { font-weight: var(--cx-w-semi); color: var(--cx-ink-2);
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.cx-doc__sup { color: var(--cx-ink-3); }
/* the current version is a LINK out of a superseded record, never a dead end */
.cx-doc__sup :is(a, button) { font: inherit; color: var(--cx-accent); background: none;
  border: 0; padding: 0; cursor: pointer; text-decoration: none; }
.cx-doc__sup :is(a, button):hover { text-decoration: underline; }
.cx-doc__role { font-size: var(--cx-sm); color: var(--cx-ink-2); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* the source — the .cx-conn grammar as a line: a glyph, the connection, what it arrived as */
.cx-doc__src { margin: 0; display: flex; align-items: center; gap: 6px; min-width: 0;
  font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-doc__src .cx-icon { width: 14px; height: 14px; flex: none; }
.cx-doc__src span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* THE VERIFIED STAMP — when, by which role, and the word. Not a pill: a stamp is a line
   of record, so it takes the mark and the ink and no fill. It is the one place in this
   block that paints --cx-canon, and it paints nothing at all until a state says so. */
.cx-doc__stamp { display: inline-flex; align-items: center; gap: 6px; min-width: 0;
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium); color: var(--cx-ink-3); }
.cx-doc__stamp .cx-icon { width: 14px; height: 14px; flex: none; }
/* THE CARD SAID ITS STATE TWICE (owner, 2026-08-29: "fix"). The header chip is
   `mark + Verified` and the stamp three lines below is `mark + Verified + date + role` —
   the same glyph and the same word inside one 268px card.

   The WORD is not actually redundant: "Verified 14 Jul" reads as WHEN it was verified,
   which is the stamp's whole job, and on a stale record the two words differ anyway
   ("Verification lapsed" against "Lapsed 30 Jun"). Dropping it would cost real meaning and
   would read as a bare date. The GLYPH is the pure duplicate, so that is what goes.

   Scoped to the CARD only, and only when a chip is actually present to carry the mark. The
   register row keeps both, because there the chip and the stamp sit in different columns
   and neither is within a glance of the other. The stamp keeps its word, so nothing here
   becomes colour alone. */
.cx-doc:has(> .cx-doc__hd > .cx-doc__state) .cx-doc__stamp > .cx-icon { display: none; }
.cx-doc__stamp b { font-weight: var(--cx-w-semi); }
.cx-doc__stamp time { font-variant-numeric: tabular-nums; }
/* the caption carries the word the mark cannot — the same grammar as a chart caption */
.cx-doc__cap { margin: 0; font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-3); }
.cx-doc__cap em { font-style: normal; color: var(--cx-ink-2); }
/* the foot — the stamp, and the review clock when verification expires on a date */
.cx-doc__foot { display: flex; align-items: center; gap: var(--cx-s3); flex-wrap: wrap;
  padding-top: var(--cx-s2); border-top: 1px solid var(--cx-line-2); }
.cx-doc__foot > .cx-clock--inline { margin-left: auto; }

/* the machine-extracted fields. The WRAPPER is .cx-reading — that component already is
   "the machine said this", tinted and sparkle-marked, and a second AI surface would be a
   second AI voice. What this shape adds is that every value carries its own word: a field
   read off a scan can never be mistaken for a field a person typed, and it stays a
   PROPOSAL until someone confirms it against the packet. */
.cx-doc__fields { margin: var(--cx-s2) 0 0; display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr); gap: 5px var(--cx-s3); font-size: var(--cx-sm); }
.cx-doc__fields dt { color: var(--cx-ai-ink); }
.cx-doc__fields dd { margin: 0; min-width: 0; display: flex; align-items: baseline; gap: 6px;
  flex-wrap: wrap; font-weight: var(--cx-w-medium); font-variant-numeric: tabular-nums; }
.cx-doc__prop { display: inline-flex; align-items: center; gap: 4px; font-size: var(--cx-xs);
  font-weight: var(--cx-w-semi); letter-spacing: var(--cx-tr-wide); text-transform: uppercase;
  color: var(--cx-ai-ink); white-space: nowrap; }
.cx-doc__prop .cx-icon { width: 11px; height: 11px; }

/* version history — newest first, each entry its version, its date and the role that
   signed it off. The current one is aria-current; the rest are dimmed, never struck.
   Striking belongs to the superseded RECORD, where it means "do not work from this";
   inside a history list every line is superseded by definition and the strike would be
   noise on every row. */
.cx-doc__hist { list-style: none; margin: 0; padding: 0; display: grid; }
.cx-doc__hist li { display: grid; grid-template-columns: 44px minmax(0, 1fr) auto;
  gap: var(--cx-s2); align-items: baseline; padding: 6px 0;
  border-bottom: 1px solid var(--cx-line-2); font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-doc__hist li:last-child { border-bottom: 0; }
.cx-doc__hist li[aria-current="true"] { color: var(--cx-ink-2); }
.cx-doc__hist b { font-weight: var(--cx-w-semi); color: var(--cx-ink-2);
  font-variant-numeric: tabular-nums; }
.cx-doc__hist span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-doc__hist time { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ── states — one attribute, read by the card, the detail and the register row alike ──
   Mark and word live in the chip or in the stamp; the border and the ink only reinforce. */
:is(.cx-doc, .cx-row--doc)[data-state="verified"] .cx-doc__state { color: var(--cx-canon); }
:is(.cx-doc, .cx-row--doc)[data-state="verified"] .cx-doc__stamp { color: var(--cx-canon); }
:is(.cx-doc, .cx-row--doc)[data-state="draft"] .cx-doc__state { color: var(--cx-ink-2); }
:is(.cx-doc, .cx-row--doc)[data-state="stale"] .cx-doc__state { color: var(--cx-warn); }
:is(.cx-doc, .cx-row--doc)[data-state="stale"] .cx-doc__stamp { color: var(--cx-warn); }
:is(.cx-doc, .cx-row--doc)[data-state="read"] .cx-doc__state { background: var(--cx-ai);
  border-color: var(--cx-ai-line); color: var(--cx-ai-ink); padding: 3px 9px; }
/* SUPERSEDED — struck and kept. The strike falls on the title and the version, the two
   things you could act on by mistake; the record it belongs to and the link to the
   current version stay plain, because those are what you are here to read. */
:is(.cx-doc, .cx-row--doc)[data-state="superseded"] .cx-doc__state { color: var(--cx-ink-3); }
:is(.cx-doc, .cx-row--doc)[data-state="superseded"] .cx-doc__title > b,
:is(.cx-doc, .cx-row--doc)[data-state="superseded"] .cx-doc__v { text-decoration: line-through;
  text-decoration-thickness: 1px; color: var(--cx-ink-3); }
/* NOT ON FILE — the document that should exist and does not */
:is(.cx-doc, .cx-row--doc)[data-state="absent"] .cx-doc__state { background: none;
  border-color: var(--cx-line); color: var(--cx-ink-3); padding: 2px 8px; }
:is(.cx-doc, .cx-row--doc)[data-state="absent"] .cx-doc__title { color: var(--cx-ink-2); }
.cx-doc[data-state="absent"] { border-style: dashed; border-color: var(--cx-line); }
/* the card's own dashed border and the "Not on file" chip carry the absence; a dashed box
   around a 13px inline glyph would be a third telling of it */

/* ── the detail form — the 372px contextual panel ─────────────────────────
   The card with its box removed, because the panel IS the box. Everything the card
   compresses is written out here — the metadata list, the version history, the extracted
   fields with their marks, the source, the stamp — since this is the surface where a
   person decides whether to trust the document, and that decision needs the whole record. */
.cx-doc--detail { border: 0; border-radius: 0; padding: 0; gap: var(--cx-s4); }
.cx-doc--detail .cx-doc__hd { padding-bottom: var(--cx-s3); border-bottom: 1px solid var(--cx-line-2); }
.cx-doc--detail .cx-doc__title { font-size: var(--cx-h3); }
/* a section label inside the detail. .cx-eyebrow owns Home's 46px rhythm and would tear
   a 372px panel apart, so the label is stated here at panel scale. */
.cx-quiet { color: var(--cx-ink-3); font-size: var(--cx-sm); }   /* a quiet line of prose; the gallery used it unstyled */
.cx-lab, .cx-doc__lab { margin: 0 0 6px; font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; color: var(--cx-ink-3); }
.cx-doc__meta { margin: 0; display: grid; grid-template-columns: minmax(0, 104px) minmax(0, 1fr);
  gap: 7px var(--cx-s3); font-size: var(--cx-sm);
  /* the measure is capped (v1.3.4): the list was built for a 372px panel, and on a
     full-width page (recipes/cockpit.html, the facts) a dd ran the whole stage. 680px
     is the cap .cx-votes__cast already gives its input; the panel variant is narrower
     than the cap and is untouched by it. */
  max-width: 680px; }
.cx-doc__meta dt { color: var(--cx-ink-3); }
.cx-doc__meta dd { margin: 0; min-width: 0; color: var(--cx-ink-2); }

/* ── the canon grouping — the current verified versions, superseded folded ───
   "What is the procedure right now" is a different question from "what has the procedure
   been", and the second must never be able to answer the first. The board holds only
   current versions; every superseded one sits behind a summary that COUNTS them, so the
   history is one click away and never in the way. The fold is the component; which
   versions are current is the consumer's query. */
.cx-docs--canon { gap: var(--cx-s2); }
/* the box is .cx-fold--boxed and the summary is .cx-fold's; only the grid placement is
   local. This construction was one of just two in the package that already said how much
   it hid — that count is now required of every fold rather than left to the author. */
.cx-docs__folded { grid-column: 1 / -1; }
.cx-docs__folded .cx-doc__hist { padding-bottom: var(--cx-s3); }

/* ── the row in a register — the .cx-row template, its own track widths ────
   Five tracks: the reading dot, the title over the record it belongs to, the version, the
   owner role, and THE EDGE. The edge carries the STAMP when the document is verified or
   stale, the state CHIP when it is neither, and a .cx-clock--inline when what bites is the
   review date — one track, three occupants, because "what do I need to know about this
   document from across the room" has exactly one answer per row. Every one of the three
   is a mark and a word.
   The widths are the CONSUMER'S to tune, the same rule .cx-row--status states; what the
   package owns is the shape. The row reuses .cx-doc__title rather than .cx-row__name so
   that one strike rule serves the card, the detail and the register. */
.cx-row--doc { grid-template-columns: 14px minmax(0, 1fr) 52px 132px 128px; align-items: center; }
.cx-row--doc .cx-doc__title { flex: none; }
.cx-row--doc .cx-doc__title > :is(b, small) { display: block; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.cx-row--doc :is(.cx-doc__stamp, .cx-doc__state, .cx-clock--inline) { justify-self: end; }
.cx-row--doc .cx-doc__v { font-size: var(--cx-sm); }

@media (max-width: 900px) {
  .cx-docs { grid-template-columns: 1fr; }
  /* the register keeps the name and the edge; the version and the owner role fold. A 44px
     row has room for one thing at its edge, and whether the document can be trusted is
     that thing. Stated here rather than in the shell's narrow block so the shape stays one
     cut-out region of this file — and this rule is the one that lands, because the base
     .cx-row narrow template has equal specificity and comes earlier. */
  .cx-row--doc { grid-template-columns: 14px minmax(0, 1fr) 128px; }
  .cx-row--doc :is(.cx-doc__v, .cx-doc__role) { display: none; }
}
/* ── map / geo frame — the DS's half of a map ─────────────────────────────
   COVERAGE.md shape 9, built 2026-08-28. Three of eight pipeline clients put
   assets on a map and the package had none. Like the chart, this is a shape the
   package cannot carry whole: a map needs a renderer, a projection and tiles,
   and this layer imports nothing and fetches nothing. So the DS ships the
   FRAME — the card, the stage, the legend, the mark specs, every state and a
   reference SVG (templates/geo-frame-640.html) — and the ENGINE STAYS IN THE
   CONSUMING APP. The app emits classed shapes into the stage and paints nothing.

   NOT A CHART, SO NOT SHADOWED. The chart card's half-pixel shadow is BEND 4,
   and bend 4 states its own limit: charts are the one shadowed thing, and the
   moment a second component takes the shadow it stops meaning anything. A plot
   floats because it is a picture of something that is not in the room. A map is
   a picture of the operator's own estate — the same zones, mains and branches
   the list underneath it names one by one — so it stays IN the plane: panel
   fill, hairline, radius-lg, no shadow. What the geo frame does take from the
   chart is the header half of the since-marker (`.cx-chart__last`, reused, not
   re-authored, so the stamp sits in the same place on every card that answers
   "what changed since I last looked") and the twin's discipline. Not its lift.

   NO DISPLAY FIGURE. The chart register puts the answer above the plot as one
   Departure Mono figure. This frame's answer is a NAME — which zone, which
   branch — and `--cx-font-figure` is display figures only. The count lives in
   the caption; each asset's own figure lives in its row, in tabular numerals on
   the working face, the way every row in this system carries a number.

   ONE ENCODING, AND IT IS SUBTRACTION. `--cx-series-1` marks the ONE thing being
   asked about — the zone over its variance target, the branch missing a rail —
   and everything else steps through the neutral ramp. No hue is added, and no
   semantic hue is spent on a map: warn and error keep meaning caution and
   failure, and "this is the one you asked about" is a SUBJECT, not a status.
   Same instinct as `.cx-bars[data-subject]`: the others are already quiet.

   STATE ON ONE ATTRIBUTE — `data-state`, declared once per object and read by
   every part below it, the `.cx-step` / `.cx-clock` idiom, at three levels:
     .cx-geo         ready · empty · loading · absent · error   (the frame)
     .cx-geo__mark   normal · attention · selected · absent     (a shape)
     .cx-geo__zone   normal · attention · absent                (a row)
   Selection is `aria-selected` on the row AND `data-state="selected"` on the
   mark, because it is one act with two expressions and the app sets both from
   one selection. The row keeps its attention state underneath it, so selecting
   the zone you were asked about never costs you the word for why it was marked.

   THE ZONE LIST IS THE TWIN, AND IT IS NOT HIDDEN. The chart's twin is a
   `<details>` because a plot's figures are otherwise unreachable — a disclosure
   for the reader the picture failed. Here the twin IS the surface: below 900px
   the stage becomes a strip and the list carries the answer alone, and the list
   is what still works when the engine does not. A disclosure would hide the one
   part that survives the error state. It is rows rather than a `<table>` for the
   same reason the rest of the product is rows; the chart's twin is the carve-out,
   not the pattern. It is a real listbox: selecting a row IS selecting a marker.

   THE PLACEHOLDER IS A SCHEMATIC, NEVER A MAP. The reference SVG draws abstract
   closed shapes in the neutral ramp. No coastline, no tiles, no imagery, no
   external request, no SMIL. A placeholder that looked like a real place would
   be a claim about geography this package has no way to keep.

   The only motion is an opacity fade on the stage group over `--cx-dur`, which
   the reduced-motion query zeroes. Zero new tokens: the stage ratios and the
   mark radii are geometry literals, the same way the chart's 196px plot is. */
.cx-geo { margin: 0; display: grid; gap: var(--cx-s3); min-width: 0;
  background: var(--cx-panel); border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-lg); padding: var(--cx-s4); }
.cx-geo__hd { display: flex; align-items: baseline; gap: var(--cx-s3); flex-wrap: wrap; }
.cx-geo__title { margin: 0; font-size: var(--cx-body); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-heading); }
.cx-geo__title small { display: block; margin-top: 1px; font-size: var(--cx-sm);
  font-weight: var(--cx-w-regular); color: var(--cx-ink-3); }
/* the last-look stamp is .cx-chart__last — the same class, on purpose */

/* ── the stage — the region the app's engine fills ───────────────────────
   A sunken well on a panel, per the settled surface mapping. The ratio is what
   makes the card survive a 372px contextual panel with no modifier: the stage
   shrinks with its container instead of being told to at a breakpoint. */
.cx-geo__stage { position: relative; aspect-ratio: 16 / 10; overflow: hidden; min-width: 0;
  background: var(--cx-panel-sunken); border: 1px solid var(--cx-line-2);
  border-radius: var(--cx-radius-md); }
.cx-geo__svg { display: block; width: 100%; height: 100%; }
.cx-geo__g { animation: cx-geo-in var(--cx-dur) var(--cx-ease) both; }
@keyframes cx-geo-in { from { opacity: 0; } to { opacity: 1; } }

/* ── mark specs — the whole app-side contract in three shapes ─────────────
   The engine emits these and nothing else. Every one paints from a CLASS: no
   fill or stroke attribute appears on any mark, so a theme flip and a state
   change are both CSS and the renderer never learns a colour. */
.cx-geo__mark { vector-effect: non-scaling-stroke; }
/* an AREA — a zone, a city, a service district */
.cx-geo__mark--zone { fill: var(--cx-line-2); stroke: var(--cx-line-strong);
  stroke-width: var(--cx-chart-hair); stroke-linejoin: round; }
/* a LINE — a main, a route, a delivery corridor */
.cx-geo__mark--line { fill: none; stroke: var(--cx-ink-3);
  stroke-width: var(--cx-chart-stroke); stroke-linecap: round; stroke-linejoin: round; }
/* a POINT — a meter, a sensor, a branch. HOLLOW at rest, so that a filled point
   is a shape difference and not only a hue difference and the state survives
   greyscale. `r` is set from CSS, the same way .cx-chart__flag sets its own. */
.cx-geo__mark--pin { r: 5; fill: var(--cx-panel); stroke: var(--cx-line-strong);
  stroke-width: var(--cx-chart-ring); }
/* ATTENTION — the one thing being asked about. Series ink AND a weight or fill
   change together; the WORD for it is in the legend and again in the asset's row. */
.cx-geo__mark--zone[data-state="attention"] { stroke: var(--cx-series-1); stroke-width: 2px;
  fill: color-mix(in oklab, var(--cx-series-1) 14%, transparent); }
.cx-geo__mark--line[data-state="attention"] { stroke: var(--cx-series-1); stroke-width: 3px; }
.cx-geo__mark--pin[data-state="attention"]  { r: 6; fill: var(--cx-series-1); stroke: var(--cx-panel); }
/* SELECTED — the accent, because selection is chrome and not data. It paints
   OVER attention rather than replacing it; the row keeps the attention word. */
.cx-geo__mark--zone[data-state="selected"] { fill: var(--cx-accent-wash);
  stroke: var(--cx-accent); stroke-width: 2px; }
.cx-geo__mark--line[data-state="selected"] { stroke: var(--cx-accent); stroke-width: 3px; }
.cx-geo__mark--pin[data-state="selected"]  { r: 7; fill: var(--cx-accent); stroke: var(--cx-panel); }
/* ABSENT — a dashed outline where a point would be. Dashing means NOT MEASURED
   everywhere else in this system and that is exactly what it means here: there
   is no coordinate. An absent mark never appears on the stage, because there is
   nowhere to put it; it appears in its row, which is the point of the twin. */
.cx-geo__mark[data-state="absent"] { fill: none; stroke: var(--cx-line-strong);
  stroke-width: var(--cx-chart-hair); stroke-dasharray: 2 3; }
/* the one text the engine may place on the stage: a name. No value ever rides
   the stage — the row carries the figure, and a figure on a map is unreadable
   at half the sizes this card is rendered at. */
.cx-geo__name { fill: var(--cx-ink-3); font-size: 11px; font-weight: var(--cx-w-medium);
  letter-spacing: 0; text-transform: none; }
.cx-geo__name--subject { fill: var(--cx-ink); font-weight: var(--cx-w-semi); }
/* the reader aims at a region, never at a 2px line */
/* THE HIT TARGET BELONGS TO THE THING THE LIST CAN SELECT. Until 2026-08-29 these were
   the six ZONE polygons — cities — while the twin list carried BRANCH rows, so every
   pointer target on the map corresponded to no row, and on the restaurant page "Makati"
   named a city polygon AND a branch row on the same screen. The pins, which ARE the
   branches, carried no target at all. Now the hits are circles on the pin coordinates and
   the zones are pure context, which also disambiguates the repeated name: the Makati pin
   is the Makati branch, and the Makati label is the city it stands in.

   r=30 in a 640-unit viewBox, which is an invisible target around a pin that paints at
   r=5 (about 7px rendered — far too small to touch, which is why the target is separate
   from the dot). The nearest pair of pins is 159 units apart, so no two targets overlap at
   any width; measured separation is 86px where the target itself measures 44px.

   The target does NOT clear 44px at every width, and that is deliberate rather than
   overlooked: the map scales with its container, so no fixed radius can. The package's own
   position, stated in the responsive block below, is that the map locates and THE ROWS
   answer — and .cx-geo__zone rows carry the 44px floor at every width. The map is a
   secondary affordance on a compliant control, not the control. */
.cx-geo__hit { fill: transparent; }
.cx-geo__hit:focus-visible { outline: none; fill: color-mix(in oklab, var(--cx-accent) 12%, transparent); }

/* ── the legend — mark and word for every category and every state ────────
   `.cx-chart__legend` did not fit: its key is a 12x2 line swatch keyed by series
   number, and a map has to separate three mark GEOMETRIES from each other before
   it separates any state. `.cx-legend` did not fit either: its key is a forced
   circle carrying a value, which is an area chip with a figure attached. So the
   key here is THE MARK ITSELF at 16px, drawn from the same classes the stage
   draws — a legend that cannot drift from what is on the map. */
.cx-geo__legend { display: flex; flex-wrap: wrap; gap: var(--cx-s2) var(--cx-s4);
  margin: 0; padding: 0; list-style: none; font-size: var(--cx-sm); color: var(--cx-ink-2); }
.cx-geo__key { display: inline-flex; align-items: center; gap: 7px; }
.cx-geo__keymark { width: 16px; height: 16px; flex: none; display: block; overflow: visible; }

/* the caption carries what the marks cannot — the unit, the question, the count,
   the illustrative marker. `.cx-chart__cap`'s -6px top margin is tuned to the
   register's 24px gap and would crush this card's 12px one, so it is authored. */
.cx-geo__cap { margin: 0; font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-3); }
.cx-geo__cap em { font-style: normal; color: var(--cx-ink-2); }

/* ── the zone list — the map's twin, and the surface below 900px ──────────
   Two columns, two rows, the `.cx-clock--row` shape: name over its sub-line on
   the left, figure over state on the right, the asset's own mark leading and
   spanning both. ONE FORM AT EVERY WIDTH, so a 372px contextual panel and a full
   panel run the same three lines. Every row clears 44px everywhere, not only
   under 900px — a map marker is a touch target on a desktop too. */
.cx-geo__zones { display: grid; margin: 0; padding: 0; list-style: none; }
.cx-geo__zone { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto;
  column-gap: var(--cx-s3); row-gap: 2px; align-items: center; min-height: 44px;
  padding: var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1);
  border-width: 0 0 1px; border-style: solid; border-color: var(--cx-line-2);
  border-radius: var(--cx-radius-md); font: inherit; text-align: left;
  color: var(--cx-ink); background: none; }
.cx-geo__zone:last-child { border-bottom-width: 0; }
button.cx-geo__zone, .cx-geo__zone[tabindex] { cursor: pointer; }
button.cx-geo__zone:hover, .cx-geo__zone[tabindex]:hover { background: var(--cx-ground); }
/* selecting a row is selecting a marker — the same paint .cx-row already uses,
   because the reader has met this exact affordance in every register */
.cx-geo__zone[aria-selected="true"] { background: var(--cx-accent-wash);
  box-shadow: inset 2px 0 0 var(--cx-accent); }
.cx-geo__zone-mark { grid-column: 1; grid-row: 1 / 3; width: 16px; height: 16px;
  display: block; overflow: visible; align-self: center; }
.cx-geo__zone-name { grid-column: 2; grid-row: 1; min-width: 0; font-size: var(--cx-h3);
  font-weight: var(--cx-w-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cx-geo__zone-sub { grid-column: 2; grid-row: 2; min-width: 0; font-size: var(--cx-sm);
  color: var(--cx-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cx-geo__zone-figure { grid-column: 3; grid-row: 1; justify-self: end; white-space: nowrap;
  font-size: var(--cx-body); color: var(--cx-ink-2); font-variant-numeric: tabular-nums; }
/* the state chip. Written out rather than borrowed from .cx-pill for the reason
   the clock's chip was: it is driven by the ROW's data-state (one source of
   truth), its calm state is the sunken counter-surface, which the semantic pill
   family has no variant for — and its attention state is a SUBJECT, so it must
   not wear warn. Mark and word, always, in every one of the three. */
.cx-geo__zone-state { grid-column: 3; grid-row: 2; justify-self: end; flex: none;
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  padding: 3px 9px; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium);
  background: var(--cx-panel-sunken); color: var(--cx-ink-2); }
.cx-geo__zone-state .cx-icon { width: 13px; height: 13px; }
/* attention — the subject rises and nothing else is brightened */
.cx-geo__zone[data-state="attention"] .cx-geo__zone-state {
  background: color-mix(in oklab, var(--cx-series-1) 12%, transparent);
  color: var(--cx-ink); font-weight: var(--cx-w-semi); }
.cx-geo__zone[data-state="attention"] .cx-geo__zone-name { font-weight: var(--cx-w-semi); }
.cx-geo__zone[data-state="attention"] .cx-geo__zone-figure { color: var(--cx-ink);
  font-weight: var(--cx-w-semi); }
/* ABSENCE, first class, in .cx-absent's dashed grammar. An asset the map cannot
   draw is not dropped from the list and never drawn at a guessed position: it is
   a row with a dashed mark, a dash for a figure and the reason in words. A
   silently missing marker is a lie about the estate. */
.cx-geo__zone[data-state="absent"] .cx-geo__zone-figure { color: var(--cx-ink-3); }
.cx-geo__zone[data-state="absent"] .cx-geo__zone-state { background: none;
  border-color: var(--cx-line); border-style: dashed; color: var(--cx-ink-3); padding: 2px 8px; }
/* the list's own foot — the count and, past a listable number, the affordance.
   `.cx-rows__legend` is exactly this line and is reused rather than re-authored. */

/* ── frame states — the card holds the stage's box so nothing jumps ───────
   `.cx-geo__state` replaces the stage and holds its box, so a card that lands
   empty and then fills does not move the list under the reader's finger. Loading
   reuses `.cx-skeleton > i`, which carries its own reduced-motion guard, rather
   than earning a second keyframe that would eventually forget to.
   IN ERROR THE LIST STILL RENDERS. That is the entire point of the twin, and it
   is why the advisory sits inside the stage's box and never over the card.
   The state box holds a FLOOR — `min-height`, the chart's grammar — and not the stage's
   ratio. A ratio-locked box whose height comes from prose derives its own minimum WIDTH from
   that height, and at 390px that is a horizontal scroll: the four state cards measured 480px
   wide inside a 288px column before this was changed (390 harness, 2026-08-28). The stage
   itself keeps its ratio because an SVG has no intrinsic prose to push against. */
.cx-geo__state { min-height: 196px; display: grid; align-content: center; min-width: 0;
  padding: var(--cx-s3); border-radius: var(--cx-radius-md);
  background: var(--cx-panel-sunken); border: 1px solid var(--cx-line-2); }
.cx-geo__state .cx-empty { padding: var(--cx-s4) var(--cx-s3); }
/* Narrowed 2026-08-29. This was `.cx-geo__state .cx-absent, .cx-geo__state .cx-advisory`,
   (0,2,0), which beat .cx-advisory--error at (0,1,0) and silently stripped --cx-error-wash —
   so the SAME failure painted two ways on one page: on the wash inside a chart state well,
   on --cx-panel inside a geo one. The rule was written for .cx-absent, which declares no
   background of its own and for which it is purely additive; it caught the advisory in
   passing. A washed advisory already reads against --cx-panel-sunken and needs no lifting. */
.cx-geo__state .cx-absent { background: var(--cx-panel); }
/* named as a child of .cx-skeleton so it clears `.cx-skeleton > i` (0,1,1); a bare
   .cx-geo__skel is (0,1,0) and silently lost, rendering the map's loading state as an
   11px hairline in a 196px well. The @media twin below is bumped in lockstep — bumping
   only this one would make the narrow override lose to the base it overrides. */
.cx-skeleton > .cx-geo__skel { height: 116px; border-radius: var(--cx-radius-md); }

/* ── responsive ──────────────────────────────────────────────────────────
   372px contextual panel: NO modifier. The stage is ratio-driven so it shrinks,
   the legend already wraps, the list is one form. A shape that needs a second
   class at 372px was authored for one width.
   Below 900px the stage becomes a STRIP — it still locates things, but the
   answer is read in the rows, which already clear 44px at every width. */
@media (max-width: 900px) {
  .cx-geo { padding: var(--cx-s3); }
  .cx-geo__stage { aspect-ratio: 16 / 6; }
  .cx-geo__state { min-height: 132px; }
  .cx-skeleton > .cx-geo__skel { height: 84px; }
}

/* ══ DISCLOSURE ═══════════════════════════════════════════════════════════
   Authored 2026-08-30 after a census found EIGHT disclosure constructions
   already shipping across 9 of 11 surfaces, 75 instances, no two alike:
   three drivers (native <details>, aria-expanded plus consumer JS, and an
   ancestor data attribute three levels up), two opposite chevron
   conventions each argued in a comment, and six of the eight hiding their
   contents silently. The gap was never that the package lacked disclosure.
   It was that nothing named it, so every consumer invented one.

   TWO FAMILIES, and the chevron is what tells a reader which one they are in.
   A third, .cx-more, was built and retired the same day — see its headstone
   below. The rule that replaced it is one sentence:

     CONTENT OPENS IN PLACE ONLY WHEN IT ENDS A BLOCK.
     A RECORD'S 10% ALWAYS GOES TO THE PEEK.

     .cx-fold   a BLOCK of secondary content that ENDS a section, in place
     .cx-peek   a RECORD's detail, overlaying the main surface from the side
     (menus)    .cx-select, .cx-scope__btn, .cx-row__more — a list FLOATS
                above the plane; unchanged, and deliberately not folded in

   CHEVRON RULE — owner, 2026-08-30. A FOLD rests at -90deg, pointing
   at the affordance, and rotates TO the drawn glyph when it opens, so the
   open state is the chevron as designed and the arrow points at the content
   it revealed. This adopts .cx-match__toggle's convention over the three
   components that held the opposite one, on the strength of its argument
   rather than its volume. A MENU keeps rotate(180deg) from the drawn glyph:
   a floating list is not revealed below the control, and the two families
   must not read alike. Two conventions, one rule each, both learnable — as
   against the eight ad-hoc ones this replaces.

   COUNT RULE. A disclosure says HOW MUCH it hides. The package already
   holds the honest-absence rule (a thing that is not there says so and says
   why); this is that rule one level up, and it is also what keeps a density
   pass honest, since content cannot be demoted into a hole that will not
   say how deep it is. Only .cx-docs__folded and .cx-cal__more kept it.

   TWO CLICKS, NEVER MORE — Ed, on the 2026-08-30 call, verbatim: "we also
   don't want it where it's like even that 10% takes like five clicks... but
   like two clicks, right? Or one click." A peek is one. A fold inside a peek
   is two. Nothing in the package costs three.
   ──────────────────────────────────────────────────────────────────────── */

/* ── the shelf. Native <details>, so it opens with no script, is a real
   button in the accessibility tree, and stays keyboard-operable for free —
   the package still imports nothing and ships no runtime. */
.cx-fold > summary { list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 7px;
  font-size: var(--cx-sm); color: var(--cx-ink-2); padding: var(--cx-s1) 0; }
.cx-fold > summary::marker { content: ""; }
.cx-fold > summary::-webkit-details-marker { display: none; }
.cx-fold > summary:hover { color: var(--cx-ink); }
.cx-fold > summary:focus-visible { outline: none; box-shadow: var(--cx-focus-ring);
  border-radius: var(--cx-radius-sm); }
/* the count is the affordance, not decoration on it: a reader knows the size
   of what they are opening BEFORE they spend the click */
.cx-fold__count { font-weight: var(--cx-w-semi); color: var(--cx-ink); }
.cx-fold > summary .cx-icon { width: 14px; height: 14px; flex: none;
  color: var(--cx-ink-3); transform: rotate(-90deg);
  transition: transform var(--cx-dur-fast) var(--cx-ease); }
.cx-fold[open] > summary .cx-icon { transform: none; }
/* the boxed variant — a fold that is its own region rather than a line under
   one. .cx-docs__folded's geometry, named so anything can take it. */
.cx-fold--boxed { border: 1px solid var(--cx-line-2);
  border-radius: var(--cx-radius-lg); padding: 0 14px; }
.cx-fold--boxed > summary { padding: 11px 0; }

/* ── the row-level tuck: RETIRED 2026-08-30, owner — "I'd prefer only to use it
   when absolutely needed", and measured, that is never. Every one of the eleven
   surfaces carrying rows or records already carries a peek: gallery 51 rows,
   both proof pages (18-row and full-registry), capture, register. There is no surface where a record's
   detail has nowhere better to go.

   The deeper reason it had to go: EXPANDING IN PLACE MOVES THE PAGE. Open row 3
   of 20 and rows 4-20 shove down under the reader's eye — which is exactly the
   objection that ruled out a centred modal for the third rail. An inline row
   expander is a worse version of the thing already rejected, and it was built
   here two hours after that ruling.

   A record's 10% goes to .cx-peek, which is ONE click and moves nothing.

   If a genuine TREE ever turns up — sub-rows the same shape as their parent,
   where the nesting is itself the information — it gets built then, under a name
   that says tree. It is not this. */

/* ── the side-peek. RULED by the owner 2026-08-30: "notion style but still a
   side-peek window — not a middle modal."

   This is a PROMOTION, not a new component. The behaviour already existed
   and was already argued: between 901 and 1240px .cx-panel--sticky becomes
   exactly this — fixed, right-anchored, 372px, popover shadow, "so it reads
   as above rather than beside" (2026-08-26). That rule was written because
   rail 264 + panel 372 starved the main track. The owner's ruling says the
   same thing is right at every width, so the mid-band exception becomes the
   default and the column becomes the exception.

   NOT A MODAL, and the differences are load-bearing: it is anchored to one
   side rather than centred, there is NO full-page scrim, and the main
   surface stays visible and oriented behind it. A reader never loses their
   place, which is the whole reason it is not a modal.

   It keys on data-peek rather than data-panel so a surface that still wants
   the column keeps it — .cx-panel--sticky is untouched, the way the accent
   lane survived the ink-chrome cut. A stage never reserves a track for a
   peek, because a peek takes none. */
.cx-peek { position: fixed; right: var(--cx-s4); top: var(--cx-s3);
  width: 372px; max-height: calc(100vh - var(--cx-s3) * 2); overflow: auto;
  z-index: 30; box-shadow: var(--cx-shadow-popover); }
.cx-stage:not([data-peek="open"]) > .cx-peek { display: none; }
/* a register inside the peek folds the same way it does inside the column —
   three tracks, no quantity, no gauge; 346px of fixed track never fit 338 */
.cx-peek .cx-row { grid-template-columns: 14px 1fr 84px; }
.cx-peek .cx-row .cx-gauge, .cx-peek .cx-row__qty { display: none; }
/* dismissal is a real control in the header, never a click-anywhere: the
   surface behind stays live, so a stray click must not cost the reader the
   panel they opened */
.cx-peek__close { margin-left: auto; }
@media (prefers-reduced-motion: no-preference) {
  .cx-peek { animation: cx-peek-in var(--cx-dur) var(--cx-ease); }
  @keyframes cx-peek-in { from { transform: translateX(8px); opacity: 0; } }
}
@media (max-width: 900px) {
  /* the sheet, unchanged from the column's own narrow answer */
  .cx-peek { position: static; width: auto; max-height: none; overflow: visible;
    box-shadow: none; animation: none; }
}

/* ── person mark — a CUSTOMER on a work surface, name-first ────────────────
   Built 2026-09-02 for the Events "Today" surface. NO FIGMA SOURCE, and that is a
   finding rather than an omission: the anchor file in the registry is empty (one page,
   zero nodes) and the four reference kits are all data-visualisation kits with no
   person pattern in them. So this is authored from the package's own monogram doctrine,
   not ported, and it is a PROPOSAL until the owner ratifies the reading below.

   THE ROLE-CODE RULE IS NOT LOOSENED, IT IS BOUNDED. .cx-queue__mono is a ROLE code and
   its comment binds it — "a role code, never a person's initials" (2026-08-26, the
   awaiting-whom cell). That rule holds because a queue waits on a DESK: the person
   leaves and the desk still owns the work, which is the same reason .cx-doc's verified
   stamp names a role and never a person. A CUSTOMER is the opposite case. A customer is
   a named individual, there is no role standing behind them, and a surface that hides
   the name behind a code is unusable to whoever is working the day. So this is a SECOND,
   DISTINCT mark rather than a relaxation of the first, and the two are told apart on
   sight, in one rule that needs no legend:

     A ROLE IS SQUARE (--cx-radius-sm). A PERSON IS ROUND (--cx-radius-pill).

   In the ordinary case the difference is louder still: the role code paints no chip
   at all (background none, transparent border — the awaiting-whom cell is bare
   letters), while a person always wears a visible one. The radius is what tells the
   two apart in the state where BOTH draw a box, which is the dashed absence both of
   them have: nobody at the desk yet, and no name on record.

   THE NAME IS THE MARK. Initials are an ABBREVIATION of a name already rendered in the
   same element — never a substitute for it, never rendered alone, and never in the role
   slot. Where there is no name on record (a bare handle, a phone number), nothing is
   guessed: the chip goes dashed and empty in .cx-absent's grammar and the handle stands
   in as the name. Two states, one attribute.

   NO PHOTOGRAPHS. The package fetches nothing (README: no @import, no remote url()), and
   an avatar image is a remote request per customer. Letters only.

   The chip is .cx-conn__logo's ratified neutral monogram, unchanged — a quiet
   counter-surface chip with ink-2 letters, a ground chip on a panel. Nothing animates. */
.cx-person { display: inline-flex; align-items: center; gap: var(--cx-s2); min-width: 0;
  font-size: var(--cx-body); color: var(--cx-ink); }
.cx-person__mono { flex: none; width: 22px; height: 22px; display: grid; place-items: center;
  border-radius: var(--cx-radius-pill); background: var(--cx-ground);
  border: 1px solid var(--cx-line); color: var(--cx-ink-2);
  font-size: 9.5px; font-weight: var(--cx-w-bold); letter-spacing: .02em; }
/* the name holds one line and ellipses. The second line is what this person is TO the
   record — "first booking", "third event this year" — never a second identifier, because
   a mark that needs an id beside it has failed to be a name. */
.cx-person__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: var(--cx-w-semi); }
.cx-person__name small { display: block; font-size: var(--cx-sm); font-weight: var(--cx-w-regular);
  color: var(--cx-ink-3); overflow: hidden; text-overflow: ellipsis; }
/* the panel-header form — .cx-conn__logo's 34px, the same chip one size up */
.cx-person--lg { gap: var(--cx-s3); font-size: var(--cx-h3); }
.cx-person--lg .cx-person__mono { width: 34px; height: 34px; font-size: var(--cx-sm); }
/* NO NAME ON RECORD — the dashed grammar, and the handle stands in. Never invented initials. */
.cx-person[data-state="unknown"] .cx-person__mono { background: none; border-style: dashed;
  border-color: var(--cx-line); color: var(--cx-ink-3); }
.cx-person[data-state="unknown"] .cx-person__name { font-weight: var(--cx-w-regular);
  color: var(--cx-ink-2); }

/* ── inbound message — a customer's own words on a work surface ────────────
   Built 2026-09-02 for the Events "Today" surface. NO FIGMA SOURCE (same finding as
   the person mark above). Authored from .cx-reading's reading size and .cx-row's
   band grammar. A PROPOSAL until ratified.

   THIS IS NOT CHAT, AND THAT DISTINCTION IS THE WHOLE COMPONENT. The AI-signal block
   states the rule — "Chat is NOT here. Chat stays in the pop-out companion" — and
   .cx-chat-fab is where a conversation lives. That rule is not bent here, because
   this is not a conversation. It is EVIDENCE: one or a few messages a customer
   actually sent, quoted on the surface where the work is done, so whoever is working
   the day can read what was asked without leaving the record. Three things follow,
   and each is a rule rather than a preference:

     INBOUND ONLY. There is no outbound message here and no composer. Every message
     sits on ONE left edge — no alternating sides, no bubble tails, no reply box. The
     moment a second voice appears the surface has become chat and belongs in the
     companion. Replying is a link out to the channel, or the companion; never here.
     THE CHANNEL IS A MARK PLUS ITS WORD. The sprite's four channel silhouettes are
     house shapes and never brand lockups (icons.svg), so the glyph stays currentColor
     and NEVER takes a brand hue — a message is not more urgent because it arrived on
     Instagram. There is deliberately no per-channel modifier and no data-channel
     attribute: nothing would read it that the word does not already say, and a
     per-channel colour is the vendor lockup the sprite refuses, one layer down.
     THE TEXT IS AT READING SIZE. --cx-h3, the size .cx-reading gives the machine's
     own sentence, because the customer's sentence is at least as worth reading.

   THREE states on one attribute, data-state, mark and word in every one:
     (unset)  read
     new      arrived since the last look. The flag carries the WORD "New", so it
              survives greyscale; the accent wash reinforces and is never the signal.
     none     the body could not be read — a photo, a voice note, a message the
              customer deleted. .cx-absent's dashed grammar with the reason written
              out, because "we cannot read this one" is a sentence a person can act on
              and an empty bubble is not.

   A LONG THREAD IS A FOLD, not a scroller: .cx-fold opens in place and says how much
   it hides, and a list of messages ends a block, which is exactly when the disclosure
   vocabulary permits it. The RECORD behind the message still goes to .cx-peek.
   Nothing here animates. */
.cx-msgs { display: grid; }
.cx-msg { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--cx-s3);
  row-gap: 5px; align-items: center; padding: 11px var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1);
  border: 0; border-bottom: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-md);
  background: none; color: var(--cx-ink); font: inherit; text-align: left; }
.cx-msgs > .cx-msg:last-child { border-bottom: 0; }
/* a message with a record behind it becomes a button, the .cx-doc precedent; one
   without stays visibly static, because a false affordance is worse than none. An
   element on a panel hovers to the ground. */
button.cx-msg { width: 100%; cursor: pointer; }
button.cx-msg:hover, button.cx-msg:focus-visible { background: var(--cx-ground); outline: none; }
/* WHO — .cx-person, composed. A sender is a customer and the package now has one mark
   for that; this block grows no second one. */
.cx-msg__who { grid-column: 1; grid-row: 1; }
/* WHEN — tabular on the working face. A message's timestamp is not a display figure. */
.cx-msg__at { grid-column: 2; grid-row: 1; justify-self: end; font-size: var(--cx-sm);
  color: var(--cx-ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* WHERE FROM — the channel's silhouette and its word, plus the flag if there is one */
.cx-msg__meta { grid-column: 1 / -1; grid-row: 2; margin: 0; display: flex; align-items: center;
  flex-wrap: wrap; gap: 7px; font-size: var(--cx-sm); color: var(--cx-ink-2); }
.cx-msg__meta .cx-icon { width: 13px; height: 13px; color: var(--cx-ink-3); }
/* WHAT WAS SAID */
.cx-msg__body { grid-column: 1 / -1; grid-row: 3; margin: 0; font-size: var(--cx-h3);
  line-height: var(--cx-lh-normal); color: var(--cx-ink); }
/* the flag — mark and word, and it paints nothing unless the state declares it, so a
   consumer may leave it in the markup for every message and let the state decide */
.cx-msg__flag { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px;
  border-radius: var(--cx-radius-sm); background: var(--cx-accent-wash);
  color: var(--cx-accent-quiet); font-size: var(--cx-sm); font-weight: var(--cx-w-medium); }
.cx-msg:not([data-state="new"]) .cx-msg__flag { display: none; }
/* NOTHING READABLE ARRIVED — the body slot carries the reason, in the dashed grammar */
.cx-msg[data-state="none"] .cx-msg__body { border: 1px dashed var(--cx-line);
  border-radius: var(--cx-radius-md); padding: 9px 11px; font-size: var(--cx-sm);
  line-height: var(--cx-lh-snug); color: var(--cx-ink-2); }
@media (max-width: 900px) {
  /* the band is a real target here when the record is behind it */
  button.cx-msg { min-height: 44px; }
}

/* ── money against a contract — collected, outstanding, and the total ──────
   Built 2026-09-02 for the Events "Today" surface. NO FIGMA SOURCE. The one thing the
   reference kits hold that fits this shape is the DBS file's "Horizontal stacked bar
   chart" guideline plate (page "Data Visualisation Guideline"), and that file carries
   no licence in the registry, so it was read for structure and nothing was taken from
   it. Authored from .cx-progress and .cx-bar--stack, which already own this geometry.
   A PROPOSAL until ratified.

   AN EXTENSION, NOT A NEW FAMILY, AND NOT EVEN A NEW TRACK. .cx-bar--stack already
   states the rule — "a stack of TWO is a ratio, and the remainder is the track" — and
   .cx-progress already owns a single ratio against a limit, its fill, its outline and
   its anchored end. So THIS BLOCK DRAWS NO BAR. The markup composes .cx-progress
   itself, verbatim, and everything here is the sentence around it: a booking's money
   is three figures that have to agree (contracted = collected + outstanding), and a
   bar that shows two of them and leaves the reader to do the subtraction is exactly
   where a money surface goes wrong.

   COLLECTED IS PROGRESS TO A TARGET, so the fill is the accent it already is — the
   same correction .cx-progress took on 2026-08-29, where a series ink on a gauge
   encoded nothing and simply arrived as the loudest thing in the row.

   OUTSTANDING IS NOT AN ERROR. Money not yet due is not a defect and never wears
   --cx-error; it is the remainder, which is the track, and the legend says so. A date
   that bites is a CLOCK and the package ships one — compose .cx-clock--inline beside
   this rather than painting a balance red. Red here would spend the error hue on
   every half-paid booking in the book, which is most of them.

   THE LEGEND KEY IS THE MARK — the geo frame's rule. Each part's swatch is the paint
   its own segment carries, so nothing has to be matched from memory.

   THREE states on one attribute, data-state, and the state's WORD is an ordinary
   .cx-pill in the header rather than a chip this block invents:
     (unset)   part collected
     settled   nothing outstanding: the track fills and takes --cx-ok, the same done
               paint .cx-clock[data-state="done"] uses
     none      a figure is missing — no contracted total yet, or nothing recorded as
               collected. A PROPORTION CANNOT BE DRAWN FROM AN UNKNOWN, so none is
               drawn: the track goes dashed and empty in .cx-clock[data-state="none"]'s
               exact grammar (the bound exists, nothing runs on it), the missing figure
               renders as an em dash on .cx-money__part--absent, and the caption says
               which figure is missing and who has it.

   ON THE DISPLAY FACE, BY OWNER RULING (2026-09-02, "insert some of the quirky flavor
   we have with the digits"): money against a contract is a finance figure, and finance
   figures are the one place the house lets the retro face out (the Insights hero, the
   post-event figures). The contracted total sits at the 27px tile scale, the two parts
   at --cx-h3 on the same face; labels and the caption stay on the working face.

   THE BAR WEARS THE CHART REGISTER, ALSO BY THAT RULING: collected is --cx-series-1
   with the chart outline, outstanding is --cx-series-2 as a second segment — the
   .cx-bar--stack grammar, where a stack of two is a ratio. This overrides, for money
   only, the 2026-08-29 progress correction that put a plain gauge on the accent; a
   booking's money is a two-part stack and reads as one, not as a gauge. The legend
   marks carry the same two paints. Nothing here animates. */
.cx-money { display: grid; gap: var(--cx-s2); min-width: 0; }
/* the contracted total — what the other two figures must add up to, so it leads */
.cx-money__hd { margin: 0; display: flex; align-items: baseline; gap: var(--cx-s2); flex-wrap: wrap;
  font-size: var(--cx-sm); color: var(--cx-ink-3); }
.cx-money__hd b { margin-left: auto; font-family: var(--cx-font-figure); font-size: 27px; font-weight: 400;
  letter-spacing: 0; line-height: 1; color: var(--cx-ink); font-variant-numeric: tabular-nums; }
/* the parts. Two, never three: a stack of two is a ratio and the remainder is the track. */
.cx-money__parts { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.cx-money__part { display: flex; align-items: center; gap: var(--cx-s2);
  font-size: var(--cx-sm); color: var(--cx-ink-2); }
.cx-money__part::before { content: ""; flex: none; width: 9px; height: 9px;
  border-radius: min(2px, 50%); background: var(--cx-line-2);
  border: var(--cx-chart-outline) solid var(--cx-chart-outline-ink); box-sizing: border-box; }
.cx-money__part--in::before { background: var(--cx-series-1); }
.cx-money__part:not(.cx-money__part--in):not(.cx-money__part--absent)::before { background: var(--cx-series-2); }
.cx-money__part b { margin-left: auto; font-family: var(--cx-font-figure); font-size: var(--cx-h3); font-weight: 400;
  letter-spacing: 0; color: var(--cx-ink); font-variant-numeric: tabular-nums; }
/* THE BAR: collected in series-1, the remainder painted series-2 as the second segment of a
   two-part stack, both under the chart outline. Scoped to .cx-money so .cx-progress keeps
   its accent gauge everywhere else. */
.cx-money .cx-progress { background: var(--cx-series-2); border: var(--cx-chart-outline) solid var(--cx-chart-outline-ink); }
.cx-money .cx-progress__fill { background: var(--cx-series-1); box-shadow: inset calc(var(--cx-chart-outline) * -1) 0 0 var(--cx-chart-outline-ink); }
/* a figure nobody has — the em dash, and the caption carries who has it */
.cx-money__part--absent { color: var(--cx-ink-3); }
.cx-money__part--absent::before { background: none; border: 1px dashed var(--cx-line); }
.cx-money__part--absent b { color: var(--cx-ink-3); font-weight: var(--cx-w-regular); }
/* the caption carries the word the marks cannot — the .cx-clock__cap grammar */
.cx-money__cap { margin: 0; font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-3); }
.cx-money__cap em { font-style: normal; color: var(--cx-ink-2); }
/* SETTLED — the done paint, on the track and on its own key */
/* SETTLED — the bar is all collected: series-1 end to end, no second segment, and the
   WORD carries the state in the header pill. STATUS HUES NEVER PAINT CHART GEOMETRY
   (owner ruling 2026-09-02, see the rule above .cx-chart): the green that had filled
   this track sat between the two series on the same panel and fought both. The key
   for the empty remainder goes neutral. */
.cx-money[data-state="settled"] .cx-progress { background: var(--cx-series-1); }
.cx-money[data-state="settled"] .cx-progress__fill { background: var(--cx-series-1); box-shadow: none; }
.cx-money[data-state="settled"] .cx-money__part:not(.cx-money__part--in)::before { background: var(--cx-line-2); }
/* A FIGURE IS MISSING — nothing is drawn, and the drawn-but-empty track says the bound
   exists while nothing runs on it */
.cx-money[data-state="none"] .cx-progress { background: none; border: 1px dashed var(--cx-line); }
.cx-money[data-state="none"] .cx-progress__fill { display: none; }

/* ══════════════════════════════════════════════════════════════════════════
   THE COCKPIT SIX — built 2026-09-02 from an owner-affirmed static mock of a
   record cockpit (a register, a record, an intake). NO FIGMA SOURCE for any of
   the six: no file in the registry holds these patterns, so each is authored
   from parts this package already owns. PROPOSALS until ratified, the same
   standing .cx-person, .cx-msg and .cx-money carry.

   THE MOCK IS THE PROVENANCE AND IT IS ALSO THE FINDING. It shipped fourteen
   page-local style blocks, every one commented with what the package could not
   express. Six of the fourteen were genuinely reusable and are below. The other
   eight stay page-local ON PURPOSE — a two-up field grid, a peek-width pad for a
   static review, a six-up filter bar — because a layout a page arranges once is
   not a component, and a class that exists to hold two things side by side is a
   utility wearing a component's name.

   WHAT THE SIX HAVE IN COMMON, and it is the reason they are one block: they
   are the parts of a RECORD THAT DECIDES. A register row that carries the one
   verb its state calls for; the tile that filters that register; the stage
   engine the record moves through; each document and whether what it says holds
   up; the vote the stage is waiting on; and the number the record is minted
   under. Zero new tokens, zero new hues, zero new faces.
   ======================================================================== */

/* ── attention tile — a live figure that is also a filter ──────────────────
   The mock's first block. .cx-tile already owns "when the answer is a number,
   not a chart" and .cx-tiles already owns the grid, so this shape adds exactly
   the two things they could not say: that a tile can be TURNED ON, and that a
   tile with nothing behind it must not draw a zero.

   IT IS AN INSTRUMENT AND A CONTROL AT ONCE, which is the whole point: the
   figure says how many, and pressing it filters the register beneath to those
   records. So it is a <button> with aria-pressed, never a div with a click
   handler — .cx-tile--go's precedent one step further, because --go opens
   something and this one FILTERS something, and a filter has an off.

   THE ACTIVE STATE SHIPS ITS WORD. Depth and an accent edge do not survive
   greyscale and do not reach a screen reader, so aria-pressed paints the edge
   AND reveals .cx-attn-tile__on — a mark and the word "Filtering" — the same
   construction .cx-msg__flag uses, and for the same reason: the flag may sit in
   the markup of every tile and the state decides whether it paints.

   THE EMPTY STATE IS NOT A ZERO, and this is the rule the block exists for. A
   27px "0" in the display face is an instrument reporting on nothing while
   looking exactly like an instrument reporting on something, and on a strip of
   three it is the loudest tile in the row. data-state="none" takes the figure
   out and puts the SENTENCE in its slot at reading size, in .cx-absent's dashed
   grammar — "nothing waiting" — with the why line saying what would appear here
   and when. A tile that cannot be pressed is not pressable: the none state
   drops the hover and the consumer ships it as a <div> or a disabled button.

   THREE STATES on one attribute plus one on aria-pressed:
     (unset)             a count, and the tile can be pressed
     aria-pressed=true   this tile is filtering the list under it
     none                nothing is waiting — the honest empty, never a zero
   The strip is .cx-attn-tiles: EQUAL columns following the count, not auto-fit,
   because three instruments that are read against each other must be the same
   size. Below 900px it stacks; nothing here animates but the edge. */
.cx-attn-tiles { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  gap: var(--cx-s3); }
.cx-attn-tile { display: grid; align-content: start; gap: 3px; min-width: 0; text-align: left;
  font: inherit; color: var(--cx-ink); background: var(--cx-panel);
  border: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-lg); padding: 14px 16px;
  transition: border-color var(--cx-dur) var(--cx-ease); }
/* the label, and the ON flag sits beside it — the state belongs to the instrument's
   name, not to its figure */
.cx-attn-tile__what { display: flex; align-items: center; gap: var(--cx-s2); flex-wrap: wrap;
  font-size: var(--cx-sm); color: var(--cx-ink-3); }
/* the count — a headline figure, so it takes the display face, which is the one
   place this shape wears it. Never the why line, never the label.
   ONE FIGURE PER TILE (v1.3.4). The first cut let a money figure ride beside the
   count in a <small> — two kinds of figure in one figure face, a count and an
   amount, read against each other as if they were the same measure. The figure face
   is for the ONE headline number; a second quantity is a sentence and goes in the
   why line as text ("three, ₱31.8M between them"). The <small> rule is gone so
   nothing can put it back. */
.cx-attn-tile__count { font-family: var(--cx-font-figure); font-size: 27px; font-weight: 400;
  letter-spacing: 0; line-height: 1.15; font-variant-numeric: tabular-nums; }
/* what the number MEANS, in one line of the client's own words. A figure with no
   sentence under it is a scoreboard, and a scoreboard decides nothing. */
.cx-attn-tile__why { font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-3); }
/* pressable — .cx-tile--go's hover, restated here because this shape carries its
   own box and does not inherit the tile's */
button.cx-attn-tile { width: 100%; cursor: pointer; }
button.cx-attn-tile:hover { border-color: var(--cx-accent); }
/* ON. The edge and the inset ring group; the word means. */
.cx-attn-tile[aria-pressed="true"] { border-color: var(--cx-accent);
  box-shadow: inset 0 0 0 1px var(--cx-accent); }
.cx-attn-tile__on { display: inline-flex; align-items: center; gap: 5px; padding: 1px 8px;
  border-radius: var(--cx-radius-sm); background: var(--cx-accent-wash);
  color: var(--cx-accent-quiet); font-size: var(--cx-xs); font-weight: var(--cx-w-semi);
  letter-spacing: var(--cx-tr-wide); text-transform: uppercase; }
.cx-attn-tile__on .cx-icon { width: 11px; height: 11px; }
.cx-attn-tile:not([aria-pressed="true"]) .cx-attn-tile__on { display: none; }
/* NOTHING WAITING — the dashed grammar, and the sentence takes the figure's slot */
.cx-attn-tile[data-state="none"] { border-style: dashed; border-color: var(--cx-line);
  background: none; }
.cx-attn-tile[data-state="none"] .cx-attn-tile__count { font-family: var(--cx-font);
  font-size: var(--cx-h3); font-weight: var(--cx-w-semi); color: var(--cx-ink-2); }
.cx-attn-tile[data-state="none"]:hover { border-color: var(--cx-line); }

@media (max-width: 900px) {
  /* three instruments do not read against each other at 390px; they stack, and the
     band is a real target because pressing one filters the list */
  .cx-attn-tiles { grid-auto-flow: row; grid-auto-columns: auto; }
  button.cx-attn-tile { min-height: 44px; }
}

/* ── the verb row — a register row with one verb and one state summary ─────
   The mock's fourth block, and its comment is the finding: EIGHT CRUSHED CELLS
   LOSE TO FIVE READABLE ONES. The first cut of that register gave the stage, the
   sales type and the owning role three tracks of their own, and at 1440 beside an
   open peek every one of them computed under 90px and truncated to nothing. They
   ride the subject's sub-line here instead, which is where .cx-row__name has
   always put a record's second line.

   FIVE TRACKS: the reading dot · the subject and its sub-line · the clock · the
   STATE SUMMARY · the row's ONE VERB. The widths are the CONSUMER'S to tune —
   the rule .cx-row--status and .cx-row--doc both state — and what the package
   owns is the shape.

   ONE VERB, AND IT IS CHOSEN BY THE ROW'S STATE. Not a menu, not three buttons,
   not a kebab: the thing that would move this record today. A menu is where the
   verb that matters goes to hide (.cx-queue__acts says the same), and a row with
   four verbs has told the reader nothing about which one is next.

   NOTHING EVER STACKS A LIST INTO A COLUMN. This is an AUTHORING RULE and the
   whole reason the summary is one line plus a sub-line: a 196px track turns three
   bullet points into a vertical noodle, and the register loses its band rhythm on
   the first row that has four things to say. If a row needs a list, the list is
   in the peek. The summary's job is to say what state the record is in and what
   is holding it, in that order, in two lines.

   THE SUMMARY CARRIES A TONE and it is never colour alone: the tone paints the
   summary's own FIGURE (four of nine votes in, checklist six of seven) and the
   words around it stay ink-2, so a greyscale reader loses an emphasis and not a
   fact. Three tones, on data-tone, and the unset one is the base state of the set
   — most rows are simply moving, and .cx-clock's block already states why the base
   state of a set is not printed.

   THE CLOCK IS COMPOSED, never re-grown. .cx-clock--inline drops into track 3
   verbatim with its own data-state, exactly as it does in a queue row.

   THE ROW ITSELF IS NEVER THE BUTTON, and this is the one place .cx-row's own idiom
   does not carry over. Every other register row opens its peek by being rendered
   `role="button" tabindex="0"`; a verb row ALREADY CONTAINS a button, and nesting a
   real one inside a role="button" is three defects at once — the row's accessible
   name swells to the whole record plus the verb, a screen reader reads the verb
   twice, and `role="button"` on a div carries no Enter or Space handler, so the
   outer affordance is a promise the markup cannot keep. THE VERB IS THE AFFORDANCE.
   A call site that also needs the record openable makes the SUBJECT a button; it
   does not wrap the row in one.

   Selection moves to `aria-current`, not `aria-selected`: aria-selected is only
   meaningful inside a widget role, and this row deliberately has none. That much is
   a correction. WHAT IT DOES NOT DO IS MAKE SELECTION AUDIBLE, and the first draft
   of this comment claimed it did.

   MEASURED 2026-09-02, Chrome's accessibility tree, this row and the package's
   existing .cx-row side by side: BOTH come back `role: generic` with an empty
   accessible name. A screen reader is told nothing that distinguishes the selected
   record from any other — the wash and the 2px inset bar are the only things saying
   "this is the one the peek is showing", and neither survives greyscale or reaches
   audio. Swapping the attribute did not fix that; it fixed the nested-button defect
   beside it, which is a different bug.

   THE PACKAGE ALREADY KNOWS THE ANSWER AND IT IS NOT AN ATTRIBUTE. .cx-select__opt
   pairs its aria-current with a VISIBLE TICK (.cx-select__tick, #i-check) — mark and
   word, the rule this file states everywhere else and the one rule row selection has
   never obeyed. The fix is a mark on the selected row, not a better attribute.
   CLOSED in v1.3.4 (owner-ruled 2026-09-02): the selected row leads its subject with
   the WORD "Showing" — see the rule beside .cx-row[aria-selected] above, which this
   row shares. A word rather than a tick, because the check glyph is spent on done,
   verified, go and holds up. The paint is .cx-row[aria-selected]'s, shared below.

   THE SUMMARY IS TWO LINES, NEVER THREE (v1.3.4). At 248px the longest summary
   wrapped its state line onto a second line and pushed the sub-line to a third, and
   a three-line band beside a one-line verb is the noodle again. Two answers, both
   taken: the summary track widens to 320px where the register has the room (a
   container query, so a register beside an open peek keeps 248), and the block is
   clamped to two lines. What the clamp hides is in the peek, which is where a row's
   long form has always lived. THE VERB IS NEVER TRUNCATED — it keeps its width track
   and its nowrap, and the clamp never touches it. */
.cx-row--verb { grid-template-columns: 14px minmax(0, 1fr) 56px 248px 184px;
  column-gap: var(--cx-s2); align-items: center; }
.cx-row--verb > * { min-width: 0; }
.cx-row--verb > .cx-row__dot { grid-column: 1; grid-row: 1; }
.cx-row--verb > .cx-row__name { grid-column: 2; grid-row: 1; }
/* the sub-line holds the facts that would each have taken a track of their own; it
   is one line and it truncates, because a wrapping sub-line breaks the band */
.cx-row--verb > .cx-row__name small { white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; }
.cx-row--verb > .cx-clock--inline { grid-column: 3; grid-row: 1; justify-self: end; }
/* the clock in this row paints its figure from its own state, the queue's precedent —
   the row's summary already carries a tone and two hues in one band is one too many */
.cx-row--verb > .cx-clock--inline[data-state="soon"] .cx-clock__figure b { color: var(--cx-warn); }
.cx-row--verb > .cx-clock--inline[data-state="over"] .cx-clock__figure b { color: var(--cx-error); }
.cx-row--verb > .cx-clock--inline .cx-clock__state { background: none;
  padding-left: 0; padding-right: 0; }
/* WHAT STATE THIS RECORD IS IN, in one line, with what is holding it under it */
.cx-row__sum { grid-column: 4; grid-row: 1; font-size: var(--cx-sm); color: var(--cx-ink-2);
  line-height: var(--cx-lh-snug);
  /* two lines: the state line and its sub-line. A summary that needs more has a peek. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
.cx-row__sum b { font-weight: var(--cx-w-semi); font-variant-numeric: tabular-nums; }
.cx-row__sum small { display: block; font-size: var(--cx-sm); color: var(--cx-ink-3);
  margin-top: 1px; }
.cx-row__sum[data-tone="warn"] b  { color: var(--cx-warn); }
.cx-row__sum[data-tone="error"] b { color: var(--cx-error); }
/* THE ONE VERB. A width, not `auto`: every row is its own grid, so an auto last
   track lets the 1fr subject differ row to row and the verbs stop lining up — the
   defect .cx-queue__rows caught in its first render, in this exact place. */
.cx-row__verb { grid-column: 5; grid-row: 1; justify-self: end; }
.cx-row__verb .cx-btn { white-space: nowrap; }

/* THE FOLD, AND IT FOLDS BY CONTAINER AS WELL AS BY VIEWPORT. The five tracks
   spend 474px before the subject gets anything, so a register beside an open peek
   at 1440 — or in any column narrower than a stage — computes the subject track to
   ZERO and the record loses its own name. That is exactly the defect .cx-queue
   found in its first proof-page render, and it is fixed the same way: the register
   folds when ITS OWN width is short, viewport aside. The container is declared with
   :has(), so only a register that actually holds verb rows becomes one and every
   existing .cx-rows is untouched. */
.cx-rows:has(> .cx-row--verb) { container-type: inline-size; }
/* the summary takes 320px where the register has the room for it (v1.3.4); beside an
   open peek the base 248 holds and the two-line clamp does the rest */
@container (min-width: 1000px) {
  .cx-row--verb { grid-template-columns: 14px minmax(0, 1fr) 56px 320px 184px; }
}
@media (max-width: 900px) {
  /* subject and clock hold the first band, the summary takes the width under them,
     and the verb takes its own line as a real 44px target. The verb is never
     dropped — it is the reason the row exists. */
  .cx-row--verb { grid-template-columns: 14px minmax(0, 1fr) auto; row-gap: 6px;
    align-items: start; align-content: center; min-height: 44px; }
  .cx-row--verb > .cx-row__dot { grid-row: 1; margin-top: 7px; }
  .cx-row--verb > .cx-clock--inline { grid-column: 3; grid-row: 1; }
  .cx-row__sum  { grid-column: 2 / -1; grid-row: 2; }
  .cx-row__verb { grid-column: 2 / -1; grid-row: 3; justify-self: start; }
  .cx-row__verb .cx-btn { min-height: 44px; }
}
@container (max-width: 820px) {
  .cx-row--verb { grid-template-columns: 14px minmax(0, 1fr) auto; row-gap: 6px;
    align-items: start; align-content: center; min-height: 44px; }
  .cx-row--verb > .cx-row__dot { grid-row: 1; margin-top: 7px; }
  .cx-row--verb > .cx-clock--inline { grid-column: 3; grid-row: 1; }
  .cx-row__sum  { grid-column: 2 / -1; grid-row: 2; }
  .cx-row__verb { grid-column: 2 / -1; grid-row: 3; justify-self: start; }
  .cx-row__verb .cx-btn { min-height: 44px; }
}

/* ── timeline — the state engine across, and the plan underneath ───────────
   The mock's sixth and seventh blocks, and the seventh IS the design. The first
   cut put each stage's requirements INSIDE its stepper column; a 105px column
   turns a sentence into a vertical noodle, which is the same failure the crushed
   eight-track register made one screen earlier. A STEPPER CARRIES POSITION. A PLAN
   NEEDS THE PAGE'S FULL WIDTH. So the two are separated here by construction:
   position across the top, requirements as wide rows underneath, one row per
   stage, and the two read as one component because they are one.

   THIS GROWS NO SECOND STEPPER. .cx-chain--across is the stepper, unchanged, and
   .cx-timeline is the frame around it: a scroll container, a per-stage width
   floor, and the companion rows. A chain of five did not need a frame; a chain of
   eleven does, and eleven is what a real lifecycle turned out to be.

   IT SCROLLS INSIDE ITSELF AND THE PAGE NEVER DOES. The floor is PER STAGE, not a
   total written down — grid-auto-columns takes a real minimum, so a chain of four
   fits its container and does not scroll while a chain of eleven overflows the
   frame and scrolls there. `min-width: min-content` on the chain is what makes the
   overflow land in the frame instead of collapsing the tracks. The floor is inside
   the 901px query because below it .cx-chain--across is the rail again, and a rail
   with a 1200px floor would be the horizontal scroll this rule exists to prevent.

   EVERY STAGE KEEPS ITS DATE HERE, which is the one behaviour the frame changes
   and it is scoped to the frame. The bare stepper closes every stage but the
   current one, because a record header asks "where is this now"; a timeline asks
   "where has this been", so the date and the role that recorded it stay open on
   the closed stages too. .cx-chain--across outside a .cx-timeline is untouched.

   AUTHORING RULE, not a declaration: NO SENTENCES IN A STEPPER COLUMN. A stage's
   name, its date, its role and its clock are four short things and they fit. Its
   requirements, its reason and its owed-list are sentences and they go to
   .cx-timeline__needs, which has the whole width. The mock proved both halves.

   .cx-timeline__needs — one wide row per stage, the stage on the left and what it
   will ask for spelled out across the rest. The CURRENT stage's row is
   data-state="now": it carries what is OWED TODAY rather than what will be asked
   for later, and it takes the accent wash, which is selection and not a semantic.
   data-state="undefined" is the stage whose rule the client has not supplied —
   listed, never dropped, and nothing is invented into it. */
/* CONTAINMENT, NOT JUST OVERFLOW, AND THIS IS THE RULE THE FIRST RENDER TAUGHT.
   `overflow-x: auto` clips what spills, but it does not stop the box itself from
   being SIZED by what is inside it: a scroll container still contributes its
   contents' min-content width up the tree, so the eleven-stage floor travelled out
   through the panel and gave the whole page a 1560px scrollWidth at a 1440
   viewport — a horizontal page scroll, which this package forbids outright.
   `container-type: inline-size` is what makes the frame's width independent of the
   chain inside it, and it is the same device .cx-queue already uses. Measured
   2026-09-02 in the gallery: THE TIMELINE ELEMENT contributed 1560 before and 1440
   after, and contributes nothing past the viewport at 390. (The gallery DOCUMENT
   still measures wider than 390 — that is .g-controls, the gallery's own toolbar
   chrome, and it predates this component.)

   IT MUST BE REACHABLE BY KEYBOARD, and that is a CALL-SITE obligation this class
   cannot discharge on its own. A scroll container is focusable in Chrome and is NOT
   in WebKit or Firefox, so a chain of eleven ends at stage 9 for anyone not using a
   mouse. Every call site ships `tabindex="0" role="region"` and an aria-label naming
   the record — the gallery and recipes/cockpit.html both do.

   THE CUE IS THE SCROLLBAR, and deliberately nothing more. `scrollbar-width: thin`
   is true exactly when there is something to scroll to and absent when there is not,
   which a painted edge-fade is not: a permanent gradient on a six-stage chain that
   fits would be the component claiming an overflow it does not have. The package
   restricts gradients to one chrome use (BEND 3 at the chart layer), so a four-layer
   scroll-shadow stack would also be a larger unratified change than the gap it
   closes. .cx-tabs at the section-tab layer has the same bare overflow and the same
   gap; it is NOT touched here — noted as precedent, not as licence. */
.cx-timeline { overflow-x: auto; min-width: 0; container-type: inline-size;
  scrollbar-width: thin; padding-bottom: var(--cx-s2); }
@media (min-width: 901px) {
  /* the floor is per stage, so the frame scrolls only when the chain is long enough
     to need it, and never because a number was written down. 128px (v1.3.4, was 112):
     a stage name is allowed TWO lines and never a third, and never an abbreviation —
     every code is spelled out, so "Contract review and order booking" has to fit on
     two, and its first line measures 111px on the working face. 112 held it by one
     pixel; the floor now leaves room. */
  .cx-timeline > .cx-chain--across { grid-auto-columns: minmax(128px, 1fr);
    min-width: min-content; align-items: start; }
  /* a timeline asks where this has BEEN, so a closed stage keeps its date and the
     role that recorded it. Scoped to the frame — the bare stepper is unchanged. */
  .cx-timeline > .cx-chain--across .cx-chain__when,
  .cx-timeline > .cx-chain--across .cx-chain__name small { display: block; }
}
/* WHO RECORDED IT — a role, never a person, because the person leaves and the role
   still owns the move. Sits under the date in the stage's own meta column. */
.cx-timeline__by { font-size: var(--cx-sm); color: var(--cx-ink-3); }
/* the plan. One row per stage: the stage on the left, what it will ask for across
   the rest, and neither is ever a column of bullets. */
.cx-timeline__needs { display: grid; margin: 0; }
.cx-timeline__needs > div { display: grid; grid-template-columns: 230px minmax(0, 1fr);
  gap: var(--cx-s2) var(--cx-s4); align-items: baseline;
  padding: var(--cx-s3) var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1);
  border-bottom: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-md); }
.cx-timeline__needs > div:last-child { border-bottom: 0; }
.cx-timeline__needs dt { min-width: 0; font-size: var(--cx-body);
  font-weight: var(--cx-w-medium); color: var(--cx-ink); }
.cx-timeline__needs dt small { display: block; margin-top: 1px; font-size: var(--cx-sm);
  font-weight: var(--cx-w-regular); color: var(--cx-ink-3); line-height: var(--cx-lh-snug); }
.cx-timeline__needs dd { margin: 0; min-width: 0; font-size: var(--cx-sm);
  line-height: var(--cx-lh-normal); color: var(--cx-ink-2); }
.cx-timeline__needs dd b { font-weight: var(--cx-w-semi); color: var(--cx-ink); }
/* who approves it — a demoted clause, not a second sentence */
.cx-timeline__needs dd em { font-style: normal; color: var(--cx-ink-3); }
/* WHAT IS OWED NOW. The wash is selection — this is the row you are on — and the
   word "owed" in the copy is what carries the meaning; depth and tint never do. */
.cx-timeline__needs > div[data-state="now"] { background: var(--cx-accent-wash);
  border-bottom-color: transparent; }
/* the stage the client has not defined: listed, and nothing is guessed into it */
.cx-timeline__needs > div[data-state="undefined"] dt { color: var(--cx-ink-2); }
.cx-timeline__needs > div[data-state="undefined"] dd { color: var(--cx-ink-3); }

@media (max-width: 900px) {
  /* the stage name takes its own line above what it asks for; a 230px label column
     leaves 100px for a sentence at 390px, which is the noodle again */
  .cx-timeline__needs > div { grid-template-columns: minmax(0, 1fr); }
}

/* ── document row — what the document says, and whether it holds up ────────
   The mock's eighth and ninth blocks. .cx-doc is a CARD in a board and stays one;
   a record at a stage asks for eight to twelve documents at once, and twelve cards
   is a tile wall where twelve rows is a list you can read down. So this is the
   card's parts RE-LAID, not re-authored: .cx-doc__title and .cx-doc__state are the
   card's own and behave identically here.

   WHAT THE ROW ADDS IS THE INTELLIGENCE, and it is two blocks and not one:
     __read     what the system READ off the document — two or three facts, in the
                machine's tinted voice. The .cx-queue__read precedent: .cx-reading
                is the AI surface, and its row-scale form is written out rather than
                shrunk, because .cx-reading's own sentence sits at --cx-h3 and a
                document row is a band.
     __verdict  whether it HOLDS UP — and this is a separate line because it is a
                JUDGEMENT ABOUT the reading, not more of the reading. Reading and
                verdict in one tinted box would let a machine's opinion wear the
                same weight as a machine's transcription, which is the one thing a
                document surface must not do.

   FOUR TONES ON THE VERDICT, on data-holds, and NEVER COLOUR ALONE — each ships a
   mark and a bold WORD, and the sentence after it stays ink-2 so a greyscale
   reader loses an emphasis and not a fact:
     yes    holds up          the reading agrees with the record
     check  check this        it disagrees, or it agrees with something that moved
     no     does not hold     it contradicts another document on this record
     none   nothing to read   there is no document, so there is nothing to judge.
            This is the honest fourth: a missing document has no verdict, and
            printing "holds up" over an absence is the fabricated zero again.

   REQUIRED IS A WORD, NOT A CHIP. The mock boxed it; container discipline
   (.cx-queue__chan, 2026-08-29) says a box around a small piece of text is a claim
   that this is a state or that you can act on it, and required-versus-advisory is
   neither. It is the .cx-doc__prop grammar instead — small caps, tracked, ink-2 for
   required and ink-3 for advisory — so the row's only containers are the ones that
   carry state.

   FIVE STATES on data-holds's sibling attribute, data-state, mark and word in every
   one. This vocabulary is the DOCUMENT'S ON A RECORD and it is deliberately not
   .cx-doc's: a canon board asks whether a version is current, and a record asks
   whether the thing arrived.
     received     it is on file
     returned     it came and it was sent back, and the reason is the verdict
     missing      it has not arrived — the dashed grammar
     superseded   struck and KEPT, .cx-doc's own rule and its own reason
     na           it does not apply here — .cx-absent--na, an absence with a reason
   Nothing here animates but the row's hover, which reduced motion zeroes. */
.cx-docs-rows { display: grid; }
.cx-docrow { display: grid; grid-template-columns: minmax(0, 1fr) 200px 172px;
  gap: var(--cx-s2) var(--cx-s3); align-items: start;
  padding: var(--cx-s3) var(--cx-s2); margin: 0 calc(var(--cx-s2) * -1);
  border-bottom: 1px solid var(--cx-line-2); border-radius: var(--cx-radius-md);
  transition: background var(--cx-dur-fast) var(--cx-ease); }
.cx-docrow:last-child { border-bottom: 0; }
.cx-docrow:hover { background: var(--cx-ground); }
.cx-docrow > * { min-width: 0; }
/* the name with its code in parentheses, and under it: at which stage, from which
   ROLE, and the reference if it has one. .cx-doc__title verbatim. */
.cx-docrow > .cx-doc__title { grid-column: 1; grid-row: 1; flex: none; }
.cx-docrow > .cx-doc__state { grid-column: 2; grid-row: 1; justify-self: start; }
/* THE VERBS SHARE A LEFT EDGE (v1.3.4). A right-aligned verb column lines up the
   ends of buttons whose widths differ by a hundred pixels, so the eye finds each
   verb in a different place down the list. The track is a fixed 172px — the width
   of "Request the document", the longest verb in the package's set — and the verb
   starts at its left edge, so every verb starts at one x. A two-verb row (Accept ·
   Open, 135px) fits inside it. */
.cx-docrow__verb { grid-column: 3; grid-row: 1; justify-self: start;
  display: flex; align-items: center; gap: var(--cx-s2); flex-wrap: wrap;
  justify-content: flex-start; }
.cx-docrow__verb .cx-btn { white-space: nowrap; }
/* required or advisory — a word in the title's own line, no container */
.cx-docrow__req { display: inline-flex; align-items: center; margin-right: 7px;
  font-size: var(--cx-xs); font-weight: var(--cx-w-semi); letter-spacing: var(--cx-tr-wide);
  text-transform: uppercase; color: var(--cx-ink-2); white-space: nowrap; }
.cx-docrow__req[data-req="advisory"] { color: var(--cx-ink-3); font-weight: var(--cx-w-medium); }
/* the intelligence, both halves, across the full width of the row */
.cx-docrow__intel { grid-column: 1 / -1; grid-row: 2; display: grid; gap: var(--cx-s2); }
/* WHAT THE SYSTEM READ — the machine's tint and sparkle at row scale, the
   .cx-queue__read construction. Two or three facts, each with its own value in
   weight, and the tail says how many and that none is confirmed. */
.cx-docrow__read { display: flex; align-items: flex-start; gap: 7px;
  padding: 5px 10px; border-radius: var(--cx-radius-sm);
  background: var(--cx-ai); border: 1px solid var(--cx-ai-line); color: var(--cx-ai-ink);
  font-size: var(--cx-sm); line-height: var(--cx-lh-snug); }
.cx-docrow__read .cx-icon { width: 13px; height: 13px; margin-top: 2px; flex: none; }
.cx-docrow__read > span { min-width: 0; }
.cx-docrow__read b { font-weight: var(--cx-w-semi); color: var(--cx-ink-2);
  font-variant-numeric: tabular-nums; }
.cx-docrow__read em { font-style: normal; color: var(--cx-ink-3); }
/* WHETHER IT HOLDS — a judgement, so it takes no container and no tint. The mark
   and the bold word carry the tone; the sentence stays ink-2. */
.cx-docrow__verdict { display: flex; align-items: flex-start; gap: 7px; margin: 0;
  font-size: var(--cx-sm); line-height: var(--cx-lh-snug); color: var(--cx-ink-2); }
.cx-docrow__verdict .cx-icon { width: 14px; height: 14px; margin-top: 1px; flex: none;
  color: var(--cx-ink-3); }
.cx-docrow__verdict b { font-weight: var(--cx-w-semi); color: var(--cx-ink-2); }
.cx-docrow__verdict[data-holds="yes"]   :is(.cx-icon, b) { color: var(--cx-ok); }
.cx-docrow__verdict[data-holds="check"] :is(.cx-icon, b) { color: var(--cx-warn); }
.cx-docrow__verdict[data-holds="no"]    :is(.cx-icon, b) { color: var(--cx-error); }
/* nothing to read is not a failure and never wears one: an absence is ink-3 */
.cx-docrow__verdict[data-holds="none"]  :is(.cx-icon, b) { color: var(--cx-ink-3); }

/* states — the chip carries mark and word; the ink and the strike only reinforce */
.cx-docrow[data-state="received"] .cx-doc__state { color: var(--cx-ink-2); }
.cx-docrow[data-state="returned"] .cx-doc__state { color: var(--cx-error); }
.cx-docrow[data-state="missing"] .cx-doc__state { background: none; padding: 2px 8px;
  border-color: var(--cx-line); color: var(--cx-ink-3); }
.cx-docrow[data-state="missing"] .cx-doc__title { color: var(--cx-ink-2); }
/* SUPERSEDED — struck and kept, .cx-doc's rule for .cx-doc's reason: the strike
   falls on the name only, so the stage it belongs to stays readable */
.cx-docrow[data-state="superseded"] .cx-doc__state { color: var(--cx-ink-3); }
.cx-docrow[data-state="superseded"] .cx-doc__title > b { text-decoration: line-through;
  text-decoration-thickness: 1px; color: var(--cx-ink-3); }
/* NOT APPLICABLE — .cx-absent--na's grammar: an absence WITH a reason, which is a
   different sentence from an absence waiting to be filled */
.cx-docrow[data-state="na"] .cx-doc__state { background: none; padding: 2px 8px;
  border-style: dashed; border-color: var(--cx-line); color: var(--cx-ink-3); }
.cx-docrow[data-state="na"] .cx-doc__title { color: var(--cx-ink-2); }

@media (max-width: 900px) {
  /* the state takes the band under the name and the verb takes its own line at 44px;
     three tracks at 390px leaves the document's name 60px, which is not a name */
  .cx-docrow { grid-template-columns: minmax(0, 1fr); row-gap: var(--cx-s2); }
  .cx-docrow > .cx-doc__title,
  .cx-docrow > .cx-doc__state,
  .cx-docrow__verb,
  .cx-docrow__intel { grid-column: 1; grid-row: auto; justify-self: start; }
  .cx-docrow__verb .cx-btn { min-height: 44px; }
}

/* ── the gate — a decision several roles record separately ─────────────────
   The mock's tenth block. COVERAGE's chain (shape 6) says WHICH stage a record is
   at; this says what the stage is WAITING ON, and they are different questions
   with different answers. A stage can be open for ninety-six days with nothing
   wrong with it except that five of nine roles have not recorded, and no chain
   mark can say that.

   IT IS NOT A QUEUE ROW AND THAT IS WHY IT IS ITS OWN SHAPE. A queue row asks who
   owes an answer and how long it has waited. A vote is four things: a ROLE, a
   WORD, a REASON and a TIME — and the reason is the load-bearing one, because a
   no go with no reason is a wall with no door, and the reason is what the decline
   letter is written from.

   NOBODY IS NAMED, EVER. The rail is ROLES: the monogram is .cx-queue__mono, whose
   own comment binds it to a role code and never to a person's initials, and the
   line beside it is the role's title. A person leaves; the role still votes.

   FOUR VOTE STATES on data-vote, mark and word in every one, one chip driven by
   the ROW so a row and its chip can never disagree — the .cx-doc__state idiom:
     go     recorded in favour
     no     recorded against, and the reason is on the record
     cond   go with a condition — the third choice, and the one a two-button gate
            silently converts into a no. It is not a half-vote; it is a go with a
            thing that has to happen first, and it wears caution, not error.
     none   nothing recorded. The calm state, on the sunken counter-surface, and
            the time cell is an em dash — never a fabricated timestamp.

   __agenda COMPOSES .cx-reading rather than growing a second machine voice: what
   is actually unresolved, across nine separate votes, is exactly the cross-item
   reading the AI-signal rule puts inside the panel it describes.

   __cast is the control for the role the viewer is acting as — the three choices
   as .cx-seg, the reason as .cx-field, and one primary verb. A CONTROL IS NOT A
   BAND: both take their own width rather than stretching the panel, because under
   a recessed register a stretched track reads as a grey slab across the page.

   Nothing here animates. No new token, no new hue, and the rubric a client scores
   its decision against is the client's own — this block renders a word and a
   reason and invents no score. */
/* THE TRACKS ARE THE LIST'S, NOT THE ROW'S (v1.3.4). The time track is sized to its
   content — a fixed 96px parked the date at the far right with a hole between it and
   the chip — and a content-sized track on a per-row grid is the defect .cx-row__verb
   names in its own comment: every row computes its own width, the 1fr differs row to
   row, and the chips stop sharing a left edge (an em dash is 13px, a date is 40). So
   the columns are declared ONCE on the list and every row is a subgrid of it: the
   time track is as wide as the widest time in the list, the chips share one edge, and
   the date sits beside the chip column rather than at the end of the band. Any other
   child of the list (a consumer nesting the agenda or the cast inside) spans the full
   width. Below 900px the row restates its own three tracks and leaves the subgrid. */
.cx-votes { display: grid; grid-template-columns: 26px minmax(0, 1fr) 160px max-content; }
.cx-votes > * { grid-column: 1 / -1; }
.cx-votes__row { display: grid; grid-template-columns: subgrid;
  gap: var(--cx-s3); align-items: baseline; padding: 9px var(--cx-s2);
  margin: 0 calc(var(--cx-s2) * -1); border-bottom: 1px solid var(--cx-line-2); }
.cx-votes__row:last-child { border-bottom: 0; }
.cx-votes__row > * { min-width: 0; }
.cx-votes__row > .cx-queue__mono { grid-column: 1; grid-row: 1; align-self: center; }
/* the role, and the reason it gave, in that order */
.cx-votes__role { grid-column: 2; grid-row: 1; font-size: var(--cx-body); color: var(--cx-ink); }
.cx-votes__role small { display: block; margin-top: 1px; font-size: var(--cx-sm);
  line-height: var(--cx-lh-snug); color: var(--cx-ink-3); }
/* the chip — .cx-queue__state's box written out for the same reason it was written
   out there: it is driven by the PARENT's attribute, and its calm state is the
   sunken counter-surface, which the .cx-pill family has no variant for. */
.cx-votes__state { grid-column: 3; grid-row: 1; justify-self: start; flex: none;
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  padding: 3px 9px; border: 1px solid transparent; border-radius: var(--cx-radius-sm);
  font-size: var(--cx-sm); font-weight: var(--cx-w-medium);
  background: var(--cx-panel-sunken); color: var(--cx-ink-2); }
.cx-votes__state .cx-icon { width: 13px; height: 13px; }
/* when it was recorded. Tabular on the working face: a vote's time is not a display
   figure. Nothing recorded takes an em dash, never a date. The track is max-content
   (v1.3.4, was 96px): a fixed track parked the date at the far right with a hole
   between it and the chip; sized to its content and right-aligned, the date sits
   beside the chip column and the two read as one cell. */
.cx-votes__when { grid-column: 4; grid-row: 1; justify-self: end; font-size: var(--cx-sm);
  color: var(--cx-ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cx-votes__row[data-vote="go"]   .cx-votes__state { background: var(--cx-ok-wash);    color: var(--cx-ok); }
.cx-votes__row[data-vote="no"]   .cx-votes__state { background: var(--cx-error-wash); color: var(--cx-error); }
.cx-votes__row[data-vote="cond"] .cx-votes__state { background: var(--cx-warn-wash);  color: var(--cx-warn); }
.cx-votes__row[data-vote="none"] .cx-votes__role  { color: var(--cx-ink-2); }
/* the agenda — .cx-reading, composed, with only its placement stated here */
.cx-votes__agenda { margin-top: var(--cx-s3); }
/* the vote control */
.cx-votes__cast { display: grid; gap: var(--cx-s3); margin-top: var(--cx-s4);
  padding: var(--cx-s4); border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-lg); background: var(--cx-panel); }
.cx-votes__cast > h4 { margin: 0; font-size: var(--cx-body); font-weight: var(--cx-w-semi); }
/* a control is not a band: both take their own width under every register type */
.cx-votes__cast .cx-seg { width: max-content; max-width: 100%; }
.cx-votes__cast .cx-field { margin-top: 0; }
.cx-votes__cast .cx-input { max-width: 680px; }

@media (max-width: 900px) {
  /* the reason takes the width; the chip and the time hold one band together */
  .cx-votes__row { grid-template-columns: 26px minmax(0, 1fr) auto; row-gap: 6px;
    align-items: start; min-height: 44px; }
  .cx-votes__state { grid-column: 2; grid-row: 2; }
  .cx-votes__when  { grid-column: 3; grid-row: 2; align-self: center; }
  .cx-votes__cast .cx-seg { width: 100%; }
  .cx-votes__cast .cx-seg button { flex: 1 1 0; min-height: 44px; }
}

/* ── minted reference — the number, and the part of it nobody has yet ──────
   The mock's thirteenth block. A record is minted under a reference in the
   client's own shape, and one segment of that shape is often set later by a
   different role. The whole component is what to do about that segment.

   THE GAP IS DRAWN AS A GAP. Not a placeholder, not zeros, not "TBD", and above
   all not a guess: a reference number is quoted into emails and purchase orders,
   and a plausible-looking invented segment is the one defect on this surface that
   costs money. .cx-mint__gap is .cx-absent's dashed grammar at glyph scale — a
   dashed rule under the unset segment — so the number reads complete in shape and
   visibly incomplete in fact.

   THE UNSET CHARACTERS ARE NOT CHARACTERS, and the first cut got this wrong. It
   rendered the pending segment as plausible letters ("...-0CC") in ink-3 with a
   dashed underline, sitting two lines above a complete reference ("...-1SG") in the
   same face — distinguished by nothing that survives a copy. Selected and pasted
   into an email, "P-26-00417-0CC" IS a complete reference number, which is the exact
   defect the paragraph above says costs money: the component was committing it while
   documenting it. The gap now renders ONE BULLET PER UNSET CHARACTER, so the segment
   still says how wide it is — the fact worth keeping — and its textContent can never
   be mistaken for a value.

   IT CARRIES AN ACCESSIBLE NAME, and `aria-label` alone did not give it one. A bare
   <span> has no role, and the accessibility tree drops aria-label from a generic
   element: the AX snapshot read "P-26-00417-0CC Category pending" with nothing
   saying which part was unset. Every call site ships `role="img"` alongside the
   label, which makes the element a leaf whose name replaces its content, so a screen
   reader gets "P-26-00417-0, product category, two characters, not yet set".

   THE GAP SHIPS ITS WORD, like every other state in this package: a
   .cx-pill--working beside the number names what is pending, and the caption says
   who sets it and whether anything downstream is held up by it. Dashes and a
   lighter ink are not a state on their own — they do not survive greyscale and
   they say nothing to a screen reader, which is why the pill is not decoration
   and why the gap carries its own accessible name at the call site.

   THE NUMBER IS A HEADLINE FIGURE, so it takes the display face — the one place
   this shape wears it. The caption and the pill stay on the working face. Two
   states: (unset) is a complete number with no pill and no gap; the gap is simply
   the segment being marked up as one. Nothing here animates. */
.cx-mint { display: flex; align-items: center; gap: var(--cx-s3); flex-wrap: wrap;
  padding: var(--cx-s4); border: 1px solid var(--cx-line);
  border-radius: var(--cx-radius-lg); background: var(--cx-panel); }
.cx-mint__n { font-family: var(--cx-font-figure); font-size: 27px; font-weight: 400;
  letter-spacing: .02em; line-height: 1.15; color: var(--cx-ink); }
/* the segment nobody has set yet — dashed, quiet, never invented, and never
   alphanumeric. The call site ships one bullet per unset character plus
   role="img" and a label; letter-spacing gives the bullets the width of the
   characters they stand in for.
   THE BULLETS ARE ON THE WORKING FACE, CENTRED ON THE FIGURE'S X-HEIGHT (v1.3.4).
   The figure face's bullet is a pixel-font glyph: small, and drawn high in its cell,
   so the gap read as a row of specks floating above the number. The bullets now
   take the working face at body size and the span is an inline-block aligned
   `middle`, which centres it on the parent's x-height — so the dots sit in the
   figure's own optical centre and the dashed rule under them lands at the
   baseline. The accessible name from v1.3.2 is untouched: role="img" and the
   label are the call site's, and this rule changes nothing they say. */
.cx-mint__gap { display: inline-block; vertical-align: middle; text-decoration: none;
  color: var(--cx-ink-3); font-family: var(--cx-font); font-size: var(--cx-body);
  line-height: .9; letter-spacing: .6em; padding: 0 0 0 .15em;
  border-bottom: 1.5px dashed var(--cx-line); }
/* the caption — who sets the pending segment, and what it does or does not hold up */
.cx-mint__cap { flex: 1 1 100%; margin: 0; font-size: var(--cx-sm);
  line-height: var(--cx-lh-snug); color: var(--cx-ink-3); }
.cx-mint__cap em { font-style: normal; color: var(--cx-ink-2); }
