/*
 * TeamNest — App layer
 * ------------------------------------------------------------------
 * A thin layer ON TOP of Bootstrap 5 that consumes tokens.css.
 * Keep this small: prefer Bootstrap utilities in templates and only
 * style the app chrome (shell layout, sidebar, topbar, cards) here.
 */

/* ---- Base ------------------------------------------------------- */
html,
body {
  height: 100%;
}

body {
  margin: 0;
  background-color: var(--tn-bg);
  color: var(--tn-text);
  font-family: var(--tn-font-sans);
  font-size: var(--tn-body-size);
  line-height: var(--tn-body-leading);
  font-weight: var(--tn-body-weight);
  letter-spacing: var(--tn-body-tracking);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a {
  color: var(--tn-primary);
  text-decoration: none;
}
a:hover {
  color: var(--tn-primary-700);
}

/* ---- Typography: levels, not sizes ------------------------------
   Loaded after bootstrap.min.css (base.html links it second) and at the
   same specificity, so these win on source order. That is the whole
   mechanism: redefining .h1-.h6 reclaims the ~344 existing uses across
   the templates without editing a single one of them.

   The split below is what enforces "an <h1> is the page title whatever
   size class it carries":

     * the ELEMENT rules carry the level, including colour and margin;
     * the SIZE CLASSES carry only the four bundle properties.

   So an h1 with a size class is still a page title set at that size, and
   an h2 with one is still a card heading. Roughly 128 templates had
   reached for a smaller size class purely to escape Bootstrap's 40px
   default, which left page titles at three different sizes across the
   product; they now all land on the ramp. No !important, no cascade
   tricks.

   Do not rewrite these with :where() -- Bootstrap's bare element selector
   is (0,0,1) and :where() is (0,0,0), so Bootstrap would win and nothing
   here would apply. */
h1, h2, h3, h4, h5, h6 {
  color: var(--tn-text);
  margin-top: 0;
}
h1 { font-size: var(--tn-h1-size); line-height: var(--tn-h1-leading); font-weight: var(--tn-h1-weight); letter-spacing: var(--tn-h1-tracking); }
h2 { font-size: var(--tn-h2-size); line-height: var(--tn-h2-leading); font-weight: var(--tn-h2-weight); letter-spacing: var(--tn-h2-tracking); }
h3 { font-size: var(--tn-h3-size); line-height: var(--tn-h3-leading); font-weight: var(--tn-h3-weight); letter-spacing: var(--tn-h3-tracking); }
h4 { font-size: var(--tn-h4-size); line-height: var(--tn-h4-leading); font-weight: var(--tn-h4-weight); letter-spacing: var(--tn-h4-tracking); }
h5 { font-size: var(--tn-h5-size); line-height: var(--tn-h5-leading); font-weight: var(--tn-h5-weight); letter-spacing: var(--tn-h5-tracking); }
h6 { font-size: var(--tn-h6-size); line-height: var(--tn-h6-leading); font-weight: var(--tn-h6-weight); letter-spacing: var(--tn-h6-tracking); }

/* Size classes: the four bundle properties ONLY. Never colour, never
   margin -- that is what stops a size class overriding a level. */
.h1 { font-size: var(--tn-h1-size); line-height: var(--tn-h1-leading); font-weight: var(--tn-h1-weight); letter-spacing: var(--tn-h1-tracking); }
.h2 { font-size: var(--tn-h2-size); line-height: var(--tn-h2-leading); font-weight: var(--tn-h2-weight); letter-spacing: var(--tn-h2-tracking); }
.h3 { font-size: var(--tn-h3-size); line-height: var(--tn-h3-leading); font-weight: var(--tn-h3-weight); letter-spacing: var(--tn-h3-tracking); }
.h4 { font-size: var(--tn-h4-size); line-height: var(--tn-h4-leading); font-weight: var(--tn-h4-weight); letter-spacing: var(--tn-h4-tracking); }
.h5 { font-size: var(--tn-h5-size); line-height: var(--tn-h5-leading); font-weight: var(--tn-h5-weight); letter-spacing: var(--tn-h5-tracking); }
.h6 { font-size: var(--tn-h6-size); line-height: var(--tn-h6-leading); font-weight: var(--tn-h6-weight); letter-spacing: var(--tn-h6-tracking); }

/* The remaining levels have no HTML element to hang off. */
.tn-display {
  font-size: var(--tn-display-size); line-height: var(--tn-display-leading);
  font-weight: var(--tn-display-weight); letter-spacing: var(--tn-display-tracking);
}
.tn-body-sm {
  font-size: var(--tn-body-sm-size); line-height: var(--tn-body-sm-leading);
  font-weight: var(--tn-body-sm-weight);
}
.tn-caption {
  font-size: var(--tn-caption-size); line-height: var(--tn-caption-leading);
  font-weight: var(--tn-caption-weight); color: var(--tn-text-muted);
}
.tn-overline {
  font-size: var(--tn-overline-size); line-height: var(--tn-overline-leading);
  font-weight: var(--tn-overline-weight); letter-spacing: var(--tn-overline-tracking);
  text-transform: uppercase; color: var(--tn-text-muted);
}

/* The display face is spent at most once per screen. */
.tn-display-serif { font-family: var(--tn-font-display); font-weight: 400; }

/* Geist's default figures are proportional, so anything that lines up in
   a column needs this or a stat tile jitters as its number changes. */
.tn-num,
.tn-stat,
.tn-mono,
.tn-table td,
.tn-table th {
  font-variant-numeric: tabular-nums;
}
.tn-mono {
  font-family: var(--tn-font-mono);
  font-size: var(--tn-mono-size);
  line-height: var(--tn-mono-leading);
}

/* ---- App shell layout ------------------------------------------ */
/* Grid: sidebar column + main column. Sidebar collapses under md.   */
.tn-shell {
  display: grid;
  grid-template-columns: var(--tn-sidebar-width) 1fr;
  grid-template-rows: var(--tn-topbar-height) 1fr;
  grid-template-areas:
    "sidebar topbar"
    "sidebar main";
  min-height: 100vh;
  /* Animates the collapse to the icon rail; both tracks are lengths, so the
     interpolation is well defined. Muted by prefers-reduced-motion below. */
  transition: grid-template-columns var(--tn-transition);
}

.tn-topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  gap: var(--tn-space-3);
  height: var(--tn-topbar-height);
  padding: 0 var(--tn-space-5);
  background: var(--tn-topbar-bg);
  border-bottom: 1px solid var(--tn-border);
  position: sticky;
  top: 0;
  z-index: 1030;
}

.tn-sidebar {
  grid-area: sidebar;
  display: flex;
  flex-direction: column;
  background: var(--tn-sidebar-bg);
  color: var(--tn-sidebar-text);
  width: var(--tn-sidebar-width);
  padding: var(--tn-space-4) var(--tn-space-3);
  /* Its own scroll box, pinned to the viewport. Without the height cap the
     column stretched to the page, the DOCUMENT scrolled instead, and every
     click (a full page load) put the reader back at the top of the menu.
     app.js carries scrollTop across loads; _sidebar.html restores it. */
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  transition: transform var(--tn-transition), width var(--tn-transition);
  z-index: 1040;
}

.tn-main {
  grid-area: main;
  padding: var(--tn-space-6);
  overflow-x: hidden;
}

/* ---- Brand ------------------------------------------------------ */
.tn-brand {
  display: flex;
  align-items: center;
  gap: var(--tn-space-2);
  color: var(--tn-sidebar-ink-strong);
  font-weight: 700;
  font-size: var(--tn-text-lg);
  padding: var(--tn-space-2) var(--tn-space-2) var(--tn-space-4);
  letter-spacing: -0.01em;
}
.tn-brand-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--tn-radius);
  background: linear-gradient(135deg, var(--tn-primary-400), var(--tn-primary-700));
  color: var(--tn-primary-contrast);
  font-weight: 800;
  flex: 0 0 auto;
}
.tn-brand-mark svg { display: block; }
/* The lockup has a light and a dark colourway; show the one for the theme. */
.tn-logo-dark { display: none !important; }
[data-theme="dark"] .tn-logo-light { display: none !important; }
[data-theme="dark"] .tn-logo-dark { display: block !important; }

/* ---- Skip link -------------------------------------------------- */
/* Off screen until focused rather than display:none, which would take it out
   of the tab order and defeat the point. */
.tn-skip-link {
  position: absolute;
  left: var(--tn-space-2);
  top: -100%;
  z-index: 1100;
  padding: var(--tn-space-2) var(--tn-space-3);
  border-radius: var(--tn-radius);
  background: var(--tn-primary);
  color: var(--tn-primary-contrast);
  font-size: var(--tn-text-sm);
  font-weight: 600;
}
.tn-skip-link:focus-visible,
.tn-skip-link:focus {
  top: var(--tn-space-2);
}

/* ---- Sidebar navigation ---------------------------------------- */
.tn-nav {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* A section heading. It is now a <summary>, so it IS the section's toggle —
   the same uppercase run it has always been, plus a pointer and a marker. */
.tn-nav-section {
  margin-top: var(--tn-space-4);
  padding: 0 var(--tn-space-2);
  font-size: var(--tn-overline-size);
  font-weight: var(--tn-overline-weight);
  line-height: var(--tn-overline-leading);
  text-transform: uppercase;
  letter-spacing: var(--tn-overline-tracking);
  color: var(--tn-sidebar-ink-muted);
}
/* The tenant rail's top-level headings (Me, People, ...) sit on a soft band
   with a brand edge, so the first level is findable at a glance in a long
   tree. Scoped to the <summary> of a section <details>: the bare
   .tn-nav-section class is also used by /ops/, the attention page and Nestor,
   which keep the plain overline.

   Every colour is a sidebar ROLE token, never a literal, so the band follows
   the theme AND a tenant-repainted rail (core/branding/chrome.py picks the
   hover wash, ink and active ink to suit the rail's luminance):
     band  = --tn-sidebar-hover       neutral 5-6% wash, light or dark
     ink   = --tn-sidebar-ink-strong  near-black / white, well past AA
     edge  = --tn-sidebar-active-text brand ink stepped for the rail's polarity
   The edge is the only brand on it and is decorative; the text does not rely
   on it. The collapsed rail hides every .tn-nav-group-toggle, so none of this
   reaches the icon-only state. Logical properties keep it right in RTL. */
.tn-nav-group--section > .tn-nav-section {
  margin-bottom: var(--tn-space-1);
  padding: var(--tn-space-2) var(--tn-space-3);
  padding-inline-start: calc(var(--tn-space-3) - 3px);
  border-inline-start: 3px solid var(--tn-sidebar-active-text);
  border-radius: var(--tn-radius);
  background: var(--tn-sidebar-hover);
  color: var(--tn-sidebar-ink-strong);
  font-weight: 700;
}

/* ---- The second level ------------------------------------------
   Sections and groups are both <details>. A section renders open and a group
   shut, which looks inconsistent and is not: a section is audience-level
   structure and there are eight of them, so shipping them shut would hide a
   whole audience's product behind an excavation on first load. A group is a
   long tail inside a section the reader has already chosen.

   text-transform is NOT set on the <details>: it would cascade into the item
   labels inside and shout the whole tree. It stays on the summary. */
.tn-nav-section-wrap,
.tn-nav-group-wrap {
  list-style: none;
}
.tn-nav-group-items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* The group's own children are indented so the two levels read as two. */
.tn-nav-group:not(.tn-nav-group--section) > .tn-nav-group-items {
  margin-inline-start: var(--tn-space-3);
  padding-inline-start: var(--tn-space-2);
  border-inline-start: 1px solid var(--tn-sidebar-border);
}
.tn-nav-group-toggle {
  display: flex;
  align-items: center;
  gap: var(--tn-space-3);
  cursor: pointer;
  user-select: none;
}
/* The default disclosure triangle is replaced by .tn-nav-chevron, which sits
   in the icon column so a group toggle lines up with the links around it. */
.tn-nav-group-toggle::-webkit-details-marker {
  display: none;
}
.tn-nav-group-toggle {
  list-style: none;
}
.tn-nav-group-toggle::marker {
  content: "";
}
.tn-nav-group:not(.tn-nav-group--section) > .tn-nav-group-toggle {
  padding: var(--tn-space-2) var(--tn-space-3);
  border-radius: var(--tn-radius);
  color: var(--tn-sidebar-text);
  font-size: var(--tn-text-sm);
  font-weight: 500;
  transition: background var(--tn-transition), color var(--tn-transition);
}
.tn-nav-group:not(.tn-nav-group--section) > .tn-nav-group-toggle:hover {
  background: var(--tn-sidebar-hover);
  color: var(--tn-sidebar-ink-strong);
}
.tn-nav-chevron {
  transition: transform var(--tn-transition);
}
.tn-nav-group[open] > .tn-nav-group-toggle .tn-nav-chevron {
  transform: rotate(90deg);
}
.tn-nav-link {
  display: flex;
  align-items: center;
  gap: var(--tn-space-3);
  padding: var(--tn-space-2) var(--tn-space-3);
  border-radius: var(--tn-radius);
  color: var(--tn-sidebar-text);
  font-size: var(--tn-text-sm);
  font-weight: 500;
  transition: background var(--tn-transition), color var(--tn-transition);
}
.tn-nav-link:hover {
  background: var(--tn-sidebar-hover);
  color: var(--tn-sidebar-ink-strong);
}
.tn-nav-link.active {
  background: var(--tn-sidebar-active-bg);
  color: var(--tn-sidebar-active-text);
}
/* The clicked link while its page is on the way: a light fill behind the
   text, full height, whose width app.js steps through --tn-nav-progress in
   time with .tn-route-progress. A background-image rather than a
   pseudo-element so nothing has to be re-stacked above it, and declared
   after .active so the `background` shorthand there does not wipe it. */
.tn-nav-link.is-loading {
  background-image: linear-gradient(
    rgba(var(--tn-primary-rgb), 0.32),
    rgba(var(--tn-primary-rgb), 0.32)
  );
  background-repeat: no-repeat;
  background-position: left center;
  background-size: var(--tn-nav-progress, 0%) 100%;
  /* Stepped from JavaScript like the top bar, so it degrades to snapping
     under prefers-reduced-motion rather than disappearing. */
  transition: background-size 400ms ease-out, background-color var(--tn-transition),
    color var(--tn-transition);
  cursor: progress;
}
html[dir="rtl"] .tn-nav-link.is-loading {
  background-position: right center;
}
/* Fixed box so the labels line up regardless of glyph width, and so the
   icon stays put when the collapsed rail clips the label away. */
.tn-nav-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 18px;
  width: 18px;
  font-size: 1rem;
  line-height: 1;
  opacity: 0.9;
}
.tn-nav-label {
  min-width: 0;
}
/* A module the tenant's plan does not include. Only rendered for users who
   can change the plan (core.billing.manage); it links to the plan picker
   rather than into a surface that would 403. */
.tn-nav-link--locked {
  opacity: 0.55;
  cursor: help;
}
.tn-nav-link--locked:hover {
  background: var(--tn-sidebar-hover);
}
.tn-nav-lock {
  margin-inline-start: auto;
  font-size: var(--tn-text-xs);
  opacity: 0.8;
}
/* Count pill: "how much is waiting for me here". Right-aligned by the same
   margin-inline-start:auto the lock badge uses, and shaped like .tn-badge so it reads
   as the same family. min-width keeps 1 and 99+ the same silhouette, which
   stops the nav from twitching as counts change. */
.tn-nav-badge {
  margin-inline-start: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  padding: 0 6px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-primary);
  color: var(--tn-primary-contrast);
  font-size: var(--tn-text-xs);
  font-weight: 700;
  line-height: 18px;
}
/* On the active row the pill sits on the active background, which is already
   the brand colour — invert so it stays legible. */
.tn-nav-link.active .tn-nav-badge {
  background: var(--tn-sidebar-active-text);
  color: var(--tn-sidebar-active-bg);
}
/* The organisation's keyboard shortcut for this link, e.g. "Ctrl + B".

   Deliberately quiet: a shortcut is read a handful of times and then known by
   the fingers, so it must not compete with the label beside it or with a count
   that means something is waiting. It is dropped outright in the collapsed
   rail — there is no room, and a key cap is not what the icon should shrink to.

   `margin-inline-start: auto` is what .tn-nav-badge uses for the same job, and
   whichever of the two comes first takes it; the pill then keeps the far end. */
/* The organisation's keyboard shortcut for this link, e.g. "Ctrl+B".

   Right-aligned, and text only: no background, no border, no chip. A cap you
   have already learnt should read as texture at the edge of the row, not as a
   second label competing with the first.

   `background` and `padding` are reset EXPLICITLY because Bootstrap styles bare
   `kbd` as a dark filled box (`background-color: var(--bs-body-color)`, light
   text, .1875rem padding, a radius). That default is what this looked like
   before the rule existed, so the resets are load-bearing rather than tidy. */
