/******************** GOLDEN STYLES ********************/

/* There is no full-bleed content area any more (decision 15 of 9 September 2026). A
   `main.tz-main--full { max-width: none }` rule lived here: a third, undocumented width that let
   the duplicate screens, the record listing and a handful of others run edge to edge on any
   monitor. header.html now maps layout='full' onto .tz-main--wide, the documented 1600px, so the
   product has two widths — 1240 and 1600 — and this file adds none. */

/* Status pills and neutral tags sit side-by-side in dense chip rows (entity/
   table/resource cards). Lens's tz-pill dot looks noisy and inconsistent next
   to the dotless tz-tag labels there, so drop the dot: chips are flat, the
   color alone carries the status. */
.tz-pill::before {
  display: none;
}

/* Row hover on ALL data tables (Bootstrap only tints rows on `.table-hover`,
   and not every table carries it). Bootstrap 5.3 paints the cell background via
   `box-shadow: inset ... var(--bs-table-bg-state)`, so a plain background-color
   is hidden — drive the state variable instead. */
.table > tbody > tr:hover > * {
  --bs-table-bg-state: var(--tz-surface-sunken);
}

/* Resource picker (create-resource table): rows are selectable — JS toggles
   .is-selected. The selected row is teal with white text, but the universal
   row-hover above paints cells with the light sunken tint through Bootstrap's
   bg-state box-shadow (which sits over background-color), making the white text
   unreadable on hover. Deepen the hover of a selected row to primary-dark so the
   white text keeps its contrast. */
#configuration-create-table tbody tr.is-selected > * {
  background-color: var(--tz-primary);
  color: var(--tz-white);
}
#configuration-create-table tbody tr.is-selected:hover > * {
  --bs-table-bg-state: var(--tz-primary-dark);
}

/* Retired .btn-light is used app-wide for Cancel/toolbar buttons — map it to the
   neutral (secondary) role so it reads teal-gray instead of Bootstrap light. */
.btn-light {
  --bs-btn-bg: var(--tz-surface-sunken); --bs-btn-border-color: var(--tz-border); --bs-btn-color: var(--tz-text-muted);
  --bs-btn-hover-bg: var(--tz-surface-hover); --bs-btn-hover-border-color: var(--tz-border-strong); --bs-btn-hover-color: var(--tz-text);
  --bs-btn-active-bg: var(--tz-surface-hover); --bs-btn-active-border-color: var(--tz-border-strong); --bs-btn-active-color: var(--tz-text);
  /* Disabled was the one state left to Bootstrap, and its #6c757d is the only
     non-teal gray that ever reached the screen — caught by the machine audit of
     the quality page's text combinations. */
  --bs-btn-disabled-color: var(--tz-text-disabled); --bs-btn-disabled-border-color: transparent;
}

/* Segmented btn-check controls (e.g. the ETL Load/Transform/Export selector):
   the focus sits on the hidden radio input, so Bootstrap styles the label via
   .btn-check:focus + .btn with its default BLUE glow (theme.css only tokenizes
   .btn:focus-visible on the button itself). Kill the blue: no ring on mouse
   selection, a teal ring only on keyboard focus. */
.btn-check:focus + .btn,
.btn-check:checked:focus + .btn {
  box-shadow: none;
}
.btn-check:focus-visible + .btn,
.btn-check:checked:focus-visible + .btn {
  box-shadow: 0 0 0 2px var(--tz-primary);
}
/* Dark: the brand-teal focus ring is near-invisible on the dark canvas — accent. */
[data-bs-theme="dark"] .btn-check:focus-visible + .btn,
[data-bs-theme="dark"] .btn-check:checked:focus-visible + .btn {
  box-shadow: 0 0 0 2px var(--tz-accent);
}

/* Neutral hairline outline for ALL non-primary outline actions. Both
   .btn-outline-secondary (Close/Cancel) and .btn-outline-primary (Search/Filter/
   New/Edit…) share it so an action toolbar never mixes teal and neutral buttons —
   teal is reserved for the one filled .btn-primary. Faint --tz-border hairline,
   neutral (not teal) text, neutral hover fill; theme-aware via the aliases. */
.btn-outline-secondary,
.btn-outline-primary {
  /* The quiet control (option E, September 2026): a hairline border on the surface
     colour makes it unmistakably a button on paper — the failure of the pure-word
     treatment was that on paper a button-word and a link are the same thing. It
     stays quiet: muted 13px text, and only the border answers the pointer. */
  --bs-btn-bg: var(--tz-surface);
  --bs-btn-color: var(--tz-text-muted); --bs-btn-border-color: var(--tz-border);
  --bs-btn-hover-bg: var(--tz-surface); --bs-btn-hover-border-color: var(--tz-border-strong); --bs-btn-hover-color: var(--tz-heading);
  --bs-btn-active-bg: var(--tz-surface-sunken); --bs-btn-active-border-color: var(--tz-border-strong); --bs-btn-active-color: var(--tz-heading);
  --bs-btn-disabled-color: var(--tz-text-disabled); --bs-btn-disabled-bg: var(--tz-surface); --bs-btn-disabled-border-color: var(--tz-border);
}

.table-text {
  font-size: 12px;
}

.card-subtitle {
  color: var(--tz-text-muted);
  font-size: 14px;
  font-weight: 400;
}


/* The expert toolbelt (duplicates board): labelled control rows under the page
   head — filtering always, the bulk actions as a second row while any card is
   selected. The label is the micro-label register. */
.tz-toolbelt { margin: 4px 0 18px; }
.tz-toolbelt__head {
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--tz-text-muted); margin: 0 0 8px;
}
/* ⚠️ The chips' row needs its own air (Nacho, 30 September 2026). It is a separate flex
   container from the controls above it, so the `gap` that spaces the buttons from each other
   does not reach across — a gap works inside one flex box, never between two. Without this the
   chips started a new line and then sat flush against the buttons, which reads as a wrap rather
   than as the second statement it is. The 8px matches the belt head's own `margin-bottom`, so
   head-to-controls and controls-to-chips are the same step. */
.tz-toolbelt__chips { margin-top: 8px; }
/* The active-filter chips reuse the records list's pill; the × inside is the
   remove affordance and the whole chip is the click target. */
.tz-toolbelt .tz-filter-pill .bi-x { margin-left: 5px; font-size: 12px; }

/* ============ The simple steward queue (refined option A, 2 September 2026) ============
   Hero count and the one door on the left; the first ten cases as quiet rows on the
   right, pagination as the only control. */
/* ⚠️ ONE COLUMN SINCE 29 SEPTEMBER 2026 (Nacho). The left-hand hero — the two figures, the
   preset's sentence, the start door and the tints' key — went into the CONTEXT BAR, which is
   where what qualifies the queue belongs; what is left is the queue itself, and it takes the
   width. Moving half of it and leaving the rest was the first attempt: a narrow column holding a
   sentence and a legend, adrift beside a list that had stepped away from it.
   The grid stays a grid with one track rather than becoming a block: the responsive rules, the
   list's own gutter and every child below are written against it. */
.tz-queue { display: grid; grid-template-columns: minmax(0, 1fr); margin-top: 6px; }
/* Only the admin has the triage pills; when they are there, the list breathes below
   them (the sibling selector IS the role condition — no filters, no extra air). */
.tz-filters + .tz-queue, .tz-filters + .tz-empty { margin-top: 26px; }
/* The figure: the big serif number with its noun beside it, both feet on one line —
   a number without a name was the review that redrew this column (3 September 2026). */
.tz-queue__fig { display: flex; align-items: flex-end; gap: 12px; }
.tz-queue__fig--rule { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--tz-border-subtle); }
.tz-queue__count {
  font-family: var(--tz-font-display); font-size: 64px; font-weight: 600; line-height: .88;
  letter-spacing: -0.03em; color: var(--tz-heading); font-variant-numeric: tabular-nums lining-nums;
}
.tz-queue__unit {
  font-family: var(--tz-font-display); font-size: 18px; font-weight: 600;
  color: var(--tz-heading); line-height: 1.15; max-width: 9em; padding-bottom: 2px;
}
.tz-queue__note { font-size: var(--tz-text-base); color: var(--tz-text-muted); margin: 10px 0 0; max-width: 26ch; }
/* ⚠️ No left rule and no gutter: they separated the list from the hero beside it, and there
   is no longer anything on that side to be separated from. */
.tz-queue__list { border-left: 0; padding-left: 0; }
/* ⚠️ A CASE IS A BLOCK, NOT A TABLE ROW (Nacho, 30 September 2026). Both variants carry the
   glance — the field-by-field comparison — so a row here is ~170px of dense data, and 12/14px of
   padding left the "See every field" link of one case a hair from the identifier of the next,
   with a hairline as the only thing saying they were different cases. 24px, which is the scale's
   own step above the 16 that would barely have shown; per side it is what the reader reads as the
   seam. The hairline stays: air separates, a rule states. */
