/* KitLog design system: display font for headings, system stack for body,
   forest-green primary + warm amber secondary accent, generous whitespace. */

/* Self-hosted (static/fonts) — no external font CDN, so no GDPR IP leak and
   no extra DNS hop. Variable weight 200-800, latin subset only, ~27KB. */
@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-var.woff2") format("woff2");
}

/* Manrope — the AUTHENTICATED app's typeface only (Quiet Linen, approved
   2026-09-07). The public site stays on Plus Jakarta Sans: --font-display is
   re-pointed inside .app-shell, never on :root, so marketing, the 404/500
   pages and the auth screens are untouched. Self-hosted on the same terms as
   the face above — no Google Fonts <link>, no @import, no runtime request, so
   no third-party IP leak. Licence vendored at static/fonts/manrope-OFL.txt. */
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../fonts/manrope-latin-var.woff2") format("woff2");
}

/* Forest & Linen — ONE palette, public pages and application alike (approved
   2026-09-05). It began as the public-site theme in marketing.css; the tokens
   live here now because a customer crossing from the marketing site into the
   register should not watch the chrome change hue. marketing.css keeps only
   the layout and component work that is genuinely marketing-shaped.
   Measured on cream #f4eddf: ink 10.49:1, muted 5.29:1, accent 6.78:1,
   danger 5.09:1, accent2-ink 4.95:1 — all AA for body text. */
:root {
  --forest: #0b332a; --cream: #f4eddf; --gold: #e8c46e;
  --ink: #123c32; --muted: #52665b; --line: #d9cdb9; --bg: var(--cream);
  --wash: #eae3d4; --accent: #145c49; --accent-dark: #0b332a;
  /* Raised surfaces. Cards were white on white and had to fight for their
     edges; on linen they separate by luminance, so the border can stay quiet. */
  --surface: #faf6ed; --surface-line: #e2d8c6;
  /* --gold is a FOREST-ONLY colour: 8.25:1 on #0b332a, but 1.44:1 on cream.
     Never put it on the canvas — it is the dark chrome's accent, nothing else. */
  /* --accent2 is for SHAPES (dots, stripes, bar fills), where 3:1 is the bar.
     Amber WORDS use --accent2-ink: #d97706 text is 2.90:1 on the amber wash
     and 3.19:1 on white, both AA failures; #9a5407 measures 5.24 and 5.76. */
  --accent2: #d97706; --accent2-wash: #fdf3e3; --accent2-ink: #9a5407;
  --warn-bg: #fff7e6; --warn-ink: #7a5200; --danger: #b3392e; --ok: #145c49;
  /* Status-pill fills. They were three hex literals sitting in the pill rules
     where no sweep of the palette would find them. --pill-ready is the fill
     for Available/OK/Active, and it is the LIGHTEST of the three on purpose:
     the register's rows are banded, so a pill has to read on both grounds,
     and a fill lighter than either is the only one that does. It measured
     1.00:1 against the unstriped row before this — the app's commonest status
     rendered as the absence of a pill. */
  --pill-ready: #fdfcf8; --pill-out: #e0ebe2; --pill-off: #e8e0d2;
  /* The picked row in the assign picker's list (#target-picker). */
  --picked: #c9dccd;
  --radius: 8px; --radius-sm: 5px;
  /* Spacing scale — the target for the one-off inline margins in templates. */
  --space-1: .4rem; --space-2: .8rem; --space-3: 1.2rem; --space-4: 1.6rem; --space-5: 2.4rem;
  --font-display: "Plus Jakarta Sans", -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --sidenav-bg: var(--forest); --sidenav-ink: #cbd8cd; --sidenav-active: #164d41;
}
h1, h2, h3, .brand { font-family: var(--font-display); }
* { box-sizing: border-box; }
/* The `hidden` attribute must beat display rules like .inline (same specificity)
   so toggled price/CTA variants actually hide. */
[hidden] { display: none !important; }
html, body { height: 100%; }
body { margin: 0; font-family: var(--font-display);
  color: var(--ink); background: var(--bg); line-height: 1.55;
  display: flex; flex-direction: column; min-height: 100vh; }
main.container { flex: 1 0 auto; }
.footer { flex-shrink: 0; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* width:100% + min-width:0 — as a flex item of body with auto side margins,
   the container would otherwise size to fit-content, letting wide data tables
   force the layout wider than the viewport on phones. */
.container { width: 100%; max-width: 1000px; min-width: 0; margin: 0 auto; padding: 1.5rem 1.25rem 3rem; }

/* Nav */
.nav { display: flex; align-items: center; gap: 1.1rem; padding: .8rem 1.25rem; border-bottom: 1px solid var(--line);
  flex-wrap: wrap; }
.nav a { color: var(--ink); font-size: .95rem; }
.nav a.brand { font-weight: 700; font-size: 1.05rem; display: inline-flex; align-items: center; gap: .45rem; }
.brand-mark { width: 1.4em; height: 1.4em; vertical-align: -.3em; border-radius: 5px; }
.footer .brand-mark { width: 1.15em; height: 1.15em; }
.nav .spacer { flex: 1; }
.inline { display: inline; }
.linklike { background: none; border: none; color: var(--ink); cursor: pointer; font: inherit; padding: 0; }
.linklike:hover { text-decoration: underline; }

/* Focus visibility. One ring for everything keyboard-reachable — links,
   buttons, summaries, whole-card links. :focus-visible, not :focus, so mouse
   clicks stay quiet. Text inputs opt out below via their own :focus rule,
   which swaps the ring for the border + box-shadow treatment. */
:focus-visible { outline: 2px solid var(--accent-dark); outline-offset: 2px; }
/* On the dark rail (sidebar, bulk bar) the green ring vanishes into the
   ground, so anything sitting directly on the forest rail gets a light ring.
   The account panel is NOT in this list: it is a white popover, and a white
   ring on it would be as invisible as the green one on the rail. */
.sidenav .brand:focus-visible,
.sidenav-links a:focus-visible,
.sidenav-foot details.account-menu summary:focus-visible,
.bulk-bar :focus-visible { outline-color: #fff; }

/* ==================== SECTION: the target picker ====================
   Who is this going to? — the searchable person/team list on the assign and
   bundle-assign pages. These rules lived in templates/app/_target_picker_css.html,
   a template holding a <style> block pulled into head_extra, and were
   therefore outside the design system: they still carried #e7f5f0 for the
   selected row, a mint from before the palette, and nobody sweeping
   kitlog.css for stale colours would ever have seen it. Moved here so the
   picker is painted from the same tokens as everything else. */
#target-picker #target-search, #target-picker #target-select { width: 100%; }
#target-picker .picker-list { list-style: none; margin: .45rem 0 0; padding: 0;
  border: 1px solid var(--surface-line); border-radius: var(--radius);
  background: var(--surface); max-height: 16rem; overflow-y: auto; }
#target-picker .picker-row { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: .1rem .55rem; padding: .45rem .7rem; cursor: pointer;
  border-bottom: 1px solid var(--surface-line); border-left: 3px solid transparent; }
#target-picker .picker-row:last-of-type { border-bottom: none; }
#target-picker .picker-row:hover, #target-picker .picker-row:focus { background: var(--wash); }
/* Inset ring (negative offset): the list scrolls, so an outside outline clips. */
#target-picker .picker-row:focus-visible { outline: 2px solid var(--accent-dark); outline-offset: -2px; }
/* --picked, not a literal, and a full step darker than the hover it used to
   be indistinguishable from — see the note in the Quiet Linen section. */
#target-picker .picker-row-selected { background: var(--picked); border-left-color: var(--accent-dark); }
#target-picker .picker-row-disabled { opacity: .55; cursor: not-allowed; }
#target-picker .picker-name { font-size: .95rem; }
#target-picker .picker-kind { font-size: .72rem; }
#target-picker .picker-meta { flex-basis: 100%; color: var(--muted); }
#target-picker .picker-none { padding: .6rem .7rem; color: var(--muted); font-size: .9rem; }
#target-picker .picker-lead-note { display: block; font-size: .82rem; color: var(--muted); margin: .4rem 0 0; }
#target-picker .picker-lead-name { font-weight: 600; }

/* ==================== SECTION: stepped forms ====================
   The signup form's screens, generalised — signup now renders these same
   class names, so there is one set of rules rather than an original and a
   copy. Everything here assumes JavaScript: without it none of these elements
   is ever shown or hidden, and the form is a plain stack. */
.form-steps-progress {
  display: flex; list-style: none; padding: 0; gap: 12px; margin: 0 0 1.4rem;
  max-width: 720px; }
.form-steps-progress li {
  flex: 1; border-top: 3px solid var(--line); padding-top: 10px;
  color: var(--muted); font-size: .82rem; line-height: 1.35; }
.form-steps-progress li[aria-current="step"] {
  border-color: var(--accent); color: var(--accent-dark); font-weight: 700; }
.form-step { border: 0; padding: 0; margin: 0; min-width: 0; }
.form-step legend {
  font-size: 1.35rem; font-weight: 700; padding: 0; color: var(--ink); }
/* A legend is focused programmatically when a screen opens, so the ring must
   not look like a click target. */
.form-step legend:focus-visible { outline-offset: 4px; }
.form-step-hint { margin: .35rem 0 1.2rem; color: var(--muted); font-size: .92rem; }
.form-step .field:last-of-type { margin-bottom: 0; }
/* Back sits away from the pair that moves you forward — and the forward pair
   is pinned to the end, so it does not jump left on the first screen where
   there is no Back to push it. Controls that move between steps make you
   re-find the button you just used. */
.form-step-actions { display: flex; align-items: center; justify-content: flex-end;
  gap: .8rem; margin-top: 1.6rem; }
.form-step-actions [data-step-back] { margin-right: auto; }

/* ==================== SECTION: the public header ====================
   Rendered by templates/_marketing_nav.html on every page a signed-out
   visitor can reach — the marketing site, but also login, password reset and
   the 404 page, which do not load marketing.css. It lives here rather than
   there because it is brand chrome: the header a customer meets on the
   pricing page and the header they meet on the login page have to be the
   same object, and for a week they were not. */
/* flex-shrink: 0 is load-bearing, not tidying. `body` is a column flex
   container, so every child of it shrinks when the page is taller than the
   viewport — and `.footer` was the only one that had said no. At 390px the
   header wrapped to three rows, was crushed back to its min-height, and had
   its lower rows painted over by <main>. Measured rather than reasoned:
   elementFromPoint at the centre of `Log in` returned the page's own <h1>,
   so the only way into the product was untappable. It was both page kinds,
   not only the ones that skip marketing.css — /pricing/ and /accounts/login/
   each put something else on top of every link but the logo. */
.marketing-nav { background: var(--forest); color: var(--cream); border: 0;
  min-height: 76px; padding: 18px max(24px, calc((100% - 1352px) / 2));
  gap: 36px; justify-content: space-between; flex-shrink: 0; }
/* min-height is the WCAG 2.2 AA 2.5.8 floor. Measured at 390px with the rule
   removed, `Log in` renders 23px on login and 22px on the marketing site — so
   this buys a pixel or two, not a redesign, and it is still the difference
   between meeting 2.5.8 and not. The anchor is inline-flex because padding
   would have moved the links apart instead. */
.marketing-nav a { color: var(--cream); font-size: 15px;
  display: inline-flex; align-items: center; min-height: 24px; }
.marketing-nav a.brand { font-size: 22px; font-weight: 750; gap: 10px; color: var(--cream); }
.marketing-nav-links { display: flex; align-items: center; flex-wrap: wrap; gap: 30px; }
/* A row, because the slot holds two actions for an admin and for anybody who
   has not confirmed their address yet: the way back into the register, and
   then the plan or the confirmation beside it rather than in place of it. */
.marketing-nav-account { display: flex; align-items: center; flex-wrap: wrap;
  justify-content: flex-end; gap: 10px 20px; min-width: 140px; }
/* The first one is the primary action in both states — `Log in` signed out,
   `Open KitLog` signed in — so one rule emphasises whichever it is. */
.marketing-nav-account a:first-child { font-weight: 650; }
/* The green focus ring disappears into forest — same rule as the sidebar. */
.marketing-nav :focus-visible, .footer :focus-visible { outline-color: var(--gold); outline-offset: 4px; }
/* The header's wrapping lived in marketing.css, which login, password reset
   and the 404 page do not load — so the header that folded neatly on the
   pricing page folded into three rows on login. It is one object, so its
   layout belongs in the file every page reads; marketing.css keeps only the
   marketing scale (a taller bar, larger type). */
@media (max-width: 1000px) {
  .marketing-nav { gap: 22px; }
  .marketing-nav-links { gap: 18px; }
  .marketing-nav-account { min-width: auto; }
}
@media (max-width: 760px) {
  .marketing-nav { gap: 18px; }
  /* The links take a row of their own, which keeps the brand and the way back
     in together on the first one — the two things a phone reader came for. */
  .marketing-nav-links { order: 3; flex-basis: 100%; gap: 14px 24px; }
}

/* Skip link — first tabbable element on app pages (base.html). Parked above
   the viewport rather than display:none so it stays focusable; landing on it
   drops it into the top-left corner. */
.skip-link { position: fixed; top: .75rem; left: .75rem; z-index: 100;
  padding: .6rem 1.1rem; border-radius: var(--radius);
  background: var(--ink); color: #fff; font-weight: 600;
  box-shadow: 0 6px 20px rgba(11,51,42,.35);
  transform: translateY(calc(-100% - 1.5rem)); }