.tn-nav-kbd {
  margin-inline-start: auto;
  flex: 0 0 auto;
  /* Blockified as a flex item, so `height` applies and the cap can never drive
     the row taller than the label's own line box however long the binding is. */
  height: 15px;
  line-height: 15px;
  align-self: center;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  /* One source of muting, not two: the token alone. Stacking `opacity` on top
     of a muted token is how a hint ends up too faint to read on one theme and
     fine on the other. */
  color: var(--tn-sidebar-ink-muted);
  /* 11px, and regular rather than 500 — the label beside it is the 500. */
  font-size: 0.6875rem;
  font-family: inherit;
  font-weight: 400;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
/* On the active row the muted ink all but vanishes against the active fill —
   the same correction .tn-nav-badge makes. */
.tn-nav-link.active .tn-nav-kbd {
  color: var(--tn-sidebar-active-text);
}
/* The LABEL yields first, not the cap. Without this a long name plus
   "Ctrl+Shift+D" runs the two into each other in a narrow sidebar, which is the
   crowding that made this look untidy. Scoped with :has() so rows carrying no
   shortcut keep wrapping exactly as they always have. */
.tn-nav-link:has(.tn-nav-kbd) .tn-nav-label {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.tn-nav-kbd + .tn-nav-badge {
  margin-inline-start: var(--tn-space-1);
}
/* A shut parent shows the sum of what is inside it, and hides it the moment it
   opens: once every child shows its own count, two pills for one queue is a
   miscount on screen. CSS rather than a second server-side pass, so it stays
   right as the reader toggles. */
.tn-nav-group[open] > .tn-nav-group-toggle .tn-nav-badge {
  display: none;
}
.tn-nav-empty .tn-nav-link {
  opacity: 0.6;
  font-style: italic;
}

/* ---- Favourites --------------------------------------------------
   Every link carries a star (add / remove) and every favourite row a "…" menu.
   Both sit over the row's trailing edge and appear on hover or keyboard focus,
   so the resting sidebar looks exactly as it did; on a touch screen there is no
   hover, so they stay faintly visible instead. They cover the badge or key cap
   while shown, which is the trade for not shifting every label sideways. */
.tn-nav-row {
  position: relative;
}
.tn-nav-fav-form {
  margin: 0;
}
/* Bootstrap makes .dropdown the positioning box; the button must hang off the
   ROW instead. Popper positions the menu from the button, so it is unaffected. */
.tn-nav-more {
  position: static;
}
.tn-nav-fav,
.tn-nav-more-btn {
  position: absolute;
  inset-inline-end: 4px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: var(--tn-radius-sm);
  background: var(--tn-sidebar-hover);
  color: var(--tn-sidebar-ink-muted);
  font-size: 0.8125rem;
  line-height: 1;
  opacity: 0;
  transition: opacity var(--tn-transition), color var(--tn-transition);
}
.tn-nav-row:hover .tn-nav-fav,
.tn-nav-row:hover .tn-nav-more-btn,
.tn-nav-row:focus-within .tn-nav-fav,
.tn-nav-row:focus-within .tn-nav-more-btn,
.tn-nav-more-btn[aria-expanded="true"] {
  opacity: 1;
}
.tn-nav-fav:hover,
.tn-nav-more-btn:hover,
.tn-nav-fav.is-on {
  color: var(--tn-sidebar-ink-strong);
}
.tn-nav-fav.is-full {
  cursor: not-allowed;
}
.tn-nav-link.active ~ .tn-nav-fav-form .tn-nav-fav,
.tn-nav-link.active ~ .tn-nav-fav,
.tn-nav-link.active ~ .tn-nav-more .tn-nav-more-btn {
  background: var(--tn-sidebar-active-bg);
  color: var(--tn-sidebar-active-text);
}
@media (hover: none) {
  .tn-nav-fav,
  .tn-nav-more-btn {
    opacity: 0.6;
    background: transparent;
  }
}
/* "3/10" beside the heading, so the cap is visible before it is hit. */
.tn-nav-fav-count {
  margin-inline-start: auto;
  font-weight: 400;
  letter-spacing: 0;
}

/* ---- Account menu ---------------------------------------------- */
.tn-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--tn-primary);
  color: var(--tn-primary-contrast);
  font-size: var(--tn-text-xs);
  font-weight: 600;
  line-height: 1;
}
/* The account dropdown holds POST forms (sign out, persona switch); their
   buttons must look identical to the anchor items beside them. */
.dropdown-menu form .dropdown-item {
  border: 0;
  background: none;
  width: 100%;
  text-align: start;
}

/* ---- Topbar pieces --------------------------------------------- */
.tn-topbar-title {
  font-weight: 600;
  font-size: var(--tn-text-lg);
  color: var(--tn-text);
  margin: 0;
}
.tn-topbar-spacer {
  flex: 1 1 auto;
}
.tn-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius);
  background: var(--tn-surface);
  color: var(--tn-text-muted);
  cursor: pointer;
  transition: background var(--tn-transition), color var(--tn-transition);
}
.tn-icon-btn:hover {
  background: var(--tn-surface-alt);
  color: var(--tn-text);
}

/* ---- Sidebar toggle (mobile) ----------------------------------- */
.tn-sidebar-toggle {
  display: none;
}

/* ---- Cards ------------------------------------------------------ */
.tn-card {
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-sm);
  padding: var(--tn-space-5);
}
.tn-card-title {
  font-size: var(--tn-text-sm);
  font-weight: 600;
  color: var(--tn-text-muted);
  margin: 0 0 var(--tn-space-2);
}
.tn-stat {
  /* A level is a bundle: size, leading, weight and tracking together. This
     rule used to hardcode weight 700 and leading 1.1 and omit the tracking
     entirely, so every stat tile rendered off-spec -- and templates then
     overrode the size inline to compensate, which broke the bundle again. */
  font-size: var(--tn-stat-size);
  line-height: var(--tn-stat-leading);
  font-weight: var(--tn-stat-weight);
  letter-spacing: var(--tn-stat-tracking);
  color: var(--tn-text);
}

/* Two money figures side by side INSIDE a card.

   Not a Bootstrap `.row`/`.col-6`: those are viewport-breakpoint columns, and
   this card is laid out by the width of its CONTAINER, not the window. In the
   Focus dashboard the card lands in a `repeat(auto-fit, minmax(16rem, 1fr))`
   track on a perfectly wide screen, so `col-6` handed each figure ~100px and a
   formatted amount at --tn-stat-size wrapped mid-number.

   `auto-fit` with a 10rem floor was the first fix and it did not finish the
   job. Two 10rem columns plus the gap need ~21rem, but the Focus track floors
   at 16rem -- so between those two numbers the grid silently dropped to one
   column, and worse, a single figure still could not fit: an Indian-grouped
   amount at --tn-stat-size (1.75rem) runs to roughly 15rem, against a ~6.5rem
   column. The rule below promised that overflow would scroll, but nothing in
   the chain had `overflow-x`, so it did not scroll -- it spilled out of the
   card. That is the "jumbled" reading of this card.

   So: an explicit two-column grid whose tracks may shrink to zero
   (`minmax(0, 1fr)` -- a bare `1fr` floors at min-content, which is what lets a
   nowrap number push the grid wider than its parent), and a figure that scales
   with the CONTAINER via `cqi` instead of staying at a fixed size that only
   fits on a wide screen. `.tn-figures-wrap` supplies that container; it is on
   this card alone rather than on `.tn-card`, because `container-type` creates a
   containment context and every card in the product is not worth that risk. */
.tn-figures-wrap {
  container-type: inline-size;
}
.tn-figures {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--tn-space-4);
}
/* One column once two of them would be too cramped to read. */
@container (max-width: 20rem) {
  .tn-figures {
    grid-template-columns: minmax(0, 1fr);
  }
}
/* An amount must not break across lines: a number split in half is not legible,
   and a shrunk one is. `cqi` is 1% of the container's inline size, so the
   figure tracks the card rather than the window -- which is the whole reason
   this card needed its own rule in the first place. `auto` is the last-resort
   backstop for a number so long that even the floor cannot hold it; now that
   the track can actually shrink, it will genuinely scroll rather than spill. */
.tn-figures .tn-stat {
  white-space: nowrap;
  overflow-wrap: normal;
  font-size: clamp(1.125rem, 8cqi, var(--tn-stat-size));
  overflow-x: auto;
  scrollbar-width: none;
}
.tn-figures .tn-stat::-webkit-scrollbar {
  display: none;
}

/* ---- Module sub-navigation -------------------------------------- */
/* `tn-subnav` was on five templates and styled in NONE -- it had no rule
   anywhere in any stylesheet. The Bootstrap-pills modules survived because
   they also carried `nav nav-pills`; recruit and assess carried only these
   class names, so their strip rendered as a row of bare underlined links with
   no active state. These are the rules those two were written against.

   One scrolling row rather than `flex-wrap`: attendance carries up to eleven
   tabs, and wrapped they became three ragged lines that pushed the page
   content below the fold and gave no clue which line was the current one.

   The flex row sits on the <nav>/<ul> itself, so both shapes in the tree are
   covered: the shared component's <nav><ul><li><a>, and recruit's and assess's
   bare <nav><a>. Those two need no markup change -- they were written against
   these class names all along. The modules still on Bootstrap pills carry
   `tn-subnav` too and pick up the shared rule and separator, which is the
   consistency the class was presumably added for. */
.tn-subnav {
  display: flex;
  gap: var(--tn-space-1);
  padding-bottom: var(--tn-space-2);
  border-bottom: 1px solid var(--tn-border);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}
.tn-subnav__list {
  display: flex;
  gap: var(--tn-space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}
.tn-subnav__item {
  display: inline-block;
  white-space: nowrap;
  padding: var(--tn-space-2) var(--tn-space-4);
  border-radius: var(--tn-radius-pill);
  font-size: var(--tn-text-sm);
  font-weight: 500;
  color: var(--tn-text-muted);
  text-decoration: none;
}
.tn-subnav__item:hover,
.tn-subnav__item:focus-visible {
  background: var(--tn-surface-alt);
  color: var(--tn-text);
  text-decoration: none;
}
.tn-subnav__item.is-active {
  background: var(--tn-primary);
  color: var(--tn-primary-contrast);
}
/* The strip scrolls, so the active tab can start out of view -- on attendance
   with eleven tabs it usually does. `scroll-margin-inline` is the padding for
   that reveal; the reveal ITSELF is `revealSubnav()` in app.js, because nothing
   here triggers a scroll on load. This property sat inert until that landed. */
.tn-subnav__item.is-active { scroll-margin-inline: var(--tn-space-4); }

/* A boundary between scope groups -- see `group_start` in components/_subnav.html.
   A rule and a little air rather than a heading: the groups are obvious once
   they are separated at all, and a visible "Team"/"Admin" label would be three
   more strings to translate for eleven pills. Margin only on the `li` so the
   pills' own spacing is untouched. */
.tn-subnav__group-start {
  margin-inline-start: var(--tn-space-3);
  padding-inline-start: var(--tn-space-3);
  border-inline-start: 1px solid var(--tn-border);
}

/* Scroll affordance. The strip has always scrolled; nothing said so, so a tab
   past the right edge was simply invisible -- there is no scrollbar to notice
   until you are already scrolling, and on a trackpad there is none at all.
   Edge fades are gradients on the scroll container, toggled by app.js only when
   there is genuinely more in that direction, so the six modules that reuse
   these class names and DON'T overflow get nothing.
   `position: sticky` on the pseudo-elements keeps them pinned to the visible
   edges while the content scrolls underneath.

   Each fade cancels its own width with ONE negative margin, on its inner side.
   It used to carry `margin-inline` on both sides, a net -2rem apiece: the list
   was dragged 32px past the start edge and the last 32px shrank out of the
   scroll range, so the first and last tabs were clipped where no scrolling
   could reach them. The fade is the PAGE colour, not --tn-surface -- the strip
   sits on the page, and a white gradient on a grey page read as a white block
   over the tabs. */
.tn-subnav {
  position: relative;
}
.tn-subnav::before,
.tn-subnav::after {
  content: "";
  position: sticky;
  z-index: 1;
  flex: 0 0 auto;
  align-self: stretch;
  width: var(--tn-space-8);
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms linear;
}
.tn-subnav::before {
  inset-inline-start: 0;
  order: -1;
  margin-inline-end: calc(var(--tn-space-8) * -1);
  background: linear-gradient(to right, var(--tn-bg), transparent);
}
.tn-subnav::after {
  inset-inline-end: 0;
  margin-inline-start: calc(var(--tn-space-8) * -1);
  background: linear-gradient(to left, var(--tn-bg), transparent);
}
[dir="rtl"] .tn-subnav::before { background: linear-gradient(to left, var(--tn-bg), transparent); }
[dir="rtl"] .tn-subnav::after { background: linear-gradient(to right, var(--tn-bg), transparent); }
.tn-subnav.is-scrollable-start::before,
.tn-subnav.is-scrollable-end::after {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .tn-subnav::before,
  .tn-subnav::after { transition: none; }
}

/* Two-level strip -- see components/_subnav.html. The levels must not look
   alike, or the second row reads as a wrapped continuation of the first:
   the top row is underlined tabs on the rule, the second is soft pills under
   it. Each row is its own `.tn-subnav`, so each scrolls and fades on its own. */
/* `gap: 0` on the rows: their only real child is the <ul>, and a gap there
   lands between it and the fade pseudo-element, indenting the whole row. */
.tn-subnav--primary,
.tn-subnav--secondary {
  gap: 0;
}
.tn-subnav--primary {
  padding-bottom: 0;
}
.tn-subnav--primary .tn-subnav__list {
  gap: var(--tn-space-6);
}
.tn-subnav__group {
  display: inline-block;
  white-space: nowrap;
  padding: var(--tn-space-2) 0 var(--tn-space-3);
  font-weight: 600;
  color: var(--tn-text-muted);
  text-decoration: none;
}
.tn-subnav__group:hover,
.tn-subnav__group:focus-visible {
  color: var(--tn-text);
  text-decoration: none;
}
/* Inset shadow rather than a border with a -1px margin: the row is a scroll
   container, which would clip anything hanging over its own bottom rule. */
.tn-subnav__group.is-active {
  color: var(--tn-primary);
  box-shadow: inset 0 -2px 0 var(--tn-primary);
}
.tn-subnav--secondary {
  padding-top: var(--tn-space-3);
  padding-bottom: 0;
  border-bottom: 0;
}
.tn-subnav--secondary .tn-subnav__item {
  padding: var(--tn-space-1) var(--tn-space-3);
}
.tn-subnav--secondary .tn-subnav__item.is-active {
  background: var(--tn-sidebar-active-bg);
  color: var(--tn-sidebar-active-text);
}

/* ---- 9-box talent grid ------------------------------------------ */
/* A 3x3 matrix, not a report table. See _nine_box.html for why it stays a
   <table>. `table-layout: fixed` is what stops the corner header sizing the
   whole grid; without it the three data columns were squeezed to whatever the
   un-wrappable header left over, and the card scrolled sideways on a desktop. */
/* `table-layout: fixed` stops the corner header sizing the table (see the
   template comment). What it does NOT do is give the columns a floor: with
   `width: 100%` and nothing else, a narrowing card just squeezes all four
   columns until the labels and counts are unreadable, because a fixed layout
   happily ignores content width. `.tn-scroll-x` is already wrapped around this
   table waiting to do something about that, so `min-width` is what lets it --
   below this, the grid scrolls at a legible size instead of being crushed. */
.tn-ninebox {
  table-layout: fixed;
  width: 100%;
  min-width: 26rem;
}
/* .tn-table sets `white-space: nowrap` on head cells so a report column header
   never wraps. Here the corner cell is a sentence and MUST wrap. */
.tn-ninebox thead th,
.tn-ninebox tbody th,
.tn-ninebox td { white-space: normal; }
/* 30% for a label column left the three DATA columns 23% each, so the 3x3 grid
   -- the thing the chart is -- read narrower than the axis caption beside it.
   22% squares it up. The caption is set on two explicit lines in the template
   rather than left to wrap, so this width is chosen for the grid rather than
   negotiated with a sentence. */
.tn-ninebox__corner {
  width: 22%;
  font-size: var(--tn-text-xs);
  font-weight: 500;
  line-height: 1.35;
  color: var(--tn-text-muted);
}
.tn-ninebox__corner span {
  display: block;
}
.tn-ninebox__axis,
.tn-ninebox__cell { text-align: center; }
.tn-ninebox__rowhead {
  font-size: var(--tn-text-sm);
  font-weight: 500;
  color: var(--tn-text-muted);
}
.tn-ninebox__cell { vertical-align: top; }
.tn-ninebox__name {
  display: block;
  font-size: var(--tn-text-xs);
  line-height: 1.3;
  color: var(--tn-text-muted);
  margin-bottom: var(--tn-space-1);
}
/* Not .tn-stat: a display-size numeral in a quarter-width column is what made
   the three rows sit at different heights with the counts off any baseline. */
.tn-ninebox__count {
  display: block;
  font-size: var(--tn-text-xl);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--tn-text);
}
.tn-ninebox__empty { color: var(--tn-text-muted); }
/* A fixed row height in a table floors rather than caps, so every box is the
   same size whatever its label wrapped to -- which is what makes it read as a
   grid instead of three unrelated rows. */
.tn-ninebox--counts tbody td,
.tn-ninebox--counts tbody th { height: 5rem; }

/* ---- Org chart tree -------------------------------------------- */
/* Lived as a <style> block duplicated byte-for-byte in two templates, wired to
   --bs-border-color / --bs-secondary-color with literal fallbacks. Those follow
   the theme only because tokens.css bridges them; the roles below say what is
   meant. The 1.4rem gutter is geometry -- it aligns with the toggle glyph. */
.tn-org-node {
  border-inline-start: 2px solid var(--tn-border);
  margin-inline-start: 1rem;
}
.tn-org-toggle {
  width: 1.4rem;
  display: inline-flex;
  justify-content: center;
  cursor: pointer;
  user-select: none;
  color: var(--tn-text-muted);
}
.tn-org-leaf {
  width: 1.4rem;
  display: inline-block;
}

/* ---- Button polish (Bootstrap .btn-primary refresh) ------------ */
.btn-primary {
  --bs-btn-bg: var(--tn-primary);
  --bs-btn-border-color: var(--tn-primary);
  --bs-btn-hover-bg: var(--tn-primary-700);
  --bs-btn-hover-border-color: var(--tn-primary-700);
  --bs-btn-active-bg: var(--tn-primary-800);
  --bs-btn-active-border-color: var(--tn-primary-800);
  --bs-btn-color: var(--tn-primary-contrast);
  --bs-btn-hover-color: var(--tn-primary-contrast);
  box-shadow: var(--tn-shadow-xs);
}
/* The everyday secondary is a plain surface with a hairline, not a
   filled grey slab competing with the primary beside it. 455 call sites,
   which makes this the highest-leverage rule in the file after the ramp. */
.btn-outline-secondary {
  --bs-btn-color: var(--tn-text);
  --bs-btn-border-color: var(--tn-border-strong);
  --bs-btn-bg: var(--tn-surface);
  --bs-btn-hover-bg: var(--tn-surface-alt);
  --bs-btn-hover-border-color: var(--tn-border-strong);
  --bs-btn-hover-color: var(--tn-text);
  --bs-btn-active-bg: var(--tn-surface-alt);
  --bs-btn-active-border-color: var(--tn-border-strong);
  --bs-btn-active-color: var(--tn-text);
}
/* Edit is blue the way Remove (.btn-outline-danger) is red, so the two
   row actions read at a glance. Built on --tn-info rather than
   --tn-primary: primary is tenant-brandable and must not turn Edit
   orange for one customer. --tn-info carries its own dark-mode value. */
