
/* Bootstrap Icons is loaded from jsDelivr in the layouts, not bundled.
   The webfont was the one asset that broke on every deployment: it is
   referenced by url() from inside the compiled CSS, so it 404s the moment
   the build's base path does not match where the site is served from.
   From a CDN there is no path to get wrong, and the class names are
   unchanged — all 484 `bi bi-*` usages across 175 views keep working. */



/* Imported AFTER the vendor sheets, not before them.
   It used to be the first import, which quietly defeated half of its purpose:
   its token *definitions* still worked (custom properties resolve where they
   are used, and the rules below consume them), but every actual rule in it
   collided with a Bootstrap rule of equal specificity and lost the cascade.
   That is why `.card` kept Bootstrap's background — the page ground — while
   the file's own header claimed it loaded last. */
@import "./design-system.css";

/* ==========================================================================
   Design tokens — CIMS
   Single source of truth for color / type / space / radius / shadow.
   Overrides Bootstrap 5.3's CSS custom properties (no Sass build needed)
   so existing Bootstrap components (btn, badge, alert, form-*, pagination…)
   pick up the palette automatically.
   ========================================================================== */
:root {
    /* Brand / semantic color — "Site & Steel"
       Deep teal reads as engineered rather than generic-admin-blue, and is
       the one hue not already spoken for by the semantic set below (success
       green, warning/accent amber, danger red, info cyan), so a primary
       button is never mistaken for a status. Amber is the accent because it
       is civil infrastructure's own vernacular — hi-vis, survey markers,
       plant. Contrast on white: primary 6.2:1 (AA for body text), accent
       3.0:1 (large text / UI only, which is all it is used for). */
    /* Enterprise Blue, adopted from the reference system
       (bnd/design/.../structure_erp/DESIGN.md and the ConstructPro screens).
       This replaces the "Site & Steel" teal by explicit request on 2026-08-29,
       reversing the 2026-08-28 decision to keep it.

       Measured before the swap rather than after, because the previous teal
       was chosen for its contrast and that protection had to survive:

         link/text on page ground #f1f4f9 : 6.19:1  (teal was 5.56:1)
         link/text on card #ffffff        : 6.82:1  (teal was 6.13:1)
         white label on a primary button  : 6.82:1  (teal was 6.13:1)

       Better than the teal on every pair, not merely acceptable. The hover
       tone is the reference's deeper primary at 8.90:1 on the page ground. */
    --cims-color-primary: #0052cc;
    --cims-color-primary-rgb: 0, 82, 204;
    --cims-color-primary-dark: #003d9b;
    --cims-color-primary-light: #3b82f6;
    /* The text colour painted ON the accent — white or near-black, whichever
       reads against it. Overridden per installation by
       partials/branding.blade.php, which picks it from the admin's chosen
       colour instead of assuming white.

       Assuming white is what made a pale brand colour unusable: the button
       rendered, the label did not. These defaults are the values
       ColourContrastService picks for the shipped palette, so an install that
       never sets a colour looks exactly as it did. */
    --cims-color-primary-ink: #ffffff;
    --cims-color-primary-dark-ink: #ffffff;
    /* The light variant is used where a shape has to read against the DARK
       chrome — the notification count on the navbar. It is pale by design, so
       its label is near-black, not white. */
    --cims-color-primary-light-ink: #111827;
    --cims-color-secondary: #5a6472;
    --cims-color-secondary-rgb: 90, 100, 114;
    --cims-color-secondary-dark: #444c57;
    --cims-color-secondary-ink: #ffffff;
    --cims-color-secondary-dark-ink: #ffffff;
    --cims-color-accent: #e07b18;
    --cims-color-accent-rgb: 224, 123, 24;
    /* #166534 not #15803d: the status badge draws this text on a 14% tint
       of itself over white, where #15803d measured 4.15:1. #166534 gives 5.75:1.

       These three corrections (success, warning, info) are mirrored by
       bnd/mobile/lib/design/cims_tokens.dart, which had drifted back to the
       lighter hues and shipped three badges below 4.5:1. That file is now
       pinned by its own contrast_test.dart. This block is the source of
       truth: change it here first, then there. */
    --cims-color-success: #166534;
    --cims-color-success-rgb: 22, 101, 52;
    /* #9a4404 not #b45309: 4.14:1 -> 5.29:1 on its own badge tint. */
    --cims-color-warning: #9a4404;
    --cims-color-warning-rgb: 154, 68, 4;
    --cims-color-danger: #b91c1c;
    --cims-color-danger-rgb: 185, 28, 28;
    /* #155e75 not #0e7490: 4.41:1 -> 5.85:1 on its own badge tint. */
    --cims-color-info: #155e75;
    --cims-color-info-rgb: 21, 94, 117;

    /* Neutrals */
    --cims-color-bg: #f1f4f9;
    --cims-color-surface: #ffffff;
    --cims-color-border: #e1e6ef;
    --cims-color-text: #131b2e;
    --cims-color-text-muted: #5b6478;
    /* Muted text is fine on a plain ground, but a row-action button adds its own
       10% self-tint on top of a striped row, and the label landed at 4.43:1 in
       that state (4.72:1 on a plain row). This is the same hue with enough
       headroom to clear 4.5:1 whichever row it lands on: 5.96:1 measured. */
    --cims-color-text-muted-strong: #4d5566;
    --cims-color-text-muted-rgb: 91, 100, 120;

    /* Sidebar (dark surface, kept for wayfinding contrast against light content) */
    --cims-sidebar-bg: #0b1220;
    --cims-sidebar-bg-elevated: #101a2e;
    --cims-sidebar-bg-active: rgba(255, 255, 255, .07);
    --cims-sidebar-text: rgba(226, 232, 245, .72);
    --cims-sidebar-text-active: #ffffff;
    /* .80, not .55. At 55% alpha over the #0b1220 rail this composited to
       3.05:1 — measured in-browser and confirmed by hand from the WCAG
       luminance formula — and it colours the 16 GROUP HEADER labels
       ("Project Management", "Cost Estimation", …), which are primary
       navigation, not decoration. AA wants 4.5:1 for text that size.
       .72 reaches only 4.37:1; .80 gives 5.12:1. Reported 2026-08-26. */
    --cims-sidebar-heading: rgba(148, 163, 195, .8);
    --cims-sidebar-border: rgba(255, 255, 255, .06);

    /* Type */
    --cims-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --cims-text-xs: .75rem;
    --cims-text-sm: .8125rem;
    --cims-text-base: .9375rem;
    --cims-text-md: 1.125rem;
    --cims-text-lg: 1.5rem;
    --cims-text-xl: 1.875rem;

    /* `label-md` from the reference system: 12px / 600 / .05em. It is the one
       type role the scale above had no slot for, and it is the role that does
       the most work in an ERP — it marks every field label, table header and
       metadata caption as chrome rather than as content, so the user's own
       data stays the most prominent text on screen. */
    --cims-text-label: .75rem;
    --cims-tracking-label: .05em;

    /* Spacing (4px rhythm — matches Bootstrap's g- and p- utility scale) */
    --cims-space-1: .25rem;
    --cims-space-2: .5rem;
    --cims-space-3: 1rem;
    --cims-space-4: 1.5rem;
    --cims-space-5: 2rem;
    --cims-space-6: 3rem;

    /* Radius / shadow — aligned to the Structure ERP reference system
       (bnd/design/.../structure_erp/DESIGN.md).

       12px is that system's signature radius for "all major containers, cards
       and input fields"; 4px is reserved for micro-elements (checkboxes, tags)
       and the pill for status badges. The previous 10px card radius was close
       enough to read as an accident rather than a decision — the whole point
       of a signature radius is that it is used consistently and recognisably. */
    --cims-radius-xs: .25rem;
    --cims-radius-sm: .5rem;
    --cims-radius-md: .75rem;
    --cims-radius-lg: 1rem;
    --cims-radius-xl: 1.5rem;
    --cims-radius-pill: 9999px;

    /* Three elevation levels, per the reference system's depth model:
         Level 1 (cards)   Y2  blur 8  @ 4%
         Level 2 (overlay) Y4  blur 12 @ 8%
       Each carries a 1-2% tint of the brand hue rather than neutral black, so
       shadows read as belonging to the palette instead of greying the page.
       The teal is dark enough (#0b6e63) that the tint reads as depth, not
       colour — it is the difference between a shadow and a smudge. */
    --cims-shadow-sm:
        0 1px 2px rgba(15, 23, 42, .04),
        0 2px 8px rgba(var(--cims-color-primary-rgb), .04);
    --cims-shadow-md:
        0 4px 12px rgba(15, 23, 42, .08),
        0 2px 6px rgba(var(--cims-color-primary-rgb), .04);
    --cims-shadow-lg:
        0 24px 48px -12px rgba(15, 23, 42, .24),
        0 8px 16px rgba(var(--cims-color-primary-rgb), .04);

    /* ----------------------------------------------------------------------
       Neutral surfaces that the rest of this file was ALREADY referencing
       without ever defining. Every one of them was written as
       `var(--cims-color-surface-muted, <literal>)`, so it silently resolved to
       a hard-coded LIGHT-THEME literal — correct by accident in light mode and
       wrong in dark, where a 4% black wash over #141c2e is invisible and a
       light-theme border hex is a bright line. Defining them here (and again
       in the dark block) is what makes those fallbacks dead code.
       ---------------------------------------------------------------------- */
    /* A recessed surface: inset panels, zebra rows, `.bg-light`. One step off
       the card, never off the page ground. */
    --cims-color-surface-alt: #f7f9fc;
    /* A translucent recess, for a wash that has to sit on an unknown ground
       (a hovered tree row, a progress track inside a tinted card). */
    --cims-color-surface-muted: rgba(19, 27, 46, .045);
    /* A hairline quieter than --cims-color-border, for divisions INSIDE a
       component where the component's own edge is already drawn. */
    --cims-color-border-subtle: rgba(19, 27, 46, .08);
    --cims-color-bg-rgb: 241, 244, 249;
    --cims-color-text-rgb: 19, 27, 46;

    /* ----------------------------------------------------------------------
       TYPE SCALE

       The sizes above are the scale; these are the other three axes a scale
       needs before it is one. The rule they encode: as type gets larger it
       gets tighter and its leading gets shorter, because tracking and leading
       that read as comfortable at 13px read as loose at 40px.

         role        size   tracking            leading
         label       xs     wide / label        snug
         body        base   base                base
         title       md/lg  snug                snug
         display     xl     tight               tight
         figure      2xl    tightest            tight
       ---------------------------------------------------------------------- */
    --cims-text-2xl: 2.5rem;

    --cims-tracking-tightest: -.03em;
    --cims-tracking-tight: -.02em;
    --cims-tracking-snug: -.01em;
    --cims-tracking-base: -.005em;
    --cims-tracking-wide: .04em;

    --cims-leading-tight: 1.1;
    --cims-leading-snug: 1.3;
    --cims-leading-base: 1.5;

    --cims-weight-regular: 400;
    --cims-weight-medium: 550;
    --cims-weight-semibold: 650;
    --cims-weight-bold: 700;

    /* The two steps the 4px spacing scale above was missing. Component padding
       kept reaching for 12px and 20px and, finding no token, hard-coded them —
       which is how a card header ended up 16px from the edge while the body
       beneath it was 20px. */
    --cims-space-2-5: .75rem;
    --cims-space-3-5: 1.25rem;

    /* ----------------------------------------------------------------------
       MOTION

       Restated from design-system.css so every custom property this file USES
       is also DEFINED in it. The values are identical, and the reduced-motion
       override there is written `:root:root` (0,2,0) precisely so it still
       outranks a plain `:root` — collapsing these to 0ms as it always did.
       ---------------------------------------------------------------------- */
    --cims-motion-fast: 120ms;
    --cims-ease-standard: cubic-bezier(.2, 0, 0, 1);
    --cims-ease-decelerate: cubic-bezier(0, 0, 0, 1);

    /* ----------------------------------------------------------------------
       CATEGORICAL DATA PALETTE

       Five series colours, in fixed order — slot 1 is always the most
       important series, and a chart with three series uses 1, 2, 3, never a
       cycled hue. Chosen against the light surface (#ffffff) and validated,
       not eyeballed: OKLCH lightness band, chroma floor, WCAG contrast, and
       OKLab ΔE between every pair under simulated protanopia/deuteranopia.
       The set clears the ALL-PAIRS gates, so it holds for a doughnut or a
       scatter as well as for bars and lines (worst pair ΔE 8.5 CVD / 17.2
       normal vision; all five ≥ 3:1 on white).

       Deliberately NOT the semantic palette: success-green and danger-red are
       reserved for state, and a series painted in them would read as a
       verdict. That is also why slots 1 and 2 are blue and orange rather than
       a red/green pair — blue↔orange is the one opposition that survives
       every common colour-vision deficiency.
       ---------------------------------------------------------------------- */
    /* 1 — the primary series. The brand blue itself, so the most important
           line on a board is the same colour as the product. */
    --cims-chart-1: #0052cc;
    /* 2 — the comparison series (last period, budget vs actual). Orange: the
           farthest hue from slot 1 that a protanope still separates. */
    --cims-chart-2: #d9600a;
    /* 3 — the third category. Green, but a data green, darker and bluer than
           --cims-color-success so it is not read as "good". */
    --cims-chart-3: #0e9f6e;
    /* 4 — the fourth category. Violet, separated from slot 1 by chroma and
           lightness as well as hue, which is what keeps it off the blue. */
    --cims-chart-4: #8b5cf6;
    /* 5 — the fifth and last. Magenta; past five, series fold into "Other". */
    --cims-chart-5: #be185d;
    /* Gridlines: ~8% of the muted ink. Present enough to read a value off,
       recessive enough that the data stays the darkest thing in the frame. */
    --cims-chart-grid: rgba(var(--cims-color-text-muted-rgb), .08);
    /* Axis ticks and series labels. Text wears an ink token, never the series
       colour — the mark beside the label already carries the identity. */
    --cims-chart-label: var(--cims-color-text-muted);

    /* Layout */
    --cims-sidebar-width: 272px;
    --cims-sidebar-width-collapsed: 76px;
    --cims-navbar-height: 64px;

    /* Bootstrap theme overrides — keeps Bootstrap component behavior,
       swaps only the palette. */
    --bs-primary: var(--cims-color-primary);
    --bs-primary-rgb: var(--cims-color-primary-rgb);
    --bs-link-color: var(--cims-color-primary);
    --bs-link-color-rgb: var(--cims-color-primary-rgb);
    --bs-link-hover-color: var(--cims-color-primary-dark);
    --bs-success: var(--cims-color-success);
    --bs-success-rgb: var(--cims-color-success-rgb);
    --bs-warning: var(--cims-color-warning);
    --bs-warning-rgb: var(--cims-color-warning-rgb);
    --bs-danger: var(--cims-color-danger);
    --bs-danger-rgb: var(--cims-color-danger-rgb);
    --bs-info: var(--cims-color-info);
    --bs-info-rgb: var(--cims-color-info-rgb);
    --bs-body-bg: var(--cims-color-bg);
    --bs-body-color: var(--cims-color-text);
    --bs-body-font-family: var(--cims-font-sans);
    --bs-border-color: var(--cims-color-border);
    --bs-border-radius: var(--cims-radius-sm);
    --bs-border-radius-lg: var(--cims-radius-md);
    --bs-border-radius-sm: .375rem;
    --bs-box-shadow-sm: var(--cims-shadow-sm);
    --bs-box-shadow: var(--cims-shadow-md);
    --bs-heading-color: var(--cims-color-text);
}

/* ==========================================================================
   Dark theme — activated by data-theme="dark" on <html>, set before first
   paint by the inline script in layouts/app.blade.php (either from a saved
   preference or, on a first-ever visit, from prefers-color-scheme). Every
   component in this file is built on the CIMS and Bootstrap custom properties
   above, so redefining them here is sufficient — no component needs its own
   dark variant. Status/KPI tints are written as an rgba() of a semantic
   -rgb token at some alpha rather than as a fixed hex, specifically so this
   block doesn't have to also override every tint color individually.
   ========================================================================== */
:root[data-theme="dark"] {
    /* The dark theme lightens the brand rather than reusing #0052cc, which on
       a #0c1220 ground measures under 3:1. #93b4ff gives 9.09:1 on the page
       and 8.27:1 on a card — the same treatment the teal had, in blue. */
    --cims-color-primary: #93b4ff;
    --cims-color-primary-rgb: 147, 180, 255;
    --cims-color-primary-dark: #b2c5ff;
    --cims-color-primary-light: #b2c5ff;
    /* Dark mode lifts the accent to a pale blue, so its ink flips to
       near-black — the same rule applied to a different background, not a
       special case. */
    --cims-color-primary-ink: #111827;
    --cims-color-primary-dark-ink: #111827;
    --cims-color-success: #22c55e;
    --cims-color-success-rgb: 34, 197, 94;
    --cims-color-warning: #f59e0b;
    --cims-color-warning-rgb: 245, 158, 11;
    --cims-color-danger: #f87171;
    --cims-color-danger-rgb: 248, 113, 113;
    --cims-color-info: #22d3ee;
    --cims-color-info-rgb: 34, 211, 238;

    --cims-color-bg: #0c1220;
    --cims-color-surface: #141c2e;
    --cims-color-border: #283148;
    --cims-color-text: #e8ecf7;
    --cims-color-text-muted: #93a0bf;
    /* Lighter, not darker, on a dark ground — same reasoning inverted. */
    --cims-color-text-muted-strong: #b6c0d8;
    --cims-color-text-muted-rgb: 147, 160, 191;

    --cims-sidebar-bg: #0a0f1c;
    --cims-sidebar-bg-elevated: #101a2e;
    --cims-sidebar-border: rgba(255, 255, 255, .08);

    --cims-color-surface-alt: #1a2338;
    --cims-color-surface-muted: rgba(255, 255, 255, .055);
    --cims-color-border-subtle: rgba(255, 255, 255, .09);
    --cims-color-bg-rgb: 12, 18, 32;
    --cims-color-text-rgb: 232, 236, 247;

    /* The same five hues, re-stepped for the dark surface rather than flipped:
       #0052cc on #141c2e measures 2.49:1, so the light steps are not merely
       dimmer here, they are unreadable. These sit in the dark lightness band
       (OKLCH L .48–.67) and clear ≥ 3:1 on #141c2e, with worst adjacent-pair
       ΔE 11.0 under deuteranopia and 18.0 under normal vision. */
    --cims-chart-1: #4a90e8;
    --cims-chart-2: #db6a15;
    --cims-chart-3: #12a476;
    --cims-chart-4: #9b7cf0;
    --cims-chart-5: #e05c92;
    /* A 8% ink wash that reads on white disappears on #141c2e; the gridline
       needs the extra alpha to stay a gridline. --cims-chart-label needs no
       dark step of its own — it resolves through --cims-color-text-muted,
       which is overridden above. */
    --cims-chart-grid: rgba(var(--cims-color-text-muted-rgb), .14);

    --cims-shadow-sm: 0 1px 2px rgba(0, 0, 0, .3), 0 1px 1px rgba(0, 0, 0, .2);
    --cims-shadow-md: 0 8px 24px -8px rgba(0, 0, 0, .55), 0 2px 6px rgba(0, 0, 0, .3);
    --cims-shadow-lg: 0 24px 48px -12px rgba(0, 0, 0, .65);

    color-scheme: dark;
}

/* ==========================================================================
   Base
   ========================================================================== */
html {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

body {
    min-height: 100vh;
    background-color: var(--cims-color-bg);
    color: var(--cims-color-text);
    font-family: var(--cims-font-sans);
    font-size: var(--cims-text-base);
    letter-spacing: -.005em;
}

h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6 {
    font-family: var(--cims-font-sans);
    letter-spacing: -.015em;
}

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

/* Accessible focus ring — never remove, only restyle */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible {
    outline: 2px solid var(--cims-color-primary);
    outline-offset: 2px;
}

/* The two navbar icon buttons are the one place the ring above does not land.
   -------------------------------------------------------------------------
   `button:focus-visible` is specificity (0,1,1); Bootstrap ships
   `.btn:focus-visible { outline: 0 }` at (0,2,0), which wins on specificity
   regardless of source order. For most buttons that is harmless — Bootstrap
   substitutes its own box-shadow ring, which does appear. But
   `.cims-navbar-icon-btn` overrides only `color` and `background-color` on
   focus, so the collapse and theme toggles end up with NO outline and NO
   shadow: their sole focus feedback is a background shift from
   rgba(255,255,255,.06) to .12, which on the near-black navbar is close to
   invisible. WCAG 2.1 AA 2.4.7.

   Measured 2026-08-25 by tabbing four screens with a real keyboard, allowing
   the 150ms focus transition to settle before sampling: 166 focus stops, 8
   with no indicator — `cimsSidebarCollapseToggle` and `cimsThemeToggle`, twice
   per page across the four. Every other control was fine once settled.

   White rather than the teal primary, which the dark navbar swallows. Matching
   Bootstrap's (0,2,0) suffices here because Bootstrap is @imported at the top
   of this file, so an equal-specificity rule wins on order. */
.cims-navbar .btn:focus-visible,
.cims-navbar-icon-btn:focus-visible,
.cims-navbar #userMenuButton:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}

/* Native HTML5 :invalid styling (red ring on empty `required` fields)
   must never render before the user has actually interacted with the
   form or the server has round-tripped a real validation error — Bootstrap
   already gates its OWN invalid styles behind `.is-invalid`/`.was-validated`,
   but the bare `:invalid` pseudo-class still paints on pristine, untouched
   fields the instant the page loads. Only show it once the field has been
   touched (`:not(:focus):not(:placeholder-shown)`) or the field carries the
   server-validated `.is-invalid` class. */
.form-control:invalid,
.form-select:invalid {
    border-color: var(--cims-color-border);
    box-shadow: none;
    background-image: none;
}

.form-control:user-invalid,
.form-select:user-invalid,
.form-control.is-invalid,
.form-select.is-invalid {
    border-color: var(--cims-color-danger);
}

.form-control,
.form-select {
    font-size: var(--cims-text-base);
    padding-block: .5625rem;
    border-color: var(--cims-color-border);
    transition: border-color .15s ease-out, box-shadow .15s ease-out;
}

.form-control:focus,
.form-select:focus {
    border-color: var(--cims-color-primary-light);
    box-shadow: 0 0 0 3px rgba(var(--cims-color-primary-rgb), .15);
}

.form-label {
    font-weight: 600;
    font-size: var(--cims-text-sm);
    color: var(--cims-color-text);
    margin-bottom: .375rem;
}

.btn {
    font-weight: 600;
    font-size: var(--cims-text-sm);
    padding-block: .5rem;
    letter-spacing: -.005em;
}

/* `<code>` shipped Bootstrap's pink `--bs-code-color: #d63384`, unmapped, at
   4.08:1 on the page ground — and it is used for real identifiers (org slugs,
   project codes, device UUIDs, language codes), not decoration. */
:root,
:root[data-theme="dark"],
:root[data-theme="light"] {
    --bs-code-color: var(--cims-color-text);
}

code {
    background: rgba(var(--cims-color-text-muted-rgb), .1);
    padding: .1rem .3rem;
    border-radius: var(--cims-radius-sm, .375rem);
}

/* `.bg-light` is `#f8f9fa` with no theme awareness — the same defect class as
   the `bg-white` card footers. In dark mode the audit log's JSON blocks were
   near-white panels carrying near-white text: 1.12:1. */
:root,
:root[data-theme="dark"],
:root[data-theme="light"] {
    --bs-light: var(--cims-color-surface-alt, var(--cims-color-bg));
    --bs-light-rgb: var(--cims-color-bg-rgb, 241, 244, 249);

    /* The BACKGROUND half of the same omission. Bootstrap's neutral grounds
       were unmapped, so anything reading them stayed light in dark mode while
       its text followed the theme. The settings screen's logo preview does
       exactly that from an inline style:

           style="background: var(--bs-tertiary-bg, #f8f9fa)"

       With the token undefined the hardcoded #f8f9fa fallback won, giving
       dark-theme muted text on a near-white panel at 2.48:1. Mapping the token
       fixes it without touching the inline style — and covers every other
       consumer of these grounds at the same time. */
    --bs-tertiary-bg: var(--cims-color-bg);
    --bs-secondary-bg: var(--cims-color-bg);
    --bs-body-tertiary-bg: var(--cims-color-bg);
    --bs-body-secondary-bg: var(--cims-color-bg);
}

.bg-light {
    background-color: var(--cims-color-bg) !important;
    color: var(--cims-color-text);
}

/* Solid semantic badges. Bootstrap picks its ink assuming ITS palette — dark
   text on a light yellow `.bg-warning`. The CIMS warning token is a dark brown
   (#9a4404), so Bootstrap's #212529 ink landed at 2.35:1. The semantic tokens
   are dark in light mode and bright in dark mode, so the ink has to flip with
   the theme rather than be chosen once. */
:root:not([data-theme="dark"]) .badge.bg-primary,
:root:not([data-theme="dark"]) .badge.bg-success,
:root:not([data-theme="dark"]) .badge.bg-warning,
:root:not([data-theme="dark"]) .badge.bg-info,
:root:not([data-theme="dark"]) .badge.bg-danger {
    color: #fff !important;
}

/* Dark mode brightens the semantic tokens, so the same chips need dark ink.
   An earlier blanket `color: #fff` here was wrong for exactly that reason and
   put `.badge.bg-primary` back to 2.47:1. */
:root[data-theme="dark"] .badge.bg-primary,
:root[data-theme="dark"] .badge.bg-success,
:root[data-theme="dark"] .badge.bg-warning,
:root[data-theme="dark"] .badge.bg-info,
:root[data-theme="dark"] .badge.bg-danger {
    color: #0b1220 !important;
}

.badge.bg-light {
    color: var(--cims-color-text) !important;
}

/* Bootstrap compiles LITERAL hexes into every `.btn-outline-*` variant, so
   mapping `--bs-primary` / `--bs-warning` (lines 97-108) never reaches them.
   `.btn-outline-primary` rendered Bootstrap blue at 4.08:1; `.btn-outline-
   warning` rendered Bootstrap's #ffc107 at **1.48:1** — "Expiring Documents"
   and "Expiring Warranty / DLP" were effectively unreadable. Off-brand and
   below the floor at the same time. */
/* `color` is set EXPLICITLY here, not only through `--bs-btn-color`.
   Setting the custom property alone was verified insufficient: with my rule
   winning the cascade (rule 3589 vs Bootstrap's 351 in the same sheet) the
   property computed to #9a4404, yet `color` still resolved to Bootstrap's
   #ffc107 — reproducibly, and even when `color: var(--bs-btn-color)` was forced
   inline. I could not account for that through the cascade, so the colour is
   declared directly: it guarantees the rendered result regardless of the
   mechanism, and the custom properties are kept so hover/active states stay
   consistent. */
.btn-outline-secondary {
    --bs-btn-color: var(--cims-color-text-muted-strong);
    --bs-btn-border-color: var(--cims-color-border);
    --bs-btn-hover-bg: var(--cims-color-text-muted);
    --bs-btn-hover-border-color: var(--cims-color-text-muted);
    color: var(--cims-color-text-muted-strong);
}

.btn-outline-success {
    color: var(--cims-color-success);
    --bs-btn-color: var(--cims-color-success);
    --bs-btn-border-color: var(--cims-color-success);
    --bs-btn-hover-bg: var(--cims-color-success);
    --bs-btn-hover-border-color: var(--cims-color-success);
    --bs-btn-active-bg: var(--cims-color-success);
    --bs-btn-active-border-color: var(--cims-color-success);
}

.btn-outline-warning {
    color: var(--cims-color-warning);
    --bs-btn-color: var(--cims-color-warning);
    --bs-btn-border-color: var(--cims-color-warning);
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: var(--cims-color-warning);
    --bs-btn-hover-border-color: var(--cims-color-warning);
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: var(--cims-color-warning);
    --bs-btn-active-border-color: var(--cims-color-warning);
}

.btn-outline-info {
    color: var(--cims-color-info);
    --bs-btn-color: var(--cims-color-info);
    --bs-btn-border-color: var(--cims-color-info);
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: var(--cims-color-info);
    --bs-btn-hover-border-color: var(--cims-color-info);
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: var(--cims-color-info);
    --bs-btn-active-border-color: var(--cims-color-info);
}

.btn-outline-danger {
    color: var(--cims-color-danger);
    --bs-btn-color: var(--cims-color-danger);
    --bs-btn-border-color: var(--cims-color-danger);
    --bs-btn-hover-bg: var(--cims-color-danger);
    --bs-btn-hover-border-color: var(--cims-color-danger);
    --bs-btn-active-bg: var(--cims-color-danger);
    --bs-btn-active-border-color: var(--cims-color-danger);
}

/* Bootstrap compiles a LITERAL #0d6efd into `.btn-outline-primary`, so mapping
   `--bs-primary` (line 93) never reaches it: the outline buttons rendered in
   Bootstrap blue rather than the brand accent, and measured 4.08:1 on the page
   ground — off-brand AND below the 4.5:1 floor. The brand teal is 5.56:1. */
.btn-outline-primary {
    color: var(--cims-color-primary);
    --bs-btn-color: var(--cims-color-primary);
    --bs-btn-border-color: var(--cims-color-primary);
    --bs-btn-hover-bg: var(--cims-color-primary);
    --bs-btn-hover-border-color: var(--cims-color-primary);
    --bs-btn-active-bg: var(--cims-color-primary-dark);
    --bs-btn-active-border-color: var(--cims-color-primary-dark);
    --bs-btn-focus-shadow-rgb: var(--cims-color-primary-rgb);
}

.btn-primary {
    --bs-btn-bg: var(--cims-color-primary);
    --bs-btn-border-color: var(--cims-color-primary);
    --bs-btn-hover-bg: var(--cims-color-primary-dark);
    --bs-btn-hover-border-color: var(--cims-color-primary-dark);
    /* The pressed state was left at Bootstrap's own #0a58ca, so every primary
       button in the product flashed stock blue for as long as it was held
       down — including on an install that had never set a brand colour, where
       the palette is teal. `.btn-outline-primary` directly above already
       derives this; `.btn-primary` simply never did. */
    --bs-btn-active-bg: var(--cims-color-primary-dark);
    --bs-btn-active-border-color: var(--cims-color-primary-dark);
    --bs-btn-focus-shadow-rgb: var(--cims-color-primary-rgb);

    /* The label follows the background instead of being fixed at white.
       Hover and active get their own ink because they sit on the DARKER
       shade, which on a pale accent can want a different answer. */
    --bs-btn-color: var(--cims-color-primary-ink);
    --bs-btn-hover-color: var(--cims-color-primary-dark-ink);
    --bs-btn-active-color: var(--cims-color-primary-dark-ink);
    --bs-btn-disabled-color: var(--cims-color-primary-ink);

    box-shadow: 0 1px 2px rgba(var(--cims-color-primary-rgb), .25);
}

/* Dark mode lightens the accent 45% toward white so it reads against a
   near-black page (see ColourContrastService::darkVariant). That makes it a
   LIGHT button, and white text on it fails: the shipped #0b6e63 lightens to
   #79afa9, where white is 2.47:1. Dark ink on the same swatch is 7.59:1.
   ColourContrastService now assesses this pairing when an admin picks a colour,
   so a brand whose lightened variant fails dark ink is refused at the source. */
/* Same problem as the primary button, one component over. The dark theme
   brightens the semantic colours so they read against a near-black page
   (--cims-color-success goes #166534 -> #22c55e), which makes a solid
   `.badge.bg-success` a LIGHT chip — and Bootstrap keeps white text on it.
   Measured 2.28:1. Dark ink on the same chip clears AA comfortably. */
:root[data-theme="dark"] .badge.bg-success,
:root[data-theme="dark"] .badge.bg-warning,
:root[data-theme="dark"] .badge.bg-info {
    color: #0b1220 !important; /* Bootstrap sets badge colour with !important */
}

/* Dark mode needs no button-ink rule of its own any more: `.btn-primary`
   reads `--cims-color-primary-ink`, and the dark block above redefines that
   token. One mechanism, both themes — the #0b1220 hardcoded here was right
   for the shipped palette and wrong for every other. */

/* --- Phase 4: surfaces, controls, chrome ---------------------------------- */
.card {
    border-color: var(--cims-color-border);
    box-shadow: var(--cims-shadow-sm);
    border-radius: var(--cims-radius-md);
}

.card-header {
    background-color: var(--cims-color-surface);
    border-bottom: 1px solid var(--cims-color-border);
    font-size: var(--cims-text-sm);
    font-weight: 600;
    letter-spacing: -.005em;
    padding-block: 1rem;
}

/* Buttons: a touch more presence and a consistent optical height across
   sizes, so a toolbar of mixed btn / btn-sm does not look ragged. */
.btn {
    font-weight: 500;
    border-radius: var(--cims-radius-sm);
    letter-spacing: -.005em;
    --bs-btn-padding-y: .4375rem;
    --bs-btn-padding-x: .875rem;
}

.btn-sm {
    --bs-btn-padding-y: .3125rem;
    --bs-btn-padding-x: .625rem;
    --bs-btn-font-size: var(--cims-text-sm);
}

.btn-lg {
    --bs-btn-padding-y: .625rem;
    --bs-btn-padding-x: 1.25rem;
}

/* Outline buttons carry most of the secondary actions in this product; the
   Bootstrap default border is heavy enough to compete with primaries. */
.btn-outline-secondary {
    --bs-btn-color: var(--cims-color-text);
    --bs-btn-border-color: var(--cims-color-border);
    --bs-btn-hover-bg: var(--cims-color-bg);
    --bs-btn-hover-color: var(--cims-color-text);
    --bs-btn-hover-border-color: var(--cims-color-text-muted);
}

/* Form controls: match the 4px rhythm and give focus a brand ring rather
   than Bootstrap's default blue glow. */
.form-control,
.form-select {
    border-color: var(--cims-color-border);
    border-radius: var(--cims-radius-sm);
    padding-block: .4375rem;
}

.form-control:focus,
.form-select:focus {
    border-color: var(--cims-color-primary-light);
    box-shadow: 0 0 0 3px rgba(var(--cims-color-primary-rgb), .15);
}

.form-label {
    font-size: var(--cims-text-sm);
    font-weight: 500;
    margin-bottom: .375rem;
}

/* Badges read as labels, not buttons: lighter weight, wider tracking, and a
   tint rather than a solid fill wherever Bootstrap's semantic classes are
   used directly. */
.badge {
    font-weight: 600;
    letter-spacing: .01em;
    padding: .3125rem .5rem;
    border-radius: .375rem;
}

/* Modals: match the card language so a popup form reads as the same system. */
.modal-content {
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-lg);
    box-shadow: var(--cims-shadow-lg);
}

.modal-header,
.modal-footer {
    border-color: var(--cims-color-border);
    padding-inline: var(--cims-space-4);
}

.modal-header {
    padding-block: var(--cims-space-3);
}

.modal-title {
    font-size: var(--cims-text-md);
    font-weight: 600;
    letter-spacing: -.015em;
}

.modal-body {
    padding: var(--cims-space-4);
}

/* Detail/show-page title row — record name + one or more <x-status-badge>
   pills, identically hand-rolled as <h1 class="d-flex align-items-center
   gap-2"> across 24 show pages (indents, purchase-orders, tenders, projects,
   budgets, vendors, and 18 more — confirmed no other h1 in the app uses
   d-flex, so this selector can't catch anything unintended). With no
   flex-wrap, a long record name plus badge text forced real horizontal page
   scroll and clipped the badge on narrow screens. One rule here fixes every
   instance at once instead of migrating 24 files to a shared component.
   overflow-wrap: anywhere lets the record-name text itself shrink below its
   single-line width — flex items default to a min-width based on their
   unwrapped content, which is what was blocking the wrap in the first
   place. */
h1.d-flex {
    flex-wrap: wrap;
    overflow-wrap: anywhere;
    row-gap: .25rem;
}

/* GIS map — was a fixed inline height: 640px, not viewport-relative; on a
   short mobile screen that leaves the filter/legend panel below it pushed
   far down, and on a very short landscape view the map itself can exceed
   the viewport. clamp() keeps it usable across phone/tablet/desktop without
   a media query per breakpoint. */
.cims-gis-map {
    width: 100%;
    height: clamp(320px, 70dvh, 640px);
}

