/* Client portal styles.
   Served from the app origin so the Content-Security-Policy can use
   `style-src 'self'` without `'unsafe-inline'`. All page-specific rules live
   here too (scoped by class), so no template needs an inline <style> block.

   The console has its own copy of this file. They started identical and are
   diverging on purpose: the portal is read by clients, the console is operated
   by staff, and a change made for one is not automatically right for the other.
   Anything genuinely shared belongs in webcore/static/charts.css. */

:root {
  /* The brand teal, kept for tints, the logo lockup and decorative fills. It
     is too light to carry text or a solid control — 2.3:1 on white — so it is
     never the colour of something you can click. */
  --luci-accent: #6AC8C2;
  /* The interactive step of the same hue, and the one that does the work:
     5.1:1 on white as text, 5.1:1 the other way with white on top, so one
     value serves links, active states and filled buttons alike. */
  --luci-accent-dark: #0E7C74;
  /* Hover for anything filled with --luci-accent-dark. A step down rather than
     up: a filled control that lightens on hover reads as going inactive. */
  --luci-accent-deeper: #0A5F59;
  /* One tint, replacing the rgba(106, 200, 194, 0.16) that was written out at
     six call sites. Mixed from the interactive teal, not the pale one, so an
     active row and its label are the same hue. */
  --luci-accent-tint: rgba(14, 124, 116, 0.09);
  /* The same tint a step up, for marking a selection on the nav rail rather
     than on white. The rail is itself teal-tinted, so the light step lands
     within a few percent of its background and the current page stops being
     obvious; over white the light step is still the right weight. */
  --luci-accent-tint-strong: rgba(14, 124, 116, 0.2);
  --luci-ink: #141B1F;
  --luci-muted: #5E6E76;
  /* Hairline and fills, biased very slightly toward the teal rather than the
     blue-grey they used to be — #D9E2EC read cold beside the accent. */
  --luci-border: #E4E9EA;
  --luci-surface: #FFFFFF;
  /* A fill, not the page's ground: hover washes, code blocks, muted pills.
     The ground is --luci-canvas below. */
  --luci-bg: #F1F4F4;
  --luci-error: #A40000;
  --luci-success: #005A00;
  /* Chart/status colors, validated for CVD separation and contrast on white.
     The brand teal is too light and low-chroma to carry chart marks, so marks
     use a darker step of the same hue; the accent stays for interactive UI. */
  --luci-warn: #9A6700;
  --luci-chart: #0D9488;
  --luci-chart-soft: #9AA7B4;
  /* Shared by the sidebar and by the blank column held open in its place, so
     the picker and the workspace it leads to cannot drift apart by a rem. */
  --luci-sidebar-width: 13rem;

  /* The page's ground, and the tint that separates the nav rail from it.
     Content sits directly on the canvas — there is no card around the page.

     The rail carries an actual tint of the brand hue rather than a neutral
     grey. At #F7F9F9 it was two shades off white: on a white canvas it read as
     a faint smudge rather than a column, and the nav is the one piece of
     furniture on the page that should be legible as a place. Its border is
     darker than the content hairline for the same reason — the rail is a
     region, not a card. */
  --luci-canvas: #FFFFFF;
  --luci-rail: #E9F1F0;
  --luci-rail-border: #D5E3E1;

  /* A radius scale rather than one value: 0.5rem for panels and cards,
     0.3125rem for controls, 0.125rem for the smallest chips; pills stay fully
     round because that is a shape, not a corner. Every element used to be
     1rem, which put the same corner on a full-width panel and a 24px button
     and so read as no decision at all. */
  --luci-radius-lg: 0.5rem;
  --luci-radius-md: 0.3125rem;
  --luci-radius-sm: 0.125rem;

  /* Elevation means "this floats above the page" — menus, dialogs, toasts —
     and nothing else. The sidebar, the app bar and the content panel are part
     of the page and no longer cast a shadow. */
  --luci-lift: 0 10px 28px rgba(20, 27, 31, 0.12);
}

* { box-sizing: border-box; }

body {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  margin: 0;
  min-height: 100vh;
  line-height: 1.5;
  color: var(--luci-ink);
  background: var(--luci-canvas);
  -webkit-font-smoothing: antialiased;
}

/* Base link style. Without this, any anchor not caught by a container rule
   (.sidebar a, table.data a, .breadcrumb a, …) falls back to the browser's
   blue/purple default — visibly foreign to the teal palette. This is the
   inline tier of the link system (see the two-tier note on table.data links):
   inline/secondary links carry the accent; structural links override to ink. */
a {
  color: var(--luci-accent-dark);
  text-decoration: none;
}

a:hover { text-decoration: underline; }

/* The `hidden` attribute has to win, everywhere.

   The UA stylesheet's `[hidden] { display: none }` is a type-level rule, so any
   class that sets `display` outweighs it — `.sidebar a { display: block }` and
   `.impersonation-filter { display: flex }` both do, and both carry markup that
   ships hidden and is revealed by app.js. Without this, an element the server
   marked hidden renders anyway and the reader sees rows that were meant to be
   held back until JavaScript decided otherwise. */
[hidden] { display: none !important; }

/* Chrome links (nav, buttons, tabs, row-action + profile dropdowns, the logo,
   the health summary) give their own hover feedback — a background or colour
   shift — so the inline underline above would be wrong on them. Each selector
   here outweighs `a:hover`, neutralising it for that property only; the inline
   text links that should underline (breadcrumb, rowlink, queue) declare it in
   their own :hover and are deliberately absent. */
.sidebar a:hover,
.mainnav a:hover,
.brand:hover,
.button:hover,
.profile-dropdown a:hover,
.actions-dropdown a:hover,
.tabs a:hover,
a.health-summary:hover { text-decoration: none; }

.page {
  margin: 0 auto;
  max-width: 28rem;
  padding: 2.5rem 1.25rem 3rem;
}

.page--wide {
  max-width: 60rem;
}

/* Fluid full-width layout: expand to the viewport, keeping side gutters.
   Used for wide data tables (e.g. the schedule list). */
.page--full {
  max-width: none;
  padding-left: 2rem;
  padding-right: 2rem;
}

/* --- Shell: sidebar + page (signed-in layout) --- */
/* Not centred: the sidebar anchors to the left edge so it never shifts as
   pages of different widths are opened; page width caps left-align beside it. */
.shell {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
  padding: 1.5rem 2rem 3rem;
}

/* Inside the shell the page no longer centres itself; width caps still apply. */
.shell .page {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0;
}


/* Holds the sidebar's column open when there is nothing to draw in it — the
   scope picker, where no scope has been chosen so there is no workspace to
   navigate. Empty space rather than an empty card: the reader should see the
   page keep its shape, not a blank panel implying something failed to load.
   Presentational only, so it stays out of the accessibility tree. */
.sidebar-gap {
  flex: none;
  width: var(--luci-sidebar-width);
}

/* A tinted rail, not a floating card. The page it sits beside is now the white
   canvas itself, so the sidebar separates from it by a shade and a hairline;
   the shadow it used to cast implied it hovered over a page it is level with. */
.sidebar {
  position: sticky;
  top: 1.5rem;
  flex: none;
  width: var(--luci-sidebar-width);
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  padding: 0.5rem;
  background: var(--luci-rail);
  border: 1px solid var(--luci-rail-border);
  border-radius: var(--luci-radius-lg);
}

.sidebar a {
  display: block;
  padding: 0.5rem 0.75rem;
  border-radius: var(--luci-radius-md);
  text-decoration: none;
  color: var(--luci-muted);
  font-weight: 600;
  font-size: 0.9375rem;
}

/* The rail is already tinted, so a hover that fills with the same family of
   grey would be invisible on it — hover goes to the surface white instead,
   which is the one value guaranteed to contrast with the rail. */
.sidebar a:hover { background: var(--luci-surface); color: var(--luci-ink); }
.sidebar a.active { background: var(--luci-accent-tint-strong); color: var(--luci-accent-dark); }

/* Outbound row (Power BI). CSS-only glyph so the CSP needs nothing new; the
   visible label stays the section name and the screen-reader span in the
   markup says it opens in a new tab. */
