/* ==========================================================================
   components.css — the shared component library.

   Every one of the four workspaces is assembled from these parts, so a
   change here changes all pages. Grids live in tables.css; breakpoint
   behaviour lives in responsive.css.
   ========================================================================== */

/* ==========================================================================
   1. BUTTONS
   ========================================================================== */
.btn-tn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-3);
  height: var(--tn-h-command);
  padding: 0 var(--tn-sp-5);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border-strong);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-family: inherit;
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--tn-dur-fast) var(--tn-ease),
              border-color var(--tn-dur-fast) var(--tn-ease),
              color var(--tn-dur-fast) var(--tn-ease);
}

.btn-tn .bi {
  flex: 0 0 auto;
  font-size: var(--tn-fs-base);
  line-height: 1;
}

.btn-tn:hover {
  background: var(--tn-surface-sunken);
  border-color: var(--tn-border-strong);
  color: var(--tn-text);
}

.btn-tn:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.btn-tn:active {
  background: var(--tn-neutral-soft);
}

/* Disabled stays legible: enough fade to read as inert, not so much that
   the label becomes decorative. */
.btn-tn[disabled],
.btn-tn.is-disabled,
.icon-btn[disabled],
.icon-btn.is-disabled {
  opacity: 0.6;
  cursor: not-allowed;
  box-shadow: none;
}

.btn-tn__caret {
  font-size: var(--tn-fs-xs) !important;
  color: var(--tn-text-muted);
}

.btn-tn--primary {
  background: var(--tn-accent);
  border-color: var(--tn-accent);
  box-shadow: var(--tn-shadow-xs);
  color: var(--tn-text-on-accent);
}

.btn-tn--primary:hover {
  background: var(--tn-accent-hover);
  border-color: var(--tn-accent-hover);
  color: var(--tn-text-on-accent);
}

.btn-tn--primary:active {
  background: var(--tn-accent-active);
}

.btn-tn--accent {
  background: var(--tn-accent-soft);
  border-color: var(--tn-accent-border);
  color: var(--tn-accent);
}

.btn-tn--accent:hover {
  background: var(--tn-accent-soft-hover);
  color: var(--tn-accent-hover);
}

.btn-tn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--tn-text-secondary);
}

.btn-tn--ghost:hover {
  background: var(--tn-surface-sunken);
  border-color: var(--tn-border);
}

/* Scoped to the toolbar variant, NOT to .btn-tn: the shared button also
   builds the primary actions, the modal footer and Sign In, and those are
   not secondary text. Here 13px puts the action row a clear step under the
   section tabs above it, which is the hierarchy the toolbar wants — the
   control height is untouched. */
.btn-tn--tool {
  height: var(--tn-h-command);
  padding: 0 var(--tn-sp-4);
  background: transparent;
  border-color: transparent;
  font-size: var(--tn-fs-sm);
}

/* Toolbar commands stay borderless until touched, so the toolbar reads as
   a row of actions rather than a row of boxes. */
.btn-tn--tool:hover {
  background: var(--tn-surface-sunken);
  border-color: var(--tn-border);
}

.btn-tn--danger {
  background: var(--tn-danger);
  border-color: var(--tn-danger);
  color: var(--tn-text-on-accent);
}

.btn-tn--danger:hover {
  background: var(--tn-danger-hover);
  border-color: var(--tn-danger-hover);
  color: var(--tn-text-on-accent);
}

.btn-tn--block {
  width: 100%;
}

.btn-tn--filter {
  display: none;               /* shown at mobile widths only */
}

.icon-btn {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: var(--tn-h-command);
  height: var(--tn-h-command);
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-lg);
  line-height: 1;
  cursor: pointer;
  transition: background var(--tn-dur-fast) var(--tn-ease),
              color var(--tn-dur-fast) var(--tn-ease);
}

.icon-btn:hover {
  background: var(--tn-surface-sunken);
  color: var(--tn-text);
}

.icon-btn:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.icon-btn.is-active {
  background: var(--tn-accent-soft);
  color: var(--tn-accent);
}

.icon-btn__badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  background: var(--tn-danger);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-text-on-accent);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-bold);
  line-height: 15px;
  text-align: center;
}

.link-btn {
  padding: 0;
  background: none;
  border: 0;
  color: var(--tn-text-link);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  text-decoration: none;
  cursor: pointer;
}

.link-btn:hover {
  text-decoration: underline;
}

/* ==========================================================================
   1b. TABLE INFO — "about this table", beside a panel title

   Deliberately quieter than both the title it follows and the actions on the
   other side of the header: it is a way in to an explanation, not a command.
   Injected next to every grid panel's title by js/table-info.js.
   ========================================================================== */
.table-info {
  display: inline-grid;
  flex: 0 0 auto;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-left: calc(var(--tn-sp-1) * -1);
  padding: 0;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
  line-height: 1;
  cursor: pointer;
  transition: color var(--tn-dur-fast) var(--tn-ease),
              background var(--tn-dur-fast) var(--tn-ease);
}

.table-info:hover,
.table-info[aria-expanded="true"] {
  background: var(--tn-neutral-soft);
  color: var(--tn-text-secondary);
}

.table-info:focus-visible {
  outline: 2px solid var(--tn-focus-outline-color);
  outline-offset: 1px;
  color: var(--tn-text-secondary);
}

/* --------------------------------------------------------------------------
   THE HELP CARD

   One surface, shared by every table, positioned by js/table-info.js against
   the PANEL that opened it — under the panel header, overlaying the top of
   the table. It is deliberately a card and not a bubble: a header band with
   its own close control, a readable body, and a quiet keyboard footer. That
   weight is what stops it reading as a tooltip that opened by accident.

   Elevation is --tn-shadow-lg, the dialog step, because this surface sits
   over live content rather than beside it.
   -------------------------------------------------------------------------- */
.table-info-pop {
  position: fixed;
  z-index: var(--tn-z-menu);
  display: flex;
  flex-direction: column;
  width: 424px;                 /* js sets the real width from the panel */
  max-width: calc(100vw - 16px);
  max-height: calc(100vh - 96px);
  overflow: hidden;             /* keeps the header band inside the radius */
  background: var(--tn-surface-overlay);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-lg);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
  line-height: var(--tn-lh-base);
}

/* --- header band ---------------------------------------------------------- */
.table-info-pop__head {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  flex: 0 0 auto;
  padding: var(--tn-sp-5) var(--tn-sp-5) var(--tn-sp-5) var(--tn-sp-6);
  background: var(--tn-surface-raised);
  border-bottom: 1px solid var(--tn-border);
}

/* The mark carries the accent so the title itself does not have to. */
.table-info-pop__mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  background: var(--tn-accent-soft);
  border: 1px solid var(--tn-accent-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-accent);
  font-size: var(--tn-fs-lg);
  line-height: 1;
}

.table-info-pop__heading {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}

.table-info-pop__eyebrow {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  line-height: 1.3;
  text-transform: uppercase;
}

.table-info-pop__title {
  overflow: hidden;
  color: var(--tn-text);
  font-size: var(--tn-fs-lg);
  font-weight: var(--tn-fw-semibold);
  line-height: 1.25;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Visible, but never competing with the title beside it: the shared icon
   button, held at the quiet end of its range until it is hovered. */
.table-info-pop__close {
  flex: 0 0 auto;
  align-self: flex-start;
  color: var(--tn-text-muted);
}

.table-info-pop__close:hover {
  color: var(--tn-text);
}

/* --- body ----------------------------------------------------------------- */
.table-info-pop__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--tn-sp-6);
  overflow-y: auto;
}

.table-info-pop__intro {
  margin: 0 0 var(--tn-sp-5);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
}

/* Marker rules rather than list-style discs: a small accent dash reads as
   deliberate at this size, where a bullet reads as a default. */
.table-info-pop__tips {
  display: grid;
  gap: var(--tn-sp-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.table-info-pop__tip {
  position: relative;
  padding-left: var(--tn-sp-6);
  color: var(--tn-text-secondary);
}

.table-info-pop__tip::before {
  content: "";
  position: absolute;
  top: 8px;
  left: 0;
  width: 8px;
  height: 2px;
  background: var(--tn-accent);
  border-radius: var(--tn-radius-pill);
}

/* --- footer --------------------------------------------------------------- */
.table-info-pop__foot {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  flex: 0 0 auto;
  padding: var(--tn-sp-4) var(--tn-sp-6);
  background: var(--tn-surface-sunken);
  border-top: 1px solid var(--tn-border-subtle);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
}

.table-info-pop__key {
  padding: 1px var(--tn-sp-3);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border-strong);
  border-radius: var(--tn-radius-xs);
  color: var(--tn-text-secondary);
  font-family: var(--tn-font-mono);
  font-size: var(--tn-fs-xxs);
  line-height: 1.5;
}

/* Small screens: a full-width sheet at the foot of the viewport rather than
   a card wider than the viewport. Still a help surface, not a modal — there
   is no scrim and the page behind stays live. */
.table-info-pop.is-sheet {
  left: var(--tn-sp-4);
  right: var(--tn-sp-4);
  bottom: var(--tn-sp-4);
  top: auto;
  width: auto;
  max-width: none;
  max-height: calc(100vh - var(--tn-sp-9));
}

/* A short, flat rise on open. It is help arriving where it was asked for, so
   it does not need to travel; anything longer would be in the way of the
   second click. Suppressed entirely under reduced motion by the global rule
   in section 18. */
@media (prefers-reduced-motion: no-preference) {
  .table-info-pop {
    animation: tn-info-in var(--tn-dur-base) var(--tn-ease) both;
  }
}

@keyframes tn-info-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* ==========================================================================
   2. THEME TOGGLE — a compact global preference, not a feature button
   ========================================================================== *//* ==========================================================================
   2. THEME TOGGLE — a compact global preference, not a feature button
   ========================================================================== */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  height: var(--tn-h-context);
  padding: 0 var(--tn-sp-5);
  background: var(--tn-surface-raised);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  color: var(--tn-text-secondary);
  font-family: inherit;
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--tn-dur-fast) var(--tn-ease),
              color var(--tn-dur-fast) var(--tn-ease);
}

.theme-toggle .bi {
  font-size: var(--tn-fs-lg);
}

.theme-toggle:hover {
  background: var(--tn-surface-sunken);
  color: var(--tn-text);
}

.theme-toggle:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

/* ==========================================================================
   3. TOPBAR PIECES
   ========================================================================== */
/* Global context selector: the quiet label sits above the value so the value
   (SWEDEN) is what the eye lands on. The <select> itself fills the whole
   control — the label floats over it — so the touch target is the full 40px
   box rather than the height of the text line. */
.context-field {
  position: relative;
  display: block;
  height: var(--tn-h-context);
  min-width: 96px;
  background: var(--tn-surface-raised);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
}

.context-field:hover {
  border-color: var(--tn-border-strong);
}

.context-field__label {
  position: absolute;
  top: 4px;
  left: var(--tn-sp-5);
  z-index: 1;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-chrome-label);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  line-height: 1;
  text-transform: uppercase;
  pointer-events: none;
}

.context-field__select {
  width: 100%;
  height: 100%;
  padding: 17px var(--tn-sp-3) 0 calc(var(--tn-sp-5) - 2px);
  background: transparent;
  border: 0;
  border-radius: var(--tn-radius-md);
  color: var(--tn-text);
  font-family: inherit;
  font-size: var(--tn-fs-chrome-lead);
  font-weight: var(--tn-fw-bold);
  line-height: 1;
  cursor: pointer;
}

.context-field__select:focus {
  outline: none;
}

.context-field__select:focus {
  outline: none;
}

.context-field:focus-within {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.context-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  padding: var(--tn-sp-2) var(--tn-sp-5);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
  white-space: nowrap;
}

.context-chip strong {
  color: var(--tn-text);
  font-weight: var(--tn-fw-semibold);
}

.context-chip--muted {
  color: var(--tn-text-muted);
}

.avatar {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  background: var(--tn-accent-soft);
  border: 1px solid var(--tn-accent-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-accent);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-bold);
}

.user-chip {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  height: var(--tn-h-context);
  padding: 0 var(--tn-sp-4) 0 var(--tn-sp-3);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-md);
  cursor: pointer;
}

.user-chip:hover {
  background: var(--tn-surface-sunken);
  border-color: var(--tn-border);
}

.user-chip:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.user-chip__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.2;
}

.user-chip__name {
  color: var(--tn-text);
  font-size: var(--tn-fs-chrome-value);
  font-weight: var(--tn-fw-bold);
  line-height: 1.25;
}

.user-chip__role {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-chrome-label);
  line-height: 1.25;
}

.user-chip__caret {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.search-results {
  position: absolute;
  top: calc(100% + var(--tn-sp-2));
  left: 0;
  right: 0;
  z-index: var(--tn-z-menu);
  max-height: 340px;
  overflow-y: auto;
  background: var(--tn-surface-overlay);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-md);
}

.search-result {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  width: 100%;
  padding: var(--tn-sp-4) var(--tn-sp-5);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--tn-border-subtle);
  text-align: left;
  cursor: pointer;
}

.search-result:last-child {
  border-bottom: 0;
}

.search-result:hover,
.search-result:focus-visible {
  background: var(--tn-grid-row-hover);
  outline: none;
}

.search-result__icon {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  background: var(--tn-surface-sunken);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
}

.search-result__body {
  min-width: 0;
  flex: 1 1 auto;
}

.search-result__title {
  display: block;
  color: var(--tn-text);
  font-size: var(--tn-fs-base);
  font-weight: var(--tn-fw-semibold);
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The navigation path is what makes a result unambiguous when two modules
   own similarly named screens. */
.search-result__path {
  display: block;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-result__mark {
  padding: 0;
  background: var(--tn-search-mark-bg);
  color: inherit;
  font-weight: var(--tn-fw-bold);
  border-radius: 2px;
}

/* Screens that exist in the navigation but are not built in this prototype */
.search-result__tag {
  flex: 0 0 auto;
  padding: 2px var(--tn-sp-3);
  background: var(--tn-neutral-soft);
  border: 1px solid var(--tn-neutral-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  white-space: nowrap;
}

.search-result.is-active {
  background: var(--tn-accent-soft);
}

.search-result.is-active .search-result__icon {
  background: var(--tn-accent);
  color: var(--tn-text-on-accent);
}

/* Module heading inside the palette */
.search-group {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--tn-sp-4) var(--tn-sp-5) var(--tn-sp-2);
  background: var(--tn-surface-overlay);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-bold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.search-results__more {
  padding: var(--tn-sp-3) var(--tn-sp-5);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

/* The palette footer: the shortcuts, stated once. */
.search-results__hint {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: var(--tn-sp-6);
  padding: var(--tn-sp-3) var(--tn-sp-5);
  background: var(--tn-surface-raised);
  border-top: 1px solid var(--tn-border);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.search-results__hint kbd {
  padding: 1px 5px;
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border-strong);
  border-radius: var(--tn-radius-xs);
  color: var(--tn-text-secondary);
  font-family: var(--tn-font-sans);
  font-size: var(--tn-fs-xxs);
}

/* ==========================================================================
   MARKET CONTEXT SWITCHER
   Market is global operating context, so it is a context control with its
   own menu rather than a form field that happens to live in the top bar.
   ========================================================================== */
.market-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-4);
  flex: 0 0 auto;
  height: var(--tn-h-context);
  padding: 0 var(--tn-sp-4) 0 var(--tn-sp-5);
  background: var(--tn-context-bg);
  border: 1px solid var(--tn-context-border);
  border-radius: var(--tn-radius-sm);
  cursor: pointer;
  transition: background var(--tn-dur-fast) var(--tn-ease),
              border-color var(--tn-dur-fast) var(--tn-ease);
}

.market-switch:hover {
  background: var(--tn-context-bg-hover);
  border-color: var(--tn-border-strong);
}

.market-switch:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.market-switch.is-open {
  background: var(--tn-context-bg-hover);
  border-color: var(--tn-accent);
}

.market-switch__icon {
  flex: 0 0 auto;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-lg);
  line-height: 1;
}

.market-switch__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
  line-height: 1.1;
}

.market-switch__label {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-chrome-label);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.market-switch__value {
  color: var(--tn-text);
  font-size: var(--tn-fs-chrome-lead);
  font-weight: var(--tn-fw-bold);
  white-space: nowrap;
}

.market-switch__caret {
  flex: 0 0 auto;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
  transition: transform var(--tn-dur-fast) var(--tn-ease);
}

.market-switch.is-open .market-switch__caret {
  transform: rotate(180deg);
}

.market-menu {
  position: fixed;
  z-index: var(--tn-z-menu);
  min-width: 232px;
  padding: var(--tn-sp-3);
  background: var(--tn-surface-overlay);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-md);
}

.market-menu__head {
  padding: var(--tn-sp-3) var(--tn-sp-4) var(--tn-sp-4);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-bold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.market-menu__item {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  width: 100%;
  padding: var(--tn-sp-4);
  background: none;
  border: 0;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text);
  font-size: var(--tn-fs-base);
  text-align: left;
  cursor: pointer;
}

.market-menu__item:hover,
.market-menu__item:focus-visible {
  background: var(--tn-accent-soft);
  color: var(--tn-accent);
  outline: none;
}

.market-menu__item.is-selected {
  font-weight: var(--tn-fw-semibold);
}

.market-menu__check {
  flex: 0 0 16px;
  color: var(--tn-accent);
  font-size: var(--tn-fs-base);
}