/* Wide tables already scroll correctly *inside* .table-responsive on narrow
   screens (Bootstrap default) rather than blowing out the page — but nothing
   hints that a table can be swiped sideways. A static edge-fade is a simpler,
   safer cue than a scroll-position-aware gradient (no JS, no risk of a stale
   mask after scrolling to the end — a minor imperfection worth the lower
   complexity here). */
.table-responsive {
    -webkit-overflow-scrolling: touch;
    position: relative;
}

/* The right-edge gradient that used to live here was removed on 2026-08-23.
   It was static — painted on every table whether or not it actually
   overflowed — so on the majority of screens it read as a stray inner shadow
   against the card edge rather than a scroll affordance. A scroll-position
   aware cue would need JS per table; the horizontal scrollbar plus the
   clipped column already signal scrollability well enough to not be worth
   that. */

.skip-link {
    position: absolute;
    top: -48px;
    left: var(--cims-space-2);
    z-index: 1100;
    background: var(--cims-color-primary);
    color: #fff;
    padding: var(--cims-space-2) var(--cims-space-3);
    border-radius: var(--cims-radius-sm);
    transition: top .15s ease-out;
}

.skip-link:focus {
    top: var(--cims-space-2);
    color: #fff;
}

/* ==========================================================================
   App shell — navbar
   ========================================================================== */
.cims-navbar {
    height: var(--cims-navbar-height);
    background-color: var(--cims-sidebar-bg) !important;
    border-bottom: 1px solid var(--cims-sidebar-border);
    /* No drop shadow: the sidebar below is the same colour, so a shadow only
       draws a seam across two surfaces that should read as one chrome. */
    box-shadow: none;
}

.cims-navbar .navbar-brand {
    letter-spacing: -.01em;
    font-size: 1.0625rem;
}

.cims-navbar .navbar-brand .cims-logo-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.125rem;
    height: 2.125rem;
    border-radius: .625rem;
    background: linear-gradient(155deg, var(--cims-color-primary-light), var(--cims-color-primary-dark));
    color: #fff;
    font-size: 1.0625rem;
    box-shadow: 0 2px 8px rgba(var(--cims-color-primary-rgb), .4);
}

.cims-navbar-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    color: rgba(255, 255, 255, .8);
    background-color: rgba(255, 255, 255, .06);
    border: 1px solid var(--cims-sidebar-border);
    border-radius: var(--cims-radius-sm);
    transition: background-color .12s ease-out, color .12s ease-out;
}

.cims-navbar-icon-btn i {
    font-size: 1.05rem;
}

.cims-navbar-icon-btn:hover,
.cims-navbar-icon-btn:focus-visible {
    color: #fff;
    background-color: rgba(255, 255, 255, .12);
}

.cims-navbar-icon-btn[aria-pressed="true"] {
    color: var(--cims-color-primary-light);
    background-color: rgba(var(--cims-color-primary-rgb), .22);
    border-color: rgba(var(--cims-color-primary-rgb), .4);
}

.cims-navbar #userMenuButton {
    background-color: rgba(255, 255, 255, .06);
    border: 1px solid var(--cims-sidebar-border);
    border-radius: var(--cims-radius-sm);
    font-size: var(--cims-text-sm);
    font-weight: 500;
}

/* Bounded at every width so a long display name can never push the topbar
   (and therefore its dropdown-menu-end, anchored to the trigger's own right
   edge) past the viewport. The full name is still available via the button's
   [title] attribute. */
.cims-username {
    max-width: 12rem;
}

/* Below 400px the brand, three icon buttons and the user name no longer fit
   the bar: measured 9px of overflow at 360px on every page. The name is the
   only expendable element — the avatar icon and the dropdown still identify
   the account, and the name is the first thing inside the menu. */
@media (max-width: 399.98px) {
    .cims-username {
        display: none;
    }

    /* min-width, not just padding: with the name and caret hidden the button
       collapsed to 18px wide — narrower than the 44px touch minimum, which is
       worse than the 9px overflow it was fixing. */
    .cims-navbar #userMenuButton {
        padding-inline: .625rem;
        min-width: 2.75rem;
        justify-content: center;
    }

    .cims-navbar #userMenuButton::after {
        display: none;
    }
}

/* The DataTables toolbar and pager render at Bootstrap's -sm sizes: search
   34px, length select 30px, page buttons 32-34px. Fine with a mouse, all under
   the 44px touch minimum.

   Keyed to pointer, not width. The first version of this used max-width:768px,
   which silently excluded every tablet — an iPad Air in landscape is 1180px
   wide and still operated with a finger, so it kept the 30px select. A
   coarse primary pointer is the actual condition; a hybrid reverts to the
   compact toolbar when its keyboard is attached, which is correct. */
@media (pointer: coarse) {
    .dt-container .dt-search input[type="search"],
    .dt-container .dt-length select {
        min-height: 2.75rem;
    }

    /* Bootstrap's .page-link is display:block, so min-height alone would pin
       the label to the top of a 44px box. */
    .dt-container .dt-paging .page-link {
        min-width: 2.75rem;
        min-height: 2.75rem;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    /* Touch-target minimum (WCAG 2.5.5 / Apple HIG / Material: 44x44px).
       These lived under max-width:991.98px, next to the sidebar-drawer rules,
       which tied "is this a finger?" to "is the sidebar docked?". Those are
       unrelated questions: a docked sidebar starts at 992px, but an iPad Pro
       in portrait is 1024px and still a finger, so every row action button in
       the app measured 38px there. Width stays for layout below; touch sizing
       belongs on the pointer. */
    .btn {
        min-height: 44px;
    }

    .cims-navbar-icon-btn,
    #cimsSidebarToggle {
        min-width: 44px;
        min-height: 44px;
    }

    .cims-sidebar .nav-link {
        min-height: 44px;
    }

    /* iOS Safari auto-zooms the viewport when a field under 16px takes focus.
       Also pointer-driven, not width-driven — an iPad zooms at 1024px too.
       min-height for the same reason as .btn: a text field is a tap target,
       and Bootstrap's default lands at 40px. Excludes textarea, which sizes
       from its rows attribute and is already well over the minimum. */
    .form-control:not(textarea),
    .form-select {
        font-size: 1rem;
        min-height: 2.75rem;
    }
}

/* Layout, not touch: below 768px the search field has no room to sit beside
   the length menu, so it takes the full row. */
@media (max-width: 767.98px) {
    .dt-container .dt-search input[type="search"] {
        min-width: 0;
        width: 100%;
    }

    .dt-container .dt-search {
        width: 100%;
    }
}

.cims-navbar #userMenuButton:hover,
.cims-navbar #userMenuButton:focus {
    background-color: rgba(255, 255, 255, .1);
}

/* ==========================================================================
   App shell — sidebar
   ========================================================================== */
.cims-sidebar {
    position: fixed;
    top: var(--cims-navbar-height);
    bottom: 0;
    left: 0;
    width: var(--cims-sidebar-width);
    overflow-y: auto;
    background-color: var(--cims-sidebar-bg);
    padding: var(--cims-space-2) 0 var(--cims-space-4);
    z-index: 1020;
    box-shadow: 1px 0 0 rgba(0, 0, 0, .18);
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, .15) transparent;
}

.cims-sidebar::-webkit-scrollbar {
    width: 6px;
}

.cims-sidebar::-webkit-scrollbar-thumb {
    background-color: rgba(255, 255, 255, .14);
    border-radius: 999px;
}

/* --- Phase 2: app shell ----------------------------------------------------
   A 19-module sidebar is the densest surface in the product. The changes are
   about scanning, not decoration: a taller row, a wider icon gutter so labels
   align on a single axis, and an active state that reads at a glance. */
.cims-sidebar .nav-link {
    display: flex;
    align-items: center;
    gap: .75rem;
    color: var(--cims-sidebar-text);
    border-radius: var(--cims-radius-sm);
    margin: .125rem var(--cims-space-2);
    padding: .5625rem .75rem;
    font-size: var(--cims-text-sm);
    font-weight: 500;
    position: relative;
    letter-spacing: -.005em;
}

.cims-sidebar .nav-link i {
    font-size: 1.0625rem;
    width: 1.375rem;
    flex: none;
    text-align: center;
    opacity: .9;
}

.cims-sidebar .nav-link {
    transition: background-color .12s ease-out, color .12s ease-out;
}

.cims-sidebar .nav-link:hover {
    color: var(--cims-sidebar-text-active);
    background-color: var(--cims-sidebar-bg-active);
}

.cims-sidebar .nav-link.active {
    color: var(--cims-sidebar-text-active);
    background-color: rgba(var(--cims-color-primary-rgb), .22);
    font-weight: 600;
}

/* The rail sits flush to the sidebar edge rather than floating half-outside
   the link, so a scanning eye gets one continuous vertical reference for
   "you are here" instead of a detached tick. */
.cims-sidebar .nav-link.active::before {
    content: "";
    position: absolute;
    left: calc(var(--cims-space-2) * -1);
    top: .1875rem;
    bottom: .1875rem;
    width: 3px;
    border-start-end-radius: 3px;
    border-end-end-radius: 3px;
    background-color: var(--cims-color-primary-light);
}

/* Icons carry the active state too — colour alone is not enough at 13px on a
   dark ground. */
.cims-sidebar .nav-link.active i {
    color: var(--cims-color-primary-light);
}

.cims-sidebar .nav-link.disabled {
    color: rgba(255, 255, 255, .3);
    pointer-events: none;
}

.cims-sidebar .nav-link .badge-soon {
    margin-left: auto;
    font-size: .625rem;
    font-weight: 600;
    letter-spacing: .03em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, .45);
    background: rgba(255, 255, 255, .08);
    border-radius: 999px;
    padding: .125rem .5rem;
}

/* Section band ("Modules", "Administration") — a non-interactive divider
   that splits the sidebar's 16 groups into scannable bands. Distinct
   from .nav-header, which is a clickable <summary> for a real group. */
.cims-sidebar .cims-nav-section {
    display: flex;
    align-items: center;
    gap: var(--cims-space-2);
    padding: 0 var(--cims-space-3);
    margin: var(--cims-space-4) 0 var(--cims-space-1);
}

.cims-sidebar .cims-nav-section > span {
    color: var(--cims-sidebar-heading);
    text-transform: uppercase;
    font-size: .6875rem;
    letter-spacing: .08em;
    font-weight: 700;
    white-space: nowrap;
}

.cims-sidebar .cims-nav-section::after {
    content: "";
    flex: 1;
    height: 1px;
    background-color: var(--cims-sidebar-border);
}

/* A sub-band inside one group's list (Masters > Registers). Sits within the
   group's own indent guide, so it stays quieter than a top-level band and
   carries no rule of its own. */
.cims-sidebar .cims-nav-subsection {
    padding: var(--cims-space-3) var(--cims-space-3) .25rem 1.75rem;
}

.cims-sidebar .cims-nav-subsection > span {
    color: var(--cims-sidebar-heading);
    text-transform: uppercase;
    font-size: .625rem;
    letter-spacing: .07em;
    font-weight: 700;
}

.cims-sidebar .nav-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    background: none;
    border: 0;
    color: var(--cims-sidebar-heading);
    text-transform: uppercase;
    font-size: var(--cims-text-xs);
    letter-spacing: .06em;
    font-weight: 600;
    padding: var(--cims-space-2) var(--cims-space-3) .375rem;
    margin-top: var(--cims-space-2);
    cursor: pointer;
}

.cims-sidebar .nav-header:hover {
    color: rgba(255, 255, 255, .6);
}

/* ---------------------------------------------------------------------------
   Sidebar group presentation.

   The headers were styled as passive section captions — small, uppercase, low
   contrast — while actually being the primary controls for a 16-group,
   ~90-link menu. At that size they read as decoration rather than something
   clickable, which is why the nav felt like an undifferentiated list. These
   rules make a group header look like the control it is, and make the section
   owning the current page visible without expanding anything.
   --------------------------------------------------------------------------- */

.cims-sidebar .nav-header {
    /* Sentence case at a readable size: these are navigation controls, not
       captions. Uppercase micro-type is harder to scan at 16 repetitions. */
    text-transform: none;
    font-size: var(--cims-text-sm);
    letter-spacing: 0;
    border-radius: var(--cims-radius-sm, .375rem);
    transition: background-color .15s ease-out, color .15s ease-out;
}

.cims-sidebar .nav-header:hover,
.cims-sidebar .nav-header:focus-visible {
    background-color: rgba(255, 255, 255, .06);
    color: #fff;
}

/* The open group is the user's current context — give it a quiet ground so the
   expanded links read as belonging to it rather than floating in the rail. */
.cims-sidebar details[open] > .nav-header {
    color: #fff;
    background-color: rgba(255, 255, 255, .05);
}

/* The section owning the current page, marked by sidebar-nav.js. Distinct from
   :open, because a user can expand a different section while still being ON a
   page — losing that cue is how people get lost in a deep menu. */
.cims-sidebar details.is-current-section > .nav-header {
    color: #fff;
    box-shadow: inset 2px 0 0 var(--cims-color-primary);
}

/* Only one group is open at a time, so its links can afford a little more room
   without lengthening the rail. */
html:not(.cims-sidebar-collapsed) .cims-sidebar details[open] > ul.nav > li > .nav-link {
    padding-top: .3rem;
    padding-bottom: .3rem;
}

/* Respect a user who has asked for less motion. */
@media (prefers-reduced-motion: reduce) {
    .cims-sidebar .nav-header,
    .cims-sidebar .nav-header .bi-chevron-down {
        transition: none;
    }
}

/* ---------------------------------------------------------------------------
   One icon column for the whole rail.

   Group headers were laid out `justify-content: space-between` over THREE
   children (icon, label, chevron), which pushes the label into the middle of
   whatever space the chevron leaves. The label's x-position therefore depended
   on its own text width, so "Budgets" started 39px further right than "Project
   Management" — the labels stepped in and out down the rail. That raggedness,
   not the grouping itself, is what read as unfinished.

   Left-align the row and let only the chevron claim the free space, so every
   icon and every label starts on the same vertical line. --------------------- */
.cims-sidebar .nav-header {
    justify-content: flex-start;
    gap: .75rem;               /* matches .nav-link, so both tiers share a column */
}

.cims-sidebar .nav-header > .bi:first-child {
    font-size: 1.0625rem;      /* identical metrics to .nav-link i */
    width: 1.375rem;
    flex: none;
    text-align: center;
    opacity: .9;
}

.cims-sidebar .nav-header .bi-chevron-down {
    margin-left: auto;         /* the only thing that pushes right */
    font-size: .75rem;
    opacity: .55;
}

/* The seven top-level links (Dashboard, Budgets, Vendors, Tenders, Contracts,
   BIM Models, Audit Logs). Each owns exactly one module-level page: their other
   routes are record-scoped, and every index page already carries its own "New …"
   button. Wrapping them in a dropdown would therefore add a chevron that expands
   to a copy of the row it hangs from, plus a "New X" entry duplicating a button
   already on the page it links to.

   So they stay single rows and instead take the group headers' box metrics —
   one baseline, one icon column, one hover ground across the whole rail. The
   absent chevron then means what it should: this row has no sub-menu. */
html:not(.cims-sidebar-collapsed) .cims-sidebar > ul.nav > li.nav-item > .nav-link {
    margin: var(--cims-space-2) 0 0;
    padding: var(--cims-space-2) var(--cims-space-3) .375rem;
    font-weight: 600;
}

.cims-sidebar .nav-header .bi-chevron-down {
    transition: transform .15s ease-out;
}

.cims-sidebar details[open] > .nav-header .bi-chevron-down {
    transform: rotate(180deg);
}

html:not(.cims-sidebar-collapsed) .cims-sidebar details > ul.nav {
    position: relative;
    margin: .25rem 0 .5rem;
    padding-left: var(--cims-space-3);
}

html:not(.cims-sidebar-collapsed) .cims-sidebar details > ul.nav::before {
    content: "";
    position: absolute;
    left: 1.375rem;
    top: 0;
    bottom: .375rem;
    width: 1px;
    background-color: var(--cims-sidebar-border);
}

html:not(.cims-sidebar-collapsed) .cims-sidebar details > ul.nav .nav-link {
    padding-left: 1.75rem;
}

html:not(.cims-sidebar-collapsed) .cims-sidebar details > ul.nav .nav-link.active::before {
    left: -.25rem;
}

.cims-sidebar summary::-webkit-details-marker {
    display: none;
}

/* ==========================================================================
   App shell — content
   ========================================================================== */
.cims-content {
    margin-left: var(--cims-sidebar-width);
    padding-top: calc(var(--cims-navbar-height) + var(--cims-space-4));
    min-height: 100vh;
    transition: margin-left .18s ease-out;
}

/* Breadcrumb.
   Bootstrap's default renders the trail as underlined body links separated by
   a "/" — visually it competes with the page heading it sits above, when its
   whole job is to be quiet chrome you read only when you are lost. Muted,
   chevron-separated, uppercase-free, and only the clickable crumbs react to
   the pointer; the current page is stated, not offered. */
.cims-breadcrumb {
    font-size: var(--cims-text-sm);
    line-height: 1.2;
}

.cims-breadcrumb .breadcrumb {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    row-gap: .25rem;
}

.cims-breadcrumb .breadcrumb-item + .breadcrumb-item {
    padding-inline-start: var(--cims-space-2);
}

/* The divider is set here rather than through --bs-breadcrumb-divider because
   that variable takes a quoted string only: a font glyph needs its own
   font-family, which the ::before below can carry. */
.cims-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
    content: "\f285"; /* bi-chevron-right */
    font-family: "bootstrap-icons";
    font-size: .625rem;
    color: var(--cims-color-text-muted);
    opacity: .6;
    padding-inline-end: var(--cims-space-2);
    line-height: 1;
    float: none;
}

[dir="rtl"] .cims-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
    content: "\f284"; /* bi-chevron-left — the trail runs the other way */
}

.cims-breadcrumb .breadcrumb-item a {
    color: var(--cims-color-text-muted);
    font-weight: 500;
    text-decoration: none;
    transition: color .15s ease-out;
}

.cims-breadcrumb .breadcrumb-item a:hover,
.cims-breadcrumb .breadcrumb-item a:focus-visible {
    color: var(--cims-color-primary);
    text-decoration: underline;
    text-underline-offset: .1875rem;
}

/* Where the reader is now: the only crumb at full text colour. */
.cims-breadcrumb .breadcrumb-item.active {
    color: var(--cims-color-text);
    font-weight: 600;
}

@media (max-width: 991.98px) {
    .cims-sidebar {
        left: -100%;
        transition: left .2s ease-in-out;
    }

    .cims-sidebar.show {
        left: 0;
    }

    .cims-content {
        margin-left: 0;
    }

    .cims-username {
        max-width: 6.25rem;
    }

    /* The username text already identifies the account — the icon is
       redundant chrome competing with it for the topbar's tightest space. */
    #userMenuButton .bi-person-circle {
        display: none;
    }
}

/* ==========================================================================
   Sidebar collapse (desktop icon rail) — toggled via #cimsSidebarCollapseToggle,
   state lives as a class on <html> (not the sidebar itself) so the pre-paint
   script in the layout can apply it before first render with zero flash.
   ========================================================================== */
@media (min-width: 992px) {
    .cims-sidebar {
        transition: width .18s ease-out;
    }

    /* Scrolls, like the expanded rail does.
       This was `overflow: visible`, so that the CSS hover fly-out (an
       ::after positioned past the right edge) would not be clipped. The
       side effect was that the rail stopped scrolling entirely: the nav is
       ~1300px of content in a ~900px column, so the last eight modules —
       Administration, Masters, Settings, Audit Logs — could not be reached
       at all while collapsed. A label that cannot be clipped is worth less
       than a menu that can be scrolled, so the fly-out moved to a
       fixed-position element (see .cims-rail-tip) and the overflow came
       back. */
    html.cims-sidebar-collapsed .cims-sidebar {
        width: var(--cims-sidebar-width-collapsed);
        overflow-y: auto;
        overflow-x: hidden;
        overscroll-behavior: contain;
    }

    html.cims-sidebar-collapsed .cims-content {
        margin-left: var(--cims-sidebar-width-collapsed);
    }

    html.cims-sidebar-collapsed .cims-navbar .navbar-brand span:not(.cims-logo-mark) {
        display: none;
    }

    html.cims-sidebar-collapsed .cims-sidebar .cims-nav-label,
    html.cims-sidebar-collapsed .cims-sidebar .nav-header .bi-chevron-down,
    html.cims-sidebar-collapsed .cims-sidebar .badge-soon {
        display: none;
    }

    html.cims-sidebar-collapsed .cims-sidebar .nav-link,
    html.cims-sidebar-collapsed .cims-sidebar .nav-header {
        justify-content: center;
        padding-inline: 0;
        margin-inline: var(--cims-space-2);
        /* `.nav-header` carries `width: 100%` for the expanded rail. Left in
           place here it stayed 76px wide *and* took the 8px inline margin, so
           it overflowed the rail and its centred icon landed 8px right of
           every link's — a rail of icons on two axes. Measured before the fix:
           link centre 38px, group centre 46px. */
        width: auto;
        /* One row height for both tiers. They were 44px and 40px, which read
           as uneven spacing down a column of otherwise identical squares. */
        min-height: 2.75rem;
        padding-block: 0;
        margin-block: .125rem;
    }

    html.cims-sidebar-collapsed .cims-sidebar .nav-link.active::before {
        left: -.5rem;
    }

    html.cims-sidebar-collapsed .cims-sidebar ul.nav.flex-column ul.nav.flex-column {
        padding-left: 0;
    }

    /* The collapsed rail is icon-only, so a text band has nothing to show
       and would only add dead vertical space between groups — it degrades
       to a plain separator rule, and the in-group sub-band disappears
       entirely. */
    html.cims-sidebar-collapsed .cims-sidebar .cims-nav-section {
        margin: var(--cims-space-3) var(--cims-space-2);
        padding: 0;
        border-top: 1px solid var(--cims-sidebar-border);
    }

    html.cims-sidebar-collapsed .cims-sidebar .cims-nav-section > span,
    html.cims-sidebar-collapsed .cims-sidebar .cims-nav-section::after,
    html.cims-sidebar-collapsed .cims-sidebar .cims-nav-subsection {
        display: none;
    }

    /* The hover label is a single fixed-position element positioned by
       sidebar-nav.js, not an ::after on the item.

       An ::after lives inside the scroll container, so showing it required
       `overflow: visible` on the rail, which cost the rail its scrollbar. A
       fixed element is positioned against the viewport instead, so the rail
       can scroll and the label still escapes it. It also appears on keyboard
       focus, which a :hover rule never did. */
    html.cims-sidebar-collapsed .cims-sidebar .nav-link,
    html.cims-sidebar-collapsed .cims-sidebar .nav-header {
        position: relative;
    }
}

.cims-rail-tip {
    position: fixed;
    z-index: 1060;
    background: var(--cims-sidebar-bg-elevated);
    color: var(--cims-sidebar-text-active);
    font-size: var(--cims-text-sm);
    font-weight: 500;
    white-space: nowrap;
    padding: .375rem .75rem;
    border-radius: var(--cims-radius-sm);
    box-shadow: var(--cims-shadow-md);
    border: 1px solid var(--cims-sidebar-border);
    pointer-events: none;
    opacity: 0;
    transform: translateX(-.25rem);
    transition: opacity .12s ease-out, transform .12s ease-out;
}

.cims-rail-tip[data-shown] {
    opacity: 1;
    transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
    .cims-rail-tip {
        transition: none;
    }
}

/* ==========================================================================
   Shared components
   ========================================================================== */

/* Page header */
.cims-page-header {
    display: flex;
    flex-wrap: wrap;
    gap: var(--cims-space-3);
    align-items: flex-start;
    justify-content: space-between;
    margin-bottom: var(--cims-space-4);
}

.cims-page-header h1,
.cims-page-header h2 {
    /* The page title is the only type on a list screen with no competition —
       giving it real size is what separates a product from a CRUD scaffold.
       Tighter tracking keeps it from feeling inflated at this weight. */
    font-size: 1.625rem;
    font-weight: 700;
    letter-spacing: -.025em;
    margin-bottom: 0;
    text-wrap: balance;
}

.cims-page-header .cims-page-subtitle {
    color: var(--cims-color-text-muted);
    font-size: var(--cims-text-sm);
    margin-top: .25rem;
    max-width: 68ch;
    line-height: 1.5;
}

/* A hairline under the header groups the title with the content beneath it
   and stops the actions row floating free of the page. */
.cims-page-header {
    padding-bottom: var(--cims-space-3);
    border-bottom: 1px solid var(--cims-color-border);
}

/* KPI stat card */
.cims-kpi-card {
    background: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-md);
    box-shadow: var(--cims-shadow-sm);
    padding: var(--cims-space-4);
    display: flex;
    align-items: flex-start;
    gap: var(--cims-space-3);
    transition: box-shadow .15s ease-out, transform .15s ease-out;
}

.cims-kpi-card:hover {
    box-shadow: var(--cims-shadow-md);
    transform: translateY(-1px);
}

.cims-kpi-card .cims-kpi-icon {
    width: 2.75rem;
    height: 2.75rem;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--cims-radius-md);
    font-size: 1.25rem;
}

/* KPI / stat icon color variants — token-driven (rgba over a CSS custom
   property) rather than fixed hex, so they stay legible in dark mode
   instead of rendering as a bright, washed-out tint on a dark surface. */
.cims-kpi-icon--primary { color: var(--cims-color-primary); background: rgba(var(--cims-color-primary-rgb), .12); }
.cims-kpi-icon--warning { color: var(--cims-color-warning); background: rgba(var(--cims-color-warning-rgb), .14); }
.cims-kpi-icon--success { color: var(--cims-color-success); background: rgba(var(--cims-color-success-rgb), .14); }
.cims-kpi-icon--danger { color: var(--cims-color-danger); background: rgba(var(--cims-color-danger-rgb), .14); }
.cims-kpi-icon--info { color: var(--cims-color-info); background: rgba(var(--cims-color-info-rgb), .14); }
.cims-kpi-icon--neutral { color: var(--cims-color-text-muted); background: rgba(var(--cims-color-primary-rgb), .06); }

.cims-kpi-card .cims-kpi-label {
    font-size: var(--cims-text-xs);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--cims-color-text-muted);
    font-weight: 600;
}

.cims-kpi-card .cims-kpi-value {
    font-size: var(--cims-text-xl);
    font-weight: 700;
    line-height: 1.2;
}

/* Dashboard hero greeting */
.cims-dashboard-hero h1 {
    font-size: var(--cims-text-xl);
    font-weight: 800;
    letter-spacing: -.02em;
}

.cims-dashboard-greeting {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    font-size: var(--cims-text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--cims-color-primary);
    margin-bottom: .375rem;
}

.cims-live-dot {
    width: .5rem;
    height: .5rem;
    border-radius: 999px;
    background: var(--cims-color-success);
    box-shadow: 0 0 0 rgba(21, 128, 61, .5);
    animation: cims-pulse 2s infinite;
}

@keyframes cims-pulse {
    0% { box-shadow: 0 0 0 0 rgba(21, 128, 61, .45); }
    70% { box-shadow: 0 0 0 6px rgba(21, 128, 61, 0); }
    100% { box-shadow: 0 0 0 0 rgba(21, 128, 61, 0); }
}

@media (prefers-reduced-motion: reduce) {
    .cims-live-dot { animation: none; }
}

/* Dashboard chart card — fills the card's actual height instead of a
   fixed pixel size, so it doesn't leave a dead gap when a taller sibling
   card (e.g. "Getting Started") stretches the row via h-100. */
.cims-chart-container {
    position: relative;
    min-height: 280px;
    width: 100%;
}

.cims-chart-container canvas {
    position: absolute;
    inset: 0;
}

/* Tab navigation (module sub-views, e.g. Master Data register types) */
.nav-tabs {
    border-bottom-color: var(--cims-color-border);
    gap: .25rem;
}

.nav-tabs .nav-link {
    border: none;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    color: var(--cims-color-text-muted);
    font-weight: 600;
    font-size: var(--cims-text-sm);
    padding: .625rem .25rem;
    margin-right: 1.25rem;
    transition: color .12s ease-out, border-color .12s ease-out;
}

.nav-tabs .nav-link:hover {
    color: var(--cims-color-primary);
    border-color: transparent;
}

.nav-tabs .nav-link.active {
    color: var(--cims-color-primary);
    background: none;
    border-bottom-color: var(--cims-color-primary);
}

/* Pagination — used by every ->links() list page */
.pagination {
    --bs-pagination-color: var(--cims-color-text-muted);
    --bs-pagination-hover-color: var(--cims-color-primary);
    --bs-pagination-hover-bg: var(--cims-color-bg);
    --bs-pagination-active-bg: var(--cims-color-primary);
    --bs-pagination-active-border-color: var(--cims-color-primary);
    --bs-pagination-border-color: var(--cims-color-border);
    --bs-pagination-focus-box-shadow: 0 0 0 3px rgba(var(--cims-color-primary-rgb), .15);
    gap: .1875rem;
}

.pagination .page-link {
    border-radius: var(--cims-radius-sm) !important;
    font-size: var(--cims-text-sm);
    font-weight: 500;
    margin: 0;
}

/* Dropdown menus (user menu, row action menus, "More" menus) */
.dropdown-menu {
    border-color: var(--cims-color-border);
    box-shadow: var(--cims-shadow-md);
    border-radius: var(--cims-radius-md);
    padding: .375rem;
}

.dropdown-item {
    border-radius: var(--cims-radius-sm);
    font-size: var(--cims-text-sm);
    padding: .5rem .625rem;
}

.dropdown-item:hover,
.dropdown-item:focus {
    background-color: var(--cims-color-bg);
}

.dropdown-item.text-danger:hover,
.dropdown-item.text-danger:focus {
    background-color: rgba(var(--cims-color-danger-rgb), .12);
}

/* Modals */
.modal-content {
    border: none;
    border-radius: var(--cims-radius-lg);
    box-shadow: var(--cims-shadow-lg);
}

.modal-header {
    border-bottom-color: var(--cims-color-border);
    padding: 1.25rem 1.5rem;
}

.modal-body {
    padding: 1.5rem;
}

.modal-footer {
    border-top-color: var(--cims-color-border);
    padding: 1rem 1.5rem;
}

/* Avatar initials chip (user lists) */
.cims-avatar {
    width: 2.25rem;
    height: 2.25rem;
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    /* Runs from --primary rather than --primary-light: white 12px initials on
       the light end measured 3.14:1 against the configured brand colour and
       3.68:1 against the shipped default, both under 1.4.3's 4.5:1 for small
       text (34-2 finding F4). The dark end was already 9.46:1, so only the
       light stop needed to move, and starting at --primary keeps the avatar
       visibly a gradient rather than flattening it. */
    background: linear-gradient(155deg, var(--cims-color-primary), var(--cims-color-primary-dark));
    color: #fff;
    font-size: .75rem;
    font-weight: 700;
    letter-spacing: .02em;
    text-transform: uppercase;
}

/* Role chip — distinct from status badges (identity, not workflow state) */
.cims-role-chip {
    display: inline-flex;
    align-items: center;
    font-size: var(--cims-text-xs);
    font-weight: 600;
    color: var(--cims-color-text-muted);
    background: var(--cims-color-bg);
    border: 1px solid var(--cims-color-border);
    padding: .1875rem .5625rem;
    border-radius: 999px;
    margin: .0625rem .25rem .0625rem 0;
}

/* Quick link row (dashboard "Getting Started" style panels) */
.cims-quicklink {
    display: flex;
    align-items: center;
    gap: var(--cims-space-3);
    padding: .625rem .75rem;
    border-radius: var(--cims-radius-sm);
    text-decoration: none;
    color: inherit;
    transition: background-color .12s ease-out;
}

.cims-quicklink:hover {
    background-color: var(--cims-color-bg);
}

.cims-quicklink:hover .cims-quicklink-title {
    color: var(--cims-color-primary);
}

.cims-quicklink-icon {
    width: 2.25rem;
    height: 2.25rem;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--cims-radius-sm);
    background: rgba(var(--cims-color-primary-rgb), .1);
    color: var(--cims-color-primary);
    font-size: 1.0625rem;
}

.cims-quicklink-title {
    display: block;
    font-weight: 600;
    font-size: var(--cims-text-sm);
    color: var(--cims-color-text);
    transition: color .12s ease-out;
}

.cims-quicklink-desc {
    display: block;
    font-size: var(--cims-text-xs);
    color: var(--cims-color-text-muted);
    margin-top: .0625rem;
}

.cims-quicklink-arrow {
    margin-left: auto;
    color: var(--cims-color-text-muted);
    font-size: .8125rem;
    flex: none;
}

/* Empty state */
.cims-empty-state {
    text-align: center;
    padding: var(--cims-space-6) var(--cims-space-3);
    color: var(--cims-color-text-muted);
}

.cims-empty-state .bi {
    font-size: 2rem;
    color: var(--cims-color-border);
    margin-bottom: var(--cims-space-2);
    display: block;
}

.cims-empty-state p {
    margin-bottom: var(--cims-space-2);
}

/* Filter bar — a lighter-weight panel than a full .card, since it's a
   utility strip (search/filter), not primary content. */
.cims-filter-bar {
    background: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-md);
    padding: var(--cims-space-3) var(--cims-space-4);
    box-shadow: var(--cims-shadow-sm);
}

.cims-filter-bar .form-label {
    font-size: var(--cims-text-xs);
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--cims-color-text-muted);
    font-weight: 600;
}

/* Status badges — color + text, never color alone */
.cims-status-badge {
    display: inline-flex;
    align-items: center;
    gap: .3125rem;
    font-size: var(--cims-text-xs);
    font-weight: 600;
    padding: .25rem .625rem;
    border-radius: 999px;
    white-space: nowrap;
}

.cims-status-badge .bi {
    font-size: .75rem;
}

.cims-status-badge--neutral { color: var(--cims-color-text-muted); background: rgba(var(--cims-color-primary-rgb), .07); }
.cims-status-badge--warning { color: var(--cims-color-warning); background: rgba(var(--cims-color-warning-rgb), .14); }
.cims-status-badge--success { color: var(--cims-color-success); background: rgba(var(--cims-color-success-rgb), .14); }
.cims-status-badge--danger { color: var(--cims-color-danger); background: rgba(var(--cims-color-danger-rgb), .14); }
.cims-status-badge--info { color: var(--cims-color-info); background: rgba(var(--cims-color-info-rgb), .14); }

.card {
    border-color: var(--cims-color-border);
}

/* --- Phase 3: data-table rhythm -------------------------------------------
   A 131-view ERP lives or dies on how a table reads. Three changes carry it:
   a header that anchors without shouting, a row rhythm loose enough to scan,
   and separators light enough that the DATA is the darkest thing on screen. */
.table thead th {
    font-size: var(--cims-text-xs);
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--cims-color-text-muted);
    font-weight: 600;
    /* A solid 1px rule under the header separates it from the body far more
       cleanly than Bootstrap's default 2px double border. */
    border-bottom: 1px solid var(--cims-color-border);
    /* Bootstrap paints every cell from --bs-table-bg in a rule that outranks
       a plain background-color here, so the token is what must be set --
       otherwise a sticky header stays translucent and rows scroll through it.
       Surface, not the page tint: the header sits on the same plane as the
       card and the 1px rule below does the separating. */
    --bs-table-bg: var(--cims-color-surface);
    background-color: var(--cims-color-surface);
    white-space: nowrap;
    padding-block: .6875rem;
}

/* The header stays put while a long list scrolls. Applied only inside a
   scroll container so it cannot stick to the viewport on a short table. */
.table-responsive .table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
}

/* Hairline separators instead of Bootstrap's full-strength border: at 131
   views the grid lines were competing with the values for attention. */
.table > tbody > tr > * {
    border-bottom-color: color-mix(in srgb, var(--cims-color-border) 65%, transparent);
}

.table > tbody > tr:last-child > * {
    border-bottom-color: transparent;
}

/* Row actions stay quiet until the row is engaged — an ERP list with a row of
   coloured buttons on every line reads as noise. Kept visible on touch and
   for keyboard users, where hover does not exist. */
/* Row actions are NOT dimmed until hover. That was tried and reverted on
   2026-08-23: combined with Bootstrap's outline buttons (white fill on a white
   card) it left the primary action on every row effectively invisible, and it
   hides the only affordance a touch user has. They are quietened with a tint
   instead — see the .table .btn-outline-* rules below. */

