/* NectarFlow — the app surface.
 *
 * Ported from `app.py`'s `_CSS`. Two things changed and nothing else:
 *
 * 1. **The tokens are custom properties rather than f-string interpolation.** Streamlit
 *    needed the palette baked into a string at render time; here `theme.css_variables()`
 *    writes `:root` per request and everything below refers to `var(--ink)`. That is what
 *    makes the palette deep-linkable without a second stylesheet.
 * 2. **Every Streamlit selector is gone** — about half the original file. Rules keyed on
 *    `[data-testid=…]`, `.st-emotion-cache-…` and `[class*="st-key-…"]` existed to reach
 *    widgets the app did not render itself. It renders all of its own HTML now, so those
 *    rules have no target and no replacement: the elements they fought are simply not
 *    here. `_DARK_WIDGETS` went with them for the same reason.
 *
 * What has *not* changed is the design. Where a rule looks fussy it is usually load-bearing
 * and the comment says why — those were measured, not eyeballed.
 */

/* ---------------------------------------------------------------- reset and root */
*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  /* One typeface, named at the root. In Streamlit this had to be repeated onto widget
     labels because the theme font reached markdown containers only; here inheritance
     works, so it is said once. Never name a family outside this rule. */
  font-family: var(--sans);
  color: var(--ink2);
  /* A whisper of the signature hue at the top left, then flat paper. */
  background:
    radial-gradient(1200px 600px at 15% -10%,
      color-mix(in srgb, var(--shade-1) 100%, transparent) 0%,
      color-mix(in srgb, var(--shade-1) 0%, transparent) 55%),
    var(--plane);
  min-height: 100vh;
}

h1, h2, h3, h4 { color: var(--ink); letter-spacing: .2px; }
strong, b { color: var(--ink); }
a { color: var(--ev); }

input, select, textarea, button { font-family: var(--sans); font-size: .9rem; }

/* ---------------------------------------------------------------- app shell */
.app { display: flex; min-height: 100vh; }

/* The rail. In Streamlit this was `[data-testid="stSidebar"]` plus a compact-mode
   override injected as a second stylesheet; here it is one element with a modifier. */
.rail {
  width: 240px; flex: 0 0 240px;
  background: var(--surface); border-right: 1px solid var(--border);
  padding: .75rem .7rem 1.5rem; display: flex; flex-direction: column; gap: .35rem;
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.rail.compact { width: 82px; flex-basis: 82px; padding-left: .45rem; padding-right: .45rem; }

.rail-toggle {
  background: var(--plane); color: var(--ink); border: 1px solid var(--border);
  border-radius: 10px; padding: .35rem; cursor: pointer; width: 100%;
}
.rail-toggle:hover { border-color: var(--ev); }

.navtile {
  display: flex; align-items: center; gap: .6rem;
  font-size: 1.15rem; line-height: 1; padding: .6rem .7rem; border-radius: 16px;
  background: var(--plane); color: var(--ink); border: 1px solid var(--border);
  text-decoration: none; transition: background .15s, border-color .15s, transform .1s;
}
.navtile:hover { border-color: var(--ev); transform: translateY(-1px); }
.navtile.on {
  border-color: var(--ev);
  background: color-mix(in srgb, var(--ev) 12%, var(--surface));
}
.navtile .t { font-size: .82rem; font-weight: 600; }
/* Compact: the label is withheld rather than shrunk — a truncated word is worse than none
   — and **the tile keeps the height it had open**. It used to take `aspect-ratio: 1/1`,
   which made every tile as tall as the rail was wide: 40px expanded against 67px compact,
   so collapsing the rail grew the icons by two thirds and the nav stopped being the same
   object in two widths. Measured, not guessed — both states report 40px now. */
.rail.compact .navtile { justify-content: center; padding: .6rem 0; }
.rail.compact .navtile .t { display: none; }

.rail-sep { border: 0; border-top: 1px solid var(--border); margin: .6rem 0; width: 100%; }
.rail-foot { margin-top: auto; display: flex; flex-direction: column; gap: .4rem; }
.rail .who { font-size: .74rem; color: var(--muted); padding: 0 .3rem; }
.rail .who b { display: block; color: var(--ink); font-size: .82rem; }
.rail.compact .who, .rail.compact .rail-label { display: none; }
/* **One height down the rail.** Every control in it is 40px — the nav tiles were, and the
   buttons below them were 41 and the toggle 33, which is the kind of difference nobody can
   name and everybody can see. Stated once here rather than tuned per control. */
.rail .btn, .rail-toggle {
  height: 40px; padding-top: 0; padding-bottom: 0;
  display: inline-flex; align-items: center; justify-content: center;
}

/* Sign out: the word when there is room for it, the symbol when there is not. The pair is
   named as the nav tiles name theirs, so the rail has one convention rather than two. */
.btn.signout .s { display: none; }
.rail.compact .btn.signout .s { display: inline-flex; align-items: center; }
.btn.signout .s svg { width: 18px; height: 18px; display: block; }
.rail.compact .btn.signout .t { display: none; }
.rail.compact .btn.signout { padding-left: 0; padding-right: 0; }

.main { flex: 1; min-width: 0; padding: .6rem 1.2rem 3rem; }
.page { max-width: 1200px; margin: 0 auto; }

/* ---------------------------------------------------------------- app header */
/* Sticky, and simply sticky: in Streamlit the card could not be the sticky element
   because every markdown block sits in a container only as tall as its own content, so
   the *wrapper* had to be selected with `:has()`. Here the header is a direct child of the
   scroll container and pins itself. */
.app-header {
  position: sticky; top: 0; z-index: 1000;
  display: flex; align-items: center; gap: .9rem; margin: 0 0 1.1rem;
  padding: .8rem 1.25rem; border-radius: 14px;
  /* Nearly opaque, not merely frosted: the card is pinned and a whole page of text
     scrolls behind it, so a translucent card lets headings ghost through. */
  background:
    linear-gradient(135deg,
      color-mix(in srgb, var(--pv) 14%, transparent),
      color-mix(in srgb, var(--ramp-a-1) 10%, transparent)),
    color-mix(in srgb, var(--surface) 97%, transparent);
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--border);
  box-shadow: 0 6px 22px rgba(36, 31, 78, .10);
}
.app-header .screen-title {
  font-size: 1.3rem; font-weight: 650; color: var(--ink); letter-spacing: .2px;
}
.app-header .spacer { flex: 1; }
.app-header .brand { display: flex; align-items: center; gap: .55rem; }
/* Wordmark over attribution, right-aligned against the logo. Stacked rather than inline
   so the logo stays beside the pair; both lines together are shorter than the 34px mark,
   so the header keeps its height. */