.search-result__meta {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

.search-results__empty {
  padding: var(--tn-sp-6);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
  text-align: center;
}

/* ==========================================================================
   4. BREADCRUMB
   ========================================================================== */
.breadcrumb-bar ol {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--tn-sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.breadcrumb-bar li {
  display: inline-flex;
  align-items: center;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
}

.breadcrumb-bar li + li::before {
  content: "\F285";
  margin-right: var(--tn-sp-3);
  font-family: bootstrap-icons, sans-serif;
  font-size: var(--tn-fs-xxs);
  color: var(--tn-text-muted);
}

.breadcrumb-bar a {
  color: var(--tn-text-secondary);
  text-decoration: none;
}

.breadcrumb-bar a:hover {
  color: var(--tn-text-link);
  text-decoration: underline;
}

.breadcrumb-bar [aria-current="page"] {
  color: var(--tn-text);
  font-weight: var(--tn-fw-semibold);
}

/* ==========================================================================
   5. TABS — section tabs, detail pane tabs, nested sub-tabs
   ========================================================================== */
.section-tabs {
  flex: 0 0 auto;
  background: var(--tn-surface);
  border-bottom: 1px solid var(--tn-border);
}

.section-tabs__scroller {
  display: flex;
  align-items: stretch;
  gap: var(--tn-sp-1);
  height: var(--tn-h-tabs);
  padding: 0 var(--tn-pad-inline);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.section-tabs__scroller::-webkit-scrollbar {
  display: none;
}

.section-tab {
  position: relative;
  flex: 0 0 auto;
  padding: 0 var(--tn-sp-5);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--tn-text-secondary);
  font-family: inherit;
  /* One step down from the 15px base. Plex sets ~6% wider than the face
     this replaced, so 14px here holds the apparent size the tab strip had
     while giving the toolbar below it room to sit a step quieter. The
     500 -> 600 active step is unchanged; the underline still does the work. */
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--tn-dur-fast) var(--tn-ease),
              border-color var(--tn-dur-fast) var(--tn-ease);
}

.section-tab:hover {
  color: var(--tn-text);
  background: var(--tn-surface-raised);
}

.section-tab:focus-visible {
  outline: none;
  box-shadow: inset var(--tn-focus-ring);
}

.section-tab.is-active {
  border-bottom-color: var(--tn-accent);
  color: var(--tn-accent);
  font-weight: var(--tn-fw-semibold);
}

/* Detail-pane tabs: the tablet transformation of a split pane */
.detail-tabs {
  display: none;
  gap: var(--tn-sp-2);
  flex: 0 0 auto;
  padding: var(--tn-sp-2);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
}

.detail-tab {
  flex: 1 1 0;
  min-height: 34px;
  padding: 0 var(--tn-sp-5);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-family: inherit;
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  cursor: pointer;
}

/* Hover and focus, which this family never had — .section-tab and .dlg-tab
   both answer the pointer and the keyboard, and a tab strip that answers
   neither is the same control behaving differently on a different screen.
   The surface is the sunken one its sibling .sub-tab uses, because both sit
   on a raised strip; the ring is the shared one every other tab draws. */
.detail-tab:hover:not(.is-active) {
  background: var(--tn-surface-sunken);
  color: var(--tn-text);
}

.detail-tab:focus-visible {
  outline: none;
  box-shadow: inset var(--tn-focus-ring);
}

.detail-tab.is-active {
  background: var(--tn-surface);
  border-color: var(--tn-border);
  box-shadow: var(--tn-shadow-xs);
  color: var(--tn-accent);
  font-weight: var(--tn-fw-semibold);
}

/* Sub-tabs inside a panel header (Prices & Allotments / Main Prices …) */
.sub-tabs {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-1);
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.sub-tabs::-webkit-scrollbar {
  display: none;
}

.sub-tab {
  flex: 0 0 auto;
  height: 26px;
  padding: 0 var(--tn-sp-5);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-pill);
  color: var(--tn-text-secondary);
  font-family: inherit;
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-medium);
  white-space: nowrap;
  cursor: pointer;
}

.sub-tab:hover {
  background: var(--tn-surface-sunken);
  color: var(--tn-text);
}

.sub-tab:focus-visible {
  outline: none;
  box-shadow: inset var(--tn-focus-ring);
}

.sub-tab.is-active {
  background: var(--tn-accent-soft);
  border-color: var(--tn-accent-border);
  color: var(--tn-accent);
  font-weight: var(--tn-fw-semibold);
}

/* ==========================================================================
   5b. DASHBOARD PRIMITIVES

   Reusable pieces, not Operations Home furniture: a metric card, the chart
   frame, a legend, a bar list, a pace meter and a timeline. Operations Home
   is the first screen to compose them, and any later dashboard can reuse them
   without copying anything. Shapes come from js/charts.js; everything here is
   presentation, and every colour is a token.
   ========================================================================== */

/* --------------------------------------------------------------------------
   METRIC CARD — one number, its movement, and a sparkline behind it
   -------------------------------------------------------------------------- */
.metric-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-gap-panel);
}

.metric-card {
  position: relative;
  display: flex;
  /* grow into whatever the last row has left over, never leave a hole */
  flex: 1 1 190px;
  flex-direction: column;
  min-width: 0;
  padding: var(--tn-sp-6) var(--tn-sp-6) 0;
  overflow: hidden;
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-xs);
}

/* A 3px rail in the card's own series colour: enough to group the KPI row by
   colour at a glance, far short of five coloured cards. */
.metric-card::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: var(--tn-chart-1);
}

.metric-card--2::before { background: var(--tn-chart-2); }
.metric-card--3::before { background: var(--tn-chart-3); }
.metric-card--4::before { background: var(--tn-chart-4); }
.metric-card--5::before { background: var(--tn-chart-5); }
.metric-card--6::before { background: var(--tn-chart-6); }

.metric-card__label {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  line-height: 1.3;
  text-transform: uppercase;
}

.metric-card__value {
  margin-top: var(--tn-sp-3);
  color: var(--tn-text);
  font-size: 27px;
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  line-height: 1.1;
}

.metric-card__caption {
  margin-top: var(--tn-sp-1);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.metric-card__delta {
  display: flex;
  /* The movement and its qualifier are one sentence, but a narrow card must
     be allowed to break it over two lines rather than push "vs last month"
     out past the edge of the viewport. */
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tn-sp-2);
  min-width: 0;
  margin-top: var(--tn-sp-4);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
  white-space: nowrap;
}

.metric-card__delta .bi {
  font-size: var(--tn-fs-sm);
  line-height: 1;
}

.metric-card__delta--up   { color: var(--tn-success); }
.metric-card__delta--down { color: var(--tn-danger); }
.metric-card__delta--flat { color: var(--tn-text-muted); }

.metric-card__note {
  color: var(--tn-text-muted);
  font-weight: var(--tn-fw-regular);
}

/* The sparkline is the card's floor, bled to both edges: it reads as the
   card's own texture rather than as a second chart to be studied. */
.metric-card__spark {
  height: 42px;
  margin: var(--tn-sp-4) calc(var(--tn-sp-6) * -1) 0;
}

/* --------------------------------------------------------------------------
   CHART FRAME
   -------------------------------------------------------------------------- */
.chart-box {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: var(--tn-pad-panel);
}

/* The plot's height is DEFINITE, and the chart inside it is taken out of
   flow. Both halves matter: an <svg> with a viewBox carries an intrinsic
   aspect ratio, so a plot sized only by min-height lets the chart size
   itself — a 620px-wide panel then gets a 620px-tall square chart, and the
   whole dashboard row stretches to match. */
.chart-plot {
  position: relative;
  flex: 0 0 auto;
  height: 150px;
}

.chart-plot .chart {
  position: absolute;
  inset: 0;
}

.chart {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* Sparklines sit in boxes that already have a definite height, so they only
   need to fill it. */
.chart--spark { height: 100%; }

.chart__grid {
  stroke: var(--tn-chart-grid);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* vector-effect keeps a 2px stroke 2px wide however the viewBox is squashed
   into the panel — without it a non-uniform scale turns lines into wedges. */
.chart__line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.chart__area { stroke: none; }

.chart__line--1 { stroke: var(--tn-chart-1); }
.chart__line--2 { stroke: var(--tn-chart-2); }
.chart__line--3 { stroke: var(--tn-chart-3); }
.chart__line--4 { stroke: var(--tn-chart-4); }
.chart__line--5 { stroke: var(--tn-chart-5); }
.chart__line--6 { stroke: var(--tn-chart-6); }

/* The reference series carries a series number too (so it lines up with the
   chart it is compared against), so this needs to outrank the colour rules
   above rather than merely follow them. */
.chart__line.chart__line--dashed {
  stroke: var(--tn-chart-compare);
  stroke-dasharray: 4 4;
  stroke-width: 1.5;
}

.chart__area--1 { fill: var(--tn-chart-1-soft); }
.chart__area--2 { fill: var(--tn-chart-2-soft); }
.chart__area--3 { fill: var(--tn-chart-3-soft); }
.chart__area--4 { fill: var(--tn-chart-4-soft); }
.chart__area--5 { fill: var(--tn-chart-5-soft); }
.chart__area--6 { fill: var(--tn-chart-6-soft); }

.chart__bar {
  transform-origin: center bottom;
}

.chart__bar--1 { fill: var(--tn-chart-1); }
.chart__bar--2 { fill: var(--tn-chart-2); }
.chart__bar--3 { fill: var(--tn-chart-3); }
.chart__bar--4 { fill: var(--tn-chart-4); }
.chart__bar--5 { fill: var(--tn-chart-5); }
.chart__bar--6 { fill: var(--tn-chart-6); }

/* --------------------------------------------------------------------------
   DONUT
   -------------------------------------------------------------------------- */
.chart--donut {
  width: auto;
  height: 100%;
  max-height: 168px;
  /* the ring is drawn round, so this one must not be stretched */
  aspect-ratio: 1 / 1;
  transform: rotate(-90deg);
}

.chart__track {
  fill: none;
  stroke: var(--tn-chart-track);
  stroke-width: 13;
}

.chart__slice {
  fill: none;
  stroke-width: 13;
  stroke-linecap: butt;
}

.chart__slice--1 { stroke: var(--tn-chart-1); }
.chart__slice--2 { stroke: var(--tn-chart-2); }
.chart__slice--3 { stroke: var(--tn-chart-3); }
.chart__slice--4 { stroke: var(--tn-chart-4); }
.chart__slice--5 { stroke: var(--tn-chart-5); }
.chart__slice--6 { stroke: var(--tn-chart-6); }

/* The donut and its key sit side by side while there is room, and stack when
   the panel gets narrow — one rule rather than a breakpoint per screen. */
.donut-block {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-6);
  height: 100%;
  padding: var(--tn-pad-panel);
}

.donut-block__chart {
  position: relative;
  flex: 0 0 auto;
  width: 132px;
  height: 132px;
}

.donut-block__centre {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  text-align: center;
  pointer-events: none;
}

.donut-block__total {
  color: var(--tn-text);
  font-size: var(--tn-fs-xl);
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.donut-block__caption {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.donut-block__key {
  flex: 1 1 150px;
  min-width: 0;
}

/* --------------------------------------------------------------------------
   LEGEND
   -------------------------------------------------------------------------- */
.chart-legend {
  display: grid;
  gap: var(--tn-sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.chart-legend__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--tn-sp-3);
  font-size: var(--tn-fs-sm);
}

.chart-legend__dot {
  width: 9px;
  height: 9px;
  border-radius: var(--tn-radius-xs);
  background: var(--tn-chart-1);
}

.chart-legend__dot--1 { background: var(--tn-chart-1); }
.chart-legend__dot--2 { background: var(--tn-chart-2); }
.chart-legend__dot--3 { background: var(--tn-chart-3); }
.chart-legend__dot--4 { background: var(--tn-chart-4); }
.chart-legend__dot--5 { background: var(--tn-chart-5); }
.chart-legend__dot--6 { background: var(--tn-chart-6); }

.chart-legend__label {
  overflow: hidden;
  color: var(--tn-text-secondary);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chart-legend__value {
  color: var(--tn-text);
  font-variant-numeric: tabular-nums;
  font-weight: var(--tn-fw-semibold);
}

.chart-legend__count {
  min-width: 44px;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* --------------------------------------------------------------------------
   AXIS
   -------------------------------------------------------------------------- */
.chart-axis {
  display: flex;
  justify-content: space-between;
  margin-top: var(--tn-sp-3);
  color: var(--tn-chart-axis);
  font-size: var(--tn-fs-xxs);
  font-variant-numeric: tabular-nums;
}

.chart-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-sp-3) var(--tn-sp-6);
  margin-bottom: var(--tn-sp-5);
}

.chart-key__item {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xs);
}

.chart-key__swatch {
  width: 14px;
  height: 3px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-chart-1);
}

.chart-key__swatch--2 { background: var(--tn-chart-2); }
.chart-key__swatch--compare {
  background: repeating-linear-gradient(90deg,
    var(--tn-chart-compare) 0 4px, transparent 4px 7px);
}

/* --------------------------------------------------------------------------
   BAR LIST — horizontal bars with real text either side
   -------------------------------------------------------------------------- */
.bar-list {
  display: grid;
  align-content: space-evenly;
  gap: var(--tn-sp-4);
  height: 100%;
  margin: 0;
  padding: var(--tn-pad-panel);
  list-style: none;
}

.bar-list--compact { gap: var(--tn-sp-3); }

.bar-list__row {
  display: grid;
  grid-template-columns: minmax(72px, 0.9fr) minmax(0, 2fr) auto;
  align-items: center;
  gap: var(--tn-sp-4);
  font-size: var(--tn-fs-sm);
}

.bar-list__label {
  overflow: hidden;
  color: var(--tn-text-secondary);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bar-list__track {
  height: 8px;
  background: var(--tn-chart-track);
  border-radius: var(--tn-radius-pill);
}

.bar-list__fill {
  display: block;
  width: 0;
  height: 100%;
  background: var(--tn-chart-1);
  border-radius: var(--tn-radius-pill);
}

.bar-list__fill--1 { background: var(--tn-chart-1); }
.bar-list__fill--2 { background: var(--tn-chart-2); }
.bar-list__fill--3 { background: var(--tn-chart-3); }
.bar-list__fill--4 { background: var(--tn-chart-4); }
.bar-list__fill--5 { background: var(--tn-chart-5); }
.bar-list__fill--6 { background: var(--tn-chart-6); }

.bar-list__value {
  color: var(--tn-text);
  font-size: var(--tn-fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   PACE METER — progress against a target
   -------------------------------------------------------------------------- */
.pace {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-5);
  height: 100%;
  padding: var(--tn-pad-panel);
}

/* the closing note settles against the foot of the card */
.pace__rows { margin-top: auto; }

.pace__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--tn-sp-4);
}

.pace__value {
  color: var(--tn-text);
  font-size: var(--tn-fs-xl);
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
}

.pace__pct {
  color: var(--tn-chart-1);
  font-size: var(--tn-fs-lg);
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
}

.pace-meter {
  position: relative;
  height: 10px;
  background: var(--tn-chart-track);
  border-radius: var(--tn-radius-pill);
  overflow: hidden;
}

.pace-meter__fill {
  display: block;
  width: 0;
  height: 100%;
  background: linear-gradient(90deg, var(--tn-chart-1), var(--tn-chart-5));
  border-radius: var(--tn-radius-pill);
}

.pace__rows {
  display: grid;
  gap: var(--tn-sp-3);
}

.pace__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--tn-sp-4);
  font-size: var(--tn-fs-sm);
}

.pace__row-label { color: var(--tn-text-secondary); }

.pace__row-value {
  color: var(--tn-text);
  font-variant-numeric: tabular-nums;
  font-weight: var(--tn-fw-medium);
}

.pace__note {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  padding-top: var(--tn-sp-4);
  border-top: 1px solid var(--tn-border-subtle);
  color: var(--tn-success);
  font-size: var(--tn-fs-xs);
}

/* --------------------------------------------------------------------------
   MINI STATS — a compact row of movements, each with its own sparkline
   -------------------------------------------------------------------------- */
.mini-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  align-content: center;
  gap: var(--tn-sp-5);
  height: 100%;
  padding: var(--tn-pad-panel);
}

.mini-stat {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: var(--tn-sp-5);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
}

.mini-stat__label {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
}

.mini-stat__note {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
}

.mini-stat__delta {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-2);
  margin-top: var(--tn-sp-4);
  font-size: var(--tn-fs-lg);
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
}

.mini-stat__delta--up   { color: var(--tn-success); }
.mini-stat__delta--down { color: var(--tn-danger); }
.mini-stat__delta--flat { color: var(--tn-text-muted); }

.mini-stat__spark {
  height: 34px;
  margin-top: var(--tn-sp-3);
}

/* --------------------------------------------------------------------------
   TIMELINE — upcoming operational events
   -------------------------------------------------------------------------- */
.timeline {
  display: grid;
  align-content: start;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.timeline__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--tn-sp-4);
  /* the 12px panel-body inset every other row list in the product uses,
     rather than the 16px reserved for bars and headers */
  padding: var(--tn-sp-4) var(--tn-sp-5);
  border-bottom: 1px solid var(--tn-grid-border);
}

/* The reference gets its own line: in a 3-of-12 column a badge beside it
   would push a real voucher number into an ellipsis, and the number is the
   one thing an operator needs to read exactly. The status joins the detail
   line underneath, where a few characters cost nothing. */
.timeline__meta-row {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  min-width: 0;
  margin-top: var(--tn-sp-2);
}

/* Layout only. The badge keeps the shared status typography — one
   semantic status, one badge, at one size, everywhere in the product. */
.timeline__meta-row .badge-tn {
  flex: 0 0 auto;
}

.timeline__meta-row .timeline__meta { margin-top: 0; }

.timeline__row:last-child { border-bottom: 0; }

.timeline__time {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.timeline__body { min-width: 0; }

.timeline__title {
  display: block;
  overflow: hidden;
  min-width: 0;
  color: var(--tn-text);
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.timeline__meta,
.timeline__sub {
  display: block;
  overflow: hidden;
  margin-top: 1px;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xs);
  line-height: 1.35;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.timeline__sub { color: var(--tn-text-muted); }

.timeline__side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--tn-sp-2);
  white-space: nowrap;
}

.timeline__value {
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   5c. DASHBOARD MOTION

   Declared under no-preference ONLY. The finished state is the default, so a
   reduced-motion visitor gets the complete dashboard with nothing to skip and
   no JavaScript branch that could leave a chart half-drawn.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {
  /* Cards arrive as one quiet lift, staggered just enough to read as a
     sequence rather than a cascade. Six steps, then it stops: a dashboard
     should be usable long before the animation would finish otherwise. */
  .home-grid > * {
    animation: tn-card-in var(--tn-dur-slow) var(--tn-ease) both;
  }

  .home-grid > *:nth-child(2) { animation-delay: 40ms; }
  .home-grid > *:nth-child(3) { animation-delay: 80ms; }
  .home-grid > *:nth-child(4) { animation-delay: 120ms; }
  .home-grid > *:nth-child(5) { animation-delay: 160ms; }
  .home-grid > *:nth-child(n+6) { animation-delay: 200ms; }

  /* Lines draw themselves in. The dash pattern IS the animation here, so a
     line that is already dashed fades instead — otherwise the draw-in would
     overwrite the very dashes that mark it as the comparison series. */
  .chart__line {
    stroke-dasharray: 1000;
    stroke-dashoffset: 1000;
    animation: tn-draw var(--tn-dur-chart-draw) var(--tn-ease) 120ms forwards;
  }

  .chart__line.chart__line--dashed {
    stroke-dashoffset: 0;
    opacity: 0;
    animation: tn-fade-in var(--tn-dur-slow) var(--tn-ease) 260ms forwards;
  }

  .chart__area {
    opacity: 0;
    animation: tn-fade-in var(--tn-dur-slow) var(--tn-ease) 320ms forwards;
  }

  .chart__bar {
    transform: scaleY(0);
    animation: tn-grow-y var(--tn-dur-chart) var(--tn-ease) 140ms forwards;
  }

  .chart__slice {
    opacity: 0;
    animation: tn-fade-in var(--tn-dur-chart) var(--tn-ease) 200ms forwards;
  }

  .bar-list__fill,
  .pace-meter__fill {
    transition: width var(--tn-dur-chart) var(--tn-ease) 160ms;
  }

  .metric-card {
    transition: box-shadow var(--tn-dur-base) var(--tn-ease),
                border-color var(--tn-dur-base) var(--tn-ease);
  }

  .metric-card:hover {
    border-color: var(--tn-border-strong);
    box-shadow: var(--tn-shadow-sm);
  }
}

@keyframes tn-card-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

@keyframes tn-draw {
  to { stroke-dashoffset: 0; }
}

@keyframes tn-fade-in {
  to { opacity: 1; }
}

@keyframes tn-grow-y {
  to { transform: scaleY(1); }
}

/* ==========================================================================
   6. SEGMENTED CONTROL — replaces the EXE's radio groups (Price Type)
   ========================================================================== */
.segmented {
  display: inline-flex;
  padding: 2px;
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
}

.segmented__option {
  height: 28px;
  padding: 0 var(--tn-sp-5);
  background: transparent;
  border: 0;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-family: inherit;
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  white-space: nowrap;
  cursor: pointer;
}

.segmented__option.is-active {
  background: var(--tn-surface);
  box-shadow: var(--tn-shadow-xs);
  color: var(--tn-accent);
  font-weight: var(--tn-fw-semibold);
}

.segmented__option:focus-visible {
  outline: none;
  box-shadow: var(--tn-focus-ring);
}

/* ==========================================================================
   7. TOOLBAR
   ========================================================================== */
.toolbar {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  flex: 0 0 auto;
  min-height: 46px;
  padding: var(--tn-sp-3) var(--tn-pad-inline);
  background: var(--tn-surface);
  border-bottom: 1px solid var(--tn-border);
}

.toolbar__group {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-2);
  min-width: 0;
}