.table > :not(caption) > * > *,
.table-sm > :not(caption) > * > * {
    padding-block: .8125rem;
    /* Horizontal padding is set explicitly because Bootstrap's `.table-sm`
       shorthand (`padding: .25rem`) would otherwise leave 4px of breathing
       room on either side of every cell -- 58 of the list views use table-sm,
       and at full card width that reads as content jammed against the edge.
       Only the vertical rhythm should differ between sm and regular tables. */
    padding-inline: .75rem;
    vertical-align: middle;
}

/* First and last cells align the table's content with the card's own content
   edge (16px card-body padding), so a column heading sits on the same vertical
   line as the page text above it rather than 12px inboard of it. */
.table > :not(caption) > * > *:first-child,
.table-sm > :not(caption) > * > *:first-child {
    padding-inline-start: 1rem;
}

.table > :not(caption) > * > *:last-child,
.table-sm > :not(caption) > * > *:last-child {
    padding-inline-end: 1rem;
}

/* A table that is the whole card body sits flush; its own cell padding then
   supplies the inset, so the two do not compound into a 32px gutter. */
.card-body.p-0 > .dt-container .table > :not(caption) > * > *:first-child,
.card-body.p-0 > .table > :not(caption) > * > *:first-child {
    padding-inline-start: 1.25rem;
}

.card-body.p-0 > .dt-container .table > :not(caption) > * > *:last-child,
.card-body.p-0 > .table > :not(caption) > * > *:last-child {
    padding-inline-end: 1.25rem;
}

.table tbody tr {
    transition: background-color .1s ease-out;
}

.table-hover > tbody > tr:hover > * {
    background-color: rgba(var(--cims-color-primary-rgb), .05);
    --bs-table-accent-bg: transparent;
}

/* ==========================================================================
   Auth screens (login / register / password reset)

   Split shell: a dark brand panel carrying the product story on the inline-
   start side, a light form panel on the inline-end. Below `lg` the brand panel
   is dropped entirely and replaced by a compact lockup above the form, so a
   phone gets the form first instead of scrolling past a marketing wall.

   Everything is built on the CIMS custom properties, so the white-label primary
   colour injected by partials/branding.blade.php and the dark theme both
   re-skin these screens without a single extra override. Inline-logical
   properties (padding-inline, inset-inline-*) are used throughout so the
   RTL locales served by SetLocale mirror correctly.
   ========================================================================== */
/* ONE CENTRED COLUMN ON ONE STAGE.

   This was `1.25fr / 1fr`: a brand panel beside the form. With the panel's copy
   removed, the larger half of the screen held a logo and a line drawing and
   nothing else, and the form sat off to one side of it. The drawing is now the
   backdrop for the whole page and the form is in the middle of it. */
.cims-auth-layout {
    position: relative;
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: var(--cims-space-4);
    /* The stage carries what the aside used to: the ground, the wash of brand
       colour, and the blueprint grid below. Centred now rather than weighted to
       one side, because the content it sits behind is centred. */
    background-color: var(--cims-sidebar-bg);
    background-image:
        radial-gradient(circle at 50% 0%, rgba(var(--cims-color-primary-rgb), .38), transparent 58%),
        radial-gradient(circle at 92% 94%, rgba(var(--cims-color-accent-rgb), .14), transparent 40%),
        linear-gradient(165deg, #0b1220 0%, #101a2e 55%, #0b1220 100%);
}

/* The blueprint grid, masked to a soft ellipse behind the card so it never
   competes with the form it sits under. */
.cims-auth-layout::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(rgba(255, 255, 255, .05) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 255, 255, .05) 1px, transparent 1px);
    background-size: 56px 56px;
    mask-image: radial-gradient(ellipse 78% 68% at 50% 44%, #000 6%, transparent 82%);
    -webkit-mask-image: radial-gradient(ellipse 78% 68% at 50% 44%, #000 6%, transparent 82%);
    pointer-events: none;
}

/* A POOL OF LIGHT BEHIND THE CARD, instead of a picture.

   The bridge that stood here was line-art scaled to 1280px across: at that size
   the arch was wider than the screen, so what a reader saw was two curves and a
   rule, not a bridge. Nothing is drawn now — the depth comes from the grid above
   and this soft bloom of the brand colour, which cannot crop badly because it
   has no edges. */
.cims-auth-layout::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        radial-gradient(ellipse 46% 40% at 50% 44%, rgba(var(--cims-color-primary-rgb), .22), transparent 70%),
        radial-gradient(ellipse 80% 50% at 50% 100%, rgba(var(--cims-color-primary-rgb), .10), transparent 72%);
    pointer-events: none;
}



/* --- Auth field ergonomics ------------------------------------------------
   The login form is the product's only single-purpose screen, so its controls
   are sized for confidence rather than density: 3.125rem fields (vs the 44px
   accessibility floor) and a heavier label. */
.cims-auth-card .form-control,
.cims-auth-card .form-select {
    min-height: 3.125rem;
    font-size: var(--cims-text-base);
    border-radius: var(--cims-radius-md);
}

.cims-auth-card .form-label {
    font-size: var(--cims-text-sm);
    font-weight: 600;
    color: var(--cims-color-text);
    margin-bottom: var(--cims-space-2);
}

.cims-auth-card .btn-primary {
    min-height: 3.125rem;
    font-size: var(--cims-text-base);
    font-weight: 600;
    border-radius: var(--cims-radius-md);
    letter-spacing: -.01em;
}

/* The field icon and reveal button are absolutely positioned from the top of
   .cims-field and centred with translateY(-50%), so `top` must equal half the
   input height: 3.125rem / 2 = 1.5625rem. Measured, not guessed -- the base
   rule's 1.5rem is calibrated for the 3rem fields used elsewhere. */
.cims-auth-card .cims-field-icon,
.cims-auth-card .cims-field-action {
    top: 1.5625rem;
}

.cims-auth-card h1 {
    font-size: 1.625rem;
    font-weight: 700;
    letter-spacing: -.02em;
    margin-bottom: var(--cims-space-2);
}

/* --- Brand panel ---------------------------------------------------------- */
.cims-auth-aside {
    display: none;
    position: relative;
    flex-direction: column;
    justify-content: space-between;
    gap: var(--cims-space-5);
    padding: var(--cims-space-6);
    overflow: hidden;
    color: #fff;
    background-color: var(--cims-sidebar-bg);
    background-image:
        radial-gradient(circle at 10% 12%, rgba(var(--cims-color-primary-rgb), .42), transparent 44%),
        radial-gradient(circle at 96% 96%, rgba(var(--cims-color-accent-rgb), .16), transparent 34%),
        linear-gradient(155deg, #0b1220 0%, #101a2e 55%, #0b1220 100%);
}

@media (min-width: 992px) {
    .cims-auth-aside {
        display: flex;
    }
}

/* Faint blueprint grid — deliberate nod to the drawing sets this product
   manages. Masked to a soft ellipse so it never competes with the copy. */
.cims-auth-aside::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(rgba(255, 255, 255, .05) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255, 255, 255, .05) 1px, transparent 1px);
    background-size: 48px 48px;
    mask-image: radial-gradient(ellipse 70% 60% at 30% 40%, #000 10%, transparent 78%);
    -webkit-mask-image: radial-gradient(ellipse 70% 60% at 30% 40%, #000 10%, transparent 78%);
    pointer-events: none;
}

.cims-auth-aside > * {
    position: relative;
    z-index: 1;
}

.cims-auth-brand {
    display: flex;
    align-items: center;
    gap: .75rem;
}

.cims-auth-brand .cims-logo-mark {
    width: 2.75rem;
    height: 2.75rem;
    flex: 0 0 2.75rem;
    border-radius: .75rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(155deg, var(--cims-color-primary-light), var(--cims-color-primary-dark));
    color: #fff;
    font-size: 1.375rem;
    box-shadow: 0 6px 18px rgba(var(--cims-color-primary-rgb), .45);
}

.cims-auth-brand-text {
    display: block;
    color: #fff;
    font-weight: 700;
    font-size: 1.1875rem;
    letter-spacing: -.015em;
    line-height: 1.2;
}

/* Light variant of the lockup, used above the form on small screens. */
.cims-auth-brand--light .cims-auth-brand-text {
    color: var(--cims-color-text);
}

/* --- The drawing ---------------------------------------------------------- */
/*
 * What used to be here: a headline, a lede, three feature bullets and three
 * statistics, with the rules to lay them out. All of it was sales copy on a
 * screen nobody reaches until after they have bought the product — and it
 * published our own build (module count, architecture, capability list) on the
 * one page exposed to the open internet.
 *
 * The panel now carries a drawing instead, and these few rules replace roughly
 * a hundred. The stroke widths are vector-fixed rather than scaled, so the line
 * weight stays a drawing's weight at every panel size.
 */
.cims-auth-figure {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1 1 auto;
    min-height: 0;          /* lets the flex child shrink on a short viewport */
    padding: var(--cims-space-4) 0;
}

.cims-auth-figure svg {
    width: 100%;
    max-width: 30rem;
    height: auto;
    max-height: 100%;
    overflow: visible;
}

.cims-auth-figure path {
    fill: none;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
}

.cims-auth-figure-ground {
    stroke: rgba(255, 255, 255, .22);
    stroke-width: 1.5;
}

.cims-auth-figure-line {
    stroke: rgba(255, 255, 255, .3);
    stroke-width: 2;
}

.cims-auth-figure-arch {
    stroke: rgba(255, 255, 255, .45);
    stroke-width: 2.25;
}

.cims-auth-figure-hanger {
    stroke: rgba(255, 255, 255, .2);
    stroke-width: 1.25;
}

/*
 * The deck is the one element in the brand colour, so a client's own colour
 * runs through the drawing rather than sitting only behind it. `-light` because
 * the panel ground is near-black and a mid-tone primary disappears on it.
 */
.cims-auth-figure-deck {
    stroke: var(--cims-color-primary-light);
    stroke-width: 3;
}

.cims-auth-figure-tick {
    stroke: rgba(255, 255, 255, .16);
    stroke-width: 1.25;
}


/* --- Form panel ----------------------------------------------------------- */
/* The centred column: brand, card, footnote. Sits ON the stage, so it carries
   no ground of its own — the card below supplies the surface, which is what
   gives the form an edge and a shadow instead of bleeding into the viewport. */
.cims-auth-main {
    position: relative;
    z-index: 1;
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--cims-space-4);
    width: 100%;
    padding: var(--cims-space-4) 0;
}

/* The brand, above the card and centred with it. */
.cims-auth-brand--stacked {
    justify-content: center;
    color: #fff;
}

.cims-auth-brand--stacked .cims-auth-brand-text {
    color: #fff;
}

.cims-auth-topbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--cims-space-3);
    min-height: 2.5rem;
}

/* The switcher sits ON THE DARK STAGE now, not on a pale panel. Styled for the
   old light ground it was muted grey on a near-black field — present, and
   unreadable. Light ink on a translucent well instead. */
/* The theme toggle, sized and coloured to sit beside the language switcher. */
.cims-auth-theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border-radius: var(--cims-radius-sm);
    background-color: rgba(255, 255, 255, .08);
    border: 1px solid rgba(255, 255, 255, .22);
    color: rgba(255, 255, 255, .92);
    line-height: 1;
}

.cims-auth-theme-toggle:hover,
.cims-auth-theme-toggle:focus-visible {
    background-color: rgba(255, 255, 255, .16);
    border-color: rgba(255, 255, 255, .34);
    color: #fff;
}

.cims-auth-topbar .dropdown > .btn {
    background-color: rgba(255, 255, 255, .08);
    border: 1px solid rgba(255, 255, 255, .22);
    color: rgba(255, 255, 255, .92);
    font-size: var(--cims-text-sm);
    font-weight: 500;
    border-radius: var(--cims-radius-sm);
    padding: .375rem .75rem;
}

.cims-auth-topbar .dropdown > .btn:hover,
.cims-auth-topbar .dropdown > .btn:focus-visible {
    background-color: rgba(255, 255, 255, .16);
    color: #fff;
    border-color: rgba(255, 255, 255, .34);
}

/* A plain wrapper now. It used to grow to fill the form half and centre the
   card inside itself; .cims-auth-main does the centring for the whole column. */
.cims-auth-panel {
    display: flex;
    justify-content: center;
    width: 100%;
}

.cims-auth-card {
    width: 100%;
    max-width: 27rem;
    background-color: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-lg);
    box-shadow: var(--cims-shadow-md);
    padding: var(--cims-space-5);
    position: relative;
}

/* Hairline brand rule along the card's top edge — the one piece of colour on
   an otherwise neutral form, tying it back to the panel beside it. */
.cims-auth-card::before {
    content: "";
    position: absolute;
    inset-inline: var(--cims-space-5);
    top: 0;
    height: 3px;
    border-radius: 0 0 3px 3px;
    background: linear-gradient(90deg, var(--cims-color-primary), var(--cims-color-accent));
}

@media (max-width: 575.98px) {
    /* THE SURFACE STAYS ON A PHONE.

       This dropped the background, border and shadow so the form read as part
       of the page — correct while the page behind it was the pale page ground.
       The stage is dark now, and a transparent card put near-black labels and a
       near-black heading onto a near-black field: on a phone the whole form but
       its two inputs was invisible. Only the padding tightens. */
    .cims-auth-card {
        padding: var(--cims-space-4);
    }
}

.cims-auth-mobile-brand {
    margin-bottom: var(--cims-space-5);
}

@media (min-width: 992px) {
    .cims-auth-mobile-brand {
        display: none;
    }
}

.cims-auth-card h1 {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -.02em;
    color: var(--cims-color-text);
    margin-bottom: .375rem;
}

.cims-auth-card .cims-auth-subtitle {
    color: var(--cims-color-text-muted);
    font-size: var(--cims-text-base);
    line-height: 1.55;
    margin-bottom: var(--cims-space-5);
}

.cims-auth-card .form-label {
    font-size: var(--cims-text-sm);
    font-weight: 600;
    color: var(--cims-color-text);
    margin-bottom: .375rem;
}

/* --- Fields --------------------------------------------------------------- */
.cims-field {
    position: relative;
}

/* Scoped to the card rather than to .cims-field, so the pages that do not
   use an icon-prefixed field (register, reset password) get the same control
   height and focus ring instead of falling back to Bootstrap's default. */
.cims-auth-card .form-control {
    height: 3rem;
    font-size: var(--cims-text-base);
    border-radius: var(--cims-radius-md);
    background-color: var(--cims-color-bg);
    border-color: var(--cims-color-border);
    transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease;
}

.cims-auth-card .form-control::placeholder {
    color: var(--cims-color-text-muted);
    opacity: .75;
}

.cims-auth-card .form-control:focus {
    background-color: var(--cims-color-surface);
    border-color: var(--cims-color-primary);
    box-shadow: 0 0 0 4px rgba(var(--cims-color-primary-rgb), .14);
}

.cims-field .form-control {
    padding-inline-start: 2.75rem;
}

/* Icon is rendered *after* the input so this sibling selector can tint it on
   focus without any JavaScript; it is absolutely positioned either way. */
.cims-field-icon {
    position: absolute;
    inset-inline-start: .9375rem;
    top: 1.5rem;
    transform: translateY(-50%);
    font-size: 1rem;
    line-height: 1;
    color: var(--cims-color-text-muted);
    pointer-events: none;
    transition: color .15s ease;
}

.cims-field .form-control:focus ~ .cims-field-icon {
    color: var(--cims-color-primary);
}

.cims-field .form-control.is-invalid ~ .cims-field-icon {
    color: var(--cims-color-danger);
}

/* Reveal button sits inside the field, so the input reserves room for it. */
.cims-field--action .form-control {
    padding-inline-end: 3rem;
}

/* Bootstrap's invalid glyph would collide with the reveal button. */
.cims-field--action .form-control.is-invalid {
    background-image: none;
}

.cims-field-action {
    position: absolute;
    inset-inline-end: .5rem;
    top: 1.5rem;
    transform: translateY(-50%);
    /* 44px minimum touch target (WCAG 2.5.8 / Apple HIG). The visual chip
       stays 2rem via the smaller icon and transparent background; only the
       hit area grows. */
    width: 2.75rem;
    height: 2.75rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: .375rem;
    background: transparent;
    color: var(--cims-color-text-muted);
    transition: color .15s ease, background-color .15s ease;
}

.cims-field-action:hover,
.cims-field-action:focus-visible {
    color: var(--cims-color-text);
    background-color: rgba(var(--cims-color-primary-rgb), .08);
}

.cims-auth-hint {
    display: none;
    align-items: center;
    gap: .375rem;
    margin-top: .5rem;
    font-size: var(--cims-text-xs);
    font-weight: 500;
    color: var(--cims-color-warning);
}

.cims-auth-hint.is-visible {
    display: flex;
}

.cims-auth-options {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--cims-space-2) var(--cims-space-3);
    margin-block: var(--cims-space-3) var(--cims-space-4);
}

.cims-auth-options .form-check {
    margin: 0;
    min-height: auto;
}

.cims-auth-options .form-check-input {
    width: 1.0625rem;
    height: 1.0625rem;
    margin-top: .1rem;
}

.cims-auth-options .form-check-label {
    font-size: var(--cims-text-sm);
    color: var(--cims-color-text-muted);
    cursor: pointer;
}

.cims-auth-link {
    font-size: var(--cims-text-sm);
    font-weight: 600;
    color: var(--cims-color-primary);
    text-decoration: none;
}

.cims-auth-link:hover {
    color: var(--cims-color-primary-dark);
    text-decoration: underline;
}

/* --- Submit --------------------------------------------------------------- */
.cims-auth-card .btn-primary {
    height: 3rem;
    font-size: var(--cims-text-base);
    font-weight: 600;
    border-radius: var(--cims-radius-md);
    box-shadow: 0 8px 18px -8px rgba(var(--cims-color-primary-rgb), .7);
    transition: transform .15s ease, box-shadow .15s ease, background-color .15s ease;
}

.cims-auth-card .btn-primary:hover:not(:disabled) {
    transform: translateY(-1px);
    box-shadow: 0 12px 24px -8px rgba(var(--cims-color-primary-rgb), .75);
}

.cims-auth-card .btn-primary:active:not(:disabled) {
    transform: translateY(0);
}

.cims-auth-card .btn-primary:disabled {
    opacity: .75;
}

.cims-auth-divider {
    display: flex;
    align-items: center;
    gap: .75rem;
    margin-block: var(--cims-space-4);
    color: var(--cims-color-text-muted);
    font-size: var(--cims-text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: .07em;
}

.cims-auth-divider::before,
.cims-auth-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background-color: var(--cims-color-border);
}

.cims-auth-meta {
    text-align: center;
    font-size: var(--cims-text-sm);
    color: var(--cims-color-text-muted);
    margin: 0;
}

/* On the dark stage, so it takes its own light ink rather than the muted grey
   that was calibrated for the pale form panel this used to sit in. */
.cims-auth-footnote {
    text-align: center;
    padding-top: var(--cims-space-3);
    color: rgba(255, 255, 255, .62);
    font-size: var(--cims-text-xs);
    position: relative;
    z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
    .cims-auth-card .btn-primary,
    .cims-auth-card .form-control,
    .cims-field-icon,
    .cims-field-action {
        transition: none;
    }

    .cims-auth-card .btn-primary:hover:not(:disabled) {
        transform: none;
    }
}

/* ==========================================================================
   DataTables — search / paging / Excel-PDF-Print export on every data table
   ========================================================================== */
.cims-dt-toolbar .dt-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: .375rem;
}

.cims-dt-toolbar .dt-buttons .btn {
    font-weight: 500;
}

/* DataTables 3.x renamed every wrapper class: `.dataTables_wrapper`,
   `.dataTables_filter`, `.dataTables_length`, `.dataTables_info` and
   `.dataTables_paginate` are 1.x names and appear nowhere in the 3.x build.
   Styling written against them silently does nothing, which is why these
   controls used to render as raw browser defaults inside an otherwise themed
   page. The 3.x DOM is:

     .dt-container > .dt-layout-row > .dt-layout-cell.dt-layout-{start|end|full}
       .dt-search  > label + input.dt-input
       .dt-length  > label + select.dt-input
       .dt-info
       .dt-paging  > ul.pagination > li.page-item > button.page-link

   Keep these selectors in sync with the installed major version. */

.dt-container .dt-search {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--cims-space-2);
}

/* The Bootstrap 5 renderer emits an EMPTY label after the input and puts the
   affordance in the placeholder instead. Collapsed so it cannot introduce a
   stray gap in the flex row; the input keeps its own accessible name via the
   placeholder and aria-controls. */
.dt-container .dt-search > label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* The Bootstrap 5 renderer stamps `form-control form-control-sm` on this
   input (NOT `dt-input`, which is the vanilla renderer's class), so it already
   carries Bootstrap's theming. These rules add only what Bootstrap does not:
   room for the search glyph, a sensible minimum width, and the brand focus
   ring. */
.dt-container .dt-search input[type="search"] {
    /* Not --cims-color-border: 1.14:1, and this is a text input whose border is
       its only boundary (WCAG 1.4.11 wants 3:1). This rule OWNS the border at
       (0,2,2), so the general .form-control fix could not reach it -- the
       search box was the last field in the app still drawn with the invisible
       hairline. Same neutral as every other field: 3.23:1. */
    border: 1px solid rgba(var(--cims-color-text-muted-rgb), .75);
    border-radius: var(--bs-border-radius);
    /* Left padding leaves room for the search glyph drawn below. */
    padding: .375rem .75rem .375rem 2rem;
    font-size: var(--cims-text-sm);
    min-width: 240px;
    margin: 0;
    background-color: var(--cims-color-surface);
    color: var(--cims-color-text);
}

.dt-container .dt-search input[type="search"]::placeholder {
    color: var(--cims-color-text-muted);
}

.dt-container .dt-search input[type="search"]:focus {
    outline: none;
    border-color: var(--cims-color-primary-light);
    box-shadow: 0 0 0 3px rgba(var(--cims-color-primary-rgb), .15);
}

/* Search glyph as an inline SVG on the input rather than a ::before with an
   icon-font codepoint. The bundler rewrites a CSS escape like "\f52a" into
   the JS form "\uf52a", which is not valid CSS, so the character silently
   resolved to empty. An SVG data URI has no escape to mangle and no
   dependency on bootstrap-icons having loaded. */
.dt-container .dt-search input[type="search"] {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%235b6478' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='7' cy='7' r='4.5'/%3E%3Cpath d='M10.5 10.5 14 14'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: left .625rem center;
    background-size: 14px 14px;
}

/* Native clear affordance overlaps nothing, but Chrome renders it dark on a
   dark surface — hide it and let Escape / clearing the field do the work. */
.dt-container .dt-search input[type="search"]::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
}

:root[data-theme="dark"] .dt-container .dt-search input[type="search"] {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2393a0bf' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='7' cy='7' r='4.5'/%3E%3Cpath d='M10.5 10.5 14 14'/%3E%3C/svg%3E");
}

.dt-container .dt-length {
    display: flex;
    align-items: center;
    gap: var(--cims-space-2);
    color: var(--cims-color-text-muted);
    font-size: var(--cims-text-sm);
}

.dt-container .dt-length select {
    border: 1px solid var(--cims-color-border);
    border-radius: var(--bs-border-radius);
    padding: .25rem 1.75rem .25rem .5rem;
    font-size: var(--cims-text-sm);
    background-color: var(--cims-color-surface);
    color: var(--cims-color-text);
    width: auto;
}

.dt-container .dt-info {
    color: var(--cims-color-text-muted);
    font-size: var(--cims-text-sm);
    padding-top: 0;
}

.dt-container .dt-paging .page-link {
    border-radius: var(--cims-radius-sm);
    font-size: var(--cims-text-sm);
    font-weight: 500;
    margin: 0 .125rem;
    border-color: var(--cims-color-border);
    color: var(--cims-color-text-muted);
    min-width: 2rem;
    text-align: center;
}

.dt-container .dt-paging .page-link:hover {
    background-color: var(--cims-color-bg);
    color: var(--cims-color-text);
}

.dt-container .dt-paging .page-item.active .page-link {
    background-color: var(--cims-color-primary);
    border-color: var(--cims-color-primary);
    color: #fff;
}

.dt-container .dt-paging .page-item.disabled .page-link {
    color: var(--cims-color-text-muted);
    opacity: .45;
}

/* Breathing room between the toolbar rows and the table itself. */
.dt-container .dt-layout-row {
    align-items: center;
    margin-bottom: var(--cims-space-3);
}

.dt-container .dt-layout-row:last-child {
    margin-bottom: 0;
    margin-top: var(--cims-space-3);
}

.dt-container .dt-empty {
    color: var(--cims-color-text-muted);
    text-align: center;
    padding: var(--cims-space-5) var(--cims-space-3);
}

/* Digits in a data table must line up column-wise. Proportional figures make
   quantities, rates and amounts ragged and hard to compare down a column.
   Applied to all cells rather than a per-column class: tabular-nums only
   affects digits, so text columns are unchanged. */
.table > :not(caption) > * > * {
    font-variant-numeric: tabular-nums;
}

table.dataTable > thead > tr > th {
    position: relative;
}

table.dataTable > thead > tr > th.dt-orderable-asc:hover,
table.dataTable > thead > tr > th.dt-orderable-desc:hover {
    color: var(--cims-color-primary);
}

/* ==========================================================================
   Phase 6 — dark-theme parity for the Phase 3/4 changes.
   Everything above is token-driven and inherits automatically; these are the
   places where a light-mode value does not simply invert.
   ========================================================================== */

/* A sticky header must be opaque or rows scroll through it. --cims-color-surface
   is already opaque in both themes, but the sticky header also needs to sit on
   the CARD surface in dark mode, which differs from the page ground. */
:root[data-theme="dark"] .table thead th {
    --bs-table-bg: var(--cims-color-surface);
    background-color: var(--cims-color-surface);
}

/* Hairline separators computed from a dark border token come out too faint to
   read as a grid at all; lift them slightly rather than reuse the 65% mix. */
:root[data-theme="dark"] .table > tbody > tr > * {
    border-bottom-color: color-mix(in srgb, var(--cims-color-border) 85%, transparent);
}

/* The hover tint is a lightening on dark, not the same low-alpha wash: at .05
   over #141c2e the row barely moves. */
:root[data-theme="dark"] .table-hover > tbody > tr:hover > * {
    background-color: rgba(var(--cims-color-primary-rgb), .1);
}

/* Bootstrap's .btn-outline-secondary hover fill uses the page ground, which on
   dark is darker than the surface it sits on — inverting the intended
   direction of the state change. */
:root[data-theme="dark"] .btn-outline-secondary {
    --bs-btn-hover-bg: rgba(255, 255, 255, .06);
}

/* Card and modal edges need more than a 1px border to separate from the page
   on a dark ground, where shadows do almost no work. */
:root[data-theme="dark"] .card,
:root[data-theme="dark"] .modal-content {
    border-color: var(--cims-color-border);
    background-color: var(--cims-color-surface);
}

/* The auth card's brand rule is the one gradient in the product; on dark it
   needs the lifted primary or it reads as a muddy stripe. */
:root[data-theme="dark"] .cims-auth-card {
    background-color: var(--cims-color-surface);
    border-color: var(--cims-color-border);
}

/* NO GROUND OF ITS OWN, in either theme.

   This painted the near-black page colour onto .cims-auth-main so the card
   stayed elevated above it — right while that element was the pale form half of
   a split screen. It is now the centred column standing ON the stage, so an
   opaque ground here covered the whole backdrop: in dark mode the drawing was
   painted and then hidden, which is why it showed in light and vanished in
   dark on identical markup. The card keeps its own surface (above), which is
   what elevates it. */

/* ==========================================================================
   Row action buttons
   Bootstrap's .btn-outline-* is a transparent fill with a coloured border. On
   a white card that reads as a white button — the action on every row was the
   least visible thing in it. These give each variant a soft tint of its own
   colour: visible at a glance, still quiet enough that 10 rows of them do not
   compete with the data. Scoped to table cells so page-level and modal
   buttons keep the standard Bootstrap treatment.
   ========================================================================== */
.table .btn-outline-primary,
.table .btn-outline-secondary,
.table .btn-outline-danger {
    --bs-btn-bg: rgba(var(--cims-color-primary-rgb), .09);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-border-color: transparent;
    font-weight: 500;
}

.table .btn-outline-primary {
    --bs-btn-color: var(--cims-color-primary);
    --bs-btn-bg: rgba(var(--cims-color-primary-rgb), .1);
    --bs-btn-hover-bg: rgba(var(--cims-color-primary-rgb), .18);
    --bs-btn-hover-color: var(--cims-color-primary-dark);
    --bs-btn-active-bg: rgba(var(--cims-color-primary-rgb), .24);
    --bs-btn-active-color: var(--cims-color-primary-dark);
}

.table .btn-outline-secondary {
    --bs-btn-color: var(--cims-color-text);
    --bs-btn-bg: rgba(var(--cims-color-text-muted-rgb, 91, 100, 120), .1);
    --bs-btn-hover-bg: rgba(var(--cims-color-text-muted-rgb, 91, 100, 120), .18);
    --bs-btn-hover-color: var(--cims-color-text);
    --bs-btn-active-bg: rgba(var(--cims-color-text-muted-rgb, 91, 100, 120), .24);
    --bs-btn-active-color: var(--cims-color-text);
}

.table .btn-outline-danger {
    --bs-btn-color: var(--cims-color-danger);
    --bs-btn-bg: rgba(var(--cims-color-danger-rgb), .1);
    --bs-btn-hover-bg: rgba(var(--cims-color-danger-rgb), .18);
    --bs-btn-hover-color: var(--cims-color-danger);
    --bs-btn-active-bg: rgba(var(--cims-color-danger-rgb), .24);
    --bs-btn-active-color: #fff;
}

/* Buttons sitting side by side in an actions cell get real separation rather
   than Bootstrap's shared-edge .btn-group seam, which looked wrong once the
   borders were gone. */
.table td .btn + .btn,
.table td form + .btn,
.table td .btn + form {
    margin-inline-start: .25rem;
}

.table td .btn-group > .btn {
    border-radius: var(--cims-radius-sm) !important;
    margin-inline-start: .25rem;
}

.table td .btn-group > .btn:first-child {
    margin-inline-start: 0;
}

/* Dark mode: a 10% tint of the lifted primary is too faint on #141c2e, and the
   neutral tint has to lighten rather than darken. */
:root[data-theme="dark"] .table .btn-outline-primary {
    --bs-btn-bg: rgba(var(--cims-color-primary-rgb), .16);
    --bs-btn-hover-bg: rgba(var(--cims-color-primary-rgb), .26);
}

:root[data-theme="dark"] .table .btn-outline-secondary {
    --bs-btn-bg: rgba(255, 255, 255, .08);
    --bs-btn-hover-bg: rgba(255, 255, 255, .14);
    --bs-btn-color: var(--cims-color-text);
    --bs-btn-hover-color: var(--cims-color-text);
}

:root[data-theme="dark"] .table .btn-outline-danger {
    --bs-btn-bg: rgba(var(--cims-color-danger-rgb), .18);
    --bs-btn-hover-bg: rgba(var(--cims-color-danger-rgb), .28);
}

/* --- Profile identity card ------------------------------------------------ */
.cims-profile-avatar {
    width: 5rem;
    height: 5rem;
    margin-inline: auto;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2rem;
    font-weight: 600;
    color: #fff;
    background: linear-gradient(155deg, var(--cims-color-primary-light), var(--cims-color-primary-dark));
    box-shadow: 0 6px 18px rgba(var(--cims-color-primary-rgb), .35);
    letter-spacing: -.02em;
}

/* The meta list is a definition list in spirit — label left, value right —
   so the rows are flush with the card edge and separated by hairlines rather
   than boxed like a nav list. */
.cims-profile-meta .list-group-item {
    background-color: transparent;
    border-color: var(--cims-color-border);
    padding-block: .6875rem;
    font-size: var(--cims-text-sm);
}

.cims-profile-meta .list-group-item:last-child {
    border-bottom: 0;
}

.cims-profile-tip .bi {
    color: var(--cims-color-primary);
}

/* The summary column follows the forms on a long page rather than scrolling
   away — the account being edited should stay visible while the user works
   through three stacked forms. Only above lg, where the two columns actually
   sit side by side; below that it is a normal stacked card. */
@media (min-width: 992px) {
    .cims-profile-aside {
        position: sticky;
        top: calc(var(--cims-navbar-height) + var(--cims-space-3));
    }
}

/* Danger zone reads as a warning without shouting: a tinted surface and a
   left rule rather than a red header bar, which at card width was the loudest
   thing on a settings page whose primary job is routine edits. */
.cims-danger-zone {
    border-color: rgba(var(--cims-color-danger-rgb), .3);
    background-color: rgba(var(--cims-color-danger-rgb), .03);
}

.cims-danger-zone .card-header {
    background-color: transparent;
    border-bottom-color: rgba(var(--cims-color-danger-rgb), .2);
}

/* Section headers carry an icon chip so the three stacked cards are
   distinguishable at a glance when scanning rather than reading. */
.cims-section-icon {
    width: 1.75rem;
    height: 1.75rem;
    border-radius: .5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: .875rem;
    background-color: rgba(var(--cims-color-primary-rgb), .1);
    color: var(--cims-color-primary);
    flex: none;
}

.cims-section-icon--danger {
    background-color: rgba(var(--cims-color-danger-rgb), .1);
    color: var(--cims-color-danger);
}

/* ---------------------------------------------------------------------------
   Searchable select (resources/js/searchable-select.js)

   The real <select> stays in the DOM as the form control and is hidden with
   Bootstrap's .visually-hidden, so it keeps submitting, keeps `required`
   validation, and keeps working if the script never loads. These rules only
   dress the combobox that sits in front of it.
   --------------------------------------------------------------------------- */
.cims-searchable-select {
    position: relative;
}