.tz-queue__row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 16px;
  align-items: center; padding: var(--tz-sp-6) 4px;
  border-bottom: 1px solid var(--tz-border-subtle);
  text-decoration: none; color: inherit;
}
.tz-queue__row:hover { background: var(--tz-n50); color: inherit; }
[data-bs-theme="dark"] .tz-queue__row:hover { background: var(--tz-surface-sunken); }
.tz-queue__n {
  font-family: var(--tz-font-display); font-size: var(--tz-text-kv); font-weight: 600;
  color: var(--tz-text-faint); text-align: right; font-variant-numeric: tabular-nums;
}
.tz-queue__what { min-width: 0; }
.tz-queue__who {
  display: block; font-weight: 600; color: var(--tz-heading);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tz-queue__sub {
  display: block; font-size: var(--tz-text-sm); color: var(--tz-text-muted); margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tz-queue__cnt { font-size: var(--tz-text-sm); color: var(--tz-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.tz-queue__pager {
  display: flex; justify-content: space-between; align-items: center; gap: 14px;
  padding-top: 14px; font-size: var(--tz-text-sm); color: var(--tz-text-muted);
}
.tz-queue__pagerbtns { display: flex; gap: 8px; }
/* The admin triage row: id chip + the classifier's sentence, then the glance —
   a label column and one column per shown record, disagreeing values dotted on
   the warning tint, exactly the case screen's marking language. */
/* The triage row: id line left, count+band right, and the glance as a second
   full-width band under both — desktop and phone alike (Nacho, 2 September 2026). */
.tz-queue__row--triage {
  /* Same air as the admin's row above; only the columns and the baseline differ. */
  grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; padding: var(--tz-sp-6) 4px;
}
.tz-queue__meta { display: inline-flex; align-items: baseline; gap: 14px; white-space: nowrap; }
.tz-queue__row--triage .tz-qprev { grid-column: 1 / -1; }
.tz-queue__idline { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.tz-queue__why {
  font-size: var(--tz-text-sm); color: var(--tz-text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* U-21: in this row the key is the identity and this is commentary, so the commentary
     gives way first. Before .tz-id could shrink at all it was flexbox that enforced the
     order, by accident — an unshrinkable item forces the whole deficit onto its sibling.
     Now that the key CAN clip, the order has to be stated, or the two shrink in proportion
     and a key that had room to be read whole is clipped alongside a sentence that had not
     started to give. The factor only has to outweigh 1; it is not a width. */
  flex-shrink: 100;
}
.tz-qprev { display: grid; gap: 3px; margin-top: 6px; min-width: 0; }
.tz-qprev__r {
  display: grid; grid-template-columns: 110px repeat(3, minmax(0, 1fr));
  gap: 10px; align-items: baseline; font-size: var(--tz-text-xs);
}
.tz-qprev__k {
  font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--tz-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 10px;
}
.tz-qprev__v {
  color: var(--tz-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The similarity tints: the row pill's own colours, applied per field. Amber is earned
   here — it marks real drift, not mere difference (the binary is-diff marking painted a
   "nearly identical" row amber and read as an alarm; retired 3 September 2026). */
.tz-qprev__v.is-hi { background: var(--tz-success-bg); color: var(--tz-heading); border-radius: 4px; padding: 0 4px; }
.tz-qprev__v.is-mid { background: var(--tz-info-bg); color: var(--tz-heading); border-radius: 4px; padding: 0 4px; }
.tz-qprev__v.is-lo { background: var(--tz-warning-bg); color: var(--tz-heading); border-radius: 4px; padding: 0 4px; }
/* The quiet legend for the tints, under the triage filters: micro-label plus the three
   swatches in the pill's own colours, one line, reading-grey. */
.tz-qprev-legend {
  display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap;
  margin: 10px 0 2px; font-size: var(--tz-text-xs); color: var(--tz-text-muted);
}
.tz-qprev-legend__label {
  font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
.tz-qprev-legend__sw { border-radius: 4px; padding: 0 6px; color: var(--tz-heading); }
.tz-qprev-legend__sw--hi { background: var(--tz-success-bg); }
.tz-qprev-legend__sw--mid { background: var(--tz-info-bg); }
.tz-qprev-legend__sw--lo { background: var(--tz-warning-bg); }
/* "Not compared" is orthogonal to the graded three: profile only, no content colour. */
.tz-qprev-legend__sw--none {
  background: none; box-shadow: inset 0 0 0 1px var(--tz-border); color: var(--tz-text-muted);
}
/* Stacked at the hero's foot, one entry per line behind the same rule as the figures. */
.tz-qprev-legend--stacked {
  flex-direction: column; align-items: flex-start; gap: 6px;
  margin: 22px 0 0; padding-top: 16px; border-top: 1px solid var(--tz-border-subtle);
}
.tz-qprev-legend--stacked .tz-qprev-legend__label { margin-bottom: 2px; }
/* ⚠️ In the context bar it gives up its own top rule and its own air: the column already rules
   and spaces every child of its own, and `--stacked` was carrying both for a place it no longer
   lives in. Two hairlines a few pixels apart is what keeping them looked like. */
.tz-qprev-legend--ctx { margin-top: 0; padding-top: 0; border-top: 0; }

/* The rest of the record hides behind the row's toggle — a class, not [hidden]: the
   row's display:grid would defeat the attribute. */
.tz-qprev__r--more { display: none; }
.tz-qprev.is-open .tz-qprev__r--more { display: grid; }
.tz-qprev__more { margin-top: 4px; font-size: 10px; justify-self: start; }

@media (max-width: 767.98px) {
  /* The row stacks: id line, then count and band right below it, then the glance. */
  .tz-queue__row, .tz-queue__row--triage { grid-template-columns: minmax(0, 1fr); gap: 6px; }
  .tz-queue__cnt { text-align: left; }
  .tz-qprev__r { grid-template-columns: 82px repeat(3, minmax(0, 1fr)); gap: 7px; }
}

/* ============ The simple duplicate case (design approved 2 September 2026) ============
   Record cards + two-mode marking: differences dotted on the warning tint, coincidences on the
   success tint — never both at once; data-mode on the card grid decides. The decision is
   three chewed tiles (an allowed box: options in a collection the user picks from). */
/* ❗️The kicker that used to open this row is retired (27 September 2026), so the case identifier
   is the row's own left-hand item — a standalone id, which is exactly the weight `.tz-id` carries.
   The rule that protected its letter-case inside an uppercasing parent went with the parent. */
/* ❗️The quiet line above a wizard title: which step of two you are on, and the case identifier.
   It is deliberately NOT a smallcap kicker — detail register, muted, no uppercase — so that it
   cannot be read, or re-used, as the eyebrow this round retired. Five nodes: the duplicate case's
   three confirmation screens and the expert dossier's head, whose copy JavaScript rewrites. */
.tz-stepline { margin: 0 0 6px; font-size: var(--tz-text-sm); color: var(--tz-text-muted); }
/* ⚠️ `.tz-case__eyerow` and `.tz-case__eyeactions` were deleted on 29 September 2026 with the row
   they laid out: the case's id and its two doors moved into the context column, which stacks them
   without being asked. The phone rule that went with them was earned — at 390px the id, "Case
   queue Esc" and "Expert mode" shared one line and scrolled the page 165px sideways with the
   Expert button off-screen — and the column is where that is answered now, because below 992px it
   renders in flow at full width and each child is its own block. */
.tz-case__title {
  font-family: var(--tz-font-display); font-size: var(--tz-text-h1); font-weight: 600;
  letter-spacing: -.015em; line-height: 1.15; color: var(--tz-heading); margin: 10px 0 0;
}
.tz-case__legend { font-size: 16px; color: var(--tz-text-muted); margin: 10px 0 0; max-width: 70ch; }
/* ⚠️ In the column it takes the column's measure, not the 70ch written for a full-width page, and
   the sentence's own size rather than 16px — the head's subtitle above it is `--tz-text-sm`, and
   two paragraphs of different sizes stacked under one rule read as two different kinds of thing. */
.tz-ctx .tz-case__legend { max-width: none; font-size: var(--tz-text-sm); line-height: 1.55; }
/* The question now heads the content, where the page used to open with the cards. */
.tz-case__title--ask { margin-top: 0; }
/* ⚠️ The group's chip needs air against the title, and the two duplicate screens hold it in two
   different places — so it takes two rules and neither covers the other (Nacho, 30 Sep 2026).
   On the expert dossier the chip is INSIDE the `<h1>`, after the `·`, and 6px of word-space put a
   mono pill flush against a display-size word. On the simple case it is the element AFTER the
   `<h1>`, and measured it sat at exactly 0px below it. */
.tz-ctx .tz-page-head__title .tz-id { margin-left: 4px; }
.tz-ctx .tz-page-head__title + .tz-id { display: inline-block; margin: 8px 0; }
/* The finding and the key to the marks, one under the other. The column rules the block above
   them; between the two paragraphs a line's worth of air is the whole separation they need. */
.tz-case__verdict .tz-case__legend + .tz-case__legend { margin-top: 10px; }
/* One origin per line, so the merged column's extra lines ARE the accumulation. */
.tz-prov-line { display: block; }
.tz-prov-line + .tz-prov-line { margin-top: 2px; }
/* The two metadata rows open the preview and are not dataset columns; a hairline under the pair
   says where the record's own model starts. */
.tz-xtable__meta + :not(.tz-xtable__meta) > td { border-top: 1px solid var(--tz-border); }
.tz-case__chip { border-radius: 4px; padding: 0 4px; color: var(--tz-heading); }
.tz-case__chip--diff { background: var(--tz-warning-bg); }
.tz-case__chip--same { background: var(--tz-success-bg); }
.tz-case__viewrow { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 18px; }
.tz-case__modes {
  display: inline-flex; border: 1px solid var(--tz-border); border-radius: var(--tz-r-full);
  overflow: hidden;
}
.tz-case__fieldsbtn {
  border: 1px solid var(--tz-border); border-radius: var(--tz-r-full); background: var(--tz-surface);
  padding: 7px 16px; font: 500 var(--tz-text-sm) var(--tz-font-sans); color: var(--tz-text-muted);
  cursor: pointer;
}
.tz-case__fieldsbtn.is-on { background: var(--tz-primary); border-color: var(--tz-primary); color: var(--tz-white); }
/* ⚠️ IN THE CONTEXT COLUMN THE PAIR STACKS (Nacho, 29 September 2026). A segmented control is a
   row of equal halves, and the two halves are sentences — "See what differs" / "See what
   coincides" — so at the column's 300px each of them broke into two lines inside its own pill and
   the control read as four buttons. Stacked they are two full-width rows with the hairline
   between them, which still says "one of these two" and is what the rest of the column does with
   anything that does not fit across.
   ⚠️ The pill radius has to be squared on the inside, or the divider meets two curves. */
.tz-ctx .tz-case__viewrow { flex-direction: column; align-items: stretch; gap: 10px; margin-top: 8px; }
.tz-ctx .tz-case__modes { flex-direction: column; border-radius: var(--tz-r-sm); }
.tz-ctx .tz-case__modes button { padding: 8px 14px; text-align: left; }
.tz-ctx .tz-case__modes button + button { border-top: 1px solid var(--tz-border); }
.tz-ctx .tz-case__fieldsbtn { border-radius: var(--tz-r-sm); padding: 8px 14px; text-align: left; }
[data-bs-theme="dark"] .tz-case__fieldsbtn.is-on { background: var(--tz-accent); border-color: var(--tz-accent); color: var(--tz-primary-dark); }
/* Fields empty in every record hide until the toggle asks for the whole record. */
.tz-case__f.is-absent { display: none; }
.tz-case__cards[data-fields="all"] .tz-case__f.is-absent { display: grid; }
.tz-case__modes button {
  border: 0; background: var(--tz-surface); padding: 7px 16px;
  font: 500 var(--tz-text-sm) var(--tz-font-sans); color: var(--tz-text-muted); cursor: pointer;
}
.tz-case__modes button.is-on { background: var(--tz-primary); color: var(--tz-white); }
[data-bs-theme="dark"] .tz-case__modes button.is-on { background: var(--tz-accent); color: var(--tz-primary-dark); }
.tz-case__cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 26px; }
.tz-case__card {
  border: 1px solid var(--tz-border); border-radius: var(--tz-r-lg);
  background: var(--tz-surface); padding: 16px 18px;
}
/* A member outside the principal's data scope (mixed group): read, not acted on. Muted, never
   hidden — the group is shown whole by design, and the lock in its title says why it is dim. */
.tz-case__card.is-out-of-scope { opacity: .62; border-style: dashed; }
.tz-case__src {
  font-family: var(--tz-font-display); font-size: 16px; font-weight: 600;
  color: var(--tz-heading); line-height: 1.3; margin-bottom: 8px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tz-case__name {
  font-family: var(--tz-font-display); font-size: 18px; font-weight: 600;
  color: var(--tz-heading); margin: 6px 0 12px; line-height: 1.3; display: inline-block;
}
.tz-case__f { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 8px; padding: 4px 0; font-size: var(--tz-text-sm); align-items: baseline; }
.tz-case__k {
  font-size: 10px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
  color: var(--tz-text-muted); line-height: 1.4;
}
.tz-case__v { color: var(--tz-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The marking, settled 3 September 2026 after two studio rounds: the first design's
   highlighter tint, kept as it was — minus the dotted underline (a second voice saying the
   same thing) and with a minimal rounding. No fade, no weight change: option E's focus by
   subtraction was tried live and dropped the same day. */
.tz-case__cards[data-mode="diff"] .tz-case__v.is-diff,
.tz-case__cards[data-mode="diff"] .tz-case__name.is-diff {
  background: var(--tz-warning-bg); color: var(--tz-heading);
  border-radius: 4px; padding: 0 4px; margin: 0 -4px;
}
.tz-case__cards[data-mode="same"] .tz-case__v.is-same,
.tz-case__cards[data-mode="same"] .tz-case__name.is-same {
  background: var(--tz-success-bg); color: var(--tz-heading);
  border-radius: 4px; padding: 0 4px; margin: 0 -4px;
}
.tz-case__decide { margin-top: 34px; }
/* The three verdicts, in the editorial register (settled 3 September 2026): no boxes, no
   filled circles — three typographic columns split by hairlines, the glyph in its colour,
   the verb answering the hover. The first design was cards with coloured badges; "fairground"
   was the review that retired it. */
.tz-case__tiles { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 12px; }
.tz-case__tile {
  border: 0; border-left: 1px solid var(--tz-border); background: none;
  padding: 10px 22px; text-align: left; cursor: pointer; font-family: var(--tz-font-sans);
  border-radius: var(--tz-r-md); transition: background-color .12s ease;
  /* A button centres its content vertically, so a tile whose sentence wraps to a third line
     started higher than its neighbours. Stacked from the top, the three icons sit on one line. */
  display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start;
}
/* The first tile keeps its text on the page's left edge: the hover ground reaches out past it
   by the same amount, so the three tiles light up as equals. */
.tz-case__tile:first-child { border-left: 0; padding-left: 14px; margin-left: -14px; }
/* ❗️THE WHOLE TILE LIGHTS UP, not only its verb: the same ground every row and pill in the
   product rests on under the pointer (the style guide's sunken surface), so a decision area
   reads as one pressable thing (Nacho, 17 September 2026). The verb still takes the link colour. */
.tz-case__tile:hover { background: var(--tz-surface-sunken); }
.tz-case__tile:hover .tz-case__verb { color: var(--tz-accent-blue); }
[data-bs-theme="dark"] .tz-case__tile:hover .tz-case__verb { color: var(--tz-accent); }
.tz-case__tile:focus-visible { outline: 2px solid var(--tz-accent-blue); outline-offset: 2px; border-radius: 4px; }
.tz-case__ico { display: inline-block; font-size: 15px; font-weight: 600; }
.tz-case__ico--ok { color: var(--tz-success-ink); }
.tz-case__ico--no { color: var(--tz-accent-blue); }
.tz-case__ico--hm { color: var(--tz-warning-ink); }
.tz-case__verb {
  display: block; font-family: var(--tz-font-display); font-size: 19px; font-weight: 600;
  color: var(--tz-heading); margin-top: 6px; transition: color .12s ease;
}
.tz-case__cons { display: block; font-size: var(--tz-text-sm); color: var(--tz-text-muted); margin-top: 5px; line-height: 1.45; }
.tz-case__skip { margin-top: 18px; font-size: var(--tz-text-sm); color: var(--tz-text-muted); display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.tz-case__sum { max-width: 640px; }
.tz-case__facts { border-top: 2px solid var(--tz-border); margin-top: 26px; }
/* The merged proposal's card (I2): the case card's own field grammar, one card, the
   fate of every value inside it — green stays, struck small type falls, amber was
   transformed by the merger. */
.tz-merged { border: 1px solid var(--tz-border); border-radius: var(--tz-r-lg); background: var(--tz-surface); margin-top: 22px; max-width: 680px; }
.tz-merged__head { padding: 14px 18px 10px; border-bottom: 1px solid var(--tz-border-subtle); }
.tz-merged__tag {
  font-size: 10px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase;
  color: var(--tz-success-ink);
}
.tz-merged__title {
  font-family: var(--tz-font-display); font-size: 18px; font-weight: 600;
  color: var(--tz-heading); margin-top: 3px; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tz-merged__body { padding: 8px 18px 14px; }
.tz-merged__f {
  display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 8px; padding: 7px 0;
  font-size: var(--tz-text-sm); align-items: baseline; border-bottom: 1px solid var(--tz-border-subtle);
}
.tz-merged__f:last-child { border-bottom: 0; }
.tz-merged__keeps { display: flex; flex-direction: column; gap: 2px; align-items: flex-start; }
.tz-merged__keepline { display: inline-flex; align-items: baseline; gap: 8px; min-width: 0; max-width: 100%; }
.tz-merged__keep {
  color: var(--tz-heading); background: var(--tz-success-bg); border-radius: 4px; padding: 0 5px;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tz-merged__keep.is-transformed { background: var(--tz-warning-bg); }
.tz-merged__note { font-size: 11px; color: var(--tz-text-muted); }
.tz-merged__drops { display: flex; flex-wrap: wrap; gap: 2px 12px; margin-top: 3px; font-size: var(--tz-text-xs); }
.tz-merged__dropline { display: inline-flex; align-items: baseline; gap: 5px; min-width: 0; }
.tz-merged__drop {
  color: var(--tz-text-muted); text-decoration: line-through; text-decoration-thickness: 1px;
  max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tz-merged__x { font-size: 11px; color: var(--tz-text-muted); font-variant-numeric: tabular-nums; }

.tz-case__commentlabel { margin: 26px 0 6px; }
.tz-case__pending { font-size: var(--tz-text-xs); color: var(--tz-text-muted); margin-top: 14px; }
@media (max-width: 991.98px) {
  .tz-case__cards { grid-template-columns: repeat(2, 1fr); }
  .tz-case__tiles { grid-template-columns: minmax(0, 1fr); }
  .tz-case__tile { border-left: 0; border-top: 1px solid var(--tz-border); padding: 14px 0; }
  .tz-case__tile:first-child { border-top: 0; padding-top: 0; }
}
@media (max-width: 767.98px) {
  .tz-case__cards { grid-template-columns: minmax(0, 1fr); }
}

/* The group's hero figures: records, similarity, deviation, classification. */
.tz-xsect-title {
  font-family: var(--tz-font-display); font-size: 18px; font-weight: 600; color: var(--tz-heading);
  margin: 22px 0 0; border-top: 2px solid var(--tz-border); padding-top: 14px;
}
.tz-xhero { display: flex; gap: 40px; margin: 12px 0 4px; padding: 0 0 12px; border-bottom: 1px solid var(--tz-border-subtle); flex-wrap: wrap; }
.tz-xhero__item { display: flex; flex-direction: column; gap: 2px; }
.tz-xhero__v {
  font-family: var(--tz-font-display); font-size: 22px; font-weight: 600;
  color: var(--tz-heading); font-variant-numeric: tabular-nums lining-nums; line-height: 1.2;
}
.tz-xhero__l {
  font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  color: var(--tz-text-muted);
}
.tz-xmaster__label {
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--tz-text-muted); margin-top: 22px;
}
.tz-xmaster { display: flex; gap: 16px; margin-top: 12px; flex-wrap: wrap; }
/* The master reuses the corporate selectable card (tz-dupcard); only sizing and the
   selected ring are this page's. */
.tz-xcard { min-width: 230px; max-width: 320px; flex: 0 1 auto; }
.tz-xcard.is-active { box-shadow: 0 0 0 2px var(--tz-primary); }
[data-bs-theme="dark"] .tz-xcard.is-active { box-shadow: 0 0 0 2px var(--tz-accent); }
.tz-xcard .tz-dupcard__foot { margin-top: 10px; }

.tz-xdetail { margin-top: 44px; border-top: 2px solid var(--tz-border); padding-top: 14px; }
.tz-xdetail__bar { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
/* All/None only make sense while hand-picking. */
.tz-xdetail__seltools { display: none; gap: 12px; }
body.tz-xmanual-on .tz-xdetail__seltools { display: inline-flex; }
.tz-xdetail__title { font-family: var(--tz-font-display); font-size: 18px; font-weight: 600; color: var(--tz-heading); }
.tz-xdetail__legend { font-size: var(--tz-text-xs); color: var(--tz-text-muted); }
.tz-xmanual {
  margin-left: auto; display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--tz-text-muted);
}
.tz-xmanual input { accent-color: var(--tz-primary); }
.tz-xdetail__wrap { margin-top: 12px; }
/* The evidence is wider than the paper: it scrolls inside its own container. */
.tz-xconfirm .tz-ledger-table-wrap, .tz-xdetail__wrap { overflow-x: auto; }
/* Both sticky rails need a fixed key width so the second one knows its offset. */
.tz-xtable__keyhead, .tz-xtable__key {
  position: sticky; left: 0; background: var(--tz-surface); z-index: 2;
  width: 150px; min-width: 150px; max-width: 150px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The proposal column: pinned right after the field names, the green rule on its far
   edge separating it from the scrolling sources.

   ❗️It used to sit on --tz-surface, the same paper as every source column, with that 2px rule
   as its only mark — and a rule between two columns reads as a table border, not as "this one
   is the answer". The column that the whole screen exists to show was the one thing on it
   nothing pointed at. It carries the success tint now, and its values are set in the heading
   colour at 600: the fill is the highlight, the rule is what separates it from what scrolls. */
.tz-xmergecol {
  position: sticky; left: 150px; background: var(--tz-success-bg); z-index: 1;
  border-right: 2px solid var(--tz-success); max-width: 240px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
td.tz-xmergecol { color: var(--tz-heading); font-weight: 600; }
/* Waiting for core to say what a partial pick would merge into. Quiet on purpose: an ellipsis in
   the heading weight would read as a value. */
td.tz-xmergecol.is-pending { color: var(--tz-text-muted); font-weight: 400; }
/* The row hover below paints every cell; a sticky column that loses its fill on hover flickers
   between "the answer" and "a source", so both sticky rails hold their own ground. */
#merge-table tbody tr:hover td.tz-xmergecol { background: var(--tz-success-bg); }
#merge-table tbody tr:hover td.tz-xtable__key { background: var(--tz-surface); }
th.tz-xmergecol .tz-pill { text-transform: none; letter-spacing: 0; }

/* ---- The confirmation table's fate marking ----
   Every confirmation screen shows the WHOLE group and says, on each column, what this verb
   does to it: the subject in the verb's own colour, what the verb spares dimmed and labelled.
   The colour is never the only channel — the word under the name says the same thing. */
.tz-xconf__name { display: block; overflow: hidden; text-overflow: ellipsis; }
.tz-xconf__fate {
  display: block; margin-top: 2px; font-family: var(--tz-font-sans);
  font-size: 10px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase;
  color: var(--tz-text-muted); min-height: 13px;
}
.tz-xconf__fate:empty { display: none; }
/* The subject: a 2px cap in the verb's colour across the top of the header, and the fate word
   in the same colour. No fill — the merge proposal is the only filled column on this screen and
   it has to stay the one thing the eye lands on. */
/* The cap is reserved on EVERY header of this table, transparent, and only coloured on the
   subject: given to the marked columns alone it would push their names 2px down the page and
   the header row would no longer read as one line. */
#merge-table thead th { border-top: 2px solid transparent; }
#merge-table th.is-subject { border-top-color: var(--tz-border-strong); }
#merge-table .is-spared { opacity: .45; }
/* ❗️On a merge the green frame goes AROUND THE RECORD THAT WILL REMAIN, not over the sources.
   The success cap used to sit on the source columns, which together with the proposal's own
   right rule drew an L around the sources and left the proposal — the one thing the screen
   exists to show — with a fill and no frame (Nacho, 17 September 2026). The sources keep their
   fate word in green and no cap; the proposal gets the cap, a left rule to match its right one,
   and a bottom rule on the last row, so the four sides close. */
#merge-table.tz-xconf--merge th.is-subject { border-top-color: transparent; }
#merge-table.tz-xconf--merge th.is-subject .tz-xconf__fate { color: var(--tz-success-ink); }
#merge-table.tz-xconf--merge th.tz-xmergecol { border-top-color: var(--tz-success); }
#merge-table.tz-xconf--merge .tz-xmergecol { border-left: 2px solid var(--tz-success); }
#merge-table.tz-xconf--merge tbody tr:last-child td.tz-xmergecol { border-bottom: 2px solid var(--tz-success); }
/* Separate and delete mark their subjects by the fate word (and, for delete, the cell tint);
   the coloured cap on the header went with the merge's (Nacho, 17 September 2026). */
#merge-table.tz-xconf--sep th.is-subject { border-top-color: transparent; }
#merge-table.tz-xconf--sep th.is-subject .tz-xconf__fate { color: var(--tz-accent-blue); }
#merge-table.tz-xconf--del th.is-subject { border-top-color: transparent; }
#merge-table.tz-xconf--del th.is-subject .tz-xconf__fate { color: var(--tz-error-ink); }
#merge-table.tz-xconf--del td.is-subject { background: var(--tz-error-bg); }
#merge-table.tz-xconf--del tbody tr:hover td.is-subject { background: var(--tz-error-bg); }
/* Rows answer the pointer, sticky rails included. */
.tz-xtable tbody tr:hover td { background: var(--tz-surface-sunken); }
.tz-xtable__cb {
  display: none; accent-color: var(--tz-primary);
  width: 18px; height: 18px; margin: 0 auto 6px;
}
body.tz-xmanual-on .tz-xtable__cb { display: block; }
body.tz-xmanual-on th.tz-xtable__rechead { cursor: pointer; text-align: center; }
.tz-xtable__grip { color: var(--tz-border); margin-right: 5px; cursor: grab; }
th.tz-xtable__rechead:hover .tz-xtable__grip { color: var(--tz-text-muted); }
body.tz-xmanual-on .tz-xtable__grip { display: none; }
.tz-xtable .is-dim { opacity: .38; }
.tz-xtable__reclink { color: inherit; text-decoration: none; }
.tz-xtable__reclink:hover { color: var(--tz-accent-blue); }
.tz-xacts { margin-top: 16px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tz-xacts__label {
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--tz-text-muted); margin-right: 8px;
}

.tz-xconfirm { margin-top: 8px; }
.tz-xconfirm__title { font-family: var(--tz-font-display); font-size: 24px; font-weight: 600; color: var(--tz-heading); margin: 12px 0 0; }
.tz-xconfirm__lead { font-size: var(--tz-text-base); color: var(--tz-text-muted); margin-top: 6px; max-width: 88ch; }
.tz-xconfirm__bar { margin-top: 18px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.tz-xconfirm__commentlabel { display: block; margin: 24px 0 6px; }
.tz-xconfirm__comment { max-width: 640px; }
.tz-xtable__res { max-width: 260px; }
.tz-xsplit__k {
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; color: var(--tz-text-muted);
}
.tz-xslot { margin-top: 14px; }


/* The dossier body: paper and rules, no card. */
.tz-dossier { margin-top: 10px; border-top: 2px solid var(--tz-border); padding-top: 16px; }
.tz-dossier__evidence { margin-top: 22px; }

/* The evidence table, editorial: uppercase field keys, hairline rows, serif record names,
   the merge-candidate column behind the green rule. */
.tz-cmp { border-collapse: collapse; width: 100%; font-size: var(--tz-text-sm); }
.tz-cmp thead th {
  text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--tz-border);
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; color: var(--tz-text-muted); white-space: nowrap;
}
.tz-cmp thead th.tz-cmp__rec {
  font-family: var(--tz-font-display); font-size: 13.5px; font-weight: 600;
  letter-spacing: 0; text-transform: none; color: var(--tz-heading);
  max-width: 220px; overflow: hidden; text-overflow: ellipsis;
}
.tz-cmp td { padding: 6px 12px; border-bottom: 1px solid var(--tz-border-subtle); vertical-align: top; }
.tz-cmp tbody tr.data-row:hover td { background: var(--tz-surface-sunken); }
.tz-cmp td.table-subtle-attribute {
  font-size: 10px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
  color: var(--tz-text-muted); white-space: nowrap;
}
.tz-cmp th.merge, .tz-cmp td.merge {
  border-left: 2px solid var(--tz-success);
}
.tz-cmp thead th.merge { color: var(--tz-success-ink); }
.tz-cmp tr.metadata td { font-size: var(--tz-text-xs); color: var(--tz-text-muted); }

/* The classifier's per-field band, painted on the evidence values — the queue's tints. */
.tz-sim { border-radius: 4px; padding: 0 4px; }
.tz-sim--hi { background: var(--tz-success-bg); color: var(--tz-heading); }
.tz-sim--mid { background: var(--tz-info-bg); color: var(--tz-heading); }
.tz-sim--lo { background: var(--tz-warning-bg); color: var(--tz-heading); }


@media (max-width: 991.98px) {
}

/* The cluster ledger (model E): the records table's passage style plus a
   selectable-row contract — pointer rows, always-visible checkboxes, the
   selected tint on .is-active (the class the selection JS already flips). */
.tz-clusters tbody tr { cursor: pointer; }
.tz-clusters tbody tr.is-active > td { background: var(--tz-surface-sunken); }
.tz-clusters__checkcol { width: 34px; }
.tz-clusters__checkcol .form-check-input { cursor: pointer; margin: 0; }
.tz-clusters__name a { font-weight: 600; color: var(--tz-heading); text-decoration: none; }
.tz-clusters__name a:hover { color: var(--tz-accent-blue); }
.tz-num { text-align: right; font-variant-numeric: tabular-nums; }
.tz-clusters th.tz-num { text-align: right; }
/* ---- Duplicates: selectable cluster/bucket cards ---- */
/* The hover/selected tint used to be written from JS as an inline
   `#f1f1f1` / `white` background. JS now toggles .is-active only, so the
   colors stay in the token layer and the checkbox reveal is pure CSS. */
.tz-dupcard {
  position: relative; cursor: pointer;
  transition: background-color .15s ease, box-shadow .15s ease;
}
.tz-dupcard:hover { box-shadow: var(--tz-shadow-sm); }
.tz-dupcard.is-active { background-color: var(--tz-surface-sunken); }
.tz-dupcard__check {
  position: absolute; top: var(--tz-sp-2); right: var(--tz-sp-2);
  display: none; z-index: 1;
}
.tz-dupcard:hover .tz-dupcard__check,
.tz-dupcard.is-active .tz-dupcard__check { display: block; }

/* Card interior, editorial (September 2026): the name in ink, the scope line as
   detail, and a closing line where the similarity is the figure — display serif,
   tabular — beside the one classification pill and the record count. The circled
   count and the progress meter retired with the pass; average/deviation live in
   the figure's tooltip. */
.tz-dupcard__name {
  display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-weight: 600; color: var(--tz-heading); text-decoration: none;
}
.tz-dupcard__name:hover { color: var(--tz-accent-blue); }
.tz-dupcard__sub { margin-top: 2px; font-size: var(--tz-text-sm); color: var(--tz-text-muted); }
.tz-dupcard__sub span + span::before { content: " · "; }
.tz-dupcard__foot { display: flex; align-items: baseline; gap: 10px; margin-top: 16px; }
.tz-dupcard__pct {
  font-family: var(--tz-font-display); font-size: 22px; font-weight: 600; line-height: 1;
  color: var(--tz-heading); font-variant-numeric: tabular-nums lining-nums;
}
.tz-dupcard__count { margin-left: auto; font-size: var(--tz-text-sm); color: var(--tz-text-muted); }

/* The quality page's coverage meter still uses the thin hairline bar. */
.tz-meter {
  flex: 1 1 auto; height: 6px;
  background: var(--tz-surface-sunken); border-radius: var(--tz-r-full);
}
.tz-meter .progress-bar { background-color: var(--tz-primary); border-radius: inherit; }

/* Shared by both duplicate grids (was an inline <style> block in each). */
.truncate-container { width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---- Duplicates: side-by-side record comparison ---- */
/* The merge-candidate column marked itself with Bootstrap's .bg-primary-subtle
   (literal #cfe2ff). It is an informational surface, so it takes the info tint. */
#comparison-table .merge { background-color: var(--tz-info-bg); }
/* Per-column similarity meters — thin, hairline-bounded, teal fill. */
#comparison-table .similarity .progress {
  height: 6px; min-width: 30px;
  background: var(--tz-surface-sunken);
  border: 1px solid var(--tz-border); border-radius: var(--tz-r-full);
}
#comparison-table .similarity .progress-bar {
  background-color: var(--tz-primary); border-radius: inherit;
}
/* Record-select checkboxes: the raw #ffc107 accent is the retired warning role;
   sized up from the form-check default because they are the row's only target. */
#comparison-table .record-select-cb { width: 18px; height: 18px; cursor: pointer; }

/* Task completion meter (task-show) — reuses the P4 meter look; the percentage
   sits beside the bar instead of inside it so the bar can be a hairline. */
.tz-taskmeter { display: flex; align-items: center; gap: var(--tz-sp-2); max-width: 260px; }
.tz-taskmeter .progress { flex: 1 1 auto; height: 6px; background: var(--tz-surface-sunken); border-radius: var(--tz-r-full); }
.tz-taskmeter .progress-bar { background-color: var(--tz-primary); border-radius: inherit; }
.tz-taskmeter__val { flex: 0 0 auto; font-size: var(--tz-text-sm); font-weight: 600; color: var(--tz-text-muted); }

.card-metric {
  font-family: var(--tz-font-display);
  font-size: clamp(16px, 2.5vw + 1.5rem, 56px);
  color: var(--tz-heading);
  font-weight: 600;
  font-variant-numeric: tabular-nums lining-nums;
  font-style: normal;
  line-height: 1em;
  white-space: nowrap;
}
/* The figure is a link, but it reads as a figure first: ink, and the link colour
   only on hover. */
.card-metric a { color: var(--tz-heading); text-decoration: none; }
.card-metric a:hover { color: var(--tz-accent-blue); }

.card-metric-subtitle {
  font-size: clamp(12px, 0.5vw + 0.5rem, 60px);
  color: var(--tz-heading);
  font-weight: 600;
  /* Index labels are long unbroken tokens — "NameApellido1Apellido2". The previous
     word-wrap: normal is the default, which does not break inside a word, so those
     overflowed the tile and printed on top of the neighbouring one. */
  overflow-wrap: anywhere;
  line-height: 1.3;
}

.table-subtle-attribute {
  color: var(--tz-text-muted);
  font-weight: 300;
}

.golden-tooltip .tooltip-inner {
  width: auto;
  font-weight: 300;
  white-space: normal;
  text-align: left;
}

.golden-tooltip .tooltip-inner ul,
.golden-tooltip .tooltip-inner ol {
    padding-left: 20px; /* Adjust this to control the left alignment */
    list-style-position: outside;
}

.golden-tooltip .tooltip-inner li {
    padding-left: 0;
    margin-left: 0;
}

.link-subtle a,
a.link-subtle {
  color: var(--tz-text);
  transition: 0.3s;
}

.link-subtle a:hover {
  color: var(--tz-accent-blue);
}

.text-pre {
  font-size: .875em;
  font-family: var(--bs-font-monospace);
}

/* Identifier shown as a code token (e.g. the record _id in a table view): it is
   no longer a link — the whole row navigates — so it just signals "this is an
   id" with a monospace, chip-like neutral surface (overrides Bootstrap's pink
   <code> color). */
.tz-id {
  font-family: var(--bs-font-monospace);
  font-size: .8125rem;
  color: var(--tz-text-muted);
  background: var(--tz-surface-sunken);
  border: 1px solid var(--tz-border);
  padding: .05rem .35rem;
  border-radius: var(--tz-r-sm);
  white-space: nowrap;
  /* U-21: an id is never wider than the box it is in. Most templates abbreviate to 9
     characters and carry the whole value in a `title`, so this costs them nothing — but the
     duplicates queue prints the case key whole, and this installation's key is a readable
     `by-name-postcode-<name>-<postcode>` string 373px long, not the short hex the template
     placeholder assumes. It pushed the queue 7px past a 390px viewport and 77px past a 320px
     one, sideways, on the steward's first screen.
     These four declarations are inert on an inline element — max-width and overflow do not
     apply to a non-replaced inline box — so they bite exactly where the id is a flex or grid
     item competing for a row's width, which is where the defect is. In the queue row that is
     beside `.tz-queue__why`, which already ellipsizes: one vocabulary for the row. */
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ❗️THE QUIET WEIGHT OF THE SAME IDENTIFIER (Nacho, 20 September 2026). A catalogue prints an
   identifier as a second line under a name, and a bordered chip on every row of a list is noise —
   so the chrome comes off and the monospace stays. It is a modifier of `.tz-id` and not a class of
   its own because there is one identifier vocabulary in this product, in two weights: the chip
   when an id stands alone as the subject of something (an eyebrow, a confirmation), this when it
   sits under the name it belongs to.

   ⚠️ It replaces NINE rules that each said the same thing scoped to one screen — entity rows,
   admin table cells, the resource selection bar, event rows, the table catalogue's dataset, the
   file catalogue's content type, the resource relation grid, the entity dossier, the settings
   confirmation. They had drifted into two colours and two sizes, roughly half each: `--tz-text-
   faint` against `--tz-text-muted`, 11px against 13px. Muted wins on the accessibility table —
   faint is n500, 3.91:1 on white, reserved for >=18px or non-text, and this is 11px of text. */
.tz-id--quiet {
  display: block; overflow: hidden; max-width: 34ch; margin-top: 4px; padding: 0;
  border: 0; border-radius: 0; background: none;
  color: var(--tz-text-muted); font-size: var(--tz-text-xs);
  text-overflow: ellipsis; white-space: nowrap;
}
/* An identifier that legitimately exceeds its column: a resource's, which is as long as whoever
   named it made it, and a content type. The chip's own `--wrap` says the same for the chip. */
.tz-id--free { max-width: none; white-space: normal; text-overflow: clip; overflow-wrap: anywhere; }
/* Beside a name rather than under it. */
.tz-id--inline { display: inline-block; margin-top: 0; max-width: 34ch; vertical-align: text-bottom; }

/* ❗️AN IDENTIFIER THAT HAS SOMEWHERE TO GO, VISIBLE ON HOVER (Nacho, 20 September 2026). At rest
   it is exactly the identifier it was — same colour, same size, no underline — because a catalogue
   where every row carries a blue link is a catalogue of links, not of names. The affordance
   arrives with the pointer and with the keyboard, which is what `:focus-visible` is for.

   ⚠️ It needs `data-tz-stop` in the markup wherever the row itself is clickable: the row selects
   and this navigates, and without the marker a click would do both. `golden-actions.js` resolves
   the innermost `[data-tz-click],[data-tz-stop]`, so the marker on the anchor is what keeps the
   row's handler from firing — and the anchor still follows its href, because nothing prevents it.

   It is also what gives a selectable row its middle click back. D1 bought the selection bar at
   the price of rows that are no longer links; the identifier is now the one thing in the row that
   opens in a new tab. */
a.tz-id, a.tz-id:visited { color: inherit; text-decoration: none; }
a.tz-id:hover, a.tz-id:focus-visible {
  color: var(--tz-accent-blue); text-decoration: underline; text-underline-offset: 2px;
}

/* ❗️THE KEYBOARD SHORTCUT, AS A PRODUCT CLASS (Nacho, 20 September 2026). It was written
   `badge bg-primary-subtle text-primary` in 31 places across 20 templates — three Bootstrap
   classes standing in for one idea, and the last `.badge` left anywhere in administration once
   the role chips became `.tz-pill`. Bootstrap's own `bg-primary-subtle` is not the product's
   info tint either: it had to be overridden by `!important` in the dark theme because
   `text-primary` there is the dark teal, illegible on the dark fill.

   One class instead, in the product's own tokens, legible in both themes without an override.
   It is not `.tz-pill` and not `.tz-tag`: a pill is a state and a tag is a value, and this is
   neither — it is a key on a keyboard, which is why it takes the monospace and a squarer
   corner than either of them. */
.tz-kbd {
  /* ⚠️ IT NEVER GIVES WAY, and `min-width` is not what holds that (Nacho, 29 September 2026).
     These chips sit inside `display: flex` buttons, where the label is text that will not shrink
     below its own measure — so the chip is the only flexible item in the group and absorbs the
     whole squeeze. On the duplicate detail's "Back to duplicates Esc", with the context column
     dragged to its 220px floor, it was pushed down to the 19px below and clipped its own word:
     the button needed 191px and had 191, so nothing was actually short of room. `min-width` is a
     floor for a ONE-LETTER chip, not a licence to crop a three-letter one.
     Measured across the nine screens that carry one, 14 chips: this cures the only clipped chip
     and makes nothing else overflow. */
  display: inline-block; flex: none; min-width: 19px; padding: 1px 5px;
  border: 1px solid var(--tz-info-bg); border-radius: var(--tz-r-sm);
  background: var(--tz-info-bg); color: var(--tz-info-fg);
  font-family: var(--tz-font-mono); font-size: var(--tz-text-xs); font-weight: 600;
  line-height: 1.5; text-align: center; white-space: nowrap;
}
/* On a filled button the chip sits on the brand fill, where the info tint has nothing to say. */
.btn-primary .tz-kbd, .btn-danger .tz-kbd {
  border-color: rgba(255, 255, 255, .45); background: rgba(255, 255, 255, .17); color: inherit;
}

/* Rows that navigate to a detail on click (table view records). Pairs with the
   universal row-hover tint to signal the whole row is a target. */
.tz-row-link {
  cursor: pointer;
}

/* Record identity is one subtitle line below the rendered title; its health and provenance are a
   row of badges below THAT. They were one flex row until 17 September 2026, and a long title
   wrapped the row's tail under the action group. */
.tz-record-head__context {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--tz-sp-2);
  margin-top: var(--tz-sp-2); color: var(--tz-text-muted); font-size: var(--tz-text-sm);
}
.tz-record-head__badges {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--tz-sp-2);
  margin-top: var(--tz-sp-2);
}
.tz-record-head__badges:empty { display: none; }

/* ❗️U-26. The two rules that used to sit here — `width: 100%` and `justify-content: flex-start`
   on this screen's action group — are gone with the markup they were written against: the group
   is inside `.tz-page-head__main` now and sits top-right on the title's line, like every other
   page's. They said the actions were "a structural row below the identity block, so changing
   modes cannot make them compete with the title for horizontal space"; the competition is real
   and it is answered in the shared rule (theme.css), which wraps the group onto its own line —
   left-aligned — exactly when the two do not fit, instead of spending a row on every render. */


/******************** THE RICH DROPDOWN (Choices), AND ace ********************/
/* ⚠️ ONE BLOCK, BOTH THEMES, AND THAT IS THE POINT.

   What used to be here was 57 rules chasing a third-party widget's markup from the outside, 25 of
   them a hand-written dark skin, because that library hard-coded #fff surfaces and
   #212529/#6c757d/#444/#999 ink into its own stylesheet: there was nowhere to put a token, so
   every colour had to be re-stated under [data-bs-theme="dark"] against a selector that would
   break the next time upstream renamed a class.

   Choices paints almost everything through --choices-* custom properties, and every --tz-* token
   below is already theme-aware. So the skin is a token map, it is written once, and dark mode
   costs nothing. Only the handful of things the library does NOT expose as a property — the two
   dropdown row states, which share one variable, and the focus ring — need a selector, and those
   are below the map.

   ace still needs its own dark rules, further down: it injects its theme CSS at runtime from a JS
   module, so no stylesheet of ours can reach it. json-editor needs none — it runs with the
   Bootstrap 5 theme, so its generated markup inherits the theme's remap. */

.choices {
  /* ---- surfaces and ink ---- */
  --choices-bg-color: var(--tz-surface);
  --choices-bg-color-dropdown: var(--tz-surface);
  --choices-bg-color-disabled: var(--bs-secondary-bg);
  --choices-disabled-color: var(--tz-surface-sunken);
  /* ⚠️ U-12: THE DISABLED CONTROL IS HOW THIS PRODUCT SHOWS A RECORD, so its ink is not
     "disabled" ink. `.choices.is-disabled .choices__item` is what paints the CHOSEN VALUE
     of a control that is switched off, and the record screen switches every control off
     until Edit is pressed — so a lookup value on the one screen whose purpose is reading a
     record was rendering in the disabled token at 2.39:1 light / 2.87:1 dark. The muted
     alias is what the read-only form-control beside it already uses (5.00:1 / 6.10:1).
     A genuinely unselectable OPTION is a different selector, `.choices__item--disabled`,
     and keeps the library's own opacity: .5 — this does not touch it. */
  --choices-item-disabled-color: var(--tz-text-muted);
  --choices-text-color: var(--tz-text);
  --choices-keyline-color: var(--bs-border-color);
  --choices-primary-color: var(--tz-primary);
  --choices-highlight-color: var(--tz-primary);
  --choices-item-color: var(--tz-white);

  /* ---- geometry: the one control height, 36px, the same number theme.css holds every field
          and every button to. It used to be Bootstrap's calc at the library's 16px, which gave 38
          empty and 44 with a value — the closed control is inline-block over an inline-block
          list, and the line box under them added six pixels nobody asked for. ---- */
  --choices-border-radius: var(--bs-border-radius);
  --choices-border-radius-item: var(--tz-r-full);
  --choices-input-height: 36px;
  --choices-inner-padding: 0 2.25rem 0 0.75rem;
  --choices-inner-one-padding: 0;
  --choices-list-single-padding: 0;
  --choices-arrow-right: 0.9rem;
  /* A Bootstrap field carries its own spacing; the library's own 24px gutter would double it. */
  --choices-guttering: 0;
  --choices-font-size-lg: 1rem;
  --choices-font-size-md: var(--tz-text-base);
  --choices-font-size-sm: var(--tz-text-sm);
  /* Above an open offcanvas (1045) and an open modal (1055). */
  --choices-z-index: 1057;

  width: 100%;
  /* Its search box and dropdown rows would otherwise fall back to the OS system-ui font instead
     of inheriting Inter. */
  font-family: var(--tz-font-sans);
}

/* The closed control, and the Bootstrap focus ring on it. The library derives its own focus
   border with color-mix(..., black), which is right in light and backwards in dark. */
.choices__inner {
  color: var(--tz-text);
  /* Flex, centred: the value sits in the middle of the 36px and the line box that used to grow
     the control by six pixels has nothing to grow. Multi-value controls still wrap and grow. */
  display: flex; align-items: center; flex-wrap: wrap;
}
.choices__list--single { display: block; }
/* ❗️The explanation of an option, inside the option (Nacho, 19 September 2026). It replaces
   `.tz-option-help`, a block of "Name: explanation" lines under the field — a legend for a control
   rather than a part of it. `choices__description` is the widget's own class; what it needs from
   us is the product's voice for a second line: smaller, muted, and not competing with the name
   above it. */
.choices__list--dropdown .choices__description,
.choices__list[aria-expanded] .choices__description {
  display: block; margin-top: 2px; font-size: var(--tz-text-sm); line-height: 1.4;
  color: var(--tz-text-muted); white-space: normal;
}
.choices__list--dropdown .choices__item--selectable.is-highlighted .choices__description {
  color: inherit; opacity: .85;
}
/* ❗️The name in semibold where it has an explanation under it (Nacho, 19 September 2026). With
   both lines at the same weight the eye reads a paragraph and has to find the name inside it; the
   weight is what makes the list scannable again.

   ⚠️ Only where there IS a description, which is what `:has()` says. A blanket rule would put
   every dropdown in the product into semibold — 77 schema pickers, the time zones, the dataset
   lists — and weight would stop meaning anything. The description keeps the body weight, because
   two emphases are none. */
.choices__list--dropdown .choices__item--choice:has(.choices__description) { font-weight: 600; }
.choices__list--dropdown .choices__description { font-weight: 400; }
/* ⚠️ Not in the closed control. The selected item draws from the same data, and a paragraph under
   the value turns a 36px field into a three-line block. */
.choices__list--single .choices__description { display: none; }

/* ❗️THE KEY OF AN OPTION, IN A SUBTLE PILL (Nacho, 20 September 2026). An option that has both a
   key and a description now draws them as two things and not as one string: the key in a quiet
   pill, the description under it. It was written `[sample-billing-raw] Aurelia Utilities - billing
   export, exactly as that system writes it` — one run of text, with the identifier fenced in
   square brackets, in the reading face, on six of this product's dropdowns. Square brackets are a
   convention a reader has to be taught; a pill is one they can see.

   ⚠️ No template override and no change to the vendored library: Choices reads `data-label-class`
   off the `<option>` and wraps the label in a span carrying it. So the shape is two attributes on
   the markup — `data-label-class` and the `data-label-description` this product already used —
   and the class below. `--id` is for a key that is a product identifier rather than a translated
   word, and it takes the identifier's monospace, because that rule holds wherever an id is drawn.

   ⚠️ It applies in the CLOSED control too, on purpose. The item template honours `labelClass` as
   well, so what the field shows once chosen is the pill and not the long sentence — the closed box
   used to carry the whole `[id] description` string and clip it. */
.choices .tz-choice-key {
  display: inline-block; padding: 1px 8px; border: 1px solid var(--tz-border);
  border-radius: var(--tz-r-full); background: var(--tz-surface-sunken);
  color: var(--tz-heading); font-size: var(--tz-text-xs); font-weight: 600;
  line-height: 1.6; white-space: nowrap;
}
.choices .tz-choice-key--id {
  max-width: 100%; overflow: hidden; font-family: var(--tz-font-mono); font-weight: 500;
  text-overflow: ellipsis; vertical-align: bottom;
}
/* The highlighted row inverts, so the pill has to stop being a light chip on a dark band. */
.choices__list--dropdown .choices__item--selectable.is-highlighted .tz-choice-key {
  border-color: currentColor; background: transparent; color: inherit;
}
/* ⚠️ The semibold that marks a name with an explanation under it is for TEXT. A pill carries its
   own weight, and inheriting 600 on top of it made the key heavier than anything else on screen. */
.choices__list--dropdown .choices__item--choice:has(.tz-choice-key) { font-weight: 400; }
/* ⚠️ THE SAME FOCUS AS EVERY OTHER FIELD, AND THIS RULE HAD BEEN LEFT BEHIND BY ONE.
   `theme.css:508` moved `.form-control:focus` / `.form-select:focus` to the accent blue with a
   3px quarter-alpha ring; this one still answered with `--bs-primary` (#024256) at 0.25rem, so a
   focused dropdown drew a DARKER and WIDER ring than the text field beside it — measured on
   root.gender: rgb(2,66,86) at 4px against rgb(0,128,170) at 3px (Nacho, 29 September 2026: "se
   queda sombreada, pero en gris, cuando los otros campos con foco usan un azulillo").
   The dark half below is not touched: it is already the accent, like theme.css's own dark half. */
.choices.is-focused .choices__inner,
.choices.is-open .choices__inner {
  border-color: var(--tz-accent-blue);
  box-shadow: 0 0 0 3px rgba(var(--tz-info-rgb), .25);
}
/* ⚠️ AND THE RING HAS TO BE ALLOWED OUT OF THE WRAPPER, or half of it is not the ring we drew.
   The library ships `.choices { overflow: hidden }` with one exception, `.choices.is-open
   { overflow: visible }` — so while the list is DOWN the halo renders round and correct, and the
   moment an option is picked the list closes, the clip comes back and the same 3px shadow is cut
   to the wrapper's square box. Colour right, shape wrong, and only after choosing: exactly what
   Nacho saw on 29 September 2026 ("una vez has seleccionado, el foco está mal ... el color bien
   pero el grosor mal"). Focused is the other state that draws outside the box, so it gets the
   same exception. Content is still clipped — by `.choices__inner`, which has its own. */
.choices.is-focused { overflow: visible; }
/* Dark (U-10): --bs-primary stays the brand teal in both themes, so this ring composited to
   1.07:1 against the dark page — the same defect the .btn / .form-control rings had in
   theme.css, and the same cure. Solid accent, 6.80:1 on n900. */
[data-bs-theme="dark"] .choices.is-focused .choices__inner,
[data-bs-theme="dark"] .choices.is-open .choices__inner {
  border-color: var(--tz-accent);
  box-shadow: 0 0 0 2px var(--tz-accent);
}
.choices__input { color: var(--tz-text); }

/* ⚠️ THE CLOSED CONTROL TRUNCATES, IT DOES NOT GROW. Choices ships no truncation at all, so a
   label longer than its column wraps and the control gets taller — and that matters here more
   than it would elsewhere: the resource forms lay their fields out on a 12-column grid, several
   at `grid_columns: 3`, and the option labels are "[identifier] description" strings a customer
   wrote. Seen on the bench: a three-column Protocol field turned one option into a column of
   single letters twenty lines tall. The widget this replaced clipped with an ellipsis, so this
   is restoring behaviour rather than inventing it. */
.choices__list--single .choices__item {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* The list itself may wrap — it is a list, it has the room — but at word boundaries where there
   are any. `break-all`, which the library sets, splits identifiers mid-word for no reason. */
.choices__list--dropdown,
.choices__list--dropdown .choices__item {
  word-break: normal;
  overflow-wrap: anywhere;
}
/* Muted ink rather than half-transparent ink: 50% of the body colour on a light ground fails
   contrast, and the placeholder is the only thing an empty required field says. */
.choices__placeholder { color: var(--tz-text-muted); opacity: 1; }

/* ⚠️ THE TWO ROW STATES SHARE ONE VARIABLE, so they are the part that still needs selectors.
   Choices paints `.is-selected` and `.is-highlighted` from --choices-highlighted-color alike; we
   want them apart, as before: the current value sits on a sunken ground, and the row under the
   cursor or the caret takes corporate teal. The teal rule comes SECOND on purpose — a row that is
   both must read teal, or the row you are about to choose is the one you cannot see. */
/* ⚠️ AND THE `[aria-expanded]` HALF OF EACH PAIR IS NOT DECORATION EITHER. The library writes
   every one of these rules twice, once for `.choices__list--dropdown` and once for
   `.choices__list[aria-expanded]`, and the dropdown element carries both — so its attribute
   variant is one selector heavier than the class one and outranks a rule that names only the
   class, however late it comes. Measured: with the class selector alone the row kept Choices'
   #f2f2f2 ground and took our white ink, which in dark mode is white on near-white. */
.choices__list--dropdown .choices__item,
.choices__list[aria-expanded] .choices__item { color: var(--tz-text); }
.choices__list--dropdown .choices__item--selectable.is-selected,
.choices__list[aria-expanded] .choices__item--selectable.is-selected {
  background-color: var(--tz-surface-sunken);
  color: var(--tz-text);
}
.choices__list--dropdown .choices__item--selectable.is-highlighted,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
  background-color: var(--tz-primary);
  color: var(--tz-white);
}
/* A second line inside the highlighted row is unreadable in its own muted ink on teal. */
.choices__list--dropdown .choices__item--selectable.is-highlighted .text-muted,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted .text-muted,
.choices__list--dropdown .choices__item--selectable.is-highlighted .choices__description,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted .choices__description {
  color: rgba(var(--tz-white-rgb), 0.85) !important;
}
/* An <optgroup> heading, which the tables screen uses to tell sources from tables apart. */
.choices__heading {
  color: var(--tz-text-muted);
  border-bottom-color: var(--bs-border-color);
}
/* "No results found" and friends. */
.choices__notice { color: var(--tz-text-muted); }

/* ⚠️ THE KEY, AS A PILL, AND WHY IT IS DRAWN HERE RATHER THAN COMPOSED IN JAVA.

   Every option in this product used to arrive as one string — "[ES] Español", "[src-crm] CRM
   extract" — concatenated on the server, in two different places, and therefore identical
   everywhere: the key could not be styled, could not be told apart from the description by anyone
   reading quickly, and could not be turned off for the enumerations where it says nothing. The
   server now sends the key and the description separately and the option template in
   golden-json-editor.js builds these two elements.

   The pill is deliberately quiet: a stored identifier is what you look for when you already know
   it, not what you read. Monospace because that is what makes `ds-customer-01` and
   `ds-customer-o1` different at a glance, and muted because the description is the sentence. */
.tz-option__key {
  display: inline-block;
  margin-right: var(--tz-sp-2);
  padding: 0 0.35em;
  border-radius: var(--tz-r-sm);
  background-color: var(--tz-surface-sunken);
  color: var(--tz-text-muted);
  font-family: var(--tz-font-mono);
  font-size: var(--tz-text-xs);
  /* The pill must not push the row taller than a row without one. */
  line-height: 1.5;
  vertical-align: baseline;
  white-space: nowrap;
}
/* On the highlighted row the muted ink is unreadable, exactly as for the description above. */
.choices__list--dropdown .choices__item--selectable.is-highlighted .tz-option__key,
.choices__list[aria-expanded] .choices__item--selectable.is-highlighted .tz-option__key {
  background-color: rgba(var(--tz-white-rgb), 0.2);
  color: rgba(var(--tz-white-rgb), 0.95);
}
/* The closed control truncates as one line (see above), so the label carries the ellipsis and the
   pill keeps its full width — a clipped identifier is worse than no identifier. */
.choices__list--single .choices__item .tz-option__label {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- inside a modal or an offcanvas ----
   The z-index dance the previous widget needed is gone with it: that library appended its list to
   <body> and then had to be lifted back over the dialog it belonged to, which took a rule for the
   modal and another for the offcanvas. Choices draws the list inside the control's own container,
   so it cannot land detached in the first place.

   What is left is the scroll. An .offcanvas-body scrolls, so a list opened near the bottom of a
   long panel would be clipped by it; bounding the list turns that into a scroll INSIDE the list,
   which is what anyone expects, instead of a list with its feet cut off. */
.offcanvas .choices__list--dropdown .choices__list,
.modal .choices__list--dropdown .choices__list {
  max-height: 240px;
}


/******************** TEST WIZARD (resource test modal) ********************/
/* The test is a small editorial workflow. The active state is communicated by type,
   rules and a checkmark; colour only reinforces it. */
#resource-test {
  --tzw-brand: var(--tz-primary);
}
[data-bs-theme="dark"] #resource-test {
  --tzw-brand: var(--tz-accent);
}
#resource-test .modal-dialog { max-width: min(1180px, calc(100vw - 36px)); }
#resource-test .modal-content {
  height: min(720px, calc(100vh - 48px)); max-height: calc(100vh - 48px); overflow: hidden;
}
#resource-test .modal-body { min-height: 0; padding: 30px; overflow-y: auto; }
.tz-resource-test__head {
  display: grid; grid-template-columns: minmax(0, 1fr) 32px; gap: 32px;
  align-items: center; padding: 22px 30px;
}
.tz-resource-test__head .btn-close {
  justify-self: end; margin: 0; padding: 8px;
}
.tz-resource-test__footer { justify-content: flex-start; padding: 13px 30px; }
.tz-resource-test__navigation { display: flex; gap: 8px; margin-left: auto; }
.tz-test-pane__lead {
  max-width: 72ch; margin: 6px 0 24px; color: var(--tz-text-muted);
  font-size: var(--tz-text-base); line-height: 1.55;
}
.tz-test-pane__title {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: 20px; font-weight: 600;
}
.tz-test-result__head {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px;
}
.tz-test-result { min-width: 0; overflow-x: auto; }
.tz-test-result > pre {
  margin: 0; padding: 16px 18px; border-top: 1px solid var(--tz-border);
  border-bottom: 1px solid var(--tz-border); background: var(--tz-surface-sunken);
  color: var(--tz-heading); font-family: var(--tz-font-mono); font-size: var(--tz-text-sm);
  line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere;
}
.tz-test-running {
  display: grid; min-height: 320px; place-items: center; align-content: center; gap: 13px;
  color: var(--tz-text-muted); font-size: var(--tz-text-base);
}

/* Result summary introduces the report rather than repeating underneath it. */
.tz-test-summary {
  display: flex; align-items: center; gap: 12px; margin: 12px 0 24px; padding-bottom: 13px;
  border-bottom: 1px solid var(--tz-border);
}
.tz-test-summary__meta {
  color: var(--tz-text-muted); font-size: var(--tz-text-sm); font-variant-numeric: tabular-nums;
}

/* ---- three-part editorial index ---- */
.tz-wizard-steps {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px;
  padding: 0 30px; border-bottom: 1px solid var(--tz-border-subtle);
}
.tz-wstep {
  display: grid; grid-template-columns: 29px minmax(0, 1fr); gap: 9px; align-items: center;
  min-height: 62px; margin-bottom: -1px; border-bottom: 3px solid transparent;
  color: var(--tz-text-muted); font-size: var(--tz-text-base);
}
.tz-wstep__dot {
  min-width: 24px; padding: 2px 5px; border: 1px solid var(--tz-border-subtle);
  border-radius: var(--tz-r-full); background: var(--tz-surface-sunken);
  color: var(--tz-text-muted); font-family: var(--tz-font-mono); font-size: 10px;
  font-variant-numeric: tabular-nums; text-align: center;
}
.tz-wstep__lbl > span { display: block; color: inherit; font-weight: 600; }
.tz-wstep__lbl small {
  display: block; margin-top: 1px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 400;
}
.tz-wstep.is-active { border-bottom-color: var(--tzw-brand); color: var(--tzw-brand); }
.tz-wstep.is-active .tz-wstep__dot {
  border-color: var(--tzw-brand); color: var(--tzw-brand); font-weight: 700;
}
.tz-wstep.is-done .tz-wstep__dot { color: var(--tzw-brand); }
.tz-wstep.is-skip { opacity: .5; }
.tz-wstep__sep { display: none; }

/* ---- step 1: the same editorial selector as the resource-type catalogue ---- */
.tz-typecards {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 28px;
}
#resource-test .tz-test-type.tz-resource-type { min-height: 108px; }
#resource-test .tz-test-type.is-selected .tz-resource-type__mark,
#resource-test .tz-test-type.is-selected > strong { border-color: var(--tzw-brand); color: var(--tzw-brand); }
#resource-test .tz-test-type.is-disabled {
  opacity: .45; cursor: not-allowed; filter: grayscale(1);
}
#resource-test .tz-test-type.is-disabled:hover { background: transparent; }

/* ---- step 2: the data-source picker ---- */
.tz-step2-field { max-width: 680px; margin-bottom: 24px; }
/* The label spec that used to be repeated here is `.form-label`'s own since U-27 (theme.css),
   and the wizard's markup already writes `.fw-semibold` for the weight — so a local copy of the
   micro-label, `!important` and all, is one of the four vocabularies rather than a need. */


/* ---- ace: the editor chrome ----
   The syntax palette comes from the ace theme JS (github_dark in dark, textmate in
   light — see tzAceTheme in main.js). Ace injects that CSS at runtime, i.e. after
   our stylesheets, so these rules win on specificity rather than order: an
   attribute plus two classes beats ace's single `.ace-github-dark`. Only the
   chrome is overridden, so the editor sits on our surface instead of GitHub's. */
[data-bs-theme="dark"] .ace_editor.ace-github-dark,
[data-bs-theme="dark"] .ace_editor.ace-github-dark .ace_gutter {
  background-color: var(--tz-bg);
  color: var(--tz-text);
}
[data-bs-theme="dark"] .ace_editor.ace-github-dark .ace_gutter {
  color: var(--tz-text-faint);
  border-right: 1px solid var(--tz-border);
}
[data-bs-theme="dark"] .ace_editor.ace-github-dark .ace_gutter-active-line {
  background-color: var(--tz-surface-sunken);
}
[data-bs-theme="dark"] .ace_editor.ace-github-dark .ace_marker-layer .ace_active-line {
  background-color: var(--tz-surface);
}
[data-bs-theme="dark"] .ace_editor.ace-github-dark .ace_cursor { color: var(--tz-accent); }

/******************** GOLDEN TEST STYLES ********************/
/* NOT dead code: <test> is a live custom element. resource-form.html renders the
   backend's connection-test output as HTML using these tags, and the page's JS
   addresses RECORD / STACK by tagName. The P4 record listed this block as unused
   — that was wrong. */

test heading {
    display: block; margin: 30px 0 12px; padding-bottom: 9px;
    border-bottom: 1px solid var(--tz-border); color: var(--tz-heading);
    font-family: var(--tz-font-display); font-size: 20px; font-weight: 600;
}
test heading:first-child { margin-top: 0; }

test code {
    color: var(--tz-heading); font-family: var(--tz-font-mono); font-size: var(--tz-text-sm);
}

test table {
    width: 100%; margin-bottom: 24px; border-collapse: collapse;
    border-top: 1px solid var(--tz-border-strong); border-bottom: 1px solid var(--tz-border);
    color: var(--tz-text); font-size: var(--tz-text-sm);
}

test table th {
    padding: 9px 12px; border-bottom: 1px solid var(--tz-border);
    background: transparent; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
    font-weight: 600; letter-spacing: .08em; text-align: left; text-transform: uppercase;
    vertical-align: bottom;
}

test table td {
    padding: 10px 12px; border-bottom: 1px solid var(--tz-border-subtle); vertical-align: top;
}
test table tbody tr:last-child td { border-bottom: 0; }
test table tbody tr:hover td { background: var(--tz-surface-sunken); }

test record,
test stack {
    display: -webkit-box; overflow: hidden; padding: 10px 12px;
    border-top: 1px solid var(--tz-border-subtle); border-bottom: 1px solid var(--tz-border-subtle);
    background: var(--tz-surface-sunken); color: var(--tz-heading);
    font-family: var(--tz-font-mono); font-size: var(--tz-text-sm); line-height: 1.5;
    overflow-wrap: anywhere; text-overflow: ellipsis; white-space: pre-wrap;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 5;
    cursor: pointer;
}

test error { color: var(--tz-error-ink); }
test warning { color: var(--tz-warning-ink); }

test badge,
test wbadge {
    display: inline-flex; align-items: center; margin-top: 5px; padding: 3px 8px;
    border: 1px solid var(--tz-border); border-radius: 999px; background: transparent;
    color: var(--tz-text-muted); font-size: 10px; font-weight: 600; line-height: 1.2;
    letter-spacing: .05em; text-transform: uppercase; vertical-align: baseline; white-space: nowrap;
}
test wbadge { border-color: rgba(var(--tz-warning-rgb), .45); color: var(--tz-warning-ink); }

@media (max-width: 767.98px) {
  #resource-test .modal-dialog { min-height: 100%; margin: 0; }
  #resource-test .modal-content { height: 100vh; max-height: 100vh; border: 0; border-radius: 0; }
  #resource-test .modal-body { padding: 22px 20px 28px; }
  .tz-resource-test__head { padding: 18px 20px 15px; }
  .tz-resource-test__footer { padding: 12px 20px; }
  .tz-wizard-steps { gap: 13px; padding: 0 20px; }
  .tz-wstep { grid-template-columns: 1fr; gap: 1px; min-height: 58px; }
  .tz-wstep__lbl small { display: none; }
  .tz-typecards { grid-template-columns: 1fr; }
}


/******************** BUG FIXES (json-editor, tokenized) ******************/

/* json-editor "add" buttons (real class json-editor-btntype-add — NOT the
   typo'd .json-editor-btn-add) render as .btn.btn-secondary.btn-sm, a filled
   gray box that doesn't match the app. Restyle them to the same neutral hairline
   outline as the other non-primary actions (no teal, so an add button never
   stands out as the only teal control, and it stays legible in dark). Selector
   .btn.json-editor-btntype-add outranks theme's .btn-secondary. */
.btn.json-editor-btntype-add {
    --bs-btn-bg: transparent;
    --bs-btn-color: var(--tz-text-muted);
    --bs-btn-border-color: var(--tz-border);
    --bs-btn-hover-bg: var(--tz-surface-sunken);
    --bs-btn-hover-color: var(--tz-text);
    --bs-btn-hover-border-color: var(--tz-border-strong);
    --bs-btn-active-bg: var(--tz-surface-hover);
    --bs-btn-active-color: var(--tz-text);
    --bs-btn-active-border-color: var(--tz-border-strong);
    margin-left: 10px;
}

/* json-editor renders object sections as Bootstrap nav-pills (vertical,
   format:tabs — used by 7 schemas) or nav-tabs (horizontal, tabs-top). The
   active pill kept Bootstrap's hardcoded blue (--bs-nav-pills-link-active-bg:
   #0d6efd) and the tab links used the raw link blue. Tokenize both to teal.
   (Applies app-wide: resource-main also uses these.) */
.nav-pills {
  --bs-nav-pills-link-active-bg: var(--tz-primary);
  --bs-nav-link-color: var(--tz-text-muted);
  --bs-nav-link-hover-color: var(--tz-primary);
}
.nav-tabs {
  --bs-nav-tabs-border-color: var(--tz-border);
  --bs-nav-link-color: var(--tz-text-muted);
  --bs-nav-link-hover-color: var(--tz-primary);
  --bs-nav-tabs-link-hover-border-color: var(--tz-border) var(--tz-border) var(--tz-border);
  --bs-nav-tabs-link-active-color: var(--tz-heading);
  --bs-nav-tabs-link-active-border-color: var(--tz-border-strong) var(--tz-border-strong) var(--tz-surface);
}

/* json-editor renders horizontal (tabs_top) datasets as a Bootstrap .card with
   boxed folder nav-tabs on a white card-body — off-brand and jarring in the
   flat editor. Restyle as flat, branded underline tabs (like the app sub-nav):
   transparent surfaces (no white card), a hairline under the tab bar, muted
   labels, and a teal underline + teal label on the active tab. Scoped to
   json-editor top-tab holders only (cards whose header carries nav-tabs). */
.tz-jsoneditor--flat .card:has(> .card-header > .nav-tabs),
.tz-jsoneditor--flat .card:has(> .card-header > .nav-tabs) > .card-header,
.tz-jsoneditor--flat .card:has(> .card-header > .nav-tabs) > .card-body {
  background: transparent;
  border: 0;
}
.tz-jsoneditor--flat .card:has(> .card-header > .nav-tabs) > .card-header {
  border-bottom: 1px solid var(--tz-border);
  padding: 0;
}
.tz-jsoneditor--flat .card:has(> .card-header > .nav-tabs) > .card-body {
  padding-left: 0;
  padding-right: 0;
}
.tz-jsoneditor--flat .nav-tabs { border-bottom: 0; }
.tz-jsoneditor--flat .nav-tabs.card-header-tabs { margin: 0; }
.tz-jsoneditor--flat .nav-tabs .nav-link {
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  margin-bottom: -1px;
  padding: 8px 14px;
  background: transparent;
  color: var(--tz-text-muted);
}
/* Hover is the HEADING alias, exactly as on the .nav-pills twin below, and not the brand
   primitive: --tz-primary is the same #024256 in both themes, so on the dark page a tab's
   label dropped to 1.36:1 under the cursor — it disappeared at the moment of being pointed
   at. Naming the alias makes the rule theme-aware and costs no dark override; in light it
   moves the label from #024256 to #02232D, which is the neighbouring ramp step and what the
   pills have always done. */
.tz-jsoneditor--flat .nav-tabs .nav-link:hover {
  color: var(--tz-heading);
  border-bottom-color: var(--tz-border-strong);
}
.tz-jsoneditor--flat .nav-tabs .nav-link.active {
  color: var(--tz-primary);
  font-weight: 600;
  background: transparent;
  border-bottom-color: var(--tz-primary);
}
/* U-11: the dark half its .nav-pills twin, further down this block, has always had. */
[data-bs-theme="dark"] .tz-jsoneditor--flat .nav-tabs .nav-link.active {
  color: var(--tz-accent);
  border-bottom-color: var(--tz-accent);
}

/* json-editor's side tabs arrive as Bootstrap pills. In a form they are navigation, not actions:
   use the same editorial line as the top tabs, rotated onto the shared vertical edge. */
.tz-jsoneditor--flat .nav-pills {
  gap: 0;
  border-right: 1px solid var(--tz-border);
  border-radius: 0;
}
.tz-jsoneditor--flat .nav-pills .nav-link {
  margin-right: -1px;
  padding: 8px 12px;
  border: 0;
  border-right: 2px solid transparent;
  border-radius: 0 !important;
  background: transparent;
  color: var(--tz-text-muted);
  text-align: left;
}
.tz-jsoneditor--flat .nav-pills .nav-link:hover {
  background: transparent;
  border-right-color: var(--tz-border-strong);
  color: var(--tz-heading);
}
.tz-jsoneditor--flat .nav-pills .nav-link.active {
  background: transparent;
  border-right-color: var(--tz-primary);
  color: var(--tz-primary);
  font-weight: 600;
}
[data-bs-theme="dark"] .tz-jsoneditor--flat .nav-pills .nav-link.active {
  border-right-color: var(--tz-accent);
  color: var(--tz-accent);
}

/* json-editor progress bars (upload / unknown-progress) — teal, not Bootstrap blue. */
.tz-jsoneditor--flat .progress-bar {
  background-color: var(--tz-primary);
}

/* In view mode json-editor disables its manipulation buttons; a disabled
   "+ Add" (or delete/move) in a read-only form is pointless clutter — hide it. */
.tz-jsoneditor--flat [class*="json-editor-btntype-"]:disabled {
    display: none;
}

/* json-editor sets an inline `display:inline-block` on object titles, so the
   flat rule needs !important to actually hide the redundant ROOT object title
   (e.g. the "Entity"/"Dataset" header duplicated by the page head). It MUST be
   scoped to the root container only: json-editor appends the root editor as
   `root_container` (data-schemapath="root") directly inside the editor holder,
   while nested sections get deeper data-schemapath containers. An unscoped
   `.je-object__title` also hit nested object sections — e.g. a column's
   collapsed "User Interface" block — and hiding that title removed its collapse
   toggle, so the whole section became invisible and unreachable. */
.tz-jsoneditor--flat > [data-schemapath="root"] > .je-object__title {
    display: none !important;
}

/* Inside tabs (vertical or top), each array item's object title duplicates its
   tab label — e.g. a "Channel 1" heading under the "Channel 1" tab. json-editor
   sets the item editor's container to the .tab-pane itself, so that redundant
   title is a direct child of the pane; hide only it (not deeper nested object
   titles). */
.tz-jsoneditor--flat .tab-pane > .je-object__title {
    display: none !important;
}

/* json-editor: a button in one of its button groups keeps all four corners rounded — the group
   is spaced out below, so its members are separate buttons, not one segmented control.
   ⚠️ It used to say `.btn` and `!important`, for EVERY button, which also flattened Bootstrap's
   own input-group corners: a date editor's calendar and clear buttons stood as two rounded
   boxes a pixel apart from their field (Nacho, 17 September 2026). Scoped to the group now;
   an input group is one control and theme.css draws it as one. */
.btn-group > .btn {
    border-radius: var(--tz-r-md) !important;
}

/* json-editor: Add at least 5px between buttons in a button group */
.btn-group > .btn {
    margin-right: 5px;
}

/* json-editor: have the buttons in a table row to be vertically aligned */
table {
    vertical-align: middle !important;
}

/* Center buttons rendered by json-editor inside their container */
.json-editor .form-control,
.json-editor .btn {
    vertical-align: middle;
}


/* form-label: always on top of the input (a rich dropdown replaces the control with a
   container of its own, so nothing else puts the label there). Its TYPE is theme.css's — the
   11px micro-label, one declaration for every field label in the product since U-27; what is
   here is where the label sits and the air under it. */
.form-label {
  position: relative;
  top: 0;
  left: 0;
  display: block;
  margin-bottom: var(--tz-sp-2);
}

/******************** RESIZABLE OFF-CANVAS DRAWER ********************/
/* A thin drag strip on the drawer's inner edge (offcanvas-end → left edge). The
   drawer is position:fixed, so this anchors to it. Width is driven by
   --bs-offcanvas-width, set inline by the drag handler and remembered per browser. */
.tz-offcanvas-resizer {
  position: absolute;
  top: 0;
  left: 0;
  width: 7px;
  height: 100%;
  cursor: ew-resize;
  z-index: 5;
  user-select: none;
}
.tz-offcanvas-resizer::before {
  content: '';
  position: absolute;
  top: 0;
  left: 2px;
  width: 2px;
  height: 100%;
  background: transparent;
  transition: background .15s ease;
}
.tz-offcanvas-resizer:hover::before,
.tz-offcanvas-resizer.is-dragging::before {
  background: var(--tz-primary);
}
[data-bs-theme="dark"] .tz-offcanvas-resizer:hover::before,
[data-bs-theme="dark"] .tz-offcanvas-resizer.is-dragging::before {
  background: var(--tz-accent);
}
/* While dragging: force the resize cursor everywhere and stop inner widgets (ace,
   json-editor, text selection) from swallowing the mousemove. */
body.tz-resizing-x,
body.tz-resizing-x * {
  cursor: ew-resize !important;
  user-select: none !important;
}
body.tz-resizing-x .offcanvas-body {
  pointer-events: none;
}

/* ===================================================================================
   Record form (the custom engine on /table/record)

   Everything is built out of the theme tokens, so the form follows the light/dark
   switch without a second set of rules — the same reason the rest of this file has no
   literal colours. The shape follows the record card of the July mocks: a section heading in
   small caps, a label above its value, and a nested dataset as a bordered block of its
   own rather than as a JSON blob behind a toggle.
   =================================================================================== */
/* The edge a section draws is the VIEW's choice, per section (SectionFrame), on an inner wrapper
   so that a box stays inside the grid gutter. There used to be one rule here — a line above
   every section but the first — and it could not tell "below" from "beside": a six-column section
   flowing next to its neighbour wore a line over its head and a heading a step lower than the
   neighbour's (17 September 2026). Rows are spaced by the grid's own gutter; a rule adds the
   breathing room it needs on its side. */
.tz-form-section__frame { height: 100%; }
.tz-form-section__frame--rule-above {
  margin-top: var(--tz-sp-4);
  padding-top: var(--tz-sp-6);
  border-top: 1px solid var(--tz-border-subtle);
}
.tz-form-section__frame--rule-below {
  margin-bottom: var(--tz-sp-4);
  padding-bottom: var(--tz-sp-6);
  border-bottom: 1px solid var(--tz-border-subtle);
}
/* The same breathing room a rule leaves above its heading, on every side: sp-5 left the fields
   almost touching the border (Nacho, 17 September 2026). */
.tz-form-section__frame--box {
  margin-top: var(--tz-sp-4);
  padding: var(--tz-sp-6);
  border: 1px solid var(--tz-border-subtle);
  border-radius: var(--tz-r-lg);
}
.tz-form-section__title {
  font-size: var(--tz-text-xs);
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--tz-n500);
  margin-bottom: var(--tz-sp-4);
}
/* A METADATA FIELD A DATA-VIEW NAMED (D14): read-only, drawn by the product, never an input.

   It borrows `.tz-field__label`'s micro-label register deliberately — it sits in the same grid
   row as the controls around it, and a different label register would read as a different KIND
   of thing rather than as the same form saying something it does not let you edit. What marks it
   as not-an-input is the absence of a box: no border, no field background, the value as text.
   Giving it a disabled control's grey box was tried and is exactly wrong, because a disabled
   control says "you may not edit this yet" and this is not editable by anyone. */
.tz-form-metadata { margin-bottom: var(--tz-sp-3); }
.tz-form-metadata__label {
  display: block;
  font-size: var(--tz-text-xs);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--tz-text-muted);
  margin-bottom: var(--tz-sp-1);
}
.tz-form-metadata__value {
  font-size: var(--tz-text-sm);
  color: var(--tz-heading);
  /* A provenance reference and a record id are machine-shaped literals, and the list they sit in
     is read by comparing one line against the next. */
  font-variant-numeric: tabular-nums;
}
/* One item per line, because the form is where every one of them is read. The list cell is the
   surface that caps and says "+N more"; a panel that did the same would be hiding the answer
   behind a hint with nothing to press. */
.tz-form-metadata__item { word-break: break-word; }
.tz-form-metadata__item + .tz-form-metadata__item { margin-top: 2px; }
.tz-form-metadata__empty { color: var(--tz-text-muted); }
.tz-field__label {
  display: block;
  font-size: var(--tz-text-xs);
  font-weight: 600;
  /* .08em, not the .04em this said until U-27: the guide's ramp declares .08–.15em for a
     micro-label, and the two json-editor copies of this spec were already writing .08. One
     vocabulary means one number, and the one inside the declared range is the one that wins.
     `.form-label` in theme.css carries the same six declarations for the same reason. */
  letter-spacing: .08em;
  text-transform: uppercase;
  /* muted, not n500: ~25 of these render per record screen at 11px, where n500
     measures 3.70:1 (audit M3). */
  color: var(--tz-text-muted);
  margin-bottom: var(--tz-sp-1);
}
/* The subject of a confirmation: its label in the micro-label register, the name the product
   uses for it, and the id underneath as detail rather than as the thing itself. */
.tz-confirm-subject { margin: 0 0 var(--tz-sp-4); }
.tz-confirm-subject b { display: block; color: var(--tz-heading); font-size: var(--tz-text-base); }
/* inline-block, not block: `.tz-id` is a chip and a chip is as wide as what it holds — as a
   block it drew a filled bar the width of the modal (Nacho, 19 September 2026). */
.tz-confirm-subject .tz-id { display: inline-block; margin-top: 4px; }
.tz-field__error {
  font-size: var(--tz-text-xs);
  color: var(--tz-error-ink);
  margin-top: var(--tz-sp-1);
}
/* Not an error: a remark about a value that is staying exactly as it is. */
.tz-field__notice {
  font-size: var(--tz-text-xs);
  color: var(--tz-warning-ink);
  margin-top: var(--tz-sp-1);
}

/* In-flow forms (guide § Form anatomy): one column capped near 480px, labels above
   their fields in the micro-label register, actions left-aligned closing the form like
   a passage of the page. The record form (.tz-form) governs its own grid. */
.tz-inflow { max-width: 480px; }
.tz-inflow .tz-field { margin-bottom: var(--tz-sp-4); }
.tz-inflow__actions { display: flex; align-items: center; gap: var(--tz-sp-2); margin-top: var(--tz-sp-6); }

/* The reveal toggle sits inside the password field, not floated over the column. */
.tz-passwrap { position: relative; }
.tz-passwrap .tz-passtoggle {
  position: absolute;
  top: 18px;
  right: 12px;
  transform: translateY(-50%);
  cursor: pointer;
  color: var(--tz-text-muted);
  border: 0;
  padding: 2px;
  background: transparent;
  line-height: 1;
}
.tz-passwrap .form-control { padding-right: 42px; }
.tz-passwrap .form-control.is-valid,
.tz-passwrap .form-control.is-invalid,
.was-validated .tz-passwrap .form-control:valid,
.was-validated .tz-passwrap .form-control:invalid { background-position: right 2.35rem center; }
.tz-passwrap .tz-passtoggle:focus-visible {
  outline: 2px solid var(--tz-accent-blue); outline-offset: 2px; border-radius: var(--tz-r-sm);
}
[data-bs-theme="dark"] .tz-passwrap .tz-passtoggle:focus-visible { outline-color: var(--tz-accent); }
.tz-password-fields { display: grid; gap: var(--tz-sp-4); }
.tz-inflow .tz-password-fields .tz-field { margin-bottom: 0; }
/* Information, not status, so no fill and no side rule (guide § When a box IS allowed: a
   tinted panel is for a state the page reports; what a password must have is a note). It was a
   sunken box with a strong left rule until 18 September 2026 — the one filled thing on the
   profile and on the password screen. A hairline above, the micro-label register for its
   title, and the checklist as it was: the dots are what report state, one rule at a time. */
.tz-password-rules {
  padding: var(--tz-sp-3) 0 0;
  border-top: 1px solid var(--tz-border-subtle);
  color: var(--tz-text-muted);
  font-size: var(--tz-text-sm);
}
.tz-password-rules__title {
  margin: 0 0 var(--tz-sp-2); color: var(--tz-text-muted);
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
.tz-password-rules__list {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 5px var(--tz-sp-4); margin: 0; padding: 0; list-style: none;
}
.tz-password-rules__list li { position: relative; padding-left: 20px; }
.tz-password-rules__list li::before {
  content: ""; position: absolute; left: 1px; top: .36em;
  width: 10px; height: 10px; border: 1px solid var(--tz-border-strong); border-radius: 50%;
}
.tz-password-rules__list li.is-met { color: var(--tz-success-ink); }
.tz-password-rules__list li.is-met::before {
  border-color: var(--tz-success-fg); background: var(--tz-success-fg);
  box-shadow: inset 0 0 0 2px var(--tz-surface);
}
@media (max-width: 575.98px) {
  .tz-password-rules__list { grid-template-columns: 1fr; }
}

/* ❗️U-24. Reading is the common case, so reading does not look like a form full of greyed-out
   boxes: the controls lose their frame and the value sits under its label, the way the
   mocks' record card shows it. Pressing Edit brings the frames back, which is also the only
   signal the page gives that the record became editable — worth keeping visible, and it is why
   the answer to "should the text fields gain boxes too, for one vocabulary" was no: boxing
   everything would spend the one signal the screen has. Ruled 13 September 2026; the guide says
   it too, under § Form anatomy.

   ⚠️ THE STATE IS `readonly` AS WELL AS `disabled`, and reading only the second is how this
   block came to apply to nothing. U-24's first half turned every text control from `disabled`
   into `readonly` so the record could be walked with the keyboard — and these selectors, written
   against `:disabled` alone, stopped matching the moment it landed. Measured on 13 September
   2026 in Chromium and Firefox: every text field in read mode was drawing a 1px border, a white
   fill and 12px of padding, i.e. the edit-mode control, and nothing said so.

   ⚠️ AND THE DROPDOWN IS NOT THE ELEMENT THESE RULES NAME. Choices wraps the native `<select>`
   in `.choices > .choices__inner` and hides the element, so `.form-select:disabled` reaches
   something nobody can see while the wrapper keeps its box, its sunken fill and its caret. That
   is the mismatch the finding is about: a dropdown framed like a control, next to text that is
   not. The read-mode vocabulary has to reach the widget that is actually drawn. */
.tz-form--readonly .form-control:disabled,
.tz-form--readonly .form-control[readonly],
.tz-form--readonly .form-select:disabled,
.tz-form--readonly .choices .choices__inner,
.tz-form--readonly .choices.is-disabled .choices__inner {
  background: transparent;
  border-color: transparent;
  padding-left: 0;
  color: var(--bs-body-color);
  /* One size for every value on the card. The text controls are already there; the dropdown is
     not — `--choices-font-size-md` is 1rem so that a closed Choices sits at the height of a
     Bootstrap `.form-select` beside it, which is a control's business and not a value's. In edit
     mode it stays as it is: the frames are back and the widget is a widget again. */
  font-size: var(--tz-text-base);
  opacity: 1;
}
.tz-form--readonly .form-select:disabled { background-image: none; }
/* The caret is a promise that the list opens. In read mode it does not. */
.tz-form--readonly .choices[data-type*="select-one"]::after { display: none; }
/* The chosen value is the value, in the same ink as every other value on the card — not the
   library's "disabled" grey, which would rank this field below the text beside it. */
.tz-form--readonly .choices.is-disabled .choices__item { color: var(--bs-body-color); }
/* The date's two icon buttons open a calendar and clear the value. Read mode disables both —
   `applyReadOnly` in golden-json-editor.js — and a disabled button that still draws is an
   affordance that lies: they are the "two dead icon buttons" of the finding. */
.tz-form--readonly .json-editor-btn-calendar,
.tz-form--readonly .json-editor-btn-clear { display: none; }
/* An empty value would otherwise be an invisible gap: the placeholder is what says
   "nothing here" rather than "nothing rendered". */
.tz-form--readonly .form-control:disabled::placeholder,
.tz-form--readonly .form-control[readonly]::placeholder { color: var(--tz-n400); opacity: .55; }

/* The record form's repeated scalars, nested-dataset blocks and raw-JSON panel lived here
   until September 2026. They belonged to the purpose-built engine that drew them, and they went
   with it: json-editor draws the record form again, on Bootstrap classes, and the sections it is
   grouped into are the .tz-form-section rules above. Nothing else in the product used them --
   .tz-field*, which every form in the product does use, is a different family and stays. */

/* ============================================================================
   Data-view layout editor, and the sections it lays out
   ----------------------------------------------------------------------------
   Every class here was being written by JavaScript with no rule behind it, so
   the width controls stacked.
   ============================================================================ */

.tz-sectionhead {
  display: flex;
  align-items: center;
  gap: var(--tz-space-3, .75rem);
  flex-wrap: wrap;
  margin-bottom: var(--tz-space-2, .5rem);
}

.tz-sectionhead input[data-role="label"] {
  flex: 1 1 12rem;
}

/* There is no folded section any more, and no `.tz-form-section--collapsed`: a data-view's
   `collapsed` flag is ignored by the form (golden-json-editor.js says why). */

/* A record being read folds every note away and shows its mark instead: the same icon, in the
   same colour, after the field's caption, the sentence in its tooltip (see `note()` in
   golden-json-editor.js). Pressing Edit takes the class off, the notes come back and the marks go.
   A cell of a table-format array has no caption, so its mark rides at the cell's right edge. */
.tz-form--readonly .tz-field__finding { display: none; }
.tz-form--readonly td.compact { position: relative; }
.tz-form--readonly .tz-field__mark--cell {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%); margin: 0;
}
.tz-field__mark { display: none; }
.tz-form--readonly .tz-field__mark {
  display: inline-block; margin-left: 5px; letter-spacing: 0; text-transform: none;
  font-family: "bootstrap-icons"; font-weight: 400; font-size: var(--tz-text-sm); line-height: 1;
  vertical-align: -1px; cursor: help;
}
.tz-field__mark--error   { color: var(--tz-error-ink); }
.tz-field__mark--error::before   { content: "\f623"; }
.tz-field__mark--warning { color: var(--tz-warning-ink); }
.tz-field__mark--warning::before { content: "\f33b"; }

/* Where a quality finding's path just took you. The outline is what makes the jump legible: a
   scroll with nothing marked at the end of it reads as the page having moved on its own. It stays
   until the next finding is followed, rather than fading on a timer, so it is still there when the
   user looks back up from the alert. */
.tz-form-target {
  outline: 2px solid var(--tz-warning, #997404);
  outline-offset: 2px;
  border-radius: 4px;
}

/* A finding's path, as a control. It is a button because it does something; it looks like a path
   because that is what it says. */
.tz-finding__path {
  font-family: var(--tz-font-mono, monospace);
  font-size: var(--tz-text-xs);
}

/* A path the form does not draw — an over-deep column, or a column a data-view left out. Marked
   and disabled rather than silently inert: the finding is still true, it just has no control. */
.tz-finding__path--missing {
  text-decoration: line-through;
  opacity: .7;
}

/* ============================================================================
   Field help toggle
   ----------------------------------------------------------------------------
   json-editor renders every schema `description` as <small class="form-text
   d-block"> under its control. The descriptions are deliberately long — they
   have to be enough without a manual — which is exactly what gets in the way
   once you know the resource. The Info button flips this class on <body> and
   the help folds away.

   Scoped to the editor: `form-text` is a Bootstrap class the rest of the page
   uses too, and hiding it wholesale would take help off screens that never had
   a way to turn it back on.

   The `#test-step2-body` half matches nothing today: the test wizard builds
   plain HTML and mounts no editor. Kept for when it grows one, and said out
   loud because the comment here used to claim it was the reason the class sits
   on <body>.

   `!important` is REQUIRED, and this is the whole reason the button did nothing
   for two releases. json-editor's bootstrap5 theme builds each description as
   `<small class="form-text d-block">` — note the second class — and Bootstrap
   ships `.d-block{display:block!important}`. An author `!important` beats an
   author normal declaration, so a plain `display: none` here never applied; and
   it beats the browser's own `[hidden]{display:none}` too, which is why setting
   the `hidden` property from JS did not work either. Both mechanisms were
   defeated by the same class.

   If a json-editor upgrade ever drops `d-block`, `HelpToggleContractTest` fails
   and this can go back to a plain declaration.

   And there are TWO families of description, not one. json-editor sends the field
   ones through `getFormInputDescription` --the .form-text above-- and those of an
   array or an object through `getDescription`, which the bootstrap5 theme does not
   override: they come out of the base theme as a <p> WITH NO CLASS AT ALL. That is
   why the switch left exactly the longest texts on screen, the ones that head a
   group.

   `p:not([class])` is safe with this theme, and only with this theme: the classless
   <p> comes from `getDescription` and from `getErrorMessage`, but bootstrap5
   overrides the second to give <div class="alert alert-danger">, and field errors
   are .invalid-feedback. Measured on a classifier's page: 18 <p>, all 18 of them
   descriptions, zero with a class attribute, zero alerts matching the selector.
   `HelpToggleContractTest` pins that condition, because the day a new json-editor
   returns errors as a bare <p> this would go from hiding help to hiding errors.
   ============================================================================ */
.tz-nohelp #editor_holder .form-text,
.tz-nohelp #editor_holder p:not([class]),
.tz-nohelp #test-step2-body .form-text {
  display: none !important;
}

/* ===== Record audit: editorial timeline, newest first ======================================= */
.tz-audit-panel { padding: 34px 0 46px; }
.tz-audit-panel__head { position: relative; margin-bottom: 26px; }
.tz-audit-panel__head .tz-narr-sub { max-width: 72ch; margin: 6px 0 0; }
.tz-record-metadata { padding: 34px 0 46px; }
.tz-record-metadata__head { margin-bottom: 22px; }
.tz-record-metadata__head .tz-narr-sub { max-width: 72ch; margin: 6px 0 0; }
.tz-record-metadata__empty { margin: 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-record-metadata-table-wrap {
  overflow-x: auto; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
}
.tz-record-metadata-table { width: 100%; min-width: 620px; border-collapse: collapse; table-layout: fixed; }
.tz-record-metadata-table th,
.tz-record-metadata-table td { padding: 9px 12px; border-bottom: 1px solid var(--tz-border-subtle); vertical-align: top; }
.tz-record-metadata-table thead th {
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 750;
  letter-spacing: .08em; text-align: left; text-transform: uppercase;
}
.tz-record-metadata-table tbody th { width: 34%; color: var(--tz-heading); font-weight: 500; text-align: left; }
.tz-record-metadata-table tbody tr:last-child > * { border-bottom: 0; }
.tz-record-metadata-table tbody tr:hover > * { background: var(--tz-surface-sunken); }
.tz-record-metadata-table code,
.tz-record-metadata-table pre {
  margin: 0; padding: 0; border: 0; background: transparent; color: var(--tz-heading);
  font-family: var(--tz-font-mono); font-size: var(--tz-text-sm); line-height: 1.5;
  overflow-wrap: anywhere; white-space: pre-wrap;
}
.tz-record-metadata-table pre { color: var(--tz-text-muted); }
.tz-audit-state {
  display: flex; align-items: flex-start; gap: 9px;
  margin: 0 0 20px; padding: 12px 0;
  border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
  color: var(--tz-text-muted); font-size: var(--tz-text-sm);
}
.tz-audit-state--warning { color: var(--tz-warning-ink); }
.tz-audit-empty {
  display: grid; justify-items: center; gap: 8px; padding: 46px 20px;
  border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
  color: var(--tz-text-muted); text-align: center;
}
.tz-audit-empty .bi { color: var(--tz-text-faint); font-size: 24px; }
.tz-audit-empty p { margin: 0; }

.tz-audit { position: relative; }
.tz-audit::before {
  content: ""; position: absolute; top: 7px; bottom: 15px; left: 98px;
  width: 1px; background: var(--tz-border);
}
.tz-audit__event { position: relative; }
.tz-audit__summary {
  display: grid; grid-template-columns: 82px 20px minmax(0, 1fr) auto;
  gap: 10px; align-items: start; padding: 0 0 25px; cursor: pointer; list-style: none;
}
.tz-audit__summary::-webkit-details-marker { display: none; }
.tz-audit__summary::marker { content: ""; }
.tz-audit__summary:hover .tz-audit__title strong { color: var(--tz-primary); }
.tz-audit__time { display: grid; justify-items: end; color: var(--tz-text-muted); line-height: 1.25; }
.tz-audit__time strong {
  color: var(--tz-heading); font-family: var(--tz-font-display); font-size: 15px;
  font-weight: 600; text-transform: lowercase;
}
.tz-audit__time span { margin-top: 3px; font-family: var(--tz-font-mono); font-size: var(--tz-text-xs); }
.tz-audit__node { position: relative; min-height: 18px; }
/* ⚠️ THE DOT SITS ON THE RAIL. The rail is drawn at 98px from the panel's left edge and the node
   column starts at 92 (82px of time column + 10px gap), so a 9px dot is centred on the rail at
   left 2px — at 6px, where it was, its centre fell four pixels to the right of the line and the
   marker read as beside the timeline rather than on it (Nacho, 17 September 2026). Move one
   number and move the other. */
.tz-audit__node::before {
  content: ""; position: absolute; top: 4px; left: 2px; width: 9px; height: 9px;
  border: 2px solid var(--tz-surface); border-radius: 50%; background: var(--tz-primary);
  box-shadow: 0 0 0 1px var(--tz-primary);
}
.tz-audit__event--insert .tz-audit__node::before {
  background: var(--tz-success-fg); box-shadow: 0 0 0 1px var(--tz-success-fg);
}
.tz-audit__event--remove .tz-audit__node::before,
.tz-audit__event--clear .tz-audit__node::before {
  background: var(--tz-error-fg); box-shadow: 0 0 0 1px var(--tz-error-fg);
}
.tz-audit__event--merge .tz-audit__node::before,
.tz-audit__event--split .tz-audit__node::before,
.tz-audit__event--disconnect .tz-audit__node::before {
  background: var(--tz-warning-fg); box-shadow: 0 0 0 1px var(--tz-warning-fg);
}
.tz-audit__summary-copy { display: grid; min-width: 0; padding-left: 7px; }
.tz-audit__title { color: var(--tz-heading); font-size: var(--tz-text-base); line-height: 1.4; }
.tz-audit__title strong { font-weight: 650; transition: color .15s ease; }
.tz-audit__actor { margin-top: 3px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); }
.tz-audit__summary-note { margin-top: 4px; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-audit__summary-comment {
  display: -webkit-box; overflow: hidden; margin-top: 5px; color: var(--tz-text-muted);
  font-size: var(--tz-text-sm); font-style: italic; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
.tz-audit__summary-comment::before { content: "“"; }
.tz-audit__summary-comment::after { content: "”"; }
/* THE HOUSE TOGGLE: the audit timeline's "See detail / Hide detail" and the metadata panel's
   "Show all / Show less" are one format, so they share one rule. */
.tz-audit__toggle, .tz-fold__toggle {
  display: inline-flex; align-items: center; gap: 6px; padding: 1px 0 0 12px;
  color: var(--tz-primary); font-size: var(--tz-text-xs); font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; white-space: nowrap;
}
/* U-11: brand teal on the dark page is 1.36:1 — accent, as everywhere else on dark. */
[data-bs-theme="dark"] .tz-audit__toggle, [data-bs-theme="dark"] .tz-fold__toggle { color: var(--tz-accent); }
.tz-audit__toggle .bi, .tz-fold__toggle .bi { font-size: 10px; transition: transform .15s ease; }
.tz-audit__toggle-hide, .tz-fold__toggle-hide { display: none; }
.tz-audit__event[open] .tz-audit__toggle-show, .tz-fold[open] .tz-fold__toggle-show { display: none; }
.tz-audit__event[open] .tz-audit__toggle-hide, .tz-fold[open] .tz-fold__toggle-hide { display: inline; }
.tz-audit__event[open] .tz-audit__toggle .bi, .tz-fold[open] .tz-fold__toggle .bi { transform: rotate(180deg); }

/* A folded machine value: the compact form on one line, clipped, and the toggle while closed;
   the pretty form once open. Two elements rather than one clamped, because the pretty form's
   first line is "[ {" and says nothing about what the value holds. */
.tz-fold__summary {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: start;
  cursor: pointer; list-style: none;
}
.tz-fold__summary::-webkit-details-marker { display: none; }
.tz-fold__summary::marker { content: ""; }
.tz-record-metadata-table .tz-fold__short { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tz-fold[open] .tz-fold__short, .tz-fold:not([open]) .tz-fold__text { display: none; }

/* A nested dataset drawn as lines (fragments/nested-lines.html): one item per line, the fields as
   pill + value separated by a middle dot, a sub-item indented one level and marked with its field's
   pill, the overflow behind the house fold. */
.tz-nested { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.tz-nested__line {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 6px; min-width: 0;
  padding-left: calc(var(--tz-depth, 0) * 14px);
}
.tz-nested__part { display: inline-flex; align-items: baseline; gap: 4px; }
/* 11px is the floor for a chip (style guide, "Type ramp"); never a fraction below it. */
.tz-nested__key, .tz-nested__field { font-size: 11px; }
.tz-nested__field { color: var(--tz-text-muted); }
.tz-nested__dot { color: var(--tz-text-faint); }
.tz-nested__fold { margin-top: 2px; }
.tz-nested__fold > .tz-fold__summary { grid-template-columns: auto; }
.tz-nested__fold[open] > .tz-fold__summary { margin-bottom: 3px; }
/* The merge proposal of a nested column: the script writes it as text, one item per line. */
.tz-xtable td.tz-xmergecol.text-pre-wrap { white-space: pre-wrap; }

.tz-audit__detail {
  margin: -11px 0 29px 119px; padding: 18px 0 0;
  border-top: 1px solid var(--tz-border); min-width: 0;
}
/* Each fact takes the width of its own words; the event reason, which is a sentence and may run
   to several lines, goes last and takes whatever is left of the row. It used to be an equal-width
   grid, which cut the sentence into a column as narrow as "Recorded changes: 16". */
.tz-audit__facts { display: flex; flex-wrap: wrap; gap: 22px 36px; }
.tz-audit__fact { display: grid; gap: 3px; min-width: 0; flex: 0 1 auto; }
.tz-audit__fact--reason { flex: 1 1 260px; order: 1; }
.tz-audit__fact > span,
.tz-audit__detail-label,
.tz-audit__comment > span {
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
}
.tz-audit__fact strong { color: var(--tz-heading); font-size: var(--tz-text-sm); font-weight: 600; }
.tz-audit__fact--machine { flex-basis: 100%; order: 2; }
.tz-audit__fact--machine pre {
  margin: 2px 0 0; color: var(--tz-heading); font-family: var(--tz-font-mono);
  font-size: var(--tz-text-xs); line-height: 1.5; overflow-wrap: anywhere; white-space: pre-wrap;
}
.tz-audit__comment { margin-top: 17px; padding-left: 13px; border-left: 2px solid var(--tz-border-strong); }
.tz-audit__comment p { margin: 3px 0 0; color: var(--tz-heading); font-size: var(--tz-text-sm); line-height: 1.55; }
.tz-audit__notice { margin-top: 17px; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-audit__related { margin-top: 22px; }
.tz-audit__related-head { display: grid; gap: 4px; margin-bottom: 12px; }
.tz-audit__related-head h3 {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: 18px; font-weight: 600;
}
.tz-audit__related-head p,
.tz-audit__related-empty { margin: 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-audit__related-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  margin: 0; padding: 0; border-top: 1px solid var(--tz-border); list-style: none;
}
.tz-audit__related-list li { min-width: 0; border-bottom: 1px solid var(--tz-border); }
.tz-audit__related-list li:nth-child(odd) { padding-right: 18px; }
.tz-audit__related-list li:nth-child(even) { padding-left: 18px; }
.tz-audit__related-list a {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  min-width: 0; padding: 10px 0; color: var(--tz-heading); text-decoration: none;
}
.tz-audit__related-list a:hover code { color: var(--tz-primary); text-decoration: underline; }
.tz-audit__related-list code {
  overflow: hidden; color: inherit; font-family: var(--tz-font-mono); font-size: var(--tz-text-sm);
  text-overflow: ellipsis; white-space: nowrap;
}
.tz-audit__related-list a > span {
  flex: 0 0 auto; color: var(--tz-primary); font-size: var(--tz-text-xs);
  font-weight: 700; letter-spacing: .04em; text-transform: uppercase; white-space: nowrap;
}
[data-bs-theme="dark"] .tz-audit__related-list a > span { color: var(--tz-accent); }
.tz-audit__related-list .bi { margin-left: 4px; font-size: 10px; }
.tz-audit__related-empty {
  padding: 11px 0; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
  font-style: italic;
}

/* A bucket decision, B7. Key-value rows on hairlines, no box: what it holds is four facts about
   the group that was judged, which is a passage of the event and not an object of its own. */
.tz-audit__bucket { margin-top: 22px; }
.tz-audit__bucket-facts {
  margin: 0; border-top: 1px solid var(--tz-border);
}
.tz-audit__bucket-facts > div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 18px;
  padding: 9px 0; border-bottom: 1px solid var(--tz-border);
}
.tz-audit__bucket-facts dt {
  flex: 0 0 auto; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
  font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
.tz-audit__bucket-facts dd {
  min-width: 0; margin: 0; color: var(--tz-heading); font-size: var(--tz-text-sm);
  overflow-wrap: anywhere; text-align: right;
}
.tz-audit__bucket-facts code { font-family: var(--tz-font-mono); font-size: var(--tz-text-sm); }

/* Twenty changes remain readable: one ruled row per field, five real columns, no boxes. */
.tz-audit-table-wrap {
  overflow-x: auto; margin-top: 20px;
  border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
}
.tz-audit-table { width: 100%; min-width: 900px; border-collapse: collapse; table-layout: fixed; }
.tz-audit-table__field-col { width: 27%; }
.tz-audit-table__kind-col { width: 11%; }
.tz-audit-table__value-col { width: 19%; }
.tz-audit-table__reason-col { width: 24%; }
.tz-audit-table caption {
  caption-side: bottom; padding: 7px 0; color: var(--tz-text-muted);
  font-size: var(--tz-text-xs); text-align: left;
}
.tz-audit-table th {
  padding: 7px 10px 8px; border-bottom: 1px solid var(--tz-border);
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 750;
  letter-spacing: .07em; text-align: left; text-transform: uppercase;
}
.tz-audit-table td {
  padding: 8px 10px; border-bottom: 1px solid var(--tz-border-subtle);
  color: var(--tz-text-muted); font-size: var(--tz-text-sm); vertical-align: top;
}
.tz-audit-table th:first-child,
.tz-audit-table td:first-child { padding-left: 0; }
.tz-audit-table th:last-child,
.tz-audit-table td:last-child { padding-right: 0; }
.tz-audit-table tbody tr:last-child td { border-bottom: 0; }
.tz-audit-table tbody tr:hover td { background: var(--tz-surface-sunken); }
.tz-audit-table__field { color: var(--tz-heading); font-weight: 600; line-height: 1.4; }
.tz-audit-table__kind {
  color: var(--tz-primary); font-size: var(--tz-text-xs); font-weight: 750;
  letter-spacing: .04em; text-transform: uppercase;
}
/* ⚠️ `:where()` AND THE POSITION, AND BOTH ARE LOAD-BEARING. This is the dark half of the
   rule above — the base kind, which is what an UPDATE and anything else without a colour of
   its own is drawn in. Written as a plain `[data-bs-theme="dark"] .tz-audit-table__kind` it
   weighs (0,2,0) and flattens the three modifiers below, which weigh (0,1,0): in dark the
   whole column went accent and "Added", "Cleared" and "Kept, not valid" stopped being
   distinguishable from "Changed" and from each other. `:where()` contributes nothing, so this
   lands at (0,1,0) and the modifiers — later, and already theme-aware through their own
   status aliases — win on order. Sitting above them says the same thing to a reader. */
:where([data-bs-theme="dark"]) .tz-audit-table__kind { color: var(--tz-accent); }
.tz-audit-table__kind--insert { color: var(--tz-success-ink); }
.tz-audit-table__kind--remove { color: var(--tz-error-ink); }
.tz-audit-table__kind--ignored { color: var(--tz-warning-ink); }
.tz-audit-table__value {
  display: block; padding: 0; border: 0; background: transparent; color: var(--tz-text-muted);
  font-family: var(--tz-font-mono); font-size: var(--tz-text-sm); line-height: 1.45;
  overflow-wrap: anywhere; white-space: pre-wrap;
}
.tz-audit-table__value--new { color: var(--tz-heading); }
.tz-audit-table__reason { display: block; color: var(--tz-text-muted); line-height: 1.45; }
.tz-audit-table__args { display: block; margin-top: 3px; color: var(--tz-text-faint); font-family: var(--tz-font-mono); }
.tz-audit-table__empty { color: var(--tz-text-faint); font-style: italic; }
.tz-audit-table__legacy { display: grid; justify-items: start; gap: 4px; color: var(--tz-text-muted); }
.tz-audit-table__row--ignored td { color: var(--tz-text-faint); }

.tz-audit__technical { margin-top: 14px; color: var(--tz-text-muted); }
.tz-audit__technical > summary { cursor: pointer; font-size: var(--tz-text-xs); }
.tz-audit__technical dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 24px; margin: 10px 0 0; }
.tz-audit__technical dl > div { display: grid; grid-template-columns: minmax(90px, auto) minmax(0, 1fr); gap: 8px; }
.tz-audit__technical dt { color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 600; }
.tz-audit__technical dd { margin: 0; color: var(--tz-heading); font-family: var(--tz-font-mono); font-size: var(--tz-text-xs); overflow-wrap: anywhere; }
.tz-audit__more { margin: 2px 0 0 119px; padding-top: 4px; }
.tz-audit__more--failed { color: var(--tz-warning-ink); }

@media (max-width: 767.98px) {
  .tz-audit::before { left: 9px; }
  .tz-audit__summary { grid-template-columns: 20px minmax(0, 1fr); gap: 6px 10px; }
  .tz-audit__time { grid-column: 2; grid-row: 1; display: flex; justify-items: start; align-items: baseline; gap: 7px; }
  .tz-audit__node { grid-column: 1; grid-row: 1 / 4; }
  .tz-audit__summary-copy { grid-column: 2; padding-left: 0; }
  .tz-audit__toggle { grid-column: 2; justify-self: start; padding-left: 0; }
  .tz-audit__detail { margin: -9px 0 27px 30px; }
  .tz-audit__facts { gap: 12px; }
  .tz-audit__fact { flex-basis: 100%; }
  .tz-audit__related-list { grid-template-columns: minmax(0, 1fr); }
  .tz-audit__related-list li:nth-child(n) { padding-right: 0; padding-left: 0; }
  .tz-audit__technical dl { grid-template-columns: minmax(0, 1fr); }
  .tz-audit__more { margin-left: 30px; }
}

/* ── Search on the record table ───────────────────────────────────────────────
   The bar, the marks on searchable column headers, and the line under a row that
   says why it is there. Everything else on this screen is unchanged; search used
   to be a page of its own and is now part of the list it produces. */
/* One control line: the field follows the same 36px rhythm as the page buttons. */
.tzs-bar { display: flex; align-items: center; gap: var(--tz-sp-3); margin: 6px 0 0; }
/* ❗️A scope strip above the bar needs its own air below it (Nacho, 19 September 2026): with the
   bar's own 6px the rule under the sentence read as the top of the search box, and the notice
   looked like a caption for it instead of a statement about the whole page. The gap is the
   page's own, measured: 32px is what the records list already leaves between the bottom of the
   filter row and the table's thick rule, so the two breaks down the page match. */
.tz-strip + .tzs-bar { margin-top: var(--tz-sp-8, 32px); }
.tzs-bar__input { height: 36px; }
.tzs-bar__field { position: relative; flex: 1 1 auto; }
.tzs-bar__field > .bi-search {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  opacity: .5; pointer-events: none;
}
.tzs-bar__input { padding-left: 34px; }

.tzs-reach { font-size: 13px; color: var(--tz-n500, #5E8794); margin-bottom: var(--tz-sp-4, 16px); }

/* The short-term note. Same voice as the reach line it sits with, pulled up under the bar and
   marked as an aside rather than a warning: it explains a narrower search, it does not report a
   problem, and nothing has been refused. */
.tzs-reach--hint { margin-top: calc(-1 * var(--tz-sp-1, 4px)); font-style: italic; opacity: .85; }

/* Discreet on purpose: it appears in every searchable header, so it has to survive
   being seen twenty times without becoming noise. */
.tzs-searchable { font-size: 10px; opacity: .45; margin-left: 4px; }

.tzs-why { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }

.tzs-truncated td {
  font-size: 13px;
  background: var(--tz-warning-tint, #FEF3C7);
  color: var(--tz-warning-deep, #92400E);
}


/* The advanced form. It replaces the bar rather than joining it: two live search controls on one
   screen is a question the user should never have to answer. */
/* Advanced search is a passage of the page, not a box (the style guide's rule). */
/* ⚠️ NO RULE ABOVE IT (Nacho, 29 September 2026). The hairline was drawn when this panel sat under
   the search bar; decision 16 made it REPLACE the bar, so the line was separating the panel from
   the page rather than from the control it grew out of, and it read as the start of a new section
   that is not there. The top padding stays: what the line was really giving was air. */
.tzs-adv {
  border: 0; border-radius: 0; padding: var(--tz-sp-4) 0 0; margin-bottom: 0;
}
/* ⚠️ THE COMBINE CHOICE TAKES THE CORPORATE BLUE, not Bootstrap's near-black. Measured before this
   rule: #02232d, which is `--tz-primary-dark` — the same black the vigency switch was wearing, and
   for the same reason: nothing claimed the checked state, so the vendor's default did. #024256 is
   the filled state every other control in the product uses. Dark takes the accent, because a
   near-black fill on a near-black surface states nothing. */
.tzs-adv .btn-check:checked + .btn {
  background: var(--tz-primary); border-color: var(--tz-primary); color: var(--tz-white);
}
[data-bs-theme="dark"] .tzs-adv .btn-check:checked + .btn {
  background: var(--tz-accent); border-color: var(--tz-accent); color: var(--tz-primary-dark);
}
.tzs-adv__head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.tzs-adv__title { font-weight: 600; }
.tzs-adv__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 20px 24px; align-items: start;
}
.tzs-adv__field { min-width: 0; }
.tzs-adv__field > label {
  display: flex; align-items: center; gap: 6px; margin-bottom: 7px;
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 650;
  letter-spacing: .09em; line-height: 1.3; text-transform: uppercase;
}
.tzs-adv__help {
  color: var(--tz-text-faint); cursor: help; font-size: 12px;
  letter-spacing: normal; text-transform: none;
}
.tzs-adv__control {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 12px; align-items: center;
}
.tzs-adv__control > .form-control { min-width: 0; height: 36px; }
.tzs-adv__radius { width: 10rem; }
.tzs-adv__only { font-size: 11px; font-style: italic; color: var(--tz-n500, #5E8794); }
/* Exact / similar is one switch with one word after it — the word names the state the switch
   is in, so it reads "Similar" when on and "Exact" when off (data-on / data-off, painted by
   CSS). It used to be "Exact ○ Similar": three parts, a switch at Bootstrap's 2em, and twice the
   width for the same bit of information. The label wraps the switch so the word toggles it. */
.tzs-mode {
  display: inline-flex; align-items: center; gap: 9px; margin: 0; white-space: nowrap;
  color: var(--tz-text-muted); font-size: var(--tz-text-sm); cursor: pointer; user-select: none;
}
.tzs-mode.form-switch { min-height: 0; padding-left: 0; margin-bottom: 0; }
.tzs-mode .form-check-input {
  flex: 0 0 auto; float: none; width: 2.75em; height: 1.5em; margin: 0; cursor: pointer;
}
.tzs-mode__word { min-width: 4.5em; }
.tzs-mode__word::after { content: attr(data-off); }
.tzs-mode .form-check-input:checked ~ .tzs-mode__word { color: var(--tz-primary); font-weight: 600; }
[data-bs-theme="dark"] .tzs-mode .form-check-input:checked ~ .tzs-mode__word { color: var(--tz-accent); }
.tzs-mode .form-check-input:checked ~ .tzs-mode__word::after { content: attr(data-on); }
.tzs-adv__foot {
  display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
  border-top: 1px solid var(--tz-n200, #D5E1E5); margin-top: 16px; padding-top: 12px;
}
.tzs-adv__lbl {
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .12em; opacity: .65;
}
.tzs-adv__sp { flex: 1 1 auto; }
.tzs-adv__limit { width: 6rem; }
@media (max-width: 575.98px) {
  .tzs-bar { align-items: stretch; flex-wrap: wrap; }
  .tzs-bar__field { flex-basis: 100%; }
  .tzs-adv__grid { grid-template-columns: minmax(0, 1fr); }
  .tzs-adv__control { grid-template-columns: minmax(0, 1fr); }
  .tzs-mode { justify-self: start; }
}
/* ===== QUALITY EXPLANATION ====================================================
   The record quality panel: how a score was formed, and what to repair first.
   Every number these paint comes from core's explanation read — the page does
   no arithmetic of its own, because the breakdown and the score have to
   describe the same population and only the evaluator knows which fields it
   actually reached. */

/* The score, as a ring. --tz-score is set inline per record, 0..100. */
.tz-score-ring {
  --tz-score: 0;
  position: relative; display: inline-grid; place-items: center;
  width: 76px; height: 76px; border-radius: var(--tz-r-full); flex: 0 0 auto;
  background: conic-gradient(
    var(--tz-score-colour, var(--tz-primary)) calc(var(--tz-score) * 1%),
    var(--tz-surface-sunken) 0);
}
.tz-score-ring::after {
  content: ""; position: absolute; inset: 8px;
  background: var(--tz-surface); border-radius: var(--tz-r-full);
}
.tz-score-ring__value {
  position: relative; z-index: 1;
  font-family: var(--tz-font-display); font-size: 22px; font-weight: 700;
  color: var(--tz-heading); line-height: 1;
}
/* ❗️THE CHART REGISTER, and the ring is where the sweep could not see it (28 September 2026,
   blind review). The arc is painted by the `conic-gradient` above, so it is an AREA — but the
   colour reaches it through `--tz-score-colour`, and `SemanticColourRegistersTest` was matching
   `background: var(--tz-error)` literally. The result was the defect the ladder exists to remove,
   on one screen: a 76px arc at #EF4444 beside the same panel's findings at #B82A2A. The rule is
   unchanged and it is the indirection that has to be watched — see that class's fourth test. */
.tz-score-ring--good    { --tz-score-colour: var(--tz-success-chart); }
.tz-score-ring--warning { --tz-score-colour: var(--tz-warning-chart); }
.tz-score-ring--bad     { --tz-score-colour: var(--tz-error-chart); }
/* No measurement: a ring with no arc, so the shape is still there but claims
   nothing. Absence is not a score of zero and must not look like one. */
.tz-score-ring--none { --tz-score: 0; --tz-score-colour: var(--tz-border); }

/* A .tz-id that may legitimately exceed a line (the 70-char quality definition):
   wrapping beats being the widest element on the page (audit H3). */
.tz-id--wrap { white-space: normal; word-break: break-all; }

/* One line item of the breakdown: a field, what it is worth, what it earned. */
.tz-factor {
  display: flex; align-items: flex-start; gap: var(--tz-sp-3);
  padding: var(--tz-sp-3) 0; border-top: 1px solid var(--tz-border-subtle);
}
.tz-factor:first-child { border-top: 0; }
.tz-factor__icon {
  display: inline-grid; place-items: center;
  width: 24px; height: 24px; border-radius: var(--tz-r-full); flex: 0 0 auto;
  font-size: var(--tz-text-xs);
}
.tz-factor--pass    .tz-factor__icon { background: var(--tz-success-bg); color: var(--tz-success-fg); }
.tz-factor--partial .tz-factor__icon { background: var(--tz-warning-bg); color: var(--tz-warning-fg); }
.tz-factor--fail    .tz-factor__icon { background: var(--tz-error-bg);   color: var(--tz-error-fg); }
.tz-factor__body { flex: 1 1 auto; min-width: 0; }
.tz-factor__name {
  margin: 0; font-size: var(--tz-text-base); font-weight: 600; color: var(--tz-heading);
  overflow-wrap: anywhere;
}
.tz-factor__note { margin: 2px 0 0; font-size: var(--tz-text-sm); color: var(--tz-text-muted); }
.tz-factor__points {
  flex: 0 0 auto; font-family: var(--tz-font-mono); font-size: var(--tz-text-sm);
  font-weight: 600; color: var(--tz-text-muted);
}
.tz-factor--fail .tz-factor__points, .tz-factor--partial .tz-factor__points { color: var(--tz-error-ink); }

/* One problem in the rail, with its headline and what to do about it. */
.tz-issue { display: flex; gap: var(--tz-sp-3); padding: var(--tz-sp-3) 0; border-top: 1px solid var(--tz-border-subtle); }
.tz-issue:first-child { border-top: 0; }
.tz-issue__icon {
  display: inline-grid; place-items: center;
  width: 28px; height: 28px; border-radius: var(--tz-r-full); flex: 0 0 auto;
}
.tz-issue--error   .tz-issue__icon { background: var(--tz-error-bg);   color: var(--tz-error-fg); }
.tz-issue--warning .tz-issue__icon { background: var(--tz-warning-bg); color: var(--tz-warning-fg); }
.tz-issue__body { min-width: 0; }
.tz-issue__title { margin: 0; font-size: var(--tz-text-base); font-weight: 600; color: var(--tz-heading); }
.tz-issue__detail { margin: 2px 0 0; font-size: var(--tz-text-sm); color: var(--tz-text-muted); overflow-wrap: anywhere; }

/* A quality finding, beside the field it is about.
   Its own element and not .tz-field__error: that one belongs to structural validation, is written
   when a save is attempted and cleared when the value stops being malformed. A finding describes
   the last measurement instead, so the two have different lifetimes and sharing a node would make
   each erase the other. Both can be true at once and both are shown at once. */
.tz-field__finding {
  margin-top: var(--tz-sp-1);
  /* ⚠️ U-23: THE BOTTOM MARGIN IS WHAT SAYS WHICH FIELD THE FINDING IS ABOUT. json-editor
     appends this node to the field's own grid column, past the `.mb-3` that separates one
     field from the next — so with a top margin and no bottom one the note sat flush against
     the label of the field BELOW it. Measured on the record form: 0-1px to the next label,
     where a field without a finding leaves 16-38px. A steward reading a Phone warning two
     pixels above "Post code" reads it as Post code's, and on a data-quality screen a finding
     pointing at the wrong field is the most expensive kind of wrong. */
  margin-bottom: var(--tz-sp-4);
  font-size: var(--tz-text-sm);
  display: flex; align-items: flex-start; gap: 6px;
}
/* Icon and sentence, nothing else, in the colour of the severity; the icon is what tells an error
   from a warning — a circle with a cross against a triangle — so the two never rely on colour
   alone. Both outline weights, so neither shouts over the other. */
.tz-field__finding::before { font-family: "bootstrap-icons"; line-height: 1.2; }
.tz-field__finding--error   { color: var(--tz-error-ink); }
.tz-field__finding--error::before   { content: "\f623"; }   /* bi-x-circle */
.tz-field__finding--warning { color: var(--tz-warning-ink); }
.tz-field__finding--warning::before { content: "\f33b"; }   /* bi-exclamation-triangle */
/* ===================================================================================================
   Data > Summary and Data > Quality (31 August 2026)

   Only what the two screens introduce. The catalogue cards, the coverage bar and the pills come from
   Bootstrap and from the .tz-* components above; what is here is the score ring, the trend, the
   distribution histogram and the issue ranking, none of which existed.
   =================================================================================================== */

/* ---- Catalogue ---------------------------------------------------------------------------------- */
.tz-catalogue { display: grid; grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); gap: 1rem; }
.tz-catalogue__stats { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--tz-border); }
.tz-catalogue__stat { padding: .75rem 1rem; border-right: 1px solid var(--tz-border); }
.tz-catalogue__stat:last-child { border-right: 0; }
.tz-catalogue__stat b { display: block; }
.tz-catalogue__stat span { color: var(--tz-text-muted); font-size: var(--tz-text-xs); }
/* An unmeasured figure is drawn as absent, never as a zero: zero is a measurement. */
.tz-catalogue__stat--unknown b { color: var(--tz-text-muted); font-weight: 600; }

/* ---- The average, as a ring ---------------------------------------------------------------------
   ⚠️ The component itself lives in the QUALITY EXPLANATION block below and is NOT redefined here.
   Both screens grew a score ring in the same week, on different branches, and the merge would have
   left whichever block came last governing both — one screen silently wearing the other's ring. What
   this adds is a size, because the quality screen's ring is the hero of the page and the record
   panel's sits inside a row. */
.tz-score-ring--hero { width: 132px; height: 132px; display: grid; margin: 0 auto; }
.tz-score-ring--hero::after { inset: 11px; }
.tz-score-ring--hero .tz-score-ring__value { font-size: 2rem; letter-spacing: -.04em; }
.tz-score-ring--hero .tz-score-ring__value small {
  display: block; margin-top: -.4rem; color: var(--tz-text-muted);
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: 0;
}

/* ---- Trend ---------------------------------------------------------------------------------------
   The vertical scale is fitted to the values and carries them, and the axis gutter is a fixed column
   so the labels cannot overlap the plot at any width. */
.tz-trend { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: .5rem; }
.tz-trend__axis { position: relative; height: 150px; }
.tz-trend__axis span { position: absolute; right: 0; transform: translateY(-50%); color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-variant-numeric: tabular-nums; }
/* ⚠️ POSITIONED, because the hover marks are no longer inside the SVG — see .tz-dot. */
.tz-trend__plot { position: relative; height: 150px; border-bottom: 1px solid var(--tz-border); }
.tz-trend__plot svg { width: 100%; height: 100%; overflow: visible; }
/* The trend line's colour lives here once, not as a literal on each polyline
   (PR #65 review, finding 13). */
.tz-trend__stroke { stroke: var(--tz-accent-blue); }
[data-bs-theme="dark"] .tz-trend__stroke { stroke: var(--tz-accent); }
.tz-trend__grid { stroke: var(--tz-border); stroke-width: 1; }
/* The min/max band under the mean. Tinted with the line's own colour rather than a second hue:
   it is the same measurement's spread, not a second series, and a band in a colour of its own
   reads as one. The opacity is what keeps the grid and the line legible through it. */
/* ⚠️ THE CHART RUNG, NOT THE PRIMITIVE. An area takes `--tz-*-chart` — the same hue tuned for
   separation rather than for contrast against the page — and SemanticColourRegistersTest says so.
   It did not catch this one: its patterns all read `background`, and an SVG area is painted with
   `fill`. The pattern now reads both. */
.tz-trend__band { fill: var(--tz-info-chart); fill-opacity: .15; stroke: none; }
[data-bs-theme="dark"] .tz-trend__band { fill: var(--tz-info-chart); fill-opacity: .22; }
/* A filled series, baseline at the axis floor. ⚠️ The chart that carries one is drawn from zero
   — see QualityViewService.chart — because the height of a fill is read as its value. Error red,
   because what it fills is the records that are wrong. */
.tz-trend__area { fill: var(--tz-error-chart); fill-opacity: .18; stroke: none; }
[data-bs-theme="dark"] .tz-trend__area { fill: var(--tz-error-chart); fill-opacity: .26; }
/* The rule marking a quality-definition change. Muted and dashed, not a warning colour: it is an
   annotation about what the two runs either side of it mean, not a problem with the data. */
.tz-trend__break { stroke: var(--tz-text-muted); stroke-width: 1; stroke-dasharray: 4 4; }
.tz-trend__break-hit { cursor: pointer; }
.tz-trend__labels { display: flex; justify-content: space-between; margin: .35rem 0 0 42px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); }

/* ---- Distribution --------------------------------------------------------------------------------
   Capped in width on purpose: stretched past about 800px the floored bars stop reading as bars and
   start reading as a dashed rule along the baseline, which is what the hatching exists to prevent. */
.tz-hist { display: block; }
.tz-hist__plot { position: relative; display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 2px; align-items: end; height: 186px; padding-top: 28px; border-bottom: 1px solid var(--tz-border); }
.tz-hist__col { display: flex; flex-direction: column; justify-content: flex-end; min-width: 0; height: 100%; }
.tz-hist__count { position: relative; z-index: 2; margin-bottom: .25rem; padding: 0 2px; border-radius: 3px; background: var(--tz-surface); color: var(--tz-text); font-size: var(--tz-text-xs); font-weight: 600; text-align: center; white-space: nowrap; }
/* ⚠️ The chart rung, not the bare primitive (28 September 2026). These are the bars of «where
   quality falls», which is exactly the chart whose colours were called out — the bands moved to the
   chart register and these did not, because the info hue had no rung to move to. It has one now. */
.tz-hist__bar { border-radius: 4px 4px 0 0; background: var(--tz-info-chart); box-shadow: 0 0 0 2px var(--tz-surface); }
/* A bucket under ~2% would be a sub-pixel bar. The floor plus the hatch is what keeps an
   almost-empty bucket from looking exactly like an empty one. */
/* The hatch keeps its RGBA half — a translucent stripe needs the channels, and `--tz-info-rgb` is
   the only form that gives them — while the solid half moves to the rung with the bar above. */
.tz-hist__bar--floored { min-height: 4px; background: repeating-linear-gradient(135deg, var(--tz-info-chart) 0 3px, rgba(var(--tz-info-rgb), .45) 3px 6px); }
.tz-hist__axis { display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 2px; margin-top: .35rem; color: var(--tz-text-muted); font-size: var(--tz-text-xs); text-align: center; }
.tz-hist__mean { position: absolute; z-index: 1; top: 16px; bottom: 0; width: 0; border-left: 2px dashed var(--tz-primary); }
.tz-hist__mean span { position: absolute; top: -19px; right: 4px; padding: 1px 6px; border-radius: var(--tz-r-full); background: var(--tz-primary); color: #fff; font-size: var(--tz-text-xs); font-weight: 700; white-space: nowrap; }
.tz-hist__side { display: flex; flex-direction: column; gap: .6rem; max-width: 320px; padding-bottom: 1.6rem; }
.tz-hist__side div { display: flex; justify-content: space-between; gap: .75rem; padding-bottom: .5rem; border-bottom: 1px solid var(--tz-border); }
.tz-hist__side div:last-child { border-bottom: 0; }
.tz-hist__side b { color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 600; }
.tz-hist__side span { font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---- Issue ranking ------------------------------------------------------------------------------- */
.tz-rank__row { display: grid; grid-template-columns: minmax(0, 1fr) 120px 52px; gap: .75rem; align-items: center; padding: .75rem 1rem; border-bottom: 1px solid var(--tz-border); }
.tz-rank__row:last-child { border-bottom: 0; }
.tz-rank__bar { height: 6px; border-radius: var(--tz-r-full); background: var(--tz-surface-sunken); }
/* A magnitude bar is an AREA: `-chart`, the same register the quality histogram and the grouped
   ranking use, so one concept is one colour wherever it is painted rather than drawn. */
.tz-rank__bar span { display: block; height: 100%; border-radius: inherit; background: var(--tz-warning-chart); }
.tz-rank__count { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
/* Severity never travels as colour alone: every row carries the icon and the word as well. */
.tz-sev { display: inline-flex; align-items: center; gap: .25rem; font-size: var(--tz-text-xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
.tz-sev--error { color: var(--tz-error-ink); }
.tz-sev--warning { color: var(--tz-warning-ink); }

@media (max-width: 991.98px) {
  .tz-hist { grid-template-columns: minmax(0, 1fr); }
  .tz-hist__side { padding-bottom: 0; }
}
/* One finding in the record's quality panel. They arrive a dozen at a time and used
   to sit 4px apart with no rule, so a message visually attached to the wrong finding
   (audit H7). A hairline in the panel's own hue keeps the wall readable. */
.tz-finding { padding-top: var(--tz-sp-2); margin-top: var(--tz-sp-2); border-top: 1px solid var(--tz-border-subtle); font-size: var(--tz-text-sm); }

/* ============================================================================
   EDITORIAL LAYOUT — the composition system of the approved mocks, measured at
   1440px and written down in docs/golden-style-guide.md. These are the classes
   the reskinned screens are BUILT from; the values are the mock's, not taste:
   head 46px above, hero 34/38, sections 38, rank rows 13/13, rules everywhere
   a box used to be. Coexists with the older components until every screen has
   crossed over.
   ============================================================================ */

/* The 11px smallcap label above a heading, saying what kind of thing follows: accent-blue,
   .15em, 14px of air under it. */
/* ❗️THERE IS NO EYEBROW ANY MORE, AND THIS IS WHAT IS LEFT OF ITS RULE (27 September 2026).
   One class used to be two different things: the page head's kicker — which repeated the active
   menu pair verbatim, so the context bar drops it, 30 nodes across 24 templates — and a section
   label INSIDE a page, which is a heading and had to stay. While one class served both, "no
   eyebrow" was a rule nobody could check and anybody could undo by re-using the class on a page
   head. So the kicker is gone and the three survivors are renamed into the class below: the
   home's two plane labels and the profile's field-group label.
   ⚠️ The retired name must exist NOWHERE — which is why it is not written here either, not even
   in prose: the rule's whole value is that one grep answers it. NoPageHeadEyebrowResidueTest is
   what holds it, and the name itself is in the style guide and in git history.
   ⚠️ Accent and not muted, decided on 20 September 2026 with both drawn side by side: it measures
   4.50:1 on white, a pass with nothing to spare, which is why the type table's micro-label row
   says muted — this label is that row's deliberate exception, not an oversight in it. */
.tz-overline {
  display: block; margin-bottom: 14px; color: var(--tz-accent-blue);
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .15em; text-transform: uppercase;
}
[data-bs-theme="dark"] .tz-overline { color: var(--tz-accent); }
/* ❗️The same kicker, for a section head inside a page rather than at the top of one. It was
   written into the two relations panels and never given a rule, so "Resource map" and "Table
   map" read as a stray line of body text above their heading. No margin of its own: the h2
   under it already carries one. */
.tz-section-kicker {
  display: block;
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .15em;
  text-transform: uppercase; color: var(--tz-accent-blue);
}
[data-bs-theme="dark"] .tz-section-kicker { color: var(--tz-accent); }

/* ═══ THE STRIP: one sentence between two rules, and the product's only banner ═══
   ❗️ONE EDITORIAL LINE FOR ALL OF THEM (Nacho, 19 September 2026). They had drifted into four
   different things: one bold lead in the semantic colour, another bold in black, one carrying its
   meaning in the icon alone and one saying nothing at all. The rule is now a single one and it has
   two halves.

   NEUTRAL is the default and it is an explanation: hairlines, no fill, muted text, and the lead in
   the heading colour. Nothing has happened and nothing is being asked of the reader.

   SEMANTIC is a strip that carries a state — this is not today's data, this cannot be decided by
   you, this was escalated. It takes the semantic tint as a background AND both rules in the
   semantic colour, and its icon and lead take the deep tone. The tint reaches past the text column
   so the strip reads as a band across the page, which is what a state deserves and an explanation
   does not.

   The lead is `.tz-strip__line` or a plain `<strong>`, indifferently: the two resolve to the same
   thing on purpose, because half the product writes one and half the other and the reader must not
   be able to tell which page they are on. */
.tz-strip {
  display: flex; align-items: center; gap: 11px; padding: 13px 0;
  border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
  font-size: var(--tz-text-base); color: var(--tz-text-muted);
}
.tz-strip b, .tz-strip strong, .tz-strip__line {
  color: var(--tz-heading); font-weight: 600; font-variant-numeric: tabular-nums;
}
.tz-strip__note { display: block; margin-top: 2px; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-strip__right { margin-left: auto; text-align: right; }
/* A strip closing a block it is not part of -- the quality hero's chart -- rather than one
   separating two sections, which the section padding already spaces. */
.tz-strip.is-spaced { margin-top: 22px; }
/* The range selector over the trend charts. Links, not buttons — the range is in the URL — so
   what marks the current one is `aria-current` and a filled chip, the same pair the navigation
   uses. It sits at the end of a section head, which is what `margin-left: auto` is for there. */
.tz-range { margin-left: auto; display: inline-flex; border: 1px solid var(--tz-border); border-radius: var(--tz-r-sm); overflow: hidden; }
.tz-range a {
  padding: 6px 13px; font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--tz-text-muted); text-decoration: none; white-space: nowrap;
}
.tz-range a + a { border-left: 1px solid var(--tz-border); }
.tz-range a:hover { background: var(--tz-surface-sunken); color: var(--tz-heading); }
.tz-range a.is-on { background: var(--tz-primary); color: var(--tz-white, #fff); }
[data-bs-theme="dark"] .tz-range a.is-on { background: var(--tz-accent); color: var(--tz-primary-dark); }
.tz-range a:focus-visible { outline: 3px solid var(--tz-accent-blue); outline-offset: -3px; }
@media (max-width: 991px) { .tz-range { margin-left: 0; } }
/* A strip in the context column is the same sentence over a third of the width, so it wraps to
   three lines and `align-items: center` then floats the icon against the middle of them. The
   glyph belongs to the first line, which is what a baseline says and `flex-start` only
   approximates — a `.bi` is taller than its text, so flush tops are not aligned tops. */
.tz-ctx .tz-strip { align-items: baseline; }
/* ⚠️ Below 992 there is no column: the aside renders in flow and every rule that spaces its
   blocks apart lives in theme.css inside the `min-width: 992px` query, so nothing separates them
   here. Measured at 900: the strip, the average and the record count ran into one another as one
   paragraph. The figures carry their own air at that width and the column's hairline carries it
   above. */
@media (max-width: 991px) {
  .tz-ctx .tz-ctx-fig,
  .tz-ctx .tz-profile-band__block { margin-top: 22px; }
}
/* A run inside a strip that must not break mid-figure ("Lowest 12 - Highest 100"). */
.tz-strip__run { white-space: nowrap; }
/* ⚠️ Inside an editor a strip STACKS. The flex row works on a 1128px page, where the sentence and
   its note sit side by side; in a 470px drawer the title took a narrow column of its own and broke
   after every word while the note kept the rest. Same strip, same colours, one column. */
.tz-admin-editor .tz-strip { display: block; padding: 13px 14px; }
.tz-admin-editor .tz-strip__note { margin-top: 4px; }

/* The semantic variants. Same shape, same spacing, one tone each. */
/* ⚠️ NO CORNER RADIUS, and this is the rule rather than a preference (Nacho, 19 September 2026).
   A strip is two rules across the page with a sentence between them; rounding its ends turns it
   into a card, which is the one thing the product decided a banner is not. The inset is only so
   the icon and the text do not sit on the edge of the tint. */
.tz-strip--warning, .tz-strip--error, .tz-strip--success, .tz-strip--info {
  padding-left: 14px; padding-right: 14px; margin-left: -14px; margin-right: -14px;
}
.tz-strip--warning { background: var(--tz-warning-bg); border-color: var(--tz-warning); }
.tz-strip--warning > .bi, .tz-strip--warning .tz-strip__line,
.tz-strip--warning > div > strong, .tz-strip--warning > strong { color: var(--tz-warning-ink); }
/* ⚠️ The second line takes the semantic tone too, softened. The muted grey it used to inherit
   measured 3.9:1 over the dark theme's band, which is under the 4.5 body text needs — and a
   sentence nobody can read is the half that explains what the first one means. */
.tz-strip--warning .tz-strip__note { color: var(--tz-warning-ink); opacity: .9; }
.tz-strip--error { background: var(--tz-error-bg); border-color: var(--tz-error); }
.tz-strip--error > .bi, .tz-strip--error .tz-strip__line,
.tz-strip--error > div > strong, .tz-strip--error > strong { color: var(--tz-error-ink); }
.tz-strip--error .tz-strip__note { color: var(--tz-error-ink); opacity: .9; }
.tz-strip--success { background: var(--tz-success-bg); border-color: var(--tz-success); }
.tz-strip--success > .bi, .tz-strip--success .tz-strip__line,
.tz-strip--success > div > strong, .tz-strip--success > strong { color: var(--tz-success-ink); }
.tz-strip--success .tz-strip__note { color: var(--tz-success-ink); opacity: .9; }
.tz-strip--info { background: var(--tz-info-bg); border-color: var(--tz-info); }
.tz-strip--info > .bi, .tz-strip--info .tz-strip__line,
.tz-strip--info > div > strong, .tz-strip--info > strong { color: var(--tz-info-ink); }
.tz-strip--info .tz-strip__note { color: var(--tz-info-ink); opacity: .9; }
/* ⚠️ The old `.tz-strip__ico--attention` said the same thing by colouring the icon and nothing
   else, which is exactly the drift this block ends. It stays as an alias so a strip that has not
   been moved to a variant yet does not lose its one mark, and it is not to be used again. */
.tz-strip__ico--attention { color: var(--tz-warning-ink); }
/* Two strips in a row share the rule between them: the pair is one block, and a 2px seam is a
   rendering accident, not a divider. A tinted one keeps its own, because its rules are the band. */
/* ⚠️ The WIDTH, not the shorthand. `border-top: 0` also resets the colour to currentColor, which
   silently turned a semantic strip's top rule into its own text colour the moment another strip
   sat above it — measured on the historical records list, where the switch sits right on top. */
.tz-strip + .tz-strip { border-top-width: 0; }

/* A tooltip that carries a sentence rather than a word. The default 200px wraps the search bar's
   statement of reach into a column eight lines tall, which is harder to read than the truncated
   placeholder it exists to complete. */
.tz-tip--wide .tooltip-inner { max-width: 460px; text-align: left; }
.tz-strip + .tz-strip--warning, .tz-strip + .tz-strip--error,
.tz-strip + .tz-strip--success, .tz-strip + .tz-strip--info { border-top-width: 1px; }

/* Where a record comes from, or where it went: one sentence with the move that follows from it.
   It is not a pill, because a pill cannot carry a destination, and the destination is the whole
   point of saying a record was merged into another one. */
.tz-prov { display: flex; align-items: flex-start; gap: 12px; padding: 14px 0;
  border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border); }
.tz-prov > .bi { font-size: 18px; line-height: 1.3; color: var(--tz-text-muted); }
.tz-prov__body { flex: 1 1 auto; min-width: 0; }
.tz-prov__line { margin: 0; color: var(--tz-heading); font-weight: 550; }
.tz-prov__note { margin: 3px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-prov__act { flex: 0 0 auto; }
/* ❗️The air below is the page's own measure, 32px — the same one the records list leaves between
   its last control and the table's rule (Nacho, 19 September 2026). Without it the provenance sat
   on the tab strip, and a statement about the record read as a label for the tabs. */
.tz-prov + * { margin-top: var(--tz-sp-8, 32px); }
/* The same two halves as the strip: a provenance that carries a state is tinted and takes the
   semantic rules; one that merely explains where a record came from stays neutral. */
.tz-prov--warning {
  background: var(--tz-warning-bg); border-color: var(--tz-warning);
  padding-left: 14px; padding-right: 14px; margin-left: -14px; margin-right: -14px;
}
.tz-prov--warning > .bi, .tz-prov--warning .tz-prov__line { color: var(--tz-warning-ink); }
.tz-prov--warning .tz-prov__note { color: var(--tz-warning-ink); opacity: .9; }
@media (max-width: 640px) {
  .tz-prov { flex-wrap: wrap; }
  .tz-prov__act { flex-basis: 100%; padding-left: 30px; }
}

/* ═══ THE SWITCH: choose one of a few, inside a form or over a list ═══
   ❗️ONE CONTROL FOR "CHOOSE ONE OF N" (Nacho, 20 September 2026). The product had three: this
   switch on the records list, a Bootstrap `btn-group` on the entity's access screen, and the round
   filter pills of the catalogues. The rule is now two lines: **inside a form, the switch**, which
   carries a label saying what the choice is about; **to filter a list, the pills**. The
   `btn-group` is gone.

   Two flavours, one look. Each state is an ADDRESS on a reading screen, so it is an anchor with
   `aria-current`; each state is a VALUE in a form, so it is a radio with its label, and the
   browser does the checking. */
.tz-switch { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* ⚠️ IN THE CONTEXT COLUMN THE LABEL TAKES ITS OWN LINE (Nacho, 29 September 2026). Side by side is
   right in a full-width strip and wrong in a 220-480px column: the two competed for a measure
   neither had, and the set was pushed against the column's edge. Scoped to the column, so the
   in-flow strip below 992px and the one on the access screen keep the row. */
.tz-ctx .tz-switch { flex-direction: column; align-items: flex-start; gap: 8px; }

/* ⚠️ A LABEL IN THE COLUMN READS LIKE ITS SIBLING, NOT LIKE A SECTION KICKER (Nacho, 29 September
   2026). `.tz-overline` is the accent-blue label a section inside a PAGE takes; beside the vigency
   switch's own muted label it looked like a different rank of heading rather than the same one. In
   the column it takes `.tz-switch__label`'s treatment — muted, .12em — so the two rows of pills are
   introduced the same way.
   ⚠️ The rule BELOW it goes too, and that exception lives in theme.css beside the separator it
   excepts — written here it loses on load order, exactly as the dev button did. */
.tz-ctx .tz-overline {
  color: var(--tz-text-muted); letter-spacing: .12em; margin-bottom: 0;
}
[data-bs-theme="dark"] .tz-ctx .tz-overline { color: var(--tz-text-muted); }
.tz-switch__label { font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--tz-text-muted); }
.tz-switch__set { display: inline-flex; gap: 2px; padding: 2px; background: var(--tz-surface);
  border: 1px solid var(--tz-border); border-radius: var(--tz-r-full); }
.tz-switch__set a,
.tz-switch__set label { display: inline-flex; align-items: center; gap: 6px; margin: 0;
  padding: 5px 13px; border-radius: var(--tz-r-full); font-size: var(--tz-text-xs);
  font-weight: 550; color: var(--tz-text-muted); text-decoration: none; cursor: pointer; }
.tz-switch__set a:hover,
.tz-switch__set label:hover { color: var(--tz-heading); }
/* ⚠️ THE CORPORATE BLUE, not `--tz-heading` (Nacho, 29 September 2026). `--tz-heading` is
   `--tz-primary-dark`, which next to the teal of every other filled control reads as plain black
   and made the switch look like a different product's component. `--tz-primary` is #024256.
   ⚠️ Dark keeps its own pair for the same reason `.tz-avatar` did before it: in dark `--tz-primary`
   is a near-black fill on a near-black surface, so the accent carries the state there. */
.tz-switch__set a[aria-current="true"],
.tz-switch__set .btn-check:checked + label {
  background: var(--tz-primary); color: var(--tz-white);
}
[data-bs-theme="dark"] .tz-switch__set a[aria-current="true"],
[data-bs-theme="dark"] .tz-switch__set .btn-check:checked + label {
  background: var(--tz-accent); color: var(--tz-primary-dark);
}
.tz-switch__set .btn-check:focus-visible + label {
  outline: 2px solid var(--tz-accent-blue); outline-offset: 2px;
}
/* A switch whose choice cannot be made right now — a locked entity, a scope column not set yet. */
.tz-switch__set .btn-check:disabled + label { opacity: .45; cursor: not-allowed; }
/* ═══ THE PROFILE: an editorial band, and a body that only holds what you can change ═══
   ❗️The page does two different jobs and they were stacked as if they were one (Nacho,
   19 September 2026). It TELLS a reader what they are here — the role, when they were last in,
   what has been granted to them — and it LETS them change two things about themselves. Stacked
   full width, the three statements read as the first items of a list whose next items were two
   forms, and the granted-access table took the whole page to draw three columns.

   The band carries the statements down the left, against a rule, and the body carries the two
   forms side by side. A form column of ~480px is also the honest width for a name: the single
   wide column had the reader typing six letters into 770 pixels. */
/* ⚠️ THE BAND'S OWN COLUMN IS GONE (Nacho, 29 September 2026): its three blocks are children of
   the context column now, so `.tz-profile-layout`'s 300px grid, `.tz-profile-band`'s right-hand
   rule and the separator between blocks all went with it — the column supplies all three. What
   stays is what styles the CONTENT of a block, which the column knows nothing about. */
.tz-profile-band__t {
  margin: 0 0 8px; font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--tz-text-muted);
}
.tz-profile-band__line { margin: 0; color: var(--tz-heading); }
.tz-profile-band__line .tz-pill { vertical-align: 1px; letter-spacing: .04em; }
.tz-profile-band__note { margin: 6px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.5; }
.tz-ctx .tz-profile-band__block .tz-strip { margin-top: 14px; }

/* A grant is a small stack and never a table: three columns do not fit 300px, and this is the
   shape the record listing already takes when the room runs out. */
.tz-grant { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--tz-border); }
.tz-grant:first-of-type { border-top: 0; padding-top: 0; }
.tz-grant__name { color: var(--tz-heading); font-weight: 600; }
.tz-grant__meta { margin-top: 6px; }
/* The values and the column they come from are one line: the column is what the values mean. */
.tz-grant__vals { margin-top: 6px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 7px; }
.tz-grant__on { color: var(--tz-text-muted); font-size: var(--tz-text-sm); }

.tz-profile-body--two {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 48px; align-items: start;
}
/* ⚠️ One column of rules inside a two-column body: at that width the pairs break mid-rule. */
.tz-profile-body--two .tz-password-rules__list { grid-template-columns: minmax(0, 1fr); }
/* ⚠️ BOTH forms open the page, not just the left one. The context column's alignment rule in
   theme.css zeroes the first content unit after the column, and "first" is the right word for a
   wrapper that STACKS its children — here they are side by side, so both sit on the top row and
   the second one's own spacing pushed its heading 38px below its neighbour's.

   Scoped to the width where the grid actually has two columns: below 1100 it collapses to one,
   the second form really is underneath the first, and it wants its spacing back. Doing it that
   way also means the number is never restated — `.tz-sect` keeps owning it. */
@media (min-width: 1101px) {
  .tz-profile-body--two > .tz-sect { padding-top: 0; }
}

@media (max-width: 1100px) {
  .tz-profile-body--two { grid-template-columns: minmax(0, 1fr); }
}

/* The hero: figure block | rule | narrated chart. No boxes. */
.tz-hero {
  display: grid; grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
  padding: 34px 0 38px;
}
/* The work page opens with this hero directly under the page head, which already carries the
   space above it; without the modifier the two paddings stacked and the figure floated. */
.tz-hero.is-flush { padding-top: 0; }
.tz-hero__fig { padding-right: 44px; border-right: 1px solid var(--tz-border); }
.tz-hero__chart { padding-left: 44px; min-width: 0; }
@media (max-width: 991px) {
  .tz-hero { grid-template-columns: 1fr; }
  .tz-hero__fig { border-right: 0; border-bottom: 1px solid var(--tz-border); padding: 0 0 28px; }
  .tz-hero__chart { padding: 28px 0 0; }
}

/* The figure is the headline: 76px display serif, severity-coloured when scored. */
.tz-figure-cap {
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; color: var(--tz-text-muted); margin-bottom: 2px;
}
.tz-figure { display: flex; align-items: baseline; gap: 9px; margin-top: 6px; }
/* Under a section heading rather than under a caption: the heading supplies the space above,
   and the meter below wants a little of its own. */
.tz-figure.is-tight { margin: 0 0 8px; }
.tz-figure__n {
  font-family: var(--tz-font-display); font-weight: 600; font-size: var(--tz-text-hero);
  line-height: .9; letter-spacing: -0.03em; color: var(--tz-heading);
  font-variant-numeric: tabular-nums lining-nums;
}
/* is-*, not tz-figure__n--*: the modifier is chosen inside a Thymeleaf ternary, and
   a literal containing double underscores is torn apart by expression preprocessing. */
/* Two smaller sizes, for the two places a figure is not the page's headline: a count that
   shares its hero with a chart (is-secondary) and a figure inside a two-up section, where the
   full hero size would out-shout the heading above it (is-compact). Named is-*, like the
   severity modifiers below and for the same reason. */
.tz-figure__n.is-secondary { font-size: 56px; }
.tz-figure__n.is-compact   { font-size: 40px; }
.tz-figure__n.is-good    { color: var(--tz-success-ink); }
.tz-figure__n.is-warning { color: var(--tz-warning-ink); }
.tz-figure__n.is-bad     { color: var(--tz-error-ink); }
.tz-figure__d { font-size: var(--tz-text-lg); color: var(--tz-text-muted); font-weight: 400; }
.tz-delta { margin-top: 16px; font-size: var(--tz-text-base); color: var(--tz-text-muted); }
.tz-delta b { color: var(--tz-heading); font-weight: 600; font-variant-numeric: tabular-nums; }
.tz-delta .tz-up { color: var(--tz-success-ink); }
.tz-delta .tz-down { color: var(--tz-error-ink); }
.tz-hero-meta { margin-top: 14px; font-size: var(--tz-text-sm); color: var(--tz-text-muted); line-height: 1.55; }

/* The chart narrates before it plots. */
.tz-narr {
  font-family: var(--tz-font-display); font-weight: 600; font-size: 19px;
  letter-spacing: -0.01em; color: var(--tz-heading); margin: 0 0 6px; line-height: 1.3;
}
.tz-narr-sub { font-size: var(--tz-text-sm); color: var(--tz-text-muted); margin: 0 0 18px; max-width: 64ch; }
.tz-foot { font-size: var(--tz-text-sm); color: var(--tz-text-muted); margin: 12px 0 0; line-height: 1.5; max-width: 90ch; }

/* What a steward wrote when they handed a case over: who, when, and in their own words.
   Sits under the Escalated pill in the administrator's ledger and under the banner on the
   case screen, so it carries no margin of its own beyond the line that precedes it. */
.tz-esc {
  display: block; font-size: var(--tz-text-sm); color: var(--tz-text-muted); line-height: 1.45;
  margin: 4px 0 0; max-width: 60ch;
}
/* On the administrator's triage row it is a grid item, and it belongs across both columns —
   the same rule .tz-qprev needs, and for the same reason: it is a line, not a cell. */
.tz-queue__row--triage .tz-esc { grid-column: 1 / -1; }
.tz-esc__who { display: block; }
.tz-esc__note { display: block; color: var(--tz-text); font-style: italic; overflow-wrap: anywhere; }

/* A section: an eyebrow of air, a rule, a serif head — never a box. */
.tz-hr { border: 0; border-top: 1px solid var(--tz-border); margin: 0; opacity: 1; }
.tz-sect { padding: 38px 0 0; }
.tz-sect__head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 22px; }
/* A heading whose section opens with a figure, not with prose: the figure's own leading is
   most of the gap already. */
.tz-sect__head.is-tight { margin-bottom: 14px; }
.tz-sect__t {
  font-family: var(--tz-font-display); font-weight: 600; font-size: var(--tz-text-h2);
  letter-spacing: -0.015em; color: var(--tz-heading); margin: 0; line-height: 1.2;
}
.tz-sect__meta { margin-left: auto; font-size: var(--tz-text-sm); color: var(--tz-text-muted); text-align: right; }
.tz-sect__meta b { color: var(--tz-heading); font-weight: 600; font-variant-numeric: tabular-nums; }
.tz-sect__note { font-size: var(--tz-text-sm); color: var(--tz-text-muted); max-width: 76ch; margin: 14px 0 0; }
/* A note that IS the section's body, standing where the content would be, rather than a
   footnote under it. */
.tz-sect__note.is-flush { margin-top: 0; }

/* Ranked issues: hairline rows — index, finding, count, bar, share. */
.tz-rank2 { border-top: 1px solid var(--tz-border); }
/* ⚠️ THE SEVERITY RULE (D-10, 27 September 2026). The colour that used to sit on every row sits
   here instead: two rules on a list of ten, rather than ten marks on ten rows. The word is the
   only thing that carries the ink — the counts beside it are muted, because they are the same
   kind of figure in both groups and colouring them would be decorating a number with a state it
   does not have.
   It is a smallcap of the micro-label's own register (11px/600, uppercase), so it reads as a
   label of the rows under it and not as a heading of its own; the section's `h2` is already the
   heading. `--tz-*-ink` and never `-deep`: the deep pair is the panel FILL in dark, which is the
   defect U-22 exists to prevent — a destructive word rendered pale. */
.tz-rank2__group {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px;
  margin: 0; padding: 22px 0 9px;
  border-bottom: 1px solid var(--tz-border);
}
/* The first group opens flush with the section's own top rule. */
.tz-rank2 > .tz-rank2__group:first-child { padding-top: 13px; }
.tz-rank2__group-name {
  display: inline-flex; align-items: baseline; gap: 6px;
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
/* ⚠️ THE SAME TWO MARKS THE FORM USES, and deliberately the same two: a steward meets these
   glyphs on the record form under a field (`.tz-field__finding`) and then meets them again here,
   over the ranking of the same problems. Two icons for one concept is how a product teaches
   somebody two vocabularies for one thing. The codepoints are bootstrap-icons' own, written the
   way `.tz-field__finding` writes them so the two stay visibly the same decision. */
.tz-rank2__group-name::before { font-family: "bootstrap-icons"; font-size: 12px; line-height: 1; }
.tz-rank2__group--error .tz-rank2__group-name { color: var(--tz-error-ink); }
.tz-rank2__group--error .tz-rank2__group-name::before { content: "\f623"; }   /* bi-x-circle */
.tz-rank2__group--warning .tz-rank2__group-name { color: var(--tz-warning-ink); }
.tz-rank2__group--warning .tz-rank2__group-name::before { content: "\f33b"; } /* bi-exclamation-triangle */
.tz-rank2__group-meta {
  font-size: var(--tz-text-sm); color: var(--tz-text-muted); font-variant-numeric: tabular-nums;
}
.tz-rank2__row {
  display: grid; grid-template-columns: minmax(0, 1fr) 96px minmax(120px, 132px) 64px;
  align-items: center; gap: 16px; padding: 13px 0;
  border-bottom: 1px solid var(--tz-border-subtle);
}
/* The same hover fill .tz-queue__row carries, and for the same reason: these rows are read
   across, and on a long ranking the eye loses the line between the field name and its share.
   The geometry does not move — no inset — so the tint lines up with the hairlines the section
   is built from. */
.tz-rank2__row:hover { background: var(--tz-n50); }
[data-bs-theme="dark"] .tz-rank2__row:hover { background: var(--tz-surface-sunken); }
.tz-rank2__t { font-size: var(--tz-text-base); color: var(--tz-text-muted); font-weight: 400; min-width: 0; }
.tz-rank2__t .tz-sev { margin-right: 6px; }
.tz-rank2__f { font-family: var(--tz-font-mono); font-size: 12px; color: var(--tz-text-muted); display: block; margin-top: 2px; }
.tz-rank2__n {
  font-family: var(--tz-font-display); font-size: var(--tz-text-kv); font-weight: 600;
  color: var(--tz-heading); text-align: right; font-variant-numeric: tabular-nums;
}
/* The pager's label is a note by typography and not by position: inside the pager row it is
   one item among the buttons, so it carries none of the note's own spacing. */
.tz-rank2__pager .tz-sect__note { margin: 0; }
.tz-rank2__bar { height: 6px; background: var(--tz-surface-sunken); border-radius: 3px; overflow: hidden; }
.tz-rank2__bar i { display: block; height: 100%; background: var(--tz-accent-blue); border-radius: 3px; }
[data-bs-theme="dark"] .tz-rank2__bar i { background: var(--tz-accent); }
/* ⚠️ A GROUPED RANKING'S BARS CARRY THEIR GROUP'S COLOUR, and that is not the per-row mark this
   round took away. A mark is a judgement made once per row — ten of them and the ink stops
   meaning anything. A bar is a QUANTITY, and every bar in a group is the same colour, so what it
   reads as is the group's own colour continuing down the page rather than ten separate verdicts.
   Scoped to the two modifiers and not to the row, because `.tz-rank2` is also the record screen's
   own findings list, which has no groups and keeps the neutral accent.

   ⚠️ `-chart` AND NOT `-ink`, and this was got wrong first (Nacho, 28 September 2026, reading the
   real screen). A bar is an AREA, which is the same job the histogram's bands do, so it takes the
   same register — and it was shipped in the ink family for one afternoon because the bar sits
   under a word that IS in the ink. The result was the exact defect the whole colour pass had just
   removed: two reds and two ambers on one page, #991B1B beside #D53838. The rule has no exception
   in it. The WORD of the group rule stays ink; everything that is painted area takes `-chart`. */
.tz-rank2__row.is-error .tz-rank2__bar i { background: var(--tz-error-chart); }
.tz-rank2__row.is-warning .tz-rank2__bar i { background: var(--tz-warning-chart); }
.tz-rank2__p { font-size: var(--tz-text-sm); color: var(--tz-text-muted); text-align: right; font-variant-numeric: tabular-nums; }
@media (max-width: 767px) {
  .tz-rank2__row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 6px; }
  .tz-rank2__bar { grid-column: 1 / 3; }
  .tz-rank2__p { display: none; }
}

/* Two-up closing sections divided by a rule, not boxed. */
.tz-two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); padding-top: 38px; align-items: start; }
.tz-two > * + * { padding-left: 44px; border-left: 1px solid var(--tz-border); }
.tz-two > *:first-child { padding-right: 44px; }
@media (max-width: 991px) {
  .tz-two { grid-template-columns: 1fr; }
  .tz-two > * + * { padding-left: 0; border-left: 0; border-top: 1px solid var(--tz-border); padding-top: 30px; margin-top: 30px; }
  .tz-two > *:first-child { padding-right: 0; }
}
.tz-kv { display: flex; align-items: baseline; justify-content: space-between; padding: 9px 0; border-bottom: 1px solid var(--tz-border-subtle); font-size: var(--tz-text-base); }
.tz-kv:last-child { border-bottom: 0; }
.tz-kv b { font-family: var(--tz-font-display); font-size: 16px; font-weight: 600; color: var(--tz-heading); font-variant-numeric: tabular-nums; }

/* The ranking's pager: a count and two quiet buttons, mock-style. */
.tz-rank2__pager { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 13px 0 0; }
.tz-rank2__pagerbtns { display: flex; gap: 8px; }
.tz-rank2__row[hidden] { display: none; }

/* Record quality is action-first: a compact score beside the repair list. The explanatory ledger
   follows in the normal page flow, with a shortcut beside the score for long issue lists. */
/* ⚠️ ONE COLUMN, because the figure left (Nacho, 29 September 2026). The hero was a two-up — the
   score beside "what to fix first", divided by a rule — and the score now lives in the page's
   context column. What is left is the repair work, which takes the whole width. */
.tz-quality-detail__hero { padding: 34px 0 38px; }
/* The figure's own rules are for the CONTEXT COLUMN now: no right-hand rule and no right padding,
   both of which were the two-up's divider and are a stray line against the column's own. */
.tz-quality-detail__figure { padding-right: 0; border-right: 0; }
/* ⚠️ THE TWO COUNTS UNDER THE FIGURE: A LINE, NOT TWO PILLS (Nacho, 29 September 2026). The figure
   above is already the screen's one piece of semantic colour — a 42 in error ink — so the counts
   carry their severity in a 6px dot and keep the sentence in muted body ink. Two filled pills here
   said the same thing twice and shouted it louder than the number they describe.
   ⚠️ The dot and not the word is coloured, because the word is a count and counts are not good or
   bad: fourteen warnings on a rich record and fourteen on a thin one read the same in ink. */
.tz-quality-detail__counts {
  display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 14px 0 0;
  color: var(--tz-text-muted); font-size: var(--tz-text-sm);
}
.tz-quality-detail__count { display: inline-flex; align-items: center; gap: 7px; }
.tz-quality-detail__count::before {
  content: ""; width: 6px; height: 6px; border-radius: var(--tz-r-full); flex: 0 0 auto;
}
/* ⚠️ THE CHART RUNG AND NOT THE INK. The design is right — the dot carries the severity and the
   word stays text — but a dot is an AREA, and an ink is a word colour tuned for contrast against
   the page rather than for separation from its neighbours. SemanticColourRegistersTest names both
   halves of this: `noInkIsUsedAsAFill` and `noPrimitiveIsLaunderedThroughACustomProperty`. */
.tz-quality-detail__count.is-error::before { background: var(--tz-error-chart); }
.tz-quality-detail__count.is-warning::before { background: var(--tz-warning-chart); }
.tz-quality-actions { min-width: 0; }
.tz-quality-actions .tz-sect__head { margin-bottom: 22px; }
.tz-quality-detail__state {
  display: grid; gap: 4px; margin: 22px 0 0; padding-top: 14px;
  border-top: 1px solid var(--tz-border-subtle);
  color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.5;
}
.tz-quality-detail__state strong { color: var(--tz-heading); font-weight: 600; }
.tz-quality-score__drift {
  display: flex; align-items: flex-start; gap: 8px;
  margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--tz-border-subtle);
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); line-height: 1.45;
}
.tz-quality-score__drift > div { display: grid; gap: 3px; }
.tz-quality-score__drift strong { color: var(--tz-heading); font-weight: 600; }
.tz-quality-detail__jump { margin-top: 24px; text-align: left; }
.tz-quality-detail__jump:hover .bi { transform: translateY(3px); }
.tz-quality-breakdown {
  padding: 30px 0 0; border-top: 1px solid var(--tz-border); scroll-margin-top: 76px;
}
.tz-quality-breakdown__head { max-width: 72ch; }
.tz-quality-breakdown__head .tz-narr-sub { margin: 6px 0 0; }

/* Approved weighted-proportion summary: the ratio is one visual relationship, while field counts
   are explanatory links rather than four unrelated headline figures. */
/* ⚠️ ONE COLUMN: the figure beside the bar is gone (Nacho, 29 September 2026). It restated the
   score a third time — the context column carries it, the record head carries it as a pill, and
   this said it again two inches from its own bar. What the section is FOR is how the number is
   arrived at, not the number. */
.tz-quality-balance {
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: 34px; align-items: center; margin-top: 20px;
}
/* ⚠️ THE UNFILLED HALF IS NEUTRAL, and it used to be `--tz-error-bg`. That is the PANEL register,
   the only one of the three that swaps per theme, so the same element measured #FEE2E2 in light and
   #991B1B in dark — a whisper and a slab (Nacho, 29 September 2026). It is also the wrong thing to
   say: what a record has not earned yet is not an error. This is the neutral `.tz-score-ring` has
   always used for its own unfilled arc, so the bar and the ring now agree.

   ⚠️ And note which sweep of SemanticColourRegistersTest would have caught it: none of the four.
   They cover a word on the pair, a word on a primitive, an ink used as a fill and an area on a bare
   primitive. An AREA on the TINT is the combination nobody wrote a sweep for. */
.tz-quality-balance__bar {
  display: flex; height: 18px; overflow: hidden; border-radius: var(--tz-r-sm);
  background: var(--tz-surface-sunken);
}
/* The same: a filled proportion is an area, not a word and not a border — so the band arrives in
   the CHART register, exactly as the histogram's bars and the score ring's arc take it. ⚠️ No
   default colour here on purpose: a fill with no band is a fill describing a score that does not
   exist, and it must not default to green. */
.tz-quality-balance__fill {
  display: block; min-width: 0; color: var(--tz-white); text-align: right;
}
.tz-quality-balance__fill.is-good    { background: var(--tz-success-chart); }
.tz-quality-balance__fill.is-warning { background: var(--tz-warning-chart); }
.tz-quality-balance__fill.is-bad     { background: var(--tz-error-chart); }
.tz-quality-balance__fill > span {
  display: block; padding-right: 7px; font-size: 10px; font-weight: 700; line-height: 18px;
}
.tz-quality-balance__labels {
  display: flex; justify-content: space-between; gap: 20px;
  margin-top: 8px; font-size: var(--tz-text-sm);
}
/* The sentence that names what the record ACHIEVED is a word, so it takes the band in the INK
   register — the same three classes the figure above it uses. The one that names what is left is
   muted: it is the neutral half of the bar, said in words. */
.tz-quality-balance__labels span:first-child.is-good    { color: var(--tz-success-ink); }
.tz-quality-balance__labels span:first-child.is-warning { color: var(--tz-warning-ink); }
.tz-quality-balance__labels span:first-child.is-bad     { color: var(--tz-error-ink); }
.tz-quality-balance__labels span:last-child { color: var(--tz-text-muted); text-align: right; }
/* ⚠️ BODY SIZE, not the micro size (Nacho, 29 September 2026). It is the sentence that explains
   how the score is formed — the one thing a reader opens this section for — and it was set two
   steps below the prose it belongs to, which read as a footnote to the bar rather than its point. */
/* ⚠️ BODY, and body is a form the guide defines rather than a size chosen here: 14px / 400 /
   muted, one of exactly three (§ Content text). It was set two steps below that, which read as a
   footnote to the bar rather than the sentence the section exists to deliver.
   ⚠️ NO `line-height`: an earlier pass set 1.55 here, which was invented — it put this one
   paragraph on 21.7px against the 21px every other sentence in the product sits on. Inherited, it
   keeps the page's rhythm. */
.tz-quality-balance__note {
  margin: 9px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-base);
}
.tz-quality-microstats {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: 17px; border-top: 1px solid var(--tz-border-subtle);
}
.tz-quality-micro { padding: 11px 14px 0; border-right: 1px solid var(--tz-border-subtle); }
.tz-quality-micro:first-child { padding-left: 0; }
.tz-quality-micro:last-child { border-right: 0; }
.tz-quality-micro b {
  display: block; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: 20px; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums;
}
.tz-quality-micro > span {
  display: block; min-height: 34px; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
}
.tz-quality-micro__link {
  display: inline-flex; align-items: center; gap: 5px; border: 0; padding: 4px 0 0;
  background: transparent; color: var(--tz-accent-blue); font-size: 10px; font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase; cursor: pointer;
}
.tz-quality-micro__link::after { content: "\2193"; transition: transform .15s ease; }
.tz-quality-micro__link[aria-expanded="true"]::after { transform: rotate(180deg); }
.tz-quality-micro__link:hover { color: var(--tz-primary); }
[data-bs-theme="dark"] .tz-quality-micro__link { color: var(--tz-accent); }

.tz-quality-field-drawer {
  margin-top: 20px; padding-top: 17px; border-top: 1px solid var(--tz-border);
}
.tz-quality-field-drawer__head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 11px;
}
.tz-quality-field-drawer__head h3 {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-kv); font-weight: 600;
}
.tz-quality-field-drawer__close {
  border: 0; padding: 0; background: transparent; color: var(--tz-text-muted);
  font-size: var(--tz-text-xs); cursor: pointer;
}
.tz-quality-field-drawer__close:hover { color: var(--tz-primary); }
.tz-quality-field-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 32px; }
.tz-quality-field-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
  padding: 8px 0; border-top: 1px solid var(--tz-border-subtle); font-size: var(--tz-text-sm);
}
.tz-quality-field-row__name { min-width: 0; color: var(--tz-heading); overflow-wrap: anywhere; }
.tz-quality-field-row__state { flex: 0 0 auto; font-size: var(--tz-text-xs); font-weight: 600; }
.tz-quality-field-row__state.is-full { color: var(--tz-success-ink); }
.tz-quality-field-row__state.is-partial { color: var(--tz-warning-ink); }
.tz-quality-field-row__state.is-none { color: var(--tz-error-ink); }

.tz-quality-factors { border-top: 1px solid var(--tz-border); }
.tz-quality-factor {
  display: grid; grid-template-columns: 24px minmax(0, 1fr) auto;
  align-items: start; gap: 12px; padding: 12px 0;
  border-bottom: 1px solid var(--tz-border-subtle);
}
.tz-quality-factor__mark {
  display: inline-grid; place-items: center; width: 22px; height: 22px;
  font-size: var(--tz-text-sm); color: var(--tz-text-muted);
}
.tz-quality-factor.is-pass .tz-quality-factor__mark { color: var(--tz-success-ink); }
.tz-quality-factor.is-fail .tz-quality-factor__mark { color: var(--tz-error-ink); }
.tz-quality-factor.is-partial .tz-quality-factor__mark { color: var(--tz-warning-ink); }
.tz-quality-factor__text h3 {
  margin: 0; color: var(--tz-heading); font-size: var(--tz-text-base); font-weight: 500;
}
.tz-quality-factor__text p {
  margin: 2px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.45;
}
.tz-quality-factor__points {
  color: var(--tz-text-muted); font-family: var(--tz-font-mono);
  font-size: var(--tz-text-sm); font-weight: 600; font-variant-numeric: tabular-nums;
}
.tz-quality-factor.is-fail .tz-quality-factor__points,
.tz-quality-factor.is-partial .tz-quality-factor__points { color: var(--tz-error-ink); }
.tz-quality-result {
  display: flex; justify-content: space-between; gap: 20px;
  padding: 14px 0; border-bottom: 1px solid var(--tz-border);
  color: var(--tz-text-muted); font-size: var(--tz-text-sm);
}
.tz-quality-result strong { color: var(--tz-heading); font-weight: 600; }
.tz-quality-result span { font-family: var(--tz-font-mono); text-align: right; }
.tz-quality-detail__note {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 12px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.5;
}
.tz-quality-findings__intro { margin: -10px 0 18px; }
.tz-quality-finding { grid-template-columns: minmax(0, 1fr) auto; }
.tz-quality-finding .tz-rank2__why { flex-wrap: wrap; }
.tz-quality-finding__action {
  align-self: center; border: 0; padding: 0; background: transparent; white-space: nowrap;
}
.tz-quality-clean {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 18px 0; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
}
.tz-quality-clean > .bi { color: var(--tz-success-ink); font-size: 20px; line-height: 1.2; }
.tz-quality-clean h3 { margin: 0; color: var(--tz-heading); font-size: var(--tz-text-base); font-weight: 600; }
.tz-quality-clean p { margin: 3px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
@media (max-width: 767.98px) {
  .tz-quality-detail__hero { padding: 24px 0 30px; }
  .tz-quality-actions { padding: 0; }
  .tz-quality-microstats, .tz-quality-field-list { grid-template-columns: minmax(0, 1fr); }
  .tz-quality-micro { padding: 10px 0; border-right: 0; border-bottom: 1px solid var(--tz-border-subtle); }
  .tz-quality-micro:last-child { border-bottom: 0; }
  .tz-quality-micro > span { min-height: 0; }
  .tz-quality-finding { grid-template-columns: minmax(0, 1fr); }
  .tz-quality-finding__action { justify-self: start; margin-top: 4px; }
}

/* The criteria panel: the fields quality actually checks, as labelled chips
   grouped by obligation. Labels, not paths — the key rides in the tooltip. */
.tz-crit { margin-top: 14px; }
.tz-crit__label {
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--tz-text-muted);
}
.tz-crit__chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }

/* Narrow screens: the editorial layout folds instead of overflowing. */
@media (max-width: 767.98px) {
  .tz-sect__head { flex-wrap: wrap; }
  .tz-sect__meta { margin-left: 0; text-align: left; }
  .tz-rank2__row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 6px; }
  .tz-rank2__bar { grid-column: 1 / 3; }
  .tz-rank2__p { display: none; }
  .tz-hero { padding: 24px 0 28px; }
  .tz-figure__n { font-size: 56px; }
}

/* Catalogue stats speak as figures: display serif, tabular, one baseline.
   This is THE definition (plus the display:block near the catalogue section) — a second
   sans block used to override it silently (PR #65 review, finding 8). */
.tz-catalogue__stat b {
  font-family: var(--tz-font-display); font-size: 22px; font-weight: 600;
  font-variant-numeric: tabular-nums lining-nums; line-height: 1.1;
}

/* The record list's scan line: one line per cell, the full value on hover.
   240px is the widest a cell may claim before it yields to its neighbours. */
.tz-records td { max-width: 240px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tz-records tr.tz-row-link:focus-visible { outline: 2px solid var(--tz-accent-blue); outline-offset: -2px; }
[data-bs-theme="dark"] .tz-records tr.tz-row-link:focus-visible { outline-color: var(--tz-accent); }
/* ❗️ONE DOT IS THE WHOLE OF THE COLOUR IN THIS COLUMN (Nacho, 30 September 2026). It was two
   FILLED pills per row — the score in its band's tint, the error count in red — and on the
   residencial listing that came to 30 filled chips over 15 visible rows, with a red one in EVERY
   row. Two things were wrong with it at once: the column outweighed the data it describes, and
   red had stopped saying which row to open, because they all had one.

   The band is one of three, which is exactly what a 7px dot encodes well, and the figure reads
   the same in plain ink. So both halves of the cell are muted text with the two NUMBERS in bold —
   the score and the count — and the eye still lands on the quantities without the row shouting.
   Chosen from a five-way studio built against this same markup, not from a description.

   ⚠️ The dot takes the `*-chart` tokens, NOT the `*-fg` ones. The fg pair is ink meant to sit on
   its own tint and is dulled for that job, so a dot drawn with it reads as a smudge at 7px; the
   chart trio is what this product already uses where a colour is a mark rather than a word, and
   it carries its own dark-theme values.

   ⚠️ `.tz-record-quality__label` went with this rewrite. It dimmed everything but a "Quality"
   prefix on the record head, and no template has drawn that prefix for some time — the comment
   was describing a screen that no longer exists, which is worse than no comment. */
.tz-records__quality { min-width: 150px; }
.tz-record-quality {
  display: inline-flex; align-items: baseline; gap: 2px;
  color: var(--tz-text-muted); font-variant-numeric: tabular-nums;
}
.tz-record-quality::before {
  content: ""; flex: none; align-self: center;
  width: 7px; height: 7px; margin-right: 7px;
  border-radius: var(--tz-r-full); background: var(--tz-border-strong);
}
.tz-record-quality.is-good::before { background: var(--tz-success-chart); }
.tz-record-quality.is-fair::before { background: var(--tz-warning-chart); }
.tz-record-quality.is-poor::before { background: var(--tz-error-chart); }
.tz-record-quality strong { font-weight: 700; }
.tz-record-quality span { opacity: .75; }
.tz-record-problems {
  margin-left: 10px; color: var(--tz-text-muted); font-variant-numeric: tabular-nums;
}
.tz-record-problems strong { font-weight: 700; }
.tz-records__updated { min-width: 150px; font-variant-numeric: tabular-nums; }
.tz-records__none { color: var(--tz-text-faint); }
.tz-records-section { margin-top: var(--tz-sp-8, 32px); }
.card.tz-flat.tz-records-card { border-top: 0; }
.card.tz-flat.tz-records-card > .card-body { padding-top: 0; }
.tz-records > thead > tr:first-child > th {
  border-top: 1px solid var(--tz-border);
  padding-top: 11px; padding-bottom: 11px;
}
.tz-ledger-table-wrap {
  width: 100%; border-top: var(--tz-table-head-rule-width) solid var(--tz-heading);
}
/* ❗️EVERY WIDE TABLE CARRIES A SIDEWAYS BAR THE PRODUCT DRAWS ITSELF (Nacho, 30 September 2026).
   `golden-hscroll.js` inserts it after each wrapper; the reasoning for not using the browser's own
   is in that file's header and is not repeated here. What matters at this end: the bar is a
   sibling of the scroller, it is STICKY to the bottom of the window so a table taller than the
   viewport does not hide it, and `.tz-hbar-host` — set by the script, never by a template —
   suppresses the native bar on the element it has taken over, so nobody ever sees two.

   ⚠️ The suppression is keyed on the class the SCRIPT adds, not on the wrapper's own selector.
   Written the other way it would hide the native bar for anyone with JavaScript off and put
   nothing in its place, which is the one outcome worse than the overlay bar we started from.

   ⚠️ Three earlier attempts are recorded because each looks reasonable and none of them works:
   restyling `::-webkit-scrollbar` (an overlay bar cannot be made to persist, and takes no layout
   space, so it paints inside the last row); the navigation's 4px hairline (glanced at, not
   dragged — invisible on a table); and `background-attachment: local` edge shadows (the cells are
   opaque white and paint straight over the wrapper's background). */
.tz-hbar {
  position: sticky; bottom: var(--tz-sp-3); z-index: 2;
  height: 12px; margin-top: 8px; border-radius: var(--tz-r-full);
  background: var(--tz-surface-sunken); border: 1px solid var(--tz-border-subtle);
  cursor: grab; touch-action: none;
}
.tz-hbar.is-held { cursor: grabbing; }
.tz-hbar:focus-visible { outline: 2px solid var(--tz-focus, var(--tz-brand)); outline-offset: 2px; }
.tz-hbar__thumb {
  position: absolute; top: 1px; bottom: 1px; left: 0;
  border-radius: var(--tz-r-full); background: var(--tz-border-strong);
}
.tz-hbar:hover .tz-hbar__thumb, .tz-hbar.is-held .tz-hbar__thumb { background: var(--tz-text-muted); }
.tz-hbar-host { scrollbar-width: none; }
.tz-hbar-host::-webkit-scrollbar { display: none; }
.tz-ledger-table-wrap > .table > thead > tr:first-child > th { border-top: 0; }
.tz-records-meta {
  display: flex; justify-content: flex-end; align-items: center;
  min-height: 20px; margin-bottom: 8px;
  color: var(--tz-text-muted); font-size: var(--tz-text-sm);
  font-variant-numeric: tabular-nums;
}

/* The warning explains the scope of the controls that follow it; leave a complete editorial beat
   before the search toolbar so the two do not read as one crowded widget. */

/* Faceted record filters. These are stateful chips, not navigation tabs: several independent
   criteria can be active together, while quality bands replace one another because the backend
   combines bounds with AND. */
.tz-filters {
  display: flex; align-items: center; flex-wrap: wrap; gap: 7px;
  margin: 12px 0 var(--tz-sp-4, 16px);
}
/* The scope notice and the filter row are one block: the values sit on the sentence's own line,
   and the rule below it keeps the same air the records page leaves between its strip and its
   filters — the 12px the row itself carries (19 September 2026). */
.tz-queue__scope { flex-wrap: wrap; gap: 6px 9px; }
.tz-queue__scope .tz-tag { flex: 0 0 auto; }
.tz-filter-pill {
  appearance: none; display: inline-flex; align-items: center; justify-content: center;
  min-height: 30px; padding: 5px 11px; border: 1px solid var(--tz-border);
  border-radius: var(--tz-r-full); background: var(--tz-surface); color: var(--tz-text-muted);
  font: inherit; font-size: var(--tz-text-xs); font-weight: 550; line-height: 1.25;
  text-align: left; white-space: nowrap; transition: border-color .12s, background .12s, color .12s;
}
/* ⚠️ The group separator inside a filter row goes when the row wraps: a divider needs something
   on both sides of it, and on a phone it landed at the end of a row with nothing after it — the
   same defect the first level's separators were narrowed to fix on 13 September 2026. */
@media (max-width: 767.98px) {
  .tz-admin-catalogue__filters > .tz-rule-sep { display: none; }
}
.tz-filter-pill:hover {
  border-color: var(--tz-border-strong); background: var(--tz-surface-hover); color: var(--tz-heading);
}
.tz-filter-pill:focus-visible {
  outline: 2px solid var(--tz-accent-blue); outline-offset: 2px;
}
/* ❗️ONE ACTIVE STATE, UNSCOPED (Nacho, 19 September 2026). This rule was written three times:
   here for the records list, again under `.tz-entity-catalogue` and again under
   `.tz-admin-catalogue`, each scoped to one screen and two of them keyed on `active` while this one
   is keyed on `is-active`. A fourth catalogue — Resources, when its rail became pills — matched
   none of them and its chosen pill came out white. The two class names both mean "this one is
   chosen", so they resolve to the same look and neither is scoped to a screen. */
.tz-filter-pill.is-active, .tz-filter-pill.is-active:hover,
.tz-filter-pill.active, .tz-filter-pill.active:hover {
  border-color: var(--tz-primary); background: var(--tz-primary); color: var(--tz-white);
}
[data-bs-theme="dark"] .tz-filter-pill.is-active,
[data-bs-theme="dark"] .tz-filter-pill.is-active:hover,
[data-bs-theme="dark"] .tz-filter-pill.active,
[data-bs-theme="dark"] .tz-filter-pill.active:hover {
  border-color: var(--tz-accent); background: var(--tz-accent); color: var(--tz-primary-dark);
}
/* The count on a filter pill: how many cases sit behind it, only when there are any. It borrows
   the pill's own ink rather than a tint of its own — the number is information, not an alarm. */
.tz-filter-pill__n {
  margin-left: 6px; padding: 0 6px; border-radius: var(--tz-r-full);
  background: var(--tz-surface-hover); color: var(--tz-heading);
  font-variant-numeric: tabular-nums; font-weight: 650;
}
.tz-filter-pill.is-active .tz-filter-pill__n,
.tz-filter-pill.active .tz-filter-pill__n { background: rgba(255, 255, 255, .18); color: var(--tz-white); }
[data-bs-theme="dark"] .tz-filter-pill.is-active .tz-filter-pill__n,
[data-bs-theme="dark"] .tz-filter-pill.active .tz-filter-pill__n {
  background: rgba(0, 0, 0, .16); color: var(--tz-primary-dark);
}
.tz-filter-combo { display: inline-flex; align-items: stretch; }
.tz-filter-combo.is-active .tz-filter-pill--dropdown {
  border-radius: var(--tz-r-full) 0 0 var(--tz-r-full);
}
.tz-filter-pill--clear {
  min-width: 29px; padding-left: 8px; padding-right: 8px; margin-left: -1px;
  border-radius: 0 var(--tz-r-full) var(--tz-r-full) 0;
}
.tz-filter-pill--clear .bi { font-size: 10px; }
[data-bs-theme="dark"] .tz-filter-pill:focus-visible { outline-color: var(--tz-accent); }
[data-bs-theme="dark"] .tz-filter-pill.is-active,
[data-bs-theme="dark"] .tz-filter-pill.is-active:hover {
  border-color: var(--tz-accent); background: var(--tz-accent); color: var(--tz-primary-dark);
}
.tz-filters .dropdown-menu { min-width: 180px; }
/* A filter menu that holds a small form rather than a list of options: the provenance pair, whose
   values are the customer's own and cannot be offered as items. Same menu, same ink; what it needs
   is room to breathe between two labelled boxes and a note. */
.tz-filters .dropdown-menu.tz-filter-form { min-width: 260px; padding: var(--tz-sp-3, 12px); }
/* ⚠️ The layout goes on `.show` and NEVER on the menu itself. `.dropdown-menu` is hidden by
   `display: none`, so a `display: flex` written on the base class overrides the primitive's own
   hiding and the menu sits open over the pill that opens it — permanently, and looking like a
   layout bug rather than a missing state. Caught by driving the page in a browser; the markup and
   every render test were green. */
.tz-filters .dropdown-menu.tz-filter-form.show { display: flex; flex-direction: column; gap: 4px; }
/* ⚠️ Rhythm and place only. The label's TYPE is declared once, on `.form-label`, and it is already
   the 11px uppercase micro-label — restating it here is the fifth vocabulary starting (U-27), which
   `FieldLabelVocabularyTest` refuses and which this rule did on its first pass. */
.tz-filter-form .form-label { margin-bottom: 0; }
.tz-filter-form .form-control + .form-label { margin-top: 8px; }
/* The note explains what a system on its own does, so it reads as the field's own help and not as
   an alarm: muted ink, no icon, no tint. */
.tz-filter-form__note {
  margin: 8px 0 4px; font-size: var(--tz-text-xs); line-height: 1.45; color: var(--tz-text-muted);
}
.tz-filter-form .btn { align-self: flex-start; }

/* The record's findings, quietly: every field already wears its own defect, so the
   aside is an index, not an alarm (audit H7 — the amber wall goes). */
.tz-findings { border-top: 1px solid var(--tz-border); padding-top: 12px; }
.tz-findings__head {
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--tz-text-muted); margin-bottom: 4px;
}
.tz-findings__head b { color: var(--tz-heading); }

/* Under a tab strip the panel's first flat section starts clean: the strip already
   drew the rule, and two hairlines 20px apart read as a mistake. */
[role="tabpanel"] > .card.tz-flat:first-child,
[role="tabpanel"] > .row:first-child .card.tz-flat:first-child { border-top: 0; }

/* Data has a split-layout wrapper between its tab panel and the form. The tabs already provide
   the section rule, so the first form card must not draw a second one underneath. */
#record-panel-data .tz-record-split > div > .card.tz-flat:first-child { border-top: 0; }


/* The section-knitting link: "See these records →". One pattern for every
   cross-reference between passages — 13px, link colour, the arrow travels. */
.tz-more {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--tz-text-xs); font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--tz-accent-blue); text-decoration: none;
}
button.tz-more { background: none; border: 0; padding: 0; cursor: pointer; }
.tz-case__skipform { display: inline; }
.tz-more .bi, .tz-more__arrow { transition: transform .15s ease; }
.tz-more:hover { color: var(--tz-accent-blue-dark); }
.tz-more:hover .bi, .tz-more:hover .tz-more__arrow { transform: translateX(3px); }
[data-bs-theme="dark"] .tz-more { color: var(--tz-accent); }
[data-bs-theme="dark"] .tz-more:hover { color: var(--tz-accent); }


/* Hover marks and the house tooltip.
   ⚠️ AN ELEMENT OVER THE PLOT, NOT A <circle> IN IT (Nacho, 29 September 2026: "los puntos sobre
   las series son ovalados y no redondos"). The plot is a 700x150 viewBox drawn with
   preserveAspectRatio="none" into roughly 940x150, so its horizontal scale is a third of its
   vertical one and every circle came out an ellipse — `vector-effect` exempts a stroke from that
   transform, never the geometry. Positioned in percentages instead, where a circle is a circle.
   The box is 16px so there is something to hit; what appears on hover is the 6px core. */
.tz-dot {
  position: absolute; width: 16px; height: 16px; margin: -8px 0 0 -8px;
  border-radius: 50%; cursor: pointer;
}
.tz-dot::after {
  content: ""; position: absolute; inset: 5px; border-radius: 50%;
  background: transparent; transition: background .1s;
}
.tz-dot:hover::after { background: var(--tz-accent-blue); }
[data-bs-theme="dark"] .tz-dot:hover::after { background: var(--tz-accent); }
/* ⚠️ A LONE MARK IS DRAWN AT REST, and every other one is not, on purpose. A mark is a hover
   target whose core stays transparent because the LINE is what the reader sees; a series of one
   point draws no line — a polyline needs two — so the chart came out blank and the value showed
   only where the cursor happened to cross a 16px circle nothing pointed at. The fill is the same
   token the line uses, so the one point reads as the series it is and not as a new kind of mark. */
.tz-dot.is-alone::after { background: var(--tz-accent-blue); }
[data-bs-theme="dark"] .tz-dot.is-alone::after { background: var(--tz-accent); }

/* The chart legends. Each swatch is drawn with the very token its shape is drawn with, which is
   why there is no sentence naming a colour anywhere near them. */
.tz-legend { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 10px 0 0; font-size: var(--tz-text-xs); color: var(--tz-text-muted); }
.tz-legend span { display: inline-flex; align-items: center; }
.tz-legend__key { display: inline-block; width: 16px; height: 3px; border-radius: 2px; margin-right: 7px; flex: 0 0 auto; }
.tz-legend__key--line { background: var(--tz-accent-blue); }
[data-bs-theme="dark"] .tz-legend__key--line { background: var(--tz-accent); }
/* ⚠️ The swatch is the shape's own colour at the shape's own opacity, mixed with the page rather
   than written as a literal rgba — a hard-coded 0,128,170 is the token's value copied by hand, and
   it stops being the token's value the first time somebody tunes the rung. */
.tz-legend__key--band { height: 11px; background: color-mix(in srgb, var(--tz-info-chart) 15%, var(--tz-surface)); border: 1px solid var(--tz-border); }
.tz-legend__key--area { height: 11px; background: color-mix(in srgb, var(--tz-error-chart) 18%, var(--tz-surface)); border: 1px solid var(--tz-border); }
/* The gap is an absence, so its swatch is the page showing through a hairline box. */
.tz-legend__key--gap { height: 11px; background: var(--tz-surface); border: 1px dashed var(--tz-border); }
.tz-tip {
  position: fixed; z-index: 1200; transform: translate(-50%, -100%);
  background: var(--tz-heading); color: var(--tz-surface);
  font-size: var(--tz-text-sm); font-weight: 500; line-height: 1.3;
  padding: 5px 10px; border-radius: 6px; pointer-events: none;
  /* pre-line, not nowrap: the histogram tip is two lines — the count on top, the band it
     belongs to underneath — and the break travels inside the message so a translator can
     move it. Single-line tips are unaffected: the box is fixed-position and sized by its
     content, so with no width to wrap against pre-line behaves exactly like nowrap. */
  white-space: pre-line; text-align: center;
  font-variant-numeric: tabular-nums;
}
.tz-trend__days { position: relative; height: 18px; margin-top: 6px; font-size: var(--tz-text-xs); color: var(--tz-text-muted); }
.tz-trend__days span { position: absolute; transform: translateX(-50%); white-space: nowrap; }
.tz-trend__days span:first-child { transform: none; }
/* ⚠️ `:not(:first-child)`, because a single measured day matches BOTH pseudo-classes and the
   later rule wins: the one label was pushed a full width to the left and ended up outside the
   chart, over the y-axis figures. A lone label is centred on its point like any middle one. */
.tz-trend__days span:last-child:not(:first-child) { transform: translateX(-100%); }

/* The histogram wears the quality bands: below 50, the middle, 80 and up. */
/* ⚠️ A BAR IS A LINK, AND IT LOOKS EXACTLY AS IT DID (Nacho, 29 September 2026). A first pass
   gave the clickable bars a resting affordance — the count in link ink, underlined, with a tint
   on hover — and he rejected it: "queda fatal… lo prefiero como antes y que el texto de la
   gráfica diga que se puede clickar". So the anchor carries no ink of its own; what says the
   bars are pressable is the sentence under the chart. The only thing kept from the anchor is
   the focus ring, which is not decoration: without it the link is unreachable by keyboard. */
.tz-hist__col { text-decoration: none; color: inherit; }
/* ⚠️ NO `cursor: default` HERE, and that attribute is the reason (Nacho, 29 September 2026).
   `[data-tip]` reads like "the ones that merely explain themselves" and is the opposite: a
   bucket holding records is drawn as an `<a href>` AND carries the tooltip, while an empty one
   is a plain `<div>` with no tooltip at all. So the rule only ever matched the real links, and
   what it did was take the hand off every one of them.
   This is not the resting affordance that was rejected — no ink, no underline, no tint. The
   pointer appears on hover, on the element under the cursor, which is the browser's own way of
   saying a thing is pressable; the sentence under the chart still carries the invitation. */
.tz-hist__col[data-tip]:hover .tz-hist__bar { filter: brightness(1.12); }
a.tz-hist__col:focus-visible { outline: 3px solid var(--tz-accent-blue); outline-offset: 2px; }
/* is-*, not tz-hist__bar--*: the modifiers are chosen inside a Thymeleaf ternary,
   where a literal's double underscores are torn apart by preprocessing — the same
   lesson the hero figure already paid for. */
/* ⚠️ THE INK FAMILY, NOT THE FILL PRIMITIVES (Nacho, 28 September 2026: the chart's colours
   jarred). The bands were #EF4444 / #F59E0B / #10B981 while the hero figure, the scoring boxes
   and the two ranking rules on the SAME PAGE drew the same three concepts in #991B1B / #92400E /
   #065F46. Both sets are "the semantic colours"; that is the trouble. One screen was painting one
   concept in two reds, and the saturated pair read as a different product pasted onto an
   editorial page.
   ⚠️ And the ink family is NOT the answer either, which is what trying it showed: error-ink and
   warning-ink are ΔE 20.9 apart — an amber dark enough to be a readable word on white is a brown,
   and a brown beside a dark red is two bands nobody can separate, least of all in the legend's
   10px swatches. So the band gets its own register, `--tz-*-chart`: the semantic colour moved 30 %
   toward its own ink. Same hue by construction — which is the half that was actually broken, the
   amber's two registers being 19.1° apart — calmer than the primitive, and ΔE 47 between the two
   that have to be told apart. See § Colour in the style guide for the three registers. */
.tz-hist__bar.is-bad  { background: var(--tz-error-chart); }
.tz-hist__bar.is-mid  { background: var(--tz-warning-chart); }
.tz-hist__bar.is-good { background: var(--tz-success-chart); }
/* `color-mix` and not `rgba(var(--*-rgb))`: the ink is composed, so it has no `-rgb` triplet and
   could not have one — its dark value is itself a mix. */
.tz-hist__bar.is-floored.is-bad  { background: repeating-linear-gradient(135deg, var(--tz-error-chart) 0 3px, color-mix(in srgb, var(--tz-error-chart) 45%, transparent) 3px 6px); }
.tz-hist__bar.is-floored.is-mid  { background: repeating-linear-gradient(135deg, var(--tz-warning-chart) 0 3px, color-mix(in srgb, var(--tz-warning-chart) 45%, transparent) 3px 6px); }
.tz-hist__bar.is-floored.is-good { background: repeating-linear-gradient(135deg, var(--tz-success-chart) 0 3px, color-mix(in srgb, var(--tz-success-chart) 45%, transparent) 3px 6px); }
.tz-hist__bar.is-floored { min-height: 4px; }
/* The legend's swatches read from the same three tokens as the bars above them and as the
   quality model boxes. Never give this its own colours: the point of the legend is that the
   colour means one thing everywhere quality is drawn. */
.tz-hist__key {
  display: flex; flex-wrap: wrap; gap: 6px 20px; margin: 12px 0 0; padding: 0;
  list-style: none; font-size: var(--tz-text-xs); color: var(--tz-text-muted);
}
.tz-hist__key li { display: inline-flex; align-items: center; gap: 7px; }
.tz-hist__keydot { width: 10px; height: 10px; border-radius: 2px; flex: none; }
/* The legend is the chart: the same three inks, or the key stops being a key. */
.tz-hist__keydot.is-bad  { background: var(--tz-error-chart); }
.tz-hist__keydot.is-mid  { background: var(--tz-warning-chart); }
.tz-hist__keydot.is-good { background: var(--tz-success-chart); }

/* Ranking rows, field-first: the name leads, the pill and sentence explain. */
.tz-rank2__field { display: block; font-size: var(--tz-text-base); font-weight: 500; color: var(--tz-heading); }
.tz-rank2__why { display: flex; align-items: baseline; gap: 8px; margin-top: 3px; font-size: var(--tz-text-sm); color: var(--tz-text-muted); }

/* The quality model, as four boxes and a mean: illustrative units, so the object
   box is the right shape for once. The is-* severity aliases colour the figures. */
.tz-model { display: flex; gap: 12px; flex-wrap: wrap; align-items: stretch; }
.tz-model__box {
  flex: 1 1 120px; min-width: 118px;
  border: 1px solid var(--tz-border); border-radius: 8px; padding: 12px 14px;
}
.tz-model__box--mean { border-color: var(--tz-border-strong); }
.tz-model__n {
  display: block; font-family: var(--tz-font-display); font-weight: 600; font-size: 28px;
  line-height: 1.05; color: var(--tz-heading); font-variant-numeric: tabular-nums;
}
.tz-model__n.is-good { color: var(--tz-success-ink); }
.tz-model__n.is-mid  { color: var(--tz-warning-ink); }
.tz-model__n.is-bad  { color: var(--tz-error-ink); }
.tz-model__d { display: block; font-size: var(--tz-text-sm); color: var(--tz-text-muted); margin-top: 5px; line-height: 1.4; }
.tz-model__c { display: block; font-size: var(--tz-text-xs); color: var(--tz-text-muted); margin-top: 4px; }


/* ==========================================================================
   Entity administration — editorial catalogue and master dossier
   ========================================================================== */
/* ❗️THE CONTROLS ARE `.tz-admin-catalogue__*`, HERE TOO (Nacho, 20 September 2026). This screen
   carried its own `__tools`, `__search` and `__filters` — twenty-six lines that were a copy of the
   administration ones, and a copy is only ever as good as the day it was taken: the focus ring had
   drifted to a solid `--tz-info-bg`, which in the dark theme is `--tz-info-deep` and paints a dark
   halo where every other search box paints a 13% blue one, and the phone gap under the box was
   13px against everyone else's 11px. Neither was decided; both were just the copy ageing.

   So the section declares BOTH classes, the way `table-main` already declared
   `tz-admin-catalogue tz-table-catalogue`: the plane's controls come from the shared family and
   what stays below is only what this catalogue really does differently — its rows are a CSS grid
   and not a table, because an entity's row shows a data flow rather than a list of cells. */
.tz-entity-catalogue { margin-top: 38px; }
/* ❗️A column for the radio since 19 September 2026: the row is selected here and acted on from
   the bar above, like every other administration catalogue. */
.tz-entity-catalogue__head,
.tz-entity-catalogue__row {
  display: grid;
  grid-template-columns: 34px 52px minmax(220px, 1.5fr) minmax(190px, 1.1fr) minmax(155px, .95fr) 115px 145px;
  gap: 18px; align-items: center;
}
.tz-entity-catalogue__head {
  min-height: 42px; border-bottom: 1px solid var(--tz-border);
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase;
}
.tz-entity-catalogue__head > :nth-child(6) { text-align: right; }
.tz-entity-catalogue__row {
  min-height: 104px; padding: 13px 0; border-bottom: 1px solid var(--tz-border-subtle);
  color: inherit; text-decoration: none; cursor: pointer;
}
.tz-entity-catalogue__row:hover {
  background: linear-gradient(90deg, var(--tz-surface-sunken), transparent 88%);
}
.tz-entity-catalogue__row:focus-visible {
  outline: 2px solid var(--tz-accent-blue); outline-offset: 2px;
}
.tz-entity-catalogue__order {
  color: var(--tz-text-faint); font-family: var(--bs-font-monospace);
  font-size: var(--tz-text-sm); font-variant-numeric: tabular-nums;
}
.tz-entity-catalogue__identity { min-width: 0; }
.tz-entity-catalogue__name {
  display: block; overflow: hidden; color: var(--tz-heading);
  font-family: var(--tz-font-display); font-size: 18px; font-weight: 600;
  line-height: 1.25; text-overflow: ellipsis; white-space: nowrap;
}
.tz-entity-catalogue__row:hover .tz-entity-catalogue__name { color: var(--tz-accent-blue); }
.tz-entity-catalogue__flow b,
.tz-entity-catalogue__capability b {
  display: block; color: var(--tz-text); font-size: var(--tz-text-sm); font-weight: 600;
}
.tz-entity-catalogue__flow i { margin: 0 5px; color: var(--tz-primary); font-style: normal; }
/* U-11: the arrow simply vanished on dark (1.36:1). Its twin .tz-entity-flow__arrow, in
   the dossier below, has carried the override since the dossier was written. */
[data-bs-theme="dark"] .tz-entity-catalogue__flow i { color: var(--tz-accent); }
.tz-entity-catalogue__capability span {
  display: block; margin-top: 4px; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
}
.tz-entity-catalogue__records { text-align: right; }
.tz-entity-catalogue__records b {
  display: block; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-kv); font-weight: 600; font-variant-numeric: tabular-nums;
}
.tz-entity-catalogue__records span { display: block; color: var(--tz-text-muted); font-size: var(--tz-text-xs); }
.tz-entity-catalogue__health > .tz-pill { display: inline-flex; }
.tz-entity-catalogue__health > small { display: block; margin-top: 4px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); }
.tz-entity-catalogue__empty { margin-top: 0; border-bottom: 1px solid var(--tz-border); }

/* ⚠️ THE CATALOGUE ASKS THE SPACE IT HAS, NOT THE WINDOW (28 September 2026, applying D-7).
   This was one of the three things the decision record left open: `.tz-entity-catalogue__row`'s
   seven columns ask for 1.019px of minimums (34+52+220+190+155+115+145 plus six 18px gaps) and the
   context bar leaves 984 at 1440, so the row overflowed by 35. The record offered two answers —
   revisit the grid, or be born with the context folded — and neither is needed: the defect was
   that a container-shaped question was being asked of the viewport. The cut below already existed
   at 1199.98 and already dropped to six columns (777px of minimums, which fits 984 with room); it
   simply never fired, because the bar takes 344px a media query cannot see.

   ⚠️ The numbers are unchanged. 1199.98 and 767.98 are the product's own cuts, now measured
   against the catalogue's own box rather than the window — so on a page with no context bar they
   fire within the gutters' width of where they always did, and on one with a bar they fire where
   the row actually runs out of room. And a container query is the only form that can follow a
   reader who DRAGS the column wider, because that width is a custom property and no media query
   can read one. */
@container catalogue (max-width: 1199.98px) {
  .tz-entity-catalogue__head,
  .tz-entity-catalogue__row {
    grid-template-columns: 30px 42px minmax(210px, 1.35fr) minmax(175px, 1fr) minmax(145px, .85fr) 105px;
    gap: 14px;
  }
  .tz-entity-catalogue__head > :nth-child(7),
  .tz-entity-catalogue__health { display: none; }
}
@container catalogue (max-width: 767.98px) {
  .tz-entity-catalogue__head { display: none; }
  .tz-entity-catalogue__row {
    grid-template-columns: 26px minmax(0, 1fr); gap: 4px 10px; padding: 18px 0;
    align-items: start;
  }
  /* ⚠️ Every cell but the radio stacks in the second column, the order number included: with the
     radio taking the first one, a cell left in column 1 would sit under it and overlap the next. */
  .tz-entity-catalogue__row > :nth-child(n+2) { grid-column: 2; }
  .tz-entity-catalogue__row .tz-admin-table__select { grid-row: 1; }
  .tz-entity-catalogue__order { color: var(--tz-text-faint); }
  .tz-entity-catalogue__health { display: block; grid-column: 2; }
  .tz-entity-catalogue__records { text-align: left; }
}

.tz-entity-dossier__status {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 19px;
}
.tz-entity-dossier__facts {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: 30px; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
}
.tz-entity-dossier__facts > div { min-height: 86px; padding: 17px 22px 14px 0; }
.tz-entity-dossier__facts > div + div { padding-left: 22px; border-left: 1px solid var(--tz-border-subtle); }
.tz-entity-dossier__facts b {
  display: block; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: 22px; font-weight: 600; line-height: 1.08; font-variant-numeric: tabular-nums;
}
.tz-entity-dossier__facts span { display: block; margin-top: 6px; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-entity-dossier {
  display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 52px;
  align-items: start; margin-top: 40px;
}
.tz-entity-dossier__toc { position: sticky; top: 18px; }
.tz-entity-dossier__toc-title {
  display: block; padding-bottom: 11px; border-bottom: 1px solid var(--tz-border);
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase;
}
.tz-entity-dossier__toc button {
  width: 100%; min-height: 45px; display: grid; grid-template-columns: 1fr auto;
  gap: 8px; align-items: center; padding: 8px 2px; border: 0;
  border-bottom: 1px solid var(--tz-border-subtle); background: transparent;
  color: var(--tz-text-muted); font-size: var(--tz-text-sm); text-align: left;
}
.tz-entity-dossier__toc button:hover { color: var(--tz-heading); }
.tz-entity-dossier__toc button.is-active { color: var(--tz-primary); font-weight: 600; }
[data-bs-theme="dark"] .tz-entity-dossier__toc button.is-active { color: var(--tz-accent); }
/* ❗️U-28: 11px, not the 9px this carried until 13 September 2026 — two steps below the
   ramp's declared floor, which at this size no longer holds contrast. The chip's own
   min-width and padding absorb the extra two pixels. */
.tz-entity-dossier__toc button i {
  min-width: 22px; padding: 2px 5px; border: 1px solid var(--tz-border-subtle);
  border-radius: var(--tz-r-full); background: var(--tz-surface-sunken); color: var(--tz-text-faint);
  font-family: var(--bs-font-monospace); font-size: var(--tz-text-xs); font-style: normal; text-align: center;
}
.tz-entity-dossier__toc button.is-active i { border-color: var(--tz-border); color: currentColor; }
/* U-28: 11px, was 9px — see the note on .tz-entity-dossier__toc button i. */
.tz-entity-dossier__toc button small {
  min-width: 20px; padding: 2px 6px; border-radius: var(--tz-r-full);
  background: var(--tz-surface-sunken); color: var(--tz-text-faint); font-size: var(--tz-text-xs); text-align: center;
}
.tz-entity-dossier__toc > p { margin: 17px 0 0; color: var(--tz-text-faint); font-size: var(--tz-text-xs); line-height: 1.5; }
.tz-entity-dossier__panels { min-width: 0; }
.tz-entity-panel { display: none; }
.tz-entity-panel.is-active { display: block; animation: tz-entity-panel-in .16s ease-out; }
@keyframes tz-entity-panel-in { from { opacity: .55; transform: translateY(3px); } }
.tz-entity-panel__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 28px;
  padding-bottom: 15px; border-bottom: 2px solid var(--tz-heading);
}
.tz-entity-panel__head > div > span {
  display: block; margin-bottom: 4px; color: var(--tz-primary); font-size: var(--tz-text-xs);
  font-weight: 600; letter-spacing: .09em; text-transform: uppercase;
}
[data-bs-theme="dark"] .tz-entity-panel__head > div > span { color: var(--tz-accent); }
.tz-entity-panel__head h2 {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-h2); font-weight: 600; letter-spacing: -.015em;
}
.tz-entity-panel__head p { max-width: 72ch; margin: 6px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-entity-panel__head > .tz-more { flex: none; margin-top: 10px; }
.tz-entity-flow-title { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; margin-top: 27px; }
.tz-entity-flow-title h3,
.tz-entity-summary-grid h3,
.tz-entity-operation h3,
.tz-entity-dependencies h3,
.tz-entity-danger h3 {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: 18px; font-weight: 600;
}
.tz-entity-flow-title > span { color: var(--tz-text-faint); font-size: var(--tz-text-xs); }
.tz-entity-flow {
  display: grid; grid-template-columns: minmax(165px, .85fr) 34px minmax(260px, 1.25fr) 34px minmax(165px, .85fr);
  align-items: stretch; margin-top: 14px;
}
.tz-entity-flow__column { min-width: 0; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border); }
.tz-entity-flow__column > header {
  min-height: 47px; display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 12px; border-bottom: 1px solid var(--tz-border-subtle);
}
.tz-entity-flow__column > header b {
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
}
.tz-entity-flow__column > header span { overflow: hidden; color: var(--tz-text-faint); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.tz-entity-flow__column > button {
  width: 100%; display: block; padding: 11px 12px; border: 0; border-bottom: 1px solid var(--tz-border-subtle);
  background: transparent; text-align: left;
}
.tz-entity-flow__column > button:hover { background: var(--tz-surface-sunken); }
.tz-entity-flow__column > button b,
.tz-entity-flow__column > button span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tz-entity-flow__column > button b { color: var(--tz-heading); font-size: var(--tz-text-sm); font-weight: 600; }
.tz-entity-flow__column > button span { margin-top: 2px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); }
.tz-entity-flow__more, .tz-entity-flow__absent { padding: 9px 12px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); }
.tz-entity-flow__more { color: var(--tz-accent-blue); }
.tz-entity-flow__arrow { display: grid; place-items: center; color: var(--tz-primary); font-size: 18px; }
[data-bs-theme="dark"] .tz-entity-flow__arrow { color: var(--tz-accent); }
.tz-entity-flow__core { padding: 8px 12px; }
.tz-entity-flow__core > div { position: relative; min-height: 48px; padding: 8px 4px 8px 34px; border-bottom: 1px solid var(--tz-border-subtle); }
.tz-entity-flow__core > div:last-child { border-bottom: 0; }
.tz-entity-flow__core i {
  position: absolute; top: 9px; left: 3px; width: 20px; height: 20px; display: grid; place-items: center;
  border-radius: var(--tz-r-full); background: rgba(var(--tz-primary-rgb), .09); color: var(--tz-primary);
  font-family: var(--bs-font-monospace); font-size: 10px; font-style: normal; font-weight: 600;
}
[data-bs-theme="dark"] .tz-entity-flow__core i { background: rgba(var(--tz-accent-rgb), .12); color: var(--tz-accent); }
.tz-entity-flow__core b, .tz-entity-flow__core span { display: block; }
.tz-entity-flow__core b { color: var(--tz-heading); font-size: var(--tz-text-sm); }
.tz-entity-flow__core span { overflow: hidden; margin-top: 1px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); text-overflow: ellipsis; white-space: nowrap; }
.tz-entity-summary-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 40px; margin-top: 34px; }
.tz-entity-summary-grid > section > header,
.tz-entity-operation > section > header,
.tz-entity-dependencies > header,
.tz-entity-danger > header {
  display: flex; align-items: baseline; justify-content: space-between; gap: 15px;
  padding-bottom: 10px; border-bottom: 1px solid var(--tz-border);
}
.tz-entity-definition { margin: 0; }
.tz-entity-definition > div {
  display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 17px;
  padding: 12px 0; border-bottom: 1px solid var(--tz-border-subtle);
}
.tz-entity-definition dt { color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 400; }
.tz-entity-definition dd { margin: 0; color: var(--tz-heading); font-size: var(--tz-text-sm); font-weight: 600; overflow-wrap: anywhere; }
.tz-entity-dossier__dataset-view { display: flex; align-items: baseline; gap: 8px; }
.tz-entity-dossier__dataset-view + .tz-entity-dossier__dataset-view { margin-top: 4px; }
.tz-entity-attention { margin: 0; padding: 0; }
.tz-entity-attention li {
  display: grid; grid-template-columns: 8px minmax(0, 1fr); gap: 10px;
  padding: 12px 0; border-bottom: 1px solid var(--tz-border-subtle); list-style: none;
}
.tz-entity-attention li > i { width: 7px; height: 7px; margin-top: 6px; border-radius: var(--tz-r-full); background: var(--tz-text-faint); }
/* Three 7px swatches read side by side — exactly the job the chart register is tuned for, and
   exactly where the bare primitives were loudest. */
.tz-entity-attention li.is-good > i { background: var(--tz-success-chart); }
.tz-entity-attention li.is-warning > i { background: var(--tz-warning-chart); }
.tz-entity-attention li.is-error > i { background: var(--tz-error-chart); }
.tz-entity-attention b { display: block; color: var(--tz-heading); font-size: var(--tz-text-sm); }
.tz-entity-attention span { display: block; margin-top: 2px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); }
.tz-entity-panel > .tz-strip { margin-top: 24px; }
.tz-entity-detail-list { margin-top: 10px; }
.tz-entity-detail-list__head,
.tz-entity-detail-list__row {
  display: grid; grid-template-columns: 34px minmax(160px, 1.1fr) minmax(160px, 1fr) minmax(160px, 1fr) minmax(130px, .8fr);
  gap: 18px; align-items: center;
}
.tz-entity-detail-list__head {
  min-height: 38px; border-bottom: 1px solid var(--tz-border); color: var(--tz-text-muted);
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
.tz-entity-detail-list__row { min-height: 67px; padding: 10px 0; border-bottom: 1px solid var(--tz-border-subtle); }
.tz-entity-detail-list__row > i { color: var(--tz-text-faint); font-family: var(--bs-font-monospace); font-size: var(--tz-text-xs); font-style: normal; }
.tz-entity-detail-list__row b { display: block; color: var(--tz-heading); font-size: var(--tz-text-sm); font-weight: 600; overflow-wrap: anywhere; }
.tz-entity-detail-list__row span { display: block; margin-top: 3px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); }
.tz-entity-detail-list--sinks .tz-entity-detail-list__head,
.tz-entity-detail-list--sinks .tz-entity-detail-list__row { grid-template-columns: 50px minmax(180px, 1.1fr) minmax(180px, 1fr) minmax(150px, .8fr); }
.tz-entity-process { margin-top: 22px; border-top: 1px solid var(--tz-border); }
.tz-entity-process > div {
  display: grid; grid-template-columns: 48px minmax(200px, .8fr) minmax(260px, 1.2fr) auto;
  gap: 20px; align-items: start; padding: 18px 0; border-bottom: 1px solid var(--tz-border-subtle);
}
.tz-entity-process > div > i {
  width: 31px; height: 31px; display: grid; place-items: center; border-radius: var(--tz-r-full);
  background: rgba(var(--tz-primary-rgb), .09); color: var(--tz-primary);
  font-family: var(--bs-font-monospace); font-size: var(--tz-text-xs); font-style: normal; font-weight: 600;
}
[data-bs-theme="dark"] .tz-entity-process > div > i {
  background: rgba(var(--tz-accent-rgb), .12); color: var(--tz-accent);
}
.tz-entity-process section > b { display: block; color: var(--tz-heading); font-family: var(--tz-font-display); font-size: 16px; }
.tz-entity-process section > code { display: block; margin-top: 5px; color: var(--tz-text-faint); font-size: var(--tz-text-xs); }
.tz-entity-process p { margin: 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.55; }
.tz-entity-process p b { color: var(--tz-text); }
.tz-entity-operation { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-top: 27px; }
.tz-entity-operation > section > div { padding: 16px 0; border-bottom: 1px solid var(--tz-border); }
.tz-entity-operation > section > div > b { display: block; color: var(--tz-heading); font-family: var(--tz-font-display); font-size: 18px; }
.tz-entity-operation > section > div > p { margin: 5px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-entity-operation__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.tz-entity-dependencies { margin-top: 37px; }
.tz-entity-dependencies .tz-entity-summary-grid { margin-top: 0; }
.tz-entity-danger { margin-top: 42px; padding-top: 17px; border-top: 1px solid rgba(var(--tz-error-rgb), .45); }
.tz-entity-danger h3 { color: var(--tz-error-ink); }
.tz-entity-danger > p { margin: 11px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-entity-danger > div { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 15px; }

.tz-entity-editor {
  display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 46px;
  align-items: start; margin-top: 37px;
}
.tz-entity-editor__toc { position: sticky; top: 18px; }
.tz-entity-editor__toc > span {
  display: block; padding-bottom: 11px; border-bottom: 1px solid var(--tz-border);
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 600;
  letter-spacing: .09em; text-transform: uppercase;
}
.tz-entity-editor__toc button {
  width: 100%; min-height: 45px; display: grid; grid-template-columns: 22px minmax(0, 1fr);
  gap: 8px; align-items: center; padding: 8px 2px; border: 0;
  border-bottom: 1px solid var(--tz-border-subtle); background: transparent;
  color: var(--tz-text-muted); text-align: left;
}
.tz-entity-editor__toc button:hover { color: var(--tz-heading); }
.tz-entity-editor__toc button.is-active { color: var(--tz-primary); }
/* ⚠️ U-11 — THE CURRENT STEP WAS THE ONLY UNREADABLE THING ON THIS PAGE, AND THE TWIN
   RAIL IN THE DOSSIER ABOVE HAD THE CURE. The label measured 1.36:1 on the dark page and the
   step number 1.02:1 on the sunken chip it sits in — an empty circle — while steps
   02-04, which are drawn in the muted alias, stayed crisp at 8.12:1. The number takes
   `currentColor`, so the two lines below are one fix, not two. */
[data-bs-theme="dark"] .tz-entity-editor__toc button.is-active { color: var(--tz-accent); }
/* U-28: 11px, was 9px — see the note on .tz-entity-dossier__toc button i. */
.tz-entity-editor__toc button i {
  min-width: 22px; padding: 2px 5px; border: 1px solid var(--tz-border-subtle);
  border-radius: var(--tz-r-full); background: var(--tz-surface-sunken); color: var(--tz-text-faint);
  font-family: var(--bs-font-monospace); font-size: var(--tz-text-xs); font-style: normal; text-align: center;
}
.tz-entity-editor__toc button b { font-size: var(--tz-text-sm); font-weight: 600; }
.tz-entity-editor__toc button.is-active i { color: currentColor; }
.tz-entity-editor__toc > p { margin: 17px 0 0; color: var(--tz-text-faint); font-size: var(--tz-text-xs); line-height: 1.5; }
.tz-entity-editor__panels { min-width: 0; }
.tz-entity-editor__panel { display: none; }
.tz-entity-editor__panel.is-active { display: block; }
.tz-entity-editor__panel > header { padding-bottom: 15px; border-bottom: 2px solid var(--tz-heading); }
.tz-entity-editor__panel > header > span {
  display: block; margin-bottom: 4px; color: var(--tz-primary); font-size: var(--tz-text-xs);
  font-weight: 600; letter-spacing: .09em; text-transform: uppercase;
}
[data-bs-theme="dark"] .tz-entity-editor__panel > header > span { color: var(--tz-accent); }
.tz-entity-editor__panel > header h2 {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-h2); font-weight: 600;
}
.tz-entity-editor__panel > header p { max-width: 72ch; margin: 6px 0 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm); }
.tz-entity-editor__fields { padding-top: 23px; }
.tz-entity-editor__fields > div { max-width: 100%; }
.tz-entity-editor__source { grid-column: 2; }
.tz-entity-editor__source.is-organized { display: none; }
/* A moved JSON-editor field is a passage of the active chapter, not a nested card. */
.tz-entity-editor__fields .card { border-right: 0; border-bottom: 0; border-left: 0; border-radius: 0; box-shadow: none; }
.tz-entity-editor__fields .card-header { padding-right: 0; padding-left: 0; background: transparent; }
.tz-entity-editor__fields .card-body { padding-right: 0; padding-left: 0; }