.toolbar__spacer {
  flex: 1 1 auto;
}

/* A select that sits in a toolbar rather than in a filter bar — the record
   the E-Mail Template page is editing. It is the shared filter control at
   the shared COMMAND height, so it lines up with the buttons beside it
   instead of standing 4px taller than all of them. */
.toolbar__select {
  width: auto;
  min-width: 180px;
  max-width: 260px;
  height: var(--tn-h-command);
  font-size: var(--tn-fs-sm);
}

.toolbar__status {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  padding-right: var(--tn-sp-3);
  color: var(--tn-success);
  font-size: var(--tn-fs-sm);
  white-space: nowrap;
}

/* ==========================================================================
   8. PAGE CONTEXT BAR — page-level selectors (Hotel, Active Contract …)
   ========================================================================== */
.context-bar {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  flex: 0 0 auto;
  min-height: var(--tn-h-context-bar);
  padding: var(--tn-sp-4) var(--tn-pad-inline);
  background: var(--tn-surface-raised);
  border-bottom: 1px solid var(--tn-border);
}

.context-bar__field {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  min-width: 0;
}

.context-bar__label {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
  white-space: nowrap;
}

.context-bar__select {
  height: var(--tn-h-input-dense);
  min-width: 0;
  max-width: 340px;
  padding: 0 var(--tn-sp-4);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-base);
  font-weight: var(--tn-fw-medium);
  cursor: pointer;
}

.context-bar__select:focus {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.context-bar__spacer {
  flex: 1 1 auto;
}

/* ==========================================================================
   9. FILTER BAR
   ========================================================================== */
/* Filters are workspace chrome, not a card: same raised surface as the
   toolbar above it, separated only by a hairline. */
.filter-bar {
  flex: 0 0 auto;
  padding: var(--tn-sp-4) var(--tn-pad-inline);
  background: var(--tn-surface-raised);
  border-bottom: 1px solid var(--tn-border-strong);
}

.filter-bar__row {
  display: flex;
  align-items: flex-end;
  gap: var(--tn-sp-4);
}

.filter-search {
  position: relative;
  flex: 1 1 300px;
  min-width: 180px;
  max-width: 420px;
}

.filter-search .bi {
  position: absolute;
  top: 50%;
  left: var(--tn-sp-5);
  transform: translateY(-50%);
  color: var(--tn-text-secondary);
  pointer-events: none;
}

.filter-search__input {
  width: 100%;
  height: var(--tn-h-input);
  padding: 0 var(--tn-sp-5) 0 var(--tn-sp-10);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-size: var(--tn-fs-base);
}

.filter-search__input::placeholder {
  color: var(--tn-input-placeholder);
}

.filter-search__input:hover {
  border-color: var(--tn-input-border-hover);
}

.filter-search__input:focus {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
  outline: none;
}

.filter-fields {
  display: flex;
  align-items: flex-end;
  gap: var(--tn-sp-4);
  flex: 1 1 auto;
  min-width: 0;
}

.filter-field {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-2);
  flex: 0 1 165px;
  min-width: 120px;
}

/* A filter that is REQUIRED — the Reservation Monitor's reservation type.
   It is not styled differently from its neighbours: same label, same
   control, plus the asterisk every required field in this application
   carries. Only the room it needs is different. */
.filter-field--required {
  flex: 0 1 190px;
}

/* Its message hangs UNDER the control rather than growing the field.

   The filter row bottom-aligns its fields so every control sits on one
   line; a message that took part in the flow would make one field taller
   than the rest and lift its label out of line with the others the moment
   it appeared. Taken out of the flow, the row is the same height whether
   the field is valid or not, and nothing moves under the pointer.

   Scoped to the bar. Below 768px js/responsive.js moves this whole group
   into the filter sheet, where the fields are stacked and the message
   belongs in the flow again. */
#filterBar .filter-field {
  position: relative;
}

#filterBar .filter-field .form-tn__error {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: var(--tn-sp-1);
}

#filterDrawerBody .filter-field .form-tn__error {
  margin-top: var(--tn-sp-1);
}

.filter-field__label {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.filter-field__control {
  width: 100%;
  height: var(--tn-h-input);
  padding: 0 var(--tn-sp-4);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-base);
  cursor: pointer;
}

.filter-field__control:hover {
  border-color: var(--tn-input-border-hover);
}

.filter-field__control:focus {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
  outline: none;
}

.filter-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  flex: 0 0 auto;
}

.filter-bar__actions .btn-tn {
  height: var(--tn-h-input);
}

.filter-summary {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--tn-sp-3);
  margin-top: var(--tn-sp-4);
  padding-top: var(--tn-sp-4);
  border-top: 1px dashed var(--tn-border);
}

.filter-summary__label {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.filter-summary__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-sp-3);
}

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  height: 26px;
  padding: 0 var(--tn-sp-2) 0 var(--tn-sp-4);
  background: var(--tn-accent-soft);
  border: 1px solid var(--tn-accent-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-accent);
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-medium);
}

.filter-chip__key {
  color: var(--tn-text-muted);
  font-weight: var(--tn-fw-regular);
}

.filter-chip__remove {
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  background: none;
  border: 0;
  border-radius: var(--tn-radius-pill);
  color: var(--tn-accent);
  font-size: var(--tn-fs-xxs);
  cursor: pointer;
}

.filter-chip__remove:hover {
  background: var(--tn-accent-border);
}

.count-pill {
  display: inline-grid;
  place-items: center;
  min-width: 22px;
  height: 20px;
  padding: 0 var(--tn-sp-3);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-text);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-bold);
  font-variant-numeric: tabular-nums;
}

/* text-inverse, not on-accent: the dark theme lightens the accent, so white
   here would drop to 2.8:1 on the mobile filter badge. */
.btn-tn--filter .count-pill {
  background: var(--tn-accent);
  border-color: var(--tn-accent);
  color: var(--tn-text-inverse);
}

/* ==========================================================================
   10. PANEL
   ========================================================================== */
/* Panels read as connected work surfaces: a hairline border, a whisper of
   contact shadow, and no card-style float. */
.panel {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-xs);
  overflow: hidden;
}

.panel__head {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  flex: 0 0 auto;
  min-height: var(--tn-h-panel-head);
  /* 2px, not 4: a 36px tool button plus 8px of padding plus the 1px border
     came to 45px and overran the 44px header height, so panels with a button
     in the header stood 1px taller than panels without one. */
  padding: var(--tn-sp-1) var(--tn-sp-5);
  background: var(--tn-surface-raised);
  border-bottom: 1px solid var(--tn-border-strong);
}

.panel__icon {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-lg);
}

/* 14/600 rather than 15/700. A panel title labels a region, it does not
   compete with the page title above it, and Plex at 700 was making every
   panel head shout at the grid underneath. */
.panel__title {
  margin: 0;
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: -0.005em;
  white-space: nowrap;
}

.panel__subtitle {
  min-width: 0;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.panel__actions {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-2);
  margin-left: auto;
  flex: 0 0 auto;
}

.panel__body {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
}

.panel__body--pad {
  padding: var(--tn-pad-panel);
}

.panel__foot {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-6);
  flex: 0 0 auto;
  min-height: var(--tn-h-panel-foot);
  padding: 0 var(--tn-sp-5);
  background: var(--tn-surface-raised);
  border-top: 1px solid var(--tn-border);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
}

.panel__foot-item strong {
  color: var(--tn-text);
  font-weight: var(--tn-fw-semibold);
}

.panel__foot-item--muted {
  color: var(--tn-text-muted);
}

.panel.is-maximised {
  position: absolute;
  inset: var(--tn-pad-workspace);
  z-index: var(--tn-z-sticky);
  box-shadow: var(--tn-shadow-md);
}

/* ==========================================================================
   11. SWITCH
   ========================================================================== */
.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  margin: 0;
  cursor: pointer;
  user-select: none;
}

.switch__input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.switch__track {
  position: relative;
  display: block;
  width: 32px;
  height: 18px;
  background: var(--tn-switch-track);
  border-radius: var(--tn-radius-pill);
  transition: background var(--tn-dur-fast) var(--tn-ease);
}

.switch__track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  background: var(--tn-switch-knob);
  border-radius: var(--tn-radius-pill);
  box-shadow: var(--tn-shadow-xs);
  transition: transform var(--tn-dur-fast) var(--tn-ease);
}

.switch__input:checked + .switch__track {
  background: var(--tn-accent);
}

.switch__input:checked + .switch__track::after {
  transform: translateX(14px);
}

.switch__input:focus-visible + .switch__track {
  box-shadow: var(--tn-focus-ring);
}

.switch__label {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   11a. THE LIFECYCLE SWITCH

   The ONE state a record is in — Active / Passive — sitting in the command
   region beside Save rather than as the twenty-second tick down a form. See
   js/state-switch.js for why it is a native checkbox underneath.

   It is not a new control. The track and the knob above are the same two
   elements the application's switch has always drawn; what this adds is the
   command-region metrics around them and the word that changes with the
   state, so the state is never carried by the colour of a track alone.
   -------------------------------------------------------------------------- */
.state-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  flex: 0 0 auto;
  height: var(--tn-h-command);
  margin: 0;
  padding: 0 var(--tn-sp-4);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  cursor: pointer;
  user-select: none;
}

.state-switch:hover {
  background: var(--tn-surface-sunken);
  border-color: var(--tn-border);
  color: var(--tn-text);
}

/* The label element cannot take :focus-visible — the input inside it does,
   and the ring is drawn on the control the eye is on. */
.state-switch:focus-within {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

/* The word IS the state, so it does not move when the state changes: a
   min-width holds the slot at the width of the longer of the two, and the
   commands beside it never shift under the pointer. */
.state-switch__label {
  min-width: 4.2em;
  color: inherit;
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-medium);
  text-align: left;
  white-space: nowrap;
}

/* ==========================================================================
   11b. CHECK CONTROL + TRANSFER-LIST CHROME

   Used by js/transfer-list.js on every Included / Excluded workflow. The
   selection toolbar sits between the panel header and the panel body, so the
   same markup serves desktop, tablet and mobile — there is no separate
   mobile transfer pattern to keep in sync.
   ========================================================================== */
/* 17px, and 17px whatever it is put inside.

   `flex: 0 0 auto` is not decoration. .check-grid__item is a flex row of a
   box and a label, and a label longer than its column was squeezing the BOX
   — measured at 15x17 on the E-Mail Template metadata row's "Default
   template", beside three 17x17 siblings in the same group. A shared
   control that changes size depending on how long the word next to it is
   has stopped being a shared control. The label truncates instead. */
.tn-check {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  margin: 0;
  accent-color: var(--tn-accent);
  cursor: pointer;
}

.tn-check:focus-visible {
  outline: 2px solid var(--tn-focus-outline-color);
  outline-offset: 2px;
}

/* A label fills the cell so the touch target is the whole cell, not the
   17px box — without making a click anywhere on the row toggle it. */
.check-hit {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 28px;
  margin: 0;
  cursor: pointer;
}

.selection-bar {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  flex: 0 0 auto;
  flex-wrap: wrap;
  min-height: var(--tn-h-selection-bar);
  padding: var(--tn-sp-3) var(--tn-sp-5);
  background: var(--tn-surface-raised);
  border-bottom: 1px solid var(--tn-border-strong);
}

/* Raised state while rows are checked, so the pending action is obvious */
.selection-bar.is-armed {
  background: var(--tn-accent-soft);
  border-bottom-color: var(--tn-accent-border);
}

.selection-bar__filter {
  position: relative;
  flex: 1 1 180px;
  min-width: 140px;
  max-width: 280px;
}

.selection-bar__filter .bi {
  position: absolute;
  top: 50%;
  left: var(--tn-sp-4);
  transform: translateY(-50%);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
  pointer-events: none;
}

.selection-bar__input {
  width: 100%;
  height: var(--tn-h-input-dense);
  padding: 0 var(--tn-sp-4) 0 var(--tn-sp-9);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-md);
}

.selection-bar__input::placeholder {
  color: var(--tn-input-placeholder);
}

.selection-bar__input:focus {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
  outline: none;
}

/* Select-all in the toolbar — the only select-all available on mobile, where
   the grid becomes record rows and has no header row. */
.selection-bar__all {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  flex: 0 0 auto;
  min-height: var(--tn-h-input-dense);
  margin: 0;
  padding: 0 var(--tn-sp-3);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
  white-space: nowrap;
  cursor: pointer;
}

.selection-bar__all input[disabled] + span {
  color: var(--tn-text-muted);
}

.selection-bar__spacer {
  flex: 1 1 auto;
}

/* Count is text, not colour alone, so selection state is never colour-only */
.selection-bar__count {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
  white-space: nowrap;
}

.selection-bar__count strong {
  color: var(--tn-text);
  font-weight: var(--tn-fw-bold);
}

.selection-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  flex: 0 0 auto;
}

/* Unsaved-changes flag — subtle, and it states its meaning in words */
.dirty-flag {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  height: var(--tn-h-command);
  padding: 0 var(--tn-sp-5);
  background: var(--tn-dirty-bg);
  border: 1px solid var(--tn-dirty-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-dirty-text);
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-semibold);
  white-space: nowrap;
}

.transfer-toolbar {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
}

/* Brief tint on rows that have just arrived, then it fades out. */
.data-grid tbody tr.is-landed,
.record.is-landed {
  animation: tn-landed var(--tn-dur-slow) var(--tn-ease) 2;
}

@keyframes tn-landed {
  from { background: var(--tn-accent-soft); }
  to   { background: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .data-grid tbody tr.is-landed,
  .record.is-landed {
    animation: none;
  }
}

/* ==========================================================================
   12. BADGES
   ========================================================================== */
/* Compact, low-saturation status chips. The dot carries the colour so the
   surface can stay quiet. */
.badge-tn {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  height: 20px;
  padding: 0 var(--tn-sp-4);
  border: 1px solid transparent;
  border-radius: var(--tn-radius-pill);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.badge-tn::before {
  content: "";
  flex: 0 0 auto;
  width: 5px;
  height: 5px;
  border-radius: var(--tn-radius-pill);
  background: currentColor;
}

.badge-tn--active {
  background: var(--tn-success-soft);
  border-color: var(--tn-success-border);
  color: var(--tn-success);
}

.badge-tn--draft {
  background: var(--tn-neutral-soft);
  border-color: var(--tn-neutral-border);
  color: var(--tn-text-secondary);
}

.badge-tn--suspended {
  background: var(--tn-warning-soft);
  border-color: var(--tn-warning-border);
  color: var(--tn-warning);
}

.badge-tn--closed {
  background: var(--tn-danger-soft);
  border-color: var(--tn-danger-border);
  color: var(--tn-danger);
}

.badge-tn--info {
  background: var(--tn-info-soft);
  border-color: var(--tn-info-border);
  color: var(--tn-info);
}

/* ==========================================================================
   13. DETAIL FIELD GROUPS
   ========================================================================== */
.detail-groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--tn-sp-6) var(--tn-sp-9);
}

.detail-groups > .detail-group {
  min-width: 0;
}

.detail-group__title {
  margin: 0 0 var(--tn-sp-3);
  padding-bottom: var(--tn-sp-2);
  border-bottom: 1px solid var(--tn-border);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.detail-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--tn-sp-4) var(--tn-sp-6);
  margin: 0;
}

.detail-field {
  min-width: 0;
}

.detail-field__label {
  display: block;
  margin-bottom: 1px;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.detail-field__value {
  display: block;
  margin: 0;
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  overflow-wrap: anywhere;
}

.detail-field__value--num {
  font-variant-numeric: tabular-nums;
}

.detail-field--wide {
  grid-column: span 2;
}

/* ==========================================================================
   14. MENUS
   ========================================================================== */
.action-menu,
.popover-menu {
  position: fixed;
  z-index: var(--tn-z-menu);
  min-width: var(--tn-w-menu);
  padding: var(--tn-sp-3);
  background: var(--tn-surface-overlay);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-md);
}

/* An export is the one command in the application that can hold the main
   thread for a noticeable moment: a twenty-column PDF of forty rows is laid
   out synchronously once its rows have arrived. The toast says what is
   happening; the cursor says it is still happening. js/export.js puts the
   class on and takes it off again in every outcome, including a failure. */
.is-exporting,
.is-exporting * {
  cursor: progress;
}

.action-menu__scrim {
  position: fixed;
  inset: 0;
  z-index: calc(var(--tn-z-menu) - 1);
  background: var(--tn-scrim);
}

.menu-head {
  padding: var(--tn-sp-4) var(--tn-sp-5) var(--tn-sp-3);
  border-bottom: 1px solid var(--tn-border-subtle);
  margin-bottom: var(--tn-sp-2);
}

.menu-head__title {
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.menu-head__sub {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.menu-item {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  width: 100%;
  min-height: 34px;
  padding: var(--tn-sp-3) var(--tn-sp-5);
  background: none;
  border: 0;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-family: inherit;
  font-size: var(--tn-fs-base);
  text-align: left;
  cursor: pointer;
}

.menu-item .bi {
  flex: 0 0 16px;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-base);
}

.menu-item:hover,
.menu-item:focus-visible {
  background: var(--tn-accent-soft);
  color: var(--tn-accent);
  outline: none;
}

.menu-item:hover .bi,
.menu-item:focus-visible .bi {
  color: var(--tn-accent);
}

.menu-item__hint {
  margin-left: auto;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.menu-item--danger {
  color: var(--tn-danger);
}

.menu-item--danger .bi {
  color: var(--tn-danger);
}

.menu-item--danger:hover,
.menu-item--danger:focus-visible {
  background: var(--tn-danger-soft);
  color: var(--tn-danger);
}

.menu-item--danger:hover .bi {
  color: var(--tn-danger);
}

.menu-sep {
  height: 1px;
  margin: var(--tn-sp-2) var(--tn-sp-3);
  background: var(--tn-border-subtle);
}

.menu-check {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  width: 100%;
  min-height: 32px;
  padding: var(--tn-sp-2) var(--tn-sp-5);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-base);
  cursor: pointer;
}

.menu-check:hover {
  background: var(--tn-surface-sunken);
}

.menu-check input {
  width: 16px;
  height: 16px;
  accent-color: var(--tn-accent);
}

.notif-list {
  max-height: 320px;
  overflow-y: auto;
}

.notif {
  display: flex;
  gap: var(--tn-sp-4);
  padding: var(--tn-sp-4) var(--tn-sp-5);
  border-radius: var(--tn-radius-sm);
}

.notif:hover {
  background: var(--tn-surface-sunken);
}

.notif__dot {
  flex: 0 0 8px;
  width: 8px;
  height: 8px;
  margin-top: 6px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-accent);
}