.app-header .brand-text {
  display: flex; flex-direction: column; align-items: flex-end; gap: .08rem;
}
.app-header .brand-name {
  font-weight: 700; font-size: 1.296rem; letter-spacing: .3px; line-height: 1.15;
  background: linear-gradient(90deg, var(--ramp-a-0), var(--pv));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
.app-header .brand .nf-mark { width: 34px; height: 34px; display: block; }

/* The mark's colours, from the palette: the cell in ink, the curve on the wordmark's own
   gradient, the dot on its far end. One rule for every place it is drawn. */
.nf-mark .nf-cell { stroke: var(--ink); opacity: .9; }
.nf-mark .nf-s0 { stop-color: var(--ramp-a-0); }
.nf-mark .nf-s1, .nf-mark .nf-dot { stop-color: var(--pv); fill: var(--pv); }

/* ---------------------------------------------------------------- the vendor's byline */
/* **The byline now moves with the palette**, and that is a reversal worth writing down.
   It used to be two literals — the site's honey-600 on light grounds, honey-500 on
   the dark palette — on the reasoning that a vendor's colour must not change with the app's
   theme or it stops being a brand. What that produced in practice was one orange mark
   sitting outside every palette, reading as something the designer had missed rather
   than as an attribution.

   So the mark and the capital-B half take `--ev`, the palette's hero: the byline belongs
   to the page it is on. Two things still hold it to *their* brand rather than ours —
   **Inter**, self-hosted, and the **InnovBees** capitalisation that makes the second half
   a separate word to colour at all. (The Brand palette, which kept the site's own
   honey-500 here, has been removed.) */
:root { --byline: var(--ev); }

/* Inter is the site's typeface, self-hosted so a client's browser makes no third-party
   request to read a byline and the app still renders its own brand offline. One weight:
   the wordmark is the only thing set in it. */
@font-face {
  font-family: "InnovBees Inter";
  src: url("inter-700-latin.8ca0a5155296.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
  /* Latin only — this face sets two words. */
  unicode-range: U+0000-00FF;
}

.ib {
  display: inline-flex; align-items: center; gap: .3rem;
  line-height: 1; white-space: nowrap;
}
.ib-mark {
  width: 15px; height: 17px; flex: none; display: block;
  /* The mark is drawn in `currentColor`, so one declaration colours all three shapes and
     the word beside it cannot drift from the logo. */
  color: var(--byline);
}
/* "powered by" is ours, not theirs — it stays the app's muted micro-caption so the
   attribution reads as a sentence with their name in it rather than as their banner. */
.ib-pre {
  font-size: .58rem; font-weight: 500; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
/* The wordmark itself, matched to the site: Inter bold, -0.02em tracking, mixed case, and
   two-tone on the capital B. **Never uppercased** — the capital is what makes the second
   half a separate word, and `text-transform` would throw that away. */
.ib-name {
  font-family: "InnovBees Inter", var(--sans);
  font-weight: 700; font-size: .74rem; letter-spacing: -.02em;
  color: var(--ink2); text-transform: none;
}
.ib-name > span { color: var(--byline); }

/* On the sign-in card the byline sits centred under the rule, with room to breathe. */
.signin-head .ib { margin-top: .35rem; }
.signin-head .ib-mark { width: 17px; height: 19px; }
.signin-head .ib-name { font-size: .82rem; }

/* ---------------------------------------------------------------- saying it quietly */
/* An ⓘ holding a sentence on hover. The explanations say what a figure is *measured
   from*, which is the difference between a number and a claim — but printed under every
   chart they were the loudest thing on the page. */
.hint {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--shade-3); color: var(--muted);
  font-family: Georgia, "Times New Roman", serif; font-style: italic;
  font-size: .68rem; font-weight: 700; cursor: help; user-select: none;
  vertical-align: middle; margin-left: .3rem;
}
.hint:hover { background: var(--ramp-b-2); color: var(--ramp-a-0); }

/* A row of stat chips replaces a sentence of figures. Reading a row of chips is a
   glance; the same content as prose is a paragraph. Each figure sits in its own <em>,
   which is also what keeps two `$` out of one text run — in markdown that pair is a LaTeX
   delimiter and the money between them renders as maths. */
.statrow { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  margin: .1rem 0 .2rem; }
.stat { display: inline-flex; align-items: baseline; gap: .1rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: .18rem .55rem; font-size: .76rem; white-space: nowrap; }
.stat b { font-weight: 400; font-size: .82rem; }
.stat em { font-style: normal; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums; }
.stat span { color: var(--muted); font-size: .72rem; }

/* Section heading: symbol + short title, explanation folded into the ⓘ. */
.sect { display: flex; align-items: center; gap: .5rem; margin: 1.1rem 0 .5rem; }
.sect-sym { font-size: 1.05rem; }
.sect-t { font-size: 1rem; font-weight: 650; color: var(--ink); letter-spacing: .2px; }

/* **The depth control, at the top right of the table it governs.** `margin-left: auto` in
   the heading's existing flex row is the whole placement — it needs no width of its own and
   it cannot collide with a long title, which wraps against it rather than under it.

   It reads as a caption rather than a form: this is a view control that writes nothing, and
   a boxed field beside a heading would claim more of the eye than the heading. `--muted`
   for the options, the page's full ink for the one that is on, so the current depth is
   legible at a glance without the cluster shouting.

   Wraps on a narrow window instead of pushing the heading off its line — three short words
   on their own line is a worse outcome than none, but a heading shoved out of view is
   worse still. */
.depth { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center;
         gap: .1rem .7rem; font-size: .78rem; color: var(--muted); }
.depth label { display: inline-flex; align-items: center; gap: .3rem;
               cursor: pointer; white-space: nowrap; }
.depth input { accent-color: var(--ev); margin: 0; }
.depth label:has(input:checked) { color: var(--ink); }
/* Divides a toggle that sits beside the depths (Show users) from the depths themselves. */
.depth .dsep { color: var(--border); }

/* The app's one empty state. Not a stack of alerts painting from another theme — a page
   that cannot draw says what is missing and how to fix it, once. */
.empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .35rem; text-align: center; padding: 2.4rem 1rem;
  background: var(--surface); border: 1px dashed var(--border); border-radius: 14px;
}
.empty > span:first-child { font-size: 2.2rem; line-height: 1; }
.empty b { color: var(--ink); font-size: 1rem; }
.empty .e-sub { color: var(--muted); font-size: .84rem; }

/* Transient feedback — an upload succeeded, a weight is out of order. These are events,
   not states: they are gone on the next run, which is why they are allowed to be loud
   where a standing condition is not. */
.flash { border-radius: 10px; padding: .55rem .8rem; font-size: .84rem; margin: .5rem 0;
  border: 1px solid var(--border); background: var(--surface); color: var(--ink); }
.flash.ok    { border-color: var(--good);
               background: color-mix(in srgb, var(--good) 12%, var(--surface)); }
.flash.warn  { border-color: var(--warn);
               background: color-mix(in srgb, var(--warn) 16%, var(--surface)); }
.flash.error { border-color: var(--crit);
               background: color-mix(in srgb, var(--crit) 10%, var(--surface)); }

/* ---------------------------------------------------------------- notifications */
/* **A passing confirmation must not move the page.** These used to be `.flash` lines
   rendered into the body, so a message arriving pushed everything below it down — and the
   control you had just clicked moved out from under the pointer. A toast is out of flow,
   over the top of the page, and gone in five seconds.

   Top centre rather than a corner: the eye is already near the top of the page after a
   save, and a corner toast on a wide screen is a long way from whatever was clicked.
   `pointer-events` is off on the stack and back on for each toast, so the region does not
   swallow clicks aimed at the page underneath it. */
.toasts {
  position: fixed; top: 1rem; left: 50%; transform: translateX(-50%);
  z-index: 60; display: flex; flex-direction: column; gap: .5rem;
  width: min(30rem, calc(100vw - 2rem)); pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex; align-items: flex-start; gap: .7rem;
  border-radius: 12px; padding: .6rem .7rem .6rem .9rem;
  border: 1px solid var(--border); background: var(--surface); color: var(--ink);
  font-size: .84rem; line-height: 1.45;
  box-shadow: 0 10px 30px rgba(36, 31, 78, .16);
  animation: toast-in .18s ease-out;
  transition: opacity .25s ease, transform .25s ease;
}
.toast .t-body { flex: 1; }
/* The ✕ is a control, so it looks like one — but recessive, because dismissing is not the
   point of the notification. */
.toast .t-close {
  flex: none; border: 0; background: none; cursor: pointer;
  color: var(--muted); font-size: .9rem; line-height: 1; padding: .1rem .2rem;
  border-radius: 6px;
}
.toast .t-close:hover { color: var(--ink); background: var(--plane); }
.toast.going { opacity: 0; transform: translateY(-6px); }

/* The same tones the inline flashes use, so a notification and a standing warning are
   recognisably the same vocabulary. Django's own tags are the class names. */
.toast.success { border-color: var(--good);
                 background: color-mix(in srgb, var(--good) 12%, var(--surface)); }
.toast.warning { border-color: var(--warn);
                 background: color-mix(in srgb, var(--warn) 16%, var(--surface)); }
.toast.error   { border-color: var(--crit);
                 background: color-mix(in srgb, var(--crit) 10%, var(--surface)); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}
/* Motion is decoration here; the message is the content. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; transition: none; }
}

/* ---------------------------------------------------------------- cards and forms */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; padding: 1.1rem 1.2rem;
  box-shadow: 0 2px 10px rgba(36, 31, 78, .05);
}
/* A chart brings its own top margin — 52px of it, for the title and legend — so the
   card's padding on that edge is counted twice. Trimmed only where a chart is the card's
   whole content, which is every chart card on the Dashboard. */
.card > .chart:first-child { margin-top: -.45rem; }

.field { display: block; margin-bottom: .8rem; }
.field > label { display: block; font-size: .8rem; color: var(--ink2); margin-bottom: .25rem; }
.field input, .field select, .field textarea {
  width: 100%; padding: .55rem .7rem; border-radius: 10px;
  border: 1px solid var(--border); background: var(--shade-1); color: var(--ink);
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--ev);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ev) 22%, transparent);
}