@media (max-width: 1099.98px) {
  .tz-entity-dossier { grid-template-columns: 190px minmax(0, 1fr); gap: 30px; }
  .tz-entity-flow { grid-template-columns: 1fr; gap: 7px; }
  .tz-entity-flow__arrow { min-height: 24px; transform: rotate(90deg); }
  .tz-entity-summary-grid, .tz-entity-operation { grid-template-columns: 1fr; }
  .tz-entity-process > div { grid-template-columns: 42px minmax(180px, .75fr) minmax(240px, 1fr); }
  .tz-entity-process > div > .tz-tag { grid-column: 2 / -1; justify-self: start; }
  .tz-entity-editor { grid-template-columns: 185px minmax(0, 1fr); gap: 28px; }
}
@media (max-width: 767.98px) {
  .tz-entity-dossier__facts { grid-template-columns: 1fr 1fr; }
  .tz-entity-dossier__facts > div:nth-child(3) { padding-left: 0; border-top: 1px solid var(--tz-border-subtle); border-left: 0; }
  .tz-entity-dossier__facts > div:nth-child(4) { border-top: 1px solid var(--tz-border-subtle); }
  .tz-entity-dossier { display: block; margin-top: 28px; }
  .tz-entity-dossier__toc { position: static; display: flex; overflow-x: auto; margin-bottom: 29px; border-bottom: 1px solid var(--tz-border); }
  .tz-entity-dossier__toc-title, .tz-entity-dossier__toc > p { display: none; }
  .tz-entity-dossier__toc button { width: auto; min-width: max-content; grid-template-columns: 1fr auto; margin-right: 16px; padding-right: 4px; border-bottom: 2px solid transparent; }
  .tz-entity-dossier__toc button.is-active { border-bottom-color: currentColor; }
  .tz-entity-dossier__toc button small { display: none; }
  .tz-entity-panel__head { display: block; }
  .tz-entity-panel__head > .tz-more { display: inline-flex; margin-top: 10px; }
  .tz-entity-detail-list__head { display: none; }
  .tz-entity-detail-list__row,
  .tz-entity-detail-list--sinks .tz-entity-detail-list__row { grid-template-columns: 28px minmax(0, 1fr); gap: 9px; padding: 14px 0; }
  .tz-entity-detail-list__row > div { grid-column: 2; }
  .tz-entity-process > div { grid-template-columns: 36px minmax(0, 1fr); gap: 11px; }
  .tz-entity-process > div > p, .tz-entity-process > div > .tz-tag { grid-column: 2; }
  .tz-entity-editor { display: block; margin-top: 27px; }
  .tz-entity-editor__toc { position: static; display: flex; overflow-x: auto; margin-bottom: 28px; border-bottom: 1px solid var(--tz-border); }
  .tz-entity-editor__toc > span, .tz-entity-editor__toc > p { display: none; }
  .tz-entity-editor__toc button { width: auto; min-width: max-content; margin-right: 16px; padding-right: 4px; border-bottom: 2px solid transparent; }
  .tz-entity-editor__toc button.is-active { border-bottom-color: currentColor; }
}