.notif__dot--read {
  background: var(--tn-border-strong);
}

.notif__title {
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
}

.notif__body {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
}

.notif__time {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

/* ==========================================================================
   15. DRAWER
   ========================================================================== */
.drawer {
  position: fixed;
  inset: 0;
  z-index: var(--tn-z-drawer);
}

.drawer__scrim {
  position: absolute;
  inset: 0;
  background: var(--tn-scrim);
}

.drawer__panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  width: min(var(--tn-w-drawer), 90vw);
  background: var(--tn-surface);
  box-shadow: var(--tn-shadow-lg);
}

.drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: 0 0 auto;
  min-height: var(--tn-h-topbar);
  padding: 0 var(--tn-sp-5);
  border-bottom: 1px solid var(--tn-border);
}

.drawer__title {
  margin: 0;
  font-size: var(--tn-fs-xl);
  font-weight: var(--tn-fw-semibold);
}

.drawer__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--tn-sp-6);
  overflow-y: auto;
}

.drawer__foot {
  display: flex;
  gap: var(--tn-sp-4);
  flex: 0 0 auto;
  padding: var(--tn-sp-5);
  border-top: 1px solid var(--tn-border);
  background: var(--tn-surface-raised);
}

.drawer__foot .btn-tn {
  height: var(--tn-h-touch);
}

/* ==========================================================================
   17. FORM
   ========================================================================== */
.form-tn__section {
  margin: 0 0 var(--tn-sp-7);
  padding: 0;
  border: 0;
}

.form-tn__section:last-child {
  margin-bottom: 0;
}

.form-tn__legend {
  width: 100%;
  margin: 0 0 var(--tn-sp-5);
  padding-bottom: var(--tn-sp-3);
  border-bottom: 1px solid var(--tn-border);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.form-tn__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--tn-sp-5) var(--tn-sp-6);
}

.form-tn__field {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-2);
  min-width: 0;
}

.form-tn__field--wide {
  grid-column: span 2;
}

.form-tn__label {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
}

.req {
  color: var(--tn-danger);
}

.form-tn__control {
  width: 100%;
  height: var(--tn-h-input);
  padding: 0 var(--tn-sp-4);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-base);
}

/* The same hover the filter controls have. A field is a field, whether
   it is in a filter bar, a dialog or a login card. */
.form-tn__control:hover:not(:disabled):not([readonly]) {
  border-color: var(--tn-input-border-hover);
}

.form-tn__control::placeholder {
  color: var(--tn-input-placeholder);
}

/* DISABLED, for the whole family at once.

   Because these controls paint their own background and colour they also
   defeat the browser's default disabled rendering, so without this a
   disabled field is indistinguishable from one that is waiting for input.
   The token has existed in both themes since the dialogs were built; only
   the [readonly] variant was using it. */
.form-tn__control:disabled,
.filter-field__control:disabled,
.filter-search__input:disabled,
.context-bar__select:disabled,
.selection-bar__input:disabled {
  background: var(--tn-input-bg-disabled);
  border-color: var(--tn-border);
  color: var(--tn-text-muted);
  cursor: not-allowed;
}

.form-tn__control:disabled::placeholder,
.filter-field__control:disabled::placeholder,
.filter-search__input:disabled::placeholder,
.selection-bar__input:disabled::placeholder {
  color: var(--tn-text-muted);
}

.form-tn__control:focus {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
  outline: none;
}

/* INVALID, for the whole control family at once.

   .filter-field__control is here because a filter can be REQUIRED: the
   Reservation Monitor cannot search without a reservation type, and the
   feedback for that has to be the feedback the application already gives
   for a required field rather than a second invalid style invented for a
   filter bar. */
.form-tn__control.is-invalid,
.filter-field__control.is-invalid {
  border-color: var(--tn-danger);
  background: var(--tn-danger-soft);
}

/* The warmer invalid fill drops the ordinary placeholder to 3.9:1, so the
   hint moves to the danger-family token rather than the whole application's
   placeholder being darkened for one state. */
.form-tn__control.is-invalid::placeholder,
.filter-field__control.is-invalid::placeholder {
  color: var(--tn-input-placeholder-invalid);
}

.form-tn__control.is-invalid:focus,
.filter-field__control.is-invalid:focus {
  box-shadow: var(--tn-focus-ring-danger);
}

.form-tn__hint {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.form-tn__error {
  display: none;
  color: var(--tn-danger);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-medium);
}

.form-tn__error.is-visible {
  display: block;
}

/* Confirmation dialog body (reuses the record modal shell) */
.confirm-body {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-sp-5);
}

.confirm-body__icon {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  background: var(--tn-dirty-bg);
  border: 1px solid var(--tn-dirty-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-dirty-text);
  font-size: var(--tn-fs-xl);
}

.confirm-body__text {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-base);
}

.form-tn__alert {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  margin-bottom: var(--tn-sp-6);
  padding: var(--tn-sp-5);
  background: var(--tn-danger-soft);
  border: 1px solid var(--tn-danger-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-danger);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
}

/* ==========================================================================
   18. STATES
   ========================================================================== */
.panel__body.is-state > .grid-wrap,
.panel__body.is-state > .record-list {
  display: none !important;
}

.state-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-4);
  min-height: 160px;
  height: 100%;
  padding: var(--tn-sp-8) var(--tn-sp-6);
  text-align: center;
}

.state-block__icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xl);
}

.state-block--error .state-block__icon {
  background: var(--tn-danger-soft);
  border-color: var(--tn-danger-border);
  color: var(--tn-danger);
}

.state-block__title {
  color: var(--tn-text);
  font-size: var(--tn-fs-lg);
  font-weight: var(--tn-fw-semibold);
}

.state-block__text {
  max-width: 42ch;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
}

.state-block__actions {
  display: flex;
  gap: var(--tn-sp-4);
  margin-top: var(--tn-sp-2);
}

/* --------------------------------------------------------------------------
   THE LOADING PRIMITIVES

   One situation, one component. Everything below is rendered by
   js/loading.js and nothing else in the application may hand-build a
   spinner, a shimmer or a veil.

   THE THREE INTENSITIES, because they are three different promises:

     quiet   the region stays interactive and keeps its content. A refresh
             the user did not ask to wait for.
     local   one region is covered and not interactive. The rest of the
             screen still is.
     page    the whole application is covered. Reserved for an operation
             that genuinely cannot be interrupted; a data refresh is never
             one of these.

   They differ in exactly two observable ways — what the veil is attached
   to, and whether it takes pointer events — so the difference is testable
   rather than a matter of how it looks.
   -------------------------------------------------------------------------- */

/* SPINNER ------------------------------------------------------------------
   display:block is not decoration. Without it the ring computes as an
   inline box, and an inline box has no height of its own: outside a flex
   parent the spinner measured 4x23px and looked like a stray pipe
   character. It rendered correctly in its three original homes only
   because all three happened to be flex containers. */
.spinner-tn {
  display: block;
  flex: none;
  width: var(--tn-spin-md);
  height: var(--tn-spin-md);
  border: 2px solid var(--tn-accent-border);
  border-top-color: var(--tn-accent);
  border-radius: var(--tn-radius-pill);
  animation: tn-spin var(--tn-spin-dur) linear infinite;
}

.spinner-tn--sm { width: var(--tn-spin-sm); height: var(--tn-spin-sm); border-width: 2px; }
.spinner-tn--lg { width: var(--tn-spin-lg); height: var(--tn-spin-lg); border-width: 3px; }

/* Beside a word, the ring sits on the text baseline rather than above it. */
.spinner-tn--inline {
  display: inline-block;
  vertical-align: -2px;
}

@keyframes tn-spin {
  to { transform: rotate(360deg); }
}

/* SKELETON -----------------------------------------------------------------
   The bug this shape is written around: .skeleton-rows used to be dropped
   into .state-block, which is a centred flex column. As a flex item it
   shrink-to-fit, its children are all percentage widths, and so every row
   measured ZERO. The only loading state four production screens actually
   use was 198px of nothing for as long as it has existed.

   width:100% and align-self:stretch are the fix, and both are here rather
   than on the host so that a skeleton is correct wherever it is placed. */
.skeleton-rows {
  width: 100%;
  align-self: stretch;
  padding: var(--tn-sp-4) var(--tn-sp-5);
}

.skeleton-row,
.skeleton-line {
  height: var(--tn-skeleton-h);
  margin-bottom: var(--tn-sp-5);
  border-radius: var(--tn-radius-xs);
  background: linear-gradient(90deg,
              var(--tn-skeleton-a) 25%,
              var(--tn-skeleton-b) 37%,
              var(--tn-skeleton-a) 63%);
  background-size: 400% 100%;
  animation: tn-shimmer var(--tn-shimmer-dur) ease-in-out infinite;
}

/* The width modifiers are the vocabulary of an unevenly-set page. Keeping
   them as named steps rather than arbitrary percentages is what stops a
   skeleton drifting into a decorative pattern. */
.skeleton-row--w100, .skeleton-line--w100 { width: 100%; }
.skeleton-row--w90,  .skeleton-line--w90  { width: 90%; }
.skeleton-row--w80,  .skeleton-line--w80  { width: 80%; }
.skeleton-row--w70,  .skeleton-line--w70  { width: 70%; }
.skeleton-row--w60,  .skeleton-line--w60  { width: 60%; }
.skeleton-row--w40,  .skeleton-line--w40  { width: 40%; }
.skeleton-row--w30,  .skeleton-line--w30  { width: 30%; }

/* the shapes a skeleton takes when it is standing in for something other
   than a row of table text */
.skeleton-line--title  { height: var(--tn-skeleton-h-title); }
.skeleton-line--cap    { height: var(--tn-skeleton-h-sm); }
.skeleton-line--last   { margin-bottom: 0; }

.skeleton-stack {
  display: flex;
  flex-direction: column;
  width: 100%;
}

/* a label above a value, the shape most of the app's read-only pairs take */
.skeleton-pair {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-3);
  width: 100%;
}

.skeleton-block {
  width: 100%;
  border-radius: var(--tn-radius-sm);
  background: linear-gradient(90deg,
              var(--tn-skeleton-a) 25%,
              var(--tn-skeleton-b) 37%,
              var(--tn-skeleton-a) 63%);
  background-size: 400% 100%;
  animation: tn-shimmer var(--tn-shimmer-dur) ease-in-out infinite;
}

.skeleton-block--chart { height: 180px; }
.skeleton-block--kpi   { height: 64px; }
.skeleton-block--field { height: var(--tn-h-input); border-radius: var(--tn-radius-sm); }

.skeleton-circle {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: var(--tn-radius-pill);
  background: linear-gradient(90deg,
              var(--tn-skeleton-a) 25%,
              var(--tn-skeleton-b) 37%,
              var(--tn-skeleton-a) 63%);
  background-size: 400% 100%;
  animation: tn-shimmer var(--tn-shimmer-dur) ease-in-out infinite;
}

/* a list row: avatar, two lines of text */
.skeleton-item {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  padding: var(--tn-sp-4) var(--tn-sp-5);
}

.skeleton-item__body {
  flex: 1 1 auto;
  min-width: 0;
}

@keyframes tn-shimmer {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* VEIL ---------------------------------------------------------------------
   Sits over content that is still there. The host gets .is-veiled, which
   only establishes the containing block — it deliberately does not hide,
   empty or re-layout anything, because the entire point of a veil (as
   opposed to a state block) is that the table underneath keeps its column
   widths and its scroll position. */
.is-veiled {
  position: relative;
}

.loading-veil {
  position: absolute;
  inset: 0;
  z-index: var(--tn-z-sticky);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-4);
  background: var(--tn-veil);
  border-radius: inherit;
}

/* QUIET: the region is still yours. The veil is a label, not a door — it
   must not swallow the click that was already on its way to a row. */
.loading-veil[data-loading-scope="quiet"] {
  pointer-events: none;
  background: transparent;
}

/* The quiet veil is a LABEL on top of live content, so its body is a pill
   rather than a full-bleed wash — the content behind it has to stay
   readable, which is the whole difference between quiet and local. */
.loading-veil[data-loading-scope="quiet"] .loading-veil__body {
  background: var(--tn-veil-strong);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
  box-shadow: var(--tn-shadow-sm);
  padding: var(--tn-sp-3) var(--tn-sp-6);
}

/* While the page veil is up, the document beneath it must not scroll —
   otherwise the one situation that justifies a full block is also the one
   where content slides around behind the thing blocking it. */
.is-page-loading {
  overflow: hidden;
}

/* PAGE: over everything, including an open dialog, because the reason to
   use it at all is that nothing else may be touched. */
.loading-veil[data-loading-scope="page"] {
  position: fixed;
  z-index: calc(var(--tn-z-toast) + 1);
  background: var(--tn-veil-strong);
}

.loading-veil__body {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  max-width: 42ch;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
  text-align: center;
}

.loading-veil__text {
  font-weight: var(--tn-fw-medium);
}

/* PROGRESS -----------------------------------------------------------------
   Determinate when the work can answer "how much of it is done"; the
   indeterminate variant when it cannot. Using the indeterminate bar for
   work that DOES know its size is the more common mistake, and it reads as
   a system that has lost track of itself. */
.progress-tn {
  position: relative;
  width: 100%;
  height: var(--tn-progress-h);
  overflow: hidden;
  background: var(--tn-progress-track);
  border-radius: var(--tn-radius-pill);
}

.progress-tn--lg { height: var(--tn-progress-h-lg); }

.progress-tn__fill {
  display: block;
  width: 0;
  height: 100%;
  background: var(--tn-progress-fill);
  border-radius: var(--tn-radius-pill);
  transition: width var(--tn-dur-base) var(--tn-ease);
}

.progress-tn--indeterminate .progress-tn__fill {
  width: 40%;
  animation: tn-progress-slide 1.1s ease-in-out infinite;
}

@keyframes tn-progress-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}

.progress-tn__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--tn-sp-4);
  margin-bottom: var(--tn-sp-3);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.progress-tn__pct {
  color: var(--tn-text-secondary);
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
}

/* BUSY BUTTON --------------------------------------------------------------
   The requirement is that the button does not change size. The old login
   implementation replaced the button's innerHTML with a spinner, so a
   "Sign in" 96px wide became a 38px circle and the layout jumped — which
   is exactly the moment a user is most likely to be clicking again.

   So: the label stays in the box and keeps holding it open, is made
   invisible rather than removed, and the spinner is centred on top. Width,
   height and position are untouched by construction rather than by
   matching a hard-coded min-width to each caller. */
.btn-tn.is-busy,
.icon-btn.is-busy {
  position: relative;
  pointer-events: none;
}

.btn-tn.is-busy > *,
.icon-btn.is-busy > * {
  visibility: hidden;
}

.btn-tn.is-busy .spinner-tn,
.icon-btn.is-busy .spinner-tn {
  position: absolute;
  top: 50%;
  left: 50%;
  visibility: visible;
  margin: calc(var(--tn-spin-sm) / -2) 0 0 calc(var(--tn-spin-sm) / -2);
}

/* On a primary or destructive button the ring has to be legible against a
   filled background, so it borrows the button's own foreground. */
.btn-tn--primary.is-busy .spinner-tn,
.btn-tn--danger.is-busy .spinner-tn {
  border-color: var(--tn-on-accent-soft);
  border-top-color: var(--tn-text-on-accent);
}

/* FIELD --------------------------------------------------------------------
   A control that is fetching its own options, or validating what was just
   typed. The spinner sits inside the control's box, so nothing reflows. */
.field__busy {
  position: absolute;
  right: var(--tn-sp-5);
  bottom: calc(var(--tn-h-input-dense) / 2 - var(--tn-spin-sm) / 2);
  pointer-events: none;
}

/* The field family is .field wrapping a .form-tn__control — js/dialog.js
   fieldHtml() — so the busy state attaches there rather than inventing a
   parallel .field-tn nobody else renders. */
.field--busy { position: relative; }

.field--busy .form-tn__control {
  padding-right: calc(var(--tn-sp-5) * 2 + var(--tn-spin-sm));
}

/* INLINE -------------------------------------------------------------------
   "Checking availability…" beside the thing being checked. */
.loading-inline {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

/* REDUCED MOTION -----------------------------------------------------------
   Every looping loader flattens to a single instant iteration, which keeps
   the shape and drops the movement. The global transition kill-switch has
   lived in this block since long before the loading system did; it stays
   here so there is exactly one place the application's motion policy is
   written down. */
@media (prefers-reduced-motion: reduce) {
  .skeleton-row,
  .skeleton-line,
  .skeleton-block,
  .skeleton-circle,
  .spinner-tn,
  .progress-tn--indeterminate .progress-tn__fill {
    animation-duration: 0.01ms;
    animation-iteration-count: 1;
  }

  * {
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   18a. IMPORT

   The Import screen introduced the one control family this design system
   had never had: a file picker. Everything here exists because of that and
   is used nowhere else, which is why it is its own short section rather
   than scattered through the panel and form ones.

   THE REAL <input type="file"> IS HIDDEN with the shared .sr-only and is
   driven by a .btn-tn. That is not a styling trick — it is the only way to
   have one button geometry in the application. A file input cannot be
   restyled into a .btn-tn across browsers; every attempt ends up drawing
   the button anyway and leaving a second, differently-sized control behind
   it. So the button is the real button, and the input is a mechanism.
   ========================================================================== */
.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-4);
  padding: var(--tn-sp-9) var(--tn-sp-6);
  background: var(--tn-drop-bg);
  border: 2px dashed var(--tn-drop-border);
  border-radius: var(--tn-radius-md);
  text-align: center;
  transition: background var(--tn-dur-fast) var(--tn-ease),
              border-color var(--tn-dur-fast) var(--tn-ease);
}

/* Dragging over it. Not an animation — a state, so it cannot be mistaken
   for the file already doing something. */
.dropzone.is-over {
  background: var(--tn-drop-bg-over);
  border-color: var(--tn-drop-border-over);
}

.dropzone__icon {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-title);
}

.dropzone__lead {
  margin: 0;
  color: var(--tn-text);
  font-size: var(--tn-fs-lg);
  font-weight: var(--tn-fw-semibold);
}

