html, body {
    font-family: var(--font-sans);
    background: var(--background);
    color: var(--foreground);
}

/* 14px body text (Ink & Limestone, 24/09/2026). On body, not html, so the rem scale that every padding and
   size in the product is written in stays at 16px — only text that inherits moves. */
body { font-size: 0.875rem; line-height: 1.5; }

h1:focus {
    outline: none;
}

/* Keyboard focus, everywhere (Ink & Limestone DS8.2, WCAG 2.4.7). Measured 24/09/2026 by tabbing through
   /projects: buttons, tabs, sort headings, nav links and the user menu drew NO focus indicator — the library
   resets loaded before this file had removed the browser's own. :focus-visible only, so a mouse click shows
   nothing and a keyboard user always sees where they are. --ring is 3.7:1 on white; on the ink sidebar the
   marker colour is used instead (declared in ShellSidebar, which owns .fx-side). Fields keep their own ring (the border + halo in the form rules). */
a[href]:focus-visible, button:focus-visible, summary:focus-visible, [role="button"]:focus-visible,
[role="tab"]:focus-visible, [tabindex]:not([tabindex="-1"]):focus-visible {
    outline: 2px solid var(--ring); outline-offset: 2px;
}

/* ===========================================================================
   Design-system tokens (UI Design PRD v1, Slice 0) — shadcn light neutral base
   + the Foundations brand trio, values verbatim from the design-source demo
   (PoC Src/app/globals.css). Light-only per D-UI3; the demo's .dark block is
   parked at the end of this section for a future revival.

   The legacy token names (--navy/--gold/--ink/--surface/--hair/--r-* …) are kept
   as ALIASES onto this set so the existing fx- component vocabulary re-skins
   without touching markup. New code should read the shadcn names directly.
   =========================================================================== */

:root {
    /* Brand palette — Building Industry style guide (docs/Foundations-StyleGuide.md).
       Steel blue primary + safety orange accent (the ONE accent, used sparingly).
       --brand-secondary has no distinct value in this palette → maps to primary, so
       existing "highlight" usages (KPI numbers, kickers, usage bars) read steel blue,
       not a third colour. */
    --brand-primary: #182421;               /* midnight ink — Ink & Limestone, 24/09/2026 */
    --brand-accent: #D6F36A;                /* citron — the primary action and the nav marker only */
    --brand-secondary: #182421;             /* = primary (no third brand colour) */

    /* Typography (§3.2) — self-hosted, no CDN */
    --font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
    --font-heading: 'Inter', var(--font-sans);   /* D-UI8: Inter throughout */
    --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

    /* Neutral + surface — style guide §2. Concrete page canvas, WHITE raised surfaces
       (cards/inputs), slate-grey text scale, steel-blue primary. */
    --background: #F6F7F3;                   /* limestone — page canvas */
    --foreground: #182421;                   /* ink — body text */
    --card: #FFFFFF;                         /* raised surface */
    --card-foreground: #182421;
    --popover: #FFFFFF;
    --popover-foreground: #182421;
    --primary: #D6F36A;                      /* citron — the primary action (CtaRole = accent) */
    --primary-foreground: #182421;
    --secondary: #F9FAF7;                    /* soft surface — faint fill */
    --secondary-foreground: #182421;
    --muted: #F9FAF7;                        /* soft surface — header bands / muted fill */
    --muted-foreground: #5F6B66;             /* grey-green — muted text */
    --accent: #F9FAF7;
    --accent-foreground: #182421;
    --destructive: #C0392B;                  /* brick red — danger button fill (white text) */
    --border: #E0E5DC;                       /* soft grey-green — hairline */
    --input: #E0E5DC;
    --ring: #808785;                         /* derived: Dark lifted 45% (BrandCss) */
    --chart-1: #182421;                      /* charts: ink family + semantics */
    --chart-2: #9AAF4C;
    --chart-3: #2E7D4F;
    --chart-4: #5F6B66;
    --chart-5: #A9B4AE;
    --sidebar: #FFFFFF;
    --sidebar-foreground: #182421;
    --sidebar-primary: #182421;
    --sidebar-primary-foreground: #FFFFFF;
    --sidebar-accent: #F9FAF7;
    --sidebar-accent-foreground: #182421;
    --sidebar-border: #E0E5DC;
    --sidebar-ring: #808785;

    /* Radius scale (§3.3 — demo @theme inline, verbatim ratios) */
    --radius: 0.625rem;
    --radius-sm: calc(var(--radius) * 0.6);
    --radius-md: calc(var(--radius) * 0.8);
    --radius-lg: var(--radius);
    --radius-xl: calc(var(--radius) * 1.4);
    --radius-2xl: calc(var(--radius) * 1.8);
    --radius-3xl: calc(var(--radius) * 2.2);
    --radius-4xl: calc(var(--radius) * 2.6);

    /* ---- Legacy aliases (fx- vocabulary) — values only; names kept ---- */
    --navy: var(--brand-primary);           /* steel blue */
    --navy-700: #111A18;                    /* derived: Dark −28% (darker chrome) */
    --indigo: #26312E;                       /* ink 600 — AI surfaces */
    --indigo-600: #101715;                   /* ink 800 */
    --gold: var(--brand-accent);            /* accent role → citron (a FILL — never text) */
    --gold-deep: #9AAF4C;                   /* derived: Accent −28% (hover fills) */
    /* The accent as TEXT (24/09/2026). BrandCss writes the accent itself where it is legible on white and Dark where
       it is too pale — citron is about 1.2:1 — so links and labels never go citron. Read this, not --brand-accent
       or --gold-deep, for any text colour. */
    --accent-text: #182421;
    /* Text on the brand dark (BrandCss emits it from OnPrimary; declared here so the fallback is the token). */
    --on-primary: #FFFFFF;
    /* The bar beside the current page in the sidebar shell: the accent, unless a brand sets its own. */
    --sidebar-shell-marker: var(--brand-accent);
    --r-input: var(--radius-md);            /* shadcn input: rounded-md */
    --r-card: var(--radius-xl);             /* shadcn card: rounded-xl */
    --r-modal: var(--radius-lg);            /* shadcn dialog: rounded-lg */

    --bg: var(--background);                /* concrete canvas */
    --ink: var(--foreground);
    --ink-2: #3B4348;                       /* slate 700 — secondary text */
    --ink-3: var(--muted-foreground);       /* slate 500 — muted text */
    --surface: var(--card);
    --surface-2: var(--card);
    --data-surface: var(--card);
    --hair: var(--border);                  /* neutral 200 */
    --hair-2: #A9B4AE;                      /* derived: Light — stronger edge */
    --field-bg: #FFFFFF;                    /* inputs are white, not concrete */
    --shadow: none;                         /* guide: prefer borders over shadows */
    --grid-head: #F9FAF7;                   /* derived: OffWhite — table header */
    --grid-head-text: #5F6B66;              /* grey-green — quiet small-caps headings */

    /* Grid rows, brandable since 17/08/2026. Defaults are exactly what the rules used to hardcode via other
       tokens, so declaring them changed nothing: rows sat on --card and were set in --ink-2, and there was no
       banding anywhere in the product. --grid-row-alt: transparent IS "no banding" — the banding rule below is
       unconditional, and painting a row transparent is the same as not painting it. */
    --grid-row: var(--card);
    --grid-row-ink: var(--ink-2);
    --grid-row-alt: transparent;

    /* Status hues — light tints + darker same-hue text (guide §2 badge rule, AA-safe).
       Chip TEXT uses the 700 shade so it passes on the 10% tint; solid fills
       (buttons) use --destructive / the 500 shades. No "info" hue in this palette →
       steel blue. */
    --c-success: #1F5837;                   /* site green 700 (chip text) */
    --c-warning: #866300;                   /* amber 800 (chip text) — 4.98:1 on its own tint; #9C7400 reached 3.85 and failed AA (DS8.2) */
    --c-danger: #8A281E;                    /* brick red 700 (chip text) */
    --c-info: var(--brand-primary);         /* no distinct info hue: the brand dark */
    --c-neutral: var(--muted-foreground);   /* slate 500 */
    --c-success-bg: rgba(46,125,79,.10);    /* site green 500 tint */
    --c-warning-bg: rgba(224,168,0,.14);    /* amber 500 tint */
    --c-danger-bg: rgba(192,57,43,.10);     /* brick red 500 tint */
    --c-info-bg: rgba(31,58,95,.10);        /* steel blue tint */
    --c-neutral-bg: var(--muted);           /* concrete 100 */
    --c-gold-bg: rgba(232,118,44,.12);      /* safety orange tint */

    /* Stat tiles are plain quiet cards now */
    --stat-bg: var(--card);
    --stat-blur: none;
}

/* Dark mode is PARKED (D-UI3 — light-only for now). The demo's .dark token set
   is preserved here for a future revival; nothing applies it.

.dark {
    --background: oklch(0.145 0 0);
    --foreground: oklch(0.985 0 0);
    --card: oklch(0.205 0 0);
    --card-foreground: oklch(0.985 0 0);
    --popover: oklch(0.205 0 0);
    --popover-foreground: oklch(0.985 0 0);
    --primary: oklch(0.922 0 0);
    --primary-foreground: oklch(0.205 0 0);
    --secondary: oklch(0.269 0 0);
    --secondary-foreground: oklch(0.985 0 0);
    --muted: oklch(0.269 0 0);
    --muted-foreground: oklch(0.708 0 0);
    --accent: oklch(0.269 0 0);
    --accent-foreground: oklch(0.985 0 0);
    --destructive: oklch(0.704 0.191 22.216);
    --border: oklch(1 0 0 / 10%);
    --input: oklch(1 0 0 / 15%);
    --ring: oklch(0.556 0 0);
    --sidebar: oklch(0.205 0 0);
    --sidebar-foreground: oklch(0.985 0 0);
    --sidebar-primary: oklch(0.488 0.243 264.376);
    --sidebar-primary-foreground: oklch(0.985 0 0);
    --sidebar-accent: oklch(0.269 0 0);
    --sidebar-accent-foreground: oklch(0.985 0 0);
    --sidebar-border: oklch(1 0 0 / 10%);
    --sidebar-ring: oklch(0.556 0 0);
}
*/

/* Inline validation states (Blazor EditForm). */
.valid.modified:not([type=checkbox]) {
    outline: 1px solid #10b981;
}

.invalid {
    outline: 1px solid #dc2626;
}

.validation-message {
    color: #dc2626;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

/* 8FA9CBA4 — reported by Ridings 18/09/2026: "the banner is yellow with either yellow text or white text,
   making it very hard to read". It was Blazor's stock lightyellow with NO colour declared at all, so
   the text took whatever the theme handed it — which on this shell is near-white. An error notice
   nobody can read is worse than none, because it looks like decoration. Red with white text, and BOTH
   halves stated: the pair is the fix, not the background alone. */
#blazor-error-ui {
    color-scheme: light only;
    background: #b91c1c;
    color: #ffffff;
    font-weight: 500;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1100;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
        color: #ffffff;
    }

    /* The link is the whole point of the banner — it must not inherit a link colour that vanishes
       against red. */
    #blazor-error-ui .reload {
        color: #ffffff;
        text-decoration: underline;
        font-weight: 600;
    }

/* ===========================================================================
   Identity /Account pages — light shadcn card on the brand-primary background
   (AuthLayout owns the shell; these restyle the Bootstrap form chrome the
   scaffolded Identity pages render). Bootstrap stays loaded — it provides the
   layout classes; we restyle the surface.
   =========================================================================== */

.auth-body .row { margin: 0; }

.auth-body .col-lg-6,
.auth-body .col-lg-4,
.auth-body .col-md-4 {
    width: 100%;
    max-width: 100%;
    flex: 0 0 100%;
    padding: 0;
}

/* Hide the "use another service" external-login column on the brand card — no
   external providers are wired (internal Identity only, per standards). */
.auth-body .col-lg-4.col-lg-offset-2,
.auth-body .col-lg-offset-2 {
    display: none;
}

.auth-body hr {
    border-color: var(--border);
    opacity: 1;
}

.auth-body label,
.auth-body .form-label {
    color: var(--ink-2);
    font-weight: 500;
    font-size: 0.82rem;
}

.auth-body .form-control {
    background: var(--background);
    border: 1px solid var(--input);
    border-radius: var(--r-input);
    color: var(--foreground);
    padding: 0.65rem 0.85rem;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}

.auth-body .form-floating > label {
    color: var(--muted-foreground);
}

.auth-body .form-control::placeholder { color: var(--muted-foreground); }

/* Floating-label fields: keep Bootstrap's geometry (the generic padding override above
   would collapse the label space) and its transparent-placeholder trick — the label IS
   the placeholder, so a visible placeholder colour makes the two overlap. */
.auth-body .form-floating > .form-control {
    height: calc(3.5rem + 2px);
    padding: 1rem 0.85rem;
}
.auth-body .form-floating > .form-control::placeholder { color: transparent; }

.auth-body .form-control:focus {
    background: var(--background);
    border-color: var(--ring);
    box-shadow: 0 0 0 3px oklch(0.708 0 0 / 0.5);
    color: var(--foreground);
}

.auth-body .checkbox label,
.auth-body .form-check-label { color: var(--ink-2); }

/* Primary action (Log in / Register) — brand accent, shadcn button shape. */
.auth-body .btn-primary,
.auth-body .btn-lg.btn-primary {
    background: var(--brand-primary);   /* steel blue + white = 11.5:1 (guide); orange stays an accent, and white-on-vivid-orange fails AA */
    border: none;
    color: #fff;
    font-weight: 600;
    font-size: 0.92rem;
    border-radius: var(--radius-lg);
    padding: 0.6rem 1.2rem;
    box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}

.auth-body .btn-primary:hover { background: var(--navy-700); color: #fff; }

.auth-body a,
.auth-body .btn-link {
    color: var(--accent-text);
    text-decoration: none;
}

.auth-body a:hover { text-decoration: underline; }

/* Validation summary + status messages on the light card. */
.auth-body .text-danger,
.auth-body .validation-message { color: var(--destructive) !important; }

.auth-body .alert,
.auth-body [role="alert"] {
    background: var(--c-danger-bg);
    border: 1px solid rgba(220,38,38,0.25);
    border-radius: var(--r-input);
    color: #b91c1c;
    padding: 0.6rem 0.9rem;
}

/* ===========================================================================
   MudBlazor dialog backdrop — shadcn overlay (flat black/50, no blur).
   =========================================================================== */
.mud-overlay-dialog {
    background-color: rgba(0, 0, 0, 0.5) !important;
}

/* ===========================================================================
   Commercial UI — shared fx- component vocabulary, restyled to the shadcn
   look (quiet cards: 1px border, flat; Montserrat headings; accent links).
   Class names are structural and unchanged (Slice 0 rule) — only the look.
   =========================================================================== */

/* --- Headings have a house look even when nobody remembered the class ------------
   A bare <h3> inherits --font-sans at the browser's default size, weight and margins. So "New customer"
   on eleven master-data pages, and 49 headings in all, were rendering in the BODY typeface while every
   heading beside them was Montserrat — the single most visible inconsistency in the application, and one
   nothing would ever have flagged.

   Set at ELEMENT specificity (0,0,1) on purpose: every existing .fx-page-title, .fx-h2, .fx-h3 and
   .fx-dialog-title beats it without changing, so this is a floor rather than an override. Scoped to
   .fx-shell so the Identity pages, which are Bootstrap's and deliberately so, are untouched.

   The alternative was adding a class to 49 headings and hoping the fiftieth remembered. */
/* --- A CORRECTION TO THE NOTE ABOVE, AND A WARNING (08/09/2026, 5AA304E6) --------------------------
   The note says this is set "at ELEMENT specificity (0,0,1) on purpose … a floor rather than an
   override". That is the INTENT; it is not what the selector does. `:is()` takes the specificity of its
   most specific argument, so `.fx-shell :is(h1, h2, h3, h4)` is (0,1,1) — a class plus an element — and it
   therefore BEATS every heading styled with a single class, which is most of them.

   Found from "the staff utilisation header is very close to the table above it": `.po-section` asks for
   26px of top margin and was computing 0, because this rule was winning.

   It is deliberately NOT changed to `:where()`, which would make it the floor it claims to be. Several
   heading classes — `.fx-h2`, `.fx-h3`, `.fx-dialog-title` — declare `margin: 0`, and they have only ever
   LOOKED right because this rule was overriding them with a 12px bottom margin. Turning it into a true
   floor lets those win and strips the bottom gap off headings across the product, which is a far larger
   change than the report that found it and not one to make from a styling ticket.

   So: if you are fighting this rule, raise your own selector's specificity (see `.fx-shell .po-section`
   in PracticeOverview.razor) rather than lowering this one. Straightening it out properly means giving
   those three classes the 12px they currently inherit by accident, and re-checking every heading in the
   product — worth doing, as its own piece of work. Flagged to Simon 08/09/2026. */
/* --- THE TOP HALF IS NOW STRAIGHTENED OUT (10/09/2026, A9B1D342 + 16194971) ------------------------
   Two more reports of the same defect, both "a section heading sits flush against the table above it":
   `.dd-section` asking 4px and `.md-section` asking 22px, both computing 0. Three reports of one cause,
   so the workaround above stops being the answer.

   The two halves of the shorthand were doing different jobs and only one of them needed the strength:

     margin-bottom  is what stops a heading jamming into the content it labels. Several classes declare
                    `margin: 0` and have only ever looked right because this rule overrode them, so it
                    stays at :is() strength — 15 classes ask for a 0 bottom margin and 8 more for a
                    specific non-12px one, and letting those 23 win is the larger, visually-checked
                    sweep the note above describes. Unchanged, deliberately.

     margin-top     was set to 0 for headings sitting at the top of a panel, where the browser default
                    looked wrong. That is a DEFAULT, not an override: a class that has explicitly asked
                    for space above itself should get it. It is therefore split out at :where() — (0,1,0),
                    beaten by any single class — so the panel-top case still reads as before and a class
                    that asks is answered.

   Five headings move, all of them section headings that follow content and all in the direction their
   own class already asked for: .dd-section, .md-section, .ak-help-sub, .dz-h3, .st-group. (.po-section
   keeps the raised-specificity workaround and is unaffected by either route.) Nothing else in the product
   declares a top margin on a heading class, so nothing else moves — swept, not assumed. */
.fx-shell :where(h1, h2, h3, h4) {
    /* A floor, not an override — see the note above. Any class that asks for space above itself wins. */
    margin-top: 0;
}

.fx-shell :is(h1, h2, h3, h4) {
    font-family: var(--font-heading);
    color: var(--ink);
    letter-spacing: -0.01em;

    /* NOT 0. These headings had the browser default (about 1em top and bottom) and 49 places were laid
       out around it; zeroing it would jam every one of them against whatever follows. .fx-h2 and .fx-h3
       ask for 0 and are overridden here on purpose — see the note above before changing that. */
    margin-bottom: 12px;
}

.fx-shell h1 { font-size: 1.875rem; font-weight: 700; }
.fx-shell h2 { font-size: 1.4rem;   font-weight: 600; }
.fx-shell h3 { font-size: 1.08rem;  font-weight: 600; }
.fx-shell h4 { font-size: 0.95rem;  font-weight: 600; }

/* --- PAGE HEADER ROW — one owner, after 41 copies (FBD76927 + 054C5A42, 10/09/2026) ---------------
   Reported as "the Show archived toggle sometimes sits on the same row as the heading and sometimes
   drops below it — the markup is identical in every case, so the difference is not a design decision".
   Exactly right, and the cause was that nothing owned this class: 41 page style blocks each declared
   their own `.fx-head-row` (40 identical, one at 20px) and five declared `.fx-head-actions` in three
   different gaps. A layout defect in a copied rule can only be fixed in the copy you happen to open.

   `> :first-child { flex: 1 1 320px; min-width: 0 }` — the title block both GROWS and SHRINKS, and that
   one line answers both reports.

     Growing takes all the free space, so every later child packs against the right with the 16px gap
     between them. `space-between` on its own divides free space between EVERY pair of children, so a
     page whose two buttons were direct children of the row got 189px between the buttons as well as
     beside the title (054C5A42). Doing it this way needs no per-page markup change, which matters: the
     fix must not depend on each page being tidied first.

     Shrinking is what stops a long subtitle pushing the actions onto a second line (FBD76927). It needs
     BOTH halves — a flex item will not shrink below its min-content width without `min-width: 0`, and
     that min-content width is what a long subtitle was setting. The 320px basis keeps a real wrap for
     narrow viewports, so the title drops to its own line rather than becoming a sliver. */
.fx-head-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 24px;
    flex-wrap: wrap;
}