/* ==========================================================================
   Access administration — editorial directories for people and API clients
   ========================================================================== */
.tz-admin-selection {
  display: flex; align-items: center; justify-content: space-between; gap: 18px;
  padding: 12px 2px; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
}
.tz-admin-selection[hidden] { display: none; }
.tz-admin-selection__subject > span {
  display: block; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
  font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
.tz-admin-selection__subject > strong {
  display: block; margin-top: 2px; color: var(--tz-heading);
  font-family: var(--tz-font-display); font-size: var(--tz-text-kv); font-weight: 600;
}
.tz-admin-selection__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; }
/* ⚠️ There is no padding rule pairing the bar with the controls any more, and there must not be
   one: two of them existed for the ORDER THAT D6 REVERSED — the bar used to come first, so they
   pushed the controls down when it appeared. Since the bar moved below the controls neither
   selector matched anything, and a rule that matches nothing is read as a rule that is working. */
/* ⚠️ THE ROW WRAPS RATHER THAN SQUEEZING THE SEARCH BOX. Without `flex-wrap` the pills win every
   argument about space and the box shrinks under them: measured on 20 September 2026, Resources'
   seven pills left it 236px at 992px and 188px at 820px, for a placeholder that reads "Filter
   resources by name or keyword". Tables and Users reach the same state later, at 820px, because
   they have fewer and shorter pills — it is the row's behaviour, not one screen's.
   With the wrap, the pills drop to a line of their own exactly when they no longer fit beside a
   full-width box, and nothing moves on the screens where they already fit. */
