/* ==========================================================================
   DIMENSIONAL — the application-wide design language
   ==========================================================================

   Layered translucent surfaces over a soft radial-tinted canvas, real elevation on
   cards, and a small amount of literal 3D on browse cards. Enterprise-clean: no
   gradients on text, no neon, no decorative illustration beyond the brand mark.

   Why this is a sibling of design-system.css rather than an edit to it. That file
   already owns focus, motion preferences, the icon-button reset, the tab strip and the
   five *semantic* status roles (success/warning/danger/info/neutral), and roughly 180
   files depend on those names. Dimensional adds a different set of things — the
   surface/ink/accent ramp, the type scale, the depth steps, and the four-state
   *workflow* vocabulary (Open/Started/Completed/Close). Workflow state and semantic
   role are separate axes: a subject area can be Completed and still have a sync in a
   danger state. Keeping the two files apart keeps that separation legible, and keeps
   this one safe to read end-to-end.

   Load order is design-system.css then this, so where the two do overlap Dimensional
   wins on equal specificity.

   Colour is authored in oklch. It is perceptually uniform, so the dark counterparts at
   the bottom are the same hues at a different lightness rather than separately
   eyeballed hex values — which is exactly what let the two themes drift apart in
   common.css.
   ========================================================================== */

:root {
    /* ── Canvas and surfaces ────────────────────────────────────────────────
       The canvas is a very low-contrast radial tint, not a flat grey. That tint is what
       gives translucent surfaces something to be translucent *over*; on a flat fill,
       backdrop-filter has no information to carry and the depth model collapses into a
       slightly blurry flat design. */
    --canvas: radial-gradient(120% 90% at 12% 0%,
            oklch(0.97 0.02 266) 0%,
            oklch(0.96 0.012 250) 45%,
            oklch(0.95 0.01 190) 100%);

    /* Three surface weights, chosen by how much of what is behind should show through.

       surface         cards and panels — slightly translucent, sits on the canvas
       surface-solid   grids, menus, modals — opaque. Dense text needs a stable backdrop,
                       and a translucent grid row turns every hover tint muddy.
       surface-chrome  the top bar — most translucent, paired with the blur below. */
    --surface: oklch(1 0 0 / 0.88);
    --surface-solid: #ffffff;
    --surface-chrome: oklch(1 0 0 / 0.62);
    --chrome-blur: blur(8px);

    --border: oklch(0.92 0.01 250);
    --border-strong: oklch(0.89 0.01 250);

    /* The one dark surface in the light theme: the SQL editor plane. Deliberately not part
       of the surface/ink ramp above — it stays this same dark value in both themes, because
       a code editor that flips to white on light mode is the one place users reliably
       complain about eye strain. */
    --editor: oklch(0.24 0.02 265);

    /* Ink ramp. Three steps only. A fourth invariably ends up meaning "slightly less
       important than the third", which is a distinction nobody can actually see. */
    --ink: oklch(0.24 0.02 260);
    --ink-2: oklch(0.46 0.02 260);
    --ink-3: oklch(0.55 0.02 260);

    /* The ETERNITYARC brand blue, not a new colour. --accent is #1b3896 — the same value
       common.css has carried as --app-primary-color, and the hue the Services hero is
       built from (#0d2a6b → #16308a with a #3896ff glow). Expressed in oklch so the
       hover step and the dark counterpart are the same hue at a different lightness
       rather than three separately chosen blues.

       It is darker than a typical accent (L 0.385, where the ramp elsewhere sits near
       0.5). That is the brand's, not an oversight, and it buys a comfortable margin for
       white text on a filled button. */
    --accent: oklch(0.385 0.158 266);
    /* Hover goes *down* in lightness. On a fill this dark, lightening on hover reads as
       the button losing focus rather than gaining it. Lands on the hero's own #0d2a6b. */
    --accent-hover: oklch(0.325 0.135 264);
    --accent-soft: oklch(0.945 0.028 266);
    /* Text/icon colour that sits *on* an accent fill. A token rather than a literal
       white because it has to flip in dark mode, where the accent lightens. */
    --accent-ink: #ffffff;

    /* ── Workflow status ────────────────────────────────────────────────────
       The four states of the mastering lifecycle. Each is a fill + text + dot + border
       quadruple. The dot is always rendered next to the label, so state never depends
       on hue alone — that covers colour-blind users, greyscale printouts, and the
       screenshots that end up in tickets.

       Semantics, surfaced in the tooltip on every pill:
         Open       defined, no work started
         Started    mapping/matching in progress
         Completed  mastered and published
         Close      archived, read-only */
    --status-open-bg: oklch(0.96 0.03 250);
    --status-open-fg: oklch(0.38 0.13 250);
    --status-open-dot: oklch(0.55 0.15 250);
    --status-open-bd: oklch(0.9 0.04 250);

    --status-started-bg: oklch(0.96 0.05 80);
    --status-started-fg: oklch(0.44 0.12 62);
    --status-started-dot: oklch(0.7 0.15 70);
    --status-started-bd: oklch(0.9 0.07 80);

    --status-completed-bg: oklch(0.955 0.04 155);
    --status-completed-fg: oklch(0.38 0.1 155);
    --status-completed-dot: oklch(0.6 0.13 155);
    --status-completed-bd: oklch(0.89 0.06 155);

    --status-close-bg: oklch(0.955 0.005 260);
    --status-close-fg: oklch(0.38 0.015 260);
    --status-close-dot: oklch(0.6 0.015 260);
    --status-close-bd: oklch(0.9 0.008 260);

    /* ── Type ───────────────────────────────────────────────────────────────
       Three families with three jobs. Sora carries headings and any number meant to be
       read as a figure. Manrope carries running UI text. JetBrains Mono carries anything
       a user might copy or compare character-by-character — codes, IDs, counts — because
       a proportional font makes those genuinely harder to scan and 0/O and 1/l/I
       ambiguous. Each falls back to the app's existing Inter, so a blocked font CDN
       degrades the typography instead of breaking the layout. */
    --font-display: 'Sora', 'Inter', system-ui, sans-serif;
    --font-ui: 'Manrope', 'Inter', system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;

    --text-page: 27px;
    --text-section: 18px;
    --text-metric: 19px;
    --text-metric-lg: 26px;
    --text-body: 14px;
    --text-body-line: 1.5;
    --text-secondary: 13px;
    --text-micro: 11px;
    --text-micro-tracking: 0.08em;
    --text-code: 12px;
    --text-code-lg: 13px;

    /* ── Elevation ──────────────────────────────────────────────────────────
       Three steps and one accent step; depth comes from these and nothing else.

       Every shadow past e1 is two layers: a 1px contact shadow that anchors the element
       to the surface below, and a wide diffuse layer with a heavily negative spread that
       reads as distance from it. The negative spread is the important part — without it
       a large shadow becomes a grey halo around the whole card rather than something
       cast beneath it. */
    --e1: 0 1px 2px rgba(16, 24, 40, .05);
    --e2: 0 1px 2px rgba(16, 24, 40, .05), 0 6px 14px -8px rgba(16, 24, 40, .30);
    --e3: 0 1px 2px rgba(16, 24, 40, .05), 0 18px 36px -26px rgba(16, 24, 40, .40);
    /* For accent-filled controls. Tinting the shadow with the accent hue makes a primary
       button read as lit; a neutral grey shadow under a saturated fill reads as dirt. */
    --e-accent: 0 2px 5px oklch(0.385 0.158 266 / .30), 0 12px 24px -10px oklch(0.385 0.158 266 / .55);

    /* The literal 3D tilt used by browse cards. Held as tokens so the two places it is
       switched off — below 1024px, and under reduced motion — only reset these. */
    --card-perspective: 1400px;
    --card-tilt-x: 7deg;
    --card-tilt-y: -9deg;
    --card-tilt-shadow: -14px 18px 34px -18px rgba(16, 24, 40, .42);

    /* ── Radius ─────────────────────────────────────────────────────────────
       Four steps, assigned by role rather than by size, so a new control never has to
       guess which number to use. */
    --r-control: 8px;
    --r-button: 11px;
    --r-panel: 14px;
    --r-card: 18px;
    --r-pill: 999px;

    /* ── Spacing — 4px scale ────────────────────────────────────────────── */
    --s1: 4px;
    --s2: 8px;
    --s3: 12px;
    --s4: 16px;
    --s5: 20px;
    --s6: 24px;
    --s8: 32px;
    --s10: 40px;
    --card-pad: 18px;
    --panel-pad: 24px;
    --grid-gap: 16px;

    /* ── Motion ─────────────────────────────────────────────────────────────
       Deliberately slower than the --dnx-duration-* set in design-system.css. That set
       exists for small state flips (hover tints, tab underlines) where 120ms is right.
       Dimensional motion moves whole surfaces, and a surface that moves in 120ms reads
       as snapping rather than lifting. */
    --dim-ease: cubic-bezier(.2, .7, .3, 1);
    --dim-duration: 220ms;
    --dim-duration-slow: 300ms;
    --dim-lift: -2px;
    --dim-lift-card: -4px;

    /* ── Shell metrics ──────────────────────────────────────────────────────
       Referenced by the top bar, the dock and every page's sticky offsets. Held here so
       changing the bar height does not require finding every `top: 58px` in the app. */
    --topbar-h: 58px;
    --dock-w: 70px;
    --dock-w-expanded: 222px;
    --dock-tile: 44px;
    --content-pad: 24px;
    --row-h: 50px;
    --grid-header-h: 40px;
    --row-hover: oklch(0.985 0.01 266);

    /* ── Mapping Designer notice bar ────────────────────────────────────────
       The amber "unsaved draft restored" bar. Not part of the four-state workflow
       ramp above — this is a one-off transient notice, not a persisted status. */
    --notice-bg: oklch(0.98 0.01 90);
    --notice-border: oklch(0.9 0.02 90);
    --notice-ink: oklch(0.5 0.1 90);

    /* ── Mapping Designer palette/node category colours ─────────────────────
       Each pair is a tinted icon-tile background at ~0.93 lightness with a
       readable foreground at ~0.5, same oklch hue for both so light/dark only
       need the lightness swapped (done in the dark block below). Grouping
       mirrors DesignerPalette's Category field: Endpoints, Filters, and the
       Transformers bucket split further by sub-role (lookup/join/union,
       aggregate/route/SQL, normalize) purely for visual variety in a long list. */
    --cat-endpoint-bg: oklch(0.93 0.03 255);
    --cat-endpoint-fg: oklch(0.5 0.16 255);
    --cat-filter-bg: oklch(0.93 0.035 300);
    --cat-filter-fg: oklch(0.5 0.16 300);
    --cat-lookup-bg: oklch(0.93 0.045 155);
    --cat-lookup-fg: oklch(0.5 0.13 155);
    --cat-aggregate-bg: oklch(0.93 0.045 80);
    --cat-aggregate-fg: oklch(0.5 0.13 80);
    --cat-normalize-bg: oklch(0.93 0.04 290);
    --cat-normalize-fg: oklch(0.5 0.15 290);
}

/* Mapping Designer's primary colour is the Vision service accent (--vision-accent in
   common.css), not the EternityArc brand navy --accent above — this screen lives under the
   Vision module and should read as part of it. Scoped to the designer's own workspace
   wrapper so redefining --accent here doesn't bleed the Vision blue into unrelated
   screens that also read --accent. */