.dropzone__hint {
  margin: 0;
  max-width: 46ch;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

/* INLINE — the same component as a form field rather than a landing surface.
   The full dropzone IS the Import screen and is sized like it: a 32px-padded
   column you aim a file at. A CMS page's top image is one field among a
   dozen, so the component turns on its side — icon, label and command on one
   line, the height of a control instead of the height of a screen. It is
   still the same drop target, with the same .is-over state. */
.dropzone--inline {
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: var(--tn-sp-5);
  padding: var(--tn-sp-4) var(--tn-sp-5);
  text-align: left;
}

.dropzone--inline .dropzone__icon { font-size: var(--tn-fs-xl); }

/* The label and its hint share the middle and push the command to the end. */
.dropzone--inline .dropzone__text {
  flex: 1 1 auto;
  min-width: 0;
}

.dropzone--inline .dropzone__lead { font-size: var(--tn-fs-md); }

.dropzone--inline .dropzone__hint {
  max-width: none;
  font-size: var(--tn-fs-xs);
}

.dropzone--inline > .btn-tn { flex: none; }

/* Narrow enough that one line will not hold all three: the command drops
   under the label rather than squeezing the hint to nothing. */
@media (max-width: 575.98px) {
  .dropzone--inline {
    flex-wrap: wrap;
    row-gap: var(--tn-sp-4);
  }

  .dropzone--inline > .btn-tn { flex: 1 0 100%; justify-content: center; }
}

/* THE CHOSEN FILE ----------------------------------------------------------
   Replaces the drop surface rather than sitting under it: two affordances
   for "choose a file" on screen at once is how a person ends up importing
   the wrong one. */
.file-card {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  padding: var(--tn-sp-5);
  background: var(--tn-surface-raised);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
}

.file-card__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 40px;
  height: 40px;
  background: var(--tn-surface-sunken);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xl);
}

.file-card__body {
  flex: 1 1 auto;
  min-width: 0;
}

.file-card__name {
  display: block;
  overflow: hidden;
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-card__meta {
  display: block;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

/* The file card as a form field — the counterpart of .dropzone--inline, and
   the same height, so choosing an image does not move the fields under it. */
.file-card--inline {
  gap: var(--tn-sp-4);
  padding: var(--tn-sp-3) var(--tn-sp-4);
}

.file-card--inline .file-card__icon {
  width: 32px;
  height: 32px;
  font-size: var(--tn-fs-lg);
}

/* The verdict on the file. Four tones, and the busy one carries the shared
   inline loader rather than a fifth colour. */
.file-card__status {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-medium);
}

.file-card__status--ok   { color: var(--tn-success); }
.file-card__status--warn { color: var(--tn-warning); }
.file-card__status--bad  { color: var(--tn-danger); }

.import-progress {
  margin-top: var(--tn-sp-6);
}

/* THE FALLBACK -------------------------------------------------------------
   What a PDF gets when its geometry does not describe a table. It is
   deliberately typeset as a document rather than as data: nothing here is
   in columns, nothing is selectable as a record, and there is no Import
   button pointed at it. */
.extract-note {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-sp-4);
  margin-bottom: var(--tn-sp-6);
  padding: var(--tn-sp-5);
  background: var(--tn-accent-soft);
  border: 1px solid var(--tn-accent-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
}

.extract-text {
  max-height: 420px;
  overflow-y: auto;
  padding: var(--tn-sp-5);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border-subtle);
  border-radius: var(--tn-radius-sm);
}

.extract-text__page {
  margin: var(--tn-sp-6) 0 var(--tn-sp-4);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.extract-text__page:first-child { margin-top: 0; }

.extract-text__line {
  margin: 0 0 var(--tn-sp-3);
  color: var(--tn-text);
  font-size: var(--tn-fs-sm);
  line-height: 1.5;
}

/* On a phone the file card stacks: a name, its size and a verdict do not
   fit on one 390px line beside two controls, and truncating the filename
   to nothing is worse than using the height. */
@media (max-width: 575.98px) {
  .file-card {
    flex-wrap: wrap;
  }

  .file-card__body { flex: 1 1 100%; order: 1; }
  .file-card__icon { order: 0; }
  .file-card__status { flex: 1 1 100%; order: 2; }

  /* The landing surface only. The inline variant is a form field and keeps
     the padding it was given above — without the :not() this rule wins on
     source order (same specificity, 170 lines later) and puts 20px back,
     which is exactly the height that made the field jump when a file was
     chosen. */
  .dropzone:not(.dropzone--inline) { padding: var(--tn-sp-7) var(--tn-sp-5); }
}

/* ==========================================================================
   18d. THE SHOWCASE BENCH

   Shared by Loading States and UI Components, and by any reference page
   after them. js/showcase.js renders it.

   THE ONE RULE THIS LAYOUT EXISTS TO ENFORCE: the controls that operate a
   demonstration sit directly above that demonstration, inside the same
   bordered block. Loading States used to put Play and Reset in the page
   header, six hundred pixels away from the thing they operated — which on a
   reference page is not a small inconvenience, because making the relation
   between an action and its result obvious is the whole job of the page.

       ┌────────────────────────────────────────────────┐
       │ Scenario [ Refresh ▾ ]           [Play] [Reset]│
       ├────────────────────────────────────────────────┤
       │ one sentence                                   │
       │ THE LIVE DEMONSTRATION                         │
       └────────────────────────────────────────────────┘

   Every part is a canonical component: the scenario control is a
   .form-tn__control select and the actions are .btn-tn commands. There is
   no showcase-only button, field or surface styling anywhere here — the
   point of these pages is to render the real thing.
   ========================================================================== */
.showcase-demo {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  overflow: hidden;
}

.showcase-demo__toolbar {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  flex: 0 0 auto;
  flex-wrap: wrap;
  padding: var(--tn-sp-4) var(--tn-sp-5);
  background: var(--tn-surface-raised);
  border-bottom: 1px solid var(--tn-border);
}

.showcase-demo__label {
  flex: none;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-medium);
}

/* a bench with a single scenario names it instead of offering a choice */
.showcase-demo__label--only {
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
}

/* No height of its own. It is a .form-tn__control, and the design contract
   holds that every page-density form control is ONE control — a toolbar is
   not a licence to invent a second signature. Only the width is set. */
.showcase-demo__select {
  flex: 0 1 260px;
  min-width: 0;
}

/* The actions ride the trailing edge of the LOCAL toolbar — still inside
   the demonstration's own block, never the page header. */
.showcase-demo__actions {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  margin-left: auto;
}

.showcase-demo__note {
  flex: 0 0 auto;
  max-width: 100ch;
  margin: 0;
  padding: var(--tn-sp-5) var(--tn-sp-6) 0;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
  line-height: 1.55;
}

/* The dominant element, and it scrolls.

   Not by preference — by measurement. Four command regions stacked, or the
   button families on a 390px phone, are genuinely taller than any viewport
   this application supports, and a stage that merely CLIPPED them put 437px
   of specimen somewhere nobody could reach. That is the same failure the
   old Loading States page had, one level down.

   The toolbar stays outside this box, so the controls remain visible while
   the specimens scroll under them — which is the arrangement every panel in
   the application already uses. */
.showcase-demo__stage {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--tn-sp-6);
  min-height: 0;
  overflow-y: auto;
  /* min-width:0 is load-bearing: a real .data-grid inside a flex child
     establishes a min-content width, and without this the stage refuses to
     shrink below it and the PAGE scrolls sideways on a phone. */
  min-width: 0;
  padding: var(--tn-sp-6);
}

/* ---- the specimen shelves ------------------------------------------------
   A showcase often shows several instances of one component side by side.
   These are the shelves they stand on — no component styling, only
   arrangement and a caption. */
.showcase-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--tn-sp-7);
}

.showcase-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--tn-sp-4);
  min-width: 0;
}

.showcase-item__cap {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-medium);
}

.showcase-item__note {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  font-variant-numeric: tabular-nums;
}

.showcase-group {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-5);
  min-width: 0;
}

.showcase-group__title {
  margin: 0;
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
}

.showcase-group__hint {
  margin: 0;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

/* a bounded surface for a specimen that needs a background of its own —
   a command region, a menu, a form */
.showcase-surface {
  min-width: 0;
  padding: var(--tn-sp-6);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border-subtle);
  border-radius: var(--tn-radius-sm);
}

/* A menu surface shown IN FLOW rather than opened over something.

   The only thing overridden is the positioning: .popover-menu is fixed
   because it is normally anchored to a trigger, and a specimen on a shelf
   has nothing to anchor to. Everything else — the border, the radius, the
   shadow, the item geometry — is the real surface, which is the whole point
   of showing it. docs/components.css does the identical thing with
   .cat-menu. */
.showcase-menu {
  position: static;
  max-width: 320px;
}

/* A real .panel standing on a stage as a specimen. The only thing changed
   is that it sizes to its content instead of competing for the height of a
   workspace it is not in — everything visible about it is the real panel. */
.showcase-panel {
  flex: 0 0 auto;
  min-height: 0;
  max-height: 340px;
}

.showcase-panel .grid-wrap {
  max-height: 240px;
}

/* The call a screen would write, shown beside its result. Monospace and
   sunken — it is a specimen of CODE, and must not be mistaken for one of
   the application's own surfaces. */
.api-call {
  max-width: 100%;
  margin: 0;
  overflow-x: auto;
  padding: var(--tn-sp-5);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border-subtle);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-family: var(--tn-font-mono);
  font-size: var(--tn-fs-xs);
  line-height: 1.6;
  white-space: pre;
}

.api-report {
  margin: 0;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
}

.api-report--ok { color: var(--tn-success); font-weight: var(--tn-fw-medium); }
.api-report--bad { color: var(--tn-danger); font-weight: var(--tn-fw-medium); }

/* ==========================================================================
   18e. CMS EXAMPLE

   Composition only. Every part of that screen — the panels, the row that
   puts two of them side by side, the form sections, the dropzone, the file
   card, the editor bar and sheet, the grid — is a component that already
   existed. What is here says how this screen arranges them: the form and the
   document share a row, so the editor's body becomes a column and its sheet
   fills whatever height the form asks for.
   ========================================================================== */

/* The chosen image, in the shared .file-card's icon slot. */
.cms-image__thumb {
  overflow: hidden;
  padding: 0;
}

.cms-image__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cms-form { min-width: 0; }

/* Half the workspace, so the SECTIONS read top to bottom and only the fields
   inside them sit side by side — the two-column composition the form already
   had, in a column half as wide. The section gap comes in from 16/20 because
   at this width the legends are close enough to be read as a group. */
.cms-form .form-tn__sections {
  gap: var(--tn-sp-5) var(--tn-sp-6);
}

/* The legend's own block, to match. 12px of margin under a rule was right
   when the sections were 16px apart on a page-wide sheet; at 12px apart in
   a half-width column the label sat closer to the group above it than to
   the fields it names. */
.cms-form .form-tn__legend {
  margin-bottom: var(--tn-sp-4);
  padding-bottom: var(--tn-sp-2);
}

/* The URL field's "/pages/" prefix.

   .field-compound puts its affix INSIDE the control's box and pads the
   control to clear it — but that padding rule is scoped to .dlg, and this
   form is the same renderer running on a PAGE. Without it the value began
   at the control's own 8px and rendered straight through the prefix.

   Not fixed by unscoping the dialog rule: its 24px was sized for a dialog's
   affixes, which are a currency symbol or a unit, and this one is a word.
   The page states the width it actually needs. Pass 31 measures the affix
   and the text start and fails if they ever meet, so the number cannot rot
   quietly. */
.cms-form .field-compound--prefix .form-tn__control {
  padding-left: 60px;
}

/* THE EDITOR COLUMN --------------------------------------------------------
   Everything here is true only while the editor is one half of a row. Stacked,
   the panel is the height of its own content and the editor goes back to
   behaving the way it does on the E-Mail screen — which matters, because
   these selectors carry an ID and would otherwise outrank every
   .editor-canvas / .editor-sheet rule in responsive.css at every width. */
@media (min-width: 1200px) {
  /* Toolbar pinned to the top of the panel, only the canvas under it moving —
     the same behaviour as .mail-editor__body, stated here rather than
     borrowed, since that class is the mail screen's grid child and carries
     its sizing with it. .panel__body on its own is a plain scrolling box, so
     without this the canvas's flex:1 has no column to grow in and the sheet
     sits at its minimum with dead space beneath it. */
  #panelEditor > .panel__body {
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  /* A lane inside a half-width column, not a page-wide desk. */
  #panelEditor .editor-canvas {
    padding: var(--tn-sp-5);
  }

  /* A floor low enough that the editor never dictates the height of the row —
     the form does — and .editor-sheet's own flex:1 takes the rest, so the
     document fills whatever the column turns out to be. */
  #cmsSheet {
    min-height: 260px;
  }
}

/* Stacked, the panel is the height of its content: nothing to clip, nothing
   to pin, and no neighbour to match — so the document asks for a workable
   height of its own and the shared phone rules for .editor-canvas apply
   without an ID standing over them. */
@media (max-width: 1199.98px) {
  #cmsSheet { min-height: 340px; }
}

/* ==========================================================================
   18d (continued). THE SHOWCASE BENCH — the stage's own surfaces

   These belong to 18d above. The CMS section was filed in front of them by
   mistake when that screen was built; the banner goes here rather than the
   rules being moved, so nothing in the cascade shifts.
   ========================================================================== */
.showcase-scroll {
  min-width: 0;
  overflow-x: auto;
}

/* the grid bench: the table brings its own horizontal scrolling */
.showcase-table {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
  overflow: hidden;
}

.showcase-table .grid-wrap {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
}

.showcase-table__foot {
  flex: 0 0 auto;
  padding: var(--tn-sp-4) var(--tn-sp-5);
  border-top: 1px solid var(--tn-border-subtle);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

.showcase-form {
  display: grid;
  gap: var(--tn-sp-6);
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  align-content: start;
  min-width: 0;
}

/* ---- mobile --------------------------------------------------------------
   The toolbar wraps: the scenario takes the first line at full width and
   the actions drop below it, still directly above the stage. */
@media (max-width: 767.98px) {
  .showcase-demo__toolbar {
    align-items: stretch;
    gap: var(--tn-sp-3);
  }

  .showcase-demo__select {
    flex: 1 0 100%;
  }

  .showcase-demo__actions {
    margin-left: 0;
    width: 100%;
  }

  /* the two commands share the width rather than huddling on one side */
  .showcase-demo__actions > .btn-tn {
    flex: 1 1 0;
    justify-content: center;
  }

  .showcase-demo__stage { padding: var(--tn-sp-5); gap: var(--tn-sp-5); }
  .showcase-demo__note { padding: var(--tn-sp-5) var(--tn-sp-5) 0; }
  .showcase-row { gap: var(--tn-sp-6); }
}

/* ==========================================================================
   18c. THE LOADING LABORATORY

   Chrome for the Loading States screen, and nothing else. The SPECIMENS on
   that page are the real production components; everything here is the
   bench they stand on — a stage, and a few neutral stand-ins (a fake page,
   a fake panel, a fake dashboard) that exist purely to give a loading state
   something to be applied TO.

   THE STAGE IS THE POINT. The previous version put ten categories down one
   page as a grid of 320px cards, and every demonstration ended up in a
   ~100px box with its own scrollbar. A loading state is something that
   HAPPENS, over about two seconds, in a region of a particular size; at
   that scale it communicates nothing. So one category now fills the
   workspace and the stage takes every pixel the panel is not using.

   Kept deliberately plain. A reference page whose scenery is more
   interesting than its exhibits teaches the scenery.
   ========================================================================== */
/* One category, one panel, and it takes the height the category has.
   Without this the panel is only as tall as its own content and the stage
   collapses to ~150px — which is the exact failure this rewrite exists to
   undo, reintroduced one level down. */
.panel--lab {
  flex: 1 1 auto;
  min-height: 0;
}

.panel--lab > .panel__body {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.demo-note {
  flex: 0 0 auto;
  max-width: 100ch;
  margin: 0 0 var(--tn-sp-5);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
  line-height: 1.55;
}

/* The dominant element. It fills the panel body and does NOT scroll: if a
   specimen needs scrolling it brings its own (a .grid-wrap), which is the
   same rule every other screen follows. */
.demo-stage {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--tn-sp-6);
  min-height: 0;
  /* min-width:0 is load-bearing, not tidiness: a real .data-grid inside a
     flex or grid child establishes a min-content width, and without this
     the stage refuses to shrink below it and the PAGE scrolls sideways on
     a phone. That is exactly how #panelGrid measured 453px at 390px. */
  min-width: 0;
  padding: var(--tn-sp-6);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border-subtle);
  border-radius: var(--tn-radius-sm);
}

/* the resting note before a demonstration is played */
.lab-hold {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-4);
  flex: 1 1 auto;
  min-height: 120px;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
  text-align: center;
}

.lab-text {
  margin: 0 0 var(--tn-sp-4);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
  line-height: 1.55;
}

.lab-text:last-child { margin-bottom: 0; }

.lab-ok {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  color: var(--tn-success);
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-medium);
}

.lab-measure {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  font-variant-numeric: tabular-nums;
}

/* ---- a stand-in page ---------------------------------------------------- */
.lab-page {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--tn-sp-7);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

.lab-page__head { margin-bottom: var(--tn-sp-7); }

.lab-page__title {
  margin: 0 0 var(--tn-sp-2);
  color: var(--tn-text);
  font-size: var(--tn-fs-xl);
  font-weight: var(--tn-fw-semibold);
}