.fx-head-row > :first-child { flex: 1 1 320px; min-width: 0; }

/* A trail back up the hierarchy: above the heading, at the left, where the arrow points (8B4A6EFC).
   Its own class rather than a bare .fx-btn so the next screen that needs one matches this by name. */
.fx-back-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 10px;
    font-size: 0.85rem;
}

.fx-head-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.fx-page-title {
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: 2rem;             /* 32px page titles (Ink & Limestone) */
    letter-spacing: -0.02em;     /* crisp: Inter tightens at display sizes */
    line-height: 1.15;
    color: var(--foreground);
    margin: 0;
    /* Resolution S3-T17 — a project title now leads with the client, which can be long: wrap, never scroll. */
    overflow-wrap: anywhere;
    min-width: 0;
}
.fx-page-sub { color: var(--muted-foreground); font-size: 0.92rem; margin-top: 4px; }
/* The second, technical half of a page description — the bit for whoever wires the integration up rather
   than the admin deciding whether they want one. Set back and smaller so the plain-English sentence above
   it reads first. In app.css beside the rule it modifies, per 31/08/2026. */
.fx-page-sub--tech { font-size: 0.84rem; opacity: 0.85; max-width: 62rem; }
.fx-page-sub--tech code { font-size: 0.95em; }

/* ---- Section headings, GLOBAL ----
   Reported 22/08/2026 (D3747BC3), on the project Delivery page: "all of the elements under each stage need
   bringing in line with the font and styling of the first section of the page … The fonts are different".

   They were, and it was not a subtle theming drift. `.fx-h2` was declared in SEVENTEEN separate page style
   blocks — byte-identical, every one of them — and used on twenty-seven pages. The other ten, Delivery
   among them, rendered their section headings as a browser-default <h2>: a different family, a different
   size, and browser margins. `.fx-h3` was worse: used on eight pages and declared NOWHERE, so every
   "Add a milestone"-style sub-heading in the product was unstyled.

   This is the same failure app.css already documents twice (the nine md-form copies, the five ::deep
   MudDataGrid blocks): a rule copied per page is a rule that is missing on the next page somebody writes.
   The declaration is here now and the per-page copies are deleted. */
.fx-h2 { font-family: var(--font-heading); font-weight: 600; font-size: 1.4rem; margin: 0; color: var(--ink); }
/* A qualifier beside a heading — "excluding closed" and the like. In app.css, beside the heading it
   modifies, for the reason four faults were reported on 31/08/2026. */
.fx-h2-note { margin-left: 8px; font-family: var(--font-body); font-weight: 400; font-size: 0.82rem;
  color: var(--muted-foreground); vertical-align: middle; }
.fx-h3 { font-family: var(--font-heading); font-weight: 600; font-size: 1.08rem; margin: 0; color: var(--ink); }

.fx-panel {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    box-shadow: var(--shadow);
    color: var(--card-foreground);
}

.fx-stat {
    border-radius: var(--r-card);
    padding: 16px 18px;
    border: 1px solid var(--border);
    background: var(--stat-bg);
    box-shadow: var(--shadow);
}
.fx-stat .k-label { font-size: 0.72rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted-foreground); font-weight: 600; }
.fx-stat .k-num {
    font-family: var(--font-heading); font-size: 1.5rem; font-weight: 700;
    margin: 6px 0 4px; font-variant-numeric: tabular-nums; color: var(--foreground);
}
.fx-stat .k-num.gold { color: var(--brand-secondary); }   /* demo KPI headline: brand-secondary */
.fx-stat .k-delta { font-size: 0.8rem; font-weight: 500; color: var(--muted-foreground); }

.num { font-variant-numeric: tabular-nums; }

/* Status / RAG chips — demo badge rule: pill, light same-hue tint + darker text. */
.chip {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 0.76rem; font-weight: 500; padding: 2px 10px; border-radius: 999px;
    border: 1px solid transparent; white-space: nowrap;
}
.chip .dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
.chip-success { background: var(--c-success-bg); color: var(--c-success); }
.chip-warning { background: var(--c-warning-bg); color: var(--c-warning); }
.chip-danger  { background: var(--c-danger-bg); color: var(--c-danger); }
.chip-info    { background: var(--c-info-bg); color: var(--c-info); }
.chip-neutral { background: var(--c-neutral-bg); color: var(--c-neutral); }
.chip-gold    { background: var(--c-gold-bg); color: var(--accent-text); }

/* Data grid — demo data-grid: flat gray-50 header, uppercase xs, hairline rows. */
.fx-grid-wrap { border-radius: var(--r-card); border: 1px solid var(--border); overflow: hidden; box-shadow: var(--shadow); }
/* A heading INSIDE a grid card, inset to match the card's own content rather than sitting proud of it at
   the card edge (D3747BC3, 31/08/2026). Padding, not margin: .fx-grid-wrap has overflow:hidden for its
   rounded corners, so a margin here would collapse against the card edge and put the text back where it
   started. In app.css beside the rule it belongs to, per 31/08/2026. */
.fx-grid-title { padding: 16px 22px 0; margin: 0 0 12px; background: var(--card); }
.fx-grid-scroll { overflow-x: auto; background: var(--data-surface); }
/* A visible scrollbar on a grid that is wider than its box. macOS hides overlay scrollbars, so a grid whose last
   columns ran past the edge gave no sign they existed (sweep 24/09/2026: seven pages, most of them as wide on the
   old design). Styling the bar makes WebKit and Chromium draw it whenever the grid actually overflows. */
.fx-grid-scroll::-webkit-scrollbar { height: 10px; width: 10px; }
.fx-grid-scroll::-webkit-scrollbar-track { background: var(--muted); }
.fx-grid-scroll::-webkit-scrollbar-thumb { background: var(--hair-2); border-radius: 999px; border: 2px solid var(--muted); }
/* Both standard properties back to auto, explicitly. In Chromium a non-auto scrollbar-width OR scrollbar-color
   switches OFF the ::-webkit-scrollbar styling above — and MudThemeProvider injects a page-wide
   `html, body * { scrollbar-width: thin; scrollbar-color: … }`, so without this line the rules above did nothing
   in Chrome (measured 25/09/2026: a 0px bar, i.e. still the hidden overlay). The class outranks `body *`. */
.fx-grid-scroll { scrollbar-width: auto; scrollbar-color: auto; }
table.fx-grid { border-collapse: collapse; width: 100%; min-width: 720px; }
table.fx-grid thead th {
    text-align: left; font-size: 0.72rem; letter-spacing: 0.05em; text-transform: uppercase;
    color: var(--grid-head-text); font-weight: 600; padding: 12px 12px;
    background: var(--grid-head);
    border-bottom: 1px solid var(--border);
}
/* 52px rows (Ink & Limestone). HEIGHT on a cell is a minimum, so a row grows for wrapped text; 6px vertical
   padding is what lets a 40px control (a Save button, a cell editor) sit in the row without making it taller.
   Tabular figures everywhere in a grid, so columns of numbers line up. */
table.fx-grid tbody td {
    height: 52px; padding: 6px 12px; font-size: 0.875rem; border-bottom: 1px solid var(--border);
    color: var(--grid-row-ink); background: var(--grid-row); font-variant-numeric: tabular-nums;
}

/* Banding. Unconditional on purpose — --grid-row-alt is `transparent` unless a brand sets it, so this rule is
   inert by default and needs no on/off switch. Specificity note: the clickable-hover rule below carries two
   classes and therefore still wins over this one pseudo-class, so hover keeps working on banded rows. */
table.fx-grid tbody tr:nth-child(even) td { background: var(--grid-row-alt); }

table.fx-grid tbody tr:last-child td { border-bottom: none; }

/* Totals row. 38172F69 (18/09/2026): "we have a total at the bottom. Please can this be designed so it's
   the same size font as the lines and it's indented the same way, with a line to separate the lines and
   the total? The same needs to happen with the figures."

   There was no tfoot rule AT ALL, so a <th> in a footer fell back to browser defaults: no padding, hence
   the indent that did not line up with the column above it, and the table's inherited font size rather
   than the 0.875rem every body row uses. It stays bold — a total should still read as a total; what Mark
   asked for was that it line up, not that it stop standing out. */
table.fx-grid tfoot th,
table.fx-grid tfoot td {
    padding: 12px 16px;
    font-size: 0.875rem;
    text-align: left;
    font-weight: 600;
    color: var(--grid-row-ink);
    background: var(--grid-row);
    border-top: 2px solid var(--border);
    border-bottom: none;
}

/* The figures line up with the column above them, which is the second half of the same report. */
table.fx-grid tfoot th.num,
table.fx-grid tfoot td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* ── Sort and filter on every grid heading (GridHeader.razor, 23/09/2026) ───────────────────────────
   Simon: "every grid in the application to have the ability to sort on any column and be able to filter on
   every column". The label is a button that cycles ascending → descending → the page's own order; the funnel
   opens a filter for that column. Both inherit the heading's colour, size and case, so they sit in any brand's
   header without a token of their own — and an active funnel is shown by being FILLED, not by a colour, for
   the same reason (a fill colour from --grid-head would need its own text pair to stay legible). */
.fx-gh { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; }
.fx-gh-num { flex-direction: row; }
.fx-gh-sort {
    display: inline-flex; align-items: center; gap: 4px; padding: 0; margin: 0;
    background: none; border: 0; cursor: pointer;
    font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit; text-align: inherit;
}
.fx-gh-sort:hover { text-decoration: underline; text-underline-offset: 3px; }
.fx-gh-sort.asc::after, .fx-gh-sort.desc::after {
    content: ""; display: inline-block; width: 0; height: 0; margin-left: 2px;
    border-left: 4px solid transparent; border-right: 4px solid transparent;
    border-bottom: 5px solid currentColor;
}
.fx-gh-sort.desc::after { transform: rotate(180deg); }
.fx-gh-funnel {
    display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 20px; height: 20px; padding: 0; margin: 0; border: 0; border-radius: 4px;
    background: none; color: inherit; cursor: pointer; opacity: .45;
}
.fx-gh-funnel:hover, .fx-gh-funnel:focus-visible, .fx-gh-funnel.on { opacity: 1; }
.fx-gf-backdrop { position: fixed; inset: 0; z-index: 1400; background: transparent; display: block; }
/* Fixed from the funnel's measured rectangle (inline top/left). Every inherited heading style is reset: the
   panel is drawn inside a th, which is uppercase, tracked and small. */
.fx-gf-panel {
    position: fixed; z-index: 1500; display: flex; flex-direction: column; gap: 8px;
    width: 280px; max-height: 420px; padding: 12px; box-sizing: border-box;
    background: var(--surface, #fff); color: var(--foreground);
    border: 1px solid var(--border); border-radius: var(--r-panel, 12px);
    box-shadow: 0 10px 30px rgba(15, 23, 42, .14);
    font-size: 0.85rem; font-weight: 400; letter-spacing: normal; text-transform: none;
    text-align: left; white-space: normal; cursor: default;
}
.fx-gf-title { font-weight: 600; font-size: 0.8rem; color: var(--muted-foreground); }
.fx-gf-field { display: flex; flex-direction: column; gap: 3px; font-size: 0.78rem; color: var(--muted-foreground); }
.fx-gf-panel .fx-input { width: 100%; box-sizing: border-box; }
/* A column's "contains" box, deliberately type="text" rather than search: it narrows one column of the grid it
   sits in, and the list-wide search on every list page is the shared SearchBox (SearchPermissionScopeTests). */
.fx-gf-contains { font-size: 0.85rem; }
.fx-gf-bulk { display: flex; gap: 12px; font-size: 0.78rem; }
.fx-gf-list { display: flex; flex-direction: column; overflow-y: auto; max-height: 220px; margin: 0 -4px; }
.fx-gf-value { display: flex; align-items: center; gap: 8px; padding: 4px; border-radius: 6px; cursor: pointer; }
.fx-gf-value:hover { background: var(--muted); }
.fx-gf-value span { overflow-wrap: anywhere; }
.fx-gf-note { font-size: 0.78rem; color: var(--muted-foreground); }
.fx-gf-foot { display: flex; justify-content: space-between; gap: 8px; padding-top: 4px; border-top: 1px solid var(--border); }
/* Said out loud under a filtered grid, so rows hidden by a funnel never read as rows that are missing. */
.fx-grid-filterbar {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 8px 4px 0; font-size: 0.82rem; color: var(--muted-foreground);
}

table.fx-grid tbody tr.clickable { cursor: pointer; }
table.fx-grid tbody tr.clickable:hover td { background: var(--muted); }
/* The refreshed grids' clickable row (Ink & Limestone) — same subtle hover as .clickable, plus the pointer. */
table.fx-grid tbody tr.fx-row-link { cursor: pointer; }
table.fx-grid tbody tr.fx-row-link:hover td { background: var(--muted); }
table.fx-grid .fx-pin-col { width: 40px; padding-left: 8px; padding-right: 0; }
/* A starred row is marked at its edge as well as by the star, so a starred block at the top reads as deliberate. */
table.fx-grid tbody tr.is-pinned td:first-child { box-shadow: inset 3px 0 0 var(--sidebar-shell-marker, var(--brand-accent)); }
table.fx-grid tbody td .proj { color: var(--foreground); font-weight: 600; }
table.fx-grid td.num, table.fx-grid th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Reported 22/08/2026 (88824980): "The table headers and table entities are often misaligned in the
   milestone table. Fees, for example, the title is aligned to the far right-hand side of the column but
   the actual fees themselves are left-aligned or centre-aligned."

   Both true, and one cause. td.num right-aligns the cell's CONTENT, which right-aligns an input BOX — but
   the number inside that box keeps the browser's own left alignment for a number field. So a read-only
   figure lined up under its heading and an editable one did not, on the same column of the same table.
   Fixed here rather than on the one page, because the same pattern is on the estimate, WIP, billing and
   application grids. */
table.fx-grid td.num input,
table.fx-grid td.num select {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* And "the save button is completely skewed", from the same report. That cell carried an inline
   `display: flex` to sit its two buttons side by side — which stops the <td> being a table cell at all,
   so it no longer shares the row's height or takes its width from the column, and the buttons floated
   high in a cell whose borders had gone their own way. The row of actions is a DIV inside the cell now,
   and the class lives here so the next actions column does not repeat the shortcut. */
table.fx-grid td .fx-grid-actions {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: nowrap;
    white-space: nowrap;
}

/* ---- MudDataGrid (.fx-dg), themed GLOBALLY ----
   Four pages carried equivalent rules in their own style blocks written as `::deep .fx-dg …`. Blazor only
   processes `::deep` in a scoped .razor.css file; inside a plain <style> block it is an invalid selector, so
   every one of those rules was dropped and all five MudDataGrids rendered with MudBlazor's defaults —
   Projects (which had the CSS) and Feedback (which had none) were byte-identical when measured. Global rules
   here need no `::deep`, cover all five, and make the grid tokens actually reach them. */
.fx-dg { background: var(--grid-row); color: var(--grid-row-ink); }
.fx-dg .mud-table-root { background: var(--grid-row); }
.fx-dg .mud-table-head th,
.fx-dg .mud-table-head .mud-table-cell {
    background: var(--grid-head); color: var(--grid-head-text);
    text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.08em; font-weight: 700;
    border-bottom: 1px solid var(--hair-2);
}
.fx-dg .mud-table-body .mud-table-row .mud-table-cell {
    background: var(--grid-row); color: var(--grid-row-ink); border-bottom: 1px solid var(--border);
    height: 52px; padding-top: 6px; padding-bottom: 6px; font-variant-numeric: tabular-nums;
}
.fx-dg .mud-table-body .mud-table-row:nth-child(even) .mud-table-cell { background: var(--grid-row-alt); }
.fx-dg .mud-table-body .mud-table-row:hover .mud-table-cell { background: var(--muted); }
/* A clickable row says so. Reported 10/09/2026 (7BC4317E): "the row background highlights on hover, and
   clicking the row opens the estimate … but the cursor stays as the default arrow". The row class was
   already being applied by Projects, Estimates and the partner dashboard — the three grids with a
   RowClick — and, like several classes before it, was declared NOWHERE, so it did nothing. (The feedback
   list is the fourth clickable grid and had solved it locally with RowStyle="cursor: pointer".) */
.fx-dg .mud-table-body .fx-dg-row { cursor: pointer; }
.fx-dg .proj { color: var(--foreground); font-weight: 600; }
.fx-dg .num, .fx-dg .num .mud-table-cell-content { text-align: right; justify-content: flex-end; font-variant-numeric: tabular-nums; }
.fx-dg .mud-table-pagination { border-top: 1px solid var(--border); color: var(--grid-row-ink); }

/* 9ECFBF39 — the pin column on the projects list. Steve, 22/09/2026: "Would like to Pin projects to the
   top of the list … Come straight to this list and wondered how I see just the ones that I'm interested
   in as opposed to searching or filtering."

   Declared HERE, in the global sheet, not in the page's own <style> block — see the note above the
   .fx-dg rules: a page-level block cannot use ::deep, and several classes before this one were declared
   somewhere that did nothing and so were invisible for months. */
.fx-dg .fx-pin-col { width: 40px; padding-left: 10px; padding-right: 0; }
.fx-pin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: none;
    border-radius: 6px;
    background: transparent;
    /* Muted until pinned, so an unpinned column does not read as a page full of controls demanding
       attention — it is an affordance, not a status. */
    color: var(--muted-foreground);
    cursor: pointer;
    transition: color 0.12s ease, background 0.12s ease;
}
.fx-pin:hover:not(:disabled) { color: var(--accent-text); background: var(--muted); }
.fx-pin:disabled { cursor: default; opacity: 0.55; }
.fx-pin.is-pinned { color: var(--accent-text); }
/* Keyboard focus must be visible: this is the only control in the row, and the row itself navigates on
   click, so somebody tabbing through needs to see which of the two they are on. */
.fx-pin:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }   /* --gold-deep was 2.3:1 on white (DS8.2) */

