/* theme.css — remaps Bootstrap 5.3 to Trazadera tokens and styles base elements.
   Loaded AFTER bootstrap.css and tokens.css. Component-level restyles are added
   per-screen in later phases. */

:root,
[data-bs-theme="light"] {
  /* ---- Semantic aliases: the ONLY things the dark theme redefines. Component
     rules consume these, never a raw ramp position, so a second theme is one
     block instead of ~190 edits. Light values resolve exactly as before. ---- */
  --tz-bg:             var(--tz-white);        /* page canvas — the page IS the paper.
     A deliberate brand extension (the book says n50 pages / white cards): flat
     sections sit directly on the page, so the page carries the paper colour.
     See docs/golden-style-guide.md § Brand extensions. */
  --tz-surface:        var(--tz-white);        /* cards, modals, dropdowns, sub-nav, inputs */
  --tz-surface-sunken: var(--tz-n100);         /* inset panels, chips, meter tracks, row hover */
  --tz-surface-hover:  var(--tz-n200);         /* one step beyond sunken: button hover/pressed fill */
  --tz-border:         var(--tz-n200);         /* outer hairlines: cards, inputs, modals */
  --tz-border-subtle:  var(--tz-n100);         /* inner dividers: card head/foot, table rows */
  --tz-border-strong:  var(--tz-n300);         /* hover / emphasis borders */
  --tz-heading:        var(--tz-primary-dark); /* h1-h6, card + page titles, stat values */
  --tz-text:           var(--tz-n800);         /* strong body: input text, meta values */
  --tz-text-muted:     var(--tz-n600);         /* default body and muted text (AA floor) */
  --tz-text-faint:     var(--tz-n500);         /* large text / non-text only — see AA notes */
  --tz-text-disabled:  var(--tz-n400);
  /* ══ THE LIGHT LADDER: one hue, three rungs, one perceptual step ══════════════════════════
     Nacho, 28 September 2026: "the semantic system, perfectly symmetric". The three rungs of a
     semantic hue in light mode are

         --tz-*-deep    the word INSIDE a tinted panel — a pill, a badge, an alert
         --tz-*-ink     the same word on the page surface, ONE STEP lighter
         --tz-*-chart   a painted area, a further step out toward the primitive

     ⚠️ ONE STEP MEANS ONE ΔE, WHICH IS NOT ONE RATIO. The four ramps are not the same length —
     #92400E→#F59E0B is far longer than #991B1B→#EF4444 — so an equal mix percentage gives a step
     nearly twice as long for the amber as for the red, which is exactly the asymmetry this
     replaces. The percentages below are each solved to put the ink ΔE ≈ 10 from its own deep:
     measured, 9.8 / 9.8 / 10.6 / 9.9. That is the symmetry, and it is why the numbers differ.

     Why a step at all, rather than ink == deep as it was until today: a tinted ground eats
     contrast, so a word on it wants to be deeper than the same word on paper. Reading the two side
     by side is what found it — the quality page shows a green figure beside its own green band.

     ⚠️ AND WHAT CANNOT BE MADE SYMMETRIC, so nobody spends another afternoon on it: the distance
     from a word to ITS OWN AREA is 11.7 / 26.9 / 20.5, not one number. Equalising that as well was
     searched and the whole space is empty: every solution that holds the inks above 5:1 and the
     bands ΔE 40 apart puts the error band back on the raw primitive (#E33F3F and up), which is the
     saturation the chart register exists to calm. The amber's ramp is simply longer than the red's.
     What the step DID buy is that every one of those three distances shrank — from 18.7 / 34.1 /
     28.2 — and the bands moved slightly further apart, 47.2 → 49.0.

     ⚠️ In DARK there is no ladder to build: the panel inverts, so its word is the pale tint while
     a word on the page and a painted area both have to be light and converge on their own —
     measured, ΔE 11.0 for the red and 0.0 for the amber and the green. The dark rungs below are
     composed for contrast on n900 and nothing else. */
  --tz-error-ink:      color-mix(in srgb, var(--tz-error-deep) 64%, var(--tz-error));
  --tz-warning-ink:    color-mix(in srgb, var(--tz-warning-deep) 81%, var(--tz-warning));
  --tz-success-ink:    color-mix(in srgb, var(--tz-success-deep) 75%, var(--tz-success));
  --tz-info-ink:       color-mix(in srgb, var(--tz-info-deep) 52%, var(--tz-info));

  --tz-error-chart:    color-mix(in srgb, var(--tz-error) 70%, var(--tz-error-ink));
  --tz-warning-chart:  color-mix(in srgb, var(--tz-warning) 70%, var(--tz-warning-ink));
  --tz-success-chart:  color-mix(in srgb, var(--tz-success) 70%, var(--tz-success-ink));
  /* ⚠️ The fourth rung, added 28 September 2026: the ladder had three because nothing painted an
     AREA in the info hue — and the quality histogram's own bars did, on the bare primitive, which
     is the register error the whole pass removed. A rung missing is how an exception gets written
     instead. */
  --tz-info-chart:     color-mix(in srgb, var(--tz-info) 70%, var(--tz-info-ink));

  --bs-primary: var(--tz-primary);
  --bs-primary-rgb: var(--tz-primary-rgb);

  --bs-body-bg: var(--tz-bg);
  --bs-body-color: var(--tz-n600);
  --bs-emphasis-color: var(--tz-primary-dark);
  --bs-secondary-color: var(--tz-n600);
  --bs-secondary-bg: var(--tz-n100);
  --bs-tertiary-bg: var(--tz-n50);
  --bs-border-color: var(--tz-n200);

  --bs-link-color: var(--tz-accent-blue);
  --bs-link-color-rgb: 0, 128, 170;
  --bs-link-hover-color: var(--tz-accent-blue-dark);

  --bs-font-sans-serif: var(--tz-font-sans);
  --bs-font-monospace: var(--tz-font-mono);

  --bs-border-radius: var(--tz-r-md);
  --bs-border-radius-sm: var(--tz-r-sm);
  --bs-border-radius-lg: var(--tz-r-lg);
  --bs-border-radius-xl: var(--tz-r-xl);

  --bs-box-shadow-sm: var(--tz-shadow-sm);
  --bs-box-shadow: var(--tz-shadow-md);
  --bs-box-shadow-lg: var(--tz-shadow-lg);

  /* teal-tinted gray ramp */
  --bs-gray-100: var(--tz-n100); --bs-gray-200: var(--tz-n200);
  --bs-gray-300: var(--tz-n300); --bs-gray-400: var(--tz-n400);
  --bs-gray-500: var(--tz-n500); --bs-gray-600: var(--tz-n600);
  --bs-gray-700: var(--tz-n700); --bs-gray-800: var(--tz-n800);
  --bs-gray-900: var(--tz-n900);

  /* Semantic hues — Bootstrap ships these as its own yellow/red/green, and
     .bg-warning / .bg-danger / .text-danger read straight from them. */
  --bs-success-rgb: var(--tz-success-rgb);
  --bs-warning-rgb: var(--tz-warning-rgb);
  --bs-danger-rgb:  var(--tz-error-rgb);
  --bs-info-rgb:    var(--tz-info-rgb);

  /* *-bg-subtle / *-border-subtle are Sass-static literals (e.g.
     --bs-primary-bg-subtle: #cfe2ff). ~60 uses app-wide — every keyboard
     shortcut chip, both toasts, the merge column — so leaving them unmapped
     leaks Bootstrap blue across the whole UI. */
  --bs-primary-bg-subtle:   var(--tz-info-bg);
  --bs-secondary-bg-subtle: var(--tz-n100);
  --bs-success-bg-subtle:   var(--tz-success-bg);
  --bs-info-bg-subtle:      var(--tz-info-bg);
  --bs-warning-bg-subtle:   var(--tz-warning-bg);
  --bs-danger-bg-subtle:    var(--tz-error-bg);
  --bs-light-bg-subtle:     var(--tz-n50);
  --bs-dark-bg-subtle:      var(--tz-n300);

  /* Bootstrap's <code> colour is a literal pink (#d63384 light / #e685b5 dark) and
     was never remapped — off-brand in both themes. */
  --bs-code-color: var(--tz-text);
  --bs-form-invalid-color: var(--tz-error-fg);
  --bs-form-invalid-border-color: var(--tz-error);
  --bs-form-valid-color: var(--tz-success-fg);
  --bs-form-valid-border-color: var(--tz-success);

  --bs-primary-border-subtle:   var(--tz-n200);
  --bs-secondary-border-subtle: var(--tz-n200);
  --bs-success-border-subtle:   var(--tz-success);
  --bs-info-border-subtle:      var(--tz-info);
  --bs-warning-border-subtle:   var(--tz-warning);
  --bs-danger-border-subtle:    var(--tz-error);
}

/* ❗️U-28. The ranked count / kv value size, which the style guide's display ramp has named since
   the reskin ("Ranked count / kv value, Source Serif 4, 16–17px") while no token carried it — so
   eight rules wrote the number by hand and 87 nodes across nine screens rendered at a size
   that was, on the evidence of the stylesheets, off the ramp. Declaring it changes not one pixel;
   what it changes is that the next one of these is a token reference and the audit stops counting
   it as a deviation. Ruled 13 September 2026.

   ⚠️ IT BELONGS IN `tokens.css`, in the type scale, whose comment says in as many words that it is
   "one source for every font-size" — and it is here because the 13 September wave gave this block
   `golden.css` and `theme.css` and gave `tokens.css` to nobody. Move the declaration into the
   scale, between `--tz-text-lg` and `--tz-text-h4`, the moment one change may touch both files:
   it is one line and every use is already a `var()`. */
:root {
  --tz-text-kv: 17px;
}

/* ============================================================================
   DARK THEME. Values from trazadera-branding SKILL.md § Dark Mode Guidelines:
   page n950, card n900 with an n800 border, body text n300, headings white,
   accent unchanged (it reads well on dark), shadows same teal tint at higher
   opacity. Only the aliases are redefined — every component rule already
   consumes them, which is what the P7 migration was for.
   ============================================================================ */
[data-bs-theme="dark"] {
  --tz-bg:             var(--tz-n900);         /* paper, dark: the surface tone itself */
  --tz-surface:        var(--tz-n900);
  --tz-surface-sunken: var(--tz-n800);
  --tz-surface-hover:  var(--tz-n700);
  --tz-border:         var(--tz-n800);
  --tz-border-subtle:  var(--tz-n800);
  --tz-border-strong:  var(--tz-n700);
  --tz-heading:        var(--tz-white);
  --tz-text:           var(--tz-n200);
  --tz-text-muted:     var(--tz-n300);
  --tz-text-faint:     var(--tz-n400);
  --tz-text-disabled:  var(--tz-n500);

  /* Status pairs swap: the tint becomes the text, the deep shade the fill. */
  --tz-success-bg: var(--tz-success-deep); --tz-success-fg: var(--tz-success-tint);
  --tz-warning-bg: var(--tz-warning-deep); --tz-warning-fg: var(--tz-warning-tint);
  --tz-error-bg:   var(--tz-error-deep);   --tz-error-fg:   var(--tz-error-tint);
  --tz-info-bg:    var(--tz-info-deep);    --tz-info-fg:    var(--tz-info-tint);

  /* ⚠️ U-22 — INK DOES NOT FOLLOW THE PANEL SWAP. The pair above is right for a tinted
     panel and wrong for a red word on the page: taking --tz-error-fg for a destructive
     button's ink handed it #FEE2E2 in dark, so Delete rendered pale pink and read as
     indistinguishable in weight from the Edit beside it — legible (12.24:1) and no longer
     a warning, which is the whole of what the colour is for.
     The primitive itself, #EF4444, measures 3.97:1 on n900 and misses AA at 13px, and
     tokens.css is the only place a hex may be written — so the ink is composed from two
     primitives that are already there. 80/20 gives #F26464: 4.82:1 on the button's own
     surface and unmistakably red (R 242 against G/B 100). */
  --tz-error-ink: color-mix(in srgb, var(--tz-error) 80%, var(--tz-error-tint));
  /* ⚠️ And the amber needs no lightening at all, which is the one place it differs from the red:
     the primitive #F59E0B measures 6.96:1 on n900 as it stands, where #EF4444 manages 3.97 and
     has to be mixed. So this is the primitive, and the mix above is not a house style to copy. */
  --tz-warning-ink: var(--tz-warning);
  /* Same shape as the amber: the primitive is 5.89:1 on n900 where the deep is 1.95, so on
     dark the ink IS the primitive and no mix is needed. */
  --tz-success-ink: var(--tz-success);
  /* The blue is the one that does need a mix: the primitive #0080AA measures 3.32:1 on n900
     and misses AA, so it is lifted toward its own tint until it passes — 5.25:1. */
  --tz-info-ink: color-mix(in srgb, var(--tz-info) 70%, var(--tz-info-tint));

  --tz-shadow-sm: var(--tz-shadow-sm-dark);
  --tz-shadow-md: var(--tz-shadow-md-dark);
  --tz-shadow-lg: var(--tz-shadow-lg-dark);
  --tz-shadow-xl: var(--tz-shadow-xl-dark);

  /* Bootstrap ships its own [data-bs-theme=dark] block earlier in bootstrap.css.
     Ours loads later and wins on what it sets, so point every var we care about
     back at the aliases rather than inheriting Bootstrap's slate palette. */
  --bs-body-bg: var(--tz-bg);
  --bs-body-color: var(--tz-text-muted);
  --bs-emphasis-color: var(--tz-heading);
  --bs-secondary-color: var(--tz-text-muted);
  --bs-secondary-bg: var(--tz-surface-sunken);
  --bs-tertiary-bg: var(--tz-bg);
  --bs-border-color: var(--tz-border);
  --bs-link-color: var(--tz-accent);
  --bs-link-hover-color: var(--tz-white);
  --bs-secondary-bg-subtle: var(--tz-surface-sunken);
  --bs-light-bg-subtle: var(--tz-surface-sunken);
  --bs-dark-bg-subtle: var(--tz-n800);

  /* Bootstrap's dark block fills these with navy/slate. bg-primary-subtle is the
     keyboard-shortcut chip in 19 templates; the danger/success ones are the toasts. */
  --bs-primary-bg-subtle:   var(--tz-info-bg);
  --bs-success-bg-subtle:   var(--tz-success-bg);
  --bs-info-bg-subtle:      var(--tz-info-bg);
  --bs-warning-bg-subtle:   var(--tz-warning-bg);
  --bs-danger-bg-subtle:    var(--tz-error-bg);
  --bs-primary-border-subtle:   var(--tz-border);
  --bs-secondary-border-subtle: var(--tz-border);
  --bs-success-border-subtle:   var(--tz-success);
  --bs-info-border-subtle:      var(--tz-info);
  --bs-warning-border-subtle:   var(--tz-warning);
  --bs-danger-border-subtle:    var(--tz-error);
  --bs-code-color: var(--tz-text);
  --bs-link-color-rgb: var(--tz-accent-rgb);
  --bs-form-invalid-color: var(--tz-error-fg);
  --bs-form-invalid-border-color: var(--tz-error);
  --bs-form-valid-color: var(--tz-success-fg);
  --bs-form-valid-border-color: var(--tz-success);
}