.dnx-designer-workspace {
    --accent: var(--vision-accent, #2b6fe0);
    --accent-hover: var(--vision-accent-deep, #0d2a6b);
    --accent-soft: var(--vision-accent-soft-bg, #e9f0ff);
    --accent-ink: #ffffff;
}

.dark-theme .dnx-designer-workspace {
    --accent: var(--vision-accent, #4f8cff);
    --accent-hover: var(--vision-accent-bright, #8fb6ff);
    --accent-soft: var(--vision-accent-soft-bg, rgba(79, 140, 255, .18));
    --accent-ink: oklch(0.17 0.02 264);
}

/* ── Dark counterparts ──────────────────────────────────────────────────────
   Same hues, inverted lightness. Three things do not simply invert:

   Surfaces get *lighter* than the canvas rather than more transparent. On a dark page a
   translucent white surface goes muddy grey and loses its edge entirely, so depth is
   carried by a raised fill instead of by transparency.

   Shadows go near-black and much stronger. A shadow that reads as depth at 5% alpha on
   white is simply invisible on a dark canvas.

   The accent lightens, and --accent-ink flips to dark. At L=0.5 the accent fails
   contrast as a text colour on a dark surface, and white text on a lightened accent
   fails against it — both ends have to move. */
.dark-theme {
    --canvas: radial-gradient(120% 90% at 12% 0%,
            oklch(0.22 0.025 266) 0%,
            oklch(0.19 0.018 260) 45%,
            oklch(0.17 0.012 240) 100%);

    --surface: oklch(0.26 0.015 265 / 0.88);
    --surface-solid: oklch(0.24 0.014 265);
    --surface-chrome: oklch(0.24 0.015 265 / 0.68);

    --border: oklch(0.34 0.014 265);
    --border-strong: oklch(0.42 0.016 265);

    --ink: oklch(0.95 0.008 260);
    --ink-2: oklch(0.76 0.012 260);
    --ink-3: oklch(0.64 0.014 260);

    /* #4f8cff — the value common.css already carries as --app-dark-primary-color, so the
       dark theme keeps the accent it has always had rather than gaining a second blue.
       Same brand hue, lifted from L 0.385 to 0.657 because the light accent is unreadable
       as text and muddy as a fill against a dark canvas. */
    --accent: oklch(0.657 0.182 262);
    --accent-hover: oklch(0.72 0.165 262);
    --accent-soft: oklch(0.30 0.055 264);
    /* Flips to near-black: white text on a fill this light fails contrast. */
    --accent-ink: oklch(0.17 0.02 264);

    --status-open-bg: oklch(0.3 0.05 250);
    --status-open-fg: oklch(0.84 0.09 250);
    --status-open-dot: oklch(0.7 0.14 250);
    --status-open-bd: oklch(0.4 0.07 250);

    --status-started-bg: oklch(0.3 0.06 70);
    --status-started-fg: oklch(0.86 0.1 75);
    --status-started-dot: oklch(0.78 0.14 70);
    --status-started-bd: oklch(0.42 0.08 70);

    --status-completed-bg: oklch(0.3 0.05 155);
    --status-completed-fg: oklch(0.85 0.09 155);
    --status-completed-dot: oklch(0.72 0.13 155);
    --status-completed-bd: oklch(0.41 0.07 155);

    --status-close-bg: oklch(0.3 0.006 260);
    --status-close-fg: oklch(0.8 0.01 260);
    --status-close-dot: oklch(0.66 0.01 260);
    --status-close-bd: oklch(0.4 0.008 260);

    --e1: 0 1px 2px rgba(0, 0, 0, .40);
    --e2: 0 1px 2px rgba(0, 0, 0, .40), 0 6px 14px -8px rgba(0, 0, 0, .70);
    --e3: 0 1px 2px rgba(0, 0, 0, .40), 0 18px 36px -26px rgba(0, 0, 0, .90);
    --e-accent: 0 2px 5px oklch(0.657 0.182 262 / .28), 0 12px 24px -10px oklch(0.657 0.182 262 / .45);
    --card-tilt-shadow: -14px 18px 34px -18px rgba(0, 0, 0, .75);

    --row-hover: oklch(0.29 0.018 266);
}

/* ==========================================================================
   TYPE ROLES
   ==========================================================================
   Named classes rather than element selectors. The app has headings at every level used
   for visual weight rather than document structure, so binding the scale to <h1>..<h4>
   would either restyle existing pages at random or force markup churn to fix semantics
   that are not this change's business. A page can adopt the role class today and have
   its heading levels corrected separately. */

.dim-page-title {
    /* !important is required, not stylistic. common.css applies
       `font-family: var(--app-font-family) !important` to h1-h6, p, span, div and every
       form control, which is every element these roles are ever used on. Manrope reaches
       the app by being that variable; Sora and the mono stack can only get there by
       outranking it. */
    font-family: var(--font-display) !important;
    font-size: var(--text-page);
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.2;
    color: var(--ink);
    margin: 0;
}

.dim-section-title {
    /* !important is required, not stylistic. common.css applies
       `font-family: var(--app-font-family) !important` to h1-h6, p, span, div and every
       form control, which is every element these roles are ever used on. Manrope reaches
       the app by being that variable; Sora and the mono stack can only get there by
       outranking it. */
    font-family: var(--font-display) !important;
    font-size: var(--text-section);
    font-weight: 600;
    line-height: 1.3;
    color: var(--ink);
    margin: 0;
}

/* Numerals in a metric are tabular so a column of figures aligns on the decimal and does
   not visibly reflow when a value ticks over. */
.dim-metric {
    /* !important is required, not stylistic. common.css applies
       `font-family: var(--app-font-family) !important` to h1-h6, p, span, div and every
       form control, which is every element these roles are ever used on. Manrope reaches
       the app by being that variable; Sora and the mono stack can only get there by
       outranking it. */
    font-family: var(--font-display) !important;
    font-size: var(--text-metric);
    font-weight: 600;
    line-height: 1.15;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

.dim-metric-lg {
    font-size: var(--text-metric-lg);
}

.dim-body {
    font-family: var(--font-ui);
    font-size: var(--text-body);
    font-weight: 500;
    line-height: var(--text-body-line, 1.5);
    color: var(--ink);
}

.dim-secondary {
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    font-weight: 500;
    line-height: 1.45;
    color: var(--ink-2);
}

/* The small uppercase label above a value or over a grid column. Uppercase at 11px needs
   the extra tracking to stay readable — without it the letterforms collide. */
.dim-micro {
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--text-micro-tracking, 0.08em);
    line-height: 1.4;
    color: var(--ink-3);
}

.dim-code {
    /* See the note on --font-display above: this has to outrank the blanket
       `font-family: … !important` in common.css or codes and IDs render proportional. */
    font-family: var(--font-mono) !important;
    font-size: var(--text-code);
    font-weight: 500;
    color: var(--ink-2);
    font-variant-numeric: tabular-nums;
}

.dim-code-lg {
    font-size: var(--text-code-lg);
}

/* ==========================================================================
   WORKFLOW STATUS PILL
   ==========================================================================
   Rendered by DnxStatusPill. Named .dim-pill rather than reusing .dnx-status so the two
   vocabularies stay visually distinguishable — they can appear in the same row. */

.dim-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    padding: 3px var(--s2);
    border-radius: var(--r-pill);
    border: 1px solid transparent;
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.5;
    white-space: nowrap;
    /* The pill is wrapped in an element carrying a title attribute for the semantics
       note; the help cursor is the only affordance that the tooltip exists. */
    cursor: help;
}

.dim-pill-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex: 0 0 auto;
}

.dim-pill-open {
    background: var(--status-open-bg);
    color: var(--status-open-fg);
    border-color: var(--status-open-bd);
}

.dim-pill-open .dim-pill-dot {
    background: var(--status-open-dot);
}

.dim-pill-started {
    background: var(--status-started-bg);
    color: var(--status-started-fg);
    border-color: var(--status-started-bd);
}

.dim-pill-started .dim-pill-dot {
    background: var(--status-started-dot);
}

.dim-pill-completed {
    background: var(--status-completed-bg);
    color: var(--status-completed-fg);
    border-color: var(--status-completed-bd);
}

.dim-pill-completed .dim-pill-dot {
    background: var(--status-completed-dot);
}

.dim-pill-close {
    background: var(--status-close-bg);
    color: var(--status-close-fg);
    border-color: var(--status-close-bd);
}

.dim-pill-close .dim-pill-dot {
    background: var(--status-close-dot);
}

/* Started is the only genuinely in-flight state, so it is the only one that animates.
   Animating all four would make the pulse mean nothing. */
.dim-pill-started .dim-pill-dot {
    animation: dimPillPulse 1.8s var(--dim-ease) infinite;
}

@keyframes dimPillPulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
}

@media (prefers-reduced-motion: reduce) {
    .dim-pill-started .dim-pill-dot {
        animation: none;
    }
}

/* ==========================================================================
   PAGE HEADER
   ========================================================================== */

.dim-page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s6);
    margin-bottom: var(--s6);
    flex-wrap: wrap;
}

.dim-page-header-text {
    min-width: 0;
}

.dim-page-description {
    margin: var(--s1) 0 0;
    max-width: 68ch;
}

.dim-page-header-actions {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-shrink: 0;
}

/* ==========================================================================
   BUTTON
   ==========================================================================
   min-height 36px at desktop, raised to 44px on coarse pointers at the bottom of this
   file — the WCAG 2.2 target size applies to touch, not to a mouse, and forcing 44px
   everywhere makes a desktop toolbar look like a phone app. */

.dim-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    min-height: 36px;
    padding: 0 var(--s4);
    border-radius: var(--r-button);
    border: 1px solid transparent;
    font-family: var(--font-ui);
    font-size: var(--text-body);
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    /* transform and box-shadow only. Transitioning background-color as well makes the
       hover feel laggy, because colour and position then arrive at different times. */
    transition: transform var(--dim-duration) var(--dim-ease),
                box-shadow var(--dim-duration) var(--dim-ease),
                background-color var(--dim-duration) var(--dim-ease),
                border-color var(--dim-duration) var(--dim-ease);
}

.dim-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

.dim-btn:not(:disabled):hover {
    transform: translateY(var(--dim-lift));
}

/* Pressing returns the control to the page. Without this the button lifts on hover and
   then does nothing on click, so the press has no physical feedback at all. */
.dim-btn:not(:disabled):active {
    transform: translateY(0);
}

.dim-btn-primary {
    background: var(--accent);
    color: var(--accent-ink);
    box-shadow: var(--e-accent);
}

.dim-btn-primary:not(:disabled):hover {
    background: var(--accent-hover);
}

.dim-btn-secondary {
    background: var(--surface);
    color: var(--ink);
    border-color: var(--border-strong);
    box-shadow: var(--e1);
}

.dim-btn-secondary:not(:disabled):hover {
    box-shadow: var(--e2);
    border-color: var(--accent);
    color: var(--accent);
}

.dim-btn-ghost {
    background: transparent;
    color: var(--ink-2);
}

.dim-btn-ghost:not(:disabled):hover {
    background: var(--accent-soft);
    color: var(--accent);
    /* A ghost button has no surface of its own, so it tints rather than lifts —
       a transparent element casting a shadow reads as a rendering bug. */
    transform: none;
}

.dim-btn-danger {
    background: var(--surface);
    color: var(--dnx-status-danger-fg);
    border-color: var(--dnx-status-danger-bd);
    box-shadow: var(--e1);
}

.dim-btn-danger:not(:disabled):hover {
    background: var(--dnx-status-danger-bg);
    box-shadow: var(--e2);
}

.dim-btn-block {
    width: 100%;
}

.dim-btn-spinner {
    width: 14px;
    height: 14px;
    flex: 0 0 auto;
    border-radius: 50%;
    /* currentColor for the arc so the spinner matches whichever variant it lands in
       without a per-variant rule. */
    border: 2px solid currentColor;
    border-right-color: transparent;
    opacity: 0.85;
    animation: dimSpin 0.7s linear infinite;
}

@keyframes dimSpin {
    to { transform: rotate(360deg); }
}

/* ==========================================================================
   FIELD
   ========================================================================== */

.dim-field {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
}

.dim-field-label {
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    font-weight: 600;
    color: var(--ink);
}

.dim-field-required {
    color: var(--dnx-status-danger-fg);
    margin-left: 2px;
}

/* The reserved row. min-height rather than a fixed height so a two-line error still fits;
   the point is that the *empty* state occupies space, not that the filled one is capped. */
.dim-field-message {
    min-height: 18px;
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 500;
    line-height: 1.5;
}

.dim-field-helper {
    color: var(--ink-3);
}

.dim-field-error {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    color: var(--dnx-status-danger-fg);
    font-weight: 600;
}

/* Styles whatever control the caller nested, so a field does not require a matching
   input component to look right. */
.dim-field :where(input, select, textarea, .dim-input) {
    width: 100%;
    min-height: 36px;
    padding: 0 var(--s3);
    border-radius: var(--r-control);
    border: 1px solid var(--border-strong);
    background: var(--surface-solid);
    color: var(--ink);
    font-family: var(--font-ui);
    font-size: var(--text-body);
    transition: border-color var(--dim-duration) var(--dim-ease),
                box-shadow var(--dim-duration) var(--dim-ease);
}

.dim-field :where(textarea) {
    padding: var(--s2) var(--s3);
    min-height: 84px;
    resize: vertical;
}

.dim-field-invalid :where(input, select, textarea, .dim-input) {
    border-color: var(--dnx-status-danger-bd);
}

/* ==========================================================================
   BROWSE CARD + GRID
   ==========================================================================
   The perspective lives on the grid, not on the card. Per-card perspective gives every
   card its own vanishing point directly in front of itself, so all of them tilt
   identically and the effect reads as a skew filter rather than as depth. */

.dim-card-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--grid-gap);
    perspective: var(--card-perspective);
    /* Without this the grid's own box clips the shadows the tilted cards cast to their
       left, which is the shadow that sells the tilt. */
    padding-bottom: var(--s2);
}

.dim-card {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    padding: var(--card-pad);
    border-radius: var(--r-card);
    background: var(--surface);
    border: 1px solid var(--border);
    color: inherit;
    text-align: left;
    text-decoration: none;
    box-shadow: var(--e2);
    transform-style: preserve-3d;
    transition: transform var(--dim-duration-slow) var(--dim-ease),
                box-shadow var(--dim-duration-slow) var(--dim-ease),
                border-color var(--dim-duration-slow) var(--dim-ease);
}

/* Interactive cards lift on hover/focus rather than tilting at rest. The 3D perspective
   tilt was this design's signature, but across two separate rounds of feedback on the
   live app it read as skewed/hard-to-scan rather than as depth — cut app-wide in favour
   of a flat card with the same elevation language as everything else in Dimensional. */
.dim-card-interactive {
    box-shadow: var(--e2);
    cursor: pointer;
    /* It is a <button>/<a>; strip the chrome that brings without changing the look. */
    appearance: none;
    font: inherit;
}

.dim-card-interactive:hover,
.dim-card-interactive:focus-visible {
    transform: translateY(var(--dim-lift-card));
    box-shadow: var(--e3);
    border-color: var(--accent);
}

.dim-card-interactive:active {
    transform: translateY(0);
    box-shadow: var(--e2);
}

.dim-card-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
}

.dim-card-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: var(--r-button);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 16px;
    flex: 0 0 auto;
}

.dim-card-name {
    /* !important is required, not stylistic. common.css applies
       `font-family: var(--app-font-family) !important` to h1-h6, p, span, div and every
       form control, which is every element these roles are ever used on. Manrope reaches
       the app by being that variable; Sora and the mono stack can only get there by
       outranking it. */
    font-family: var(--font-display) !important;
    font-size: 15px;
    font-weight: 600;
    color: var(--ink);
    line-height: 1.3;
    /* Two lines then ellipsis. Unbounded, a long subject-area name makes one card in the
       row taller than the rest and the whole grid loses its baseline. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.dim-card-code {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dim-card-metrics {
    display: flex;
    gap: var(--s4);
    margin-top: auto;
    padding-top: var(--s3);
    border-top: 1px solid var(--border);
}

.dim-card-metric {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}

.dim-card-metric-value {
    /* !important is required, not stylistic. common.css applies
       `font-family: var(--app-font-family) !important` to h1-h6, p, span, div and every
       form control, which is every element these roles are ever used on. Manrope reaches
       the app by being that variable; Sora and the mono stack can only get there by
       outranking it. */
    font-family: var(--font-display) !important;
    font-size: var(--text-metric);
    font-weight: 600;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}

/* ==========================================================================
   SKELETON
   ==========================================================================
   A moving highlight over a static base, not a pulsing opacity. Opacity pulsing on a
   list makes the whole block breathe, which draws more attention than the content that
   eventually replaces it. */

.dim-skeleton-set {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
}

.dim-skeleton {
    border-radius: var(--r-control);
    background: linear-gradient(90deg,
            var(--accent-soft) 0%,
            var(--surface-solid) 50%,
            var(--accent-soft) 100%);
    background-size: 200% 100%;
    animation: dimShimmer 1.4s var(--dim-ease) infinite;
}

.dim-skeleton-text { height: 12px; }
.dim-skeleton-row { height: var(--row-h); border-radius: var(--r-control); }
.dim-skeleton-card { height: 168px; border-radius: var(--r-card); }

@keyframes dimShimmer {
    from { background-position: 200% 0; }
    to { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .dim-skeleton {
        animation: none;
        background: var(--accent-soft);
    }
}

/* ==========================================================================
   RESPONSIVE
   ==========================================================================
   Two breakpoints, matching the brief. The card grid steps down a column at each, and
   the 3D tilt is dropped entirely below 1024px: a tilted card on a narrow screen wastes
   horizontal space it does not have, and the effect depends on seeing several cards at
   once, which by then you cannot. */

@media (max-width: 1279px) {
    .dim-card-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (max-width: 1023px) {
    .dim-card-grid {
        grid-template-columns: minmax(0, 1fr);
        perspective: none;
    }

    .dim-card-interactive {
        transform: none;
        box-shadow: var(--e2);
    }

    .dim-card-interactive:hover,
    .dim-card-interactive:focus-visible {
        transform: translateY(var(--dim-lift));
    }
}

/* Touch. Applies by input device rather than by width, because a large tablet is a touch
   device at a desktop width and its targets still need to be 44px. */
@media (pointer: coarse) {
    .dim-btn {
        min-height: 44px;
    }

    .dim-field :where(input, select, textarea, .dim-input) {
        min-height: 44px;
    }
}

@media (prefers-reduced-motion: reduce) {
    /* Transforms off, opacity and colour kept — the state change still has to register. */
    .dim-card-interactive,
    .dim-card-interactive:hover,
    .dim-card-interactive:focus-visible,
    .dim-card-interactive:active,
    .dim-btn:hover,
    .dim-btn:active {
        transform: none;
    }
}

/* ==========================================================================
   UTILITY
   ==========================================================================
   Visible to assistive tech, not to the eye. The 1px-clip technique rather than
   display:none or visibility:hidden — both of those remove the element from the
   accessibility tree too, which defeats the entire purpose. */

.dim-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ==========================================================================
   DATA GRID
   ========================================================================== */

.dim-grid {
    background: var(--surface-solid);
    border: 1px solid var(--border);
    border-radius: var(--r-panel);
    box-shadow: var(--e1);
    /* Clips the sticky header's corners to the panel radius. Without it the header's
       fill squares off the top two corners as soon as the body scrolls. */
    overflow: hidden;
}

/* The scroll container, not the table. A table cannot scroll itself, and position:sticky
   on the header resolves against the nearest scrolling ancestor — so this element is what
   makes the sticky header work at all. */
.dim-grid-scroll {
    overflow: auto;
    max-height: var(--grid-max-height, none);
}

.dim-grid-table {
    width: 100%;
    border-collapse: separate;
    /* separate + 0 rather than collapse: border-collapse:collapse drops the borders of
       sticky cells in every browser, so the header loses its bottom rule on scroll. */
    border-spacing: 0;
    font-family: var(--font-ui);
    font-size: 13.5px;
    color: var(--ink);
}

.dim-grid-table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    height: var(--grid-header-h);
    padding: 0 var(--s3);
    text-align: left;
    /* Opaque, not var(--surface): rows scrolling underneath a translucent header are
       visible through it. */
    background: var(--surface-solid);
    border-bottom: 1px solid var(--border-strong);
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--text-micro-tracking);
    color: var(--ink-3);
    white-space: nowrap;
}