.btn-outline-edit {
  --bs-btn-color: var(--tn-info);
  --bs-btn-border-color: var(--tn-info);
  --bs-btn-hover-color: var(--tn-surface);
  --bs-btn-hover-bg: var(--tn-info);
  --bs-btn-hover-border-color: var(--tn-info);
  --bs-btn-focus-shadow-rgb: var(--tn-info-rgb);
  --bs-btn-active-color: var(--tn-surface);
  --bs-btn-active-bg: var(--tn-info);
  --bs-btn-active-border-color: var(--tn-info);
  --bs-btn-disabled-color: var(--tn-info);
  --bs-btn-disabled-border-color: var(--tn-info);
}
.btn {
  --bs-btn-border-radius: var(--tn-radius-control);
  font-weight: 500;
}

/* ---- Badges for status ----------------------------------------- */
.tn-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-1);
  padding: 2px var(--tn-space-2);
  border-radius: var(--tn-radius-pill);
  font-size: var(--tn-text-xs);
  font-weight: 600;
}
/* Tint plus a soft inset ring in the badge's own ink at 22%. The ring
   exists because on a white card the tints alone read as a highlight
   rather than a status; at full strength it would read as a border and
   fight the pill. */
.tn-badge--success { background: var(--tn-success-bg); color: var(--tn-success); box-shadow: inset 0 0 0 1px rgba(var(--tn-success-rgb), 0.22); }
.tn-badge--warn    { background: var(--tn-warn-bg);    color: var(--tn-warn);    box-shadow: inset 0 0 0 1px rgba(var(--tn-warn-rgb), 0.22); }
.tn-badge--danger  { background: var(--tn-danger-bg);  color: var(--tn-danger);  box-shadow: inset 0 0 0 1px rgba(var(--tn-danger-rgb), 0.22); }
.tn-badge--info    { background: var(--tn-info-bg);    color: var(--tn-info);    box-shadow: inset 0 0 0 1px rgba(var(--tn-info-rgb), 0.22); }

/* "No state worth colouring". Referenced by 16 templates long before it
   was defined anywhere, so until now every one of them rendered as an
   unstyled pill. */
.tn-badge--muted   { background: var(--tn-muted-bg);   color: var(--tn-muted);   box-shadow: inset 0 0 0 1px var(--tn-border); }

/* Overdue, said quietly. A row that is late carries a soft
   `tn-badge--danger` pill with a clock icon, and its due date in the danger
   colour -- never a red frame round the row. A thick red border (with
   `border-danger` colouring all four sides) turned a list with a few late items
   into a wall of alarms and out-shouted the row's own buttons. The dashboard's
   attention list already worked this way; the inbox, the requests register and
   My learning now match it. Tokens only, so dark mode follows. */
.tn-due { white-space: nowrap; }
.tn-due.is-overdue { color: var(--tn-danger); font-weight: 600; }

/* Spellings that grew in templates alongside the canonical four. Defined
   as aliases rather than swept out of 11 call sites: a class that
   silently renders nothing is a worse problem than a second name for a
   tone that already exists. */
.tn-badge--warning { background: var(--tn-warn-bg);    color: var(--tn-warn);    box-shadow: inset 0 0 0 1px rgba(var(--tn-warn-rgb), 0.22); }
.tn-badge--ok      { background: var(--tn-success-bg); color: var(--tn-success); box-shadow: inset 0 0 0 1px rgba(var(--tn-success-rgb), 0.22); }

/* ---- Flash / messages ------------------------------------------ */
.tn-flash-stack {
  margin-bottom: var(--tn-space-4);
}

/* Floated into view by app.js when a save restores the reader's scroll
   position: inline at the top of <main> it would be off screen. */
.tn-flash-stack.is-floating {
  position: fixed;
  top: calc(var(--tn-topbar-height) + var(--tn-space-3));
  inset-inline-end: var(--tn-space-4);
  z-index: 1080;
  width: min(28rem, calc(100vw - 2 * var(--tn-space-4)));
  margin: 0;
  box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15);
}

/* ---- htmx UX --------------------------------------------------- */
/* Fade content in/out during swaps; show requests via .htmx-request */
.htmx-indicator {
  opacity: 0;
  transition: opacity var(--tn-transition);
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
}

/* ---- Navigation progress --------------------------------------- */
/* The bar across the top edge of the window while the next page is on
   its way. Full-page navigation is the norm here (nothing is boosted),
   so this is the product's only "something is happening" signal.

   Pinned to the viewport rather than placed in flow: .tn-shell is a grid
   whose first row is the topbar, and a bar in normal flow would push
   every page down by 3px for the life of the document. */
.tn-route-progress {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  height: 3px;
  /* Above .tn-topbar (1030) and the drawers and modals above it (max
     1100) and the environment pill (1300), because the bar reports on a
     navigation that is leaving all of them behind. */
  z-index: 1310;
  pointer-events: none;
  opacity: 0;
  background: transparent;
}

.tn-route-progress.is-busy {
  opacity: 1;
}

.tn-route-progress > span {
  display: block;
  height: 100%;
  width: 0;
  background: var(--tn-primary-500);
  /* The only decoration: a soft leading edge, so the bar reads as motion
     rather than as a border somebody left switched on. */
  box-shadow: 0 0 6px rgba(var(--tn-primary-rgb), 0.7);
  /* The WIDTH is stepped from JavaScript, never from a keyframe. The
     prefers-reduced-motion rule further down this file clamps every
     animation AND transition to 0.01ms, so a keyframed bar would be
     permanently invisible to the readers with the strongest claim on a
     static one. Transitioning width degrades correctly instead: clamped,
     it simply snaps between steps and the bar is still there. */
  transition: width 200ms ease-out, opacity 200ms linear;
}

.tn-route-progress.is-done > span {
  opacity: 0;
}

/* ---- Landing (marketing) --------------------------------------- */
.tn-landing {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  position: relative;
  isolation: isolate;
  background: var(--tn-bg);
}
.tn-hero {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
}
.tn-hero h1 {
  font-size: clamp(2rem, 4vw, 3.25rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--tn-text);
}
.tn-hero p.lead {
  color: var(--tn-text-muted);
  max-width: 38rem;
}

/* =================================================================
   Signed-out surfaces — the shared pre-login design
   -----------------------------------------------------------------
   These deliberately mirror web/assets/css/styles.css so /signup/ and
   /auth/login/ read as the same product as the marketing site the user
   arrived from. Same ambient blobs, same grid wash, same elevated card.

   Headings are the one deliberate divergence: the marketing site sets them
   in --tn-font-display, these use --tn-font-sans. A tenant's front door
   carries the org's own name, and rendering someone else's company in
   TeamNest's display serif reads as our typography, not theirs — so the
   signed-out surfaces stay on Inter, matching the signed-in app.

   They use the ALIASED token names (--bg, --brand, --r-xl, --shadow-lg)
   defined in tokens.css, which is what lets this stay copy-compatible
   with the marketing stylesheet instead of forking from it.
   ================================================================= */

/* Ambient background. Purely decorative, so it is aria-hidden in markup
   and pinned behind content with a negative z-index. */
.tn-auth-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
}
.tn-blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(100px);
}
.tn-blob-1 {
  top: -140px;
  left: 50%;
  transform: translateX(-50%);
  width: 900px;
  height: 400px;
  background: rgba(127, 138, 255, 0.16);
}
.tn-blob-2 {
  top: 12%;
  right: -10%;
  width: 360px;
  height: 360px;
  background: rgba(249, 115, 102, 0.09);
}
.tn-blob-3 {
  top: 30%;
  left: -8%;
  width: 320px;
  height: 320px;
  background: rgba(16, 185, 129, 0.09);
}
[data-theme="dark"] .tn-blob-1 { background: rgba(99, 102, 241, 0.22); }
[data-theme="dark"] .tn-blob-2 { background: rgba(249, 115, 102, 0.12); }
[data-theme="dark"] .tn-blob-3 { background: rgba(16, 185, 129, 0.12); }

.tn-grid-pattern {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right, var(--tn-text) 1px, transparent 1px),
    linear-gradient(to bottom, var(--tn-text) 1px, transparent 1px);
  background-size: 60px 60px;
  opacity: 0.025;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 70%);
  -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 70%);
}

/* Translucent sticky header, matching .site-header on the marketing site. */
.tn-auth-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: saturate(180%) blur(14px);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid var(--border-soft);
}
.tn-auth-logo {
  height: 34px;
  width: auto;
  display: block;
}

/* ---- Two-pane signed-out layout -------------------------------- */
/* Selling copy on the left, the form on the right. The pitch pane is the
   marketing site's argument carried onto the page where the decision is
   actually made, so a visitor never loses the thread mid-signup.

   Below lg the pitch is dropped entirely rather than stacked above the
   form: on a phone it would push the thing the user came to do below the
   fold, and re-reading the pitch is not why they tapped the button. */
.tn-auth-split {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--tn-space-8);
  align-items: center;
}
@media (min-width: 992px) {
  .tn-auth-split {
    grid-template-columns: 1.05fr 1fr;
    gap: clamp(2rem, 6vw, 5rem);
    align-items: start;
    padding-block: var(--tn-space-6);
  }
}

.tn-auth-pitch {
  display: none;
}
@media (min-width: 992px) {
  .tn-auth-pitch {
    display: block;
    position: sticky;
    top: 7rem;
  }
}
.tn-pitch-title {
  font-family: var(--tn-font-sans);
  font-weight: 600;
  font-size: clamp(2.25rem, 3.6vw, 3.4rem);
  line-height: 1.06;
  letter-spacing: -0.02em;
  color: var(--tn-text);
  margin: 0 0 1rem;
}
.tn-pitch-lead {
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--tn-text-muted);
  max-width: 34rem;
  margin: 0 0 2rem;
}

.tn-pitch-list {
  list-style: none;
  margin: 0 0 2rem;
  padding: 0;
  display: grid;
  gap: 1rem;
}
.tn-pitch-item {
  display: flex;
  gap: 0.85rem;
  align-items: flex-start;
}
.tn-pitch-check {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  margin-top: 1px;
  border-radius: 50%;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 0.8125rem;
}
.tn-pitch-item-title {
  font-weight: 600;
  color: var(--tn-text);
  line-height: 1.35;
}
.tn-pitch-item-body {
  font-size: var(--tn-text-sm);
  color: var(--tn-text-muted);
  line-height: 1.5;
}

/* Quiet trust strip under the bullets. */
.tn-pitch-trust {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.25rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-pitch-trust i { color: var(--mint); }

/* The card itself: rounder and more elevated than an in-app .tn-card,
   because it is the only object on the page. */
.tn-auth-card {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
  padding: clamp(1.5rem, 4vw, 2.5rem);
}
.tn-auth-narrow { max-width: 27rem; }
.tn-auth-wide   { max-width: 34rem; }

.tn-auth-title {
  font-family: var(--tn-font-sans);
  font-weight: 600;
  font-size: clamp(1.75rem, 3.4vw, 2.35rem);
  letter-spacing: -0.015em;
  line-height: 1.12;
  color: var(--tn-text);
  margin: 0 0 0.35rem;
}
.tn-auth-sub {
  color: var(--tn-text-muted);
  font-size: var(--tn-text-sm);
  margin: 0 0 1.5rem;
}
.tn-eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--brand);
  margin-bottom: 0.6rem;
}

/* ---- Signup step indicator ------------------------------------- */
.tn-steps {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  list-style: none;
  margin: 0 0 1.5rem;
  padding: 0;
  font-size: var(--tn-text-xs);
}
.tn-step {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--tn-text-muted);
}
/* The connector between steps. Drawn as a flexible pseudo-element rather than a
   border so it absorbs the leftover width and the row stays edge to edge. */
.tn-step + .tn-step::before {
  content: "";
  width: 1.25rem;
  flex: 1 1 auto;
  height: 1px;
  background: var(--border);
  margin-inline-end: 0.25rem;
}
.tn-step-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  font-weight: 600;
  font-size: 0.6875rem;
  line-height: 1;
}
.tn-step.is-current .tn-step-mark {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--tn-primary-contrast);
}
.tn-step.is-current { color: var(--tn-text); font-weight: 600; }
.tn-step.is-done .tn-step-mark {
  background: var(--mint-soft);
  border-color: var(--mint);
  color: var(--mint);
}
/* Labels are the first thing to go when there is no room for them. */
@media (max-width: 420px) {
  .tn-step-label { display: none; }
}

/* ---- Lifecycle progress bar (payroll runs) ---------------------
   One bar per step, coloured by that step's OWN state (run_page.progress_steps),
   never by position: a run can be approved with attendance never closed. Shared
   by the run page and the runs list. Tokens only, so dark mode follows. */
.tn-progress-steps {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--tn-space-2);
  list-style: none;
  margin: 0 0 var(--tn-space-4);
  padding: 0;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-progress-step { min-width: 0; }
.tn-progress-step::before {
  content: "";
  display: block;
  height: 4px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-border);
  margin-bottom: var(--tn-space-2);
}
.tn-progress-step__label { display: block; color: var(--tn-text); font-weight: 500; }
.tn-progress-step__note { display: block; }
.tn-progress-step.is-done::before { background: var(--tn-success); }
.tn-progress-step.is-current::before { background: var(--tn-primary); }
.tn-progress-step.is-current .tn-progress-step__label,
.tn-progress-step.is-current .tn-progress-step__note { color: var(--tn-primary); font-weight: 600; }
.tn-progress-step.is-attention::before { background: var(--tn-warn); }
.tn-progress-step.is-todo .tn-progress-step__label,
.tn-progress-step.is-skipped .tn-progress-step__label { color: var(--tn-text-muted); }
.tn-progress-step.is-skipped::before {
  background: repeating-linear-gradient(90deg, var(--tn-border) 0 6px, transparent 6px 10px);
}
/* On a phone the five labels do not fit side by side: stack them, with the
   bar turned into a rule down the left. */
@media (max-width: 575.98px) {
  .tn-progress-steps { grid-template-columns: 1fr; gap: var(--tn-space-1); }
  .tn-progress-step {
    display: grid;
    grid-template-columns: 4px 1fr;
    column-gap: var(--tn-space-2);
  }
  .tn-progress-step::before { height: auto; margin: 0; grid-row: span 3; }
}

/* ---- Checklist (payroll run: "Before you approve") -------------
   One finding per row, each with at most one fix on the right. The mark
   carries the tone; the row itself stays plain, so a run with four warnings
   reads as a list to work through rather than a wall of alerts. */
.tn-checklist { list-style: none; margin: 0; padding: 0; }
.tn-checklist__row {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-space-3);
  padding: var(--tn-space-3) 0;
  border-top: 1px solid var(--tn-border);
}
.tn-checklist__row:first-child { border-top: 0; }
.tn-checklist__mark { flex: 0 0 auto; line-height: 1.4; color: var(--tn-text-muted); }
.tn-checklist__row.is-ok .tn-checklist__mark { color: var(--tn-success); }
.tn-checklist__row.is-warn .tn-checklist__mark { color: var(--tn-warn); }
.tn-checklist__row.is-danger .tn-checklist__mark { color: var(--tn-danger); }
.tn-checklist__row.is-info .tn-checklist__mark { color: var(--tn-info); }
.tn-checklist__body { flex: 1 1 auto; min-width: 0; }
.tn-checklist__title { font-size: var(--tn-text-sm); font-weight: 500; color: var(--tn-text); }
.tn-checklist__detail { font-size: var(--tn-text-xs); color: var(--tn-text-muted); }
.tn-checklist__action { flex: 0 0 auto; }
@media (max-width: 575.98px) {
  .tn-checklist__row { flex-wrap: wrap; }
  .tn-checklist__action { flex-basis: 100%; padding-inline-start: calc(1em + var(--tn-space-3)); }
}

/* ---- Run totals: one headline figure, two sums ------------------
   The sums are written as arithmetic (Gross - Deductions = Net pay) so the
   middle term is always on screen; the total row is ruled off above. */
.tn-total-hero {
  font-size: var(--tn-stat-size);
  font-weight: 600;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
  color: var(--tn-text);
}
.tn-total-delta { display: inline-block; margin-top: var(--tn-space-1); font-size: var(--tn-text-xs); color: var(--tn-text-muted); }
.tn-total-delta:hover { color: var(--tn-primary); }
.tn-sum { font-size: var(--tn-text-sm); }
.tn-sum__row {
  display: flex;
  justify-content: space-between;
  gap: var(--tn-space-3);
  padding: var(--tn-space-1) 0;
}
.tn-sum__row dt { font-weight: 400; color: var(--tn-text-muted); }
.tn-sum__row dd { margin: 0; font-variant-numeric: tabular-nums; color: var(--tn-text); }
.tn-sum__total { border-top: 1px solid var(--tn-border); margin-top: var(--tn-space-1); padding-top: var(--tn-space-2); }
.tn-sum__total dt, .tn-sum__total dd { font-weight: 600; color: var(--tn-text); }

/* ---- Run page: tabs, lines, selection, activity ---------------- */
/* A card whose own children set the padding (tabs flush to its edge). */
.tn-card--flush { padding: 0; }
.tn-lines-search { max-width: 18rem; }
.tn-col-check { width: 2.5rem; }
/* One selection bar for compare and recompute, shown while anything is ticked. */
.tn-selection-bar {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--tn-space-2);
  padding: var(--tn-space-1) var(--tn-space-2);
  border-radius: var(--tn-radius-sm);
  background: rgba(var(--tn-primary-rgb), 0.1);
  color: var(--tn-text);
}
.tn-selection-bar form { margin: 0; }
/* The +/- breakdown toggle on each line (was an inline <style> on the page). */
.tn-line-toggle { width: 1.9rem; padding-left: 0; padding-right: 0; line-height: 1; font-weight: 600; }
.tn-line-toggle[aria-expanded="true"] .tn-when-collapsed,
.tn-line-toggle:not([aria-expanded="true"]) .tn-when-expanded { display: none; }
.tn-activity { list-style: none; margin: 0; padding: 0; font-size: var(--tn-text-sm); }
.tn-activity__item {
  display: flex;
  gap: var(--tn-space-3);
  padding: var(--tn-space-2) 0;
  border-top: 1px solid var(--tn-border);
}
.tn-activity__item:first-child { border-top: 0; }
.tn-activity__when { flex: 0 0 6.5rem; color: var(--tn-text-muted); font-variant-numeric: tabular-nums; }
.tn-activity__what { flex: 1 1 auto; min-width: 0; }
.tn-activity__note { margin-top: var(--tn-space-1); color: var(--tn-text-soft); }
.tn-checklist__row.is-pending .tn-checklist__mark { color: var(--tn-border-strong); }
.tn-checklist__row.is-pending .tn-checklist__title { color: var(--tn-text-muted); }
.tn-checklist__when { font-weight: 400; color: var(--tn-text-muted); }
/* "This run is locked ..." -- one quiet line with its one way forward. */
.tn-locked-note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--tn-space-2) var(--tn-space-3);
  padding: var(--tn-space-3) var(--tn-space-4);
  border-radius: var(--tn-radius-md);
  background: var(--tn-surface-alt);
  font-size: var(--tn-text-sm);
  color: var(--tn-text-soft);
}
.tn-locked-note form { margin: 0; }