.sidebar a.external::after {
  content: " \2197";
  font-weight: 400;
  opacity: 0.7;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Group heading. A <p> rather than a heading element: it labels a list of
   links, it is not a landmark, and an <h2> here would land between the page's
   own headings in a screen reader's outline. */
.sidebar-label {
  margin: 0.25rem 0 0.375rem;
  padding: 0 0.75rem;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--luci-muted);
}

/* Collapsible section. <details> carries the open/closed state itself, so the
   Content-Security-Policy never has to admit a toggle script — see the note in
   base.html. */
.sidebar-group > summary {
  display: block; /* suppresses the default disclosure marker in Safari */
  padding: 0.5rem 0.75rem;
  border-radius: var(--luci-radius-md);
  color: var(--luci-muted);
  font-weight: 600;
  font-size: 0.9375rem;
  cursor: pointer;
  list-style: none;
}

/* The other half of hiding the default marker: Safari draws it through this
   pseudo-element, which `list-style: none` alone does not reach. */
.sidebar-group > summary::-webkit-details-marker { display: none; }

.sidebar-group > summary::after {
  content: "›";
  float: right;
  transition: transform 0.15s ease;
  display: inline-block;
}

.sidebar-group[open] > summary::after { transform: rotate(90deg); }

.sidebar-group > summary:hover { background: var(--luci-surface); color: var(--luci-ink); }
.sidebar-group > summary.active { color: var(--luci-accent-dark); }

/* Children are indented against a rule rather than by padding alone, so the
   nesting survives the row-wrapped mobile layout below. */
.sidebar-sub {
  margin: 0.125rem 0 0.25rem 0.75rem;
  padding-left: 0.5rem;
  border-left: 1px solid var(--luci-rail-border);
  display: flex;
  flex-direction: column;
  gap: 0.0625rem;
}

.sidebar-sub a {
  padding: 0.375rem 0.625rem;
  font-weight: 500;
  font-size: 0.875rem;
}

/* Nothing here marks a section as unbuilt: every row is --luci-muted, at 4.6:1
   against the rail. The two devices that used to do it are both gone and should
   not come back in the same form — see the note in base.html for why a dim
   cannot be both legible and distinguishable, and why the badge that replaced
   it was dropped. */

@media (prefers-reduced-motion: reduce) {
  .sidebar-group > summary::after { transition: none; }
}

@media (max-width: 52rem) {
  .shell {
    flex-direction: column;
    padding: 1rem 1rem 2.5rem;
    gap: 1rem;
  }
  .sidebar {
    position: static;
    width: 100%;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
  }
  /* Nothing to hold open once the shell stacks: the sidebar is a row of chips
     above the content here, so the placeholder would be an empty block and a
     gap pushing the page down for no reason. */
  .sidebar-gap { display: none; }
  /* The label names a column; wrapped into a row of chips it names nothing, so
     it comes out rather than sitting mid-row as a stray word. */
  .sidebar-label { display: none; }
  /* A collapsed group is a chip beside the flat links; an open one needs the
     full width for its children, which is what the basis flip does. */
  .sidebar-group { flex: 0 0 auto; }
  .sidebar-group[open] { flex: 1 0 100%; }
}

/* --- App bar (top menu + profile) --- */
/* The hairline is the whole separation. The 1px shadow under it was doing the
   same job a second time, and on a white canvas it read as a smudge. */
.appbar {
  background: var(--luci-surface);
  border-bottom: 1px solid var(--luci-border);
}

.appbar-inner {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.625rem 2rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  text-decoration: none;
  color: var(--luci-ink);
}

.brand-logo {
  display: block;
  height: 1.75rem;
  width: auto;
  border: 0;
  margin: 0;
}

.brand-tag {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--luci-muted);
}

/* Environment badge: quiet on dev and local, unmissable on prod. It replaces
   the ceremony the TUI's --config flag used to provide — on prod the badge is
   the one visual cue that a queued job will run against client data. */
.env-badge {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  padding: 0.1875rem 0.5625rem;
  border-radius: 999px;
  border: 1px solid var(--luci-border);
  color: var(--luci-muted);
  background: transparent;
  white-space: nowrap;
}

.env-badge--prod {
  color: #fff;
  background: var(--luci-error);
  border-color: var(--luci-error);
}

.mainnav {
  display: flex;
  gap: 0.25rem;
  margin-left: 0.5rem;
}

.mainnav--end { margin-left: auto; }

.mainnav a {
  padding: 0.4rem 0.75rem;
  border-radius: var(--luci-radius-md);
  text-decoration: none;
  color: var(--luci-muted);
  font-weight: 600;
  font-size: 0.9375rem;
}

.mainnav a:hover { background: var(--luci-bg); color: var(--luci-ink); }
.mainnav a.active { background: var(--luci-accent-tint); color: var(--luci-accent-dark); }

.profile-menu { margin-left: auto; position: relative; }

.profile-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
}
.profile-menu > summary::-webkit-details-marker { display: none; }
.profile-menu > summary::marker { content: ""; }

/* The initial is a control that opens a menu, so it takes the interactive teal
   and white on top rather than the pale brand fill with ink on it — that pair
   was 1.9:1 and the one letter in it was the least legible glyph on the page. */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  background: var(--luci-accent-dark);
  color: #FFFFFF;
  font-weight: 700;
  font-size: 0.9375rem;
}

.profile-dropdown {
  position: absolute;
  right: 0;
  top: calc(100% + 0.5rem);
  min-width: 14rem;
  background: var(--luci-surface);
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-lg);
  box-shadow: var(--luci-lift);
  padding: 0.5rem;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.profile-id {
  margin: 0.25rem 0.5rem 0.5rem;
  padding-bottom: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  align-items: flex-start;
  border-bottom: 1px solid var(--luci-border);
}

.profile-email { font-weight: 600; font-size: 0.875rem; word-break: break-all; }

.profile-dropdown a,
.profile-dropdown .linklike {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem;
  margin: 0;
  border: 0;
  border-radius: var(--luci-radius-md);
  background: transparent;
  color: var(--luci-ink);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}

.profile-dropdown a:hover,
.profile-dropdown .linklike:hover { background: var(--luci-bg); }
.profile-dropdown form { margin: 0; }

/* --- Breadcrumb --- */
.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
  margin-bottom: 1.25rem;
  font-size: 0.875rem;
  color: var(--luci-muted);
}

.breadcrumb a { color: var(--luci-muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--luci-ink); text-decoration: underline; }
.breadcrumb .sep { color: var(--luci-border); }
.breadcrumb [aria-current="page"] { color: var(--luci-ink); font-weight: 600; }

/* Signed-out pages — sign in, MFA, set password — are a single narrow form on
   an otherwise empty canvas, and there the panel is genuinely a card: it gives
   the form an edge and a centre. It keeps the border and loses only the
   shadow, which was announcing a lift the card does not have. */
.panel {
  background: var(--luci-surface);
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-lg);
  padding: 1.5rem;
  /* Grow with content wider than the page's width cap (the data tables) so
     rows never bleed past the panel edge. A .table-wrap scroll container is
     not an option on those tables because the row-action dropdown menus must
     overflow the table. */
  width: fit-content;
  min-width: 100%;
}

/* Inside the shell it is not a card at all. A signed-in page already sits in a
   frame — the app bar above it and the rail beside it — so wrapping its
   contents in a second bordered, shadowed box put a card inside a card and
   left the real cards (stat tiles, chart cards, tables) a level deeper still.
   Here the content sits directly on the canvas and the tiles are the only
   things with edges, which is what makes them read as objects.

   It must not grow, either. The edge the ``width: fit-content`` above protects
   is the card's, and there is no card here — all that growth does is stretch
   every sibling of the table, the toolbar above all, to the width of the widest
   table, which puts the controls in it off the right of the screen. The table
   itself still overflows, which is what its dropdowns need; the page's chrome
   stays inside the viewport, which is what reaching it needs. */
.shell .panel {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
  width: auto;
}

.panel h1 {
  margin: 0 0 0.75rem;
  font-size: 1.4375rem;
  line-height: 1.15;
  font-weight: 800;
  letter-spacing: -0.025em;
  text-wrap: balance;
}

