/* ==========================================================================
   Fabricata Track — design system

   Sibling to Fabricata Cut, not a twin. Everything structural is shared with
   Cut and matched to its actual values: the neutral ramp, the radius scale
   (6/12/14/18/24), the soft wide shadows, Inter for text with Montserrat 800
   for display, 15px body, 1240px content, gradient primary buttons at weight
   800, 14px inputs with a 4px focus ring, a 290px sidebar with the topbar
   inside the content column, and the pill vocabulary.

   What differs is the job: Cut is blue and optimises material; Inspect is teal
   and verifies it; Track is plum and measures progress against the plan.
   Green, amber and red are therefore *never* the accent here: they are
   reserved for schedule health (on track, at risk, overdue), and a status
   pill must not be mistakable for a button. Track's own surfaces -- progress
   bars, stage bars, the Gantt -- are appended at the end of this file.

   This file started as Fabricata Inspect's app.css so the family's components
   are pixel-identical; the tokens below are Track's.
   ========================================================================== */

/*
  Self-hosted type.

  These were loaded from fonts.googleapis.com with a <link>, which is a
  render-blocking request to a third party on every page: an extra DNS lookup,
  TLS handshake and round trip before any text can paint, plus the privacy
  cost of every visitor's IP reaching Google. The five faces actually used
  total 116 KB and now ship from the same origin as everything else.

  Latin subset only. The application is English, and the full unicode range
  would be several times the size for glyphs no page renders.
*/
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/static/fonts/inter-latin-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 600;
  font-display: swap; src: url("/static/fonts/inter-latin-600-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 700;
  font-display: swap; src: url("/static/fonts/inter-latin-700-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 800;
  font-display: swap; src: url("/static/fonts/inter-latin-800-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Montserrat"; font-style: normal; font-weight: 800;
  font-display: swap; src: url("/static/fonts/montserrat-latin-800-normal.woff2") format("woff2");
}

:root {
  /* Neutrals — shared with Cut */
  --bg:              #f4f7fb;
  --bg-2:            #eef3f9;
  --surface:         #ffffff;
  --surface-2:       #f8fbff;
  --surface-3:       #eef4ff;
  --border:          #dbe5f0;
  --border-strong:   #c5d2e0;
  --text:            #0f172a;
  --muted:           #64748b;
  --muted-2:         #94a3b8;
  /* Cut's --muted is 4.43:1 on --bg and --muted-2 only 2.56:1 on white -- both
     short of AA once the type drops under 13px, which is most of the metadata
     in this app. This is the token for that text. Inspection work happens on a
     phone in workshop light; the extra contrast is not decoration. */
  --muted-ink:       #55657a;
  --steel:           #475569;
  --charcoal:        #0f172a;

  /* Primary — Track plum. The Fabricata product colours are Cut blue #2563eb,
     Flow violet #7c3aed, Track plum #8e2ba3 and Inspect green #0e7a4f; plum keeps
     Track in the purple family without being mistaken for Flow. */
  --primary:         #8e2ba3;
  --primary-strong:  #74208a;
  --primary-deep:    #5e1a70;
  --primary-soft:    #f9effb;
  --primary-line:    #eacff1;

  /* Cut's blue is kept for cross-product links */
  --family-blue:     #2563eb;

  /* Status semantics -- schedule health uses these, never the accent */
  --success:         #15803d;
  --success-soft:    #ebf9ef;
  --success-line:    #cde9d4;
  --warning:         #b45309;
  --warning-soft:    #fef4e6;
  --warning-line:    #fbd9a5;
  --danger:          #b91c1c;
  --danger-soft:     #fef2f2;
  --danger-line:     #f3cfcf;
  --info:            #1d4ed8;
  --info-soft:       #e9f1ff;
  --info-line:       #cad9ff;
  --neutral-soft:    #f5f7fb;
  --neutral-line:    #e3e8f1;

  --surface-dark:    #111923;
  --surface-dark-2:  #17212d;

  /* Elevation — Cut's values */
  --shadow-sm: 0 10px 24px rgba(15,23,42,.06);
  --shadow-md: 0 20px 48px rgba(15,23,42,.09);
  --shadow-lg: 0 28px 70px rgba(15,23,42,.13);

  --radius-xl: 24px;
  --radius-lg: 18px;
  --radius-md: 14px;
  --radius-sm: 12px;
  --radius-xs: 6px;

  --content: 1240px;
  --sidebar: 290px;

  --font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --display: "Montserrat", "Inter", system-ui, sans-serif;
  /* Not a typewriter face. References want their digits to line up when you
     scan a column of them; they do not want to look like a terminal. Inter
     with tabular figures gives the alignment without the connotation. The
     real monospace stack is kept for the one place that needs it -- a reset
     link, where a character-for-character read matters. */
  --mono: "Inter", system-ui, sans-serif;
  --mono-true: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* The focus indicator is two things: a solid outline that has to pass
     WCAG 1.4.11 (3:1 against what is next to it), and a soft halo that is
     decoration. The halo alone was doing the job before -- 4px of the accent
     at 16% alpha, which measures 1.20:1 on the page ground and 1.21:1 on a
     card, so a keyboard user could not see where they were. It stays, but
     --focus-outline is now what actually satisfies the criterion:
     #74208a on --bg #f4f7fb is 8.44:1. */
  --focus-outline:   #74208a;
  --ring: 0 0 0 4px rgba(142,43,163,.16);
  --focus-edge:      #ce8be0;
  /* The foreground for anything sitting ON the accent. In light mode the
     accent is a deep plum and white reads on it (6.9:1); in dark mode the accent
     lifts to a light plum, where white collapses to about 2.3:1. This
     token moves with it so a filled button never loses its label. */
  --on-primary:      #ffffff;
  /* The filled button's two gradient stops, as tokens, so the contrast of
     the label against the *lighter* stop is testable. White on the lighter
     stop #74208a is 9.07:1; the test suite recomputes it from these tokens. */
  --btn-from:        #74208a;
  --btn-to:          #5e1a70;
  --sidebar-bg:      linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
  --app-bg:          linear-gradient(180deg, #f8fbff 0%, #eef4fb 100%);
  /* The sticky section switcher on a phone. A token rather than a literal so
     it inverts with the rest of the palette. */
  --section-nav-bg:      rgba(244,247,251,.94);
  --section-nav-bg-blur: rgba(244,247,251,.8);
  --scrim:           rgba(15,23,42,.45);
}

/*
  Dark palette.

  Every token above is redefined here rather than patching individual rules,
  which is why the whole application inverts from one block. The roles are
  preserved exactly: --surface is still the thing a card sits on, --primary is
  still the accent, and the semantic pairs still read as pass / warn / fail.

  Two deliberate choices. Backgrounds are a warm-neutral slate rather than
  pure black, because pure black kills the elevation the whole layout depends
  on. And the accent lifts from #8e2ba3 to #d58be8: the deeper plum that
  carries a white label on a light page does not clear AA as text on a dark
  one, so the token moves rather than the contrast dropping.
*/
@media (prefers-color-scheme: dark) {
  :root {
    --bg:              #0b1220;
    --bg-2:            #0e1626;
    --surface:         #131c2b;
    --surface-2:       #172232;
    --surface-3:       #1d293b;
    --border:          #26344a;
    --border-strong:   #35455e;
    --text:            #e8eef8;
    --muted:           #94a3b8;
    --muted-2:         #64748b;
    --muted-ink:       #a7b4c7;
    --steel:           #94a3b8;
    /* Deliberately NOT inverted: --charcoal is only ever a background for a
       white foreground (the dark chip, the spec header, .btn-dark). Flipping
       it turned white text onto a light ground. */
    --charcoal:        #16202f;

    --primary:         #d58be8;
    --primary-strong:  #e6b3f2;
    --primary-deep:    #f2d6f8;
    --primary-soft:    #2b1734;
    --primary-line:    #4b2757;

    --family-blue:     #60a5fa;

    --success:         #4ade80;
    --success-soft:    #0e2a1a;
    --success-line:    #1c4a30;
    --warning:         #fbbf24;
    --warning-soft:    #2c2008;
    --warning-line:    #4d3a10;
    --danger:          #f87171;
    --danger-soft:     #2c1416;
    --danger-line:     #4d2226;
    --info:            #7dabff;
    --info-soft:       #101c33;
    --info-line:       #1f3358;
    --neutral-soft:    #172232;
    --neutral-line:    #26344a;

    --surface-dark:    #0b1220;
    --surface-dark-2:  #131c2b;

    /* Shadows do almost nothing on a dark ground, so elevation comes from
       the surface ramp instead and these stay subtle. */
    --shadow-sm: 0 10px 24px rgba(0,0,0,.34);
    --shadow-md: 0 20px 48px rgba(0,0,0,.44);
    --shadow-lg: 0 28px 70px rgba(0,0,0,.55);

    --ring: 0 0 0 4px rgba(213,139,232,.26);
    /* #e6b3f2 on the dark ground #0b1220 is 10.8:1, well clear of 3:1. */
    --focus-outline:   #e6b3f2;
    --focus-edge:      #d58be8;
    --on-primary:      #2a0b33;
    --btn-from:        #d58be8;
    --btn-to:          #e6b3f2;
    --sidebar-bg:      linear-gradient(180deg, #131c2b 0%, #0e1626 100%);
    --app-bg:          linear-gradient(180deg, #0b1220 0%, #0e1626 100%);
    --section-nav-bg:      rgba(11,18,32,.94);
    --section-nav-bg-blur: rgba(11,18,32,.8);
    --scrim:           rgba(0,0,0,.62);
  }
}

*, *::before, *::after { box-sizing: border-box; }

/* The ``hidden`` attribute has to mean hidden. The browser's own rule for it is
   a plain ``display: none`` that any class setting ``display`` overrides -- and
   ``.btn`` sets ``inline-flex``. So the first-run tour's Back and Next buttons,
   which app.js hides with ``el.hidden = true``, were showing on every step. */
[hidden] { display: none !important; }

/* Touch behaviour on every control the app owns. `manipulation` removes the
   double-tap-to-zoom delay without disabling pinch zoom, and the tap
   highlight becomes the accent rather than the browser's default grey box. */
a, button, .btn, label, summary, select,
input[type="checkbox"], input[type="radio"], input[type="file"] {
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(142,43,163,.18);
}

html {
  -webkit-text-size-adjust: 100%;
  /* Tells the browser to render its own furniture -- scrollbars, form
     controls, the caret -- to match. Without it, dark pages get light
     scrollbars. */
  color-scheme: light dark;
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--display);
  color: var(--text);
  letter-spacing: -.04em;
  line-height: 1.08;
  font-weight: 800;
  text-wrap: balance;
}
h3 { font-size: 1.15rem; font-weight: 800; letter-spacing: -.02em; }
h4 { font-size: .95rem; letter-spacing: -.01em; }
p { margin: 0 0 .75rem; }
p:last-child { margin-bottom: 0; }

a { color: var(--primary-strong); text-decoration: none; font-weight: 600; }
a:hover { text-decoration: underline; }

.btn:focus-visible, a:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, summary:focus-visible,
button:focus-visible {
  /* A real outline, not just the halo. `outline: none` with only a 16%-alpha
     box-shadow left keyboard users with a 1.20:1 indicator -- effectively
     invisible, and the first thing to disappear on a laptop screen in
     workshop light, which is where this product is used. */
  outline: 2px solid var(--focus-outline);
  outline-offset: 2px;
  box-shadow: var(--ring);
  border-radius: var(--radius-sm);
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--text); color: var(--bg); padding: 12px 16px;
  border-radius: 0 0 var(--radius-md) 0; font-weight: 700;
}
.skip:focus { left: 0; }

/* ------------------------------------------------------------------ brand */

/* --------------------------------------------------------------- the mark ---
   One lockup, used everywhere, matching Fabricata Cut's construction:
   [arc + square]  FABRICATA  [tile with INSPECT knocked out].

   The full wordmark shows from 560px up; below that only the mark, because a
   1261 x 200 lockup either shrinks to an illegible smear or pushes the header
   row past the viewport. Cut ships both variants for the same reason. */
.brand { display: flex; align-items: center; gap: 12px; color: inherit; min-width: 0; }
.brand:hover { text-decoration: none; }
/* The lockup is a fixed 1261 x 200 asset scaled by its height, so its width is
   189px and is not negotiable. It must never be asked to shrink to make room
   for something beside it -- a squashed wordmark is worse than a wrapped
   layout -- so it opts out of flex shrinking and the layout works around it. */
.brand-lockup { height: 30px; width: auto; display: block; flex: 0 0 auto; }
.brand-mark { width: 36px; height: 36px; flex: 0 0 auto; display: none; }
.brand--footer .brand-lockup { height: 34px; }
.brand--footer { margin-bottom: 12px; }

/* --------------------------------------------------- the workspace name ---
   The sidebar is 290px wide with 22px of padding, so its top row has 246px to
   spend. The lockup takes 189 of that and the gap another 12, which left the
   company name 45px: "Meridian Manufacturing" rendered as "Mer…". The name of
   the workspace you are signed in to is not decoration -- in a product where
   one person does third-party work for several companies it is the answer to
   "whose data am I looking at" -- and it was being silently cut to nothing by
   an ellipsis rule that was doing exactly what it was told.

   So the two stack. The name gets the full 246px on its own line, which fits
   about 34 characters, and the rule below it replaces the vertical divider
   that separated them side by side.

   Only where there is a subtitle at all: the marketing, auth and error-page
   lockups pass none and keep the plain horizontal row. */
.brand--stacked { flex-direction: column; align-items: flex-start; gap: 8px; }
.brand-subtitle {
  font-size: 12.5px; color: var(--muted-ink); font-weight: 600;
  max-width: 100%; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding-left: 12px; margin-left: 2px; border-left: 1px solid var(--border);
}
/* Stacked, the name sits under the mark rather than beside it, so the divider
   becomes a rule above it instead of a bar to its left.

   Two lines before it gives up, not one. The sidebar is a fixed 290px at every
   window size, so a long name cannot be rescued by a wider screen -- one line
   holds about 34 characters and "Northgate Structural Steel & Plate
   Fabrication (Pty) Ltd" is 53. Two lines cover every company name we have
   seen and still leave the navigation where it was. Past that it does
   truncate, deliberately, and the `title` on the element carries the whole
   name for a hover and for a screen reader. */
.brand--stacked .brand-subtitle {
  width: 100%;
  padding: 7px 0 0; margin-left: 0;
  border-left: 0; border-top: 1px solid var(--border);
  white-space: normal; overflow-wrap: anywhere; line-height: 1.35;
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2;
}

/* ------------------------------------------------------------------ shell */

.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar) minmax(0, 1fr);
  min-height: 100dvh;
  background: var(--app-bg);
}

.sidebar {
  position: sticky; top: 0; height: 100dvh;
  padding: 22px;
  border-right: 1px solid var(--border);
  background: var(--sidebar-bg);
  display: flex; flex-direction: column; gap: 18px;
  z-index: 25; overflow-y: auto;
}
/* ``align-items: flex-start`` because the brand is now two stacked lines and
   the drawer's close button belongs beside the first of them, not centred
   against both. ``min-width: 0`` on the brand so its own truncation can
   actually engage instead of the row growing past the sidebar. */
.sidebar-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.sidebar-top .brand { flex: 1 1 auto; min-width: 0; }
.sidebar-close {
  display: none; background: none; border: 0; font-size: 28px;
  line-height: 1; cursor: pointer; color: var(--muted-ink);
  width: 44px; height: 44px; border-radius: var(--radius-sm);
  align-items: center; justify-content: center; flex: 0 0 auto;
}
.sidebar-section { display: grid; gap: 10px; }
.sidebar-section-label {
  font-size: 12.5px; font-weight: 700; color: var(--muted-ink);
  letter-spacing: -.005em;
}
.sidebar-nav { display: grid; gap: 7px; }
.sidebar-nav a, .sidebar-nav button {
  display: flex; align-items: center; gap: 11px;
  padding: 12px 14px; border-radius: var(--radius-md);
  color: var(--text); font-weight: 700; font-size: 14px;
  font-family: inherit; background: none; border: 1px solid transparent;
  width: 100%; text-align: left; cursor: pointer;
}
.sidebar-nav a:hover, .sidebar-nav button:hover {
  background: var(--surface-2); text-decoration: none;
}
.sidebar-nav a.active {
  background: var(--primary-soft);
  border-color: var(--primary-line);
  color: var(--primary-strong);
}
.sidebar-nav svg { width: 18px; height: 18px; flex: 0 0 auto; opacity: .8; }
.sidebar-nav a.active svg { opacity: 1; }
.sidebar-foot { margin-top: auto; display: grid; gap: 10px; }
.sidebar-user {
  display: flex; align-items: center; gap: 11px;
  padding: 12px; border-radius: var(--radius-md);
  background: var(--surface-2); border: 1px solid var(--border);
}
.avatar {
  width: 36px; height: 36px; border-radius: 12px; flex: 0 0 auto;
  background: linear-gradient(135deg, var(--btn-from), var(--btn-to));
  color: var(--on-primary); display: grid; place-items: center;
  font-size: 13px; font-weight: 800; letter-spacing: -.02em;
}
.sidebar-user-meta { min-width: 0; display: grid; gap: 1px; }
.sidebar-user-meta strong { font-size: 13.5px; font-weight: 800; letter-spacing: -.01em; }
.sidebar-user-meta span {
  font-size: 11.5px; color: var(--muted-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.content-wrap { min-width: 0; display: flex; flex-direction: column; }
.topbar { display: grid; gap: 16px; padding: 24px 28px 0; }
.topbar-main {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 18px; flex-wrap: wrap;
}
.topbar-kicker-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.topbar-actions { display: flex; gap: 10px; flex-wrap: wrap; }
/* A single-button form that must not become a layout box of its own. Issuing a
   report is a POST, so the button has to sit in a form, but the form should be
   invisible to the surrounding flex container -- the button itself stays the
   flex item and keeps the sizing rules below. */
.inline-form { display: contents; }
.page-title {
  font-family: var(--display); font-weight: 800; letter-spacing: -.04em;
  font-size: clamp(1.35rem, 5.2vw, 1.9rem);
  overflow-wrap: anywhere;
}
.page-sub { color: var(--muted-ink); font-size: 14px; margin-top: 4px; }
.page-stack { padding: 20px 28px 48px; display: grid; gap: 18px; align-content: start; }

.app-nav-toggle {
  display: none; border: 1px solid var(--border); background: var(--surface);
  border-radius: var(--radius-sm); font-size: 18px; line-height: 1;
  cursor: pointer; min-width: 44px; min-height: 44px;
  align-items: center; justify-content: center;
}

.eyebrow {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 12.5px; font-weight: 700; color: var(--muted-ink);
  letter-spacing: -.005em;
}
.breadcrumb {
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
  font-size: 12.5px; color: var(--muted-ink); font-weight: 600;
  min-width: 0;
}
.breadcrumb a { color: var(--muted-ink); font-weight: 600; }
.breadcrumb span, .breadcrumb a {
  max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ------------------------------------------------------------------ cards */

.card, .sub-card, .table-card, .empty-state, .status-card, .metric-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.card, .metric-card, .status-card {
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
}
.card { padding: 26px; }
.card-head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: -26px -26px 20px; padding: 18px 26px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.card-head h2, .card-head h3 { font-size: 1.02rem; }
.card-head .spacer { flex: 1; }
.card-hint { width: 100%; font-size: 12.5px; color: var(--muted-ink); margin-top: 2px; }
.card-foot {
  margin: 22px -26px -26px; padding: 18px 26px;
  border-top: 1px solid var(--border); background: var(--surface-2);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
}
.table-card { padding: 0; overflow: hidden; }
/* A card foot pulls itself out to the card's own 26px padding with negative
   margins. A table card has no padding, so those margins pushed the foot --
   and the "Add a user" form inside it -- 26px past the card on each side. */
.table-card > .card-foot { margin: 0; }
.table-card > .card-head { margin: 0; border-radius: var(--radius-lg) var(--radius-lg) 0 0; }

/* ----------------------------------------------------------------- metrics */

.stats-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(178px, 1fr));
}
.metric-card { padding: 20px 18px; display: block; color: inherit; }
a.metric-card:hover {
  text-decoration: none; transform: translateY(-1px);
  box-shadow: 0 18px 44px rgba(15,23,42,.08);
}
.metric-card span.metric-label {
  display: block; font-size: 12.5px; line-height: 1.35; font-weight: 700;
  color: var(--muted-ink); letter-spacing: -.005em;
  margin-bottom: 12px;
}
.metric-value {
  font-family: var(--display); font-size: 2rem; font-weight: 800;
  line-height: 1; letter-spacing: -.04em;
  font-variant-numeric: tabular-nums;
}
.metric-foot { font-size: 12px; color: var(--muted-ink); margin-top: 6px; font-weight: 600; }
.metric-card--primary .metric-value { color: var(--primary-strong); }
.metric-card--danger  .metric-value { color: var(--danger); }
.metric-card--warning .metric-value { color: var(--warning); }
.metric-card--success .metric-value { color: var(--success); }

/* ---------------------------------------------------------------- buttons */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 12px 17px; min-height: 44px;
  border-radius: var(--radius-sm); border: 1px solid transparent;
  background: var(--surface); color: var(--text);
  font-family: inherit; font-weight: 800; font-size: 14px;
  line-height: 1.15; letter-spacing: -.01em;
  cursor: pointer; text-align: center; white-space: nowrap;
  transition: transform .18s ease, border-color .18s ease,
              box-shadow .18s ease, background .18s ease, color .18s ease;
}
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); text-decoration: none; }
/* A physical push on press, and no 300ms double-tap-zoom delay on a phone. */
.btn:active { transform: translateY(0) scale(.985); }