/* Verification code inputs: wide tracking so 6 digits stay countable. */
.tn-code-input {
  font-size: 1.25rem;
  letter-spacing: 0.4em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* One box per digit ({% otp_boxes %} + js/otp-boxes.js). The plain field above
   is the no-JS fallback; the script hides it and these take over. */
.tn-otp__boxes {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-start;
}
.tn-otp__box {
  width: 3rem;
  height: 3.5rem;
  padding: 0;
  font-size: 1.5rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  font-family: var(--bs-font-monospace, ui-monospace, monospace);
  text-align: center;
  color: var(--bs-body-color);
  background: var(--bs-body-bg);
  border: 1.5px solid var(--bs-border-color);
  border-radius: 0.6rem;
  caret-color: var(--bs-primary);
  transition: border-color 0.15s, box-shadow 0.15s;
}
.tn-otp__box:focus {
  outline: 0;
  border-color: var(--bs-primary);
  box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.2);
}
.tn-otp__box.is-invalid { border-color: var(--bs-danger); }
.tn-otp__caption { margin-top: 0.4rem; }
.tn-otp__hint { color: var(--bs-warning-text-emphasis); font-weight: 600; }
/* Six boxes still fit a 320px phone. */
@media (max-width: 400px) {
  .tn-otp__boxes { gap: 0.35rem; }
  .tn-otp__box { width: 2.5rem; height: 3rem; font-size: 1.25rem; }
}

/* "or" rule between the provider buttons and the credential form. */
.tn-auth-divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.25rem 0;
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.tn-auth-divider::before,
.tn-auth-divider::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--border);
}

/* Provider buttons. Neutral by design — a coloured button per provider
   would compete with the primary action and with each other. */
.tn-social-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.7rem 1rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--bg-elev);
  color: var(--tn-text);
  font-weight: 600;
  font-size: 0.9375rem;
  transition: background var(--tn-transition), border-color var(--tn-transition),
    box-shadow var(--tn-transition), transform var(--tn-transition);
}
.tn-social-btn:hover {
  background: var(--bg-soft);
  border-color: var(--tn-border-strong);
  color: var(--tn-text);
  box-shadow: var(--shadow-sm);
}
.tn-social-btn:active { transform: translateY(1px); }
.tn-social-btn svg { flex: 0 0 auto; }

.tn-auth-footer {
  margin-top: 1.5rem;
  text-align: center;
  font-size: var(--tn-text-sm);
  color: var(--tn-text-muted);
}

/* ---- Password strength meter (static/js/signup-form.js) --------- */
/* Built entirely by JS, so none of this renders without it — which is
   deliberate. With JS off the field keeps Django's password_validators
   help text underneath and nothing looks broken or half-drawn.

   Colours come from the status tokens, which already have dark-mode
   overrides, so this block needs no theme rules of its own. */

/* Wrapper the reveal button is positioned against. */
.tn-pw-field { position: relative; }
.tn-pw-field .form-control { padding-inline-end: 2.75rem; }

.tn-pw-reveal {
  position: absolute;
  top: 50%;
  right: 0.25rem;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--tn-text-muted);
  cursor: pointer;
  transition: color var(--tn-transition), background var(--tn-transition);
}
.tn-pw-reveal:hover { color: var(--tn-text); background: var(--bg-soft); }
.tn-pw-reveal:focus-visible { outline: var(--tn-focus-outline-width) solid var(--tn-primary); outline-offset: 2px; }

.tn-pw { margin-top: 0.5rem; }

.tn-pw-bar {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.25rem;
}
.tn-pw-seg {
  height: 4px;
  border-radius: 999px;
  background: var(--border-strong);
  transition: background var(--tn-transition);
}

/* One hue per score rather than a gradient: the point is a verdict, and
   an amber bar that is merely longer than the red one reads as progress
   when it is still going to be rejected.

   The level class goes on .tn-pw itself (set by the JS) so both the bar
   and the readout can key off it with a plain descendant selector — no
   :has(), which this stylesheet does not otherwise rely on. */
.tn-pw-level-0 { --tn-pw-hue: var(--tn-danger); }
.tn-pw-level-1 { --tn-pw-hue: var(--tn-danger); }
.tn-pw-level-2 { --tn-pw-hue: var(--tn-warn); }
.tn-pw-level-3 { --tn-pw-hue: var(--tn-info); }
.tn-pw-level-4 { --tn-pw-hue: var(--tn-success); }

.tn-pw-seg.is-on { background: var(--tn-pw-hue, var(--tn-text-muted)); }
.tn-pw-readout { color: var(--tn-pw-hue, var(--tn-text-muted)); }

.tn-pw-readout {
  margin: 0.375rem 0 0;
  min-height: 1.25em; /* reserve the line so the checklist does not jump */
  font-size: var(--tn-text-xs);
  font-weight: 600;
}

.tn-pw-checks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.125rem 1rem;
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
}
.tn-pw-check {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-pw-check.is-ok { color: var(--tn-success); }
.tn-pw-mark {
  flex: 0 0 auto;
  width: 0.75rem;
  text-align: center;
  font-weight: 700;
}

/* ---- Workspace home page (tenant host "/") ---------------------- */
/* The org's front door. Specified as ONE screen with nothing below the
   fold, which is the only reason this is not just .tn-auth-split: that
   layout drops its left pane under 992px, and here the left pane holds
   the mission — the substance of the page, not a sales aside.

   Every vertical measure is vh-aware rather than fixed, so the whole
   thing compresses to fit a short laptop viewport instead of pushing the
   sign-in button out of sight. It is allowed to scroll as a last resort
   (very small windows, large text sizes); clipping instead would hide
   content from exactly the people who can least afford it. */
.tn-home {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  position: relative;
  isolation: isolate;
  background: var(--tn-bg);
}
.tn-home-main {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  padding-block: clamp(1.75rem, 6vh, 4rem);
}
.tn-home-split {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1.75rem, 5vh, 3rem);
  align-items: center;
}
@media (min-width: 992px) {
  .tn-home-split {
    grid-template-columns: 1.1fr 1fr;
    gap: clamp(2rem, 6vw, 5rem);
  }
}

/* Product attribution, pinned above the fold's content rather than sharing
   the org's logo slot. Deliberately small: on a tenant's front door the
   workspace is the subject and TeamNest is the footnote. It sits outside
   .tn-home-main so it never competes for the centred grid's vertical space. */
.tn-home-brandbar {
  flex: 0 0 auto;
  padding-block: clamp(1rem, 3vh, 1.75rem);
}
.tn-home-brandmark {
  height: auto;
  max-height: clamp(1.5rem, 3vh, 2.125rem);
  width: auto;
  display: block;
}

.tn-home-logo {
  height: auto;
  max-height: clamp(2.25rem, 5vh, 3.25rem);
  max-width: 13rem;
  width: auto;
  display: block;
  margin-bottom: clamp(1rem, 3vh, 1.75rem);
}
.tn-home-title {
  font-family: var(--tn-font-sans);
  font-weight: 600;
  font-size: clamp(1.9rem, 4vw, 3.1rem);
  line-height: 1.06;
  letter-spacing: -0.02em;
  color: var(--tn-text);
  /* Long single-word workspace names would otherwise widen the grid
     column and squeeze the card off the screen. */
  overflow-wrap: anywhere;
  margin: 0;
}
.tn-home-facts {
  list-style: none;
  margin: clamp(1.25rem, 4vh, 2rem) 0 0;
  padding: 0;
  display: grid;
  gap: clamp(0.85rem, 2.5vh, 1.25rem);
  max-width: 34rem;
}
/* The card column keeps the same cap as the signed-out card so the two
   pages line up when a visitor clicks through to sign in. */
.tn-home-aside {
  width: 100%;
  max-width: 27rem;
  margin-inline: auto;
}

/* ---- Form controls --------------------------------------------- */
/* Django renders {{ form.field }} as a bare <input> with no class, so
   every form built that way showed browser-default boxes next to the
   hand-written ones that did carry .form-control. The widgets now set
   the class themselves (see core/tenants/forms.py); this styles it. */
.form-control,
.form-select {
  border-radius: var(--r-md);
  border-color: var(--border-strong);
  background: var(--bg-elev);
  color: var(--tn-text);
  padding: 0.6rem 0.85rem;
  font-size: 0.9375rem;
  transition: border-color var(--tn-transition), box-shadow var(--tn-transition);
}
.form-control::placeholder { color: var(--text-faint); }
.form-control:focus,
.form-select:focus {
  background: var(--bg-elev);
  color: var(--tn-text);
  border-color: var(--brand-500);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand) 15%, transparent);
}
.form-label {
  font-size: var(--tn-text-sm);
  font-weight: 600;
  color: var(--tn-text);
  margin-bottom: 0.35rem;
}
.form-text {
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
}
.input-group-text {
  border-radius: var(--r-md);
  border-color: var(--border-strong);
  background: var(--bg-soft);
  color: var(--tn-text-muted);
  font-size: 0.9375rem;
}

/* ---- Buttons: marketing parity --------------------------------- */
/* .btn-pill is the marketing site's CTA shape; sharing the class means
   a "Start free" button is identical on both surfaces. */
.btn-pill { border-radius: var(--r-md); }
.btn-lg {
  padding: 0.75rem 1.25rem;
  font-size: 1rem;
}
.btn-primary {
  box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--brand) 50%, transparent);
}

/* ---- Responsive: collapse sidebar to an off-canvas drawer ------ */
@media (max-width: 991.98px) {
  .tn-shell {
    grid-template-columns: 1fr;
    grid-template-areas:
      "topbar"
      "main";
  }
  .tn-sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    height: auto;
    transform: translateX(-100%);
    box-shadow: var(--tn-shadow-lg);
  }
  .tn-shell.tn-sidebar-open .tn-sidebar {
    transform: translateX(0);
  }
  .tn-sidebar-toggle {
    display: inline-flex;
  }
  .tn-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.5);
    z-index: 1035;
  }
}

/* ---- Desktop: collapse the sidebar to an icon rail -------------- */
/* Driven by data-tn-sidebar on <html> (set pre-paint in base.html, toggled
   by app.js via the topbar button or Ctrl+\). Scoped to >=992px on purpose:
   below that the sidebar is an off-canvas drawer and a 72px rail would be
   nonsense. The whole layout follows one custom property, because .tn-shell
   sizes its grid from it and .tn-sidebar its width. */
@media (min-width: 992px) {
  html[data-tn-sidebar="collapsed"] .tn-shell {
    --tn-sidebar-width: 72px;
  }
  html[data-tn-sidebar="collapsed"] .tn-sidebar {
    padding-inline-start: var(--tn-space-2);
    padding-inline-end: var(--tn-space-2);
    overflow-x: hidden;
  }
  html[data-tn-sidebar="collapsed"] .tn-nav-link,
  html[data-tn-sidebar="collapsed"] .tn-brand {
    justify-content: center;
    gap: 0;
  }
  /* Anchors the count dot below. */
  html[data-tn-sidebar="collapsed"] .tn-nav-link {
    position: relative;
  }
  /* Clipped, not display:none — every link must keep its accessible name. */
  html[data-tn-sidebar="collapsed"] .tn-nav-label,
  html[data-tn-sidebar="collapsed"] .tn-brand > span:not(.tn-brand-mark) {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* Uppercase headings and the plan-lock badge have no icon to shrink to.
     The group toggle goes for a different reason: a toggle whose label you
     cannot read is a button with no visible effect. */
  html[data-tn-sidebar="collapsed"] .tn-nav-group-toggle,
  html[data-tn-sidebar="collapsed"] .tn-nav-kbd,
  html[data-tn-sidebar="collapsed"] .tn-nav-lock,
  html[data-tn-sidebar="collapsed"] .tn-nav-fav,
  html[data-tn-sidebar="collapsed"] .tn-nav-more {
    display: none;
  }
  /* A hairline stands in for the dropped headings so the sections still read
     as separate runs. It hangs off the section wrapper now that the heading
     itself is inside the <details> it opens. */
  html[data-tn-sidebar="collapsed"] .tn-nav-section-wrap {
    border-top: 1px solid var(--tn-sidebar-border);
    padding-top: var(--tn-space-2);
    margin-top: var(--tn-space-2);
  }
  /* Section collapse is SUSPENDED here, and the group flattens. With the
     heading gone there is nothing left to collapse into: a shut section would
     be a run of icons that vanished with no visible cause. app.js forces every
     <details> open; these rules undo the indent so the children join the same
     single column of icons, each keeping its own count. */
  html[data-tn-sidebar="collapsed"] .tn-nav-group:not(.tn-nav-group--section)
    > .tn-nav-group-items {
    margin-inline-start: 0;
    padding-inline-start: 0;
    border-inline-start: 0;
  }
  html[data-tn-sidebar="collapsed"] .tn-nav-icon {
    font-size: 1.125rem;
  }
  /* The number will not fit in a 72px rail, but hiding the badge outright
     (what .tn-nav-lock does) would throw away the one thing a badge is for.
     It becomes a dot on the icon's corner; the digits stay in the DOM for
     screen readers and app.js puts the count in the hover title. */
  html[data-tn-sidebar="collapsed"] .tn-nav-badge {
    position: absolute;
    top: 4px;
    right: 14px;
    min-width: 0;
    width: 9px;
    height: 9px;
    padding: 0;
    margin: 0;
    overflow: hidden;
    font-size: 0;
    line-height: 0;
    color: transparent;
    box-shadow: 0 0 0 2px var(--tn-sidebar-bg);
  }
  /* Hover naming is the browser's own tooltip: app.js copies data-tn-label
     into `title` while collapsed. A CSS ::after bubble would be clipped by
     the sidebar's own scroll box, which no z-index can escape. */
}

/* =================================================================
   Responsive layer
   -----------------------------------------------------------------
   The shell previously had exactly ONE media query (the off-canvas
   drawer above). Everything below is the rest of the mobile story:
   tables that stop overflowing, a bottom tab bar, and touch targets
   that meet WCAG 2.2 AA.
   ================================================================= */

/* ---- Data tables: card stack on small screens ------------------- */
/* Requires every <td> to carry data-label="<column header>", which
   components/_data_table.html emits automatically. Without the label
   the stacked rows lose their column context. */
@media (max-width: 767.98px) {
  .tn-table--stack thead {
    display: none;
  }
  .tn-table--stack tbody,
  .tn-table--stack tr,
  .tn-table--stack td {
    display: block;
    width: 100%;
  }
  .tn-table--stack tr {
    margin-bottom: var(--tn-space-3);
    padding: var(--tn-space-3);
    border: 1px solid var(--tn-border);
    border-radius: var(--tn-radius-md);
    background: var(--tn-surface);
  }
  .tn-table--stack td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--tn-space-3);
    border: 0;
    padding: var(--tn-space-1) 0;
    text-align: start;
  }
  .tn-table--stack td::before {
    content: attr(data-label);
    flex: 0 0 40%;
    color: var(--tn-text-muted);
    font-size: var(--tn-text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  /* The primary cell reads as the card's heading, so it needs no label. */
  .tn-table--stack td[data-primary]::before {
    display: none;
  }
  .tn-table--stack td[data-primary] {
    font-weight: 600;
    font-size: var(--tn-text-base);
    padding-bottom: var(--tn-space-2);
  }
}

/* Anything genuinely too wide to stack scrolls in its OWN box, so the
   page body never scrolls horizontally. */
.tn-scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* ---- Bottom tab bar (small viewports only) ---------------------- */
.tn-bottom-nav {
  display: none;
}
@media (max-width: 991.98px) {
  .tn-bottom-nav {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 1030;
    display: flex;
    justify-content: space-around;
    background: var(--tn-surface);
    border-top: 1px solid var(--tn-border);
    padding-bottom: env(safe-area-inset-bottom, 0);
  }
  .tn-bottom-nav-link {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: 56px;
    padding: var(--tn-space-2) var(--tn-space-1);
    color: var(--tn-text-muted);
    font-size: var(--tn-text-xs);
    text-align: center;
    line-height: 1.2;
  }
  .tn-bottom-nav-link.active {
    color: var(--tn-primary);
    font-weight: 600;
  }
  /* Bigger than the sidebar's: it is the tab's main affordance, the label
     below it is only 12px. */
  .tn-bottom-nav .tn-nav-icon {
    font-size: 1.25rem;
  }
  /* A tab is a column, so margin-inline-start:auto has nothing to push against; the
     pill rides the icon's top-right corner instead. */
  .tn-bottom-nav-link {
    position: relative;
  }
  .tn-bottom-nav .tn-nav-badge {
    position: absolute;
    top: 4px;
    left: 50%;
    margin-inline-start: 2px;
    min-width: 16px;
    padding: 0 4px;
    font-size: var(--tn-overline-size);
    line-height: 16px;
    background: var(--tn-danger);
    color: var(--tn-primary-contrast);
  }
  /* Reserve room so the last row of content is never hidden behind it. */
  .tn-main {
    padding-bottom: calc(56px + var(--tn-space-6) + env(safe-area-inset-bottom, 0));
  }
}

/* ---- Touch targets --------------------------------------------- */
/* Coarse pointers only, so desktop density is untouched. */
@media (pointer: coarse) {
  .tn-icon-btn {
    width: 44px;
    height: 44px;
  }
  .tn-nav-link {
    padding: var(--tn-space-3);
  }
  .dropdown-item {
    padding-top: 0.625rem;
    padding-bottom: 0.625rem;
  }
  .btn {
    min-height: 44px;
  }
}

/* ---- Focus visibility ------------------------------------------- */
/* The shell shipped with no focus ring at all — a keyboard user could
   not tell where they were. */
:focus-visible {
  outline: var(--tn-focus-outline-width) solid var(--tn-primary);
  outline-offset: 2px;
  border-radius: var(--tn-radius-control);
}

/* Controls that paint their own ring drop the outline -- the two
   together stack into a double halo of two different radii, which is
   what made tabbing across a single form change the ring's shape. */
.form-control:focus-visible,
.form-select:focus-visible,
.btn:focus-visible {
  outline: none;
  box-shadow: var(--tn-focus-ring);
}

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

/* ---- Shared components ------------------------------------------ */
.tn-page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-space-3);
  margin-bottom: var(--tn-space-5);
}
/* The in-content page heading. An h2, not an h1: the topbar already
   renders the page name as the document h1 on every screen including the
   ~95 that have no page header at all, so promoting this one would give
   those pages two h1s and leave the rest with none. 27 templates had
   already drifted to h1 here and were shipping a duplicate. */