.lab-page__sub {
  margin: 0;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

.lab-page__grid {
  display: grid;
  gap: var(--tn-sp-6);
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  margin-bottom: var(--tn-sp-7);
}

.lab-stamp {
  margin: var(--tn-sp-6) 0 0;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.lab-card {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-2);
  min-width: 0;
  padding: var(--tn-sp-5);
  background: var(--tn-surface-raised);
  border: 1px solid var(--tn-border-subtle);
  border-radius: var(--tn-radius-sm);
}

.lab-card__cap {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.lab-card__value {
  color: var(--tn-text);
  font-size: var(--tn-fs-xl);
  font-weight: var(--tn-fw-semibold);
  font-variant-numeric: tabular-nums;
}

.lab-card__note {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
}

/* ---- a stand-in panel --------------------------------------------------- */
.lab-panel {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

.lab-panel__head {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  padding: var(--tn-sp-5) var(--tn-sp-6);
  background: var(--tn-surface-raised);
  border-bottom: 1px solid var(--tn-border);
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
}

.lab-panel__body {
  position: relative;
  padding: var(--tn-sp-6);
}

/* ---- the table bench ---------------------------------------------------- */
.lab-table {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

/* The grid brings its own horizontal scrolling — the established
   convention — so the page never has to. */
.lab-table .grid-wrap {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
}

.lab-table__foot {
  flex: 0 0 auto;
  min-height: 20px;
  padding: var(--tn-sp-4) var(--tn-sp-5);
  border-top: 1px solid var(--tn-border-subtle);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

/* ---- the form bench ----------------------------------------------------- */
.lab-form {
  position: relative;
  display: grid;
  gap: var(--tn-sp-6);
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  align-content: start;
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--tn-sp-6);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

/* ---- the button bench --------------------------------------------------- */
.lab-buttons {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: var(--tn-sp-8);
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--tn-sp-7);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

.lab-button {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--tn-sp-4);
}

.lab-button__cap {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-medium);
}

/* ---- text ---------------------------------------------------------------- */
.lab-text-demo {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 80ch;
  padding: var(--tn-sp-6);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

.lab-inline-skel { display: inline-block; min-width: 160px; vertical-align: middle; }

.lab-pairs {
  display: grid;
  gap: var(--tn-sp-7);
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  align-content: start;
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--tn-sp-6);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

.lab-pair { display: flex; flex-direction: column; gap: var(--tn-sp-2); min-width: 0; }

.lab-pair__cap {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.lab-pair__value {
  color: var(--tn-text);
  font-size: var(--tn-fs-lg);
  font-weight: var(--tn-fw-medium);
}

/* ---- the dashboard bench ------------------------------------------------ */
.lab-dash {
  display: grid;
  gap: var(--tn-sp-6);
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  grid-template-areas: 'kpis kpis' 'chart list';
  align-content: start;
  flex: 1 1 auto;
  min-width: 0;
}

.lab-dash__kpis {
  grid-area: kpis;
  display: grid;
  gap: var(--tn-sp-6);
  grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
  min-width: 0;
}

.lab-dash__chart,
.lab-dash__list {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-5);
  min-width: 0;
  padding: var(--tn-sp-6);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

.lab-dash__chart { grid-area: chart; }
.lab-dash__list { grid-area: list; }

.lab-dash__cap {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-medium);
}

.lab-bars {
  display: flex;
  align-items: flex-end;
  gap: var(--tn-sp-3);
  min-height: 160px;
  height: 160px;
}

.lab-bar {
  flex: 1 1 auto;
  min-width: 6px;
  height: 0;
  background: var(--tn-chart-1);
  border-radius: var(--tn-radius-xs) var(--tn-radius-xs) 0 0;
  transition: height var(--tn-dur-chart) var(--tn-ease);
}

.lab-row {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  padding: var(--tn-sp-4) 0;
  border-bottom: 1px solid var(--tn-border-subtle);
}

.lab-row:last-child { border-bottom: 0; }

.lab-row__mark {
  flex: none;
  width: 8px;
  height: 8px;
  background: var(--tn-chart-1);
  border-radius: var(--tn-radius-pill);
}

.lab-row__body { display: flex; flex-direction: column; gap: var(--tn-sp-1); min-width: 0; }

.lab-row__title {
  color: var(--tn-text);
  font-size: var(--tn-fs-sm);
  font-weight: var(--tn-fw-medium);
}

.lab-row__meta {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

/* ---- search and file ---------------------------------------------------- */
.lab-search,
.lab-file {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-5);
  align-content: start;
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--tn-sp-6);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

.lab-file { align-items: flex-start; }
.lab-file > .progress-tn,
.lab-file > .progress-tn__meta { width: 100%; }

.lab-search__state {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  padding: var(--tn-sp-5) 0;
  font-size: var(--tn-fs-md);
}

/* "Still looking" and "nothing found" must not be mistakable for one
   another: the empty state is muted, static and bordered; the loading one
   carries the moving ring and names what it is searching. */
.lab-search__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--tn-sp-4);
  padding: var(--tn-sp-8);
  background: var(--tn-surface-sunken);
  border: 1px dashed var(--tn-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
}

.lab-search__hit {
  padding: var(--tn-sp-5);
  border-bottom: 1px solid var(--tn-border-subtle);
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
}

.lab-search__hit:last-child { border-bottom: 0; }

/* ---- the blocking bench ------------------------------------------------- */
.lab-block {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
}

.lab-region {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--tn-sp-5);
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--tn-sp-7);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
}

/* ---- the dialog bench --------------------------------------------------- */
.lab-dlg { display: grid; gap: var(--tn-sp-5); }

/* ---- narrow ------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .lab-dash {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: 'kpis' 'chart' 'list';
  }
}

@media (max-width: 767.98px) {
  /* On a phone the panel head cannot carry a title, a demonstration
     switcher AND two commands on one 44px line: the .sub-tabs strip scrolls
     under the Play button and the demonstration you are choosing is the one
     you cannot see. So the head wraps, and the switcher gets its own row at
     full width — where it scrolls horizontally on its own terms, which is
     the behaviour .sub-tabs already has. */
  .panel--lab .panel__head {
    flex-wrap: wrap;
    align-items: center;
    padding-bottom: var(--tn-sp-4);
  }

  .panel--lab .panel__head > .sub-tabs {
    order: 3;
    flex: 1 0 100%;
    padding-top: var(--tn-sp-3);
  }

  .panel--lab .panel__head > .panel__actions {
    order: 2;
    margin-left: auto;
  }

  .demo-stage { padding: var(--tn-sp-5); gap: var(--tn-sp-5); }
  .lab-page,
  .lab-buttons,
  .lab-form,
  .lab-region { padding: var(--tn-sp-5); }
  .lab-buttons { gap: var(--tn-sp-6); }
  .lab-page__grid { grid-template-columns: minmax(0, 1fr); }
}

/* ==========================================================================
   18b. SCROLLBARS
   Browser-default bars are the loudest raw cue in a dense grid app. These
   are subtle, themed and stay out of the way, without hiding the affordance.
   ========================================================================== */
/* Menus, drawers, modals and panels are wheeled, not dragged. They stay
   exactly as thin as they were. */
.panel__body,
.drawer__body,
.notif-list,
.search-results,
.action-menu,
.popover-menu,
.sidebar__nav {
  scrollbar-width: thin;
  scrollbar-color: var(--tn-scrollbar-thumb) transparent;
}

/* A data grid is different: a wide table is scrolled sideways by grabbing
   its bar, so the bar has to be worth aiming at. One step up — auto rather
   than thin — and the horizontal and vertical bars take the same value, so
   a grid that scrolls both ways looks deliberate rather than mismatched.

   BOTH mechanisms are set, to the same visual size, on purpose. Chrome 121+
   and Firefox honour the standard properties and ignore ::-webkit-scrollbar
   entirely for any element that has scrollbar-width set; Safari and older
   WebKit honour only the pseudo-elements. Leaving the pseudo-element at the
   old 11px would have meant the two engines quietly disagreeing. */
.grid-wrap {
  scrollbar-width: auto;
  scrollbar-color: var(--tn-scrollbar-thumb) transparent;
}

/* The one hover state the standard property can express: the thumb darkens
   while the pointer is anywhere over the grid it belongs to. */
.grid-wrap:hover {
  scrollbar-color: var(--tn-scrollbar-thumb-hover) transparent;
}

.grid-wrap::-webkit-scrollbar {
  width: var(--tn-scrollbar-grid);
  height: var(--tn-scrollbar-grid);
}

.panel__body::-webkit-scrollbar,
.drawer__body::-webkit-scrollbar,
.notif-list::-webkit-scrollbar,
.search-results::-webkit-scrollbar,
.action-menu::-webkit-scrollbar,
.popover-menu::-webkit-scrollbar {
  width: var(--tn-scrollbar);
  height: var(--tn-scrollbar);
}

.grid-wrap::-webkit-scrollbar-track,
.panel__body::-webkit-scrollbar-track,
.drawer__body::-webkit-scrollbar-track,
.notif-list::-webkit-scrollbar-track,
.search-results::-webkit-scrollbar-track,
.action-menu::-webkit-scrollbar-track,
.popover-menu::-webkit-scrollbar-track {
  background: transparent;
}

.grid-wrap::-webkit-scrollbar-thumb,
.panel__body::-webkit-scrollbar-thumb,
.drawer__body::-webkit-scrollbar-thumb,
.notif-list::-webkit-scrollbar-thumb,
.search-results::-webkit-scrollbar-thumb,
.action-menu::-webkit-scrollbar-thumb,
.popover-menu::-webkit-scrollbar-thumb {
  background: var(--tn-scrollbar-thumb);
  border: 3px solid transparent;
  background-clip: padding-box;
  border-radius: var(--tn-radius-pill);
}

.grid-wrap::-webkit-scrollbar-thumb:hover,
.panel__body::-webkit-scrollbar-thumb:hover,
.drawer__body::-webkit-scrollbar-thumb:hover {
  background: var(--tn-scrollbar-thumb-hover);
  background-clip: padding-box;
}

/* A grid bar is grabbed, so it also answers to the pointer being near it
   rather than exactly on it. */
.grid-wrap:hover::-webkit-scrollbar-thumb {
  background: var(--tn-scrollbar-thumb-hover);
  background-clip: padding-box;
}

.grid-wrap::-webkit-scrollbar-corner,
.panel__body::-webkit-scrollbar-corner {
  background: transparent;
}

/* ==========================================================================
   ADVANCED FILTER WORKSPACE (Reservation Monitor)
   A dense grouped filter area rather than a wall of inputs or a stack of
   cards: column separation and a quiet heading are enough structure, and the
   whole thing collapses to a chip so the grid can take the space back.
   ========================================================================== */
.adv-filter {
  margin-top: var(--tn-sp-4);
  border-top: 1px solid var(--tn-border);
}

.adv-filter__toggle {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-4);
  width: 100%;
  padding: var(--tn-sp-4) 0 var(--tn-sp-3);
  background: none;
  border: 0;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
  text-align: left;
  cursor: pointer;
}

.adv-filter__toggle:focus-visible {
  outline: none;
  box-shadow: var(--tn-focus-ring);
  border-radius: var(--tn-radius-sm);
}

.adv-filter__caret {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
  transition: transform var(--tn-dur-fast) var(--tn-ease);
}

.adv-filter.is-collapsed .adv-filter__caret {
  transform: rotate(-90deg);
}

.adv-filter__summary {
  padding: 1px var(--tn-sp-4);
  background: var(--tn-accent-soft);
  border: 1px solid var(--tn-accent-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-accent);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
}

.adv-filter__body {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--tn-sp-5) var(--tn-sp-7);
  padding-bottom: var(--tn-sp-5);
}

.adv-filter.is-collapsed .adv-filter__body {
  display: none;
}

/* Column separation instead of five bordered cards. */
.adv-group + .adv-group {
  padding-left: var(--tn-sp-7);
  border-left: 1px solid var(--tn-border-subtle);
}

.adv-group__title {
  margin: 0 0 var(--tn-sp-4);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-bold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

/* Two columns per group keeps the whole advanced area about three rows
   tall, so the master grid still owns the screen with filters expanded. */
.adv-group__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--tn-sp-4) var(--tn-sp-5);
}

.adv-field {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-2);
  min-width: 0;
}

/* The same eyebrow .filter-field__label uses. These two sit in one bar. */
.adv-field__label {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.adv-field__control {
  width: 100%;
  height: var(--tn-h-input);
  padding: 0 var(--tn-sp-4);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-md);
}

.adv-field__control:hover {
  border-color: var(--tn-input-border-hover);
}

.adv-field__control:focus {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
  outline: none;
}

.adv-check {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-4);
  min-height: 34px;
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  cursor: pointer;
}

/* toolbar toggle button in its pressed state */
.btn-tn--tool.is-active {
  background: var(--tn-accent-soft);
  border-color: var(--tn-accent-border);
  color: var(--tn-accent);
}

/* ==========================================================================
   HOT FIX CONTROL  (behaviour: js/grid-hotfix.js, grid styling: tables.css)

   Injected into the panel header beside Columns, Maximise and the panel
   menu, so it reads as one of that table's controls rather than as a page
   command. It is a real toggle: aria-pressed carries the state, the icon
   fills when it is on, and the flag beside the panel title says the state in
   words — so the mode is never communicated by colour alone.
   ========================================================================== */
.btn-tn--hotfix {
  flex: 0 0 auto;
}

.btn-tn--hotfix:hover {
  background: var(--tn-hotfix-soft-strong);
  border-color: var(--tn-hotfix-border);
  color: var(--tn-hotfix);
}

/* .is-active is added alongside aria-pressed="true", which is the state a
   screen reader reads; this is only what the eye gets. */
.btn-tn--hotfix.is-active {
  background: var(--tn-hotfix-soft-strong);
  border-color: var(--tn-hotfix-border);
  color: var(--tn-hotfix);
  font-weight: var(--tn-fw-semibold);
}

.btn-tn--hotfix:focus-visible {
  outline: none;
  border-color: var(--tn-hotfix);
  box-shadow: 0 0 0 3px var(--tn-hotfix-ring);
}

/* A panel header is a title, a count, a subtitle and its controls, and at a
   split-pane width there is no spare room in it. The flag takes the
   subtitle's place rather than squeezing it: the subtitle is standing
   context ("Contract 6769670 · EUR · PP Price") and the flag is what is
   true right now. It comes straight back when Hot Fix is switched off. */
.panel.is-hotfix .panel__subtitle {
  display: none;
}

/* The mode, in words, on the panel itself. */
.hotfix-flag {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-2);
  flex: 0 0 auto;
  padding: 1px var(--tn-sp-4);
  background: var(--tn-hotfix-soft-strong);
  border: 1px solid var(--tn-hotfix-border);
  border-radius: var(--tn-radius-pill);
  color: var(--tn-hotfix);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
  white-space: nowrap;
}

/* ==========================================================================
   GUIDANCE RING  (behaviour: TN.util.attention, used by js/transfer-list.js)

   "The control you want next is this one." Three seconds, then gone — there
   is no looping state, and the JS removes the class as well as the animation
   ending, so nothing can be left pulsing on the page.

   box-shadow only. The control must not move, must not reflow the toolbar
   around it and must stay clickable throughout, which rules out outline
   offsets, transforms, borders and anything with a layout cost.
   ========================================================================== */
.is-guided {
  position: relative;
  z-index: 1;
  animation: tn-guide 1s var(--tn-ease) 3;
}

@keyframes tn-guide {
  0%   { box-shadow: 0 0 0 0 var(--tn-guide-ring), 0 0 0 0 var(--tn-guide-ring-wide); }
  35%  { box-shadow: 0 0 0 3px var(--tn-guide-ring), 0 0 0 8px var(--tn-guide-ring-wide); }
  100% { box-shadow: 0 0 0 0 var(--tn-guide-ring), 0 0 0 0 var(--tn-guide-ring-wide); }
}

/* Reduced motion keeps the MESSAGE and drops the movement: the same ring,
   held still for the same three seconds. components.css flattens every
   transition-duration under this query but says nothing about animation, so
   the animation is named and switched off explicitly here. */
@media (prefers-reduced-motion: reduce) {
  .is-guided {
    animation: none;
    box-shadow: 0 0 0 3px var(--tn-guide-ring), 0 0 0 8px var(--tn-guide-ring-wide);
  }
}

/* ==========================================================================
   COLUMN CHOOSER
   Rendered inside the shared .popover-menu surface, so radius, elevation,
   border and theming all come from the existing menu language. Only the
   list itself is new.
   ========================================================================== */
.col-chooser {
  width: 268px;
  max-width: 100%;
}

.col-chooser__search {
  position: relative;
  padding: var(--tn-sp-2) var(--tn-sp-2) var(--tn-sp-3);
}

.col-chooser__search .bi {
  position: absolute;
  top: 50%;
  left: var(--tn-sp-5);
  transform: translateY(-60%);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
  pointer-events: none;
}

.col-chooser__input {
  width: 100%;
  height: 34px;
  padding: 0 var(--tn-sp-4) 0 var(--tn-sp-9);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-md);
}

.col-chooser__input:focus {
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
  outline: none;
}

.col-chooser__count {
  padding: 0 var(--tn-sp-4) var(--tn-sp-3);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

/* The list scrolls inside the menu rather than growing it off-screen. */
.col-chooser__list {
  max-height: 280px;
  overflow-y: auto;
  border-top: 1px solid var(--tn-border-subtle);
  border-bottom: 1px solid var(--tn-border-subtle);
}

/* Same surface as the action menu, so the same row rhythm: 34px tall, the
   same 12px inset and the same control-to-label gap. It was 32px with an 8px
   inset, which read as a slightly different component inside the very same
   popover. */
.col-choice {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  min-height: 34px;
  padding: var(--tn-sp-3) var(--tn-sp-5);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  cursor: pointer;
}

.col-choice:hover {
  background: var(--tn-accent-soft);
}

.col-choice__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The declared column type, shown quietly so the list stays scannable. */
.col-choice__type {
  flex: 0 0 auto;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

.col-chooser__empty {
  margin: 0;
  padding: var(--tn-sp-6) var(--tn-sp-4);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
  text-align: center;
}

.col-chooser__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-sp-4);
  padding: var(--tn-sp-3) var(--tn-sp-2) var(--tn-sp-2);
}

/* The same compact button the tour's footer uses — one dense control
   height across the overlays, rather than a 32px each of them invented. */
.col-chooser__foot .btn-tn {
  height: var(--tn-h-input-dense);
  font-size: var(--tn-fs-sm);
}

/* A disabled item in any shared menu (e.g. Sort ascending when already
   sorted ascending) stays readable but is clearly inert. */
.menu-item.is-disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.menu-item.is-disabled:hover {
  background: none;
  color: inherit;
}

/* On a phone the chooser is a sheet, so the list can use the height. */
.popover-menu.is-sheet .col-chooser {
  width: 100%;
}

.popover-menu.is-sheet .col-chooser__list {
  max-height: 46vh;
}

/* A quiet section label inside a long enterprise row menu. Not focusable,
   not an item — it only groups what follows. */
.menu-group {
  padding: var(--tn-sp-4) var(--tn-sp-4) var(--tn-sp-2);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-bold);
  letter-spacing: var(--tn-tracking-caps);
  text-transform: uppercase;
}

/* A reservation menu is long by design, so it scrolls inside itself rather
   than running off the viewport. */
.action-menu {
  max-height: calc(100vh - 24px);
  overflow-y: auto;
}

/* ==========================================================================
   19. TOASTS
   ========================================================================== */
.toast-stack {
  position: fixed;
  bottom: var(--tn-sp-5);
  right: var(--tn-sp-6);
  z-index: var(--tn-z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-4);
  width: 340px;
  max-width: calc(100vw - var(--tn-sp-9));
  pointer-events: none;
}

.toast-tn {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-sp-4);
  padding: var(--tn-sp-5);
  background: var(--tn-surface-overlay);
  border: 1px solid var(--tn-border);
  border-left: 3px solid var(--tn-accent);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-md);
  animation: tn-toast-in var(--tn-dur-base) var(--tn-ease);

  /* Click-through: a toast sits over the bottom-right of the workspace,
     which is exactly where a panel's sticky action column lives. Only the
     dismiss button takes pointer events, so a toast can never block a
     control underneath it. */
  pointer-events: none;
}

.toast-tn--success { border-left-color: var(--tn-success); }
.toast-tn--warning { border-left-color: var(--tn-warning); }
.toast-tn--danger  { border-left-color: var(--tn-danger); }

.toast-tn__icon {
  flex: 0 0 auto;
  color: var(--tn-accent);
  font-size: var(--tn-fs-lg);
  line-height: 1.2;
}

.toast-tn--success .toast-tn__icon { color: var(--tn-success); }
.toast-tn--warning .toast-tn__icon { color: var(--tn-warning); }
.toast-tn--danger  .toast-tn__icon { color: var(--tn-danger); }

.toast-tn__body {
  flex: 1 1 auto;
  min-width: 0;
}

.toast-tn__title {
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
}

.toast-tn__text {
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-sm);
}