/* --- Impersonation bar ---
   Full width, above the appbar, on every page for as long as a staff member is
   viewing a client. Deliberately not `.banner` (an inline pill) and deliberately
   not a toast: it must not be dismissible, and it must never be ambiguous which
   identity is live. Amber rather than error red — nothing is wrong, but nothing
   on the page is your own account either. */
.impersonation-bar {
  background: var(--luci-warn);
  color: #FFFFFF;
}

/* Gutters match .appbar-inner, not a content column. The bar is chrome sitting
   directly on top of the appbar, so the stop control has to line up with the
   account avatar below it; a max-width of its own left it inset by the centring
   margin on any wide page (.page--full runs to a 2rem gutter) and read as
   misaligned against everything it touches. */
.impersonation-bar-inner {
  padding: 0.5rem 2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  align-items: center;
  justify-content: space-between;
  font-size: 0.875rem;
}

.impersonation-bar-text strong { font-weight: 700; }

/* The sentence takes the slack so the two controls sit together on the right;
   without a basis it wraps character by character as the checkbox grows. */
.impersonation-bar-text { flex: 1 1 20rem; }

/* Rendered hidden by the template and revealed by app.js — see the banner. The
   border is what stops a bare checkbox reading as part of the sentence beside
   it on a bar this colour. */
.impersonation-filter {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0.25rem 0.625rem;
  border: 1px solid rgba(255, 255, 255, 0.45);
  border-radius: var(--luci-radius-md);
  cursor: pointer;
  white-space: nowrap;
}

.impersonation-filter input {
  margin: 0;
  width: 0.9375rem;
  height: 0.9375rem;
  accent-color: #FFFFFF;
}

.impersonation-bar form { margin: 0; }

/* margin:0 is load-bearing, not tidiness. The base `button` rule sets
   margin-top: 1.25rem for buttons that end a form, and a flex item establishes
   a BFC — so that margin cannot collapse out of the <form> around this button.
   It made the form 20px taller than its button, and `align-items: center`
   centres the *form*, leaving the button pinned at its bottom edge and sitting
   visibly below the sentence beside it. The profile dropdown's .linklike
   resets margin for the same reason. */
.impersonation-bar .linklike {
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: #FFFFFF;
  font: inherit;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
}

/* The Scope picker's role control: one select in the toolbar, above the table,
   answered by whichever View is pressed.

   The global `select` rule sets width:100% with a top margin, which here would
   stretch the control across the toolbar and push its label onto its own line.
   Label and control belong on one line — the sentence is "view as a viewer" —
   so this is the flex container and the select sizes to content. */
.role-field {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.role-field label {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--luci-muted);
  white-space: nowrap;
  margin: 0;
}
.role-field select {
  width: auto;
  margin-top: 0;
  padding: 0.4rem 0.6rem;
  border-radius: var(--luci-radius-md);
}

/* Same collapsed-margin trap as the impersonation bar above: the base rule
   gives a form-ending button a 1.25rem top margin, which a table cell cannot
   collapse away, and the row would sit taller than every other. */
table.data--tight td > button[type="submit"] { margin-top: 0; }

/* Sentence case. Uppercase tracking is reserved for the two label roles that
   earn it — column heads and stat labels — and spending it here as well meant
   a standing status pill shouted louder than the data it sat above. */
.banner {
  display: inline-block;
  margin: 0 0 1rem;
  padding: 0.25rem 0.625rem;
  border-radius: 999px;
  background: var(--luci-accent-tint);
  color: var(--luci-accent-dark);
  font-size: 0.8125rem;
  font-weight: 600;
}

.error { color: var(--luci-error); margin: 0 0 1rem; }
.notice { color: var(--luci-success); margin: 0 0 1rem; }

label {
  display: block;
  margin-top: 1rem;
  font-weight: 600;
  font-size: 0.9375rem;
}

input {
  width: 100%;
  padding: 0.625rem 0.75rem;
  margin-top: 0.375rem;
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-md);
  font: inherit;
  background: #fff;
}

/* The ring is drawn in the interactive teal, not the pale brand one: at 35%
   alpha of #6AC8C2 the focused field was barely distinguishable from an
   unfocused one, which is the one state a keyboard user cannot afford to lose. */
input:focus {
  outline: 2px solid rgba(14, 124, 116, 0.4);
  border-color: var(--luci-accent-dark);
}

/* Filled controls take the interactive teal with white on it. The pale accent
   with ink text was the palest thing on the page while also being the most
   important — a primary action should not be the quietest object on screen. */
button,
.button {
  display: inline-block;
  margin-top: 1.25rem;
  padding: 0.625rem 1rem;
  border: 0;
  border-radius: var(--luci-radius-md);
  background: var(--luci-accent-dark);
  color: #FFFFFF;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}

button:hover,
.button:hover {
  background: var(--luci-accent-deeper);
}

button.secondary,
.button.secondary {
  background: transparent;
  color: var(--luci-muted);
  border: 1px solid var(--luci-border);
}

button.secondary:hover,
.button.secondary:hover {
  background: var(--luci-bg);
  color: var(--luci-ink);
}

/* Standalone destructive button (dropdown items restyle it themselves). */
button.danger {
  background: transparent;
  color: var(--luci-error);
  border: 1px solid var(--luci-error);
}

button.danger:hover {
  background: rgba(164, 0, 0, 0.08);
}

/* Checkbox row: keep the box beside its text instead of full-width. */
label.checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

label.checkbox input {
  width: auto;
  margin-top: 0;
}

code { word-break: break-all; font-size: 0.85em; }

.panel img {
  display: block;
  margin-top: 0.5rem;
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-lg);
}

.table-wrap { overflow-x: auto; }

/* Tabular figures across every data table: counts and percentages sit in
   columns and are compared down the column, which proportional digits make
   impossible — a column of 1s is visibly narrower than a column of 8s. */
table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
  font-variant-numeric: tabular-nums;
}

table.data th,
table.data td {
  padding: 0.625rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--luci-border);
  vertical-align: middle;
  white-space: nowrap;
}

/* One of the two places uppercase tracking is kept — the other is .stat-label.
   Smaller and more tightly tracked than it used to be so that "data label" is
   a voice of its own, distinct from a form label, which is now sentence case
   at reading size. Previously both were 0.8125rem/0.04em/uppercase and the
   filter row above a table was indistinguishable from the table's own head. */
table.data th {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-weight: 700;
  color: var(--luci-muted);
}

/* Denser variant for a list that is scanned and picked from rather than read
   across — the whole table wants to be visible at once. Only the padding and
   the row-action button shrink; type size and borders stay, so it still reads
   as the same table. */
table.data--tight th,
table.data--tight td {
  padding: 0.3125rem 0.75rem;
}

table.data--tight td .secondary {
  margin-top: 0;
  padding: 0.25rem 0.625rem;
  font-size: 0.8125rem;
}

/* A second line under a cell's main value: on Schedules, the operator's own
   name for a schedule beneath the report it produces. Block, because inline it
   ran on from the link beside it and read as part of the same phrase — and
   `.secondary` could not be reused for it, since that class is the small-button
   style and carries button padding. */
table.data td .cell-sub {
  display: block;
  font-size: 0.8125rem;
  color: var(--luci-muted);
}

/* Subtle zebra banding. :where() keeps specificity (0,1,2) — below the
   per-status row shading and :target/.row-target highlight, so those still win.

   Taken out during the visual refresh on the argument that the per-row hairline
   already separated rows, and put back: on a table wide enough to run past the
   viewport, the hairline tells you where a row ends but not which row your eye
   is on halfway across it. That is what the banding is for, and it is the wide
   tables — not the short ones the argument was made against — that need it. */
:where(table.data) tbody tr:nth-child(even) {
  background: rgba(20, 27, 31, 0.025);
}

/* Two-tier link colour, applied consistently across the app:
     • inline / secondary links (a jump within a cell, a hint, body copy) carry
       the accent — this rule and the base `a` rule above.
     • structural links (the primary cell that opens the row's detail page)
       carry ink — the a.rowlink rule below — so the whole row reads as one
       clickable object rather than a coloured link.
   Same table, two colours, but the difference now encodes link role. */
table.data td a:not(.button):not(.rowlink) {
  color: var(--luci-accent-dark);
  font-weight: 600;
  text-decoration: none;
}