.tn-page-title {
  font-size: var(--tn-h2-size);
  line-height: var(--tn-h2-leading);
  font-weight: var(--tn-h2-weight);
  letter-spacing: var(--tn-h2-tracking);
  color: var(--tn-text);
  margin: 0;
}
.tn-page-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-space-2);
}

.tn-empty {
  padding: var(--tn-space-8) var(--tn-space-4);
  /* Solid, never dashed: a dashed box reads as a drop target or a
     placeholder that failed to load, and an empty list is neither. */
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  background: var(--tn-surface);
}
.tn-empty-title {
  font-weight: 600;
  color: var(--tn-text);
  margin-bottom: var(--tn-space-1);
}

.tn-table {
  --bs-table-bg: var(--tn-surface);
  --bs-table-color: var(--tn-text);
  --bs-table-border-color: var(--tn-border);
}
.tn-table thead th {
  /* A table is a card containing rows: the head sits on the recessed
     plane and carries the only heavy rule; rows are separated by subtle
     hairlines, and the last row has none because the container's own
     border closes it. */
  background: var(--tn-surface-sunk);
  font-size: var(--tn-overline-size);
  text-transform: uppercase;
  letter-spacing: var(--tn-overline-tracking);
  color: var(--tn-text-muted);
  font-weight: var(--tn-overline-weight);
  white-space: nowrap;
  border-bottom: 1px solid var(--tn-border);
}
.tn-table tbody td {
  border-bottom: 1px solid var(--tn-border-subtle);
}
.tn-table tbody tr:last-child td {
  border-bottom: 0;
}

/* Utility used by the data table + card headings to allow text-truncate
   inside a flex child (which otherwise refuses to shrink). */
.min-w-0 {
  min-width: 0;
}

/* Keep a long free-text cell from widening its table.

   Every table in Expenses sits inside `.tn-scroll-x`, so a long merchant name
   or a paragraph-length rejection reason did not wrap -- it stretched the
   table and pushed the action column off-screen behind a horizontal scrollbar,
   where nobody looks for a button. Bootstrap's `.text-truncate` needs a width
   to do anything and there was no clamp utility here at all.

   Two lines, then an ellipsis; the full text stays in `title`. `max-width` is
   what actually bounds it — `line-clamp` alone does nothing in a table cell
   that is free to grow. */
.tn-cell-clamp {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 22rem;
  overflow-wrap: anywhere;
}

/* One line, for a cell where two would push the row too tall (a name). */
.tn-cell-clamp--1 {
  -webkit-line-clamp: 1;
  line-clamp: 1;
  max-width: 14rem;
}

/* ---- Ops console ------------------------------------------------ */
/* The console reuses the app shell wholesale; only the brand tag and
   the filter bar are specific to it. */
.tn-ops-tag {
  margin-inline-start: var(--tn-space-1);
  font-size: var(--tn-text-xs);
  font-weight: 500;
  opacity: 0.7;
}
.tn-filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-space-2);
  align-items: center;
}
.tn-filter-bar .form-control,
.tn-filter-bar .form-select {
  max-width: 16rem;
}
@media (max-width: 575.98px) {
  .tn-filter-bar .form-control,
  .tn-filter-bar .form-select {
    max-width: none;
    flex: 1 1 100%;
  }
}

/* ---- Ops team dashboards (ADR 0084) ------------------------------ */
/* One page, panels chosen by permission. KPI tiles are a container-fit
   grid rather than Bootstrap columns: a panel with three figures and one
   with seven should both fill the width without a breakpoint table. */
.tn-ops-panel {
  margin-bottom: var(--tn-space-6);
}
.tn-ops-panel__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--tn-space-2);
  margin-bottom: var(--tn-space-3);
}
.tn-ops-panel__title {
  margin: 0;
  font-size: var(--tn-h5-size);
  line-height: var(--tn-h5-leading);
  font-weight: var(--tn-h5-weight);
  letter-spacing: var(--tn-h5-tracking);
  color: var(--tn-text);
}
.tn-ops-panel__hint {
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--tn-space-3);
  margin-bottom: var(--tn-space-4);
}
.tn-kpi {
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-1);
  min-width: 0;
  padding: var(--tn-space-4);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-inline-start-width: 3px;
  border-radius: var(--tn-radius-card);
  color: var(--tn-text);
  text-decoration: none;
  transition: box-shadow var(--tn-motion, 150ms) ease;
}
a.tn-kpi:hover,
a.tn-kpi:focus-visible {
  box-shadow: var(--tn-shadow-sm);
}
.tn-kpi__label {
  font-size: var(--tn-overline-size);
  line-height: var(--tn-overline-leading);
  font-weight: var(--tn-overline-weight);
  letter-spacing: var(--tn-overline-tracking);
  text-transform: uppercase;
  color: var(--tn-text-muted);
}
.tn-kpi__value {
  font-size: var(--tn-stat-size);
  line-height: var(--tn-stat-leading);
  font-weight: var(--tn-stat-weight);
  letter-spacing: var(--tn-stat-tracking);
  color: var(--tn-text);
  overflow-wrap: anywhere;
}
.tn-kpi__value--sm {
  font-size: var(--tn-text-lg);
}
.tn-kpi__sub {
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-kpi--info { border-inline-start-color: var(--tn-info); }
.tn-kpi--success { border-inline-start-color: var(--tn-success); }
.tn-kpi--warn { border-inline-start-color: var(--tn-warn); }
.tn-kpi--danger { border-inline-start-color: var(--tn-danger); }

/* Ageing buckets: a count, its value, and a bar. <progress> rather than an
   inline width so no style attribute is needed (CSP) and the proportion is
   announced to assistive technology for free. */
.tn-ageing {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--tn-space-3);
}
.tn-ageing__cell {
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-1);
  min-width: 0;
}
.tn-ageing__label {
  font-size: var(--tn-text-xs);
  font-weight: 600;
  color: var(--tn-text-muted);
}
.tn-ageing__count {
  font-size: var(--tn-text-lg);
  font-weight: 600;
  color: var(--tn-text);
}
.tn-ageing__bar {
  appearance: none;
  width: 100%;
  height: 6px;
  border: 0;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-surface-alt);
  overflow: hidden;
}
.tn-ageing__bar::-webkit-progress-bar {
  background: var(--tn-surface-alt);
  border-radius: var(--tn-radius-pill);
}
.tn-ageing__bar::-webkit-progress-value {
  background: var(--tn-primary);
  border-radius: var(--tn-radius-pill);
}
.tn-ageing__bar::-moz-progress-bar {
  background: var(--tn-primary);
  border-radius: var(--tn-radius-pill);
}
.tn-ageing__bar--warn::-webkit-progress-value { background: var(--tn-warn); }
.tn-ageing__bar--warn::-moz-progress-bar { background: var(--tn-warn); }
.tn-ageing__bar--danger::-webkit-progress-value { background: var(--tn-danger); }
.tn-ageing__bar--danger::-moz-progress-bar { background: var(--tn-danger); }

/* The sales windows table: three periods side by side, collapsing to a
   stacked list on a phone rather than scrolling sideways. */
.tn-window-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--tn-text-sm);
}
.tn-window-table th,
.tn-window-table td {
  padding: var(--tn-space-2) var(--tn-space-3);
  border-bottom: 1px solid var(--tn-border-subtle);
  text-align: end;
  vertical-align: top;
}
.tn-window-table th:first-child,
.tn-window-table td:first-child {
  text-align: start;
  color: var(--tn-text-muted);
}
.tn-window-table thead th {
  font-size: var(--tn-overline-size);
  letter-spacing: var(--tn-overline-tracking);
  text-transform: uppercase;
  color: var(--tn-text-muted);
}
@media (max-width: 575.98px) {
  .tn-window-table thead { display: none; }
  .tn-window-table tr { display: block; padding: var(--tn-space-2) 0; }
  .tn-window-table td { display: flex; justify-content: space-between; border: 0; }
  .tn-window-table td::before {
    content: attr(data-label);
    color: var(--tn-text-muted);
  }
  .tn-window-table td:first-child { font-weight: 600; color: var(--tn-text); }
  .tn-window-table td:first-child::before { content: none; }
}

/* A proforma's timeline on its detail page. */
.tn-timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-2);
  font-size: var(--tn-text-sm);
}
.tn-timeline li {
  padding-inline-start: var(--tn-space-3);
  border-inline-start: 2px solid var(--tn-border);
}
.tn-timeline li.is-done {
  border-inline-start-color: var(--tn-success);
}

/* ---- Settings (org-admin) --------------------------------------- */
/* Two-column inside the page: a section rail plus the body. The rail
   moves above the content on narrow screens rather than squeezing it. */
.tn-settings {
  display: grid;
  grid-template-columns: var(--tn-settings-rail-width, 232px) 1fr;
  gap: var(--tn-space-6);
  align-items: start;
}
.tn-settings-rail {
  position: sticky;
  top: calc(var(--tn-topbar-height) + var(--tn-space-4));
}
.tn-settings-list {
  list-style: none;
  margin: 0 0 var(--tn-space-3);
  padding: 0;
}
.tn-settings-link {
  display: block;
  padding: var(--tn-space-2) var(--tn-space-3);
  border-radius: var(--tn-radius);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-sm);
}
.tn-settings-link:hover {
  background: var(--tn-surface-alt);
  color: var(--tn-text);
}
.tn-settings-link.active {
  background: var(--tn-primary-50);
  color: var(--tn-primary-700);
  font-weight: 600;
}
[data-theme="dark"] .tn-settings-link.active {
  background: rgba(79, 70, 229, 0.18);
  color: var(--tn-primary-300);
}
.tn-settings-body {
  min-width: 0;
}
@media (max-width: 991.98px) {
  .tn-settings {
    grid-template-columns: 1fr;
  }
  .tn-settings-rail {
    position: static;
    border-bottom: 1px solid var(--tn-border);
    padding-bottom: var(--tn-space-3);
  }
  .tn-settings-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--tn-space-1);
  }
}

/* ---- Settings search -------------------------------------------------
   Results replace nothing: they render below the input while the grouped
   card grid stays put, so clearing the box returns you to the full list
   without a page load. Each result is a stacked block rather than a table
   row because the path line ("Attendance policy > Shift policies") is the
   part that answers "where do I go", and it must not truncate. */
.tn-settings-results {
  display: grid;
  gap: var(--tn-space-1);
  max-height: 26rem;
  overflow-y: auto;
}
.tn-settings-result {
  display: block;
  padding: var(--tn-space-2) var(--tn-space-3);
  border-radius: var(--tn-radius);
  border: 1px solid transparent;
  color: inherit;
  text-decoration: none;
}
.tn-settings-result:hover,
.tn-settings-result:focus-visible {
  background: var(--tn-surface-alt);
  border-color: var(--tn-border);
  color: inherit;
}
.tn-settings-result-title {
  display: block;
  font-weight: 600;
  font-size: var(--tn-text-sm);
}
.tn-settings-result-path,
.tn-settings-result-help {
  display: block;
}

/* Template gallery: a compact label/value ladder so cards of different
   heights still line their facts up against each other. */
.tn-template-highlights {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 var(--tn-space-3);
  align-items: baseline;
  margin: 0;
}
.tn-template-highlights dt {
  font-weight: 500;
}
.tn-template-highlights dd {
  margin: 0;
}

/* ---- Help desk conversation ------------------------------------- */
.tn-msg {
  padding: var(--tn-space-3) 0;
  border-bottom: 1px solid var(--tn-border);
}
.tn-msg:last-child { border-bottom: 0; }
.tn-msg-meta {
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
  margin-bottom: var(--tn-space-1);
}
/* Internal notes are tinted so an agent cannot mistake one for a reply the
   employee can read. They are also excluded at the query level. */
.tn-msg--internal {
  background: var(--tn-warn-bg);
  border-radius: var(--tn-radius);
  padding: var(--tn-space-3);
  border-bottom: 0;
  margin-bottom: var(--tn-space-2);
}

/* ==================================================================
   Home dashboard
   ------------------------------------------------------------------
   Only --tn-* tokens are referenced, so the dark theme comes free from
   the semantic surface roles in tokens.css. No literal colours here.
   ================================================================== */

/* ---- Hero band -------------------------------------------------- */
.tn-hero {
  display: flex;
  flex-direction: column;
  /* Explicit, because the marketing .tn-hero rule above sets `center` and
     `flex: 1 1 auto`, and both would otherwise leak into this one. */
  align-items: stretch;
  flex: none;
  gap: var(--tn-space-3);
  padding: var(--tn-space-4) var(--tn-space-6) var(--tn-space-6);
  border-radius: var(--tn-radius-lg);
  background: linear-gradient(
    135deg,
    var(--tn-primary-50) 0%,
    var(--tn-surface) 55%,
    var(--tn-surface) 100%
  );
  border: 1px solid var(--tn-border);
}
[data-theme="dark"] .tn-hero {
  /* The light gradient reads as a wash of grey on a dark surface, so the
     dark theme keeps the tint but anchors it to the elevated surface. */
  background: linear-gradient(
    135deg,
    var(--tn-surface-alt) 0%,
    var(--tn-surface) 60%,
    var(--tn-surface) 100%
  );
}
/* Top row: punch-state pill on the left, layout switcher on the right. The
   switcher used to own a whole band above the hero for three words. */
.tn-hero__top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-space-3);
  min-height: 2.25rem;
}
.tn-hero__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--tn-space-4);
}
.tn-hero__body { min-width: 0; }
.tn-hero__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-2);
  font-size: var(--tn-text-xs);
  font-weight: 600;
  color: var(--tn-text-muted);
  margin: 0;
  padding: var(--tn-space-1) var(--tn-space-3);
  border-radius: var(--tn-radius-pill);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
}
.tn-hero__dot {
  width: 8px;
  height: 8px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-text-muted);
  flex: none;
}
.tn-hero__dot--in       {
  background: var(--tn-success);
  /* A slow pulse while the clock is running: the one live thing on the page. */
  animation: tn-live-pulse 2.4s ease-in-out infinite;
}
@keyframes tn-live-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--tn-success-rgb), 0.45); }
  50%      { box-shadow: 0 0 0 5px rgba(var(--tn-success-rgb), 0); }
}
.tn-hero__dot--on_break { background: var(--tn-warn); }
.tn-hero__dot--out      { background: var(--tn-text-muted); }
.tn-hero__title {
  font-size: var(--tn-text-3xl);
  font-weight: 700;
  color: var(--tn-text);
  margin: 0 0 var(--tn-space-2);
  line-height: 1.15;
}
.tn-hero__title em {
  font-family: "Instrument Serif", Georgia, serif;
  font-style: italic;
  font-weight: 400;
  color: var(--tn-primary);
}
.tn-hero__meta {
  font-size: var(--tn-text-sm);
  color: var(--tn-text-muted);
  margin: 0;
}
.tn-hero__action { flex: none; }

/* ---- Worked-time ring ------------------------------------------- */
.tn-worked-ring {
  flex: none;
  width: 76px;
  height: 76px;
  border-radius: var(--tn-radius-pill);
  border: 3px solid var(--tn-border);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  line-height: 1.1;
  text-align: center;
}
.tn-worked-ring--in       { border-color: var(--tn-success); }
.tn-worked-ring--on_break { border-color: var(--tn-warn); }
.tn-worked-ring__value {
  font-size: var(--tn-text-lg);
  font-weight: 700;
  color: var(--tn-text);
}
.tn-worked-ring__unit {
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-worked-ring__label {
  font-size: 0.625rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--tn-text-muted);
  margin-top: 2px;
}

/* ---- Persona bands ---------------------------------------------- */
.tn-band__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-space-3);
  margin-bottom: var(--tn-space-3);
}
.tn-band__title {
  font-size: var(--tn-text-lg);
  font-weight: 600;
  color: var(--tn-text);
  margin: 0;
}

/* ---- Feed ------------------------------------------------------- */
.tn-feed-item {
  padding: var(--tn-space-3) 0;
  border-bottom: 1px solid var(--tn-border);
}
.tn-feed-item:last-child { border-bottom: 0; padding-bottom: 0; }
.tn-feed-item:first-child { padding-top: 0; }
.tn-feed-item__head {
  display: flex;
  align-items: center;
  gap: var(--tn-space-2);
  margin-bottom: var(--tn-space-2);
}
.tn-feed-item__pin { color: var(--tn-text-muted); font-size: var(--tn-text-xs); }
.tn-feed-item__title {
  font-weight: 600;
  color: var(--tn-text);
  margin: 0 0 var(--tn-space-1);
}
.tn-feed-item__body {
  font-size: var(--tn-text-sm);
  color: var(--tn-text-muted);
  margin: 0 0 var(--tn-space-2);
}

/* ---- Compact lists ---------------------------------------------- */
.tn-section-label {
  font-size: var(--tn-text-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--tn-text-muted);
  margin: 0 0 var(--tn-space-2);
}
.tn-mini-list { margin: 0; }
.tn-mini-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-space-3);
  padding: var(--tn-space-2) 0;
  font-size: var(--tn-text-sm);
}
.tn-mini-list__icon {
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: var(--tn-radius);
  background: var(--tn-surface-alt);
  color: var(--tn-text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--tn-text-sm);
}