/* The topbar is --tz-primary-dark (#02232D) and the dark page is n950 (#0B1D24) —
   1.15:1 apart, so the bar would dissolve into the page. Separate them with a
   hairline instead of inventing a fill the brand book does not define. */
[data-bs-theme="dark"] .tz-topbar { border-bottom: 1px solid var(--tz-n800); }

/* Selection: accent background with primary-dark text (brand book). */
[data-bs-theme="dark"] ::selection { background: var(--tz-accent); color: var(--tz-primary-dark); }

/* Form fields in dark, and a deliberate divergence from the brand book.
   The book's "n800 border" is guidance for CARDS, where the n900 fill already
   separates the component from the n950 page. A field is identified BY its border,
   and n800-on-n900 is 1.33:1 — invisible, failing WCAG 1.4.11's 3:1 for UI
   components. Two fixes: recess the field to the page colour so it reads as an
   input well, and take the border to n500 (3.82:1 against that fill). */
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select,
[data-bs-theme="dark"] textarea.form-control {
  background-color: var(--tz-bg);
  border-color: var(--tz-n500);
}
[data-bs-theme="dark"] .input-group-text { border-color: var(--tz-n500); }
/* Same reasoning for the topbar hairline: n800 on n950 is 1.54:1, so the separator
   would not separate. n600 clears 3:1 while staying a hairline. */
[data-bs-theme="dark"] .tz-topbar { border-bottom-color: var(--tz-n600); }

body {
  background-color: var(--bs-body-bg);
  color: var(--bs-body-color);
  font-family: var(--bs-font-sans-serif);
  font-size: var(--tz-text-base);
  /* Inter renders harsh with macOS subpixel AA — smooth it. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Base UI controls share the 14px base for a consistent rhythm.
   ⚠️ U-25 — .btn IS NOT IN THIS LIST, AND ITS ABSENCE IS THE FIX. A direct `font-size`
   here beats Bootstrap's own `.btn { font-size: var(--bs-btn-font-size) }` on equal
   specificity, because this file loads later — so THE ONE BUTTON's
   `--bs-btn-font-size: var(--tz-text-sm)` at the end of this file was set, inherited by
   nothing, and silently overruled. The product shipped two button label sizes: 14px for
   every default button and 13px only where `.btn-sm` (two classes) beat this rule — the
   variant the settlement says is dead was the only one arriving at its stated size, and
   screens carried both at once. It is the same shadowing the type-scale comment in
   tokens.css already describes one layer down. */
.form-control, .form-select, .form-label, .input-group-text,
.table, .dropdown-item, .nav-link, .badge {
  font-size: var(--tz-text-base);
}
small, .small, .text-muted, .form-text { font-size: var(--tz-text-sm); }

/* Buttons never wrap — icon + label + shortcut badge stay on a single line. */
.btn { white-space: nowrap; }

/* The serif display face is reserved for the few large titles; everything else is
   Inter. Semibold, not bold: Source Serif 4 at 700 goes black at display sizes, and
   the editorial register wants a confident title, not a shouted one. */
h1, h2, .h1, .h2 {
  font-family: var(--tz-font-display);
  color: var(--tz-heading);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.15;
}
h3, h4, h5, h6, .h3, .h4, .h5, .h6 {
  font-family: var(--bs-font-sans-serif);
  color: var(--tz-heading);
  font-weight: 600;
  letter-spacing: -0.005em;
}
h1, .h1 { font-size: var(--tz-text-h1); }
h2, .h2 { font-size: var(--tz-text-h2); }
h3, .h3 { font-size: var(--tz-text-h3); }
h4, .h4 { font-size: var(--tz-text-h4); }
h5, .h5 { font-size: var(--tz-text-lg); }
h6, .h6 { font-size: var(--tz-text-base); }

/* Fixed-width: standardized on JetBrains Mono everywhere code appears. */
code, pre, kbd, samp, .font-monospace {
  font-family: var(--tz-font-mono);
}
.ace_editor, .ace_editor .ace_text-layer, .ace_editor .ace_line {
  font-family: var(--tz-font-mono) !important;
}

/* ============================================================================
   BUTTON ROLES — normalized to 4. Every button maps to exactly one:
     Primary         .btn-primary          solid teal    — the one main action/view
     Secondary       .btn-outline-*        neutral hairline outline — toolbar / non-primary actions
     Neutral         .btn-secondary        filled gray   — cancel / toolbar / utility
     Danger          .btn-danger/-outline  red           — destructive only
   Retired (do not reintroduce): btn-light + btn-default (→ btn-secondary),
   btn-outline-secondary (→ btn-secondary), btn-dark, btn-warning/-outline-warning
   (→ btn-danger by intent). .btn-link is a separate no-chrome inline-action
   utility (icon edit/delete/cancel), not an emphasis role — left to Bootstrap.
   ============================================================================ */

/* Primary → solid teal */
.btn-primary {
  --bs-btn-bg: var(--tz-primary);
  --bs-btn-border-color: var(--tz-primary);
  --bs-btn-hover-bg: var(--tz-primary-dark);
  --bs-btn-hover-border-color: var(--tz-primary-dark);
  --bs-btn-active-bg: var(--tz-primary-dark);
  --bs-btn-active-border-color: var(--tz-primary-dark);
  --bs-btn-disabled-bg: var(--tz-primary);
  --bs-btn-disabled-border-color: var(--tz-primary);
}
/* The dark accent-filled `.btn-primary` rule that stood here predated THE one
   button and out-specified it — `[data-bs-theme="dark"] .btn-primary` beats the
   consolidated block's bare `.btn-primary`, so every resting primary in dark
   mode kept an accent fill the settlement had retired. The dark half of the
   consolidated block (end of this file) is the only dark button rule now. */

/* Non-primary outline actions (.btn-outline-primary) share the neutral hairline
   treatment of .btn-outline-secondary — styled together in golden.css so an action
   toolbar never mixes teal and neutral outline buttons. Teal is reserved for the
   one filled .btn-primary. */

/* Neutral → filled gray */
.btn-secondary {
  --bs-btn-bg: var(--tz-surface); --bs-btn-border-color: var(--tz-border); --bs-btn-color: var(--tz-text-muted);
  --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);
}

/* Danger → solid / outline red (destructive only). ⚠️ THE RULES ARE NOT HERE, and the empty
   space is the point: a `.btn-danger` block and a `.btn-outline-danger` block used to sit at
   this spot, and every declaration in both was re-set, at the same specificity and later in
   the file, by the DESTRUCTIVE half of THE ONE BUTTON at the foot of this stylesheet. Nothing
   they said reached a screen. Verified by dumping all seventeen --bs-btn-* variables plus the
   three computed colours for both classes in both themes, with the two blocks present and
   deleted: identical.
   They were left standing when the settlement consolidated the roles, and the next person to
   read them wasted an edit on it — the U-22 fix in this branch was written into the dead
   `.btn-outline-danger` block first, where it did nothing. Destructive lives in one place. */

/* focus = visible ring, not glow (scoped to interactive elements) */
.btn:focus-visible,
.form-control:focus,
.form-select:focus,
a:focus-visible,
.nav-link:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--tz-primary);
  border-color: var(--tz-primary);
}

/* ⚠️ THE DARK HALF OF THE SAME RULE, AND IT IS NOT DECORATION (U-10).
   --tz-primary is a raw brand primitive, not one of the semantic aliases the dark block
   at the top of this file redefines, so the ring above resolves to #024256 in BOTH themes.
   Measured against the dark page (n900): 1.36:1, where WCAG 1.4.11 asks 3:1 of a focus
   indicator — and because the rule cancels the browser outline FIRST, there is no fallback
   underneath it. A keyboard user in dark mode had nothing at all.
   The accent is what dark uses for emphasis everywhere else in this file, and it measures
   6.80:1 on n900. The three siblings that already knew this — .btn-close, .btn-check + .btn
   and .tz-passtoggle — carry the same override; these are the ones that did not.
   Solid, not the light theme's soft glow: a 25%-alpha halo composites to 1.32:1 on a dark
   ground however wide it is drawn, so on dark the ring is ink. */