.dim-grid-sort {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    color: inherit;
    cursor: pointer;
}

.dim-grid-sort:hover {
    color: var(--accent);
}

/* The inactive caret is dimmed rather than hidden, so a sortable column advertises itself
   before you hover it. */
.dim-grid-sort .bi-chevron-expand {
    opacity: 0.4;
}

.dim-grid-th-numeric,
.dim-grid-td-numeric {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.dim-grid-row {
    height: var(--row-h);
    transition: background-color var(--dnx-duration-fast) var(--dim-ease);
}

.dim-grid-table tbody td {
    padding: 0 var(--s3);
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

/* Last row keeps the panel's rounded bottom edge instead of a rule against it. */
.dim-grid-table tbody tr:last-child td {
    border-bottom: 0;
}

.dim-grid-row:hover {
    background: var(--row-hover);
}

.dim-grid-row.is-selected {
    background: var(--accent-soft);
}

.dim-grid-check {
    width: 44px;
    text-align: center;
}

.dim-grid-actions-col {
    width: 52px;
    text-align: right;
}

/* The mono secondary line under a name — a code, an id, a source system. */
.dim-grid-name {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}

.dim-grid-name-primary {
    font-weight: 600;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Skeleton rows carry no border, so a loading grid does not draw six rules across an
   area that has no data in it yet. */
.dim-grid-row-skeleton td {
    border-bottom: 0 !important;
    padding: var(--s1) var(--s3);
}

.dim-grid-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s3);
    padding: var(--s10) var(--s6);
    text-align: center;
    color: var(--ink-2);
}

.dim-grid-state-icon {
    font-size: 28px;
    color: var(--ink-3);
}

.dim-grid-footer {
    border-top: 1px solid var(--border);
    background: var(--surface-solid);
}

/* ==========================================================================
   FILTER BAR
   ========================================================================== */

.dim-filterbar {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex-wrap: wrap;
    padding: var(--s3) 0;
}

.dim-filterbar-search {
    position: relative;
    flex: 1 1 240px;
    max-width: 360px;
    display: flex;
    align-items: center;
}

.dim-filterbar-search .bi-search {
    position: absolute;
    left: var(--s3);
    color: var(--ink-3);
    font-size: 13px;
    /* The icon sits over the input, so it would otherwise swallow the click that should
       focus the field. */
    pointer-events: none;
}

.dim-filterbar-search .dim-input {
    width: 100%;
    min-height: 36px;
    padding: 0 var(--s3) 0 var(--s8);
    border-radius: var(--r-button);
    border: 1px solid var(--border-strong);
    background: var(--surface-solid);
    color: var(--ink);
    font-family: var(--font-ui);
    font-size: var(--text-body);
}

.dim-filterbar-chips {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
}

.dim-filterbar-end {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin-left: auto;
}

/* A chip is the pill's colours on a control. Inactive it is outline-only, so the row of
   chips does not compete with the actual status pills in the grid below it. */
.dim-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    min-height: 30px;
    padding: 0 var(--s3);
    border-radius: var(--r-pill);
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--ink-2);
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    font-weight: 600;
    cursor: pointer;
    transition: background-color var(--dnx-duration-fast) var(--dim-ease),
                border-color var(--dnx-duration-fast) var(--dim-ease),
                color var(--dnx-duration-fast) var(--dim-ease);
}

/* Active takes the status's own fill, so the filter and the rows it selects are visibly
   the same thing. The .dim-pill-* modifier supplies the colours; only the active state
   opts into them. */
.dim-chip.is-active.dim-pill-open {
    background: var(--status-open-bg);
    color: var(--status-open-fg);
    border-color: var(--status-open-bd);
}

.dim-chip.is-active.dim-pill-started {
    background: var(--status-started-bg);
    color: var(--status-started-fg);
    border-color: var(--status-started-bd);
}

.dim-chip.is-active.dim-pill-completed {
    background: var(--status-completed-bg);
    color: var(--status-completed-fg);
    border-color: var(--status-completed-bd);
}

.dim-chip.is-active.dim-pill-close {
    background: var(--status-close-bg);
    color: var(--status-close-fg);
    border-color: var(--status-close-bd);
}

/* Inactive chips still show their status dot, in the status colour — that is what lets
   you find the "Started" chip without reading all four labels. */
.dim-chip.dim-pill-open .dim-pill-dot { background: var(--status-open-dot); }
.dim-chip.dim-pill-started .dim-pill-dot { background: var(--status-started-dot); }
.dim-chip.dim-pill-completed .dim-pill-dot { background: var(--status-completed-dot); }
.dim-chip.dim-pill-close .dim-pill-dot { background: var(--status-close-dot); }

/* The chip's pulse is suppressed: four chips, one of them throbbing permanently, is a
   distraction in a control strip. The pulse belongs on the row, where it means "this
   record is moving". */
.dim-chip.dim-pill-started .dim-pill-dot {
    animation: none;
}

.dim-chip-count {
    color: inherit;
    opacity: 0.72;
}

/* ==========================================================================
   PAGINATION
   ========================================================================== */

.dim-pagination {
    display: flex;
    align-items: center;
    gap: var(--s4);
    padding: var(--s2) var(--s3);
}

.dim-pagination-selection {
    color: var(--accent);
    font-weight: 600;
}

.dim-pagination-nav {
    display: flex;
    align-items: center;
    gap: var(--s1);
    margin-left: auto;
}

.dim-pagination-current {
    padding: 0 var(--s2);
    color: var(--ink-2);
}

/* ==========================================================================
   BREADCRUMB
   ========================================================================== */

.dim-breadcrumb ol {
    display: flex;
    align-items: center;
    gap: var(--s2);
    list-style: none;
    margin: 0;
    padding: 0;
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    font-weight: 500;
}

.dim-breadcrumb li {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-width: 0;
}

.dim-breadcrumb a {
    color: var(--ink-2);
    text-decoration: none;
}

.dim-breadcrumb a:hover {
    color: var(--accent);
    text-decoration: underline;
}

.dim-breadcrumb [aria-current="page"] {
    color: var(--ink);
    font-weight: 600;
}

.dim-breadcrumb-sep {
    color: var(--ink-3);
}

/* ==========================================================================
   KPI CARD + SPARKLINE
   ========================================================================== */

.dim-kpi {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    padding: var(--card-pad);
    border-radius: var(--r-card);
    background: var(--surface);
    border: 1px solid var(--border);
    box-shadow: var(--e2);
    /* Flat, not tilted: a KPI is a readout, and tilting a number makes it fractionally
       harder to read in exchange for nothing. */
}

.dim-kpi-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
}

.dim-kpi-icon {
    color: var(--ink-3);
    font-size: 15px;
}

.dim-kpi-value-row {
    display: flex;
    align-items: baseline;
    gap: var(--s2);
}

.dim-kpi-delta {
    display: inline-flex;
    align-items: center;
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    font-weight: 700;
}

.dim-kpi-delta.is-up { color: var(--status-completed-fg); }
.dim-kpi-delta.is-down { color: var(--dnx-status-danger-fg); }

.dim-kpi-progress {
    height: 6px;
    border-radius: var(--r-pill);
    background: var(--accent-soft);
    overflow: hidden;
}

.dim-kpi-progress-fill {
    height: 100%;
    background: var(--accent);
    border-radius: inherit;
    transition: width var(--dim-duration-slow) var(--dim-ease);
}

/* Reserves the trend slot on cards that have neither a bar nor a series, so one card in
   a KPI row is never shorter than the others. */
.dim-kpi-spacer {
    height: 28px;
}

.dim-sparkline {
    width: 100%;
    height: 28px;
    display: block;
    overflow: visible;
}

.dim-sparkline-line {
    fill: none;
    stroke: var(--accent);
    stroke-width: 1.5;
    /* Non-scaling so the stroke stays 1.5px despite preserveAspectRatio="none" stretching
       the viewBox horizontally — without it the line thins out on a wide card. */
    vector-effect: non-scaling-stroke;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.dim-sparkline-fill {
    fill: var(--accent-soft);
    stroke: none;
}

/* ==========================================================================
   FORM SECTION + ACTIONS + ERROR SUMMARY
   ========================================================================== */

.dim-form-section {
    /* A <fieldset> carries browser default chrome that no amount of padding fixes. */
    border: 0;
    margin: 0 0 var(--s8);
    padding: 0;
    min-width: 0;
}

.dim-form-section-legend {
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* A <legend> is not a normal block in every engine; float:none + width:100% is what
       makes it lay out predictably as a heading row above the grid. */
    float: none;
    width: 100%;
    padding: 0;
    margin-bottom: var(--s4);
    padding-bottom: var(--s2);
    border-bottom: 1px solid var(--border);
}

.dim-form-section-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s4) var(--s5);
}

.dim-form-section-single .dim-form-section-grid {
    grid-template-columns: minmax(0, 1fr);
}

@media (max-width: 1279px) {
    .dim-form-section-grid {
        grid-template-columns: minmax(0, 1fr);
    }
}

.dim-form-actions {
    position: sticky;
    bottom: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: var(--s4);
    padding: var(--s3) var(--s4);
    /* Chrome weight plus a blur, so form content scrolling underneath is legibly behind
       the bar rather than showing through it. */
    background: var(--surface-chrome);
    backdrop-filter: var(--chrome-blur);
    -webkit-backdrop-filter: var(--chrome-blur);
    border-top: 1px solid var(--border);
    border-radius: 0 0 var(--r-panel) var(--r-panel);
}

.dim-form-actions-note {
    min-width: 0;
}

.dim-form-actions-buttons {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin-left: auto;
}

.dim-error-summary {
    padding: var(--s4);
    margin-bottom: var(--s6);
    border-radius: var(--r-panel);
    background: var(--dnx-status-danger-bg);
    border: 1px solid var(--dnx-status-danger-bd);
    color: var(--dnx-status-danger-fg);
}

/* tabindex="-1" makes it programmatically focusable; without this it would also draw a
   ring when clicked, which it should not — it is not a control. */
.dim-error-summary:focus {
    outline: none;
}

.dim-error-summary-head {
    display: flex;
    align-items: center;
    gap: var(--s2);
    margin-bottom: var(--s2);
}

.dim-error-summary-list {
    margin: 0;
    padding-left: var(--s5);
    font-family: var(--font-ui);
    font-size: var(--text-body);
}

.dim-error-summary-list a {
    color: inherit;
    font-weight: 600;
}

/* ==========================================================================
   TRUST BAR
   ========================================================================== */

.dim-trustbar {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-width: 120px;
}

.dim-trustbar-track {
    flex: 1 1 auto;
    height: 6px;
    border-radius: var(--r-pill);
    background: var(--dnx-status-neutral-bg);
    overflow: hidden;
}

.dim-trustbar-fill {
    height: 100%;
    border-radius: inherit;
    transition: width var(--dim-duration-slow) var(--dim-ease);
}

/* Banded by score, because the useful reading is "is this trustworthy", not "how long is
   this bar". The band is also named in the tooltip, so colour is never the only carrier. */
.dim-trustbar-high { background: var(--status-completed-dot); }
.dim-trustbar-medium { background: var(--status-started-dot); }
.dim-trustbar-low { background: var(--dnx-status-danger-fg); }

.dim-trustbar-value {
    flex: 0 0 auto;
    min-width: 24px;
    text-align: right;
    color: var(--ink);
}

/* ==========================================================================
   MOBILE — grid becomes a stacked card list
   ==========================================================================
   Below 768px a 6-column table either scrolls horizontally (so half the columns are
   permanently invisible) or squeezes every column to unreadable. Neither is a list.

   Reflowing with display:block would flatten the table's semantics along with its layout,
   so the roles are restated explicitly: the element stops being a table visually but keeps
   announcing as one. Each cell carries its column name via a data-label attribute the
   caller sets, which is what replaces the now-hidden header. */

@media (max-width: 767px) {
    .dim-grid-table,
    .dim-grid-table tbody,
    .dim-grid-table tr,
    .dim-grid-table td {
        display: block;
        width: 100%;
    }

    .dim-grid-table thead {
        /* Hidden visually, still in the accessibility tree — the column names are what the
           data-label pseudo-elements below are echoing. */
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
    }

    .dim-grid-row {
        height: auto;
        padding: var(--s3);
        margin-bottom: var(--s2);
        border: 1px solid var(--border);
        border-radius: var(--r-panel);
        background: var(--surface);
        box-shadow: var(--e1);
    }

    .dim-grid-table tbody td {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--s3);
        padding: var(--s1) 0;
        border-bottom: 0;
        text-align: right;
    }

    .dim-grid-table tbody td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 auto;
        font-size: var(--text-micro);
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: var(--text-micro-tracking);
        color: var(--ink-3);
        text-align: left;
    }

    .dim-grid-actions-col,
    .dim-grid-check {
        width: auto;
    }

    .dim-page-header-actions {
        width: 100%;
    }

    .dim-form-actions {
        /* Full-width buttons on a phone: two 36px buttons side by side at 375px leaves
           each of them below the comfortable tap width. */
        flex-direction: column;
        align-items: stretch;
    }

    .dim-form-actions-buttons {
        margin-left: 0;
        flex-direction: column-reverse;
    }

    .dim-form-actions-buttons .dim-btn {
        width: 100%;
    }
}

/* ==========================================================================
   STYLE GUIDE (/design-system)
   ==========================================================================
   Layout for the living style guide only. Kept here rather than in a <style> block on the
   page so the guide is built from the same stylesheet it documents — a page that styles
   itself privately can look correct while the shared file is broken. */

.ds-page {
    max-width: 1180px;
    margin: 0 auto;
    padding: var(--content-pad);
    background: transparent;
}

.ds-section {
    margin-bottom: var(--s10);
    padding-bottom: var(--s8);
    border-bottom: 1px solid var(--border);
}