/* The pinned block at the top of the list is marked on the ROW as well as in the star, so it reads as
   deliberate rather than as an odd sort order.

   A left accent rather than a background, on purpose. The row-background rules above are
   (0,5,0) — `.fx-dg .mud-table-body .mud-table-row:hover .mud-table-cell` and the :nth-child(even)
   stripe — so a background here would have to match that specificity and would then beat HOVER, taking
   the hover feedback off exactly the rows somebody uses most. An inset box-shadow fights nothing; it is
   the same pattern the estimate-revision list already uses for a selected row. */
.fx-dg .mud-table-body .mud-table-row.fx-dg-row-pinned .mud-table-cell:first-child {
    box-shadow: inset 3px 0 0 0 var(--gold-deep);
}

.fx-grid-subtle { color: var(--muted-foreground); }

/* Buttons — shadcn shapes (rounded-lg, h-8-ish). Base = outline variant;
   fx-btn-gold = the primary CTA (legacy name, accent role); danger = tint. */
/* 56392174 — the 2FA QR code, drawn server-side as an SVG. Bounded so it cannot fill the column on a
   wide screen, and given a white plate because a QR code inverted by a dark brand will not scan. */
.fx-qr { width: 220px; max-width: 100%; margin: 12px 0; padding: 8px; background: #ffffff; border: 1px solid var(--border); border-radius: var(--radius-md); }
.fx-qr svg { display: block; width: 100%; height: auto; }
/* The note under the QR. Defined HERE, beside .fx-qr, and not in a page's own <style> block: that
   habit is what produced four separate layout faults reported on 31/08/2026, where a class looked
   shared because several pages used it and was in fact defined in only one of them. */
.fx-qr-note { max-width: 46rem; margin: 4px 0 12px; padding: 10px 12px; font-size: 0.86rem; line-height: 1.55;
  color: var(--muted-foreground); background: var(--muted);
  border: 1px solid var(--border); border-left: 3px solid var(--brand-accent);
  border-radius: var(--radius-md, 8px); }
.fx-qr-note strong { color: var(--foreground); }

/* 93287299 — the password rules, said before the field rather than after the rejection. Shown on the
   forced-change page and on account settings; the list itself is generated from IdentityOptions. */
.fx-password-rules-intro { margin: 0 0 4px; font-size: 0.82rem; color: var(--muted-foreground); }
.fx-password-rules { margin: 0; padding-left: 18px; font-size: 0.82rem; color: var(--muted-foreground); line-height: 1.6; }

/* Height from --control-h (24/09/2026), the same as every field, so a button stands level with the box
   beside it. MIN-height, not height: a label that wraps on a narrow screen must grow rather than clip. */
/* A partner admin inside a tenant's environment (25/09/2026): a quiet bar at the top of every page naming whose
   environment this is, with the way out. Tokens only, so a tenant's brand recolours it. */
.fx-partner-bar {
    display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    margin: 0 0 16px; padding: 8px 12px; border-radius: var(--r-input);
    background: var(--c-info-bg); color: var(--foreground); border: 1px solid var(--border); font-size: 0.85rem;
}

.fx-btn {
    display: inline-flex; align-items: center; gap: 6px; justify-content: center;
    box-sizing: border-box; min-height: var(--control-h, 32px);
    font-family: var(--font-sans); font-weight: 500; font-size: 0.875rem;
    padding: 0 14px; border-radius: var(--r-input); border: 1px solid var(--border);
    color: var(--foreground); background: var(--background); text-decoration: none; cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.fx-btn:hover { background: var(--muted); color: var(--foreground); }
/* A toggle button showing its current state — the programme's "Whole programme" view, which is either on
   or off and previously gave no sign which. */
.fx-btn.is-on { background: var(--muted); border-color: var(--input); font-weight: 600; }
.fx-btn-gold { background: var(--primary); color: var(--primary-foreground); border-color: transparent; box-shadow: 0 1px 2px rgba(0,0,0,0.08); }
/* Hover derives from the CTA fill (each channel -10%) instead of the near-black oklch() that was here.
   That literal was a shadcn leftover: it made the primary button jump from steel blue to almost black on
   hover, and on a rebranded portal it would have done that whatever the brand's colours were. --cta-hover
   is emitted with the resolved brand, and falls back to the primary here for a page rendered before any
   brand has been read. */
.fx-btn-gold:hover:not(:disabled) { background: var(--cta-hover, var(--primary)); color: var(--primary-foreground); }
/* Citron is the PAGE's primary action (Ink & Limestone). Inside a grid row — a line's Save, a row's Approve — a
   primary button is one of many, and a column of citron buttons turns the accent into wallpaper. So there it
   takes the plain button's look, with ink text so it still reads as the action. (Sweep 24/09/2026: the estimate
   lines showed eleven.) The page header's primary is outside any grid and stays citron. */
table.fx-grid .fx-btn.fx-btn-gold { background: var(--card); color: var(--foreground); border-color: var(--border); box-shadow: none; font-weight: 600; }
table.fx-grid .fx-btn.fx-btn-gold:hover:not(:disabled) { background: var(--muted); color: var(--foreground); }
.fx-btn-danger { background: var(--c-danger-bg); color: var(--destructive); border-color: transparent; }
.fx-btn-danger:hover:not(:disabled) { background: rgba(220,38,38,0.18); color: var(--destructive); }
/* The ARMED half of a two-step destructive action — the click that actually deletes. 9049E7E7 came back
   a second time ("the button is very similar to the original button") because both halves wore
   .fx-btn-danger: the tint says "careful" but it says it identically before and after arming, so the
   second click looks like a retry of the first rather than a different act. This is the fill the
   --destructive token was written for (see its comment: "danger button fill (white text)"), which
   .fx-btn-danger never used. Solid red, white text — nobody presses this one believing it is Edit. */
.fx-btn-danger-solid { background: var(--destructive); color: #fff; border-color: var(--destructive); }
.fx-btn-danger-solid:hover:not(:disabled) { background: #A33023; border-color: #A33023; color: #fff; }
/* Small buttons take the same height — they differ in padding and type size, which is what makes them
   read as secondary — so a row of field, button and small button lines up. */
.fx-btn-sm { box-sizing: border-box; min-height: var(--control-h, 32px); padding: 0 10px; font-size: 0.8rem; }
/* A square button carrying a single glyph — the up/down nudges on a quote line. Defined HERE rather than
   in EstimateDetail's own style block: four faults reported on 31/08/2026 were all classes that looked
   shared because several pages used them and were defined in only one of them.
   min-width equal to the height is what stops the two arrows being different widths, since ↑ and ↓ do not
   measure the same in every face. */
.fx-btn-icon { min-width: var(--control-h, 32px); padding: 0 6px; text-align: center; line-height: 1.2; font-size: 0.85rem; }
/* Lighter still, for a remove control that sits INSIDE a list item rather than in an actions column.
   FCC8A917 was reported twice: .fx-btn-icon cut the padding but kept .fx-btn's border, background and
   radius, so 52 of them still read as 52 boxes stacked down the page — "it's better but it still looks
   wrong, it looks too big". A row-level remove is chrome, not a button: no box at all until you are on
   it, and red on hover so what it does is never in doubt. Colour only on hover keeps the list readable;
   the glyph stays reachable because the hit area is unchanged. */
.fx-btn-bare {
    border: none; background: transparent; padding: 0 4px; min-width: 0;
    color: var(--muted-foreground); font-size: 1rem; line-height: 1;
    font-family: var(--font-sans); cursor: pointer;
    transition: background .12s ease, color .12s ease;
    border-radius: var(--radius-sm, 4px);
}
.fx-btn-bare:hover:not(:disabled) { background: var(--c-danger-bg); color: var(--destructive); }
.fx-btn-bare:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }   /* the accent is a fill — citron is 1.2:1 on white (DS8.2) */
/* Dragging a quote line (CFB4309D, 31/08/2026). The lifted row fades; the row you are over takes a rule
   along its top edge, which is where the dragged line will land — a whole-row highlight reads as "this row
   is selected" rather than "it goes here". In app.css, not a page block, per 31/08/2026. */
.ea-row-dragging > td { opacity: 0.4; }
.ea-row-over > td { box-shadow: inset 0 2px 0 0 var(--brand-accent); }
tr[draggable="true"] { cursor: grab; }
tr[draggable="true"]:active { cursor: grabbing; }
.fx-btn-icon:disabled { opacity: 0.35; cursor: default; }
.fx-btn:disabled { opacity: 0.5; cursor: default; }

.fx-link { color: var(--accent-text); text-decoration: none; font-size: 0.85rem; font-weight: 500; }
.fx-link:hover { text-decoration: underline; }

/* ===========================================================================
   Top navigation (Slice 1 — demo nav.tsx). Text links + grouped dropdowns on
   the white bar. Active = the demo's topButtonClass; dropdown panels = the
   demo's white rounded-xl ring/shadow popover.
   =========================================================================== */
.fx-topnav { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; min-width: 0; }

.fx-top-link {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 7px 12px; border-radius: var(--radius-lg); border: none;
    background: transparent; color: var(--ink-2); text-decoration: none;
    font-family: inherit; font-size: 0.875rem; font-weight: 500; cursor: pointer;
    white-space: nowrap;
    transition: background .12s ease, color .12s ease;
}
.fx-top-link:hover { background: var(--muted); color: var(--foreground); }
/* Active nav = steel-blue primary + white (reserves safety orange for the one CTA;
   the old orange-bg + steel-text combination failed contrast). */
.fx-top-link.on { background: var(--brand-primary); color: #fff; font-weight: 600; }

.fx-top-dd { position: relative; }
.fx-top-chevron { transition: transform .12s ease; }
.fx-top-link.open .fx-top-chevron { transform: rotate(180deg); }

.fx-top-backdrop { position: fixed; inset: 0; z-index: 1400; background: transparent; }

.fx-top-panel {
    position: absolute; left: 0; top: calc(100% + 6px); z-index: 1500;
    min-width: 220px;
    background: var(--popover); border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    box-shadow: 0 10px 30px rgba(0,0,0,0.12);
    overflow: hidden; padding: 5px;
}
.fx-top-panel a {
    display: block; padding: 9px 14px; border-radius: var(--radius-md);
    color: var(--ink-2); text-decoration: none; font-size: 0.875rem;
    transition: background .12s ease, color .12s ease;
}
.fx-top-panel a:hover { background: var(--muted); color: var(--foreground); }
.fx-top-panel a.on { background: rgba(11,120,94,0.12); color: var(--brand-primary); font-weight: 600; }

.fx-empty { text-align: center; padding: 56px 24px; color: var(--muted-foreground); }

/* ===========================================================================
   Master-data detail 360s (Slice 3 — demo customer/supplier detail pages):
   header row, KPI strip, native tab bar (demo shadcn tabs underline), detail
   cards with label/value rows, inline edit form.
   =========================================================================== */
.cd-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin: 10px 0 20px; flex-wrap: wrap; }
/* Reported 08/09/2026 (764A1C26): "On the Project Spend page, the KPI is positioned on the right-hand
   side, whereas on the Cost Events Outstanding page, the KPI is positioned below the subtitle."

   Same page, same CSS — the only difference was the length of the explanation. This row wraps, and the
   title block defaults to min-width:auto, so a two-line subtitle could not shrink and pushed the stat
   card onto the next line instead. Exactly the trap behind the Clear button on the feedback list
   (EEF5B823) one report earlier: a flex item that will not shrink makes the row grow or wrap.

   Letting the title block shrink keeps the card where it is designed to sit, on every metric. The
   640px rule below still gives the card a full-width row on a phone, which is where wrapping is right. */
.cd-head > :first-child { flex: 1 1 320px; min-width: 0; }
.cd-head-actions { display: flex; align-items: center; gap: 10px; }

/* Reported 26/08/2026 ("Design Flaw — can we smarten the total, it looks scrunched in the corners"),
   from /reporting/drill. .cd-head is a wrapping flex row and .fx-stat inherits the default
   `flex: 0 1 auto`, so the headline figure was free to SHRINK: a long money value wrapped inside
   the box and sat hard against its own padding. Pin the box's width instead of letting the number
   fight for room, and keep the figure on one line.

   That fix also right-aligned the text, and THAT part was reported as a fault in its own right
   (2D957C58, 08/09/2026): "the KPI for the total cost on the project spend has the text aligned to the
   right when the other KPI's on other screens have centrally aligned text". The alignment is now left,
   which is what every other .fx-stat in the product does — eleven pages use the card and this was the
   only right-aligned one. Deliberately NOT centred, despite the wording of the report: nothing here is
   centred, and matching the ten would be undone by matching the description.

   The width pin and the nowrap are what actually cured the scrunching, so they stay. */
.cd-head > .fx-stat { flex: 0 0 auto; min-width: 210px; padding: 16px 20px; }
.cd-head > .fx-stat .k-num { white-space: nowrap; }
@media (max-width: 640px) { .cd-head > .fx-stat { min-width: 100%; } }
/* The drill-through table's own total row. It previously wore "dd-rollup dd-annual", whose CSS is
   scoped to DirectorsDashboard.razor and therefore did nothing here — leaving the word and the figure
   in opposite corners with no rule between them and the last data row. Reported twice (F767319C). */
.dt-total > td {
    border-top: 2px solid var(--border);
    background: var(--stat-bg);
    padding-top: 14px;
    padding-bottom: 14px;
    font-weight: 700;
}
/* Reported a THIRD time 31/08/2026: "Can the format match, and ident the totals slightly, it's looks
   untidy." Two things, and the first one was mine. The row had been given small uppercase letter-spaced
   text, which is header styling — so the total sat in a table of sentence-case rows looking like a
   heading that had slipped to the bottom. It now uses the same size and case as the data above it and
   says "Total" in bold, which is the whole difference a total needs.

   The indent is the other half, and it is the original complaint read properly: "scrunched in the
   CORNERS", plural. The label was hard against the left edge and the figure hard against the right, so
   both ends get breathing room rather than just the one. The values stay aligned with the column above —
   the indent is on the total row's outer edges only, not on the numbers, or the figure would no longer
   line up with the column it totals. */
/* Sortable drill headings, and the reference column that was wrapping.

   Reported 01/09/2026 (D90657C3): "the project reference column needs condensing" — an invoice number was
   breaking across two lines as "SIN-" / "000009", which reads as a rendering fault rather than a narrow
   column. nowrap on the first cell stops it; the column is content-width so it costs almost nothing.

   The sort affordance is a caret drawn in the header, not an icon font or an SVG per heading — one rule,
   no assets, and it rotates rather than swapping glyphs so ascending and descending cannot drift apart. */
/* A billing date that has passed with the line still unbilled (397A5691, 01/09/2026). Colour AND weight,
   not colour alone — a red date that is otherwise identical to the others is invisible to anyone who does
   not distinguish red, and this is the cell that says money is late. In app.css, per 31/08/2026. */
/* Qualified to beat `table.fx-grid tbody td`, which sets a colour at specificity (0,1,3) — a bare
   .bs-overdue is (0,1,0) and loses. Reported 01/09/2026 (397A5691): "you said it would turn red... but it
   just made the due date bold", which is precisely what losing only the COLOUR looks like, because nothing
   competes for font-weight. The bug is invisible in the markup and invisible in this file; it only exists
   in the relationship between the two.

   This applies to any rule that colours a `td` DIRECTLY. A span inside the cell is fine — it inherits, and
   inheritance loses to any direct rule on the span. */
table.fx-grid tbody td.bs-overdue { color: var(--destructive); font-weight: 600; }

/* ---- Platform ▸ Test plan ----
   In app.css rather than the page's own style block, per the four faults reported on 31/08/2026 where a
   class looked shared and was declared in one page only. */
/* ---- test-plan console: tabs and the automatic-run panel (05/09/2026) ----------------------------
   The page was reported as confusing: two jobs sharing one screen, and the primary action hidden behind
   a precondition. Tabs separate the jobs; the numbered steps make the run panel readable as a sequence
   rather than as four unrelated controls. */
.tp-tabs { display: flex; gap: 4px; margin: 0 0 14px; border-bottom: 1px solid var(--hair); }
.tp-tab {
    font: inherit; font-size: 0.9rem; font-weight: 600; padding: 9px 18px; cursor: pointer;
    background: none; border: none; border-bottom: 2px solid transparent; color: var(--ink-3);
    margin-bottom: -1px;
}
.tp-tab:hover { color: var(--ink); }
.tp-tab.is-on { color: var(--brand-primary); border-bottom-color: var(--brand-primary); }

.tp-auto { padding: 6px 18px 18px; margin-bottom: 12px; }
.tp-auto-step { display: flex; gap: 14px; padding: 16px 0; border-top: 1px solid var(--hair); }
.tp-auto-step:first-child { border-top: none; }
.tp-auto-num {
    flex: 0 0 26px; height: 26px; border-radius: 50%; margin-top: 2px;
    background: var(--brand-primary); color: #fff;
    display: flex; align-items: center; justify-content: center;
    font-size: 0.8rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.tp-auto-body { flex: 1; min-width: 0; }
.tp-auto-body .fx-h3 { margin: 0 0 10px; }

/* The filter row sits inside a step here, so it drops the panel chrome it has on the by-hand tab. */
.tp-filters-flush { border: none; background: none; box-shadow: none; padding: 0; margin: 0 0 10px; }

.tp-auto-count { margin: 10px 0 0; font-size: 0.95rem; }
.tp-auto-count b { font-size: 1.15rem; font-variant-numeric: tabular-nums; }
.tp-auto-hint { margin: 6px 0 0; font-size: 0.82rem; max-width: 74ch; }
.tp-auto-env { min-width: 320px; max-width: 420px; }
.tp-auto-prepare { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 10px; }
.tp-auto-prepare .fx-grid-subtle { max-width: 62ch; font-size: 0.82rem; }

.tp-runner { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 0.92rem; }
.tp-runner .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.tp-runner-on .dot { background: var(--c-success); }
.tp-runner-off .dot { background: var(--muted-foreground); }

.tp-auto-go {
    display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
    padding: 16px 0 0; border-top: 1px solid var(--hair);
}
.tp-auto-run { font-size: 0.95rem; padding: 10px 22px; }
.tp-auto-note { margin: 12px 0 0; }

/* Raise-as-feedback opt-in. Sits under the Go row rather than beside the button: it changes what a run
   DOES to somebody else's working queue, so it wants reading, not clicking past. */
.tp-auto-raise { display: flex; flex-direction: column; gap: 4px; padding: 12px 0 0; }
.tp-auto-raise label { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; cursor: pointer; }
.tp-auto-raise .fx-grid-subtle { max-width: 68ch; font-size: 0.8rem; }

.tp-auto-results { padding: 14px 18px; margin-bottom: 12px; }
.tp-auto-results-head { display: flex; flex-wrap: wrap; gap: 12px; align-items: baseline; margin-bottom: 8px; }
.tp-auto-results-head .fx-h3 { margin: 0; }

.tp-runbar { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-end; justify-content: space-between; padding: 14px 18px; margin-bottom: 12px; }
.tp-runpick { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; }
.tp-newrun { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.tp-newrun-name { min-width: 240px; }

.tp-field { display: flex; flex-direction: column; gap: 3px; font-size: 0.76rem; color: var(--muted-foreground); }
.tp-field-wide { flex: 1 1 240px; }
.tp-field > select, .tp-field > input { font-size: 0.86rem; }

.tp-tallies { display: flex; flex-wrap: wrap; gap: 12px; align-items: baseline; font-size: 0.82rem; }
.tp-tally { color: var(--muted-foreground); }
/* Weight as well as colour on all three: a run's headline numbers are exactly where colour-only coding
   fails somebody, and pass/fail is the distinction that matters most on the page. */
.tp-pass { color: var(--c-success); font-weight: 600; }
.tp-fail { color: var(--destructive); font-weight: 600; }
.tp-blocked { color: var(--accent-text); font-weight: 600; }

.tp-filters { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-end; padding: 12px 18px; margin-bottom: 10px; }
.tp-count { margin: 0 0 12px; }
.tp-source { margin-top: 10px; font-size: 0.76rem; word-break: break-all; }
.tp-import-msg { margin-top: 6px; font-size: 0.82rem; color: var(--destructive); }
.tp-import-ok { color: var(--muted-foreground); }

.tp-story { margin-bottom: 18px; }
.tp-story-head { padding: 14px 20px 10px; background: var(--card); }
.tp-story-title { font-size: 1.05rem; margin: 0 0 4px; }
.tp-story-narrative { margin: 0 0 4px; color: var(--foreground); font-size: 0.88rem; }
.tp-story-meta { margin: 0; font-size: 0.75rem; color: var(--muted-foreground); display: flex; flex-wrap: wrap; gap: 6px; }
.tp-caveat { margin: 8px 0 0; padding: 8px 10px; font-size: 0.8rem; background: var(--muted);
  border-left: 3px solid var(--brand-accent); border-radius: var(--radius-md, 8px); }

/* The id is a button because it opens the steps. Stripped back to reading as a link, since a real button
   here would compete with Pass/Fail at the other end of the row. */
.tp-idbtn { background: none; border: 0; padding: 0; font: inherit; color: var(--brand-primary);
  cursor: pointer; text-decoration: underline; white-space: nowrap; }
.tp-row-open > td { background: var(--muted); }

/* The story id in its heading: a button that isolates the story, styled as the id it replaces so the
   heading still reads as a heading. */
.tp-storyid { background: none; border: 0; padding: 0; font: inherit; color: var(--brand-primary);
  cursor: pointer; text-decoration: underline; }
.tp-showall { margin-left: 6px; font-size: 0.72rem; font-weight: 400; }
/* The title is the expand control. Styled as the heading it replaces so the card still reads as a card. */
.tp-storytoggle { background: none; border: 0; padding: 0 0 0 6px; font: inherit; color: inherit;
  cursor: pointer; text-align: left; }
.tp-chev { margin-left: 8px; font-size: 0.8rem; color: var(--muted-foreground); }
.tp-story-progress { margin: 4px 0 0; font-size: 0.76rem; display: flex; gap: 6px; flex-wrap: wrap; }

/* ---- Guide Me ---- */
.gm-dialog .gm-gap { padding: 8px 10px; margin-bottom: 12px; font-size: 0.84rem;
  background: var(--muted); border-left: 3px solid var(--brand-accent); border-radius: var(--radius-md, 8px); }
.gm-pre { margin-bottom: 12px; font-size: 0.84rem; }
.gm-pre summary { cursor: pointer; color: var(--muted-foreground); }
.gm-pre ol { margin: 6px 0 0; padding-left: 20px; line-height: 1.5; }

/* The step is the thing being read, so it gets the weight on the dialog. */
.gm-step { padding: 14px 16px; background: var(--muted); border-radius: var(--radius-md, 8px); }
.gm-step-count { margin: 0 0 4px; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted-foreground); }
.gm-step-text { margin: 0; font-size: 1rem; line-height: 1.5; }

.gm-nav { display: flex; align-items: center; gap: 8px; margin: 10px 0 16px; }
.gm-progress { margin-left: auto; font-size: 0.74rem; color: var(--muted-foreground); }

.gm-expected h4 { margin: 0 0 8px; font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted-foreground); }
/* row direction and an explicit box size, for the reason .md-check needed both: a form rule that stretches
   every input turns a checkbox into a full-width bar and pushes its own label onto the next line. */
.gm-check { display: flex; flex-direction: row; align-items: flex-start; gap: 10px; padding: 5px 0;
  font-size: 0.88rem; line-height: 1.45; cursor: pointer; }
.gm-check > input[type="checkbox"] { flex: 0 0 auto; width: 16px; height: 16px; margin: 2px 0 0; }
.gm-tally { margin: 8px 0 0; font-size: 0.76rem; color: var(--muted-foreground); }
.gm-keep { color: var(--accent-text); }

.gm-note { display: flex; flex-direction: column; gap: 4px; margin-top: 16px; font-size: 0.78rem;
  color: var(--muted-foreground); }
.gm-note textarea { font: inherit; font-size: 0.88rem; color: var(--foreground); }

/* The per-expectation marks on the plan row. Glyph AND colour: a row of coloured dots says nothing to
   anyone who does not separate red from green, and this is the detail the walkthrough exists to produce. */
.tp-expect { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 3px; }
.tp-expect-one { font-size: 0.8rem; font-weight: 700; cursor: help; }
.tp-expect-met { color: var(--c-success); }
.tp-expect-missed { color: var(--destructive); }
/* Blocked: never reached, not checked-and-failed. Muted rather than red, because red is a claim. */
.tp-expect-unreached { color: var(--muted-foreground); }
.tp-expect-count { margin-left: 6px; font-size: 0.72rem; color: var(--muted-foreground); }

.tp-gap { margin-left: 8px; padding: 1px 6px; font-size: 0.68rem; border-radius: 999px;
  background: var(--muted); color: var(--muted-foreground); border: 1px solid var(--border); }

.tp-outcome { font-weight: 600; font-size: 0.82rem; }
.tp-outcome-pass { color: var(--c-success); }
.tp-outcome-fail { color: var(--destructive); }
.tp-outcome-blocked { color: var(--accent-text); }
.tp-note { font-size: 0.75rem; color: var(--muted-foreground); margin-top: 2px; max-width: 40rem; }

.tp-pass-btn { border-color: var(--c-success); color: var(--c-success); }
.tp-fail-btn { border-color: var(--destructive); color: var(--destructive); }

.tp-noteform { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 8px 4px; font-size: 0.82rem; }
.tp-noteform input { flex: 1 1 320px; }

.tp-detail { display: flex; flex-wrap: wrap; gap: 28px; padding: 12px 4px; }
.tp-detail-block { flex: 1 1 260px; min-width: 240px; }
.tp-detail-block h4 { margin: 0 0 6px; font-size: 0.76rem; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--muted-foreground); }
.tp-detail-block ol, .tp-detail-block ul { margin: 0; padding-left: 18px; font-size: 0.84rem; line-height: 1.5; }
.tp-detail-meta { flex-basis: 100%; margin: 4px 0 0; font-size: 0.74rem; color: var(--muted-foreground);
  display: flex; flex-wrap: wrap; gap: 6px; }
/* The "include closed" switch beside the pipeline search (EA3346B6, 01/09/2026). Explicit 16px on the box
   for the same reason .md-check needs it: .fx-field input { width: 100% } stretches any input it can
   reach, and a checkbox stretched to the row pushes its own label onto the next line. */
.ol-closed { display: inline-flex; flex-direction: row; align-items: center; gap: 8px; margin-top: 10px;
  font-size: 0.86rem; color: var(--muted-foreground); cursor: pointer; }
.ol-closed > input[type="checkbox"] { flex: 0 0 auto; width: 16px; height: 16px; margin: 0; }
/* Scoped to the drill table, NOT to .fx-grid generally. The first version of this rule applied to every
   grid in the product, and plenty of them carry a long name in the first column — nowrap there would push
   the table wider than its scroller on half the master-data screens. A one-page complaint does not get an
   app-wide rule. */
.dt-grid td:first-child, .dt-grid th:first-child { white-space: nowrap; }
.dt-total > td:first-child { padding-left: 22px; }
.dt-total > td:not(:first-child):not(.num) { font-weight: 500; color: var(--muted-foreground); }

/* BB8A2E0D — an explanatory panel: how a screen works, above the controls it is explaining. Readable
   measure and normal weight, because it is prose to be read once rather than a status to be scanned. */
.fx-note-panel { padding: 18px 22px; margin-bottom: 18px; }
.fx-note-panel .fx-h3 { margin: 0 0 8px; }
.fx-note-panel p { margin: 0 0 8px; max-width: 78ch; font-size: 0.88rem; line-height: 1.55; color: var(--ink-2); }
.fx-note-panel p:last-child { margin-bottom: 0; }

/* 9D5C64A5 — the reason a drill is empty, under the empty state. Normal weight and a readable measure:
   it is an explanation to be read, not a warning to be glanced at. */
.dt-empty-note {
    margin: 10px auto 0; max-width: 52ch; font-size: 0.86rem; line-height: 1.5;
    color: var(--muted-foreground);
}
.dt-total > td.num { font-size: 1.05rem; padding-right: 22px; }

.cd-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 20px; }

.cd-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 18px; }
.cd-tab {
    padding: 8px 14px; border: none; background: transparent; cursor: pointer;
    font-family: inherit; font-size: 0.875rem; font-weight: 500; color: var(--muted-foreground);
    border-bottom: 2px solid transparent; margin-bottom: -1px;
    transition: color .12s ease, border-color .12s ease;
}
.cd-tab:hover { color: var(--foreground); }
.cd-tab.on { color: var(--brand-primary); border-bottom-color: var(--brand-accent); font-weight: 600; }