[data-bs-theme="dark"] .btn:focus-visible,
[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus,
[data-bs-theme="dark"] a:focus-visible,
[data-bs-theme="dark"] .nav-link:focus-visible {
  box-shadow: 0 0 0 2px var(--tz-accent);
  border-color: var(--tz-accent);
}
/* Same reasoning, for the three rings written elsewhere in this file: the checkbox (its
   halo is primary at 25%, 1.07:1), the pagination link, and the invalid field — the last
   one is left alone, --tz-error measures 3.97:1 on n900 and already passes. */
[data-bs-theme="dark"] .form-check-input:focus {
  border-color: var(--tz-accent);
  box-shadow: 0 0 0 2px var(--tz-accent);
}
[data-bs-theme="dark"] .pagination {
  --bs-pagination-focus-box-shadow: 0 0 0 2px var(--tz-accent);
}

a { color: var(--bs-link-color); text-decoration: none; }
/* Unified link policy: no underline anywhere; interactivity is a colour shift on
   hover. Structural links (below) inherit text colour and reveal the link colour. */
a:hover { text-decoration: none; }

/* Heroicons via sprite + fragment inherit text color */
.tz-icon { display: inline-block; vertical-align: -0.125em; color: currentColor; }

/* Compact padding for data tables (tz-table). */
.tz-table > :where(thead, tbody) > tr > :where(th, td) { padding: 6px 10px; }

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

/* ---- App shell: a light editorial masthead (L1) + underline sub-nav (L2).
   The September 2026 reskin retired the dark-teal bar: an editorial page is paper
   edge to edge, and the chrome separates with a rule, not a colour block. It also
   dissolves two audit findings at once — the invisible focus ring on the dark bar
   (M4) and the sub-AA "Admin User" text (M3) — because the links now sit on the
   same surface the rest of the theme is contrast-checked against. ---- */
.tz-topbar {
  position: sticky; top: 0; z-index: 1030;
  display: flex; align-items: center; gap: 12px;
  height: 60px; padding: 0 56px;
  background: var(--tz-surface);
  border-bottom: 1px solid var(--tz-border);
}
.tz-brand { display: inline-flex; align-items: center; gap: 10px; height: 60px; text-decoration: none; margin-right: 32px; }
.tz-brand__icon { height: 28px; width: 28px; }
/* An <img> cannot recolor with the theme, so light/dark are two files and CSS
   shows exactly one. */
.tz-brand__icon--dark { display: none; }
[data-bs-theme="dark"] .tz-brand__icon--light { display: none; }
[data-bs-theme="dark"] .tz-brand__icon--dark { display: inline; }
/* Icon and word, both in the corporate teal, nothing else. */
.tz-brand__word {
  font-family: var(--tz-font-display); font-size: 20px; font-weight: 700;
  letter-spacing: -0.015em; color: var(--tz-primary); line-height: 1;
}
[data-bs-theme="dark"] .tz-brand__word { color: var(--tz-white); }
.tz-topbar-spacer { flex: 1 1 auto; }
.tz-user { display: inline-flex; align-items: center; color: var(--tz-heading); }
/* The corner shows only the avatar. The menu fits a normal full name on one line;
   exceptional names ellipsize instead of making the whole dropdown unwieldy. */
.tz-user-menu { min-width: 270px; max-width: calc(100vw - 24px); }
/* The theme, as ONE row (decision 8 of 9 September 2026): the label, then a segmented control of
   the three modes. It replaced a header and three menu items. The options keep .dropdown-item so
   Bootstrap's arrow-key navigation still walks them; these rules only undo that class's full-width
   block shape. What is stored and how is untouched -- tzTheme in main.js, localStorage 'tz-theme'. */
.tz-theme-switch {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 4px 16px;
}
.tz-theme-switch__label { font-size: var(--tz-text-sm); color: var(--tz-text-muted); white-space: nowrap; }
.tz-theme-switch__options {
  display: inline-flex; gap: 2px; padding: 2px;
  border-radius: var(--tz-r-full); background: var(--tz-surface-sunken);
}
/* Icon-only since 17 September 2026: the word lives in the tooltip and in the hidden span. */
.tz-user-menu .tz-theme-switch__option.dropdown-item {
  display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 26px;
  padding: 0; border-radius: var(--tz-r-full);
  font-size: 14px; line-height: 1; color: var(--tz-text-muted);
  background: transparent; white-space: nowrap;
}
.tz-user-menu .tz-theme-switch__option.dropdown-item:hover { color: var(--tz-heading); background: var(--tz-surface-hover); }
.tz-user-menu .tz-theme-switch__option.dropdown-item.active {
  color: var(--tz-heading); background: var(--tz-surface); font-weight: 600;
  box-shadow: 0 0 0 1px var(--tz-border);
}
.tz-user-menu .tz-theme-switch__option.dropdown-item:focus-visible {
  outline: 2px solid var(--tz-primary); outline-offset: 1px;
}
[data-bs-theme="dark"] .tz-user-menu .tz-theme-switch__option.dropdown-item:focus-visible { outline-color: var(--tz-accent); }
.tz-user-menu-name {
  overflow: hidden; max-width: 100%; white-space: nowrap; text-overflow: ellipsis;
}
/* Focus that survives the light chrome (audit M4: the entity select had outline:none
   and an invisible ring; the topbar ring was primary-on-primary-dark, ~1.4:1). */
.tz-topnav > a:focus-visible, .tz-brand:focus-visible {
  outline: 3px solid var(--tz-accent-blue); outline-offset: 2px; border-radius: var(--tz-r-sm);
}
.form-select:focus, .form-control:focus {
  border-color: var(--tz-accent-blue);
  box-shadow: 0 0 0 3px rgba(var(--tz-info-rgb), .25);
}
[data-bs-theme="dark"] .tz-topnav > a:focus-visible,
[data-bs-theme="dark"] .tz-brand:focus-visible { outline-color: var(--tz-accent); }

/* The sections of the place you are in — quiet words on paper, the current one simply ink:
   13px at weight 400, colour-only hover, no pill. ⚠️ Since the single row of 27 September 2026
   this nav holds the SUBSCREENS, which is what the underline-tab `.tz-subnav` under the bar used
   to hold; that second row is gone, and with it the only two rules in the product that depended
   on a 105px chrome. The links keep the shape they had here, not the tabs' — a row inside a bar
   is words, and an underline needs a baseline of its own to sit on. */
/* ⚠️ U-18 — THE CONTAINMENT IS NOT A PHONE RULE, AND SCOPING IT TO ONE COST EVERY SCREEN
   BETWEEN 768px AND 1139px. These four declarations lived inside the `max-width: 767.98px`
   block below, whose own comment says it was written for an earlier version of this same bug.
   Above 767px they stopped applying, the nav went back to its natural 886px, and the bar
   pushed the DOCUMENT sideways: measured 374px of horizontal scroll at 768 and 118px at 1024,
   on 17 of 17 screens, with the account avatar landing at x=1141 — off the viewport, so
   profile, theme and sign-out were unreachable without scrolling the page sideways first.
   A nav that does not fit has to scroll inside itself at EVERY width; the phone block keeps
   only what is really about phones (the gutters).
   The height is part of the fix, not tidying: `overflow-x: auto` makes this a scroll
   container, which clips in both axes, and the links' focus outline is drawn 5px outside
   their 34px box. Standing the nav up to the bar's own 60px gives the ring its room back. */
.tz-topnav {
  display: flex; align-items: center; gap: 2px;
  /* ⚠️ `0 1 auto`, not `1 1 auto`: the nav is the SECTIONS now, not the whole first level, and it
     sits between the place control and the corner. Growing into the bar's slack would push the
     fade below onto empty paper at every width and leave the corner nowhere to be. The slack
     belongs to .tz-topbar-spacer. */
  min-width: 0; flex: 0 1 auto; height: 60px;
  /* ⚠️ THE PADDING IS THE FOCUS RING'S ROOM, AND THE NEGATIVE MARGIN GIVES IT BACK.
     A link's ring is drawn 5px outside its box (3px outline at a 2px offset) and the first
     link starts flush with the nav's content edge, so the scroll container clipped the whole
     left side of it: measured at 1440, 114 ring pixels in the 5px band left of "Your work"
     before this bar became a scroll container, 0 after. Clipping happens at the PADDING box,
     so 6px of inline padding puts the ring back inside; the matching negative margin takes
     the 6px off the layout again, which is why every link lands on the pixel it did before.
     `scroll-padding-inline` does the same job for the far end: without it the browser scrolls
     a focused link flush to the scrollport edge and clips the ring it just revealed. */
  padding-inline: 6px;
  margin-inline: -6px;
  scroll-padding-inline: 6px;
  overflow-x: auto;
  /* ⚠️ AND THE SCROLLBAR IS VISIBLE, EXCEPT ON A PHONE. Hiding it was right while this block
     only ran under the phone breakpoint, where a finger swipes the bar and a scrollbar is
     just clutter. Applied at every width it took the affordance away from the pointer that
     has no swipe: between 768px and 1139px the entities and Administration are past the edge
     and the only way to them was shift+wheel — nobody's first guess. Overflowing and looking
     as if it does not is worse than the sideways scroll this rule replaced, because that at
     least was visible. A scrollbar appears only when there IS overflow, so nothing shows at
     1140 and above; it is drawn as a 4px hairline in the border colour rather than the OS
     default, which would be a slab across the masthead. */
  scrollbar-width: thin;
  scrollbar-color: var(--tz-border-strong) transparent;
}
.tz-topnav::-webkit-scrollbar { height: 4px; }
.tz-topnav::-webkit-scrollbar-track { background: transparent; }
.tz-topnav::-webkit-scrollbar-thumb {
  background: var(--tz-border-strong); border-radius: var(--tz-r-full);
}
/* ⚠️ AND A FADE AT THE TRAILING EDGE, BECAUSE A SCROLLBAR IS NOT A PROMISE. Where the OS
   draws overlay scrollbars — macOS by default, and this repository's own headless Chromium,
   which is how the omission was measured — the bar above is invisible until something is
   already scrolling, so on its own it does not tell anybody the bar has more in it. The fade
   does, at rest, in every engine.
   ⚠️ It used to say the nav is `flex: 1 1 auto`, so "wherever the links fit there is slack to
   its right and the gradient falls on empty paper" — and this branch changed the nav to
   `flex: 0 1 auto` (see its own note above), which makes the nav shrink to its content: when the
   links fit, there is NO slack and those 28px land on the last word. That is why the class below
   exists and why it is not an optimisation.
   `:not(:focus-within)` is not tidiness — a mask fades the element AND its descendants'
   outlines, so without it the focus ring of the last link would dim exactly when a keyboard
   user needed it. The gradient's colour is a stencil, not a colour: mask-mode is alpha here,
   so only the opacity of the two stops matters. */
.tz-topnav:not(:focus-within) {
  -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 1) calc(100% - 28px), transparent);
  mask-image: linear-gradient(to right, rgba(0, 0, 0, 1) calc(100% - 28px), transparent);
}
/* ⚠️ AND THE FADE GOES AS SOON AS THE BAR FITS, WHICH IS THE NORMAL CASE. The gradient above
   assumes the nav is a scroll container with something past its edge; once the *More (n)* menu
   absorbs the sections that do not fit, the content ends exactly at the edge and those last 28px
   land on the last word instead of on empty paper — a permanently half-faded word on every screen.
   (This said «once "All" absorbs the entities», which was the entity bar: that bar and that panel
   went with the one-row chrome.)
   The script adds this class when, after fitting, the nav no longer overflows, and takes it off
   when it still does (a phone too narrow even for the collapsed row). ⚠️ WITH NO SCRIPT THE CLASS
   IS NEVER ADDED AND THE LAST WORD STAYS HALF-FADED — which is the cost of the fallback, not a
   claim that nothing changes: this comment used to say the fade "behaves exactly as it did
   before", and with `flex: 0 1 auto` that is no longer true. It is the state a CSP-enforced page
   or a no-JS reader gets, and it is legible; the affordance is what degrades, not the row.
   Two classes, so it outweighs the `:not()` above rather than needing an !important. */
.tz-topnav.tz-topnav--fits {
  -webkit-mask-image: none;
  mask-image: none;
}
.tz-topnav > a {
  display: inline-flex; align-items: center; height: 34px;
  padding: 0 11px; border-radius: var(--tz-r-sm);
  color: var(--tz-text-muted); font-size: var(--tz-text-sm); font-weight: 400;
  text-decoration: none; white-space: nowrap;
  transition: color .15s ease;
}
.tz-topnav > a:hover { color: var(--tz-heading); background: var(--tz-surface-sunken); }
.tz-topnav > a.active { color: var(--tz-heading); font-weight: 600; }

/* ---- "All": the entities that do not fit (decision 2 of 9 September 2026) --------------------
   The bar pins as many entities as fit and the rest live in this menu. The script in
   header.html decides how many that is; everything here is what the two states look like.

   The rule below is belt and braces, and says so. `.tz-topnav > a` sets `display: inline-flex`,
   an author declaration that would beat the UA's `[hidden]` — but Bootstrap's reboot already
   ships `[hidden] { display: none !important; }`, and that is what actually hides an overflowing
   entity today. This line keeps the bar working if the reboot is ever dropped or trimmed, which
   is the kind of change nobody would connect to the menu. It is written against .tz-topbar rather
   than .tz-topnav so the same line also covers the menu and the button, which are the bar's
   children and not the nav's. */
.tz-topbar [hidden] { display: none; }

/* ══ THE PLACE — where you are, and everywhere you can go from here ══════════════════════════
   The one control of the bar that holds STATE rather than destinations, which is why it is boxed
   and the sections beside it are not: it is labelled with the current entity (or Administration,
   or Home) and changing it changes what the whole page is about. A category label — the word
   "Entities" — belongs to a menu of destinations, where there is no current value; this is not
   that, and the box is what says so.
   It is a bare <button>, not a .btn: the one-button policy at the foot of this file would give it
   the product's quiet 36px control, and this is a state indicator, not an action. */
.tz-place { position: relative; flex: 0 0 auto; min-width: 0; }
.tz-place__toggle {
  display: inline-flex; align-items: center; gap: 8px;
  max-width: 280px; height: 34px; padding: 0 12px;
  border: 1px solid var(--tz-border); border-radius: var(--tz-r-md);
  background: var(--tz-surface); color: var(--tz-heading);
  font-family: inherit; font-size: var(--tz-text-base); font-weight: 600; line-height: 1;
  white-space: nowrap; cursor: pointer;
}
.tz-place__label { overflow: hidden; text-overflow: ellipsis; }
/* The caret says "there is more here" before the menu is opened; it is Bootstrap's own triangle,
   drawn by hand because this control is not a Bootstrap dropdown. */
.tz-place__toggle::after {
  content: ""; flex: 0 0 auto; width: 0; height: 0; margin-top: 1px;
  border-top: 4px solid var(--tz-text-faint);
  border-right: 4px solid transparent; border-left: 4px solid transparent;
}
.tz-place__toggle:hover { border-color: var(--tz-border-strong); background: var(--tz-surface-sunken); }
.tz-place__toggle[aria-expanded="true"] { border-color: var(--tz-border-strong); background: var(--tz-surface-sunken); }
.tz-place__toggle:focus-visible {
  outline: 3px solid var(--tz-accent-blue); outline-offset: 2px; border-radius: var(--tz-r-md);
}
[data-bs-theme="dark"] .tz-place__toggle:focus-visible { outline-color: var(--tz-accent); }
/* ⚠️ Nothing to switch to: the same node, disabled, without the caret and without the affordance.
   Two nodes would be two copies of the label (D-3). */
.tz-place__toggle.is-fixed { cursor: default; opacity: 1; }
.tz-place__toggle.is-fixed::after { display: none; }
.tz-place__toggle.is-fixed:hover { background: var(--tz-surface); border-color: var(--tz-border); }

/* The panel hangs from .tz-place, which is an ordinary flex item of the sticky bar — not from
   .tz-topnav, which is a scroll container and clips in both axes. */
.tz-place__menu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 1031;
  margin: 0; padding: 6px; list-style: none;
  min-width: 240px; max-width: min(360px, calc(100vw - 24px));
  max-height: calc(100vh - 96px); overflow-y: auto;
  background: var(--tz-surface); border: 1px solid var(--tz-border);
  border-radius: var(--tz-r-md); box-shadow: var(--tz-shadow-md);
}
.tz-place__menu > li > a {
  display: block; padding: 7px 10px; border-radius: var(--tz-r-sm);
  color: var(--tz-text); font-size: var(--tz-text-sm); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tz-place__menu > li > a:hover { background: var(--tz-surface-sunken); color: var(--tz-heading); }
.tz-place__menu > li > a.active { color: var(--tz-heading); font-weight: 600; }
.tz-place__menu > li > a:focus-visible { outline: 3px solid var(--tz-accent-blue); outline-offset: -1px; }
[data-bs-theme="dark"] .tz-place__menu > li > a:focus-visible { outline-color: var(--tz-accent); }
.tz-place__sep { margin: 6px 4px; border-top: 1px solid var(--tz-border-subtle); }
.tz-place__none {
  padding: 7px 10px; color: var(--tz-text-muted); font-size: var(--tz-text-sm);
}
.tz-place__search { padding: 2px 4px 6px; }
.tz-place__search input {
  display: block; width: 100%; height: 34px; padding: 0 10px;
  border: 1px solid var(--tz-border); border-radius: var(--tz-r-sm);
  background: var(--tz-surface); color: var(--tz-text); font-size: var(--tz-text-sm);
}
.tz-place__search input:focus { outline: none; border-color: var(--tz-accent-blue); }
[data-bs-theme="dark"] .tz-place__search input:focus { border-color: var(--tz-accent); }

/* ══ THE SECTIONS THAT DID NOT FIT ══════════════════════════════════════════════════════════
   The measured overflow of D-6: ten administration sections need 879px and the bar has ~728 at
   1280 and ~888 at 1440. The panel is a child of this span rather than of the bar, so it needs no
   script to place it — which is the whole of what the two old level menus needed one for. */
.tz-topnav__more { position: relative; display: inline-flex; align-items: center; flex: 0 1 auto; min-width: 0; }
.tz-topnav__more-toggle {
  display: inline-flex; align-items: center; gap: 6px; height: 34px;
  padding: 0 11px; border: 0; border-radius: var(--tz-r-sm);
  background: transparent; color: var(--tz-text-muted);
  font-family: inherit; font-size: var(--tz-text-sm); font-weight: 400; line-height: 1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;
}
.tz-topnav__more-toggle::after {
  content: ""; flex: 0 0 auto; width: 0; height: 0; margin-top: 1px;
  border-top: 4px solid currentColor; border-right: 4px solid transparent;
  border-bottom: 0; border-left: 4px solid transparent;
}
.tz-topnav__more-toggle:hover,
.tz-topnav__more-toggle[aria-expanded="true"] { color: var(--tz-heading); background: var(--tz-surface-sunken); }
.tz-topnav__more-toggle:focus-visible { outline: 3px solid var(--tz-accent-blue); outline-offset: 2px; }
[data-bs-theme="dark"] .tz-topnav__more-toggle:focus-visible { outline-color: var(--tz-accent); }
.tz-topnav__more-menu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 1031;
  margin: 0; padding: 6px; list-style: none;
  min-width: 200px; max-width: min(320px, calc(100vw - 24px));
  max-height: calc(100vh - 96px); overflow-y: auto;
  background: var(--tz-surface); border: 1px solid var(--tz-border);
  border-radius: var(--tz-r-md); box-shadow: var(--tz-shadow-md);
}
.tz-topnav__more-menu > li > a {
  display: block; padding: 7px 10px; border-radius: var(--tz-r-sm);
  color: var(--tz-text); font-size: var(--tz-text-sm); text-decoration: none; white-space: nowrap;
}
.tz-topnav__more-menu > li > a:hover { background: var(--tz-surface-sunken); color: var(--tz-heading); }
.tz-topnav__more-menu > li > a.active { color: var(--tz-heading); font-weight: 600; }
.tz-topnav__more-menu > li > a:focus-visible { outline: 3px solid var(--tz-accent-blue); outline-offset: -1px; }
[data-bs-theme="dark"] .tz-topnav__more-menu > li > a:focus-visible { outline-color: var(--tz-accent); }