.ds-section > .dim-section-title {
    margin-bottom: var(--s2);
}

.ds-section > .dim-secondary {
    max-width: 76ch;
    margin: 0 0 var(--s5);
}

.ds-subhead {
    display: block;
    margin: var(--s6) 0 var(--s3);
}

.ds-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s3);
    margin-bottom: var(--s4);
}

.ds-stack {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
}

/* The type specimens put the sample and its spec on one baseline, so the spec reads as an
   annotation rather than as a second sample. */
.ds-stack > div {
    display: flex;
    align-items: baseline;
    gap: var(--s3);
    flex-wrap: wrap;
}

.ds-two-col {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s6);
}

.ds-swatches {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
    gap: var(--s4);
}

.ds-swatch {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
}

/* Checkerboard behind each chip, so the translucent surface tokens visibly read as
   translucent instead of as slightly-off white. */
.ds-swatch-chip {
    height: 56px;
    border-radius: var(--r-panel);
    border: 1px solid var(--border);
    background-image:
        linear-gradient(45deg, var(--border) 25%, transparent 25%),
        linear-gradient(-45deg, var(--border) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--border) 75%),
        linear-gradient(-45deg, transparent 75%, var(--border) 75%);
    background-size: 12px 12px;
    background-position: 0 0, 0 6px, 6px -6px, -6px 0;
}

.ds-elevations {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s6);
    /* The diffuse layers need room, or adjacent samples clip each other's shadows and all
       four look like the same step. */
    padding: var(--s4) var(--s2) var(--s8);
}

.ds-elevation {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 150px;
    height: 84px;
    border-radius: var(--r-panel);
    background: var(--surface-solid);
    border: 1px solid var(--border);
}

.ds-status-item {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex: 1 1 260px;
}

.ds-kpi-row {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--grid-gap);
}

.ds-state-switch {
    margin: var(--s4) 0 var(--s3);
}

.ds-form-panel {
    padding: var(--panel-pad) var(--panel-pad) 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-panel);
    box-shadow: var(--e1);
    /* The sticky footer sticks to this panel's own scroll box, not the page's. */
    overflow: hidden;
}

.ds-trust-demo {
    max-width: 320px;
}

@media (max-width: 1279px) {
    .ds-kpi-row {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (max-width: 767px) {
    .ds-kpi-row,
    .ds-two-col {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ==========================================================================
   DROPDOWN (DnxSelect)
   ==========================================================================
   Overrides the DnxSelect styling in common.css, which predates the design language.
   That block hard-codes Tailwind's blue-500/700 (#3b82f6, #1d4ed8) for selection, a
   GitHub-ish dark palette (#0d1117, #161b22) that matches nothing else in the dark
   theme, single-layer shadows, and radii of 6/8/10px — none of which are on the scale.
   The result read as a control borrowed from another product.

   Targets the same class names rather than renaming them, so all existing usages pick
   this up without touching a single call site. Specificity is matched exactly and this
   file loads later, so it wins on source order.

   The native <select> is handled separately, via the --m3-field-* tokens in common.css.
   A native select's *open* option list is drawn by the operating system and cannot be
   styled at all — which is precisely why anything that needs a styled menu has to be
   this component rather than a <select>. */

.dnx-select-trigger {
    min-height: 44px;
    padding: 0 var(--s3);
    background-color: var(--surface-solid);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-control);
    color: var(--ink);
    font-family: var(--font-ui);
    font-size: var(--text-body);
    font-weight: 500;
    box-shadow: var(--e1);
    transition: border-color var(--dim-duration) var(--dim-ease),
                box-shadow var(--dim-duration) var(--dim-ease);
}

.dnx-select-sm .dnx-select-trigger {
    min-height: 36px;
    font-size: var(--text-secondary);
}

.dnx-select-trigger:hover:not(:disabled) {
    border-color: var(--accent);
}

/* Open is a distinct state from hover: the trigger and its menu are one surface, so the
   trigger keeps the accent border for as long as the menu is showing. */
.dnx-select-trigger[aria-expanded="true"] {
    border-color: var(--accent);
    box-shadow: var(--e2);
}

.dnx-select-trigger.is-invalid {
    border-color: var(--dnx-status-danger-bd);
}

.dnx-select-chevron {
    color: var(--ink-3);
    font-size: 0.8rem;
    transition: transform var(--dim-duration) var(--dim-ease),
                color var(--dim-duration) var(--dim-ease);
}

.dnx-select-chevron.open {
    transform: rotate(180deg);
    color: var(--accent);
}

.dark-theme .dnx-select-trigger {
    background-color: var(--surface-solid);
    border-color: var(--border-strong);
    color: var(--ink);
}

.dark-theme .dnx-select-chevron {
    color: var(--ink-3);
}

/* --- Menu ---------------------------------------------------------------- */

.dnx-select-menu {
    top: calc(100% + var(--s1));
    padding: var(--s1);
    border: 1px solid var(--border);
    border-radius: var(--r-panel);
    /* Solid, not var(--surface). A menu overlaps arbitrary content; at 88% opacity the
       text underneath shows through and the options become unreadable. */
    background-color: var(--surface-solid);
    box-shadow: var(--e3);
    max-height: 288px;
}

.dark-theme .dnx-select-menu {
    border-color: var(--border);
    background-color: var(--surface-solid);
    box-shadow: var(--e3);
}

/* The option is now an <li role="option">, not a <button>. A listbox's children have to
   be options — a button nested inside one is announced twice and breaks the "3 of 12"
   position count that makes a long list navigable. The consequence for styling is that
   :disabled no longer applies, so the disabled state keys off aria-disabled, which is
   the attribute actually carrying the meaning. */
.dnx-select-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    width: 100%;
    padding: var(--s2) var(--s3);
    border-radius: var(--r-control);
    color: var(--ink);
    font-family: var(--font-ui);
    font-size: var(--text-body);
    font-weight: 500;
    text-align: left;
    cursor: pointer;
    transition: background-color var(--dnx-duration-fast) var(--dim-ease),
                color var(--dnx-duration-fast) var(--dim-ease);
}

.dnx-select-item:hover:not([aria-disabled="true"]) {
    background-color: var(--accent-soft);
    color: var(--accent);
}

.dnx-select-item[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
}

/* The keyboard's active option. Styled identically to hover so there is one "this is the
   one you are about to choose" appearance, whichever input device you are using — two
   different highlights on screen at once is the usual bug in a hand-rolled dropdown. */
.dnx-select-item.is-active {
    background-color: var(--accent-soft);
    color: var(--accent);
}

.dnx-select-item.selected {
    background-color: var(--accent-soft);
    color: var(--accent);
    font-weight: 600;
}

.dnx-select-check {
    color: var(--accent);
    font-size: 0.95rem;
}

.dark-theme .dnx-select-item {
    color: var(--ink);
}

.dark-theme .dnx-select-item:hover:not([aria-disabled="true"]),
.dark-theme .dnx-select-item.is-active,
.dark-theme .dnx-select-item.selected {
    background-color: var(--accent-soft);
    color: var(--accent);
}

.dark-theme .dnx-select-check {
    color: var(--accent);
}

.dnx-select-group-label {
    padding: var(--s2) var(--s3) var(--s1);
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--text-micro-tracking);
    color: var(--ink-3);
}

.dark-theme .dnx-select-group-label {
    color: var(--ink-3);
}

/* A menu longer than its box needs to say so. Without a visible track the list looks
   complete at whatever option happens to land on the bottom edge. */
.dnx-select-menu {
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}

/* Style guide only: the dropdown gallery. Each cell is given room below for the menu to
   open into, so the samples do not overlap each other when several are opened. */
.ds-dropdown-row {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--s5);
    align-items: start;
}

.ds-dropdown-row > div {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
}

/* ==========================================================================
   FOCUS — no highlight when you click into a field
   ==========================================================================
   Clicking into an input used to paint a 3px accent glow, recolour the border and run a
   .28s animation (the M3 field rules in common.css, plus a dozen per-screen :focus
   rules). A mouse user already knows which box they just clicked; the highlight is
   noise, and on a dense form several of them in sequence read as errors.

   Removed for pointer focus only, via :focus:not(:focus-visible).

   :focus fires on click and on Tab alike. :focus-visible fires only when the browser
   judges an indicator is actually needed — keyboard navigation, essentially. Splitting
   on it is what lets the highlight disappear for the mouse while a keyboard user keeps
   the only cue they have about where they are. Deleting the rules outright would have
   taken both, and design-system.css enforces the keyboard ring with !important
   precisely because 33 hand-written `outline: none` declarations had already done that
   once.

   !important is required to outrank the M3 rules, which set these with !important
   themselves.

   The invalid state is excluded: a field showing an error keeps its red border whether
   or not it has focus, because that border is content, not decoration. */

html[data-modality="pointer"] :is(input,
    select,
    textarea,
    .form-control,
    .form-select,
    .dim-input,
    .dnx-select-trigger):focus:not(.is-invalid):not([aria-invalid="true"]) {
    box-shadow: none !important;
    border-color: var(--border-strong) !important;
    /* The design-system.css accessibility ring is suppressed here too, and only here.
       That rule carries !important deliberately — 33 hand-written `outline: none`
       declarations had already removed keyboard focus once across this app. This
       selector is (0,2,1) against its (0,1,1), so it wins on specificity rather than by
       weakening the floor: the moment data-modality flips back to keyboard, the ring
       returns everywhere, untouched. */
    outline: none !important;
    /* The M3 focus rule also nudges the select chevron down a few pixels and runs a
       .28s m3FieldFocus animation. Both are part of the same acknowledgement and go
       with it. */
    animation: none !important;
    background-position: right 13px center !important;
}

/* The custom dropdown's trigger is a <button>, so the M3 field rules never reach it.
   Its *open* state keeps its accent border — that is not decoration, it says a menu is
   on screen — but a closed, clicked trigger looks like any other resting control. */
html[data-modality="pointer"] .dnx-select-trigger:focus:not([aria-expanded="true"]) {
    border-color: var(--border-strong) !important;
    box-shadow: var(--e1) !important;
    outline: none !important;
}

/* Text selection inside a field is untouched. That highlight is the user's own
   selection, not a focus indicator, and losing it would break copy and paste. */

/* ==========================================================================
   APPLICATION ADOPTION
   ==========================================================================
   Everything above defines the design language and the new primitives. This section
   applies it to the ~90 routes and 185 components that already exist.

   It works by re-styling the class names those screens already use rather than by
   editing them. The usage counts across the .razor files are lopsided enough to make
   that the obvious approach: 273 `form-control`, 243 `btn`, 130 `common-btn`,
   130 `badge`, 111 `alert`, 52 `common-card`. Roughly two dozen selectors here reach
   every screen in the product; covering the same ground by hand would be 185 file edits
   and 185 chances to break logic that is not supposed to change at all.

   Scope is strictly paint: colour, type, radius, shadow, border. No display, position,
   width, margin or flex property is touched anywhere in this section — those hold the
   existing layouts together, and this is a presentation change.

   Bootstrap loads from a CDN before this file and the `common-*` rules live in
   common.css, which also loads earlier, so equal-specificity selectors here win on
   source order. !important appears only where the rule being replaced already used it.
   ========================================================================== */

/* -- Canvas ----------------------------------------------------------------
   The app painted a fixed radial gradient in literal hex (#f7f9fc to #e2e7f0) that
   never responded to the dark theme. Swapped for the token, which does. Declared as
   background-image so it keeps beating the !important background-color on the same
   rule without needing !important of its own. */
body {
    background-image: var(--canvas) !important;
    background-attachment: fixed;
    color: var(--ink);
}

/* -- Buttons ---------------------------------------------------------------
   Two parallel button systems existed: Bootstrap .btn-* and the app .common-btn-*.
   Both are brought onto one shape, radius and elevation, so a dialog containing one of
   each stops looking like two dialogs. */

.btn,
.common-btn {
    border-radius: var(--r-button);
    font-family: var(--font-ui);
    font-weight: 600;
    transition: transform var(--dim-duration) var(--dim-ease),
                box-shadow var(--dim-duration) var(--dim-ease),
                background-color var(--dim-duration) var(--dim-ease),
                border-color var(--dim-duration) var(--dim-ease);
}

.btn:not(:disabled):hover,
.common-btn:not(:disabled):hover {
    transform: translateY(var(--dim-lift));
}

.btn:not(:disabled):active,
.common-btn:not(:disabled):active {
    transform: translateY(0);
}

/* Bootstrap generates .btn-primary from a literal hex rather than from --bs-primary,
   so overriding that variable alone would never have reached it. The rule has to be
   explicit. */
.btn-primary,
.common-btn-primary {
    background-color: var(--accent);
    border-color: var(--accent);
    color: var(--accent-ink);
    box-shadow: var(--e-accent);
}

.btn-primary:not(:disabled):hover,
.btn-primary:not(:disabled):focus,
.common-btn-primary:not(:disabled):hover {
    background-color: var(--accent-hover);
    border-color: var(--accent-hover);
    color: var(--accent-ink);
}

.btn-secondary,
.btn-outline-secondary,
.common-btn-secondary {
    background-color: var(--surface);
    border-color: var(--border-strong);
    color: var(--ink);
    box-shadow: var(--e1);
}

.btn-secondary:not(:disabled):hover,
.btn-outline-secondary:not(:disabled):hover,
.common-btn-secondary:not(:disabled):hover {
    background-color: var(--surface-solid);
    border-color: var(--accent);
    color: var(--accent);
    box-shadow: var(--e2);
}

/* The semantic variants keep their meaning but take the shared status palette, so
   danger is one red across Bootstrap buttons, common buttons, badges and alerts
   instead of four different ones. */
.btn-success, .common-btn-success {
    background-color: var(--dnx-status-success-fg);
    border-color: var(--dnx-status-success-fg);
    color: #fff;
}

.btn-danger, .common-btn-danger {
    background-color: var(--dnx-status-danger-fg);
    border-color: var(--dnx-status-danger-fg);
    color: #fff;
}

.btn-warning, .common-btn-warning {
    background-color: var(--dnx-status-warning-fg);
    border-color: var(--dnx-status-warning-fg);
    color: #fff;
}

.btn-link {
    color: var(--accent);
    text-decoration: none;
}

.btn-link:hover {
    color: var(--accent-hover);
    text-decoration: underline;
}

/* Small buttons take the control radius, not the button radius: 11px on a 28px-tall
   control reads as a pill by accident. */
.btn-sm,
.common-btn-sm {
    border-radius: var(--r-control);
    font-size: var(--text-secondary);
}

/* -- Cards and panels ---------------------------------------------------- */

.card,
.common-card {
    border-radius: var(--r-panel);
    border: 1px solid var(--border);
    background-color: var(--surface);
    box-shadow: var(--e1);
}

.card-header,
.common-card-header {
    background-color: transparent;
    border-bottom: 1px solid var(--border);
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--ink);
}

.common-title,
.card-title {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--ink);
}

/* The brief rules out gradients on text. This class painted a three-stop gradient
   through a clipped background, which also renders as invisible text wherever the clip
   is unsupported and defeats every contrast check. Flattened to solid ink. */