.cims-searchable-select__list {
    /*
     * FIXED, not absolute — and the reason is worth keeping.
     *
     * These pickers sit inside `.table-responsive`, which is `overflow-x:
     * auto`. An overflow ancestor CLIPS an absolutely-positioned descendant,
     * and no z-index escapes a clip: z-index orders what is painted, it does
     * not decide what is cut off. The list was being sliced at the table's
     * edge, so a ten-row list showed as one and a half rows.
     *
     * Fixed is positioned against the viewport, which no overflow ancestor can
     * clip. `top`, `left`, `width` and `max-height` are set by
     * searchable-select.js, which also keeps it under its input on scroll and
     * flips it above when the room below runs out.
     */
    position: fixed;
    z-index: 1056; /* above a Bootstrap modal (1055) */
    margin: 0;
    padding: 0;
    list-style: none;
    max-height: 15rem;
    overflow-y: auto;
    background: var(--bs-body-bg, #fff);
    border: 1px solid var(--bs-border-color, #dee2e6);
    border-radius: 0.375rem;
    box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}

.cims-searchable-select__option {
    padding: 0.375rem 0.75rem;
    cursor: pointer;
}

.cims-searchable-select__option:hover,
.cims-searchable-select__option.is-active {
    background: var(--bs-primary-bg-subtle, #cfe2ff);
}

/* Keyboard focus must be visible on the option the arrow keys are on, not only
   on hover — otherwise the control is unusable without a mouse. */
.cims-searchable-select__option.is-active {
    outline: 2px solid var(--bs-primary, #0d6efd);
    outline-offset: -2px;
}

/* ---------------------------------------------------------------------------
   Select with an in-dropdown search (resources/js/select-search.js)
   ---------------------------------------------------------------------------
   The sibling control above turns the field ITSELF into a text box you type
   into. This one keeps a field that looks and reads like a select, and puts
   the search inside the panel it opens — which is the shape people arrive
   expecting, and the one that does not hide the current value behind a
   placeholder the moment you focus it.

   The real <select> is still in the DOM underneath, hidden with
   .visually-hidden, still submitting. These rules only dress what sits in
   front of it.
   --------------------------------------------------------------------------- */
.cims-select-search {
    position: relative;
}

/* A <button> wearing .form-select, so it is indistinguishable from the native
   control beside it on the same row. */
.cims-select-search__trigger {
    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The unchosen state reads as placeholder text, not as a value. */
.cims-select-search__trigger--placeholder {
    color: var(--cims-color-text-muted);
}

.cims-select-search__panel {
    /*
     * FIXED, not absolute — and the reason is worth keeping, because raising
     * z-index looks like the fix and is not.
     *
     * These pickers sit inside `.table-responsive`, which is `overflow-x:
     * auto`. An overflow ancestor CLIPS an absolutely-positioned descendant,
     * and z-index does not escape a clip: it orders what is painted, it does
     * not decide what is cut off. The panel was being sliced at the table's
     * edge, so a full list rendered as about one visible row.
     *
     * Fixed is positioned against the viewport, which no overflow ancestor can
     * clip. `top`, `left` and `width` are then set by select-search.js, which
     * also keeps the panel under its trigger while the page or the table
     * scrolls.
     */
    position: fixed;
    z-index: 1056; /* above a Bootstrap modal (1055) */
    margin-block-start: 0;
    padding: .375rem;
    background: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-sm);
    box-shadow: var(--cims-shadow-md);
}

/* Opened near the bottom of a short scroll container — a modal body, most
   often — the panel flips above the field rather than off the screen. */
/* Flipping is done in script now: a fixed panel has no anchor to sit above,
   so `inset-block-end: 100%` would place it against the viewport rather than
   against its field. The class is kept only as a styling hook. */
.cims-select-search__panel--above {
    margin-block: 0;
}

.cims-select-search__search {
    margin-block-end: .375rem;
}

.cims-select-search__list {
    margin: 0;
    padding: 0;
    list-style: none;
    /* A starting value only — select-search.js measures the room under the
       field on open and sets an inline max-height to match. */
    max-height: 13rem;
    overflow-y: auto;
}

.cims-select-search__option {
    padding: .375rem .5rem;
    border-radius: var(--cims-radius-sm);
    cursor: pointer;
    font-size: var(--cims-text-sm);
}

.cims-select-search__option:hover,
.cims-select-search__option.is-active {
    background: rgba(var(--cims-color-primary-rgb), .1);
}

/* Keyboard focus stays on the search box while the arrows move through the
   list, so the active row has to show where the keyboard is by itself. */
.cims-select-search__option.is-active {
    outline: 2px solid var(--cims-color-primary);
    outline-offset: -2px;
}

.cims-select-search__option.is-disabled {
    color: var(--cims-color-text-muted);
    cursor: not-allowed;
}

.cims-select-search__empty {
    margin: 0;
    padding: .5rem;
    font-size: var(--cims-text-sm);
    color: var(--cims-color-text-muted);
}

/* ==========================================================================
   Print
   ==========================================================================

   The application had NO print rules at all — verified by grepping the whole
   of resources/ for `@media print` and finding nothing. Every screen therefore
   printed with its full chrome: a fixed sidebar, a fixed navbar, the DataTables
   search box, page-length select, pagination and export buttons, and a column
   of View/Edit/Delete buttons that mean nothing on paper.

   That matters here more than it would in most apps. This is an ERP whose
   users print budget sheets, estimates, NCRs and approval histories for client
   meetings and site records — the printed page is a deliverable, not an
   afterthought.

   SCOPE — and one thing this deliberately does NOT fix.

   These rules apply only to screens rendered through `layouts.app`. Three PDF
   templates are standalone — `estimates/pdf`, `reports/export-pdf` and
   `budgets/variance-report-pdf` — and never `@extends` the layout or load this
   stylesheet, so nothing here reaches them. They are already print-shaped and
   need no help; the point is that a change here is not a change there. (Those
   three are also the files exempted in TableAccessibleNameTest::ALLOWED, for
   the same underlying reason.)

   A paginated table still prints only the rows currently in the DOM —
   DataTables renders one page at a time, and CSS cannot reach the rest. Users
   wanting the full set have the Excel / PDF / Print buttons the toolbar already
   provides. Stated here so the limit is known rather than discovered.
   ------------------------------------------------------------------------- */

@media print {
    /* --- palette ---------------------------------------------------------
       A user reading in dark mode who hits Ctrl+P would otherwise print a
       near-black page: `--cims-color-bg` is #0c1220 under [data-theme="dark"],
       and that resolves the same at print time. Forcing the light values here
       is far more robust than trying to override each rule that consumes them,
       and it is why this block redefines tokens rather than colours. */
    :root,
    :root[data-theme="dark"],
    :root[data-theme="light"] {
        --cims-color-bg: #ffffff;
        --cims-color-surface: #ffffff;
        --cims-color-border: #999999;
        --cims-color-text: #000000;
        --cims-color-text-muted: #444444;
        --cims-color-text-muted-rgb: 68, 68, 68;
        --cims-color-surface-alt: #ffffff;
        --cims-color-surface-muted: rgba(0, 0, 0, .05);
        --cims-color-border-subtle: #bbbbbb;
        --cims-color-bg-rgb: 255, 255, 255;
        --cims-color-text-rgb: 0, 0, 0;

        /* Same reason, one layer down: a board printed from dark mode would
           otherwise put the DARK series steps on white paper, and the stat
           panel's #1a2338 ground would print as a black box behind its
           number. These are the light steps, which is what paper is. */
        --cims-chart-1: #0052cc;
        --cims-chart-2: #d9600a;
        --cims-chart-3: #0e9f6e;
        --cims-chart-4: #8b5cf6;
        --cims-chart-5: #be185d;
        --cims-chart-grid: rgba(68, 68, 68, .18);
    }

    @page {
        margin: 12mm;
    }

    /* --- no motion on paper ----------------------------------------------
       Not cosmetic. `.cims-content` carries `transition: margin-left .18s`, so
       when print media applies the margin ANIMATES from the sidebar width down
       to 0. A headless renderer that prints the moment the media query flips
       captures that mid-flight and bakes a phantom left indent into the PDF.
       Measured here across successive frames: 272px -> 231px -> 194px -> 0.
       Transitions have no meaning on paper anyway, so kill them outright
       rather than trying to time around them. */
    *,
    *::before,
    *::after {
        transition: none !important;
        animation: none !important;
    }

    html,
    body {
        background: #fff !important;
        color: #000 !important;
    }

    /* --- chrome that must not reach paper --------------------------------
       The sidebar and navbar are `position: fixed`, which some browsers repeat
       on EVERY printed page. Hiding them removes that failure mode as well as
       the wasted space. */
    .cims-navbar,
    .cims-sidebar,
    .cims-dt-toolbar,
    .dt-length,
    .dt-search,
    .dt-paging,
    .dt-buttons,
    .modal,
    .modal-backdrop,
    .offcanvas,
    .offcanvas-backdrop,
    .toast-container,
    .btn,
    .no-print {
        display: none !important;
    }

    /* The last column is the row's actions — DataTables already marks it
       `.no-export` because it is not data. The same is true on paper. */
    td.no-export,
    th.no-export {
        display: none !important;
    }

    /* Escape hatch for anything that should appear ONLY on paper. */
    .print-only {
        display: block !important;
    }

    /* --- content reclaims the page --------------------------------------- */
    .cims-content {
        margin-left: 0 !important;
        padding-top: 0 !important;
        min-height: 0 !important;
    }

    /* --- surfaces --------------------------------------------------------- */
    .card {
        border: 1px solid #999 !important;
        box-shadow: none !important;
        break-inside: avoid;
    }

    .card-header {
        background: transparent !important;
        border-bottom: 1px solid #999 !important;
    }

    /* --- status badges ----------------------------------------------------
       The one rule here that is not cosmetic. Badges are white text on a
       coloured ground, and browsers omit background colours from print by
       default — so "Approved" / "Rejected" would print as white on white and
       vanish. On a budget approval sheet that is the single most important
       word on the page. Rendering them as bordered black text keeps the
       meaning without depending on `print-color-adjust`, which wastes ink and
       is not honoured everywhere. */
    .badge {
        color: #000 !important;
        background: transparent !important;
        border: 1px solid #666 !important;
        font-weight: 600;
    }

    /* --- tables ----------------------------------------------------------- */
    table {
        width: 100% !important;
        border-collapse: collapse !important;
    }

    /* Repeat the header row on every printed page — without this a table that
       breaks across pages leaves the later pages as unlabelled columns. */
    thead {
        display: table-header-group;
    }

    tr,
    img {
        break-inside: avoid;
    }

    th,
    td {
        border: 1px solid #999 !important;
        background: transparent !important;
    }

    /* Sort arrows are interaction affordances; on paper they read as noise. */
    table.dataTable thead th.dt-orderable-asc span.dt-column-order,
    table.dataTable thead th.dt-orderable-desc span.dt-column-order,
    table.dataTable thead th.dt-ordering-asc span.dt-column-order,
    table.dataTable thead th.dt-ordering-desc span.dt-column-order {
        display: none !important;
    }

    /* --- links ------------------------------------------------------------
       Underlined blue on paper looks like a defect, and the URL is unusable
       anyway on an internal system behind auth. Keep the text, drop the
       decoration. */
    a {
        color: #000 !important;
        text-decoration: none !important;
    }
}

/* ---------------------------------------------------------------------------
   Dark-mode muted text (bug report, 2026-08-25)

   `--bs-body-color` was mapped to the CIMS token but `--bs-secondary-color`
   was not — and Bootstrap 5.3's `.text-muted`, `.form-text` and table
   secondary text all read THAT. So in dark mode every muted string kept
   Bootstrap's light-theme grey (#6c757d) on a #0c1220 background: roughly
   2.4:1, far below the 4.5:1 AA floor, and visibly almost unreadable —
   "No designations found." being the clearest example.

   Mapping the Bootstrap tokens onto the CIMS ones fixes every consumer at
   once rather than chasing individual utilities. The dark muted token
   (#93a0bf on #141c2e) is ~6.4:1, and the light one (#5b6478 on #fff)
   ~5.9:1 — both clear AA.
   --------------------------------------------------------------------------- */
:root {
    --bs-secondary-color: var(--cims-color-text-muted);
    --bs-secondary-color-rgb: var(--cims-color-text-muted-rgb);
    --bs-tertiary-color: var(--cims-color-text-muted);

    /* The same omission, one layer deeper and far more damaging.
       -----------------------------------------------------------------------
       Bootstrap colours EVERY table cell through
         `.table > :not(caption) > * > * { color: var(--bs-table-color-state,
              var(--bs-table-color-type, var(--bs-table-color))) }`
       and `--bs-table-color` resolves to `--bs-emphasis-color`, which ships as
       `#000` and was never mapped here. The cell rule out-specifies the table's
       inherited colour, so in DARK mode every cell rendered BLACK on the
       #0c1220 ground — measured 1.12:1, and confirmed by screenshot: project
       names, fiscal years, amounts and user names were all effectively
       invisible. Only the anchor-coloured links survived.

       That is most of the content in an ERP. Found 2026-08-26 by an app-wide
       composited contrast sweep; `TextTokenContrastTest` covers it now. */
    --bs-emphasis-color: var(--cims-color-text);
    --bs-emphasis-color-rgb: var(--cims-color-text-rgb, 19, 27, 46);
    --bs-table-color: var(--cims-color-text);
    --bs-table-striped-color: var(--cims-color-text);
    --bs-table-hover-color: var(--cims-color-text);
    --bs-table-active-color: var(--cims-color-text);
}

/* Bootstrap sets these with !important on the utility, so the utility itself
   is restated rather than relying on token inheritance alone. */
.text-muted,
.form-text {
    color: var(--cims-color-text-muted) !important;
}

/* An empty-state line is the one piece of text a user reads when nothing else
   is on screen. It must not be the faintest thing on the page. */
.cims-empty-state p,
.cims-empty-state .text-muted,
td .text-muted {
    color: var(--cims-color-text-muted) !important;
}

/* ---------------------------------------------------------------------------
   Collapsed sidebar icon alignment (bug report, 2026-08-25)

   With the rail collapsed, top-level icons were centred but icons inside an
   open group were not: the indentation rules that draw the tree in the
   expanded sidebar are scoped to `:not(.cims-sidebar-collapsed)`, but the
   nested list itself still carried list padding, so its icons sat a few pixels
   left of the others. The result was a rail of icons that did not line up.

   Every indentation source is neutralised explicitly here rather than relying
   on the expanded-state rules simply not applying — that is what left the gap
   in the first place.
   --------------------------------------------------------------------------- */
@media (min-width: 992px) {
    html.cims-sidebar-collapsed .cims-sidebar ul.nav,
    html.cims-sidebar-collapsed .cims-sidebar details > ul.nav {
        padding-left: 0;
        padding-inline-start: 0;
        margin-left: 0;
    }

    /* The vertical tree line has nothing to connect in an icon-only rail. */
    html.cims-sidebar-collapsed .cims-sidebar details > ul.nav::before {
        display: none;
    }

    /* One rule for every depth, so a nested link cannot keep an indent that a
       shallower one has had removed. */
    html.cims-sidebar-collapsed .cims-sidebar .nav-link,
    html.cims-sidebar-collapsed .cims-sidebar .nav-header,
    html.cims-sidebar-collapsed .cims-sidebar details > ul.nav .nav-link {
        justify-content: center;
        padding-left: 0;
        padding-right: 0;
        margin-inline: var(--cims-space-2);
        text-align: center;
    }

    /* The icon is the only visible child; fixing its box stops sub-pixel
       drift between links whose hidden labels differ in length. */
    html.cims-sidebar-collapsed .cims-sidebar .nav-link > .bi,
    html.cims-sidebar-collapsed .cims-sidebar .nav-header > .bi {
        margin: 0;
        width: 1.25rem;
        text-align: center;
        flex: 0 0 1.25rem;
    }
}

/* ---------------------------------------------------------------------------
   Side-menu search (UX programme U5, resources/js/sidebar-search.js)

   The input is created by the script, not the Blade layout: without JavaScript
   it would be a control that does nothing, and a dead control is worse than an
   absent one. These rules therefore only ever apply when it exists.
   --------------------------------------------------------------------------- */
.cims-sidebar-search {
    padding: var(--cims-space-2) var(--cims-space-3);
}

/* The rail is dark in BOTH themes, so this input cannot use the page text
   tokens — and `color: inherit` silently did.
   -------------------------------------------------------------------------
   `.cims-sidebar` sets no `color` of its own (each nav link sets its own), so
   `inherit` walked up to `body` and resolved to `--cims-color-text`: #131b2e,
   a near-black navy meant for a near-white page. Composited over the dark rail
   the typed text was black-on-black — invisible, with only the spellcheck
   underline showing anything had been typed. The placeholder had the same bug
   via `--cims-color-text-muted`.

   Reported from the running app on 2026-08-26. Fixed by using the sidebar's own
   tokens, which are defined for a dark ground in every theme. */
.cims-sidebar-search .form-control {
    background-color: rgba(255, 255, 255, .06);
    border-color: var(--cims-sidebar-border);
    color: var(--cims-sidebar-text-active);
    caret-color: var(--cims-sidebar-text-active);
}

/* `--cims-sidebar-heading` is deliberately dim (55% alpha) for section captions
   and measured only 2.76:1 here — placeholder text is text, and AA wants 4.5:1.
   The brighter sidebar body token clears it comfortably. */
.cims-sidebar-search .form-control::placeholder {
    color: var(--cims-sidebar-text);
    opacity: 1; /* Firefox dims placeholders further by default */
}

/* Bootstrap re-asserts `--bs-body-color` on .form-control:focus, which would
   reinstate the dark page text the moment the user clicked in. */
.cims-sidebar-search .form-control:focus {
    background-color: rgba(255, 255, 255, .1);
    color: var(--cims-sidebar-text-active);
}

/* Hidden in the collapsed icon rail: a text box in a 64px column cannot be
   typed into, and the labels it filters are not shown there anyway. */
html.cims-sidebar-collapsed .cims-sidebar-search {
    display: none;
}

/* ==========================================================================
   Structure ERP reference alignment
   ==========================================================================
   Source: bnd/design/.../structure_erp/DESIGN.md.

   This is an IMPROVEMENT pass, not a redesign. No screen's markup, fields or
   workflow changed; no colour token changed either — the palette here carries
   measured WCAG ratios in its comments (see :root) and the brand teal was
   kept deliberately. What changes is the structural language the reference
   system specifies and this file had not yet adopted consistently: the 12px
   signature radius, tinted elevation, the `label-md` type role, pill status
   badges, and the density of the surfaces those sit on.

   Appended as one block rather than edited in place: the file is being worked
   on concurrently, and a contiguous section is both reviewable in isolation
   and trivially separable. Later rules win at equal specificity, which is the
   intended relationship — these refine the base rules above, they do not
   replace them.
   ========================================================================== */

/* --- Status badges: pill ---------------------------------------------------
   "Pill Shape: Reserved for status badges and primary action buttons to make
   them instantly recognisable as interactive or state-driven." A rounded
   rectangle badge is the same shape as a table cell, a card and an input; the
   pill is the one shape in the system that means "this is a state". */
.badge {
    border-radius: var(--cims-radius-pill);
    padding: .3125rem .625rem;
    letter-spacing: .02em;
}

/* --- label-md: field labels, table headers, metadata ------------------------
   Applied to the roles the reference system names. Table headers already used
   uppercase + .06em tracking and are left alone — they were right. */
.form-label,
.cims-field-label {
    font-size: var(--cims-text-label);
    font-weight: 600;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
    margin-bottom: .375rem;
}

/* A required marker should not be louder than the label it qualifies. */
.form-label .text-danger {
    font-weight: 700;
    margin-inline-start: .125rem;
}

/* --- Inputs: signature radius + the reference's focus treatment -------------
   "Active State: border changes to primary with a 2px soft outer glow."
   The glow is ADDITIVE to the existing focus-visible outline, never a
   replacement: the outline is the accessibility affordance and removing it to
   chase a visual is how focus indicators get lost. */
.form-control,
.form-select,
.input-group-text {
    border-radius: var(--cims-radius-md);
}

.form-control:focus,
.form-select:focus {
    border-color: var(--cims-color-primary);
    box-shadow: 0 0 0 3px rgba(var(--cims-color-primary-rgb), .12);
}

/* --- Cards: signature radius, and a header that separates -------------------
   "Header: includes a bottom border separating the title from the content for
   complex data views." Already true; what was missing is that a card header
   should read as a label, not as body text. */
.card {
    border-radius: var(--cims-radius-md);
}

.card-header {
    font-size: var(--cims-text-label);
    font-weight: 700;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
}

/* Prose inside a card header opts out of the label treatment.

   A card header reading as a LABEL is right for "Materials" or "RA Bills". It
   is wrong for a sentence: uppercasing a full line of prose removes the word
   shapes a reader navigates by, so the estimate's step panels — which carry a
   heading, a description and the name of work in their header — read as
   shouting and were markedly harder to scan than the tables beneath them. */
.card-header .cims-header-prose,
.card-header .cims-header-prose * {
    text-transform: none;
    letter-spacing: normal;
    font-weight: 400;
}

/* A card header carrying its own controls needs to align them, not stack. */
.card-header.cims-card-header-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--cims-space-3);
}

.modal-content {
    border-radius: var(--cims-radius-lg);
}

/* --- Section separation ----------------------------------------------------
   The reference system separates bands of content with space and a hairline
   rather than with more boxes. An ERP screen that nests card-in-card to show
   grouping ends up with four borders between two facts. */
.cims-section {
    margin-block: var(--cims-space-5);
}

.cims-section + .cims-section {
    padding-block-start: var(--cims-space-5);
    border-block-start: 1px solid var(--cims-color-border);
}

.cims-section-title {
    font-size: var(--cims-text-label);
    font-weight: 700;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
    margin-bottom: var(--cims-space-3);
}

/* --- Sticky action bar -----------------------------------------------------
   A long ERP form's submit sits below the fold, so the user scrolls to the
   bottom to find out whether they may save. Pinning the action row keeps the
   commit available at every scroll position. `position: sticky` inside normal
   flow, so it never covers the last field the way a fixed bar does. */
.cims-action-bar {
    position: sticky;
    bottom: 0;
    z-index: 3;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--cims-space-2);
    padding: var(--cims-space-3);
    margin-block-start: var(--cims-space-4);
    background-color: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-md);
    box-shadow: var(--cims-shadow-md);
}

.cims-action-bar .cims-action-bar-spacer {
    margin-inline-start: auto;
}

/* Sticky positioning depends on the scroll container having room to scroll.
   On a short viewport the bar would pin over the form it belongs to, so below
   the height where that starts to bite it simply returns to flow. */
@media (max-height: 600px) {
    .cims-action-bar {
        position: static;
    }
}

/* --- Avatars ---------------------------------------------------------------
   People are the primary object in an approval workflow; an initial in a
   circle identifies one faster than a name in a cell. Sized on the 8px grid. */
.cims-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 2rem;
    block-size: 2rem;
    border-radius: var(--cims-radius-pill);
    background-color: rgba(var(--cims-color-primary-rgb), .12);
    color: var(--cims-color-primary-dark);
    font-size: var(--cims-text-xs);
    font-weight: 700;
    letter-spacing: .02em;
    text-transform: uppercase;
    flex: 0 0 auto;
    border: 2px solid var(--cims-color-surface);
}

.cims-avatar-sm {
    inline-size: 1.5rem;
    block-size: 1.5rem;
    font-size: .625rem;
}

.cims-avatar-lg {
    inline-size: 2.5rem;
    block-size: 2.5rem;
    font-size: var(--cims-text-sm);
}

/* Overlapping stack for "who is on this" without spending a column on it. */
.cims-avatar-stack {
    display: inline-flex;
    align-items: center;
}

.cims-avatar-stack > .cims-avatar + .cims-avatar {
    margin-inline-start: -.5rem;
}

/* --- Workflow timeline -----------------------------------------------------
   A vertical rail of stages. The rail is drawn on the list, not on each item,
   so it is one continuous line rather than a stack of segments that betray
   their joins at different zoom levels. */
.cims-timeline {
    position: relative;
    list-style: none;
    margin: 0;
    padding: 0 0 0 var(--cims-space-5);
}

.cims-timeline::before {
    content: "";
    position: absolute;
    inset-block: .75rem;
    inset-inline-start: .6875rem;
    inline-size: 2px;
    background-color: var(--cims-color-border);
    border-radius: var(--cims-radius-pill);
}

.cims-timeline-step {
    position: relative;
    padding-block: var(--cims-space-2);
    padding-inline-start: var(--cims-space-3);
}

.cims-timeline-step::before {
    content: "";
    position: absolute;
    inset-inline-start: calc(var(--cims-space-5) * -1 + .3125rem);
    inset-block-start: 1rem;
    inline-size: .875rem;
    block-size: .875rem;
    border-radius: var(--cims-radius-pill);
    background-color: var(--cims-color-surface);
    border: 2px solid var(--cims-color-border);
}

.cims-timeline-step.is-complete::before {
    background-color: var(--cims-color-success);
    border-color: var(--cims-color-success);
}

.cims-timeline-step.is-rejected::before {
    background-color: var(--cims-color-danger);
    border-color: var(--cims-color-danger);
}

/* The stage awaiting action is the one thing on the panel the user can act
   on, so it is the only stage given a fill, a ring and a tinted ground. */
.cims-timeline-step.is-current::before {
    background-color: var(--cims-color-primary);
    border-color: var(--cims-color-primary);
    box-shadow: 0 0 0 4px rgba(var(--cims-color-primary-rgb), .16);
}

.cims-timeline-step.is-current {
    background-color: rgba(var(--cims-color-primary-rgb), .05);
    border-radius: var(--cims-radius-sm);
}

.cims-timeline-step-title {
    font-weight: 600;
    font-size: var(--cims-text-sm);
    color: var(--cims-color-text);
}

.cims-timeline-step-meta {
    font-size: var(--cims-text-xs);
    color: var(--cims-color-text-muted-strong, #4d5566);
}

/* --- Responsive ------------------------------------------------------------
   The reference system drops to 16px margins below 768px. A workflow panel
   built as a row must become a column there, or the avatars and the action
   buttons fight for the same 320px. */
@media (max-width: 767.98px) {
    .cims-action-bar {
        flex-direction: column;
        align-items: stretch;
    }

    .cims-action-bar .cims-action-bar-spacer {
        margin-inline-start: 0;
    }

    .cims-action-bar .btn {
        inline-size: 100%;
    }
}

/* --- Workflow card ---------------------------------------------------------
   The Joint Verification panel, and the pattern for any approval chain. Three
   parties sign in sequence; the panel's job is to answer "how far along is
   this, and is it waiting on me" before the user reads a single word. */
.cims-workflow-progress-label {
    font-size: var(--cims-text-label);
    font-weight: 600;
    letter-spacing: var(--cims-tracking-label);
    color: var(--cims-color-text-muted-strong, #4d5566);
    white-space: nowrap;
}

/* Full-bleed under the header, so the bar reads as the card's own progress
   rather than as a widget sitting inside the body. */
.cims-workflow-card .cims-workflow-progress {
    block-size: .25rem;
    border-radius: 0;
    background-color: var(--cims-color-border);
}

.cims-workflow-card .cims-timeline {
    margin-block: var(--cims-space-1);
}

/* A signed stage gets the success tint; the initials stay legible on it. */
.cims-avatar-signed {
    background-color: rgba(var(--cims-color-success-rgb), .14);
    color: var(--cims-color-success);
}

/* The KI-43 explanation is a footnote to its stage, not a peer of it. */
.cims-timeline-hint {
    margin-block: var(--cims-space-2) 0;
    padding-inline-start: var(--cims-space-2);
    border-inline-start: 2px solid var(--cims-color-border);
}

/* Guards against a long signatory name forcing the card wider than its
   column, which on a 5-column layout pushes the action buttons off-screen. */
.min-width-0 {
    min-inline-size: 0;
}

/* ==========================================================================
   Phase 0 — interaction-state and density vocabulary
   ==========================================================================
   The token layer described colour, type, space, radius and elevation but had
   no vocabulary for STATE. Every screen therefore invented its own hover and
   its own disabled look, which is the single largest source of the
   inconsistency this rollout exists to remove. Naming them once is what makes
   "every page feels like one product" enforceable rather than aspirational.
   ========================================================================== */

:root {
    /* Interaction states, expressed as alpha over the current surface so they
       compose on white, on a striped row and on a dark card without three
       separate definitions. */
    --cims-state-hover: rgba(var(--cims-color-primary-rgb), .06);
    --cims-state-active: rgba(var(--cims-color-primary-rgb), .12);
    --cims-state-selected: rgba(var(--cims-color-primary-rgb), .10);
    --cims-state-disabled-opacity: .55;

    /* One focus ring for the whole application. Focus was previously drawn
       three different ways depending on the control. */
    --cims-focus-ring: 0 0 0 3px rgba(var(--cims-color-primary-rgb), .28);
    --cims-focus-ring-danger: 0 0 0 3px rgba(var(--cims-color-danger-rgb), .28);

    /* Feedback surfaces. Tints, not saturated fills: an ERP shows several of
       these at once and full-strength banners turn a screen into a traffic
       light. */
    --cims-surface-success: rgba(var(--cims-color-success-rgb), .10);
    --cims-surface-warning: rgba(var(--cims-color-warning-rgb), .10);
    --cims-surface-danger: rgba(var(--cims-color-danger-rgb), .10);
    --cims-surface-info: rgba(var(--cims-color-info-rgb), .10);

    /* Skeleton loading. The application had no loading state at all — every
       screen went from blank to full, which reads as a hang on a slow link. */
    --cims-skeleton-base: rgba(var(--cims-color-text-muted-rgb), .12);
    --cims-skeleton-sheen: rgba(var(--cims-color-text-muted-rgb), .04);

    /* Row density. A list and a form want different rhythms; the reference
       system's 56px row suits a list, not a nested detail table. */
    --cims-row-height: 3.5rem;
    --cims-row-height-compact: 2.5rem;
    --cims-row-padding: .875rem;
    --cims-row-padding-compact: .5rem;

    /* Icons stay Bootstrap Icons — swapping icon sets across 289 views buys
       nothing functional. What was missing is a consistent SIZE: the same icon
       rendered at whatever the surrounding font-size happened to be. */
    --cims-icon-sm: .875rem;
    --cims-icon-md: 1rem;
    --cims-icon-lg: 1.25rem;
}

/* --- Focus: one ring, everywhere -------------------------------------------
   Additive to the existing :focus-visible outline, never a replacement — the
   outline is the accessibility affordance and is what survives forced-colours
   mode, where box-shadow is dropped entirely. */
.btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible,
.form-check-input:focus-visible,
.page-link:focus-visible,
.dropdown-item:focus-visible {
    box-shadow: var(--cims-focus-ring);
}

.btn-danger:focus-visible,
.btn-outline-danger:focus-visible {
    box-shadow: var(--cims-focus-ring-danger);
}

/* --- Disabled: one treatment ----------------------------------------------- */
.btn:disabled,
.btn.disabled,
.form-control:disabled,
.form-select:disabled,
fieldset:disabled .btn {
    opacity: var(--cims-state-disabled-opacity);
    cursor: not-allowed;
}

/* --- Icon sizing ----------------------------------------------------------- */
.cims-icon-sm { font-size: var(--cims-icon-sm); }
.cims-icon-md { font-size: var(--cims-icon-md); }
.cims-icon-lg { font-size: var(--cims-icon-lg); }

/* An icon inside a button should optically match the label, not inherit a
   heading's size when the button happens to sit in one. */
.btn > .bi {
    font-size: var(--cims-icon-md);
    vertical-align: -.125em;
}

.btn-sm > .bi {
    font-size: var(--cims-icon-sm);
}

/* --- Skeleton loading ------------------------------------------------------
   `prefers-reduced-motion` stops the sweep rather than the skeleton: the
   placeholder still communicates "loading", it just stops moving. */
.cims-skeleton {
    display: block;
    background-color: var(--cims-skeleton-base);
    border-radius: var(--cims-radius-sm);
    background-image: linear-gradient(
        90deg,
        transparent 0%,
        var(--cims-skeleton-sheen) 50%,
        transparent 100%
    );
    background-size: 200% 100%;
    animation: cims-skeleton-sweep 1.4s ease-in-out infinite;
}

.cims-skeleton-text {
    block-size: .75rem;
    margin-block: .375rem;
    border-radius: var(--cims-radius-pill);
}

.cims-skeleton-text:last-child {
    inline-size: 60%;
}

.cims-skeleton-circle {
    border-radius: var(--cims-radius-pill);
}

@keyframes cims-skeleton-sweep {
    from { background-position: 200% 0; }
    to { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .cims-skeleton {
        animation: none;
        background-image: none;
    }
}

/* --- Feedback surfaces ------------------------------------------------------
   Bootstrap's .alert-* ship literal hexes that never see the brand tokens, so
   an "info" alert rendered Bootstrap cyan next to CIMS info teal. */
.alert-success { background-color: var(--cims-surface-success); border-color: rgba(var(--cims-color-success-rgb), .28); color: var(--cims-color-success); }
.alert-warning { background-color: var(--cims-surface-warning); border-color: rgba(var(--cims-color-warning-rgb), .28); color: var(--cims-color-warning); }
.alert-danger  { background-color: var(--cims-surface-danger);  border-color: rgba(var(--cims-color-danger-rgb), .28);  color: var(--cims-color-danger); }
.alert-info    { background-color: var(--cims-surface-info);    border-color: rgba(var(--cims-color-info-rgb), .28);    color: var(--cims-color-info); }

/* ==========================================================================
   Phase 1 — shared component styling
   ========================================================================== */

/* --- Page header ----------------------------------------------------------- */
.cims-page-header-identity {
    display: flex;
    align-items: flex-start;
    gap: var(--cims-space-3);
    min-inline-size: 0;
}

.cims-page-header-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    flex: 0 0 auto;
    border-radius: var(--cims-radius-md);
    background-color: rgba(var(--cims-color-primary-rgb), .10);
    color: var(--cims-color-primary);
    font-size: var(--cims-icon-lg);
}

.cims-page-eyebrow {
    margin: 0 0 .125rem;
    font-size: var(--cims-text-label);
    font-weight: 700;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
}

/* Pinned to just under the fixed navbar, not to 0, or the actions slide
   beneath it. */
.cims-page-header-actions-sticky {
    position: sticky;
    inset-block-start: calc(var(--cims-navbar-height) + var(--cims-space-2));
    z-index: 2;
}

/* --- Empty state variants ---------------------------------------------------
   The icon is what carries the distinction at a glance; the copy carries it
   for anyone who reads. Both, not either. */
.cims-empty-state--error i {
    color: var(--cims-color-danger);
}

.cims-empty-state--no-results i {
    color: var(--cims-color-text-muted-strong, #4d5566);
}

/* --- Skeleton grouping ----------------------------------------------------- */
.cims-skeleton-group {
    display: block;
    inline-size: 100%;
}

/* --- KPI card: additive only ------------------------------------------------
   `.cims-kpi-card` ALREADY EXISTS above (line ~1330) as a horizontal card —
   icon beside label and value — and the dashboard's four tiles use it. An
   earlier draft of this block redefined the class as a column and, because it
   sits later in the file, silently won: the dashboard's icons stacked above
   their labels and the tiles grew from 96px to 140px.

   The app having one KPI card and this rollout adding a second is precisely
   the inconsistency the rollout exists to remove, so `x-kpi-card` now emits
   the EXISTING structure. Only the parts that genuinely did not exist —
   trend, caption, and the linked variant — are defined here, and none of them
   touch the layout. */
.cims-kpi-foot {
    display: flex;
    align-items: center;
    gap: var(--cims-space-2);
    flex-wrap: wrap;
    margin-block-start: .25rem;
    font-size: var(--cims-text-xs);
}

.cims-kpi-trend {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    font-weight: 600;
}

.cims-kpi-trend--success { color: var(--cims-color-success); }
.cims-kpi-trend--warning { color: var(--cims-color-warning); }
.cims-kpi-trend--danger  { color: var(--cims-color-danger); }
.cims-kpi-trend--info    { color: var(--cims-color-info); }

.cims-kpi-caption {
    color: var(--cims-color-text-muted-strong, #4d5566);
}

/* The linked variant needs the anchor to look like the div it replaces. */
a.cims-kpi-card {
    text-decoration: none;
    color: inherit;
}

a.cims-kpi-card:hover,
a.cims-kpi-card:focus-visible {
    border-color: rgba(var(--cims-color-primary-rgb), .35);
}

/* --- Info group (detail pages) ---------------------------------------------
   `dt` is the label and `dd` the value; the pair is the unit, so the grid is
   applied to the pair wrappers rather than to dt/dd individually — otherwise a
   long value in one column silently pushes the next pair's label out of line. */
.cims-info-group + .cims-info-group {
    margin-block-start: var(--cims-space-4);
}

.cims-info-grid {
    display: grid;
    gap: var(--cims-space-3) var(--cims-space-4);
    margin: 0;
}

.cims-info-grid > div {
    min-inline-size: 0;
}

.cims-info-grid dt {
    font-size: var(--cims-text-label);
    font-weight: 600;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
    margin-block-end: .125rem;
}

.cims-info-grid dd {
    margin: 0;
    font-size: var(--cims-text-base);
    color: var(--cims-color-text);
    overflow-wrap: anywhere;
}

@media (min-width: 768px) {
    .cims-info-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cims-info-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .cims-info-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* --- Filter card ------------------------------------------------------------ */
.cims-filter-card .card-body {
    padding-block: var(--cims-space-3);
}

/* --- Data table: density and footer ----------------------------------------
   The comfortable rhythm is the existing one and stays the default — it was
   deliberately tuned (see the Phase 3 data-table block above). `compact` is
   opt-in for dense sub-tables, where the list rhythm wastes vertical space
   the surrounding detail page needs. */
.cims-table--compact > :not(caption) > * > * {
    padding-block: var(--cims-row-padding-compact);
}

/* Pagination moves out of the scroll container into a footer of its own, so
   it no longer scrolls sideways with a wide table — the previous layout put
   the pager inside `.table-responsive`, where a table wider than its card
   carried the page links off-screen with it. */
.cims-table-footer {
    background-color: var(--cims-color-surface);
    border-block-start: 1px solid var(--cims-color-border);
    padding-block: var(--cims-space-2);
}

.cims-table-footer .pagination {
    margin-block-end: 0;
}

/* ==========================================================================
   Phase 3 — forms
   ========================================================================== */

/* --- Modal forms -----------------------------------------------------------
   With `modal-dialog-scrollable` the footer is pinned, so it needs the same
   separation a sticky action bar gets — otherwise the last field appears to
   run into the buttons. */
.modal-dialog-scrollable .modal-footer {
    background-color: var(--cims-color-surface);
    border-block-start: 1px solid var(--cims-color-border);
}

.modal-body {
    padding: var(--cims-space-4);
}

/* --- Helper and error text --------------------------------------------------
   Helper text explains a field BEFORE it is filled in; the error explains it
   after. They were the same size and colour, so a validation message read as
   more guidance rather than as something to fix. */
.form-text {
    font-size: var(--cims-text-xs);
    color: var(--cims-color-text-muted-strong, #4d5566);
    margin-block-start: .25rem;
}

.invalid-feedback,
.cims-input-error {
    font-size: var(--cims-text-xs);
    font-weight: 600;
    color: var(--cims-color-danger);
    margin-block-start: .25rem;
}

/* An invalid control should be identifiable without relying on colour alone
   (WCAG 1.4.1): the border thickens as well as changing hue. */
.form-control.is-invalid,
.form-select.is-invalid {
    border-width: 2px;
    border-color: var(--cims-color-danger);
}

.form-control.is-invalid:focus,
.form-select.is-invalid:focus {
    box-shadow: var(--cims-focus-ring-danger);
}

/* --- Field groups -----------------------------------------------------------
   A section header inside a form, so a 20-field record reads as three or four
   labelled groups rather than one undifferentiated wall. */
.cims-form-section + .cims-form-section {
    margin-block-start: var(--cims-space-4);
    padding-block-start: var(--cims-space-4);
    border-block-start: 1px solid var(--cims-color-border);
}

.cims-form-section-title {
    font-size: var(--cims-text-label);
    font-weight: 700;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
    margin-block-end: var(--cims-space-2);
}

.cims-form-section-hint {
    font-size: var(--cims-text-xs);
    color: var(--cims-color-text-muted-strong, #4d5566);
    margin-block: -.25rem 0 var(--cims-space-2);
}

/* --- Record header ---------------------------------------------------------
   The opening block of a detail page (reference: 360_record_view). Status,
   then identity, then actions — in that order, because that is the order the
   questions are asked: what state is this in, what is it, what can I do. */
/* Tighter, because it is the lid and not the contents.

   At 1.5rem of padding all round plus a 1.5rem gap above the metrics and
   another below the card, the opening block of every detail page stood 265px
   tall before a word of the record itself. On the estimate that pushed the
   eleven-step sequence — the thing the page exists to walk you through — to
   408px down, half a screen in. Same information, less air. */
.cims-record-header {
    background-color: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-md);
    box-shadow: var(--cims-shadow-sm);
    padding: var(--cims-space-3);
    margin-block-end: var(--cims-space-3);
}

.cims-record-header-top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--cims-space-3);
    flex-wrap: wrap;
}

.cims-record-header-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--cims-space-2);
    margin-block-end: var(--cims-space-2);
}

.cims-record-code {
    font-size: var(--cims-text-label);
    font-weight: 600;
    letter-spacing: var(--cims-tracking-label);
    color: var(--cims-color-text-muted-strong, #4d5566);
}

.cims-record-title {
    font-size: var(--cims-text-lg);
    font-weight: 700;
    letter-spacing: -.02em;
    line-height: 1.15;
    margin: 0;
    color: var(--cims-color-text);
}

.cims-record-subtitle {
    margin: .25rem 0 0;
    font-size: var(--cims-text-sm);
    color: var(--cims-color-text-muted-strong, #4d5566);
}

.cims-record-header-actions {
    display: flex;
    align-items: center;
    gap: var(--cims-space-2);
    flex-wrap: wrap;
}

/* --- Metric strip ----------------------------------------------------------
   Two to four figures across the foot of the record header. Grid rather than
   flex so the columns align whatever the label lengths, which is the whole
   reason it reads as a strip and not as three unrelated numbers. */
.cims-record-metrics {
    display: grid;
    gap: var(--cims-space-2) var(--cims-space-5);
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    margin-block-start: var(--cims-space-3);
    padding-block-start: var(--cims-space-2);
    border-block-start: 1px solid var(--cims-color-border);
}

.cims-metric-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--cims-space-2);
}

.cims-metric-label {
    font-size: var(--cims-text-label);
    font-weight: 600;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
}

.cims-metric-value {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--cims-color-text);
}

.cims-metric-value--success { color: var(--cims-color-success); }
.cims-metric-value--warning { color: var(--cims-color-warning); }
.cims-metric-value--danger  { color: var(--cims-color-danger); }

.cims-metric-bar {
    margin-block-start: .375rem;
    block-size: .5rem;
    border-radius: var(--cims-radius-pill);
    background-color: var(--cims-color-border);
    overflow: hidden;
}

.cims-metric-fill {
    display: block;
    block-size: 100%;
    border-radius: var(--cims-radius-pill);
    background-color: var(--cims-color-primary);
}

.cims-metric-fill--success { background-color: var(--cims-color-success); }
.cims-metric-fill--warning { background-color: var(--cims-color-warning); }
.cims-metric-fill--danger  { background-color: var(--cims-color-danger); }

/* --- KPI alert tile --------------------------------------------------------
   The reference's CRITICAL tile: a tinted ground and a coloured border so one
   figure in the row reads as needing action. Deliberately only a border and a
   wash — the number stays the same size as its neighbours, because a row where
   one tile is also physically bigger stops being a row. */
.cims-kpi-card--alert {
    border-width: 1px;
}

.cims-kpi-card--alert-danger {
    background-color: var(--cims-surface-danger);
    border-color: rgba(var(--cims-color-danger-rgb), .35);
}

.cims-kpi-card--alert-warning {
    background-color: var(--cims-surface-warning);
    border-color: rgba(var(--cims-color-warning-rgb), .35);
}

.cims-kpi-card--alert-success {
    background-color: var(--cims-surface-success);
    border-color: rgba(var(--cims-color-success-rgb), .35);
}

/* --- Donut centre total ----------------------------------------------------
   The reference puts the total in the hole of the doughnut. Positioned over
   the canvas rather than drawn into it, so it stays selectable text and scales
   with the user's font size instead of being baked into a bitmap. */
.cims-donut-wrap {
    position: relative;
}

.cims-donut-centre {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    text-align: center;
}

.cims-donut-total {
    font-size: var(--cims-text-lg);
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    color: var(--cims-color-text);
}

.cims-donut-caption {
    font-size: var(--cims-text-label);
    font-weight: 600;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
}

/* A KPI row is only a row if the tiles line up. One tile carrying an extra
   trend line made it 170px against its neighbours' 126px, which reads as a
   layout fault rather than as emphasis — and the alert tile is precisely the
   one that must not look like a mistake. Bootstrap's columns are already equal
   height; the card just has to fill its column. */
.cims-kpi-card {
    block-size: 100%;
}

/* --- Detail drawer ---------------------------------------------------------
   Reference: ncr_non_conformance_report. Wider than Bootstrap's default 400px
   because it carries a two-column field grid, and full width on a phone where
   a side panel is just a narrower page. */
.cims-drawer {
    inline-size: min(560px, 100vw);
}

@media (max-width: 767.98px) {
    .cims-drawer {
        inline-size: 100vw;
    }
}

.cims-drawer-header {
    align-items: flex-start;
    border-block-end: 1px solid var(--cims-color-border);
}

/* Sticky, so the actions stay reachable however long the record is — the same
   reasoning as the form action bar. */
.cims-drawer-footer {
    position: sticky;
    inset-block-end: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--cims-space-2);
    padding: var(--cims-space-3);
    background-color: var(--cims-color-surface);
    border-block-start: 1px solid var(--cims-color-border);
}