.btn {
  display: inline-block; width: 100%; text-align: center; cursor: pointer;
  padding: .6rem 1rem; border-radius: 999px; border: 1px solid transparent;
  background: var(--ev); color: #fff; font-weight: 650; font-size: .9rem;
}
.btn:hover { filter: brightness(1.06); }
.btn.secondary {
  background: var(--surface); color: var(--ink); border-color: var(--border);
  font-weight: 600;
}
.btn.secondary:hover { border-color: var(--ev); filter: none; }
.btn.link { background: none; border: 0; color: var(--muted); font-weight: 500;
  font-size: .8rem; text-decoration: underline; width: auto; padding: .2rem; }

/* ---------------------------------------------------------------- the sign-in card */
/* The gate's own surface. It is a page in its own right — an unauthenticated visitor
   gets this and nothing else, no rail and no page body — so it centres on the plane
   rather than sitting inside the app shell. */
.gate { display: flex; align-items: center; justify-content: center;
  min-height: 100vh; padding: 2rem 1rem; }
.signin { width: 100%; max-width: 430px; background: var(--surface);
  border: 1px solid var(--border); border-radius: 18px; overflow: hidden;
  box-shadow: 0 18px 50px rgba(36, 31, 78, .12); }
.signin-head { padding: 1.6rem 1.6rem 1.1rem; text-align: center; }
.signin-head .nf-mark { width: 62px; height: 62px; border-radius: 50%; padding: .6rem;
  display: block; margin: 0 auto; box-sizing: border-box;
  background: color-mix(in srgb, var(--ramp-a-1) 12%, var(--surface)); }
.signin-head .brand-name {
  display: block; margin-top: .5rem;
  font-weight: 700; font-size: 1.5rem; letter-spacing: .3px;
  background: linear-gradient(90deg, var(--ramp-a-0), var(--pv));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
.signin-head .rule { width: 44px; height: 2px; margin: .9rem auto .7rem;
  background: linear-gradient(90deg, var(--ramp-a-0), var(--pv)); border-radius: 2px; }
.signin-head h1 { font-size: 1.05rem; margin: 0; }
.signin-body { padding: 0 1.6rem 1.6rem; border-top: 1px solid var(--border);
  padding-top: 1.1rem; }
.signin .note { font-size: .78rem; color: var(--muted); margin: .8rem 0 0;
  text-align: center; }

/* ---------------------------------------------------------------- tables */
/* `html_table` renders a frame as this. Never `st.dataframe` — that drew to a canvas and
   painted from a theme fixed at boot, so it stayed light while the page went dark and no
   CSS reached it. There are no canvas grids left. */
.dtbl { width: 100%; border-collapse: collapse; font-size: .8rem;
  background: var(--surface); border-radius: 12px; overflow: hidden; }
.dtbl th { text-align: left; font-weight: 600; color: var(--muted);
  text-transform: uppercase; font-size: .68rem; letter-spacing: .06em;
  padding: .5rem .6rem; border-bottom: 1px solid var(--border);
  background: var(--shade-1); white-space: nowrap; }
.dtbl td { padding: .42rem .6rem; border-bottom: 1px solid var(--grid);
  color: var(--ink2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 320px; }
.dtbl td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* A count that is *about* the row rather than a quantity being compared down the column
   sits in the middle of it. Right alignment is for figures a reader scans as a column —
   money, hours — and the seat count is read against the limit beside it instead. */
.dtbl td.mid, .dtbl th.mid { text-align: center; }
.dtbl td.mid { font-variant-numeric: tabular-nums; }

/* Seats used / seats held. The pair reads as one value, so it is laid out as one. */
.dtbl td.seats { white-space: nowrap; }
.dtbl td.seats .used { font-weight: 650; color: var(--ink); }
.dtbl td.seats .of { color: var(--muted); margin: 0 .1rem; }
.dtbl input.cell.lim { width: 3.6rem; text-align: center; min-width: 0; }
/* The spinner is noise on a value that is usually set once. */
.dtbl input.cell.lim::-webkit-outer-spin-button,
.dtbl input.cell.lim::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.dtbl input.cell.lim { -moz-appearance: textfield; }
.dtbl tr:hover td { background: var(--shade-1); }
.dtbl tr:last-child td { border-bottom: 0; }
/* **The two administrative tables share one column geometry.** They carry the same seven
   columns' worth of room — an identity, two details, an access window, a status and the
   actions — and until the widths were pinned each sized itself to its own content, so
   Status sat at one x in the clients table and another in the accounts table directly
   below it, and the delete button moved with it. `table-layout: fixed` plus a width per
   column is what makes the two read as one screen rather than as two screens stacked.

   The first column takes whatever is left, because it holds the name and that is the one
   thing worth giving space to. `min-width` keeps the geometry on a narrow window: the
   wrapper scrolls sideways rather than the columns collapsing into each other. */
/* **Status and the actions land at the same x in every table on the screen**, and they
   are pinned *from the right* so a table with a different number of columns can join in.
   The project-numbers table has five columns to the accounts table's seven; measuring from
   the right is what lets both put Status in the same place without pretending to have the
   same shape. */
.dtbl.admin7, .dtbl.admintail { table-layout: fixed; min-width: 70rem; }
.dtbl.admin7 th:nth-last-child(2), .dtbl.admin7 td:nth-last-child(2),
.dtbl.admintail th:nth-last-child(2), .dtbl.admintail td:nth-last-child(2) { width: 6.5rem; }
.dtbl.admin7 th:last-child, .dtbl.admin7 td:last-child,
.dtbl.admintail th:last-child, .dtbl.admintail td:last-child { width: 13rem; }
/* **Every fixed width above is room taken from the first column**, which is the one
   holding the thing a reader is actually looking for — a client's name, or a person's name
   and address. They were 10/10/8/8/7/14rem and the identity column was left with about
   286px, enough to truncate "Northwind Energy" and to cut an address in half. Trimmed to
   9/9/7/7/6.5/13 it gets about 375px, and nothing else lost anything it was using: a
   date field is 5.3rem of content, "Application Admin" 6.9, and five buttons 10.6.

   What is left over goes to the name, which is the column worth the room. */
.dtbl.admintail th:first-child, .dtbl.admintail td:first-child { width: 11rem; }
.dtbl.admintail th:nth-last-child(3), .dtbl.admintail td:nth-last-child(3) { width: 8rem; }
.dtbl.admin7 th:nth-child(2), .dtbl.admin7 td:nth-child(2) { width: 9rem; }
.dtbl.admin7 th:nth-child(3), .dtbl.admin7 td:nth-child(3) { width: 9rem; }
.dtbl.admin7 th:nth-child(4), .dtbl.admin7 td:nth-child(4) { width: 7rem; }
.dtbl.admin7 th:nth-child(5), .dtbl.admin7 td:nth-child(5) { width: 7rem; }
/* Five controls on the widest line — edit, disable, bin, reset, give access — so the
   actions column above is sized for that cluster rather than for the common one. A cluster
   that overflows its cell is a delete button nobody can see. */
/* The line revealed by ✚ spans the table, so it opts out of the fixed widths above. */
.dtbl.admin7 tr.grantrow td { width: auto; }

/* **Giving access, on one line.** It is a sentence with two dropdowns in it — "Give Dana
   access to 4458 Brightwater as Viewer" — rather than a panel of labelled fields, because
   it asks for exactly two things and reads as what it does. It wraps rather than
   overflowing: this is the one cell in the table with no fixed width. */
.gadd { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
        padding: .15rem 0; }
.gtitle { color: var(--ink2); }

/* The identity column holds two fields on one line — a name and an address — so they take
   a share of it rather than a fixed `size`. With the column widths pinned above, a `size`
   in characters is a promise the column cannot keep: the address was squeezed to an
   ellipsis and the row said nothing about who it was. */
.dtbl.admin7 td:first-child input.cell.strong { width: 44%; }
.dtbl.admin7 td:first-child input.cell.cnt { width: 52%; }

/* Wide content scrolls inside its own box; the page must never scroll sideways. */
.tblwrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 12px; }

/* ---------------------------------------------------------------- KPI tiles */
/* Four across, and the tile pins its three parts. The rows were once four, then five,
   then four, which made the middle row's cards a fifth narrower than those above and
   below — one card in three widths down a single page. A `min-height` fixed none of the
   raggedness either: a two-word label wraps where "CPI" does not, so the value under it
   started a line lower than its neighbours. Hence `.klabel` reserving two lines whether
   it needs them or not, the flex column, and the chip pushed down with `margin-top:auto`.
   Measured on this page: twelve tiles at **286×136**, label 40px, value at y=57, chip at
   y=100 — identical to the pixel across all three strips. When the strips still looked
   ragged the fault was not here at all but *between* the rows, which carried no margin
   while the columns carried 12.8px; see the vertical rhythm block below. */
.kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--gap); }
.ktile { display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; padding: 1rem 1.1rem; min-height: 136px;
  box-shadow: 0 2px 10px rgba(36, 31, 78, .05); }