.service-title-gradient {
    background: none !important;
    -webkit-background-clip: initial !important;
    background-clip: initial !important;
    -webkit-text-fill-color: currentColor !important;
    color: var(--ink) !important;
    font-family: var(--font-display) !important;
    font-weight: 600;
}

/* -- Tables -------------------------------------------------------------- */

.table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--ink);
    font-family: var(--font-ui);
    font-size: 13.5px;
    color: var(--ink);
    border-color: var(--border);
}

.table > thead > tr > th {
    background-color: var(--surface-solid);
    border-bottom: 1px solid var(--border-strong);
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--text-micro-tracking);
    color: var(--ink-3);
}

.table > tbody > tr > td {
    border-color: var(--border);
    vertical-align: middle;
}

.table-hover > tbody > tr:hover > * {
    --bs-table-accent-bg: var(--row-hover);
    background-color: var(--row-hover);
    color: var(--ink);
}

/* -- Badges ---------------------------------------------------------------
   130 uses, mostly through Bootstrap bg-* utilities. Given the shared pill shape and
   the status palette so they read as the same family as the workflow pills without
   claiming to be one: a badge is a label, a pill is a lifecycle state. */

.badge {
    border-radius: var(--r-pill);
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 700;
    letter-spacing: 0.03em;
    padding: 0.25em 0.6em;
}

.badge.bg-primary { background-color: var(--accent) !important; color: var(--accent-ink); }
.badge.bg-success { background-color: var(--dnx-status-success-bg) !important; color: var(--dnx-status-success-fg); }
.badge.bg-danger  { background-color: var(--dnx-status-danger-bg) !important;  color: var(--dnx-status-danger-fg); }
.badge.bg-warning { background-color: var(--dnx-status-warning-bg) !important; color: var(--dnx-status-warning-fg); }
.badge.bg-info    { background-color: var(--dnx-status-info-bg) !important;    color: var(--dnx-status-info-fg); }
.badge.bg-secondary { background-color: var(--dnx-status-neutral-bg) !important; color: var(--dnx-status-neutral-fg); }

/* -- Alerts -------------------------------------------------------------- */

.alert {
    border-radius: var(--r-panel);
    border-width: 1px;
    font-family: var(--font-ui);
    font-size: var(--text-body);
    box-shadow: var(--e1);
}

.alert-success { background-color: var(--dnx-status-success-bg); border-color: var(--dnx-status-success-bd); color: var(--dnx-status-success-fg); }
.alert-danger  { background-color: var(--dnx-status-danger-bg);  border-color: var(--dnx-status-danger-bd);  color: var(--dnx-status-danger-fg); }
.alert-warning { background-color: var(--dnx-status-warning-bg); border-color: var(--dnx-status-warning-bd); color: var(--dnx-status-warning-fg); }
.alert-info    { background-color: var(--dnx-status-info-bg);    border-color: var(--dnx-status-info-bd);    color: var(--dnx-status-info-fg); }

/* -- Forms ----------------------------------------------------------------
   Paint on inputs, selects and textareas is already handled by the --m3-field-* tokens
   in common.css, which were re-pointed at this ramp. What is left is the surrounding
   type and the controls those rules do not cover. */

.form-label {
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    font-weight: 600;
    color: var(--ink);
}

.form-text,
.form-check-label {
    font-family: var(--font-ui);
    color: var(--ink-2);
}

.form-check-input:checked {
    background-color: var(--accent);
    border-color: var(--accent);
}

.input-group-text {
    background-color: var(--surface);
    border-color: var(--border-strong);
    color: var(--ink-2);
    border-radius: var(--r-control);
    font-family: var(--font-ui);
}

/* -- Modals -------------------------------------------------------------- */

.modal-content,
.modern-modal .modal-content {
    border-radius: var(--r-card);
    border: 1px solid var(--border);
    /* Opaque. A modal is the one surface that must never let the page behind it show
       through, which is the entire point of a modal. */
    background-color: var(--surface-solid);
    box-shadow: var(--e3);
}

.modal-header,
.modal-footer {
    border-color: var(--border);
}

.modal-title {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--ink);
}

.modal-backdrop.show {
    /* Lighter than the Bootstrap default of 0.5 black: this design leans on elevation to
       separate the modal, so the backdrop only has to suppress the page, not black it
       out. */
    opacity: 0.42;
}

/* -- Text utilities -------------------------------------------------------
   309 uses of text-muted and 123 of text-secondary. Bootstrap resolves both to a grey
   that ignores the theme, which made them the single largest source of low-contrast
   text in dark mode. Pointed at the ink ramp, which flips. */

.text-muted { color: var(--ink-3) !important; }
.text-secondary { color: var(--ink-2) !important; }
.text-dark { color: var(--ink) !important; }
.text-primary { color: var(--accent) !important; }
.text-danger { color: var(--dnx-status-danger-fg) !important; }
.text-success { color: var(--dnx-status-success-fg) !important; }
.text-warning { color: var(--dnx-status-warning-fg) !important; }

/* `small` is the most-used class in the app after the icon font, at 720 uses. */
small, .small {
    font-size: var(--text-secondary);
}

.fw-semibold { font-weight: 600; }
.fw-bold { font-weight: 700; }

/* bg-light is used as a panel fill on 49 elements and stayed near-white in dark mode. */
.bg-light {
    background-color: var(--accent-soft) !important;
}

/* -- Spinners -------------------------------------------------------------
   98 uses. Given the accent so an in-flight state reads as the colour of this product
   rather than the Bootstrap default blue. */

.spinner-border,
.spinner-grow {
    color: var(--accent);
}

/* -- Links --------------------------------------------------------------- */

a:not(.btn):not(.nav-link):not(.dim-card) {
    color: var(--accent);
}

a:not(.btn):not(.nav-link):not(.dim-card):hover {
    color: var(--accent-hover);
}

/* -- Dark theme corrections -----------------------------------------------
   The rules above are token-driven, so most of the dark theme follows for free. These
   are the cases where a Bootstrap utility hard-codes a value no token can reach. */

.dark-theme .table {
    --bs-table-color: var(--ink);
    color: var(--ink);
}

.dark-theme .card,
.dark-theme .common-card,
.dark-theme .modal-content {
    background-color: var(--surface-solid);
    border-color: var(--border);
}

.dark-theme .bg-white,
.dark-theme .bg-body {
    background-color: var(--surface-solid) !important;
}

.dark-theme .border {
    border-color: var(--border) !important;
}

/* ==========================================================================
   SHELL
   ==========================================================================
   The top bar, the navigation sidebar, the main content area and the toast.

   Restyled in place rather than re-architected. The brief describes a 58px top bar and a
   70px icon dock that expands to a 222px labelled rail, and the existing ServiceHeader +
   CommonSidebar already occupy exactly those two roles — with collapse state, custom
   sidebar content injection, permission-filtered items and a search field wired through
   SidebarContentService. Replacing the markup would put all of that behaviour at risk to
   change what the two elements look like, and behaviour is explicitly out of scope here.

   So the metrics and the surfaces come from the tokens, and the structure stays.
   ========================================================================== */

/* -- Top bar ---------------------------------------------------------------
   Translucent and blurred over the canvas. This is the one place --surface-chrome is
   used: it is the most transparent of the three surface weights, which only works
   because there is a blur behind it turning whatever scrolls underneath into a wash
   rather than legible text. */
.admin-header {
    height: var(--topbar-h);
    min-height: var(--topbar-h);
    background: var(--surface-chrome) !important;
    backdrop-filter: var(--chrome-blur);
    -webkit-backdrop-filter: var(--chrome-blur);
    border-bottom: 1px solid var(--border);
    /* Sticky rather than fixed: fixed would take the bar out of flow and every page below
       would need a compensating top offset, which is the kind of change that breaks 90
       layouts at once. */
    position: sticky;
    top: 0;
    z-index: 1030;
    box-shadow: none;
}

.admin-header-title {
    font-family: var(--font-display) !important;
    font-size: var(--text-section);
    font-weight: 600;
    color: var(--ink);
}

.admin-logo-text {
    font-family: var(--font-display) !important;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ink);
}

/* The header icons become proper 36px tiles that tint on hover, rather than bare glyphs
   with a hit area the size of the glyph itself. */
.admin-header-icon,
.admin-header-icon-logout {
    width: 36px;
    height: 36px;
    border-radius: var(--r-control);
    color: var(--ink-2);
    transition: background-color var(--dnx-duration-fast) var(--dim-ease),
                color var(--dnx-duration-fast) var(--dim-ease);
}

.admin-header-icon:hover {
    background-color: var(--accent-soft);
    color: var(--accent);
}

.admin-header-icon-logout:hover {
    background-color: var(--dnx-status-danger-bg);
    color: var(--dnx-status-danger-fg);
}

/* The schema/context chip in the header — a mono value, because it is an identifier the
   user compares against what they see in the grid. */
.admin-header-schema,
.schema-label {
    font-family: var(--font-mono) !important;
    font-size: var(--text-code);
    color: var(--ink-2);
}

/* -- Navigation dock -------------------------------------------------------
   The collapsed rail is the dock. Items become 44px rounded tiles that lift on hover and
   fill with the accent when active, which is what makes the current module findable at a
   glance instead of by reading. */

.admin-sidebar {
    background: var(--surface) !important;
    border-right: 1px solid var(--border);
}

.admin-sidebar-header {
    border-bottom: 1px solid var(--border);
    min-height: var(--topbar-h);
}

.menu-title {
    font-family: var(--font-ui) !important;
    font-size: var(--text-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--text-micro-tracking);
    color: var(--ink-3);
}

.nav-label {
    font-family: var(--font-ui) !important;
    font-size: var(--text-body);
    font-weight: 500;
    color: var(--ink-2);
}

.toggle-btn {
    width: 32px;
    height: 32px;
    border-radius: var(--r-control);
    border: 0;
    background: transparent;
    color: var(--ink-3);
    transition: background-color var(--dnx-duration-fast) var(--dim-ease),
                color var(--dnx-duration-fast) var(--dim-ease);
}

.toggle-btn:hover {
    background-color: var(--accent-soft);
    color: var(--accent);
}

/* The "Add" entry in the nav rail — same palette-item shape as a menu link, tinted green
   to read as a create action rather than a destination, with a hairline separating it
   from the real navigation below. */