/* An accent-bordered block for a named part of the record — root cause,
   corrective action. The border carries the meaning, so the tint stays faint. */
.cims-drawer-section {
    border-inline-start: 3px solid var(--cims-color-primary);
    background-color: rgba(var(--cims-color-primary-rgb), .05);
    border-radius: var(--cims-radius-sm);
    padding: var(--cims-space-3);
    margin-block-end: var(--cims-space-3);
}

.cims-drawer-section--danger {
    border-inline-start-color: var(--cims-color-danger);
    background-color: var(--cims-surface-danger);
}

.cims-drawer-section-title {
    font-size: var(--cims-text-label);
    font-weight: 700;
    letter-spacing: var(--cims-tracking-label);
    text-transform: uppercase;
    color: var(--cims-color-text-muted-strong, #4d5566);
    margin-block-end: .25rem;
}

/* A row whose drawer is open, so the list still says which record you are
   reading. */
.cims-row-active > * {
    background-color: var(--cims-state-selected) !important;
}


/* ==========================================================================
   MATERIAL 3 COMPONENT LAYER
   ==========================================================================
   Appended rather than @imported: `@import` is only legal before other rules,
   so a separate file would load BEFORE everything above and lose every tie.
   This layer has to win, so it lives at the end. Tokens are in
   design-system.css, which is imported at the top where load order does not
   matter for custom properties.

   Scope: appearance only. No rule here changes a layout enough to move a
   control, hide a field, or alter what a page does -- the surfaces below are
   the ones a user reads as "quality" (press feedback, elevation, focus,
   header behaviour under scroll) and every one of them is a paint change.
   Motion honours the reduced-motion collapse already in design-system.css,
   because durations resolve from tokens rather than being written inline.
   ========================================================================== */

/* --- Interactive feedback ------------------------------------------------
   Bootstrap changes only background-color on :active. M3 reads press as a
   physical response, so the control also drops a hair. 1px is deliberate:
   enough to feel, too small to reflow anything next to it. */
.btn,
.cims-kpi-card,
.cims-status-badge,
.nav-link,
.dropdown-item {
    transition:
        background-color var(--cims-motion-fast) var(--cims-ease-standard),
        border-color var(--cims-motion-fast) var(--cims-ease-standard),
        box-shadow var(--cims-motion-fast) var(--cims-ease-standard),
        color var(--cims-motion-fast) var(--cims-ease-standard),
        transform var(--cims-motion-fast) var(--cims-ease-standard);
}

.btn:active:not(:disabled):not(.disabled) {
    transform: translateY(1px);
}

/* Raised buttons carry elevation and gain it on hover; the rest stay flat, so
   the primary action on a screen is the one that looks liftable. */
.btn-primary,
.btn-danger,
.btn-success {
    box-shadow: var(--cims-shadow-sm);
}

.btn-primary:hover:not(:disabled),
.btn-danger:hover:not(:disabled),
.btn-success:hover:not(:disabled) {
    box-shadow: var(--cims-shadow-md);
}

.btn-primary:active:not(:disabled),
.btn-danger:active:not(:disabled),
.btn-success:active:not(:disabled) {
    box-shadow: var(--cims-shadow-sm);
}

/* --- Focus ------------------------------------------------------------
   The 2px ring above is kept exactly as it is -- it is the WCAG 2.4.7
   evidence and it passes. This only adds a soft halo outside it, which is
   decoration: the outline alone still satisfies the contrast requirement if
   the halo is never painted. */
.btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible {
    box-shadow: 0 0 0 4px rgba(var(--cims-color-primary-rgb), .16);
}

/* --- Data tables ---------------------------------------------------------
   A sticky header was written here first and removed after measuring it: the
   nearest scrollport is Bootstrap's `.table-responsive`, whose `overflow-x:
   auto` forces `overflow-y` to `auto` as well, so it becomes the containing
   block for `position: sticky` -- and it never scrolls vertically, the PAGE
   does. `top: 0` therefore resolved against a box that never moves and the
   header never stuck. It rendered a convincing shadow and did nothing.

   Making it work needs `.table-responsive` to own a vertical scroll region
   (a max-height), which is how Atlassian and Procore do it -- but that gives
   every table its own inner scrollbar, which is a change to how the page
   scrolls, not a change to how it looks. Flagged for a decision instead of
   shipped quietly under a visual-only brief.

   What is kept is the part that was never sticky: a solid header background
   and a hairline under it, so the header reads as a header while it is on
   screen. */
.table > thead > tr > th {
    background: var(--cims-color-surface);
    box-shadow: inset 0 -1px 0 var(--cims-color-border);
}

/* Sticky headers, now that the scroll region exists.
   -------------------------------------------------------------------------
   `max-height` rather than `height`, which is what makes this safe: a table
   shorter than the cap is laid out exactly as before -- no inner scrollbar, no
   sticky behaviour, nothing to notice. Only a table long enough to have lost
   its header off the top of the screen gets its own scroll region, which is
   precisely the case the header was needed for.

   The cap leaves room for the navbar, page header and filter bar above and the
   pagination footer below; the footer lives in `.card-footer`, outside this
   box, so paging controls never scroll away with the rows. */
.card-body.table-responsive {
    max-height: calc(100vh - 20rem);
    overflow-y: auto;
}

/* The card's top padding sits ABOVE the pinned header, and rows scroll up into
   that strip -- a sliver of the previous row floating over the column names.
   Removing it pins the header flush; `th` carries its own padding, so nothing
   crowds.

   The selector is this long on purpose. It was `.card-body.table-responsive`
   (0,2,0), which design-system.css then outweighed with
   `.cims-content .card > .card-body { padding: 1.25rem }` (0,3,0) -- so the
   padding came back at 20px and the bleed returned, on every list screen in
   the app, while the rule that was supposed to prevent it sat right here
   looking correct. Matching that selector's shape is what makes this win. */
.cims-content .card > .card-body.table-responsive {
    padding-top: 0;
}

/* `.dt-container` is in the path because DataTables wraps the table in one
   after init, which a plain child combinator from the scroll box no longer
   reaches. */
.card-body.table-responsive > .table > thead > tr > th,
.card-body.table-responsive > .dt-container .table > thead > tr > th {
    position: sticky;
    top: 0;
    z-index: 2;
}

/* Numbers are compared down a column, so they need equal-width digits and a
   right edge that lines up. Applied only to cells the markup already marks as
   numeric -- nothing here guesses at content. */
.table td.text-end,
.table th.text-end,
.text-tabular {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

/* --- Elevated surfaces ---------------------------------------------------
   Modals, drawers and menus are the three surfaces that float above the page.
   They should read as one family and sit clearly above it.

   Two levels, not one: a dropdown is attached to the control that opened it
   and is dismissed by looking away, so it takes --cims-shadow-md; a modal or
   a drawer blocks the page behind it and takes --cims-shadow-lg. Giving both
   the same shadow was what made a row's action menu read as heavy as a
   confirmation dialog. */
.modal-content,
.offcanvas {
    border-radius: var(--cims-radius-md);
    box-shadow: var(--cims-shadow-lg);
}

.dropdown-menu {
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-md);
    box-shadow: var(--cims-shadow-md);
    padding: var(--cims-space-2);
}

.dropdown-item {
    border-radius: var(--cims-radius-sm);
    padding: var(--cims-space-2) var(--cims-space-2-5);
}

/* Menus that appear instantly read as a glitch; 
   the decelerate curve is M3's "entering the screen" easing. */
.dropdown-menu.show {
    animation: cims-menu-in var(--cims-motion-fast) var(--cims-ease-decelerate);
}

@keyframes cims-menu-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
}


/* --- Row actions -----------------------------------------------------------
   The Actions cell of a list row.

   It was `text-nowrap`, which is right for two or three buttons and wrong for
   eight: the estimates list gained Start PO, Start Tender and Start Agreement
   and the table began scrolling sideways, cutting the last buttons off screen.

   Wrapping alone was worse. Nine columns squeeze this one to the width of a
   single button, so every button landed on its own line and "Start Agreement"
   broke across two — a row three hundred pixels tall. The cell needs a floor
   on its width before wrapping helps, and the labels must not break inside
   themselves. */
/* A FLOOR on the width, so wrapping helps rather than hurts.

   Nine columns squeeze this one to about the width of a single button, and
   without a floor every action landed on its own line — a row three hundred
   pixels tall with "Start Agreement" broken across two lines.

   16.25rem is the widest floor that still lets the whole table fit at a normal
   window width; 17rem put it 11px into a horizontal scroll and clipped the
   Actions header, and 16rem cost a third line of buttons. It is a floor, not a
   width: `.table-responsive` still scrolls when the window is genuinely too
   narrow, which is what it is for. */
td.cims-row-actions {
    white-space: normal;
    min-width: 16.25rem;
}

/* A flex row INSIDE the cell, not margins on the cell's children.
   Margins were tried both ways and neither works: left-only let the last button
   on a line sit flush against the edge and put the table 11px into a horizontal
   scroll, and margins on both sides cost enough width to push the buttons onto
   a third line. A gap is exactly the space between, and none at the ends.
   The flex container is a DIV, not the `td` — a table cell told to be a flex
   container stops being a table cell, and the column alignment goes with it. */
.cims-row-actions-inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: .25rem;
}

/* A label wraps between buttons, never inside one. */
td.cims-row-actions .btn {
    white-space: nowrap;
}


/* ==========================================================================
   RECOVERED LAYER — the stage strip, the estimate summary, the legal sheet,
   the master-data nav, the checkbox select, the tracking strip and the photo
   tiles.
   ==========================================================================
   These rules were lost from this file on 2026-09-20 and restored from the
   compiled bundle `public/build/assets/app-DqU4Gdqy.css`, built 2026-09-19.

   HOW THEY WERE LOST, so it is not done again: an edit script replaced
   "everything from the first occurrence of a marker to the end of the file".
   The marker was a section banner of the form used throughout this file, so
   `str.index` matched an EARLIER banner than the one intended and ~47KB after
   it was replaced by a 25-line block. The damaged file was then published over
   `public/css/app.css` — which is untracked, so the good copy went with it.

   Two rules follow from that:
     - never anchor an edit to a marker whose form repeats in the file;
     - `php artisan assets:publish` OVERWRITES `public/css/*`. Check that the
       published copy is not ahead of source before running it.

   The comments that documented these rules did not survive — the bundle is
   minified, so only the declarations could be recovered. Formatting is
   regenerated, and the range-syntax media queries the bundler emitted
   (`width<=991.98px`) are written back as `max-width`, which every supported
   browser parses.
   ========================================================================== */

tbody:has(>tr:not(.cims-row-placeholder))>.cims-row-placeholder {
    display:none;
}

.cims-master-nav {
    top:calc(var(--cims-navbar-height) + var(--cims-space-3));
    max-height:calc(100vh - var(--cims-navbar-height) - var(--cims-space-6));
    flex-direction:column;
    display:flex;
    position:sticky;
}

.cims-master-nav .card-body {
    min-height:0;
    overflow-y:auto;
}

@media (max-width: 991.98px) {
    .cims-master-nav {
        max-height:18rem;
        position:static;
    }

}