.klabel { color: var(--muted); font-size: .78rem; text-transform: uppercase;
  letter-spacing: .08em; line-height: 1.6; min-height: 2.5rem; }
.kvalue { color: var(--ink); font-weight: 640; line-height: 1.4;
  font-size: clamp(1.3rem, 1.1vw + .95rem, 1.8rem);
  /* "$1,234,567" and "97%" share a row, so the long one shrinks rather than wraps. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The verdict chip. The hue sits in the fill and the border while the text stays in ink:
   no palette here has a green, and `#FFD330` as type is 1.44:1 and simply unreadable. So
   the word and the ▲/▼ carry the meaning and colour only supports it. */
.kchip { display: inline-block; margin-top: auto; align-self: flex-start;
  font-size: .72rem; font-weight: 600; color: var(--ink);
  border: 1px solid transparent; border-radius: 999px;
  padding: .05rem .5rem; letter-spacing: .01em; }
.kchip.good { border-color: var(--good);
              background: color-mix(in srgb, var(--good) 18%, var(--surface)); }
.kchip.warn { border-color: var(--warn);
              background: color-mix(in srgb, var(--warn) 24%, var(--surface)); }
.kchip.crit { border-color: var(--crit);
              background: color-mix(in srgb, var(--crit) 14%, var(--surface)); }

@media (max-width: 1100px) { .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 720px) {
  .app { flex-direction: column; }
  .rail { width: 100%; flex-basis: auto; height: auto; position: static;
          flex-direction: row; flex-wrap: wrap; }
  .kpis { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------- look-ahead */
/* One chip per gate, coloured by this document's state at it. Five states, and "not
   applicable" — a gate the schedule never planned for this document — must read as
   absent rather than as outstanding, or the look-ahead flags gates nobody ever had. */
.gpath { display: flex; flex-wrap: wrap; gap: .22rem; }
.gc { font-size: .62rem; font-weight: 600; letter-spacing: .04em;
  padding: .08rem .34rem; border-radius: 5px; border: 1px solid var(--border);
  color: var(--ink); }
.gc.done { border-color: var(--good);
           background: color-mix(in srgb, var(--good) 22%, var(--surface)); }
.gc.miss { border-color: var(--crit);
           background: color-mix(in srgb, var(--crit) 18%, var(--surface)); }
.gc.due  { border-color: var(--ramp-a-1);
           background: color-mix(in srgb, var(--ramp-a-1) 18%, var(--surface)); }
.gc.todo { background: var(--plane); color: var(--muted); }
.gc.na   { background: transparent; color: var(--muted); opacity: .55; }

.sev { display: inline-block; font-size: .68rem; font-weight: 600; color: var(--ink);
  border: 1px solid transparent; border-radius: 999px; padding: .05rem .5rem;
  white-space: nowrap; }

/* A discipline heading inside a table — the hierarchy the look-ahead groups by. */
.dtbl tr.disc td { background: var(--shade-1); font-weight: 650; color: var(--ink);
  border-top: 1px solid var(--border); }
.late { color: var(--crit); font-weight: 600; }

/* The controls strip above a report: a horizon, a toggle, a few filters. */
.controls { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .7rem;
  margin: .6rem 0 1rem; }
.controls .field { margin: 0; min-width: 150px; }
/* A field marked `grow` takes the room left over, which pushes the button that follows it
   to the right end of the row. Without it a two-field form sits huddled at the left with a
   button beside it and a metre of empty space after, and the name — the longest thing
   anybody types here — gets the same 150px as the number. */
.controls .field.grow { flex: 1 1 12rem; }
.controls .field > label { font-size: .72rem; color: var(--muted); }
.controls .btn { width: auto; padding: .5rem 1.1rem; }

/* **A filter bar belongs to one table, and it looks like it.** These were one `.controls`
   row above both hierarchies carrying every filter for both, which made a reader work out
   from the labels alone which table each control changed — and forced the deliverable
   discipline filter to be called "Deliverable discipline" to be told apart from the
   activity one four controls to its left.

   Sitting on its table it is quieter than a form: smaller fields, a tighter gap, and a
   rule under it that reads as the top edge of the table rather than as a panel of its own.
   The button is `.btn.sm` for the same reason — Apply is not the page's main action.

   Its fields are narrower than `.controls`' 150px because there are four of them plus a
   search on the deliverables bar, and a bar that wraps to three lines is no longer a bar. */
.fbar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .45rem .7rem;
        margin: .5rem 0 0; padding: .45rem .1rem .55rem;
        border-bottom: 1px solid var(--border); }
.fbar .field { margin: 0; min-width: 8.5rem; flex: 0 1 auto; }
.fbar .field > span { display: block; font-size: .7rem; color: var(--muted);
                      margin-bottom: .15rem; }
.fbar .field select, .fbar .field input {
  width: 100%; padding: .3rem .5rem; font-size: .78rem; border-radius: 8px;
  border: 1px solid var(--border); background: var(--shade-1); color: var(--ink);
}
/* **Apply and Clear sit at the bar's right end**, not after the last dropdown: the two
   bars carry a different number of filters, so following the last one would put Apply at a
   different x on each and a reader would have to find it twice. `margin-left: auto` pins
   the pair right, which is the same argument that pins the administrative tables' actions
   column from the right rather than from the left.

   `width: auto` is load-bearing — `.btn` is `width: 100%`, which is meant for a form's
   full-width submit and would otherwise have these two fighting the fields for the row. */
.fbar-acts { margin-left: auto; display: flex; align-items: center; gap: .35rem;
             margin-bottom: .05rem; }
.fbar .btn { width: auto; }
/* Clear is an `<a>` so it needs the line-height a `<button>` gets for nothing. */
.fbar-acts a.btn { text-decoration: none; line-height: 1.35; }

.pbar { position: relative; background: var(--track); border-radius: 999px;
  height: 15px; min-width: 80px; overflow: hidden; }
.pbar i { position: absolute; inset: 0 auto 0 0; background: var(--ev); display: block; }
/* Past its budget: the fill takes the palette's red and the label stays ink, bold — red
   type on a red fill would be the one figure nobody could read. */
.pbar.over i { background: var(--crit); }
.pbar.over span { font-weight: 700; }
.pbar span { position: relative; font-size: .66rem; color: var(--ink);
  display: block; text-align: center; line-height: 15px; }
/* **The label is drawn twice** (`ui._on_fill`): ink underneath, and a copy over it clipped
   to the filled width, in white or near-black by the fill's own luminance
   (`theme.on_fill`). A figure half over a deep fill then reads white where it
   crosses the fill and ink on the track — on Light the ink *is* the EV fill's deep, so a
   single label vanished into its own bar. */
.pbar span.pon { position: absolute; inset: 0; color: var(--on-ev, #fff); }
.pbar.over span.pon { color: var(--on-crit, #fff); }

/* ---------------------------------------------------------------- vertical rhythm */
/* One gap, used for the space *between* blocks and the space *inside* a grid alike. The
   three KPI strips gave the game away: 12.8px between the columns of a row and 0px
   between the rows themselves, so twelve tiles of identical size still read as a slab
   rather than as three rows of four. Every measurement said the tiles were perfect —
   286x136, label 40, value at y=57 — because the fault was never in the tile. */
:root { --gap: .8rem; }

/* No `min-height` here. `charts.render` writes the figure's own height inline instead:
   the div must reserve something or the page jumps as each chart paints, but a constant
   is wrong for every figure that is not that height. */
.chart { width: 100%; }
.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gap); }

/* **Every block, one rule.** Written first as a list of the pairs that actually occur —
   `.kpis + .kpis`, `.grid2 + .card`, and seven more — which is a list that is wrong the
   moment a page puts two blocks together in an order nobody enumerated. It was already
   wrong twice on its first run: `.numbers + .kpis` and `.grid2 + .numbers` both fell
   through and rendered flush. An adjacent-sibling rule over everything cannot miss a
   combination, and the exceptions below are then deliberate rather than accidental.

   Adjacent siblings only, so nothing carries a trailing margin into the foot of the
   page. */
.page > * + * { margin-top: var(--gap); }

/* Chip rows are a line of text rather than a block, so they sit closer to what they
   annotate — consistently, and with a little more room above the first of a run. */
.page > .statrow { margin-top: .35rem; margin-bottom: 0; }
.page > .kpis + .statrow, .page > .card + .statrow { margin-top: .6rem; }
/* A section heading belongs to the thing under it, not to the thing above. */
.page > .sect + * { margin-top: .35rem; }

/* The Dashboard's table twin for the two donuts, collapsed by default. Open it and every
   value in the rings is readable as a number — which is the relief the brand teal is kept
   on: it misses the chroma and contrast checks and stays anyway, because it is the
   signature hue and the sheet has no darker step to snap to. */
/* **A card, not a bare line of text.** These hold the forms that create an account, a
   client or a project number, and as an unadorned summary they read as a caption somebody
   might not realise was clickable — and once opened, the fields floated on the page with
   nothing saying where the form began or ended. The border is what makes it a thing you
   open; `[open]` gives the summary its own separator so the heading stays attached to the
   box rather than to whatever follows it. */
.numbers {
  margin-bottom: 0; border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface); overflow: hidden;
}
.numbers > summary {
  cursor: pointer; font-size: .84rem; font-weight: 600; color: var(--ink2);
  padding: .6rem .9rem; list-style: none; user-select: none;
}
.numbers > summary:hover { background: color-mix(in srgb, var(--plane) 55%, var(--surface));
                           color: var(--ink); }
.numbers[open] > summary { border-bottom: 1px solid var(--border); }
.numbers > summary::before { content: "▸ "; color: var(--muted); }
.numbers[open] > summary::before { content: "▾ "; }
.numbers > summary::-webkit-details-marker { display: none; }
/* The body gets the padding, so a table inside still reaches the box's own edges. */
.numbers > :not(summary) { padding: .8rem .9rem; }
.numbers > .tblwrap { padding: 0; border: 0; border-radius: 0; }

/* A figure that grows with its data — Cost by WBS draws every code — inside a box that
   keeps the row's height. The alternative is a top 10, which hides the small code that
   has doubled. */
.card.scrolls { max-height: 380px; overflow-y: auto; }

/* A one-line aside beside a control, for when the week shown and the data date differ. */
.controls .note { font-size: .78rem; color: var(--muted); align-self: center; }

/* ---------------------------------------------------------------- the range slider */
/* Styled from the palette rather than left native: a browser's default track and thumb
   are the OS accent colour, which is the one thing on the page that does not follow the
   palette — and under Dark it is a bright blue nub on a dark card.
   `-webkit-appearance:none` is what lets the track and thumb be styled at all; without
   it the rules below are accepted and silently ignored. */
.field.slider { min-width: 230px; }
.field.slider .sv {
  color: var(--ink); font-weight: 650; font-variant-numeric: tabular-nums;
}
input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 20px; background: transparent; cursor: pointer; margin: 0;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 5px; border-radius: 999px; background: var(--track);
}
input[type="range"]::-moz-range-track {
  height: 5px; border-radius: 999px; background: var(--track);
}
/* The thumb is centred on the track by hand: `margin-top` is half the track height minus
   half the thumb's, and Firefox needs no such correction, hence the two blocks. */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px; border-radius: 50%; margin-top: -5.5px;
  background: var(--ev); border: 2px solid var(--surface);
  box-shadow: 0 1px 4px rgba(36, 31, 78, .28);
}
input[type="range"]::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--surface);
  background: var(--ev); box-shadow: 0 1px 4px rgba(36, 31, 78, .28);
}
input[type="range"]:focus-visible { outline: 2px solid var(--ev); outline-offset: 4px; }