/* ---- "Jump to" tiles -------------------------------------------- */
/* Compact link rows inside the "Jump to" card. auto-fill with a 200px floor:
   one column in Standard's narrow right-hand slot, several in Focus. */
.tn-tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--tn-space-1);
}
.tn-tile {
  display: flex;
  align-items: center;
  gap: var(--tn-space-3);
  padding: var(--tn-space-2);
  border: 1px solid transparent;
  border-radius: var(--tn-radius);
  background: transparent;
  text-decoration: none;
  transition: border-color var(--tn-motion, 150ms) ease,
              box-shadow var(--tn-motion, 150ms) ease,
              transform var(--tn-motion, 150ms) ease;
}
.tn-tile:hover,
.tn-tile:focus-visible {
  background: var(--tn-surface-alt);
  border-color: var(--tn-border);
}
.tn-tile__icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--tn-radius);
  background: var(--tn-primary-50);
  color: var(--tn-primary);
  font-size: var(--tn-text-base);
}
[data-theme="dark"] .tn-tile__icon {
  /* primary-50 is a near-white tint — invisible against a dark card. */
  background: var(--tn-surface-alt);
}
.tn-tile__icon--calendar  { background: var(--tn-info-bg);    color: var(--tn-info); }
.tn-tile__icon--celebrate { background: var(--tn-success-bg); color: var(--tn-success); }
.tn-tile__label {
  display: block;
  font-size: var(--tn-text-sm);
  font-weight: 600;
  color: var(--tn-text);
}
.tn-tile__caption {
  display: block;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 575.98px) {
  .tn-hero { padding: var(--tn-space-5); }
  .tn-hero__title { font-size: var(--tn-text-2xl); }
  .tn-hero__action { width: 100%; }
  .tn-hero__action .btn { width: 100%; }
  .tn-tile-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}

/* ---- Home: section furniture ------------------------------------ */
.tn-band__sub {
  font-size: var(--tn-text-sm);
  color: var(--tn-text-muted);
  margin: 2px 0 0;
}
/* A quiet text link for a card or band header ("View logs", "See all 5").
   Buttons are for doing; these only go somewhere. */
.tn-card-link {
  font-size: var(--tn-text-sm);
  font-weight: 600;
  color: var(--tn-primary);
  text-decoration: none;
  white-space: nowrap;
}
.tn-card-link:hover { color: var(--tn-primary-700); text-decoration: underline; }

/* Cards in a row share a height (h-100); --stack makes the body a column so
   the footer link lands on the same baseline in every card of the row. */
.tn-card--stack { display: flex; flex-direction: column; }
.tn-card-foot {
  margin-top: auto;
  padding-top: var(--tn-space-4);
  font-size: var(--tn-text-sm);
  font-weight: 600;
  color: var(--tn-primary);
  text-decoration: none;
  align-self: flex-start;
}
.tn-card-foot:hover { color: var(--tn-primary-700); }
.tn-card-foot .bi { transition: transform var(--tn-transition); display: inline-block; }
.tn-card-foot:hover .bi { transform: translateX(3px); }

/* A lazily-loaded grid slot whose fragment came back empty: hide the column
   so the row closes up instead of keeping a hole. */
.tn-lazy-slot:not(:has(*)) { display: none; }

/* ---- Home: "Your day" time card --------------------------------- */
.tn-dayclock__state {
  font-weight: 600;
  color: var(--tn-text);
}
/* Played once when a punch swaps the card in, so the reader sees their action
   land. `swapped` is only set on the htmx response, never on a page load. */
.tn-card--flash { animation: tn-card-flash 1.2s ease-out 1; }
@keyframes tn-card-flash {
  0%   { box-shadow: 0 0 0 3px rgba(var(--tn-success-rgb), 0.45); border-color: var(--tn-success); }
  100% { box-shadow: var(--tn-shadow-sm); border-color: var(--tn-border); }
}

/* ---- Home: "Waiting on you" ------------------------------------- */
.tn-attn { display: flex; flex-direction: column; }
.tn-attn__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--tn-space-3);
  margin-bottom: var(--tn-space-3);
}
.tn-attn__list { list-style: none; margin: 0; padding: 0; }
.tn-attn__list li + li { border-top: 1px solid var(--tn-border); }
.tn-attn__row {
  display: flex;
  align-items: center;
  gap: var(--tn-space-3);
  padding: var(--tn-space-3) var(--tn-space-2);
  margin: 0 calc(-1 * var(--tn-space-2));
  border-radius: var(--tn-radius);
  color: var(--tn-text);
  text-decoration: none;
  transition: background var(--tn-transition);
}
.tn-attn__row:hover,
.tn-attn__row:focus-visible { background: var(--tn-surface-alt); color: var(--tn-text); }
/* The left-edge mark: triage by colour before reading a word. */
.tn-attn__mark {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-primary-300);
}
.tn-attn__row.is-overdue .tn-attn__mark { background: var(--tn-danger); }
.tn-attn__title {
  display: block;
  font-weight: 600;
  font-size: var(--tn-text-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tn-attn__sub {
  display: block;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tn-attn__due {
  flex: none;
  font-size: var(--tn-text-xs);
  font-weight: 600;
  color: var(--tn-text-muted);
}
.tn-attn__row.is-overdue .tn-attn__due { color: var(--tn-danger); }
.tn-attn__go { flex: none; color: var(--tn-text-muted); font-size: var(--tn-text-xs); }

/* An empty state that reads as good news, not as a missing card. */
.tn-allclear {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--tn-space-6) var(--tn-space-4);
}
.tn-allclear__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-success-bg);
  color: var(--tn-success);
  font-size: 1.35rem;
  margin-bottom: var(--tn-space-3);
}
.tn-allclear--quiet .tn-allclear__icon { background: var(--tn-surface-alt); color: var(--tn-text-muted); }
.tn-allclear__title { font-weight: 600; color: var(--tn-text); margin: 0 0 2px; }

/* ---- Home: team / org metric tiles ------------------------------ */
/* auto-fit, so five or six tiles fill their rows evenly instead of leaving a
   half-empty orphan row the way a fixed four-column Bootstrap row did. */
.tn-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--tn-space-4);
}
.tn-metric {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--tn-space-4) var(--tn-space-5);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  box-shadow: var(--tn-shadow-sm);
  color: var(--tn-text);
  text-decoration: none;
  transition: border-color var(--tn-transition), box-shadow var(--tn-transition), transform var(--tn-transition);
}
a.tn-metric:hover,
a.tn-metric:focus-visible {
  color: var(--tn-text);
  border-color: var(--tn-primary-300);
  box-shadow: var(--tn-shadow);
  transform: translateY(-1px);
}
.tn-metric__icon {
  position: absolute;
  top: var(--tn-space-4);
  inset-inline-end: var(--tn-space-4);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--tn-radius);
  background: var(--tn-surface-alt);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-sm);
}
.tn-metric__label {
  font-size: var(--tn-text-sm);
  font-weight: 600;
  color: var(--tn-text-muted);
  padding-inline-end: 2.25rem;
}
.tn-metric__value {
  font-size: var(--tn-stat-size);
  line-height: var(--tn-stat-leading);
  font-weight: var(--tn-stat-weight);
  letter-spacing: var(--tn-stat-tracking);
  margin: var(--tn-space-1) 0;
}
.tn-metric__note {
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
/* Needs action: an accent edge and a tinted icon. Zero never gets this. */
.tn-metric.is-attn { border-inline-start: 3px solid var(--tn-warn); }
.tn-metric.is-attn .tn-metric__icon { background: var(--tn-warn-bg); color: var(--tn-warn); }
.tn-metric.is-alert { border-inline-start: 3px solid var(--tn-danger); }
.tn-metric.is-alert .tn-metric__icon { background: var(--tn-danger-bg); color: var(--tn-danger); }

/* ---- Home: "Coming up" ------------------------------------------ */
.tn-mini-list__icon--celebrate { background: var(--tn-success-bg); color: var(--tn-success); }
.tn-mini-list__icon--calendar  { background: var(--tn-info-bg); color: var(--tn-info); width: 36px; height: 36px; }
.tn-datechip {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1;
  font-size: 0.5625rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.tn-datechip b { font-size: var(--tn-text-sm); letter-spacing: 0; }
.tn-mini-list li.is-today .tn-today { color: var(--tn-success); }

/* ---- Focus: date line + switcher -------------------------------- */
.tn-focus__top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-space-3);
  margin-bottom: var(--tn-space-2);
}
.tn-focus__top .tn-focus__date { margin: 0; }
.tn-focus__main > .tn-hero__action { margin-bottom: var(--tn-space-6); }

@media (prefers-reduced-motion: reduce) {
  .tn-hero__dot--in,
  .tn-card--flash { animation: none; }
  a.tn-metric:hover,
  a.tn-metric:focus-visible { transform: none; }
}

/* ---- Layout switcher -------------------------------------------- */
/* A segmented control made of one small POST form per option, so each
   segment is a real <button> and the whole strip degrades to three
   buttons with no JS at all. The forms are the flex items; the gap
   belongs to the strip, not to them. */
.tn-viewswitch {
  display: inline-flex;
  align-items: stretch;
  gap: 1px;
  padding: 3px;
  background: var(--tn-surface-sunk);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
}
.tn-viewswitch form { display: flex; }
.tn-viewswitch__item {
  border: 0;
  background: transparent;
  color: var(--tn-text-muted);
  font-size: var(--tn-text-sm);
  font-weight: 600;
  padding: var(--tn-space-2) var(--tn-space-4);
  border-radius: var(--tn-radius-pill);
  transition: var(--tn-transition);
  white-space: nowrap;
}
.tn-viewswitch__item:hover { color: var(--tn-text); }
.tn-viewswitch__item.is-active {
  background: var(--tn-surface);
  color: var(--tn-text);
  box-shadow: var(--tn-shadow-xs);
}

/* ---- Focus layout ----------------------------------------------- */
/* Two columns: a narrow rail carrying the day's punches, and one
   reading column. It collapses to a single column below lg, where a
   200px gutter costs more than it says. */
.tn-focus {
  display: grid;
  grid-template-columns: 14rem minmax(0, 1fr);
  gap: var(--tn-space-8);
  align-items: start;
}
/* Capped at a reading measure: Focus is one editorial column, and a lede or an
   inbox row stretched across a wide monitor is hard to track back from. */
.tn-focus__main { min-width: 0; max-width: 48rem; }

/* The rail. Times are right-aligned against the spine so the eye reads
   one column of figures rather than a ragged edge. */
.tn-focus__rail { min-width: 0; }
.tn-focus__timeline {
  list-style: none;
  margin: 0 0 var(--tn-space-6);
  padding: 0;
  position: relative;
}
.tn-focus__timeline::before {
  content: "";
  position: absolute;
  top: 0.6rem;
  bottom: 0.6rem;
  right: 0;
  width: 1px;
  background: var(--tn-border);
}
.tn-focus__moment {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  column-gap: var(--tn-space-3);
  padding: 0 var(--tn-space-4) var(--tn-space-6) 0;
  position: relative;
  text-align: end;
}
.tn-focus__time {
  font-family: var(--tn-font-mono);
  font-size: var(--tn-text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--tn-text);
}
.tn-focus__what {
  grid-column: 1;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
/* Sits on the spine, so it is positioned against the row rather than
   flowed with the text. */
.tn-focus__dot {
  grid-row: 1 / span 2;
  grid-column: 2;
  position: absolute;
  right: -4px;
  top: 0.35rem;
  width: 9px;
  height: 9px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-surface);
  border: 2px solid var(--tn-border-strong);
}
.tn-focus__dot--clock_in { border-color: var(--tn-success); }
.tn-focus__dot--clock_out { border-color: var(--tn-text-muted); }
.tn-focus__dot--break_start,
.tn-focus__dot--break_end { border-color: var(--tn-warn); }
.tn-focus__dot--now {
  background: var(--tn-primary);
  border-color: var(--tn-primary);
}
.tn-focus__moment--now .tn-focus__time { color: var(--tn-primary); }

/* The reading column. */
.tn-focus__date {
  font-size: var(--tn-overline-size);
  line-height: var(--tn-overline-leading);
  font-weight: var(--tn-overline-weight);
  letter-spacing: var(--tn-overline-tracking);
  text-transform: uppercase;
  color: var(--tn-text-faint);
  margin: 0 0 var(--tn-space-4);
}
.tn-focus__greeting {
  font-size: var(--tn-display-size);
  line-height: var(--tn-display-leading);
  letter-spacing: var(--tn-display-tracking);
  font-weight: var(--tn-display-weight);
  color: var(--tn-text);
  margin: 0 0 var(--tn-space-4);
}
.tn-focus__greeting em {
  font-family: var(--tn-font-display);
  font-style: italic;
  font-weight: 400;
  color: var(--tn-primary);
}
/* Prose, not a caption: this paragraph is the page's summary, so it is
   set at reading size and measure rather than muted small print. */
.tn-focus__lede {
  font-size: var(--tn-text-lg);
  line-height: 1.6;
  color: var(--tn-text-soft);
  max-width: 44rem;
  margin: 0 0 var(--tn-space-6);
}
.tn-focus__figure {
  font-family: var(--tn-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--tn-text);
}

.tn-focus__section { margin-bottom: var(--tn-space-8); }
.tn-focus__label {
  font-size: var(--tn-overline-size);
  line-height: var(--tn-overline-leading);
  font-weight: var(--tn-overline-weight);
  letter-spacing: var(--tn-overline-tracking);
  text-transform: uppercase;
  color: var(--tn-text-muted);
  padding-bottom: var(--tn-space-3);
  border-bottom: 1px solid var(--tn-border);
  margin: 0 0 var(--tn-space-2);
}
.tn-focus__list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.tn-focus__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--tn-space-4);
  padding: var(--tn-space-4) 0;
  border-bottom: 1px solid var(--tn-border-subtle);
}
.tn-focus__list--plain .tn-focus__row { border-bottom: 0; padding: var(--tn-space-2) 0; }
/* An overdue row earns a marker in the margin, not a red background:
   four red panels stop reading as urgent and start reading as decor. */
.tn-focus__row.is-overdue {
  box-shadow: inset 2px 0 0 0 var(--tn-danger);
  padding-inline-start: var(--tn-space-4);
}
.tn-focus__row-title {
  display: block;
  font-size: var(--tn-text-lg);
  color: var(--tn-text);
  text-decoration: none;
}
.tn-focus__row-title:hover { color: var(--tn-primary); }
.tn-focus__row-sub {
  display: block;
  font-size: var(--tn-text-sm);
  color: var(--tn-text-muted);
  margin-top: 2px;
}
.tn-focus__due {
  flex: none;
  font-family: var(--tn-font-mono);
  font-size: var(--tn-text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--tn-text-muted);
  white-space: nowrap;
}
.tn-focus__due.is-overdue { color: var(--tn-danger); }
.tn-focus__quiet {
  font-size: var(--tn-text-sm);
  color: var(--tn-text-muted);
  padding-top: var(--tn-space-4);
  margin: 0;
}
.tn-focus__more {
  display: inline-block;
  margin-top: var(--tn-space-4);
  font-size: var(--tn-text-sm);
}
@media (max-width: 991.98px) {
  .tn-focus {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--tn-space-6);
  }
  /* Stacked, the rail is no longer a gutter, so its right-hand spine and
     right alignment become arbitrary. */
  .tn-focus__rail { order: 2; }
  .tn-focus__timeline::before { left: 0; right: auto; }
  .tn-focus__moment {
    text-align: start;
    grid-template-columns: auto minmax(0, 1fr);
    padding: 0 0 var(--tn-space-4) var(--tn-space-4);
  }
  .tn-focus__time { grid-column: 1; }
  .tn-focus__what { grid-column: 2; }
  .tn-focus__dot { left: -4px; right: auto; }
}

@media (max-width: 575.98px) {
  .tn-focus__greeting { font-size: var(--tn-text-3xl); }
  .tn-focus__lede { font-size: var(--tn-text-base); }
  .tn-viewswitch { display: flex; width: 100%; }
  .tn-viewswitch form { flex: 1; }
  .tn-viewswitch__item { width: 100%; }
}

/* Alpine sets x-cloak on elements that must not flash before it boots. */
[x-cloak] { display: none !important; }

/* ---- Nestor assistant drawer ------------------------------------ */
/* A right-hand overlay rather than a third grid column: .tn-shell is a
   grid with named areas ("sidebar topbar" / "sidebar main") and has no
   slot for one, so adding a column would reflow every existing page.
   z-index ladder in use elsewhere: topbar 1030, backdrop 1035,
   sidebar 1040 — the drawer sits above all three. */
/* The launcher floats over the bottom-right corner, which is exactly where
   a pager's "Next" sits. Reserve its height below the content on every page
   that shows it, so the last row of a page is never underneath it. */
.tn-main--with-launcher {
  padding-bottom: calc(var(--tn-space-6) + 4.5rem);
}

.tn-nestor-launcher {
  position: fixed;
  right: var(--tn-space-5);
  bottom: var(--tn-space-5);
  z-index: 1041;
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-2);
  padding: var(--tn-space-3) var(--tn-space-5);
  border: 1px solid var(--tn-border);
  border-radius: 999px;
  background: var(--tn-surface);
  color: var(--tn-text);
  font-size: var(--tn-text-sm);
  font-weight: 600;
  box-shadow: var(--tn-shadow-lg);
  transition: var(--tn-transition);
}
.tn-nestor-launcher:hover { background: var(--tn-surface-alt); }
.tn-nestor-launcher i { color: var(--tn-primary); }

.tn-nestor-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1042;
  background: rgba(15, 23, 42, 0.5);
}

.tn-nestor-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 1045;
  width: min(420px, 100vw);
  display: flex;
  flex-direction: column;
  background: var(--tn-surface);
  border-inline-start: 1px solid var(--tn-border);
  box-shadow: var(--tn-shadow-lg);
}

.tn-nestor-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--tn-space-3);
  padding: var(--tn-space-5);
  border-bottom: 1px solid var(--tn-border);
}
.tn-nestor-title {
  margin: 0;
  font-size: var(--tn-text-lg);
  font-weight: 600;
}
.tn-nestor-title i { color: var(--tn-primary); }
.tn-nestor-sub {
  margin: 0;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}