table.data td a:not(.button):not(.rowlink):hover {
  text-decoration: underline;
}

/* Structural tier: rows open their detail page via the first column. */
table.data a.rowlink {
  font-weight: 600;
  color: var(--luci-ink);
  text-decoration: none;
}

table.data a.rowlink:hover {
  color: var(--luci-accent-dark);
  text-decoration: underline;
}

/* Whole-row click affordance. app.js adds .rowlink-row to any row it has wired
   — a row whose first cell is a primary link, or one whose action is a submit
   button — and forwards a plain click there; the pointer and a translucent
   hover wash advertise it. The wash sits on the cells (not the row) as a
   translucent layer so it stacks over the zebra banding and the per-status row
   tint rather than replacing them. */
.rowlink-row { cursor: pointer; }
.rowlink-row:hover > td { background-color: rgba(20, 27, 31, 0.045); }

.pill {
  display: inline-block;
  padding: 0.125rem 0.5rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  background: var(--luci-accent-tint);
  color: var(--luci-accent-dark);
}

.pill--muted { background: var(--luci-bg); color: var(--luci-muted); }
/* Why a page is not on the impersonated client's own menu. Staff-only, and
   deliberately quiet: it is a statement about who is looking rather than about
   the page, so it does not compete with the page's own heading. "Coming soon"
   is the loud one, and it lives beside the heading on placeholder.html because
   every reader gets it. */
.pill--off { background: var(--luci-bg); color: var(--luci-muted); border: 1px solid var(--luci-border); }
.pill--warn { background: rgba(164, 0, 0, 0.1); color: var(--luci-error); }
.pill--overdue { background: rgba(154, 103, 0, 0.12); color: var(--luci-warn); }
.pill--ok { background: rgba(0, 90, 0, 0.1); color: var(--luci-success); }

.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}

.row-actions form { display: inline; margin: 0; }

.row-actions button,
.row-actions .button {
  margin-top: 0;
  padding: 0.3125rem 0.625rem;
  font-size: 0.8125rem;
}

.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1rem;
}

/* Right-hand button group inside a toolbar; margin keeps it right-aligned
   even when it is the toolbar's only child (org tab toolbars). A full 1rem
   between controls: the user list puts a search box, a checkbox and a button
   side by side, and at 0.5rem the three ran into each other. */
.toolbar-actions { display: flex; align-items: center; gap: 1rem; margin-left: auto; }

/* Intro copy sharing a toolbar's row with the control on the right, instead of
   the control taking a row of its own and leaving a band of empty space above
   the table. Capped so a long sentence keeps its own comfortable measure rather
   than running the full page width up to the filter box. */
.toolbar-intro {
  margin: 0;
  max-width: 46rem;
}

/* "Updated Nm ago" freshness label beside the refresh button. */
.toolbar-age {
  color: var(--luci-muted);
  font-size: 0.8125rem;
  white-space: nowrap;
  margin-right: 0.25rem;
}
.toolbar-actions .button { margin-top: 0; }
.toolbar .button { margin-top: 0; }

/* Compact live-filter search box sitting in a table toolbar (overrides the
   full-width base input rule). */
.table-filter {
  width: auto;
  min-width: 15rem;
  margin-top: 0;
  padding: 0.5rem 0.7rem;
}

/* --- Confirmation dialog ---
   Replaces window.confirm (see app.js). The native dialog is drawn by the
   browser against the top of the tab next to the origin name, where it reads as
   a warning about the site rather than a question about the row just clicked,
   and no stylesheet can reach it. A modal <dialog> is centred by the UA's own
   `inset: 0; margin: auto`, which is why nothing here positions it — the rules
   below only undo the UA's border and padding and dress it as a portal card. */
.confirm-dialog {
  max-width: min(28rem, calc(100vw - 2rem));
  padding: 1.5rem;
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-lg);
  background: var(--luci-surface);
  color: var(--luci-ink);
  /* Deeper than --luci-lift on purpose: a modal is the one thing on screen
     that sits above everything, including the menus that use the shared token. */
  box-shadow: 0 20px 48px rgba(20, 27, 31, 0.2);
}

.confirm-dialog::backdrop { background: rgba(20, 27, 31, 0.45); }

.confirm-dialog-message { margin: 0; }

.confirm-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.5rem;
}

/* The base button rule's margin-top is for a button ending a form; these sit in
   a row of their own. Smaller than a page action, too: at full size two short
   words filled half the dialog and read as the weightiest thing on screen,
   which the question above them should be. */
.confirm-dialog-actions button {
  margin-top: 0;
  padding: 0.375rem 0.875rem;
  font-size: 0.875rem;
}

/* Inline "Reapply DB security" form in the Security tab toolbar. */
.reapply-form {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Icon-only toolbar buttons (tooltip carries the label). */
.button-refresh,
.button-add {
  font-size: 1.125rem;
  line-height: 1;
  padding: 0.5rem 0.7rem;
}

.field-hint { color: var(--luci-muted); font-size: 0.875rem; margin: 0.25rem 0 0; }

/* An input with a helper button beside it (e.g. "Now" on next run at). */
.input-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  margin-top: 0.375rem;
}

.input-row input { flex: 1; margin-top: 0; }
.input-row button { margin-top: 0; white-space: nowrap; }

/* --- Toasts (transient flash notifications) --- */
.toast-region {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-width: min(24rem, calc(100vw - 2rem));
}

.toast {
  padding: 0.75rem 1rem;
  border-radius: var(--luci-radius-md);
  background: var(--luci-ink);
  color: #fff;
  font-size: 0.9375rem;
  box-shadow: var(--luci-lift);
  cursor: pointer;
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.toast--ok { background: var(--luci-success); }
.toast.toast--hide { opacity: 0; transform: translateY(-0.5rem); }

/* Form select/textarea controls (schedule form). Kept alongside the input
   rules above so text fields, selects, and textareas render consistently. */
select,
textarea {
  width: 100%;
  padding: 0.625rem 0.75rem;
  margin-top: 0.375rem;
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-md);
  font: inherit;
  background: #fff;
}

textarea {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875rem;
  min-height: 8rem;
  resize: vertical;
}

select:focus,
textarea:focus {
  outline: 2px solid rgba(14, 124, 116, 0.4);
  border-color: var(--luci-accent-dark);
}

/* --- Health/status page --- */
.status-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.75rem 1.5rem;
  margin: 1.25rem 0;
  align-items: center;
}
.status-grid dt { color: var(--luci-muted); font-weight: 600; }
.status-grid dd { margin: 0; }

/* --- Schedule list --- */
/* Subtle per-status row shading, keyed to core_schedule.status. */
table.data tr.status-running   { background: rgba(106, 200, 194, 0.12); }
table.data tr.status-complete  { background: rgba(0, 90, 0, 0.06); }
table.data tr.status-paused    { background: var(--luci-bg); }
table.data tr.status-cancelled { background: var(--luci-bg); }
table.data tr.status-failed    { background: rgba(164, 0, 0, 0.07); }
/* pending keeps the default surface — it is the normal active state. */

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 1rem;
  margin-bottom: 1rem;
}
/* A filter label names a control you operate; a column head names data you
   read. They used to be typeset identically, which left the filter row above a
   table looking like a second, floating header for it. Filters now speak in
   sentence case at reading size and the uppercase voice belongs to data alone. */
.filters label {
  margin: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--luci-muted);
}
.filters select {
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-md);
  font: inherit;
  background: #fff;
}
.filters button { margin-top: 0; }
.filters .filters-meta {
  align-self: center;
  font-size: 0.875rem;
  color: var(--luci-muted);
}

/* --- Dashboard --- */
/* Status banner: the page's one-line verdict, always first. */
.banner-status {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 1rem 0 1.25rem;
  padding: 0.85rem 1.1rem;
  border-radius: var(--luci-radius-lg);
}

.banner-status--warn {
  background: rgba(154, 103, 0, 0.09);
  border: 1px solid rgba(154, 103, 0, 0.25);
}

.banner-status--ok {
  background: rgba(13, 148, 136, 0.08);
  border: 1px solid rgba(13, 148, 136, 0.3);
}

/* Neutral variant: something is queued and visible, but nothing is wrong —
   a schema job politely waiting out a reporting run should not read as an
   incident, or the warn colour stops meaning anything. */