.skip-link:focus, .skip-link:focus-visible { transform: none; outline-color: #fff; }

/* Company account menu (native <details>; static/js/kitlog.js only adds
   close-on-outside-click). The trigger is the org's own name — uppercased
   here rather than in the template so assistive tech reads the real casing. */
details.account-menu { position: relative; }
details.account-menu summary { cursor: pointer; list-style: none; color: var(--ink);
  display: flex; align-items: center; gap: .5rem;
  font-family: var(--font-display); font-size: .8rem; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase;
  padding: .45rem .5rem; border-radius: calc(var(--radius) - 4px); }
details.account-menu summary::-webkit-details-marker { display: none; }
/* min-width:0 — a flex item won't shrink below min-content without it, so a
   long company name would widen the sidebar instead of ellipsising. */
.account-org { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-caret { flex: none; opacity: .8; transition: transform .15s ease; }
details.account-menu[open] .account-caret { transform: rotate(180deg); }

/* No overflow:hidden — the bubble tail below has to escape the box. The
   header rounds its own top corners instead. */
.account-menu-panel { position: absolute; right: 0; top: 100%; margin-top: .5rem; background: #fff;
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 20px 48px rgba(11,51,42,.34), 0 2px 8px rgba(11,51,42,.14);
  min-width: 15rem; z-index: 20; }
.account-head { padding: .8rem .95rem; border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--wash), #fff);
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0; }
/* overflow-wrap — the greeting falls back to the email address, which has no
   spaces to break at and would otherwise push the panel wide. */
.account-greeting { font-family: var(--font-display); font-weight: 700; font-size: 1rem;
  color: var(--ink); line-height: 1.3; overflow-wrap: anywhere; }
.account-role { display: inline-block; margin-top: .4rem; padding: .1rem .55rem;
  background: #e7f5f0; border: 1px solid #cfe8e0; border-radius: 999px;
  color: var(--accent-dark); font-size: .74rem; font-weight: 700; }
.account-number { margin-top: .45rem; color: var(--muted); font-size: .8rem; }
.account-number strong { color: var(--ink); font-weight: 600; }
/* Rows are inset pills rather than full-bleed bars, so the highlight reads as
   a selected item instead of a stripe across the panel. */
.account-links { display: flex; flex-direction: column; padding: .35rem; }
.account-links:empty { display: none; }
.account-links a, .account-logout button {
  display: flex; align-items: center; gap: .65rem; width: 100%; text-align: left;
  font: inherit; font-size: .94rem; color: var(--ink); cursor: pointer;
  background: none; border: 0; padding: .5rem .6rem;
  border-radius: calc(var(--radius) - 2px); transition: background .12s ease, color .12s ease; }
.account-links a svg, .account-logout button svg { flex: none; opacity: .75; }
.account-links a:hover { background: #e7f5f0; color: var(--accent-dark); text-decoration: none; }
.account-links a:hover svg { opacity: 1; }
.account-logout { border-top: 1px solid var(--line); padding: .35rem; }
.account-logout button { color: var(--danger); }
.account-logout button:hover { background: #fdeceb; }
.account-logout button:hover svg { opacity: 1; }

/* Banners & messages */
.banner { padding: .55rem 1.25rem; font-size: .92rem; }
.banner-warn { background: var(--warn-bg); color: var(--warn-ink); }
.banner-impersonate { background: #ffe9e6; color: var(--danger); font-weight: 600; }
.messages { max-width: 1000px; margin: .8rem auto 0; padding: 0 1.25rem; }
.msg { padding: .6rem .9rem; border-radius: var(--radius); margin-bottom: .5rem; font-size: .93rem; }
.msg-success { background: #e7f5f0; color: var(--accent-dark); }
.msg-error { background: #fdeceb; color: var(--danger); }
.msg-warn { background: var(--warn-bg); color: var(--warn-ink); }
.msg-info { background: var(--wash); color: var(--muted); }

/* Buttons — micro-interactions: gentle lift + shadow on hover, snap back on press */
.btn { display: inline-block; padding: .55rem 1.1rem; border-radius: var(--radius); border: 1px solid var(--line);
  background: #fff; color: var(--ink); font-size: .95rem; cursor: pointer; font-family: inherit;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease, background .15s ease; }
.btn:hover { text-decoration: none; border-color: var(--accent); transform: translateY(-1px);
  box-shadow: 0 4px 14px rgba(20,92,73,.16); }
.btn:active { transform: translateY(0); box-shadow: none; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.btn-primary:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
.btn-danger { color: var(--danger); border-color: #eccfcc; }
.btn-danger:hover { border-color: var(--danger); box-shadow: 0 4px 14px rgba(179,57,46,.14); }
.btn-sm { padding: .3rem .7rem; font-size: .85rem; }

/* Hero + marketing */
.hero { text-align: center; padding: 3.5rem 0 2.5rem;
  background: radial-gradient(1100px 480px at 85% -15%, rgba(20,92,73,.09), transparent 60%),
              radial-gradient(800px 380px at -10% 115%, rgba(20,92,73,.05), transparent 55%); }
.hero h1 { font-size: 2.9rem; font-weight: 800; margin: 0 0 .6rem; letter-spacing: -.03em; }
.hero p.sub { font-size: 1.15rem; line-height: 1.6; color: var(--muted); max-width: 640px; margin: 0 auto 1.6rem; }
.hero .cta-note { font-size: .88rem; color: var(--muted); margin-top: .6rem; }
/* Two-column hero (text + product mockup) used on the homepage */
.hero.hero-split { display: grid; grid-template-columns: 1.05fr .95fr; gap: 2.5rem; align-items: center;
  text-align: left; }
/* min-width:0 — grid items otherwise refuse to shrink below the mockup's
   nowrap text (min-content), overflowing the viewport on phones. */
.hero.hero-split > div { min-width: 0; }
.hero.hero-split p.sub { margin: 0 0 1.6rem; }
@media (max-width: 880px) {
  .hero.hero-split { grid-template-columns: 1fr; text-align: center; }
  .hero.hero-split p.sub { margin: 0 auto 1.6rem; }
  /* width:100% matters: with auto margins alone the item shrink-to-fits and
     floors at the mockup's nowrap min-content, overflowing small screens. */
  .hero-vis { width: 100%; max-width: 460px; margin: 0 auto; }
}
.section { padding: 2.2rem 0; border-top: 1px solid var(--line); }
.section h2 { font-size: 1.5rem; margin-top: 0; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
.card { border: 1px solid var(--line); border-radius: var(--radius); padding: 1.2rem 1.3rem; background: #fff;
  transition: box-shadow .2s ease, transform .2s ease; }
.cards .card:hover { box-shadow: 0 8px 24px rgba(11,51,42,.08); transform: translateY(-2px); }
.card.washed { background: var(--wash); border: none; }
.quote { font-style: italic; color: var(--muted); }
.price-card { border: 1px solid var(--line); border-radius: var(--radius); padding: 1.5rem;
  transition: box-shadow .2s ease, transform .2s ease; }
.price-card:hover { box-shadow: 0 10px 28px rgba(11,51,42,.10); transform: translateY(-2px); }
.price-card.featured { border: 2px solid var(--accent); }
.pricing-plans > .price-card { display: flex; flex-direction: column; border-width: 2px; }
.pricing-plans > .price-card > .btn { margin-top: auto; align-self: flex-start; }
/* The billing page's current-plan card. Same outline the marketing pages give
   a recommendation, but here it is always paired with a "Current plan" pill —
   an outline that means nothing is what made Growth look chosen. */
.price-card.current { border: 2px solid var(--accent); background: #eef3ea; }

/* The hero's HTML/CSS dashboard mockup (.app-mock and its .mock-* parts) was
   removed on 2026-09-08: no template had rendered it for some time, so it was
   ~25 rules drawing the pre-Quiet-Linen app — white cards, old radii — that
   nothing could see and no sweep would flag, because the uniformity test looks
   for classes without rules, not for rules without classes. The hero shows a
   real screenshot of the real application instead (static/marketing/). */
.price { font-size: 2rem; font-weight: 700; }
/* nowrap so the period moves as one unit. The annual headline is a monthly
   equivalent since 2026-10-02 (DECISIONS §84), so "£32.50" is wider than the
   "£390" it replaced and "/mo + VAT" no longer fits beside it at 390px — it
   broke after the "+", orphaning the word VAT on a line of its own. */
.price small { font-size: 1rem; font-weight: 400; color: var(--muted); white-space: nowrap; }
/* Monthly/Annual segmented toggle — two ?interval= links, priced in the view,
   so both billing periods exist with scripting blocked. */
.interval-toggle { display: inline-flex; align-items: center; gap: .15rem; margin: 0 0 1.4rem;
  padding: .22rem; border: 1px solid var(--line); border-radius: 99px; background: #fff; }
.interval-toggle button, .interval-toggle a { border: none; background: none; font: inherit; cursor: pointer;
  padding: .4rem .95rem; border-radius: 99px; color: var(--muted); white-space: nowrap; }
.interval-toggle a { text-decoration: none; display: inline-block; }
.interval-toggle a:hover { color: var(--ink); text-decoration: none; }
.interval-toggle button.active, .interval-toggle a.active { background: var(--accent); color: #fff; }
.interval-toggle .save { font-size: .82rem; opacity: .85; }
.price-sub { margin-top: .3rem; min-height: 1.2em; }
.faq dt { font-weight: 600; margin-top: 1rem; }
.faq dd { margin: .25rem 0 0; color: var(--muted); }
.steps { counter-reset: step; padding-left: 0; }
.steps li { list-style: none; margin-bottom: .9rem; padding-left: 2.4rem; position: relative; }
.steps li::before { counter-increment: step; content: counter(step); position: absolute; left: 0; top: 0;
  width: 1.7rem; height: 1.7rem; border-radius: 50%; background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: .9rem; }

/* Search-intent pages: the real product screen is the visual proof. The
   surrounding layout stays quiet so these pages feel like KitLog, not an SEO
   microsite bolted onto it. */
.intent-hero { padding-bottom: 2rem; }
.product-shot { margin: 0; border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; background: #fff; box-shadow: 0 22px 55px rgba(11,51,42,.14); }
.product-shot img { display: block; width: 100%; height: auto; aspect-ratio: 8 / 5; object-fit: cover;
  object-position: top left; background: var(--wash); }
.product-shot figcaption { padding: .55rem .8rem; border-top: 1px solid var(--line);
  color: var(--muted); font-size: .8rem; }
.proof-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin: 0 0 1rem; padding: 0;
  border: 1px solid var(--line); border-radius: var(--radius); background: #fff; }
.proof-strip li { list-style: none; padding: .9rem 1rem; text-align: center; font-size: .9rem;
  font-weight: 600; border-right: 1px solid var(--line); }
.proof-strip li:last-child { border-right: 0; }
.intent-intro { max-width: 790px; }
.intent-intro p { font-size: 1.08rem; line-height: 1.75; }
.intent-feature-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 2.4rem; }
.intent-feature-grid article { padding: 1.2rem 0; border-top: 1px solid var(--line); }
.intent-feature-grid h3 { margin: 0 0 .35rem; font-size: 1.05rem; }
.intent-feature-grid p { margin: 0; color: var(--muted); }
.intent-workflow .steps { max-width: 820px; }
.intent-workflow .steps li { padding-bottom: .7rem; line-height: 1.55; }
.intent-boundary { margin: 2rem 0; padding: 1.5rem 1.7rem; border-left: 4px solid var(--accent);
  background: var(--wash); border-radius: 0 var(--radius) var(--radius) 0; }
.intent-boundary h2 { margin: 0 0 .45rem; font-size: 1.25rem; }
.intent-boundary p { margin: 0; max-width: 850px; }
.intent-price { display: flex; justify-content: space-between; align-items: center; gap: 2rem; }
.intent-price > div:first-child { max-width: 720px; }
.intent-price .btn { white-space: nowrap; }
.intent-faq dl { max-width: 850px; }
@media (max-width: 720px) {
  .proof-strip { grid-template-columns: 1fr 1fr; }
  .proof-strip li:nth-child(2) { border-right: 0; }
  .proof-strip li:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
  .intent-feature-grid { grid-template-columns: 1fr; }
  .intent-price { display: block; }
  .intent-price > div + div { margin-top: 1rem; }
}

/* The offboarding page's equipment-return checklist (/features/employee-
   offboarding/). It sits on the page rather than behind a download because a
   checklist you can only have in exchange for an email address is a lead form
   wearing a checklist's clothes — and it is printable because its reader is
   standing at a handover with a pen and a cardboard box.

   Printing is the browser's own command. A window.print() button would be an
   enhancement at best and, for anyone with scripting blocked, the only route
   to paper — so there isn't one, and the page says to use Ctrl+P instead. The
   rule below is what makes that produce a checklist rather than a landing
   page: on this page, and only this page, print hides everything but the
   sheet. That is what .page-offboarding on <body> is for. */
.checklist-sheet { border: 1px solid var(--surface-line); border-radius: var(--radius);
  background: var(--surface); padding: 1.6rem 1.8rem; }
.checklist-sheet h2 { margin-top: 0; }
.checklist-source { color: var(--muted); font-size: .85rem; margin: 0 0 1.3rem; }
.checklist-group + .checklist-group { margin-top: 1.5rem; }
.checklist-group h3 { margin: 0 0 .5rem; font-size: 1.05rem; }
.checklist { list-style: none; margin: 0; padding: 0; max-width: 820px; }
/* break-inside so a printed item is never split across two sheets of paper. */
.checklist li { position: relative; padding-left: 1.9rem; margin-bottom: .55rem;
  line-height: 1.5; break-inside: avoid; }
.checklist li::before { content: ""; position: absolute; left: 0; top: .3em;
  width: 1.05rem; height: 1.05rem; border: 1px solid var(--muted);
  border-radius: var(--radius-sm); }
@media print {
  body.page-offboarding .marketing-nav,
  body.page-offboarding main.container > *:not(.checklist-sheet) { display: none !important; }
  body.page-offboarding .checklist-sheet { border: 0; padding: 0; background: none; }
}

/* App */
/* The trail was .84rem muted-on-white, which is under the size at which a
   link reads as a link. The links now carry the ink weight and only the
   separators and the current page stay quiet. */
/* Stays a block, not a flex row: twenty-odd templates write their trail as
   bare "<a>Dashboard</a> / Assets", and flex would wrap each run of text in
   an anonymous item and space it out unevenly. */
.crumbs { font-size: .92rem; color: var(--muted); margin: -.2rem 0 1rem; }
.crumbs a { color: var(--ink); font-weight: 500; }
.crumbs a:hover { color: var(--accent-dark); }
/* A separate, tappable way back to the parent. The trail tells you where you
   are; this is the control you actually hit, at a size a thumb can find. */
.crumb-back { display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; margin-right: .45rem; vertical-align: middle;
  border: 1px solid var(--line); border-radius: 50%; color: var(--ink);
  background: #fff;
  transition: border-color .15s ease, background .15s ease, color .15s ease; }
.crumb-back:hover { border-color: var(--accent); background: #e7f5f0;
  color: var(--accent-dark); text-decoration: none; }
.crumb-trail { vertical-align: middle; }
.page-head { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.2rem; }
.page-head h1 { font-size: 1.5rem; margin: 0; }
/* App heading scale. .section h2 defaults to the marketing 1.5rem — the same
   size as the page h1 above, so app pages (import, settings) had no visible
   hierarchy. Scoped to .app-main because marketing pages without a hero
   (templates.html) share .section and keep the larger size. */
.app-main .section h2 { font-size: 1.2rem; }
.page-head .spacer { flex: 1; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.stat { border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.2rem; }
.stat .num { font-size: 1.9rem; font-weight: 700; }
.stat .lbl { color: var(--muted); font-size: .9rem; }
a.stat-link { display: block; color: inherit; text-decoration: none; transition: border-color .12s, transform .12s; cursor: pointer; }
a.stat-link:hover { border-color: var(--accent); transform: translateY(-1px); text-decoration: none; }
a.stat-link:hover .lbl { color: var(--accent-dark); }
/* Register tables on linen. A 1px rule cannot do this job any more: the warm
   palette's quietest usable line is #d9cdb9, which measures 1.35:1 on cream —
   fine under an airy marketing page, invisible between two dense rows of kit.
   So rows are separated the way the public comparison table already separates
   them, by BANDING rather than hairlines: a raised surface, alternate rows in
   --wash, and the rule kept only as a faint seam. Same mechanism on both
   sides of the product, which is the point of one palette. */
table.list { width: 100%; border-collapse: collapse; font-size: .94rem;
  background: var(--surface); border: 1px solid var(--surface-line);
  border-radius: var(--radius); overflow: hidden; }
table.list th { text-align: left; font-size: .8rem; text-transform: uppercase; letter-spacing: .04em;
  color: var(--cream); background: var(--forest); border-bottom: 0;
  padding: .55rem .6rem; font-weight: 650; }
table.list td { border-bottom: 1px solid var(--surface-line); padding: .55rem .6rem; vertical-align: top; }
table.list tbody tr:nth-child(even) td, table.list tr:nth-child(even) td { background: var(--wash); }
table.list tr:last-child td { border-bottom: 0; }
/* Row actions stay on one line. The register's verbs are two words by rule
   ("Mark returned", never "Return"), and in Plus Jakarta Sans they wrap in a
   narrow column — which doubles the height of every row that has an action
   and makes a five-item list scroll. The column may be wide; the rows may not
   be tall. */
table.list td .btn { white-space: nowrap; }
/* Hover has to beat the band, so it is a tint of the accent rather than
   --wash, which is now the band's own colour and would read as no change. */
table.list tr:hover td, table.list tbody tr:nth-child(even):hover td { background: #dfe9dd; }
/* A table already inside a card or dashboard panel must not draw a second
   card around itself — it borrows the surface it is standing on and keeps
   only the banding. Without this, a #faf6ed table inside a white panel reads
   as a rendering fault rather than a design. */
.card table.list, .dash-panel table.list, .split-cols table.list, details.fold table.list {
  background: transparent; border: 0; border-radius: 0; }
/* Status pills. The pill-<status> names are the legacy tier and keep working;
   markup that means "this is bad/at risk/fine/inert" rather than a literal
   asset status uses the semantic tier (pill-danger/-warn/-ok/-neutral), so
   red is never borrowed for "no leader" and amber never for "3 of 5 out". */
/* Pills carry a border, and it is not decoration. Their fills are all light
   tints, and since the register's rows are banded they now sit on two
   different light grounds — measured, the base pill's --wash fill scored
   exactly 1.00 against the --wash band, i.e. invisible, and the neutral fill
   1.03. A pill has to read as a pill on either row, so the shape is drawn by
   the border and the meaning by the text, with the fill as reinforcement
   rather than the only signal. Each family's border is its own ink at low
   alpha, so it composites correctly over whichever ground it lands on. */
.pill { display: inline-block; padding: .1rem .6rem; border-radius: 99px; font-size: .8rem;
  background: var(--surface); color: var(--muted); border: 1px solid rgba(18,60,50,.28); }
/* Was cornflower blue (#e8f0fb/#1f4e96, 7.07:1) — correct contrast, wrong
   family, and it appears on nearly every row of the register. Re-cut in the
   palette's own green at 8.31:1. */
/* Assigned is filled and Available is an outline chip: the two commonest
   states in the register were two near-identical greens, and separating
   them by weight says the right thing — something is out, versus nothing
   to do. */
.pill-assigned { background: var(--pill-out); color: #0f4a3c; border-color: rgba(15,74,60,.38); }
/* .pill-active is a person's status and is generated as pill-{{ status }};
   it had no rule at all, so every active member of staff wore the base
   pill — the one that measured 1.00 against the band.
   The fill was var(--surface), which inside the app is table.list's OWN
   background: on an unstriped row the app's commonest status measured exactly
   1.00:1 and read as the absence of a pill, while Assigned and Retired both
   carried a visible tint. --pill-ready is lighter than both bands instead of
   equal to one of them — 1.08:1 on the plain row, 1.17:1 on the stripe, which
   is a better floor than any other pill in the set (Assigned bottoms out at
   1.02, Retired at 1.09). The border still draws the shape, per the decision
   recorded in CLAUDE.md; the fill only says "ready" by being the lightest
   thing on the row. */
.pill-available, .pill-ok, .pill-active { background: var(--pill-ready);
  color: var(--accent-dark); border-color: rgba(20,92,73,.40); }
.pill-repair, .pill-warn { background: var(--warn-bg); color: var(--warn-ink);
  border-color: rgba(122,82,0,.34); }
.pill-retired, .pill-left, .pill-neutral { background: var(--pill-off); color: #544c3f;
  border-color: rgba(84,76,63,.34); }
.pill-overdue, .pill-leaving, .pill-danger { background: #fdeceb; color: var(--danger);
  border-color: rgba(179,57,46,.32); }
.pill-current { background: var(--accent); color: #fff; border-color: var(--accent);
  vertical-align: middle; }
/* Inline control rows (search, add-item). Padding comes from the shared input
   styling; `stretch` makes the input, select and button share one height
   instead of sitting at three different sizes on the same line. */
.filters { display: flex; gap: .6rem; margin: 1rem 0; flex-wrap: wrap; align-items: stretch; }
.filters .btn { display: inline-flex; align-items: center; }
/* Search boxes carry the longest placeholders in the app ("Search assets by
   tag, name, category…"), which the default input width cuts off. Grow to
   fill the row; min-width:0 lets it shrink again on a phone rather than
   forcing the row wider than the screen. */
.filters input[type="search"] { flex: 1 1 22rem; min-width: 0; max-width: 34rem; }
.empty { border: 2px dashed var(--line); border-radius: var(--radius); padding: 2.5rem; text-align: center; color: var(--muted); }
.empty h2 { color: var(--ink); }
/* The 404 — the one .empty box whose heading is the page heading. */
.empty h1 { color: var(--ink); font-size: 1.5rem; }

/* Shared list pager (templates/_pager.html) */
.pager { display: flex; gap: .8rem; align-items: center; margin: 1rem 0; flex-wrap: wrap; }
.pager-where { color: var(--muted); font-size: .92rem; }

/* Zero-state welcome (first login, empty register) */
.welcome { text-align: center; padding: 2rem 0 0;
  background: radial-gradient(900px 420px at 50% -20%, rgba(20,92,73,.09), transparent 60%); }
.welcome-eyebrow { display: inline-block; background: #e7f5f0; color: var(--accent-dark);
  font-size: .8rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  padding: .28rem .85rem; border-radius: 99px; margin: 0 0 1.1rem; }
.welcome h1 { font-size: 2.2rem; letter-spacing: -.02em; margin: 0 0 .5rem; }
.welcome-sub { font-size: 1.15rem; color: var(--muted); max-width: 560px; margin: 0 auto 2.2rem; }
.welcome-paths { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem;
  max-width: 720px; margin: 0 auto 2.4rem; text-align: left; }
.welcome-card { display: flex; flex-direction: column; padding: 1.4rem 1.4rem 1.2rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: #fff; color: var(--ink);
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; }
.welcome-card:hover { text-decoration: none; transform: translateY(-2px);
  border-color: var(--accent); box-shadow: 0 8px 24px rgba(20,92,73,.16); }
.welcome-card strong { font-size: 1.08rem; margin-bottom: .3rem; }
.welcome-card p { margin: 0; color: var(--muted); font-size: .92rem; line-height: 1.5; flex: 1; }
.welcome-icon { width: 2.7rem; height: 2.7rem; border-radius: var(--radius); flex: none;
  display: flex; align-items: center; justify-content: center;
  background: #e7f5f0; color: var(--accent); margin-bottom: .8rem; }
.welcome-go { font-weight: 600; color: var(--accent); margin-top: .9rem; font-size: .95rem; }
.welcome-card-primary { background: linear-gradient(160deg, var(--accent), var(--accent-dark));
  border-color: var(--accent-dark); color: #fff; }
.welcome-card-primary p { color: #cfe9e1; }
.welcome-card-primary .welcome-icon { background: rgba(255,255,255,.16); color: #fff; }
.welcome-card-primary .welcome-go { color: #fff; }
.welcome-card-primary:hover { border-color: var(--accent-dark);
  box-shadow: 0 10px 28px rgba(11,51,42,.35); }
.welcome-steps { max-width: 820px; margin: 0 auto; background: var(--wash);
  border: 1px solid var(--line); border-radius: var(--radius); padding: 1.5rem 1.5rem 1.4rem; }
.welcome-steps h2 { margin: 0 0 1.2rem; font-size: 1.05rem; }
.welcome-rail { list-style: none; display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 1rem; margin: 0; padding: 0; text-align: left; }
.welcome-rail li { position: relative; font-size: .92rem; line-height: 1.45; color: var(--muted); }
/* connector line between step circles */
.welcome-rail li:not(:last-child)::after { content: ""; position: absolute; top: .95rem;
  left: 2.5rem; right: -.6rem; height: 2px; background: var(--line); }
.wnum { display: flex; width: 1.9rem; height: 1.9rem; border-radius: 50%;
  background: var(--accent); color: #fff; font-weight: 700; font-size: .9rem;
  align-items: center; justify-content: center; margin-bottom: .5rem; }
/* The zero state's third way in. Same washed panel as the setup rail below
   it, but it is a form: the action is a POST, so what sits inside is a
   button and not a card-shaped link. */
.welcome-sample { max-width: 820px; margin: 0 auto 1.4rem; background: var(--wash);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.3rem 1.5rem; text-align: left; }
.welcome-sample strong { display: block; font-size: 1.05rem; margin-bottom: .35rem; }
.welcome-sample p { margin: 0 0 1rem; color: var(--muted); font-size: .92rem; line-height: 1.55; }
.welcome-note { margin: 1.6rem 0 0; color: var(--muted); font-size: .9rem; }
@media (max-width: 720px) {
  .welcome h1 { font-size: 1.7rem; }
  .welcome-paths { grid-template-columns: 1fr; }
  .welcome-rail { grid-template-columns: 1fr; gap: .9rem; }
  .welcome-rail li:not(:last-child)::after { display: none; }
}

/* Dashboard attention queue */
.attention-head { display: flex; align-items: center; gap: .7rem; margin: 0 0 .7rem; }
.attention-head h2 { margin: 0; font-size: 1.3rem; }
/* The base list, for a context outside .app-shell. Nothing renders it there
   today — the queue is a signed-in component — but a stark #fff and a
   retired-palette pink sat here as the fallback, so the fallback now agrees
   with the palette it would fall back to. The flat banded treatment every
   real render gets is in the Quiet Linen section. */
.attention { list-style: none; padding: 0; margin: 0 0 1.6rem;
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  background: var(--surface); }
.attention li { display: flex; align-items: center; gap: .8rem; padding: .75rem .9rem;
  border-bottom: 1px solid var(--line); border-left: 4px solid transparent; }
.attention li:last-child { border-bottom: none; }
.attention .att-icon { font-size: 1.1rem; line-height: 1; flex: none; width: 1.4rem; text-align: center; }
.attention .att-text { flex: 1; min-width: 0; }
.attention .btn { flex: none; }
.att-urgent { border-left-color: var(--danger); background: #fdeceb; }
.att-warn { border-left-color: var(--warn-ink); }
.att-info { border-left-color: var(--accent); }
.attention-clear { border: 1px solid var(--line); border-radius: var(--radius); background: var(--wash);
  padding: 1rem 1.1rem; margin: 0 0 1.6rem; color: var(--accent-dark); }
@media (max-width: 640px) {
  .attention li { flex-wrap: wrap; }
  .attention .att-text { flex-basis: calc(100% - 2.2rem); }
  .attention .btn { margin-left: 2.2rem; }
}

/* Forms */

/* Every typeable control gets the same look wherever it sits. This used to be
   scoped to form.stack, so inline controls — the asset search box, the
   untagged-item add row, the quantity and note fields in return tables — fell
   back to the browser's square default and looked like a different product.
   Checkboxes and radios keep their native rendering. */
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), select, textarea {
  font: inherit; color: var(--ink); background: #fff;
  padding: .45rem .6rem; border: 1px solid var(--line); border-radius: var(--radius);
  transition: border-color .12s, box-shadow .12s;
}
/* Quiet Linen fields. Pure white was the last stark surface left in the app
   once the panels went warm, and a white field on a linen panel reads as a
   hole in the page. --app-field is a warm near-white that still sits ABOVE
   the panel it is on, and the resting border is --app-control-line, not the
   decorative --app-line: 3.24:1 against the field's own fill, 3.05:1 against
   the panel, so the control has a real boundary rather than a hint of one.
   Deliberately placed BEFORE the :focus, ::placeholder and :disabled rules
   below: this selector ties with them on specificity, so source order is what
   keeps the focus border, the focus halo and the disabled wash intact. */
.app-shell input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
.app-shell select, .app-shell textarea {
  background: var(--app-field); border-color: var(--app-control-line);
  border-radius: var(--app-radius-control); }
/* Order cannot save the red error border: form.stack's has-error rule carries
   one class fewer than the selector above and would lose on specificity, so
   the app states it again. A field that failed validation must not be the one
   field wearing the ordinary boundary. */
.app-shell form.stack .field.has-error input,
.app-shell form.stack .field.has-error select,
.app-shell form.stack .field.has-error textarea { border-color: var(--danger); }
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):focus, select:focus, textarea:focus {
  /* The glow is the border's own colour at low alpha. It was a green from a
     palette before last, so a focused field was ringed in one green and haloed
     in another — which reads as a muddy edge rather than as a wrong colour,
     and is why it survived two palettes. */
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(20,92,73,.12);
}
input::placeholder, textarea::placeholder { color: var(--muted); opacity: .75; }
input:disabled, select:disabled, textarea:disabled { background: var(--wash); color: var(--muted); }
textarea { resize: vertical; }

form.stack .field { margin: 0 0 1.05rem; max-width: 460px; }
form.stack label { display: block; font-weight: 600; font-size: .9rem; margin-bottom: .3rem; }
form.stack .req { color: var(--danger); font-weight: 400; }
/* Stacked forms are full-width with roomier padding; the look is inherited. */
form.stack input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), form.stack select, form.stack textarea {
  width: 100%; padding: .6rem .75rem; }
form.stack textarea { min-height: 4.5rem; }
form.stack .field-help, form.stack .helptext { display: block; font-size: .82rem; color: var(--muted); margin-top: .3rem; }
form.stack .field-err { font-size: .84rem; color: var(--danger); margin-top: .3rem; }
form.stack .field.has-error input, form.stack .field.has-error select, form.stack .field.has-error textarea { border-color: var(--danger); }
form.stack .form-actions { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.3rem; }

/* Either/or picker (assign to a team OR a person). Two stacked dropdowns
   joined by "…or a person" read as two questions; this reads as one choice
   with two answers. */
.pick-target { border: 1px solid var(--line); border-radius: var(--radius); background: var(--wash);
  padding: .4rem 1.25rem 1.25rem; margin: 0 0 1.4rem; max-width: 720px;
  display: grid; grid-template-columns: 1fr auto 1fr; gap: 0 1.25rem; align-items: start; }
.pick-target legend { font-weight: 600; font-size: .95rem; padding: 0 .45rem; }
.pick-option { padding-top: .6rem; }
.pick-option > label { display: block; font-weight: 600; font-size: .9rem; margin-bottom: .35rem; }
.pick-option .field-help { display: block; font-size: .82rem; color: var(--muted); margin-top: .35rem; }
.pick-or { position: relative; align-self: stretch; display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; }
.pick-or::before { content: ""; position: absolute; top: .4rem; bottom: .4rem; width: 1px; background: var(--line); }
.pick-or span { position: relative; background: var(--wash); padding: .35rem 0; }
/* Only one option to offer (e.g. an event with no teams) — no divider, so
   don't hold a column open for it. */
.pick-target-single { grid-template-columns: 1fr; }
/* Django's default error <ul>. NOT an as_p leftover: _form_fields.html emits
   it for non-field errors inside the msg-error box, and the target picker
   renders {{ form.target.errors }} as one. The as_p field-row fallback
   (form.stack > p) is gone on purpose — a form dodging _form_fields.html
   should look broken, not quietly styled. */
.errorlist { color: var(--danger); font-size: .88rem; margin: .2rem 0; padding-left: 1rem; }
/* ==================== SECTION: forms on linen ====================
   The signup form is the approved reference (2026-09-05), and what makes it
   read the way it does is not the palette alone — it is that the fields sit
   on a raised panel instead of floating on the canvas. So every form gets
   that panel: the auth pages here, and the register's own add/edit forms
   below. Both were bare before, on white, where "bare" was invisible.

   The auth card was 420px and unstyled; it is the same object as the signup
   card now, so login, password reset and the trial form agree. */
.auth-box { max-width: 560px; margin: 2rem auto; background: var(--surface);
  border: 1px solid var(--surface-line); border-radius: var(--radius); padding: 32px; }

/* An entity form — a <form class="stack"> standing directly in the page, with
   real fields in it. :has() keeps this out of the filter and search bars,
   which are .stack too but hold no .field, and out of the auth pages, whose
   form is inside .auth-box rather than a child of <main>. */
main.container > form.stack:has(.field) {
  background: var(--surface); border: 1px solid var(--surface-line);
  border-radius: var(--radius); padding: 26px 28px; max-width: 720px; }
main.container > form.stack:has(.field) .form-actions { margin-bottom: 0; }
@media (max-width: 640px) {
  main.container > form.stack:has(.field) { padding: 20px 18px; }
  .auth-box { padding: 22px 18px; }
}

/* Google / Microsoft sign-in buttons (templates/auth/_oidc_buttons.html).
   Full-width so the stack reads as one column of equal choices, and painted
   in the shared button tokens rather than in either provider's brand colour:
   a Google-blue button on a linen card is the one element on the page that
   belongs to somebody else's design system, and the palette guard would have
   to grow an exception for it. The "or" rule is drawn with the existing
   --line token and carries no text of its own beyond the word. */
.oidc-options { margin-top: var(--space-4); display: flex; flex-direction: column; gap: 10px; }
.oidc-or { display: flex; align-items: center; gap: 12px; margin: 0;
  color: var(--muted); font-size: .85rem; }
.oidc-or::before, .oidc-or::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.oidc-btn { width: 100%; text-align: center; }

/* Labels print sheet */
.label-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6mm; }
.label { border: 1px dashed #bbb; border-radius: var(--radius-sm); padding: 4mm; display: flex; gap: 4mm; align-items: center;
  break-inside: avoid; }
.label img { width: 22mm; height: 22mm; }
.label .tag { font-weight: 700; font-size: 12pt; }
.label .name { font-size: 9pt; color: #333; }
@media print {
  .nav, .footer, .messages, .banner, .no-print { display: none !important; }
  .container { max-width: none; padding: 0; }
  .label { border-style: solid; border-color: #ddd; }
  @page { size: A4; margin: 10mm; }
}

/* Footer */
/* One footer. It was linen-on-linen in the app and forest on the public
   pages, which made the same block of links look like two different sites'
   footers. Forest everywhere: it is a brand surface, and it is the only place
   --gold can be used as text (8.25:1 here, 1.44:1 on the canvas). */
.footer { border-top: 0; background: var(--forest); color: #d8e0d4;
  font-size: .9rem; line-height: 1.4; }
.footer a, .footer .muted { color: #d8e0d4; }
.footer a:hover { color: var(--cream); }
.footer strong { color: var(--cream); }
.footer :focus-visible { outline-color: var(--gold); }
.footer-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .9rem 1rem; padding: 1.1rem 1.25rem .5rem; }

/* Own padding, so the container's 3rem bottom padding doesn't apply here. */
.footer-legal { padding: 0 1.25rem 1.1rem; }

/* The signed-in footer: one line, not four columns. A working page already
   has a sidebar full of navigation, so repeating the marketing site map under
   every register would be chrome competing with the task. What stays is what
   a footer is actually for on a paid product — who you are dealing with and
   the legal links. Same forest shell as the public footer (so the two are one
   footer with two contents, which is what test_page_uniformity checks), same
   1320px measure and fluid padding as the content column above it, so the
   line ends where the page does. Wraps to a stack on a phone. */
.footer-app-inner { max-width: 1320px; display: flex; align-items: center;
  justify-content: space-between; gap: .6rem 1.25rem; flex-wrap: wrap;
  padding: .75rem clamp(1.1rem, 3vw, 3rem); font-size: .78rem; }
.footer-app-links { display: inline-flex; gap: 1rem; flex-wrap: wrap; }
/* Inherit, not --muted: the footer is forest, and the canvas muted token
   would be unreadable on it. The company line recedes by opacity instead. */
.footer-app-inner small { color: inherit; opacity: .82; }

@media (max-width: 640px) {
  .hero h1 { font-size: 2.2rem; }
  .label-grid { grid-template-columns: repeat(2, 1fr); }
  /* Wide data tables scroll inside themselves instead of stretching the
     page — otherwise phones zoom the whole layout out to fit the table.
     body overflow-x is clipped so overflowing content can never expand
     the layout viewport; the tables stay swipeable via their own scroll. */
  html, body { overflow-x: clip; }
  table.list { display: block; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  table.list th, table.list td { white-space: nowrap; }
  table.list td small { white-space: normal; }  /* long notes may wrap */
  .page-head { flex-wrap: wrap; gap: .4rem; }
  /* Either/or picker stacks; the divider turns from a vertical rule into a
     horizontal one so "or" still reads as separating the two options. */
  .pick-target { grid-template-columns: 1fr; gap: 0; }
  .pick-or { padding: .9rem 0; }
  .pick-or::before { top: 50%; bottom: auto; left: 0; right: 0; width: auto; height: 1px; }
  .pick-or span { padding: 0 .7rem; }
}

/* Address autocomplete on signup. The list overlays the fields below it, so
   the field keeps a stacking context and the list is absolutely placed. */
#address-lookup-field { position: relative; }
.address-suggestions { position: absolute; z-index: 20; left: 0; right: 0; margin: .2rem 0 0; padding: 0;
  list-style: none; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(11,51,42,.12); max-height: 16rem; overflow-y: auto; }
.address-suggestion { padding: .55rem .75rem; font-size: .9rem; cursor: pointer; }
.address-suggestion + .address-suggestion { border-top: 1px solid var(--line); }
.address-suggestion:hover { background: var(--wash); }

/* Turnstile. The widget is a fixed-size iframe, so in the compact inline
   template-request forms it gets its own row rather than squashing the email
   field. Only ever present when TURNSTILE_SITE_KEY is set. */
.cf-turnstile-field { margin: .6rem 0; }
form.filters .cf-turnstile-field { flex-basis: 100%; margin: .4rem 0 0; }

/* Comparison pages (/vs/...). table.list nowraps cells on mobile, which is
   wrong for prose comparisons — these cells hold sentences, so they wrap and
   the table scrolls sideways inside its own container instead. */
.compare-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; margin-top: 1rem; }
table.compare { width: 100%; border-collapse: collapse; font-size: .94rem; }
table.compare th { text-align: left; font-size: .8rem; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); border-bottom: 2px solid var(--line); padding: .55rem .6rem; }
table.compare td { border-bottom: 1px solid var(--line); padding: .55rem .6rem; vertical-align: top;
  min-width: 11rem; }
table.compare td:first-child { font-weight: 600; min-width: 8rem; }

/* ==================== SECTION: marketing polish ==================== */
/* Eyebrow pill above hero h1s — carries the plain positioning statement so
   the h1 itself can stay a hook instead of explaining the product. */
.hero-eyebrow { display: inline-flex; align-items: center; gap: .4rem; margin-bottom: 1rem;
  padding: .32rem .85rem; border-radius: 99px; background: var(--wash);
  color: var(--accent-dark); font-weight: 600; font-size: .85rem; }
.hero h1 .h1-accent { color: var(--accent); }
/* Marketing type scale. The .hero h1/p.sub sizes live with the base .hero
   rules above; only the h2 bump sits here, gated on a preceding .hero sibling
   because app pages (import, settings) also use .section and must stay on
   the app scale. */
.hero ~ .section h2 { font-size: 1.85rem; font-weight: 700; }
.hero .btn-primary { padding: .75rem 1.4rem; font-size: 1.05rem;
  box-shadow: 0 4px 18px rgba(20,92,73,.25); }
/* Persona lines under the "Sound familiar?" quotes — archetypes, never
   testimonials, so they stay quiet and unitalicised next to the quote. */
.quote .quote-by { font-style: normal; font-size: .82rem; color: var(--muted); margin-top: .6rem; }
/* Step icons sit inline before each title; the numbered circle remains the
   primary marker, so these stay small and decorative. */
.steps .step-icon { display: inline-block; vertical-align: -.15rem; margin-right: .4rem;
  color: var(--accent); }
.steps .step-icon svg { display: block; }
/* The mockup's "1 urgent" pill is the hero's single warm accent — amber here
   only; the real app keeps overdue red (.pill-overdue above). */
.hero-vis .pill-overdue { background: var(--accent2-wash); color: var(--accent2); }

/* ==================== SECTION: app shell & sidebar ==================== */

/* Body is a flex column; the shell is one child that owns the viewport and
   splits sidebar from content. The column must allow shrinking (min-width:0)
   or wide data tables push the grid past the viewport. */
/* Quiet Linen (approved 2026-09-07) rides on this same rule. The neutrals are
   re-pointed HERE and nowhere else: everything the app renders is inside
   .app-shell — content, chrome and footer alike — while marketing pages, the
   auth screens, templates/500.html, the emails, the PDFs and the label sheet
   sit outside it and keep the :root Forest & Linen values unchanged. That is
   the whole reason this is a scoped override rather than a second palette.

   Four surfaces, measured against the two text colours that land on them:
     canvas  #f4f2ec  muted 4.95  ink 10.82  control edge 3.02
     surface #f8f3e9  muted 5.01  ink 10.96  control edge 3.05
     inset   #e7ece4  muted 4.62  ink 10.11  control edge 2.82 (no controls sit
                                             directly on it — see below)
     field   #fcfaf5  muted 5.31  ink 11.62  control edge 3.24
   --app-muted #606b65 is the FLOOR, not a suggestion: lightening it puts help
   text under 4.5:1 on the sage inset first, where nobody looks.
   --app-line #d8d0c2 is DECORATIVE (1.28-1.47:1 — a seam, not a rule) and must
   never be the only visible boundary of a control; every control in the app is
   given --app-control-line #918b82 explicitly further down, and a field always
   has its own --app-field fill on the inside of that border (3.24:1), which is
   what keeps the boundary legible even where the outside is the sage inset.
   --gold is absent on purpose: 1.44:1 on cream, forest chrome only. */
.app-shell { flex: 1 0 auto; display: grid; grid-template-columns: 230px 1fr; min-height: 100vh; max-width: 100vw;
  --app-canvas: #f4f2ec;
  --app-surface: #f8f3e9;
  --app-inset: #e7ece4;
  --app-field: #fcfaf5;
  --app-ink: #183c33;
  --app-muted: #606b65;
  --app-line: #d8d0c2;
  --app-control-line: #918b82;
  /* The same edge, one step darker, for a control sitting on the sage inset.
     #918b82 measures 3.02-3.24:1 on canvas, panel and field but only 2.82 on
     --app-inset, and controls DO rest on the sage — the asset page's
     "Currently with" block has two buttons on it. A boundary that misses 3:1
     on one of the four grounds is not a boundary; this one clears it on all
     four (3.13 sage, 3.35 canvas, 3.39 panel, 3.59 field). */
  --app-control-line-inset: #8a8379;
  --app-hover: #e1e8df;
  /* The secondary counter tile. The plan spelled this as
     color-mix(in srgb, var(--wash) 70%, var(--surface)); nothing else in this
     stylesheet uses color-mix(), and one tile is a thin reason to take on a
     new browser-support dependency, so the same mix is stated as a literal:
     0.7x#e7ece4 + 0.3x#f8f3e9 = #eceee6. Muted measures 4.74:1 on it. */
  --app-inset-soft: #eceee6;
  /* Semantic bands for the flattened attention rows. Warm enough to read as
     bands inside a linen panel rather than as white alert cards laid on it.
     danger #f6e7e4: --danger 4.93, muted 4.61, ink 10.08.
     warn   #f7edda: --accent2-ink 4.96, muted 4.77, ink 10.43. */
  --app-tint-danger: #f6e7e4;
  --app-tint-warn: #f7edda;
  /* The resting edge of a Delete button. --danger itself would make the most
     destructive control the loudest object on a quiet page, and the old
     #eccfcc measured 1.40:1 on the button's own fill — a control with no
     visible boundary at all, which is the thing --app-control-line exists to
     stop. This is --danger lightened 30% toward the field: 3.28:1 on
     --app-field (the fill it always has inside it), 3.09 on --app-surface,
     3.05 on the canvas. */
  --app-danger-line: #c9736a;
  --app-radius-panel: 12px;
  --app-radius-control: 8px;
  --app-radius-row: 7px;
  --font-app: "Manrope", "Segoe UI", system-ui, -apple-system, sans-serif;

  --bg: var(--app-canvas);
  --surface: var(--app-surface);
  --wash: var(--app-inset);
  --ink: var(--app-ink);
  --muted: var(--app-muted);
  --line: var(--app-line);
  --surface-line: var(--app-line);
  --radius: var(--app-radius-panel);
  --radius-sm: var(--app-radius-row);
  --font-display: var(--font-app);

  font-family: var(--font-app);
  background: var(--app-canvas); }
.app-main { display: flex; flex-direction: column; min-width: 0; background: var(--bg); }
/* overflow-x — wide data tables scroll inside the content column instead of
   forcing the whole shell (and the fixed sidebar) past the viewport.
   The working measure is wider than the marketing container's 1000px: a
   register is a dense two-column board, not a column of prose. Fluid side
   padding so 320px keeps its content and 1440px keeps its margins. */
.app-main main.container { flex: 1 0 auto; overflow-x: auto;
  max-width: 1320px; padding: 1.75rem clamp(1.1rem, 3vw, 3rem) 3rem; }

/* Sidebar: brand / links / account footer. Only the middle section may
   scroll — overflow on the aside itself would clip the account panel. */
/* min-width:0 — as a grid item the aside's auto minimum would set the track
   floor to the width of every nav link when it collapses to a row. */
.sidenav { background: var(--sidenav-bg); color: var(--sidenav-ink);
  display: flex; flex-direction: column; padding: 1rem .8rem;
  position: sticky; top: 0; height: 100vh; min-width: 0;
  /* dvh tracks mobile browser chrome; browsers without it keep the 100vh above. */
  height: 100dvh; }
.sidenav .brand { color: #fff; font-weight: 700; font-size: 1.1rem;
  display: inline-flex; align-items: center; gap: .5rem; padding: .2rem .55rem .95rem; }
.sidenav .brand:hover { text-decoration: none; }
.sidenav-links { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: .15rem; }
.sidenav-links a { color: var(--sidenav-ink); font-size: .95rem; padding: .48rem .6rem;
  border-radius: calc(var(--radius) - 4px); }
.sidenav-links a:hover { background: var(--sidenav-active); color: #fff; text-decoration: none; }
.sidenav-links a.active,
.sidenav-links a[aria-current="page"] { background: var(--sidenav-active); color: #fff; font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent); }
.sidenav-foot { flex: none; border-top: 1px solid rgba(255,255,255,.12);
  margin-top: .7rem; padding: .7rem .55rem 0; }
.sidenav-foot details.account-menu summary { color: #fff; }
.sidenav-foot details.account-menu summary:hover,
.sidenav-foot details.account-menu[open] summary { background: var(--sidenav-active); }
/* Opens upward: a drop-down from the sidebar footer would land below the
   viewport. color resets — the panel is white on a dark parent. */
.sidenav-foot .account-menu-panel { top: auto; bottom: 100%; left: 0; right: auto;
  margin: 0 0 .6rem; color: var(--ink); width: 100%; min-width: 14rem; }
/* Speech-bubble tail, pointing back down at the name you clicked. Two stacked
   triangles: the outer one is the border colour, the inner one white and a
   pixel shy of it, which leaves a 1px edge along the diagonals. */
.account-menu-panel::before, .account-menu-panel::after {
  content: ""; position: absolute; width: 0; height: 0;
  border: 9px solid transparent; border-bottom: 0; border-top-color: var(--line);
  top: 100%; left: 50%; margin-left: -9px; }
.account-menu-panel::after { border-width: 8px; border-bottom: 0; border-top-color: #fff;
  top: calc(100% - 1px); margin-left: -8px; }

/* Slim header bar in the content column: spacer + notification bell. */
.app-header { display: flex; align-items: center; gap: 1rem; padding: .45rem 1.25rem;
  border-bottom: 1px solid var(--line); background: var(--bg); }
.app-header .spacer { flex: 1; }
details.notif-menu { position: relative; }
details.notif-menu summary { cursor: pointer; list-style: none; display: inline-flex;
  align-items: center; position: relative; color: var(--muted); padding: .35rem; border-radius: 50%; }
details.notif-menu summary::-webkit-details-marker { display: none; }
details.notif-menu summary:hover, details.notif-menu[open] summary { color: var(--ink); background: var(--wash); }
.notif-bell { display: block; }
/* --accent2-ink, not --accent2: the count is text, and white on #d97706 is
   a 3.19:1 AA failure at this size. White on #9a5407 measures 5.76:1. */
.notif-badge { position: absolute; top: -3px; right: -5px; background: var(--accent2-ink); color: #fff;
  font-size: .68rem; font-weight: 700; line-height: 1; padding: .18rem .32rem;
  border-radius: 999px; min-width: 1.5em; text-align: center; }
.notif-panel { position: absolute; right: 0; top: 100%; margin-top: .5rem; background: #fff;
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(11,51,42,.14); width: min(26rem, 86vw); z-index: 30; }
.notif-list { list-style: none; margin: 0; padding: 0; max-height: 60vh; overflow-y: auto; }
/* Muted by default: read items stay listed but stop shouting; unread carry
   the weight (.notif-unread below). */
.notif-list li { display: flex; gap: .6rem; align-items: flex-start; color: var(--muted);
  padding: .6rem .9rem; border-bottom: 1px solid var(--line); font-size: .92rem; }
.notif-list a { color: var(--ink); }
.notif-empty { padding: .9rem; color: var(--muted); font-size: .92rem; }
.notif-all { display: block; padding: .6rem .9rem; font-size: .9rem; border-top: 1px solid var(--line); }
.notif-list li.notif-unread { color: inherit; font-weight: 600; }
.notif-list li.notif-unread a { color: var(--ink); }
.notif-actions { display: flex; gap: .9rem; align-items: center; justify-content: space-between;
  padding: .6rem .9rem; font-size: .9rem; border-top: 1px solid var(--line); }
.notif-dot { display: inline-block; width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--accent2); margin-right: .45rem; vertical-align: middle; }
.attention li.notif-unread .att-text { font-weight: 600; }

/* Marketing footer inside the app column: kept for consistency, quieter. */
.app-main .footer { font-size: .9rem; }

/* Help icon: same size and hover as the bell so the two read as a pair. */
.header-icon { display: inline-flex; align-items: center; color: var(--muted);
  padding: .35rem; border-radius: 50%; }
.header-icon:hover { color: var(--ink); background: var(--wash); text-decoration: none; }

/* ---- Phone navigation (base.html: details.nav-menu) ---------------------
   What this replaced: below 900px the sidebar used to flatten into a
   horizontally scrolling strip. At 375px that strip showed about 199px of
   links — three destinations of eleven — and the other eight were reachable
   only by a sideways drag with nothing on screen saying so.

   Now the sidebar is hidden outright below the breakpoint and this bar takes
   over: a native <details> whose summary is the whole compact top bar, and
   whose panel is IN THE PAGE FLOW rather than floating. In-flow is the load-
   bearing choice — an overlay can be clipped by an ancestor, and with
   scripting blocked it can be opened at the same time as the notification
   panel and land on top of it. Opening this pushes the header and the page
   down instead, so the two menus can never overlap.

   Exactly one of .sidenav and details.nav-menu is displayed at any width, so
   the two renderings of _nav_links.html never both reach the accessibility
   tree. */
details.nav-menu { display: none; background: var(--sidenav-bg); color: var(--sidenav-ink); }
/* Keyed on the class as well as the element: .nav-bar-brand and .nav-bar-btn
   are styled by name, and a .nav-bar that matched no rule read as dead markup
   in a sweep of every page's classes. */
details.nav-menu > summary, .nav-bar { cursor: pointer; list-style: none; color: #fff;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: .55rem 1rem; }
details.nav-menu > summary::-webkit-details-marker { display: none; }
/* Negative offset: the bar is full-bleed, so a ring drawn outside it would
   sit past the viewport edge. White, because the green one disappears into
   the dark rail (same reason as the sidebar rules near the top of the file). */
details.nav-menu > summary:focus-visible { outline-color: #fff; outline-offset: -3px; }
.nav-bar-brand { display: inline-flex; align-items: center; gap: .5rem;
  font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; }
/* 44px minimum target, and a border so it reads as a button rather than a
   word — this is the only way to every other page on a phone. */
.nav-bar-btn { display: inline-flex; align-items: center; gap: .45rem;
  min-height: 44px; padding: 0 .8rem; font-size: .95rem; font-weight: 600;
  border: 1px solid rgba(255,255,255,.35); border-radius: var(--radius); }
details.nav-menu > summary:hover .nav-bar-btn,
details.nav-menu[open] .nav-bar-btn { background: var(--sidenav-active); }
/* The opened panel is a white sheet under the dark bar — the same treatment
   as the account and notification popovers, so the account rows it carries
   need no restyling at all. */
.nav-panel { background: #fff; color: var(--ink); border-bottom: 1px solid var(--line); }
/* The sidebar's .48rem rows are a mouse size; these are thumb-sized. */
.nav-panel .sidenav-links { display: flex; flex-direction: column; gap: 0;
  padding: .35rem; overflow: visible; }
.nav-panel .sidenav-links a { display: flex; align-items: center; min-height: 44px;
  padding: .5rem .75rem; font-size: 1rem; color: var(--ink); }
.nav-panel .sidenav-links a:hover { background: var(--wash); color: var(--ink); }
/* Keeps the inset accent bar from the rule above; only the colours change,
   because a dark-rail highlight on a white sheet would be a black slab. */
.nav-panel .sidenav-links a.active,
.nav-panel .sidenav-links a[aria-current="page"] { background: #e7f5f0; color: var(--accent-dark); }
.nav-account { border-top: 1px solid var(--line); }
.nav-account .account-head { border-radius: 0; }
.nav-account .account-links a, .nav-account .account-logout button { min-height: 44px; }

@media (max-width: 900px) {
  .app-shell { grid-template-columns: 1fr; }
  .sidenav { display: none; }
  details.nav-menu { display: block; }
}

/* The page heading's actions wrapped ragged on a phone: .page-head's .spacer
   pushes them right, so the first action finished hard against the right edge
   and the second started at the left edge of the next line — two controls in
   the same group reading as two unrelated things. Below 640px the title takes
   its own row and the spacer stops pushing, so every action lines up on the
   left under it, in order, however many there are. */
@media (max-width: 640px) {
  .app-main .page-head { row-gap: .6rem; }
  .app-main .page-head h1 { flex: 1 0 100%; }
  .app-main .page-head .spacer { display: none; }
}

/* At 320px the header's search field measured 57px — nothing clipped and no
   overflow, so every hard criterion passed and the control was still useless:
   you cannot read a query in 57px. It takes a row of its own under the page
   name and the icons instead, where it gets the full width (202px at 320,
   against 57 before). 440px is the line because that is where a phone in
   portrait stops: 430 gave the field 140px and 390 gave it 107, while the
   first width above the breakpoint, 520, gives it 214 and the page name room
   to stop ellipsising. `order` rather than markup, because the header is
   shared by all 56 pages and the search must stay after the page name in the
   tab order at every other width. */
@media (max-width: 440px) {
  .app-header { flex-wrap: wrap; row-gap: .4rem; }
  .app-header .global-search { order: 1; flex: 1 0 100%; }
}

/* The print rule above predates the shell — app chrome must not print either. */
@media print { .sidenav, .app-header, details.nav-menu { display: none !important; } }

/* ==================== SECTION: Quiet Linen surfaces ====================
   Everything here is .app-shell-scoped, and the scope is the point: the same
   class names paint the public pages, the auth screens and the 404, and those
   keep the :root palette. The problem being solved is that the signed-in app
   mixed three surface languages — #fff cards, --surface forms, --wash selected
   regions — so a dashboard panel was a white box holding more white boxes, on
   a canvas warmer than either. Four layers now, and only four: canvas, panel,
   inset, field.

   Two habits this section deliberately breaks. Static content panels do not
   lift on hover — movement means "this does something", and every card in the
   app was claiming it. And nothing here carries a drop shadow: elevation is a
   behaviour signal, so it stays with the popovers and menus that actually
   float. */
.app-shell .card,
.app-shell details.fold { background: var(--surface); border-color: var(--surface-line);
  border-radius: var(--radius); box-shadow: none; }
/* The sage inset: the asset page's current-assignment block and every other
   "this is the secondary thing" surface. It keeps a visible edge — it is now
   only 1.08:1 against the panel it sits on, which is the right amount of
   separation for a quiet region and not enough on its own to say where it
   ends. */
.app-shell .card.washed { background: var(--wash); border: 1px solid var(--surface-line); }
.app-shell .cards .card:hover { box-shadow: none; transform: none; }
/* The attention queue, wherever it renders. It was flattened under `.dash`
   only, so /app/notifications/ — which the bell's "See all" opens, showing
   the same items — kept the pre-refresh bordered panel with 1px-ruled square
   rows. One component, one treatment: bands inside no panel at all, each with
   its semantic left edge. The dashboard keeps only what is genuinely
   dashboard-shaped (its margins), further down the file.

   The semantic rules name `li` on purpose. `.attention li` sets the border-left
   SHORTHAND and a background at (0,1,1); `.att-urgent` was (0,1,0) and lost
   both, so every row on both pages resolved to the same grey band with a
   --line edge and the measured danger/warn tints painted nowhere. Raising the
   semantic selectors above the base row (rather than reaching for !important)
   is what makes them win, and it must stay that way: a longhand
   `border-left-color` under a shorthand `border-left` is a silent loss. */
.app-shell .attention { border: none; background: transparent; overflow: visible;
  display: grid; gap: .4rem; }
.app-shell .attention li { border: 0; border-left: 4px solid var(--surface-line);
  border-radius: var(--app-radius-row); padding: .6rem .8rem; background: var(--wash); }
.app-shell .attention li.att-urgent { border-left-color: var(--danger);
  background: var(--app-tint-danger); }
.app-shell .attention li.att-warn { border-left-color: var(--accent2);
  background: var(--app-tint-warn); }
.app-shell .attention li.att-info { border-left-color: var(--accent);
  background: var(--wash); }
/* The row action sits on a tinted band, so it takes the panel's own surface
   to stay legible as a control on all three. */
.app-shell .attention .btn { background: var(--surface); }
.app-shell .attention-clear { border-style: dashed; border-color: var(--app-control-line);
  background: var(--wash); }
/* The zero-state paths ARE links, so they keep their lift; only the white
   goes. Same for anything else that is a card-shaped <a>. */
.app-shell .welcome-card { background: var(--surface); border-color: var(--surface-line); }
/* ...but NOT the primary one. That card is the accent-filled call to action on
   an empty register — "upload the spreadsheet you already have" — and it
   carries white text. The rule above is one class more specific than
   .welcome-card-primary's own gradient, so it repainted the card warm and left
   the text white: 1.11:1, on the first screen a new customer ever sees. Same
   shape as DECISIONS §67, found by signing up rather than by reading the
   cascade. It is restated here, after the override, rather than by weakening
   the override — the other zero-state cards do want the panel. */
.app-shell .welcome-card-primary { background: linear-gradient(160deg, var(--accent), var(--accent-dark));
  border-color: var(--accent-dark); color: #fff; }
.app-shell .welcome-card-primary .welcome-icon { background: rgba(255,255,255,.16); color: #fff; }

/* Controls get the boundary, panels get the seam. --line is a 1.28-1.47:1
   decoration in this scope, which is fine between two rows of a table and not
   fine as the only edge a button has, so every control names
   --app-control-line itself. The variants are excluded rather than restated:
   .btn-primary is a filled accent and .btn-danger carries its own warning
   edge, and both would otherwise be flattened by the extra class here. The
   hover border has to be said again for the same reason. */
.app-shell .btn { border-radius: var(--app-radius-control); }
/* Secondary buttons take the field fill rather than white, so a row of
   controls and a row of inputs belong to the same object. .btn-primary keeps
   its accent fill; .btn-danger has no fill of its own and inherits this one. */
.app-shell .btn:not(.btn-primary) { background: var(--app-field); }
.app-shell .btn:not(.btn-primary):not(.btn-danger) { border-color: var(--app-control-line); }
.app-shell .btn:not(.btn-primary):not(.btn-danger):hover { border-color: var(--accent); }
.app-shell .global-search button { background: var(--app-field);
  border-color: var(--app-control-line); border-radius: var(--app-radius-control); }
.app-shell .global-search button:hover { border-color: var(--accent); color: var(--accent-dark); }
/* The help index's search pill draws the field itself — the <input> inside it
   is a transparent slot with no border — so the pill takes the field fill and
   the control edge, and the input is told again to stay transparent. Without
   the second rule the app's field fill paints a rectangle inside the pill.
   The :not() chain is not decoration: it is what makes this selector outrank
   the app-wide field rule it is correcting. */
.app-shell .kb-search { background: var(--app-field); border-color: var(--app-control-line);
  box-shadow: none; }
.app-shell .kb-search input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]) {
  background: none; }
.app-shell .crumb-back { background: var(--app-field); border-color: var(--app-control-line); }
.app-shell .crumb-back:hover { border-color: var(--accent); background: var(--app-hover);
  color: var(--accent-dark); }
/* 8px on a 44px thumb target: 12px panel corners on a control that size read
   as a pill it is not. */
.app-shell .nav-bar-btn { border-radius: var(--app-radius-control); }
/* Row hover stays a tint of the accent rather than --wash, which is the
   banded row's own colour — the reason is unchanged, only the tint moves into
   the sage family with the rest of the app. Same measured value as the
   #dfe9dd it replaces: muted text reads 4.44:1 on either, which is why this
   is a hover state and not a resting one. */
.app-shell table.list tr:hover td,
.app-shell table.list tbody tr:nth-child(even):hover td { background: var(--app-hover); }
/* ROW-scope headers are not list headings. `table.list th` is the forest bar
   at the top of a register — asked for by name, and it stays exactly as it
   is. But /app/account/ writes its two summary tables as label/value rows,
   `<tr><th>Your login</th><td>…</td></tr>`, and the same rule painted a
   224px forest slab down the side of the page: a column of dark chrome
   standing beside sage banded values, on the one page that is a definition
   list rather than a register.
   `tr:has(td)` is what separates them: a heading row holds only `th`, a
   label/value row always holds both, so this can never reach a `thead`. The
   cell becomes what it actually is — a quiet label — and the value beside it
   keeps the row's own band. Muted on the two grounds it lands on measures
   5.01:1 (--surface) and 4.62:1 (--wash); the seam on the right gives the
   column an edge without drawing a second panel. */
.app-shell table.list tr:has(td) > th { background: none; color: var(--muted);
  font-size: .88rem; font-weight: 650; text-transform: none; letter-spacing: normal;
  border-bottom: 1px solid var(--surface-line);
  border-right: 1px solid var(--surface-line); }
.app-shell table.list tr:has(td):last-child > th { border-bottom: 0; }

/* ---- The rest of the sweep (2026-09-08) --------------------------------
   The first pass moved the surfaces a dashboard screenshot shows. What it
   left behind was every OTHER page's white: the popovers, the phone menu
   sheet, the help index cards, the billing interval control — pure #fff on a
   linen canvas reads as a hole punched in the page, and it was still the
   app's commonest surface.

   One rule decides each case. A RESTING surface joins the four layers. A true
   OVERLAY may stay lighter and keep its shadow, because elevation is a
   behaviour signal and the spec says so — but "lighter" now means --app-field,
   the top layer of this system, not a colour from outside it.

   Everything here is .app-shell-scoped for the same reason the block above
   is: these class names also paint the marketing site, the auth screens, the
   404 and the ops console, and those keep the :root Forest & Linen palette. */

/* Overlays: the sidebar account drop-up and the notification bell. Fill only
   — the shadow and the 1px seam are what say "this floats", and --app-field
   still sits above the canvas (1.07) and above the panels (1.06), so the
   panel reads as raised while belonging to the app. The bubble tail's inner
   triangle has to move with the fill or the drop-up grows a white notch. */
.app-shell .account-menu-panel,
.app-shell .notif-panel { background: var(--app-field); }
.app-shell .account-menu-panel::after { border-top-color: var(--app-field); }
.app-shell .account-head { background: linear-gradient(180deg, var(--app-inset), var(--app-field)); }
/* The role chip in that panel was #e7f5f0 with a #cfe8e0 edge — a cool mint
   from before the palette, on what is now a warm sheet. Sage fill, and the
   base pill's own border ink so it is drawn by its shape like every other
   chip in the app. */
.app-shell .account-role { background: var(--wash); border-color: rgba(18,60,50,.28); }
/* Menu rows: the sage hover the rest of the app uses, and the 7px row radius
   instead of calc(--radius - 2px), which became 10px when --radius went to 12
   and belongs to no step of the 12/8/7 scale. */
.app-shell .account-links a:hover { background: var(--app-hover); }
.app-shell .account-links a,
.app-shell .account-logout button { border-radius: var(--app-radius-row); }

/* The phone navigation sheet is IN THE FLOW, not an overlay — it pushes the
   page down rather than floating over it — so it is a resting surface and
   takes the panel layer. The current-page row keeps its inset accent bar
   (that is the signal); only the mint tint moves into the sage family. */
.app-shell .nav-panel { background: var(--surface); }
.app-shell .nav-panel .sidenav-links a.active,
.app-shell .nav-panel .sidenav-links a[aria-current="page"] { background: var(--app-hover); }

/* Buttons standing on the sage inset — the asset page's "Currently with"
   block, and anything else that becomes a .card.washed. The field fill is the
   brightest thing in the system, so on the inset it out-shouts the primary
   action beside it and inverts the hierarchy: 1.15:1 against the sage, where
   the panel surface is 1.08:1. Same move .dash .attention .btn already makes
   on the tinted alert bands, for the same reason. */
.app-shell .card.washed .btn:not(.btn-primary) { background: var(--surface); }
/* ...and the darker edge, because the outside of that border is the sage. */
.app-shell .card.washed .btn:not(.btn-primary):not(.btn-danger),
.app-shell .dash .attention .btn:not(.btn-primary):not(.btn-danger) {
  border-color: var(--app-control-line-inset); }
/* The button hover shadow used to be corrected here: the base rule carried
   the retired brand green and this restated it in the current accent, so the
   app was right and the public site was wrong. The base rule is the current
   accent now (see .btn:hover), which makes an override to the same value a
   rule that can only ever drift, so it is gone rather than kept. */
/* Delete is the one control the sweep above deliberately skipped, and it was
   left with a 1.40:1 edge. See --app-danger-line. */
.app-shell .btn-danger { border-color: var(--app-danger-line); }

/* The zero state is an invitation, and it was drawn in a 1.37:1 dashed line
   nobody can see: --line is a seam between two table rows, not a boundary.
   The panel fill gives the box a body and the control edge (3.05:1 on that
   fill, 3.02 on the canvas) gives it an outline that reads as "something
   goes here". .empty-page — the 404 — is outside the shell and untouched. */
.app-shell .empty { background: var(--surface); border-color: var(--app-control-line); }

/* The stepper's rail: three 3px bars saying which screen you are on, in a
   colour that measured 1.37:1. The completed/pending bars take the control
   edge; :not() leaves the accent bar on the current step alone, because the
   two selectors otherwise tie and source order would quietly win. */
.app-shell .form-steps-progress li:not([aria-current="step"]) { border-top-color: var(--app-control-line); }

/* Billing's Monthly/Annual control is a real control (two links in a group),
   and it was a white pill with a decorative edge. */
.app-shell .interval-toggle { background: var(--app-field); border-color: var(--app-control-line); }

/* /help/ renders inside the shell for anyone signed in, and its cards were
   the largest block of white left in the product. The cards are links, so
   they keep their lift; only the fill moves. (The hero's hover shadow and the
   search pill's focus glow were also restated here in the current accent,
   because the base rules held the retired green — those base rules carry the
   accent themselves now, so the two overrides are gone. The pill's *resting*
   shadow is still dropped, up at .app-shell .kb-search: elevation belongs to
   things that float, and a text field does not.) */
.app-shell .kb-card { background: var(--surface); border-color: var(--surface-line); }
/* "START HERE" was the last uppercase wide-tracked eyebrow left in the app.
   Dashboard panel titles were moved off exactly this treatment because a
   tracked-out capital label shouts at the same volume as the content under
   it — and /help/ renders inside the shell for every signed-in customer, so
   leaving it here just moved the shouting to a different page. The section's
   own title in core/kb.py is already sentence case ("Start here"); only the
   CSS was capitalising it. Same weight and colour, one step larger so it
   still reads as a label rather than as small print. */
.app-shell .kb-eyebrow { text-transform: none; letter-spacing: normal; font-size: .82rem; }
/* The hero's 16px corner matched no step of the 12/8/7/pill scale. It was
   commented as a deliberate hero treatment, but the hero is a panel like any
   other panel here — the gradient and the 116px icon are what make it a hero
   — so it joins the panel step. The public /help/ is left on 16px with the
   rest of the marketing typography this pass does not touch. */
.app-shell .kb-hero { border-radius: var(--app-radius-panel); }

/* First-login zero state: the page wash and the card lift were the retired
   green too, and were restated here in the accent for the signed-in half of
   the product only. Both base rules are the accent now, so those two
   overrides are gone; the cards themselves went warm in the pass above and
   that override stays, because it is a real difference. */

/* ---- The page-by-page audit of all 56 signed-in pages (2026-09-08) ------
   What the sweep above moved was every surface a *component* owns. What this
   moves is the handful of places where a component is right and the PAGE it
   lands on is not: a plan card that never got a fill, a disclosure whose only
   boundary is a seam, an eyebrow left shouting after the panel titles stopped.
   Same rules as everything else here — four layers, control edges on
   controls, seams on panels, and .app-shell scope throughout. */

/* Billing's plan cards were the last transparent panels in the app: outlined
   rectangles standing on the canvas, directly under a real --surface panel on
   the same short page, so the page showed two different ideas of what a
   module is. They are static content panels — the button inside each one is
   the control — so they take the panel layer and, like .cards .card above,
   stop lifting. (.cards .card got that neutraliser in the first pass and
   .price-card did not, which left the movement switched off only for someone
   who had asked for reduced motion.)
   `.current` was a #eef3ea mint from the retired palette, and it is the card
   the paying customer sees every time: it becomes the sage inset — the
   system's own "this is the selected region" layer — inside the accent
   outline it already had. Ink measures 10.11:1 on it, muted 4.62:1. */
.app-shell .price-card { background: var(--surface); border-color: var(--surface-line); }
.app-shell .price-card:hover { box-shadow: none; transform: none; }
.app-shell .price-card.featured, .app-shell .price-card.current { border-color: var(--accent); }
.app-shell .price-card.current { background: var(--wash); }

/* A fold-away editing section ("Add members", "Assign kit to this team") is a
   ~50px band whose entire surface is the click target, and its only boundary
   was --app-line at 1.28:1 — precisely what the spec forbids a control to
   rely on. Closed, it IS a control and takes the control edge (3.05:1 on the
   panel fill it carries). Opened, it becomes a panel holding a form, so the
   outline drops back to a seam and the boundary moves to the summary's own
   bottom edge, which is still the thing you click to close it. */
.app-shell details.fold { border-color: var(--app-control-line); }
.app-shell details.fold[open] { border-color: var(--surface-line); }
.app-shell details.fold[open] > summary { border-bottom-color: var(--app-control-line); }

/* The labels page's selection tray. Its colours used to live in a page-scoped
   <style> block, which CLAUDE.md allows for layout and not for colour — the
   layout stays there, the palette is here. The tray is a module container
   holding three controls, so it is a panel: panel fill, seam, panel radius,
   and the controls inside it draw their own edges. A chip carries a remove
   button, so the chip gets a real boundary rather than the seam it had. */
.app-shell .label-tray { background: var(--surface); border: 1px solid var(--surface-line);
  border-radius: var(--app-radius-panel); }
.app-shell .label-chip { background: var(--app-field);
  border: 1px solid var(--app-control-line); }

/* The app had two zero-state languages. List and search pages get `.empty` —
   a panel with a body and a 3:1 dashed edge. Every DETAIL page instead got a
   bare italic sentence floating on the canvas: team detail has four of them,
   person detail three, saying the same kind of thing with nothing around it.
   `.quote` is not only a zero state in here, though — it also carries a
   person's notes and a line of instruction above a form — so this is not the
   dashed `.empty` box, which would announce "nothing here" over a paragraph
   of prose. It is the sage inset band: a quiet aside that reads correctly
   either way, with the fill (1.07:1 on the canvas, 1.08:1 on a panel) giving
   it the body it was missing. Italic goes with it; a band does not need to
   lean as well. Scoped to <p> so the inline `<small class="quote">` notes in
   the import screens stay inline, and to .app-shell so the marketing pages'
   pull-quotes are untouched. */
.app-shell p.quote { background: var(--wash); border: 1px solid var(--surface-line);
  border-radius: var(--app-radius-row); padding: .7rem .9rem; margin: .8rem 0 1.1rem;
  font-style: normal; }
/* Except in the knowledge base, where the same class is a standfirst: the
   sentence under an article's title and the lede on the /help/ index. Those
   are prose being read, not a region of a working screen, and a band around
   them would box the first line of every help page. Restored to exactly what
   they rendered before, italic included. */
.app-shell .kb-index p.quote, .app-shell .kb-article p.quote {
  background: none; border: 0; border-radius: 0; padding: 0; margin: 1em 0;
  font-style: italic; }

/* Settings and Users stack a full-measure table on top of a form panel, and
   the panel's 720px cap made two modules on one page visibly different
   widths. The cap is right for a page that IS a form — a lone 1320px panel
   around two fields is worse — so it lifts only where the container also
   holds a list of its own. The fields keep their own 460px measure either
   way, which is what the spec actually asks for. */
.app-main main.container:has(> table.list) > form.stack:has(.field) { max-width: none; }

/* The assign picker's selected row was #dfe9dd and its hover --wash #e7ece4:
   1.04:1 apart, so the row you had chosen and the row you were pointing at
   were the same row. The selected fill is now a real sage band — 1.20:1
   against the hover, 1.30:1 against the list surface — and because muted text
   on it measures 3.85:1, the row's meta line moves up to ink (8.43:1) rather
   than being left under the floor. The 3px accent edge it already had stays;
   the fill is reinforcement, not the only signal. */
.app-shell #target-picker .picker-row-selected .picker-meta { color: var(--ink); }
.app-shell #target-picker .picker-row-selected .picker-name { font-weight: 700; }

/* Off-system radii. `.crumb-back` was the only circular control in the app —
   a 50% pill on a 2rem square, beside buttons at 8px — so it joins the
   control step. Left alone deliberately: `img.brand-mark` at 5px, which is
   artwork rather than a surface; `.account-head` at calc(--radius - 1px),
   which is the correct inner radius for a 12px panel with a 1px border. */
.app-shell .crumb-back { border-radius: var(--app-radius-control); }

/* The org name in the sidebar was rendered UPPERCASE with .04em tracking on
   all 56 pages — the last tracked-out capitals in the app after the dashboard
   panel titles gave theirs up, and spec item 9 asks for sentence case. It is
   forest chrome, where a little shouting is more defensible than on the
   canvas, but this is a company's actual name: uppercasing it makes "McQueen
   AV" read as an acronym and tells assistive tech nothing it did not already
   have. Sentence case, and a step up in size so it still reads as the label
   on a menu rather than as body text that happens to be bold. */
.app-shell details.account-menu summary { text-transform: none; letter-spacing: normal;
  font-size: .92rem; }

/* Three #e7f5f0 tints the sweep missed, found by walking every element of
   every rendered page and asking what its computed background actually is
   rather than by reading the stylesheet. It is the cool mint from before
   2026-09-05, and inside a warm app it reads as a different product's
   highlight: the /help/ card icon tiles (the most-repeated one — every
   article card carries it), the success flash message, and the first-login
   zero state's badge and icon. All four become the sage inset, which is what
   every other "quiet highlighted thing" in the app already is. The icons keep
   --accent / --accent-dark on it: 6.59:1 and 11.51:1. */
.app-shell .kb-card-icon,
.app-shell .msg-success,
.app-shell .welcome-eyebrow,
.app-shell .welcome-icon { background: var(--wash); }
/* Sentence case, like every other heading in the app. This was the last
   tracked-out capital left on a customer-facing app screen after /help/'s
   eyebrow went. */
.app-shell .welcome-eyebrow { text-transform: none; letter-spacing: 0; }

/* Page titles: sentence case, tight, and one size larger than the marketing
   1.5rem so an app page has a visible top of its hierarchy. Actions stay in
   .page-head beside the title and keep wrapping below 640px. */
.app-main .page-head h1 { font-size: 1.7rem; font-weight: 750; letter-spacing: -.02em; }

/* Section rhythm on the detail pages. Asset, person, team and bundle detail
   are a stack of unlabelled regions — an identity card, a table, a note form,
   a history — and they were spaced by whatever margin each element happened
   to own, which is what produced the large accidental gaps. One 24-28px beat
   for the lot, applied to direct children of the content column so it cannot
   reach inside a panel and re-space a form. */
.app-main main.container > .cards { margin-bottom: 1.75rem; }
.app-main main.container > h2,
.app-main main.container > .split-cols { margin-top: 1.75rem; }
.app-main main.container > h2 { margin-bottom: .75rem; }
.app-main main.container > table.list,
.app-main main.container > .empty { margin-bottom: 1.75rem; }
.app-main main.container > form.stack { margin-bottom: 1.5rem; }

/* ==================== SECTION: knowledge base (/help/) ==================== */

/* --- Index page: search, hero, card grid --------------------------------- */
/* The index is a directory, not prose, so it gets a wider measure than the
   1000px article column. The articles themselves stay narrow (.kb-article). */
main.container.kb-index { max-width: 1120px; }
/* The sprite is a definitions block; without this it reserves a line box. */
.kb-sprite { display: none; }

.kb-search { display: flex; align-items: center; gap: .6rem; margin: 1rem 0 .3rem;
  padding: .1rem .9rem; background: #fff; border: 1px solid var(--line);
  border-radius: 999px; box-shadow: 0 1px 2px rgba(11,51,42,.05);
  transition: border-color .15s ease, box-shadow .15s ease; }
.kb-search:focus-within { border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(20,92,73,.13); }
.kb-search-icon { flex: none; color: var(--muted); }
/* appearance:none — Safari and Chrome give type=search a native clear button
   and an inset shadow that fight the pill. */
.kb-search input { flex: 1; min-width: 0; appearance: none; -webkit-appearance: none;
  border: 0; outline: 0; background: none; font: inherit; font-size: 1rem;
  padding: .7rem 0; color: var(--ink); }
.kb-search input::-webkit-search-cancel-button { display: none; }
.kb-search-clear { flex: none; border: 0; background: none; cursor: pointer;
  color: var(--muted); font-size: 1.4rem; line-height: 1; padding: 0 .2rem; }
.kb-search-clear:hover { color: var(--ink); }
.kb-search-count { color: var(--muted); font-size: .88rem; margin: .5rem 0 0; }

.kb-hero { display: flex; align-items: center; gap: 1.5rem; margin: 1.4rem 0 2rem;
  padding: 1.6rem 1.8rem; border-radius: 16px; /* deliberate hero treatment; everything else is --radius / --radius-sm */ color: var(--ink);
  background: linear-gradient(135deg, #e2ece3 0%, #efeade 60%, var(--surface) 100%);
  border: 1px solid var(--surface-line);
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; }
.kb-hero:hover { text-decoration: none; transform: translateY(-2px);
  border-color: var(--accent); box-shadow: 0 12px 30px rgba(20,92,73,.15); }
.kb-hero-text { flex: 1; min-width: 0; }
.kb-eyebrow { display: block; text-transform: uppercase; letter-spacing: .08em;
  font-size: .72rem; font-weight: 700; color: var(--accent-dark); margin-bottom: .35rem; }
.kb-hero h2 { margin: 0 0 .35rem; font-size: 1.65rem; letter-spacing: -.01em; }
.kb-hero p { margin: 0 0 1.1rem; color: var(--muted); max-width: 46ch; }
.kb-hero-cta { display: inline-flex; align-items: center; gap: .45rem; }
/* Decorative, and the first thing to go when the row gets tight. */
.kb-hero-icon { flex: none; width: 116px; height: 116px; color: var(--accent);
  opacity: .22; stroke: currentColor; fill: none; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round; }

.kb-block { margin: 2rem 0; }
.kb-block-head h2 { margin: 0 0 .1rem; font-size: 1.3rem; }
.kb-block-head p { margin: 0 0 .9rem; color: var(--muted); font-size: .93rem; }
.kb-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: .85rem; }
/* A section holding one article gets the full width rather than a lonely card
   in a half-empty row — which is how the glossary stays prominent. */
.kb-grid > .kb-card:only-child { grid-column: 1 / -1; }

.kb-card { display: flex; align-items: flex-start; gap: .9rem; padding: 1rem 1.1rem;
  background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--ink);
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; }
.kb-card:hover { text-decoration: none; transform: translateY(-2px);
  border-color: var(--accent); box-shadow: 0 10px 24px rgba(11,51,42,.10); }
.kb-card-icon { flex: none; display: grid; place-items: center;
  width: 2.6rem; height: 2.6rem; border-radius: var(--radius);
  background: #e7f5f0; color: var(--accent-dark);
  transition: background .15s ease, color .15s ease; }
.kb-card-icon svg { width: 1.25rem; height: 1.25rem; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.kb-card:hover .kb-card-icon { background: var(--accent); color: #fff; }
.kb-card-text { min-width: 0; }
.kb-card-title { display: block; font-weight: 600; margin-bottom: .15rem; }
.kb-card:hover .kb-card-title { color: var(--accent-dark); }
.kb-card-blurb { display: block; color: var(--muted); font-size: .9rem; line-height: 1.45; }

.kb-empty { color: var(--muted); padding: 1.5rem 0; }
.kb-foot { margin-top: 2rem; }

@media (max-width: 640px) {
  .kb-hero { padding: 1.2rem 1.2rem; gap: 1rem; }
  .kb-hero h2 { font-size: 1.35rem; }
  .kb-hero-icon { display: none; }
}

/* --- Article page -------------------------------------------------------- */
.kb-section { margin: 1.6rem 0; }
.kb-section h2 { margin-bottom: .2rem; }
.kb-list { list-style: none; padding: 0; margin: .6rem 0 0; }
.kb-list li { padding: .55rem 0; border-bottom: 1px solid var(--line); }
.kb-list li:last-child { border-bottom: none; }
.kb-list a { font-weight: 600; }
/* Blurb on its own line: the title is the scannable bit, the blurb is for
   whoever isn't sure the title means what they hope it means. */
.kb-blurb { display: block; color: var(--muted); font-size: .92rem; }

.kb-article { max-width: 42rem; }
/* Readable measure and generous spacing — these pages are read by someone
   who is already stuck, often on a phone. */
.kb-body { line-height: 1.65; }
.kb-body h2 { margin-top: 1.8rem; }
.kb-body h3 { margin-top: 1.3rem; }
.kb-body li { margin: .35rem 0; }
.kb-body ol, .kb-body ul { padding-left: 1.3rem; }
.kb-body code { background: var(--wash); padding: .1rem .3rem; border-radius: 3px; }
.kb-body table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
.kb-body th, .kb-body td { border: 1px solid var(--line); padding: .45rem .6rem;
  text-align: left; vertical-align: top; }
.kb-body th { background: var(--wash); }

/* 16:9 without a fixed height, so it scales down cleanly on a phone. */
.kb-video { position: relative; padding-bottom: 56.25%; height: 0; margin: 1.2rem 0;
  border-radius: var(--radius); overflow: hidden; background: #000; }
.kb-video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* ==================== SECTION: dashboard redesign ==================== */
/* Everything scoped under .dash — .attention/.stat/table.list are shared with
   other pages and must keep their base look there. */
.dash .dash-panel { background: var(--surface); border: 1px solid var(--surface-line);
  border-radius: var(--radius); padding: 1.35rem 1.5rem; box-shadow: none; min-width: 0; }
/* Titles are calm sentence-case section headings, not uppercase eyebrows.
   They were tracked-out muted capitals, which is a label for a field rather
   than a heading for a module: three of them stacked down a page read as
   form furniture, and the shouting competed with the alert rows and the
   counts that are the actual content. Same weight, ink instead of muted,
   normal case, tight tracking. */
.dash .dash-panel h2 { margin: 0 0 1rem; font-family: var(--font-display); font-size: .95rem;
  font-weight: 700; letter-spacing: -.01em; color: var(--ink); }

/* Setup checklist — same card language, washed so it reads as temporary. */
.dash .dash-setup { background: var(--wash); box-shadow: none; margin-bottom: 1.5rem; }
.dash-setup-head { display: flex; align-items: baseline; gap: .8rem; }
.dash .dash-setup-head h2 { flex: 1; margin-bottom: .6rem; }
.dash-dismiss { color: var(--muted); font-size: .85rem; }
.dash-setup-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap;
  gap: .3rem 1.6rem; }
.dash-setup-list li { padding: .15rem 0; font-size: .95rem; }
.dash-setup-list .step-done { color: var(--accent); font-weight: 700; }
.dash-setup-list .step-open { color: var(--muted); }
.dash-setup-list s { color: var(--muted); }

/* "You are exploring with sample data" — the same washed card as the setup
   checklist, and washed for the same reason: it describes a state the account
   is passing through rather than part of the register. Not dismissible, so it
   carries no dismiss control to style. */
.dash .dash-sample { background: var(--wash); box-shadow: none; margin-bottom: 1.5rem; }
.dash .dash-sample p { margin: 0 0 .9rem; color: var(--muted);
  font-size: .95rem; line-height: 1.55; }
.dash .dash-sample p:last-child { margin-bottom: 0; }

/* Board: attention panel left (wider), counters + events stacked right. The
   right column has a 20rem floor so the 2x2 counter lattice never squeezes to
   the point where a four-digit count wraps under its label; the left column's
   minmax(0, …) is what lets it give way instead of forcing a page-wide scroll.
   24px gutters, matching the section rhythm above and below. */
.dash-grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(20rem, 1fr);
  gap: 1.5rem; align-items: start; margin-bottom: 1.5rem; }
.dash-side { display: grid; gap: 1.5rem; min-width: 0; }
/* The attention panel runs the height of the board. `align-items: start`
   sizes both columns to their content, and the right one stacks two panels —
   so at 1440px the primary, wider column finished 479px above the secondary
   one and the largest empty area on the dashboard was bare canvas in the
   middle of it, between two full-width panels. Stretching the queue is the
   only half of this a stylesheet can fix: it is the module that is *expected*
   to grow, so a panel with room under its rows reads as deliberate where a
   hole in the board reads as a mistake. Only this panel — `align-items` on
   the grid would also stretch `.dash-side`, whose auto rows would take the
   extra height and leave the counters lattice floating in a tall box. */
.dash-grid > .dash-attn { align-self: stretch; }
@media (max-width: 900px) { .dash-grid { grid-template-columns: 1fr; } }

/* Attention items are BANDS INSIDE ONE PANEL, not cards inside a card. They
   used to carry a full tinted border and their own radius on a white panel,
   which is the box-inside-box the redesign exists to remove — and on a warm
   panel the near-white alert tints would have made it worse, not better.
   What survives is the part that carries meaning: the 4px left edge, in the
   semantic colour, with a warm band behind it. No border, no shadow; the
   spacing and the tint do the separating.

   The bands themselves now live with `.app-shell .attention` in the Quiet
   Linen section, because the notifications page renders the same list and was
   still on the pre-refresh treatment. What is left here is the part that is
   genuinely about the board: the list is one module inside a panel, so it
   drops the standalone bottom margin the page-level list carries. */
.dash .attention-head { margin: 0 0 .85rem; }
.dash .attention-head h2 { margin: 0; }
.dash .attention { margin: 0; }
.dash .attention-clear { margin: 0; }

/* Live counters: bold 2x2 tiles, display numerals. Overdue tile goes amber
   with a red count when anything is late (.stat-alert set in the template).
   All eight counters live in one grid so every gutter is the same 0.8rem;
   `grid-auto-rows: 1fr` keeps the rows equal height as well — the smaller
   .stat-sm numerals would otherwise make short rows, and a lattice with even
   gaps but uneven rows still reads as misaligned. */
.dash .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 1fr;
  gap: .75rem; margin: 0; }
.dash .stat { background: var(--wash); border: 1px solid transparent;
  border-radius: var(--app-radius-row); padding: .9rem 1.05rem; }
/* tabular-nums: the four operational counts change every reload, and
   proportional digits make the row twitch sideways when 1 becomes 8. */
.dash .stat .num { font-family: var(--font-display); font-size: 2.1rem; font-weight: 800;
  line-height: 1.2; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.dash .stat .lbl { font-size: .84rem; }
.dash a.stat-link:hover { border-color: var(--accent); background: var(--surface); }
.dash .stat-alert { background: var(--accent2-wash); border-color: #f3ddb8; }
.dash .stat-alert .num { color: var(--danger); }
.dash .stat-alert .lbl { color: var(--accent2-ink); font-weight: 600; }
.dash a.stat-link.stat-alert:hover { border-color: var(--accent2); background: var(--accent2-wash); }

/* Upcoming events, compacted for the narrow column. */
.dash-upcoming table.list { font-size: .9rem; }
.dash-upcoming table.list th, .dash-upcoming table.list td { padding: .45rem .45rem; }
.dash-upcoming table.list tr:last-child td { border-bottom: none; }
.dash-upcoming .btn-sm { white-space: nowrap; }

.dash-jobs { margin: .2rem 0 0; }

/* ==================== SECTION: detail-page furniture ====================
   Shared by the person, team and bundle pages so the three read the same
   way: what it IS first, what it HOLDS next, editing controls folded away
   behind a disclosure, history last. Kept here rather than in per-page
   <style> blocks precisely because the three pages must not drift apart. */

/* A fold-away editing section. Native <details> — no JavaScript, and the
   browser's own find-in-page opens it. */
/* In-cell disclosure for a list page's count column (_quick_view.html).
   Lighter than details.fold, which is a page-level box: this one has to sit
   inside a table cell without changing the row's height while shut. The
   caret, the hover wash and the focus ring all come from rules that already
   exist — nothing new is painted here, only placed. */
details.quick { display: inline-block; }
details.quick > summary { cursor: pointer; list-style: none; color: var(--accent);
  display: inline-flex; align-items: center; gap: .3rem; min-height: 24px; }
details.quick > summary::-webkit-details-marker { display: none; }
details.quick > summary::before { content: "▸"; color: var(--muted); font-size: .9em; }
details.quick[open] > summary::before { content: "▾"; }
details.quick > summary:hover { text-decoration: underline; }
/* In the page flow, not floating over it: an overlay needs script to place
   and dismiss itself, and the row below moving down is a smaller surprise
   than a panel that cannot be reached with scripting blocked. */
.quick-body { margin-top: .4rem; padding: .5rem .6rem; background: var(--app-panel);
  border: 1px solid var(--line); border-radius: var(--radius-sm); min-width: 14rem; }
.quick-list { list-style: none; margin: 0; padding: 0; }
.quick-list li { padding: .2rem 0; font-size: .9rem; line-height: 1.35; }
.quick-list li + li { border-top: 1px solid var(--line); }
.quick-more { display: inline-block; margin-top: .45rem; font-size: .88rem; }
.quick-zero { color: var(--muted); }

details.fold { border: 1px solid var(--line); border-radius: var(--radius);
  margin: 1rem 0; background: #fff; }
details.fold > summary { cursor: pointer; list-style: none; padding: .75rem 1rem;
  font-family: var(--font-display); font-weight: 700; font-size: .95rem;
  display: flex; align-items: center; gap: .5rem; }
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary::before { content: "▸"; color: var(--muted); font-size: .9em; }
details.fold[open] > summary::before { content: "▾"; }
details.fold > summary:hover { background: var(--wash); border-radius: var(--radius); }
details.fold[open] > summary { border-bottom: 1px solid var(--line); border-radius: var(--radius) var(--radius) 0 0; }
details.fold .fold-body { padding: .9rem 1rem 1.1rem; }
details.fold .fold-body > :first-child { margin-top: 0; }
details.fold summary .fold-hint { font-weight: 400; color: var(--muted); font-size: .88rem; }

/* Two histories side by side where there's room, stacked where there isn't.
   min-width:0 so a wide table scrolls inside its column instead of forcing
   the page wider. */
.split-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.2rem 1.6rem; align-items: start; }
.split-cols > section { min-width: 0; overflow-x: auto; }
.split-cols > section > h2, .split-cols > section > h3 { margin-top: 0; }
@media (max-width: 900px) { .split-cols { grid-template-columns: 1fr; } }

/* Multi-select on list pages. The bar is a normal in-flow block by default —
   a bar that floats over every list page whether or not anything is ticked is
   the failure this guards against. JS adds .is-sticky once a selection
   exists, and only then does it pin to the bottom of the viewport. */
.bulk-bar { display: flex; align-items: center;
  gap: .8rem; flex-wrap: wrap; margin-top: .8rem; padding: .7rem .9rem;
  background: var(--sidenav-bg); color: #fff; border-radius: var(--radius);
  box-shadow: 0 6px 20px rgba(11,51,42,.22); }
.bulk-bar.is-sticky { position: sticky; bottom: 0; z-index: 5; }
.bulk-bar[hidden] { display: none !important; }
.bulk-bar .bulk-count { font-weight: 700; }
.bulk-bar .btn { background: #fff; }
.bulk-bar .linklike { color: var(--sidenav-ink); }
td.bulk-cell, th.bulk-cell { width: 2.2rem; }

/* ==================== SECTION: dashboard panel edges ====================
   The two boards (attention, counters) used to be outlined in the sidebar's
   forest, to tie the white cards back to the rail. Quiet Linen retires that:
   on a warm canvas with warm panels the boxes already belong to the page, and
   a dark outline around an ordinary content panel is the loudest line on a
   screen whose job is to be calm. The hook stays — it is in the dashboard
   markup and the class must not go rule-less — and now simply agrees with
   every other panel edge. */
.dash .dash-panel-edge { border-color: var(--surface-line); }
/* Counter tiles for the smaller entity counts — same tile, quieter numerals
   AND a softer inset, so the four operational counts (assets, out on loan,
   available, overdue) read as the primary row without the grid being split in
   two. One .stat-grid, eight equally clickable tiles, two levels of emphasis.
   (Their shared grid — columns, equal rows — lives with .dash .stat-grid in
   the dashboard section above.) */
.dash .stat-sm { background: var(--app-inset-soft); }
.dash .stat-sm .num { font-size: 1.5rem; }

/* Automatic email history: a real section, not a footnote. It follows the
   board directly, at the same 24-32px beat as everything else (the .dash-grid
   margin below it collapses into this one, landing at 28px), and its entries
   stay flat rows separated by a quiet divider — a second stack of cards under
   the board is exactly the box-inside-box this pass removed upstairs. */
.dash-jobs-panel { margin-top: 1.75rem; }
.dash-jobs-panel .job-line { display: flex; gap: .6rem; align-items: baseline;
  padding: .45rem 0; border-bottom: 1px solid var(--surface-line); font-size: .92rem; }
.dash-jobs-panel .job-line:last-child { border-bottom: none; }
.dash-jobs-panel .job-when { color: var(--muted); font-size: .85rem; white-space: nowrap; }
.dash-jobs-panel .job-fail { color: var(--danger); font-weight: 600; }
.dash-jobs-panel details.fold { margin: .6rem 0 0; }

/* Notifications page: unread marker + per-item actions. */
.notif-actions-row { display: flex; gap: .4rem; align-items: center; flex-wrap: wrap; }

/* Pending deletions: an approval queue, so it reads as a request rather than
   an alarm — amber like the other "someone is waiting on you" surfaces. */
.dash .dash-pending { background: var(--accent2-wash); border-color: #f3ddb8;
  margin-bottom: 1.5rem; }
.dash .dash-pending h2 { color: var(--accent2-ink); }
.dash .dash-pending table.list { background: var(--surface); border-radius: var(--app-radius-row); }

/* ---------------------------------------------------------------------------
   Ops console (staff-only, under the admin path). Everything here is prefixed
   `ops-` on purpose: the founder pages and the customer dashboard share a
   vocabulary (attention lists, status pills) and the customer rules already own
   the unprefixed names — `.att-warn` is the app dashboard's, not this one's.
   --------------------------------------------------------------------------- */
.ops-tabs { display: flex; gap: .3rem; flex-wrap: wrap; border-bottom: 1px solid var(--line);
  margin: 0 0 1.4rem; }
.ops-tabs a { padding: .5rem .95rem; border: 1px solid transparent; border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0; color: var(--muted); font-size: .94rem;
  margin-bottom: -1px; }
.ops-tabs a:hover { background: var(--wash); text-decoration: none; color: var(--ink); }
.ops-tabs a.active { background: #fff; border-color: var(--line); color: var(--ink);
  font-weight: 600; border-bottom: 1px solid #fff; }

/* Alarms. Sits above everything else on the dashboard and is the only block
   allowed to be loud — if it's empty the page has nothing urgent in it. */
.ops-attention { border: 1px solid #f0d4d1; border-left: 4px solid var(--danger);
  border-radius: var(--radius); background: #fdf6f5; padding: 1rem 1.2rem; margin-bottom: 1.6rem; }
.ops-attention h2 { margin: 0 0 .6rem; font-size: 1.1rem; }
.ops-attention-clear { border-color: var(--surface-line); border-left-color: var(--ok); background: #eef3ea; }
.ops-attention-clear p { margin: 0; color: var(--muted); }
.ops-att-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.ops-att-list li { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
  background: #fff; border: 1px solid var(--line); border-radius: calc(var(--radius) - 3px);
  padding: .6rem .8rem; }
.ops-att-body { flex: 1; min-width: 12rem; font-size: .95rem; }
.ops-att-dot { flex: none; width: .6rem; height: .6rem; border-radius: 50%; background: var(--muted); }
.ops-att-bad .ops-att-dot { background: var(--danger); }
.ops-att-warn .ops-att-dot { background: var(--accent2); }

/* Health cards: one per section, whole card is the link. */
.ops-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1rem; margin-bottom: 1.6rem; }
.ops-card { display: block; border: 1px solid var(--line); border-left: 4px solid var(--line);
  border-radius: var(--radius); padding: 1rem 1.1rem; color: inherit; background: #fff; }
.ops-card:hover { text-decoration: none; box-shadow: 0 8px 24px rgba(11,51,42,.08);
  transform: translateY(-2px); }
.ops-card, .ops-card:hover { transition: transform .12s ease, box-shadow .12s ease; }
.ops-card-head { display: flex; align-items: center; gap: .5rem; font-family: var(--font-display); }
.ops-card-line { margin: .5rem 0 .7rem; font-size: .92rem; color: var(--muted); }
.ops-card-metrics { display: flex; gap: 1rem; flex-wrap: wrap; font-size: .82rem; color: var(--muted); }
.ops-card-metrics b { font-size: 1.15rem; color: var(--ink); font-weight: 700; }
.state-dot { width: .65rem; height: .65rem; border-radius: 50%; background: var(--ok); flex: none; }
.state-ok { border-left-color: var(--ok); }
.state-warn { border-left-color: var(--accent2); }
.state-warn .state-dot { background: var(--accent2); }
.state-bad { border-left-color: var(--danger); }
.state-bad .state-dot { background: var(--danger); }

/* Section-level callouts on the detail pages. */
.ops-alert { border: 1px solid var(--line); border-left: 4px solid var(--line);
  border-radius: var(--radius); padding: .2rem 1.1rem 1rem; margin-bottom: 1.6rem; }
.ops-alert h2 { font-size: 1.05rem; }
.ops-alert-bad { border-color: #f0d4d1; border-left-color: var(--danger); background: #fdf6f5; }
.ops-alert-warn { border-color: #f3ddb8; border-left-color: var(--accent2); background: var(--accent2-wash); }
.ops-alert-ok { border-color: var(--surface-line); border-left-color: var(--ok); background: #eef3ea; }
.ops-alert table.list { background: #fff; border-radius: calc(var(--radius) - 3px); }

/* Sparklines. Pure CSS heights from the view's integer percentages — no chart
   library and no JavaScript, so the page draws with scripts blocked. Each bar
   carries its own title; the container carries the summary for screen readers. */
.ops-charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1rem; margin-bottom: 1.6rem; }
.chart-box { border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.1rem; }
.chart-head { display: flex; justify-content: space-between; align-items: baseline;
  gap: .6rem; flex-wrap: wrap; margin-bottom: .7rem; }
.spark { display: flex; align-items: flex-end; gap: 2px; height: 90px; }
.spark-bar { flex: 1; min-width: 3px; background: var(--accent); border-radius: 2px 2px 0 0; }
.spark-alt { background: var(--accent2); }
.spark-zero { background: var(--line); }
.chart-axis { display: flex; justify-content: space-between; color: var(--muted); margin-top: .35rem; }

/* Share-of-total bar inside a table cell. */
.meter { display: inline-block; width: 120px; height: .5rem; background: var(--wash);
  border-radius: 99px; overflow: hidden; vertical-align: middle; margin-right: .5rem; }
.meter-fill { display: block; height: 100%; background: var(--accent); }

.ops-foot { margin-top: 2rem; }

/* The ops console on Quiet Linen. It renders inside .app-shell only when a
   staff user also has an org, and today the founder accounts have none — so
   it usually draws in the :root palette and nothing above was obviously
   wrong. That is exactly why it is worth doing now: the day a founder account
   gains an org, five pages would appear half-migrated, with #fff cards and a
   hover lift sitting on linen. Scoped like everything else, so the palette it
   falls back to is unchanged.

   Same rules as the customer app. Cards and tab panels take the panel layer;
   the alert blocks keep their meaning in the 4px left edge and take the
   measured semantic tints rather than a pre-palette pink; the health cards
   stop lifting, because a card that navigates is still a static panel here
   (the whole console is links) and the lift was the only movement left in the
   app after .cards and .price-card gave theirs up. The active tab is a
   control, so its outline is --app-control-line and not the seam. */
.app-shell .ops-tabs { border-bottom-color: var(--app-control-line); }
.app-shell .ops-tabs a.active { background: var(--surface); border-color: var(--app-control-line);
  border-bottom-color: var(--surface); }
/* Fill only, no `border-color`, on the two rules below. The health card's
   meaning is its 4px left edge, set by `.state-ok/-warn/-bad` at (0,1,0);
   a `border-color` shorthand here would be (0,2,0) and would silently repaint
   it the seam colour — the same trap that flattened the attention queue. The
   base rules already resolve --line to --app-line inside this scope. */
.app-shell .ops-card { background: var(--surface); }
.app-shell .chart-box { background: var(--surface); border-color: var(--surface-line); }
.app-shell .ops-card:hover { box-shadow: none; transform: none; }
.app-shell .ops-att-list li, .app-shell .ops-alert table.list { background: var(--surface);
  border-radius: var(--app-radius-row); }
.app-shell .ops-attention, .app-shell .ops-alert-bad { background: var(--app-tint-danger);
  border-color: var(--surface-line); border-left-color: var(--danger); }
.app-shell .ops-alert-warn { background: var(--app-tint-warn);
  border-color: var(--surface-line); border-left-color: var(--accent2); }
.app-shell .ops-attention-clear, .app-shell .ops-alert-ok { background: var(--wash);
  border-color: var(--surface-line); border-left-color: var(--ok); }

/* Billing: the paid subscription summary. A customer who is paying should be
   able to answer "what am I on, what's next, how much" without leaving the
   page — the old version was one sentence and a pricing grid. */
.sub-headline { margin: 0 0 .8rem; font-size: 1.25rem; }
.sub-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: .8rem 1.2rem; margin: 0 0 1rem; }
.sub-facts dt { font-size: .78rem; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); margin-bottom: .15rem; }
.sub-facts dd { margin: 0; font-weight: 600; }
.sub-summary .note { background: var(--warn-bg); color: var(--warn-ink);
  padding: .55rem .8rem; border-radius: var(--radius); }
/* Cancellation stays findable and stops competing with the primary action. */
.cancel-line { margin-top: 1.4rem; }
.cancel-line a { color: var(--danger); }

/* A fieldset that groups semantically (radios + their <legend> question)
   without the browser's box drawing a form inside a form. */
.fieldset-plain { border: 0; padding: 0; margin: 0; }

/* Global search in the app header: <form class="global-search" role="search">
   with an input[type=search] and a small submit. Sized to sit inside the slim
   bar without changing its height. */
.global-search { display: flex; align-items: center; gap: .4rem;
  flex: 0 1 22rem; min-width: 0; }
.global-search input[type="search"] { flex: 1; min-width: 0; font-size: .9rem;
  padding: .32rem .65rem; }
.global-search button { flex: none; font: inherit; font-size: .85rem; cursor: pointer;
  padding: .32rem .8rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: #fff; color: var(--ink); }
.global-search button:hover { border-color: var(--accent); color: var(--accent-dark); }
/* Which page the header belongs to — quiet, and first to give way. */
.header-pagename { color: var(--muted); font-size: .92rem; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Margin utilities on the --space-* scale. These exist to replace the one-off
   inline style="margin-top:…" attributes scattered through templates — reach
   for these instead of writing a new inline style. */
/* Zero variants, for cancelling an element's own margin (an h2 inside a
   page-head, a .filters row butted against what follows). Declared before
   the scale variants so `class="m-0 mb-2"` resolves the way it reads. */
.m-0 { margin: 0; } .mt-0 { margin-top: 0; } .mb-0 { margin-bottom: 0; }
.mt-1 { margin-top: var(--space-1); } .mb-1 { margin-bottom: var(--space-1); }
.mt-2 { margin-top: var(--space-2); } .mb-2 { margin-bottom: var(--space-2); }
.mt-3 { margin-top: var(--space-3); } .mb-3 { margin-bottom: var(--space-3); }
.mt-4 { margin-top: var(--space-4); } .mb-4 { margin-bottom: var(--space-4); }
.mt-5 { margin-top: var(--space-5); } .mb-5 { margin-bottom: var(--space-5); }

/* The rest of the 2026-08-24 inline-style sweep: every class below replaces a
   style="..." attribute that lived in a template. Utilities stay single-purpose;
   anything wider grew into a named component instead. The only inline styles
   left in templates are dynamic ({{ }} widths/heights, per CLAUDE.md on the
   ops charts) and the two anti-spam honeypots, whose off-screen position is
   the mechanism. test_template_lint.py enforces that. */
.muted { color: var(--muted); }
.danger-ink { color: var(--danger); }
.nowrap { white-space: nowrap; }
.rounded { border-radius: var(--radius); }  /* a banner/msg used in-content, not edge-to-edge */
.span-all { grid-column: 1 / -1; }          /* one card taking the whole .cards row */
.grow { flex: 1; min-width: 0; }            /* flex filler that can still shrink */

/* Table column widths. col-pick is the tick-this-row checkbox/radio column;
   the w-* are the few fixed column widths the tables use, in rem. */
.col-pick { width: 2rem; }
.w-6 { width: 6rem; } .w-7 { width: 7rem; } .w-9 { width: 9rem; } .w-14 { width: 14rem; }

/* Control widths. !important is deliberate: these replaced inline styles,
   which outranked every sheet rule — without it form.stack's full-width
   input rule and .filters' search-input min-width:0 quietly win instead. */
.input-qty { width: 5rem !important; }      /* small quantity <input type=number> */
.minw-280 { min-width: 280px !important; }  /* keep a search box usable in a flex row */
.maxw-280 { max-width: 280px; }             /* stop a table-cell input stretching */

/* The slim-nav note shown while email is unverified. */
.nav-note { color: var(--muted); font-size: .92rem; }

/* Error-summary list inside a .msg-error box (_form_fields.html). */
.msg .err-list { margin: .35rem 0 0 1.1rem; padding: 0; }

/* The 404's .empty box is a whole page, not a slot in one. */
.empty-page { margin: var(--space-5) auto; max-width: 520px; }

/* Sub-choice radios indented under their parent option (kit_conflicts). */
.option-indent { display: block; margin-left: 1.4rem; }

/* A row of action buttons/forms between sections (project board). Margin
   matches .filters so adjacent control rows share one rhythm. */
.action-row { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; margin: 1rem 0; }

/* Event prep checklist (project_detail) — the same widget as the dashboard's
   setup checklist (.dash-setup-*); it shares .dash-dismiss for the button. */
.prep-head { display: flex; align-items: baseline; }
.prep-head .spacer { flex: 1; }
.prep-steps { list-style: none; padding: 0; margin: var(--space-1) 0 0; }
.prep-steps li { display: inline-block; margin-right: var(--space-3); padding: .15rem 0; }
.prep-steps .step-done { color: var(--accent); }
.prep-steps .step-done-label { color: var(--muted); }

/* Kit-readiness bar (project_detail). The fill's width percentage is computed
   in the view and set inline — dynamic styles stay in the template, same rule
   as the ops charts. */
.progress { background: var(--wash); border-radius: 99px; height: 10px; margin-top: var(--space-1); }
.progress-fill { background: var(--accent); border-radius: 99px; height: 10px; }

/* Currency symbols sit inside money fields without changing the standard stacked form layout. */
.money-input { position: relative; }
.money-input > span { position: absolute; top: 0; bottom: 0; left: 14px; display: grid;
  place-items: center; color: var(--muted); pointer-events: none; }
form.stack .field .money-input input:not([type="hidden"]) { padding-left: 30px; }

/* Reduced motion: decorative transitions are switched off and lifted
   elements stay flat so nothing jumps even instantly. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
  .btn:hover, .cards .card:hover, .price-card:hover, a.stat-link:hover,
  .welcome-card:hover, .kb-hero:hover, .kb-card:hover, .ops-card:hover { transform: none; }
}