/* ---------------------------------------------------------------- hierarchy rows */
/* Discipline → WBS → Document, indented by an em-space in the label itself rather than by
   a padding rule per level: the indent then survives the cell's `title` attribute, which
   is what makes a clipped 8-segment task code readable on hover. */
.dtbl tr.lv-disc > td { font-weight: 600; background: color-mix(in srgb, var(--plane) 55%, var(--surface)); }
.dtbl tr.lv-wbs > td { background: color-mix(in srgb, var(--plane) 25%, var(--surface)); }
.dtbl tr.lv-act > td, .dtbl tr.lv-doc > td { font-weight: 400; }
.dtbl td.bars { min-width: 130px; }
.dtbl td .pbar { margin: 1px 0; }

/* **The hierarchies are tighter than every other table here, and the measurements say
   why.** A row was 47px at all three levels and the activity table holds 79 of them, so a
   520px box showed eleven — a reader spent most of the page scrolling to find the row they
   were reporting on. Three things made the 47, and none of them was the type, which is
   already .8rem:

   - **the bar cell**, two 15px bars and their margins, 32px of the 47;
   - **13.4px of cell padding**, which is right for a row of prose and generous for a row
     that is mostly a bar;
   - **the group rows' caption on its own line**, because `.cnt` is `display: block`
     everywhere else — so every discipline and WBS row was two lines high to say
     "1 activities".

   Scoped to `.dtbl.tree`, which today is exactly these two tables: the administrative
   tables are pinned at 38px and one line, and the Task Code Budget's bars sit one to a
   cell where the height buys nothing. Measured after: 34px an activity row and 25px a
   group row, so the same box shows about nineteen. */
.dtbl.tree td { padding: .3rem .6rem; }
.dtbl.tree td .pbar { height: 11px; margin: .5px 0; }
/* The label rides the bar's height down with it. It is a second statement of a figure the
   bar already draws — and, on an activity, of the one in the % Complete column beside it —
   so it is the thing in the cell that can afford to be small. */
.dtbl.tree td .pbar span { font-size: .6rem; line-height: 11px; }
/* **On the same line as the label, not under it.** A count of what a group holds is about
   the label it follows, which is what `.cnt` means; stacking it made every parent row two
   lines to carry four characters. */
.dtbl.tree td .cnt { display: inline; margin-left: .45rem; }

/* ---------------------------------------------------------------- the % Complete cell */
/* **A figure, and the ✎ that overrides it.** The cell is answered on every row from the
   activity's own schedule, so what it holds by default is text; the input belongs to the
   one row somebody has unlocked. Laid out as a flex line rather than left to inline flow,
   or the number, the marker and the buttons sit on the text baseline at three different
   heights and the column reads as ragged wherever a row is open.

   `form.inline` is `display: inline` at (0,0,1,1), so this has to be at least as specific
   to win — hence `form.inline.pcell` rather than a bare class. */
form.inline.pcell {
  display: inline-flex; align-items: center; justify-content: center; gap: .25rem;
  white-space: nowrap;
}
/* **`.btn` is `width: 100%`**, which is right for a card's action and wrong for four
   glyphs on one line: as flex items they each claimed the cell's whole width, the line
   overflowed, and every item was shrunk to pay for it — the input came back 33.8px wide,
   which is not a field anybody can read a percentage in. The same `width: auto` the filter
   bar's buttons need, for the same reason. The input is pinned besides, since it is the
   one item here whose width means something. */
form.inline.pcell .btn { width: auto; }
form.inline.pcell input.pct { flex: 0 0 auto; }
/* The input, on the same stadium the administrative rows use: it is a control that is
   locked until a row is unlocked, which is what `.cell` means, and `tr.editing` is what
   gives it its ink, its surface and its border. Narrow enough for "100", centred with the
   heading, and with no spinner — a stepper on a percentage invites one-at-a-time nudging
   up to 100. */
.dtbl input.cell.pct { width: 3.6rem; min-width: 0; text-align: center; }
.dtbl input.cell.pct::-webkit-outer-spin-button,
.dtbl input.cell.pct::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.dtbl input.cell.pct { -moz-appearance: textfield; }
/* **The estimate says so.** A bare number under a heading reading "% Complete" is a claim,
   and most of this column is read off a calendar until somebody states otherwise. Marked
   on the estimates rather than on the reported figures: the qualification belongs on the
   figure that carries one. A caption, in the caption's own ink — never a colour on the
   number itself, which would be a status rendering without its word. */
.est { font-size: .68rem; color: var(--muted); margin-left: .2rem; }
/* The SOW check slot. Its label and the revision it alternates with sit on the same line,
   or the column jogs wherever an issued document meets one that has not issued. */