/* ⚠️ The catalogue is a CONTAINER, which is what lets its rows ask how much room they have rather
   than how wide the window is — see the note on the row grid further down. `inline-size` and not
   `size`: the block's height is still its content's, which is what a table needs. */
.tz-admin-catalogue { container-type: inline-size; container-name: catalogue; }
.tz-admin-catalogue__tools {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: 13px 28px; padding: 0 0 18px; border-bottom: 2px solid var(--tz-heading);
}
.tz-admin-catalogue__filters { flex: 1 1 auto; }
.tz-admin-catalogue__search { position: relative; flex: 0 1 390px; margin: 0; }
.tz-admin-catalogue__search .bi {
  position: absolute; top: 50%; left: 13px; z-index: 1; color: var(--tz-text-muted);
  transform: translateY(-50%); pointer-events: none;
}
.tz-admin-catalogue__search input {
  width: 100%; height: 40px; padding: 7px 12px 7px 39px;
  border: 1px solid var(--tz-border); border-radius: var(--tz-r-sm);
  background: var(--tz-surface); color: var(--tz-text); outline: none;
}
/* U-16: muted, not faint. --tz-text-faint is n500, which the accessibility table reserves
   for >=18px or non-text; on white it measures 3.91:1 and this is a 14px sentence — the only
   thing the empty search box says. Same token, same reason, as `.form-control::placeholder`. */