.cims-master-group>summary {
    align-items:center;
    gap:var(--cims-space-2);
    font-size:var(--cims-text-xs);
    letter-spacing:.02em;
    text-transform:uppercase;
    color:var(--cims-color-text-muted-strong,#4d5566);
    cursor:pointer;
    -webkit-user-select:none;
    user-select:none;
    border-radius:.375rem;
    margin-bottom:.125rem;
    padding:.45rem .5rem;
    font-weight:600;
    list-style:none;
    display:flex;
}

.cims-master-group>summary::-webkit-details-marker {
    display:none;
}

.cims-master-group>summary::marker {
    content:"";
}

.cims-master-group>summary:before {
    content:"";
    border-bottom:1.5px solid;
    border-right:1.5px solid;
    flex:none;
    width:.45rem;
    height:.45rem;
    transition:transform .15s;
    transform:rotate(-45deg);
}

.cims-master-group[open]>summary:before {
    transform:rotate(45deg);
}

.cims-master-group>summary:hover {
    background:var(--cims-color-surface-muted,#0000000a);
    color:var(--cims-color-text,inherit);
}

.cims-master-link {
    font-size:var(--cims-text-sm);
    color:var(--cims-color-text,inherit);
    border-left:2px solid #0000;
    border-radius:0 .375rem .375rem 0;
    margin-bottom:.0625rem;
    padding:.35rem .5rem .35rem 1.55rem;
    line-height:1.35;
    text-decoration:none;
    transition:background-color .12s,border-color .12s;
    display:block;
}

.cims-master-link:hover, .cims-master-link:focus-visible {
    background:var(--cims-color-surface-muted,#0000000a);
    color:var(--cims-color-primary,inherit);
    text-decoration:none;
}

.cims-master-link.active {
    background:rgba(var(--cims-color-primary-rgb), .1);
    border-left-color:var(--cims-color-primary);
    color:var(--cims-color-primary);
    font-weight:600;
}

.cims-master-group[hidden] {
    display:none!important;
}

.cims-perm-scroll {
    max-height:26rem;
    overflow-y:auto;
}

.cims-perm-scroll thead th {
    z-index:1;
    background:var(--cims-color-surface);
    box-shadow:inset 0 -1px 0 var(--cims-color-border);
    position:sticky;
    top:0;
}

details[open]>summary>.cims-perm-chevron {
    transform:rotate(90deg);
}

.cims-perm-chevron {
    transition:transform .15s ease-out;
}

.cims-stage-list {
    counter-reset:cims-stage;
}

.cims-stage {
    position:relative;
}

.cims-stage:before, .cims-stage:after {
    content:"";
    background:var(--cims-color-border);
    width:2px;
    position:absolute;
    inset-inline-start:calc(var(--bs-list-group-item-padding-x,1rem) + 1rem - 1px);
}

.cims-stage:before {
    height:calc(50% - 1rem);
    top:0;
}

.cims-stage:after {
    top:calc(50% + 1rem);
    bottom:0;
}

.cims-stage:first-child:before, .cims-stage:last-child:after {
    display:none;
}

.cims-stage.is-done:before, .cims-stage.is-done:after {
    background:var(--cims-color-success);
}

.cims-stage-number {
    z-index:1;
    border:2px solid var(--cims-color-border);
    background:var(--cims-color-surface);
    width:2rem;
    height:2rem;
    color:var(--cims-color-text-muted);
    border-radius:50%;
    flex:none;
    justify-content:center;
    align-items:center;
    font-size:.875rem;
    font-weight:600;
    line-height:1;
    display:inline-flex;
    position:relative;
}

.cims-stage-number.is-done {
    background:var(--cims-color-success);
    border-color:var(--cims-color-success);
    color:#fff;
}

.cims-stage-number.is-current {
    border-color:var(--cims-color-primary);
    color:var(--cims-color-primary);
    box-shadow:0 0 0 3px rgba(var(--cims-color-primary-rgb), .15);
}

.cims-stage.is-current {
    background:rgba(var(--cims-color-primary-rgb), .04);
}

.cims-stage.is-upcoming .cims-stage-number {
    border-style:dashed;
}

.cims-stage-list .min-width-0 {
    min-width:0;
}

.cims-stage-strip {
    scrollbar-width:none;
    -ms-overflow-style:none;
    align-items:stretch;
    gap:0;
    margin:0;
    padding:.75rem 1rem;
    list-style:none;
    display:flex;
    position:relative;
    overflow-x:auto;
}

.cims-stage-strip::-webkit-scrollbar {
    display:none;
}

.cims-stage-strip.is-scrollable {
    -webkit-mask-image:linear-gradient(90deg,#0000 0,#000 1.5rem calc(100% - 1.5rem),#0000 100%);
    mask-image:linear-gradient(90deg,#0000 0,#000 1.5rem calc(100% - 1.5rem),#0000 100%);
}

.cims-stage-strip.is-scrollable.is-at-start {
    -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 1.5rem),#0000 100%);
    mask-image:linear-gradient(90deg,#000 calc(100% - 1.5rem),#0000 100%);
}

.cims-stage-strip.is-scrollable.is-at-end {
    -webkit-mask-image:linear-gradient(90deg,#0000 0,#000 1.5rem);
    mask-image:linear-gradient(90deg,#0000 0,#000 1.5rem);
}

.cims-stage-step {
    flex:8.5rem;
    min-width:6.25rem;
    position:relative;
}

.cims-stage-step:before {
    content:"";
    top:1rem;
    background:var(--cims-color-border);
    width:100%;
    height:2px;
    position:absolute;
    inset-inline-end:50%;
}

.cims-stage-step:first-child:before {
    display:none;
}

.cims-stage-step.is-done:before {
    background:var(--cims-color-success);
}

.cims-stage-step-link {
    z-index:1;
    text-align:center;
    color:inherit;
    border-radius:var(--cims-radius-sm);
    flex-direction:column;
    align-items:center;
    gap:.25rem;
    padding:0 .5rem .25rem;
    text-decoration:none;
    display:flex;
    position:relative;
}

.cims-stage-step-link:hover, .cims-stage-step-link:focus-visible {
    background:rgba(var(--cims-color-primary-rgb), .06);
    color:inherit;
}

.cims-stage-step-label {
    font-size:.8125rem;
    font-weight:600;
    line-height:1.25;
}

.cims-stage-step-meta {
    color:var(--cims-color-text-muted);
    font-size:.75rem;
}

.cims-stage-step.is-open .cims-stage-step-link {
    background:rgba(var(--cims-color-primary-rgb), .1);
}

.cims-stage-step.is-open .cims-stage-step-label {
    color:var(--cims-color-primary);
}

.cims-stage-panel .modal-body>.table-responsive {
    overflow-y:hidden;
}

.cims-stage-panel {
    scroll-margin-block-start:calc(var(--cims-navbar-height) + var(--cims-space-3));
}

.modal-header .cims-stage-strip {
    padding-inline:0;
    padding-bottom:0;
}

.cims-modal-wide {
    max-width:min(96vw,1600px);
}

.cims-rate-summary {
    border:1px solid var(--cims-color-border);
    border-radius:var(--cims-radius-sm);
    background:rgba(var(--cims-color-primary-rgb), .04);
    padding:.75rem 1rem;
}

.cims-rate-summary dd {
    font-variant-numeric:tabular-nums;
}

.cims-estimate-part {
    align-items:start;
    gap:var(--cims-space-2) var(--cims-space-3);
    padding:var(--cims-space-3);
    border:1px solid var(--cims-color-border);
    border-radius:var(--cims-radius-sm);
    background:var(--cims-color-surface);
    grid-template-columns:auto 1fr auto;
    margin-block-end:var(--cims-space-3);
    display:grid;
}

.cims-estimate-part-mark {
    border-radius:var(--cims-radius-pill);
    block-size:1.75rem;
    inline-size:1.75rem;
    font-size:var(--cims-text-sm);
    color:var(--cims-color-primary);
    background:rgba(var(--cims-color-primary-rgb), .12);
    justify-content:center;
    align-items:center;
    font-weight:700;
    display:inline-flex;
}

.cims-estimate-part-title {
    font-size:var(--cims-text-base);
    color:var(--cims-color-text);
    margin:0;
    font-weight:600;
}

.cims-estimate-part-note {
    margin:var(--cims-space-1) 0 0;
    font-size:var(--cims-text-sm);
    color:var(--cims-color-text-muted);
}

.cims-estimate-part-amount {
    font-size:var(--cims-text-md);
    text-align:end;
    color:var(--cims-color-text);
    margin:0;
    font-weight:700;
}

.cims-estimate-part-detail {
    grid-column:2/-1;
    margin-block-start:var(--cims-space-3);
}

.cims-estimate-lines {
    gap:0;
    margin:0;
    display:grid;
}

.cims-estimate-line {
    justify-content:space-between;
    align-items:baseline;
    gap:var(--cims-space-3);
    padding-block:var(--cims-space-2);
    border-block-end:1px solid var(--cims-color-border-subtle,var(--cims-color-border));
    display:flex;
}

.cims-estimate-line:last-child {
    border-block-end:0;
}

.cims-estimate-line>dt {
    color:var(--cims-color-text);
    margin:0;
    font-weight:500;
}

.cims-estimate-line>dd {
    text-align:end;
    white-space:nowrap;
    color:var(--cims-color-text);
    margin:0;
    font-weight:600;
}

.cims-estimate-breakdown {
    gap:var(--cims-space-2) var(--cims-space-4);
    grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
    display:grid;
}

.cims-estimate-grand {
    padding:var(--cims-space-4);
    border:1px solid rgba(var(--cims-color-primary-rgb), .35);
    border-radius:var(--cims-radius-md);
    background:rgba(var(--cims-color-primary-rgb), .06);
}

.cims-estimate-grand-head {
    align-items:center;
    gap:var(--cims-space-3);
    grid-template-columns:auto 1fr auto;
    display:grid;
}

.cims-estimate-part-mark--grand {
    block-size:2.25rem;
    inline-size:2.25rem;
    font-size:var(--cims-text-base);
    background:rgba(var(--cims-color-primary-rgb), .18);
}

.cims-estimate-grand-label {
    font-size:var(--cims-text-md);
    color:var(--cims-color-text);
    font-weight:700;
}

.cims-estimate-grand-amount {
    font-size:var(--cims-text-xl);
    text-align:end;
    color:var(--cims-color-primary);
    margin:0;
    font-weight:700;
    line-height:1.2;
}

.cims-estimate-grand-words {
    margin:var(--cims-space-3) 0 0;
    border-block-start:1px solid rgba(var(--cims-color-primary-rgb), .25);
    font-size:var(--cims-text-sm);
    color:var(--cims-color-text);
    padding-block-start:var(--cims-space-3);
}

@media (max-width: 575.98px) {
    .cims-estimate-part, .cims-estimate-grand-head {
        grid-template-columns:auto 1fr;
    }

    .cims-estimate-part-amount, .cims-estimate-grand-amount {
        text-align:start;
        grid-column:2/-1;
    }

    .cims-estimate-part-detail {
        grid-column:1/-1;
    }

}

.cims-photo-tile img {
    aspect-ratio:4/3;
    object-fit:cover;
    border-radius:var(--bs-border-radius,.375rem);
    border:1px solid var(--bs-border-color);
    background:var(--bs-secondary-bg);
    width:100%;
}

.cims-photo-tile figcaption {
    margin-top:.25rem;
    font-size:.75rem;
    line-height:1.3;
}

.cims-track-strip {
    gap:var(--cims-space-1);
    flex-wrap:wrap;
    display:flex;
}

.cims-track-cell {
    border-radius:var(--cims-radius-xs,.1875rem);
    border:1px solid var(--cims-color-border);
    background:var(--cims-color-surface-muted,var(--bs-secondary-bg));
    block-size:1rem;
    inline-size:1rem;
}

.cims-track-cell--worked {
    border-color:rgba(var(--cims-color-primary-rgb), .55);
    background:rgba(var(--cims-color-primary-rgb), .55);
}

a.cims-track-cell {
    text-decoration:none;
    display:block;
}

a.cims-track-cell:hover, a.cims-track-cell:focus-visible {
    background:var(--cims-color-primary);
    border-color:var(--cims-color-primary);
}

.cims-track-legend {
    align-items:center;
    gap:var(--cims-space-2);
    margin-block-start:var(--cims-space-3);
    display:flex;
}

.cims-track-heading {
    font-size:var(--cims-text-label);
    letter-spacing:var(--cims-tracking-label);
    text-transform:uppercase;
    color:var(--cims-color-text-muted-strong);
    margin-block-end:var(--cims-space-2);
    font-weight:600;
}

.cims-stage-step-next {
    margin-top:var(--cims-space-1,.25rem);
    letter-spacing:.02em;
    font-size:.6875rem;
    font-weight:600;
}

.cims-stage-panel .cims-doc-heading {
    display:none;
}

.cims-checkbox-select {
    position:relative;
}

.cims-checkbox-select__panel {
    z-index:1056;
    background:var(--bs-body-bg);
    border:1px solid var(--bs-border-color);
    border-radius:var(--bs-border-radius);
    margin-top:.25rem;
    padding:.75rem;
    position:absolute;
    left:0;
    right:0;
    box-shadow:0 .5rem 1rem #00000026;
}

.cims-checkbox-select__list {
    max-height:15rem;
    overflow-y:auto;
}

.cims-checkbox-select__chips {
    flex-wrap:wrap;
    gap:.35rem;
    display:flex;
}

.cims-checkbox-select__chip {
    background:var(--bs-secondary-bg);
    border:1px solid var(--bs-border-color);
    border-radius:2rem;
    align-items:center;
    gap:.35rem;
    padding:.15rem .5rem;
    font-size:.8125rem;
    display:inline-flex;
}

.cims-checkbox-select__chip-x {
    color:var(--bs-secondary-color);
    background:0 0;
    border:0;
    padding:0;
    font-size:1rem;
    line-height:1;
}

.cims-checkbox-select__chip-x:hover {
    color:var(--bs-danger);
}

.cims-brand-name {
    white-space:nowrap;
}

.cims-brand-logo {
    object-fit:contain;
}

.cims-powered-by {
    text-align:center;
    color:var(--bs-secondary-color,#6c757d);
    padding:.75rem 1rem 1.25rem;
    font-size:.8125rem;
}

.cims-legal {
    max-width:44rem;
    margin:0 auto;
    padding:1.5rem 1.25rem 3rem;
}

.cims-legal-header {
    border-bottom:1px solid var(--bs-border-color,#dee2e6);
    padding-bottom:1.25rem;
}

.cims-legal-sheet h1 {
    margin-top:2rem;
    margin-bottom:.35rem;
    font-size:1.6rem;
    font-weight:700;
}

.cims-legal-meta {
    color:var(--bs-secondary-color,#6c757d);
    margin-bottom:2rem;
    font-size:.875rem;
}

.cims-legal-footer {
    border-top:1px solid var(--bs-border-color,#dee2e6);
    color:var(--bs-secondary-color,#6c757d);
    flex-wrap:wrap;
    justify-content:space-between;
    align-items:center;
    gap:.75rem 1.25rem;
    margin-top:2.5rem;
    padding-top:1.25rem;
    font-size:.875rem;
    display:flex;
}

.cims-legal-body {
    line-height:1.7;
}

.cims-legal-body h2 {
    margin-top:1.75rem;
    margin-bottom:.6rem;
    font-size:1.15rem;
    font-weight:600;
}

.cims-legal-body h3 {
    margin-top:1.25rem;
    margin-bottom:.5rem;
    font-size:1rem;
    font-weight:600;
}

.cims-legal-body p, .cims-legal-body ul, .cims-legal-body ol {
    margin-bottom:1rem;
}

.cims-legal-body li {
    margin-bottom:.35rem;
}

.cims-legal-body hr {
    margin:1.75rem 0;
}

.cims-powered-by a {
    color:inherit;
    text-decoration:underline;
}



/* --- Linked documents ------------------------------------------------------
   The cards in the "Raised against this estimate" dialog, and the icon tile
   the Approvals page reuses for its stage headings.

   Written on 2026-09-20 and destroyed the same day by the slice described in
   the RECOVERED LAYER note above — these rules were inside it. They could not
   come back with the rest: the bundle the others were recovered from was built
   the day before they existed. Rewritten from scratch.

   The dialog was a flat list before this: a number, a status badge and a
   chevron so faint the client reported the link as missing. It was there, as a
   grey glyph at the far edge of a row that gave no other sign of being a link.
   A card each, with the type tinted, the number set large, and an arrow that is
   a real target. */
.cims-linked-doc {
    display: block;
    text-decoration: none;
    color: inherit;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--cims-radius-md, .5rem);
    background: var(--bs-body-bg);
    transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}

/* The whole card is the link, so nothing inside it is underlined — the card
   itself is the affordance. Without this the browser's own link styling turned
   every card into a stack of blue underlined text. */
.cims-linked-doc,
.cims-linked-doc:hover,
.cims-linked-doc:focus-visible {
    text-decoration: none;
}

.cims-linked-doc:hover,
.cims-linked-doc:focus-visible {
    border-color: var(--bs-primary-border-subtle, #9ec5fe);
    box-shadow: 0 .35rem .9rem rgba(0, 0, 0, .09);
    transform: translateY(-1px);
    color: inherit;
}

/* The tinted square. Sized in rem so it holds its proportion against the code
   beside it at any root size. */
.cims-linked-doc-icon {
    flex: 0 0 auto;
    width: 2.5rem;
    height: 2.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: .55rem;
    font-size: 1.15rem;
}

.cims-linked-doc-label {
    font-size: var(--cims-text-xs, .75rem);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--cims-color-text-muted-strong, #4d5566);
}

.cims-linked-doc-code {
    font-weight: 600;
    font-size: 1rem;
    font-variant-numeric: tabular-nums;
    color: var(--cims-color-text);
    line-height: 1.2;
}

/* The affordance the flat list was missing. Bordered so it reads as a target
   when the card is at rest, filled once the card is under the pointer. */
.cims-linked-doc-go {
    flex: 0 0 auto;
    width: 2rem;
    height: 2rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--bs-border-color);
    border-radius: 50%;
    color: var(--cims-color-text-muted-strong, #4d5566);
    transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}

.cims-linked-doc:hover .cims-linked-doc-go,
.cims-linked-doc:focus-visible .cims-linked-doc-go {
    background: var(--bs-primary, #0d6efd);
    border-color: var(--bs-primary, #0d6efd);
    color: #fff;
}


/* --- Scrollable modals with a form wrapper ---------------------------------
   `modal-dialog-scrollable` pins the header and footer and scrolls only the
   field area. It did neither here, on any of the forty-odd `x-crud-modal`
   screens, and the taller the form the worse it read: the Organizations edit
   dialog ran 1202px of fields inside an 894px dialog, so the last fields and
   the whole footer — Cancel and Save — sat below the fold with nothing to
   scroll. Reported from the running app.

   Bootstrap's layout assumes `.modal-content > .modal-body`. It sets
   `overflow: hidden` on the content and lets the body flex to fill what is
   left. `x-crud-modal` puts the <form> in between, because the header, body and
   footer all have to post together — and a plain block <form> is not a flex
   container, so the body inside it has nothing to size against and simply grows
   to its content. The content clips it, and the footer with it.

   The fix is to let the form be the flex column Bootstrap expected the content
   to be. `min-height: 0` on both is what actually permits the shrink: a flex
   item's default `min-height: auto` refuses to go below its content, which is
   the reason an overflow container inside a flex column so often does nothing. */
.modal-dialog-scrollable .modal-content > form {
    display: flex;
    flex-direction: column;
    min-height: 0;
    max-height: 100%;
    overflow: hidden;
}

.modal-dialog-scrollable .modal-content > form > .modal-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

.modal-dialog-scrollable .modal-content > form > .modal-header,
.modal-dialog-scrollable .modal-content > form > .modal-footer {
    flex: 0 0 auto;
}

/* ==========================================================================
   `hidden` beats the display utilities.

   Bootstrap's `.d-flex`, `.d-block` and friends are `!important`, so an
   element written as `<div class="alert d-flex" hidden>` stays VISIBLE —
   the attribute's `display:none` is a UA rule and loses outright. Every
   dialog that fills a box from JS and hides it when empty depends on this
   line; without it the shared forward dialog showed its warning box on
   documents that had no warning.

   Standard in Normalize and in Bootstrap's own reboot; it was in the build
   this file was rebuilt from and was lost on the way in.
   ========================================================================== */
[hidden] {
    display: none !important;
}

/* ==========================================================================
   The navbar bell, its dropdown, and the pulse.

   The pulse means ONE thing: a document is sitting with you. It is not driven
   by unread notifications — an animation that fires because somebody approved
   something you raised is an animation people learn to ignore, and then it
   cannot tell them the thing it exists to tell them.
   ========================================================================== */
/* The unread count.

   Four things were wrong with it and all four showed in a screenshot:

     - A WHITE RING. `box-shadow: 0 0 0 2px var(--cims-color-surface)` is there
       to separate the badge from whatever it sits on, and that token is
       #ffffff — the colour of a CARD. This badge sits on the navbar, which is
       near-black, so the ring drew a bright white outline around it.

     - UNREADABLE TEXT. White on the pale accent measured 3.65:1, under the 4.5
       this size of text needs.

     - NOT ROUND, AND TOO SMALL FOR ITS CONTENT. 18x16 is an oval, and "99+"
       spilled out past the edge of it.

     - FLOATING OFF THE BUTTON, overhanging the top by 9px, which put it level
       with the edge of the bar.

   `inline-block`, not `inline-flex`. As a flex container the text becomes an
   anonymous flex item, and flex items shrink by default — so the box stayed at
   its 18px minimum however long the label got, and three characters rendered
   outside the circle. An inline-block grows with its content, and `line-height`
   equal to the height centres a single line without needing flex at all. */
.cims-bell-count {
    /* `!important` on the two that place the box.

       Without them the badge was laid out as a FLEX ITEM of
       `.cims-navbar-icon-btn`, which is a 36px `inline-flex` box — so the
       count sat in the button's flow and, because flex items shrink, stayed
       at its 18px minimum however long the label got. "99+" then rendered
       outside its own circle. Taking it out of flow is what makes the badge
       a badge rather than a second icon competing with the bell. */
    position: absolute !important;
    top: 1px;
    right: -2px;

    display: inline-block !important;
    box-sizing: border-box;
    text-align: center;

    /* Height and line-height match, so the digit sits centred. min-width makes
       one and two digits a circle; the box widens past it on its own for
       "99+". */
    height: 1.125rem;
    line-height: 1.125rem;
    min-width: 1.125rem;
    padding: 0 0.3rem !important;

    font-size: 0.625rem;
    font-weight: 700;

    /* Its own colours, not a semantic variant's. The count keeps the brand
       colour so the bell and its number read as one object, and takes the dark
       ink because the light variant is pale by design — the same rule that
       chooses every button label in the product. */
    background: var(--cims-color-primary-light);
    color: var(--cims-color-primary-light-ink);
    /* The bar it actually sits on, not a card. */
    box-shadow: 0 0 0 2px var(--cims-sidebar-bg);
}

/* THE BELL ITSELF CARRIES THE SIGNAL — filled, in the installation's colour,
   and breathing.

   Two earlier attempts were not visible enough and the reason is worth keeping.
   The first drew a ring ORBITING the button, in a fixed amber that belonged to
   no theme: the eye went to the space around the icon, not the icon. The second
   tinted the OUTLINE glyph — a one-pixel stroke changing colour is almost
   nothing to see on a dark navbar, however bright the colour is.

   What reads at a glance is a filled SHAPE that changes size. So the outline
   bell is swapped for the solid one and the whole glyph pulses: bigger, smaller,
   with a soft halo of the same colour behind it.

   The colour is `--cims-color-primary`, which `partials/branding.blade.php`
   overrides per organisation and per theme — set a brand colour in Settings and
   this follows it, with no second place to update. */
.cims-bell-pulse .bi-bell::before {
    /* `bi-bell-fill`. Swapped here rather than by adding a second `<i>` to the
       markup, so the alerting state is entirely a class on the button and
       nothing in the DOM has to be kept in step with it. */
    content: "\f189";
}

/* `--cims-color-primary-light`, NOT `--cims-color-primary`.

   The navbar is dark in both themes, and the brand primary measured 2.75:1
   against it — which is why this still read as "not visible" after two passes.
   The lifted variant of the same hue measures 5.09:1 and is already defined for
   exactly this purpose; a comment forty lines up this file says the dark navbar
   swallows the primary, which was the clue. Branding overrides both tokens
   together, so a custom brand colour still carries through. */
.cims-bell-pulse .bi {
    color: var(--cims-color-primary-light);
    animation: cims-bell-blink 1.4s ease-in-out infinite;
    transform-origin: 50% 55%;
    /* The glyph is small; a filled one carries more weight at a size the navbar
       can afford than a larger outline would. */
    font-size: 1.15rem;
}

@keyframes cims-bell-blink {
    0%, 100% {
        transform: scale(1);
        opacity: 1;
        filter: drop-shadow(0 0 0 rgba(var(--cims-color-primary-rgb), 0));
    }
    /* Bigger, and lit. */
    35% {
        transform: scale(1.3);
        opacity: 1;
        filter: drop-shadow(0 0 8px rgba(var(--cims-color-primary-rgb), 1));
    }
    /* Smaller and dimmer — the dip is what makes it read as a blink rather than
       a wobble. It never goes out: a glyph that disappears entirely reads as a
       rendering fault on a slow frame. */
    70% {
        transform: scale(.88);
        opacity: .5;
        filter: drop-shadow(0 0 2px rgba(var(--cims-color-primary-rgb), .35));
    }
}

/* `.cims-bell-count` now carries its own background, ink and separator (see
   above). The `.bg-danger` override that used to live here is gone with the
   class it was undoing. */

/* Movement is a barrier for some readers and a migraine trigger for others, and
   this one repeats forever. The SIGNAL stays — the bell is still filled, still
   in the brand colour, still haloed — it simply stops moving. */
@media (prefers-reduced-motion: reduce) {
    .cims-bell-pulse .bi {
        animation: none;
        transform: scale(1.18);
        filter: drop-shadow(0 0 7px rgba(var(--cims-color-primary-rgb), .9));
    }
}

.cims-bell-menu {
    width: min(23rem, calc(100vw - 1.5rem));
    padding: 0;
    overflow: hidden;
}

.cims-bell-head {
    padding: 0.75rem 0.875rem;
    border-bottom: 1px solid var(--cims-color-border);
}

.cims-bell-who {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    min-width: 0;
}

/* Initials, not a photo. There is no avatar in this system and a generic
   person glyph on every row tells the reader nothing; two letters at least
   distinguish one account from another at a glance. */
.cims-bell-avatar {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--cims-color-primary);
    background: rgba(var(--cims-color-primary-rgb), 0.12);
}

.cims-bell-name {
    display: block;
    font-size: 0.8125rem;
    font-weight: 650;
    line-height: 1.2;
}

.cims-bell-role {
    display: block;
    font-size: 0.6875rem;
    color: var(--cims-color-text-muted);
    line-height: 1.3;
    margin-top: 0.05rem;
}

/* The "waiting for you" band. Its own colour field, because it is the one
   line in this menu that is a call to act rather than a record of something
   that already happened. */
.cims-bell-tasks-band {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.55rem 0.875rem;
    font-size: 0.75rem;
    font-weight: 650;
    text-decoration: none;
    color: var(--cims-color-warning);
    background: rgba(var(--cims-color-warning-rgb), 0.10);
    border-bottom: 1px solid var(--cims-color-border);
}

.cims-bell-tasks-band:hover {
    background: rgba(var(--cims-color-warning-rgb), 0.16);
    color: var(--cims-color-warning);
}

.cims-bell-section {
    padding: 0.55rem 0.875rem 0.3rem;
    font-size: 0.625rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--cims-color-text-muted);
}

.cims-bell-foot {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0.875rem;
    border-top: 1px solid var(--cims-color-border);
    background: var(--cims-color-bg);
}

.cims-bell-foot-link {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    font-size: 0.75rem;
    font-weight: 550;
    color: var(--cims-color-text-muted);
    text-decoration: none;
    background: none;
    border: 0;
    padding: 0;
}

.cims-bell-foot-link:hover { color: var(--cims-color-primary); }

/* The event mark on each row — scannable by shape before it is read. */
/* Restored with the grid, not replaced by it.

   Rewriting this block dropped `color: inherit` and `text-decoration: none`
   along with the old layout, and every notification row came back as a blue
   underlined link — the default anchor styling, which is exactly the "static
   website" look this pass exists to remove. Caught in the browser; the
   stylesheet compiled fine and the markup never changed. */
[data-bell-list] {
    max-height: 22rem;
    overflow-y: auto;
}

.cims-bell-empty {
    padding: 1.5rem 0.875rem;
    text-align: center;
}

.cims-bell-item {
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: 0.55rem;
    align-items: start;
    padding: 0.55rem 0.875rem;
    border-bottom: 1px solid var(--cims-color-border);
    color: inherit;
    text-decoration: none;
}

.cims-bell-item:last-child { border-bottom: 0; }

.cims-bell-item:hover,
.cims-bell-item:focus-visible {
    background: var(--cims-color-bg);
    color: inherit;
}

.cims-bell-item.is-unread {
    background: rgba(var(--cims-color-primary-rgb), 0.06);
}

.cims-bell-item-text {
    display: block;
    font-size: 0.8125rem;
    line-height: 1.35;
    color: var(--cims-color-text);
}

.cims-bell-item-meta {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.6875rem;
    color: var(--cims-color-text-muted);
}

.cims-bell-item-mark {
    grid-row: 1 / span 3;
    font-size: 0.9375rem;
    line-height: 1.3;
    margin-top: 0.1rem;
}

.cims-bell-item-mark.is-good { color: var(--cims-color-success); }
.cims-bell-item-mark.is-bad  { color: var(--cims-color-danger); }
.cims-bell-item-mark.is-warn { color: var(--cims-color-warning); }
.cims-bell-item-mark.is-info { color: var(--cims-color-primary); }

.cims-bell-item-flag {
    display: inline-block;
    margin-top: 0.3rem;
    padding: 0.05rem 0.4rem;
    border-radius: 999px;
    font-size: 0.625rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    background: var(--cims-color-warning);
    /* The surface colour, not a fixed dark: `--cims-color-warning` is a dark
       brown in the light theme and amber in the dark one, so a single literal
       is unreadable against one of them. */
    color: var(--cims-color-surface);
}

/* The unread row on the notifications page — the same tint the bell dropdown
   uses, so "unread" looks the same in both places. */
.cims-notification-unread {
    background: rgba(var(--cims-color-primary-rgb), 0.06);
}

/* ==========================================================================
   INSIGHTS — the four category dashboards.

   One stylesheet for four boards drawn by one renderer. Every colour here is
   a token, so the whole thing follows the theme rather than freezing the
   light palette into a second design system.

   The layout rule throughout: eight figures, then charts, then detail. A
   reader should be able to answer the board's question from the top strip
   alone and only scroll if the answer was interesting.
   ========================================================================== */

/* --- the hub --------------------------------------------------------------- */
.cims-hub-head {
    margin-bottom: 1.5rem;
}

.cims-hub-title {
    font-size: 1.75rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    margin: 0 0 0.25rem;
}

.cims-hub-sub {
    color: var(--cims-color-text-muted);
    margin: 0;
}

.cims-hub-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
    gap: 1rem;
}

.cims-hub-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.875rem;
    padding: 1.25rem 1.25rem 1rem;
    background: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: 0.75rem;
    color: inherit;
    text-decoration: none;
    overflow: hidden;
    transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}

/* The accent stripe is the only thing that differs between the four cards.
   Four different card colours would make the hub a colour chart; one stripe
   makes them a set. */
.cims-hub-card::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 4px;
    background: var(--cims-hub-accent, var(--cims-color-primary));
}

.cims-hub-card--primary { --cims-hub-accent: var(--cims-color-primary); }
.cims-hub-card--success { --cims-hub-accent: var(--cims-color-success); }
.cims-hub-card--warning { --cims-hub-accent: var(--cims-color-warning); }
.cims-hub-card--info    { --cims-hub-accent: var(--cims-color-info); }

.cims-hub-card:hover,
.cims-hub-card:focus-visible {
    transform: translateY(-2px);
    border-color: var(--cims-hub-accent);
    box-shadow: 0 8px 24px rgba(var(--cims-color-text-muted-rgb), 0.16);
    color: inherit;
}

.cims-hub-num {
    position: absolute;
    top: 0.75rem;
    right: 1rem;
    font-size: 2.75rem;
    font-weight: 800;
    line-height: 1;
    color: var(--cims-hub-accent);
    opacity: 0.12;
    pointer-events: none;
}

.cims-hub-card-head {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
}

.cims-hub-icon {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 0.625rem;
    font-size: 1.125rem;
    color: var(--cims-hub-accent);
    background: color-mix(in srgb, var(--cims-hub-accent) 12%, transparent);
}

.cims-hub-card-title {
    display: block;
    font-weight: 650;
    font-size: 1rem;
    line-height: 1.3;
}

.cims-hub-card-question {
    display: block;
    font-size: 0.8125rem;
    color: var(--cims-color-text-muted);
    margin-top: 0.15rem;
}

.cims-hub-blurb {
    font-size: 0.8125rem;
    color: var(--cims-color-text-muted);
    line-height: 1.45;
}

.cims-hub-figures {
    display: flex;
    gap: 1.25rem;
    flex-wrap: wrap;
    padding-top: 0.75rem;
    border-top: 1px solid var(--cims-color-border);
    margin-top: auto;
}

.cims-hub-figure-value {
    display: block;
    font-size: 1.125rem;
    font-weight: 700;
    line-height: 1.2;
    /* Tabular figures so the three numbers on a card line up with the three on
       the card beside it instead of drifting with their digit widths. */
    font-variant-numeric: tabular-nums;
}

.cims-hub-figure-label {
    display: block;
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--cims-color-text-muted);
    margin-top: 0.1rem;
}

.cims-hub-go {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--cims-hub-accent);
}

/* --- the category switcher -------------------------------------------------- */
.cims-board-switch {
    display: flex;
    gap: 0.25rem;
    margin-bottom: 1.25rem;
    padding-bottom: 0.25rem;
    border-bottom: 1px solid var(--cims-color-border);
    overflow-x: auto;
    /* The four never wrap onto a second line on a phone; they scroll, which
       keeps the current one findable in the same place it always is. */
    scrollbar-width: none;
}

.cims-board-switch::-webkit-scrollbar { display: none; }

.cims-board-switch-item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.5rem 0.875rem;
    border-radius: 0.5rem 0.5rem 0 0;
    font-size: 0.8125rem;
    font-weight: 550;
    white-space: nowrap;
    color: var(--cims-color-text-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -0.3125rem;
}

.cims-board-switch-item:hover {
    color: var(--cims-color-text);
    background: var(--cims-color-bg);
}

.cims-board-switch-item.is-current {
    color: var(--cims-color-primary);
    border-bottom-color: var(--cims-color-primary);
    background: transparent;
}

/* --- board header ----------------------------------------------------------- */
.cims-board-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}

.cims-board-head-main {
    display: flex;
    align-items: center;
    gap: 0.875rem;
}

.cims-board-icon {
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 0.75rem;
    font-size: 1.25rem;
}

.cims-board-icon--primary { color: var(--cims-color-primary); background: rgba(var(--cims-color-primary-rgb), 0.12); }
.cims-board-icon--success { color: var(--cims-color-success); background: rgba(var(--cims-color-success-rgb), 0.12); }
.cims-board-icon--warning { color: var(--cims-color-warning); background: rgba(var(--cims-color-warning-rgb), 0.12); }
.cims-board-icon--info    { color: var(--cims-color-info);    background: rgba(var(--cims-color-info-rgb), 0.12); }

.cims-board-title {
    font-size: 1.375rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    margin: 0;
}

.cims-board-question {
    margin: 0.1rem 0 0;
    font-size: 0.875rem;
    color: var(--cims-color-text-muted);
}

.cims-board-period {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.cims-board-period-label {
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
    color: var(--cims-color-text-muted);
    margin: 0 0.25rem 0 0;
}

.cims-board-period-sep { color: var(--cims-color-text-muted); }
.cims-board-period input[type="date"] { width: 9.5rem; }

/* --- KPI strip --------------------------------------------------------------- */
.cims-kpi-grid {
    display: grid;
    /* FOUR across, explicitly — not `auto-fit`.
       `auto-fit` packed as many as would fit, which on a wide laptop meant
       five in the first row and three in the second: a ragged block where the
       eye expects two even rows of four, and the 5th tile (the least
       important of the top row) sitting where the most important of the
       second row should be. Eight figures are contracted; the grid can be
       told so. */
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.75rem;
}

@media (max-width: 75rem) {
    .cims-kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 36rem) {
    .cims-kpi-grid { grid-template-columns: minmax(0, 1fr); }
}

.cims-kpi {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding: 0.875rem 1rem;
    background: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-left: 3px solid var(--cims-kpi-accent, var(--cims-color-border));
    border-radius: 0.625rem;
    color: inherit;
    text-decoration: none;
    min-width: 0;
}

a.cims-kpi:hover,
a.cims-kpi:focus-visible {
    border-color: var(--cims-kpi-accent, var(--cims-color-primary));
    border-left-color: var(--cims-kpi-accent, var(--cims-color-primary));
    box-shadow: 0 4px 14px rgba(var(--cims-color-text-muted-rgb), 0.12);
    color: inherit;
}

/* Tone is the LEFT EDGE only.

   A tinted card background makes a wall of eight tiles read as an alarm
   panel; a 3px edge is enough to find the one that needs attention and quiet
   enough that seven neutral tiles stay readable. */
.cims-kpi--good    { --cims-kpi-accent: var(--cims-color-success); }
.cims-kpi--warn    { --cims-kpi-accent: var(--cims-color-warning); }
.cims-kpi--bad     { --cims-kpi-accent: var(--cims-color-danger); }
.cims-kpi--neutral { --cims-kpi-accent: var(--cims-color-border); }

.cims-kpi-top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.5rem;
}

.cims-kpi-name {
    font-size: 0.6875rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.045em;
    color: var(--cims-color-text-muted);
    line-height: 1.3;
}

.cims-kpi-icon {
    flex: 0 0 auto;
    font-size: 0.9375rem;
    color: var(--cims-kpi-accent, var(--cims-color-text-muted));
    opacity: 0.85;
}

/* The drill-through chevron, on tiles that actually link somewhere.

   `_kpi-grid.blade.php` renders it only when the KPI has an `href`, so its
   presence already means "this goes somewhere" — but with no rule behind the
   class it sat there permanently, which turns a hover affordance into eight
   pieces of permanent clutter.

   Revealed on focus as well as hover: an affordance only a mouse can find is
   not an affordance. */
.cims-kpi-go {
    flex: 0 0 auto;
    font-size: var(--cims-text-xs, .75rem);
    color: var(--cims-color-primary);
    opacity: 0;
    transform: translateX(-2px);
    transition: opacity .13s ease, transform .13s ease;
}

a.cims-kpi:hover .cims-kpi-go,
a.cims-kpi:focus-visible .cims-kpi-go {
    opacity: 1;
    transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
    .cims-kpi-go {
        transition: none;
    }
}

.cims-kpi-figure {
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    /* A long money figure shrinks rather than breaking the grid. */
    overflow-wrap: anywhere;
}

.cims-kpi-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem;
    font-size: 0.75rem;
    color: var(--cims-color-text-muted);
    line-height: 1.35;
}

.cims-kpi-delta {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
    font-weight: 600;
}

/* Coloured by the service's VERDICT, not by the sign of the number — a rise
   can be the bad news (committed cost against budget) and a fall can be the
   good news (outstanding coming down). */
.cims-kpi-delta--good { color: var(--cims-color-success); }
.cims-kpi-delta--warn { color: var(--cims-color-warning); }
.cims-kpi-delta--bad  { color: var(--cims-color-danger); }
.cims-kpi-delta--flat { color: var(--cims-color-text-muted); }

/* --- charts ------------------------------------------------------------------ */
.cims-chart-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75rem;
}

@media (max-width: 62rem) {
    .cims-chart-grid { grid-template-columns: minmax(0, 1fr); }
}

.cims-chart-card {
    display: flex;
    flex-direction: column;
    background: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: 0.625rem;
    padding: 1rem 1.125rem 1.125rem;
    min-width: 0;
}

.cims-chart-card--full { grid-column: 1 / -1; }

.cims-chart-head { margin-bottom: 0.75rem; }

.cims-chart-title {
    font-size: 0.9375rem;
    font-weight: 650;
    margin: 0;
    line-height: 1.3;
}

.cims-chart-hint {
    margin: 0.2rem 0 0;
    font-size: 0.75rem;
    color: var(--cims-color-text-muted);
    line-height: 1.4;
}

.cims-chart-body {
    position: relative;
    /* A fixed height, because Chart.js with maintainAspectRatio:false will
       otherwise grow to whatever the grid gives it and the two charts in a row
       end up different heights. */
    height: 16rem;
}

.cims-chart-card--full .cims-chart-body { height: 18rem; }

.cims-chart-canvas { max-width: 100%; }

.cims-chart-body--gauge { height: 14rem; }

.cims-gauge-centre {
    position: absolute;
    inset: 0;
    display: grid;
    place-content: center;
    text-align: center;
    pointer-events: none;
}

.cims-gauge-value {
    display: block;
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.cims-gauge-label {
    display: block;
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--cims-color-text-muted);
    margin-top: 0.25rem;
}

/* An empty chart says so. An empty axis grid reads as "still loading" or
   "broken", and on a young installation most boards have at least one. */
.cims-chart-empty {
    display: grid;
    place-content: center;
    justify-items: center;
    gap: 0.5rem;
    height: 16rem;
    text-align: center;
    color: var(--cims-color-text-muted);
    background: var(--cims-color-bg);
    border-radius: 0.5rem;
    border: 1px dashed var(--cims-color-border);
}

.cims-chart-empty i { font-size: 1.5rem; opacity: 0.55; }
.cims-chart-empty p { margin: 0; font-size: 0.8125rem; max-width: 22rem; }

/* --- detail tables ------------------------------------------------------------ */
.cims-muted-count {
    font-size: 0.75rem;
    color: var(--cims-color-text-muted);
}

.cims-data-table th {
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--cims-color-text-muted);
    font-weight: 600;
    white-space: nowrap;
}

.cims-data-table td {
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
}

.cims-cell--good { color: var(--cims-color-success); font-weight: 600; }
.cims-cell--warn { color: var(--cims-color-warning); font-weight: 600; }
.cims-cell--bad  { color: var(--cims-color-danger);  font-weight: 600; }

/* A percentage worth comparing down a column, drawn as a bar behind the
   number rather than beside it — no extra column, and the eye reads the
   column as a shape. */
.cims-cell-bar-wrap {
    position: relative;
    display: inline-block;
    min-width: 4.5rem;
    padding: 0.1rem 0.35rem;
    border-radius: 0.25rem;
    overflow: hidden;
    background: var(--cims-color-bg);
}

.cims-cell-bar {
    position: absolute;
    inset: 0 auto 0 0;
    width: var(--v, 0%);
    background: rgba(var(--cims-color-primary-rgb), 0.22);
}

.cims-cell-bar-text {
    position: relative;
    font-weight: 600;
}

/* RAG dot. Colour is never the only carrier — every dot has a `title` and an
   aria-label naming the state and the reason, because a red/green matrix is
   unusable to a colour-blind reader otherwise. */
.cims-rag {
    display: inline-block;
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    border: 1px solid rgba(0, 0, 0, 0.12);
    cursor: help;
}

.cims-rag--green { background: var(--cims-color-success); }
.cims-rag--amber { background: var(--cims-color-warning); }
.cims-rag--red   { background: var(--cims-color-danger); }
.cims-rag--grey  { background: var(--cims-color-border); }

@media print {
    .cims-board-switch,
    .cims-board-period { display: none !important; }
    .cims-chart-card,
    .cims-kpi { break-inside: avoid; }
}

/* ==========================================================================
   The Insights strip on the landing dashboard.

   The four boards live at /insights. This is the door to them from the screen
   everybody actually opens — a link inside a collapsed sidebar group is a link
   nobody finds, which is exactly how somebody already running this code
   reported that "nothing was added to the dashboard".

   Deliberately flatter and quieter than the hub's own cards: this is a way
   through, not the destination.
   ========================================================================== */
.cims-insight-strip {
    background: var(--cims-color-surface);
    border: 1px solid var(--cims-color-border);
    border-radius: 0.75rem;
    padding: 0.875rem 1rem 1rem;
}

.cims-insight-strip-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
}

.cims-insight-strip-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.625rem;
}

@media (max-width: 62rem) {
    .cims-insight-strip-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 36rem) {
    .cims-insight-strip-grid { grid-template-columns: minmax(0, 1fr); }
}

.cims-insight-tile {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    padding: 0.75rem 0.875rem 0.75rem 0.875rem;
    border: 1px solid var(--cims-color-border);
    border-left: 3px solid var(--cims-tile-accent, var(--cims-color-primary));
    border-radius: 0.5rem;
    background: var(--cims-color-bg);
    color: inherit;
    text-decoration: none;
    overflow: hidden;
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.cims-insight-tile--primary { --cims-tile-accent: var(--cims-color-primary); }
.cims-insight-tile--success { --cims-tile-accent: var(--cims-color-success); }
.cims-insight-tile--warning { --cims-tile-accent: var(--cims-color-warning); }
.cims-insight-tile--info    { --cims-tile-accent: var(--cims-color-info); }

.cims-insight-tile:hover,
.cims-insight-tile:focus-visible {
    transform: translateY(-1px);
    box-shadow: 0 6px 16px rgba(var(--cims-color-text-muted-rgb), 0.14);
    color: inherit;
}

.cims-insight-tile-num {
    position: absolute;
    top: 0.3rem;
    right: 0.6rem;
    font-size: 1.75rem;
    font-weight: 800;
    line-height: 1;
    color: var(--cims-tile-accent);
    opacity: 0.13;
    pointer-events: none;
}

.cims-insight-tile-icon {
    color: var(--cims-tile-accent);
    font-size: 1rem;
    line-height: 1;
}

.cims-insight-tile-title {
    font-size: 0.8125rem;
    font-weight: 650;
    line-height: 1.25;
}

.cims-insight-tile-question {
    font-size: 0.6875rem;
    color: var(--cims-color-text-muted);
    line-height: 1.3;
}

/* ==========================================================================
   PRODUCT POLISH

   Applied at the design-system level, not per screen. This file already
   defines a good token set — radius, three elevation levels, a spacing scale
   — and the complaint was never that the tokens were wrong. It was that the
   application of them stopped at "Bootstrap with a new primary colour", so
   every screen read as a template: flat cards, default focus rings, default
   button weights, table headers that look like body text.

   Everything below targets the shared primitives every one of the 175 views
   already uses, so the whole product moves at once and no view is edited.
   Appended last, so it wins the cascade over the Bootstrap rules above it.
   ========================================================================== */

/* --- typography ------------------------------------------------------------ */
h1, .h1, h2, .h2, h3, .h3 {
    /* Tighter tracking at display sizes. Interface headings set at default
       tracking read as body text scaled up rather than as headings. */
    letter-spacing: -0.02em;
    font-weight: 700;
}

h4, .h4, h5, .h5, h6, .h6 {
    letter-spacing: -0.01em;
    font-weight: 650;
}

/* --- cards ------------------------------------------------------------------ */
.card {
    border-radius: var(--cims-radius-md);
    border-color: var(--cims-color-border);
    box-shadow: var(--cims-shadow-sm);
}

.card-header {
    padding: 0.75rem 1rem;
    font-size: 0.8125rem;
    font-weight: 650;
    letter-spacing: 0.01em;
    background: transparent;
    border-bottom: 1px solid var(--cims-color-border);
}

/* A card the whole of which is a link should say so on hover. Scoped to
   anchors so static cards do not move under the pointer. */
a.card:hover,
a.card:focus-visible {
    box-shadow: var(--cims-shadow-md);
    border-color: var(--cims-color-primary);
}

/* --- buttons ---------------------------------------------------------------- */
.btn {
    --bs-btn-font-weight: 550;
    border-radius: var(--cims-radius-sm);
    letter-spacing: 0.005em;
    transition: background-color 0.15s ease, border-color 0.15s ease,
                box-shadow 0.15s ease, transform 0.08s ease;
}

.btn:active { transform: translateY(0.5px); }

.btn-primary {
    box-shadow: 0 1px 2px rgba(var(--cims-color-primary-rgb), 0.24);
}

.btn-primary:hover {
    box-shadow: 0 2px 8px rgba(var(--cims-color-primary-rgb), 0.32);
}

.btn-sm { border-radius: var(--cims-radius-xs); }

/* ONE focus ring for the whole product.

   Bootstrap's default is a 4px translucent blob in its own blue, which on a
   re-themed product is both the wrong colour and the wrong shape. This is a
   2px ring with a surface-coloured gap, so it reads on any background
   including inside a coloured row — and it is the same ring on a button, an
   input, a select and a link, which is what makes keyboard navigation
   learnable. */
.btn:focus-visible,
.form-control:focus,
.form-select:focus,
.form-check-input:focus,
.dropdown-item:focus-visible,
.nav-link:focus-visible,
a:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--cims-color-surface),
                0 0 0 4px rgba(var(--cims-color-primary-rgb), 0.55);
}

/* --- form controls ----------------------------------------------------------- */
.form-control,
.form-select {
    border-color: var(--cims-color-border);
    border-radius: var(--cims-radius-sm);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.form-control:hover:not(:focus),
.form-select:hover:not(:focus) {
    border-color: rgba(var(--cims-color-text-muted-rgb), 0.45);
}

.form-label {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--cims-color-text-muted);
    margin-bottom: 0.3rem;
}

/* --- tables ------------------------------------------------------------------- */
.table > thead > tr > th {
    font-size: 0.6875rem;
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: 0.045em;
    color: var(--cims-color-text-muted);
    background: var(--cims-color-bg);
    border-bottom: 1px solid var(--cims-color-border);
    white-space: nowrap;
}

.table > tbody > tr {
    transition: background-color 0.12s ease;
}

.table-hover > tbody > tr:hover > * {
    background: rgba(var(--cims-color-primary-rgb), 0.045);
}

.table > :not(caption) > * > * {
    border-bottom-color: var(--cims-color-border);
}

/* --- overlays ------------------------------------------------------------------ */
.dropdown-menu {
    border-color: var(--cims-color-border);
    border-radius: var(--cims-radius-md);
    box-shadow: var(--cims-shadow-md);
    padding: 0.3rem;
}

.dropdown-item {
    border-radius: var(--cims-radius-xs);
    font-size: 0.8125rem;
    padding: 0.4rem 0.6rem;
}

.modal-content {
    border: 1px solid var(--cims-color-border);
    border-radius: var(--cims-radius-lg);
    box-shadow: var(--cims-shadow-lg);
}

.modal-header,
.modal-footer {
    border-color: var(--cims-color-border);
}

.modal-header { padding: 1rem 1.25rem; }

/* --- badges and alerts ---------------------------------------------------------- */
.badge {
    font-weight: 600;
    letter-spacing: 0.01em;
    border-radius: var(--cims-radius-pill);
    padding: 0.32em 0.62em;
}

.alert {
    border-radius: var(--cims-radius-md);
    border-width: 1px;
}

/* --- pagination ------------------------------------------------------------------ */
.page-link {
    border-color: var(--cims-color-border);
    color: var(--cims-color-text-muted);
    font-size: 0.8125rem;
}

.page-item.active .page-link {
    background: var(--cims-color-primary);
    border-color: var(--cims-color-primary);
}

/* --- the sidebar's active row ------------------------------------------------------ */
.cims-nav-sub .nav-link {
    border-radius: var(--cims-radius-xs);
    transition: background-color 0.13s ease, color 0.13s ease;
}

/* --- motion ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
    .btn,
    .card,
    .table > tbody > tr,
    .form-control,
    .form-select,
    .cims-nav-sub .nav-link {
        transition: none !important;
    }

    .btn:active { transform: none; }
}

/* --- table density ----------------------------------------------------------------
   "CIMS Platform Super Admin" wrapping onto four lines and "20 Sep 2026" onto
   three made every row a different height, which is what a squeezed table
   looks like — and it reads as unfinished however good the rest of the page
   is. The columns were not too many, they were too narrow.

   So the table gets a sensible minimum and its own container scrolls, which is
   the pattern `.table-responsive` exists for and was already wrapping these
   tables in. Nothing new is introduced and the page itself still never scrolls
   sideways. */
/* COLUMNS SIZE TO THEIR CONTENT, AND THE CONTAINER SCROLLS.

   Widening the table to a fixed minimum was the wrong fix and the numbers said
   so: at 62rem the estimate register still broke "20 Sep 2026" onto three
   lines and the vendor register broke "VEN-003" onto two. The cause is
   `width: 100%` — the table was being made to fit the card whatever that cost
   the columns, and with nine columns it costs every one of them.

   `width: auto` lets each column take the width its content needs, `nowrap`
   stops a value breaking mid-value, and `.table-responsive` — already wrapping
   every one of these tables — scrolls when the total exceeds the card. The
   page itself still never scrolls sideways.

   `!important` because DataTables writes a pixel width inline on the table it
   initialises, and an inline style is not otherwise reachable.

   A DESCENDANT selector, not a child one: DataTables inserts its own
   `div.dt-container` between `.table-responsive` and the table, so
   `.table-responsive > .table` matched nothing on exactly the screens this was
   written for — and the rule looked applied because the plain tables it did
   match were already fine. */
.table-responsive .table {
    width: auto !important;
    min-width: 100%;
}

.table-responsive .table > thead > tr > th,
.table-responsive .table > tbody > tr > td {
    white-space: nowrap;
}

.table > tbody > tr > td {
    vertical-align: middle;
    padding-top: 0.55rem;
    padding-bottom: 0.55rem;
}