.nav-item-add {
    color: var(--dnx-status-success-fg, #198754);
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    margin-bottom: var(--s1, 8px);
    padding-bottom: calc(var(--s2) + var(--s1, 8px));
}

.nav-item-add .palette-item-icon {
    color: var(--dnx-status-success-fg, #198754);
}

.nav-item-add:hover {
    background-color: var(--dnx-status-success-bg, var(--accent-soft));
}

/* -- Main content ----------------------------------------------------------
   Transparent, so the canvas painted on <body> shows through. It was white, which meant
   the canvas was only ever visible in the few pixels around the content. */
.common-main-content {
    --common-main-content-bg: transparent;
    background: transparent;
    padding: var(--content-pad);
}

.dark-theme .common-main-content {
    --common-main-content-bg: transparent;
    background: transparent;
}

/* -- Toast -----------------------------------------------------------------
   Restyled only. It is driven by NotificationService.OnShowNotification, which pages
   across the app already call; replacing it would mean touching state management, which
   is out of scope. The markup and the service contract are untouched.

   Bottom-right and auto-dismissing already; what it lacked was the design language and a
   status colour that matched the rest of the app. */

.custom-toast {
    border-radius: var(--r-panel);
    background: var(--surface-solid);
    border: 1px solid var(--border);
    box-shadow: var(--e3);
    font-family: var(--font-ui);
    font-size: var(--text-body);
    color: var(--ink);
}

/* The type is carried by a left edge rather than by tinting the whole surface: at toast
   size a full status wash competes with the mascot sitting inside it. */
.custom-toast.success { border-left: 3px solid var(--dnx-status-success-fg); }
.custom-toast.error,
.custom-toast.danger { border-left: 3px solid var(--dnx-status-danger-fg); }
.custom-toast.warning { border-left: 3px solid var(--dnx-status-warning-fg); }
.custom-toast.info { border-left: 3px solid var(--dnx-status-info-fg); }

.custom-toast-message {
    font-family: var(--font-ui);
    color: var(--ink);
}

.custom-toast-close {
    border: 0;
    background: transparent;
    color: var(--ink-3);
    border-radius: var(--r-control);
    min-width: 32px;
    min-height: 32px;
}

.custom-toast-close:hover {
    background-color: var(--accent-soft);
    color: var(--accent);
}

/* -- Responsive ------------------------------------------------------------
   Below 768px the content padding tightens. The sidebar already collapses via the
   flex-lg-row on its container, so the dock behaviour is inherited rather than
   re-specified here. */

@media (max-width: 767px) {
    .common-main-content {
        padding: var(--s3);
    }

    .admin-header {
        /* The blur is dropped on small screens. backdrop-filter over a full-width bar is
           one of the more expensive things a mobile GPU can be asked to do every frame,
           and the bar is small enough there that the effect is barely visible anyway. */
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        background: var(--surface-solid) !important;
    }
}

/* ==========================================================================
   ICONS — inherit their context instead of a fixed gradient
   ==========================================================================
   common.css paints every .bi with a fixed cyan-to-navy gradient through a clipped
   background, setting `color: transparent` and `-webkit-text-fill-color: transparent`
   to let the gradient show through the glyph.

   The consequence is that no icon can ever take the colour of the thing it belongs to.
   A validation icon next to red error text renders blue. A white icon on an accent
   button renders blue. A status pill icon renders blue regardless of status. There is a
   partial escape hatch in that file (`.text-danger .bi` and friends), but it only fires
   when the *parent* happens to carry a Bootstrap text utility, which almost nothing in
   the new primitives does.

   Restored to currentColor, which is what an icon font is for: the glyph then takes the
   colour of whatever it sits inside, and every existing colour rule starts working on
   icons for free. It also removes ~1000 gradients from the page, which the brief rules
   out anyway.

   !important throughout because the rules being replaced use it. */

.bi {
    background: none !important;
    -webkit-background-clip: initial !important;
    background-clip: initial !important;
    -webkit-text-fill-color: currentColor !important;
    color: inherit !important;
}

/* ==========================================================================
   CORRECTIONS
   ========================================================================== */

/* The header context chip. Reverted from mono: it holds a connection *name*
   ("Master Platform Connection"), which is prose, not an identifier to be compared
   character by character. Mono is for codes and IDs; applying it to a display name made
   the top bar read like a terminal. */
.admin-header-schema,
.schema-label {
    font-family: var(--font-ui) !important;
    font-size: var(--text-secondary);
    color: var(--ink-2);
}

/* The style guide colour swatches. The checkerboard behind them exists to prove the
   translucent surface tokens are actually translucent, but it was drawn in
   var(--border) — far too pale against white to see, so --surface and --surface-solid
   looked identical and the demonstration failed. Given a mid grey with enough contrast
   to read through an 88% white fill. */
.ds-swatch-chip {
    background-image:
        linear-gradient(45deg, #b9c2cf 25%, transparent 25%),
        linear-gradient(-45deg, #b9c2cf 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, #b9c2cf 75%),
        linear-gradient(-45deg, transparent 75%, #b9c2cf 75%);
}

/* The chip is the checkerboard; the fill stacks on top of it. Two layers rather than
   one because background-image paints above background-color, so a single element can
   never show a translucent token *over* its own checker. */
.ds-swatch-chip {
    position: relative;
    overflow: hidden;
}

.ds-swatch-fill {
    position: absolute;
    inset: 0;
    display: block;
}

/* ==========================================================================
   DEPTH — applied across the application
   ==========================================================================
   Extends the depth model from the browse cards to icons, service cards, panels,
   tables, modals and buttons.

   Two mechanisms, used for different things:

   ROTATION (literal 3D) goes on things you look *at* and click through — browse cards,
   service cards, icon tiles. They are objects on a surface, and a shared vanishing point
   makes a grid of them read as physical.

   LAYERING (elevation + edge light) goes on things you *read* — tables, panels, modals,
   form surfaces. A tilted table is actively worse than a flat one: rows stop sharing a
   baseline and columns stop aligning, which is the entire job of a table. These get
   depth without rotation.

   The edge highlight is what sells depth on a flat fill. A shadow alone only separates an
   element from the page; a 1px inset light line along the top edge reads as the surface
   catching light, which is what makes it look raised rather than cut out.
   ========================================================================== */

:root {
    /* Top-edge light on a raised surface. Warmer and brighter than a plain white line so
       it reads as light rather than as a border. */
    --edge-light: inset 0 1px 0 rgba(255, 255, 255, 0.75);
    --edge-light-soft: inset 0 1px 0 rgba(255, 255, 255, 0.5);
    /* Bottom-edge shade, for tiles deep enough to show one. Together with the top light
       this gives a surface a readable thickness. */
    --edge-shade: inset 0 -1px 0 rgba(16, 24, 40, 0.06);

    /* A tile is small, so it needs a stronger tilt than a card to read as tilted at all —
       rotation is perceived relative to the element, not in absolute degrees.

       Tuned down from 12/-14. At 34px with a 220px perspective, that was steep enough that
       the glyph inside started to look skewed rather than angled, which reads as a
       rendering fault rather than as depth. 9/-11 still clearly separates the tile from the
       card plane while keeping the icon legible. */
    --tile-tilt-x: 9deg;
    --tile-tilt-y: -11deg;
}

.dark-theme {
    /* On a dark surface a 75%-white edge reads as a glowing outline. Dropped to a faint
       light line, and the shade becomes true black to keep the thickness readable. */
    --edge-light: inset 0 1px 0 rgba(255, 255, 255, 0.08);
    --edge-light-soft: inset 0 1px 0 rgba(255, 255, 255, 0.05);
    --edge-shade: inset 0 -1px 0 rgba(0, 0, 0, 0.4);
}

/* ==========================================================================
   ICON TILES
   ==========================================================================
   Every icon that sits in a tile becomes a raised object: a soft accent fill, a lit top
   edge, a real shadow, and a slight 3D rotation. The icon inside is pushed forward on
   its own Z plane so it floats above its tile rather than being painted on it — that
   parallax is what separates this from a rounded square with a shadow.

   Bare inline icons (in a sentence, in a button label) are deliberately excluded. Depth
   on a 14px glyph mid-paragraph is noise, and it would fight the text baseline. */

.dim-card-icon,
.svc-card-icon,
.dnx-service-icon,
.admin-header-icon,
.dim-kpi-icon {
    transform-style: preserve-3d;
    transition: transform var(--dim-duration) var(--dim-ease),
                box-shadow var(--dim-duration) var(--dim-ease);
}

.dim-card-icon,
.dnx-service-icon {
    /* A light-to-tint sheen across the tile, which is what gives a flat fill a sense of
       a surface angled to a light source. Built from two existing tokens rather than
       relative-colour syntax: an unsupported oklch(from …) makes the whole declaration
       invalid, and the tile would lose its fill entirely rather than degrade. */
    background: linear-gradient(160deg, var(--surface-solid) 0%, var(--accent-soft) 100%);
    box-shadow: var(--e2), var(--edge-light);
    transform: perspective(220px) rotateX(var(--tile-tilt-x)) rotateY(var(--tile-tilt-y));
}

/* The glyph rides forward of its tile. translateZ needs preserve-3d on the parent, which
   the rule above sets. */
.dim-card-icon > i,
.dnx-service-icon > i {
    transform: translateZ(8px);
}

/* On hover the tile flattens to face the viewer, in step with the card flattening around
   it — the two movements have to share a duration or the card looks like it is peeling
   away from its own icon. */
.dim-card-interactive:hover .dim-card-icon,
.dim-card-interactive:focus-visible .dim-card-icon {
    transform: perspective(220px) rotateX(0) rotateY(0) translateZ(6px);
    box-shadow: var(--e3), var(--edge-light);
}

/* Dashboard service icons are images, not glyphs, so they take the depth on themselves
   rather than through a tile. */
.svc-card-icon {
    transform: perspective(300px) rotateX(8deg) rotateY(-10deg);
    filter: drop-shadow(0 6px 10px rgba(16, 24, 40, 0.28));
}

.svc-card:hover .svc-card-icon {
    transform: perspective(300px) rotateX(0) rotateY(0) translateY(-2px) scale(1.04);
    filter: drop-shadow(0 10px 16px rgba(16, 24, 40, 0.34));
}

/* Header icon tiles: no rotation (they sit in a 58px bar where a tilted tile would fight
   the horizontal rhythm), but they gain a real pressed/raised response. */
.admin-header-icon:hover,
.admin-header-icon-logout:hover {
    box-shadow: var(--e1), var(--edge-light);
    transform: translateY(-1px);
}

.admin-header-icon:active,
.admin-header-icon-logout:active {
    transform: translateY(0);
    box-shadow: none;
}

/* ==========================================================================
   SERVICE CARDS — the dashboard grid
   ==========================================================================
   These are the app equivalent of browse cards: a grid of things you click through to a
   module. Given the same treatment, so the dashboard and every module landing view share
   one signature.

   The perspective is set on the container. Per-card perspective would give each card its
   own vanishing point and the whole grid would tilt identically, which reads as a skew
   filter rather than as depth. */

.svc-grid,
.services-grid,
.dashboard-services {
    perspective: var(--card-perspective);
}

.svc-card {
    transform: rotateX(5deg) rotateY(-6deg);
    box-shadow: var(--card-tilt-shadow), var(--edge-light);
    transform-style: preserve-3d;
    transition: transform var(--dim-duration-slow) var(--dim-ease),
                box-shadow var(--dim-duration-slow) var(--dim-ease);
}

/* Shallower than the browse card (5/-6 against 7/-9): service cards are wider and carry
   a paragraph of description, and text legibility degrades with rotation faster than a
   short label does. */
.svc-card:hover,
.svc-card:focus-within {
    transform: rotateX(0) rotateY(0) translateY(var(--dim-lift-card));
    box-shadow: var(--e3), var(--edge-light);
}

.svc-card:active {
    transform: rotateX(0) rotateY(0) translateY(0);
    box-shadow: var(--e2), var(--edge-light);
}

/* ==========================================================================
   PANELS, CARDS, MODALS — layered, not rotated
   ========================================================================== */

.card,
.common-card,
.dim-kpi,
.dim-grid,
.ds-form-panel {
    box-shadow: var(--e1), var(--edge-light);
}

/* Only cards that are actually interactive lift. A static panel rising under the cursor
   promises a click target that is not there. */
.card:hover,
.common-card:hover,
.dim-kpi:hover {
    box-shadow: var(--e2), var(--edge-light);
    transform: translateY(var(--dim-lift));
}

.card,
.common-card,
.dim-kpi {
    transition: transform var(--dim-duration) var(--dim-ease),
                box-shadow var(--dim-duration) var(--dim-ease);
}

.modal-content,
.modern-modal .modal-content {
    box-shadow: var(--e3), var(--edge-light);
}

/* Buttons get the lit top edge too, which is what makes a filled button read as a raised
   key rather than a coloured rectangle. */
.dim-btn-primary,
.btn-primary,
.common-btn-primary {
    box-shadow: var(--e-accent), var(--edge-light-soft);
}

.dim-btn-secondary,
.btn-secondary,
.btn-outline-secondary,
.common-btn-secondary {
    box-shadow: var(--e1), var(--edge-light);
}

/* ==========================================================================
   TABLES — depth without rotation
   ==========================================================================
   The grid is a raised panel, the header is a plane above the rows, and a hovered row
   comes forward. Nothing rotates: a tilted table breaks the shared baseline and the
   column alignment that make a table readable at all.

   The header shadow is the important part. A sticky header with no shadow looks painted
   onto the first row; with one, rows visibly pass underneath it. */

.dim-grid,
.common-table-card,
.table-responsive {
    box-shadow: var(--e1), var(--edge-light);
}

.dim-grid-table thead th,
.table > thead > tr > th {
    /* Cast downward only, so the shadow falls on the rows below and not up over the
       filter bar. */
    box-shadow: 0 1px 0 var(--border-strong), 0 4px 8px -6px rgba(16, 24, 40, 0.35);
}

.dark-theme .dim-grid-table thead th,
.dark-theme .table > thead > tr > th {
    box-shadow: 0 1px 0 var(--border-strong), 0 4px 10px -6px rgba(0, 0, 0, 0.7);
}

/* A hovered row lifts a hair off the surface. position:relative gives it a stacking
   context so its shadow paints over its neighbours rather than under them — without it
   the effect is invisible. Kept very small: at 50px row height anything more turns a
   scan down a column into a wobble. */
.dim-grid-row,
.table-hover > tbody > tr {
    position: relative;
    transition: box-shadow var(--dnx-duration-fast) var(--dim-ease),
                transform var(--dnx-duration-fast) var(--dim-ease);
}

.dim-grid-row:hover,
.table-hover > tbody > tr:hover {
    z-index: 1;
    box-shadow: var(--e2);
    transform: translateY(-1px);
}

/* ==========================================================================
   REDUCED MOTION AND SMALL SCREENS
   ==========================================================================
   Everything above is decoration. Both of these turn the rotation off and keep only the
   flat elevation, which still communicates the layering. */

@media (max-width: 1023px) {
    .svc-card,
    .svc-card:hover {
        transform: none;
        box-shadow: var(--e2), var(--edge-light);
    }

    .dim-card-icon,
    .dnx-service-icon,
    .svc-card-icon,
    .svc-card:hover .svc-card-icon {
        transform: none;
    }

    .dim-card-icon > i,
    .dnx-service-icon > i {
        transform: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .svc-card,
    .svc-card:hover,
    .svc-card:active,
    .dim-card-icon,
    .dnx-service-icon,
    .svc-card-icon,
    .svc-card:hover .svc-card-icon,
    .dim-card-interactive:hover .dim-card-icon,
    .card:hover,
    .common-card:hover,
    .dim-kpi:hover,
    .dim-grid-row:hover,
    .table-hover > tbody > tr:hover,
    .admin-header-icon:hover,
    .admin-header-icon-logout:hover {
        transform: none;
    }

    .dim-card-icon > i,
    .dnx-service-icon > i {
        transform: none;
    }
}

/* ==========================================================================
   CARD SHELL + OVERLAID ACTIONS
   ==========================================================================
   The shell carries the grid position; the card fills it. Actions are positioned over
   the card but are siblings of it in the DOM, because a button inside an anchor is
   invalid HTML. */

.dim-card-shell {
    position: relative;
    display: flex;
    min-width: 0;
}

.dim-card-shell > .dim-card {
    flex: 1 1 auto;
    min-width: 0;
}

/* The action button's usual corner is also the status pill's slot inside .dim-card-top.
   Bottom placement collided with the mono code line on short cards instead, so the fix is
   to keep the button top-right (where a corner action is expected) and reserve room for it
   so the pill shifts left rather than rendering underneath it. */
.dim-card-shell:has(.dim-card-actions) .dim-card-top {
    padding-right: 44px;
}

.dim-card-actions {
    position: absolute;
    top: var(--s2);
    right: var(--s2);
    z-index: 2;
    display: flex;
    gap: var(--s1);
    /* Hidden until the card is hovered or something inside it takes focus, so a grid of
       cards is not a grid of delete buttons. focus-within keeps it reachable by keyboard,
       which a hover-only reveal would not be. */
    opacity: 0;
    transition: opacity var(--dim-duration) var(--dim-ease);
}

.dim-card-shell:hover .dim-card-actions,
.dim-card-shell:focus-within .dim-card-actions {
    opacity: 1;
}

/* Touch has no hover, so the affordance would never appear. Always shown there. */
@media (pointer: coarse) {
    .dim-card-actions { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .dim-card-actions { transition: none; }
}

/* The browse-pattern page wrapper. Padding comes from the shell content area, so this
   only owns the vertical rhythm between the header and the grid. */
.dim-browse {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
}

/* Fallback content padding for pages not already inside a shell region that supplies it
   (.common-main-content already carries --content-pad). Safe to stack: padding does not
   compound visually when both apply the same token to the same box. */
.dim-page-pad {
    padding: var(--content-pad, 24px);
}

/* ==========================================================================
   BUILDER HEADER — Subject Area Builder (design 2a), shared by any screen with a
   back-chevron / name / status / draft-actions header.
   ========================================================================== */

/* No background of its own — .sa-tabs-container right below it (SubjectAreaTabs.razor) is
   opaque var(--app-surface), and this row used to show the page's tinted background showing
   through instead, reading as a separate colored band sitting above a plain white tab strip.
   Matching that same surface color here merges the two into one continuous bar with the tab
   row "on the next line" instead of two visually distinct sections. */
.dim-builder-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    flex-wrap: wrap;
    padding: var(--s2) var(--s3) var(--s3);
    background: var(--app-surface);
}

.dark-theme .dim-builder-header {
    background: #1a1f2b;
}

.dim-builder-header-left {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-width: 0;
}

.dim-builder-title {
    font-family: var(--font-display);
    font-size: var(--text-section);
    font-weight: 600;
    color: var(--ink);
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dim-builder-reference-tag {
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--text-micro-tracking);
    color: var(--accent-2, var(--accent));
    background: var(--accent-soft);
    border-radius: var(--r-control);
    padding: 2px 8px;
}

.dim-builder-unsaved {
    font-family: var(--font-mono);
    font-size: var(--text-code);
    color: var(--ink-3);
    white-space: nowrap;
}

.dim-builder-header-right {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
}

/* SubjectAreaDetailContent.razor's outer wrapper predates Dimensional and still carries a
   hardcoded height: calc(100vh - 64px) plus a solid white background. Fixed-viewport-height
   math like that assumes it owns the whole browser window; nested inside the shell's own
   scroll region it instead ends up a few pixels taller than the space actually available,
   which pushed a scrollbar (with its up/down step-arrows) onto the page and rendered as a
   second, visibly separate "page" sitting on top of the canvas. Both are cleared to flow
   naturally within the shell's own scroll container instead. */
.sa-detail-container {
    height: auto !important;
    background: transparent !important;
}

.sa-content-area {
    background: transparent !important;
    overflow: visible !important;
}

/* ==========================================================================
   FLOATING DIALOG EXEMPTION — common.css turns every Bootstrap-style ModalShell
   dialog (`.modal.d-block > .modal-dialog > .modal-content`) into a full-height,
   right-anchored drawer app-wide. That's the right call for large configuration
   panels, but wrong for small utility popups that were built to float centered —
   the user profile card, the connection-context switcher, and now the column
   Properties dialog. Add "dim-floating-dialog" to a dialog's SizeClass to opt out
   of the drawer conversion (common.css excludes it via :not()) and get this
   centered/floating presentation instead.
   ========================================================================== */

.modal-dialog.dim-floating-dialog {
    /* The drawer conversion's justify-content:flex-end/align-items:stretch live on the
       *outer* .modal element, two levels up, so they can't be scoped off by a selector
       on .modal-dialog itself. margin:auto is the standard flex-item trick instead — it
       absorbs the row's free space on both axes and centers the dialog regardless of
       which value currently wins on the ancestor.

       align-items is deliberately left alone: Bootstrap's own .modal-dialog-centered
       (added whenever ModalShell's Centered param is true, which it is here) sets
       align-items:center on this same element to vertically center .modal-content
       within it. An earlier version of this rule set align-items:normal to fight the
       drawer's stretch, but normal *computes* to stretch for a flex container — so it
       clobbered Bootstrap's centering instead and stretched .modal-content to fill the
       tall invisible .modal-dialog box (from .modal-dialog-centered's own
       min-height: calc(100% - 3.5rem)), which is what made short dialogs like the user
       profile card render at full viewport height. margin:auto alone is sufficient.

       min-height/background/border/box-shadow/overflow are reset for a second, separate
       reason: a *legacy* generic `.modal-dialog { ... }` rule (common.css, the original
       non-Bootstrap "modal-overlay" dialog family) paints a background, border,
       border-radius and box-shadow directly onto .modal-dialog itself and gives it
       overflow:auto — because in that older system .modal-dialog *was* the visible card,
       with no separate .modal-content layer. Class names collide, so it still applies
       here even though ModalShell's .modal-content is the real visible surface. Left
       alone, .modal-dialog-centered's min-height floor plus that painted background
       renders as a second, taller white box with .modal-content centered inside it — the
       outer box's own fill reads as dead space above (and below) the real content. */
    margin: auto;
    min-height: 0;
    max-height: none;
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    overflow: visible;
    transform: none;
    animation: dimFloatingDialogIn 0.2s ease both;
}

.modal-dialog.dim-floating-dialog > .modal-content {
    height: auto;
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    /* Several callers pass ContentClass="common-card", whose background is the legacy
       --app-surface token — repointed at Dimensional's --surface, which is deliberately
       translucent (oklch(1 0 0 / 0.88)) so page-level cards let the canvas tint show
       through underneath them. A floating dialog has no canvas underneath, only the
       blurred/dimmed page it's covering, so that translucency reads as visual noise
       instead. --surface-solid is the opaque counterpart the design system already
       reserves for exactly this ("Grids, menus, modals" per docs/DESIGN_SYSTEM.md) —
       this rule's specificity (.modal-dialog.dim-floating-dialog > .modal-content, 3
       classes) already beats .common-card's (1 class), so no !important needed. */
    background: var(--surface-solid);
    border: none;
    border-radius: var(--r-modal, 16px);
    box-shadow: var(--e-modal, var(--e3));
}

/* Belt-and-suspenders alongside the .modal-body exclusion in common.css's noise-texture
   rule (search "Drawer surface: noise body"): explicitly pins header/body/footer to the
   same opaque surface as .modal-content, so a floating dialog reads as one flat white
   panel regardless of which legacy rule a given ContentClass/markup combination happens
   to pick up. */
.modal-dialog.dim-floating-dialog .modal-header,
.modal-dialog.dim-floating-dialog .modal-body,
.modal-dialog.dim-floating-dialog .modal-footer {
    background: var(--surface-solid);
    background-image: none;
}

@keyframes dimFloatingDialogIn {
    from { opacity: 0; transform: translateY(8px) scale(0.98); }
    to { opacity: 1; transform: none; }
}

/* ==========================================================================
   TRANSFORM CONFIG CHROME — shared header/footer styling for the flow/
   transformer "Configure Transformation" dials: PipelineDesigner's own
   Source/Target dial, and ModelAddOperations' TransformationConfiguration
   mode (every other node type — Filter, Sorter, Aggregator, etc.). Matches
   the column Properties popup's typographic hierarchy (title + muted
   subline, a proper footer rule) now that both dials are centered again
   instead of right-side drawers.

   Deliberately scoped to just the shared chrome. Each of the 15+
   transformation types still has its own hand-built config fields (Filter's
   Column/Condition/Value, Source's Connection/Schema/Table, and so on) —
   rebuilding every one of those into colprop's grouped-block/toggle-row
   pattern is a much larger pass than this one; this only touches what's
   common to all of them.
   ========================================================================== */

.dnx-transform-heading {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.dnx-transform-title {
    font-family: var(--font-display);
    font-size: var(--text-section);
    font-weight: 600;
    color: var(--ink);
    margin: 0;
}

.dnx-transform-subline {
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    font-weight: 400;
    color: var(--ink-2);
}

.dnx-transform-footer {
    padding: var(--s4) var(--s5) !important;
    margin-top: 0 !important;
    border-top: 1px solid var(--border) !important;
    background: var(--surface-solid);
}

/* The floating ConfigurationDial (the round step-type icon overlapping the top-left corner)
   was dropped from both transform popups on request, and common.css's .dial-modal-content
   clip-path is a notch cut specifically to make room for that dial. Without it the notch
   reads as a chunk missing from the panel's corner, so it's removed here and swapped for a
   normal rounded-rect shadow, for both the model-ops-modal (Filter/Aggregator/etc.) and
   pipeline-side-drawer (Source/Target) variants. */
.dial-modal-container.model-ops-modal .dial-modal-content,
.dial-modal-container.pipeline-side-drawer .dial-modal-content {
    clip-path: none !important;
    filter: none !important;
    box-shadow: 0 24px 64px rgba(12, 18, 32, .28) !important;
}

/* ::before draws the faint ring the dial used to sit inside; with no dial there it's just a
   stray circle outline poking out of the top-left corner. .dial-modal-header's 160px left
   padding existed purely to keep the title clear of that same dial. Both are dial-only
   affordances, so both are switched off for the now dial-less variants. */
.dial-modal-container.model-ops-modal .dial-modal-content::before,
.dial-modal-container.pipeline-side-drawer .dial-modal-content::before {
    display: none !important;
}

.dial-modal-container.model-ops-modal .dial-modal-header,
.dial-modal-container.pipeline-side-drawer .dial-modal-header {
    padding-left: 1.5rem !important;
}

/* dial-modal-content's own background is --app-surface; dnx-transform-footer was set to
   --surface-solid, a different token that (depending on theme) doesn't match, reading as a
   separate colored strip under Cancel/Save instead of a flush footer rule. Match the panel's
   actual background so the footer looks like part of the same surface, the way colprop's
   footer (no background override of its own) does. */
.dnx-transform-footer {
    background: var(--app-surface) !important;
}

.dark-theme .dnx-transform-footer {
    background: #1e2226 !important;
}

/* common.css's .dial-modal-container.wide-modal fixes height at 80vh regardless of content —
   fine for the designer-sized wizard steps it was built for, but it left short forms (Filter's
   three fields) with a large dead band of empty white space above the footer. The same problem
   was already solved for .compact-modal (auto height, flex chain broken so nothing stretches to
   fill); this does the equivalent for the transform popups, capped at 85vh so long forms
   (Aggregator, Router with many branches) still scroll instead of overflowing the viewport. */
.dial-modal-container.model-ops-modal.wide-modal,
.dial-modal-container.pipeline-side-drawer.wide-modal {
    height: auto !important;
    max-height: 85vh !important;
}

.dial-modal-container.model-ops-modal.wide-modal .dial-modal-content,
.dial-modal-container.pipeline-side-drawer.wide-modal .dial-modal-content {
    height: auto !important;
    max-height: 85vh !important;
}

.dial-modal-container.model-ops-modal.wide-modal .dial-modal-body,
.dial-modal-container.pipeline-side-drawer.wide-modal .dial-modal-body,
.dial-modal-container.model-ops-modal.wide-modal .dnx-noise-content,
.dial-modal-container.pipeline-side-drawer.wide-modal .dnx-noise-content,
.dial-modal-container.model-ops-modal.wide-modal .dial-form-area,
.dial-modal-container.pipeline-side-drawer.wide-modal .dial-form-area,
.dial-modal-container.model-ops-modal.wide-modal .dial-form-content,
.dial-modal-container.pipeline-side-drawer.wide-modal .dial-form-content {
    flex: none !important;
    overflow: visible !important;
}

.dial-modal-container.model-ops-modal.wide-modal .dial-form-scroll-body,
.dial-modal-container.pipeline-side-drawer.wide-modal .dial-form-scroll-body {
    flex: none !important;
    max-height: calc(85vh - 220px);
    overflow-y: auto !important;
}

.dnx-transform-id-chip {
    font-size: var(--text-tiny, 0.7rem);
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ink-2);
    background: var(--app-tint-panel-b);
    border: 1px solid var(--border);
    border-radius: var(--r-pill, 999px);
    padding: 2px 8px;
    white-space: nowrap;
}

/* Left "Sections" nav, same visual language as SubjectAreaColumns.razor's .colprop-nav —
   duplicated (not shared) because that block lives scoped to a single .razor file's own
   <style> tag and isn't guaranteed to be in the DOM on pages that never render it. Each of
   the 15+ transform types is still a single flat form (unlike columns, which has genuinely
   separate Validation/Search/Survivorship/Trust concerns), so there is only ever one real,
   enabled item — General — here. The nav exists for the same reason colprop's own mostly-
   disabled items do: matching chrome, honest about what's actually implemented. */
.dnx-transform-layout {
    display: flex;
    gap: var(--s5);
    align-items: flex-start;
    min-height: 0;
}

.dnx-transform-nav {
    flex: 0 0 168px;
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    padding-right: var(--s4);
    border-right: 1px solid var(--border);
}

.dnx-transform-nav-label {
    font-size: var(--text-tiny, 0.7rem);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-3, var(--ink-2));
    margin-bottom: 2px;
}

.dnx-transform-nav-item {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: 6px var(--s3);
    border-radius: var(--r-control);
    font-size: var(--text-secondary);
    font-weight: 600;
    color: var(--ink-2);
    cursor: default;
}

.dnx-transform-nav-item.is-active {
    background: var(--app-tint-panel-b);
    color: var(--ink);
}

.dnx-transform-content {
    flex: 1 1 auto;
    min-width: 0;
}

@media (max-width: 720px) {
    .dnx-transform-layout {
        flex-direction: column;
    }

    .dnx-transform-nav {
        flex-direction: row;
        border-right: none;
        border-bottom: 1px solid var(--border);
        padding-right: 0;
        padding-bottom: var(--s3);
    }
}

/* ==========================================================================
   COLUMN PROPERTIES DIALOG — the centered-modal redesign now lives entirely in a
   scoped <style> block inside SubjectAreaColumns.razor (.colprop-* classes), next
   to the markup it styles, rather than here. This file used to carry the retargeted
   .field-config-modal-*/.dim-drawer-* rules for the old right-side-drawer version;
   those classes are no longer rendered by anything, so the rules were removed.
   .dnx-alert-warning-soft survives — the dialog still uses it for the
   system-managed-column notice.
   ========================================================================== */

/* System-managed notice — the brief's amber "changes take effect on next publish" tone,
   applied to the alert-warning box SubjectAreaColumns already renders for locked columns. */
.dnx-alert-warning-soft {
    background: var(--status-started-bg) !important;
    color: var(--status-started-fg) !important;
    border: 1px solid var(--status-started-bd) !important;
    border-radius: var(--r-control) !important;
}

/* ==========================================================================
   COLUMN CARD GRID — design 2a canvas. Retargets .fields-tiles-grid /
   .field-display-tile (SubjectAreaColumns.razor) onto Dimensional elevation.

   No tilt here, deliberately: the tilt is for browse/entity cards a user scans
   and clicks into (Subject Area List's cards). These are dense, functional
   field tiles read edge-to-edge in a working grid, and the perspective skew
   just made the text harder to read without adding anything — cut per
   feedback after seeing it live.
   ========================================================================== */

.fields-tiles-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: var(--s3);
}

.field-display-tile {
    /* common.css predates the grid conversion and still carries `width: max-content;
       max-width: 260px` from when this was a flex-wrap layout — left unset here, a tile
       sized itself to its own unwrapped text (up to 260px) instead of its grid cell, so a
       long system column name (CONSOLIDATION_STATUS_CODE and friends) rendered wider than
       its track and visibly overlapped the next card regardless of the ellipsis rule on
       .field-display-name. Grid items also default to min-width: auto, which independently
       would have fought that same ellipsis. Both are cleared here. */
    width: 100% !important;
    max-width: none !important;
    min-width: 0 !important;
    background: var(--surface) !important;
    border: 1px solid var(--border) !important;
    border-radius: var(--r-card, 14px) !important;
    box-shadow: var(--e2) !important;
    transition: transform var(--dim-duration, 280ms) var(--dim-ease),
                box-shadow var(--dim-duration, 280ms) var(--dim-ease);
}

.field-display-tile:hover,
.field-display-tile:focus-within {
    transform: translateY(-3px);
    box-shadow: var(--e3) !important;
}

.field-display-tile.selected {
    outline: 2px solid var(--accent) !important;
    outline-offset: 2px;
    box-shadow: var(--e-accent) !important;
}

.field-display-name {
    font-family: var(--font-mono) !important;
    color: var(--ink) !important;
}

.field-display-icon-box {
    background: var(--accent-soft, var(--surface)) !important;
    color: var(--accent) !important;
}

.field-system-lock-badge {
    color: var(--ink-3) !important;
}

@media (prefers-reduced-motion: reduce) {
    .field-display-tile {
        transition: none !important;
    }
}

/* ==========================================================================
   STAGING AREA — design 2b. Searchable list on the left, the selected area's
   pipeline/columns on the right. Shared by LandingTable.razor and (once migrated)
   StageAreaTab.razor's list surface.
   ========================================================================== */

.dim-stage-layout {
    display: flex;
    gap: var(--s3);
    height: 100%;
    min-height: 0;
}

.dim-stage-sidebar {
    width: 280px;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-panel, 11px);
    overflow: hidden;
}

.dim-stage-search {
    display: flex;
    align-items: center;
    gap: var(--s1, 8px);
    padding: var(--s2);
    border-bottom: 1px solid var(--border);
    color: var(--ink-3);
}

.dim-stage-search-input {
    flex: 1;
    border: none;
    background: transparent;
    font-family: var(--font-ui);
    font-size: var(--text-body);
    color: var(--ink);
    outline: none;
}

.dim-stage-list {
    flex: 1;
    overflow-y: auto;
    padding: var(--s1, 8px);
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* The active item is accent-bordered rather than filled, so the row's own status pill
   (rendered by the pipeline card, not here) keeps carrying colour meaning on its own. */
.dim-stage-item {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    padding: var(--s2);
    border: 1px solid transparent;
    border-radius: var(--r-control);
    background: transparent;
    text-align: left;
    width: 100%;
    transition: background-color var(--dim-duration, 200ms) var(--dim-ease),
                border-color var(--dim-duration, 200ms) var(--dim-ease);
}

.dim-stage-item:hover {
    background: var(--accent-soft);
}

.dim-stage-item.active {
    background: var(--accent-soft);
    border-color: var(--accent);
}

.dim-stage-item-name {
    font-family: var(--font-ui);
    font-weight: 600;
    font-size: var(--text-body);
    color: var(--ink);
}

.dim-stage-item-desc {
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    color: var(--ink-3);
}

.dim-stage-content {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    overflow-y: auto;
}

/* ── Pipeline strip — Landing → Staging → Subject Area → Reject ───────────
   Four equal cards in one row, each with a 3px colour top rule. No arrow glyphs:
   row order plus rule colour carry the sequence, per the brief. */
.dim-pipeline {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--s2);
    margin-bottom: var(--s3);
}

.dim-pipeline-card {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: var(--s2);
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: 3px solid transparent;
    border-radius: var(--r-panel, 11px);
    box-shadow: var(--e1);
    min-width: 0;
}

.dim-pipeline-card.landing { border-top-color: var(--status-close-dot); }
.dim-pipeline-card.staging { border-top-color: var(--status-open-dot); }
.dim-pipeline-card.subject { border-top-color: var(--status-completed-dot); }
.dim-pipeline-card.reject { border-top-color: var(--dnx-status-danger-fg, #dc3545); }

.dim-pipeline-label {
    font-family: var(--font-ui);
    font-size: var(--text-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--text-micro-tracking);
    color: var(--ink-3);
}

.dim-pipeline-name {
    font-family: var(--font-ui);
    font-size: var(--text-body);
    font-weight: 600;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dim-pipeline-code {
    font-family: var(--font-mono);
    font-size: var(--text-code);
    color: var(--ink-2);
    word-break: break-all;
}

@media (max-width: 900px) {
    .dim-pipeline {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* ── Kafka routing panel ──────────────────────────────────────────────── */
.dim-kafka-panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-panel, 11px);
    padding: var(--s3);
    margin-bottom: var(--s3);
}

.dim-kafka-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--s2);
}

.dim-kafka-panel-title {
    display: flex;
    align-items: center;
    gap: 6px;
    font-family: var(--font-display);
    font-size: var(--text-section);
    font-weight: 600;
    color: var(--ink);
    margin: 0;
}

.dim-count-chip {
    font-family: var(--font-mono);
    font-size: var(--text-code);
    color: var(--ink-2);
    background: var(--surface-solid);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 1px 8px;
}

.dim-info-callout {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: var(--s2);
    background: var(--dnx-status-info-bg);
    color: var(--dnx-status-info-fg);
    border: 1px solid var(--dnx-status-info-bd, transparent);
    border-radius: var(--r-control);
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
}

.dim-inline-link {
    background: none;
    border: none;
    padding: 0;
    color: var(--accent);
    font-family: var(--font-ui);
    font-size: var(--text-secondary);
    text-decoration: underline;
}

.dim-inline-link:hover {
    color: var(--accent-hover);
}

.dim-kafka-rule-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s2);
    padding: var(--s2);
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    margin-bottom: var(--s1, 8px);
    background: var(--surface-solid);
}

.dim-kafka-rule-name {
    font-family: var(--font-ui);
    font-weight: 600;
    color: var(--ink);
}

.dim-kafka-rule-condition {
    font-family: var(--font-mono);
    font-size: var(--text-code);
    color: var(--ink-2);
}

.dim-kafka-branch-editor {
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    padding: var(--s2);
    margin-top: var(--s2);
    background: var(--surface-solid);
}

/* Row-count pill: green-tinted, next to "Results" label */

/* Credential modal (ModalShell) — 420px, rounded 16, modal shadow, pill footer buttons */
.sql-connect-modal .modal-content {
    width: 420px !important;
    max-width: 92vw;
    border-radius: 16px !important;
    box-shadow: 0 12px 40px oklch(0% 0 0 / 20%) !important;
    border: none !important;
    font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
}

.sql-connect-modal .modal-backdrop,
.sql-connect-modal.dnx-modal-backdrop-semi {
    background: oklch(20% 0.01 260 / 45%) !important;
}

.sql-connect-modal .form-label {
    font-size: 12px !important;
    font-weight: 600 !important;
    color: oklch(45% 0.02 260) !important;
}

.sql-connect-modal .form-control {
    border-radius: 10px !important;
    border: 1px solid oklch(88% 0.01 255) !important;
    padding: 9px 12px !important;
    font-size: 13px !important;
}

.sql-connect-modal .btn-primary {
    background: oklch(45% 0.18 265) !important;
    border-color: oklch(45% 0.18 265) !important;
    border-radius: 999px !important;
    font-weight: 700 !important;
}

.sql-connect-modal .btn-primary:hover {
    background: oklch(40% 0.18 265) !important;
    border-color: oklch(40% 0.18 265) !important;
}

.sql-connect-modal .btn-secondary {
    border-radius: 999px !important;
    font-weight: 600 !important;
}

/* Left sidebar (DatabaseExplorer) + connection selector — neutral/border/radius tokens */

/* ==========================================================================
   FLOW & TRANSFORMER — design 2d. The canvas itself (connectors, parallel-branch
   layout) is a bespoke diagram renderer, not the browse-card grid the rest of the
   app uses, and re-architecting it is out of scope for a presentation-only pass.
   This section retargets its existing status vocabulary onto Dimensional tokens:
   "ready" reads as Completed (configured), "pending" as Started (unconfigured),
   matching the brief's node-state-pill convention exactly.
   ========================================================================== */

.flow-status-badge {
    font-family: var(--font-ui) !important;
}

.flow-status-badge.ready {
    background: var(--status-completed-bg) !important;
    color: var(--status-completed-fg) !important;
    border-color: var(--status-completed-bd) !important;
}

.flow-status-badge.pending {
    background: var(--status-started-bg) !important;
    color: var(--status-started-fg) !important;
    border-color: var(--status-started-bd) !important;
}

.flow-node-chip {
    font-family: var(--font-mono) !important;
    background: var(--accent-soft) !important;
    border-color: var(--border) !important;
    color: var(--ink-2) !important;
}

/* Canvas — neutral dot grid, not accent-tinted, so it reads the same regardless of which
   module's accent colour --accent currently resolves to (see .dnx-designer-workspace's
   Vision-accent override above). Layered onto whatever background-color the container
   already carries so scroll/zoom transforms applied in script (both designers pan/scale
   this element) keep working unmodified. */
.designer-canvas-container,
.dnx-designer-canvas {
    background-color: var(--accent-soft) !important;
    background-image: radial-gradient(var(--border-strong) 1px, transparent 1px) !important;
    background-size: 22px 22px !important;
}

/* Node cards — 196-ish px cards at --e2, lifting to --e3 on hover, accent outline when
   selected. Structure (header/icon/title/actions/body/summary) is untouched; this is a
   colour, radius and elevation retarget of the existing .pipeline-node family shared by
   FlowDesigner.razor and PipelineDesigner.razor. */
.pipeline-node {
    border-radius: var(--r-card, 14px) !important;
    border: 1px solid var(--border) !important;
    background: var(--surface) !important;
    box-shadow: var(--e2) !important;
}

.pipeline-node:hover {
    box-shadow: var(--e3) !important;
}

.pipeline-node.selected {
    border-color: var(--accent) !important;
    box-shadow: var(--e-accent) !important;
}

.pipeline-node.running {
    border-color: var(--accent) !important;
}

.pipeline-node-header {
    border-bottom-color: var(--border) !important;
}

.pipeline-node-title {
    font-family: var(--font-mono) !important;
    font-weight: 700 !important;
    color: var(--ink) !important;
}

.pipeline-node-body {
    color: var(--ink-2) !important;
}

.pipeline-node-chip {
    font-family: var(--font-mono) !important;
    background: var(--accent-soft) !important;
    color: var(--accent) !important;
    border-color: var(--border) !important;
}

.pipeline-node-btn:hover {
    background: var(--accent-soft) !important;
    color: var(--accent) !important;
}

.pipeline-node-btn.danger:hover {
    background: var(--dnx-status-danger-bg) !important;
    color: var(--dnx-status-danger-fg) !important;
}

/* "Click to configure" pill — amber notice tokens, not the accent ramp, since this is a
   warning-style nudge rather than a themed status. Shared .pipeline-status-badge.pending
   class, scoped to the designer so QueryTabs/other consumers are unaffected. */
.dnx-designer-workspace .pipeline-status-badge.pending {
    background: var(--notice-bg) !important;
    color: var(--notice-ink) !important;
    border: 1px solid var(--notice-border) !important;
    border-radius: 5px !important;
    font-size: 11.5px !important;
    font-weight: 600 !important;
    padding: 0.15rem 0.5rem !important;
}

/* ── Pipeline header/toolbar row — icon, title, validity badge, zoom/action buttons ──── */
/* Matches the mapping-tab strip right below it (.pipeline-mapping-tabs .query-tabs-container,
   still on common.css's var(--app-tint-hover-a) since nothing here overrode it) — this row
   used to sit on a different token (--surface-solid) with its own border-bottom, reading as a
   separately colored band above the tab strip instead of one continuous bar. This is the rule
   that actually wins in the designer (it out-specifies + !important's the common.css
   .pipeline-header-row rule of the same name), so the earlier common.css edit alone had no
   visible effect. */
.dnx-designer-workspace .pipeline-header-row {
    background: var(--app-tint-hover-a) !important;
    border-bottom: none !important;
    border-radius: var(--r-panel, 10px) var(--r-panel, 10px) 0 0 !important;
}

.dnx-designer-workspace .pipeline-mapping-tabs .query-tabs-container {
    background: var(--app-tint-hover-a) !important;
}

.dnx-designer-workspace .pipeline-header-icon {
    background: var(--accent) !important;
    border-radius: var(--r-control, 8px) !important;
}

.dnx-designer-workspace .pipeline-header-title {
    font-family: var(--font-mono) !important;
    font-size: 14.5px !important;
    font-weight: 700 !important;
    color: var(--ink) !important;
}

.dnx-designer-workspace .pipeline-validity-badge {
    font-size: 11.5px !important;
    font-weight: 600 !important;
}

.dnx-designer-workspace .pipeline-validity-badge.invalid {
    color: var(--dnx-status-danger-fg) !important;
}

.dnx-designer-workspace .pipeline-validity-badge.valid {
    color: var(--status-completed-fg) !important;
}

.dnx-designer-workspace .pipeline-zoom-btn,
.dnx-designer-workspace .pipeline-zoom-group {
    background: var(--surface-solid) !important;
    border-color: var(--border) !important;
    border-radius: var(--r-control, 6px) !important;
    color: var(--ink-2) !important;
    font-size: 12.5px !important;
    font-weight: 600 !important;
}

.dnx-designer-workspace .pipeline-zoom-btn:hover:not(:disabled) {
    background: var(--accent-soft) !important;
    border-color: var(--accent) !important;
    color: var(--accent) !important;
}

.dnx-designer-workspace .pipeline-zoom-group .pipeline-zoom-btn {
    border-radius: 0 !important;
}

.dnx-designer-workspace #pipeline-run-btn {
    background: var(--accent) !important;
    border-color: var(--accent) !important;
    color: var(--accent-ink) !important;
    font-weight: 700 !important;
}

.dnx-designer-workspace #pipeline-run-btn:hover:not(:disabled) {
    background: var(--accent-hover) !important;
    border-color: var(--accent-hover) !important;
}