@keyframes tn-toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ==========================================================================
   20. DIALOGS  (behaviour: js/dialog.js)

   One shell, five archetypes. Everything in this section is shared: the
   dialogs in the prototype differ by the SPEC they are opened with — a size,
   a layout, a set of sections — and never by a stylesheet of their own.

     [data-size]     sm  md  lg  xl  full     how wide it may grow
     [data-layout]   form  split  editor  plain
     [data-variant]  tool                     a supporting palette

   Density is the point. These replace Win32 forms with thirty fields on
   them, and a design that turns thirty fields into three screens of
   scrolling has not modernised the screen, it has made it slower.
   ========================================================================== */

.dlg-layer {
  position: relative;
  z-index: var(--tn-z-modal);
}

.dlg {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--tn-sp-8);
}

.dlg__scrim {
  position: absolute;
  inset: 0;
  background: var(--tn-dlg-scrim);
}

.dlg__box {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-height: 100%;
  background: var(--tn-dlg-bg);
  border: 1px solid var(--tn-dlg-border);
  border-radius: var(--tn-radius-lg);
  box-shadow: var(--tn-dlg-shadow);
  overflow: hidden;
}

/* Sizes, not widths: a dialog asks for room and takes what the viewport can
   spare. min() rather than a media query, because the answer is the same at
   every breakpoint — as wide as it asked for, or all of it. */
.dlg[data-size="sm"]   .dlg__box { width: min(460px, 100%); }
.dlg[data-size="md"]   .dlg__box { width: min(760px, 100%); }
.dlg[data-size="lg"]   .dlg__box { width: min(1040px, 100%); }
.dlg[data-size="xl"]   .dlg__box { width: min(1320px, 100%); }
.dlg[data-size="full"] .dlg__box { width: min(1560px, 100%); height: 100%; }

/* --------------------------------------------------------------------------
   The head: identity on the left, commands on the right.

   It is a toolbar rather than a title bar with a button in it — the legacy
   dialogs put Save, Save+, Delete and Close along the top and that is where
   an operator reaches for them. Sticky by construction: the head and the
   foot are flex-fixed and only the body scrolls.
   -------------------------------------------------------------------------- */
.dlg__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-sp-6);
  flex: 0 0 auto;
  min-height: 52px;
  padding: var(--tn-sp-4) var(--tn-sp-5) var(--tn-sp-4) var(--tn-sp-7);
  background: var(--tn-dlg-head-bg);
  border-bottom: 1px solid var(--tn-dlg-border);
}

.dlg__id {
  display: flex;
  align-items: baseline;
  gap: var(--tn-sp-4);
  flex: 1 1 auto;
  min-width: 0;
}

/* 16px, not 18. A dialog title names a record; it is not a page heading, and
   at 18 it started competing with the page title behind the scrim. */
.dlg__title {
  margin: 0;
  flex: 0 0 auto;
  color: var(--tn-text);
  font-size: var(--tn-fs-lg);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: -0.005em;
  white-space: nowrap;
}

.dlg__status {
  flex: 0 0 auto;
}

.dlg__sub {
  margin: 0;
  min-width: 0;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ==========================================================================
   THE COMMAND REGION  —  ONE component, two surfaces

   js/commands.js renders this, and it renders it identically whether it is
   going into a dialog head or into the page header of a full-page entity
   editor. Save is one component; these are the rules that make it one.

   .dlg__tools stays on the dialog's own instance as a layout hook — the
   dialog head recomposes to two rows on a phone and a page header does not
   — but nothing about the BUTTONS may be stated there. It moved here
   because a 13px label in a dialog beside a 14px label on a page is exactly
   the drift this component exists to prevent.
   ========================================================================== */
.cmd-region {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--tn-sp-2);
  flex: 0 1 auto;
  flex-wrap: wrap;
}

/* THE CANONICAL COMMAND BUTTON.

   Keyed on .js-cmd — the class js/commands.js puts on every command it
   renders — and NOT on the container. A canonical command is the same
   button wherever it is put: in a dialog head, in a page header, or on its
   own in a page toolbar beside a dirty flag, which is where the transfer
   lists' "Save Changes" lives. Keyed on the container instead, that last
   one came out at 14px beside every other Save in the application at 13px.

   .btn-tn.js-cmd beats both the .btn-tn base and .btn-tn--tool on
   specificity, so a command's metrics never depend on its variant. */
.btn-tn.js-cmd {
  height: var(--tn-h-command);
  padding: 0 var(--tn-sp-5);
  font-size: var(--tn-fs-sm);
}

/* The lifecycle slot. It collapses to nothing on a surface that has no
   lifecycle state, which is what keeps the remaining commands in the same
   order rather than in the same pixels. */
.cmd-region__state {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}

.cmd-region__sep {
  width: 1px;
  height: 20px;
  margin: 0 var(--tn-sp-2);
  background: var(--tn-border-strong);
}

/* A page header's command region hugs the right edge of the header, which
   is where .page-header__right already puts it; it needs no width of its
   own and must not stretch. */
.cmd-region--page {
  flex: 0 0 auto;
}

/* Destructive, and quiet until it is wanted: a Delete that shouts from the
   top of every edit dialog is a Delete that eventually gets pressed. */
.btn-tn--danger-quiet {
  background: transparent;
  border-color: transparent;
  color: var(--tn-danger);
}

.btn-tn--danger-quiet:hover {
  background: var(--tn-danger-soft);
  border-color: var(--tn-danger-border);
  color: var(--tn-danger);
}

.dlg__close {
  flex: 0 0 auto;
}

/* --------------------------------------------------------------------------
   Tabs inside a dialog

   The same family as the application's section tabs — an underline, an
   accent, one weight step — a size down, because they sit inside a surface
   rather than across the top of a screen.
   -------------------------------------------------------------------------- */
.dlg__tabs {
  display: flex;
  align-items: stretch;
  gap: var(--tn-sp-1);
  flex: 0 0 auto;
  /* the same 20px .dlg__head, .dlg__body and .dlg__foot are inset by, so
     the tab strip's first tab starts on the title's own left edge */
  padding: 0 var(--tn-sp-7);
  background: var(--tn-dlg-head-bg);
  border-bottom: 1px solid var(--tn-dlg-border);
  overflow-x: auto;
  scrollbar-width: none;
}

.dlg__tabs::-webkit-scrollbar {
  display: none;
}

.dlg-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-sp-3);
  flex: 0 0 auto;
  height: 38px;
  padding: 0 var(--tn-sp-5);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--tn-text-secondary);
  font-family: inherit;
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--tn-dur-fast) var(--tn-ease),
              border-color var(--tn-dur-fast) var(--tn-ease);
}

.dlg-tab:hover {
  color: var(--tn-text);
}

.dlg-tab:focus-visible {
  outline: none;
  box-shadow: inset var(--tn-focus-ring);
}

.dlg-tab.is-active {
  border-bottom-color: var(--tn-accent);
  color: var(--tn-accent);
  font-weight: var(--tn-fw-semibold);
}

/* --------------------------------------------------------------------------
   Body
   -------------------------------------------------------------------------- */
.dlg__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--tn-sp-6) var(--tn-sp-7);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.dlg[data-layout="editor"] .dlg__body {
  display: flex;
  flex-direction: column;
  padding: 0;
}

/* The panel is a real tab stop — js/dialog.js gives it tabindex so a
   keyboard user lands in the section they just switched to. Removing the
   outline and putting nothing back left that stop invisible. The shared
   focus ring, drawn inside so it cannot be clipped by the scroller. */
.dlg__panel:focus-visible {
  outline: none;
  box-shadow: inset var(--tn-focus-ring);
}

/* Main + aside. The aside is the configuration half of the legacy forms —
   flags, currencies, inventory — and it is narrow on purpose: it holds short
   controls, and giving it half the dialog would leave the main column too
   thin for the names and addresses it carries. */
.dlg__split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 320px);
  gap: var(--tn-sp-7);
  align-items: start;
}

.dlg__aside {
  min-width: 0;
  padding-left: var(--tn-sp-7);
  border-left: 1px solid var(--tn-border);
}

.dlg__main {
  min-width: 0;
}

/* --------------------------------------------------------------------------
   Foot — only where a dialog has something to say or a decision to take.
   Most edit dialogs put their commands in the head instead.
   -------------------------------------------------------------------------- */
.dlg__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-sp-5);
  flex: 0 0 auto;
  padding: var(--tn-sp-4) var(--tn-sp-7);
  background: var(--tn-dlg-foot-bg);
  border-top: 1px solid var(--tn-dlg-border);
}

.dlg__foot-note {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

.dlg__foot-actions {
  display: flex;
  gap: var(--tn-sp-4);
}

.dlg__foot-actions .btn-tn {
  height: var(--tn-h-input);
  padding: 0 var(--tn-sp-6);
}

/* --------------------------------------------------------------------------
   The supporting palette

   A tool window, not a modal: no scrim, parked against the bottom-right, and
   deliberately not focus-trapped — it exists to be dipped into and left, and
   trapping it would strand the caret away from the editor it serves.
   -------------------------------------------------------------------------- */
.dlg[data-variant="tool"] {
  inset: auto var(--tn-sp-8) var(--tn-sp-8) auto;
  align-items: flex-end;
  justify-content: flex-end;
  padding: 0;
  pointer-events: none;
}

.dlg[data-variant="tool"] .dlg__box {
  width: 300px;
  max-height: min(60vh, 520px);
  box-shadow: var(--tn-dlg-tool-shadow);
  pointer-events: auto;
}

.dlg[data-variant="tool"] .dlg__head {
  min-height: 44px;
  padding-left: var(--tn-sp-6);
}

.dlg[data-variant="tool"] .dlg__title {
  font-size: var(--tn-fs-md);
}

.dlg[data-variant="tool"] .dlg__body {
  padding: var(--tn-sp-4);
}

/* ==========================================================================
   21. THE DIALOG FORM LANGUAGE

   Two grids. Sections sit on twelve columns and declare how many they take;
   the fields inside a section sit on that section's own 1-4 column grid and
   declare the same. Between them they produce every layout in the legacy
   screens without a single dialog owning a layout.
   ========================================================================== */
.form-tn__sections {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--tn-sp-6) var(--tn-sp-7);
  align-items: start;
}

.form-tn__section[data-span="3"]  { grid-column: span 3; }
.form-tn__section[data-span="4"]  { grid-column: span 4; }
.form-tn__section[data-span="5"]  { grid-column: span 5; }
.form-tn__section[data-span="6"]  { grid-column: span 6; }
.form-tn__section[data-span="7"]  { grid-column: span 7; }
.form-tn__section[data-span="8"]  { grid-column: span 8; }
.form-tn__section[data-span="9"]  { grid-column: span 9; }
.form-tn__section[data-span="12"] { grid-column: span 12; }

/* The section itself carries no chrome by default. A form of thirty fields
   wrapped in ten cards is ten borders competing with the data; the legend
   and the space around it are enough hierarchy, and `boxed` is there for the
   two or three groups that genuinely are a sub-object. */
.form-tn__section {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.form-tn__section--boxed {
  padding: var(--tn-sp-5) var(--tn-sp-6) var(--tn-sp-6);
  background: var(--tn-dlg-inset-bg);
  border: 1px solid var(--tn-dlg-inset-border);
  border-radius: var(--tn-radius-md);
}

.form-tn__section--boxed > .form-tn__legend {
  margin-bottom: var(--tn-sp-4);
  padding-bottom: 0;
  border-bottom: 0;
}

.form-tn__section--plain > .form-tn__legend {
  padding-bottom: 0;
  border-bottom: 0;
}

.form-tn__legend {
  display: flex;
  align-items: baseline;
  gap: var(--tn-sp-4);
}

.form-tn__legend-note {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  font-weight: var(--tn-fw-regular);
  letter-spacing: normal;
  text-transform: none;
}

/* The field grid. Two columns by default, because that is what a dense
   operational form wants; three and four for short controls, one for a
   column that is already narrow. */
.form-tn__grid {
  gap: var(--tn-sp-4) var(--tn-sp-5);
}

.form-tn__grid[data-cols="1"] { grid-template-columns: minmax(0, 1fr); }
.form-tn__grid[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.form-tn__grid[data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.form-tn__grid[data-cols="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* --------------------------------------------------------------------------
   A field

   Label above control, both compact. The label is 12px and quiet: in a form
   of thirty fields the labels are the noise and the values are the signal,
   and a 15px label makes the form twice as tall for nothing.
   -------------------------------------------------------------------------- */
.field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.field.is-span-2 { grid-column: span 2; }
.field.is-span-3 { grid-column: span 3; }
.field.is-span-4 { grid-column: span 4; }

.dlg .form-tn__label {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  font-weight: var(--tn-fw-medium);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dlg .form-tn__control {
  height: var(--tn-h-input-dense);
  padding: 0 var(--tn-sp-4);
  font-size: var(--tn-fs-md);
}

.dlg .form-tn__control--area {
  height: auto;
  padding: var(--tn-sp-3) var(--tn-sp-4);
  line-height: var(--tn-lh-base);
  resize: vertical;
}

.dlg .form-tn__control[readonly] {
  background: var(--tn-input-bg-disabled);
  color: var(--tn-text-secondary);
}

/* A value that is displayed rather than edited — a generated code, a
   computed total. It occupies the same slot as a control so a row of fields
   stays a row. */
.form-tn__static {
  display: flex;
  align-items: center;
  height: var(--tn-h-input-dense);
  padding: 0 var(--tn-sp-4);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border-subtle);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-variant-numeric: tabular-nums;
}

.dlg .form-tn__hint,
.dlg .form-tn__error {
  font-size: var(--tn-fs-xxs);
}

/* --------------------------------------------------------------------------
   Compound field — a control with something on its trailing edge

   The legacy screens glued a red clear button beside every lookup select and
   put the unit in the label. One pattern instead: the affix and the clear
   sit INSIDE the control's box, so a row of eight lookups is a row of eight
   identical shapes rather than eight controls each a different width.
   -------------------------------------------------------------------------- */
.field-compound {
  position: relative;
  display: flex;
  align-items: center;
  min-width: 0;
}

.field-compound .form-tn__control {
  flex: 1 1 auto;
  min-width: 0;
}

.field-compound__affix {
  position: absolute;
  right: var(--tn-sp-4);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
  pointer-events: none;
}

.field-compound__affix--pre {
  right: auto;
  left: var(--tn-sp-4);
}

/* Room for whatever is sitting on the edge. The modifier is on the wrapper
   (js/dialog.js puts it there), so this is a plain descendant selector and a
   control with nothing on its edge keeps the padding every other control in
   the application has. */
.dlg .field-compound--trailing .form-tn__control {
  padding-right: var(--tn-sp-9);
}

.dlg .field-compound--prefix .form-tn__control {
  padding-left: var(--tn-sp-8);
}

.field-compound__clear {
  position: absolute;
  right: 3px;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
  line-height: 1;
  cursor: pointer;
}

.field-compound__clear:hover {
  background: var(--tn-neutral-soft);
  color: var(--tn-danger);
}

.field-compound__clear:focus-visible {
  outline: 2px solid var(--tn-focus-outline-color);
  outline-offset: -2px;
}

/* A clearable SELECT. Its own caret is switched off and drawn as an icon, so
   the clear and the caret sit beside each other instead of on top of one
   another — which is exactly what a native arrow plus an overlaid button
   looks like, and it happens on about half the lookups in these forms. */
.dlg .field-compound--select .form-tn__control--select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--tn-sp-10);
}

.field-compound--select .field-compound__clear {
  right: 22px;
}

.field-compound__caret {
  position: absolute;
  right: var(--tn-sp-4);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   Range — two controls that mean one thing

   One label, one box, a drawn separator. Repeating the label ("Sale Start
   Date", "Sale End Date") costs a line of vertical space and says the same
   word twice; the relation is what the operator needs to see.
   -------------------------------------------------------------------------- */
.field-range {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-2);
  min-width: 0;
}

.field-range .form-tn__control {
  flex: 1 1 0;
  min-width: 0;
}

.field-range__sep {
  flex: 0 0 auto;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-sm);
}

/* --------------------------------------------------------------------------
   Check clusters

   The seven weekdays three times over is 21 checkboxes in the Price Period
   dialog alone. As full-width rows that is 21 lines; as a grid it is six.
   -------------------------------------------------------------------------- */
.check-grid {
  display: grid;
  gap: var(--tn-sp-2) var(--tn-sp-4);
}

.check-grid[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.check-grid[data-cols="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.check-grid[data-cols="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.check-grid[data-cols="7"] { grid-template-columns: repeat(7, minmax(0, 1fr)); }

.check-grid__item {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  min-height: 24px;
  min-width: 0;
  margin: 0;
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
  white-space: nowrap;
  cursor: pointer;
}

/* The label is what gives way when the column is narrow — never the box.
   See the note above .tn-check. */
.check-grid__item > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.check-grid__item:hover {
  color: var(--tn-text);
}

.check-grid__item > span {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A lone flag sitting in a row of labelled controls lines up with them
   rather than floating at the top of its cell. */
.field--check {
  justify-content: flex-end;
  padding-bottom: var(--tn-sp-2);
}

/* --------------------------------------------------------------------------
   A brand / identity preview beside operational data

   Small on purpose. The legacy Agency screen showed a customer logo next to
   the invoice fields; that is a reference, not a profile page, so it is the
   size of two form rows and no larger.
   -------------------------------------------------------------------------- */
.entity-mark {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-3);
  padding: var(--tn-sp-5);
  background: var(--tn-surface-sunken);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
}

.entity-mark__frame {
  display: grid;
  place-items: center;
  height: 72px;
  background: var(--tn-surface);
  border: 1px solid var(--tn-border-subtle);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-accent);
  font-size: var(--tn-fs-xl);
  font-weight: var(--tn-fw-bold);
  letter-spacing: 0.04em;
}

.entity-mark__name {
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.entity-mark__meta {
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

/* ==========================================================================
   22. THE E-MAIL TEMPLATE EDITOR

   Not another data-entry dialog: a composition surface with metadata above
   it. The sheet is paper in both themes, because that is the ground the mail
   will be read on; everything around it is the application's own chrome.
   ========================================================================== */
/* --tn-border, not --tn-dlg-border: the editor is a PANEL on a page now,
   not a band inside a dialog shell, so its rules belong to the same family
   as .panel__head. The two tokens are one colour in Light, which is why the
   difference only ever showed in Dark. */
.editor-meta {
  flex: 0 0 auto;
  padding: var(--tn-sp-5) var(--tn-sp-7);
  border-bottom: 1px solid var(--tn-border);
}

.editor-bar {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-2);
  flex: 0 0 auto;
  flex-wrap: wrap;
  padding: var(--tn-sp-3) var(--tn-sp-6);
  background: var(--tn-editor-bar-bg);
  border-bottom: 1px solid var(--tn-border);
}

.editor-bar__group {
  display: flex;
  align-items: center;
  gap: 1px;
}

.editor-bar__sep {
  width: 1px;
  height: 18px;
  margin: 0 var(--tn-sp-3);
  background: var(--tn-border-strong);
}

.editor-bar__spacer {
  flex: 1 1 auto;
}

/* The compact end of the shared control scale rather than a size of its
   own: the same dense metric every dialog field uses, so the toolbar
   lines up with the controls beside it instead of near them. */
.editor-btn {
  display: grid;
  place-items: center;
  width: var(--tn-h-input-dense);
  height: var(--tn-h-input-dense);
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
  cursor: pointer;
}

.editor-btn:hover {
  background: var(--tn-surface-sunken);
  color: var(--tn-text);
}

.editor-btn:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.editor-btn.is-active {
  background: var(--tn-accent-soft);
  border-color: var(--tn-accent-border);
  color: var(--tn-accent);
}

.editor-select {
  height: 28px;
  padding: 0 var(--tn-sp-3);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-family: inherit;
  font-size: var(--tn-fs-sm);
}

.editor-select:focus {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

/* --------------------------------------------------------------------------
   THE CANVAS AND THE DOCUMENT

   The canvas is the ground the document sits on, and its only job is to be
   a gutter wide enough to say "this white rectangle is the thing you are
   editing". It was 20px of gutter around a 760px document in a lane
   1330px wide at 1920x1080 — 57% of the lane used, the rest grey. That
   reads as a preview of a letter rather than the surface you write one on.

   It is a column flex container now, so the document can be told to FILL
   it — see .editor-sheet below. `align-items: center` keeps the document
   centred without margin:auto fighting the flex sizing.
   -------------------------------------------------------------------------- */
.editor-canvas {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--tn-sp-6) var(--tn-sp-5);
  background: var(--tn-surface-sunken);
  overflow-y: auto;
}

/* --------------------------------------------------------------------------
   DROP FEEDBACK

   Two states and no more:

     is-drop-ready    a field has been picked up somewhere on the page, and
                      this is where it can go. An inset hairline in the
                      accent, which is the colour this application already
                      uses for "the thing you are acting on".
     is-drop-target   the pointer is actually over the canvas. The same
                      hairline, doubled, over the accent's soft fill.

   No lift, no bounce, no dashed marching border, no colour that is not
   already in the theme. The operator is reading a letter; the editor's job
   during a drag is to say "yes, here" and get out of the way.
   -------------------------------------------------------------------------- */
.editor-canvas.is-drop-ready {
  box-shadow: inset 0 0 0 1px var(--tn-accent-border);
}

.editor-canvas.is-drop-target {
  background: var(--tn-accent-soft);
  box-shadow: inset 0 0 0 2px var(--tn-accent);
}

.editor-canvas.is-drop-target .editor-sheet {
  border-color: var(--tn-accent-border);
}

/* THE DOCUMENT. The primary working surface of this screen, and sized like
   one.

   WIDTH — two rules, and the larger wins:

     max(760px, 78%)   78% of the canvas on a wide lane, so at 1920x1080 the
                       document is ~1010px of a 1330px lane instead of 760.
                       760px is the floor, so no width that already worked
                       gets smaller: at 1440x900 the lane is 856 and 78% of
                       it would be 643, so the floor keeps the document at
                       760 — 89% of the lane.

     min(100%, 1100px) never wider than the lane, and never wider than a
                       line anybody wants to read. Past roughly 1410px of
                       lane the cap takes over and the percentage stops
                       rising; that is the deliberate end of the trade
                       between "fill the workspace" and "keep a measure".

   HEIGHT — `flex: 1 0 auto` in a column canvas: fill whatever height the
   canvas has, and grow past it when the letter is longer than the screen,
   at which point the CANVAS scrolls and the toolbar and metadata above it
   do not move. min-height is the floor for the one case where the canvas
   has no definite height of its own — the phone, where the whole editor
   body scrolls instead (responsive.css). */
.editor-sheet {
  width: max(760px, 78%);
  max-width: min(100%, 1100px);
  flex: 1 0 auto;
  min-height: 420px;
  padding: var(--tn-sp-8);
  background: var(--tn-editor-bg);
  border: 1px solid var(--tn-editor-border);
  border-radius: var(--tn-radius-sm);
  box-shadow: var(--tn-shadow-sm);
  color: var(--tn-editor-text);
  font-size: var(--tn-fs-base);
  line-height: var(--tn-lh-base);
}

.editor-sheet:focus-visible {
  outline: 2px solid var(--tn-focus-outline-color);
  outline-offset: 2px;
}

.editor-sheet p {
  margin: 0 0 var(--tn-sp-5);
}

.editor-sheet h1,
.editor-sheet h2,
.editor-sheet h3 {
  margin: 0 0 var(--tn-sp-5);
  color: var(--tn-editor-text);
}

.editor-sheet ul,
.editor-sheet ol {
  margin: 0 0 var(--tn-sp-5);
  padding-left: var(--tn-sp-8);
}

/* A merge field, rendered as one object rather than as eight characters of
   punctuation — an operator has to be able to see at a glance what in this
   letter is text and what is going to be replaced when it is sent. */
.mail-token {
  display: inline-block;
  padding: 0 var(--tn-sp-3);
  background: var(--tn-editor-token-bg);
  border: 1px solid var(--tn-editor-token-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-editor-token-text);
  font-family: var(--tn-font-mono);
  font-size: var(--tn-fs-xs);
  white-space: nowrap;
}

/* The source view: the same content, as markup — and therefore the same
   surface, at the same size. A source view that was half the width of the
   document it edits would be a second decision about the same thing. */
.editor-source {
  display: block;
  width: max(760px, 78%);
  max-width: min(100%, 1100px);
  flex: 1 0 auto;
  min-height: 420px;
  padding: var(--tn-sp-6);
  background: var(--tn-input-bg);
  border: 1px solid var(--tn-input-border);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-input-text);
  font-family: var(--tn-font-mono);
  font-size: var(--tn-fs-sm);
  line-height: var(--tn-lh-base);
  resize: vertical;
}

/* --------------------------------------------------------------------------
   The merge-field library

   A panel on the left of the E-Mail Template workspace on desktop, the same
   list inside a sheet on a phone — one set of buttons either way, moved
   rather than re-rendered (js/pages/page-mail.js).

   Every item is a BUTTON that is also draggable. That ordering matters: the
   control answers Enter, Space and a pointer first, and drag is the
   accelerator on top of it. A div with a drag handler would be neither.
   -------------------------------------------------------------------------- */
.field-palette {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-3);
}

.field-palette__hint {
  margin: 0 0 var(--tn-sp-4);
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
  line-height: var(--tn-lh-base);
}

.field-palette__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.field-palette__row {
  display: block;
}

.field-palette__item {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-3);
  width: 100%;
  min-height: 34px;
  padding: var(--tn-sp-2) var(--tn-sp-4) var(--tn-sp-2) var(--tn-sp-2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--tn-radius-sm);
  color: var(--tn-text);
  font-family: inherit;
  font-size: var(--tn-fs-md);
  text-align: left;
  cursor: grab;
}

.field-palette__item:hover {
  background: var(--tn-accent-soft);
  border-color: var(--tn-accent-border);
  color: var(--tn-accent);
}

.field-palette__item:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

/* The grip says "this can be picked up" without a word of instruction, and
   it is the only thing on the row that changes when the pointer is over it
   beside the row itself. */
.field-palette__grip {
  flex: 0 0 auto;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-md);
}