label.sow { display: inline-flex; align-items: center; gap: .35rem; font-size: .8rem;
            font-weight: 400; white-space: nowrap; cursor: pointer; }

/* The bar legend. Three swatches and their names, small enough to read as a caption. */
.pleg { display: flex; gap: 1rem; font-size: .78rem; color: var(--ink2);
        margin: .2rem 0 .4rem; }
.pleg span { display: inline-flex; align-items: center; gap: .35rem; }
.pleg i { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }

p.note { font-size: .8rem; color: var(--muted); margin: .3rem 0; }

/* ---------------------------------------------------------------- source rows */
/* Nine sources at one row each. A grid rather than a table because the last cell is a
   file input, and the whole row has to stay one line at a readable height — as separate
   blocks these sat further apart than they were tall. */
.srcs { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
.srow {
  display: grid; grid-template-columns: minmax(0, 2.6fr) 2rem 7rem minmax(0, 2.2fr);
  align-items: center; gap: .6rem; padding: .5rem .8rem;
}
/* **The separator is on the row, and the exception is on the last row.** In Streamlit
   this was the trap: `st.columns` puts each horizontal block alone inside its own
   wrapper, so every block was `:last-child` of its own parent and one rule erased the
   separator on all seven rows. Here they really are siblings. */
.srow + .srow { border-top: 1px solid var(--border); }
.srow:nth-child(even) { background: color-mix(in srgb, var(--plane) 45%, var(--surface)); }
.srow-n { font-size: .86rem; color: var(--ink); display: flex; align-items: center;
          gap: .4rem; min-width: 0; }
.srow-s { font-size: 1rem; }
.srow-m { text-align: center; font-size: .95rem; }
.srow-d { font-size: .76rem; color: var(--muted); white-space: nowrap;
          overflow: hidden; text-overflow: ellipsis; }
/* The row's controls — picker, ⬇️, 📐, 🗑 — sit at its right end, in one place down the
   whole list whatever each row carries. */
.srow-u { display: flex; align-items: center; justify-content: flex-end; gap: .4rem; }
/* **A file picker is a button that says whether it holds a file** (filepick.js). The
   native input — "Choose file" beside "No file chosen", painted by the OS outside the
   palette — sits invisibly inside the label, still focusable and still `required`. Once a
   file is chosen the button takes the good colour *and* a ✓ with the file's name: colour
   never renders on its own here, and "good" is a teal or cyan in every palette, not a
   green. */
.filepick { position: relative; cursor: pointer; width: auto; flex: none; gap: .3rem;
            max-width: 16rem; }
.filepick input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0;
                               left: 50%; bottom: 0; overflow: hidden;
                               padding: 0; border: 0; min-width: 0; }
.filepick:focus-within { outline: 2px solid var(--pv); outline-offset: 2px; }
.filepick.picked { border-color: var(--good); color: var(--ink);
                   background: color-mix(in srgb, var(--good) 16%, var(--surface)); }
.fp-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fp-head { display: block; font-size: .72rem; color: var(--muted); margin-bottom: .25rem; }
/* `.btn` is `width: 100%`, so in this flex row the ⬇️ and the 🗑 would each claim the whole
   cell and squeeze the file picker; they take their own width instead. */
.srow-u .btn { width: auto; flex: none; text-decoration: none; }

/* The invoices table's actions: ⬇️ and 🗑 side by side at their own width, as on the
   source rows above it. The delete is a one-button form, so it must not break the line. */
.inv-acts { display: flex; gap: .4rem; justify-content: flex-end; }
.inv-acts form { margin: 0; }
.inv-acts .btn { width: auto; flex: none; text-decoration: none; }
/* One line: the fields, then ＋ Add invoice at their right. Description takes the slack. */
.inv-add { margin-bottom: var(--gap); flex-wrap: nowrap; }
.inv-add .field { min-width: 0; }
.inv-add .field.grow { flex: 1 1 10rem; }
.inv-add .inv-go { flex: none; white-space: nowrap; }
/* An unlocked invoice row: its fields fill their cells, and the optional replacement
   document sits under the description rather than widening the table. */
.dtbl.inv input.cell { width: 100%; }
.dtbl.inv .inv-file { margin-top: .3rem; }
/* Nothing uploaded yet: the ⬇️ stays in its place, greyed, and does nothing. */
.srow-u .btn.off { opacity: .35; cursor: not-allowed; pointer-events: none;
                   filter: grayscale(1); }

/* **The weight cell is a control, and it is stadium-shaped for the same reason the
   administrative tables' fields are**: an editable cell in a table of square cells has to
   look like something you type in rather than a cell somebody has outlined.

   It differs from `.dtbl input.cell` in one way, and the difference is the point. Those
   are locked until a row is unlocked, so they wear no border until they are; this one is
   *always* editable — there is no ✎ on this table — so it carries its border and its
   surface all the time. A field that only looks like a field once you have guessed to
   click it is a field nobody edits.

   **Centred, and the heading with it.** It was right-aligned under a left-aligned "Weight
   %", so the column's values and the word above them stood at opposite edges of the same
   cell. Centring is what suits it: this is a short percentage read *against the gate on
   its row*, not a money column scanned down against its neighbours — which is the same
   reason the seat count and the People count are centred rather than `.num`.

   No spinner: the value is typed, and a stepper on a percentage invites one-at-a-time
   nudging up a list that has to end at 100. */
input.wt {
  width: 5.5rem; text-align: center; font-size: .78rem; color: var(--ink);
  height: 1.6rem; box-sizing: border-box; padding: .16rem .6rem;
  border-radius: 999px; border: 1px solid var(--border);
  background: var(--shade-1);
}
input.wt:focus {
  outline: none; border-color: var(--ev);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ev) 22%, transparent);
}
input.wt::-webkit-outer-spin-button,
input.wt::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input.wt { -moz-appearance: textfield; }

/* ---------------------------------------------------------------- admin tables */
/* A sortable heading is a link, so it has to stop looking like one: the underline and the
   link colour both fight the table's own quiet head row. The arrow carries the state —
   ▲/▼ on the *active* column and ⇅ on the rest, because an arrow on every heading says
   which columns can sort rather than which one is sorting, and only the second is
   something the reader needs. */
.dtbl th.sortable a {
  color: inherit; text-decoration: none; display: inline-block; white-space: nowrap;
}
.dtbl th.sortable a:hover { color: var(--ink); }
.dtbl th.sortable.on { color: var(--ink); }

/* The same affordance, done in the browser. `tablesort.js` wires every other `.dtbl`, so
   these have to look and behave like the server-sorted headings above — the reader is not
   told which of the two a given table uses, and should not have to care. */
.dtbl th.jsort { cursor: pointer; user-select: none; }
.dtbl th.jsort:hover, .dtbl th.jsort:focus-visible { color: var(--ink); }
.dtbl th.jsort.on { color: var(--ink); }
.smark { margin-left: .3rem; opacity: .8; }

/* The action cluster. Forms are inline so a row of buttons is one line rather than a
   stack — a `<form>` is a block by default and each control would otherwise claim its own
   row. */
form.inline { display: inline; }
.dtbl td.acts { white-space: nowrap; }
.dtbl td.acts form { display: inline-flex; gap: .25rem; align-items: center; }
.dtbl td.win { white-space: nowrap; }

/* ---------------------------------------------------------------- editing a row */
/* **A row is edited where it is read.** These fields used to be a separate "Manage an
   account" panel below the table, which meant picking the account a second time from a
   dropdown and matching it back to the row you were looking at.

   Locked, a field is the value and nothing else: no border, no fill, and **the same
   `--ink2` every other `.dtbl td` uses**, so a table of inputs reads as a table rather
   than as a greyed-out one. It was `--muted` at first, which is the token for a *caption*
   — two steps lighter than a cell — and the effect was that the accounts and clients
   tables looked disabled beside the provisioning log directly below them, which is
   ordinary markup. Unlocked by the ✎ the field takes the page's full ink, a surface and a
   border — **that step from ink2 to ink is the state**, and it is why saving (which
   re-renders the row locked) needs to say nothing. Stadium rather than rectangular: an
   editable cell inside a table of square cells needs to look like a control rather than a
   cell somebody has outlined. */