.banner-status--info {
  background: var(--luci-bg);
  border: 1px solid var(--luci-border);
}

.banner-status-head { margin: 0; font-weight: 700; }
.banner-status-sub { margin: 0; font-size: 0.875rem; color: var(--luci-muted); }
.banner-status-sub a { color: inherit; font-weight: 600; }
.banner-status-cta { margin-top: 0; margin-left: auto; white-space: nowrap; }

/* A job's captured output, on the banner and the job page. Migration previews
   put a whole DDL statement or sentence-length summary on one line each —
   wrap rather than stretch the card into a page-wide horizontal scroll. */
pre.output {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Live job banner: the submitting page's own view of the job it just queued
   (see _job_banner.html). Top-aligned rather than centred because a finished
   preview brings its output <pre> with it; the spinner is the visible "this
   is refreshing" cue for the [data-poll-src] poller. */
.job-banner { align-items: flex-start; }
.job-banner-main { flex: 1; min-width: 0; }
.job-banner .output { margin: 0.75rem 0 0; max-height: 24rem; overflow: auto; }
.job-banner form { margin: 0.6rem 0 0; }
.job-banner [data-credential-slot] .detail-grid { margin: 0.75rem 0; }
.job-banner-spinner {
  flex: none;
  width: 1rem;
  height: 1rem;
  margin-top: 0.2rem;
  border: 2px solid var(--luci-border);
  border-top-color: var(--luci-accent-dark);
  border-radius: 50%;
  animation: job-banner-spin 0.8s linear infinite;
}

/* The card's dismiss ×: quiet by design — it undoes nothing, it only clears
   the card, so it must not compete with the real actions inside it. */
.job-banner-close {
  flex: none;
  align-self: flex-start;
  margin: 0;
  padding: 0 0.25rem;
  border: 0;
  border-radius: var(--luci-radius-md);
  background: transparent;
  color: var(--luci-muted);
  font-size: 1.25rem;
  line-height: 1;
  font-weight: 400;
}

.job-banner-close:hover {
  background: transparent;
  color: var(--luci-ink);
}

@keyframes job-banner-spin {
  to { transform: rotate(360deg); }
}

/* Stat tiles, chart fills and series keys live in charts.css so the CDECT
   print pages, which do not load this file, still paint them. */

/* Hero variant: the run tile leads and gets the widest column. */
.stat-grid--hero { grid-template-columns: 1.5fr 1fr 1fr 1fr; }

@media (max-width: 44rem) {
  .stat-grid--hero { grid-template-columns: 1fr 1fr; }
  .stat-tile--hero { grid-column: 1 / -1; }
}

.stat-hero {
  font-size: 2.75rem;
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums;
}

.stat-hero-of { font-size: 1.0625rem; font-weight: 600; color: var(--luci-muted); }

.stat-live {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--luci-muted);
}

.live-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--luci-chart);
}

@media (prefers-reduced-motion: no-preference) {
  .live-dot { animation: live-pulse 2s ease-in-out infinite; }
  @keyframes live-pulse { 50% { opacity: 0.35; } }
}

.dash-section { margin-top: 1.75rem; }

.dash-section h2 {
  margin: 0 0 0.5rem;
  font-size: 1.125rem;
  line-height: 1.2;
  font-weight: 750;
  letter-spacing: -0.015em;
}

.dash-section .toolbar { margin-bottom: 0.5rem; }
.dash-section .toolbar h2 { margin: 0; }

/* Run activity strip: 24 hourly columns, complete stacked under failed. */
.activity-strip {
  display: block;
  width: 100%;
  height: 5.5rem;
}

.activity-strip .bar-ok { fill: var(--luci-chart); }
.activity-strip .bar-failed { fill: var(--luci-error); }
.activity-strip g:hover .bar-ok { fill: var(--luci-accent-dark); }

.strip-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 0.375rem;
  font-size: 0.75rem;
  color: var(--luci-muted);
  font-variant-numeric: tabular-nums;
}

.strip-legend {
  display: inline-flex;
  gap: 1rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--luci-muted);
}

.strip-legend > span { display: inline-flex; align-items: center; gap: 0.35rem; }

.key {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: var(--luci-radius-sm);
  display: inline-block;
}

.key--ok { background: var(--luci-chart); }
.key--failed { background: var(--luci-error); }

/* Attention queue: one row per unhealthy schedule, action last. */
table.queue td { white-space: normal; }
.queue-what a { font-weight: 600; color: var(--luci-ink); text-decoration: none; }
.queue-what a:hover { text-decoration: underline; }
.queue-meta { display: block; color: var(--luci-muted); font-size: 0.875rem; }
td.queue-when { color: var(--luci-muted); font-size: 0.8125rem; white-space: nowrap; }

/* Row-actions dropdown: one compact trigger per row instead of a button strip.
   Plain <details>, so it works without JS; app.js adds close-on-outside-click. */
.actions-menu { position: relative; display: inline-block; }

.actions-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-block;
  padding: 0.3125rem 0.625rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--luci-muted);
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-md);
  background: var(--luci-surface);
  white-space: nowrap;
}
.actions-menu > summary::-webkit-details-marker { display: none; }
.actions-menu > summary::marker { content: ""; }
.actions-menu > summary:hover { background: var(--luci-bg); color: var(--luci-ink); }
.actions-menu[open] > summary { background: var(--luci-bg); color: var(--luci-ink); }

.actions-dropdown {
  position: absolute;
  right: 0;
  top: calc(100% + 0.25rem);
  min-width: 10rem;
  background: var(--luci-surface);
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-lg);
  box-shadow: var(--luci-lift);
  padding: 0.375rem;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.actions-dropdown form { margin: 0; }

.actions-dropdown a,
.actions-dropdown button {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0.45rem 0.6rem;
  border: 0;
  border-radius: var(--luci-radius-md);
  background: transparent;
  color: var(--luci-ink);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}

.actions-dropdown a:hover,
.actions-dropdown button:hover { background: var(--luci-bg); }
/* A menu item a staff member may see but not use (User Management under
   impersonation): drawn, named, disabled, with the reason in the hint under
   it. A missing item reads as a feature that does not exist. */
.actions-dropdown button[disabled] {
  color: var(--luci-muted);
  cursor: not-allowed;
  opacity: 0.7;
}
.actions-dropdown button[disabled]:hover { background: transparent; }
.actions-dropdown .field-hint { padding: 0.5rem 0.75rem 0.25rem; max-width: 16rem; }
/* The same idea for a toolbar button: the link stays where the button is, out
   of the tab order and inert, with the reason in its title. */
.button[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; pointer-events: none; }
.actions-dropdown button.danger { color: var(--luci-error); }
.actions-dropdown button.danger:hover { background: rgba(164, 0, 0, 0.08); }

/* The attention queue links here with #s-<id>; ring the row rather than
   recolouring it, so the status tint (usually failed-red) stays visible.
   app.js swaps the :target match for the .row-target class and strips the
   hash from the URL, so the ring is gone after a refresh; :target remains as
   the no-JS fallback. */
table.data tr:target,
table.data tr.row-target {
  outline: 2px solid var(--luci-accent-dark);
  outline-offset: -2px;
  scroll-margin-top: 1rem;
}

/* Checkbox column on the selectable report tables. Narrow, and the box loses
   the full-width `input` rule that would otherwise stretch it across the cell. */
table.data th.col-select,
table.data td.col-select { width: 1.5rem; padding-right: 0.25rem; }

table.data .col-select input {
  width: auto;
  margin: 0;
  accent-color: var(--luci-accent-dark);
}

/* "As separate files" beside a download button. The base `label` rule pushes
   every label onto its own line, which is wrong for one sharing a row with a
   button — and `label.checkbox` sets the layout but not the margin. Its text is
   an option on a control, not a column head, so it takes the muted weight of a
   filter label rather than a form label's. */
.toolbar-actions label.checkbox.toolbar-option {
  margin: 0;
  font-weight: 500;
  font-size: 0.875rem;
  color: var(--luci-muted);
  white-space: nowrap;
  cursor: pointer;
}

.toolbar-actions label.checkbox.toolbar-option input { accent-color: var(--luci-accent-dark); }