/* ══ THE CORNER ICONS ══ The gear is administration's second door (D-5). An anchor, because it
   goes somewhere; sized like the avatar beside it, so the corner reads as one group. */
.tz-topbar__icon {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 34px; height: 34px; border: 1px solid transparent; border-radius: var(--tz-r-sm);
  background: none; color: var(--tz-text-muted); font-size: var(--tz-text-lg); text-decoration: none;
}
.tz-topbar__icon:hover { color: var(--tz-heading); background: var(--tz-surface-sunken); }
.tz-topbar__icon.active {
  color: var(--tz-heading); background: var(--tz-surface-sunken); border-color: var(--tz-border);
}
.tz-topbar__icon:focus-visible { outline: 3px solid var(--tz-accent-blue); outline-offset: 2px; }
[data-bs-theme="dark"] .tz-topbar__icon:focus-visible { outline-color: var(--tz-accent); }

/* ══ ONE HAIRLINE RULE, EVERYWHERE A ROW NEEDS ONE ══════════════════════════════════════════
   ⚠️ It used to be `.tz-subnav-sep`, and the second level it was named after is gone. Two of its
   three callers were never navigation anyway — the data-access filter pills and the admin
   catalogue's — so the name was describing the first place it happened to be used rather than
   what it is. `--quiet` is the lighter weight the group rules inside a row of sections want. */
.tz-rule-sep { width: 1px; height: 20px; background: var(--tz-border); margin: 0 6px; flex: 0 0 auto; }
.tz-rule-sep--quiet { background: var(--tz-surface-hover); }
/* The mock's content column: 1076px of content inside 56px gutters at 1440. Pages
   that genuinely need width (the record tables) opt into --wide and keep it. */
.tz-main { max-width: 1240px; margin: 0 auto; padding: 46px 56px 96px; }
/* Wider container for data-dense pages (opt-in per page). */
.tz-main--wide { max-width: 1600px; }
/* ⚠️ EDGE TO EDGE, for the screen that IS a table (Nacho, 29 September 2026). The record listing
   has one job and it is showing columns, so a cap wastes exactly the room it needs — and with the
   context column taking 300 of it, a 1600 cap on a wide monitor left the table narrower than the
   window could have given it. The gutters stay: full width is not no margin. */
.tz-main--full { max-width: none; }

/* ════════════ THE CONTEXT BAR — always, in both planes ════════════════════════════════════════
   D-7 of 27 September 2026. The left column carries the page head, the page's general actions and
   the filters; the data area carries the data. The argument is in the decision record and it is
   measured: the editorial head is written for a READING measure (66ch) and was sitting on a DATA
   measure (1328px), so it stacked three blocks because it had given up the width, not because it
   needed the height. The first row of data on a data screen started 345–476px down — 52–58 % of a
   820px laptop before anything was shown.

   ⚠️ IT IS ONE ELEMENT, `<aside class="tz-ctx">`, and that is a markup requirement rather than a
   styling preference. Scattering the head, the actions and the filters across the grid as separate
   children puts the filters ~800px BELOW the table, because the content of row 1 stretches row 1.

   ⚠️ FOLDING IT BUYS WIDTH, NOT HEIGHT. Measured: open, the first row of data is at 238px and the
   table is 980px; folded, the first row is still at 238 and the table is 1328. So the control is
   "I need columns, not context" and it is named for that, not "hide".

   ⚠️ AND `:has()` IS DELIBERATE. Every screen gets a bar eventually, but they are converted one at
   a time, and a `.tz-main` that were always a grid would leave a 300px hole on every page not yet
   converted. `:has()` makes each intermediate state coherent: a page with an aside is a grid, a
   page without one renders exactly as it did. */
@media (min-width: 992px) {
  .tz-main:has(> .tz-ctx) {
    display: grid;
    grid-template-columns: var(--tz-ctx-w, 300px) minmax(0, 1fr);
    column-gap: 44px;
    align-items: start;
    /* ⚠️ THE THIRD WIDTH, AND IT IS THE BAR'S ARITHMETIC RATHER THAN A NEW OPT-IN. The bar with
       its gap takes 344px. Against the base 1240 that would leave a data column of 896 — narrower
       than the 1128 a page has with no bar at all, which would make the context bar a net loss on
       every screen it is supposed to help. D-8's own measurements assume the content box is the
       window less the gutters, which is what this is: 1780 is above any monitor in use, so in
       practice the column is the window and the cap only stops a 4K screen setting 66ch of lead
       beside three metres of table. */
    max-width: 1780px;
  }
  /* ⚠️ EXCEPT A PAGE THAT ASKED FOR THE WHOLE WINDOW (Nacho, 29 September 2026). The 1780 above
     was chosen as "above any monitor in use", and that stopped being true the first time the record
     listing was opened on a 2560 screen: the cap left 780px of empty page beside a table that had
     columns to show. The reasoning behind 1780 still holds for a page whose content is prose beside
     a table — it is why this exception is on `--full` and not on the rule itself. The context
     column cannot run away with the extra room either: it is clamped at 480. */
  .tz-main--full:has(> .tz-ctx) { max-width: none; }
  .tz-main:has(> .tz-ctx) > * { grid-column: 2; }
  .tz-main > .tz-ctx {
    grid-column: 1;
    /* ⚠️ The span is what makes it sticky for the whole page rather than for its own row. An item
       placed in row 1 sticks within row 1 and stops the moment that row scrolls past, which on a
       long table is almost immediately. The number only has to exceed the children any `<main>`
       has — measured, the most in the product today is 23 (`/table/record`) — and the implicit
       rows it creates are empty and zero-height. `1 / -1` does NOT work: -1 is the last line of
       the EXPLICIT grid, and every row here is implicit. */
    grid-row: 1 / span 200;
    position: sticky; top: 86px;
    max-height: calc(100vh - 110px); overflow-y: auto;
    /* ⚠️ AND 100px OF HEIGHT BEYOND THE CONTENT, which is what the rule is for. The aside is
       sticky, so it is exactly as tall as what is in it: on the home of a fresh installation
       that is 31px of title, and its right-hand rule came out as a grey segment the length of a
       scrollbar thumb beside a page ten times taller. `padding-bottom` rather than a
       `min-height` because the height wanted is the CONTENT's plus a hundred, and no length
       expresses that — a fixed minimum would be a fourth number to keep in step with whatever
       the page puts in the column. */
    padding-right: 26px; padding-bottom: 100px; border-right: 1px solid var(--tz-border);
  }

  /* ⚠️ THE CONTENT STARTS WHERE THE COLUMN STARTS (Nacho, 29 September 2026). Every content unit
     in the product carries its own top padding — `.tz-sect` 38px, `.tz-hero` 34px — and that
     padding is spacing from whatever sat above it. The first one has nothing above it, so beside
     a column head that is flush with the top of the grid it reads as the page being out of line
     with itself: two things that begin together, beginning 38px apart.

     ⚠️ The adjacent sibling, not `:first-of-type`. `:first-of-type` would keep matching the first
     section even on a page that opens with a strip or a banner before it — zeroing the padding of
     something in the middle of the page and leaving the real first block still indented. What is
     wanted is "whatever comes immediately after the column", which is what `+` says. */
  .tz-main > .tz-ctx + section { padding-top: 0; }
  .tz-main > .tz-ctx + .tz-strip { margin-top: 0; }
  /* ⚠️ AND THROUGH A WRAPPER, because "whatever comes immediately after the column" is not always
     the content unit itself. The profile's two forms sit in a grid of their own, so the element
     after the column is a plain `div` and the rule above missed it: measured, its heading started
     at 144 while the column's title was at 106. Only the FIRST child, so a wrapper's later
     sections keep the spacing that separates them from the one above. */
  .tz-main > .tz-ctx + div > section:first-child { padding-top: 0; }

  /* The head inside the column. It is the same markup — U-26's one group, still nested in
     `.tz-page-head__main` — reading at the width it actually has: the title drops to the section
     size because 40px of display serif in a 300px column is two words a line, and the action group
     takes a line of its own because the title column cannot reach its 34rem basis at any title
     length in 348px. That is the wrap rule working as designed, not an exception to it. */
  .tz-ctx .tz-page-head { margin-bottom: 0; }
  .tz-ctx .tz-page-head__main { display: block; }
  .tz-ctx .tz-page-head__title { font-size: var(--tz-text-h2); line-height: 1.2; }
  .tz-ctx .tz-page-head__sub { max-width: none; margin-top: 10px; font-size: var(--tz-text-sm); line-height: 1.55; }
  /* ⚠️ THE GROUP KEEPS THE ROW THE BASE RULE GIVES IT — it is not a stacked list (Nacho,
     29 September 2026). It was `flex-direction: column; align-items: stretch`, which stretched every
     button to the column's full width and left its label stranded at the far left of an empty box.
     A resizable column made that worse at every drag: the buttons grew with the column and the gap
     between label and edge grew with them. `.tz-page-head__actions` is already
     `display:flex; gap:8px; flex-wrap:wrap`, so doing nothing here is the whole fix — each button
     is as wide as its own words, they sit from the left, and they wrap onto a second line when the
     column is narrow instead of being stretched to hide that they do not fit. */
  .tz-ctx .tz-page-head__actions { margin-top: 18px; }
  /* A label longer than the narrowest column wraps inside its own button rather than pushing the
     column's scrollbar out. */
  .tz-ctx .tz-page-head__actions > * { max-width: 100%; }

  /* Whatever the page put below its head in the column — filters, a switch strip, a search —
     sits under a rule rather than between two, because the column's own right-hand rule is
     already saying where the context ends. */
  .tz-ctx > :not(.tz-page-head) {
    margin-top: 20px; padding: 18px 0 0;
    border-top: 1px solid var(--tz-border-subtle); border-bottom: 0;
  }
  /* ⚠️ EXCEPT WHAT FOLLOWS A LABEL, which is the group that label names (Nacho, 29 September 2026).
     The rule above separates blocks, and a label plus its pills is ONE block: left alone it drew a
     second hairline a few pixels under the heading and cut it off from its own content. One rule
     above the pair, the label, then the pills — the same shape the vigency switch already had. */
  .tz-ctx > .tz-overline + * { margin-top: 8px; padding-top: 0; border-top: 0; }
  /* ⚠️ Every filter row in the column reads left, whatever it does over a table. The admin
     catalogues' own rule is `justify-content: flex-end`, which is right when the pills share a
     row with a search box at the other end of it — and that search is over the table now, so in
     here they would be a block of pills pushed against a rule for no reason. */
  .tz-ctx .tz-filters,
  .tz-ctx .tz-admin-catalogue__filters { display: flex; flex-wrap: wrap; gap: 7px; justify-content: flex-start; }

  /* ⚠️ A MENU IN THE COLUMN IS AS WIDE AS THE COLUMN, and the reason is that its widest line is
     customer text. A `.dropdown-menu` sizes to its longest item with `white-space: nowrap`, so the
     duplicates board's index picker — whose labels are the index descriptions somebody typed —
     came out 513px wide inside a 300px column and lay across the board. Cap it at the column's own
     width, which the grip may have changed, and let the long ones wrap instead.

     The other half of this, forcing every one of them to open DOWNWARD and stopping the column's
     `overflow-y` cutting them at its edge, cannot be done from here: it is Popper's placement, and
     it lives in `main.js` under `tz-ctx dropdowns`. */
  .tz-ctx .dropdown-menu {
    max-width: var(--tz-ctx-w, 300px);
    /* ⚠️ And no taller than the screen, scrolling inside itself. The index list grows with the
       installation, and the menu is placed `position: fixed` — so one taller than the viewport
       is not merely ugly, its last entries cannot be reached by scrolling anything. It is also
       what lets the menu keep opening downward on a short screen instead of flipping up. */
    max-height: calc(100vh - 150px); overflow-y: auto;
  }
  .tz-ctx .dropdown-item { white-space: normal; }

  /* The width handle. It is `position: fixed` and placed by the script from the column's own right
     edge: a handle inside the scrolling aside would scroll away from the rule it drags. */
  .tz-ctx-grip {
    position: fixed; top: 60px; bottom: 0; z-index: 1028;
    width: 9px; margin-left: -4px; cursor: col-resize;
  }
  .tz-ctx-grip::after { content: ""; position: absolute; inset: 0 4px; background: transparent; }
  .tz-ctx-grip:hover::after, .tz-ctx-grip.is-drag::after { background: var(--tz-accent-blue); }
  [data-bs-theme="dark"] .tz-ctx-grip:hover::after,
  [data-bs-theme="dark"] .tz-ctx-grip.is-drag::after { background: var(--tz-accent); }
}

/* ⚠️ BELOW 992 THERE IS NO CONTEXT BAR: the context renders in flow, before the content, exactly
   as it does today — which is why every rule above is inside the media query and none of them is
   undone below it. 992 is the `lg` cut the product already uses twelve times, NOT a number of this
   component's own. 768 was ruled out by measurement: a data page's content box is the window less
   112px of gutters and the bar with its gap takes 348, so 768 leaves 308px of table and both
   administration catalogues overflow. The rule is therefore not "mobile" — the context bar needs a
   minimum data area, below which it costs more than it gives. */
.tz-ctx-grip { display: none; }
@media (min-width: 992px) {
  .tz-ctx-grip { display: block; }
}
/* ⚠️ Only where there is a column to drag. The bar is being applied screen by screen, and a handle
   that resizes nothing is worse than an absent one. */
body:not(:has(.tz-ctx)) .tz-ctx-grip { display: none; }



/* card polish — hairline border, no glow */
.card { border: 1px solid var(--tz-border); border-radius: var(--tz-r-lg); box-shadow: none; }

/* footer — drop the old NiceAdmin sidebar offset */
.footer { margin-left: 0 !important; text-align: center; padding: 16px; color: var(--tz-text-muted); font-size: var(--tz-text-sm); background: transparent; }
.footer .copyright { color: var(--tz-text-muted); }