/* Chat layout: thread bar, scrolling transcript, pinned composer.
   The body is the flex COLUMN and owns no scrolling of its own -- the
   transcript inside it does. Before this the composer sat in the normal flow
   after the turns, so a long conversation pushed the box you type into off the
   bottom of the panel. min-height:0 on both is what lets the middle child
   actually shrink; without it a flex item refuses to go below its content and
   the panel grows instead of scrolling. */
.tn-nestor-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}
.tn-nestor-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--tn-space-5);
}
.tn-nestor-composer {
  flex: none;
  padding: var(--tn-space-4) var(--tn-space-5) var(--tn-space-5);
  border-top: 1px solid var(--tn-border);
  background: var(--tn-surface);
}
/* The thread bar is a header for the transcript, so it keeps the panel's side
   padding even though its parent no longer has any. */
.tn-nestor-threadbar-wrap { padding: 0 var(--tn-space-5); }

.tn-nestor-form { margin-bottom: 0; }
.tn-nestor-input { resize: vertical; }
.tn-nestor-form-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-space-3);
  margin-top: var(--tn-space-2);
}

/* Suggestion pills under the ask box. Wrapping, not scrolling: there are at
   most four and they are narrowed per user, so a horizontal scroller would
   hide half of a list short enough to just show. */
.tn-nestor-suggests {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-space-2);
  margin: 0 0 var(--tn-space-3);
}
.tn-nestor-suggest {
  padding: var(--tn-space-1) var(--tn-space-3);
  border: 1px solid var(--tn-border);
  border-radius: 999px;
  background: var(--tn-surface-alt);
  font-size: var(--tn-text-xs);
  line-height: 1.6;
  color: var(--tn-text-muted);
  text-align: start;
}
.tn-nestor-suggest:hover {
  border-color: var(--tn-primary);
  color: var(--tn-text);
}

.tn-nestor-run { margin-bottom: var(--tn-space-5); }
.tn-nestor-question {
  margin: 0 0 var(--tn-space-2);
  font-weight: 600;
  font-size: var(--tn-text-sm);
}
.tn-nestor-answer {
  font-size: var(--tn-text-sm);
  line-height: var(--tn-leading);
  white-space: pre-wrap;
}
.tn-nestor-thinking { color: var(--tn-text-muted); }

/* The answer node is written to twice by two different renderers. The live
   stream sets .textContent (raw model text), which is why the container keeps
   white-space: pre-wrap; the poll swap that follows then re-renders it through
   |nestor_answer as real block markup, where pre-wrap would show the tags'
   own indentation as gaps. So the blocks reset it and the container keeps it. */
.tn-nestor-answer > p,
.tn-nestor-answer > ul,
.tn-nestor-answer > ol { white-space: normal; }
.tn-nestor-answer > p { margin: 0 0 var(--tn-space-3); }
.tn-nestor-answer > p:last-child { margin-bottom: 0; }
.tn-nestor-list {
  margin: 0 0 var(--tn-space-3);
  padding-inline-start: var(--tn-space-5);
}
.tn-nestor-list:last-child { margin-bottom: 0; }
.tn-nestor-list li { margin-bottom: var(--tn-space-1); }
.tn-nestor-answer code {
  font-family: var(--tn-font-mono);
  font-size: 0.9em;
  padding: 0 var(--tn-space-1);
  border-radius: var(--tn-radius-sm);
  background: var(--tn-surface-alt);
}

/* Citation markers. Superscript rather than inline "[1]" so a sentence with
   three of them still reads as a sentence; they anchor to the source list
   below, which :target then highlights so a jump lands visibly. */
.tn-nestor-ref {
  font-size: 0.7em;
  font-weight: 600;
  color: var(--tn-primary);
  padding-inline-start: 1px;
}
.tn-nestor-ref-link { text-decoration: none; }
.tn-nestor-ref-link:hover .tn-nestor-ref { text-decoration: underline; }
.tn-nestor-source:target {
  background: var(--tn-surface-alt);
  border-radius: var(--tn-radius-sm);
}

.tn-nestor-sources {
  margin: var(--tn-space-3) 0 0;
  padding-top: var(--tn-space-3);
  border-top: 1px solid var(--tn-border);
}
.tn-nestor-source {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--tn-space-2);
  font-size: var(--tn-text-xs);
  margin-bottom: var(--tn-space-1);
}
.tn-nestor-cite {
  font-family: var(--tn-font-mono);
  color: var(--tn-primary);
}
.tn-nestor-source-title { color: var(--tn-text); }
/* Marks a passage that came from a fact resolver rather than a document —
   there is no document to link to, and pretending otherwise would give a
   live figure the appearance of a citable source. */
.tn-nestor-pill {
  padding: 0 var(--tn-space-2);
  border-radius: 999px;
  background: var(--tn-mint-soft);
  color: var(--tn-text);
  font-size: var(--tn-text-xs);
  font-weight: 600;
}

.tn-nestor-error {
  padding: var(--tn-space-3);
  border-radius: var(--tn-radius);
  background: var(--tn-danger-bg);
  font-size: var(--tn-text-sm);
}

.tn-nestor-threadbar {
  flex: none;
  padding: var(--tn-space-4) var(--tn-space-5) 0;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--tn-space-3);
  padding-bottom: var(--tn-space-3);
  border-bottom: 1px solid var(--tn-border);
}
.tn-nestor-threadmeta { min-width: 0; }
.tn-nestor-threadtitle {
  margin: 0;
  font-size: var(--tn-text-xs);
  font-weight: 600;
  color: var(--tn-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tn-nestor-threadtokens {
  margin: 0;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-nestor-newchat {
  flex: none;
  padding: var(--tn-space-1) var(--tn-space-3);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
  background: none;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-nestor-newchat:hover { border-color: var(--tn-primary); color: var(--tn-text); }
.tn-nestor-usage {
  margin: var(--tn-space-2) 0 0;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}

.tn-nestor-history { margin-top: var(--tn-space-5); }
.tn-nestor-history-item {
  display: block;
  width: 100%;
  padding: var(--tn-space-2) 0;
  border: 0;
  border-bottom: 1px solid var(--tn-border);
  background: none;
  text-align: start;
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-nestor-history-item:hover { color: var(--tn-text); }

.tn-nestor-hint { margin-top: var(--tn-space-5); }

@media (max-width: 575.98px) {
  /* Full width on a phone; a 420px panel would leave a useless sliver. */
  .tn-nestor-panel { width: 100vw; }
  /* Clear of the bottom nav, which the shell renders below 992px. */
  .tn-nestor-launcher { bottom: calc(var(--tn-space-5) + 56px); }
}

/* ---- Style guide (/settings/branding/style-guide/) --------------
   Layout only. Every colour, size and shadow on that page is applied
   inline as var(--tn-…) from core/branding/styleguide.py, so the page
   is painted by the tokens it documents and cannot drift from them. */
.tn-swatch-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--tn-space-3);
}
.tn-swatch-chip {
  height: 3rem;
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-control);
}
.tn-swatch-name {
  display: block;
  margin-top: var(--tn-space-1);
  font-size: var(--tn-caption-size);
  color: var(--tn-text-muted);
  word-break: break-all;
}
.tn-spec-row {
  display: flex;
  align-items: baseline;
  gap: var(--tn-space-4);
  padding: var(--tn-space-3) 0;
  border-bottom: 1px solid var(--tn-border-subtle);
}
.tn-spec-row:last-child {
  border-bottom: 0;
}
.tn-spec-label {
  flex: 0 0 7rem;
  font-size: var(--tn-caption-size);
  color: var(--tn-text-muted);
}
.tn-spec-sample {
  min-width: 0;
}
.tn-elevation-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--tn-space-4);
}
.tn-elevation-tile {
  padding: var(--tn-space-4);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-card);
  font-size: var(--tn-caption-size);
  color: var(--tn-text-muted);
}

/* ---- Settings rail: a scroll box, and collapsible groups ---------------
   The rail was `position: sticky` with no height limit and no overflow, which
   for twenty-one groups and ninety-six screens meant the bottom of it was
   literally unreachable: sticky pins the top, the element is taller than the
   viewport, and nothing scrolls it independently. Giving it its own box is
   what makes sticky mean something -- and what lets app.js scroll the active
   row into view. */
.tn-settings-rail {
  max-height: calc(100vh - var(--tn-topbar-height) - var(--tn-space-8));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-inline-end: var(--tn-space-1);
}
.tn-settings-rail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-space-2);
  margin-bottom: var(--tn-space-2);
}
/* Sits in the rail like a link, not like a form control: it is a shortcut for
   twenty-one clicks, not an action on the page. */
/* A compact icon toggle, not a worded button: the rail is 232px wide and the
   words took a third of it to say something you need once. Deliberately not
   .tn-icon-btn, which is 38px square and built for the topbar -- at this size
   it would out-weigh the heading it sits beside. */
.tn-settings-expand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius);
  background: var(--tn-surface);
  color: var(--tn-text-muted);
  cursor: pointer;
  transition: background var(--tn-transition), color var(--tn-transition);
}
.tn-settings-expand:hover {
  background: var(--tn-surface-alt);
  color: var(--tn-text);
}
/* The rail's three fixed entries. Pills because they belong to no group:
   drawn as indented rows they read as children of a heading that is not
   there. Wrapping, so they keep their shape in a 232px rail. */
.tn-settings-quick {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tn-space-1);
  margin: 0 0 var(--tn-space-4);
  padding: 0;
  list-style: none;
}
.tn-settings-quick-link {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-2);
  padding: var(--tn-space-1) var(--tn-space-3);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
  background: var(--tn-surface);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
  font-weight: 600;
  text-decoration: none;
  /* The labels are the product's established wording and must not wrap inside
     a pill; the row wraps instead, one pill per line where the rail is narrow. */
  white-space: nowrap;
  transition: background var(--tn-transition), color var(--tn-transition);
}
.tn-settings-quick-link:hover {
  background: var(--tn-surface-alt);
  color: var(--tn-text);
}
.tn-settings-quick-link.active {
  background: var(--tn-primary-50);
  border-color: var(--tn-primary-200);
  color: var(--tn-primary-700);
}

.tn-settings-group-toggle {
  display: flex;
  align-items: center;
  gap: var(--tn-space-2);
  padding: var(--tn-space-2) var(--tn-space-2);
  border-radius: var(--tn-radius);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  list-style: none;
}
/* The default disclosure triangle, removed in all three dialects. The chevron
   is the affordance, and two arrows on one row read as a rendering fault.
   `display: flex` above already drops `display: list-item` in modern engines,
   but Safari still draws ::-webkit-details-marker and older Chrome still draws
   ::marker, so all three are stated rather than assumed. */
.tn-settings-group-toggle::-webkit-details-marker {
  display: none;
}
.tn-settings-group-toggle::marker {
  content: "";
}
.tn-settings-group-toggle:hover {
  background: var(--tn-surface-alt);
  color: var(--tn-text);
}
.tn-settings-chevron {
  display: inline-flex;
  flex: 0 0 auto;
  transition: transform var(--tn-transition);
}
.tn-settings-group[open] > .tn-settings-group-toggle .tn-settings-chevron {
  transform: rotate(90deg);
}
.tn-settings-group-icon {
  display: inline-flex;
  flex: 0 0 auto;
  opacity: 0.75;
}
.tn-settings-group > .tn-settings-list {
  margin-bottom: var(--tn-space-2);
}

/* Rail rows carry their screen's icon, the same glyph the sibling strip and
   the page heading draw. Fixed box so labels line up regardless of glyph
   width. */
.tn-settings-link {
  display: flex;
  align-items: center;
  gap: var(--tn-space-2);
}
.tn-settings-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 16px;
  width: 16px;
  opacity: 0.7;
}
.tn-settings-link.active .tn-settings-icon {
  opacity: 1;
}

/* ---- The sibling strip -------------------------------------------------
   One scrolling row, never a wrapping block: the largest groups hold twelve
   screens, and wrapping those to three lines would push the page's own heading
   below the fold on exactly the busiest screens. */
.tn-settings-siblings {
  display: flex;
  gap: var(--tn-space-2);
  align-items: center;
  margin-bottom: var(--tn-space-5);
  padding-bottom: var(--tn-space-2);
  border-bottom: 1px solid var(--tn-border);
}
.tn-settings-sibling {
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-2);
  flex: 0 0 auto;
  padding: var(--tn-space-2) var(--tn-space-3);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
  background: var(--tn-surface);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-sm);
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--tn-transition), color var(--tn-transition);
}
.tn-settings-sibling:hover {
  background: var(--tn-surface-alt);
  color: var(--tn-text);
}
.tn-settings-sibling.active {
  background: var(--tn-primary-50);
  border-color: var(--tn-primary-200);
  color: var(--tn-primary-700);
  font-weight: 600;
}
.tn-settings-sibling i {
  opacity: 0.75;
}

/* The screen's glyph beside its title, tying the rail, the strip and the page
   to one another. */
.tn-page-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-inline-end: var(--tn-space-2);
  color: var(--tn-primary-700);
}

/* ---- Quick-jump palette (Ctrl/Cmd+K) -----------------------------------
   z-index ladder in use elsewhere: topbar 1030, backdrop 1035, sidebar 1040,
   Nestor launcher 1041 / backdrop 1042 / panel 1045. The palette sits above
   all of them -- it is the one surface that must be reachable from whatever
   else is open.

   The launcher is bottom-CENTRE because bottom-right is the Nestor assistant's
   and two floating pills in one corner is how you get a product where neither
   can be hit. It is desktop-only: below 992px the bottom navigation owns that
   strip, and a pill floating over it would cover a primary control. */
.tn-palette-launcher {
  position: fixed;
  left: 50%;
  bottom: var(--tn-space-5);
  transform: translateX(-50%);
  z-index: 1050;
  display: inline-flex;
  align-items: center;
  gap: var(--tn-space-2);
  padding: var(--tn-space-2) var(--tn-space-5);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-pill);
  background: var(--tn-surface);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-sm);
  font-weight: 500;
  box-shadow: var(--tn-shadow-lg);
  transition: var(--tn-transition);
}
.tn-palette-launcher:hover {
  background: var(--tn-surface-alt);
  color: var(--tn-text);
  border-color: var(--tn-primary-200);
}
.tn-palette-launcher i {
  color: var(--tn-primary);
}
.tn-palette-kbd {
  padding: 0 var(--tn-space-1);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-sm);
  background: var(--tn-surface-alt);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
  line-height: 1.5;
}
.tn-palette-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1051;
  background: rgba(15, 23, 42, 0.55);
}

/* Anchored high rather than centred: the list grows downward as you type, and
   a vertically-centred panel would slide the input under the reader's eyes
   while they are using it. */
.tn-palette-panel {
  position: fixed;
  top: 12vh;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1052;
  width: min(640px, calc(100vw - var(--tn-space-8)));
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-card);
  background: var(--tn-surface);
  box-shadow: var(--tn-shadow-lg);
}
.tn-palette-input-wrap {
  display: flex;
  align-items: center;
  gap: var(--tn-space-3);
  padding: var(--tn-space-4) var(--tn-space-5);
  border-bottom: 1px solid var(--tn-border);
  color: var(--tn-text-muted);
}
.tn-palette-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  color: var(--tn-text);
  font-size: var(--tn-text-lg);
}
.tn-palette-input:focus {
  outline: none;
}
.tn-palette-close {
  border: 0;
  background: none;
  padding: 0;
}
.tn-palette-list {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--tn-space-2);
}
/* While a newer response is in flight the previous rows stay put and dim,
   rather than blanking. A list that empties on every keystroke reads as
   "nothing matches" for as long as the request takes. */
.tn-palette-list.is-stale {
  opacity: 0.55;
}
.tn-palette-row {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-space-3);
  padding: var(--tn-space-2) var(--tn-space-3);
  border-radius: var(--tn-radius);
  color: var(--tn-text);
  text-decoration: none;
}
/* The binding for a destination that has one. Pushed to the far end and
   nudged down to sit on the title's line, since the row aligns to flex-start
   so a two-line result reads from the top. */
.tn-palette-row-kbd {
  margin-inline-start: auto;
  flex: 0 0 auto;
  margin-top: 2px;
  height: 16px;
  line-height: 16px;
  padding: 0 4px;
  font-size: 0.6875rem;
  white-space: nowrap;
}
/* Hover and the keyboard highlight are the SAME state, not two competing
   ones -- app.js moves the highlight on mousemove so there is never more than
   one row claiming to be selected. */
.tn-palette-row.is-active,
.tn-palette-row:hover {
  background: var(--tn-primary-50);
  color: var(--tn-primary-700);
}
.tn-palette-row-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 20px;
  width: 20px;
  padding-top: 2px;
  opacity: 0.7;
}
.tn-palette-row-main {
  min-width: 0;
  display: grid;
  gap: 2px;
}
.tn-palette-row-title {
  display: flex;
  align-items: center;
  gap: var(--tn-space-2);
  font-size: var(--tn-text-sm);
  font-weight: 600;
}
/* The path is what answers "where do I go", so it must not truncate -- the
   same reasoning the settings search results block records. */
.tn-palette-row-path {
  font-size: var(--tn-text-xs);
  color: var(--tn-text-muted);
}
.tn-palette-row-help {
  font-size: var(--tn-text-xs);
  color: var(--tn-text-soft);
}
.tn-palette-row.is-active .tn-palette-row-path,
.tn-palette-row.is-active .tn-palette-row-help {
  color: inherit;
  opacity: 0.85;
}
.tn-palette-note {
  margin: 0;
  padding: var(--tn-space-4) var(--tn-space-3);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-sm);
}
.tn-palette-foot {
  display: flex;
  align-items: center;
  gap: var(--tn-space-4);
  padding: var(--tn-space-2) var(--tn-space-5);
  border-top: 1px solid var(--tn-border);
  background: var(--tn-surface-alt);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
}
.tn-palette-foot-spacer {
  margin-inline-start: auto;
}
/* Below the rail breakpoint the settings rail is a static jump menu, so the
   scroll box it needs on desktop would trap the page's own scroll. */