.cd-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }
.cd-card { padding: 18px 20px; }
.cd-card h3 { margin: 0 0 12px; font-family: var(--font-heading); font-size: 1rem; font-weight: 600; }
.cd-row { display: flex; justify-content: space-between; gap: 16px; padding: 6px 0; font-size: 0.875rem; }
.cd-row-label { color: var(--muted-foreground); flex-shrink: 0; }
.cd-row-value { text-align: right; min-width: 0; overflow-wrap: anywhere; }   /* a long unbroken value wraps inside the card */
.cd-notes-text { margin: 0; font-size: 0.875rem; color: var(--ink-2); white-space: pre-wrap; }

.cd-edit { padding: 18px 20px; margin-bottom: 20px; }
.cd-edit h3 { margin: 0 0 12px; font-family: var(--font-heading); font-size: 1rem; font-weight: 600; }
/* align-items: end is the fix for a reported defect (77BDD542, 21/08/2026): "The order number isn't
   aligned with the other options and just needs tweaking slightly so it's clean and tidy."

   The label is a flex COLUMN — caption above input — and every cell in a grid row shares that row's
   height. "Order number (the client's)" is long enough to wrap onto a second line at this column width,
   which made its cell taller and pushed its input DOWN relative to Quoted on and Order received beside
   it. Bottom-aligning the cells lines the inputs up whatever the caption does, so the same thing cannot
   happen again the next time somebody writes a two-word label. Global, because .cd-form is on every
   project/master-data edit form and the next long label will be on a different one. */
.cd-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; align-items: end; }
.cd-form label { display: flex; flex-direction: column; gap: 4px; font-size: 0.78rem; color: var(--ink-2); font-weight: 500; }
/* The caption grows upward from the field rather than pushing it down. */
.cd-form label > input, .cd-form label > select, .cd-form label > textarea { margin-top: auto; }
.cd-form input {
    font-family: var(--font-sans); font-size: 0.9rem; color: var(--foreground);
    background: var(--field-bg); border: 1px solid var(--input); border-radius: var(--r-input);
    padding: 8px 11px; outline: none;
}
.cd-form input:focus { border-color: var(--ring); box-shadow: 0 0 0 3px oklch(0.708 0 0 / 0.5); }
.cd-form .cd-notes { grid-column: 1 / -1; }
.cd-edit-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 14px; }
/* cd-form select — global, matches cd-form input (was page-local in ProjectDetail). */
.cd-form select {
    font-family: var(--font-sans); font-size: 0.9rem; color: var(--foreground);
    background: var(--field-bg); border: 1px solid var(--input); border-radius: var(--r-input);
    padding: 8px 11px; outline: none; width: 100%;
}
.cd-form select:focus { border-color: var(--ring); box-shadow: 0 0 0 3px oklch(0.708 0 0 / 0.5); }

/* ---------------------------------------------------------------------------
   md-form fields — the master-data editors (customers, suppliers, cost codes,
   catalogue, rates, materials, assets, party links, finance reference).

   GLOBAL on purpose. Each of those nine pages previously carried its own copy of
   this rule with the pre-re-skin dark-theme values (a white-alpha fill over a
   white-alpha border). Against the concrete/white surfaces this palette actually
   uses, a 15%-white border on a white card is INVISIBLE — the fields had no edge
   at all, so "there is no outline of the boxes that need filling in" (reported on
   the new-customer form). Nine copies is also how the next re-skin reintroduces
   the same bug nine times, so the rule lives here and matches .cd-form/.fx-field.
   --------------------------------------------------------------------------- */
.md-form input, .md-form select, .md-form textarea {
    font-family: var(--font-sans); font-size: 0.88rem; color: var(--foreground);
    background: var(--field-bg); border: 1px solid var(--input); border-radius: var(--r-input);
    padding: 7px 10px; outline: none;
    transition: border-color .12s ease, box-shadow .12s ease;
}
.md-form input::placeholder, .md-form textarea::placeholder { color: var(--muted-foreground); }
.md-form input:focus, .md-form select:focus, .md-form textarea:focus {
    border-color: var(--ring); box-shadow: 0 0 0 3px oklch(0.708 0 0 / 0.5);
}
/* A checkbox must keep its native box — the field styling above would erase it. */
.md-form input[type="checkbox"] {
    width: auto; padding: 0; box-shadow: none; background: none; border: none;
}