.dtbl input.cell, .dtbl select.cell {
  font: inherit; font-size: .78rem; color: var(--ink2);
  /* One height for every kind of control in a row. A date input is a couple of pixels
     taller than a text one — it carries a picker — which was enough to make the one
     account row that had dates 40px against its neighbours' 38. */
  height: 1.45rem; box-sizing: border-box;
  padding: .16rem .6rem; border-radius: 999px;
  border: 1px solid transparent; background: transparent;
  max-width: 100%; min-width: 4.5rem; text-overflow: ellipsis;
}
/* **`hidden` loses to a `display` of its own**, and that is what put a Save and a Cancel
   button on every row of a table nobody was editing. The UA sheet's `[hidden]{display:none}`
   is a plain element rule, so `.btn { display: inline-flex }` outranks it and the attribute
   silently does nothing. Stated here once, for every control on the page. */
[hidden] { display: none !important; }

/* A locked select must read as the value, not as a control: left native it keeps its
   dropdown arrow, which promises a menu that will not open. */
/* The address beside the name keeps the caption ink: it is secondary to the value it sits
   with, which is what `.cnt` means everywhere else here. Written as its own rule because
   `.dtbl input.cell` (0,0,2,1) outranks a bare `.cnt` (0,0,1,0).

   **On one line, not two.** `.cnt` is `display: block` everywhere else, which stacked the
   address under the name and made every account row 61px against the clients table's 38 —
   two tables on one screen with rows two thirds different in height, for no reason a
   reader could see. Overriding the display is the whole fix: the row becomes one line like
   every other row on the page.

   The account's name was a `<b>` before it became a field and stays bold: it is what the
   row is *about*, and the weight is its identity — only the colour changes with state. */
.dtbl input.cell.strong { font-weight: 700; }
.dtbl input.cell.cnt {
  color: var(--muted); display: inline-block; margin-left: .15rem;
}
/* **A field's text must land where a plain cell's text lands.** A control sits at the
   cell's content edge and then adds its own padding, so every editable column started
   9.6px to the right of its own heading and of every read-only column beside it — the
   roles looked indented against "ROLE" above them. Pulling the control back by exactly its
   own padding puts the *text* on the cell's line and leaves the stadium's rounded edge
   sitting in the cell's padding, where it belongs.

   Only the first control in a cell, and not in a centred or right-aligned one: the address
   follows the name on the same line, and the seat limit is centred against the count it is
   read with. */
.dtbl td:not(.mid):not(.num) > .cell:first-child { margin-left: -.6rem; }
.dtbl select.cell { padding-right: .3rem; }
.dtbl select.cell:disabled { -webkit-appearance: none; appearance: none; opacity: 1; }
.dtbl tr.editing select.cell { -webkit-appearance: auto; appearance: auto;
  padding-right: .3rem; }
.dtbl input.cell:read-only { cursor: default; }
/* `appearance: none` on a locked date field: its picker icon is a control, and a control
   on a field nobody can type in is an invitation that does nothing. */
.dtbl input.cell[type="date"]:read-only::-webkit-calendar-picker-indicator { display: none; }
.dtbl tr.editing input.cell, .dtbl tr.editing select.cell {
  color: var(--ink); background: var(--surface); border-color: var(--border);
}
.dtbl tr.editing input.cell:focus, .dtbl tr.editing select.cell:focus {
  outline: none; border-color: var(--ev);
}
.dtbl tr.editing > td { background: color-mix(in srgb, var(--ev) 5%, var(--surface)); }

.btn.sm { padding: .18rem .5rem; font-size: .76rem; }
.cnt { display: block; color: var(--muted); font-size: .74rem; }

/* The one-time password: monospace, selectable, and visibly a thing to copy. It exists in
   readable form nowhere else, so it must not look like ordinary prose. */
code.onetime {
  display: inline-block; margin-left: .4rem; padding: .15rem .5rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .86rem; letter-spacing: .02em; user-select: all;
  background: var(--plane); border: 1px solid var(--border); border-radius: 6px;
}

/* A grant row: the project, and what they are in it. */
.grantrow {
  display: flex; align-items: center; gap: .6rem; padding: .3rem 0;
  border-bottom: 1px solid var(--border);
}
.grantrow > label { flex: 1; display: flex; align-items: center; gap: .45rem;
  font-size: .85rem; }
.grantrow select { max-width: 190px; }

/* Forms that stack their control rows rather than running them together. */
form.stack { display: flex; flex-direction: column; gap: .5rem; margin: .5rem 0; }
form.stack .controls { margin: 0; }
.field.grow { flex: 1; min-width: 220px; }

/* The status pill, reusing the severity chip. Colour is never alone: every one ships its
   word, because no palette here has a green and "good" is a teal with no inherited
   meaning. */
.sev.good { border-color: var(--good);
            background: color-mix(in srgb, var(--good) 18%, var(--surface)); }
.sev.warn { border-color: var(--warn);
            background: color-mix(in srgb, var(--warn) 24%, var(--surface)); }
.sev.crit { border-color: var(--crit);
            background: color-mix(in srgb, var(--crit) 14%, var(--surface)); }