.tz-admin-catalogue__search input::placeholder { color: var(--tz-text-muted); }
.tz-admin-catalogue__search input:focus {
  border-color: var(--tz-accent-blue); box-shadow: 0 0 0 2px rgba(var(--tz-accent-blue-rgb), .13);
}
.tz-admin-catalogue__filters { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 7px; }
/* ⚠️ RETIRED 28 SEPTEMBER 2026: it matched nothing after the filter pills moved into the context
   bar, which made them no longer descendants of `.tz-admin-catalogue`. Every pill in the product is
   on the 30px base now, and the guide's list of floors said 32 for one that no longer existed. The
   `@media (pointer: coarse)` extension to 44px is unscoped and is what actually matters on touch. */
.tz-admin-catalogue > .table-responsive {
  width: 100%; overflow-x: visible;
  border-top: var(--tz-table-head-rule-width) solid var(--tz-heading);
}
/* ⚠️ ONE HEAVY RULE, NEVER TWO STACKED. The controls close with a 2px rule and the table opens
   with one, and when nothing is drawn between them they are the same rule — so the second is
   suppressed. It used to be enough to say "the tools, then the table", but D6 put the selection
   bar between the two on 20 September and the bar is HIDDEN most of the time: the adjacency broke,
   both rules rendered, and the result was a 4px slab above the column headers that nobody chose.
   Measured on Users, Tables and Resources.

   Two selectors and not one `~`, deliberately: with the bar SHOWN the two rules are not adjacent
   any more and both are doing their own job — one closing the controls above the bar, one opening
   the table below it. What is wrong is only the case where they touch. */