/* Status pills — low-chroma dot + label (never color-only). */
.tz-pill {
  display: inline-flex; align-items: center; gap: 6px;
  /* Its own face, never the parent's: the records list draws "Historical" inside the serif
     <h1>, and the pill inherited the display face there while the same pill on the record head
     sat in Inter (Nacho, 17 September 2026). */
  font-family: var(--tz-font-sans);
  font-size: var(--tz-text-sm); font-weight: 600; line-height: 1.3;
  padding: 2px 9px; border-radius: var(--tz-r-full);
  background: var(--tz-surface-sunken); color: var(--tz-text-muted); white-space: nowrap;
}
.tz-pill::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: 0 0 auto;
}
.tz-pill--success { background: var(--tz-success-bg); color: var(--tz-success-fg); }
.tz-pill--warning { background: var(--tz-warning-bg); color: var(--tz-warning-fg); }
.tz-pill--error   { background: var(--tz-error-bg);   color: var(--tz-error-fg); }
.tz-pill--info    { background: var(--tz-info-bg);    color: var(--tz-info-fg); }
.tz-pill--neutral { background: var(--tz-surface-sunken);       color: var(--tz-text-muted); }
/* Identity, not status: the filled primary pill names what the user IS (their
   role). Dark swaps to the accent, as emphasis does everywhere on dark. */
.tz-pill--primary { background: var(--tz-primary); color: var(--tz-white); }
[data-bs-theme="dark"] .tz-pill--primary { background: var(--tz-accent); color: var(--tz-primary-dark); }

/* Audit timeline (table-record) — the last rule family still coming from the
   NiceAdmin style.css. Ported with tokens: the rail was #eceefe lavender at 4px
   and the dot #888 grey. Class names kept so the markup needs no edit. */
.timeline {
  position: relative; list-style: none;
  margin: 0; padding: 0 0 0 var(--tz-sp-6);
  border-left: 2px solid var(--tz-border);
  font-size: var(--tz-text-base);
}
.timeline .timeline-item { position: relative; padding-bottom: var(--tz-sp-4); }
.timeline .timeline-item:last-child { padding-bottom: 0; }
.timeline .timeline-item::after {
  content: ""; position: absolute; display: block;
  left: calc(-1 * var(--tz-sp-6) - 7px); top: 5px;
  width: 10px; height: 10px; border-radius: 50%;
  background-color: var(--tz-primary);
  border: 2px solid var(--tz-surface);
}

/* Tag chip — neutral, no dot. */
.tz-tag {
  display: inline-flex; align-items: center;
  /* Its own face, like .tz-pill: "Current" sits inside the records list's serif <h1>. */
  font-family: var(--tz-font-sans);
  font-size: var(--tz-text-sm); font-weight: 500;
  padding: 2px 8px; border-radius: var(--tz-r-sm);
  background: var(--tz-surface-sunken); color: var(--tz-text-muted);
}
/* The historical semantic on a flat tag. The product says "this is not today" in one colour,
   the warning amber, wherever it says it: the pill in a page title, this tag on a search result,
   and the strip between rules (Nacho, 19 September 2026). */
.tz-tag--warning { background: var(--tz-warning-bg); color: var(--tz-warning-fg); }

/* Card chrome — align Bootstrap .card to the tz-card look so legacy pages
   (management screens, install/audit) match the agents/locations cards. */
.card { border: 1px solid var(--tz-border); border-radius: var(--tz-r-lg); box-shadow: none; }
/* A card that is a SECTION of the page, not an object in a collection: it loses the
   box and keeps a rule. Content aligns to the page column — the section is a passage
   of the document, not a container. See docs/golden-style-guide.md, "When a box IS
   allowed"; object cards, overlays, status panels and the auth card stay boxed. */
.card.tz-flat {
  background: transparent; border: 0; border-top: 1px solid var(--tz-border);
  border-radius: 0;
}
.card.tz-flat > .card-header, .card.tz-flat > .card-body, .card.tz-flat > .card-footer {
  padding-left: 0; padding-right: 0;
}
.card.tz-flat > .card-header { border-bottom: 0; padding-top: 24px; padding-bottom: 4px; }
.card.tz-flat > .card-footer { border-top: 1px solid var(--tz-border-subtle); }
/* Section titles on flat sections take the full display size — the mock's 26px. */
.card.tz-flat > .card-header .h5, .card.tz-flat > .card-header .h6 {
  font-size: var(--tz-text-h2);
}
.card-header, .card-footer { background: transparent; }
.card-header { border-bottom: 1px solid var(--tz-border-subtle); }
.card-footer { border-top: 1px solid var(--tz-border-subtle); }
.card > .card-header, .card > .card-body, .card > .card-footer { padding: 20px 24px; }
.card-title {
  font-family: var(--tz-font-display); font-size: var(--tz-text-h3); font-weight: 600;
  letter-spacing: -0.01em; line-height: 1.3; color: var(--tz-heading); margin: 0;
}
.card-header .h5, .card-header .h6 {
  font-family: var(--tz-font-display); font-size: var(--tz-text-h4); font-weight: 600;
  letter-spacing: -0.01em;
}
.card-title a { color: inherit; text-decoration: none; }
.card-title a:hover { color: var(--bs-link-color); }
.card-title a:hover { color: var(--tz-primary); }
.card-subtitle { font-family: var(--tz-font-sans); letter-spacing: 0; color: var(--tz-text-muted); font-weight: 400; font-size: var(--tz-text-sm); }

/* Tables — consistent quiet header + tokenized rows on every page. */
.table { --bs-table-hover-bg: var(--tz-surface-sunken); }
.table > thead > tr > th {
  font-size: var(--tz-text-xs); font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--tz-text-muted);
  border-bottom: 1px solid var(--tz-border); white-space: nowrap;
}
.table > :where(thead, tbody) > tr > :where(th, td) { padding: 11px 12px; vertical-align: middle; }

/* Badges — pill shape + tinted token chips so Bootstrap badges read like
   tz-pill/tz-tag. Status semantics preserved; see GoldenUtils.badgeClassification. */
.badge {
  border-radius: var(--tz-r-full); font-size: var(--tz-text-sm); font-weight: 600;
  padding: 3px 9px;
}
.badge.text-bg-light,
.badge.text-bg-secondary { background-color: var(--tz-surface-sunken) !important;       color: var(--tz-text-muted) !important;       font-weight: 500; }
.badge.text-bg-success   { background-color: var(--tz-success-bg) !important; color: var(--tz-success-fg) !important; font-weight: 500; }
.badge.text-bg-danger    { background-color: var(--tz-error-bg) !important;   color: var(--tz-error-fg) !important;   font-weight: 500; }
.badge.text-bg-warning   { background-color: var(--tz-warning-bg) !important; color: var(--tz-warning-fg) !important; font-weight: 500; }
.badge.text-bg-info      { background-color: var(--tz-info-bg) !important;    color: var(--tz-info-fg) !important;    font-weight: 500; }
/* ⚠️ The keyboard-shortcut override went with the Bootstrap classes it patched (20 September
   2026). `.tz-kbd` in golden.css draws from `--tz-info-bg` / `--tz-info-fg`, which the two themes
   already define as a legible pair, so there is nothing left to correct with `!important`. */

/* Contextual page header: breadcrumb trail on top, then title left / actions right.
   The one header pattern for every screen — replaces NiceAdmin's .pagetitle plus
   its .breadcrumb (raw #899bbd, hover #51678f). */
.tz-page-head { display: flex; flex-direction: column; gap: 8px; margin-bottom: 36px; }
.tz-page-head__main {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}
/* ❗️U-26, and this is the rule the 13 September ruling adds to the guide: the title column is
   never squeezed to make room for the actions — below 34rem the action group wraps onto a line of
   its own instead, and `justify-content: space-between` leaves a lone item on that line at the
   START, which is why a wrapped group aligns left. Without a flex-basis here the column's
   automatic minimum is its min-content width, and `.tz-page-head__title` sets
   `overflow-wrap: anywhere` (U-17), so that minimum is ONE CHARACTER: the actions would keep the
   line at any width and a long title would ladder down the page a word at a time. The
   alternative, letting the group push the line wider, scrolls the whole document sideways.
   It is `:not(.tz-page-head__actions)` and not `:first-child` because one head (`/task/show`)
   puts its `<h1>` straight into the row rather than wrapping it. */
.tz-page-head__main > :not(.tz-page-head__actions) { flex: 1 1 34rem; min-width: 0; }
/* Breadcrumb — quiet and small; n600 keeps 13px text AA-legible. A slash separator
   means none of Bootstrap's .breadcrumb machinery is needed. */
.tz-page-head__crumbs { font-size: var(--tz-text-sm); color: var(--tz-text-muted); }
.tz-page-head__crumbs ol {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  list-style: none; margin: 0; padding: 0;
}
.tz-page-head__crumbs li { display: inline-flex; align-items: center; gap: 6px; }
/* n500, not n400: the slash is generated content that some screen readers
   announce, so it clears the 3:1 non-text floor (3.85:1) instead of sitting at
   n400's 2.55:1 — while still reading quieter than the n600/n700 crumb links. */
.tz-page-head__crumbs li:not(:first-child)::before {
  content: "/"; color: var(--tz-text-faint);
}
.tz-page-head__crumbs a { color: var(--tz-text-muted); text-decoration: none; }
.tz-page-head__crumbs a:hover { color: var(--bs-link-color); }
.tz-page-head__crumbs .active,
.tz-page-head__crumbs [aria-current="page"] { color: var(--tz-text-muted); font-weight: 600; }
/* The title is the page's headline, in the serif display face at h1 size — the
   editorial reskin's central move. Every screen shares this one pattern, which is
   why the whole product changes register here. */
.tz-page-head__title {
  margin: 0; font-family: var(--tz-font-display);
  font-size: var(--tz-text-h1); font-weight: 600; letter-spacing: -0.015em;
  line-height: 1.15; color: var(--tz-heading);
  /* U-17: half the titles in this product END IN AN IDENTIFIER, and a snake_case id is one
     unbreakable token in a 40px face — "Access to residencial_master" measured 324px inside a
     320px viewport and pushed the whole document sideways. `anywhere` and not `break-word`:
     only `anywhere` shrinks the min-content contribution, which is what a flex item's automatic
     minimum size is computed from, so `break-word` would wrap the text and still reserve the
     token's full width for the box. */
  overflow-wrap: anywhere;
}
/* Muted, not faint: the sub renders at body sizes when it stands alone under the
   title (the two data screens), and n400 fails AA there — measured at 3.70:1 in
   the September 2026 UI audit (M3). n600 keeps it quiet without failing. */
/* ❗️ONE SUBTITLE (Nacho, 20 September 2026). It was written two ways — this, and a
   `.tz-page-head__lead` in golden.css at 14px over 70ch with a 9px gap — split almost evenly
   across the product, so the same sentence was a different size depending on which screen you
   were on: 16px on the records list and the profile, 14px on all ten administration screens.
   The style guide settles it and this is the half that already conformed: "lead max-width 62–66ch
   at 16px", "title → lead 12px". `__lead` is gone and its fourteen users say `__sub`, which is
   also the word the guide's own § The page head uses. */
.tz-page-head__sub {
  max-width: 66ch; margin: 12px 0 0; color: var(--tz-text-muted);
  font-size: var(--tz-text-lg); font-weight: 400; line-height: 1.55;
}
/* ⚠️ NOT the subtitle: the qualifier that names WHICH one, inside the `<h1>` itself — "Events ·
   sink for residential", "Task · 3f2a". It wore `__sub` on three screens, which is one class doing
   two jobs: the subtitle is a block under the title with a width and a gap, and this is an inline
   run inside it, where a max-width and a margin-top mean nothing. Same size and same colour — they
   looked alike because they are meant to, and that is exactly why the two had to be told apart in
   the markup rather than in the eye. */
.tz-page-head__qualifier {
  color: var(--tz-text-muted); font-size: var(--tz-text-lg); font-weight: 400;
  letter-spacing: normal;
}

/* Inline edit affordance — an icon-only action that surfaces beside a title on
   hover, for content whose own name is editable (the dashboard).
   Revealed on hover of the title AND on keyboard focus, and left visible where
   hover does not exist: a hover-only control is unreachable by keyboard and
   invisible on touch. */
.tz-page-head__title .tz-inline-edit {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; margin-left: var(--tz-sp-2);
  vertical-align: middle; border-radius: var(--tz-r-md);
  color: var(--tz-text-faint); font-size: var(--tz-text-base); text-decoration: none;
  opacity: 0;
  transition: opacity .15s ease, background-color .15s ease, color .15s ease;
}
.tz-page-head__title:hover .tz-inline-edit,
.tz-page-head__title .tz-inline-edit:focus-visible { opacity: 1; }
.tz-page-head__title .tz-inline-edit:hover {
  background: var(--tz-surface-sunken); color: var(--tz-primary);
}
@media (hover: none) {
  .tz-page-head__title .tz-inline-edit { opacity: 1; }
}
/* Wraps so a long toolbar (the bucket-detail action groups run to 5 buttons)
   reflows onto more rows at narrow widths instead of scrolling the page. */
.tz-page-head__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* ⚠️ A rule for an action group rendered as a SIBLING of `.tz-page-head__main` — its own row
   under the title — used to sit here, giving it a top margin. U-26 closed the two pages that
   produced that markup (`/table/record` and `/resource/edit`), and no template produces it now:
   every action group is inside `__main`. A group that wraps takes the row gap above, which is
   `gap: 16px` on the flex container itself, so the rhythm did not move with the rule. Rules kept
   for a selector nothing renders are rules nobody can tell are broken — it went with them. */
/* ⚠️ A phone media block used to follow, wrapping this toolbar's .btn-group and .d-flex children:
   both are nowrap by construction, and at 390px /duplicate/show — three filter dropdowns in one
   d-flex, five bucket actions in one btn-group — pushed the page 108px sideways. That page is
   retired, and it was the only one whose page-head toolbar held either child; on the cluster board
   the same controls live in .tz-toolbelt, below the head, which wraps on its own. Rules kept for a
   selector no template produces are rules nobody can tell are broken, so they went with it. Put
   them back, with the render half of PhoneWidthToolbarsWrapTest, if a toolbar grows one again. */
/* Parent-context line under the title (replaces breadcrumbs on deep pages, e.g.
   a record naming its table). Quiet, small, with a link back to the parent. */
.tz-page-head__parent {
  display: flex; align-items: center; gap: 6px;
  margin-top: 4px; font-size: var(--tz-text-sm); color: var(--tz-text-muted);
}
.tz-page-head__parent a { color: var(--tz-text-muted); text-decoration: none; }
.tz-page-head__parent a:hover { color: var(--bs-link-color); }

/* Empty state — centered icon tile + headline + helper + optional CTA.
   Spec component; replaces the ad-hoc raw-hex block on the cluster grid. */