/* The download button carries a count of what is ticked, so it must never wrap
   to a second line and change the toolbar's height as rows are selected. The
   hint sharing its row is the half that gives way instead.

   It is not a `.button`, so the toolbar's margin reset does not reach it and
   the base button rule's top margin survives — which, since the toolbar centres
   the margin box, leaves the button sitting visibly below the checkbox beside
   it. Zero it here so the pair share a centre line. */
.button-download { margin-top: 0; white-space: nowrap; }
.toolbar .field-hint { min-width: 0; }

/* Sortable column headers (tables with data-sortable): click affordance and
   direction indicator on the active column. */
table[data-sortable] thead th:not([data-nosort]) {
  cursor: pointer;
  user-select: none;
}
table[data-sortable] thead th:not([data-nosort]):hover { color: var(--luci-ink); }
table[data-sortable] thead th[aria-sort="ascending"]::after { content: " ▲"; font-size: 0.625rem; }
table[data-sortable] thead th[aria-sort="descending"]::after { content: " ▼"; font-size: 0.625rem; }

/* Heartbeat age shown in place of the next run while a report is running. */
.heartbeat { color: var(--luci-accent-dark); font-weight: 600; white-space: nowrap; }

/* Run history: error text may be long; wrap it rather than stretch the table
   past its panel. Must out-rank the table.data td nowrap default. */
table.data td.run-error {
  white-space: normal;
  overflow-wrap: anywhere;
  color: var(--luci-muted);
}

/* --- Entity detail pages (org / schedule / input / output) --- */
.entity-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.entity-head h1 { margin: 0; }
.entity-head .spacer { flex: 1; }
.entity-code { color: var(--luci-muted); font-weight: 500; font-size: 1rem; }

/* A section heading carrying that section's state on the right — the org
   Security tab's three stages. Same shape as .entity-head one level down, so a
   page whose sections each have a status reads as a column of states rather
   than a column of headings. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
}

.section-head h2 { margin: 0; }
.section-head .spacer { flex: 1; }

/* A hint that carries a warning rather than an explanation (e.g. a scope that
   reaches organisations the page it is on cannot show). Amber, not the error
   red: it is a fact worth noticing, not something that went wrong. */
.field-hint--warn { color: var(--luci-warn); }
/* A hint standing on its own above body content, not under a form field: it
   needs the paragraph spacing .error has rather than the field one. */
.field-hint--block { margin: 0 0 1rem; }

/* A second, optional action folded into a form — ticked by default, and
   answering a question the operator would otherwise have had to come back for.
   The accent edge marks it as part of the form's outcome rather than a note
   about it. */
.paired-action {
  margin-top: 1.25rem;
  padding: 0.875rem 1rem;
  border: 1px solid var(--luci-border);
  border-left: 3px solid var(--luci-accent-dark);
  border-radius: var(--luci-radius-md);
  background: var(--luci-accent-tint);
}

.paired-action .field-hint { padding-left: 1.6rem; }

/* Tab bar under an entity header; tabs are plain links to sibling routes. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.125rem;
  margin: 1rem 0 1.375rem;
  border-bottom: 1px solid var(--luci-border);
}

.tabs a {
  padding: 0.45rem 0.875rem;
  margin-bottom: -1px;
  text-decoration: none;
  color: var(--luci-muted);
  font-weight: 600;
  font-size: 0.9375rem;
  border-bottom: 2px solid transparent;
  border-radius: var(--luci-radius-md) var(--luci-radius-md) 0 0;
}

.tabs a:hover { color: var(--luci-ink); background: var(--luci-bg); }
.tabs a[aria-current="page"] {
  color: var(--luci-accent-dark);
  border-bottom-color: var(--luci-accent-dark);
}

.tabs .count {
  margin-left: 0.375rem;
  padding: 0 0.45rem;
  border-radius: 999px;
  background: var(--luci-bg);
  color: var(--luci-muted);
  font-size: 0.75rem;
  font-weight: 700;
}

.tabs a[aria-current="page"] .count {
  background: var(--luci-accent-tint);
  color: var(--luci-accent-dark);
}

/* Read-only field grid on detail pages. */
/* The label column is capped rather than plain max-content: a label can be data
   (a facility and report-type name, a credential key), and one long label sized
   the column to its full width, leaving the value column zero-wide and wrapping
   a path one character per line. The 45% keeps that true on a narrow viewport,
   where 14rem alone is still most of the row; `anywhere` on dt keeps the cap
   effective for an unbroken token. Labels under the cap size exactly as before. */
dl.detail-grid {
  display: grid;
  grid-template-columns: fit-content(min(14rem, 45%)) minmax(0, 1fr);
  gap: 0.625rem 1.5rem;
  margin: 1.25rem 0 0;
  align-items: baseline;
}

dl.detail-grid dt { color: var(--luci-muted); font-weight: 600; font-size: 0.875rem; overflow-wrap: anywhere; }
dl.detail-grid dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* Pretty-printed JSONB config on detail pages. */
pre.json {
  margin: 0.5rem 0 0;
  padding: 0.875rem 1rem;
  background: var(--luci-bg);
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-md);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  line-height: 1.55;
  overflow-x: auto;
}

/* Credential keys shown name-only; values never round-trip to the browser.
   A table (not stacked flex rows) so the key column sizes to the widest key on
   the page and every value lines up in one column, regardless of key length or
   the "protected" pill. */
.secret-list {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}
.secret-row { border-bottom: 1px solid var(--luci-border); }
.secret-row:last-child { border-bottom: 0; }
.secret-key {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  font-weight: 400;
  text-align: left;
  vertical-align: baseline;
  white-space: nowrap;      /* keep name + "protected" pill together */
  width: 1px;               /* shrink key column to its content */
  padding: 0.45rem 1.5rem 0.45rem 0;
}
.secret-val {
  color: var(--luci-muted);
  letter-spacing: 0.15em;
  vertical-align: baseline;
  padding: 0.45rem 0;
}

/* Facility-code chips (inputs' text[] column, detail grids, tables). */
.fac-list {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

/* Org list health rollup: issue pills linking to the org's Schedules tab. */
a.health-summary {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  text-decoration: none;
}

a.health-summary:hover .pill { text-decoration: underline; }

/* Facility checkbox group on the input form. */
.checkbox-group {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-top: 0.375rem;
}

.checkbox-group label.checkbox {
  margin-top: 0;
  font-weight: 500;
}

/* The schema list flows into columns once there is room for them. Scoped to
   .schema-set rather than every .checkbox-group: the scope org picker's rows
   carry a name, a code and sometimes a pill, so they need the full width and
   would wrap raggedly. Columns keep the whole selection on screen at a glance,
   which is the property the "show it whole" note below depends on. */
.schema-set .checkbox-group {
  columns: 12rem auto;
  column-gap: 1.5rem;
  display: block;
}

.schema-set .checkbox-group label.checkbox {
  break-inside: avoid;
  padding: 0.125rem 0;
}

/* RLS schema selection: a preset picker above the schemas it ticks. The list is
   short enough to show whole — scrolling it would hide grants an operator is
   about to revoke. */
.schema-set {
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-md);
  padding: 0.5rem 1rem 1rem;
  margin-top: 0.75rem;
}

.schema-set legend {
  padding: 0 0.35rem;
  font-weight: 600;
}

.schema-set .checkbox-group {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: 0.25rem 1rem;
}

/* A hint indented under the radio/checkbox option it explains. */
.field-hint--nested {
  margin: 0 0 0.25rem 1.6rem;
}

h2.detail-section {
  margin: 1.75rem 0 0.25rem;
  font-size: 1.05rem;
  font-weight: 750;
  letter-spacing: -0.015em;
}

/* --- Small layout utilities (replace former inline style attributes) --- */
/* Wraps a form's submit/cancel buttons, spacing them from the fields above. */
.form-actions { margin-top: 1.25rem; }
/* An .error paragraph that follows body content and needs top spacing. */
.error--top { margin-top: 1rem; }
/* Section divider between a form and a related action group. */
.divider {
  margin: 1.5rem 0;
  border: none;
  border-top: 1px solid var(--luci-border);
}