.tz-admin-catalogue__tools + .table-responsive,
.tz-admin-catalogue__tools + .tz-admin-selection[hidden] + .table-responsive { border-top: 0; }
/* ⚠️ …but a matrix keeps its own scroll. `.tz-admin-catalogue > .table-responsive` above sets
   `overflow-x: visible`, which is right for a five-column directory and wrong for Data access,
   whose column count is the customer's entity count: the scroll has to stay inside the container
   or a wide matrix pushes the whole document sideways (U-18/U-19). More specific, so it wins. */
.tz-admin-catalogue > .tz-ledger-table-wrap { overflow-x: auto; }

/* ❗️THE ROW'S SUBJECT, AS A LINK (Nacho, 21 September 2026). Data access is the one catalogue
   whose row opens by clicking its name rather than by being selected, and the name was a bare
   anchor: blue, underlined on hover, so a column of principals read as a column of links. It keeps
   the catalogue's ink and weight now and takes the colour with the pointer — the same bargain the
   quiet identifier makes two lines below it. */
.tz-admin-table__subject { color: inherit; text-decoration: none; }
.tz-admin-table__subject:hover strong,
.tz-admin-table__subject:focus-visible strong,
.tz-admin-table__subject:hover,
.tz-admin-table__subject:focus-visible {
  color: var(--tz-accent-blue); text-decoration: underline; text-underline-offset: 2px;
}
/* ⚠️ Without `strong` it is a column header, not a row's subject: Data access names its entities
   in the head and each one is the way into that entity's access list. Same bargain — the header's
   own ink at rest, the colour with the pointer — so the head reads as a head and not as a row of
   links. */
.tz-admin-table { width: 100%; min-width: 0; margin-bottom: 0; table-layout: fixed; }
.tz-admin-table > tbody > tr { cursor: pointer; }
.tz-admin-table > :where(thead, tbody) > tr > :where(th, td) { padding: 13px 12px; }
.tz-admin-table > :where(thead, tbody) > tr > :first-child { padding-left: 2px; }
.tz-admin-table > :where(thead, tbody) > tr > :last-child { padding-right: 2px; }
.tz-admin-table > thead > tr > th {
  height: 43px; vertical-align: middle; color: var(--tz-text-muted);
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase;
}
.tz-admin-table__select { width: 36px; }
.tz-admin-table__identity { width: 30%; min-width: 0; }
.tz-admin-table__identity strong {
  display: block; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-kv); font-weight: 600; line-height: 1.3; overflow-wrap: anywhere;
}
.tz-admin-table__identity > span {
  display: block; margin-top: 4px; color: var(--tz-text-muted); font-size: var(--tz-text-sm);
  overflow-wrap: anywhere;
}
.tz-admin-table__access { min-width: 0; }

/* ❗️THE USERS CATALOGUE'S FIVE COLUMNS (Nacho, 21 September 2026). "Access" used to carry three
   unrelated answers stacked in one cell — the role, how the account signs in, and how many
   entities it may see — and it is two columns now, with the third gone to Data access where it is
   read and written. Stated and summing to 100%, through a `<colgroup>`: `table-layout: fixed`
   takes its geometry from the first row, so widths on the `<td>`s alone are ignored. */
.tz-user-table__identity { width: 26%; }
.tz-user-table__role { width: 13%; }
.tz-user-table__kind { width: 13%; }
.tz-user-table__locale { width: 17%; }
.tz-user-table__activity { width: 18%; }
.tz-user-table__status { width: 13%; }
/* ⚠️ @container, NOT @media, and this block was the one that got left behind on 28 September 2026
   — its siblings at the SAME cut, in the SAME container, were converted and these were not (found by
   a blind review). These `<col>`s live inside `<section class="tz-admin-catalogue">`, which declares
   `container-type: inline-size`, and the context bar takes up to 344px the window cannot see: at a
   1400px window the table's own box is 944, so the designed relief — drop the zone — never fired and
   `table-layout: fixed` overflowed the nowrap chips instead. Same number, asked of the right box. */
@container catalogue (max-width: 1199.98px) {
  /* The zone is the first thing to go: it is the one value on the row a reader can find in the
     editor, and the two chips beside it cannot shrink below their words. */
  .tz-user-table__locale { display: none; }
  .tz-user-table__identity { width: 32%; }
  .tz-user-table__role { width: 15%; }
  .tz-user-table__kind { width: 15%; }
  .tz-user-table__activity { width: 22%; }
  .tz-user-table__status { width: 16%; }
}
.tz-admin-table__tags { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; }
.tz-admin-table__secondary {
  display: block; margin-top: 6px; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
}
.tz-admin-table__activity { min-width: 0; }
.tz-admin-table__activity time,
.tz-admin-table__date time {
  color: var(--tz-text); font-family: var(--bs-font-monospace); font-size: var(--tz-text-sm);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.tz-admin-table__activity small,
.tz-admin-table__date small {
  display: block; margin-top: 5px; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
}
.tz-admin-table__status { width: 130px; min-width: 0; white-space: nowrap; }
.tz-admin-table > tbody > tr:has(.form-check-input:checked) > * {
  --bs-table-bg-state: var(--tz-info-bg);
}
.tz-admin-table > tbody > tr:has(.form-check-input:focus-visible) > * {
  --bs-table-bg-state: var(--tz-surface-sunken);
}
.tz-admin-catalogue__empty {
  border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
}
.tz-table-catalogue { margin-top: 38px; }
.tz-table-directory > tbody > tr:hover .tz-admin-table__identity strong { color: var(--tz-accent-blue); }
/* ❗️AND THEY ADD UP HERE TOO (20 September 2026). Same cause: the relations column left and its
   share was never reassigned, so a fixed layout spread it wherever it liked. Four columns and the
   radio, stated, summing to 100%. */
.tz-table-directory__identity { width: 34%; }
.tz-table-directory__dataset { width: 22%; min-width: 0; }
/* State and the three conservation tags, which wrap onto two lines on a narrow window. */
.tz-table-directory__state { width: 30%; }
.tz-table-directory__records { width: 14%; }
.tz-table-directory__records strong {
  color: var(--tz-heading); font-family: var(--tz-font-display); font-size: 18px;
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.tz-table-directory__status small {
  display: block; margin-top: 6px; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
}
.tz-table-editor__lead {
  max-width: 58ch; margin: 0 0 25px; color: var(--tz-text-muted);
  font-size: var(--tz-text-sm); line-height: 1.55;
}
/* The ETL action is a choice between three modes of one form: the house tab line (the L2
   sub-nav, json-editor's top tabs) — words on a hairline, the active one in teal with a 2px
   underline. It used to be a three-cell segmented control between two rules with a tinted
   active cell, which is a component this design does not have anywhere else. */
.tz-etl-mode {
  display: flex; gap: 4px; margin-top: 2px; border-bottom: 1px solid var(--tz-border);
}
.tz-etl-mode__choice {
  margin-bottom: -1px; padding: 8px 14px; border-bottom: 2px solid transparent;
  color: var(--tz-text-muted); font-size: var(--tz-text-sm); font-weight: 500; cursor: pointer;
}
.tz-etl-mode__choice:hover { color: var(--tz-heading); border-bottom-color: var(--tz-border-strong); }
.tz-etl-mode .btn-check:focus-visible + .tz-etl-mode__choice {
  outline: 2px solid var(--tz-accent-blue); outline-offset: -2px;
}
.tz-etl-mode .btn-check:checked + .tz-etl-mode__choice {
  color: var(--tz-primary); font-weight: 600; border-bottom-color: var(--tz-primary);
}
[data-bs-theme="dark"] .tz-etl-mode .btn-check:checked + .tz-etl-mode__choice {
  color: var(--tz-accent); border-bottom-color: var(--tz-accent);
}
/* A fieldset of switches is a section of the panel: air above it (the grid's 16px gutter alone
   left the legend sitting on the previous field), a rule, the legend, then one hairline between
   options. Every switch in a panel lives in ONE such section — a lone switch between two rules
   is a section with no name, which is what the ETL panel used to paint three times. */
.tz-table-editor__options {
  margin-top: 30px; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
}
.tz-table-editor__options > legend { float: none; width: auto; margin: 15px 0 0; }
.tz-table-editor__group + .tz-table-editor__group { border-top: 1px solid var(--tz-border-subtle); }
.tz-table-editor__group[hidden] { display: none; }
.tz-table-editor__options > p {
  margin: 4px 0 2px; color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.45;
}
.tz-table-editor__option { min-height: 52px; margin: 0; padding-top: 14px; padding-bottom: 14px; }
.tz-table-editor__option + .tz-table-editor__option { border-top: 1px solid var(--tz-border-subtle); }
.tz-table-editor__option label { cursor: pointer; }
.tz-table-editor__option strong { display: block; color: var(--tz-heading); font-size: var(--tz-text-sm); font-weight: 600; }
.tz-table-editor__option small {
  display: block; margin-top: 3px; color: var(--tz-text-muted); font-size: var(--tz-text-xs); line-height: 1.4;
}
.tz-table-editor__nested {
  display: grid; gap: 13px; margin: 0; padding: 15px 0 4px 2.5rem;
  border-top: 1px solid var(--tz-border-subtle);
}
.tz-table-editor__nested[hidden] { display: none; }

/* Settings administration — a growing page of editorial areas, beginning with
   installable sample projects. The content is a dossier, not a card grid. */
.tz-settings-section { margin-top: 38px; }
.tz-settings-section__head {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 350px); gap: 46px;
  align-items: end; padding: 20px 0 23px;
  border-top: 2px solid var(--tz-heading); border-bottom: 1px solid var(--tz-border);
}
.tz-settings-section__eyebrow {
  display: block; margin-bottom: 6px; color: var(--tz-text-muted);
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
}
.tz-settings-section__head h2 {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-h2); font-weight: 600; letter-spacing: -.01em;
}
.tz-settings-section__head > div > p {
  max-width: 72ch; margin: 8px 0 0; color: var(--tz-text-muted);
  font-size: var(--tz-text-sm); line-height: 1.55;
}
.tz-settings-section__task-note {
  display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 10px;
  padding-left: 20px; border-left: 1px solid var(--tz-border);
  color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.5;
}
.tz-settings-section__task-note .bi { padding-top: 2px; color: var(--tz-accent-blue); font-size: var(--tz-text-kv); }
.tz-settings-section__task-note p { margin: 0; }
.tz-settings-section__task-note a { font-weight: 600; }
.tz-settings-sample {
  display: grid; grid-template-columns: minmax(0, 1fr) 285px; gap: 48px;
  align-items: center; padding: 29px 0 30px; border-bottom: 1px solid var(--tz-border);
}
.tz-settings-sample__main { min-width: 0; }
.tz-settings-sample__status { margin-bottom: 10px; }
.tz-settings-sample__main > h3 {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: 23px; font-weight: 600; letter-spacing: -.01em;
}
.tz-settings-sample__description {
  max-width: 82ch; margin: 8px 0 0; color: var(--tz-text-muted);
  font-size: var(--tz-text-base); line-height: 1.6;
}
.tz-settings-sample__facts {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  margin: 24px 0 0; padding: 0;
}
.tz-settings-sample__facts > div {
  min-width: 0; padding: 0 16px; border-left: 1px solid var(--tz-border-subtle);
}
.tz-settings-sample__facts > div:first-child { padding-left: 0; border-left: 0; }
.tz-settings-sample__facts dt {
  margin-bottom: 6px; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
  font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
}
.tz-settings-sample__facts dd { overflow: hidden; margin: 0; text-overflow: ellipsis; }
.tz-settings-sample__facts code,
.tz-settings-sample__facts a {
  color: var(--tz-text); font-family: var(--bs-font-monospace); font-size: var(--tz-text-xs);
  overflow-wrap: anywhere;
}
.tz-settings-sample__facts a:hover { color: var(--bs-link-color); }
.tz-settings-sample__actions {
  padding-left: 24px; border-left: 1px solid var(--tz-border-subtle);
}
.tz-settings-sample__actions > p {
  margin: 0 0 14px; color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.5;
}
.tz-settings-sample__actions form { margin: 0; }
.tz-settings-sample__buttons { display: flex; flex-wrap: wrap; gap: 7px; }
.tz-settings-section__empty { border-bottom: 1px solid var(--tz-border); }
.tz-settings-confirm {
  padding: 14px 1px; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
}
.tz-settings-confirm > span {
  display: block; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
  font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
.tz-settings-confirm > strong {
  display: block; margin-top: 4px; color: var(--tz-heading);
  font-family: var(--tz-font-display); font-size: 18px; font-weight: 600;
}
.tz-token-secret { margin-bottom: 36px; }
.tz-token-secret__copy { max-width: 820px; margin-top: 14px; }
.modal-body > .tz-field { margin: 15px 0; }
.modal-body > .tz-field .form-control[readonly] {
  color: var(--tz-heading); font-weight: 600;
}
.offcanvas.tz-admin-editor { --bs-offcanvas-width: min(480px, 100vw); }
.tz-admin-editor__title {
  margin: 0; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-h2); font-weight: 600; letter-spacing: -.01em;
}
.tz-admin-dialog__title {
  color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-h2); font-weight: 600; letter-spacing: -.01em;
}
.tz-admin-table__sort { display: inline-flex; align-items: center; gap: 5px; }
.tz-admin-table__sort a { color: inherit; text-decoration: none; }
.tz-admin-table__sort a:hover { color: var(--bs-link-color); text-decoration: underline; }
.tz-file-table__size {
  width: 130px; font-family: var(--bs-font-monospace); font-size: var(--tz-text-sm);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.tz-file-table__type { width: 20%; min-width: 0; }
.tz-admin-pagination {
  display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 8px;
  padding-top: 16px; border-top: 1px solid var(--tz-border);
}

/* Resources are Golden's configuration graph, so their directory has one extra level over the
   ordinary administration tables: a stable functional taxonomy and an inspectable relation map. */
/* ❗️One column since 19 September 2026: the families moved from a rail on the left to the row of
   filter pills every other administration catalogue uses, so there is nothing beside the table. */
.tz-resource-catalogue { margin-top: 38px; }
/* ⚠️ `.tz-resource-family` stays: the CREATE chooser still uses it, which is a list of resource
   kinds inside a drawer and not a filter. The rail's own two classes are gone. */
.tz-resource-family {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px;
  width: 100%; min-height: 45px; margin: 0; padding: 8px 2px;
  border: 0; border-bottom: 1px solid var(--tz-border-subtle); border-radius: 0;
  background: transparent; color: var(--tz-text-muted); text-align: left;
}
.tz-resource-family:hover { color: var(--tz-heading); }
.tz-resource-family.is-active {
  color: var(--tz-primary); font-weight: 600;
}
[data-bs-theme="dark"] .tz-resource-family.is-active { color: var(--tz-accent); }
/* U-28: 11px, was 9px — see the note on .tz-entity-dossier__toc button i. */
.tz-resource-family small {
  min-width: 20px; padding: 2px 6px; border-radius: var(--tz-r-full);
  background: var(--tz-surface-sunken); color: var(--tz-text-faint);
  font-family: var(--bs-font-monospace); font-size: var(--tz-text-xs); font-weight: 500;
  font-variant-numeric: tabular-nums; text-align: center;
}
.tz-resource-family.is-active small { color: currentColor; }
/* ⚠️ The two rules that styled this catalogue's counts as a bare `<small>` went on 20 September
   2026: the count is `.tz-filter-pill__n` now, the badge Data access was already drawing, and it
   is painted by `golden-filter-counts.js` on every catalogue rather than by hand on this one. */
.tz-resource-directory { min-width: 0; }
.tz-resource-table-wrap {
  width: 100%; overflow-x: visible;
  border-top: var(--tz-table-head-rule-width) solid var(--tz-heading);
}
/* ❗️FOUR COLUMNS AND THEY ADD UP (Nacho, 20 September 2026). The relations column went when the
   dependency graph left the listing, and its 14% stayed in the `<colgroup>` as a ghost: measured
   at 1920px, the table was 1488px wide and its columns summed to 1295, so the last 193px of every
   row were empty and the rules ran out past the content. The four that are left sum to 100%. */
.tz-resource-table__identity { width: 44%; }
.tz-resource-table__purpose { width: 19%; min-width: 0; }
.tz-resource-table__purpose > strong {
  display: block; color: var(--tz-heading); font-size: var(--tz-text-sm); line-height: 1.35;
}
.tz-resource-table__purpose small {
  display: block; margin-top: 4px; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
}
.tz-resource-relation-count {
  display: block; width: max-content; max-width: 100%; margin: 0; padding: 1px 0;
  border: 0; border-bottom: 1px dotted var(--tz-border-strong); background: transparent;
  color: var(--bs-link-color); font-size: var(--tz-text-xs); text-align: left;
}
.tz-resource-relation-count + .tz-resource-relation-count { margin-top: 4px; }
.tz-resource-relation-count:hover { border-bottom-style: solid; }
.tz-resource-table__updated { width: 19%; }
.tz-resource-table__updated time {
  display: block; max-width: 100%; color: var(--tz-text);
  font-family: inherit; font-size: var(--tz-text-sm); line-height: 1.4;
  white-space: normal; overflow-wrap: anywhere;
}
.tz-resource-table__status { width: 18%; padding-left: 18px !important; white-space: normal; }
.tz-resource-table__status .tz-pill { margin: 2px 3px 2px 0; }
.tz-resource-table > tbody > tr:hover .tz-resource-table__identity strong { color: var(--bs-link-color); }
.tz-resource-relations {
  margin-top: 34px; padding: 22px 2px 25px;
  border-top: 2px solid var(--tz-heading); border-bottom: 1px solid var(--tz-border);
}
.tz-resource-relations[hidden] { display: none; }
.tz-resource-relations__head {
  display: flex; align-items: start; justify-content: space-between; gap: 24px;
}
.tz-resource-relations__head h2 {
  margin: 3px 0 5px; color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: var(--tz-text-h2); font-weight: 600; letter-spacing: -.01em;
}
.tz-resource-relations__head p {
  max-width: 78ch; margin: 0; color: var(--tz-text-muted); font-size: var(--tz-text-sm);
}
.tz-resource-relations__grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 38px; margin-top: 22px;
}
.tz-resource-relations__grid h3 {
  margin: 0; padding-bottom: 8px; border-bottom: 1px solid var(--tz-border);
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase;
}
.tz-resource-relations__grid ul { margin: 0; padding: 0; list-style: none; }
.tz-resource-relations__grid li {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 9px;
  min-height: 43px; padding: 9px 0; border-bottom: 1px solid var(--tz-border-subtle);
}
/* ⚠️ No colour of its own: the identifier vocabulary governs here too (20 September 2026). This
   list used to paint its ids `--bs-link-color` at rest, which made the relations panel the one
   place in the plane where an identifier is blue. What says these entries go somewhere is the
   kind tag before them and the arrow after them, and the colour arrives on hover like everywhere
   else. `min-width` stays: the anchor is this grid's middle column. */
.tz-resource-relations__grid li > a { min-width: 0; }
.tz-resource-relations__grid li > .bi { color: var(--tz-text-faint); font-size: var(--tz-text-xs); }
.tz-resource-relations__empty {
  margin: 0; padding: 14px 0; border-bottom: 1px solid var(--tz-border-subtle);
  color: var(--tz-text-muted); font-size: var(--tz-text-sm);
}

/* Resource create/edit/view. One long technical form still reads as a page: title, actions,
   air, then fields. The ID in these selectors deliberately outranks json-editor's
   Bootstrap theme and our later generic bridge without leaking into record forms. */
/* ❗️U-26: the `display: block` that took this head out of the shared flex row, and the action
   group's `margin-top: 21px` below it, are gone with the markup — the actions are inside
   `.tz-page-head__main` and on the title's line. Only the head's own bottom margin is this
   screen's business, because the form below brings its own. */
.tz-resource-form-head { margin-bottom: 0; }
.tz-resource-form {
  margin: 42px 0 52px;
}
#editor_holder.tz-resource-form__editor {
  min-width: 0; padding: 0;
}
/* No vertical gutter, on purpose. json-editor hides a dependent property by hiding its column
   and leaves the enclosing .row in the flow, and Bootstrap pulls every row up by the gutter
   (margin-top: -gutter-y). A DATASET column hides eight properties, so eight empty rows pulled
   the container 32px above its last visible field and the row controls landed on top of the
   last checkbox. The rhythm between fields is the .form-group margin below, not the gutter. */
#editor_holder.tz-resource-form__editor .row {
  --bs-gutter-x: 28px;
  --bs-gutter-y: 0;
}
#editor_holder.tz-resource-form__editor .form-group { margin-bottom: 22px; }
/* The micro-label is `.form-label`'s own since U-27 (theme.css) — this screen was where it was
   first written, one copy of it, and what is left here is only the margin this form's rhythm
   asks for. */
#editor_holder.tz-resource-form__editor .form-label { margin-bottom: 7px; }
#editor_holder.tz-resource-form__editor .form-text,
#editor_holder.tz-resource-form__editor p:not([class]) {
  margin-top: 7px; color: var(--tz-text-muted); font-size: var(--tz-text-sm); line-height: 1.5;
}
/* No control-height exception here any more: the resource form used to hold its fields at 40px
   while every other screen sat at 35, 36, 38 or 44 depending on the widget. One height, 36, in
   theme.css and in `.choices` above (17 September 2026). */
#editor_holder.tz-resource-form__editor .card {
  margin: 26px 0 0 !important; padding: 20px 0 0;
  border: 0; border-top: 1px solid var(--tz-border); border-radius: 0;
  background: transparent; box-shadow: none;
}
#editor_holder.tz-resource-form__editor .card > .card-header,
#editor_holder.tz-resource-form__editor .card > .card-body,
#editor_holder.tz-resource-form__editor .card > .card-footer {
  padding-right: 0; padding-left: 0; background: transparent;
}
/* A section title is a serif heading that opens a passage: the hairline sits ABOVE it, then
   air, then the title, then its fields. json-editor builds the title as an <h3> holding the
   <label> and the "add" button group side by side, so the heading is a flex row with a gap —
   the button no longer touches the word, and it takes the sans face back explicitly, because
   Bootstrap's .btn inherits font-family and would otherwise render in the heading's serif. */
#editor_holder.tz-resource-form__editor .card-title {
  display: flex !important; align-items: center; gap: 14px;
  margin: 30px 0 16px; padding-top: 22px; border-top: 1px solid var(--tz-border);
  color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: 20px; font-weight: 600; letter-spacing: 0;
}
#editor_holder.tz-resource-form__editor .card-title > label { margin: 0; }
#editor_holder.tz-resource-form__editor .card-title > .btn-group:empty { display: none; }
#editor_holder.tz-resource-form__editor .card-title .btn {
  font-family: var(--tz-font-sans); font-weight: 500; letter-spacing: 0;
}
/* The panel under a visible title used to carry its own top rule 26px below the heading, which
   split the title from its content. The title owns the rule now, so that panel goes flush.
   The root panel (its title is hidden, its rule is the page-head rule) and the tab panes (their
   title is the tab label) keep theirs. */
#editor_holder.tz-resource-form__editor .card-title:not([data-schemapath="root"] > *):not(.tab-pane > *) ~ .card {
  margin-top: 0 !important; padding-top: 0; border-top: 0;
}
/* theme.css flattens every json-editor object title. Resource schemas contain real nested
   sections, so bring those back while keeping the root and duplicate tab captions out. */
#editor_holder.tz-resource-form__editor > [data-schemapath="root"] > .je-object__title,
#editor_holder.tz-resource-form__editor .tab-pane > .je-object__title { display: none !important; }
#editor_holder.tz-resource-form__editor .btn.json-editor-btntype-add { margin-left: 0; }
/* Array rows inside the form are fields laid out in columns, not a data table: no ink rule on
   top, no hairline per row, no hover, and the cells flush left so the inputs line up with the
   fields above them. The only rules are the header's underline and a closing hairline. */
#editor_holder.tz-resource-form__editor table {
  margin-bottom: 20px; border-top: 0;
  border-bottom: 1px solid var(--tz-border); border-collapse: collapse;
}
#editor_holder.tz-resource-form__editor table > thead > tr > th {
  padding: 0 14px 8px 0; border-bottom: 1px solid var(--tz-border); background: transparent;
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
}
#editor_holder.tz-resource-form__editor table > tbody > tr > td {
  padding: 5px 14px 5px 0; border: 0; background: transparent; vertical-align: middle;
}
#editor_holder.tz-resource-form__editor table > tbody > tr:first-child > td { padding-top: 10px; }
#editor_holder.tz-resource-form__editor table > tbody > tr:last-child > td { padding-bottom: 10px; }
#editor_holder.tz-resource-form__editor table > tbody > tr > td:last-child,
#editor_holder.tz-resource-form__editor table > thead > tr > th:last-child { padding-right: 0; }
/* The 22px field rhythm above does not belong inside a cell: it made every row 22px taller
   than its input and pushed the input above the row's centre, so the row buttons — centred —
   sat 11px lower than the field beside them. */