@media (max-width: 991.98px) {
  .tn-settings-rail {
    max-height: none;
    overflow-y: visible;
  }
  /* The bottom navigation owns this strip below 992px, so the pill sits above
     it rather than over a primary control. Hiding it here instead would take
     the palette away from exactly the viewport that most needs a shortcut. */
  .tn-palette-launcher {
    bottom: calc(56px + var(--tn-space-3) + env(safe-area-inset-bottom, 0px));
    padding: var(--tn-space-2) var(--tn-space-4);
  }
  .tn-palette-launcher-text {
    display: none;
  }
  .tn-palette-panel {
    top: var(--tn-space-4);
    max-height: calc(100vh - var(--tn-space-8));
  }
  .tn-palette-foot {
    display: none;
  }
  /* The list is a wrapping chip row down here, not a stack, so the indent that
     lines children up under their group's label has nothing to line up with. */
  .tn-settings-group > .tn-settings-list > li > .tn-settings-link {
    padding-inline-start: var(--tn-space-3);
  }
}

/* A group's children line up under its label rather than under its chevron,
   which is what makes the second level legible without a guide line. */
.tn-settings-group > .tn-settings-list > li > .tn-settings-link {
  padding-inline-start: var(--tn-space-6);
}

/* ---------------------------------------------------------------------------
   Setup wizard — the module picker's cards.

   Tokens only (ADR 0063): no literal colours, radii or spacing, so the grid
   follows the theme and both colour schemes without a second definition.

   The SELECTED state is driven by :has() on the real checkbox rather than by a
   JS-toggled class. A mirrored class has two sources of truth and they drift —
   most visibly when the browser restores a checked box on back-navigation and
   nothing runs to re-apply the class.
   --------------------------------------------------------------------------- */
.tn-setup-cards {
  display: grid;
  gap: var(--tn-space-3);
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
}

.tn-setup-card {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--tn-space-3);
  padding: var(--tn-space-4);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  cursor: pointer;
  transition: border-color var(--tn-duration) var(--tn-ease),
              box-shadow var(--tn-duration) var(--tn-ease);
}

.tn-setup-card:hover {
  border-color: var(--tn-border-strong);
  box-shadow: var(--tn-shadow-xs);
}

/* The checkbox stays a real, focusable, labelled control — it is moved to the
   corner, never hidden, so keyboard and screen-reader users get the same
   affordance the pointer does. */
.tn-setup-card__check {
  position: absolute;
  top: var(--tn-space-3);
  right: var(--tn-space-3);
  margin: 0;
}

.tn-setup-card:has(.tn-setup-card__check:checked) {
  border-color: var(--tn-primary);
  box-shadow: 0 0 0 1px var(--tn-primary);
}

.tn-setup-card:has(.tn-setup-card__check:focus-visible) {
  outline: var(--tn-focus-outline-width) solid var(--tn-focus-ring-color);
  outline-offset: 2px;
}

.tn-setup-card__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--tn-radius-sm);
  background: var(--tn-surface-sunk);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-lg);
}

.tn-setup-card:has(.tn-setup-card__check:checked) .tn-setup-card__icon {
  background: var(--tn-muted-bg);
  color: var(--tn-primary);
}

.tn-setup-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--tn-space-1);
  min-width: 0;
  /* Room for the checkbox so a long title never runs under it. */
  padding-inline-end: var(--tn-space-5);
}

.tn-setup-card__title {
  font-weight: 600;
  color: var(--tn-text);
}

.tn-setup-card__blurb {
  font-size: var(--tn-text-sm);
  color: var(--tn-text-muted);
}

/* Core HR: shown so the customer can see what they are getting, with no
   control, because absence of a switch row IS its on-state. */
.tn-setup-card--fixed {
  cursor: default;
  background: var(--tn-surface-alt);
}

/* Alpine sets x-cloak until it boots; without this the GSTIN field flashes on
   screen for non-Indian customers on every load of the company step. */
[x-cloak] {
  display: none !important;
}

/* ---- Pre-boarding document preview ------------------------------ */
/* The document a new hire is about to sign, shown beside the signature box
   rather than behind a link to a new tab. Tall enough that a page of a
   contract is readable without scrolling the frame itself, capped against the
   viewport so it cannot push the signing controls off a laptop screen -- the
   whole point is that both are visible at once. */
.tn-doc-preview {
  display: block;
  height: min(60vh, 32rem);
  background: var(--tn-surface-alt);
}

/* ===========================================================================
   Keyboard-shortcut help overlay ("?")

   Deliberately plainer than the quick-jump palette: that one is a tool you work
   inside, this one is a card you read once and dismiss. Same tokens, same
   z-index band, no search box and no result highlighting.

   Visibility is the `hidden` attribute rather than a class, so the dialog is
   inert to a screen reader as well as invisible — see partials/_shortcut_help.html.
   =========================================================================== */
.tn-shortcut-help[hidden] {
  display: none;
}
.tn-shortcut-help-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1051;
  background: rgba(15, 23, 42, 0.55);
}
.tn-shortcut-help-panel {
  position: fixed;
  top: 12vh;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1052;
  width: min(480px, calc(100vw - var(--tn-space-8)));
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: var(--tn-space-4);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-card);
  background: var(--tn-surface);
  box-shadow: var(--tn-shadow-lg);
}
.tn-shortcut-help-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tn-space-3);
  margin-bottom: var(--tn-space-3);
}
.tn-shortcut-help-title {
  margin: 0;
  font-size: var(--tn-text-lg);
  font-weight: 600;
}
.tn-shortcut-help-list {
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}
/* The key caps line up as a column on the right: the point of this card is to
   be scanned for one row, not read top to bottom. */
.tn-shortcut-help-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--tn-space-3);
  padding: var(--tn-space-2) 0;
  border-bottom: 1px solid var(--tn-border-subtle);
}
.tn-shortcut-help-row:last-child {
  border-bottom: 0;
}
.tn-shortcut-help-screen {
  min-width: 0;
}
/* Which section it lives in, for the handful of labels that are ambiguous on
   their own ("Reports", "Documents"). */
.tn-shortcut-help-where {
  display: block;
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
}
.tn-shortcut-help-note {
  margin: var(--tn-space-3) 0 0;
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
}

@media (max-width: 575.98px) {
  .tn-shortcut-help-panel {
    top: var(--tn-space-4);
    width: calc(100vw - var(--tn-space-4));
  }
}

/* ---- Roles & permissions matrix (settings/roles) --------------- */
/* A spreadsheet, so it behaves like one: the role row and the Permission
   column stay put while the grid scrolls under them. Both stick to
   .tn-matrix-scroll, NOT the page -- any overflow wrapper is a scroll container
   on both axes, which is why the old header scrolled away with the rows. The
   height cap makes that box the thing that scrolls vertically; 13rem leaves
   room for the topbar, the module tabs and the sticky save bar below. */
.tn-matrix-scroll {
  overflow: auto;
  max-height: calc(100vh - var(--tn-topbar-height) - 13rem);
  max-height: calc(100dvh - var(--tn-topbar-height) - 13rem);
  min-height: 16rem;
  overscroll-behavior: contain;
  border-radius: inherit;
}
/* `separate` so the borders travel with the sticky cells; under the default
   `collapse` the header's rule stays behind and rows show through the gap. */
.tn-matrix {
  border-collapse: separate;
  border-spacing: 0;
}
.tn-matrix thead th {
  position: sticky;
  top: 0;
  z-index: 2;
}
.tn-matrix tbody td:first-child,
.tn-matrix thead th:first-child {
  position: sticky;
  inset-inline-start: 0;
  z-index: 1;
  min-width: 16rem;
  max-width: 22rem;
  white-space: normal;
  box-shadow: inset -1px 0 0 var(--tn-border);
}
[dir="rtl"] .tn-matrix tbody td:first-child,
[dir="rtl"] .tn-matrix thead th:first-child {
  box-shadow: inset 1px 0 0 var(--tn-border);
}
/* The corner is pinned on both axes, so it sits above both. */
.tn-matrix thead th:first-child {
  z-index: 3;
}
/* Row tracking across a wide grid: without it the eye loses the row
   somewhere around the sixth role column. */
.tn-matrix tbody tr:hover > td {
  --bs-table-bg: var(--tn-surface-sunk);
}
/* Beats Bootstrap's `.table > * > * > *` padding without !important. */
.tn-matrix td.tn-matrix-cell {
  padding: 0;
}
.tn-matrix-hit {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.75rem;
  min-width: 5rem;
  margin: 0;
  cursor: pointer;
}
.tn-matrix-hit:has(input:disabled) {
  cursor: not-allowed;
}
/* An edit not yet saved is marked where it was made. */
/* Out-specifies the hover rule, so a changed cell stays marked under it. */
.tn-matrix tbody tr > td.is-changed {
  --bs-table-bg: var(--tn-warn-bg);
}

/* Save bar: sticks to the bottom of the viewport while its form is on screen,
   so Save never needs a scroll to the end of a long tab. Below lg the fixed
   bottom tab bar (56px) owns the viewport's last strip, so it sits above it. */
.tn-savebar {
  position: sticky;
  bottom: var(--tn-space-3);
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tn-space-3);
  padding: var(--tn-space-3) var(--tn-space-4);
  background: var(--tn-surface);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-card);
  box-shadow: var(--tn-shadow-lg);
}
.tn-savebar.is-dirty .tn-savebar-status {
  color: var(--tn-warn) !important;
  font-weight: 600;
}
@media (max-width: 991.98px) {
  .tn-savebar {
    bottom: calc(56px + var(--tn-space-2) + env(safe-area-inset-bottom, 0px));
  }
}

/* ---- Notification bell (ADR 0091) -------------------------------
   Topbar bell, its unread badge and the dropdown list. Tokens only, so
   dark mode follows the theme with no extra rules. */
.tn-bell-badge {
  position: absolute;
  top: -4px;
  inset-inline-end: -4px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-danger);
  color: #fff;
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 18px;
  text-align: center;
}
.tn-bell-menu {
  width: min(24rem, calc(100vw - 2rem));
  max-height: min(32rem, 75vh);
  overflow-y: auto;
}
.tn-bell-head { border-bottom: 1px solid var(--tn-border); }
.tn-bell-open {
  display: flex;
  align-items: flex-start;
  gap: var(--tn-space-2);
  white-space: normal;
  border-bottom: 1px solid var(--tn-border);
}
.tn-bell-text { display: flex; flex-direction: column; min-width: 0; }
.tn-bell-title { overflow-wrap: anywhere; }
.tn-bell-item.is-unread .tn-bell-title,
.tn-notif-row.is-unread .tn-notif-title { font-weight: 600; }
.tn-bell-meta { font-size: 0.75rem; color: var(--tn-text-muted); }
.tn-bell-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  margin-top: 0.45rem;
  border-radius: 50%;
  background: var(--tn-primary);
}
.tn-notif-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--tn-space-2);
  padding: var(--tn-space-3) var(--tn-space-4);
  border-bottom: 1px solid var(--tn-border);
}
.tn-notif-row:last-child { border-bottom: 0; }
.tn-notif-row.is-unread { background: var(--tn-surface-alt); }
.tn-notif-main { display: flex; gap: var(--tn-space-2); min-width: 0; }

/* ---- Environment pill (partials/_env_pill.html) ------------------
   Hangs from the top edge so it reads as part of the window frame, not the
   page. pointer-events: none — it must never sit on top of a control. */
.tn-env-pill {
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1300;
  padding: 0.1rem 0.75rem 0.15rem;
  border-radius: 0 0 0.6rem 0.6rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  line-height: 1.4;
  pointer-events: none;
  user-select: none;
  background: var(--tn-muted-bg);
  color: var(--tn-text);
  box-shadow: var(--tn-shadow-xs);
}
.tn-env-pill--dev  { background: var(--tn-info-bg);    color: var(--tn-info);    box-shadow: inset 0 0 0 1px rgba(var(--tn-info-rgb), 0.35); }
.tn-env-pill--uat  { background: var(--tn-warn-bg);    color: var(--tn-warn);    box-shadow: inset 0 0 0 1px rgba(var(--tn-warn-rgb), 0.35); }
.tn-env-pill--prod { background: var(--tn-danger-bg);  color: var(--tn-danger);  box-shadow: inset 0 0 0 1px rgba(var(--tn-danger-rgb), 0.35); }
.tn-env-pill--local { background: var(--tn-success-bg); color: var(--tn-success); box-shadow: inset 0 0 0 1px rgba(var(--tn-success-rgb), 0.35); }
@media print { .tn-env-pill { display: none; } }

/* ---- Payroll runs list (/payroll/) ------------------------------
   The list's own pieces; the progress bar and the status badge are the run
   page's. Tokens only, so dark mode follows. */
/* A failed background draft build: list-only (run_list.PROBLEM). */
.tn-progress-step.is-problem::before { background: var(--tn-danger); }
.tn-progress-step.is-problem .tn-progress-step__label,
.tn-progress-step.is-problem .tn-progress-step__note { color: var(--tn-danger); font-weight: 600; }

/* "N things need you": one line per item, its one link on the right. */
.tn-attention {
  background: var(--tn-warn-bg);
  border: 1px solid rgba(var(--tn-warn-rgb), 0.35);
  border-radius: var(--tn-radius-md);
  padding: var(--tn-space-3) var(--tn-space-4);
}
.tn-attention__title {
  font-size: var(--tn-text-sm);
  font-weight: 600;
  color: var(--tn-warn);
  margin: 0 0 var(--tn-space-2);
}
.tn-attention__list { list-style: none; margin: 0; padding: 0; font-size: var(--tn-text-sm); }
.tn-attention__item {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--tn-space-3);
  padding: var(--tn-space-2) 0;
  border-top: 1px solid rgba(var(--tn-warn-rgb), 0.2);
}
.tn-attention__action { flex-shrink: 0; }

/* History: a financial-year header row, and rows that open their run. */
.tn-table-group > th {
  background: var(--tn-muted-bg);
  color: var(--tn-text-muted);
  font-size: var(--tn-text-xs);
  font-weight: 600;
}
.tn-row-link { position: relative; }

/* No runs yet: three numbered steps. */
.tn-first-run { max-width: 40rem; }
.tn-first-run__steps { list-style: none; margin: 0; padding: 0; }
.tn-first-run__step {
  display: flex;
  align-items: center;
  gap: var(--tn-space-3);
  padding: var(--tn-space-3) 0;
  border-top: 1px solid var(--tn-border);
}
.tn-first-run__body { flex: 1; min-width: 0; }
.tn-first-run__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--tn-radius-pill);
  background: var(--tn-info-bg);
  color: var(--tn-info);
  font-size: var(--tn-text-xs);
  font-weight: 600;
}
.tn-first-run__step.is-done .tn-first-run__mark { background: var(--tn-success-bg); color: var(--tn-success); }

/* Holiday policy picker (calendars/widgets/holiday_eligibility.html).
 * `[hidden]` first and with !important: the toolbar, the out-of-year banner and
 * the per-calendar tools are all `d-flex`, and Bootstrap's
 * `.d-flex { display:flex !important }` beat the attribute -- so the banner
 * could never be dismissed and "Untick them" looked like it did nothing. */
.tn-holiday-eligibility [hidden] { display: none !important; }
.tn-holiday-search { max-width: 20rem; }
.tn-holiday-group-head { background: var(--bs-tertiary-bg); border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0; }
.tn-holiday-group > .list-group { max-height: 26rem; overflow-y: auto; }
.tn-holiday-row { cursor: pointer; padding-top: .45rem; padding-bottom: .45rem; }
.tn-holiday-row .form-check-input { flex-shrink: 0; margin: 0; }
.tn-holiday-row:has(.form-check-input:checked) { background: var(--bs-primary-bg-subtle); }
.tn-holiday-date { min-width: 6.5rem; font-variant-numeric: tabular-nums; }

/* ---- Employee avatar: profile photo, initials when there is none ----------
 * people/photo/_avatar.html. The initials are always rendered UNDER the photo,
 * so a photo that fails to load (app.js removes a broken .tn-avatar-img)
 * falls back to them without a round-trip. Tokens only, so dark mode is free:
 * --tn-primary and --tn-primary-rgb are both redefined under [data-theme=dark]. */
.tn-avatar {
  --tn-avatar-size: 2.5rem;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--tn-avatar-size);
  height: var(--tn-avatar-size);
  border-radius: 50%;
  overflow: hidden;
  background: rgba(var(--tn-primary-rgb), 0.14);
  color: var(--tn-primary);
  font-weight: 600;
  font-size: calc(var(--tn-avatar-size) * 0.38);
  line-height: 1;
  letter-spacing: 0.02em;
  user-select: none;
  box-shadow: 0 0 0 1px var(--tn-border);
}
.tn-avatar-sm { --tn-avatar-size: 2rem; }
.tn-avatar-lg { --tn-avatar-size: 4rem; }
.tn-avatar-xl { --tn-avatar-size: 6rem; }
@media (min-width: 768px) { .tn-avatar-xl { --tn-avatar-size: 8rem; } }
.tn-avatar-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--tn-surface);
}

/* ---- Profile photo manager (people/photo/_photo_manager.html) ------------ */
.tn-photo-manager [hidden] { display: none !important; }
.tn-photo-consent {
  background: var(--tn-surface-alt);
  border: 1px solid var(--tn-border);
  border-radius: var(--tn-radius-md);
  padding: 0.75rem 1rem;
}
.tn-photo-crop {
  position: relative;
  width: min(20rem, 100%);
  aspect-ratio: 1 / 1;
  margin: 0 auto;
  overflow: hidden;
  border-radius: var(--tn-radius-md);
  background: var(--tn-surface-sunk);
  touch-action: none;
  cursor: grab;
}
.tn-photo-crop:active { cursor: grabbing; }
.tn-photo-crop:focus-visible { outline: 3px solid rgba(var(--tn-primary-rgb), 0.5); outline-offset: 2px; }
.tn-photo-crop img {
  position: absolute;
  top: 0;
  left: 0;
  max-width: none;
  transform-origin: 0 0;
  pointer-events: none;
  user-select: none;
}
/* The circle the avatar will show, over a dimmed square. */
.tn-photo-crop::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: 0 0 0 999px rgba(0, 0, 0, 0.45);
  border: 2px solid rgba(255, 255, 255, 0.85);
  pointer-events: none;
}