.tz-empty { text-align: center; padding: var(--tz-sp-8) var(--tz-sp-4); }
.tz-empty__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 56px; height: 56px; margin-bottom: var(--tz-sp-4);
  border-radius: var(--tz-r-lg); background: var(--tz-surface-sunken);
  /* n500 is allowed here: non-text glyph (3.5:1, threshold 3.0). */
  color: var(--tz-n500); font-size: 1.75rem;
}
.tz-empty__title {
  font-size: var(--tz-text-h4); font-weight: 600;
  color: var(--tz-heading); margin: 0 0 var(--tz-sp-2);
}
.tz-empty__desc {
  color: var(--tz-text-muted); max-width: 560px;
  margin: 0 auto var(--tz-sp-4); line-height: 1.6;
}

/* Stat row — labeled KPI columns split by hairlines (bucket detail header).
   Label above value: the label is the quiet micro-caption, the value carries. */
.tz-statrow { display: flex; flex-wrap: wrap; gap: var(--tz-sp-4) 0; }
.tz-statrow__item {
  flex: 1 1 0; min-width: 130px;
  padding: 0 var(--tz-sp-4); border-left: 1px solid var(--tz-border-subtle);
}
.tz-statrow__item:first-child { padding-left: 0; border-left: 0; }
/* n600, not n500: an 11px label is normal-size text, and n500 on white is
   3.9:1 — the accessibility table reserves n500 for >=18px or non-text. */
.tz-statrow__k {
  font-size: var(--tz-text-xs); text-transform: uppercase; letter-spacing: .04em;
  font-weight: 700; color: var(--tz-text-muted); margin-bottom: var(--tz-sp-2);
}
.tz-statrow__v {
  font-size: var(--tz-text-h2); font-weight: 600;
  line-height: 1.2; color: var(--tz-heading);
}
/* A badge in the value slot keeps chip scale — it must not inherit h2 sizing. */
.tz-statrow__v .badge { font-size: var(--tz-text-sm); vertical-align: middle; }

/* JSONEditor flattening: hide its own object header (our contextual header replaces it) and
   strip the nested Bootstrap card chrome so fields sit directly in our panel. Scoped via the
   tz-jsoneditor--flat class — only applied where the schema is a single (flat) object. */
.tz-jsoneditor--flat .je-object__title,
.tz-jsoneditor--flat .je-object__controls { display: none; }
.tz-jsoneditor--flat .card { border: 0; box-shadow: none; background: transparent; }
.tz-jsoneditor--flat .card-body { padding: 0; }

/* Error pages (error-main with app shell, error-plain standalone). */
.tz-error {
    min-height: 60vh;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    text-align: center; padding: var(--tz-sp-8) var(--tz-sp-4);
}
/* The figure is the headline (guide § Principles): the status code takes the hero
   ramp — display serif, ink — not the mono of an identifier. It is what the page
   is about. */
.tz-error__code {
    font-family: var(--tz-font-display); font-weight: 600; line-height: .9;
    font-size: clamp(3.5rem, 12vw, 4.75rem);
    color: var(--tz-heading); letter-spacing: -0.03em; font-variant-numeric: tabular-nums;
}
.tz-error__title {
    font-family: var(--tz-font-display);
    font-size: var(--tz-text-h2); font-weight: 600; color: var(--tz-heading);
    margin: var(--tz-sp-4) 0 var(--tz-sp-2);
}
.tz-error__desc { color: var(--tz-text-muted); max-width: 460px; margin: var(--tz-sp-6) auto var(--tz-sp-8); line-height: 1.8; }
.tz-error__actions { display: flex; gap: var(--tz-sp-2); flex-wrap: wrap; justify-content: center; }
.tz-error--plain { min-height: 0; padding: var(--tz-sp-6) 0 0; }

/* ============================================================================
   COMPONENT LIBRARY. Strategy: override the Bootstrap classes already present in
   the markup (.btn, .form-control, .table, .alert, .modal, .badge, .nav-underline)
   so every page is restyled with no markup churn. Bespoke tz-* classes only where
   Bootstrap has no equivalent (status pills, tag chips, the page header, the
   auth-page layout). All colors via the semantic aliases at the top of this file.
   ============================================================================ */

/* ---- Form fields ---- */
/* ⚠️ ONE CONTROL HEIGHT: 36px, the button's (see THE ONE BUTTON below), and this is the rule
   that makes a field agree with it. Bootstrap sizes a field from its type — 1.5em + .75rem +
   2px — which at this theme's 14px lands on 35, one short of the button standing beside it;
   the search bar and the advanced search were already forcing 36 by hand, one selector each.
   min-height and not height, so a textarea still grows and the touch rule further down can
   raise it. The rich dropdown (Choices) is held to the same number in golden.css, and it used
   to sit at 38 empty and 44 with a value in it (Nacho, 17 September 2026). */
.form-control, .form-select {
  min-height: 36px;
  border-color: var(--tz-border); border-radius: var(--tz-r-md);
  color: var(--tz-text); background-color: var(--tz-surface);
}
/* The small variant dies with the button's: one height, .form-control-sm included. */
.form-control.form-control-sm, .form-select.form-select-sm { min-height: 36px; }

/* An input group is ONE control: the field and its buttons share a border and only the outer
   corners are rounded. Bootstrap writes exactly that, and the theme's radius on `.btn` used to
   overrule it, so a date editor's calendar and clear buttons stood as two rounded boxes a pixel
   apart from the field. The icon sits centred because the button's gap and side padding are
   for a label, and these buttons have none. */
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
  border-top-left-radius: 0; border-bottom-left-radius: 0;
}
.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),
.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) {
  border-top-right-radius: 0; border-bottom-right-radius: 0;
}
.input-group > .btn { --bs-btn-border-radius: var(--tz-r-md); }
/* An icon-only button — a row's delete, the date editor's calendar and clear — has no label to
   leave a gap for, so the icon sits in its middle. The class is put on by golden-json-editor.js
   (`tidyIconButtons`), which is what knows a button's span holds no words. */
.btn.tz-btn--icon { gap: 0; padding: 0 11px; }
/* ⚠️ U-16: MUTED, NOT DISABLED. A placeholder is the only thing an empty field says —
   on the records search it is the whole instruction — and the disabled token measured
   2.70:1 light (n400 on white) and 3.82:1 dark (n500 on the recessed field), both under
   AA. Muted reads at 5.64:1 / 8.12:1 and still sits a step below the value's own ink
   (--tz-text, n800 / n200), which is what keeps a hint from looking like a value. Same
   decision, and the same reason, as `.choices__placeholder` in golden.css. */
.form-control::placeholder { color: var(--tz-text-muted); }
.form-control:hover:not(:focus):not(:disabled),
.form-select:hover:not(:focus):not(:disabled) { border-color: var(--tz-border-strong); }
/* ❗️U-27. THE FIELD LABEL IS THE 11px UPPERCASE MICRO-LABEL, here as it is in `.tz-field__label`,
   and this rule is what makes that true of the json-editor screens. The product had four label
   vocabularies: this class at 13px/500 sentence case (the record and the entity form, both drawn
   by json-editor), `.tz-field__label`'s micro-label on the hand-written forms, and two local
   copies of the micro-label written against `#editor_holder` and `.tz-step2-field`. Ruled on
   13 September 2026: the micro-label wins and json-editor adopts it — the hierarchy a record
   needs is the value carrying the weight rather than its name. The cost was put to the owner and
   accepted: a long label such as "Consumo anual estimado (kWh)" reads slightly worse in caps.
   An exception for the record form was refused, because an exception per screen is how four
   vocabularies happen in the first place.

   ⚠️ It belongs on the bare class and not inside a json-editor scope, and that is measured, not
   tidiness: the entity form MOVES json-editor's fields out of `#editor_holder` into its own
   panels (`.tz-entity-editor__fields`), so a rule hung off the holder or off
   `.tz-jsoneditor--flat` reaches the record and not the entity. Three templates write
   `.form-label` by hand (two in entity-access.html, one in table-record.html's save modal) and
   they are field labels too, so they are in scope rather than collateral. The label's PLACE — on
   its own line above the control, and the air under it — is a different rule and stays where it
   is, in golden.css beside the reason it exists. */
.form-label {
  font-size: var(--tz-text-xs);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  /* Muted and not n500: these render at 11px, where n500 measures 3.70:1 (audit M3). */
  color: var(--tz-text-muted);
}
.form-text, .form-control-plaintext { color: var(--tz-text-muted); }
.form-control.is-invalid, .form-select.is-invalid { border-color: var(--tz-error); }
.form-control.is-invalid:focus, .form-select.is-invalid:focus {
  box-shadow: 0 0 0 2px var(--tz-error); border-color: var(--tz-error);
}
.invalid-feedback { color: var(--tz-error-ink); font-size: var(--tz-text-sm); }
.form-floating > label { color: var(--tz-text-muted); }
.input-group-text { background: var(--tz-surface-sunken); border-color: var(--tz-border); color: var(--tz-text-muted); }
.form-check-input:checked { background-color: var(--tz-primary); border-color: var(--tz-primary); }
.form-check-input:focus { border-color: var(--tz-primary); box-shadow: 0 0 0 2px rgba(var(--tz-primary-rgb),.25); }

/* ---- Alerts (flat, tinted, borderless) ---- */
.alert { border: 1px solid transparent; border-radius: var(--tz-r-md); font-size: var(--tz-text-base); padding: 12px 14px; }
/* Status alerts keep their tinted fill — status is fill-worthy — plus the hairline
   every raised surface owes the brand book ("card without border" anti-pattern). */
.alert-danger  { --bs-alert-bg: var(--tz-error-bg);   --bs-alert-color: var(--tz-error-fg);   border-color: rgba(var(--tz-error-rgb), .35); }
.alert-success { --bs-alert-bg: var(--tz-success-bg); --bs-alert-color: var(--tz-success-fg); border-color: rgba(var(--tz-success-rgb), .35); }
.alert-warning { --bs-alert-bg: var(--tz-warning-bg); --bs-alert-color: var(--tz-warning-fg); border-color: rgba(var(--tz-warning-rgb), .35); }
/* Informational is not status: an editorial strip — two rules and a line of text —
   instead of a filled box. In dark mode the old fill was a saturated blue block
   louder than anything else on the page (audit M10). */
.alert-primary, .alert-info {
  --bs-alert-bg: transparent; --bs-alert-color: var(--bs-body-color);
  border: 0; border-top: 1px solid var(--tz-border); border-bottom: 1px solid var(--tz-border);
  border-radius: 0; padding: 12px 2px;
}
.alert a { color: inherit; font-weight: 600; }

/* .table-light pins Bootstrap's own light fill (one use, resource-main:725) —
   point it at the aliases so the header follows the theme. */
.table-light {
  --bs-table-bg: var(--tz-surface-sunken);
  --bs-table-color: var(--tz-text-muted);
  --bs-table-border-color: var(--tz-border);
}

/* ---- Tables (header, dividers, hover, stripe). tz-table density rules are above. ---- */
.table {
  --bs-table-color: var(--tz-text-muted);
  --bs-table-border-color: var(--tz-border-subtle);
  --bs-table-striped-bg: var(--tz-surface-sunken);
  --bs-table-striped-color: var(--tz-text-muted);
  --bs-table-hover-bg: var(--tz-surface-sunken);
  --bs-table-hover-color: var(--tz-text);
  font-size: var(--tz-text-base);
}
.table > thead > tr > th {
  font-size: var(--tz-text-xs); text-transform: uppercase; letter-spacing: .04em;
  font-weight: 600; color: var(--tz-text-muted);
  border-bottom: 1px solid var(--tz-border); background: transparent;
}
.table > tbody > tr > td { border-bottom-color: var(--tz-border-subtle); vertical-align: middle; color: var(--tz-text-muted); }

/* Modal/toast close X. Bootstrap's default focus shadow is a literal blue
   (--bs-btn-close-focus-shadow: rgba(13,110,253,.25)) and .btn-close is not a
   .btn, so the tokenized .btn:focus-visible ring does not reach it. NiceAdmin's
   style.css used to suppress it entirely; give it the same 2px teal ring every
   other control has, on keyboard focus only. */
.btn-close { --bs-btn-close-focus-shadow: none; }
.btn-close:focus-visible { box-shadow: 0 0 0 2px var(--tz-primary); }
[data-bs-theme="dark"] .btn-close:focus-visible { box-shadow: 0 0 0 2px var(--tz-accent); }

/* ---- Modals ---- */
.modal-content {
  border: 1px solid var(--tz-border); border-radius: var(--tz-r-lg);
  box-shadow: var(--tz-shadow-lg);
}
.modal-header { border-bottom: 1px solid var(--tz-border-subtle); padding: 16px 20px; }
.modal-title { font-size: var(--tz-text-lg); font-weight: 600; color: var(--tz-heading); }
.modal-body { padding: 16px 20px; }
.modal-footer { border-top: 1px solid var(--tz-border-subtle); padding: 12px 20px; }
.modal-backdrop { background-color: var(--tz-n950); }
.modal-backdrop.show { opacity: .4; }

/* ---- Tabs (underline) ---- */
.nav-underline { border-bottom: 1px solid var(--tz-border); gap: 2px; }
.nav-underline .nav-link {
  color: var(--tz-text-muted); font-weight: 500; border: 0; border-bottom: 2px solid transparent;
  padding: 8px 12px; margin-bottom: -1px;
}
.nav-underline .nav-link:hover { color: var(--tz-text-muted); }
.nav-underline .nav-link.active {
  color: var(--tz-primary); border-bottom-color: var(--tz-primary);
  font-weight: 600; background: transparent;
}
/* Dark: brand teal is too dark for the active tab — use the legible accent teal. */
[data-bs-theme="dark"] .nav-underline .nav-link.active {
  color: var(--tz-accent); border-bottom-color: var(--tz-accent);
}
/* A card header holding underline tabs already shows the tabs' own bottom border; drop the
   header border so the two don't stack into a double line. */
.card > .card-header:has(> .nav-underline) { border-bottom: 0; }

/* ---- Auth pages (login / reset / password): the letterhead ----
   Option A, settled with Nacho on 2 September 2026. The floating card retires —
   even as an allowed box it read as the old style — and the page composes like
   the opening page of a report: the app's own brand (icon + Golden), an eyebrow,
   the serif headline, a rule, then the form as a passage. One left-aligned
   column, generous air. */
/* ❗️Centred on both axes since 17 September 2026, amending Option A: the letterhead used to
   hang from the top of the viewport with 96px of padding above it, which on a tall screen left
   the whole passage sitting in the upper third under a large empty band. The column now sits in
   the middle of the page. min-height, never height, so a page taller than the viewport —reset's
   SSO caveat, a stack of validation errors— grows the wrapper instead of centring the overflow
   out of reach; the 48px of vertical padding is what the column keeps on a short screen. The
   dvh line is for mobile, where 100vh counts a browser chrome that is not there. */