#editor_holder.tz-resource-form__editor td > .form-group { margin-bottom: 0; }
#editor_holder.tz-resource-form__editor td > .btn-group { white-space: nowrap; }
/* Side tabs. json-editor hard-codes the tab column as col-md-2 — a sixth of the width, about
   200px here — and an indexer tab reads "Index [name-sur1-sur2-email-fuzzy]: Nombre + 1er
   apellido + 2o apellido + email (FUZZY combinado)", which wrapped to five lines and turned the
   rail into a column of paragraphs. The rail takes what a label needs, up to a third of the row,
   and the content column takes the rest. A label gets two lines and then an ellipsis; the full
   text is on the tab's title attribute, set from resource-form.html. */
#editor_holder.tz-resource-form__editor .row:has(> .col-md-2 > .nav-pills) > .col-md-2 {
  flex: 0 0 auto; width: auto; min-width: 200px; max-width: 30%;
}
#editor_holder.tz-resource-form__editor .row:has(> .col-md-2 > .nav-pills) > .col-md-10 {
  flex: 1 1 0; width: auto; min-width: 0;
}
#editor_holder.tz-resource-form__editor .nav-pills .nav-link {
  padding: 7px 14px 7px 12px; font-size: var(--tz-text-sm); line-height: 1.35;
}
#editor_holder.tz-resource-form__editor .nav-pills .nav-link > span {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden; overflow-wrap: anywhere;
}

/******************** THE RECORD FORM'S MULTI-VALUED COLUMNS ********************/
/* ⚠️ A MULTI-VALUED COLUMN IS A FIELD, NOT A SECTION, and until now the form said otherwise —
   in two type treatments, on the same screen, for the same thing.

   json-editor's array editor builds its header as
   `<span class="h3 card-title level-N" style="display:inline-block">`, which is a different code
   path from the one that labels a single-valued column (`getFormInputLabel` -> `.form-label`, the
   11px uppercase micro-label of U-27). Nothing in the data-view asks for that and nothing here
   ever chose it: the class lands on `.card-title`, whose spec in theme.css is the CARD heading —
   Inter 16px/600 in `--tz-heading`. So on residencial_master "Nombre" was an 11px muted label and
   "Correo", "Móvil" and "Teléfono fijo" — the three array columns — were 16px near-black headings
   shouting over their own fields (Nacho, 29 September 2026).

   The resource form hit the same collision and answered it the other way, twenty lines above: its
   schemas DO contain real nested sections, so there the title takes the 20px display serif. Two
   answers because they are two different things, and that is why neither is written on
   `.card-title` itself.

   ⚠️ Scoped to `#editor_holder.tz-record-form__editor`, whose class exists only to be this
   selector's handle — `.tz-form` is generic and would reach whatever adopts it next. */
#editor_holder.tz-record-form__editor .card-title {
  /* !important: json-editor writes `display:inline-block` inline on the element itself. */
  display: flex !important; align-items: center; gap: var(--tz-sp-2);
  margin: 0 0 var(--tz-sp-2);
  font-family: var(--tz-font-sans); font-size: var(--tz-text-xs); font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; line-height: 1.3;
  color: var(--tz-text-muted);
}
/* The add/remove group sits beside the word and must not inherit the label's type: uppercase at
   .08em turns a button into a second caption. */
#editor_holder.tz-record-form__editor .card-title > .btn-group:empty { display: none; }
#editor_holder.tz-record-form__editor .card-title .btn {
  font-size: var(--tz-text-xs); font-weight: 500; letter-spacing: 0; text-transform: none;
}

/* ❗️VIEW MODE HAS EXACTLY ONE HOVER AFFORDANCE: THE FIELD'S OWN OUTLINE.

   U-24 took the frames off so that reading looks like reading, and the outline that comes back
   under the pointer is what says "this is a field, and Edit would let you into it". A single text
   field does that already — `theme.css:1283`, `.form-control:hover:not(:focus):not(:disabled)`
   painting `--tz-border-strong`, which a `readonly` control matches and a `disabled` one does not.

   The other three kinds each failed it differently, and none of the three was anybody's decision —
   they are the three places where json-editor's markup happens to miss, or overshoot, a selector
   written for something else (Nacho, 29 September 2026, measured on residencial_master):

     · the dropdown did nothing at all. Choices hides the real `<select>` — which is `disabled`
       and therefore excluded from the hover rule twice over — and draws `.choices__inner`, which
       is not a `.form-control` and so is named by nothing;
     · a multi-valued column drew a grey slab across the whole row AND the input's outline inside
       it, two affordances for one field. json-editor renders an array as `<table class="table">`,
       so `golden.css:21` — the row tint for ALL DATA TABLES — reached a form control. It is wrong
       in edit mode for the same reason, so it goes in both;
     · a date lit its outline with the right-hand corners square. Its control is an `.input-group`
       whose two icon buttons are hidden in view mode (`.json-editor-btn-calendar` above) but still
       in the DOM, so Bootstrap's `:not(:last-child)` keeps flattening the corners against a
       neighbour nobody can see.

   ⚠️ An EMPTY multi-valued column still shows nothing under the pointer, and that is structural
   rather than unfixed: json-editor renders no row for it, so there is no field there to outline. */
/* ❗️THE AIR IS BOUGHT SIDEWAYS ONLY, BECAUSE THERE IS ONE CONTROL HEIGHT AND IT IS 36px
   (`theme.css:1250`, and that rule says of itself that it is a rule).

   Three answers were tried against Nacho's "unos píxeles a derecha e izquierda, para que el texto
   y el posible icono no lleguen a tocar la caja", and two of them are wrong in ways worth writing
   down, because each looks right until it is measured:

     · padding on hover shifts the value sideways under the pointer;
     · padding always breaks U-24's `padding-left: 0`, which is what lines a value up with its own
       caption — the whole reason reading looks like reading here;
     · an `outline` at `outline-offset: 4px` does keep the value still and does give air, and it
       was live for about an hour. It grows the ring on FOUR sides, so the box a reader sees under
       the pointer came out 44px tall against the product's 36. Aire, sí; una caja de otro tamaño,
       no (Nacho, 29 September 2026).

   So the box itself grows, and only to the left: `padding-left` puts the air inside it and an equal
   negative `margin-left` takes it back out of the layout, which leaves the value exactly where the
   caption is and the box hanging `--tz-sp-1` into the gutter. `width` is restored by the same
   amount so the right edge does not travel with it. Height is untouched, and so is every row. */
/* ⚠️ THE GEOMETRY GOES ON THE WRAPPER, NOT ON THE BOX THAT CARRIES THE BORDER, and for a
   dropdown those are two different elements. `.choices` has `overflow: hidden`, so hanging
   `.choices__inner` 4px to its left cut the highlighted box's left side clean off — measured on
   root.gender: wrapper from 400.0, inner from 396.0 (Nacho, 29 September 2026: "el lado izquierdo
   de la caja resaltada está cortado", también en idioma y país). Moving the wrapper moves the
   clip with it, and the inner then fills it as it always did. */
#editor_holder.tz-record-form__editor.tz-form--readonly .form-control:not(.choices__input),
#editor_holder.tz-record-form__editor.tz-form--readonly .choices {
  margin-left: calc(-1 * var(--tz-sp-1));
  width: calc(100% + var(--tz-sp-1));
}
#editor_holder.tz-record-form__editor.tz-form--readonly .form-control:not(.choices__input),
#editor_holder.tz-record-form__editor.tz-form--readonly .choices__inner {
  padding-left: var(--tz-sp-1);
}
/* The dropdown's hover, which nothing reached: Choices hides the real `<select>` — `disabled`, and
   so excluded from `theme.css:1283` twice over — and draws `.choices__inner`, which is not a
   `.form-control`. The ink is the same one every other field lights up with. */
#editor_holder.tz-record-form__editor.tz-form--readonly .choices:hover .choices__inner {
  border-color: var(--tz-border-strong);
}
/* The cell mark rode ON the control's right border: `td.compact` pads 12px, the control fills the
   rest, so `right: 12px` put the icon's right edge exactly on the box's edge — the two lines
   touching. It sits inside the control now, and the cell reserves the room so a long value cannot
   slide under it. Only a cell that HAS a mark pays that width: on a table of 98px cells, reserving
   it on every one would be a third of the field spent on an icon most of them do not have. */
#editor_holder.tz-record-form__editor.tz-form--readonly .tz-field__mark--cell { right: 16px; }
#editor_holder.tz-record-form__editor.tz-form--readonly td.compact:has(> .tz-field__mark--cell) .form-control,
#editor_holder.tz-record-form__editor.tz-form--readonly td.compact:has(> .tz-field__mark--cell) .choices__inner {
  padding-right: 30px;
}
#editor_holder.tz-record-form__editor .table > tbody > tr:hover > * {
  --bs-table-bg-state: transparent;
}
/* ❗️A ROW OF AN ARRAY ALIGNS ON ITS CONTROLS, WHICH IS NOT WHAT CENTRING EACH CELL DOES.
   `golden.css`'s `table { vertical-align: middle !important }` is written for a data table, where
   a cell holds one value and centring reads well. Here a cell holds a control AND, in edit mode,
   the note under it — so a cell with a note centres the PAIR and its control rides up, while its
   neighbour without one centres alone. Measured on root.document in edit mode: same 108px row,
   controls at y=1132 and y=1161, 29px apart across five columns (Nacho, 29 September 2026).
   Top, so every control in a row starts on the same line and the notes hang below it. */
#editor_holder.tz-record-form__editor table > tbody > tr > td { vertical-align: top !important; }
#editor_holder.tz-record-form__editor.tz-form--readonly .input-group > .form-control {
  border-radius: var(--tz-r-md);
}

/* The creation dialog is deliberately a catalogue, not a fake first page of the schema editor. */
.tz-resource-create-dialog { --bs-modal-width: min(1180px, calc(100vw - 36px)); }
.tz-resource-create-dialog .modal-content {
  height: min(760px, calc(100vh - 48px)); max-height: calc(100vh - 48px); overflow: hidden;
}
.tz-resource-create__head { align-items: center; padding: 23px 28px 20px; }
.tz-resource-create__body {
  display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 36px;
  min-height: 0; padding: 0 28px; overflow: hidden;
}
.tz-resource-create__families {
  overflow: visible; min-width: 0; padding: 23px 0 30px;
  border-right: 0; background: transparent;
}
.tz-resource-create__catalogue { overflow-y: auto; min-width: 0; padding: 23px 0 30px; }
.tz-resource-create__search { display: block; width: 100%; max-width: none; margin: 0 0 22px; }
.tz-resource-create__group + .tz-resource-create__group { margin-top: 28px; }
.tz-resource-create__group > h3 {
  margin: 0; padding: 0 2px 9px; border-bottom: 2px solid var(--tz-heading);
  color: var(--tz-heading); font-family: var(--tz-font-display);
  font-size: 18px; font-weight: 600;
}
.tz-resource-create__options {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 28px;
}
.tz-resource-type {
  position: relative; min-width: 0; min-height: 126px; padding: 15px 38px 14px 8px;
  border: 0; border-bottom: 1px solid var(--tz-border-subtle); border-radius: 0;
  background: transparent; color: var(--tz-text); text-align: left;
}
.tz-resource-type:hover { background: var(--tz-surface-sunken); }
.tz-resource-type.is-selected { background: var(--tz-surface-sunken); }
.tz-resource-type__mark {
  position: absolute; top: 15px; right: 8px; display: grid; width: 20px; height: 20px;
  place-items: center; border: 1px solid var(--tz-border); border-radius: 999px;
  color: transparent; font-size: 11px;
}
.tz-resource-type.is-selected .tz-resource-type__mark {
  border-color: var(--tz-primary); color: var(--tz-primary);
}
/* U-11: the chosen card lost both its tick and its title on dark (1.02:1 on the sunken
   fill the selection paints). The test wizard's copy of these two rules resolves through
   --tzw-brand and has been right all along; the catalogue's copy names the primitive. */
[data-bs-theme="dark"] .tz-resource-type.is-selected .tz-resource-type__mark {
  border-color: var(--tz-accent); color: var(--tz-accent);
}
.tz-resource-type > strong {
  display: block; margin-bottom: 5px; color: var(--tz-heading);
  font-family: var(--tz-font-display); font-size: var(--tz-text-kv); font-weight: 600;
}
.tz-resource-type.is-selected > strong { color: var(--tz-primary); }
[data-bs-theme="dark"] .tz-resource-type.is-selected > strong { color: var(--tz-accent); }
.tz-resource-type > span:not(.tz-resource-type__tags) {
  display: block; color: var(--tz-text-muted); font-size: var(--tz-text-xs); line-height: 1.5;
}
.tz-resource-type__tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; }
/* U-28: 11px, was 9px — see the note on .tz-entity-dossier__toc button i. */
.tz-resource-type__tags i {
  padding: 2px 7px; border: 1px solid var(--tz-border); border-radius: 999px;
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-style: normal;
  font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
}
.tz-resource-create__empty { padding-top: 40px; border: 0; }
.tz-resource-create__footer { flex-wrap: nowrap; padding: 14px 28px; background: var(--tz-surface-sunken); }
.tz-resource-create__selection { min-width: 0; margin-right: auto; }
.tz-resource-create__selection strong {
  display: block; overflow: hidden; max-width: 54ch; color: var(--tz-heading);
  font-family: var(--tz-font-display); font-size: 16px; font-weight: 600;
  text-overflow: ellipsis; white-space: nowrap;
}
.tz-resource-create__selection span {
  display: block; margin-top: 2px; color: var(--tz-text-muted); font-size: var(--tz-text-xs);
}

@media (max-width: 991.98px) {
  .tz-resource-catalogue { grid-template-columns: 1fr; gap: 25px; }
  .tz-resource-families {
    display: flex; overflow-x: auto; padding: 0 0 13px; border-right: 0; border-bottom: 1px solid var(--tz-border);
  }
  .tz-resource-families__title { display: none; }
  .tz-resource-families > .tz-resource-family {
    grid-template-columns: auto auto; width: auto; min-width: max-content;
    border-left: 0; border-bottom: 3px solid transparent;
  }
  .tz-resource-families > .tz-resource-family.is-active { border-bottom-color: var(--tz-primary); }
  .tz-resource-table__updated { display: none; }
  .tz-resource-table__identity { width: 48%; }
  .tz-resource-table__purpose { width: 28%; }
  .tz-resource-table__status { width: 24%; }
}

@media (max-width: 767.98px) {
  .tz-resource-table-wrap { overflow-x: auto; }
  .tz-resource-table { min-width: 720px; }
  .tz-resource-relations__grid { grid-template-columns: 1fr; gap: 24px; }
  .tz-resource-create-dialog { margin: 0; min-height: 100%; }
  .tz-resource-create-dialog .modal-content { min-height: 100vh; max-height: 100vh; border: 0; border-radius: 0; }
  .tz-resource-create__body { display: block; overflow-y: auto; }
  .tz-resource-create__families {
    display: flex; overflow-x: auto; padding: 10px 0; border-right: 0; border-bottom: 1px solid var(--tz-border);
  }
  .tz-resource-create__families > .tz-entity-dossier__toc-title { display: none; }
  .tz-resource-create__families .tz-resource-family {
    grid-template-columns: auto auto; width: auto; min-width: max-content;
    border-left: 0; border-bottom: 3px solid transparent;
  }
  .tz-resource-create__families .tz-resource-family.is-active { border-bottom-color: var(--tz-primary); }
  .tz-resource-create__catalogue { overflow: visible; padding: 20px 0 28px; }
  .tz-resource-create__options { grid-template-columns: 1fr; }
  .tz-resource-create__footer { align-items: stretch; flex-wrap: wrap; }
  .tz-resource-create__selection { flex-basis: 100%; }
}
.tz-file-upload__note { display: block; margin-bottom: 22px; }
.tz-file-upload__note p { margin: 0; }
.tz-file-upload__note p + p { margin-top: 6px; }
.tz-file-upload__note ~ .tz-field { margin: 0 0 18px; }
.tz-field__optional { color: var(--tz-text-faint); font-weight: 500; text-transform: none; }
.tz-event-queue-table > tbody > tr { cursor: pointer; }
.tz-event-table > tbody > tr { cursor: default; }
.tz-event-table > tbody > tr[data-selectable="true"] { cursor: pointer; }
.tz-event-queue-table__metric { width: 130px; }
.tz-event-queue-table__metric a { color: inherit; text-decoration: none; }
.tz-event-queue-table__metric strong {
  color: var(--tz-heading); font-family: var(--tz-font-display); font-size: 24px;
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.tz-event-queue-table__metric a:hover strong { color: var(--bs-link-color); }
.tz-event-queue-table__next {
  display: block; max-width: 190px; margin-top: 6px;
  color: var(--tz-text-muted); font-size: var(--tz-text-xs); line-height: 1.4;
}
.tz-event-page__state {
  display: flex; align-items: center; flex-wrap: wrap; gap: 9px;
  margin-top: 12px; color: var(--tz-text-muted); font-size: var(--tz-text-sm);
}
.tz-event-id-search {
  display: flex; align-items: center; flex: 0 1 600px; gap: 7px; min-width: 0;
}
.tz-event-id-search .tz-admin-catalogue__search { flex: 1 1 auto; }
.tz-event-table__identity { width: 220px; }
.tz-event-table__payload { width: 42%; min-width: 0; }
.tz-event-payload__toggle {
  display: inline-flex; align-items: center; gap: 7px; margin: 0 0 8px; padding: 0;
  border: 0; background: transparent; color: var(--bs-link-color);
  font-size: var(--tz-text-xs); font-weight: 600;
}
.tz-event-payload__toggle:hover { text-decoration: underline; }
.tz-event-payload pre {
  overflow: auto; max-height: 320px; margin: 0; padding: 11px 13px;
  border: 1px solid var(--tz-border-subtle); border-radius: var(--tz-r-sm);
  background: var(--tz-surface-sunken); color: var(--tz-text);
  font-size: var(--tz-text-xs); line-height: 1.5; white-space: pre-wrap; word-break: break-word;
}

@media (max-width: 767.98px) {
  .tz-admin-selection { display: block; }
  .tz-admin-selection[hidden] { display: none; }
  .tz-admin-selection__actions { justify-content: flex-start; margin-top: 10px; }
  .tz-admin-catalogue__tools { display: block; }
  .tz-admin-catalogue__search { max-width: none; }
  .tz-admin-catalogue__filters { justify-content: flex-start; margin-top: 11px; }
  .tz-admin-catalogue > .table-responsive { overflow: visible; }
  /* One card per row: the head is dropped, each row becomes a grid, and every cell prints its
     own column name from `data-label`. Decision 10 of the 9 September register put the record
     listing on this same mechanism (U-19: `sample_customer` was 2,657px wide in a 350px
     window), so the selectors below are a LIST rather than a second copy — the record listing
     opts in with `.tz-records--cards` and inherits every rule and every later fix. It is not
     given `.tz-admin-table` itself because that class also carries a desktop treatment —
     `table-layout: fixed`, its own cell padding, the selected-row tint — and the record
     listing's desktop table is settled. */
  .tz-admin-table,
  .tz-records--cards { display: block; min-width: 0; }
  .tz-admin-table > thead,
  .tz-records--cards > thead { display: none; }
  .tz-admin-table > tbody,
  .tz-records--cards > tbody { display: block; }
  .tz-admin-table > tbody > tr,
  .tz-records--cards > tbody > tr {
    display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 10px 12px;
    padding: 17px 0; border-top: 1px solid var(--tz-border-subtle);
  }
  .tz-admin-table > tbody > tr > td,
  .tz-records--cards > tbody > tr > td {
    display: block; grid-column: 2; min-width: 0; padding: 0; border: 0;
  }
  .tz-admin-table > tbody > tr > .tz-admin-table__select {
    grid-column: 1; grid-row: 1 / span 5; width: auto; padding: 2px 0 0;
  }
  .tz-admin-table > tbody > tr > .tz-admin-table__identity { grid-row: 1; }
  .tz-admin-table td[data-label]::before,
  .tz-records--cards td[data-label]::before {
    content: attr(data-label); display: block; margin-bottom: 4px;
    color: var(--tz-text-muted); font-size: var(--tz-text-xs); font-weight: 600;
    letter-spacing: .07em; text-transform: uppercase;
  }
  .tz-admin-table__status { white-space: normal; }
  .tz-table-directory__records { width: auto; text-align: left !important; }
  .tz-file-table__size { width: auto; }
  .tz-admin-pagination { justify-content: stretch; }
  /* No select column, so the 28px gutter would be 28px of nothing. The event tables already
     had this case; the record listing is the third. */
  .tz-event-queue-table--readonly > tbody > tr,
  .tz-event-table--readonly > tbody > tr,
  .tz-records--cards > tbody > tr { grid-template-columns: minmax(0, 1fr); }
  .tz-event-queue-table--readonly > tbody > tr > td,
  .tz-event-table--readonly > tbody > tr > td,
  .tz-records--cards > tbody > tr > td { grid-column: 1; }
  /* ❗️A card cell takes the card's whole width. The data-view's list gives each column a width in
     percent, written inline on the cell for the desktop table, and inside a card that percent
     was still honoured: "Tax ID" sat in 42px of a 350px card with the rest empty beside it
     (Nacho, 18 September 2026). !important because the width is an inline style. */
  .tz-records--cards > tbody > tr > td,
  .tz-records--cards > tbody > tr > th { width: auto !important; max-width: none !important; }
  /* ❗️AND THE CARD PACKS ITS FIELDS. One field per line left half of every line empty next to
     a postcode or a two-letter country: the card is a wrapping row now, each field takes the
     width its words need from 140px up and grows to close the line, and a long value wraps
     inside its own field. The identity leads on a line of its own. The clusters ledger keeps
     its grid: it has a check column the flow would swallow (Nacho, 18 September 2026).

     ⚠️ AND IT IS EVERY CARD TABLE, not the record listing alone (19 September 2026). The
     administration tables were left on the two-column grid above, where a cell the `> td` rule
     does not reach — a `<th scope="row">` identity, a status cell — was auto-placed into an
     implicit column and sized by its content: on /security/users the name and the email wrapped
     inside 93px with 250px of empty card beside them. The flow does not care which tag a cell
     is, which is the point.

     The grid declarations above and the per-table ones below stay written and are inert for
     these tables: `display: flex` here is the later rule. They are kept rather than deleted
     because `.tz-clusters` still uses them. */
  .tz-admin-table > tbody > tr,
  .tz-records--cards:not(.tz-clusters) > tbody > tr {
    display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px 18px;
  }
  .tz-admin-table > tbody > tr > td,
  .tz-admin-table > tbody > tr > th,
  .tz-records--cards:not(.tz-clusters) > tbody > tr > td,
  .tz-records--cards:not(.tz-clusters) > tbody > tr > th {
    flex: 1 1 140px; min-width: 0; overflow-wrap: anywhere;
    /* A number is right-aligned on the desktop table so magnitudes line up down a column; in a
       card there is no column to line up with, and a lone figure hugging the right edge of its
       field reads as misplaced. */
    text-align: left !important;
  }
  /* The select checkbox is a control, not a field: it takes its own width and the identity
     beside it closes the first line. */
  .tz-admin-table > tbody > tr > .tz-admin-table__select { flex: 0 0 auto; width: auto; padding: 2px 0 0; }
  .tz-admin-table > tbody > tr > .tz-admin-table__identity { flex: 1 1 60%; }
  .tz-records--cards:not(.tz-clusters) > tbody > tr > .tz-records__identity,
  .tz-records--cards:not(.tz-clusters) > tbody > tr > th[scope="row"],
  .tz-admin-table > tbody > tr > th[scope="row"]:not(.tz-admin-table__identity) { flex-basis: 100%; }
  /* ⚠️ The desktop listing gives each cell one line and ellipsis at 240px, which is the right
     scan line for a table and reads as a truncated value in a card — where the label above it
     has just promised the whole thing. A card wraps instead, and an unbroken token (an id, a
     URL, a code) breaks rather than widening the page: this is the rule that keeps the ONE
     horizontal scroll bar the listing is allowed to the wide content that owns it. */
  .tz-records--cards > tbody > tr > td {
    max-width: none; overflow: visible; white-space: normal;
    overflow-wrap: anywhere; text-overflow: clip;
  }
  /* The card IS a record, so its identifier leads and carries no label of its own — the same
     place the admin card puts its `__identity` cell. */
  .tz-records--cards > tbody > tr > .tz-records__identity { grid-row: 1; }
  .tz-records--cards > tbody > tr > .tz-records__identity .tz-id {
    color: var(--tz-heading); font-size: var(--tz-text-sm); font-weight: 600;
  }
  /* U-19's third listing, `/access`. It is the only one whose leading cell is a `th scope="row"`
     — a row header, kept because it is what names the row to a screen reader — and the rules above
     are written against `td`. Restated here rather than widening those selector lists: two other
     screens put a `th scope="row"` inside a listing that has not been asked for this treatment,
     and widening the list would card them up without anyone deciding to. It leads the card, the
     way an identifier leads a record's. */
  .tz-records--cards > tbody > tr > th[scope="row"] {
    display: block; grid-column: 1; grid-row: 1; min-width: 0; padding: 0; border: 0;
    color: var(--tz-heading); font-size: var(--tz-text-sm); font-weight: 600;
    overflow-wrap: anywhere;
  }
  /* U-19's second listing: the duplicate clusters ledger. It opts into `.tz-records--cards` like
     the record listing, and it is the ONE card in the product that carries a checkbox — the rule
     above drops the 28px gutter because neither the record listing nor the read-only event tables
     have a selection column. Given back here, with the checkbox spanning the whole card exactly as
     `.tz-admin-table__select` does: a bulk merge whose control had scrolled away from the group it
     acts on would be worse on a phone than on a desktop, not better. The identifier leads through
     `.tz-records__identity`, which is the record listing's own cell and needs nothing new. */
  .tz-clusters.tz-records--cards > tbody > tr { grid-template-columns: 28px minmax(0, 1fr); }
  .tz-clusters.tz-records--cards > tbody > tr > td { grid-column: 2; }
  .tz-clusters.tz-records--cards > tbody > tr > .tz-clusters__checkcol {
    grid-column: 1; grid-row: 1 / span 8; width: auto; padding: 2px 0 0;
  }
  /* A figure is right-aligned so a column of them can be compared down the page. In a card there
     is no column: the value would sit at the far edge from the label that names it. */
  .tz-clusters.tz-records--cards > tbody > tr > .tz-num { text-align: left; }
  /* A table wrapped inside a card keeps the page from scrolling sideways. */
  .tz-records-card .tz-ledger-table-wrap { overflow-x: visible; }
  .tz-event-queue-table__metric { width: auto; }
  .tz-event-id-search { width: 100%; }
  .tz-event-table { min-width: 0; }
  .tz-event-table__identity,
  .tz-event-table__payload { width: auto; min-width: 0; }
}

@media (max-width: 991.98px) {
  .tz-settings-section__head { grid-template-columns: 1fr; gap: 18px; align-items: start; }
  .tz-settings-section__task-note { padding-left: 0; border-left: 0; }
  .tz-settings-sample { grid-template-columns: 1fr; gap: 22px; }
  .tz-settings-sample__actions {
    padding: 18px 0 0; border-top: 1px solid var(--tz-border-subtle); border-left: 0;
  }
}

@media (max-width: 575.98px) {
  .tz-settings-sample__facts { grid-template-columns: 1fr 1fr; gap: 18px 0; }
  .tz-settings-sample__facts > div:nth-child(odd) { padding-left: 0; border-left: 0; }
}

/* ============ The home (front door), 8 September 2026 ============
   Direction B for the viewer and the steward, D for the administrator. Both are drawn as
   passages of the page — the mock's cards are not ported, because the style guide says a
   four-sided border claims the thing inside is an object and none of these are.

   .tz-lede is the priority band: the one thing to do on the left, at full width, the estate's
   quiet figures on the right behind a rule. It is .tz-hero's mirror — that one puts the narrow
   figure first because its subject IS the figure; here the subject is an action. */
.tz-lede {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 300px);
  padding: 4px 0 34px; border-bottom: 1px solid var(--tz-border);
}
.tz-lede__main { padding-right: 44px; min-width: 0; }
.tz-lede__side { padding-left: 44px; border-left: 1px solid var(--tz-border); }
/* Standing on its own, with a section heading of its own above it, it keeps neither. */
.tz-lede__side.is-flush { padding-left: 0; border-left: 0; max-width: 420px; }
.tz-lede__form { margin-top: 14px; }
/* Wide enough for a real entity description, and it stops growing: the field beside it is what
   should take the room a wide window gives. */
.tz-lede__select { height: 36px; flex: 0 1 220px; width: auto; }
.tz-lede__actions { margin-top: 24px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
@media (max-width: 991px) {
  .tz-lede { grid-template-columns: 1fr; }
  .tz-lede__main { padding: 0 0 28px; }
  .tz-lede__side { padding: 28px 0 0; border-left: 0; border-top: 1px solid var(--tz-border); }
}

/* The two planes: equal columns, one rule between them, and nothing that ranks one above the
   other — which is the entire argument of direction D. */
.tz-planes {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  padding: 4px 0 30px; border-bottom: 1px solid var(--tz-border);
}
.tz-plane { min-width: 0; padding-right: 40px; }
.tz-plane + .tz-plane { padding: 0 0 0 40px; border-left: 1px solid var(--tz-border); }
.tz-plane__t {
  font-family: var(--tz-font-display); font-size: 24px; font-weight: 600; letter-spacing: -0.01em;
  color: var(--tz-heading); margin: 2px 0 0; line-height: 1.25;
}
.tz-plane__kpis { display: flex; gap: 32px; flex-wrap: wrap; margin-top: 18px; }
.tz-plane__kpi b {
  display: block; font-family: var(--tz-font-display); font-size: 26px; font-weight: 600;
  color: var(--tz-heading); font-variant-numeric: tabular-nums lining-nums; line-height: 1.1;
}
.tz-plane__kpi span {
  display: block; margin-top: 2px; font-size: var(--tz-text-xs); font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--tz-text-muted);
}
.tz-plane__list { margin-top: 22px; }
.tz-plane__item {
  display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; gap: 12px; align-items: center;
  padding: 10px 0; border-top: 1px solid var(--tz-border-subtle);
}
.tz-plane__item > .bi { color: var(--tz-text-muted); }
.tz-plane__copy { min-width: 0; }
.tz-plane__copy b {
  display: block; font-weight: 600; color: var(--tz-heading);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tz-plane__copy span { display: block; font-size: var(--tz-text-sm); color: var(--tz-text-muted); }
.tz-plane__foot { margin: 18px 0 0; display: flex; gap: 22px; flex-wrap: wrap; }
@media (max-width: 991px) {
  .tz-planes { grid-template-columns: 1fr; }
  .tz-plane { padding: 0 0 26px; }
  .tz-plane + .tz-plane { padding: 26px 0 0; border-left: 0; border-top: 1px solid var(--tz-border); }
}

/* The row list: the home's own way of listing entities. Rows, not the catalogue's cards —
   the catalogue card is an object you pick from a shelf, and these lines are a passage that
   ranks. The name column takes the slack; the metrics are tabular so they line up down the
   page even when their labels differ. */
.tz-rows { border-top: 2px solid var(--tz-border); }
.tz-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto;
  gap: 10px 26px; align-items: baseline;
  padding: 13px 4px; border-bottom: 1px solid var(--tz-border-subtle);
}
.tz-row:hover { background: var(--tz-surface-sunken); }
.tz-row__name { min-width: 0; }
.tz-row__name b {
  display: block; font-family: var(--tz-font-display); font-size: 16px; font-weight: 600;
  color: var(--tz-heading); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tz-row__name span { display: block; font-size: var(--tz-text-sm); color: var(--tz-text-muted); }
.tz-row__m { text-align: right; min-width: 88px; }
/* The steward's home, direction C: one lane per front of the job, side by side, stacking on a
   narrow window. A lane's rows carry fewer measures than the estate rows they came from, so the
   row's four-column grid would leave a hole where a measure is absent: the lane's rows flow
   instead, with the name taking what is left. */
.tz-home-lanes { display: grid; grid-template-columns: 1fr 1fr; gap: 0 56px; }
.tz-home-lane__row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px 26px; }
.tz-home-lane__row > .tz-row__name { flex: 1 1 10rem; }
.tz-home-lane__row > .tz-more { margin-left: auto; }
/* ⚠️ `.tz-row__name span` and `.tz-row__m span` are both `display: block`, which is right for the
   estate rows they were written for and wrong for two things this lane puts inside them: a pill,
   which would stretch the width of the cell, and the "/100" after a mean, which would drop to a
   line of its own. Both are inline here, and only here. */
.tz-home-lane__row .tz-row__name .tz-pill { display: inline-flex; }
.tz-home-lane__row .tz-row__m b span { display: inline; }
.tz-row__of, .tz-kv__of { font-size: .72em; font-weight: 400; color: var(--tz-text-muted); }
.tz-lede__sidemore { margin: 12px 0 0; }
@media (max-width: 991.98px) { .tz-home-lanes { grid-template-columns: minmax(0, 1fr); gap: 0; } }
.tz-row__m b {
  display: block; font-weight: 600; color: var(--tz-heading);
  font-variant-numeric: tabular-nums lining-nums;
}
.tz-row__m span {
  display: block; font-size: var(--tz-text-xs); color: var(--tz-text-muted);
  letter-spacing: .04em;
}
.tz-row__s { min-width: 96px; }
@media (max-width: 767px) {
  .tz-row { grid-template-columns: minmax(0, 1fr) auto; }
  .tz-row__s { grid-column: 1 / -1; }
}

/* ════════════ DENSITY — from the role, overridable on the profile ═════════════════════════════
   D-9 of 27 September 2026. ADMIN draws high, every other role normal, and the profile overrides
   both. The value is in the DEFAULT and not in the switch: a density control is found by 5–20 % of
   users, and the role is already in the session — it separates the reader who checks from the one
   who lives in the tool. `#golden.density()` is the one bean that resolves it and stamps
   `data-tz-density` on <html>; nothing else in the product reads a role to decide a size.

   ⚠️ WHAT DENSITY MAY MOVE, AND WHAT IT MUST NOT. Padding, and the row floors that padding cannot
   reach. **No type size drops and no pressable target shrinks** — WCAG 2.2 (2.5.8) asks for 24×24
   and the selection radio already measures 14×14, passing only through the *equivalent target*
   exception because the whole row is clickable. That is precisely why the row cannot shrink until
   it stops being one.

   ⚠️ AND A FLOOR IS NOT A PADDING. Take the entity catalogue's padding from 13px to 8 and the row
   does not move one pixel, because `min-height: 104px` holds it up from below. "Padding only" was
   the first draft of this decision and it would have delivered nothing on the screen an
   administrator lives on. The nine floors below are density's; the eight accessibility floors and
   the two reserved spaces named in the style guide are not, and are deliberately absent here. */
[data-tz-density="high"] .table > :where(thead, tbody) > tr > :where(th, td) { padding-top: 6px; padding-bottom: 6px; }
[data-tz-density="high"] .tz-admin-table > :where(thead, tbody) > tr > :where(th, td) { padding-top: 7px; padding-bottom: 7px; }
[data-tz-density="high"] .tz-rank2__row { padding-top: 8px; padding-bottom: 8px; }
/* ⚠️ The first group's own rule is 13px at normal density (`.tz-rank2 > :first-child`), so a flat
   14 here INCREASED it by a pixel — measured on a blind review, and the only rule in this block whose
   effect went the wrong way. Density never adds space: 12 for the first, 14 for the one that follows
   a list of rows and needs to separate from it. */
[data-tz-density="high"] .tz-rank2__group { padding-top: 14px; padding-bottom: 7px; }
[data-tz-density="high"] .tz-rank2 > .tz-rank2__group:first-child { padding-top: 12px; }
[data-tz-density="high"] .tz-queue__row { padding-top: 8px; padding-bottom: 8px; }
[data-tz-density="high"] .tz-entity-catalogue__row { padding-top: 8px; padding-bottom: 8px; }
[data-tz-density="high"] .tz-entity-detail-list__row { padding-top: 6px; padding-bottom: 6px; }
[data-tz-density="high"] .tz-kv { padding-top: 6px; padding-bottom: 6px; }

/* The nine the style guide lists, each cut by about a fifth. ⚠️ `.tz-entity-catalogue__row` is an
   <a>, so it is a pressable target and may never go under 44px — 80 leaves it ample room, and this
   is the rule wherever a row is itself the control. */
[data-tz-density="high"] .tz-entity-catalogue__row { min-height: 80px; }
[data-tz-density="high"] .tz-entity-catalogue__head { min-height: 34px; }
[data-tz-density="high"] .tz-entity-detail-list__row { min-height: 52px; }
[data-tz-density="high"] .tz-entity-detail-list__head { min-height: 32px; }
[data-tz-density="high"] .tz-entity-dossier__facts > div { min-height: 68px; padding-top: 13px; padding-bottom: 11px; }
[data-tz-density="high"] .tz-resource-relations__grid li { min-height: 36px; }
[data-tz-density="high"] .tz-entity-flow__column > header { min-height: 40px; }
[data-tz-density="high"] .tz-wstep { min-height: 50px; }
[data-tz-density="high"] #resource-test .tz-test-type.tz-resource-type { min-height: 86px; }
/* ⚠️ The tenth, and the only one with a hard stop under it: this row CONTAINS a switch, so the row
   is the target. 46 is as low as it goes, and 44 is the reason. */
[data-tz-density="high"] .tz-table-editor__option { min-height: 46px; }