.field-palette__item:hover .field-palette__grip {
  color: var(--tn-accent);
}

/* The name over the token, not beside it — see the note in
   js/mail-editor.js. Two lines, one target. */
.field-palette__body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex: 1 1 auto;
  min-width: 0;
}

.field-palette__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: var(--tn-lh-tight);
}

/* The token the field will write. Quiet, monospaced and aria-hidden: it is
   the same information the name already gives, shown so an operator reading
   the document can match a token in the letter to a row in this list. */
.field-palette__token {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--tn-text-muted);
  font-family: var(--tn-font-mono);
  font-size: var(--tn-fs-xxs);
  line-height: var(--tn-lh-tight);
}

/* The dragged item quiets rather than lifting: the thing to look at during
   a drag is where it is going, not where it came from. */
.field-palette__item.is-dragging {
  cursor: grabbing;
  background: var(--tn-surface-sunken);
  border-color: var(--tn-border);
  color: var(--tn-text-muted);
  opacity: 0.65;
}

.field-palette__item.is-dragging .field-palette__grip,
.field-palette__item.is-dragging .field-palette__token {
  color: var(--tn-text-muted);
}

/* ==========================================================================
   23. THE DIALOG PATTERN LIST  (dialogs.html)

   A launcher per archetype, so the system can be reviewed without hunting
   for the grid row that happens to open each one.
   ========================================================================== */
.pattern-list {
  display: flex;
  flex-direction: column;
  gap: var(--tn-sp-2);
  padding: var(--tn-sp-4);
}

.pattern {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-5);
  width: 100%;
  padding: var(--tn-sp-4) var(--tn-sp-5);
  background: transparent;
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  color: var(--tn-text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--tn-dur-fast) var(--tn-ease),
              border-color var(--tn-dur-fast) var(--tn-ease);
}

/* One row is an <a>, because it goes to a page rather than opening a
   dialog — the E-Mail Template workspace. It is the same row either way, so
   the underline the browser puts on a link is the only thing to undo. */
.pattern,
.pattern:hover,
.pattern:focus {
  text-decoration: none;
}

.pattern:hover {
  background: var(--tn-accent-soft);
  border-color: var(--tn-accent-border);
  color: var(--tn-text);
}

.pattern:focus-visible {
  outline: none;
  border-color: var(--tn-accent);
  box-shadow: var(--tn-focus-ring);
}

.pattern__icon {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  background: var(--tn-accent-soft);
  border-radius: var(--tn-radius-sm);
  color: var(--tn-accent);
  font-size: var(--tn-fs-lg);
}

.pattern__body {
  flex: 1 1 auto;
  min-width: 0;
}

.pattern__title {
  display: block;
  color: var(--tn-text);
  font-size: var(--tn-fs-md);
  font-weight: var(--tn-fw-semibold);
}

.pattern__meta {
  display: block;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xs);
}

.pattern__go {
  flex: 0 0 auto;
  color: var(--tn-text-muted);
}

/* ==========================================================================
   24. THE PAGE TOUR  (behaviour: js/tour.js, steps: js/tours.js)

   Generic by construction. Nothing here is named after a screen: the six
   tours in the prototype differ by the areas they NAME, and the difference
   lives in js/tours.js rather than in a stylesheet.

   The spotlight is four fixed panes arranged around the target's rectangle,
   leaving a hole the page shows through at its own natural depth — see the
   note at the top of js/tour.js for why raising the target instead is the
   version that breaks on these screens.
   ========================================================================== */

/* The ? beside the page title. Contextual help, so it is the quiet icon
   button the panel headers already use, not a call to action. */
.page-header__headline {
  display: flex;
  align-items: center;
  gap: var(--tn-sp-2);
  min-width: 0;
}

/* An .icon-btn like every other control in the chrome. It carried its own
   30px until the consistency pass measured it against its peers. */
.page-header__help {
  flex: 0 0 auto;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-lg);
}

.page-header__help:hover {
  background: var(--tn-accent-soft);
  color: var(--tn-accent);
}

.page-header__help[aria-pressed="true"] {
  background: var(--tn-accent-soft);
  color: var(--tn-accent);
}

/* --------------------------------------------------------------------------
   The layer
   -------------------------------------------------------------------------- */
.tour-layer {
  position: fixed;
  inset: 0;
  z-index: var(--tn-z-tour);
  /* The layer itself is only a container: the panes and the bubble take
     their own pointer events, and everything between them is the hole. */
  pointer-events: none;
}

/* One of the four panes around the target. Together they dim the whole
   viewport except the rectangle being described, and together they absorb
   every click aimed at the page underneath. */
.tour-pane {
  position: fixed;
  background: var(--tn-tour-scrim);
  pointer-events: auto;
}

/* Over the hole. Transparent, so the page shows through unchanged, and
   clickable, so it does not: a tour is explanatory, and a click that landed
   on a grid row would change the selection the tour is describing. */
.tour-shield {
  position: fixed;
  background: transparent;
  pointer-events: auto;
  cursor: default;
}

/* The ring on the hole's edge. Drawn OVER the target rather than on it, so
   the panel's own border is never touched and nothing about its size or
   position changes while the tour runs.

   THE GLOW IS INSET, and that is a correction rather than a preference.

   It used to be an outward 4px spread: `0 0 0 4px`. A non-inset shadow is
   painted outside the element's border box, and .tour-spot is the LAST
   child of the layer — so those four pixels landed on top of the dimming
   panes, all the way around the target, in a colour that is neither the
   ring nor the scrim. Along the bottom edge of a panel that reads as
   exactly what it is: a second border, drawn outside the real one, with a
   band of the wrong colour between them.

   Measured on Package SPO / Price Details at 1600x1000, the four rows
   immediately under the panel came out at luminance 56 against a scrim at
   12 in Dark — five times brighter than the surface it sat on, which is
   the "white strip" this fixes. In Light it was the same band, darker than
   its surroundings instead of lighter (130 against 147).

   Inset, the whole spotlight is confined to the target's own rectangle:
   2px of accent at the boundary and 4px of glow inside it. Nothing the
   tour draws extends past the edge of the thing it is describing, on any
   of the four sides. */
.tour-spot {
  position: fixed;
  border: 2px solid var(--tn-tour-ring);
  border-radius: var(--tn-radius-md);
  box-shadow: inset 0 0 0 4px var(--tn-tour-ring-glow);
  pointer-events: none;
  transition: top var(--tn-dur-base) var(--tn-ease),
              left var(--tn-dur-base) var(--tn-ease),
              width var(--tn-dur-base) var(--tn-ease),
              height var(--tn-dur-base) var(--tn-ease);
}

/* --------------------------------------------------------------------------
   The coachmark

   The application's own surface language — the dialog's border, radius and
   elevation at a smaller size. Compact on purpose: it explains one area,
   and a card the size of a dialog would cover the thing it is pointing at.
   -------------------------------------------------------------------------- */
.tour-mark {
  position: fixed;
  width: 316px;
  max-width: calc(100vw - var(--tn-sp-8));
  padding: var(--tn-sp-5) var(--tn-sp-6) var(--tn-sp-5);
  background: var(--tn-tour-mark-bg);
  border: 1px solid var(--tn-tour-mark-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-tour-mark-shadow);
  pointer-events: auto;
}

.tour-mark:focus-visible,
.tour-mark:focus {
  outline: none;
}

.tour-mark__head {
  display: flex;
  align-items: baseline;
  gap: var(--tn-sp-3);
  margin-bottom: var(--tn-sp-3);
}

.tour-mark__title {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  color: var(--tn-text);
  font-size: var(--tn-fs-base);
  font-weight: var(--tn-fw-semibold);
  letter-spacing: -0.005em;
}

/* "Step 2 of 6" in words, not a row of dots: the position in a tour is a
   fact somebody may need read out, and a dot cannot be read out. */
.tour-mark__progress {
  flex: 0 0 auto;
  color: var(--tn-text-muted);
  font-size: var(--tn-fs-xxs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The corner close on an overlay header, the same size as the dialog's.
   26px was below the shared icon-button floor, which also put it under
   the touch minimum on the one surface that becomes a full-width sheet. */
.tour-mark__close {
  flex: 0 0 auto;
  align-self: flex-start;
  margin: -4px -6px 0 0;
  font-size: var(--tn-fs-sm);
}

.tour-mark__body {
  margin: 0 0 var(--tn-sp-5);
  color: var(--tn-text-secondary);
  font-size: var(--tn-fs-md);
  line-height: var(--tn-lh-base);
}

/* A hairline of progress under the message. Decoration on top of the
   sentence above, never instead of it. */
.tour-mark__rail {
  height: 2px;
  margin-bottom: var(--tn-sp-5);
  background: var(--tn-border-subtle);
  border-radius: var(--tn-radius-pill);
  overflow: hidden;
}

.tour-mark__rail-fill {
  display: block;
  height: 100%;
  background: var(--tn-accent);
  border-radius: var(--tn-radius-pill);
  transition: width var(--tn-dur-base) var(--tn-ease);
}

.tour-mark__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-sp-4);
}

/* The compact end of the shared scale, not a number of its own. */
.tour-mark__foot .btn-tn {
  height: var(--tn-h-input-dense);
  padding: 0 var(--tn-sp-5);
  font-size: var(--tn-fs-sm);
}

/* --------------------------------------------------------------------------
   The arrow — a rotated square tucked under the bubble's edge, so the
   bubble keeps one border and the arrow inherits it.
   -------------------------------------------------------------------------- */
.tour-mark__arrow {
  position: absolute;
  width: 10px;
  height: 10px;
  background: var(--tn-tour-mark-bg);
  border: 1px solid var(--tn-tour-mark-border);
  transform: rotate(45deg);
}

.tour-mark[data-placement="right"] .tour-mark__arrow {
  left: -6px;
  margin-top: -5px;
  border-right: 0;
  border-top: 0;
}

.tour-mark[data-placement="left"] .tour-mark__arrow {
  right: -6px;
  margin-top: -5px;
  border-left: 0;
  border-bottom: 0;
}

.tour-mark[data-placement="bottom"] .tour-mark__arrow {
  top: -6px;
  margin-left: -5px;
  border-right: 0;
  border-bottom: 0;
}

.tour-mark[data-placement="top"] .tour-mark__arrow {
  bottom: -6px;
  margin-left: -5px;
  border-left: 0;
  border-top: 0;
}

/* --------------------------------------------------------------------------
   THE SHEET VARIANT

   Below the phone breakpoint a 316px bubble beside a target that is the
   full width of the screen has nowhere to be. The spotlight stays exactly
   as it is — the target is still lit — and the explanation becomes a sheet
   across the width of the screen, where a thumb already is.

   Two of them, because "which edge" is still a placement decision: the
   sheet takes the side of the target with more room, so a target near the
   bottom of a phone screen is explained from above rather than covered.
   js/tour.js chooses; these two rules are the same card at either end.
   -------------------------------------------------------------------------- */
.tour-mark[data-placement="sheet"],
.tour-mark[data-placement="sheet-top"] {
  right: var(--tn-sp-4);
  left: var(--tn-sp-4);
  width: auto;
  max-width: none;
  border-radius: var(--tn-radius-lg);
}

.tour-mark[data-placement="sheet"] {
  top: auto;
  bottom: var(--tn-sp-4);
}

.tour-mark[data-placement="sheet-top"] {
  top: var(--tn-sp-4);
  bottom: auto;
}

.tour-mark[data-placement="sheet"] .tour-mark__arrow,
.tour-mark[data-placement="sheet-top"] .tour-mark__arrow {
  display: none;
}

/* --------------------------------------------------------------------------
   While a tour is running

   Hot Fix's amber ring and the tour spotlight are two things claiming the
   same panel edge. The tour wins for as long as it is on screen, and the
   mode's own styling comes straight back on exit — the MODE is never
   switched off, only quieted. See js/tour.js.
   -------------------------------------------------------------------------- */
body.is-touring .panel.is-hotfix {
  box-shadow: var(--tn-shadow-xs);
  border-color: var(--tn-hotfix-border);
}

/* Motion is one short transition on the ring and one on the bubble. No
   bouncing arrow, no pulse, nothing that runs forever. */
.tour-mark {
  animation: tn-tour-in var(--tn-dur-base) var(--tn-ease);
}

@keyframes tn-tour-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .tour-mark {
    animation: none;
  }

  .tour-spot,
  .tour-mark__rail-fill {
    transition: none;
  }
}