.dnx-designer-workspace .designer-clear-btn {
    background: transparent !important;
    border: 1px solid var(--dnx-status-danger-bd, #f0b0ae) !important;
    color: var(--dnx-status-danger-fg) !important;
}

.dnx-designer-workspace .designer-clear-btn:hover:not(:disabled) {
    background: var(--dnx-status-danger-bg) !important;
}

/* ── Tab bar (MappingTabs.razor / .query-tab) ─────────────────────────────────────── */
.dnx-designer-workspace .pipeline-mapping-tabs .query-tab {
    font-family: var(--font-mono) !important;
    font-size: 13px !important;
    font-weight: 600 !important;
}

.dnx-designer-workspace .pipeline-mapping-tabs .query-tab.active {
    border-bottom: 2px solid var(--accent) !important;
    color: var(--ink) !important;
}

.dnx-designer-workspace .pipeline-mapping-tabs .query-tab.active i {
    color: var(--accent) !important;
}

/* ── Fit-to-screen — floating square button, bottom-right of the canvas ─────────────── */
.dnx-designer-workspace .pipeline-canvas-area {
    position: relative;
}

.dnx-designer-workspace .pipeline-fit-to-screen-btn {
    position: absolute;
    right: var(--s4, 16px);
    bottom: var(--s4, 16px);
    width: 34px;
    height: 34px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--surface-solid);
    border: 1px solid var(--border);
    border-radius: var(--r-control, 8px);
    box-shadow: var(--e1);
    color: var(--ink-2);
    cursor: pointer;
    z-index: 5;
}