/* Validator output on the Database tabs. The schema and security validators
   already emit aligned, indented text for the terminal console; it is rendered
   verbatim so both consoles word findings identically. Wraps rather than
   scrolls horizontally: findings are prose-length, not code. */
pre.report {
  margin: 1rem 0 0;
  padding: 0.875rem 1rem;
  background: var(--luci-bg);
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-md);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Deferred-panel loading state (Database schema/security checks). The panel is
   fetched after the page paints, so the tab is usable while a multi-second
   validator runs. */
.check-loading {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin: 1rem 0 0;
  color: var(--luci-muted);
  font-size: 0.9375rem;
}
.spinner {
  flex: none;
  width: 1rem;
  height: 1rem;
  border: 2px solid var(--luci-border);
  border-top-color: var(--luci-accent-dark);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Respect a reduced-motion preference: keep the indicator, drop the spin. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}


/* --- Director of Nursing report ------------------------------------------ */
/* One facility, one completed month, seven tabs (routes/don.py). The tiles
   are the portal's stat tiles with a prior-month line beneath the figure; the
   two block treatments mark what is AI-drafted and what the service completes,
   matching the key on the cover. Colours come from the tokens above, so the
   report cannot drift from the rest of the portal. */

/* The dashboard's header: selectors, resolved scope and tabs travel together
   and stay on screen. A long tab otherwise leaves the reader with no statement
   of which facility, month or wing they are looking at. */
.don-head {
  position: sticky;
  top: 0;
  z-index: 5;
  padding-top: 0.25rem;
  margin-bottom: 0.75rem;
  background: var(--luci-surface, #fff);
  border-bottom: 1px solid var(--luci-border);
}
.don-head .toolbar { margin-bottom: 0.5rem; }
.don-head .filters { margin-bottom: 0.35rem; }
.don-head .tabs { margin-bottom: 0; }
@media (max-width: 48rem) {
  /* Too tall to pin on a phone: it would take most of the viewport. */
  .don-head { position: static; }
}

/* The provenance key is a colour key, so it stays one click away at all times
   rather than occupying a quarter of the first screen. */
.don-key-menu { position: relative; display: inline-block; }
.don-key-menu > summary { list-style: none; cursor: pointer; }
.don-key-menu > summary::-webkit-details-marker { display: none; }
.don-key-menu[open] > ul {
  position: absolute;
  right: 0;
  z-index: 10;
  margin-top: 0.35rem;
  padding: 0.75rem 0.9rem;
  min-width: 17rem;
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-lg);
  background: var(--luci-surface, #fff);
  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.12);
}

/* The one figure the header cannot carry: the denominator every share is
   taken against. */
.don-scope-line {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.25rem;
  margin: 0 0 1rem;
  color: var(--luci-muted);
}
.don-scope-line strong { color: var(--luci-ink); }

/* The line under the selectors that says what the page is scoped to. */
.don-context {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin: 0 0 0.25rem;
  color: var(--luci-muted);
}
.don-context strong { color: var(--luci-ink); }
.don-context-filter {
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--luci-border);
  border-radius: 999px;
  font-weight: 600;
  color: var(--luci-ink);
}

/* Section headings carry no number in the dashboard. The deck numbers its
   pages because they arrive in a fixed order; a tab can be entered directly,
   so the number would mean nothing here. */
.don-section-head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin: 1.75rem 0 0.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--luci-border);
}
.tabs + .don-section-head { margin-top: 0; padding-top: 0; border-top: 0; }
.don-section-head h2 { margin: 0; }
.don-section-head .field-hint { margin: 0.125rem 0 0; }

.don-grid {
  display: grid;
  gap: 1rem;
  margin-bottom: 1rem;
}
.don-grid--summary { grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); }

/* The two block treatments: AI-drafted narrative and user input. */
.don-block {
  border-radius: var(--luci-radius-lg);
  padding: 1rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.don-block .chart-title { margin-bottom: 0; }

/* Collapsed by default in the dashboard: the label, the treatment and the
   reason stay visible, and only the prompt body folds away. The export renders
   these blocks open and full height, which is the right call on a page. */
.don-block--fold { display: block; padding: 0.6rem 0.9rem; }
.don-block--fold > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
}
.don-block--fold > summary::-webkit-details-marker { display: none; }
.don-block--fold > summary::after {
  content: "+";
  margin-left: auto;
  font-weight: 700;
  color: var(--luci-muted);
}
.don-block--fold[open] > summary::after { content: "−"; }
.don-block--fold[open] > summary { margin-bottom: 0.6rem; }
.don-fold-title { font-weight: 700; }
.don-fold-state { font-size: 0.8125rem; color: var(--luci-muted); font-style: italic; }

/* A gated field is a generated figure whose source could not run, not a field
   the service forgot. Same control, different reason, stated differently. */
.don-block--gated { border-style: solid; border-color: var(--luci-border); background: transparent; }
.don-block--gated .prov-label--user { color: var(--luci-muted); }
.don-reason--gated { font-style: normal; font-weight: 600; color: var(--luci-ink); }
.don-block--ai {
  border: 1px solid rgba(13, 148, 136, 0.3);
  background: rgba(13, 148, 136, 0.08);
}
.don-block--user {
  border: 1px dashed var(--luci-warn);
  background: rgba(154, 103, 0, 0.05);
}
.prov-label {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}
.prov-label--ai { color: var(--luci-accent-dark); }
.prov-label--user { color: var(--luci-warn); }
.ai-body { margin: 0; color: var(--luci-muted); font-size: 0.9375rem; font-style: italic; }
.don-reason { margin: 0; font-style: italic; }
.ui-prompt { margin: 0; color: var(--luci-warn); font-size: 0.9375rem; opacity: 0.85; }

/* Falls tab: rows of cards, position tables and the hour-of-day stack. */
.don-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: 1rem;
  margin-bottom: 1rem;
  align-items: stretch;
}
.don-row--wide-first { grid-template-columns: minmax(0, 2fr) minmax(18rem, 1fr); }
/* Donut beside a bar chart: the ring needs only its legend's width, not a wide column. */
.don-row--donut-bar { grid-template-columns: max-content minmax(18rem, 1fr); }
.don-row--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 64rem) {
  .don-row--wide-first,
  .don-row--donut-bar,
  .don-row--three { grid-template-columns: 1fr; }
}
.don-row > .chart-card,
.don-row > .don-block { margin-bottom: 0; }
.don-stack { display: flex; flex-direction: column; gap: 1rem; }
.don-stack > .chart-card,
.don-stack > .don-block { margin-bottom: 0; }

/* Three columns in a third-width card: fixed layout so the month headers wrap
   instead of pushing the figures past the card edge. */
table.data.don-position { table-layout: fixed; }
table.data.don-position th,
table.data.don-position td { white-space: normal; }
table.data.don-position th:first-child { width: 40%; }
.don-position-prior { color: var(--luci-muted); font-style: italic; font-weight: 500; }

/* Medication, wounds and nutrition tabs. A user-input grid keeps the dashed
   warm treatment on every cell so an em dash cannot be read as a zero. */
table.data.don-ui-grid { table-layout: fixed; margin-top: 0.25rem; }
table.data.don-ui-grid th,
table.data.don-ui-grid td { white-space: normal; }
.don-ui-cell { color: var(--luci-warn); font-weight: 500; }

/* Risk pages: a withheld chart keeps its frame and says why the plot is missing. */
.don-withheld-plot {
  min-height: 9rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  border: 1px dashed var(--luci-border);
  border-radius: var(--luci-radius-lg);
  background: #FBFCFC;
  text-align: center;
  padding: 1rem;
}
.don-withheld-caption { font-style: italic; color: var(--luci-muted); font-weight: 600; }
.don-withheld-axis {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin: 0.5rem 0 0;
  font-size: 0.75rem;
  color: var(--luci-muted);
}
table.data.don-withheld-table { table-layout: fixed; }
table.data.don-withheld-table th,
table.data.don-withheld-table td { white-space: normal; }
.don-tbc { color: var(--luci-muted); font-style: italic; }