/* In-flight state. The label stays; a spinner joins it, so the button still
   says what it does while it is doing it. */
.btn.is-busy { position: relative; padding-left: 40px; }
.btn.is-busy::before {
  content: ""; position: absolute; left: 16px; top: 50%;
  width: 14px; height: 14px; margin-top: -7px;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; opacity: .8;
  animation: btn-spin .7s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
.btn svg { width: 17px; height: 17px; }
.btn:disabled, .btn[disabled] { opacity: .62; cursor: not-allowed; transform: none; box-shadow: none; }

.btn-primary {
  background: linear-gradient(135deg, var(--btn-from), var(--btn-to));
  color: var(--on-primary); box-shadow: 0 14px 30px rgba(142,43,163,.22);
}
.btn-primary:hover { color: var(--on-primary); box-shadow: 0 18px 38px rgba(142,43,163,.28); }
.btn-dark {
  background: linear-gradient(135deg, #1e293b, var(--charcoal));
  color: #fff; box-shadow: 0 14px 30px rgba(15,23,42,.2);
}
.btn-dark:hover { color: #fff; }
.btn-secondary { border-color: var(--border-strong); background: var(--surface); color: var(--text); }
.btn-secondary:hover { background: var(--primary-soft); border-color: var(--primary-line); color: var(--primary-strong); }
.btn-ghost { border-color: var(--border); background: var(--surface-2); color: var(--muted-ink); }
.btn-ghost:hover { color: var(--text); }
.btn-danger { border-color: var(--danger-line); background: var(--surface); color: var(--danger); }
.btn-danger:hover { background: var(--danger-soft); }
.btn-sm { padding: 10px 13px; font-size: 13px; min-height: 44px; border-radius: var(--radius-sm); }
.btn-block { width: 100%; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; }

/* ------------------------------------------------------------------ forms */

/* ``align-items: start`` is load-bearing. A grid row stretches its children to
   the tallest of them by default, so a field sitting beside one that carries
   help text grew its own input from 44px to 56px and pushed it 12px down the
   row -- every pair where only one side was annotated came out visibly
   crooked. The field's own ``align-content`` does the same job one level down,
   for the label / control / help stack inside it. */
.form-grid {
  display: grid; gap: 16px; grid-template-columns: repeat(12, 1fr);
  align-items: start;
}
.f-12 { grid-column: span 12; }
.f-8, .f-6, .f-4 { grid-column: span 12; }
.f-3 { grid-column: span 6; }

.field { display: grid; gap: 7px; min-width: 0; align-content: start; }

/* A button that belongs to the row of fields beside it rather than to a card
   foot. ``align-self`` rather than a flex container: with the grid no longer
   stretching its children (see .form-grid) there is nothing for an inner
   ``align-items: flex-end`` to push against, and the button drifted to the top
   of the row. This pins it to the bottom of whatever the tallest field is. */
.field-action { display: flex; align-items: flex-end; align-self: end; }
.field-action .btn { min-height: 44px; }

/* A value the form shows but does not let anybody set -- the report revision,
   which is raised automatically. Sized like an input so it sits on the same
   baseline as the fields beside it, but plainly not one: no border, no
   surface, and it does not take focus. */
.static-value {
  min-height: 44px; padding: 12px 0 0; font-size: 14px; font-weight: 700;
  color: var(--text);
}
.static-value .help { font-weight: 500; }
.label { font-size: 12.5px; font-weight: 800; color: var(--text); letter-spacing: -.01em; }
.label .req { color: var(--danger); margin-left: 2px; }
.help { font-size: 12px; color: var(--muted-ink); line-height: 1.45; }

input, select, textarea {
  width: 100%; font: inherit; font-size: 14px;
  padding: 13px 14px; min-height: 44px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius-md);
  transition: border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
/* --muted-2 is 2.56:1 on white, which fails AA for placeholder text. A
   placeholder carries the example value that tells someone what shape the
   field wants, so it has to be readable, not a ghost. */
input::placeholder, textarea::placeholder { color: var(--muted-ink); opacity: 1; }
input:focus, select:focus, textarea:focus { border-color: var(--focus-edge); background: var(--surface); }
textarea { min-height: 104px; resize: vertical; line-height: 1.5; }
select {
  appearance: none; padding-right: 38px; background-color: var(--surface);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2364748b'%3E%3Cpath d='M4.5 6.5L8 10l3.5-3.5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center; background-size: 16px;
}
input[type="file"] { padding: 9px; }
input[type="checkbox"], input[type="radio"] {
  width: 19px; height: 19px; min-height: 0; padding: 0; flex: 0 0 auto;
  accent-color: var(--primary); cursor: pointer; margin: 0;
}

.check {
  display: flex; align-items: flex-start; gap: 10px;
  font-size: 13px; cursor: pointer; padding: 6px 0; font-weight: 600;
}
fieldset { border: 0; padding: 0; margin: 0 0 22px; }
fieldset > legend {
  font-size: 12.5px; font-weight: 700; letter-spacing: -.005em; color: var(--muted-ink); padding: 0 0 12px;
}
fieldset:disabled input, fieldset:disabled select, fieldset:disabled textarea { background: var(--surface-2); }

.matrix {
  display: grid; gap: 2px 18px; grid-template-columns: 1fr;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 14px 16px; background: var(--surface);
}
.matrix .check { border-bottom: 1px solid var(--border); padding: 9px 0; }

.repeat-row {
  display: grid; gap: 8px; grid-template-columns: 1fr; align-items: start;
  padding: 12px; border: 1px solid var(--border);
  border-radius: var(--radius-md); margin-bottom: 8px; background: var(--surface-2);
}

/* A party is four fields plus a header strip, so it gets a card of its own
   rather than a row in a grid: at four columns on a phone every field would be
   too narrow to read the value inside it. */
.party-row {
  padding: 14px 16px 4px; border: 1px solid var(--border);
  border-radius: var(--radius-md); margin-bottom: 10px; background: var(--surface-2);
}
.party-row-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px;
}
.party-index {
  font-size: 12.5px; font-weight: 700; letter-spacing: -.005em; color: var(--muted-ink);
}
.party-row .form-grid { row-gap: 10px; }

/* The read-only view of the same parties, on the project page. The mark box is
   a fixed size whatever the logo's shape, so a tall square and a wide
   horizontal lockup leave the text starting on the same line. */
.party-list { display: grid; gap: 10px; }
.party-card {
  display: grid; gap: 12px; align-items: center;
  grid-template-columns: 64px 1fr;
  padding: 14px 16px; border: 1px solid var(--border);
  border-radius: var(--radius-md); background: var(--surface);
}
.party-card-mark {
  width: 64px; height: 42px; display: grid; place-items: center;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2); overflow: hidden;
}
.party-card-mark img { max-width: 56px; max-height: 34px; object-fit: contain; }
.party-card-initials { font-size: 13px; font-weight: 800; color: var(--muted-ink); }
.party-card-role {
  font-size: 12.5px; font-weight: 700; letter-spacing: -.005em; color: var(--muted-ink);
  display: flex; align-items: center; gap: 8px; margin-bottom: 3px;
}
.party-card-body strong { font-size: 15px; display: block; }
.party-card-actions { grid-column: 1 / -1; display: flex; gap: 8px; flex-wrap: wrap; }
@media (min-width: 720px) {
  .party-card { grid-template-columns: 64px 1fr auto; }
  .party-card-actions { grid-column: auto; justify-content: flex-end; }
}

/* Visually hidden but still focusable and still clickable by its label --
   which is how the file input behind "Add logo" works. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* User-defined report rows. Label and value stack on a phone and sit side by
   side from 640px, with the remove control always reachable. */
.custom-rows { display: grid; gap: 10px; }
.custom-row {
  display: grid; gap: 10px; grid-template-columns: 1fr auto; align-items: end;
  padding: 12px; border: 1px solid var(--border);
  border-radius: var(--radius-md); background: var(--surface-2);
}
.custom-row .field { grid-column: 1; }
.custom-row-remove {
  grid-column: 2; grid-row: 1 / -1;
  min-width: 44px; padding: 0; font-size: 20px; align-self: stretch;
}
.sr-label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;
}