.tz-auth-wrap {
  min-height: 100vh; min-height: 100dvh;
  display: flex; align-items: center; justify-content: center; padding: 48px 24px;
}
.tz-auth-col { width: 400px; max-width: 100%; }
/* The masthead's .tz-brand carries the icon/wordmark pair; here it opens the
   letterhead instead of a bar, so it sheds the bar's height and margin. */
.tz-auth-col .tz-brand { height: auto; margin-right: 0; }
/* ❗️Those 72px are the gap that opens the letterhead, and they used to ride on the eyebrow —
   so when login dropped its eyebrow on 17 September 2026 the gap went with it and the title
   rode up under the wordmark. The space belongs to the passage, not to whichever line happens
   to sit first in it, so it was given to the title instead. ⚠️ Since the eyebrow was retired
   everywhere (27 September 2026) this is the ONLY rule holding that gap: reset and password
   used to lead with one and now lead with the title, so all three auth screens match here. */
.tz-auth-col .tz-brand + .tz-auth-title { margin-top: 72px; }
.tz-auth-title {
  font-family: var(--tz-font-display); font-size: var(--tz-text-h1); font-weight: 600;
  letter-spacing: -0.015em; line-height: 1.15; color: var(--tz-heading); margin: 0;
}
.tz-auth-sub { font-size: 16px; color: var(--tz-text-muted); margin: 12px 0 0; }
.tz-auth-form { margin-top: 38px; padding-top: 30px; border-top: 1px solid var(--tz-border); }
.tz-auth-form .tz-field { margin-bottom: 22px; }
.tz-auth-form .tz-inflow__actions { margin-top: 30px; }
/* Intro paragraph of a form passage (reset's SSO caveat) — body text, measured. */
.tz-auth-help { font-size: var(--tz-text-base); color: var(--tz-text-muted); line-height: 1.6; margin: 0 0 22px; }
/* Status panels (wrong credentials, mail sent) keep their fill; they open the
   passage, above the form's rule. */
.tz-auth-col .alert { margin: 24px 0 0; }
.tz-auth-foot {
  margin-top: 38px; padding-top: 14px; border-top: 1px solid var(--tz-border-subtle);
  display: flex; justify-content: space-between; gap: 16px;
  font-size: var(--tz-text-sm); color: var(--tz-text-muted);
}
.tz-auth-foot a { color: var(--tz-accent-blue); }

/* ---- Utilities: keep the .bg-light surface teal-tinted (code blocks, panels) ---- */
.bg-light { background-color: var(--tz-surface-sunken) !important; }

/* ============================================================================
   LEGACY BRIDGE — styles formerly supplied by NiceAdmin (static/css/style.css).
   NOTE: style.css was retired from header-head.html and deleted in P6. The rules
   below are the token-based equivalents of the NiceAdmin selectors that were
   still live in markup at that point.
   ============================================================================ */

/* .card-title appears on headings (already covered by the h* rules) AND on plain
   <div>s that previously relied solely on NiceAdmin. Style the class directly so
   both render identically. Montserrat is reserved for h1/h2, so card titles take
   the Inter sub-heading treatment to match .tz-card__title. */
.card-title {
  font-family: var(--tz-font-sans);
  font-size: var(--tz-text-lg);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
  color: var(--tz-heading);
  margin-bottom: var(--tz-sp-3);
}

/* Dropdowns: NiceAdmin supplied the shadow/border polish; keep a tokenized one
   (topbar user menu, pagination per-page menu). */
.dropdown-menu {
  --bs-dropdown-border-color: var(--tz-border);
  --bs-dropdown-border-radius: var(--tz-r-md);
  --bs-dropdown-box-shadow: var(--tz-shadow-md);
  --bs-dropdown-link-hover-bg: var(--tz-surface-sunken);
  --bs-dropdown-link-active-bg: var(--tz-primary);
  font-size: var(--tz-text-base);
}

/* Pagination — Bootstrap compiles the active bg to a literal blue, so remap the
   --bs-pagination-* vars to brand tokens (teal active, neutral links). */
.pagination {
  --bs-pagination-color: var(--tz-text-muted);
  --bs-pagination-bg: var(--tz-surface);
  --bs-pagination-border-color: var(--tz-border);
  --bs-pagination-hover-color: var(--tz-heading);
  --bs-pagination-hover-bg: var(--tz-surface-sunken);
  --bs-pagination-hover-border-color: var(--tz-border);
  --bs-pagination-focus-color: var(--tz-heading);
  --bs-pagination-focus-bg: var(--tz-surface-sunken);
  --bs-pagination-focus-box-shadow: 0 0 0 2px var(--tz-primary);
  --bs-pagination-active-color: var(--tz-white);
  --bs-pagination-active-bg: var(--tz-primary);
  --bs-pagination-active-border-color: var(--tz-primary);
  --bs-pagination-disabled-color: var(--tz-text-disabled);
  --bs-pagination-disabled-bg: var(--tz-surface);
  --bs-pagination-disabled-border-color: var(--tz-border);
}

/* Labeled read-only field rows — the display counterpart to a form. Used where
   a screen shows data that was previously dressed as `readonly` inputs.
   __k is n600, not n500: a 13px label is normal-size text, and n500 on white is
   3.9:1 — the accessibility table reserves n500 for >=18px or non-text. */
.tz-meta { display: flex; flex-direction: column; }
.tz-meta__row {
  display: flex; align-items: baseline; gap: var(--tz-sp-4);
  padding: 7px 0; border-bottom: 1px solid var(--tz-border-subtle);
  font-size: var(--tz-text-sm);
}
.tz-meta__row:last-child { border-bottom: 0; }
.tz-meta__k {
  flex: 0 0 132px; color: var(--tz-text-muted); font-weight: 600;
}
/* Values wrap and can hold a badge, <code>, <pre> or multi-line text. */
.tz-meta__v { flex: 1 1 auto; min-width: 0; color: var(--tz-text); overflow-wrap: anywhere; }
.tz-meta__v pre, .tz-meta__v code { font-size: var(--tz-text-sm); margin: 0; }
.tz-meta__v pre {
  background: var(--tz-surface-sunken); border: 1px solid var(--tz-border-subtle);
  border-radius: var(--tz-r-sm); padding: 8px 10px; white-space: pre-wrap;
}
/* Stack label above value on narrow viewports so neither gets squeezed. */
@media (max-width: 575.98px) {
  .tz-meta__row { flex-direction: column; gap: var(--tz-sp-1); }
  .tz-meta__k { flex-basis: auto; }
}

/* Stack the rail above the panes on narrow viewports. */

/* ===== RECORD TABS ============================================================
   The record screen's own strip: Data / Quality / Audit / Metadata, switching
   panels in place. It is NOT the bar's own `.tz-topnav` — that one is
   navigation between routes and lives in the sticky masthead. These are tabs
   within one page, so they scroll with their content.

   ⚠️ role="tab" and not aria-pressed. The v4 mock draws these as toggle
   buttons, which is a different widget: a toggle button announces "pressed",
   offers no arrow-key movement between siblings, and gives a screen reader no
   relationship to the panel it reveals. The markup here is tablist / tab /
   tabpanel with a roving tabindex, which is what the pattern actually is. */
.tz-record-tabs {
  display: flex; align-items: center; gap: var(--tz-sp-4);
  margin: 0 0 var(--tz-sp-4);
  border-bottom: 1px solid var(--tz-border);
  overflow-x: auto;
}
.tz-record-tabs__tab {
  display: inline-flex; align-items: center; gap: var(--tz-sp-2);
  height: 42px; padding: 0 var(--tz-sp-1);
  background: none; border: 0; border-bottom: 2px solid transparent;
  color: var(--tz-text-muted); font-size: var(--tz-text-base); font-weight: 500;
  white-space: nowrap; cursor: pointer;
}
.tz-record-tabs__tab:hover { color: var(--tz-heading); }
.tz-record-tabs__tab:disabled {
  color: var(--tz-text-faint); cursor: not-allowed; opacity: .58;
}
.tz-record-tabs__tab:disabled:hover { color: var(--tz-text-faint); }
.tz-record-tabs__tab[aria-selected="true"] {
  color: var(--tz-primary); border-bottom-color: var(--tz-primary); font-weight: 600;
}
/* Dark: brand teal is too dark for an active label — the accent reads. Same
   decision, and the same reason, as the masthead's own active link. */
[data-bs-theme="dark"] .tz-record-tabs__tab[aria-selected="true"] {
  color: var(--tz-accent); border-bottom-color: var(--tz-accent);
}
/* The focus ring must survive the transparent background: keyboard users move
   through these with arrow keys and nothing else marks where they are. */
.tz-record-tabs__tab:focus-visible {
  outline: 2px solid var(--tz-accent-blue); outline-offset: -2px; border-radius: var(--tz-r-sm);
}
.tz-record-tabs__count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 18px; padding: 0 6px;
  background: var(--tz-surface-sunken); color: var(--tz-text-muted);
  border-radius: var(--tz-r-full); font-size: var(--tz-text-xs); font-weight: 600;
}
.tz-record-tabs__tab[aria-selected="true"] .tz-record-tabs__count {
  background: var(--tz-primary); color: var(--tz-white);
}
[data-bs-theme="dark"] .tz-record-tabs__tab[aria-selected="true"] .tz-record-tabs__count {
  background: var(--tz-accent); color: var(--tz-n950);
}

/* ===== RECORD SPLIT LAYOUT ====================================================
   Content beside a rail. The rail carries what to do next, so on a phone it
   comes FIRST: a steward opening a record on a phone should see "three things
   need attention" before scrolling through the fields that are fine. */
.tz-record-split { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: var(--tz-sp-4); }
/* With the findings aside gone the rail is usually empty (only the legacy block ever
   fills it now): an empty rail disappears and the form takes the whole row. */
.tz-record-split > aside:not(:has(*)) { display: none; }
.tz-record-split:not(:has(> aside *)) { grid-template-columns: minmax(0, 1fr); }
.tz-record-split__side { align-self: start; position: sticky; top: 72px; }
/* ⚠️ THE SPLIT ASKS THE SPACE IT HAS, NOT THE WINDOW (28 September 2026, applying D-7). The second
   of the three things the decision record left open: with the context bar this screen has three
   columns, and the record measured the form at 648px at 1440 and 488 at 1280 — "the number to
   watch". Its answer was "the record may want to be born with the context folded", which is a
   per-page default fighting a preference the reader has already expressed and the browser already
   remembers.
   The real defect is the entity catalogue's: these cuts ask the WINDOW how much room the form has,
   and the bar takes 344px the window cannot see. Asked of the panel instead, 1279.98 and 991.98 do
   exactly what they were written to do — at 1440 with the bar open the panel is 984, below 992, so
   the quality rail stacks under the form and the form gets all 984 rather than 648; fold the bar
   and the rail comes back beside it at its full 320. Nothing is born folded and no number moved.
   The 640px rule stays a media query on purpose: it is about the order things are read in on a
   phone, which is a property of the device and not of the box. */
#record-panel-data { container-type: inline-size; container-name: record; }
@container record (max-width: 1279.98px) {
  .tz-record-split { grid-template-columns: minmax(0, 1fr) 280px; }
}
@container record (max-width: 991.98px) {
  .tz-record-split { grid-template-columns: minmax(0, 1fr); }
  .tz-record-split__side { position: static; }
}
@media (max-width: 640px) {
  .tz-record-split__side { order: -1; }
}

/* ---- Narrow screens: the gutters close up. The chrome's own containment is NOT here any
   more — it is on .tz-topnav itself, at every width; see the ⚠️ note there (U-18). ---- */
@media (max-width: 767.98px) {
  /* ⚠️ Below `md` the sections do not render as a row at all — they move WHOLE into the menu,
     labelled with the active one (D-8, measured: 23px over at 390px). So this is no longer about
     a swipe: the row is hidden, and what is left is the place control, the "more" button and the
     corner. The scrollbar rule stays for the band between this cut and the width where the row
     genuinely fits, where a finger still does the scrolling.

     ⚠️ AND THE REST OF THIS BLOCK IS ARITHMETIC, MEASURED AT 390px. What is left still has to
     fit, and with the desktop values it did not: the brand icon, a 38vw place control, the rule,
     the gear and the avatar came to 325px of the 390, leaving 65 for a button that wants 85 —
     so "Usuarios ▾" rendered as "Usuario" with its last letter clipped and no ellipsis, because
     the caret is a flex item that takes its room first. Six declarations buy the 46px back: the
     gutters (8), the gaps (10), the brand's own margin (8), the place control at 34vw (15) and
     the rule, which is 13px of decoration between two things a phone reader can already tell
     apart. */
  .tz-topbar { padding: 0 12px; gap: 6px; }
  .tz-brand { margin-right: 2px; }
  .tz-topbar > .tz-rule-sep { display: none; }
  .tz-topnav { scrollbar-width: none; }
  .tz-topnav::-webkit-scrollbar { display: none; }
  .tz-place__toggle { max-width: 34vw; padding: 0 9px; }
  .tz-topnav__more-toggle { max-width: 40vw; padding: 0 8px; }
  .tz-main { padding: 28px 20px 56px; }
}

/* ---- Phone: the last 103px, so that the first level FITS instead of nearly fitting ----------
   ⚠️ MEASURED, NOT ESTIMATED, AND THE ARITHMETIC IS THE ARGUMENT. With "All" collapsing the
   entities (see the block above), a 390px bar still asked for 308px of chrome in the 205px it
   has: *Your work* · sep · All · sep · *Administration*, 103px past the edge, with
   "Administration" the word that lands past it. That is N-3 with three digits knocked off it —
   it was 2,106px in 193px — but it is still a first-level entry a phone user cannot see.

   Three declarations close it, and each is measured at 390px in Chromium and Firefox:
     · the wordmark, 73px with its gap. The icon stays, and it is still the link to the root:
       what goes is the word beside it, which is the one thing on this bar that is not
       navigation. 320 → 390px is where the whole product's chrome does not fit at once, and
       between the brand and the navigation the navigation wins.
     · the two separators, 12px each. They are aria-hidden decoration between groups that the
       order already separates.
     · 3px of inline padding per link, 18px over the six that are left.
   Total 115px against the 103px needed, so the bar fits at 390 with room, and at 360 it is
   18px short — below that the nav still scrolls inside itself and the fade still says so.

   ⚠️ The breakpoint is `sm`, NOT the 767.98px block above, on purpose: at 575px and at 768px
   the bar already fits with the wordmark, and taking it away where it costs nothing to keep is
   a loss for no gain. It is the same breakpoint PhoneWidthToolbarsWrapTest calls "the phone
   breakpoint the rest of this CSS uses". */