/* Shared 4-up stat grid (Home dashboard, partner workspace). */
.fx-stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 16px; }
@media (max-width: 980px) { .fx-stat-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .fx-stat-grid { grid-template-columns: 1fr; } }

/* ============================================================================
   D23 partner workspace — DEFINED console cards.
   The shadcn default (--shadow:none, white card on a white page, no card padding)
   leaves card edges invisible and content flush to the border. These pages are dense
   admin consoles, so partner cards get a FINDABLE edge (stronger border + a gentle
   elevation), real PADDING, and header BANDS that separate title from data.
   Candidate to promote app-wide + fold into the styleguide once signed off.
   ============================================================================ */
.pt-crumb { font-size: 0.82rem; color: var(--muted-foreground); margin-bottom: 14px; }
.pt-crumb a { color: var(--ink-2); text-decoration: none; }
.pt-crumb a:hover { color: var(--accent-text); }
.pt-crumb span { margin: 0 6px; }
.pt-kicker { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--brand-secondary); margin: 0 0 2px; }

/* Shared console-card shell — findable edge + padding (the fix for "no edges / no padding"). */
.pt-hero, .pt-card, .pt-create-panel {
    background: var(--card);
    border: 1px solid var(--hair-2);
    border-radius: var(--r-card);
    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 10px 24px -12px rgba(16, 24, 40, 0.18);
    padding: 20px 22px;
}
.pt-create-panel { margin-bottom: 16px; }
/* Stat cards get the same stronger edge + lift (later than .fx-stat, so it wins). */
.pt-stat { border: 1px solid var(--hair-2); box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06); }

.pt-hero { margin-bottom: 16px; position: relative; overflow: hidden; }
.pt-hero.is-suspended::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--destructive); }
.pt-hero-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.pt-identity { display: flex; align-items: center; gap: 14px; }
.pt-monogram { width: 48px; height: 48px; border-radius: 12px; flex-shrink: 0; display: grid; place-items: center; background: var(--brand-primary); color: #fff; font-family: var(--font-heading); font-weight: 800; font-size: 1.3rem; }
.pt-title-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.pt-num-sub { color: var(--muted-foreground); font-weight: 600; font-size: 1rem; }
.pt-num-word { font-size: 1.2rem; }
.pt-warn { color: var(--c-warning); }
.pt-suspend-note { margin: 14px 0 0; padding: 8px 12px; font-size: 0.82rem; background: var(--c-danger-bg); color: var(--c-danger); border-radius: var(--radius-md); }

/* Two independent columns — each packs its own cards (no cross-column void). */
.pt-cols { display: flex; gap: 16px; align-items: flex-start; }
.pt-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 16px; }
@media (max-width: 900px) { .pt-cols { flex-direction: column; } }

/* Header BAND — bled to the card edges (negative margin cancels card padding), faint
   tint + divider + a bold Montserrat heading, so the header clearly outranks the data. */
.pt-panel-head {
    display: flex; align-items: center; gap: 9px;
    margin: -20px -22px 18px; padding: 13px 22px;
    background: var(--muted); border-bottom: 1px solid var(--hair-2);
    border-radius: var(--r-card) var(--r-card) 0 0; color: var(--brand-primary);
}
.pt-panel-head h3 { font-family: var(--font-heading); font-weight: 700; font-size: 1.02rem; color: var(--ink); margin: 0; }
.pt-ico { flex-shrink: 0; }
.pt-head-meta { margin-left: auto; font-size: 0.8rem; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }

.pt-actions { display: flex; gap: 10px; margin-top: 16px; }
.pt-env-add, .pt-user-add { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--hair); }
.pt-hint { font-size: 0.8rem; color: var(--muted-foreground); margin: 8px 0 0; }