/* A date or a person's name should break between words, never inside one, and
   these columns are the ones that were losing that fight. */
.table > tbody > tr > td {
    overflow-wrap: break-word;
    word-break: normal;
}

/* ==========================================================================
   LINKS — an application's links, not a document's.

   Measured before this was written: across nine screens, 487 visible links,
   46 of them underlined and 90 of them in raw link-blue. That is the single
   biggest reason the product reads as a static website — an estimate title in
   a table is not a hyperlink to another document, it is the record's NAME, and
   painting it blue-and-underlined says "this is a web page" on every screen
   at once.

   Three kinds of link, three treatments:

     1. RECORD links — a name or number that opens the thing it names. The
        overwhelming majority. They take the body colour and weight, and the
        brand colour on hover. Clickability is carried by weight, the cursor
        and the hover, which is how every application-grade table does it.

     2. ACTION links — "View all", "Open Insights". Brand colour, never
        underlined; they sit alone and read as controls.

     3. PROSE links — a link inside a sentence. These KEEP their underline,
        deliberately: WCAG 1.4.1 requires something other than colour to
        distinguish a link from the text around it, and inside a paragraph
        there is nothing else to do that job. This is the one place the
        underline is load-bearing rather than decorative.
   ========================================================================== */

/* 1 & 2 — the default for EVERY anchor in the application.

   Deliberately `a`, not `a:not([class])`. The first cut used the latter and it
   missed the commonest shape in this codebase: `<a class="small">Open
   Insights</a>`. One utility class was enough to fall through to Bootstrap's
   blue-and-underlined default, which is precisely the look this block exists
   to remove — and the miss would have been invisible on nine of ten links.

   `a` has specificity 0,0,1, so every class-based component below keeps its
   own colour without needing `!important`: `.btn`, `.dropdown-item`,
   `.nav-link`, `.page-link` and the `cims-*` components all win on their own. */
a {
    color: var(--cims-color-primary);
    text-decoration: none;
}

a:hover {
    color: var(--cims-color-primary-dark);
}

/* 1 — RECORD links: anywhere a link names the row, card or list item it sits
   in. Body colour and weight, brand colour on hover. */
.table a:not(.btn):not(.dropdown-item):not(.page-link),
.list-group-item a:not(.btn):not(.dropdown-item),
.cims-data-table a:not(.btn),
td a:not(.btn):not(.dropdown-item),
.cims-kpi a,
.card-body a:not(.btn):not(.dropdown-item):not(.nav-link):not([class*="cims-"]) {
    color: var(--cims-color-text);
    font-weight: 600;
    text-decoration: none;
    transition: color 0.13s ease;
}

.table a:not(.btn):not(.dropdown-item):not(.page-link):hover,
.list-group-item a:not(.btn):not(.dropdown-item):hover,
.cims-data-table a:not(.btn):hover,
td a:not(.btn):not(.dropdown-item):hover,
.card-body a:not(.btn):not(.dropdown-item):not(.nav-link):not([class*="cims-"]):hover {
    color: var(--cims-color-primary);
    text-decoration: none;
}

/* 2 — a small standalone link in a card header ("View All") reads as a
   control, so it gets the brand colour and a chevron's worth of weight.

   `:not(.btn)` is load-bearing, and its absence was a real defect.

   Half the card headers in this product end in an `<a class="btn btn-primary">`
   — "New", "Add", "Create". This rule's specificity (0,1,1) beats `.btn`'s
   (0,1,0), so it repainted those buttons' labels in the brand colour, on a
   brand-coloured background: contrast 1.00, a solid block with the word
   invisible inside it. It was reported as "button text not visible, in many
   places", and many places is exactly right — it was every card header in the
   application with a button in it.

   Rule 3 below already excludes `.btn` for prose links. This one did not. */
.card-header a:not(.btn),
.cims-card-header-actions a:not(.btn) {
    color: var(--cims-color-primary);
    font-weight: 600;
    text-decoration: none;
}

.card-header a:not(.btn):hover,
.cims-card-header-actions a:not(.btn):hover {
    color: var(--cims-color-primary-dark);
}

/* 3 — PROSE links keep the underline. A link inside running text has no other
   way to announce itself, and colour alone fails WCAG 1.4.1. */
p a:not(.btn):not([class*="cims-"]),
.alert a:not(.btn),
.form-text a,
.cims-page-subtitle a,
footer a {
    text-decoration: underline;
    text-underline-offset: 0.15em;
    text-decoration-thickness: 1px;
    color: var(--cims-color-primary);
    font-weight: inherit;
}

p a:not(.btn):not([class*="cims-"]):hover,
.alert a:not(.btn):hover,
.form-text a:hover,
footer a:hover {
    color: var(--cims-color-primary-dark);
}

/* Anchors that are really buttons must keep the button's own colour — the
   record-link rules above deliberately exclude `.btn`, and this is the belt to
   that pair of braces. */
a.btn {
    text-decoration: none;
}

a.btn:hover {
    text-decoration: none;
}


/* ==========================================================================
   REFINEMENT PASS — type scale, rhythm, elevation, and the two chart forms
   that are not charts.

   Appended after LINKS so it is the last word in the cascade. Nothing here
   renames a class, changes a screen's shape, or introduces a colour that is
   not already a token: it is the same design system, executed harder.

   Three things were wrong and all three were consistency failures rather
   than taste failures:

     1. TYPE had sizes but no scale. Tracking and leading were set per
        component, so a 24px figure and a 12px caption carried the same
        letter-spacing and the large type read as body text enlarged. Figures
        were tabular in eight places and proportional in nine, which is why
        a column of money did not line up with the column beside it.

     2. RHYTHM was ad-hoc rem values. A card header was 16px from the edge
        while the body under it was 20px — a 4px step visible on every screen
        in the product, and invisible to whoever wrote either rule.

     3. ELEVATION had three good tokens applied by mood. A row's action menu
        carried the same shadow as a modal; four components each invented
        their own hover shadow out of a muted-ink rgba, none of which had a
        dark-mode step.
   ========================================================================== */

/* --- 1. TYPE ---------------------------------------------------------------
   The scale is on :root. This applies it to the roles that already exist. */

/* DISPLAY — a board or record title. Largest type on the screen, so the
   tightest tracking and the shortest leading. */
.cims-hub-title,
.cims-board-title {
    font-size: var(--cims-text-xl);
    font-weight: var(--cims-weight-bold);
    letter-spacing: var(--cims-tracking-tight);
    line-height: var(--cims-leading-tight);
}

.cims-record-title {
    letter-spacing: var(--cims-tracking-tight);
    line-height: var(--cims-leading-tight);
}

/* TITLE — a card's or a tile's own name. One step down, so one step looser. */
.cims-chart-title,
.cims-hub-card-title,
.cims-insight-tile-title,
.cims-quicklink-title,
.cims-timeline-step-title,
.cims-form-section-title,
.cims-drawer-section-title {
    letter-spacing: var(--cims-tracking-snug);
    line-height: var(--cims-leading-snug);
}

/* LABEL — the caps chrome that marks something as a field name, a column or
   a caption rather than as the user's data. One size, one weight, one
   tracking, everywhere. They were spread across .6875/.75rem, 500/600/700 and
   .04/.05/.06em, which is enough variation to read as carelessness even
   though no single instance looks wrong. */
.cims-kpi-name,
.cims-kpi-card .cims-kpi-label,
.cims-metric-label,
.cims-gauge-label,
.cims-board-period-label,
.cims-hub-figure-label,
.cims-field-label,
.cims-linked-doc-label,
.cims-workflow-progress-label,
.cims-section-title,
.cims-stat-label {
    font-size: var(--cims-text-label);
    font-weight: var(--cims-weight-semibold);
    letter-spacing: var(--cims-tracking-label);
    line-height: var(--cims-leading-snug);
}

/* A table header is a label too, but it must not grow: these columns size to
   their content, so a wider header is a wider table. Tracking only. */
.table > thead > tr > th,
.cims-data-table th {
    letter-spacing: var(--cims-tracking-wide);
}

/* FIGURE — every number the product displays as a number.

   `tabular-nums` on all of them, with `"tnum" 1` as the belt to that brace
   for the font stack's older members: proportional figures are why a column
   of currency drifts by a digit's width from row to row, and it is the single
   most legible difference between a spreadsheet and a report. */
.cims-kpi-figure,
.cims-kpi-value,
.cims-kpi-card .cims-kpi-value,
.cims-metric-value,
.cims-gauge-value,
.cims-donut-total,
.cims-hub-figure-value,
.cims-hub-num,
.cims-insight-tile-num,
.cims-estimate-grand-amount,
.cims-estimate-part-amount,
.cims-bell-count,
.cims-muted-count,
.cims-stage-number,
.cims-stat-value,
.cims-pair-value {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

/* The large figures also take display tracking and leading. The small ones
   (a count in a caption, a step number in a circle) deliberately do not:
   negative tracking at 12px closes the gaps a reader needs. */
.cims-kpi-figure,
.cims-kpi-card .cims-kpi-value,
.cims-gauge-value,
.cims-donut-total,
.cims-estimate-grand-amount,
.cims-stat-value {
    letter-spacing: var(--cims-tracking-tight);
    line-height: var(--cims-leading-tight);
}

.cims-gauge-value { font-size: var(--cims-text-xl); }

/* --- 2. RHYTHM -------------------------------------------------------------
   Every padding below is a step on the spacing scale. The audit rule was that
   nothing may end up shorter than ~90% of what it was — this is a pass for
   consistency, not for density — and the table row is the only thing that
   moves down at all, by 4%. */

/* The 4px step that was visible on every screen: header 16px, body 20px. */
.card-header {
    padding: var(--cims-space-2-5) var(--cims-space-3-5);
}

.card-body {
    padding: var(--cims-space-3-5);
}

.card-footer {
    padding: var(--cims-space-2-5) var(--cims-space-3-5);
    background: transparent;
    border-top: 1px solid var(--cims-color-border);
}

.modal-header,
.modal-footer {
    padding: var(--cims-space-3) var(--cims-space-3-5);
}

.modal-body {
    padding: var(--cims-space-3-5);
}

/* KPI tile: 14/16 becomes 16/16 — squarer, and on the scale. */
.cims-kpi {
    gap: var(--cims-space-1);
    padding: var(--cims-space-3);
}

/* Chart card: 16/18/18 becomes 16/20/20, so its content column lines up with
   a .card-body's beside it on a mixed board. */
.cims-chart-card {
    padding: var(--cims-space-3) var(--cims-space-3-5) var(--cims-space-3-5);
}

.cims-chart-head { margin-bottom: var(--cims-space-2-5); }

.cims-insight-strip { padding: var(--cims-space-3); }
.cims-insight-strip-head { margin-bottom: var(--cims-space-2-5); }

/* Table rows. 8.8px of padding was the one genuinely arbitrary number in the
   file; 8px is the scale step under it and lands the row at 96% of its
   current height — inside the audit's floor, and the whole product's tables
   now share a rhythm with its cards. The header gains 4px instead of losing
   any: a column name should sit slightly further from its first value than
   two values sit from each other. */
.table > thead > tr > th {
    padding-block: var(--cims-space-2-5);
}

.table > tbody > tr > td {
    padding-block: var(--cims-space-2);
}

/* --- 3. ELEVATION ----------------------------------------------------------
   One rule, three levels:

     flat     a hairline border and no shadow. Anything that is part of the
              page's own surface: board tiles, chart cards, strips.
     raised   --cims-shadow-sm. A .card — a container the page is built from.
     overlay  --cims-shadow-md for something attached to its trigger (menu,
              popover, toast), --cims-shadow-lg for something that blocks the
              page (modal, drawer).

   The hover lifts below were four different hand-rolled rgba shadows, each
   computed from the muted-ink token, none of which had a dark-mode value —
   on the dark theme they painted a grey haze where a shadow should be. */
.cims-kpi,
.cims-chart-card,
.cims-insight-strip,
.cims-insight-tile,
.cims-hub-card,
.cims-filter-card {
    box-shadow: none;
}

a.cims-kpi:hover,
a.cims-kpi:focus-visible,
.cims-insight-tile:hover,
.cims-insight-tile:focus-visible,
.cims-hub-card:hover,
.cims-hub-card:focus-visible {
    box-shadow: var(--cims-shadow-md);
}

.card { box-shadow: var(--cims-shadow-sm); }

.popover,
.toast,
.tooltip-inner {
    box-shadow: var(--cims-shadow-md);
}

/* --- 4. THE FIRST CHART ON A BOARD -----------------------------------------
   A board is read top-left first whether or not anything says so, so the
   chart that answers the board's question should look like the one to read.

   It keeps its grid span — the layout does not change — and earns prominence
   from three small things instead: a border tinted toward the brand rather
   than neutral, the raised card's shadow, one step more padding, and a title
   one step up the scale. Deliberately no fill and no accent bar: this is
   "start here", not "something is wrong here", and the tinted-surface version
   of this read as an alert. */
.cims-chart-card--primary {
    padding: var(--cims-space-3-5) var(--cims-space-4) var(--cims-space-4);
    border-color: rgba(var(--cims-color-primary-rgb), .28);
    box-shadow: var(--cims-shadow-sm);
}

.cims-chart-card--primary .cims-chart-head {
    margin-bottom: var(--cims-space-3);
}

.cims-chart-card--primary .cims-chart-title {
    font-size: var(--cims-text-md);
    letter-spacing: var(--cims-tracking-snug);
}

/* --- 5. STAT — one number, presented as one number -------------------------
   Some of a board's questions have a single-value answer: a count, a ratio, a
   days figure. Drawing that as a one-bar bar chart is the commonest way a
   dashboard looks unfinished — the axis, the gridlines and the legend are all
   scaffolding around a number that needed none of them.

   So the chart body gets a stat instead: label above, figure at display size,
   context beneath, centred in exactly the height the chart would have taken,
   on a quietly recessed panel so it reads as a deliberate choice rather than
   as a chart that failed to render. The panel's border is solid, which is
   what separates it at a glance from .cims-chart-empty's dashed one. */
.cims-stat {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--cims-space-2);
    height: 100%;
    min-width: 0;
    padding: var(--cims-space-3);
    text-align: center;
}

.cims-chart-body > .cims-stat {
    background: var(--cims-color-surface-alt);
    border: 1px solid var(--cims-color-border-subtle);
    border-radius: var(--cims-radius-sm);
}

/* .cims-stat-label takes its size, weight and tracking from the LABEL rule
   above, with the rest of the product's caps chrome. */
.cims-stat-label {
    margin: 0;
    text-transform: uppercase;
    color: var(--cims-color-text-muted);
}

.cims-stat-value {
    margin: 0;
    font-size: var(--cims-text-2xl);
    font-weight: var(--cims-weight-bold);
    line-height: var(--cims-leading-tight);
    letter-spacing: var(--cims-tracking-tightest);
    color: var(--cims-color-text);
    /* A long money figure wraps rather than widening the card. */
    overflow-wrap: anywhere;
}

.cims-stat-context {
    margin: 0;
    max-width: 26rem;
    font-size: var(--cims-text-sm);
    line-height: var(--cims-leading-snug);
    color: var(--cims-color-text-muted);
}

/* --- 6. PAIR — a handful of labelled magnitudes ----------------------------
   The other shape a chart is wrong for: four or five named values that are
   compared to each other and to nothing else. A bar chart spends an axis, a
   grid and a legend to say what a label, a bar and a number say directly, and
   at five categories the axis labels are the widest thing on the card.

   Three columns: name, magnitude, value. The track is recessed and the fill
   is the primary series colour, so a pair list on a board is visibly the same
   data language as the charts beside it. */
.cims-pair {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--cims-space-2-5);
    height: 100%;
    min-width: 0;
}

.cims-pair-row {
    display: grid;
    grid-template-columns: minmax(5rem, 32%) 1fr auto;
    align-items: center;
    gap: var(--cims-space-2-5);
    min-width: 0;
}

/* Truncates rather than wrapping: a wrapped label makes one row taller than
   the rest and the list stops reading as a column. The full text belongs in
   a title attribute on the element. */
.cims-pair-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--cims-text-sm);
    line-height: var(--cims-leading-snug);
    color: var(--cims-color-text-muted);
}

.cims-pair-track {
    position: relative;
    width: 100%;
    min-width: 0;
    height: .5rem;
    border-radius: var(--cims-radius-pill);
    background: var(--cims-color-surface-muted);
    /* An inset hairline rather than a border, so the track's own width is not
       2px different from the fill's 100%. */
    box-shadow: inset 0 0 0 1px var(--cims-color-border-subtle);
    overflow: hidden;
}

/* --cims-chart-1 rather than --cims-color-primary on purpose. In light mode
   they are the same hex; in dark mode the primary token is lightened to
   #93b4ff for TEXT on a near-black ground, which as a 8px filled bar is a
   glare. --cims-chart-1's dark step is tuned for exactly this: a mark. */
.cims-pair-bar {
    display: block;
    height: 100%;
    width: var(--v, 0%);
    max-width: 100%;
    border-radius: var(--cims-radius-pill);
    background: var(--cims-chart-1);
    transition: width .3s var(--cims-ease-decelerate);
}

.cims-pair-value {
    text-align: right;
    white-space: nowrap;
    font-size: var(--cims-text-sm);
    font-weight: var(--cims-weight-semibold);
    color: var(--cims-color-text);
}

/* --- 7. MOTION -------------------------------------------------------------
   Extends the reduced-motion blocks above to the one thing this section
   animates. */
@media (prefers-reduced-motion: reduce) {
    .cims-pair-bar {
        transition: none;
    }
}

/* ==========================================================================
   SLIDE-OVER — the DSR vs market comparison.

   A panel over the sheet, not a page instead of it. The whole question is
   "what would this same work cost at market", and taking the figures being
   compared off the screen to answer it would defeat the panel.
   ========================================================================== */
.cims-slideover {
    position: fixed;
    inset: 0;
    z-index: 1055;
    display: flex;
    justify-content: flex-end;
}

/* The page behind must not scroll while the panel is open — two scrollbars
   and the wrong one moving is the commonest slide-over defect. */
body.cims-slideover-open { overflow: hidden; }

.cims-slideover-scrim {
    position: absolute;
    inset: 0;
    background: rgba(12, 18, 32, 0.45);
    animation: cims-fade-in 0.15s ease-out;
}

.cims-slideover-sheet {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(62rem, 100vw);
    background: var(--cims-color-surface);
    border-left: 1px solid var(--cims-color-border);
    box-shadow: var(--cims-shadow-lg);
    animation: cims-slide-in 0.2s ease-out;
}

@keyframes cims-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes cims-slide-in { from { transform: translateX(1.5rem); opacity: 0; } to { transform: none; opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
    .cims-slideover-scrim,
    .cims-slideover-sheet { animation: none; }
}

.cims-slideover-head,
.cims-slideover-foot {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 1rem 1.25rem;
    flex: 0 0 auto;
}

.cims-slideover-head { border-bottom: 1px solid var(--cims-color-border); }

.cims-slideover-foot {
    border-top: 1px solid var(--cims-color-border);
    background: var(--cims-color-bg);
}

.cims-slideover-title {
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    margin: 0;
}

.cims-slideover-sub {
    margin: 0.1rem 0 0;
    font-size: 0.8125rem;
    color: var(--cims-color-text-muted);
}

.cims-slideover-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 0 1.25rem 1.25rem;
}

/* --- the headline totals ---------------------------------------------------- */
.cims-cmp-totals {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    padding: 0.875rem 1.25rem;
    background: var(--cims-color-bg);
    border-bottom: 1px solid var(--cims-color-border);
    flex: 0 0 auto;
    flex-wrap: wrap;
}

.cims-cmp-total-label {
    display: block;
    font-size: 0.625rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--cims-color-text-muted);
}

.cims-cmp-total-value {
    display: block;
    font-size: 1.125rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    margin-top: 0.1rem;
}

.cims-cmp-arrow { color: var(--cims-color-text-muted); }

.cims-cmp-total--delta { margin-left: auto; text-align: right; }
.cims-cmp-total--delta.is-up .cims-cmp-total-value { color: var(--cims-color-danger); }
.cims-cmp-total--delta.is-down .cims-cmp-total-value { color: var(--cims-color-success); }

/* --- one sheet inside the panel ---------------------------------------------- */
.cims-cmp-section { padding-top: 1.25rem; }

.cims-cmp-section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
}

.cims-cmp-section-title {
    font-size: 0.875rem;
    font-weight: 650;
    margin: 0;
}

.cims-cmp-section-sum {
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
    color: var(--cims-color-text-muted);
}

.cims-cmp-section-note {
    margin: 0.2rem 0 0.5rem;
    font-size: 0.75rem;
    color: var(--cims-color-text-muted);
}

.cims-cmp-table th {
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--cims-color-text-muted);
    white-space: nowrap;
}

.cims-cmp-table td { font-size: 0.8125rem; vertical-align: middle; }
.cims-cmp-detail { font-size: 0.6875rem; color: var(--cims-color-text-muted); }