/* Company logo. The preview sits on a chequerboard so a transparent PNG is
   obviously transparent, which is what determines whether it prints cleanly
   over the report header's tinted fill. */
.logo-row { display: grid; gap: 18px; grid-template-columns: 1fr; align-items: start; }
.logo-preview {
  display: grid; place-items: center; min-height: 120px; padding: 18px;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background-color: var(--surface);
  background-image:
    linear-gradient(45deg, var(--surface-3) 25%, transparent 25%),
    linear-gradient(-45deg, var(--surface-3) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--surface-3) 75%),
    linear-gradient(-45deg, transparent 75%, var(--surface-3) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}
.logo-preview img { max-width: 100%; max-height: 88px; height: auto; display: block; }
.logo-empty {
  font-family: var(--display); font-size: 1.05rem; font-weight: 800;
  letter-spacing: -.03em; color: var(--muted-ink); text-align: center;
}

.filters { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.filters .field { flex: 1 1 180px; }

/* ----------------------------------------------------------------- tables */

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { width: 100%; border-collapse: separate; border-spacing: 0; }
/* ``baseline``, not ``top``. A row holds plain text beside a 44px button and a
   pill, and with top alignment the text sat at the top of the row while the
   button's label sat 15px lower -- "TRIPC-IR-TECH-001" visibly above
   "Continue" on the dashboard. Baseline lines the first line of every cell up
   with the button's label, and still keeps a two-line cell (a title over its
   project name) reading from the top rather than floating to the middle. */
th, td {
  padding: 14px 18px; text-align: left; vertical-align: baseline; border-top: 1px solid #edf2f7;
  /* A long unbroken token -- a 40-character client document number, a pasted
     drawing reference, a concatenated tag -- otherwise sets its column's
     minimum width and dictates the layout of the whole table. Measured at
     1425px with one such value: the Number column took 332px, Project
     collapsed to 148, Location to 113, the row grew from 76px to 452px tall,
     and 65px of table -- the action column -- went behind a scrollbar. */
  overflow-wrap: anywhere;
}
/* The row's action must stay reachable even when the table does scroll.

   The shadow is the cue that there is more table behind the pinned column, so
   it is only drawn when there is: app.js sets ``data-overflow`` on the wrapper
   when the table is actually wider than it. It used to be unconditional, which
   drew a grey rule down the left of every action column on the dashboard --
   including tables that fitted with room to spare and had nothing behind them.

   The pinned cell also follows the row hover. It carries its own background so
   scrolled content cannot show through it, and that background used to stay
   white while the rest of the row turned grey, leaving the button in a box. */
/* The header is pinned only when its column's cells are: a pinned header over
   an ordinary last column sat on top of the column before it once the table
   scrolled (the manual-entry grid showed it). */
.table-wrap table td.nowrap:last-child,
.table-wrap table:has(td.nowrap:last-child) th:last-child {
  position: sticky; right: 0; background: var(--surface);
}
.table-wrap[data-overflow] table td.nowrap:last-child,
.table-wrap[data-overflow] table:has(td.nowrap:last-child) th:last-child {
  box-shadow: -12px 0 12px -12px rgba(15,23,42,.18);
}
.table-wrap table tbody tr:hover td.nowrap:last-child { background: var(--surface-2); }
.table-wrap table thead th:last-child { background: var(--surface-2); }

/* ------------------------------------------- a table whose last column acts
   Opt-in, on the dashboard's list cards. Two things were making the row
   buttons look staggered from one card to the next.

   *The action column floated.* It was sized like any other column, so where it
   started depended on how long the titles and client names in the other
   columns were, and its buttons sat at its left edge. "Continue", "Open" and
   "Inspect" each ended at a different x-position, and none of them lined up
   with the "All open" / "View all" button in their own card's header. Here the
   column shrinks to its buttons (``width: 1%`` with ``nowrap`` is how a table
   column says "as narrow as the content allows") and they sit flush right.

   *The table's outer edge was not the card's.* A card head pads its content
   26px from the card edge, 20px below 1024px; a table cell pads 18px. So even
   right-aligned, the row buttons ended 8px past the header button above them,
   and the first column started 8px left of the section title. The outermost
   cells take the card head's padding, so a card's title, its first column, its
   header action and its row actions all share two vertical lines. */
/* ``td:first-of-type``, not ``:first-child``: the Settings user list wraps each
   row's cells in a <form>, which the parser leaves behind as an empty first
   child of the row, so the name cell is not the row's first child. */
.table--actions th:first-child, .table--actions td:first-of-type { padding-left: 26px; }
.table--actions th:last-child, .table--actions td:last-child {
  padding-right: 26px; width: 1%; white-space: nowrap; text-align: right;
}
/* Two actions in one row sit together at the right, with the same gap every
   button group in the product uses -- rather than whatever width the
   whitespace between two inline links happened to render at. */
.row-actions { display: inline-flex; gap: 8px; justify-content: flex-end; }
/* The header action stays on the right when the header wraps. A long title
   ("Pick up where you left off") no longer fits beside its button on a phone,
   the row wraps, and the button started the second line -- at the left, on the
   opposite side of the card from every row button beneath it. The spacer takes
   the free space while they share a line, so this auto margin only does
   anything once the button is on a line of its own. */
.table-card:has(.table--actions) > .card-head .spacer + .btn { margin-left: auto; }
/* A status pill never breaks. Cells allow a break anywhere, so that one long
   unbroken reference cannot take over a table (see ``th, td`` above), and pills
   inherited it: a long inspection title claimed the width, the table squeezed
   the Status column to almost nothing, and "In Progress" came out as
   "In / Progre / ss" on three lines. With the pill held to one line the Status
   column cannot shrink below it, and the title -- which can wrap sensibly --
   gives way instead. */
.table--actions .pill { white-space: nowrap; }

/* On a phone, a ``table-cards`` table stops being a table: each row becomes a
   stacked card with the actions across its foot (see the 760px block below).
   The rules above are about lining up a real table's last *column*, and they
   outrank the card rules, so without this a card's action cell would be 1%
   wide with its buttons squeezed against the right. Here the card wins back
   its own layout, and a pair of actions shares the card's full width. */
@media (max-width: 760px) {
  .table-cards.table--actions td { width: 100%; }
  .table-cards.table--actions td:first-of-type { padding-left: 14px; }
  .table-cards.table--actions td:last-child { padding-right: 14px; text-align: left; }
  .table-cards .row-actions { display: flex; flex: 1 1 auto; width: 100%; }
}
@media (max-width: 1024px) {
  .table--actions th:first-child, .table--actions td:first-of-type { padding-left: 20px; }
  .table--actions th:last-child, .table--actions td:last-child { padding-right: 20px; }
}
thead th {
  border-top: 0; background: var(--surface-2); color: var(--muted-ink);
  font-size: 12.5px; font-weight: 700;
  letter-spacing: .08em; white-space: nowrap;
}
tbody tr:hover { background: var(--surface-2); }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
.ref { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 13px;
       white-space: nowrap; font-weight: 700; letter-spacing: -.01em; }

/* ------------------------------------------------------------------ pills */

.pill {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 7px 11px; border-radius: 999px;
  font-size: 12px; font-weight: 800; line-height: 1;
  border: 1px solid transparent;
  background: var(--neutral-soft); color: var(--muted-ink); border-color: var(--neutral-line);
}
.pill-info    { background: var(--info-soft);    color: var(--info);    border-color: var(--info-line); }
.pill-success { background: var(--success-soft); color: var(--success); border-color: var(--success-line); }
.pill-warning { background: var(--warning-soft); color: var(--warning); border-color: var(--warning-line); }
.pill-danger  { background: var(--danger-soft);  color: var(--danger);  border-color: var(--danger-line); }
.pill-primary { background: var(--primary-soft); color: var(--primary-strong); border-color: var(--primary-line); }
.pill-dot::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

/* ----------------------------------------------------------------- alerts */

.flashes { display: grid; gap: 10px; }
.flash {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 14px 16px; border-radius: var(--radius-md);
  font-size: 14px; font-weight: 600; border: 1px solid transparent;
}
.flash-success { background: var(--success-soft); color: #14532d; border-color: var(--success-line); }
.flash-error   { background: var(--danger-soft);  color: #7f1d1d; border-color: var(--danger-line); }
.flash-info    { background: var(--info-soft);    color: #1e3a8a; border-color: var(--info-line); }

.banner {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 14px 16px; border-radius: var(--radius-md);
  font-size: 13px; font-weight: 600;
  background: var(--warning-soft); color: #78350f; border: 1px solid var(--warning-line);
}
.banner code { font-family: var(--mono); font-size: 12.5px; }

/* --------------------------------------------------------------- sections */

.section-nav {
  display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px;
  -webkit-overflow-scrolling: touch;
  /* Hide the scrollbar's own affordance on platforms that draw one inline,
     since the fade below is the cue and two cues read as clutter. */
  scrollbar-width: none;
}
.section-nav::-webkit-scrollbar { display: none; }

/* The fade that says "there is more this way". Applied by app.js only on the
   side that actually has tabs hidden behind it, so it never lies. With no
   JavaScript the strip still scrolls -- it simply loses the hint. */
.section-nav[data-overflow] {
  -webkit-mask-image: var(--section-nav-mask);
          mask-image: var(--section-nav-mask);
}
.section-nav[data-overflow="end"] {
  --section-nav-mask: linear-gradient(to right, #000 calc(100% - 44px), transparent 100%);
}
.section-nav[data-overflow="start"] {
  --section-nav-mask: linear-gradient(to right, transparent 0, #000 44px);
}
.section-nav[data-overflow="both"] {
  --section-nav-mask: linear-gradient(to right, transparent 0, #000 44px,
                                      #000 calc(100% - 44px), transparent 100%);
}
.section-nav a {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 16px; border-radius: 999px; font-size: 13px; font-weight: 800;
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
  white-space: nowrap;
}
.section-nav a:hover {
  background: var(--primary-soft); border-color: var(--primary-line);
  color: var(--primary-strong); text-decoration: none;
}
.scroll-target { scroll-margin-top: 18px; }

/* The design system's small uppercase label, as a standalone heading rather
   than baked into one component. Used above the two party blocks on a request
   for inspection, where a full <h3> would compete with the card's own title. */
.section-label {
  font-size: 12.5px; font-weight: 700; letter-spacing: -.005em; color: var(--muted-ink);
  margin: 0 0 12px;
}

/* Two related blocks side by side above 720px, stacked below it. A phone is
   not wide enough to read two label/value lists in parallel -- squeezing them
   into two columns wraps every value onto three lines. */
.split-2 { display: grid; gap: 22px; grid-template-columns: 1fr; }
@media (min-width: 720px) {
  .split-2 { grid-template-columns: 1fr 1fr; gap: 28px; }
}

.kv { display: grid; grid-template-columns: minmax(120px, 34%) 1fr; gap: 8px 18px; font-size: 14px; }
.kv dt { color: var(--muted-ink); font-size: 12.5px; font-weight: 700; }
.kv dd { margin: 0; overflow-wrap: anywhere; font-weight: 500; }

.empty-state { text-align: center; padding: 44px 24px; color: var(--muted-ink); }
/* An empty state that sits inside a card already has the card's border and
   shadow round it, so it drops its own and tightens up. */
.empty-state.empty-state--inset { border: 0; box-shadow: none; padding: 28px 20px; }
.empty-state h3 { color: var(--text); margin-bottom: 8px; }
.empty-state p { font-size: 14px; max-width: 46ch; margin-inline: auto; }
.empty-icon {
  width: 52px; height: 52px; margin: 0 auto 14px;
  border-radius: var(--radius-md); display: grid; place-items: center;
  background: var(--primary-soft); color: var(--primary-strong);
}

/* ----------------------------------------------------------------- photos */

/* ------------------------------------------------- how wide a card gets ---
   `auto-fill` fits as many columns of at least the floor as it can, so a wider
   window means *more, narrower* cards, not bigger ones -- the column width
   converges on the floor exactly as the screen gets larger. That is the right
   behaviour for a gallery and it is also why this section used to break the
   wrong way round: maximised, the cards sat at their 248px minimum and the
   last button in each one was cut off by `.photo-card`'s `overflow: hidden`,
   while the same page at half-screen width had 300px cards and looked fine.

   Two things fix that, and both are here rather than on the grid alone,
   because a floor tuned to today's buttons breaks again the day a button is
   added. The floors below are set so the normal case is comfortable, and the
   card's own contents (further down) are made able to wrap and shrink so no
   floor is load-bearing.

   `min(100%, …)` on each floor so a container narrower than the floor cannot
   force a column wider than itself, which is what turns a tight card into a
   horizontally scrolling page. */

/* A gallery of thumbnails packs tightly. 148px is plenty for a picture with a
   line of text under it. */
.photo-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 148px), 1fr));
}

/* A grid whose cards each carry an editing form does not. Each one holds a
   description, a section, a "linked to finding" picker and a row of buttons,
   and at 148px the picker showed about three characters of a finding reference
   while the buttons wrapped onto three rows. The form decides the column
   width, not the thumbnail.

   264px rather than 248: the button row needs 198px of the card's 224px of
   inner width, which was 26px of headroom for every future label and every
   font that renders a touch wider than Inter. */
.photo-grid--editable { grid-template-columns: repeat(auto-fill, minmax(min(100%, 264px), 1fr)); }
.photo-body .label {
  font-size: 12.5px; font-weight: 700; letter-spacing: .06em;
  color: var(--muted-ink); margin-bottom: -3px;
}
/* ``margin: 0`` is load-bearing, and its absence was the real cause of the
   "buttons cut off at full screen" report.

   A photo card is a <figure>, and browsers give <figure> a 40px margin on each
   side. The templates cancelled it with an inline ``style="margin:0"`` -- which
   the Content-Security-Policy (``style-src 'self'``) silently discards. So in
   production every card was 80px narrower than its grid column, centred with a
   gap either side. At full screen the columns sit near their floor, the cards
   shrank below the ~222px the action row needs, and ``overflow: hidden`` cut the
   last button off. At half screen the columns were wide enough to absorb it.

   Local renders never showed it, because they are not served with the CSP.
   Owning the margin here, in the component, is what makes it true everywhere. */
.photo-card {
  margin: 0;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface); overflow: hidden; display: flex; flex-direction: column;
  box-shadow: var(--shadow-sm);
}
/* A card in the photo picker is a <label> round a checkbox: the whole card is
   the target, so it says so. */
.photo-card--pick { cursor: pointer; }
/* The editing form inside a project-library card. */
.photo-form { display: grid; gap: 8px; min-width: 0; }
.photo-thumb {
  position: relative; aspect-ratio: 4 / 3; background: var(--surface-3);
  display: block; overflow: hidden;
}
.photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-seq {
  position: absolute; top: 8px; left: 8px;
  background: rgba(15,23,42,.86); color: #fff;
  font-size: 12.5px; font-weight: 700; padding: 3px 7px; border-radius: 7px;
}
.photo-tag {
  position: absolute; top: 8px; right: 8px;
  background: var(--btn-from); color: var(--on-primary);
  font-size: 10.5px; font-weight: 800; padding: 3px 7px; border-radius: 7px;
}
/* `grid-template-columns: minmax(0, 1fr)` rather than the implicit `auto`.
   An auto-sized grid column is at least the min-content width of what is in
   it, and a row of `white-space: nowrap` buttons has a large one -- so the
   column grew past the card, and the card's `overflow: hidden` quietly cut off
   whatever stuck out. Pinning the column to the card's width makes the
   contents fit the card instead of the card failing to show the contents. */
.photo-body {
  padding: 12px; display: grid; gap: 8px;
  grid-template-columns: minmax(0, 1fr); min-width: 0;
}
.photo-body input, .photo-body select, .photo-body textarea {
  font-size: 13px; padding: 9px 11px; min-height: 38px; border-radius: var(--radius-sm);
  min-width: 0; max-width: 100%;
}
.photo-body textarea { min-height: 58px; line-height: 1.4; }

/* The action row wraps. Buttons carry `white-space: nowrap` and so refuse to
   shrink below their labels, which means a row that does not fit does not
   compress -- it overflows, and gets clipped. Wrapping turns "one button is
   invisible" into "the buttons take two lines", which is the right trade on a
   control the whole card exists to offer. `.photo-actions-end` keeps the icon
   buttons together so they wrap as a group and stay right-aligned rather than
   breaking up mid-row. */
.photo-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.photo-actions-end { display: flex; gap: 6px; align-items: center; margin-left: auto; }

/* Captions are typed by an inspector and are routinely a filename off a
   camera, which is one unbreakable 40-character token. Without this the token
   sets the card's min-content width, the card cannot grow, and the text is
   clipped mid-word. */
.photo-meta {
  font-size: 11.5px; color: var(--muted-ink); font-weight: 600;
  min-width: 0; overflow-wrap: anywhere;
}
/* The checkbox row in the photo picker. Same story: the caption beside the box
   is a flex item that has to be allowed to wrap. */
.photo-body .check { min-width: 0; }
.photo-body .check > span { min-width: 0; overflow-wrap: anywhere; }

/* The bar above the photo picker: a heading, a select-all, and the button that
   pulls the ticked photographs into the inspection. Was inline styles on the
   element; it is a named row now because it has real sizing rules rather than
   a flex declaration.

   `margin-left: auto` pushes the controls to the right of the heading and
   `flex-wrap` lets them drop below it instead of running past the card. The
   heading is allowed to shrink so that it, rather than the button, is what
   gives way -- the button is the point of the row. */
.photo-picker-bar {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 10px 12px; margin-bottom: 12px;
}
.photo-picker-bar > strong { font-size: 14px; min-width: 0; }
.photo-picker-bar > .check { margin-left: auto; min-width: 0; }
.photo-picker-bar .btn { flex: 0 0 auto; }

.dropzone {
  border: 2px dashed var(--border-strong); border-radius: var(--radius-lg);
  padding: 28px 20px; text-align: center; background: var(--surface-2);
  transition: border-color .18s ease, background .18s ease;
}
.dropzone.is-over { border-color: var(--primary); background: var(--primary-soft); }
.dropzone-hint { font-size: 13px; color: var(--muted); margin-top: 12px; }

.upload-queue { margin-top: 14px; display: grid; gap: 8px; }
.upload-item {
  display: flex; align-items: center; gap: 12px;
  font-size: 13px; font-weight: 600; padding: 10px 14px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.upload-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload-state { font-weight: 800; font-size: 12px; }
.upload-item.is-done .upload-state { color: var(--success); }
.upload-item.is-error .upload-state { color: var(--danger); }
.progress { height: 5px; width: 78px; flex: 0 0 auto; background: var(--border); border-radius: 999px; overflow: hidden; }
.progress-bar { height: 100%; width: 0; background: var(--primary); transition: width .18s ease; }

/* --------------------------------------------------------------- findings */

.finding {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  margin-bottom: 12px; overflow: hidden; background: var(--surface);
  box-shadow: var(--shadow-sm);
}
.finding-head {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 14px 16px; background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}
.finding-ref { font-family: var(--mono); font-size: 12px; color: var(--muted-ink); font-weight: 700; }
.finding-title { font-weight: 800; font-size: 14px; flex: 1; min-width: 0; overflow-wrap: anywhere; letter-spacing: -.01em; }
.finding-body { padding: 16px; }
.finding summary { padding: 11px 16px; cursor: pointer; font-size: 13px; font-weight: 700; color: var(--muted); }
.finding summary:hover { color: var(--text); }
/* --------------------------------------------------- first-run walkthrough */

/* Styled to work both as an inline block (no scripting: the dialog carries
   `open`, so all four steps are simply on the page) and as a centred modal
   once the script calls showModal(). */
.tour {
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg); background: var(--surface);
  color: var(--text); padding: 0; width: min(560px, 100%);
  margin: 0 0 18px;
}
.tour[open] { display: block; }
.tour::backdrop { background: var(--scrim); }
@supports (backdrop-filter: blur(3px)) {
  .tour::backdrop { backdrop-filter: blur(3px); }
}
.tour-inner { display: grid; gap: 20px; padding: 26px; }
.tour-head { display: grid; gap: 8px; }
.tour-head h2 { font-size: 1.4rem; }
.tour-head p { color: var(--muted); font-size: 14px; margin: 0; }

.tour-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 18px; }
.tour-step { display: flex; gap: 14px; align-items: flex-start; }
.tour-num {
  width: 30px; height: 30px; flex: 0 0 auto; border-radius: 10px;
  background: var(--primary-soft); color: var(--primary-strong);
  display: grid; place-items: center; font-size: 13px; font-weight: 800;
}
.tour-step h3 { font-size: 1rem; margin-bottom: 4px; }
.tour-step p { color: var(--muted); font-size: 13.5px; margin: 0 0 8px; }
.tour-link { font-size: 13px; font-weight: 700; }

.tour-foot {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding-top: 18px; border-top: 1px solid var(--border);
}
.tour-spacer { flex: 1; }
.tour-progress {
  font-size: 12px; font-weight: 700; color: var(--muted-ink);
  font-variant-numeric: tabular-nums;
}

/* The phone rules for the walkthrough live in the single 640px block at the
   end of this file, not in a second one here: one breakpoint, one place. */

/* ------------------------------------------------------- concerns register */

.concern-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.concern {
  border: 1px solid var(--border); border-left: 3px solid var(--warning);
  border-radius: var(--radius-md); background: var(--surface); padding: 14px 16px;
}
.concern.is-overdue { border-left-color: var(--danger); background: var(--danger-soft); }
.concern.is-closed  { border-left-color: var(--success); }
.concern-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.concern-title { font-weight: 800; font-size: 14px; letter-spacing: -.01em; flex: 1 1 220px; min-width: 0; }
.concern-age { font-size: 12px; color: var(--muted-ink); font-weight: 700; font-variant-numeric: tabular-nums; }
.concern-kv { margin-top: 12px; font-size: 13.5px; }
.concern-closeout { margin-top: 12px; }
.concern-closeout > summary { display: inline-flex; list-style: none; cursor: pointer; }
.concern-closeout > summary::-webkit-details-marker { display: none; }

.finding--adverse     { border-left: 3px solid var(--danger); }
.finding--observation { border-left: 3px solid var(--info); }
.finding--pass        { border-left: 3px solid var(--success); }

/* ------------------------------------------------------------------- auth */

.auth-shell { min-height: 100dvh; display: grid; place-items: center; padding: 32px 20px; background: var(--app-bg); }
.auth-grid {
  width: min(var(--content), 100%);
  display: grid; grid-template-columns: 1fr; gap: 24px; align-items: stretch;
}
.auth-card, .auth-showcase {
  border-radius: var(--radius-lg); box-shadow: var(--shadow-md);
  border: 1px solid var(--border);
}
.auth-card { background: var(--surface); padding: 34px; }
.auth-card h1 { font-size: clamp(1.7rem, 2.6vw, 2.2rem); margin-bottom: 8px; }
.auth-card .lead { color: var(--muted); font-size: 14.5px; margin-bottom: 26px; }
.auth-showcase {
  background: linear-gradient(135deg, #0f172a, #0f3f3a);
  color: #fff; padding: 34px; border-color: transparent;
  box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column; gap: 22px;
}
.auth-showcase h2, .auth-showcase h3 { color: #fff; }
.auth-showcase .eyebrow { color: #e6b3f2; }
.auth-showcase p { color: rgba(255,255,255,.78); font-size: 14px; }
.auth-steps { display: grid; gap: 14px; margin-top: auto; }
.auth-step { display: flex; gap: 13px; align-items: flex-start; }
.auth-step-num {
  width: 30px; height: 30px; flex: 0 0 auto; border-radius: 10px;
  background: rgba(213,139,232,.16); color: #e6b3f2;
  display: grid; place-items: center; font-size: 12.5px; font-weight: 800;
}
.auth-step strong { display: block; font-size: 14px; font-weight: 800; margin-bottom: 2px; }
.auth-step span { font-size: 13px; color: rgba(255,255,255,.7); }
.auth-alt { margin-top: 22px; text-align: center; font-size: 13.5px; color: var(--muted); font-weight: 600; }

/* The status code on an error page: present for orientation, but not the
   loudest thing on it. The headline is what the reader needs. */
.error-code {
  font-family: var(--mono); font-size: 12px; font-weight: 700;
  letter-spacing: .14em; color: var(--muted-ink); margin: 0 0 6px;
}

/* Password recovery pages carry no showcase panel — one card, centred. The
   doubled class outranks the two-column rule in the 960px breakpoint below
   without depending on source order. */
.auth-grid.auth-grid--single { width: min(520px, 100%); grid-template-columns: 1fr; }
.label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.label-link { font-size: 12.5px; font-weight: 700; color: var(--primary-strong); }

/* ------------------------------------------------------------- phone shell */

/*
  The tab bar only exists below 1024px (see the breakpoint block). Everything
  here describes how it looks when it is there; `display: none` by default
  keeps it out of the way of the desktop sidebar entirely rather than merely
  hiding it off-screen.
*/
.tabbar {
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  background: var(--surface);
  border-top: 1px solid var(--border);
  box-shadow: 0 -8px 28px rgba(15,23,42,.09);
  /* The home indicator on a modern phone overlaps the bottom edge. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
@supports (backdrop-filter: blur(12px)) {
  .tabbar { background: color-mix(in srgb, var(--surface) 86%, transparent); backdrop-filter: blur(12px); }
}
.tabbar .tab, .tabbar-slot {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; padding: 8px 4px 6px; min-height: 58px;
  font: inherit; font-size: 10.5px; font-weight: 800; letter-spacing: -.01em;
  color: var(--muted-ink); background: none; border: 0; cursor: pointer;
  text-decoration: none;
}
.tabbar .tab:hover { text-decoration: none; }
.tabbar .tab svg { width: 21px; height: 21px; opacity: .8; }
.tabbar .tab.is-active { color: var(--primary-strong); }
.tabbar .tab.is-active svg { opacity: 1; }

/* The raised slot: whatever this page is actually for. On a project or an
   inspection that is the camera, which is why it is the one control an
   inspector can hit without looking. */
.tabbar-slot { position: relative; }
.tabbar-fab {
  position: absolute; bottom: 10px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; width: 62px; height: 62px; border-radius: 22px; border: 0;
  background: linear-gradient(135deg, var(--btn-from), var(--btn-to));
  color: var(--on-primary); font: inherit; font-size: 9.5px; font-weight: 800;
  box-shadow: 0 10px 26px rgba(142,43,163,.4); cursor: pointer;
}
.tabbar-fab:hover { color: var(--on-primary); text-decoration: none; }
.tabbar-fab:active { transform: translateY(1px); }
.tabbar-fab svg { width: 22px; height: 22px; }

/* ---------------------------------------------------------------- capture */

.capture-row { display: grid; gap: 10px; grid-template-columns: 1fr; }
.btn-capture { cursor: pointer; min-height: 52px; font-size: 15px; }

/* ---------------------------------------------------- tables that collapse */

/*
  A nine-column table on a 390px screen is a horizontal scroll nobody performs.
  Below 760px these tables become one card per row, with each cell labelled by
  its column heading. Above it they stay real tables, which is what the data
  wants on a desk.
*/
@media (max-width: 760px) {
  .table-cards thead { display: none; }
  .table-cards, .table-cards tbody, .table-cards tr, .table-cards td { display: block; width: 100%; }
  /* ``width: auto``, overriding the 100% set on the whole family above. A
     block with side margins fills what is left once the margins are taken; a
     block that is 100% wide *and* has 14px margins is 28px wider than its
     container. Every phone card overhung the list by 13px, and the list's
     ``overflow: hidden`` cut off its right border and padding. */
  .table-cards tr {
    border: 1px solid var(--border); border-radius: var(--radius-md);
    margin: 0 14px 12px; padding: 4px 0; background: var(--surface);
    width: auto;
  }
  .table-cards tr:hover { background: var(--surface); }
  .table-cards td {
    border-top: 0; padding: 7px 14px;
    display: flex; align-items: baseline; gap: 12px;
  }
  .table-cards td::before {
    content: attr(data-label);
    flex: 0 0 34%; font-size: 12.5px; font-weight: 700; letter-spacing: .08em; color: var(--muted-ink);
  }
  /* The first cell is the row's identity: no label, and given prominence. */
  .table-cards td:first-child {
    padding-top: 12px; font-weight: 800; font-size: 14px;
  }
  .table-cards td:first-child::before { content: none; }
  .table-cards td.num { text-align: left; }
  .table-cards td:empty { display: none; }
  .table-cards td:last-child { padding-bottom: 12px; }
  .table-cards td:last-child::before { content: none; }
  .table-cards td:last-child .btn { width: 100%; }
  .table-wrap:has(.table-cards) { overflow-x: visible; }
}

/* ---------------------------------------------------------------- helpers */

.muted { color: var(--muted); }
.text-danger { color: var(--danger); }
.divider-text { display: flex; align-items: center; gap: 12px; margin: 18px auto; max-width: 420px; color: var(--muted); font-size: 12.5px; }
.divider-text::before, .divider-text::after { content: ""; flex: 1; border-top: 1px solid var(--border); }
tr.row-error td { background: var(--danger-soft); }
.logo-preview { max-height: 56px; max-width: 240px; object-fit: contain; background: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 6px; }
.small { font-size: 13px; }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
/* Where the characters genuinely have to be read one at a time. */
.mono-true { font-family: var(--mono-true); font-size: 12.5px; }
.nowrap { white-space: nowrap; }
.hidden { display: none !important; }
.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.divider { height: 1px; background: var(--border); margin: 20px 0; border: 0; }
.stack { display: grid; gap: 18px; }

/* ------------------------------------------------------------ breakpoints */

@media (min-width: 640px) {
  .f-8 { grid-column: span 8; }
  .f-6 { grid-column: span 6; }
  .f-4 { grid-column: span 4; }
  .f-3 { grid-column: span 3; }
  .matrix { grid-template-columns: repeat(2, 1fr); }
  .matrix .check { border-bottom: 0; }
  .repeat-row { grid-template-columns: 150px 1fr 96px 46px; align-items: center; }
  .logo-row { grid-template-columns: minmax(0, 260px) minmax(0, 1fr); gap: 24px; }
  .custom-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr) auto; align-items: end; }
  .custom-row .field:first-of-type { grid-column: 1; }
  .custom-row .field:last-of-type { grid-column: 2; }
  .custom-row-remove { grid-column: 3; grid-row: 1; align-self: end; height: 44px; }
}

@media (min-width: 960px) {
  .matrix { grid-template-columns: repeat(4, 1fr); }
  .auth-grid { grid-template-columns: minmax(0, .95fr) minmax(360px, .8fr); }
}

/* ------------------------------------------------- container-aware columns
   The breakpoints above measure the *window*, which is the wrong ruler for a
   form that sits inside a card inside a content column beside a 290px
   sidebar. At a 1100px window the card is only about 780px wide, and at
   1040px it is 720px -- but both are "min-width: 960px" as far as the window
   is concerned, so three-up fields and a four-column checklist were being
   asked to fit in half the space they were sized for. That is the half-screen
   layout the window queries could not see.

   So the spans follow the card. The window queries stay as the fallback for
   anything that cannot do container queries; where they can, these override.  */
@supports (container-type: inline-size) {
  .card, .table-card, .party-row, .repeat-row { container-type: inline-size; }

  .f-8, .f-6, .f-4 { grid-column: span 12; }
  .f-3 { grid-column: span 6; }
  .matrix { grid-template-columns: 1fr; }

  @container (min-width: 560px) {
    .f-8 { grid-column: span 8; }
    .f-6 { grid-column: span 6; }
    .f-4 { grid-column: span 4; }
    .f-3 { grid-column: span 3; }
  }
  @container (min-width: 460px) {
    .matrix { grid-template-columns: repeat(2, 1fr); }
    .matrix .check { border-bottom: 0; }
  }
  @container (min-width: 860px) {
    .matrix { grid-template-columns: repeat(3, 1fr); }
  }
  @container (min-width: 1080px) {
    .matrix { grid-template-columns: repeat(4, 1fr); }
  }

  /* Released items carry a revision, so they are five columns rather than the
     generic row's four. Declared after the window queries above so it wins
     over ``.repeat-row`` at equal specificity. */
  .repeat-row--released { grid-template-columns: 1fr; }
  @container (min-width: 560px) {
    .repeat-row--released {
      grid-template-columns: minmax(0, 150px) minmax(0, 1fr) minmax(0, 92px) minmax(0, 84px) 46px;
      align-items: end;
    }
  }
}

/* Attendance and signatory rows: company, person and designation carry equal
   weight, so they share the width equally -- unlike the generic row, which
   gives its first column 150px and its third 96px. This was an inline
   ``grid-template-columns`` that the CSP discarded, so production fell back to
   the generic split and put a designation in 96px. As a class it also gets to
   stack on a phone, which the inline version would have refused to do. */
@supports (container-type: inline-size) {
  .repeat-row.repeat-row--thirds { grid-template-columns: 1fr; }
  @container (min-width: 560px) {
    .repeat-row.repeat-row--thirds {
      grid-template-columns: repeat(3, minmax(0, 1fr)) 46px; align-items: center;
    }
  }
}
@supports not (container-type: inline-size) {
  @media (min-width: 640px) {
    .repeat-row.repeat-row--thirds {
      grid-template-columns: repeat(3, minmax(0, 1fr)) 46px; align-items: center;
    }
  }
}

/* The same five columns for anything without container queries. */
@media (min-width: 860px) {
  .repeat-row--released {
    grid-template-columns: minmax(0, 150px) minmax(0, 1fr) minmax(0, 92px) minmax(0, 84px) 46px;
    align-items: end;
  }
}

@media (min-width: 620px) {
  .capture-row { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 1024px) {
  .app-shell { grid-template-columns: 1fr; }
  .sidebar {
    position: fixed; left: 0; top: 0; bottom: 0;
    width: min(320px, 88vw); height: 100dvh;
    transform: translateX(-104%); transition: transform .22s ease;
    box-shadow: var(--shadow-lg);
    /* Scrolling the drawer to its end must not then scroll the page behind. */
    overscroll-behavior: contain;
  }
  .sidebar.is-open { transform: translateX(0); }
  .sidebar-close { display: flex; }
  /* The drawer and its backdrop sit above the tab bar (z-index 30): the tab
     bar showing through an open drawer reads as a rendering fault. */
  .sidebar { z-index: 32; }
  .backdrop { position: fixed; inset: 0; z-index: 31; background: var(--scrim); }
  .topbar { padding: 18px 18px 0; }
  .page-stack { padding: 16px 18px 40px; }
  .card { padding: 20px; }
  .card-head { margin: -20px -20px 16px; padding: 15px 20px; }
  .card-foot { margin: 18px -20px -20px; padding: 15px 20px; }

  /* The tab bar replaces the topbar hamburger below this width -- two ways to
     open the same drawer, one of them out of thumb reach, is one too many. */
  .tabbar { display: flex; align-items: stretch; }
  .app-nav-toggle { display: none; }
  .app-shell { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
  /* Anchor jumps and focus scrolls stop clear of the tab bar rather than
     landing underneath it. */
  html { scroll-padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px)); }

  /* Sections are how you move around a long inspection on a phone, so the
     switcher stays put while the page scrolls under it. */
  .section-nav {
    position: sticky; top: 0; z-index: 20;
    margin: 0 -18px; padding: 10px 18px;
    /* Was a hardcoded rgba(244,247,251,...), which in dark mode blended to a
       near-white band across an otherwise dark page. The token follows the
       palette. */
    background: var(--section-nav-bg);
    border-bottom: 1px solid var(--border);
  }
  @supports (backdrop-filter: blur(10px)) {
    .section-nav { background: var(--section-nav-bg-blur); backdrop-filter: blur(10px); }
  }
  .scroll-target { scroll-margin-top: 72px; }


  .card-foot .btn + .btn { margin-top: 2px; }
}

@media (max-width: 640px) {
  /*
    iOS Safari zooms the whole page whenever a focused input's text is under
    16px, and then leaves it zoomed. On a form this long that is the single
    most disruptive thing the interface can do to someone standing at a
    machine, so on a phone the fields are 16px whatever the desk layout uses.
  */
  input, select, textarea { font-size: 16px; padding: 13px 14px; }
  .photo-body input, .photo-body select, .photo-body textarea { font-size: 16px; }

  /* Two photographs across on a phone: big enough to judge, small enough to
     scan a shoot of thirty. */
  .photo-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  .topbar-actions { width: 100%; }
  .topbar-actions .btn { flex: 1 1 auto; }
  .dropzone { padding: 18px 14px; }

  .tour { width: 100%; }
  .tour-inner { padding: 20px; }
  .tour-foot .btn { flex: 1 1 auto; }

  /* The 44px floor applies to the controls inside a photograph card too --
     they were sized for a mouse. */
  .photo-body input, .photo-body select { min-height: 44px; }
  .photo-body textarea { min-height: 72px; }
  /* A checkbox row is one generous target, label included, with no dead
     zone between the box and its text. */
  .check { min-height: 44px; align-items: center; padding: 8px 0; }
}

@media print {
  .sidebar, .topbar-actions, .section-nav, .btn, .app-nav-toggle { display: none !important; }
  .app-shell { display: block; background: #fff; }
  .card { border: 1px solid #ccc; box-shadow: none; break-inside: avoid; }
}

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


/* ------------------------------------------------------------ brand marks */

/* Two independent axes, so they are resolved together rather than fighting.
   *Form*: full lockup on a normal screen, mark alone under 560px. *Colourway*:
   the wordmark's ink is #0F172A and vanishes on a dark ground, so both assets
   ship and CSS picks one -- filtering the dark variant would shift the teal.

   All four start hidden and exactly one is shown, which is longer than
   toggling but cannot produce the state where a breakpoint rule and a theme
   rule both claim `display` and the wrong pair wins. */
.brand-lockup, .brand-mark { display: none; }

.brand-lockup--light { display: block; }
@media (prefers-color-scheme: dark) {
  .brand-lockup--light { display: none; }
  .brand-lockup--dark  { display: block; }
}
@media (max-width: 560px) {
  .brand-lockup--light, .brand-lockup--dark { display: none; }
  .brand-mark--light { display: block; }
  .brand-subtitle { display: none; }
}
@media (max-width: 560px) and (prefers-color-scheme: dark) {
  .brand-mark--light { display: none; }
  .brand-mark--dark  { display: block; }
}

/* --------------------------------------------------------------- threads ---
   Support conversations. Two sides, so the customer can tell at a glance which
   messages are theirs and which are ours without reading the attribution. */
.thread { list-style: none; margin: 0 0 24px; padding: 0; display: grid; gap: 14px; }
.thread-msg {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 14px 16px; background: var(--surface);
}
.thread-msg--staff { background: var(--primary-soft); border-color: var(--primary-line); }
.thread-meta {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 8px; font-size: 13px;
}
.thread-body p { margin: 0 0 8px; font-size: 14.5px; line-height: 1.6; overflow-wrap: anywhere; }
.thread-body p:last-child { margin-bottom: 0; }

/* ------------------------------------------------------------ support panel
   Fabricata's own shell, deliberately unlike the customer application.

   An employee here is looking at other companies' accounts. If this looked
   like a workspace they would eventually forget which one they were in, so it
   does not: a dark bar across the top, the word Fabricata, and none of the
   workspace navigation. */
body.is-admin { background: var(--bg); }

.adminbar {
  background: var(--charcoal); color: #e8eef8;
  border-bottom: 3px solid var(--primary);
  position: sticky; top: 0; z-index: 40;
}
.adminbar-inner {
  width: min(var(--content), calc(100% - 40px)); margin-inline: auto;
  display: flex; align-items: center; gap: 22px; min-height: 62px; flex-wrap: wrap;
}
.adminbar-brand {
  display: inline-flex; align-items: center; gap: 10px;
  color: #e8eef8; font-size: 15px; letter-spacing: -.01em;
}
.adminbar-brand:hover { text-decoration: none; color: #ffffff; }
.adminbar-brand strong { font-family: var(--display); font-weight: 800; }
.adminbar-nav { display: flex; gap: 4px; margin-inline-end: auto; }
.adminbar-nav a {
  color: #a7b4c7; font-weight: 700; font-size: 14px;
  padding: 8px 14px; border-radius: var(--radius-sm);
}
.adminbar-nav a:hover { color: #ffffff; background: rgba(255,255,255,.08); text-decoration: none; }
.adminbar-nav a.active { color: #ffffff; background: rgba(213,139,232,.18); }
.adminbar-user {
  display: flex; align-items: center; gap: 12px;
  font-size: 13px; color: #a7b4c7;
}
.adminbar-user .btn-ghost {
  background: transparent; border-color: rgba(255,255,255,.22); color: #e8eef8;
}
.adminbar-user .btn-ghost:hover { background: rgba(255,255,255,.1); color: #ffffff; }

.admin-main {
  width: min(var(--content), calc(100% - 40px)); margin-inline: auto;
  padding: 26px 0 60px; display: grid; gap: 18px; align-content: start;
}
.admin-head { display: grid; gap: 4px; }

@media (max-width: 700px) {
  .adminbar-inner { padding-block: 10px; gap: 12px; }
  .adminbar-nav { order: 3; width: 100%; }
  .adminbar-user { font-size: 12px; }
  .adminbar-user span { display: none; }
}

/* ======================================================= no inline styles ===
   Everything below replaces a ``style="…"`` attribute that used to sit in a
   template.

   Those attributes never worked in production. The Content-Security-Policy in
   app/__init__.py is ``style-src 'self'``, which blocks inline style
   attributes as well as <style> elements, so the browser discarded all 126 of
   them. Development never showed it -- a page rendered locally or in a test is
   not served with the header -- and the product quietly looked slightly wrong
   everywhere instead: photo cards 80px too narrow, forms that were meant to sit
   inline stacked on top of each other, signatory rows in the wrong columns,
   spacing collapsed.

   The CSP is right and stays. Loosening it to ``'unsafe-inline'`` would reopen
   CSS injection to fix a spacing problem. So the styles live here, served from
   'self', and tests/e2e.py fails if a ``style=`` attribute reappears in a
   template.

   ---- components ------------------------------------------------------- */

/* The headline on the sign-in and sign-up showcase panel. */
.auth-showcase-title { font-size: 1.55rem; }

/* A small heading inside a card, above a sub-list. */
.subhead { font-size: .95rem; margin-bottom: 12px; }

/* A full-bleed band across the bottom of a card: the one-time reset link on
   the settings page, and the note under it. */
.card-band { padding: 14px 26px; border-top: 1px solid var(--border); }
.card-band--accent { padding: 18px 26px; background: var(--primary-soft); }

/* A recessed panel that opens beneath a toggle -- "Add a finding". */
.inset-panel {
  margin-top: 16px; padding: 20px;
  border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface-2);
}

/* ---- utilities ----------------------------------------------------------

   One job each, for the one-off adjustments a component class would be
   overkill for.

   ``!important`` on purpose, and only here. These stand in for inline styles,
   which beat every selector in the stylesheet; without it a utility would lose
   to any rule more specific than a single class -- ``.card-head h2``,
   ``.filters .field`` -- and quietly stop doing its job, which is the same
   failure this block exists to remove. It is the convention every utility
   library uses for the same reason.

   Order matters where two apply to one element: ``.m-0`` comes before the
   single-side margins so ``m-0 mt-8`` means "8px on top, nothing elsewhere". */

.m-0  { margin: 0 !important; }
.mt-4  { margin-top: 4px !important; }
.mt-6  { margin-top: 6px !important; }
.mt-8  { margin-top: 8px !important; }
.mt-10 { margin-top: 10px !important; }
.mt-12 { margin-top: 12px !important; }
.mt-14 { margin-top: 14px !important; }
.mt-16 { margin-top: 16px !important; }
.mt-18 { margin-top: 18px !important; }
.mb-0  { margin-bottom: 0 !important; }
.mb-8  { margin-bottom: 8px !important; }
.mb-10 { margin-bottom: 10px !important; }
.mb-12 { margin-bottom: 12px !important; }
.mb-16 { margin-bottom: 16px !important; }
.mb-18 { margin-bottom: 18px !important; }
.mb-20 { margin-bottom: 20px !important; }
.mb-24 { margin-bottom: 24px !important; }

.d-block       { display: block !important; }
.d-inline      { display: inline !important; }
.d-inline-flex { display: inline-flex !important; }

.flex-1      { flex: 1 !important; }
.flex-wrap   { flex-wrap: wrap !important; }
.flex-w-200  { flex: 0 1 200px !important; }
.flex-w-210  { flex: 0 1 210px !important; }
.flex-w-240  { flex: 0 1 240px !important; }
.gap-10      { gap: 10px !important; }
.gap-12      { gap: 12px !important; }
.self-center { align-self: center !important; }
.self-end    { align-self: end !important; }
.justify-center { justify-content: center !important; }

.w-100     { width: 100% !important; }
.min-w-0   { min-width: 0 !important; }
.maxw-220  { max-width: 220px !important; }
.maxw-240  { max-width: 240px !important; }
.maxw-860  { max-width: 860px !important; }

.text-center { text-align: center !important; }
.fs-12-5     { font-size: 12.5px !important; }
.fs-14       { font-size: 14px !important; }
.fw-600      { font-weight: 600 !important; }
.break-all   { word-break: break-all !important; }
.cursor-pointer { cursor: pointer !important; }

/* ==========================================================================
   Fabricata Track surfaces
   Everything above is the shared family system; everything below is Track's.
   ========================================================================== */

/* ---- progress bars (SVG, see the `bar` macro) ---------------------------- */
.pbar { display: block; overflow: visible; }
.pbar-track { fill: var(--neutral-line); }
.pbar-fill { fill: var(--primary); }
.pbar--ok .pbar-fill { fill: var(--success); }
.pbar--risk .pbar-fill { fill: var(--warning); }
.pbar--bad .pbar-fill { fill: var(--danger); }
.pbar--muted .pbar-fill { fill: var(--muted-2); }
@media (prefers-reduced-motion: no-preference) {
  .pbar-fill { transition: width .45s ease; }
}

/* ---- tab strip: the one level of navigation inside a project or settings - */
.tabs {
  display: flex; gap: 2px; overflow-x: auto; margin-top: 18px;
  border-bottom: 1px solid var(--border); scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tabs a {
  display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 14px;
  font-size: 14px; font-weight: 700; color: var(--muted-ink); white-space: nowrap;
  border-bottom: 3px solid transparent; margin-bottom: -1px;
}
.tabs a:hover { color: var(--text); text-decoration: none; }
.tabs a[aria-current="page"] { color: var(--primary-strong); border-bottom-color: var(--primary); }
.tabs a .count {
  font-size: 11.5px; font-weight: 700; color: var(--muted-ink); background: var(--neutral-soft);
  border: 1px solid var(--neutral-line); border-radius: 999px; padding: 0 7px; line-height: 18px;
}

/* ---- stat tiles ---------------------------------------------------------- */
.stats { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.stat {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 18px 20px; box-shadow: var(--shadow-sm); min-width: 0;
}
.stat-label { font-size: 12.5px; font-weight: 700; color: var(--muted-ink); }
.stat-value {
  font-family: var(--display); font-weight: 800; font-size: 28px; letter-spacing: -.03em;
  line-height: 1.1; margin-top: 6px; font-variant-numeric: tabular-nums;
}
.stat-sub { font-size: 13px; color: var(--muted-ink); margin-top: 4px; font-variant-numeric: tabular-nums; }
.stat .pbar { margin-top: 12px; }

.num { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.nowrap { white-space: nowrap; }
.check--plain { display: flex; gap: 10px; align-items: flex-start; font-size: 14px; line-height: 1.45; }
.check--plain input { margin-top: 3px; width: 18px; height: 18px; flex: 0 0 auto; }
.weekdays { display: flex; flex-wrap: wrap; gap: 8px; }
.weekdays .check { min-width: 120px; }
.link-box {
  font-family: var(--mono-true); font-size: 13px; word-break: break-all; padding: 12px 14px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.grid-2 { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); align-items: start; }
.row-details > summary { list-style: none; cursor: pointer; display: inline-flex; }
.row-details > summary::-webkit-details-marker { display: none; }
.row-details[open] { min-width: min(100%, 320px); }
.role-guide { display: grid; gap: 4px; color: var(--muted-ink); }
.auth-note { font-size: 14px; color: var(--muted-ink); }

/* ---- project screens -------------------------------------------------------- */
.kv > div { display: contents; }
.kv--cols { grid-template-columns: repeat(auto-fit, minmax(150px, auto) minmax(160px, 1fr)); }
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
.filter-bar .field { min-width: 180px; flex: 0 1 auto; }
.filter-bar .field:first-of-type { flex: 1 1 260px; }
.inline-form { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.inline-form input:not([type]), .inline-form input[type="text"] { min-width: 200px; flex: 1 1 220px; }
.row-link { color: var(--text); font-weight: 600; }
.progress-cell { min-width: 150px; }
.progress-cell .pbar { margin-bottom: 4px; }
.num-left { font-variant-numeric: tabular-nums; color: var(--muted-ink); }
.chip { display: inline-flex; gap: 6px; align-items: center; font-size: 12.5px; font-weight: 600; margin: 2px 6px 2px 0;
  padding: 2px 9px; border-radius: 999px; background: var(--neutral-soft); border: 1px solid var(--neutral-line); }
.field-error { display: block; color: var(--danger); font-size: 13px; font-weight: 600; margin-top: 4px; }
.has-error input, .has-error select, .has-error textarea { border-color: var(--danger); }
.card-head--toggle { cursor: pointer; list-style: none; padding-right: 58px; position: relative; }
.card-head--toggle::-webkit-details-marker { display: none; }
/* The open/closed chevron: a collapsed card must look like something to open,
   not like an empty card. */
.card-head--toggle::after {
  content: ""; position: absolute; right: 26px; top: 50%;
  width: 9px; height: 9px; margin-top: -7px;
  border-right: 2px solid var(--muted-ink); border-bottom: 2px solid var(--muted-ink);
  transform: rotate(45deg); transition: transform .18s ease;
}
details[open] > .card-head--toggle::after { transform: rotate(-135deg); margin-top: -2px; }
.card-head--toggle:hover::after { border-color: var(--primary); }
details.card:not([open]) > .card-head { margin-bottom: -26px; border-bottom: 0; border-radius: var(--radius-lg); }
.qty-input { width: 96px; }
.check-list { margin: 0; padding-left: 18px; display: grid; gap: 6px; font-size: 14px; }
.map-grid { display: grid; gap: 12px 16px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr)); }
.pager { display: flex; gap: 12px; align-items: center; justify-content: center; padding: 14px; border-top: 1px solid var(--border); }

/* health banner */
.health { border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--surface); padding: 16px 20px;
  border-left: 5px solid var(--success); box-shadow: var(--shadow-sm); }
.health--at_risk { border-left-color: var(--warning); background: var(--warning-soft); }
.health--overdue { border-left-color: var(--danger); background: var(--danger-soft); }
.health--complete { border-left-color: var(--primary); }
.health--not_started, .health--no_plan, .health--no_items, .health--on_hold, .health--cancelled { border-left-color: var(--border-strong); }
.health-head { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center; }
.health-dates { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 14px; color: var(--muted-ink); }
.health-dates strong { color: var(--text); }
.health-dates .late { color: var(--danger); font-weight: 700; }
.health-reasons { margin: 10px 0 0; padding-left: 18px; font-size: 14.5px; display: grid; gap: 4px; }

/* stage bars */
.stage-bars { display: grid; gap: 4px; }
.stage-bar { display: grid; grid-template-columns: minmax(120px, 170px) 1fr 48px; grid-template-rows: auto auto;
  gap: 2px 12px; align-items: center; color: var(--text); padding: 7px 8px; border-radius: 10px; font-weight: 600; font-size: 14px; }
.stage-bar:hover { background: var(--surface-2); text-decoration: none; }
.stage-bar .stage-kg { grid-column: 2 / 4; font-weight: 500; }
.stage-bar .num { text-align: right; }

/* activity feed */
.feed { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; font-size: 14px; }
.feed li { display: grid; gap: 2px; }
.feed-when { font-size: 12px; color: var(--muted-ink); font-weight: 600; }

/* bulk bar and the items grid */
.bulk-bar { display: flex; flex-wrap: wrap; gap: 8px 10px; align-items: center; background: var(--primary-soft);
  border: 1px solid var(--primary-line); border-radius: var(--radius-md); padding: 10px 14px; }
.bulk-label { font-weight: 800; font-size: 14px; color: var(--primary-deep); }
.bulk-bar select { min-height: 36px; width: auto; }
.bulk-sep { width: 1px; align-self: stretch; background: var(--primary-line); margin: 0 4px; }
.scope-all { font-size: 13px; }
.items-table { width: 100%; border-collapse: collapse; font-size: 13.5px; font-variant-numeric: tabular-nums; }
.items-table th, .items-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: middle; }
.items-table thead th { position: sticky; top: 0; background: var(--surface-2); font-size: 11.5px; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted-ink); z-index: 1; white-space: nowrap; }
.items-table tbody tr:hover { background: var(--surface-2); }
.items-table .num { text-align: right; }
.col-check { width: 36px; }
.col-check input { width: 18px; height: 18px; }
.stage-col { max-width: 92px; white-space: normal !important; line-height: 1.2; }
.stage-cell { text-align: center !important; font-weight: 700; font-size: 12.5px; min-width: 64px; }
.stage-cell--done { color: var(--success); background: var(--success-soft); }
.stage-cell--part { color: var(--warning); background: var(--warning-soft); }
.stage-cell--open { color: var(--muted-2); }
.stage-cell--flag { color: var(--danger); background: var(--danger-soft); font-size: 11.5px; }
.stage-cell--na { background: repeating-linear-gradient(135deg, transparent 0 5px, var(--neutral-line) 5px 6px); }

/* ---- the Gantt (public/static/js/gantt.js) ------------------------------------ */
.gantt { display: grid; grid-template-columns: minmax(180px, 280px) minmax(0, 1fr); border: 1px solid var(--border);
  border-radius: var(--radius-lg); background: var(--surface); overflow: hidden; box-shadow: var(--shadow-sm); }
.gantt-labels { border-right: 1px solid var(--border); padding-top: 44px; background: var(--surface); }
.gantt-label { display: flex; justify-content: space-between; align-items: center; gap: 8px; height: 34px;
  padding: 0 12px; font-size: 13px; color: var(--text); border-bottom: 1px solid transparent; overflow: hidden; }
.gantt-label span:first-child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
a.gantt-label:hover { background: var(--surface-2); text-decoration: none; }
.gantt-label--group { font-weight: 800; font-size: 12.5px; background: var(--surface-2); color: var(--muted-ink);
  letter-spacing: .02em; text-transform: uppercase; }
.gantt-pct { font-variant-numeric: tabular-nums; font-size: 12px; color: var(--muted-ink); font-weight: 700; }
.gantt-label--overdue .gantt-pct { color: var(--danger); }
.gantt-label--at_risk .gantt-pct { color: var(--warning); }
.gantt-label--complete .gantt-pct { color: var(--success); }
.gantt-timeline { overflow-x: auto; overflow-y: hidden; }
.gantt-svg { display: block; font-family: var(--font); }
.gantt:not(.is-ready) .gantt-labels { display: none; }
.gantt:not(.is-ready) { grid-template-columns: 1fr; }
.gantt-loading { padding: 18px; color: var(--muted-ink); font-size: 14px; }
.gantt.is-ready .gantt-loading { display: none; }
.g-axis { fill: var(--muted-ink); font-size: 11px; font-weight: 600; }
.g-axis--month { fill: var(--text); font-weight: 800; font-size: 12px; }
.g-axis--today { fill: var(--danger); font-weight: 800; }
.g-axis--delivery { fill: var(--primary-strong); font-weight: 800; }
.g-month { stroke: var(--border-strong); stroke-width: 1; }
.g-week { stroke: var(--border); stroke-width: 1; }
.g-axisline { stroke: var(--border-strong); }
.g-weekend { fill: var(--neutral-soft); }
.g-group { fill: var(--surface-2); }
.g-baseline { fill: var(--muted-2); opacity: .75; }
.g-bar { fill: var(--primary-soft); stroke: var(--primary-line); stroke-width: 1; }
.g-fill { fill: var(--primary); opacity: .9; }
.g-bar--complete { fill: var(--success-soft); stroke: var(--success-line); }
.g-bar--complete + .g-fill { fill: var(--success); }
.g-bar--at_risk { stroke: var(--warning); stroke-width: 2; }
.g-bar--overdue { stroke: var(--danger); stroke-width: 2; fill: var(--danger-soft); }
.g-bar--blocked, .g-bar--on_hold { stroke: var(--warning); stroke-dasharray: 4 3; stroke-width: 2; }
.g-bar--cancelled { fill: var(--neutral-soft); stroke: var(--neutral-line); opacity: .6; }
.g-milestone { fill: var(--primary); stroke: var(--surface); stroke-width: 1.5; }
.g-milestone.g-bar--complete { fill: var(--success); }
.g-milestone.g-bar--overdue { fill: var(--danger); }
.g-projection { stroke: var(--warning); stroke-width: 2; stroke-dasharray: 4 3; }
.g-projection-end { fill: var(--warning); }
.g-actual { fill: var(--text); }
.g-today { stroke: var(--danger); stroke-width: 1.5; stroke-dasharray: 5 4; }
.g-delivery { stroke: var(--primary-strong); stroke-width: 1.5; }
.gantt-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 12.5px; color: var(--muted-ink); margin-top: 10px; }
.gantt-legend span { display: inline-flex; align-items: center; gap: 6px; }
.lg { display: inline-block; width: 22px; height: 10px; border-radius: 3px; }
.lg-bar { background: var(--primary-soft); border: 1px solid var(--primary-line); }
.lg-fill { background: var(--primary); }
.lg-base { height: 4px; background: var(--muted-2); }
.lg-proj { height: 0; border-top: 2px dashed var(--warning); }
.lg-today { width: 0; height: 12px; border-left: 2px dashed var(--danger); }
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: var(--surface); }
.seg a { padding: 6px 12px; font-size: 13px; font-weight: 700; color: var(--steel); border-right: 1px solid var(--border); }
.seg a:last-child { border-right: 0; }
.seg a[aria-current="true"] { background: var(--primary-soft); color: var(--primary-strong); }
.seg a:hover { text-decoration: none; background: var(--surface-2); }
.toolbar { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; justify-content: space-between; }
.insight { background: var(--warning-soft); border: 1px solid var(--warning-line); color: #78350f; border-radius: var(--radius-sm);
  padding: 10px 12px; font-size: 14px; font-weight: 600; margin: 0 0 10px; }
.curve { width: 100%; height: auto; display: block; }
.curve-grid { stroke: var(--border); stroke-width: 1; }
.curve-label { fill: var(--muted-ink); font-size: 11px; font-family: var(--font); }
.curve-area { fill: var(--primary-soft); }
.curve-earned { fill: none; stroke: var(--primary); stroke-width: 2.5; stroke-linejoin: round; }
.curve-planned { fill: none; stroke: var(--muted-2); stroke-width: 2; stroke-dasharray: 6 4; }
.curve-dot { fill: var(--primary); stroke: var(--surface); stroke-width: 2; }
.lg-plan { border-top-color: var(--muted-2); }
.h-sub { font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted-ink); margin: 18px 0 8px; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .insight { color: var(--warning); } }
.attention { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.attention li { border-left: 4px solid var(--warning); padding: 4px 0 4px 12px; }
.attention-head { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; }
.attention p { margin: 4px 0 0; color: var(--muted-ink); }
.late { color: var(--danger); font-weight: 700; }

/* ---- layout fixes: forms, empty states inside cards, entry grid ------------- */
/* A form of several cards needs the same rhythm as the page around it. */
form.stack > .card, form.stack > details.card, form.stack > .table-card { margin: 0; }
.form-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.form-actions--page { padding: 4px 0 8px; }
/* An empty state inside a card is a message, not a second card. */
.card .empty-state, .table-card .empty-state {
  border: 0; box-shadow: none; background: transparent; border-radius: 0; padding: 34px 22px;
}
/* Text placed straight into a table card (which has no padding of its own). */
.table-card > p, .table-card > .help { margin: 0; padding: 18px 26px; }
.table-card > .inline-form { padding: 18px 26px; }
.entry-grid td { vertical-align: top; }
.entry-grid input { width: 100%; min-width: 88px; }
.entry-grid .cell-num { text-align: right; font-variant-numeric: tabular-nums; min-width: 84px; }
.entry-grid .cell-mark { min-width: 112px; font-weight: 600; }
.entry-grid tr.row-error-note td { padding-top: 0; }
.entry-grid thead th { white-space: nowrap; }
.curve-today { stroke: var(--danger); stroke-width: 1.5; stroke-dasharray: 4 4; opacity: .8; }
/* The overview's Gantt: the whole project at a glance, capped in height. */
.gantt--compact { max-height: 440px; overflow-y: auto; }
.entry-grid input::placeholder { color: var(--muted-2); font-weight: 400; }
/* A grid item is as wide as its content unless told otherwise, so one wide table
   (the entry grid) widened the page instead of scrolling inside its own box. */
.page-stack > *, .stack > *, .grid-2 > * { min-width: 0; }
/* Hidden (screen-reader) labels are absolutely positioned. Without a positioned
   ancestor they escape a table's scroll box and widen the whole page. */
.table-wrap { position: relative; }
/* On a phone the primary action of a form should not need a precise tap. Only
   on a phone: at tablet and laptop widths a full-width button beside a text
   box looked like a second text box. */
@media (max-width: 640px) {
  .card-foot .btn, .form-actions .btn { flex: 1 1 100%; }
}

/* ---- billing ---------------------------------------------------------------- */
.banner--blocking { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-line); }
.lead-line { font-size: 15px; }
.stat-of { font-size: .55em; color: var(--muted-ink); font-weight: 700; letter-spacing: 0; }
.plan-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); }
.plan-card { border: 1px solid var(--border); border-radius: var(--radius-md); padding: 18px; display: grid; gap: 10px;
  align-content: start; background: var(--surface); }
.plan-card h3 { font-size: 1.05rem; }
.plan-card--current { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary) inset; }
.plan-price { font-family: var(--display); font-size: 1.4rem; font-weight: 800; letter-spacing: -.02em; margin: 0; }
.plan-price.muted { font-family: inherit; font-size: 14px; font-weight: 600; }