/* Wing comparison (18.4): horizontal scroll with the row-label column pinned. */
.don-compare-scroll { overflow-x: auto; }
table.data.don-compare { min-width: 100%; }
table.data.don-compare th,
table.data.don-compare td { white-space: normal; vertical-align: top; }
table.data.don-compare .don-compare-label {
  position: sticky;
  left: 0;
  background: var(--luci-surface, #fff);
  min-width: 16rem;
  z-index: 1;
}
table.data.don-compare thead .don-compare-label { z-index: 2; }
table.data.don-compare td.num { min-width: 9rem; }
.don-compare-prior {
  display: block;
  font-style: italic;
  color: var(--luci-muted);
}
.don-compare-tbc td { color: var(--luci-muted); }
/* Relative shading within a row (spec 1.6): five steps of the chart colour, so
   nothing here can read as a red/amber/green judgement. */
.don-shade-0 { background: transparent; }
.don-shade-1 { background: rgba(13, 148, 136, 0.08); }
.don-shade-2 { background: rgba(13, 148, 136, 0.18); }
.don-shade-3 { background: rgba(13, 148, 136, 0.30); }
.don-shade-4 { background: rgba(13, 148, 136, 0.44); }
.don-compare-key {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.75rem 0 0;
  color: var(--luci-muted);
}
.don-shade-swatches { display: inline-flex; gap: 2px; flex-shrink: 0; }
.don-shade-swatches > span {
  width: 0.9rem;
  height: 0.9rem;
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-sm);
}

/* Report coverage: three provenance columns built from the field register. */
.don-block--generated { border: 1px solid var(--luci-border); background: #FBFCFC; }
.don-coverage-group { margin: 0; display: flex; flex-direction: column; gap: 0.1rem; font-size: 0.875rem; }
.don-coverage-group span { color: var(--luci-muted); }
.don-coverage-list { margin: 0; padding-left: 1.1rem; font-size: 0.9375rem; }
.don-coverage-list li { margin-bottom: 0.25rem; }
.don-coverage-constraint { margin: 0.5rem 0 0; padding-top: 0.5rem; border-top: 1px solid var(--luci-border); }

/* --- DoN report: table, caption and empty-state conventions (spec 1.12, 1.15).
   Scoped to the report's own tables. The portal's `table.data th` is uppercase
   at 0.6875rem, which is the house data-label voice everywhere else and is
   below the report's 10 pt floor; overriding it globally would restyle every
   table in the console, so the report's two table classes opt out here. */
table.data.don-position th,
table.data.don-compare th {
  text-transform: none;
  letter-spacing: 0;
  vertical-align: bottom;
}

/* First column left, every value column centred, each header aligned with its
   column. Centred is easier to read down a column than right-aligned once the
   cells carry a second line beneath the figure. */
table.data.don-position th.num,
table.data.don-position td.num,
table.data.don-compare th.num,
table.data.don-compare td.num { text-align: center; }

/* The six-word line under a column header saying what the column holds. It
   describes; it never defines. The definition lives in the info bubble. */
.don-qualifier {
  display: block;
  margin-top: 0.125rem;
  font-style: italic;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--luci-muted);
}

/* 16 px clear between a block subheading and the first row of its table, and
   12 px between the last row and the note that sits below it. */
.chart-card > .chart-title + table.data,
.chart-card > .chart-title + .don-compare-scroll { margin-top: 1rem; }
.don-note { margin-top: 0.75rem; font-style: italic; }

/* A block with no data keeps its full height, its subheading and its border, so
   the grid never collapses and the reader sees the gap where the gap is. */
.don-empty {
  display: flex;
  align-items: center;
  min-height: 8rem;
  margin: 0;
}


/* --- DoN dashboard: browser widths ------------------------------------- */
/* The deck is 16:9 and fixed. The dashboard is not, so chart rows go two-up on
   a wide screen and one-up below roughly 64rem rather than shrinking a chart
   until its labels collide. */
.chart-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
@media (max-width: 64rem) {
  .chart-row { grid-template-columns: 1fr; }
}
.don-grid--summary { grid-template-columns: 1fr; }

/* ------------------------------------------------------------------------
   Trust Centre (/trust). The one page anyone can read, so it also renders
   without the shell: .page--trust sets its own width either way. Questions are
   <details> rows, so the page reads as a list of headings until one is opened;
   the detailed answer, shown only when signed in, sits under the public one in
   a tinted block so a client can see which part is theirs alone.
   ------------------------------------------------------------------------ */
.page--trust { max-width: 64rem; }
.appbar-inner > .button { margin: 0 0 0 auto; padding: 0.45rem 0.9rem; white-space: nowrap; }
.trust-hero { display: grid; gap: 0.5rem; margin-bottom: 1.75rem; }
.trust-hero h1 { margin: 0; }
.trust-meta { margin: 0; color: var(--luci-muted); font-size: 0.85rem; }
.trust-lede { margin: 0; color: var(--luci-muted); max-width: 60ch; }
.trust-notice {
  margin: 0.5rem 0 0;
  padding: 0.6rem 0.85rem;
  background: var(--luci-rail);
  border: 1px solid var(--luci-rail-border);
  border-radius: var(--luci-radius-md);
  font-size: 0.9rem;
}
/* The at-a-glance strip: one tile per item of the at-a-glance section, label
   over value, as many to a row as fit. */
.trust-glance {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.75rem;
  margin: 0 0 2rem;
}
.trust-glance > div {
  border: 1px solid var(--luci-border);
  border-radius: var(--luci-radius-md);
  padding: 0.7rem 0.9rem;
  background: var(--luci-surface);
}
.trust-glance dt {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--luci-muted);
}
.trust-glance dd { margin: 0.25rem 0 0; font-weight: 600; }
.trust-layout { display: grid; grid-template-columns: 12rem minmax(0, 1fr); gap: 2rem; align-items: start; }
.trust-toc { position: sticky; top: 1rem; display: grid; gap: 0.1rem; font-size: 0.9rem; }
.trust-toc a { color: var(--luci-muted); text-decoration: none; padding: 0.3rem 0.5rem; border-radius: var(--luci-radius-sm); }
.trust-toc a:hover { background: var(--luci-bg); color: var(--luci-ink); }
/* minmax(0, 1fr), not the implicit auto column: an auto track grows to its
   widest section's min-content, so four long sub-processor rows pushed every
   question box past the card's right edge. Its cells wrap (table.data is
   nowrap everywhere else), and on a phone the table scrolls in its own box --
   a wrapper, so the table itself stays a table and fills the column. */
.trust-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2rem; min-width: 0; }
.trust-table { overflow-x: auto; }
.trust-body table.data { width: 100%; }
.trust-body table.data td { white-space: normal; }
.trust-section { display: grid; gap: 0.5rem; scroll-margin-top: 1rem; }
.trust-section h2 { margin: 0; }
.trust-summary { margin: 0 0 0.25rem; color: var(--luci-muted); font-size: 0.92rem; }
details.trust-q { border: 1px solid var(--luci-border); border-radius: var(--luci-radius-md); background: var(--luci-surface); }
details.trust-q > summary {
  cursor: pointer;
  list-style: none;
  padding: 0.7rem 0.9rem;
  font-weight: 600;
  display: flex;
  gap: 0.6rem;
}
details.trust-q > summary::-webkit-details-marker { display: none; }
details.trust-q > summary::before { content: "+"; color: var(--luci-accent-dark); font-weight: 700; width: 0.8rem; flex: none; }
details.trust-q[open] > summary::before { content: "\2013"; }
details.trust-q > summary:focus-visible { outline: 2px solid var(--luci-accent-dark); outline-offset: 2px; }
.trust-a { padding: 0 0.9rem 0.9rem 2.3rem; display: grid; gap: 0.6rem; }
.trust-a p { margin: 0; }
.trust-detail {
  border-left: 3px solid var(--luci-accent);
  background: var(--luci-bg);
  padding: 0.6rem 0.8rem;
  border-radius: 0 var(--luci-radius-md) var(--luci-radius-md) 0;
  display: grid;
  gap: 0.4rem;
  font-size: 0.93rem;
}
.trust-detail-label {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--luci-accent-dark);
}
@media (max-width: 760px) {
  /* minmax(0, 1fr), not 1fr: a bare 1fr track will not shrink below its
     content's min-content width, and the sub-processor table pushed the whole
     column off the right edge of a phone. */
  .trust-layout { grid-template-columns: minmax(0, 1fr); }
  .trust-toc { display: none; }
}