@media (max-width: 575.98px) {
  /* Scoped to the bar: login, password and reset draw the same .tz-brand on a page with no
     navigation to make room for, and they keep the word at every width. */
  .tz-topbar .tz-brand__word { display: none; }
  .tz-rule-sep { margin: 0 4px; }
  .tz-topnav > a, .tz-topnav__more-toggle { padding: 0 8px; }
}


/* ============================================================================
   THE ONE BUTTON (September 2026, settled with Nacho).
   Every button in the product is the same control: 36px tall, surface fill,
   hairline border, muted 13px/500 text. No filled primary, no size variants —
   sameness IS the simplification. The only permitted difference is DESTRUCTIVE,
   which lights red instead of teal under the pointer — and only then.
   Hover lights the whole body: teal fill, white text — the strong contrast is
   the pointer's reward, not a resting state. Dark mode swaps teal for accent.
   This block loads last and overrides every earlier role on purpose, so no
   screen needs adjusting one by one.
   ============================================================================ */
.btn,
.btn-primary, .btn-secondary,
.btn-outline-primary, .btn-outline-secondary,
.btn-success, .btn-outline-success, .btn-info, .btn-outline-info {
  --bs-btn-font-size: var(--tz-text-sm);
  --bs-btn-font-weight: 500;
  --bs-btn-color: var(--tz-text-muted);
  --bs-btn-bg: var(--tz-surface);
  --bs-btn-border-color: var(--tz-border);
  --bs-btn-hover-color: var(--tz-white);
  --bs-btn-hover-bg: var(--tz-primary);
  --bs-btn-hover-border-color: var(--tz-primary);
  --bs-btn-active-color: var(--tz-white);
  --bs-btn-active-bg: var(--tz-primary-dark);
  --bs-btn-active-border-color: var(--tz-primary-dark);
  --bs-btn-disabled-color: var(--tz-text-disabled);
  --bs-btn-disabled-bg: var(--tz-surface);
  --bs-btn-disabled-border-color: var(--tz-border);
}
.btn {
  height: 36px;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 0 14px; line-height: 1;
}
/* Size variants die: one height everywhere, .btn-sm and .btn-lg included. The size is set
   through the Bootstrap variable and not as a direct declaration (U-25): one source governs
   the label size of every button, so a later change to --bs-btn-font-size reaches all of
   them instead of all-but-two. */
.btn.btn-sm, .btn.btn-lg { height: 36px; padding: 0 14px; --bs-btn-font-size: var(--tz-text-sm); }
/* ⚠️ AND THE GROUP FORM OF THE SAME TWO VARIANTS, WHICH IS NOT THE SAME SELECTOR. Bootstrap
   sizes a group by its parent — `.btn-group-sm > .btn`, (0,2,0) — and never adds `.btn-sm` to
   the child, so the rule above cannot see it. Left out, U-25 closed the shadowing on `.btn`
   and immediately reopened it one selector along: on /table/show?adv=1 the All fields / Any
   field pair sat at 14px beside Clear and Search at 13px, in the same panel. Only the size
   variable needs restating — Bootstrap sizes the padding through --bs-btn-padding-*, which
   `.btn`'s own direct `padding` already overrules. */
.btn-group-sm > .btn, .btn-group-lg > .btn { --bs-btn-font-size: var(--tz-text-sm); }
/* .btn-link stays the inline-action utility (icon edit/remove inside rows) and is
   exempt from the control chrome — it has no box to keep equal. */
.btn.btn-link { height: auto; padding: 0; border: 0; background: none; }

/* DESTRUCTIVE: the one permitted difference, and it only shows when the pointer
   arrives — the hover and the press light red instead of teal. At rest it is the
   same button as every other: muted ink, surface fill, hairline border. It used to
   wear red ink at rest too, and on a record page that made Delete the loudest
   thing on the line while it was merely available (17 September 2026). */
.btn-danger, .btn-outline-danger {
  --bs-btn-bg: var(--tz-surface);
  --bs-btn-border-color: var(--tz-border);
  --bs-btn-hover-color: var(--tz-white);
  --bs-btn-hover-bg: var(--tz-error);
  --bs-btn-hover-border-color: var(--tz-error);
  --bs-btn-active-color: var(--tz-white);
  --bs-btn-active-bg: var(--tz-error);
  --bs-btn-active-border-color: var(--tz-error);
  --bs-btn-disabled-color: var(--tz-text-disabled);
  --bs-btn-disabled-bg: var(--tz-surface);
  --bs-btn-disabled-border-color: var(--tz-border);
}

/* Dark: the lit body is the accent, as everywhere else emphasis is. */
[data-bs-theme="dark"] .btn,
[data-bs-theme="dark"] .btn-primary, [data-bs-theme="dark"] .btn-secondary,
[data-bs-theme="dark"] .btn-outline-primary, [data-bs-theme="dark"] .btn-outline-secondary {
  --bs-btn-hover-color: var(--tz-primary-dark);
  --bs-btn-hover-bg: var(--tz-accent);
  --bs-btn-hover-border-color: var(--tz-accent);
  --bs-btn-active-color: var(--tz-primary-dark);
  --bs-btn-active-bg: var(--tz-accent);
  --bs-btn-active-border-color: var(--tz-accent);
}
/* ❗️DESTRUCTIVE STAYS RED IN DARK. The rule above names `.btn` itself, so it reached the
   destructive buttons too and lit Delete in the accent, exactly like Edit beside it — the one
   permitted difference between buttons, gone in the theme where it is needed most (Nacho,
   17 September 2026). The error tokens are already the dark-aware pair, so no second colour. */
[data-bs-theme="dark"] .btn-danger, [data-bs-theme="dark"] .btn-outline-danger {
  --bs-btn-hover-color: var(--tz-white);
  --bs-btn-hover-bg: var(--tz-error);
  --bs-btn-hover-border-color: var(--tz-error);
  --bs-btn-active-color: var(--tz-white);
  --bs-btn-active-bg: var(--tz-error);
  --bs-btn-active-border-color: var(--tz-error);
}

/* ❗️A TOGGLE'S PRESSED STATE IS A STATE, NOT A ROLE, and it was the one thing the one-button
   settlement took away without noticing. `help-toggle.js` swapped `btn-outline-secondary` for
   `btn-primary` to say the field help was on — written before every role was flattened to the
   same quiet control, after which the two classes render identically and the button said
   nothing. The only colour it ever showed was the hover, which is why it read as "the state is
   out of sync", and on a phone, where hover sticks to whatever was last tapped, it read as
   permanently on (Nacho, 19 September 2026).
   Keyed on `aria-pressed`, which the button already carries and which is the accessible source
   of truth, so no class swap can drift from it. The look is the product's own pressed look, the
   one the case screen's mode buttons use.

   ⚠️ `.tz-filter-toggle` opts OUT of the paint while keeping the state (Nacho, 29 September
   2026). It is for a toggle whose on-ness is already stated somewhere the reader can act on —
   today the duplicates board's Escalated, whose filter leaves a removable chip with the others.
   There the solid fill said the same thing twice, and said it in the one place you could not
   undo it from. Excluded rather than overridden on purpose: restating the unpressed look would
   mean a second copy of `.btn-outline-primary`'s three tokens, in two themes, to drift.
   ⚠️ It removes the COLOUR, never the `aria-pressed`: a screen reader has no chip to look at. */
.btn[aria-pressed="true"]:not(.tz-filter-toggle) {
  --bs-btn-color: var(--tz-white);
  --bs-btn-bg: var(--tz-primary);
  --bs-btn-border-color: var(--tz-primary);
}
[data-bs-theme="dark"] .btn[aria-pressed="true"]:not(.tz-filter-toggle) {
  --bs-btn-color: var(--tz-primary-dark);
  --bs-btn-bg: var(--tz-accent);
  --bs-btn-border-color: var(--tz-accent);
}
/* ---- The development shortcut: dark, and deliberately outside the four button roles ----------
   ⚠️ IT HAS TO LIVE HERE, AFTER THE RESTING-STATE BLOCK ABOVE, and that is the whole reason this
   rule is in theme.css rather than beside its layout in golden.css: that block sets `.btn` last
   "on purpose, so no screen needs adjusting one by one", so an earlier file cannot win. Written in
   golden.css it rendered white and looked like a broken token.
   ⚠️ NOT `btn-dark`, which the roles block above retires by name. The four roles are the PRODUCT's
   vocabulary and this button is not product: LoginController publishes it only outside the prod
   profile and only under `trazadera.development`, so in every deployment it does not exist. Dark so
   a developer reads it as exceptional rather than as a second way in (Nacho, 29 September 2026). */
.btn.tz-devbtn {
  --bs-btn-bg: var(--tz-n800);
  --bs-btn-border-color: var(--tz-n800);
  --bs-btn-color: var(--tz-white);
  --bs-btn-hover-bg: var(--tz-n900);
  --bs-btn-hover-border-color: var(--tz-n900);
  --bs-btn-hover-color: var(--tz-white);
  --bs-btn-active-bg: var(--tz-n900);
  --bs-btn-active-border-color: var(--tz-n900);
  --bs-btn-active-color: var(--tz-white);
}

/* ⚠️ AND NO HOVER WHERE THERE IS NO POINTER. "Hover lights the whole body — the strong contrast
   is the pointer's reward, never a resting state" (guide, § Forms and actions). A touch screen
   has no pointer: the browser applies `:hover` to whatever was last tapped and leaves it there
   until something else is tapped, so every button a finger touched stayed lit — and on a toggle
   that is indistinguishable from "on". The hover variables fall back to the resting ones there;
   the press still answers, through `:active`. */
@media (hover: none) {
  .btn, .btn-primary, .btn-secondary, .btn-outline-primary, .btn-outline-secondary,
  .btn-success, .btn-outline-success, .btn-info, .btn-outline-info,
  .btn-danger, .btn-outline-danger {
    --bs-btn-hover-color: var(--bs-btn-color);
    --bs-btn-hover-bg: var(--bs-btn-bg);
    --bs-btn-hover-border-color: var(--bs-btn-border-color);
  }
}

/* The keyboard chip stays readable when the body lights up. */
.btn:hover .badge, .btn:active .badge {
  background: rgba(var(--tz-white-rgb), .22) !important;
  color: var(--tz-white) !important;
}
[data-bs-theme="dark"] .btn:hover .badge, [data-bs-theme="dark"] .btn:active .badge {
  background: rgba(2, 35, 45, .25) !important;
  color: var(--tz-primary-dark) !important;
}

/* ============================================================================
   TOUCH TARGETS (U-20, register decision 9 of 7 September 2026).
   Under a coarse pointer every control is at least 44px in its smallest
   dimension. Measured before this block: action links 16.5px, the account
   avatar 30px, filter pills 30px, buttons 36px — nothing in the product reached
   44 and a finger has no hover to correct with.

   ⚠️ `@media (pointer: coarse)` ONLY, and that is the whole of the decision.
   THE ONE BUTTON above argues its 36px at length and the desktop keeps it: a
   mouse is precise, the rule is about sameness, and inflating every button on a
   desktop to phone proportions would undo the settlement rather than serve it.

   It sits at the end of the last stylesheet on purpose. .tz-filter-pill and
   .tz-more are declared in golden.css, which loads first; putting the policy
   here lets one block raise every control without an `!important` anywhere, and
   a touch policy is one decision, not five scattered ones.

   .btn-link is deliberately left out: `.btn.btn-link { height: auto }` above is
   two classes to this rule's one, so an inline icon action inside a table row
   keeps its size and a row on a phone does not become 44px of chrome.
   ============================================================================ */
@media (pointer: coarse) {
  .btn { height: 44px; }
  .btn.btn-sm, .btn.btn-lg { height: 44px; }
  .tz-avatar { width: 44px; height: 44px; }
  /* ❗️THE TARGET GROWS, THE PILL DOES NOT. `min-height: 44px` on a 30px pill whose radius is
     full turned every filter into a fat capsule — 44px of height around 15px of text, six of
     them stacked on a phone (Nacho, 18 September 2026). The rule this block states is about the
     area a finger can hit, and that area does not have to be painted: the pill keeps its own
     box and a transparent pseudo-element extends it to 44px. Adjacent rows meet and do not
     overlap: the wrap's row gap is 7px and this reaches 7px each way. */
  .tz-filter-pill { position: relative; }
  .tz-filter-pill::before {
    content: ""; position: absolute; left: 0; right: 0;
    top: calc((44px - 100%) / -2); bottom: calc((44px - 100%) / -2);
  }
  /* The clear-filter × has no label to widen it, so its own target is widened the same way. */
  .tz-filter-pill--clear::before { left: calc((44px - 100%) / -2); right: calc((44px - 100%) / -2); }
  .tz-more { min-height: 44px; }
  /* The bar's two panels are lists of tap targets, which is the shape .tz-more above already
     covers; their rows are 32px on their padding alone. The TOGGLES are deliberately not here:
     they are words in a row of 34px words, and raising one alone would make the only 44px control
     on the masthead the newest one. Whether the whole bar rises is one decision about the bar,
     not a side effect of this one. */
  .tz-place__menu > li > a,
  .tz-topnav__more-menu > li > a { min-height: 44px; display: flex; align-items: center; }

  /* ⚠️ A TEXT FIELD IS A TOUCH TARGET TOO, and three of them stand in the SAME ROW as a
     button: .tzs-bar__input, .tzs-adv__control > .form-control and .tz-lede__select are all
     pinned at 36px, so raising only the buttons left the records search with a 36px field
     between 44px controls. `min-height` rather than `height` because a used height is
     max(height, min-height) — one rule reaches all three of those fixed heights, the
     .form-control-sm on the advanced search's limit box, and every ordinary field besides,
     without restating a single selector.
     The rich dropdown moves with them, and that is not decoration: --choices-input-height
     exists, by its own comment in golden.css, so a Choices control "sits at the height of a
     .form-select standing next to it in the same row". Raising one and not the other would
     break the pairing that variable was written to hold. */
  .form-control, .form-select { min-height: 44px; }
  .tz-admin-catalogue__search input { min-height: 44px; }
  .choices { --choices-input-height: 44px; }
}