/* Internal list dividers use the FAINT hairline (--hair); the card border (--hair-2) is the strong edge. */
.pt-licence-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--hair); }
.pt-licence-row:last-of-type { border-bottom: none; }
.pt-licence-text { min-width: 0; }
.pt-licence-name { margin: 0; font-weight: 600; font-size: 0.9rem; color: var(--foreground); }
.pt-licence-blurb { margin: 0; font-size: 0.78rem; color: var(--muted-foreground); }
.pt-licence-controls { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.pt-required { font-size: 0.78rem; }

.pt-env-list, .pt-user-list { display: flex; flex-direction: column; }
.pt-env-row { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--hair); font-size: 0.86rem; }
.pt-env-row:last-child { border-bottom: none; }
.pt-env-name { font-weight: 500; color: var(--foreground); flex: 1; }
.pt-env-meta { font-size: 0.78rem; color: var(--muted-foreground); }
.pt-env-enter { flex: none; }   /* Enter — a partner works inside the environment (25/09/2026) */
.pt-user-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--hair); }
.pt-user-row:last-child { border-bottom: none; }
.pt-user-avatar { width: 28px; height: 28px; border-radius: 999px; flex-shrink: 0; display: grid; place-items: center; background: var(--brand-primary); color: #fff; font-size: 0.74rem; font-weight: 700; font-family: var(--font-heading); }
.pt-user-email { font-size: 0.86rem; color: var(--ink-2); flex: 1; }
.pt-user-role { flex-shrink: 0; }

.pt-cred { display: flex; gap: 10px; align-items: flex-start; margin-top: 16px; padding: 12px 14px; background: var(--c-success-bg); border-left: 3px solid var(--brand-secondary); border-radius: var(--radius-md); color: var(--c-success); }
.pt-cred-title { margin: 0 0 2px; font-size: 0.76rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
.pt-cred-body { margin: 0; font-size: 0.85rem; color: var(--ink-2); font-family: var(--font-mono); overflow-wrap: anywhere; }

.pt-env-use { display: flex; align-items: center; gap: 8px; min-width: 120px; }
.pt-env-track { flex: 1; height: 6px; max-width: 80px; background: var(--muted); border-radius: 999px; overflow: hidden; }
.pt-env-fill { display: block; height: 100%; background: var(--brand-secondary); border-radius: 999px; }
.pt-env-count { font-size: 0.8rem; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }

.pt-empty { text-align: center; padding: 48px 24px; color: var(--muted-foreground); }
.pt-empty svg { color: var(--brand-primary); opacity: 0.35; margin-bottom: 10px; }
.pt-empty-title { font-family: var(--font-heading); font-weight: 600; font-size: 1.05rem; color: var(--foreground); margin: 0 0 4px; }

.pt-hero, .pt-card { animation: pt-rise 0.34s ease both; }
.pt-col:nth-child(1) .pt-card:nth-child(2) { animation-delay: 0.06s; }
.pt-col:nth-child(2) .pt-card:nth-child(1) { animation-delay: 0.06s; }
.pt-col:nth-child(2) .pt-card:nth-child(2) { animation-delay: 0.12s; }
@keyframes pt-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .pt-hero, .pt-card { animation: none; } }

/* Shared horizontal bar list (Directors' dashboard breakdowns, Practice Overview
   utilisation) — single-hue proportional bars with text values (dataviz: text wears
   text tokens; the bar carries magnitude only). */
/* Reported 08/09/2026 (BF5FC7E3): "when a value is labelled as 0, the positioning of the label affects the
   alignment of the bars and surrounding text, making the chart look uneven."

   The third column was `auto` — sized to its own content — and every ROW is its own grid. So "0% (0/0h)"
   produced a narrower value column than "200% (45/23h)", the 2fr track absorbed the difference, and that
   one row's bar started and ended somewhere else with its value pushed right. Nothing to do with zero as
   such: any short value did it.

   A fixed value column makes every row's geometry identical, which is what "positioned independently"
   means. 150px comfortably holds the widest real content on both surfaces that use this row — the
   utilisation figure with its hours, and the Directors' dashboard's money — and the dashboard had the same
   fault waiting (£0 beside £87,500), so this is fixed at the rule rather than on the page that noticed. */
.dd-bar-row { display: grid; grid-template-columns: minmax(110px, 1fr) 2fr 150px; align-items: center; gap: 10px; padding: 5px 0; font-size: 0.85rem; }
.dd-bar-label { color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-bar-track { height: 10px; background: var(--muted); border-radius: 4px; overflow: hidden; }
.dd-bar-fill { display: block; height: 100%; background: #007fa3; border-radius: 4px; }
.dd-bar-value { color: var(--foreground); font-weight: 500; }

/* PRD 18 §18.2 — drill-through affordances. Every clickable figure looks clickable; a number that
   opens a record list but reads as plain text is a feature nobody finds. */
a.dd-drill { color: inherit; text-decoration: none; cursor: pointer; }
a.dd-drill:hover { text-decoration: underline; text-underline-offset: 3px; }
a.fx-stat.dd-drill:hover { border-color: var(--gold); }
td .dd-drill { display: inline-block; }

.dd-half { font-weight: 600; }
.dd-clarity { margin-top: 18px; padding: 14px 16px; }
.dd-clarity summary { cursor: pointer; font-weight: 600; }
.dd-defs { margin: 12px 0 0; display: grid; grid-template-columns: minmax(140px, 220px) 1fr; gap: 6px 16px; font-size: 0.85rem; }
.dd-defs dt { color: var(--foreground); font-weight: 600; }
.dd-defs dd { margin: 0; color: var(--ink-2); }
.dd-def { display: block; margin-top: 4px; font-size: 0.72rem; font-weight: 400; color: var(--ink-2); }

/* §18.1 — the reporting landing page: one block per financial year, buttons through to each area. */
.rh-years { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.rh-year { padding: 18px 20px; }
.rh-year-title { margin: 0 0 12px; font-size: 1.05rem; display: flex; align-items: center; gap: 8px; }
.rh-chip { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; padding: 2px 8px; border-radius: 999px; background: var(--muted); color: var(--ink-2); }
.rh-chip-next { background: transparent; border: 1px solid var(--muted); }
.rh-buttons { display: flex; flex-wrap: wrap; gap: 8px; }

/* ===========================================================================
   Dialogs — MudDialog provides the shell (radius via --r-modal + the flat
   overlay above); these style title, form fields and notes on the light card.
   =========================================================================== */
.fx-dialog-head { display: flex; flex-direction: column; gap: 4px; }
.fx-dialog-title { font-family: var(--font-heading); font-weight: 600; font-size: 1.25rem; color: var(--foreground); margin: 0; }
.fx-dialog-sub { font-size: 0.88rem; color: var(--ink-2); margin: 0; }
.fx-dialog-note { font-size: 0.8rem; color: var(--muted-foreground); margin: 0; }

.fx-form { display: flex; flex-direction: column; gap: 16px; min-width: min(440px, 80vw); padding-top: 4px; }
.fx-field { display: flex; flex-direction: column; gap: 6px; }
.fx-field label { font-size: 0.78rem; color: var(--ink-2); font-weight: 500; }
.fx-field .fx-optional { color: var(--muted-foreground); font-weight: 400; }
/* A one-line explanation under a field — "carried through from the enquiry", "no active streams are
   configured". Used by the accept-estimate dialog and declared nowhere, so those lines rendered as ordinary
   body paragraphs with default margins, reading as content rather than as a hint. */
.fx-field-hint { font-size: 0.76rem; color: var(--muted-foreground); margin: 2px 0 0; line-height: 1.4; }
/* --- A tick box inside .fx-field (5A333A97, 02/09/2026) ---------------------------
   "When subscribing to webhooks the design and click boxes aren't lined up."

   `.fx-field input` below sets width:100%, padding and a border — and reaches the CHECKBOXES in the
   webhook event list and the API-key scope list, stretching each one across the row and pushing its own
   label onto the next line. app.css already carries a comment naming this exact trap on `.ol-closed`, and
   `.md-form` has the same exemption; `.fx-field` never got one, so every tick box in a dialog was wrong.

   The row itself is shared here too — it was defined identically inside two dialogs and nowhere else. */
.fx-field input[type="checkbox"],
.fx-field input[type="radio"] {
    width: auto;
    flex: 0 0 auto;
    padding: 0;
    margin: 0;
    border: none;
    background: none;
    box-shadow: none;
}

.fx-check {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 0;
    cursor: pointer;
    font-weight: 400;
}

/* A value shown once and never again — an issued key, a signing secret. */
.fx-pw-ok {
    padding: 12px 14px;
    border-radius: 12px;
    background: rgba(74, 222, 128, 0.12);
    border: 1px solid rgba(74, 222, 128, 0.42);
    color: var(--ink);
    font-family: var(--font-mono, ui-monospace, Menlo, monospace);
    font-size: 0.82rem;
    word-break: break-all;
}

.fx-field input, .fx-field select {
    font-family: var(--font-sans); font-size: 0.9rem; color: var(--foreground);
    background: var(--field-bg); border: 1px solid var(--input); border-radius: var(--r-input);
    padding: 9px 12px; outline: none; width: 100%;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
    transition: border-color .12s ease, box-shadow .12s ease;
}
.fx-field input::placeholder { color: var(--muted-foreground); }
.fx-field input:focus, .fx-field select:focus { border-color: var(--ring); box-shadow: 0 0 0 3px oklch(0.708 0 0 / 0.5); }

/* ---- .fx-input — the field styling as a class, for a field outside .fx-field ----
   Used on four pages and, like .fx-form-row and .fx-h3, declared nowhere. Most of those uses happen to sit
   inside a .fx-field and were carried by the rules above, which is exactly why it went unnoticed. Two are
   not: the amount box on the project billing form (in a table cell) and the rename box on the programme,
   both of which rendered with no border, no padding and no focus ring — an editable money field with no
   visible edge, on the screen a reported pence defect was raised from. */
.fx-input {
    font-family: var(--font-sans); font-size: 0.9rem; color: var(--foreground);
    background: var(--field-bg); border: 1px solid var(--input); border-radius: var(--r-input);
    padding: 9px 12px; outline: none; width: 100%;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
    transition: border-color .12s ease, box-shadow .12s ease;
}
.fx-input::placeholder { color: var(--muted-foreground); }
/* A clear focus outline: the field's own edge goes to the brand dark and a soft halo of the same colour sits
   outside it, so the focused field is unmistakable without borrowing the accent. */
.fx-input:focus { border-color: var(--brand-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-primary) 14%, transparent); }
.fx-input:disabled { background: var(--muted); color: var(--muted-foreground); }
.fx-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

/* ---- .fx-form-row — a row of fields filling a panel ----
   The other half of the 22/08/2026 styling report: "the words and drop downs all need indenting."

   This class was used on three pages (project Delivery, Billing and Resourcing) and declared NOWHERE, so
   it did nothing at all. `.fx-panel` deliberately carries no padding — it is a bare surface, and pages
   that need padding add their own class — so an undeclared row class left the labels and selects hard
   against the panel's border with no inset, which is what was reported. On Delivery it was worse than
   cosmetic: the div also carries `.fx-form`, a flex COLUMN, so the four fields of "Add a milestone"
   stacked full-width down the page instead of sitting in a row.

   Two classes in the selector (`.fx-form.fx-form-row`) rather than one, so it beats `.fx-form`'s
   flex-direction whichever order the two rules end up in. Equal specificity losing to source order is
   the trap the ShellSidebar/MainLayout shells hit. */
.fx-form-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 16px;
    padding: 18px 20px;
}
.fx-form.fx-form-row { flex-direction: row; min-width: 0; padding-top: 18px; }
/* Fields share the row and wrap rather than squeezing below a usable width. */
.fx-form-row > .fx-field { flex: 1 1 200px; min-width: 0; }
/* A field holding only a button aligns with the inputs beside it rather than stretching. */
.fx-form-row > .fx-field > button { flex: 0 0 auto; }
@media (max-width: 520px) { .fx-field-row { grid-template-columns: 1fr; } }
.fx-form-error {
    font-size: 0.84rem; color: var(--destructive); background: var(--c-danger-bg);
    border: 1px solid rgba(220,38,38,0.25); border-radius: var(--r-input); padding: 9px 12px;
}

/* ---------------------------------------------------------------------------
   Environment pills + dots (D23 §8).
   GLOBAL on purpose: AdminEnvironments and PartnerTenantView render these, and a
   partner / platform-owner principal is in NO environment, so the header badge
   component is not rendering on exactly those pages. A component <style> block only
   exists while its component renders — keeping these here is what stops those pills
   from losing their styling.
   --------------------------------------------------------------------------- */
.fx-env-badge {
    font-size: 0.66rem; font-weight: 700; letter-spacing: 0.05em;
    padding: 2px 6px; border-radius: 999px;
}
.fx-env-badge.fx-env--live { background: rgba(220,38,38,0.12); color: #dc2626; }
.fx-env-badge.fx-env--uat { background: rgba(3,105,161,0.12); color: #0369a1; }
.fx-env-badge.fx-env--test { background: rgba(180,83,9,0.12); color: #b45309; }
.fx-env-badge.fx-env--sandbox { background: rgba(124,58,237,0.12); color: #7c3aed; }

.fx-env-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.fx-env-dot.fx-env--live { background: #dc2626; }
.fx-env-dot.fx-env--uat { background: #0369a1; }
.fx-env-dot.fx-env--test { background: #b45309; }
.fx-env-dot.fx-env--sandbox { background: #7c3aed; }

/* LE-WC-4 — "no legal entity assigned" block (replaces page content, not an inline banner). */
.fx-nocompany {
    max-width: 44rem; margin: 8vh auto; padding: 28px 32px;
    background: var(--card); border: 1px solid var(--border);
    border-radius: var(--radius-lg); text-align: left;
}
.fx-nocompany h1 {
    font-family: var(--font-display, inherit); font-size: 1.4rem; font-weight: 700;
    margin: 0 0 12px; color: var(--foreground);
}
.fx-nocompany p { margin: 0 0 10px; color: var(--ink-2); line-height: 1.6; }
.fx-nocompany .fx-nocompany-sub { font-size: 0.86rem; color: var(--muted-foreground); margin-bottom: 0; }

/* PRD 18 §20.6 (REQ-195) — contacts edited in the box where they are shown. The affordance sits on the
   card, so nobody has to leave the surface to correct a phone number. */
.cd-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.cd-inline-edit {
    background: transparent; border: 1px solid var(--muted); border-radius: 999px;
    padding: 2px 12px; font: inherit; font-size: 0.76rem; color: var(--ink-2); cursor: pointer;
}
.cd-inline-edit:hover { border-color: var(--brand-primary); color: var(--brand-primary); }
.cd-inline-form { display: grid; gap: 8px; margin-top: 10px; }
.cd-inline-form label { display: flex; flex-direction: column; gap: 3px; font-size: 0.78rem; color: var(--ink-2); }
.cd-inline-form input { padding: 6px 9px; border: 1px solid var(--muted); border-radius: 8px; font: inherit; }

/* PRD 18 §20.5 (REQ-194) — sortable column headings on the hand-built grids. */
.fx-sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.fx-sortable:hover { color: var(--brand-primary); }
.fx-sortable.on { color: var(--brand-primary); font-weight: 600; }

/* Brand logo upload (A34505A3). The URL box and the file picker sit on one line so uploading reads as
   the primary path and pasting a link as the alternative, which is the way round the report implies. */
.ab-logo-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.ab-logo-row > input[type="text"] { flex: 1 1 280px; min-width: 0; }
.ab-logo-preview { display: inline-flex; align-items: center; justify-content: center;
    margin-top: 8px; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--r-card);
    background: #fff; }
/* The dark-surface mark is usually white, so it needs a dark ground to be visible at all. */
.ab-logo-preview.ab-on-dark { background: var(--brand-navy, #0a2240); }
.ab-logo-preview img { max-height: 46px; max-width: 260px; display: block; }
.ab-upload-error { color: var(--danger, #b3261e); font-size: 0.85rem; margin: 6px 0 0; }

/* A checkbox and its label on ONE line. Reported 31/08/2026 (73C647ED): "tick box doesn't line up next
   to the entity to assign to an individual" — in Manage companies the box sat ABOVE the company name.
   .md-check was only ever styled as ".md-form .md-check" inside individual master-data pages' SCOPED
   blocks, so anywhere else it fell back to the surrounding column layout. That is the second class
   today found doing nothing outside the page it was written on; the pages that already style it use a
   more specific selector and still win. */
.md-check {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
}
/* The checkbox must also be told it is NOT a text field. ".fx-field input { width: 100% }" applies to
   every input inside a field, checkbox included — so the box stretched the full row and pushed its own
   label onto the next line. That, not the flex direction, was what put the tick above the name. */
.md-check > input[type="checkbox"] {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    margin: 0;
    padding: 0;
    box-shadow: none;
}

/* ---- Detail-page header block -------------------------------------------------------------------
   Reported 31/08/2026 (D3747BC3), twice: "nothing has been indented within the delivery plan section
   or the add a milestone section", and the elements should match "the first section of the page where
   it says Harrison Blake Properties Phase Two".

   The cause was not the delivery page. These classes were written into individual pages' SCOPED style
   blocks — EstimateDetail, ProjectPurchaseOrders and ProjectCis each carry their own copy — so on a
   page that used them without also defining them (ProjectDelivery, Authorisations) they matched
   nothing and the content sat hard against the card edge with no padding at all. .fx-panel itself sets
   no padding, so the class was the only thing providing it.

   Promoted here so a shared-looking class is actually shared. The pages carrying their own copies are
   unaffected: identical rules, and scoped selectors win regardless. This is the third report today
   with the same root cause, the others being the drill-through total and the Manage companies
   checkbox — worth doing a sweep for other classes in the same position. */
.fx-detail-head { padding: 24px 26px; }
.fx-detail-head-main { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.fx-detail-head-actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; align-self: flex-start; }
/* Sized by HOW MANY stats there are, not by a number written down. `repeat(3, auto)` forced every page
   with four or five to redefine the whole rule, and each redefinition carried its own gap and margin —
   which is how 32px, 40px and 28px ended up on three different detail pages. grid-auto-flow does the same
   job for any count, so there is nothing left to override. */
.fx-detail-stats {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: auto;
    gap: 32px;
    margin-top: 22px;
}

/* Resolution S5-T07 — the authorisations queue as cards on a phone; the grid above 720px is unchanged. */
.ap-cards { display: none; }
.ap-card { background: var(--surface); border: 1px solid var(--hair); border-radius: 14px; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.ap-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.ap-card-title { font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.ap-card-value { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); white-space: nowrap; }
.ap-card-subject { color: var(--ink-2); overflow-wrap: anywhere; }
.ap-card-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; }
.ap-card-facts dt { font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); }
.ap-card-facts dd { margin: 2px 0 0; color: var(--ink); overflow-wrap: anywhere; }
.ap-card-actions { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }

@media (max-width: 720px) {
    .fx-detail-stats { grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); gap: 18px; }
    .ap-cards { display: grid; gap: 12px; }
    .ap-desktop { display: none; }
}
.fx-mini { display: flex; flex-direction: column; gap: 4px; }
.m-label { font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
.m-num { font-family: var(--font-heading); font-size: 1.45rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--ink); }
.m-num.gold { color: var(--accent-text); }

/* The panel wrapping "Add a milestone" and its like: a heading hard against the edge is the same
   defect in a different place, so give a bare panel the same inset its neighbours have. */
/* --- The heading-pads-itself mechanism, and why it is now scoped ---------------------------------
   These rules pad a panel's HEADING and body when the panel itself carries no padding. They exist
   because `.fx-panel` deliberately has none: a panel wrapping a full-bleed grid wants its table to
   reach the edges.

   Scoped `:not(.fx-pad):not(.fx-pad-sm)` on 08/09/2026, when the same complaint arrived for the fifth
   time and the 21 panels that wanted padding were given `fx-pad` outright. Two mechanisms for one job
   is what produced the half-styled look Charlotte kept reporting — an indented heading over content
   hard against the edge — so the special case now yields to the panel's own padding rather than
   stacking 26px on top of it. */
.fx-panel:not(.fx-pad):not(.fx-pad-sm) > .fx-h3:first-child { padding: 18px 26px 0; margin-bottom: 0; }
/* Reported 08/09/2026 (F18EB070): "There is no space between the edge of these tables and the content",
   on the New stage template and Import from CSV panels.

   .fx-panel carries no padding of its own (a panel wrapping a grid wants none), so a panel opening with a
   section heading pads the HEADING here and then had to pad its body separately — and the body rule named
   `.fx-form` only, and only as an IMMEDIATE sibling. Every panel using `.md-form` instead got an indented
   heading above content sitting hard against the edge, which is why the heading looked deliberate and the
   form looked broken. The general sibling selector also reaches the body of the CSV panel, where a
   paragraph of explanation sits between the heading and the form. Three panels were affected across
   task templates and staff. */
.fx-panel:not(.fx-pad):not(.fx-pad-sm) > .fx-h3:first-child ~ .fx-form,
.fx-panel:not(.fx-pad):not(.fx-pad-sm) > .fx-h3:first-child ~ .md-form { padding: 14px 26px 20px; }
.fx-panel:not(.fx-pad):not(.fx-pad-sm) > .fx-h3:first-child ~ .fx-page-sub { padding: 0 26px; }

/* --- Quote revision strip (B59F6DC6, 01/09/2026) ---------------------------------
   Reported: "I've revised a quote but I can't go back to a previous revision to either view it or
   accept it." The strip lists every revision beneath the quote header.

   Declared HERE and not in EstimateDetail.razor's style block: four faults reported on 31/08/2026 all
   came from classes that read as shared and were defined in one page only. */
.ed-revisions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 18px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
}

.ed-rev-label {
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-3);
    margin-right: 4px;
}

.ed-rev {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 6px 12px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--card);
    color: var(--ink-2);
    text-decoration: none;
    font-size: 0.84rem;
    transition: border-color 0.12s ease, background 0.12s ease;
}

.ed-rev:hover { border-color: var(--gold-deep); background: var(--muted); }

/* The revision on screen. A left gold bar as well as the border, so which one is current does not rest
   on a single hue — the same reason the status chips carry a dot. */
.ed-rev.is-current {
    border-color: var(--gold-deep);
    background: var(--muted);
    box-shadow: inset 3px 0 0 0 var(--gold-deep);
    cursor: default;
}

.ed-rev-no { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.ed-rev-val { font-variant-numeric: tabular-nums; color: var(--ink-3); }

.ed-rev-note {
    margin-top: 12px;
    padding: 10px 14px;
    border-radius: 8px;
    background: var(--muted);
    border-left: 3px solid var(--gold-deep);
    font-size: 0.85rem;
    color: var(--ink-2);
}

/* --- Billing client picker on the project overview (500C54B1, 01/09/2026) --------
   The Customer row edits in place rather than opening a dialog: it is one field, and a modal for one
   select is the kind of ceremony that stops people correcting a record at all. */
.cd-customer-pick { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* The customer picker (24/09/2026). Sized only — the box itself is an .fx-input, so it takes the house
   field look and the one control height rather than a rule of its own. */
.cd-customer-picker {
    min-width: 220px;
}

/* The quiet "Change" affordance beside a value. Deliberately low-contrast until hovered — the row is a
   fact first and an editable field second. */
.cd-row-edit {
    margin-left: 10px;
    padding: 0;
    border: none;
    background: none;
    color: var(--ink-3);
    font-family: inherit;
    font-size: 0.78rem;
    text-decoration: underline;
    cursor: pointer;
}

.cd-row-edit:hover { color: var(--accent-text); }


/* --- Drill-through project column (D90657C3, 01/09/2026) -------------------------
   "The project reference column needs condensing. There is a test project with a long name."
   One long name was stretching the column across the table and squeezing everything else.

   Truncated rather than wrapped: the rows are a scannable list, and a wrapped cell makes one row
   twice the height of its neighbours, which is harder to read than an ellipsis. The full name stays
   reachable — the cell carries it as title text. */
table.fx-grid td.dt-proj {
    max-width: 260px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The first column is nowrap (line 782), so a long project name in column two had nothing to give.
   This lets the name be the thing that yields rather than the reference. */
@media (max-width: 1200px) {
    table.fx-grid td.dt-proj { max-width: 170px; }
}

/* --- Field hint under an authoring input (CF93E9DA, 01/09/2026) ------------------
   The header markup box now resets every section and line beneath it, so the box has to say so before
   somebody types into it rather than only in the confirmation afterwards. In app.css, not the page's
   scoped block — the same rule that four faults on 31/08 came from breaking. */
.ea-hint {
    display: block;
    margin-top: 4px;
    font-size: 0.74rem;
    line-height: 1.35;
    color: var(--ink-3);
    font-weight: 400;
}

/* --- API key help (EF844B33, 31/08/2026) -----------------------------------------
   "Unable to Test — please provide an example of how this can be tested." The page issued keys and never
   said what to call with one. */
.ak-help { margin-top: 26px; padding: 22px 24px; }
.ak-help-title { font-family: var(--font-heading); font-size: 1.05rem; font-weight: 600; color: var(--ink); margin: 0 0 10px; }
.ak-help-sub { font-size: 0.82rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); margin: 20px 0 10px; }
.ak-help-line { font-size: 0.86rem; line-height: 1.55; color: var(--ink-2); margin: 0 0 12px; }

/* The runnable example. Scrolls rather than wraps: a wrapped curl line copies with a newline in the
   middle of the key, which is exactly the kind of thing that produces a 401 nobody can explain. */
.fx-ak-curl {
    margin: 0 0 12px;
    padding: 12px 14px;
    border-radius: 10px;
    background: var(--muted);
    border: 1px solid var(--border);
    color: var(--ink);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 0.78rem;
    line-height: 1.5;
    overflow-x: auto;
    white-space: pre;
}

/* The result of the live test on the issue dialog. */
.fx-ak-test {
    margin-top: 10px;
    padding: 10px 12px;
    border-radius: 10px;
    font-size: 0.82rem;
    line-height: 1.45;
}

.fx-ak-test.is-ok { background: rgba(74, 222, 128, 0.12); border: 1px solid rgba(74, 222, 128, 0.42); color: var(--ink); }
.fx-ak-test.is-bad { background: rgba(192, 57, 43, 0.10); border: 1px solid rgba(192, 57, 43, 0.38); color: var(--ink); }

/* --- Overdue rows on My Work -----------------------------------------------------
   Same defect as .bs-overdue above, found while adding assigned activities (47C44B8D) and present in two
   places that had already shipped: `.mw-overdue` is (0,1,0) and loses the colour to
   `table.fx-grid tbody td` at (0,1,3), so those cells rendered BOLD BUT NOT RED — which is precisely what
   was reported on the billing screen as 397A5691.

   It also used `var(--danger, …)`, and --danger is not a token this product defines, so it silently took
   the hardcoded fallback and would not follow a tenant's brand. --destructive is the real one. */
table.fx-grid tbody td.mw-overdue { color: var(--destructive); font-weight: 600; }

/* --- Invoice picked out by the drill-through (63312631, 02/09/2026) --------------
   "I've clicked an invoice and it's nto being found, but when i look at the project, I can see it is
   there." The link was dead; it now lands on the billing page, and this marks the row that was clicked so
   a page of a dozen invoices still answers which one. */
table.fx-grid tbody tr.pb-highlight > td {
    background: var(--muted);
    box-shadow: inset 3px 0 0 0 var(--gold-deep);
}

/* --- Force the quote's header markup down (CF93E9DA, 02/09/2026) -----------------
   Its own button because the automatic reset is gated on the value CHANGING, and re-entering the same
   figure did nothing — which is what "I've just tested this and it hasn't worked" was. */
.ea-apply-markup { margin-top: 8px; align-self: flex-start; }

/* --- Hint under a master-data field (D071F350, 02/09/2026) -----------------------
   "Please define what code means?" A field whose meaning is not on the screen is a field people either
   leave blank or fill with the wrong thing. */
.md-hint {
    display: block;
    margin-top: 4px;
    font-size: 0.74rem;
    line-height: 1.4;
    color: var(--ink-3);
    font-weight: 400;
}

/* --- Panel padding, two sizes and no third -----------------------------------------
   .fx-panel deliberately sets NO padding: a panel wrapping a grid has to be flush to the card edge, and
   giving the base class an inset would put a white gutter around every table in the application.

   The cost of that was fourteen inline paddings across nine files, in eight different values — 14px,
   18px 20px, 20px, 24px, 24px 26px, 26px, 14px 16px, 32px 34px — so the same card had a different inset
   depending on which page you were on.

   24px 26px is the house figure: it is what .fx-detail-head already uses on ten detail pages, and the
   26px matches the horizontal inset .fx-panel > .fx-h3:first-child sets. The small size is for a filter
   strip or a toolbar, where the full inset makes a one-line control look like a section. */
/* A tick box with its label on one line. Reported 08/09/2026 as the second half of 26FEF8A1: "the Bill
   this Client section… looks very cramped, particularly around the checkbox and accompanying text."

   It was declared inside BillingScreen.razor's own <style> block and used from ProjectBilling.razor — a
   DIFFERENT component. These style blocks are global once their component renders and absent when it does
   not, so the class simply had no definition on the page that was reporting the problem: no flex, no gap,
   the box against its text. Promoted here, and the duplicate removed from BillingScreen, so there is one
   definition rather than one per page that remembers to carry it.

   The explicit box size is the exemption .ol-closed, .fx-field and .fx-searchbox all document: a blanket
   width on `input` reaches a checkbox and stretches it across its row. */
.fx-inline { display: inline-flex; align-items: center; gap: 8px; }
.fx-inline > input[type="checkbox"] { flex: 0 0 auto; width: 16px; height: 16px; margin: 0; padding: 0; }

.fx-pad { padding: 24px 26px; }
.fx-pad-sm { padding: 14px 16px; }

/* An explanatory paragraph under a section heading. Distinct from .md-hint, which sits under a single
   FIELD: this one is wider, sits above the thing it describes, and carries its own bottom margin. They
   collided under one name for a day — the classifications page defined its own .md-hint at 0.82rem while
   app.css had 0.74rem, so the same class meant two sizes depending on the page. */
.md-lead {
    font-size: 0.82rem;
    color: var(--muted-foreground);
    margin: 0 0 10px;
    max-width: 70ch;
    line-height: 1.5;
}

/* --- The actions column on a grid -------------------------------------------------
   The largest duplicate family found in the audit: 25 pages each carried these two identical rules, and a
   26th carried a completely different `.fx-actions-col { display: flex; gap: 6px; }`, so one screen's
   actions column behaved unlike the other twenty-five.

   `width: 1%` is the usual table trick for "as narrow as the content allows" — the browser resolves a
   percentage width against the table and every other column takes the remainder. */
.fx-actions-col { width: 1%; white-space: nowrap; text-align: right; }
table.fx-grid td.fx-actions-col,
table.fx-grid th.fx-actions-col { text-align: right; }

/* Buttons in it. Thirteen pages carried this identical rule; one carried it with an extra
   `table.fx-grid td` qualifier for no reason anybody recorded. */
.fx-actions-col .fx-btn { margin-left: 6px; }

/* --- The master-data form grid, ONE definition -----------------------------------
   Every master-data page carried its own copy — 36 rules across 11 pages — and they had drifted:
   nine used `auto-fill` with `gap: 12px 16px` and a label at `.78rem; opacity: .85`, while two used
   `auto-fit` with `gap: 12px` and a label at `0.86rem; color: var(--ink-3)`. So two of the eleven simply
   looked different, and one page had resorted to `!important` to force a tick box back into line.

   The majority grid wins. The label colour does NOT: `opacity: .85` on a <label> fades everything inside
   it, and the input is inside it — so nine pages were rendering every form field at 85% strength while
   two rendered them fully. A token colour affects the label text and leaves the field alone, which is
   what was meant. */
.md-form {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 12px 16px;
}
/* Reported 08/09/2026 (9D46AE85): "The 'Add to Plan' and 'Log Variation' buttons have noticeably more
   padding between the button text and the edge than other buttons in the app, making them appear larger."

   The padding was never different — the BUTTON was. A grid item stretches to fill its cell on both axes
   by default, so a button sharing this grid with labelled fields was blown up to a 220px-minimum column
   and to the height of the tallest field in its row. It looked like a button with too much padding
   because that is exactly what a stretched button looks like.

   `align-self: end` rather than `start`, so it still lines up with the bottom of the fields beside it,
   which is what the layout was reaching for. Nineteen buttons across the master-data and billing forms
   sit in one of these grids, so this is fixed at the rule. */
.md-form > button { justify-self: start; align-self: end; width: auto; }

.md-form label {
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: 0.78rem;
    color: var(--ink-3);
}

/* A field that needs the whole row — a notes box, an address line. */
.md-form .md-wide { grid-column: 1 / -1; }

/* Native option text is painted by the OS list, which does not inherit the field colour. */
.md-form select option { color: var(--ink); }

/* A tick box in a master-data form (500C54B1).
   Reported within the hour as DD3879AF — "Descriptions are overlaying text boxes" — because this shipped
   as a bare `.md-check` at (0,1,0) while the page's own `.md-form label` is (0,2,0) and kept winning
   `flex-direction: column`. The row then laid out wrong AND overflowed its 220px grid cell, printing over
   the Address 1 field beside it.

   Two fixes, both needed: out-specify the page rule, and span the whole row so a tick box with a sentence
   next to it is not squeezed into one column of a four-column grid. */
.md-form label.md-check {
    grid-column: 1 / -1;
    display: flex;
    flex-flow: row wrap;
    align-items: center;
    gap: 8px;
}

.md-form label.md-check .md-hint { flex-basis: 100%; margin-top: 2px; }

/* --- Due-to-bill grid (397A5691, 02/09/2026) -------------------------------------
   "At the moment none of the headers line up. I appreciate this is dictated by the legnth of the client
   name, but can you redo the design so it looks more appealing."

   Right on both counts. Each project used to render its own table, so every one sized its columns to its
   own content. There is now ONE table with a header row per group inside it, and fixed widths so the
   longest client name cannot push a column out of line with the block above it.

   min-width raises the shared `table.fx-grid` floor of 720px, which is far too low here: the
   eight non-flexible columns alone total 1080px, so below ~1300px the browser crushed Stage to ZERO
   width and the stage name vanished entirely rather than the container scrolling. 1080 + a 220px Stage
   minimum. Verified at 1366 and 1280: Stage 0px -> 220px, container scrolls. */
table.fx-grid.bs-grid { table-layout: fixed; min-width: 1300px; }

/* Truncation applies to the TEXT columns only. Blanket-applying it to every cell — which is what shipped
   first — clipped the Open button in the actions column and cut the group totals to "£72,1…". Reported
   as "the due to bill table isn't formatted, and the open button has been indented", and it was mine. */
table.fx-grid.bs-grid tbody td:not(.dt-actions),
table.fx-grid.bs-grid thead th { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The group header row spans the table and carries a money total; it must never be clipped. */
table.fx-grid.bs-grid tbody tr.bs-group > td { overflow: visible; text-overflow: clip; }

.bs-c-ref { width: 8rem; }
.bs-c-client { width: 14rem; }
/* `width: auto` does NOT absorb the slack under `table-layout: fixed` — it splits the leftover instead
   of taking it, so at 1920 the nine columns totalled 1343px inside a 1606px table and left a 263px dead
   strip down the right-hand side. Reported as "the page is still indented" (04/09/2026), and against the
   two auto-layout tables below it on the same page — both of which fill the width — the top table simply
   looked shrunken. `width: 100%` is the fixed-layout idiom for "take whatever the others don't".
   Measured on the deployed page, not reasoned about: gutter 263 -> 0 at 1920, 103 -> 0 at 1600. */
.bs-c-stage { width: 100%; }        /* the one column that gets the slack */
.bs-c-due { width: 7.5rem; }
.bs-c-num { width: 8rem; }
.bs-c-pct { width: 6.5rem; }
/* Wide enough for the button plus the cell's own padding — 5.5rem was not, so the button was clipped by
   the truncation rule above. Measured against the rendered button rather than guessed. */
.bs-c-act { width: 7.5rem; }

/* The project or client heading, now a row rather than an h3 above its own table. */
table.fx-grid.bs-grid tbody tr.bs-group > td {
    background: var(--muted);
    font-weight: 600;
    color: var(--ink);
    border-top: 1px solid var(--border);
    padding-top: 14px;
    padding-bottom: 10px;
}

table.fx-grid.bs-grid tbody tr.bs-group:first-child > td { border-top: none; }

/* --- Planned cashflow, directors' dashboard (A35F0BD4, 01/09/2026) ---------------
   "If it goes beyond that month then it needs to highlight in red." A month that has passed with its
   planned slice still unbilled.

   `table.fx-grid tbody td` specificity, per the .mw-overdue lesson — a bare class loses the colour and
   keeps only the weight, which reads as a half-applied rule. */
table.fx-grid tbody td.dd-cash-late { color: var(--destructive); font-weight: 600; }

/* A hint that is a WARNING rather than guidance (CF93E9DA) — the quote is not using the figure the box
   states, which is a fact the author needs rather than an instruction. */
.ea-hint-warn { color: var(--destructive); font-weight: 500; }

/* Show-completed switch, sitting in the section heading (9F67C134). */
.mw-showdone {
    display: inline-flex; align-items: center; gap: 6px; margin-left: 14px;
    font-size: 0.8rem; font-weight: 400; color: var(--ink-3); cursor: pointer;
}

/* --- A finished row on My Work ---------------------------------------------------
   Third instance of the specificity trap in one day: `.mw-done` is (0,1,0) and lost the COLOUR to
   `table.fx-grid tbody td` at (0,1,3), so rows struck through but never greyed — half-applied, exactly
   like the overdue dates reported as 397A5691. */
table.fx-grid tbody td.mw-done { text-decoration: line-through; color: var(--muted-foreground); }

/* ── Test plan: the automation panel (test-runner handover) ─────────────────────────────────────── */
.tp-automation { margin-bottom: 14px; display: flex; flex-direction: column; gap: 10px; }
.tp-automation-head { display: flex; flex-direction: column; gap: 2px; }
.tp-automation-title { font-weight: 600; color: var(--ink); }
.tp-automation-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* The command is meant to be selected and pasted, so it wraps rather than scrolling out of reach. */
.tp-automation-cmd {
    display: block;
    padding: 10px 12px;
    border-radius: 10px;
    background: var(--surface-2, rgba(0, 0, 0, 0.05));
    border: 1px solid var(--line);
    font-family: var(--font-mono, ui-monospace, Menlo, monospace);
    font-size: 0.82rem;
    color: var(--ink);
    overflow-wrap: anywhere;
    user-select: all;
}

/* ---------------------------------------------------------------------------
   Test plan — Defects tab

   The classification hues are an IDENTITY palette: the colour follows the class,
   in a fixed order, so filtering the table never repaints the survivors. They are
   not eyeballed — the five identity hues pass chroma, CVD separation (worst
   adjacent pair ΔE 9.1 protan) and normal-vision separation (ΔE 22.9) against the
   light surface. Aqua and yellow sit below 3:1 contrast, which obliges visible
   labels: the legend beneath the bar names every segment with its count, and the
   table below repeats all of it, so no reading depends on telling two fills apart.

   --tp-c-none is deliberately the MUTED INK and deliberately not a hue. It means
   "nobody has judged this yet", which is the absence of an identity rather than
   one more of them, and it fails the chroma floor on purpose.

   Dark steps for the parked dark mode (D-UI3), so a future revival does not have
   to re-derive them: #3987e5 #d95926 #199e70 #c98500 #9085e9.
   --------------------------------------------------------------------------- */
:root {
    --tp-c-defect:   #2a78d6;   /* slot 1 blue    — product defect */
    --tp-c-runner:   #eb6834;   /* slot 2 orange  — runner fault   */
    --tp-c-pack:     #1baf7a;   /* slot 3 aqua    — pack fault     */
    --tp-c-fixture:  #eda100;   /* slot 4 yellow  — fixture fault  */
    --tp-c-gap:      #4a3aa7;   /* slot 5 violet  — known gap      */
    --tp-c-none:     #898781;   /* neutral        — unclassified   */
}

.tp-def-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px;
    margin-bottom: 16px; }
.tp-def-tile { background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
    padding: 14px 16px; display: flex; flex-direction: column; gap: 2px; }
.tp-def-figure { font-size: 1.9rem; font-weight: 600; line-height: 1.1; color: var(--ink);
    font-variant-numeric: tabular-nums; }
.tp-def-label { font-size: 0.78rem; color: var(--muted-foreground); }
.tp-def-sub { font-size: 0.72rem; color: var(--muted-foreground); }
.tp-def-tile-alarm .tp-def-figure { color: var(--c-danger); }

/* The composition bar. A 2px surface gap between segments so two fills never
   touch — the same relief the contrast WARN asks for, and it keeps adjacent
   segments separable without relying on hue at all. */
.tp-def-bar { display: flex; width: 100%; height: 22px; border-radius: 6px; overflow: hidden;
    background: var(--border); gap: 2px; margin: 6px 0 10px; }
.tp-def-seg { height: 100%; min-width: 2px; }
.tp-def-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 0.78rem;
    color: var(--muted-foreground); }
.tp-def-key { display: inline-flex; align-items: center; gap: 6px; }
.tp-def-dot { width: 10px; height: 10px; border-radius: 3px; flex: none; }

.tp-def-trend { width: 100%; height: 120px; display: block; }
.tp-def-trend .grid { stroke: var(--border); stroke-width: 1; }
.tp-def-trend .line { fill: none; stroke: var(--tp-c-defect); stroke-width: 2; }
.tp-def-trend .dot { fill: var(--tp-c-defect); }
.tp-def-trend .reopen { fill: var(--c-danger); }
.tp-def-trend .axis { fill: var(--muted-foreground); font-size: 10px; }

.tp-def-changed { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; margin-bottom: 8px; }
.tp-def-chip { font-size: 0.78rem; }
.tp-def-row-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.tp-def-observed { font-size: 0.78rem; color: var(--muted-foreground); overflow-wrap: anywhere; }
.tp-def-sig { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.72rem;
    color: var(--muted-foreground); }

/* Execution before findings — see RunComparison.MostlyDidNotRun for why this sits above the tiles. */
.tp-def-exec { border: 1px solid var(--border); border-radius: 10px; padding: 12px 16px; margin-bottom: 12px;
    background: var(--surface); font-size: 0.9rem; color: var(--ink); }
.tp-def-exec-alarm { border-color: var(--c-danger); background: var(--c-danger-bg); }
.tp-def-exec-note { margin: 6px 0 0; font-size: 0.82rem; color: var(--c-danger); }
.tp-def-blocked { margin-bottom: 14px; font-size: 0.85rem; }
.tp-def-blocked summary { cursor: pointer; color: var(--muted-foreground); }
.tp-def-blocked-list { margin: 8px 0 0; padding-left: 20px; color: var(--muted-foreground); line-height: 1.6; }

/* ── The searchable picker (Picker.razor, 24/09/2026) ─────────────────────────────────────────────────
   The box is an ordinary .fx-input, so it takes the house field look and the one control height below with no
   rules of its own beyond room for the × and the chevron. The list is fixed to the viewport (picker.js) and
   starts hidden until it has been placed, so it never flashes at the top of the page for a frame. */
.fx-picker { position: relative; display: inline-block; width: 100%; min-width: 0; vertical-align: middle; }
/* The wrapper owns the width; the box always fills it and keeps room for the × and the chevron. These three
   are !important on purpose, and only these: pages style bare inputs by descent (".ea-insert input { max-width:
   420px }", ".fx-dialog .fx-lines-table input { padding: 6px 8px }"), and either one, at higher specificity,
   left the chevron outside a capped box or ran the text under it — both seen on the rollout. Colours, border
   and font still follow the page, so a picker looks like the fields around it. */
.fx-picker > .fx-picker-input { width: 100% !important; max-width: none !important; padding-right: 52px !important; text-overflow: ellipsis; }
.fx-picker-chevron {
    position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
    display: inline-flex; color: var(--muted-foreground); pointer-events: none;
}
.fx-picker-clear {
    position: absolute; right: 28px; top: 50%; transform: translateY(-50%);
    width: 18px; height: 18px; padding: 0; border: 0; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: none; color: var(--muted-foreground); font-size: 1rem; line-height: 1; cursor: pointer;
}
.fx-picker-clear:hover { background: var(--muted); color: var(--foreground); }
.fx-picker-list {
    position: fixed; z-index: 1500; visibility: hidden; overflow-y: auto; box-sizing: border-box;
    padding: 4px; background: var(--surface, #fff); color: var(--foreground);
    border: 1px solid var(--border); border-radius: var(--r-input);
    box-shadow: 0 10px 30px rgba(15, 23, 42, .14);
    font-size: 0.875rem; font-weight: 400; letter-spacing: normal; text-transform: none; text-align: left;
}
.fx-picker-option {
    display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
    padding: 7px 10px; border-radius: 6px; cursor: pointer;
}
.fx-picker-option:hover, .fx-picker-option.is-active { background: var(--muted); }
.fx-picker-option.is-selected .fx-picker-text { font-weight: 600; }
.fx-picker-text { min-width: 0; overflow-wrap: anywhere; }
.fx-picker-detail { flex-shrink: 0; font-size: 0.78rem; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
.fx-picker-note { padding: 7px 10px; font-size: 0.8rem; color: var(--muted-foreground); }

/* The Foundations mark (FoundationsMark.razor). A block, so an inline SVG does not sit on the text baseline with
   a gap beneath it; its colour comes from wherever it is placed. */
.fx-product-mark { display: block; flex: none; }

/* A dialog must not hold a transform once it is open (24/09/2026). MudBlazor's opening animation runs with
   fill-mode "both", which leaves transform: scale(1) on .mud-dialog for as long as it is open, and ANY
   transform makes an element the containing block for its position: fixed descendants. Every list this
   product fixes to the viewport (the Picker, the grid filter funnel, row actions) was therefore clipped by
   the dialog's scroll area: the role editor's permission list was cut off at the dialog's lower edge.
   "backwards" still plays the zoom-in, then leaves no transform behind. */
.mud-dialog { animation-fill-mode: backwards; }


/* ── Ink & Limestone: page header, KPIs, breadcrumb, tabs (24/09/2026) ───────────────────────────────────────
   docs/design/UI2-ink-limestone-design.md §4. Colours only through tokens, so a brand pinned to an older
   palette (Ridings) keeps its colours and takes only the shape. */
.fx-ph { display: flex; align-items: flex-end; gap: 28px; flex-wrap: wrap; margin-bottom: 24px; }
.fx-ph--record { align-items: flex-start; }
.fx-ph-main { flex: 1 1 320px; min-width: 0; }
.fx-ph-eyebrow {
    font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--muted-foreground); margin-bottom: 6px;
}
.fx-ph-titlerow { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.fx-ph .fx-page-sub { margin: 6px 0 0; font-size: 0.95rem; }
.fx-ph-kpis { display: flex; align-items: stretch; flex-wrap: wrap; }
.fx-ph-kpis .fx-kpi { padding: 0 22px; border-left: 1px solid var(--border); }
.fx-ph-kpis .fx-kpi:first-child { border-left: 0; padding-left: 0; }
.fx-ph-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.fx-kpi { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.fx-kpi-label { font-size: 0.75rem; color: var(--muted-foreground); font-weight: 500; }
.fx-kpi-value {
    font-size: 1.625rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2;
    color: var(--foreground); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.fx-kpi-note { font-size: 0.75rem; color: var(--muted-foreground); }
/* An exception, and only an exception — a negative margin. Zero is not red (Ink & Limestone brief). */
.fx-kpi-value.is-exception { color: var(--destructive); }

/* The record variant's KPIs: one bordered row, figures divided by hairlines. */
.fx-kpi-row {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    background: var(--card); border: 1px solid var(--border); border-radius: var(--r-card);
    padding: 16px 0; margin-bottom: 24px;
}
.fx-kpi-row .fx-kpi { padding: 0 24px; border-left: 1px solid var(--border); }
.fx-kpi-row .fx-kpi:first-child { border-left: 0; }
.fx-kpi-row .fx-kpi-label { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.7rem; font-weight: 600; }
.fx-kpi-row .fx-kpi-value { font-size: 1.375rem; }

/* Breadcrumb — in the sidebar shell's top band, or above the page in the top-bar shell. */
.fx-crumbs { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; color: var(--muted-foreground); min-width: 0; }
.fx-crumbs-back {
    display: inline-grid; place-items: center; width: 28px; height: 28px; margin-right: 2px;
    border-radius: var(--r-input); color: var(--foreground); text-decoration: none;
}
.fx-crumbs-back:hover { background: var(--muted); }
/* Earlier crumbs keep their width; only the current page (the last) gives way and truncates. */
.fx-crumbs-link { color: var(--foreground); text-decoration: none; white-space: nowrap; flex-shrink: 0; }
.fx-crumbs-link:hover { text-decoration: underline; }
.fx-crumbs-here { color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.fx-crumbs-sep { color: var(--hair-2); }
.fx-main-crumbs:not(:empty) { margin-bottom: 14px; }

/* Tabs — text, with an underline on the current one in --accent-text: ink here, a brand's own accent where that
   is legible (Ridings keeps its orange rule). One look for record tabs and list filters alike. */
/* Wraps rather than scrolls: a scrolled strip hides its scrollbar on macOS, so the project record's thirteen tabs
   were cut off at the edge with nothing to say more existed (sweep 24/09/2026). */
.fx-tabs { display: flex; flex-wrap: wrap; gap: 0 4px; border-bottom: 1px solid var(--border); margin-bottom: 20px; }
.fx-tab {
    appearance: none; background: none; border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px;
    padding: 10px 14px; font: inherit; font-size: 0.9rem; color: var(--muted-foreground); cursor: pointer;
    text-decoration: none; white-space: nowrap;
}
.fx-tab:hover { color: var(--foreground); }
.fx-tab.on { color: var(--foreground); font-weight: 600; border-bottom-color: var(--accent-text); }


/* A button that reads as text — a figure that opens its breakdown (9D1293E1), GridHost's "Clear filters". In
   app.css since 24/09/2026: it was declared on the commercial page only, so every other use rendered unstyled. */
.fx-linkbtn {
    background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
    cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px;
}
.fx-linkbtn:hover { text-decoration-style: solid; }

/* A KPI that links to its detail: the figure is the link, and it says so on hover. */
.fx-kpi--link { color: inherit; text-decoration: none; border-radius: var(--r-input); }
.fx-kpi--link:hover .fx-kpi-value { text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 2px; }

/* ── Ink & Limestone: the grid toolbar, selection, row parts, pager (24/09/2026) ─────────────────────────── */
.fx-gridhost { min-width: 0; }
.fx-gt {
    display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    padding: 12px 16px; background: var(--card); border-bottom: 1px solid var(--border);
}
.fx-gt-left, .fx-gt-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.fx-gt-left { flex: 1 1 280px; }
.fx-gt-search { position: relative; flex: 0 1 360px; min-width: 200px; display: block; }
.fx-gt-search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--muted-foreground); pointer-events: none; }
.fx-gt-search .fx-input { padding-left: 34px; }
.fx-btn.is-on { border-color: var(--brand-primary); }
.fx-gt-count {
    display: inline-grid; place-items: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
    background: var(--brand-primary); color: var(--on-primary, #fff); font-size: 0.68rem; font-weight: 600;
}
.fx-gt-density {
    display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-input); overflow: hidden;
    height: var(--control-h); box-sizing: border-box;
}
.fx-gt-density button {
    width: var(--control-h); border: 0; background: var(--card); color: var(--muted-foreground); cursor: pointer;
    display: grid; place-items: center;
}
.fx-gt-density button + button { border-left: 1px solid var(--border); }
.fx-gt-density button.is-on { background: var(--brand-primary); color: var(--on-primary, #fff); }
.fx-gt--bulk { justify-content: flex-start; background: var(--muted); }
.fx-gt-selected { font-weight: 600; margin-right: 6px; }
.fx-gt-panel { width: 300px; max-height: 360px; overflow-y: auto; }
.fx-gt-hint { margin: 4px 0 8px; font-size: 0.82rem; color: var(--muted-foreground); }
.fx-gt-chips { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.fx-gt-chips li {
    display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 8px 6px 10px;
    border: 1px solid var(--border); border-radius: var(--r-input); font-size: 0.82rem;
}
.fx-gt-chips button { border: 0; background: none; font-size: 1rem; line-height: 1; cursor: pointer; color: var(--muted-foreground); }
.fx-gt-col { display: flex; padding: 4px 0; }
.fx-gt-views { list-style: none; margin: 0 0 10px; padding: 0; }
.fx-gt-viewpick {
    display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 8px; border: 0; background: none;
    border-radius: 6px; font: inherit; text-align: left; cursor: pointer; color: var(--foreground);
}
.fx-gt-viewpick:hover, .fx-gt-views li.is-on .fx-gt-viewpick { background: var(--muted); }
.fx-gt-views li.is-on .fx-gt-viewpick { font-weight: 600; }
.fx-gt-tag { font-size: 0.68rem; color: var(--muted-foreground); border: 1px solid var(--border); border-radius: 4px; padding: 0 5px; }
.fx-gt-viewacts { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.fx-gt-save { display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--border); padding-top: 10px; }
.fx-gt-error { color: var(--destructive); font-size: 0.8rem; margin: 8px 0 0; }

/* Selection column: narrow, centred, and the row it ticks reads as selected — restrained, a tint not a fill. */
th.fx-sel-col, td.fx-sel-col { width: 44px; text-align: center; }
.fx-sel { width: 16px; height: 16px; accent-color: var(--brand-primary); cursor: pointer; vertical-align: middle; }
table.fx-grid tbody tr.is-selected td,
.fx-dg .mud-table-body .mud-table-row.is-selected .mud-table-cell { background: color-mix(in srgb, var(--brand-primary) 5%, var(--grid-row)); }

/* Row parts: bold name over a quieter number; a person as a small avatar and a short name. */
.fx-cell-primary { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.fx-cell-title { font-weight: 600; color: var(--foreground); }
.fx-cell-sub { font-size: 0.76rem; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
.fx-person { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }
.fx-person-name { overflow: hidden; text-overflow: ellipsis; }
.fx-avatar {
    display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; flex: none;
    background: var(--muted); border: 1px solid var(--border); color: var(--foreground);
    font-size: 0.66rem; font-weight: 700; letter-spacing: 0.02em;
}
.fx-avatar::before { content: attr(data-initials); }
/* A figure that is an exception gets a tint; a healthy one is just the figure. Zero is neither (the brief). */
.fx-fig-exception {
    display: inline-block; padding: 1px 8px; border-radius: 6px; font-weight: 600;
    background: var(--c-danger-bg); color: var(--c-danger);
}
.fx-fig-good { color: var(--c-success); font-weight: 600; }
.fx-row-chevron { color: var(--hair-2); width: 32px; text-align: right; }

/* Sticky headings on the list grids — the ones with a toolbar (Ink & Limestone). A sticky element sticks within its
   nearest scroll container, and .fx-grid-scroll is one (overflow-x), so it is given a height to scroll in;
   without that the heading "sticks" to a box that never scrolls. Embedded grids without a toolbar are left alone. */
.fx-gridhost:has(.fx-gt) .fx-grid-scroll { max-height: calc(100vh - 180px); overflow: auto; }
.fx-gridhost:has(.fx-gt) table.fx-grid thead th { position: sticky; top: 0; z-index: 2; }

/* Headings stay on one line in the refreshed grids, so a narrow column truncates rather than stacking words. */
.fx-gridhost table.fx-grid thead th, .fx-gridhost .fx-dg .mud-table-head .mud-table-cell { white-space: nowrap; }

/* House-table pager: "1–25 of 140 projects" and the page buttons, under the grid. */
.fx-grid-pager {
    display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    padding: 10px 16px; background: var(--card); border-top: 1px solid var(--border);
}
.fx-grid-pager-count { font-size: 0.82rem; color: var(--muted-foreground); }
.fx-grid-pager-pages { display: inline-flex; gap: 4px; align-items: center; }
.fx-btn-icon.is-on { background: var(--muted); border-color: var(--brand-primary); font-weight: 600; }
.fx-grid-pager-gap { padding: 0 4px; color: var(--muted-foreground); }
/* S0-T12 — "Rows per page": 25, 50, 100 or All, saved with a view. The select keeps the house control height. */
.fx-grid-pager-size { display: inline-flex; align-items: center; gap: 8px; font-size: 0.82rem; color: var(--muted-foreground); margin-left: auto; }
.fx-grid-pager-size select { width: auto; }


/* ── Ink & Limestone: the form pattern (24/09/2026) ─────────────────────────────────────────────────────────
   A numbered nav, one column of fields with labels above and hints below, a snapshot rail, and the save bar. */
.fx-form-layout { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 32px; align-items: start; }
.fx-form-layout.has-rail { grid-template-columns: 200px minmax(0, 1fr) 260px; }
.fx-form-nav { position: sticky; top: 84px; display: flex; flex-direction: column; gap: 2px; }
.fx-form-nav-title {
    font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--muted-foreground); margin: 0 0 8px 12px;
}
.fx-form-nav-link {
    position: relative; display: flex; gap: 12px; align-items: baseline; padding: 8px 12px; border-radius: var(--r-input);
    color: var(--muted-foreground); text-decoration: none; font-size: 0.875rem;
}
.fx-form-nav-link:hover { color: var(--foreground); background: var(--muted); }
.fx-form-nav-link.is-on { color: var(--foreground); font-weight: 600; background: var(--muted); }
.fx-form-nav-link.is-on::before {
    content: ""; position: absolute; left: 0; top: 7px; bottom: 7px; width: 3px; border-radius: 0 3px 3px 0;
    background: var(--sidebar-shell-marker, var(--brand-accent));
}
.fx-form-nav-n { font-variant-numeric: tabular-nums; font-size: 0.78rem; color: var(--muted-foreground); }
.fx-form-main { display: flex; flex-direction: column; gap: 32px; max-width: 640px; }
.fx-form-section { scroll-margin-top: 84px; }
.fx-form-section-title { font-size: 1.125rem; font-weight: 600; margin: 0; color: var(--foreground); letter-spacing: -0.01em; }
.fx-form-section-sub { margin: 4px 0 0; font-size: 0.85rem; color: var(--muted-foreground); }
.fx-form-fields { display: flex; flex-direction: column; gap: 16px; margin-top: 16px; }
/* A field: the label above, the control, then an optional hint or error beneath. */
.fx-form-field { display: flex; flex-direction: column; gap: 6px; font-size: 0.85rem; color: var(--foreground); }
/* The label carries the weight, not the field — so a hint, an empty-state line or an editor inside it stays regular. */
.fx-form-field > label, label.fx-form-field { font-weight: 500; }
.fx-form-field .fx-optional { color: var(--muted-foreground); font-weight: 400; }
.fx-form-field > .fx-input, .fx-form-field > select, .fx-form-field > input, .fx-form-field > textarea, .fx-form-field > .fx-picker { width: 100%; font-weight: 400; }
.fx-form-field textarea { min-height: 88px; padding-top: 8px; }
/* A bare control in a form field takes the house field look, so a select or a suggestion input dropped into the
   pattern without the .fx-input class still has an edge and the focus outline. */
.fx-form-field > select, .fx-form-field > input:not([type="checkbox"], [type="radio"], .fx-input) {
    font-family: var(--font-sans); font-size: 0.9rem; font-weight: 400; color: var(--foreground);
    background: var(--field-bg); border: 1px solid var(--input); border-radius: var(--r-input); padding: 0 12px;
}
.fx-form-field > select:focus, .fx-form-field > input:not([type="checkbox"], [type="radio"], .fx-input):focus {
    outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-primary) 14%, transparent);
}
.fx-form-hint { font-size: 0.78rem; font-weight: 400; color: var(--muted-foreground); }
/* Resolution S1-T02 — the required marker a mandatory-field rule puts on a label while the field is empty. */
.fx-required-marker { color: var(--destructive); font-weight: 600; margin-left: 3px; }
/* Resolution S1-T04 — the shared attachments panel: a drop zone the (invisible) file input covers, so a file
   dragged anywhere on it is taken, and a click opens the picker. Tokens only; no control height set here. */
.ap-panel { display: grid; gap: 12px; margin-top: 24px; }
.ap-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.ap-filter select { width: 220px; }
.ap-upload { display: flex; gap: 12px; align-items: stretch; flex-wrap: wrap; }
.ap-folder { width: 220px; flex: none; }
.ap-drop { position: relative; flex: 1 1 280px; display: flex; align-items: center; justify-content: center; gap: 10px;
    flex-wrap: wrap; padding: 14px 16px; border: 1.5px dashed var(--border); border-radius: var(--radius);
    background: var(--muted); color: var(--muted-foreground); }
.ap-drop:hover, .ap-drop:focus-within { border-color: var(--ring); }
.ap-drop.is-busy { opacity: .7; }
.ap-drop-input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }

/* ---- Resolution S10-T03 — the supplier Compliance tab ---- */
.sct-head { margin-bottom: 12px; }
.sct-status { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.sct-issues { margin: 10px 0 0; padding-left: 18px; display: grid; gap: 6px; }
.sct-issues li { display: flex; align-items: center; gap: 10px; }
.ap-drop.has-file .ap-drop-text { font-weight: 600; }
/* Beside a field the error is plain red text, not the tinted box the same class draws at the top of a form. */
.fx-form-field .fx-form-error { font-size: 0.78rem; font-weight: 500; color: var(--destructive); background: none; border: 0; padding: 0; }
/* A success line after an action (a key issued, access granted) — declared once rather than per dialog. */
.fx-note-ok {
    font-size: 0.84rem; color: var(--c-success); background: var(--c-success-bg);
    border: 1px solid color-mix(in srgb, var(--c-success) 25%, transparent); border-radius: var(--r-input); padding: 9px 12px;
}
/* A short form (a handful of fields in a panel) keeps the one-column measure of the sectioned form. */
.fx-form-narrow { max-width: 640px; }
.fx-form-field.has-error > .fx-input, .fx-form-field.has-error > input, .fx-form-field.has-error > select { border-color: var(--destructive); }
/* Two short, related fields side by side — dates, postcode and city — and one column again on a narrow screen. */
.fx-form-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.fx-form-rail {
    position: sticky; top: 84px; background: var(--card); border: 1px solid var(--border); border-radius: var(--r-card);
    padding: 20px;
}
.fx-form-rail-title { font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted-foreground); margin: 0 0 14px; }
.fx-form-rail dl { margin: 0; display: flex; flex-direction: column; gap: 12px; }
.fx-form-rail dt { font-size: 0.78rem; color: var(--muted-foreground); }
.fx-form-rail dd { margin: 0; font-size: 0.9rem; color: var(--foreground); }
.fx-form-monogram {
    display: grid; place-items: center; width: 52px; height: 52px; border-radius: var(--r-input); margin-bottom: 14px;
    background: var(--muted); border: 1px solid var(--border); font-weight: 700; font-size: 1rem; color: var(--foreground);
}
@media (max-width: 1100px) {
    .fx-form-layout, .fx-form-layout.has-rail { grid-template-columns: minmax(0, 1fr); }
    .fx-form-nav { position: static; flex-direction: row; flex-wrap: wrap; }
    .fx-form-nav-title { display: none; }
    .fx-form-rail { position: static; }
}
@media (max-width: 640px) { .fx-form-pair { grid-template-columns: 1fr; } }

/* The save bar, fixed to the foot of the form while it is open. */
.fx-savebar {
    position: sticky; bottom: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin: 24px 0 0; padding: 12px 20px; background: var(--card); border: 1px solid var(--border);
    border-radius: var(--r-card); box-shadow: 0 -6px 18px rgba(24, 36, 33, 0.06);
}
.fx-savebar-state { display: inline-flex; align-items: center; gap: 8px; font-size: 0.875rem; color: var(--muted-foreground); }
.fx-savebar.is-dirty .fx-savebar-state { color: var(--foreground); font-weight: 500; }
.fx-savebar-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--c-warning, #E0A800); }
.fx-savebar-actions { display: inline-flex; gap: 10px; }

/* No spinner arrows on any number field that remains (S0-T10, REQ-RPT-055). The house NumberInput is a text box
   and has none; this covers whatever still says type="number" — the arrows, and the wheel/arrow-key nudging that
   went with them, changed figures people were only reading. Deliberately ABOVE the one-height block, and it sets no
   height: that block must stay the last word on control height. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }

/* ── One height for every control (24/09/2026) ────────────────────────────────────────────────────────
   Simon: "I would be worried if we are using multiple different controls, that they would appear in the forms
   at different heights." They already did. Measured on the live site before this rule: text/number/date
   inputs 41px, the same-styled select 39px beside them, unstyled selects 25/33/35px, MudBlazor fields 56px,
   buttons 35px (small 27px). Nothing set a height — each came out of whichever family's padding and font
   applied (.fx-field, .fx-input, .cd-form, .md-form, .cd-inline-form, page-local rules) — so a form's fields
   were only as level as the rules that happened to style them.

   32px is the design source's own figure: the shadcn PoC's input, select and default button are all h-8.

   ONE rule, deliberately specific (each :not() adds weight) and deliberately last, so it wins over every family
   rule without editing four hundred fields — and a field that genuinely must differ has to say so, with
   .fx-control-free, rather than differ by accident. Vertical padding is zeroed because the height now does
   that job: the old 9px + 9px + a 1.5 line box would overflow 32px and clip descenders.

   Not touched: tick boxes and radios, file/colour/range pickers, textareas, multi-line selects, the command
   bar and Ask inputs (borderless inputs inside a styled bar), MudBlazor's own internals, the Identity pages'
   Bootstrap fields (their floating labels need Bootstrap's height), and Quill's link box. */
:root { --control-h: 40px; }

/* Compact density (Ink & Limestone): "a compact table setting for people working with lots of rows". Scoped to
   the grid it is set on, it brings rows to 40px AND the controls inside them to 32px — the same token the
   one-height rule reads, so a compact grid's editors and buttons shrink with it rather than overflowing. */
.fx-density-compact { --control-h: 32px; }
.fx-density-compact table.fx-grid tbody td,
.fx-density-compact .fx-dg .mud-table-body .mud-table-row .mud-table-cell { height: 40px; padding-top: 4px; padding-bottom: 4px; }

/* The :not() clauses are chained with NO white space between them. A line break there is a descendant
   combinator — "a :not(…) element INSIDE an input" — and the first version of this rule matched nothing at
   all for exactly that reason, which only a browser measurement showed. */
input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"], [type="hidden"], [type="image"], [type="submit"], [type="button"], [type="reset"]):not(.form-control, .fx-control-free):not(.mud-input-control *, .mud-picker *, .ql-container *, .ql-tooltip *),
select:not([multiple], [size], .form-select, .fx-control-free):not(.mud-input-control *) {
    box-sizing: border-box;
    height: var(--control-h);
    padding-top: 0;
    padding-bottom: 0;
    line-height: normal;
}