.dnx-designer-workspace .pipeline-fit-to-screen-btn:hover {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent);
}

/* Component palette (DesignerPalette.razor) — colored tile + name + description,
   cursor:grab per the brief. Scoped to the designer's own wrapper so the same
   .palette-item class shared by the left nav dock is unaffected. */
.designer-palette-wrapper .palette-item {
    cursor: grab;
    border-radius: 10px !important;
    font-family: var(--font-ui) !important;
    background: var(--surface, var(--app-surface)) !important;
    border: 1px solid var(--border, var(--app-border-soft)) !important;
    box-shadow: var(--e1);
    margin-bottom: 0.35rem;
}

.designer-palette-wrapper .palette-item:hover {
    background: var(--accent-soft) !important;
    border-color: var(--accent, #0d6efd) !important;
    box-shadow: var(--e2);
    transform: translateY(-2px);
}

.designer-palette-wrapper .palette-item-icon {
    width: 28px !important;
    height: 28px !important;
    min-width: 28px !important;
    border-radius: 7px !important;
    font-size: 0.9rem !important;
}

/* ==========================================================================
   RESPONSIVE — the three breakpoints from the brief, applied to the surfaces
   built in this pass (Staging Area's sidebar+pipeline, the column card grid,
   and the Flow palette). The shell's own dock/top-bar breakpoints already
   live earlier in this file.
   ========================================================================== */

/* 768–1279px: dock stays collapsed (handled by the shell), card grids drop to
   2-up, the Flow palette collapses to icon-only. */
@media (min-width: 768px) and (max-width: 1279px) {
    .fields-tiles-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .dim-pipeline {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .designer-palette-wrapper .fw-semibold.text-truncate,
    .designer-palette-wrapper .text-muted.text-truncate {
        display: none;
    }
}

/* <768px: cards go 1-up and flat (tilt already gated to >=1024px above), the
   stage sidebar stacks above its content instead of sitting beside it. */
@media (max-width: 767px) {
    .fields-tiles-grid,
    .dim-pipeline {
        grid-template-columns: minmax(0, 1fr);
    }

    .dim-stage-layout {
        flex-direction: column;
    }

    .dim-stage-sidebar {
        width: 100%;
        max-height: 40vh;
    }
}