.cims-cmp-rate {
    width: 7rem;
    display: inline-block;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* A placeholder shows the inherited figure; an entered value is the estimate's
   own. The two must not look alike, or nobody can tell which they are seeing. */
.cims-cmp-rate::placeholder { color: var(--cims-color-text-muted); opacity: 0.85; }
.cims-cmp-rate:not(:placeholder-shown) { font-weight: 650; border-color: var(--cims-color-primary); }
.cims-cmp-saving { opacity: 0.6; }

.cims-cmp-was {
    display: block;
    font-size: 0.625rem;
    color: var(--cims-color-text-muted);
    margin-top: 0.15rem;
}

.cims-cmp-up { color: var(--cims-color-danger); font-weight: 600; }
.cims-cmp-down { color: var(--cims-color-success); font-weight: 600; }
.cims-cmp-loading { padding: 1.5rem 0; }

@media (max-width: 48rem) {
    .cims-slideover-sheet { width: 100vw; }
    .cims-cmp-totals { gap: 0.75rem; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE PROJECT LIST AND ITS LEVEL PROGRESS
   ═══════════════════════════════════════════════════════════════════════════
   One row per job, and four levels telling a reader how far it has got. The
   whole point is that this is legible across a page of rows without being read
   word by word — so the level is a row of lamps first and a sentence second.

   Three states, three treatments, and they must differ by more than colour:
   complete fills, pending outlines, not-started greys out. A reader who cannot
   distinguish red from green still sees a filled lamp against a hollow one.
   ═══════════════════════════════════════════════════════════════════════════ */

/* EVERY CELL STARTS ON THE ROW'S FIRST LINE.

   Top alignment itself comes from `align-top` in the markup — Bootstrap's
   alignment utilities carry `!important` and a rule here cannot override the
   class the table already had. What this adds is the room: a row that grows to
   three lines of buttons needs breathing space above and below, or the rows run
   into one another. */
/* `.table > tbody > tr > td` further up this file sets `vertical-align: middle`
   and its own padding, and it is more specific than a class on the table — so
   `align-top` in the markup and a plainer rule here both lost to it, and the
   cells went on centring. Matched at the same shape to win on source order. */
.cims-project-table > tbody > tr > td,
.cims-project-table > tbody > tr > th {
    vertical-align: top;
    padding-top: .8rem;
    padding-bottom: .8rem;
}

/* Room between wrapped lines of buttons, so three lines read as three lines
   rather than as one dense block. */
.cims-project-verbs { row-gap: .3rem !important; }

/* --- the level chip on the row -------------------------------------------- */
/* ONE LOOK FOR "WHERE IT IS", whatever level that is.

   The chip used to be coloured by STATUS — amber while a level was in progress,
   dashed grey before it had started — so two jobs both sitting at Level 2 wore
   two different chips, and the colour answered a question the reader had not
   asked. Reported as exactly that: the colours create confusion.

   The chip now says one thing, in one style: which level this job is on. The
   only state it still distinguishes is FINISHED, because "this job is done" is
   a different kind of fact from "this job is at level 3" and a reader scanning
   for live work needs to skip the finished ones at a glance.

   The level's own detail — what is complete, what is pending, what has not
   started — is in the dialog the chip opens, which is where somebody asking
   about one job is already looking. */
.cims-level-chip {
    font-weight: 600;
    white-space: nowrap;
    border: 1.5px solid rgba(var(--cims-color-primary-rgb), .35);
    background: rgba(var(--cims-color-primary-rgb), .08);
    color: var(--cims-color-primary);
}
.cims-level-chip:hover,
.cims-level-chip:focus-visible {
    border-color: var(--cims-color-primary);
    background: rgba(var(--cims-color-primary-rgb), .16);
    color: var(--cims-color-primary);
}

/* Finished. Green, and the only chip that differs — a reader scanning a page of
   live work should be able to skip these without reading them. */
.cims-level-chip--complete {
    border-color: rgba(var(--cims-color-success-rgb), .45);
    background: rgba(var(--cims-color-success-rgb), .12);
    color: var(--cims-color-success);
}
.cims-level-chip--complete:hover,
.cims-level-chip--complete:focus-visible {
    border-color: var(--cims-color-success);
    background: rgba(var(--cims-color-success-rgb), .2);
    color: var(--cims-color-success);
}

/* --- the verbs on the row ------------------------------------------------- */
/* In the open, not behind a menu: this screen exists so a reader can see what to
   do next without hunting for it. They wrap rather than scroll, because a row
   with eight verbs is a row with a lot going on and hiding half of it off the
   right edge is the dropdown problem again in another shape.

   COLOUR CARRIES THE DOCUMENT, not the verb. Seven greys in a row forced a
   reader to read every label before finding the one they wanted; now the eye
   finds "the purple one" (purchase order) or "the green one" (measurement book)
   and reads only that. Weight carries the verb instead: a tinted button opens
   something, a solid one does something — start it, send it on, approve it.

   Colour is never the only cue: every button carries its full label.

   NO ICONS. Each button used to lead with one, and at 11.5px an icon is a
   smudge that costs a letter's worth of width and says less than the letter
   would — eight of them in a cell made a row of marks a reader had to look past
   to reach the words. The label is the control; it reads at a glance and it
   reads correctly at any size. */
.cims-project-verbs .btn {
    /* Bigger than before, because the icons are gone and the room they took is
       better spent on the thing being read. */
    font-size: .76rem;
    padding-block: .28rem;
    padding-inline: .6rem;
    border-radius: .375rem;
    line-height: 1.3;
    font-weight: 600;
    /* One height for every button, whatever its border weight, so a wrapped row
       reads as rows and not as a ragged pile. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 1.75rem;
    white-space: nowrap;
}

.cims-verb {
    --verb: 51 65 85;           /* slate, the fallback family */
    border: 1px solid rgb(var(--verb) / .28);
    background: rgb(var(--verb) / .10);
    color: rgb(var(--verb));
    font-weight: 600;
}
.cims-verb:hover,
.cims-verb:focus-visible {
    background: rgb(var(--verb) / .16);
    border-color: rgb(var(--verb) / .55);
    color: rgb(var(--verb));
}

/* DOING something, rather than opening it.

   This was a FILLED button with white text, and it was reported as unreadable
   three times. The contrast maths said otherwise — 5.5:1 and 7:1 — and the
   maths was answering the wrong question: at 11.5px, white text on a saturated
   ground is thin, and thin white type washes out on a screen that is not the
   one it was measured on. A ratio is a floor, not a guarantee of legibility.

   So nothing here puts white on colour. An action is marked by WEIGHT instead:
   a stronger wash, a 2px ring and bolder type, with the same dark ink every
   other button uses. The eye still lands on it first, and the text is the same
   dark colour that was never in question. */
/* The wash stays LIGHT. Deepening it to .22 to mark the action dropped the
   text to 3.66:1 — ink and ground are the same hue here, so darkening one
   darkens the gap between them. The ring and the weight carry the emphasis; the
   background stays where the contrast is. */
.cims-verb.is-solid {
    background: rgb(var(--verb) / .12);
    border: 2px solid rgb(var(--verb) / .85);
    color: rgb(var(--verb));
    font-weight: 700;
    /* The 2px ring would otherwise make a solid button a pixel taller than the
       quiet ones beside it, and a row of buttons that do not sit on one line
       reads as a mistake. */
    padding-block: calc(.28rem - 1px);
}
.cims-verb.is-solid:hover,
.cims-verb.is-solid:focus-visible {
    background: rgb(var(--verb) / .20);
    border-color: rgb(var(--verb));
    color: rgb(var(--verb));
}

/* One hue per document, far enough apart to tell at a glance and in the order
   the work happens.

   DARK ENOUGH TO CARRY WHITE TEXT. The first cut used mid-tone hues — teal
   #0d9488, green #16a34a, cyan #0891b2 — and a filled button in any of them put
   white text at around 3.3:1, which is below AA and which a reader simply
   reported as "I cannot see it". These are the deepened shades of the same
   hues, every one above 4.5:1 against white:
   
       estimate 6.70  ·  po 7.10  ·  tender 7.03  ·  agreement 7.09
       mb 7.13  ·  tracking 7.27  ·  bill 6.29

   Green and teal each needed a second pass: measured against plain white they
   looked fine, and against the surface they are actually painted on — a wash of
   themselves over a striped row — they came in at 3.99:1 and 4.20:1. The ground
   a button sits on is the number that matters, not the one the palette was
   picked against.

   The same value is the INK of the tinted buttons, where it sits on a 10% wash
   of itself — so the quiet buttons read at better than 4.7:1 on the page ground
   too. One token per family, correct in both treatments. */
.cims-verb--estimate  { --verb: 29 78 216; }    /* blue   — the estimate itself */
.cims-verb--po        { --verb: 109 40 217; }   /* violet — purchase order */
.cims-verb--tender    { --verb: 17 94 89; }     /* teal   — tender */
.cims-verb--agreement { --verb: 146 64 14; }    /* amber  — agreement */
.cims-verb--mb        { --verb: 22 101 52; }    /* green  — measurement book */
.cims-verb--tracking  { --verb: 21 94 117; }    /* cyan   — site diary */
.cims-verb--bill      { --verb: 190 18 60; }    /* rose   — billing */

/* On a dark ground the mid-tone inks fall below contrast, so the tint carries
   more of the work and the text is lifted. The hues are unchanged — a reader who
   learned "violet is the purchase order" in one theme keeps it in the other.

   `:root:not([data-theme="light"])` must sit INSIDE the media query. Written
   outside it, it matches the default "follow the system" state in a light
   browser too, and the dark treatment was applied to every button on a white
   page — washed-out text on a heavy tint, which is what prompted this note. */
:root[data-theme="dark"] .cims-verb {
    background: rgb(var(--verb) / .20);
    border-color: rgb(var(--verb) / .50);
    color: rgb(var(--verb) / .98);
    filter: brightness(1.35);
}
:root[data-theme="dark"] .cims-verb.is-solid {
    background: rgb(var(--verb) / .22);
    border-color: rgb(var(--verb) / .95);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .cims-verb {
        background: rgb(var(--verb) / .20);
        border-color: rgb(var(--verb) / .50);
        color: rgb(var(--verb) / .98);
        filter: brightness(1.35);
    }

    :root:not([data-theme="light"]) .cims-verb.is-solid {
        background: rgb(var(--verb) / .22);
        border-color: rgb(var(--verb) / .95);
    }
}

/* --- the dialog ---------------------------------------------------------- */
.cims-level {
    border: 1px solid var(--bs-border-color); border-radius: .5rem;
    padding: .75rem .9rem; margin-bottom: .6rem; background: var(--bs-body-bg);
}

/* Where the reader is standing. One level carries it, and it is a left edge
   rather than a fill, so the row's own states stay readable inside it. */
.cims-level--current { border-left: 4px solid var(--bs-primary); }
.cims-level--complete { background: var(--bs-success-bg-subtle); border-color: var(--bs-success-border-subtle); }
.cims-level--not-started { opacity: .72; }

.cims-level-head { display: flex; align-items: flex-start; gap: .7rem; }

.cims-level-badge {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 1.9rem; height: 1.9rem; border-radius: 50%;
    font-weight: 700; font-size: .85rem;
    border: 1.5px solid var(--bs-border-color); color: var(--bs-secondary-color);
}
.cims-level--complete .cims-level-badge { background: var(--bs-success); border-color: var(--bs-success); color: #fff; }
.cims-level--pending .cims-level-badge { border-color: var(--bs-warning); color: var(--bs-warning-text-emphasis); }

.cims-level-title { font-weight: 600; }
.cims-level-summary { font-size: .82rem; color: var(--bs-secondary-color); }

.cims-level-state {
    margin-left: auto; align-self: center; white-space: nowrap;
    font-size: .74rem; font-weight: 600; letter-spacing: .03em; text-transform: uppercase;
    color: var(--bs-secondary-color);
}
.cims-level--complete .cims-level-state { color: var(--bs-success-text-emphasis); }
.cims-level--pending .cims-level-state { color: var(--bs-warning-text-emphasis); }

/* --- the documents inside a level ---------------------------------------- */
.cims-level-items { list-style: none; margin: .65rem 0 0; padding: 0 0 0 2.6rem; }

.cims-level-item {
    display: flex; align-items: baseline; gap: .6rem;
    padding: .3rem 0; font-size: .86rem;
    border-top: 1px solid var(--bs-border-color-translucent);
}
.cims-level-item:first-child { border-top: 0; }
.cims-level-item--not-started { color: var(--bs-secondary-color); }

.cims-level-item-label { font-weight: 500; min-width: 10rem; }
.cims-level-item-state { color: var(--bs-secondary-color); }
.cims-level-item-open { margin-left: auto; white-space: nowrap; }


/* --- the project list's classification column ------------------------------ */
/* Four facts in one cell, and the only thing that makes them readable is that
   each one says what it is. Unlabelled, "Detailed Estimate / Bridge and
   Structure / Building Work / Deck and Girder" is four phrases a reader has to
   already know the order of.

   Label above value rather than beside it: beside it, the four values start at
   four different x-positions and the column stops scanning vertically. */
/* TWO COLUMNS: the labels in one, the answers in the other.

   Stacked, this was label-value-label-value down a single column, and a reader
   could not tell which lines were the headings — the thing the labels were
   added to fix. Side by side, the labels line up into a column of their own and
   the eye separates them from the answers before reading either.

   The label column is a fixed width so all four answers start at the same x
   across every row of the table: a ragged left edge on the values is what makes
   a list like this unscannable. */
.cims-classification {
    display: grid;
    grid-template-columns: 6.3rem minmax(0, 1fr);
    gap: .15rem .5rem;
    align-items: baseline;
}
.cims-classification dt {
    /* Sentence case, not uppercase. At this size uppercase with letter-spacing
       is both wider and harder to read, and width is what this column is short
       of. */
    font-size: .66rem;
    font-weight: 500;
    color: var(--bs-secondary-color);
    line-height: 1.25;
    margin: 0;
    /* A label that wraps to two lines drops its own value half a line out of
       step with the three beside it, and the column stops reading as a table of
       pairs. The label column is sized for the longest of the four instead. */
    white-space: nowrap;
}
.cims-classification dd {
    font-size: .74rem;
    font-weight: 600;
    line-height: 1.25;
    margin: 0;
}

/* --- the progress bar on a row, and on a level card ----------------------- */
/* Deliberately small. On the list it sits under the level chip in a 10rem
   column, where it answers "how far through that level" — a question the chip
   alone cannot answer, since a job can sit at level 3 for a month. */
.cims-progress-mini { display: flex; align-items: center; gap: .4rem; }
.cims-progress-mini__track {
    flex: 1 1 auto;
    height: .4rem;
    border-radius: 999px;
    background: rgba(var(--cims-color-primary-rgb), .14);
    overflow: hidden;
}
.cims-progress-mini__fill {
    height: 100%;
    border-radius: 999px;
    background: var(--cims-color-primary);
    /* A job at 0% must still read as a bar at zero, not as a missing element. */
    min-width: 0;
    transition: width .3s ease;
}
.cims-progress-mini__value {
    flex: 0 0 auto;
    font-size: .68rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--bs-secondary-color);
}

/* --- the overall ring on the progress page -------------------------------- */
/* An arc is a picture of a quantity and not a reading of it, so the number is
   printed in the middle in plain type and the arc is the thing you see from
   across the room. */
.cims-donut__track { stroke: rgba(var(--cims-color-primary-rgb), .14); }
.cims-donut__fill {
    stroke: var(--cims-color-primary);
    transition: stroke-dasharray .4s ease;
}
.cims-donut__value {
    font-size: 1.6rem;
    font-weight: 700;
    fill: var(--bs-body-color);
    font-variant-numeric: tabular-nums;
}

/* --- the four level cards ------------------------------------------------- */
/* The left edge carries the state, so four cards in a row can be read as a
   sequence without reading any of their words. */
.cims-level-card { border-left: 4px solid var(--bs-border-color); }
.cims-level-card--complete { border-left-color: var(--cims-color-success); }
.cims-level-card--pending { border-left-color: var(--cims-color-primary); }
.cims-level-card--not-started { border-left-color: var(--bs-border-color); }

.cims-level-badge {
    background: rgba(var(--cims-color-primary-rgb), .12);
    color: var(--cims-color-primary);
    font-variant-numeric: tabular-nums;
}
.cims-level-card--complete .cims-level-badge {
    background: rgba(var(--cims-color-success-rgb), .12);
    color: var(--cims-color-success);
}

/* A milestone is a thing that either happened or did not, so done and not-done
   must differ by more than a tick nobody looks at: the done ones recede. */
.cims-milestones li { display: flex; gap: .4rem; align-items: start; padding-block: .15rem; }
.cims-milestones li .bi { color: var(--bs-secondary-color); line-height: 1.5; }
.cims-milestones li.is-done { color: var(--bs-secondary-color); }
.cims-milestones li.is-done .bi { color: var(--cims-color-success); }

.cims-waiting {
    background: rgba(var(--cims-color-primary-rgb), .07);
    border-radius: .35rem;
    padding: .35rem .5rem;
    color: var(--cims-color-primary);
    font-weight: 600;
}

.cims-fact-grid dd { font-weight: 600; }

/* --- photograph chips on the progress page -------------------------------- */
.cims-photo-chip {
    width: 3.5rem;
    height: 3.5rem;
    border-radius: .35rem;
    overflow: hidden;
    border: 1px solid var(--bs-border-color);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--bs-tertiary-bg);
}
.cims-photo-chip img { width: 100%; height: 100%; object-fit: cover; }
.cims-photo-chip--more { font-size: .75rem; font-weight: 700; color: var(--bs-secondary-color); }

/* --- the project table's geometry ----------------------------------------- */
/* `table-layout: fixed`, so the widths declared on the header cells are the
   widths the browser uses.

   Under the default `auto` layout a table is never narrower than its content,
   and the Project column's longest line — a title, a client and a holder's name
   on one row — held the table 322px wider than the card at 1280px. Everything
   to the right of it, which is the entire Actions column, went off the edge of
   the screen. Those buttons are what the list exists to offer.

   The cost is that long text wraps instead of pushing the table wider, which is
   the behaviour already wanted here: rows grow taller, they do not escape. */
/* `!important` on the width, and only here.

   DataTables measures the table on init and writes the result back as an INLINE
   style — 1279.61px against a 1118px card. An inline style beats a stylesheet,
   so the declared column widths were being honoured inside a table that was
   itself 162px too wide, and the overflow came out of the right-hand column.
   `autoWidth: false` does not stop it; it only stops the per-column sizing.

   This is the one selector allowed to shout, because it is answering something
   that shouts first. */
/* `table.dataTable.cims-project-table`, not `.cims-project-table`.

   DataTables' own stylesheet carries `table.dataTable { table-layout: auto }`,
   which at specificity 0,1,1 beat a bare class selector — so `table-layout:
   fixed` was in the stylesheet, computed as "fixed" when asked, and never
   governed the layout. The table went on sizing itself from its content: 1280px
   inside a 1118px card, with the entire Actions column off the right edge.

   Matching DataTables' shape and adding the class wins it outright. */
table.dataTable.cims-project-table,
table.cims-project-table {
    table-layout: fixed;
    width: 100% !important;
}
/* Cells WRAP here.

   A `white-space: nowrap` inherited from the table styling kept every cell on
   one line. Under the content-sized layout that merely made the table wider;
   under the fixed layout above, the column can no longer grow, so the project
   name and client ran straight across the Classification column beside it and
   the two sets of words overlapped.

   `anywhere` as well as `normal`, because an estimate title can be one long
   unbroken string and a word that cannot break would reproduce the overlap on
   exactly the rows nobody tested. */
table.dataTable.cims-project-table > tbody > tr > td,
table.dataTable.cims-project-table > tbody > tr > th,
.cims-project-table td,
.cims-project-table th,
.cims-project-table td * {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* …except the things that must stay on one line: a level chip reading
   "Level 2" broken across two lines, or a money figure split mid-number, is
   worse than a slightly wider column. */
.cims-project-table .text-nowrap,
.cims-project-table .cims-level-chip,
.cims-project-table .cims-progress-mini__value,
.cims-project-table .text-tabular { white-space: nowrap; }

/* DataTables inserts a <colgroup> of measured pixel widths. Under a fixed
   layout the table's used width is the LARGER of its own width and the sum of
   those columns, so a colgroup summing to 1280px held the table 162px wider
   than its card however many times the stylesheet asked for 100%. That is the
   overflow, and `autoWidth: false` does not prevent the colgroup being written.

   Returning the columns to `auto` hands sizing back to the widths declared on
   the header cells, which is where this screen's column widths are decided. */
.cims-project-table > colgroup > col { width: auto !important; }
/* The colgroup DataTables injects.

   It writes a <col> per column carrying the width it measured, and under a
   fixed layout a table is as wide as the SUM of its column widths — 1280px
   against a 1118px card, which put the whole Actions column off the right edge
   of the screen. `autoWidth: false` does not prevent the colgroup; it only
   stops the per-column measuring from running twice.

   `auto` hands the decision back to the widths declared on the header cells,
   which is where this screen's column sizes are written and the only place a
   reader editing this page would think to look for them. */
.cims-project-table > colgroup > col { width: auto !important; }

/* --- the progress page, after it was cut down ------------------------------ */
/* The next-step sentence. This is the one line the page is built around, so it
   is the only thing on the screen allowed to look like an instruction. */
.cims-next-step {
    font-weight: 600;
    color: var(--cims-color-primary);
}
.cims-next-step--done { color: var(--cims-color-success); }

.cims-level-tick { color: var(--cims-color-success); }
.cims-level-done { color: var(--cims-color-success); font-weight: 600; }

/* The milestone list, folded away. A disclosure rather than a list: the detail
   is worth keeping and is not worth forty lines of it on arrival. */
.cims-steps > summary {
    cursor: pointer;
    color: var(--bs-secondary-color);
    list-style: none;
}
.cims-steps > summary::-webkit-details-marker { display: none; }
.cims-steps > summary::before {
    content: "\f285";             /* bi-chevron-right */
    font-family: bootstrap-icons;
    display: inline-block;
    margin-right: .3rem;
    transition: transform .15s ease;
}
.cims-steps[open] > summary::before { transform: rotate(90deg); }
.cims-steps > summary:hover { color: var(--cims-color-primary); }

/* What kind of job this is, as chips. Same ink as the rest of the page — these
   are a reminder of which job you are looking at, not a control. */
.cims-tag {
    background: rgba(var(--cims-color-primary-rgb), .10);
    color: var(--cims-color-primary);
    border: 1px solid rgba(var(--cims-color-primary-rgb), .25);
    font-weight: 600;
}

/* --- the four level lamps on a project row -------------------------------- */
/* One lamp per level, numbered, and colour means exactly one thing per lamp:

      green   this level is finished
      blue    this is the level the job is on now
      grey    nothing here has started

   Numbered rather than plain dots, because a reader asking "which level is it
   on" wants to say the answer out loud, and four unlabelled dots make them
   count. The number also survives the colour being invisible to somebody —
   state is never carried by hue alone. */
.cims-levels { display: flex; gap: .25rem; }
.cims-lamp {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: .35rem;
    font-size: .72rem;
    font-weight: 700;
    line-height: 1;
    text-decoration: none;
    border: 1.5px solid transparent;
    font-variant-numeric: tabular-nums;
}
a.cims-lamp:hover { filter: brightness(.94); text-decoration: none; }

/* Finished. The only solid lamp — a run of them reads as a bar of progress,
   which is the thing being asked about. */
/* Scoped through `.cims-levels`, so a lamp that is a link keeps its own colour.

   `td a:not(.btn):not(.dropdown-item)` — the rule that makes ordinary table
   links read as body text — scores (0,2,2) and was repainting every lamp in the
   body ink. Background and border came through correctly, so all three states
   had the right shape and the same coloured number: the one thing four lamps
   exist to avoid. The rule to beat is `.table a:not(.btn):not(.dropdown-item):not(.page-link)`
   at (0,4,1) — every `:not()` argument counts toward the score. Matching that
   through the table and wrapper classes, and sitting later in the file, settles
   it without touching the record-link rule, which is doing its own job
   correctly everywhere else. */
.cims-project-table .cims-levels a.cims-lamp.cims-lamp--complete,
.cims-levels .cims-lamp.cims-lamp--complete {
    background: rgba(var(--cims-color-success-rgb), .16);
    border-color: rgba(var(--cims-color-success-rgb), .55);
    color: var(--cims-color-success);
}

/* Where the job is now. Ringed, so it stands out from both its neighbours. */
.cims-project-table .cims-levels a.cims-lamp.cims-lamp--pending,
.cims-levels .cims-lamp.cims-lamp--pending {
    background: rgba(var(--cims-color-primary-rgb), .14);
    border-color: var(--cims-color-primary);
    color: var(--cims-color-primary);
}

/* Not started. Deliberately quiet and deliberately still legible: this is the
   state most of a new job is in, and a lamp nobody can read is not restful,
   it is broken. */
.cims-project-table .cims-levels a.cims-lamp.cims-lamp--not-started,
.cims-levels .cims-lamp.cims-lamp--not-started {
    background: var(--bs-tertiary-bg);
    border-color: var(--bs-border-color);
    color: var(--bs-secondary-color);
}

/* --- the four levels, in the dialog the row's chip opens ------------------- */
/* A list of steps, not a grid of cards: these happen in order, and a numbered
   column down the left is what says so. */
.cims-level-step {
    padding: .75rem 0;
    border-bottom: 1px solid var(--bs-border-color);
}
.cims-level-step:last-child { border-bottom: 0; }

.cims-level-step__num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    font-size: .72rem;
    font-weight: 700;
    background: var(--bs-tertiary-bg);
    color: var(--bs-secondary-color);
    border: 1.5px solid var(--bs-border-color);
}
.cims-level-step__state { color: var(--bs-secondary-color); font-weight: 600; }

.cims-level-step--complete .cims-level-step__num {
    background: rgba(var(--cims-color-success-rgb), .16);
    border-color: rgba(var(--cims-color-success-rgb), .55);
    color: var(--cims-color-success);
}
.cims-level-step--complete .cims-level-step__state { color: var(--cims-color-success); }

.cims-level-step--pending .cims-level-step__num {
    background: rgba(var(--cims-color-primary-rgb), .14);
    border-color: var(--cims-color-primary);
    color: var(--cims-color-primary);
}
.cims-level-step--pending .cims-level-step__state { color: var(--cims-color-primary); }

/* The level's own badge, once it is finished. */
.cims-level-step--complete .cims-level-step__num {
    background: var(--cims-color-success);
    border-color: var(--cims-color-success);
    color: #fff;
}

/* --- a level, in the progress dialog: the LEVEL is the heading ------------- */
/* It was a digit in a small circle next to a title reading "Estimate" — and
   level 1 contains a module also called Estimate, so the heading and its own
   first child said the same word and the level vanished between them. A reader
   could not tell which module belonged to which level.

   Now the level is named in words and leads; the stage it covers is the
   subtitle; the modules are indented under both. The indent is what carries the
   hierarchy, so it has to be visible — hence the rule down the left. */

.cims-level-step:last-child { border-bottom: 0; }

.cims-level-step__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
}

/* The headline. Small caps-ish and tracked, so "LEVEL 2" reads as a label for
   everything beneath it rather than as another item in the list. */
.cims-level-step__badge {
    display: inline-flex;
    align-items: center;
    font-size: .68rem;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
    padding: .2rem .5rem;
    border-radius: .3rem;
    background: var(--bs-tertiary-bg);
    color: var(--bs-secondary-color);
    border: 1px solid var(--bs-border-color);
}
/* NO WHITE ON COLOUR, here either.

   Small uppercase white type on a saturated ground washes out — established on
   the row's action buttons, and this badge is the same size and the same idea.
   State is carried the same way those are: a light wash of the colour, a ring
   in it, and the dark ink of that family for the letters. */
.cims-level-step--complete .cims-level-step__badge {
    background: rgba(var(--cims-color-success-rgb), .14);
    border-color: rgba(var(--cims-color-success-rgb), .7);
    color: var(--cims-color-success);
}
.cims-level-step--pending .cims-level-step__badge {
    background: rgba(var(--cims-color-primary-rgb), .14);
    border-color: rgba(var(--cims-color-primary-rgb), .7);
    color: var(--cims-color-primary);
}

/* The stage this level covers. */
.cims-level-step__title {
    font-weight: 700;
    font-size: .95rem;
    margin-top: .35rem;
}

/* The modules sit in a panel under the level that owns them. A left rule said
   "these belong to that" while they were a vertical list; a row of circles
   needs a frame around it instead, or the connector reads as a rule of its own. */
.cims-level-step .cims-mods {
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: .5rem;
    padding: .6rem .5rem .5rem;
    margin-top: .6rem;
}
.cims-level-step--complete .cims-mods { border-color: rgba(var(--cims-color-success-rgb), .35); }
.cims-level-step--pending .cims-mods { border-color: rgba(var(--cims-color-primary-rgb), .35); }

/* ---------------------------------------------------------------------------
   The secondary button.

   Bootstrap's `.btn-secondary` is a fixed grey, so an installation that set a
   secondary brand colour saw it ignored on every Cancel button in the product.
   This rule existed once and was lost — the Settings screen kept offering
   "Secondary Button Colour", `partials/branding.blade.php` kept emitting
   `--cims-color-secondary`, and nothing consumed it. A setting that saves,
   reports success, and changes nothing on screen is worse than one that is
   not offered.

   Its ink is chosen the same way the primary's is, so a pale secondary colour
   is as usable as a dark one.
--------------------------------------------------------------------------- */
.btn-secondary {
    --bs-btn-bg: var(--cims-color-secondary);
    --bs-btn-border-color: var(--cims-color-secondary);
    --bs-btn-hover-bg: var(--cims-color-secondary-dark);
    --bs-btn-hover-border-color: var(--cims-color-secondary-dark);
    --bs-btn-active-bg: var(--cims-color-secondary-dark);
    --bs-btn-active-border-color: var(--cims-color-secondary-dark);
    --bs-btn-disabled-bg: var(--cims-color-secondary);
    --bs-btn-disabled-border-color: var(--cims-color-secondary);
    --bs-btn-focus-shadow-rgb: var(--cims-color-secondary-rgb);

    --bs-btn-color: var(--cims-color-secondary-ink);
    --bs-btn-hover-color: var(--cims-color-secondary-dark-ink);
    --bs-btn-active-color: var(--cims-color-secondary-dark-ink);
    --bs-btn-disabled-color: var(--cims-color-secondary-ink);
}

/* === the Level dialog ====================================================== */
/* Built in the order a reader asks: one ring (how far), four circles (which
   level), one panel (what is in it).

   This has been rebuilt twice. The version before drew the MODULES as circles
   in a row with the levels as plain headings — so the thing the dialog is named
   after had no shape, and four rows of circles read as four unrelated steppers.
   The levels are the sequence; they are what the circles are for. A module is a
   document inside one level, and a set of documents is a list. */

/* --- 2. the four levels, as one stepper ----------------------------------- */
.cims-lvsteps {
    display: flex;
    list-style: none;
    padding: 0;
    margin: 0;
}
.cims-lvstep { position: relative; flex: 1 1 0; min-width: 0; }

/* The connector, behind the circles, joining centre to centre. */
.cims-lvstep + .cims-lvstep::before {
    content: "";
    position: absolute;
    top: 1.35rem;
    right: 50%;
    width: 100%;
    height: 2px;
    background: var(--bs-border-color);
    z-index: 0;
}
/* The line fills in behind the levels already finished, so the stepper itself
   shows the distance travelled. */
.cims-lvstep--complete + .cims-lvstep::before { background: var(--cims-color-success); }

.cims-lvstep__button {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .4rem;
    width: 100%;
    padding: .25rem .25rem .5rem;
    background: none;
    border: 0;
    border-radius: .5rem;
    text-align: center;
    cursor: pointer;
}
.cims-lvstep__button:hover { background: var(--bs-tertiary-bg); }

.cims-lvstep__circle {
    width: 2.7rem;
    height: 2.7rem;
    border-radius: 50%;
    border: 2px solid var(--bs-border-color);
    background: var(--bs-body-bg);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1rem;
    font-weight: 700;
    color: var(--bs-secondary-color);
    transition: transform .12s ease, box-shadow .12s ease;
}
.cims-lvstep--complete .cims-lvstep__circle {
    background: var(--cims-color-success);
    border-color: var(--cims-color-success);
    color: #fff;
}
.cims-lvstep--pending .cims-lvstep__circle {
    border-color: var(--cims-color-primary);
    color: var(--cims-color-primary);
    background: rgba(var(--cims-color-primary-rgb), .12);
}

/* The one being read. A ring around the circle rather than a change of colour:
   colour here already means state, and one property cannot mean two things. */
.cims-lvstep.is-chosen .cims-lvstep__circle {
    box-shadow: 0 0 0 4px rgba(var(--cims-color-primary-rgb), .22);
    transform: scale(1.06);
}
.cims-lvstep.is-chosen .cims-lvstep__name { color: var(--cims-color-primary); }

.cims-lvstep__caption { display: flex; flex-direction: column; min-width: 0; }
.cims-lvstep__name {
    font-size: .68rem;
    font-weight: 800;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}
.cims-lvstep__title {
    font-size: .72rem;
    line-height: 1.2;
    color: var(--bs-secondary-color);
}

/* --- 3. the documents inside the chosen level ----------------------------- */
.cims-lvdetail {
    background: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: .6rem;
    padding: .85rem;
}
.cims-lvdetail__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .15rem;
}
.cims-lvdetail__state--complete { color: var(--cims-color-success); font-weight: 700; }
.cims-lvdetail__state--pending { color: var(--cims-color-primary); font-weight: 700; }
.cims-lvdetail__state--not-started { color: var(--bs-secondary-color); font-weight: 600; }

/* A document. A row, because documents are a list — the circles above are the
   sequence, and repeating that shape here made the two read as one thing. */
.cims-doc {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .5rem .6rem;
    border-radius: .45rem;
    border: 1px solid var(--bs-border-color);
    background: var(--bs-body-bg);
    text-decoration: none;
    margin-top: .4rem;
}
a.cims-doc:hover { border-color: var(--cims-color-primary); background: rgba(var(--cims-color-primary-rgb), .05); }

.cims-doc__dot {
    flex: 0 0 auto;
    width: 1.3rem;
    height: 1.3rem;
    border-radius: 50%;
    border: 2px solid var(--bs-border-color);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: .75rem;
}
.cims-doc--complete .cims-doc__dot {
    background: var(--cims-color-success);
    border-color: var(--cims-color-success);
    color: #fff;
}
.cims-doc--pending .cims-doc__dot {
    border-color: var(--cims-color-primary);
    box-shadow: inset 0 0 0 2px var(--bs-body-bg), inset 0 0 0 .65rem var(--cims-color-primary);
}

.cims-doc__text { display: flex; flex-direction: column; min-width: 0; flex-grow: 1; }
.cims-doc__label { font-size: .82rem; font-weight: 600; color: var(--cims-color-text); }
.cims-doc__state { font-size: .72rem; color: var(--bs-secondary-color); }
.cims-doc--complete .cims-doc__state { color: var(--cims-color-success); }
.cims-doc--pending .cims-doc__state { color: var(--cims-color-primary); }
.cims-doc__go { flex: 0 0 auto; color: var(--cims-color-primary); }

/* --- the four level boxes on the progress page ---------------------------- */
/* Each box is a LEVEL, and picking one puts its verbs in the bar at the top.
   The left edge carries state so the four read as a sequence without being
   read; the chosen one is lifted instead of recoloured, because colour here
   already means state and one property cannot mean two things. */
.cims-level-box {
    border-left: 4px solid var(--bs-border-color);
    transition: box-shadow .12s ease, border-color .12s ease;
}
/* THE ACTIONS COLUMN STAYS PUT.

   This is the widest table in the book — fifteen columns, six of them inputs —
   so it scrolls sideways inside its own box. With Actions as an ordinary last
   column, a reader who typed four figures scrolled the Save button off the
   right edge and reported it missing. Pinned to the right edge of the scroll
   box, it is always the thing nearest the hand. */
.cims-es-table th.cims-es-actions,
.cims-es-table td.cims-es-actions {
    position: sticky;
    right: 0;
    z-index: 2;
    background: var(--bs-body-bg);
    box-shadow: -.35rem 0 .4rem -.35rem rgba(0, 0, 0, .25);
}
.cims-es-table thead th.cims-es-actions { background: var(--bs-tertiary-bg); z-index: 3; }
.cims-es-row--excess td.cims-es-actions,
.cims-es-row--saving td.cims-es-actions { background: var(--bs-body-bg); }

/* --- Excess & Saving Statement ------------------------------------------- */
/* The two directions are the whole point of the statement, so they are told
   apart by colour as well as by the word: excess reads as a warning, saving as
   a credit, which is how an engineer reads them. */
.cims-es-group { font-size: .68rem; letter-spacing: .04em; text-transform: uppercase; }
.cims-es-group--excess { background: rgba(185, 28, 28, .08); color: #b91c1c; }
.cims-es-group--saving { background: rgba(22, 101, 52, .08); color: #166534; }

.cims-es-row--excess td:first-child { box-shadow: inset .2rem 0 0 #b91c1c; }
.cims-es-row--saving td:first-child { box-shadow: inset .2rem 0 0 #166534; }

/* The typed cells are narrow on purpose: six input boxes across a thirteen
   column table need to read as figures, not as a form. */
.cims-es-cell { min-width: 6.5rem; }
.cims-es-cell .form-control { padding: .15rem .35rem; font-size: .8rem; }

/* Struck, never typed — the same treatment the bar bending schedule gives its
   derived weights, so a reader learns the convention once. */
.cims-es-derived {
    display: inline-block;
    min-width: 5rem;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--cims-color-text);
}

.cims-es-justify { max-width: 16rem; font-size: .8rem; }

.cims-es-status {
    display: inline-block;
    font-size: .68rem;
    font-weight: 600;
    line-height: 1;
    padding: .2rem .45rem;
    border-radius: 999px;
    background: var(--bs-secondary-bg);
    color: var(--bs-secondary-color);
}
.cims-es-status--excess { background: rgba(185, 28, 28, .12); color: #b91c1c; }
.cims-es-status--saving { background: rgba(22, 101, 52, .12); color: #166534; }
.cims-es-status--manual { background: rgba(146, 64, 14, .12); color: #92400e; margin-left: .2rem; }

.cims-es-pill {
    display: inline-block;
    font-size: .72rem;
    font-weight: 600;
    padding: .2rem .5rem;
    border-radius: 999px;
    background: var(--bs-tertiary-bg);
    color: var(--cims-color-text);
    margin-left: .25rem;
}
.cims-es-pill--excess { background: rgba(185, 28, 28, .12); color: #b91c1c; }
.cims-es-pill--saving { background: rgba(22, 101, 52, .12); color: #166534; }

/* THE FOOT. Each total sits under the column it totals, and the net row is the
   one figure the statement exists to produce — so it is the only one given
   weight, a rule above it and a tinted ground. */
.cims-es-foot td { border-top: 1px solid var(--bs-border-color); }
.cims-es-foot--net td {
    background: var(--bs-tertiary-bg);
    border-top: 2px solid var(--bs-border-color);
    font-size: .9rem;
}
.cims-es-net--excess { color: #b91c1c; }
.cims-es-net--saving { color: #166534; }

/* The justification panel needs a footer the offcanvas does not ship. */
#mb-es-panel .offcanvas-footer { background: var(--bs-body-bg); }
#mb-es-panel { width: min(30rem, 92vw); }

.cims-dash-grouplabel {
    display: block;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}
/* The four main boards carry the accent edge: they are the front door, and a
   grid where everything looks equal is a grid with no front door. */
.cims-dash-choice--main { border-left: 3px solid var(--cims-color-primary); }

/* A role's dashboards: a grid of tickable boards, each naming what it holds. */
.cims-dash-choice {
    display: flex;
    gap: .55rem;
    align-items: flex-start;
    width: 100%;
    height: 100%;
    padding: .5rem .6rem;
    border: 1px solid var(--bs-border-color);
    border-radius: .5rem;
    cursor: pointer;
    transition: border-color .12s ease, background-color .12s ease;
}
.cims-dash-choice:hover { border-color: rgba(var(--cims-color-primary-rgb), .5); }
.cims-dash-choice:has(input:checked) {
    border-color: var(--cims-color-primary);
    background: rgba(var(--cims-color-primary-rgb), .06);
}
.cims-dash-choice__name { display: block; font-size: .82rem; font-weight: 600; color: var(--cims-color-text); }
.cims-dash-choice__tiles { display: block; font-size: .7rem; color: var(--bs-secondary-color); line-height: 1.35; }
.cims-dash-choice--custom { border-style: dashed; }

/* The role's dashboard picker: the chosen board's tiles, named. */
.cims-dash-preview {
    display: flex;
    flex-wrap: wrap;
    gap: .3rem;
    min-height: 2.4rem;
    align-items: flex-start;
    align-content: flex-start;
    padding: .4rem .5rem;
    border: 1px dashed var(--bs-border-color);
    border-radius: .45rem;
}
.cims-dash-chip {
    display: inline-block;
    font-size: .7rem;
    font-weight: 600;
    line-height: 1;
    padding: .3rem .5rem;
    border-radius: 999px;
    background: rgba(var(--cims-color-primary-rgb), .1);
    color: var(--cims-color-primary);
}

.cims-level-box { cursor: pointer; }
/* The whole card picks the level, so it has to look pickable. The document
   links and the menu inside it keep their own cursor. */
.cims-level-box:hover { border-color: rgba(var(--cims-color-primary-rgb), .45); box-shadow: 0 .25rem .75rem rgba(0,0,0,.06); }
.cims-level-box a, .cims-level-box .dropdown-toggle { cursor: pointer; }

/* The level menu carries the same verbs as the bar above it, printables
   included, and tints each icon with that verb's tone. */
.cims-level-menu__item { display: flex; align-items: center; gap: .55rem; font-size: .82rem; }
.cims-level-menu__icon { color: rgb(var(--verb, 51 65 85)); font-size: .9rem; width: 1rem; text-align: center; }

.cims-level-box--complete { border-left-color: var(--cims-color-success); }
.cims-level-box--pending { border-left-color: var(--cims-color-primary); }

.cims-level-box.is-chosen {
    box-shadow: 0 0 0 2px rgba(var(--cims-color-primary-rgb), .35), var(--bs-box-shadow-sm);
}

/* The heading IS the selector — a reader aiming at a box should not have to
   find a control inside it. */
.cims-level-box__pick {
    display: flex;
    flex-direction: column;
    gap: .1rem;
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    min-width: 0;
}
.cims-level-box__name {
    font-size: .68rem;
    font-weight: 800;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}
.cims-level-box.is-chosen .cims-level-box__name { color: var(--cims-color-primary); }
.cims-level-box__title { font-weight: 700; font-size: .95rem; line-height: 1.25; }
.cims-level-box__pick:hover .cims-level-box__title { color: var(--cims-color-primary); }

/* Which level the verb bar is currently showing. Without it the bar silently
   changes contents and a reader cannot tell why. */
.cims-verbbar__label {
    font-size: .68rem;
    font-weight: 800;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
    margin-right: .35rem;
}

/* --- the filter bar, when it is inside the filter CARD --------------------- */
/* A PANEL INSIDE A PANEL drew two borders.

   `.cims-filter-bar` was written as a standalone strip: its own surface,
   border, radius, shadow and padding — "a lighter-weight panel than a full
   .card". The `<x-filter-bar>` component then put it inside a real `.card`,
   so both painted their own box and the result was two rounded outlines 13px
   apart with two shadows. Reported as a double border; the pixels agreed —
   border colour at x=14 and again at x=27.

   Inside the card, the card IS the panel. The bar keeps only its layout.

   Scoped to `.cims-filter-card` on purpose: screens that still place
   `.cims-filter-bar` on its own, outside a card, are unaffected and keep the
   surface they were designed with. */
.cims-filter-card .cims-filter-bar {
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
}

/* And the card itself keeps the flat treatment it asks for: `.cims-filter-card
   { box-shadow: none }` is defined earlier in this file than the general
   `.card { box-shadow: var(--cims-shadow-sm) }`, so at equal specificity the
   general rule won and the filter card wore a shadow it had opted out of. */
.card.cims-filter-card { box-shadow: none; }

/* --- the verbs, coloured by WHAT THEY DO -----------------------------------
   Colour used to carry the DOCUMENT. On the row most people look at — an
   estimate not past level 1 — every button belongs to the estimate, so all of
   them came out the same blue and the row read as one long smear. Reported
   twice as "all the buttons are the same colour".

   The hue now says what pressing it does. Within one row a reader can find the
   verb they want without reading any of them:

     read     slate    View, Open, PDF — nothing changes
     edit     amber    changes the document
     approve  green    signs it off
     forward  blue     moves it to somebody else
     start    violet   raises a document that does not exist yet
     answer   teal     records a decision about what the job needs

   Still no white on colour — the ink is the dark member of each family, on a
   light wash of it, the rule the action buttons have followed since. */
.cims-verb--read     { --verb: 51 65 85; }      /* slate 700  */
.cims-verb--edit     { --verb: 146 64 14; }     /* amber 800  */
.cims-verb--approve  { --verb: 22 101 52; }     /* green 800  */
.cims-verb--forward  { --verb: 29 78 216; }     /* blue 700   */
.cims-verb--start    { --verb: 109 40 217; }    /* violet 700 */
.cims-verb--answer   { --verb: 17 94 89; }      /* teal 800   */
/* Return sits next to Forward on the same row, and direction is the only thing
   that separates them — so it takes the red the module pages already use for
   handing work back, not another shade of the blue that means "onward". */
.cims-verb--return   { --verb: 185 28 28; }     /* red 700    */

/* WHICH document, kept as a thin left edge on the buttons that name one.
   "Start PO" and "Start Tender" are both `start`, and the job of telling them
   apart belongs to something — but to a 3px edge, not to the whole button. */
.cims-verb-doc--po        { border-left: 3px solid rgb(109 40 217 / .9); }
.cims-verb-doc--tender    { border-left: 3px solid rgb(17 94 89 / .9); }
.cims-verb-doc--agreement { border-left: 3px solid rgb(146 64 14 / .9); }
.cims-verb-doc--mb        { border-left: 3px solid rgb(22 101 52 / .9); }
.cims-verb-doc--tracking  { border-left: 3px solid rgb(21 94 117 / .9); }
.cims-verb-doc--bill      { border-left: 3px solid rgb(190 18 60 / .9); }

/* --- the "what to print" dialog ------------------------------------------- */
/* A row per document: tick, icon, name, state. Absent documents are shown
   rather than hidden, because their ABSENCE is information — somebody checking
   whether the bill is in the file needs to see there is no bill, not wonder
   whether they mis-ticked something. */

.cims-dossier-hint {
    display: flex;
    gap: .5rem;
    align-items: flex-start;
    font-size: .8rem;
    line-height: 1.45;
    color: var(--bs-secondary-color);
    margin-bottom: .75rem;
}
.cims-dossier-hint .bi { color: var(--cims-color-primary); font-size: .95rem; line-height: 1.3; }

/* Count on the left, the control on the right, separated from the list by a
   rule — so the button belongs to the list it governs. */
.cims-dossier-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding-bottom: .55rem;
    margin-bottom: .6rem;
    border-bottom: 1px solid var(--bs-border-color);
}
.cims-dossier-toolbar__count {
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--bs-secondary-color);
}
/* Fixed width: "Select all" and "Select none" differ in length, and without it
   the button changed size under the pointer every time it was pressed. */
.cims-dossier-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .35rem;
    min-width: 7.5rem;
    font-size: .75rem;
    font-weight: 600;
    padding: .3rem .6rem;
    border-radius: .4rem;
}

/* The list scrolls rather than pushing the footer off a short screen. */
.cims-dossier-list { max-height: 19rem; overflow-y: auto; margin: 0 -.15rem; padding: 0 .15rem; }

.cims-dossier-row {
    display: flex;
    align-items: center;
    gap: .65rem;
    padding: .6rem .7rem;
    border: 1px solid var(--bs-border-color);
    border-radius: .5rem;
    margin-bottom: .4rem;
    cursor: pointer;
    transition: border-color .12s ease, background-color .12s ease, box-shadow .12s ease;
}
.cims-dossier-row:last-child { margin-bottom: 0; }
.cims-dossier-row:hover { border-color: var(--cims-color-primary); background: rgba(var(--cims-color-primary-rgb), .04); }

/* Ticked rows carry the accent, so what is in the file is readable without
   checking seven checkboxes one at a time. */
.cims-dossier-row:has(input:checked) {
    border-color: var(--cims-color-primary);
    background: rgba(var(--cims-color-primary-rgb), .06);
}
.cims-dossier-row:focus-within {
    box-shadow: 0 0 0 .2rem rgba(var(--cims-color-primary-rgb), .2);
    border-color: var(--cims-color-primary);
}

.cims-dossier-row__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 2rem;
    height: 2rem;
    border-radius: .45rem;
    background: rgba(var(--cims-color-primary-rgb), .1);
    color: var(--cims-color-primary);
    font-size: .95rem;
}

/* Not raised: quiet, and not clickable. The cursor says so before the pointer
   reaches the checkbox. */
.cims-dossier-row.is-absent { background: var(--bs-tertiary-bg); cursor: default; opacity: .7; }
.cims-dossier-row.is-absent:hover { border-color: var(--bs-border-color); background: var(--bs-tertiary-bg); }
.cims-dossier-row.is-absent .cims-dossier-row__icon { background: var(--bs-secondary-bg); color: var(--bs-secondary-color); }

.cims-dossier-row__text { display: flex; flex-direction: column; align-items: flex-start; gap: .2rem; min-width: 0; }
.cims-dossier-row__label { font-weight: 600; font-size: .85rem; line-height: 1.25; color: var(--cims-color-text); }

/* A pill, so the state reads as a state and not as part of the name. */
.cims-dossier-row__state {
    display: inline-block;
    font-size: .68rem;
    font-weight: 600;
    line-height: 1;
    padding: .2rem .45rem;
    border-radius: 999px;
    background: rgba(var(--cims-color-primary-rgb), .1);
    color: var(--cims-color-primary);
}
.cims-dossier-row__state.is-absent { background: var(--bs-secondary-bg); color: var(--bs-secondary-color); }

.cims-dossier-note {
    font-size: .73rem;
    color: var(--bs-secondary-color);
    margin: .7rem 0 0;
    padding-top: .6rem;
    border-top: 1px dashed var(--bs-border-color);
}

/* --- Bar Bending Schedule: derived cells ---------------------------------- */
/*
 * T8..T32 and the total are struck from the diameter, so they are readouts, not
 * fields. Styled to sit at the same height and alignment as the inputs beside
 * them — a derived cell that jumps the row height makes the table read as
 * broken — but deliberately without a border or a white ground, because the one
 * thing it must not look like is somewhere to type.
 */
.cims-bbs-derived {
    display: block;
    padding: .25rem .5rem;
    min-height: calc(1.5em + .5rem + 2px);
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--cims-color-text-muted);
}

.cims-bbs-derived.fw-semibold {
    color: var(--cims-color-text);
}

/* Why an estimate cannot be forwarded yet, under its own disabled button.

   Narrow and quiet: this is a checklist the drafter works through, not an
   error. It sits in an actions cell, so it is capped rather than allowed to
   stretch the column to the width of its longest sentence. */
.cims-forward-blockers {
    max-width: 22rem;
    margin-left: auto;
    text-align: left;
    color: var(--cims-color-text-muted-strong, #4d5566);
}

.cims-forward-blockers li {
    margin-bottom: 0.15rem;
}