@media (max-width: 1000px) { .grid2 { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------- print */
/* The client report is the one screen that goes on paper, and the screen layout is built
   to scroll — tables in a fixed-height box, cells that never wrap. On paper that means
   everything below the fold and right of the margin is simply gone. So printing undoes
   the scrolling, the clipping and the app chrome.
 
   Backgrounds have to be asked for explicitly: browsers drop them when printing unless
   `print-color-adjust` says otherwise, and without them a severity badge is a word in a
   white box and the gate-path chips lose the only thing distinguishing them. */
@media print {
  @page { margin: 14mm; }

  html, body { background: #fff; }

  /* The chrome goes: the rail, the sticky header, every control, the print button
     itself. What is left is the report. */
  .rail, .app-header, .controls, .noprint, form { display: none !important; }
  .app { display: block; }
  .main { padding: 0; }
  .page { max-width: none; }

  /* Undo the scroll boxes. A table in a 420px window prints its first eight rows and
     silently drops the rest — which is the failure mode that matters, because nothing on
     the page says anything is missing. */
  .tblwrap { overflow: visible !important; max-height: none !important; border: 0; }
  .dtbl td { white-space: normal; max-width: none; }

  /* Keep every fill and border. */
  *, *::before, *::after {
    -webkit-print-color-adjust: exact !important;
    print-color-adjust: exact !important;
  }

  /* Do not split a table row, a tile or a chart across a page break. */
  tr, .ktile, .chart, .card { break-inside: avoid; page-break-inside: avoid; }
  thead { display: table-header-group; }   /* repeat headings on every page */

  .kpis { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .grid2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .card { box-shadow: none; }
  .hint { display: none; }                 /* a tooltip is nothing on paper */
}

/* **The Trend Register.** Fixed geometry like the administrative tables, so the ticks and
   the actions sit at one x however long a description runs. The text columns **wrap in
   full** rather than truncating: the description is the part of a trend somebody needs to
   read, and a one-line ellipsis with the rest on hover hid exactly that. Rows grow to fit,
   and cells align to the top so a long description does not float its neighbours. */
.dtbl.trendtbl { table-layout: fixed; min-width: 74rem; }
.dtbl.trendtbl td { vertical-align: top; }
/* Shares of the table rather than fixed widths, so the two text columns take whatever the
   window gives them: fixed widths summed to 70rem and left Description 60px wide. Named
   by class rather than position, so a column added or withdrawn does not shift the rest. */
.dtbl.trendtbl th { white-space: normal; }
.dtbl.trendtbl .c-no { width: 6%; }
.dtbl.trendtbl .c-title { width: 9%; }
.dtbl.trendtbl .c-desc { width: 17%; }
.dtbl.trendtbl .c-resp { width: 11%; }
.dtbl.trendtbl .c-res { width: 18%; }
.dtbl.trendtbl .c-hrs { width: 5.5%; }
.dtbl.trendtbl .c-tick { width: 6.5%; }
.dtbl.trendtbl .c-var { width: 7%; }
.dtbl.trendtbl .c-acts { width: 7%; }
.dtbl.trendtbl .wrap { white-space: normal; overflow-wrap: break-word; display: block; }
.dtbl.trendtbl input.cell:not(.lim), .dtbl.trendtbl textarea.cell { width: 100%; }
.dtbl.trendtbl textarea.cell { resize: vertical; font: inherit; }
.tlines { display: flex; flex-direction: column; gap: .3rem; align-items: flex-start; }
/* A line is two rows: what is needed (resource, hours), then where it is budgeted (the
   task code, the full width, because a code is 29 characters and its label more). */
.tline { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .3rem;
  align-items: center; width: 100%; }
.tline .tl-pick { grid-column: 1 / -1; min-width: 0; width: 100%; }
.tlines .tline + .tline { margin-top: .25rem; padding-top: .3rem;
  border-top: 1px dashed var(--border); }
.tline select { flex: 1 1 auto; min-width: 0; }
.tline input[type=number] { width: 5rem; flex: 0 0 auto; }
.tl-head { font-size: .8rem; color: var(--muted); }
.tl-read { white-space: normal; }
.tl-code { display: block; font-size: .72rem; color: var(--muted); overflow-wrap: anywhere; }
.tl-code.nocode { font-style: italic; }
.tl-read + .tl-read { margin-top: .15rem; }
.dtbl .tline input.cell.lim { width: 3.4rem; }
.dtbl .tline select.cell { min-width: 0; }
.newtrend textarea { width: 100%; font: inherit; resize: vertical; }
.newtrend .tlines { max-width: 40rem; }
/* A multi-line field takes the cell's ink and the editing state's colours like every other
   `.cell`, but a rounded rectangle rather than a stadium — a pill four lines tall is not a
   shape anybody reads as a text box. It is only ever shown unlocked. */
.dtbl textarea.cell {
  font-size: .78rem; box-sizing: border-box; padding: .3rem .5rem; border-radius: .5rem;
  color: var(--ink); background: var(--surface); border: 1px solid var(--border);
}
.dtbl textarea.cell:focus { outline: none; border-color: var(--ev); }
.dtbl.trendtbl td:not(.mid):not(.num) > .cell:first-child { margin-left: 0; }
/* `.btn` is `width: 100%` for the rail; the ＋ under a trend's lines is a small control
   beside them, not a bar across the form. */
.tlines .btn.addline { width: auto; padding-inline: .8rem; }

/* ---------------------------------------------------------------- the bell */
/* **On every page behind the gate, in the header's own bar**, left of the wordmark. The
   count is the one red thing in the header: an unread notification is the one state the
   bar has to announce, and `--crit` is the palette's word for "needs you". Hidden at zero.
   The preview and the panel hang from the bell's right edge so they stay on screen however
   wide the title is. */
.bell { position: relative; display: flex; align-items: center; }
.bell-btn {
  position: relative; display: grid; place-items: center; width: 38px; height: 38px;
  border-radius: 999px; border: 1px solid var(--border); background: var(--surface);
  color: var(--ink2); cursor: pointer; padding: 0;
}
.bell-btn:hover, .bell-btn[aria-expanded="true"] { color: var(--ink); border-color: var(--ev); }
.bell-btn:focus-visible { outline: 2px solid var(--ev); outline-offset: 2px; }
.bell-btn svg { width: 19px; height: 19px; }
.bell-count {
  position: absolute; top: -5px; right: -6px; min-width: 18px; height: 18px;
  padding: 0 5px; border-radius: 999px; box-sizing: border-box;
  background: var(--crit); color: #fff; font-size: .68rem; font-weight: 700;
  line-height: 18px; text-align: center; border: 2px solid var(--surface);
}
.bell-preview, .bell-panel {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 1100;
  width: min(360px, calc(100vw - 32px)); box-sizing: border-box;
  background: var(--surface); color: var(--ink); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: 0 12px 32px rgba(0, 0, 0, .35);
}
.bell-preview {
  padding: .75rem .9rem; cursor: pointer; border-left: 3px solid var(--ev);
  animation: bell-in .18s ease-out;
}
@keyframes bell-in { from { opacity: 0; transform: translateY(-6px); } }
.bp-title { font-weight: 650; font-size: .88rem; }
.bp-body { font-size: .8rem; color: var(--ink2); margin-top: .25rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bp-more { font-size: .72rem; color: var(--muted); margin-top: .35rem; }
.bell-panel { max-height: min(70vh, 480px); overflow-y: auto; padding: .35rem 0; }
.bell-head { display: flex; align-items: center; justify-content: space-between;
  padding: .45rem .9rem .55rem; border-bottom: 1px solid var(--border); font-size: .88rem; }
.bell-all { background: none; border: 0; color: var(--ev); cursor: pointer;
  font-size: .78rem; padding: 0; }
.bell-list { list-style: none; margin: 0; padding: 0; }
.bell-item { padding: .6rem .9rem .6rem 1.6rem; position: relative; cursor: pointer;
  border-bottom: 1px solid var(--grid); }
.bell-item:last-child { border-bottom: 0; }
.bell-item:hover, .bell-item:focus-visible {
  background: color-mix(in srgb, var(--ev) 8%, var(--surface)); outline: none; }
.bi-title { font-size: .84rem; color: var(--ink2); }
.bi-meta { font-size: .72rem; color: var(--muted); margin-top: .15rem; }
/* Unread is the word's weight and a dot, never colour alone. */
.bell-item.unread .bi-title { color: var(--ink); font-weight: 650; }
.bell-item.unread::before { content: ""; position: absolute; left: .7rem; top: .95rem;
  width: 7px; height: 7px; border-radius: 50%; background: var(--ev); }
.bell-empty { padding: .8rem .9rem; margin: 0; }

/* The on-screen window a notification opens in. A native `<dialog>`, so focus is trapped
   and Escape closes it without any of that being written here. */
.note-dialog {
  width: min(560px, calc(100vw - 32px)); box-sizing: border-box; padding: 1.2rem 1.3rem;
  border-radius: 14px; border: 1px solid var(--border);
  background: var(--surface); color: var(--ink);
  box-shadow: 0 24px 64px rgba(0, 0, 0, .45);
}
.note-dialog::backdrop { background: rgba(0, 0, 0, .55); }
.note-meta { font-size: .75rem; color: var(--muted); }
.note-title { margin: .35rem 0 .7rem; font-size: 1.1rem; }
.note-body { font-size: .9rem; color: var(--ink2); white-space: pre-line;
  overflow-wrap: break-word; max-height: 55vh; overflow-y: auto; }
.note-acts { display: flex; gap: .5rem; justify-content: flex-end; margin-top: 1.1rem; }
.note-acts .btn { width: auto; padding-inline: 1.1rem; }

/* ---------------------------------------------------------------- resource utilisation */
/* Drawn as the Deliverable Status table is; only its row classes differ, so a count of
   the other hierarchies' `lv-disc` rows is not also a count of this table's. Same grounds. */
.dtbl.restbl th.num { text-align: right; }
.dtbl.restbl tr.lv-rdisc > td, .dtbl.restbl tr.lv-bdisc > td { font-weight: 600; background: color-mix(in srgb, var(--plane) 55%, var(--surface)); }
.dtbl.restbl tr.lv-rcode > td, .dtbl.restbl tr.lv-bcode > td { background: color-mix(in srgb, var(--plane) 25%, var(--surface)); }
.dtbl.restbl tr.lv-res > td { font-weight: 400; }
/* The word carries it; the colour only points at it. */
.dtbl.restbl .neg { color: var(--crit); font-weight: 600; }
/* A person under a grade: one line, their utilised hours, in the caption's ink — they
   are the detail behind the grade's figure, not a fourth level of budget. */
.dtbl.restbl tr.lv-rper > td { font-weight: 400; color: var(--muted); }
/* An adjusted figure that differs from the baseline is the re-cut: bold, on a tint of the
   hue the legend names for it, so the codes that moved stand out down the column. */
.dtbl.restbl td.adj { font-weight: 700; color: var(--ink);
  background: color-mix(in srgb, var(--ramp-d-0) 26%, transparent); }

/* Two-factor enrolment. The QR sits on a white plate whatever the palette: a scanner reads
   the contrast between modules and ground, and a dark card would hand it a dark code. */
.mfa-enrol { display: flex; flex-direction: column; align-items: center; gap: .5rem; margin: 0 0 .8rem; }
.mfa-qr { background: #fff; padding: 8px; border-radius: 8px; line-height: 0; }
.mfa-qr svg { width: 176px; height: 176px; display: block; }
.mfa-qr svg path { fill: #000; }
.mfa-key { display: flex; flex-direction: column; align-items: center; gap: .15rem; }
.mfa-key code { font-size: .9rem; letter-spacing: .04em; color: var(--ink); word-break: break-all; text-align: center; }
.mfa-codes { columns: 2; margin: 0 0 1rem; padding-left: 1.4rem; text-align: left; }
.mfa-codes code { font-size: .9rem; color: var(--ink); }
a.btn.signout { text-decoration: none; margin-bottom: .35rem; }
