/* Code Block Viewers (JSON, Mermaid, etc.) - Common Styles */
.code-block-wrapper {
    margin: 0.75rem 0;
    border-radius: 6px;
    overflow: hidden;
}

.json-viewer-container {
    font-size: 0.9em;
    padding: 1rem;
    border-radius: 6px;
    overflow-x: auto;
    overflow-y: visible;
}

.mermaid-viewer-container {
    padding: 1rem;
    border-radius: 6px;
    text-align: center;
    overflow-x: auto;
    overflow-y: visible;
}

.mermaid-viewer-container svg {
    max-width: 100%;
    height: auto;
}

.code-parse-error,
.code-block {
    padding: 1rem;
    border-radius: 6px;
    margin: 0;
    font-family: 'Courier New', monospace;
    font-size: 0.85em;
    overflow-x: auto;
}

.code-parse-error code,
.code-block code {
    display: block;
    white-space: pre;
}

.code-error-message {
    padding: 0.5rem 1rem;
    font-size: 0.85em;
    font-weight: 500;
    border-radius: 0 0 6px 6px;
}

/* Chat Message Expand/Collapse - Common Styles */

.message-text.collapsed,
.thinking-content.collapsed {
    max-height: 200px;
    overflow: hidden;
    position: relative;
}

.message-text.collapsed::after,
.thinking-content.collapsed::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 60px;
    pointer-events: none;
}

.message-text.expanded,
.thinking-content.expanded {
    max-height: none;
}

.message-expand-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.5rem;
    padding: 0.35rem 0.75rem;
    font-size: 0.85em;
    font-weight: 500;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.2s ease;
}

.message-expand-toggle:hover {
    transform: translateY(-1px);
}

.message-expand-toggle i {
    font-size: 0.9em;
    transition: transform 0.2s ease;
}

.message-expand-toggle.expanded i {
    transform: rotate(180deg);
}

/* ================================================================
   LAYOUT MANAGEMENT PANEL - COMMON STYLES
   ================================================================ */

/* Layout list container */
.layout-management-main-form .list-group-item {
    display: flex !important;
    align-items: center !important;
    padding: 0.5rem !important;
}

/* Edit and delete buttons container - fixed width for alignment */
.layout-management-main-form .btn-group {
    display: flex !important;
    align-items: center !important;
    flex-shrink: 0 !important;
    gap: 2px;
}

/* All action buttons - fixed size for consistent alignment */
.layout-management-main-form .layout-action-btn,
.layout-management-main-form .btn-group .btn-link {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    width: 24px !important;
    height: 24px !important;
    min-width: 24px !important;
    padding: 0 !important;
    margin: 0 !important;
    font-size: 12px !important;
    text-decoration: none !important;
    border: none !important;
    background: transparent !important;
}

/* Icon inside buttons */
.layout-management-main-form .layout-action-btn i,
.layout-management-main-form .btn-group .btn-link i {
    font-size: 12px !important;
    line-height: 1 !important;
}

.layout-management-main-form .layout-name-link {
    min-width: 0;
    flex-grow: 1;
}

.daterangepicker .calendar-time select {
    min-width: 57px;
}

.authoring-drag-item.ui-draggable-dragging {
    max-width: 120px;
}

/* The dot is drawn at 0x0 (the two !important widths below), so its colour is invisible today —
   but `indianred` is a hard-coded literal in the one stylesheet every skin loads, and the moment
   somebody gives it a size it paints off-palette on all five. Bound to the danger token, which
   every skin defines, with the original literal as the fallback. */
.vertical-time-simple .vertical-timeline-element-icon::after {
    content: '';
    position: absolute;
    background: var(--red, indianred);
    left: 50%;
    top: 50%;
    margin: -4px 0 0 -4px;
    display: block;
    width: 0 !important;
    height: 0 !important;
    border-radius: 20px;
}

.header-dots .icon-wrapper-alt i {
    margin-left: 16px;
}

.icon-wrapper.icon-wrapper-alt.rounded-circle .badge {
    min-width: inherit;
    top: -17px;
    right: 8px;
}

.RichForm .validation-message {
    padding: 5px 4px 2px 0;
}

/* ================================================================
   FORM VALIDATION MESSAGE PANEL - Common structural styles
   Color overrides are in individual theme files
   ================================================================ */

.form-validation-message-panel {
    display: none; /* Hidden by default, shown via JS when errors exist */
}

.form-validation-message-panel .validation-header {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
    font-weight: 600;
}

.form-validation-message-panel .validation-header i {
    font-size: 1.2em;
}

.form-validation-message-panel .validation-title {
    font-size: 0.95rem;
}

.form-validation-message-panel .validation-error-list {
    list-style: none;
    padding-left: 0;
    margin-bottom: 0;
}

.form-validation-message-panel .validation-error-item {
    display: flex;
    align-items: flex-start;
    padding: 0.35rem 0;
}

.form-validation-message-panel .validation-error-item:last-child {
    border-bottom: none;
}

.form-validation-message-panel .validation-error-item i {
    margin-right: 0.5rem;
    margin-top: 0.15rem;
    flex-shrink: 0;
}

/* Field error links - clickable to focus field */
.form-validation-message-panel .field-error-link {
    display: flex;
    align-items: flex-start;
    text-decoration: none;
    color: inherit;
    width: 100%;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    transition: background-color 0.2s ease;
}

.form-validation-message-panel .field-error-link:hover {
    text-decoration: none;
}

.form-validation-message-panel .field-error-link i {
    margin-top: 0.2rem;
}

.form-validation-message-panel .field-name {
    margin-right: 0.25rem;
}

/* Warning sections */
.form-validation-message-panel .validation-global-warnings,
.form-validation-message-panel .validation-field-warnings {
    display: none; /* Hidden by default */
}

.form-validation-message-panel .validation-warning-list {
    list-style: none;
    padding-left: 0;
    margin-bottom: 0;
}

.form-validation-message-panel .validation-warning-item {
    display: flex;
    align-items: flex-start;
    padding: 0.35rem 0;
}

.form-validation-message-panel .validation-warning-item:last-child {
    border-bottom: none;
}

.form-validation-message-panel .validation-warning-item i {
    margin-right: 0.5rem;
    margin-top: 0.15rem;
    flex-shrink: 0;
}

/* Field warning links - clickable to focus field */
.form-validation-message-panel .field-warning-link {
    display: flex;
    align-items: flex-start;
    text-decoration: none;
    color: inherit;
    width: 100%;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    transition: background-color 0.2s ease;
}

.form-validation-message-panel .field-warning-link:hover {
    text-decoration: none;
}

.form-validation-message-panel .field-warning-link i {
    margin-top: 0.2rem;
}

/* Ignore warnings button */
.form-validation-message-panel .ignore-warnings-btn {
    font-size: 0.8rem;
    padding: 0.2rem 0.5rem;
}

/* Highlight effect structural styles */
.validation-highlight {
    border-radius: 4px;
}

/* Responsive adjustments */
@media (max-width: 768px) {
    .form-validation-message-panel .validation-header {
        flex-wrap: wrap;
    }

    .form-validation-message-panel .field-error-link {
        flex-wrap: wrap;
    }

    .form-validation-message-panel .field-name {
        flex-basis: 100%;
        margin-bottom: 0.25rem;
    }
}
/* Field Addressed State - Visual feedback when user modifies a field that had an error/warning */
.form-validation-message-panel .field-error-link.field-addressed,
.form-validation-message-panel .field-warning-link.field-addressed {
    text-decoration: line-through;
    opacity: 0.6;
    position: relative;
}

.form-validation-message-panel .field-error-link.field-addressed .error-message,
.form-validation-message-panel .field-warning-link.field-addressed .warning-message {
    text-decoration: line-through;
}

.form-validation-message-panel .field-error-link.field-addressed .field-name,
.form-validation-message-panel .field-warning-link.field-addressed .field-name {
    text-decoration: line-through;
}

.form-validation-message-panel .addressed-indicator {
    color: var(--green, #28a745);
    font-weight: bold;
    margin-left: 8px;
    text-decoration: none !important;
    display: inline-block;
}

.form-validation-message-panel .field-error-link.field-addressed .addressed-indicator {
    color: var(--green, #28a745);
}

.form-validation-message-panel .field-warning-link.field-addressed .addressed-indicator {
    color: var(--green, #28a745);
}

/* Prevent strikethrough on the checkmark */
.form-validation-message-panel .field-addressed .addressed-indicator {
    text-decoration: none !important;
}

/* ================================================================
   PDF templates plugin — theme-agnostic fixes that apply in every skin
   Built on --bs-* CSS variables which each theme already redefines,
   so the same rules adapt to the active palette automatically.
   ================================================================ */

/* Fullscreen toggle uses btn-outline-dark for a "subtle/neutral" look in the
   Standard skin, but the bootstrap default resolves both the border and the
   text to --bs-dark — which equals the background color in every dark skin,
   making the icon and label disappear. Bind the colors to --bs-body-color
   and --bs-border-color so the button stays legible against any palette,
   and on hover invert to a filled-in look that matches the surrounding
   outline buttons (Add/Duplicate/Remove). */
.pdftemplate-fullscreen-toggle.btn-outline-dark,
.pdftemplate-fullscreen-toggle.btn-outline-dark:focus {
    color: var(--bs-body-color);
    border-color: var(--bs-border-color);
    background-color: transparent;
}

.pdftemplate-fullscreen-toggle.btn-outline-dark:hover,
.pdftemplate-fullscreen-toggle.btn-outline-dark:active {
    color: var(--bs-body-bg);
    border-color: var(--bs-body-color);
    background-color: var(--bs-body-color);
}

/* pdfme's field list (right sidebar) renders each row with an inline
   `style="color: rgba(0, 0, 0, 0.88)"` on the <div title="..."> that holds
   the schema-type SVG (rect, qr, text, ...). The SVG uses
   stroke="currentColor", so on every dark skin those icons stay near-black
   and disappear against the row's dark background. The same hard-coded
   inline color affects the left tool-palette icons too. Force every titled
   wrapper inside the designer to inherit its row's color, which each theme
   already paints with a legible foreground via its .pdfme-designer-*
   rules. */
.pdfme-designer-right-sidebar div[title],
.pdfme-designer-left-sidebar div[title] {
    color: inherit !important;
}

/* ─── CRUD Join section (FieldEditorDialog) ─────────────────────────────
   The "Join" panel inside the field editor used Bootstrap's bg-light, which
   in dark themes resolves to --current-line (a dark surface) while labels
   keep their default inherited color — making "Keep the original FK field
   in the DTO too" and similar text near-invisible on dark themes.

   This block keeps the visual separation (border + slightly raised surface)
   while guaranteeing legible text in every theme. CSS variables fall back
   to Bootstrap-equivalent light values so the standard (light) theme,
   which doesn't expose them, still renders the panel the way it did
   before. */
.crud-join-section {
    background-color: var(--current-line, #f8f9fa);
    color: var(--foreground, #212529);
    border-color: var(--comment, #dee2e6) !important;
}
.crud-join-section .form-label,
.crud-join-section .form-check-label,
.crud-join-section table,
.crud-join-section th,
.crud-join-section td,
.crud-join-section strong,
.crud-join-section span {
    color: var(--foreground, inherit);
}
.crud-join-section .text-muted,
.crud-join-section .form-text {
    color: var(--comment, #6c757d) !important;
}
.crud-join-section .form-control,
.crud-join-section .form-select {
    background-color: var(--background, #fff);
    color: var(--foreground, #212529);
    border-color: var(--comment, #ced4da);
}
.crud-join-section .table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--foreground, #212529);
    --bs-table-border-color: var(--comment, #dee2e6);
}

/* ============================================================
   Unused-rule highlighting — Rules list "Mark unused rules"
   Theme-aware via the shared --red / --bs-danger custom props
   (defined in dracula.css, dark-blue.css, etc.); fallbacks keep
   it correct on the light/standard theme too.
   ============================================================ */
tr.rule-row-unused > td {
    background-color: rgba(220, 53, 69, 0.10) !important;
    color: var(--red, #b02a37) !important;
}
tr.rule-row-unused > td:first-child {
    box-shadow: inset 4px 0 0 0 var(--red, #dc3545);
}
tr.rule-row-unused .card-title {
    color: var(--red, #b02a37) !important;
    font-weight: 600;
}
/* "Not used" badge — solid themed red, beats the table-cell text colour. */
.badge.rule-unused-badge {
    background-color: var(--red, #dc3545) !important;
    color: #fff !important;
    font-weight: 600;
    letter-spacing: .2px;
}
.rule-scan-progress .progress {
    margin-top: .25rem;
}


/* Prohibited (readonly-by-rule) form fields — rendered as a label.form-control by the
   form controls' initProhibited(). The DISABLED look itself comes from each theme skin:
   every theme's ".form-control:disabled" selector group also lists
   "label.form-control.nct-prohibited" (standard/dark/dark-blue/dracula/forest), so the
   label gets exactly that theme's disabled background/border/color. Theme skins load
   AFTER this file, so their equal-specificity !important rules win over the fallback
   below — the fallback only matters if a skin lacks a disabled rule.
   min-height keeps the box from collapsing when the value is empty (Bootstrap input height). */
label.form-control.nct-prohibited {
    background-color: rgba(127, 127, 127, 0.22) !important;
    min-height: calc(1.5em + 0.75rem + 2px);
    margin-bottom: 0;
    cursor: default;
}

/* ================================================================================================
   AJAX WAIT — all skins

   WHAT IT IS. jQuery blockUI (`common/js/common-d.js showTheBlockPanel`) and the Wicket BlockPanel
   both inject the same thing: `.loader > .ball-grid-pulse > nine <div>`, inside a box blockUI
   styles INLINE as `background:#fff; border:3px solid #aaa`. No text, ever.

   WHAT WAS WRONG WITH IT. Three things, and the box was the worst of them:
     - a filled card whose only content is a spinner is chrome around chrome. It draws a hard
       rectangle over the content the user is waiting for and tells them nothing;
     - nine dots pulsing on nine different delays is nine moving objects. The eye cannot settle on
       any of them, so a two-second wait feels longer than it is;
     - saturated dots make the busiest moment of the screen also the loudest.

   WHAT IT IS NOW. One ring on the frosted veil, no card. A faint full track says where the figure
   is; a single arc sweeps it. One object, one axis, one speed — the shape a wait should have. The
   halo under it is a drop-shadow of the ring itself, so the figure sits in the page rather than
   floating in a hole.

   HOW, WITHOUT TOUCHING THE MARKUP. The nine divs are hidden and the ring is drawn on the
   container's two pseudo-elements: ::before is the track, ::after the arc (later, so it paints on
   top). The arc is a conic-gradient masked into a ring — that is what allows a GRADIENT arc, which
   a border-based spinner cannot do; the Dark skin uses it to sweep its own brand pair.

   Colour comes from `--bs-primary`, which all five skins define — including standard.css, which has
   no semantic palette. Stylesheet `!important` beats blockUI's inline styles.
   ================================================================================================ */
@keyframes nct-wait-spin {
    to { transform: rotate(1turn); }
}

@keyframes nct-wait-breathe {
    0%, 100% { opacity: .30; }
    50%      { opacity: .85; }
}

/* No card. The ring sits on the veil — the veil is already the thing saying "busy", and a second
   container around a spinner adds a rectangle and no information. */
.blockUI.blockMsg,
div.blockMsg,
.blockPage .loader {
    background: none !important;
    background-color: transparent !important;
    border: none !important;
    box-shadow: none !important;
    padding: 0 !important;
    color: var(--bs-primary, #3f6ad8) !important;
}

/* blockUI hard-codes width:30%; top:40%; left:35% inline — hug the content and true-centre it. */
.blockUI.blockMsg,
div.blockMsg {
    width: auto !important;
    min-width: 0 !important;
    top: 50% !important;
    left: 50% !important;
    transform: translate(-50%, -50%) !important;
}

.blockUI.blockMsg .loader,
div.blockMsg .loader {
    display: flex !important;
    align-items: center;
    justify-content: center;
}

/* The vendor sizes this 57px wide and lets nine floated children give it height. With the children
   gone it would collapse, so it gets its own box. */
.ball-grid-pulse {
    position: relative;
    width: 44px !important;
    height: 44px !important;
    /* The ring's own light. A drop-shadow follows the alpha of what it is cast from, so this
       tracks the arc as it sweeps instead of sitting under it as a static blob. */
    filter: drop-shadow(0 0 14px rgba(var(--bs-primary-rgb, 63, 106, 216), .45));
}

.ball-grid-pulse > div,
.ball-grid-pulse > div.bg-white {
    display: none !important;
}

/* the track — where the arc is going */
.ball-grid-pulse::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 2.5px solid currentColor;
    opacity: .16;
}

/* the arc — one sweep of the accent, masked out of a conic gradient into a 2.5px ring */
.ball-grid-pulse::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: conic-gradient(from 0turn,
            transparent 0turn 0.05turn,
            currentColor 0.62turn,
            transparent 0.95turn);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
    mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
    animation: nct-wait-spin .85s linear infinite;
}

/* THE VEIL — dim the backdrop, do not PAINT OVER it.
   ------------------------------------------------------------------------------------------------
   The old veil was a black wash: `rgba(8,12,20,.44)`, and `rgba(2,4,8,.58)` in the Dark skin. On a
   light page that is fine. On a near-black one it is not, and the reason is arithmetic: over the
   Dark skin's panel `#0c1422` a 58% black wash composites to `#060b13` — DARKER than the page
   ground `#080e19`. The blocked region therefore became the blackest rectangle on the screen, with
   every edge, row and header inside it crushed to the same value. It did not read as "busy", it
   read as a hole punched in the page, or as a component that failed to render.

   You cannot dim by darkening when there is no headroom left below you. So the dimming is done with
   `backdrop-filter: brightness()`, which scales what is behind PROPORTIONALLY: a bright table goes
   to 72% of itself and stays a table, and a dark panel barely moves because it had nothing to give.
   The tint that remains is a LIGHT film, not a dark one — that is what makes frosted glass on a
   dark UI read as a layer of glass rather than as an absence. Blur and a small desaturation do the
   rest of the "not yours to touch" work.

   Everything is tokenised because the light skin needs the opposite balance: on white, 72% is the
   lights going out, and a white film is invisible. standard.css overrides all three. */
.blockUI.blockOverlay,
div.blockOverlay {
    background-color: var(--wait-veil, rgba(150, 180, 220, .05)) !important;
    opacity: 1 !important;
    backdrop-filter: blur(4px) saturate(var(--wait-sat, 80%)) brightness(var(--wait-dim, .72)) !important;
    -webkit-backdrop-filter: blur(4px) saturate(var(--wait-sat, 80%)) brightness(var(--wait-dim, .72)) !important;
}

/* Without backdrop-filter there is nothing to dim WITH, so the wash comes back — but only there. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .blockUI.blockOverlay,
    div.blockOverlay {
        background-color: var(--wait-veil-flat, rgba(10, 16, 28, .58)) !important;
    }
}

/* A slow, very low-contrast band travelling across the veil.
   This is the part that matters when the blocked region is EMPTY — a tab body that has not loaded
   yet has no content to blur, so a veil over it is just a rectangle, and a 44px ring alone in a
   1480x290 band looks lost rather than busy. The sweep gives the REGION a state instead of putting
   a second object in it: the area itself reads as being filled. It is the skeleton idiom without
   any skeleton markup, which matters here because the blocked element is arbitrary content this
   stylesheet has never seen. 4.5% white at 105 degrees is deliberately near the threshold — it
   should be noticed only in peripheral vision. */
@keyframes nct-wait-sweep {
    from { background-position: 150% 0; }
    to   { background-position: -70% 0; }
}

.blockUI.blockOverlay::after,
div.blockOverlay::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image: linear-gradient(105deg,
            transparent 32%,
            var(--wait-sweep, rgba(255, 255, 255, .045)) 48%,
            transparent 64%);
    background-repeat: no-repeat;
    background-size: 260% 100%;
    animation: nct-wait-sweep 2.1s ease-in-out infinite;
}

/* A spinner is the one animation that carries meaning — remove it and the user cannot tell a slow
   request from a dead page. So it is not switched off, it is switched to something that does not
   move: the whole ring breathes instead of rotating. */
@media (prefers-reduced-motion: reduce) {
    .ball-grid-pulse::after {
        animation: none;
        background: none;
        border: 2.5px solid currentColor;
        -webkit-mask: none;
        mask: none;
    }

    .ball-grid-pulse::before {
        animation: nct-wait-breathe 1.6s ease-in-out infinite;
    }

    /* A band crossing the screen is exactly the kind of large-area motion this setting is for. */
    .blockUI.blockOverlay::after,
    div.blockOverlay::after {
        animation: none;
        background-image: none;
    }
}

/* ================================================================
   PLUGIN EDITOR BUTTON STRIP — structure (all themes)

   The strip that appears over a component in authoring mode ("Edit / Reset Tabs /
   Settings / Save component / Save common component") is built in JS by mrjun's
   ViewEditBehavior.showEditorButtons with INLINE styles:
       container -> background: #2096f3
       buttons   -> color: white; background: transparent
   Inline styles beat stylesheets, so every colour rule below and in the per-theme
   blocks needs !important. The hook is the container class the behaviour adds,
   `plugin-editor-btn-container`, which both the mrjun and the nct-ui behaviour use.

   Layout/shape lives here; colour lives in each themes/<name>.css.
   ================================================================ */
.plugin-editor-btn-container {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 3px !important;
    border-radius: 10px !important;
    border: 1px solid transparent;
    box-shadow: 0 10px 24px rgba(0, 0, 0, .28);
    backdrop-filter: saturate(160%) blur(8px);
    -webkit-backdrop-filter: saturate(160%) blur(8px);
}

.plugin-editor-btn-container .btn {
    height: 28px;
    display: inline-flex !important;
    align-items: center;
    padding: 0 .65rem !important;
    margin: 0 !important;
    border: 0 !important;
    border-radius: 7px !important;
    font-size: .75rem !important;
    font-weight: 600 !important;
    line-height: 1 !important;
    white-space: nowrap;
    box-shadow: none !important;
    transition: background-color .15s ease, color .15s ease;
}

/* Context menu the nct-ui behaviour opens on double-click; appended to <body>, so it
   cannot inherit anything from the plugin it belongs to. */
.nct-plugin-context-menu {
    padding: 5px !important;
    border-radius: 11px !important;
}

.nct-plugin-context-menu .dropdown-header {
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    opacity: .7;
}

.nct-plugin-context-menu .dropdown-item {
    border-radius: 7px;
    font-size: .8125rem;
}

/* ================================================================================================
   THEME SWITCH — structure (all skins)

   The Dark <-> Standard control in the app header (HeaderThemeSelectorPanel). It is the same
   object in every skin, so its geometry is written once here and each themes/<name>.css supplies
   only the palette — the same split the plugin editor button strip above already uses.

   HOW IT READS: a pill with a sun at one end and a moon at the other, and a knob that sits ON the
   icon for the theme currently in force. The icon under the knob takes the knob's ink; the other
   stays quiet. There is no label and no colour change of the track, because the position of the
   knob IS the state — that is the one thing a switch is better at than a menu.

   WHY THE KNOB IS BEHIND THE ICONS (z-index 0 vs 1): sliding an opaque disc OVER the icon would
   hide it, and then the control shows one icon and a blank, which reads as broken rather than as
   chosen.

   A skin here is a whole-stylesheet swap (PluginBasePage.renderHead links exactly one
   themes/<skin>.css), and the panel does that swap in place from JavaScript rather than reloading.
   The .26s travel below therefore runs at the same moment the new sheet is being fetched: the knob
   moves on pointer-down, the sheet lands, and the page changes under it. Where a swap cannot be
   correct — a skin that ships JS, or a page carrying per-skin authored CSS — the panel falls back
   to a full reload and the knob's position is re-rendered by the server instead.
   ================================================================================================ */
/* `.header-dots` is a flex row with the default `align-items: stretch` (base.css), and every other
   thing in it is a full-height <div class="dropdown">. The switch's wrapper carries no such class,
   so it stretched to the row and the 30px pill floated above the bell/flag/avatar centre line.
   Centring the ROW is the fix that does not also hand the wrapper `position: relative`. */
.header-dots {
    align-items: center;
}

.nct-theme-switch {
    display: inline-flex;
    align-items: center;
    padding: 0;
    margin: 0 .35rem;
    border: 0;
    background: none;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.nct-theme-switch:hover,
.nct-theme-switch:focus {
    text-decoration: none;
}

.nct-theme-switch__track {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 30px;
    border: 1px solid transparent;
    border-radius: 999px;
    transition: background-color .22s ease, border-color .22s ease, box-shadow .22s ease;
}

/* GEOMETRY — every number below is measured in the track's PADDING box, because that is the
   containing block of an absolutely positioned child, and base.css sets a universal
   `box-sizing: border-box`. So the 60x30 track with a 1px border gives a 58x28 field:

       knob     22px, inset 3px  ->  3 + 22 + 3 = 28 vertically (centred) and a rest centre at 14
       icon     28px wide, left:0 / right:0  ->  centres at 14 and 44
       travel   44 - 14 = 30px

   Change the track's width and exactly two of those follow: the icon width (half the track's field)
   and the travel. Getting them out of step is invisible at rest and shows as a knob sitting a
   couple of pixels off the moon.

   Rest position is the SUN and `is-dark` is what moves it, which matches the control's aria: the
   switch is named "Dark theme" and aria-checked="true" means on, so checked == knob to the right.
   Left-is-off / right-is-on is also what every OS switch does, and a theme control that reversed it
   would read as "light is on" to anyone who does not stop to look at the two icons. */
.nct-theme-switch__knob {
    position: absolute;
    z-index: 0;
    top: 3px;
    left: 3px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    transition: transform .26s cubic-bezier(.4, 0, .2, 1), background .22s ease, box-shadow .22s ease;
    will-change: transform;
}

.nct-theme-switch.is-dark .nct-theme-switch__knob {
    transform: translateX(30px);
}

/* `top: 0; bottom: 0` rather than a height: the cell then IS the track's field, so the icon is
   centred by construction and cannot overflow it the way a hard 30px did. */
.nct-theme-switch__ico {
    position: absolute;
    z-index: 1;
    top: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    font-size: 15px;
    line-height: 1;
    pointer-events: none;
    transition: color .22s ease;
}

.nct-theme-switch__ico--sun  { left: 0; }
.nct-theme-switch__ico--moon { right: 0; }

/* The control carries no visible label, so the accessible name is a real, translated string kept
   off-screen rather than a title attribute — a title is not announced reliably and never on touch. */
.nct-theme-switch__sr {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* SCOPED TO `.app-header` ON PURPOSE. The same TopUserProfilePanel is embedded by the public
   site's own bar (SiteNavPlugin), which is NOT inside `.app-header` and dresses the account block
   from dokie-site.css. An unprefixed rule here would reach across and repaint the site's avatar
   with the app shell's chrome. Everything in this block that touches the account cluster carries
   the prefix for that reason. */

/* The account menu clips its banner to its own corners. Radius is a var so a skin that defines a
   scale gets it and every other skin keeps the 8px the template already uses everywhere. */
.app-header .dropdown-menu.dropdown-menu-lg {
    overflow: hidden;
    border-radius: var(--radius, 8px);
}

.app-header .dropdown-menu.dropdown-menu-lg .dropdown-menu-header,
.app-header .dropdown-menu.dropdown-menu-lg .dropdown-menu-header-inner {
    border-top-left-radius: var(--radius, 8px);
    border-top-right-radius: var(--radius, 8px);
}

/* Colour-free affordances for the account cluster, so every skin gets them and only the tints
   differ. base.css gives the trigger no hover, no focus and no open state at all. */
.app-header .header-btn-lg .widget-content-left .btn-group > a.btn img.rounded-circle {
    transition: box-shadow .2s ease;
}

.app-header .header-btn-lg .widget-content-left .btn-group > a.btn .fa-angle-down {
    transition: transform .2s ease, color .16s ease;
}

.app-header .header-btn-lg .widget-content-left .btn-group.show > a.btn .fa-angle-down {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    .nct-theme-switch__knob,
    .nct-theme-switch__track,
    .nct-theme-switch__ico,
    .app-header .header-btn-lg .widget-content-left .btn-group > a.btn img.rounded-circle,
    .app-header .header-btn-lg .widget-content-left .btn-group > a.btn .fa-angle-down {
        transition: none;
    }
}

/* ================================================================================================
   OBJECT / RULE SELECTOR — all skins

   The control behind every "pick a rule" field (`ObjectSelectorAbstractField`): a read-only value
   on the left, one trigger on the right. It is one of the most-used controls in the authoring
   panels and it was the least finished one — the trigger carried `style="background: white"` in
   the markup, so on four of the five skins it was a white plate that no stylesheet could repaint,
   and it kept its own small height while the value beside it wrapped to two or three lines,
   leaving the hamburger floating against the top edge.

   Written here rather than per skin because every colour below resolves through `--bs-*`, which
   all five define — including standard.css, which has no semantic palette at all. Specificity is
   (0,2,0) on purpose: this file loads BEFORE the skin, and the skins paint `.input-group-text` at
   (0,1,0) with `!important`, so a bare class here would lose. Among `!important` declarations the
   cascade compares specificity before source order, which is what makes the two-class form win.
   ================================================================================================ */
.input-group.nct-objsel {
    flex-wrap: nowrap;      /* the trigger must never drop onto its own line */
    align-items: stretch;   /* ...and must match whatever height the value takes */
}

/* One border weight for both halves. The skins give `.form-control` a 3px fence (2px on Dark after
   its rebuild) and `.input-group-text` 2px, so the two halves of this control never lined up in ANY
   skin — a 1px step visible right where they meet. Scoped to this control, so no other input group
   changes; the COLOUR still comes from whichever skin is loaded. */
.input-group.nct-objsel > .nct-objsel__value,
.input-group.nct-objsel > .nct-objsel__choose {
    border-width: 1px !important;
    border-style: solid !important;
}

/* The value. It is a <label>, not an input, so it has to be told to lay its text out like one —
   and to WRAP, because a rule path ("rimmContext · plant · code · Max length (4)") is regularly
   longer than the panel is wide and truncating it hides the part that identifies the rule. */
.input-group.nct-objsel > .nct-objsel__value {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    height: auto;
    min-height: 34px;
    white-space: normal;
    overflow-wrap: anywhere;
    line-height: 1.35;
    margin-bottom: 0;
    border-top-right-radius: 0 !important;
    border-bottom-right-radius: 0 !important;
    border-right-width: 0 !important;
}

/* The trigger. `align-self: stretch` is the fix the user could see: it is what makes the button as
   tall as the value beside it. The border is dropped on the shared edge and redrawn as an inset
   hairline, so the two halves read as ONE control with a divider rather than as two boxes that
   happen to touch. */
.input-group.nct-objsel > .nct-objsel__choose {
    display: flex;
    align-items: stretch;
    align-self: stretch;
    padding: 0 !important;
    min-width: 40px;
    background-color: var(--bs-tertiary-bg, rgba(127, 127, 127, .08)) !important;
    background-image: none !important;
    color: var(--bs-body-color) !important;
    border-top-left-radius: 0 !important;
    border-bottom-left-radius: 0 !important;
    border-left-width: 0 !important;
    box-shadow: inset 1px 0 0 0 var(--bs-border-color);
    transition: background-color .16s ease;
}

/* Both modes — the plain <i> when there is one action, and the .btn when there are several — fill
   the trigger completely, so the whole 40px column is the hit target rather than a 16px glyph. */
.input-group.nct-objsel > .nct-objsel__choose > .nct-objsel__btn {
    display: flex !important;
    flex: 1 1 auto;
    align-items: center;
    justify-content: center;
    gap: .3rem;
    margin: 0 !important;
    padding: 0 .6rem !important;
    border: 0 !important;
    border-radius: 0 !important;
    background: none !important;
    color: inherit !important;
    line-height: 1;
    cursor: pointer;
}

.input-group.nct-objsel > .nct-objsel__choose:hover {
    background-color: var(--bs-secondary-bg, rgba(127, 127, 127, .16)) !important;
}

.input-group.nct-objsel > .nct-objsel__choose > .nct-objsel__btn:hover,
.input-group.nct-objsel > .nct-objsel__choose > .nct-objsel__btn:focus {
    color: var(--bs-primary) !important;
    background: none !important;
    box-shadow: none !important;
}

/* base.css ends with `a, button, .btn { outline: none !important }`, so without this the trigger
   has no keyboard focus state at all. */
.input-group.nct-objsel > .nct-objsel__choose > .nct-objsel__btn:focus-visible {
    outline: 2px solid var(--bs-primary) !important;
    outline-offset: -2px !important;
}

/* Bootstrap's caret is a bordered triangle with a 0.255em left margin; next to a hamburger at this
   size the margin reads as a gap. `.show` turns it over, so the control says whether the menu is
   open without the user having to look for the menu. */
.input-group.nct-objsel .nct-objsel__btn.dropdown-toggle::after {
    margin-left: 0;
    opacity: .7;
    transition: transform .18s ease;
}

.input-group.nct-objsel .dropdown.show .nct-objsel__btn.dropdown-toggle::after {
    transform: rotate(180deg);
    opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
    .input-group.nct-objsel > .nct-objsel__choose,
    .input-group.nct-objsel .nct-objsel__btn.dropdown-toggle::after {
        transition: none;
    }
}

/* ================================================================================================
   THEME SWAP GUARD — all skins

   The header switch exchanges the skin stylesheet in place, with no reload. For two animation
   frames around that exchange the document is asked to change almost every colour it paints, and
   anything with a `transition` on one of those properties tries to animate the change. That is not
   just ugly: a transition whose own declaration disappears mid-flight (it lived in the sheet being
   removed) can leave the property parked at the value it was interpolating from — a card that
   keeps a dark background-color under a light background-image, for instance.

   So: freeze every transition and animation while the swap happens, and let go two frames later.

   WHY AN ID. The skins write 43 rules that set `transition` with `!important`, the most specific of
   them at (0,6,2). Among `!important` declarations the cascade compares specificity BEFORE source
   order, so no class-based guard in THIS file — which loads before the skin — could win. One id
   selector is (1,0,0) and beats all of them outright. The id is set on <html> by
   `nctApplyTheme` and removed again immediately; nothing else in the product selects on it.
   ================================================================================================ */
#nct-theme-swapping *,
#nct-theme-swapping *::before,
#nct-theme-swapping *::after {
    transition: none !important;
    animation: none !important;
}

/* ================================================================================================
   THEME MENU — structure (all skins)

   The right-button menu on the theme switch: every skin the platform ships, each with a swatch of
   its own ground colour. Opened by HeaderThemeSelectorPanel.js, which adds `.show`.

   It reuses `.dropdown-menu` on purpose — that class is already painted by every skin, so the menu
   inherits the surface, the border and the shadow the rest of the product's menus have. Only the
   things Bootstrap cannot know are here: where it sits, and what a swatch looks like.
   ================================================================================================ */
.nct-theme-switch-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
}

.nct-theme-menu {
    /* `!important` because base.css turns EVERY dropdown fixed below the lg breakpoint
       (`.popover, .dropdown-menu { position: fixed !important }`), where the template relies on
       Popper to place them. This menu has no Popper — it is anchored to its own wrapper — so on a
       narrow window a fixed box would resolve `right: 0` against the viewport and land in the
       corner instead of under the switch. */
    position: absolute !important;
    /* Same reason as `position`, and the same block: below the lg breakpoint base.css also pins
       `left: 5% !important; top: 50% !important` on every dropdown. All four offsets have to be
       taken back or the menu detaches from its switch on a narrow window. */
    top: calc(100% + 8px) !important;
    right: 0 !important;
    left: auto !important;
    bottom: auto !important;
    z-index: 1021;          /* above the header (1020 in base.css), below a modal */
    display: none;
    /* `max-content`, not auto. An absolutely-positioned box shrink-to-fits against its containing
       block, and the wrapper here is only as wide as a 60px switch — which makes the result depend
       on where the control happens to sit rather than on what is in the menu. Sizing it to its own
       content is the only thing that renders the same in the header, in a narrow drawer, and in a
       test harness. */
    width: max-content;
    min-width: 190px;
    max-width: 260px;
    margin: 0;
    padding: 6px;
    text-align: left;
}

.nct-theme-menu.show {
    display: block;
}

/* Not a heading — a label for the group under it. Same kicker the account menu and the table heads
   use, so the product has one way of saying "this is a section". */
.nct-theme-menu__title {
    padding: 6px 10px 8px;
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .075em;
    text-transform: uppercase;
    opacity: .72;
}

.nct-theme-menu__item {
    display: flex !important;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 7px 10px !important;
    border: 0;
    border-radius: 8px;
    background: none;
    text-align: left;
    cursor: pointer;
}

/* The swatch IS the theme's background colour, set inline from the enum. The ring is what keeps
   Standard's near-white visible on a light menu and Dark's near-black on a dark one — the two that
   most need telling apart, and the two a bare disc would hide. */
.nct-theme-menu__dot {
    flex: 0 0 auto;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    box-shadow: inset 0 0 0 1px rgba(127, 127, 127, .55);
}

/* The row in force. A left rail rather than a tick: it marks the whole row, reads at a glance, and
   costs no horizontal space in a 190px menu. */
.nct-theme-menu__item.is-active {
    font-weight: 600;
    box-shadow: inset 2px 0 0 0 var(--bs-primary, #3f6ad8);
}

.nct-theme-menu__item.is-active .nct-theme-menu__dot {
    box-shadow: inset 0 0 0 1px rgba(127, 127, 127, .55),
                0 0 0 2px var(--bs-primary, #3f6ad8);
}

/* The switch is a link with no href, so it gets neither a pointer nor a focus ring for free. */
.nct-theme-switch {
    cursor: pointer;
}

.nct-theme-switch:focus-visible {
    outline: 2px solid var(--bs-primary, #3f6ad8);
    outline-offset: 3px;
    border-radius: 999px;
}

@media (prefers-reduced-motion: reduce) {
    .nct-theme-menu__item {
        transition: none;
    }
}

/* ================================================================================================
   THE FLOATING THEME SWITCH — all skins

   The same control the application header carries, for the pages that have no header: sign-in,
   registration, reset password. Those are the pages a returning user meets first, and the ones
   where arriving in the wrong theme used to leave them no way out.

   `fixed`, not absolute: the auth pages carry a full-bleed animated background and a centred card,
   and an absolutely-positioned control would scroll away from the corner on a short viewport.
   z-index sits above the background layers (auth-form.css tops out in the low hundreds) and below
   a modal.
   ================================================================================================ */
.nct-theme-float {
    position: fixed;
    top: 16px;
    right: 18px;
    z-index: 1030;
    display: flex;
    align-items: center;
}

/* THE PLATE, and it is not decoration. In the header the switch reads because it sits on a bar
   that is lighter than the page. On an auth page there is no bar: the skin gives the track
   `--surface-1`, which on Dark is #0c1422 against a #080e19 ground — a 1.1:1 difference. Measured
   on the real sign-in page the control was there, correctly positioned, and effectively invisible.
   A corner control on an empty ground has to carry its own surface. */
.nct-theme-float .nct-theme-switch__track {
    background-color: var(--bs-secondary-bg, rgba(127, 127, 127, .18)) !important;
    box-shadow: 0 6px 20px rgba(0, 0, 0, .30), inset 0 1px 0 0 rgba(255, 255, 255, .05);
    backdrop-filter: blur(6px) saturate(140%);
    -webkit-backdrop-filter: blur(6px) saturate(140%);
}

.nct-theme-float .nct-theme-switch:hover .nct-theme-switch__track {
    background-color: var(--bs-tertiary-bg, rgba(127, 127, 127, .26)) !important;
}

@media (max-width: 575.98px) {
    .nct-theme-float {
        top: 10px;
        right: 10px;
    }
}

/* ================================================================================================
   RUNG — THE BUTTON SYSTEM. One authoring site, five token sets.
   ================================================================================================

   WHY THIS EXISTS. Before it, each of the five skins carried its own 300-line button section, and
   they had drifted so far that the same class was a different control in each. Measured, in the
   shipping files:

     - dark        `btn-secondary` and `btn-light` were byte-identical (dE76 = 0.0), and so were
                   `btn-outline-primary` and `btn-outline-info`. `btn-danger` was a 1.17:1 wash with a
                   2.16:1 edge — quieter than the "Save draft" beside it, so a destructive
                   confirmation could not shout. `btn-dark` and `btn-outline-dark` were never styled
                   at all and fell through to raw Bootstrap #343a40: 1.50:1, invisible.
     - dark-blue   `--green` was literally the same hex as `--purple`, so primary, success and info
                   were one colour before a single rule ran.
     - forest      the same, in green.
     - dracula     `--cyan`, `--pink`, `--purple` and `--green` were all aliased to one slate hex,
                   while the same file still painted its chat and alert panels in real Dracula.
     - standard    no semantic palette at all — every colour a literal, which is how filled-warning
                   (#FFC107) and outline-warning (#E8A658) ended up different hues.

   THE IDEA. A button is exactly one RUNG times one ROLE. The rung says how loud it is and is told
   apart by GEOMETRY; the role says what it means and is told apart by HUE. Nothing else in the
   stylesheet is allowed to carry either. That separation is what makes "danger must be louder than
   primary here" expressible at all — before it, loudness fell out of whichever hue a class happened
   to be, so it could not be asked for.

     W0 BARE   ink only, no box
     W1 GHOST  transparent + 1px role edge + role ink
     W2 TINT   baked opaque wash + role edge + role ink
     W3 SOLID  saturated role slab + baked ink on the far side of it, transparent 1px border

   THE CLASS NAMES THE RUNG. The four ACTION fills (primary, danger, success, warning) are W3; the
   three NON-ACTION fills (secondary, dark, info) are W2; every `btn-outline-*` is W1; `btn-link` is
   W0. Bootstrap's filled/outline pair is finally TWO RUNGS AT ONE HUE instead of two hues at one
   rung — which is the whole reason the two were indistinguishable before.

   `btn-info` is demoted on purpose. There is no such thing as an informational commit, and in four
   of five skins "info blue" and "brand blue" collapsed into the same button. Demoting it is the only
   thing that kills that permanently.

   ONE LOUD THING PER SURFACE. A container may only LOWER a rung and may never touch a hue: a table
   cell caps at W0, a toolbar caps at W1 with `btn-primary` exempt, a footer caps at W3 with a quota
   of one. That is why a toolbar of five controls comes out as one slab and four hairlines rather
   than a rainbow, while the red word in it stays red.

   NO DEPTH, IN ANY STATE, EVER. No box-shadow, no transform, no filter, no opacity, no gradient with
   a vertical or diagonal component. Hover moves HUE, not brightness. This is a product decision, and
   it is enforced mechanically below rather than left to each skin to remember.

   WHY SO MUCH `!important`. base.css declares the entire Bootstrap button layer TWICE (around lines
   2450 and 11630) and the markup — 2229 buttons carrying `btn-sm` — cannot change. An un-important
   rule at equal specificity loses on source order. This is managed debt, not a fix; the win is that
   it now lives in ~30 rules instead of 70 blocks across five files.

   NOTE ON ORDER: this file loads BEFORE the skin, so a skin rule at equal specificity wins. That is
   deliberate and it is how dark.css keeps its own primary: its ramp block is (0,3,0)/(0,4,0) and
   outranks the (0,2,0) W3 rule here. Any skin that wants to depart from the system has to do so
   loudly, at a higher specificity, where it can be found.
   ================================================================================================ */

/* -- 0. ROLE BINDING ------------------------------------------------------------------------------
   Each colour class binds six generic slots; every rung rule paints from the slots. That is why a
   rung is written once instead of six times, and why a container can lower a rung without ever
   touching the hue. `[class*="btn-gradient-"]` is here so ArchitectUI's eleven gradient names land
   on a defined cell instead of falling through to raw Bootstrap. */
.btn {
    /* `color` here, not only in the rung rules: a button carrying no colour class at all (26 in the
       product, plus every btn-shadow / btn-pill / btn-transition-only button) matched no rung and
       kept base.css's #495057 on a transparent ground — 1.38:1 in dracula. */
    color: var(--r-ink);
    --r-fill: var(--srf-3);
    --r-fill-hv: var(--srf-3);
    --r-on: var(--ink-1);
    --r-tint: var(--srf-3);
    --r-edge: var(--edge-ui);
    --r-ink: var(--ink-2);
}

.btn-primary, .btn-outline-primary,
.btn-alternate, .btn-outline-alternate,
.btn-focus, .btn-outline-focus,
[class*="btn-gradient-primary"], [class*="btn-gradient-focus"], [class*="btn-gradient-alternate"] {
    --r-fill: var(--w3-brand);       --r-fill-hv: var(--w3-brand-hv);
    --r-on: var(--ink-on-brand);     --r-tint: var(--w2-brand);
    --r-edge: var(--edge-brand);     --r-ink: var(--ink-brand);
}

.btn-danger, .btn-outline-danger, [class*="btn-gradient-danger"] {
    --r-fill: var(--w3-negative);    --r-fill-hv: var(--w3-negative-hv);
    --r-on: var(--ink-on-negative);  --r-tint: var(--w2-negative);
    --r-edge: var(--edge-negative);  --r-ink: var(--ink-negative);
}

.btn-success, .btn-outline-success, [class*="btn-gradient-success"] {
    --r-fill: var(--w3-positive);    --r-fill-hv: var(--w3-positive-hv);
    --r-on: var(--ink-on-positive);  --r-tint: var(--w2-positive);
    --r-edge: var(--edge-positive);  --r-ink: var(--ink-positive);
}

.btn-warning, .btn-outline-warning, [class*="btn-gradient-warning"] {
    --r-fill: var(--w3-caution);     --r-fill-hv: var(--w3-caution-hv);
    --r-on: var(--ink-on-caution);   --r-tint: var(--w2-caution);
    --r-edge: var(--edge-caution);   --r-ink: var(--ink-caution);
}

.btn-info, .btn-outline-info, [class*="btn-gradient-info"] {
    --r-fill: var(--w3-info);        --r-fill-hv: var(--w3-info-hv);
    --r-on: var(--ink-on-info);      --r-tint: var(--w2-info);
    --r-edge: var(--edge-info);      --r-ink: var(--ink-info);
}
/* The neutral roles inherit the .btn defaults: secondary, dark, light, link, outline-secondary,
   outline-dark, outline-light, outline-link, the neutral gradients, and btn-default. */

/* -- 1. GEOMETRY, AND THE DEPTH BAN ---------------------------------------------------------------
   HEIGHTS ARE UNCHANGED. Measured before touching anything: 37.2 / 33.2 / 29.2px for lg / default /
   sm in all five skins. They are stated explicitly here only so they stop depending on the sum of a
   padding and a line-height.

   THE TYPE RAMP IS NEW, because there wasn't one. base.css sets `.btn-lg{1.1rem}` and
   `.btn-sm{.968rem}` at (0,1,0) around line 2650, then re-declares `.btn{.8rem}` at (0,1,0) around
   line 10940 — later, so it wins for all three. Measured: large, default and small all rendered at
   exactly 12.8px. Three size classes, one size.

   `btn-xs` was worse than absent. Bootstrap 5 does not define it at all, so the five uses in the
   product inherited the BODY font: measured 14px type in an 18.6px box — smaller than btn-sm and
   simultaneously the largest label of any button in the system. */
.btn, a.btn, button.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .45em;
    height: 33px;
    padding: 0 12px;
    font-size: 14px;
    font-weight: 500;
    line-height: 1;
    border: 1px solid transparent;
    text-decoration: none;
    white-space: nowrap;
    background-image: none !important;
    box-shadow: none !important;
    transform: none !important;
    filter: none !important;
    opacity: 1 !important;
    transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}

.btn:not(.btn-pill):not(.btn-square) { border-radius: var(--btn-radius, 8px); }
.btn-pill { border-radius: 999px; }
.btn-square { border-radius: 0; }

.btn-lg, a.btn-lg, button.btn-lg { height: 37px; padding: 0 18px; font-size: 15px; }
.btn-sm, a.btn-sm, button.btn-sm { height: 29px; padding: 0 9px;  font-size: 13px; }
.btn-xs, a.btn-xs, button.btn-xs { height: 24px; padding: 0 7px;  font-size: 12px; }

.btn-wide  { min-width: 12rem; }
.btn-block { width: 100%; }

/* A `btn-icon-only` that is ALSO a `dropdown-toggle` IS NOT ICON-ONLY — Bootstrap draws it a caret
   in `::after`, and the caret is 6px wide with a 5px margin. Forced to a 29px square with zero
   padding, the button's own caret fell 11px OUTSIDE it (measured). At rest nobody saw it, because
   there was no box; the moment the hover box appeared it was a box that did not contain its own
   caret. This is the CRUD row action — it is on every row of every table in the product. */
.btn.btn-icon-only.dropdown-toggle,
.btn.btn-sm.btn-icon-only.dropdown-toggle,
.btn.btn-xs.btn-icon-only.dropdown-toggle,
.btn.btn-lg.btn-icon-only.dropdown-toggle {
    width: auto;
    padding: 0 8px;
}

/* `.btn.btn-icon-only` and not `.btn-icon-only`: at (0,1,0) the padding reset lost to
   `.btn-sm { padding: 0 9px }` (0,1,0, declared later) and to `button.btn` (0,1,1), so an icon-only
   button kept 9px of side padding inside a 29px box and its glyph overflowed by 3px. */
.btn.btn-icon-only        { width: 33px; padding: 0; }
.btn.btn-lg.btn-icon-only { width: 37px; }
.btn.btn-sm.btn-icon-only { width: 29px; }
.btn.btn-xs.btn-icon-only { width: 24px; }
.btn.btn-icon-vertical    { flex-direction: column; height: auto; padding: 8px 12px; gap: .3em; }

/* A `btn-link` is a bare word and sometimes wraps something taller than a button — the header's
   44px notification bell and language flag are `<button class="btn btn-link">` with a round
   `icon-wrapper` inside. A fixed 33px height cropped them by 7px. */
.btn.btn-link, a.btn.btn-link, button.btn.btn-link { height: auto; min-height: 29px; }
.btn-icon-lg > i, .btn-icon-lg svg { font-size: 24px; width: 24px; height: 24px; }

/* Edge WIDTH is not allowed to be a second weight signal — rule 1 gives that job to the fill. */
.btn-outline-2x { border-width: 1px; }
/* Style, not weight. This is the legitimate "add new" affordance and it survives. */
.btn.btn-dashed { border-style: dashed !important; }

/* These three animated a background sweep or a text slide. Restricted to colour. */
.btn-transition, .btn-transition-alt, .btn-transition-text {
    transition: background-color .12s ease, border-color .12s ease, color .12s ease !important;
}

/* One rule replaces the 40-45 line per-family icon-ink table each skin carried. A glyph inside a
   button is part of the label and takes the label's colour — nothing else is defensible, and the
   old tables are why a lock icon on the primary was 3.0:1 while its text was 6.3:1. */
.btn > i, .btn svg, .btn .btn-icon-wrapper, .btn [class*="pe-7s-"],
.btn [class*="lnr-"], .btn [class*="fa-"] { color: currentColor !important; }
.btn svg * { fill: currentColor; }

/* Dead depth classes, kept as no-ops so that no markup has to change.
   `btn-shadow` painted `0 2px 10px rgba(63,106,216,.4)` — a hard halo in a blue no skin uses.
   `btn-hover-shine` wiped an `rgba(255,255,255,.4)` panel across the face on hover: gloss. */
.btn-shadow, .btn-shadow:hover, [class*="btn-shadow-"] { box-shadow: none !important; }
.btn-hover-shine::after, .btn-shine::after,
.btn-hover-shine::before, .btn-shine::before { display: none !important; }

/* -- 2. W3 SOLID ---------------------------------------------------------------------------------
   The four ACTION fills. Inside one skin every W3 sits at the same luminance, so a role may change
   hue and chroma but can never buy weight; `negative` alone carries a small chroma licence, which is
   how danger shouts without out-weighing the commit. */
.btn.btn-primary, .btn.btn-danger, .btn.btn-success, .btn.btn-warning,
.btn.btn-alternate, .btn.btn-focus,
.btn[class*="btn-gradient-primary"], .btn[class*="btn-gradient-danger"],
.btn[class*="btn-gradient-success"], .btn[class*="btn-gradient-warning"],
.btn[class*="btn-gradient-focus"], .btn[class*="btn-gradient-alternate"] {
    background-color: var(--r-fill) !important;
    background-image: none !important;
    border-color: transparent !important;
    color: var(--r-on) !important;
}

/* Hover is a HUE move at constant luminance — never a brightening, never a shadow. */
.btn.btn-primary:hover, .btn.btn-danger:hover, .btn.btn-success:hover,
.btn.btn-warning:hover, .btn.btn-alternate:hover, .btn.btn-focus:hover {
    background-color: var(--r-fill-hv) !important;
}

.btn.btn-primary:active, .btn.btn-danger:active, .btn.btn-success:active,
.btn.btn-warning:active, .btn.btn-alternate:active, .btn.btn-focus:active {
    background-color: color-mix(in oklab, var(--r-fill) 88%, black) !important;
}

/* -- 3. W2 TINT ---------------------------------------------------------------------------------
   The wash is a BAKED opaque value, never a runtime alpha. Alpha is why `btn-secondary` rendered as
   four different colours on four different grounds. */
.btn.btn-secondary, .btn.btn-dark, .btn.btn-info,
.btn[class*="btn-gradient-secondary"], .btn[class*="btn-gradient-dark"],
.btn[class*="btn-gradient-info"] {
    background-color: var(--r-tint) !important;
    background-image: none !important;
    border-color: var(--r-edge) !important;
    color: var(--r-ink) !important;
}

/* Neutral W2 takes the primary ink: it is a real action, just not the one. */
.btn.btn-secondary, .btn.btn-dark,
.btn[class*="btn-gradient-secondary"], .btn[class*="btn-gradient-dark"] {
    color: var(--ink-1) !important;
}

.btn.btn-secondary:hover, .btn.btn-dark:hover {
    background-color: var(--srf-2) !important;
    color: var(--ink-1) !important;
}

.btn.btn-info:hover {
    background-color: var(--srf-3) !important;
}

/* -- 4. W1 GHOST --------------------------------------------------------------------------------- */
.btn[class*="btn-outline-"]:not(.btn-outline-link),
.btn.btn-light, .btn.btn-default,
.btn[class*="btn-gradient-light"] {
    background-color: transparent !important;
    background-image: none !important;
    border-color: var(--r-edge) !important;
    color: var(--r-ink) !important;
}

/* A ghost hovers into its own tint — i.e. it climbs one rung, in its own hue. */
.btn[class*="btn-outline-"]:not(.btn-outline-link):hover,
.btn.btn-light:hover, .btn.btn-default:hover {
    background-color: var(--r-tint) !important;
}

/* -- 5. W0 BARE ----------------------------------------------------------------------------------
   NEUTRAL ink, not brand. This is the fix for "two blues in a footer": `btn-link` was painted in the
   brand colour in every skin, so Cancel and Publish were the same hue and Cancel was often the
   brighter of the two. */
.btn.btn-link, .btn.btn-outline-link, .btn[class*="btn-gradient-link"] {
    background-color: transparent !important;
    background-image: none !important;
    border-color: transparent !important;
    color: var(--ink-2) !important;
    padding-left: 6px;
    padding-right: 6px;
    text-decoration: none !important;
}

/* The underline is the only affordance a bare control has left, so it has to beat base.css:9039's
   `.btn-link, a { text-decoration: none !important }`. */
.btn.btn-link:hover, .btn.btn-outline-link:hover {
    color: var(--ink-1) !important;
    text-decoration: underline !important;
}

/* -- 6. CONTAINER CAPS ---------------------------------------------------------------------------
   A container may LOWER the rung and may NEVER touch the hue. The `.btn.btn` idiom bumps these to
   (0,3,0)+ so they outrank the rung rules above without needing a third layer of !important. */

/* A TABLE CELL CAPS AT W0. Three glyphs on forty rows is 120 marks — it must not be 120 boxes.
   The hue survives the cap, so a row's delete is still red; only its box is gone. */
.table td .btn.btn, td .btn.btn,
.crud-actions-cell .btn.btn, .process-actions-cell .btn.btn,
.table-actions .btn.btn, .btn-actions-pane-right .btn.btn {
    background-color: transparent !important;
    background-image: none !important;
    border-color: transparent !important;
    color: var(--r-ink) !important;
}

/* When the ROW ground moves, the glyph has to move with it: the cap paints ink once, so on a hovered
   or selected row the same glyph measured 3.56-4.29:1. This is a legibility lift, not a reveal. */
.table-hover tbody tr:hover .btn.btn,
.table tbody tr:hover .btn.btn,
.table tbody tr.table-active .btn.btn,
.table tbody tr.selected .btn.btn {
    color: var(--ink-1) !important;
}

/* Only the glyph under the pointer gets a box back. A destructive row action is therefore quiet
   until you are on it, and unmistakable the moment you are. */
.table td .btn.btn:hover, td .btn.btn:hover,
.crud-actions-cell .btn.btn:hover, .process-actions-cell .btn.btn:hover,
.table-actions .btn.btn:hover, .btn-actions-pane-right .btn.btn:hover {
    /* --srf-2, not --r-tint: the neutral tint IS --srf-3, the pressed surface, and a saturated navy
       slab around a 16px glyph is heavier than the glyph. A row action should acknowledge the
       pointer, not announce itself. Semantic row actions keep their own tint. */
    background-color: var(--srf-2) !important;
    border-color: var(--r-edge) !important;
}

.table td .btn.btn-danger:hover, .table td .btn[class*="btn-outline-danger"]:hover,
td .btn.btn-danger:hover, td .btn[class*="btn-outline-danger"]:hover,
.crud-actions-cell .btn.btn-danger:hover, .crud-actions-cell .btn[class*="btn-outline-danger"]:hover {
    background-color: var(--r-tint) !important;
}

/* A TOOLBAR CAPS AT W1, with btn-primary exempt: one slab, then hairlines that all share one edge
   colour, so the strip reads as a rhythm and the meaning is carried by the words. */
.toolbar-controls .btn.btn:not(.btn-primary):not(.btn-link),
.btn-actions-pane-left .btn.btn:not(.btn-primary):not(.btn-link) {
    background-color: transparent !important;
    background-image: none !important;
    border-color: var(--r-edge) !important;
    color: var(--r-ink) !important;
}

.toolbar-controls .btn.btn:not(.btn-primary):not(.btn-link):hover,
.btn-actions-pane-left .btn.btn:not(.btn-primary):not(.btn-link):hover {
    background-color: var(--r-tint) !important;
}

/* A FOOTER CAPS AT A QUOTA OF ONE W3. The LAST eligible sibling keeps the slab and the earlier ones
   fall to W2 in their own hue, so a footer with Save and Delete has one commit and one warning
   rather than two shouts. Scoped to footers on purpose: in a TOOLBAR the loud button is the first
   one (Create), so a blanket rule would invert it. */
:is(.modal-footer, .card-footer, .form-actions)
:is(.btn-primary, .btn-danger, .btn-success, .btn-warning):has(~ :is(.btn-primary, .btn-danger, .btn-success, .btn-warning)) {
    background-color: var(--r-tint) !important;
    background-image: none !important;
    border-color: var(--r-edge) !important;
    color: var(--r-ink) !important;
}

/* -- 7. STATES -----------------------------------------------------------------------------------
   `.active` is +1 rung, capped at W2, with the hue forced to BRAND — the same pair a selected table
   row uses, so a selected segment and a selected row agree by construction. It was DEAD in all five
   skins: every outline family declared a transparent background with !important, which outranked
   base.css's un-important `.active` fill, so nothing was ever marked as selected. */
.btn.btn.active, .btn-check:checked + .btn.btn, .btn.btn[aria-pressed="true"] {
    background-color: var(--w2-brand) !important;
    background-image: none !important;
    border-color: var(--edge-brand) !important;
    color: var(--ink-brand) !important;
    z-index: 1;
}

/* Focus is an OUTLINE on the surface, never a box-shadow: a shadow is depth, and a coloured ring
   around a coloured button is a glow. Both higher specificity AND !important are required, because
   base.css ends with `a, button, .btn { outline: none !important }`. */
.btn.btn:focus { box-shadow: none !important; }

.btn.btn:focus-visible {
    outline: 2px solid var(--focus) !important;
    outline-offset: 2px !important;
}

/* Disabled keeps the RUNG GEOMETRY and drains the hue. `opacity` is not used: compositing the whole
   control against whatever is behind it is how the old rule produced 1.47:1 to 1.81:1 labels in four
   skins — text that is not muted but gone. Scoped to `.btn`, never bare `button:disabled`, which at
   (0,1,1) turned a disabled text link and a disabled 28px icon button into filled slabs. */
.btn.btn:disabled, .btn.btn.disabled { opacity: 1 !important; cursor: default; }

:is(.btn-primary, .btn-danger, .btn-success, .btn-warning, .btn-alternate, .btn-focus,
    .btn-secondary, .btn-dark, .btn-info).btn:disabled,
:is(.btn-primary, .btn-danger, .btn-success, .btn-warning, .btn-alternate, .btn-focus,
    .btn-secondary, .btn-dark, .btn-info).btn.disabled {
    background-color: var(--dis-fill) !important;
    /* Not optional: the MOTION layer paints a travelling gradient on every W3 at (0,2,0), and this
       rule is (0,3,0) — it wins the colour and would still lose the image, so a disabled Danger
       stayed fully red while a disabled Primary went grey. A dead button has no colour and no
       travel. */
    background-image: none !important;
    border-color: var(--dis-fill) !important;
    color: var(--dis-ink) !important;
}

.btn[class*="btn-outline-"]:not(.btn-outline-link):disabled,
.btn[class*="btn-outline-"]:not(.btn-outline-link).disabled,
.btn.btn-light:disabled, .btn.btn-default:disabled {
    background-color: transparent !important;
    border-color: var(--dis-edge) !important;
    color: var(--dis-ink) !important;
}

.btn.btn-link:disabled, .btn.btn-link.disabled,
.btn.btn-outline-link:disabled {
    background-color: transparent !important;
    border-color: transparent !important;
    color: var(--dis-ink) !important;
    text-decoration: none !important;
}

/* -- 8. BTN-GROUP — one control, not three pills --------------------------------------------------
   !important is required here and it is the reason the shape guard above can keep its `:not()`
   without one: dark.css put `!important` on the guard, which outranked Bootstrap's own un-important
   `.btn-group > .btn:not(:last-child)` radius rules, and every segmented control in that skin
   rendered as three separate 8px pills overlapping by the -1px margin. */
.btn-group > .btn.btn { border-radius: 0 !important; margin-left: -1px; }

/* THE END SEGMENTS ARE FOUND BY BUTTON, NOT BY ELEMENT POSITION, and that distinction is the whole
   rule. `:first-child` / `:last-child` only describe a segmented control whose children are ALL
   buttons. A dropdown group is not that: `<div class="btn-group"><a class="btn">…</a><div
   class="dropdown-menu">…</div></div>` — its last child is the MENU, so the only button in it never
   got its right corners back and rendered `8px/0/0/8px`: round on the left, square on the right,
   inside a fully rounded row. That is the pages tree in the authoring panel, on every row.

   `:not(:has(~ .btn))` is "a button with no button after it" — the last BUTTON, whatever non-button
   elements follow it. `:not(.btn ~ .btn)` is "a button with no button before it". Together they give
   a lone button both ends, a segmented control its two ends, and neither needs to know what else the
   group contains. Specificity of `:not()`/`:has()` is that of its most specific argument, so these
   are (0,4,0) and win over the zeroing rule above. */
.btn-group > .btn.btn:not(.btn ~ .btn) {
    border-top-left-radius: var(--btn-radius, 8px) !important;
    border-bottom-left-radius: var(--btn-radius, 8px) !important;
    margin-left: 0;
}

.btn-group > .btn.btn:not(:has(~ .btn)) {
    border-top-right-radius: var(--btn-radius, 8px) !important;
    border-bottom-right-radius: var(--btn-radius, 8px) !important;
}

.btn-group-sm > .btn { height: 29px; padding: 0 9px;  font-size: 13px; }
.btn-group-lg > .btn { height: 37px; padding: 0 18px; font-size: 15px; }

/* ================================================================================================
   RUNG — MOTION
   ================================================================================================

   THE DISTINCTION THIS SECTION EXISTS TO MAKE. The veto was on DEPTH, not on movement, and the first
   pass conflated them: banning shadows, bevels and hover-lifts, it also flattened the transitions to
   a 120ms colour swap and left the buttons correct but inert. Those are two different things.

     DEPTH says "this object is above the page"  — shadow, bevel, gloss, lift, glow.  Still banned.
     MOTION says "this object heard you"          — travel, arrival, easing, a glyph that reacts.

   Everything below is the second kind. Nothing here moves a BUTTON in z, and nothing adds a shadow;
   the buttons stay exactly as flat as they were. What changes is that colour now ARRIVES instead of
   being swapped, and the glyph acknowledges the pointer.

   ONE CURVE for the whole system. `cubic-bezier(.2, .8, .2, 1)` is a decelerate — it leaves fast and
   settles slowly, which is what makes an interface feel quick AND smooth at the same time. A linear
   or `ease` curve at the same duration feels mechanical, and that is most of what was missing.
   ================================================================================================ */
:root {
    --btn-ease: cubic-bezier(.2, .8, .2, 1);
    --btn-dur: .19s;      /* colour arriving */
    --btn-dur-press: .08s;/* pressing — must beat the eye */
    --btn-dur-glyph: .26s;/* the glyph, deliberately slower than its own button */
}

/* The edge answers before the fill does. A 70ms stagger is below conscious notice and is most of
   what separates a control that feels considered from one that feels switched. */
.btn, a.btn, button.btn {
    transition: background-color var(--btn-dur) var(--btn-ease),
                background-position var(--btn-dur) var(--btn-ease),
                border-color .12s var(--btn-ease),
                color .12s var(--btn-ease),
                outline-offset .14s var(--btn-ease) !important;
}

/* -- W3: THE COLOUR TRAVELS ----------------------------------------------------------------------
   Every solid button is painted at 200% width with its rest colour on the right half and its hover
   colour on the left, joined by a soft 16% edge. Hover slides that edge across, so the new colour
   FLOODS IN from one side rather than appearing everywhere at once.

   This is the same idea dark's primary already used for its brand ramp, generalised to every role
   and every skin — and it is horizontal-only, so it stays inside the rule that forbids a gradient
   with a vertical or diagonal component (those imply a light source; a horizontal one does not).
   dark's own primary rule is (0,2,0) and declared later, so its blue->green sweep still wins. */
.btn.btn-primary, .btn.btn-danger, .btn.btn-success, .btn.btn-warning,
.btn.btn-alternate, .btn.btn-focus,
.btn[class*="btn-gradient-primary"], .btn[class*="btn-gradient-danger"],
.btn[class*="btn-gradient-success"], .btn[class*="btn-gradient-warning"],
.btn[class*="btn-gradient-focus"], .btn[class*="btn-gradient-alternate"] {
    /* 300%, not 200%, and the soft edge parked in the middle third. THE ARITHMETIC MATTERS: with a
       200% image the rest window is the image's right HALF, i.e. fractions .50-1.00, which still
       clips the leading edge of a transition centred on .50 — measured as a visible gradient band on
       the resting button, worst on the roles whose two stops are furthest apart. At 300% the rest
       window is .667-1.00 and the hover window is .00-.333, so BOTH ends are solid colour and the
       soft edge lives in .40-.60, entirely between them. It is only ever seen in motion. */
    background-image: linear-gradient(to right,
            var(--r-fill-hv) 0%, var(--r-fill-hv) 40%,
            var(--r-fill) 60%, var(--r-fill) 100%) !important;
    background-size: 300% 100% !important;
    background-position: 100% 0 !important;
    background-repeat: no-repeat !important;
}

.btn.btn-primary:hover, .btn.btn-danger:hover, .btn.btn-success:hover,
.btn.btn-warning:hover, .btn.btn-alternate:hover, .btn.btn-focus:hover,
.btn[class*="btn-gradient-primary"]:hover, .btn[class*="btn-gradient-danger"]:hover,
.btn[class*="btn-gradient-success"]:hover, .btn[class*="btn-gradient-warning"]:hover {
    background-position: 0 0 !important;
}

/* Pressed drops the travelling layer entirely and lands on one deepened colour, instantly. The
   contrast between a 190ms arrival and an 80ms snap is what makes the press feel like a click. */
.btn.btn-primary:active, .btn.btn-danger:active, .btn.btn-success:active,
.btn.btn-warning:active, .btn.btn-alternate:active, .btn.btn-focus:active {
    background-image: none !important;
    background-color: color-mix(in oklab, var(--r-fill) 86%, black) !important;
    transition-duration: var(--btn-dur-press) !important;
}

/* -- THE GLYPH ANSWERS ----------------------------------------------------------------------------
   `transform` is banned on the BUTTON, because moving a button in z is the depth cue. On the glyph
   inside it, a transform is expression: the icon is part of the label, and a label that reacts is
   not a label that floats. Deliberately slower than the fill so the two do not move as one block. */
.btn > i, .btn svg, .btn .btn-icon-wrapper,
.btn [class*="pe-7s-"], .btn [class*="lnr-"], .btn [class*="fa-"] {
    transition: transform var(--btn-dur-glyph) var(--btn-ease),
                color .12s var(--btn-ease);
    will-change: transform;
}

/* A glyph beside a label leans into the reading direction; a glyph that IS the button grows a
   little, because it has no label to lean towards. */
.btn:hover > i, .btn:hover svg, .btn:hover .btn-icon-wrapper,
.btn:hover [class*="pe-7s-"], .btn:hover [class*="lnr-"], .btn:hover [class*="fa-"] {
    transform: translateX(1.5px);
}

.btn.btn-icon-only:hover > i, .btn.btn-icon-only:hover svg,
.btn.btn-icon-only:hover .btn-icon-wrapper,
.btn.btn-icon-only:hover [class*="pe-7s-"], .btn.btn-icon-only:hover [class*="fa-"] {
    transform: scale(1.14);
}

.btn:active > i, .btn:active svg, .btn:active .btn-icon-wrapper,
.btn:active [class*="pe-7s-"], .btn:active [class*="fa-"] {
    transform: scale(.9);
    transition-duration: var(--btn-dur-press);
}

/* Where the glyph already MEANS a motion, perform it. This is the part that reads as craft rather
   than as an effect, because nothing moves that has no reason to. */
.btn:hover [class*="pe-7s-refresh"], .btn:hover [class*="fa-sync"],
.btn:hover [class*="fa-redo"], .btn:hover [class*="fa-rotate"],
.btn:hover [class*="lnr-sync"], .btn:hover [class*="pe-7s-refresh-2"] {
    transform: rotate(180deg);
}

.btn:hover [class*="pe-7s-plus"], .btn:hover [class*="fa-plus"], .btn:hover [class*="lnr-plus"] {
    transform: rotate(90deg);
}

.btn:hover [class*="pe-7s-trash"], .btn:hover [class*="fa-trash"] {
    transform: rotate(-9deg);
}

.btn:hover [class*="pe-7s-angle-right"], .btn:hover [class*="pe-7s-next"],
.btn:hover [class*="fa-arrow-right"], .btn:hover [class*="fa-chevron-right"] {
    transform: translateX(3px);
}

.btn:hover [class*="pe-7s-angle-left"], .btn:hover [class*="pe-7s-prev"],
.btn:hover [class*="fa-arrow-left"], .btn:hover [class*="fa-chevron-left"] {
    transform: translateX(-3px);
}

.btn:hover [class*="pe-7s-download"], .btn:hover [class*="fa-download"] { transform: translateY(2px); }
.btn:hover [class*="pe-7s-upload"],   .btn:hover [class*="fa-upload"]   { transform: translateY(-2px); }

/* The caret turns over when its menu opens — the one piece of state in a row action that was never
   drawn at all. */
.dropdown-toggle::after {
    transition: transform .2s var(--btn-ease);
}

.show > .btn.dropdown-toggle::after,
.btn.dropdown-toggle.show::after {
    transform: rotate(180deg);
}

/* -- FOCUS ARRIVES ---------------------------------------------------------------------------------
   The ring collapses from 5px to its 2px offset. It reads as the ring landing on the control rather
   than being switched on, and it costs nothing — `outline` is not a box-shadow and not depth. */
.btn.btn:focus-visible {
    outline-offset: 2px !important;
}

.btn.btn:not(:focus-visible) {
    outline-offset: 5px;
}

/* -- ROW ACTIONS: THE BOX ARRIVES ------------------------------------------------------------------
   At rest these are bare glyphs, 120 of them on a forty-row table. The box has to arrive gently or
   the whole Actions column flickers as the pointer crosses it. */
.table td .btn.btn, td .btn.btn,
.crud-actions-cell .btn.btn, .process-actions-cell .btn.btn {
    transition: background-color .22s var(--btn-ease),
                border-color .16s var(--btn-ease),
                color .12s var(--btn-ease) !important;
}

/* -- REDUCED MOTION ---------------------------------------------------------------------------------
   Everything above is decoration on top of a state change that is already carried by colour, so all
   of it can simply stop. The colour still changes; only the travel does not. */
@media (prefers-reduced-motion: reduce) {
    .btn, a.btn, button.btn {
        transition: background-color .01s linear, border-color .01s linear, color .01s linear !important;
        background-position: 100% 0 !important;
    }

    .btn:hover, .btn.btn-primary:hover, .btn.btn-danger:hover,
    .btn.btn-success:hover, .btn.btn-warning:hover {
        background-color: var(--r-fill-hv) !important;
        background-image: none !important;
    }

    .btn > i, .btn svg, .btn .btn-icon-wrapper,
    .btn [class*="pe-7s-"], .btn [class*="lnr-"], .btn [class*="fa-"],
    .dropdown-toggle::after {
        transition: none !important;
        transform: none !important;
    }
}

/* ================================================================================================
   TOASTS — every nctSuccess / nctError / nctWarning / nctInfo, in every skin.
   ================================================================================================

   WHAT DRAWS THEM. `AlertUtils` calls `window.nctsuccess|ncterror|nctwarning|nctinfo`, which
   `common/js/common-d.js` forwards to **toastr**. So everything below styles toastr's own DOM:
   `#toast-container > .toast.toast-<type>` holding `.toast-title`, `.toast-message`,
   `.toast-close-button` and `.toast-progress`.

   WHAT WAS WRONG. Each skin carried a private copy, and they had drifted into two different wrong
   answers. Measured before this section existed:
     dark      a muted panel — but the ink stayed `var(--background)`, so a success toast was
               near-black text on a dark green panel, about 1.2:1. Unreadable.
     the rest  a fully saturated slab with white ink, straight out of 2013. White on standard's
               `#f7b924` warning measures 1.9:1 and on its `#3ac47d` success 2.2:1 — both fail AA
               outright, and the saturated bar is the loudest object on the screen for five seconds.
   Neither belonged to any skin's palette: dracula's success was `#4a7fa8`, a BLUE.

   WHAT IT IS NOW. A toast is a floating PANEL, not a coloured bar. It sits on the skin's raised
   surface, exactly where a dropdown or a modal sits, and its meaning is carried the way the button
   system carries meaning — by a role-coloured edge, a role-coloured icon and a role-coloured title,
   over body text that is simply legible. Saturation is spent on a 3px rail and a 20px glyph rather
   than on 300px of background, which is why it can be noticed without shouting.

   THE ONE PLACE A SHADOW IS STILL ALLOWED. The button system forbids shadows because a button is
   part of the page. A toast genuinely is not: it is an overlay above everything, and the shadow is
   what says so. It is strictly neutral — no coloured glow — and toastr's own four-layer tinted
   shadow (a different tint per type) is replaced.

   THE ICON IS A MASK, NOT A PICTURE. toastr ships a white PNG per type. A white glyph works on a
   saturated bar and disappears on a light panel, which is why the light skin could never have this
   design. Painting the same shape as a `mask-image` over `background-color: var(--t-accent)` makes
   the glyph take the role colour on every skin, at any size, with no second asset. */

/* -- role binding, the same idea as the buttons: six slots, one rule per rung ---------------------- */
#toast-container > .toast-success { --t-accent: var(--w3-positive); --t-ink: var(--ink-positive); --t-edge: var(--edge-positive); }
#toast-container > .toast-error   { --t-accent: var(--w3-negative); --t-ink: var(--ink-negative); --t-edge: var(--edge-negative); }
#toast-container > .toast-warning { --t-accent: var(--w3-caution);  --t-ink: var(--ink-caution);  --t-edge: var(--edge-caution); }
#toast-container > .toast-info,
#toast-container > div            { --t-accent: var(--w3-info);     --t-ink: var(--ink-info);     --t-edge: var(--edge-info); }

/* Clear the 60px application header. Pinned at 12px, toastr's default, a toast landed ON the header
   and covered the account cluster — visible in the screenshot that started this. */
#toast-container.toast-top-right,
#toast-container.toast-top-left,
#toast-container.toast-top-center,
#toast-container.toast-top-full-width {
    top: 72px !important;
}

#toast-container.toast-top-right,
#toast-container.toast-bottom-right { right: 16px !important; }

#toast-container > div,
#toast-container > .toast {
    width: 340px !important;
    padding: 12px 34px 12px 46px !important;
    background-color: var(--srf-2) !important;
    /* toastr's per-type PNG. Replaced by the mask below, which can take a colour. */
    background-image: none !important;
    border: 1px solid var(--t-edge) !important;
    /* The rail. It is the loudest thing about the toast and it costs 3px. */
    border-left: 3px solid var(--t-accent) !important;
    border-radius: 10px !important;
    /* Neutral, and the only shadow left in the system — a toast really is above the page. */
    box-shadow: 0 12px 32px -8px rgba(0, 0, 0, .45), 0 2px 6px rgba(0, 0, 0, .30) !important;
    /* toastr ships `opacity: .9`, which mutes the text as much as the panel. */
    opacity: 1 !important;
    color: var(--ink-1) !important;
    transition: border-color .18s var(--btn-ease, ease), box-shadow .18s var(--btn-ease, ease);
}

#toast-container > div:hover,
#toast-container > .toast:hover {
    opacity: 1 !important;
    border-color: var(--t-accent) !important;
    box-shadow: 0 16px 40px -8px rgba(0, 0, 0, .55), 0 2px 6px rgba(0, 0, 0, .34) !important;
}

/* The glyph, drawn as a mask so it takes the role colour. */
#toast-container > div::before {
    content: '';
    position: absolute;
    left: 15px;
    top: 14px;
    width: 20px;
    height: 20px;
    background-color: var(--t-accent);
    -webkit-mask: var(--t-icon) center / 20px 20px no-repeat;
    mask: var(--t-icon) center / 20px 20px no-repeat;
}

#toast-container > .toast-success {
    --t-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><path d='M22 11.08V12a10 10 0 1 1-5.93-9.14'/><polyline points='22 4 12 14.01 9 11.01'/></svg>");
}

#toast-container > .toast-error {
    --t-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><line x1='12' y1='8' x2='12' y2='12'/><line x1='12' y1='16' x2='12.01' y2='16'/></svg>");
}

#toast-container > .toast-warning {
    --t-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><path d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/><line x1='12' y1='9' x2='12' y2='13'/><line x1='12' y1='17' x2='12.01' y2='17'/></svg>");
}

#toast-container > .toast-info,
#toast-container > div {
    --t-icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><line x1='12' y1='16' x2='12' y2='12'/><line x1='12' y1='8' x2='12.01' y2='8'/></svg>");
}

/* The title carries the role, the message carries the information. Splitting them is what lets the
   body text be plainly legible while the toast is still identifiable at a glance. */
#toast-container .toast-title {
    color: var(--t-ink) !important;
    font-weight: 600 !important;
    font-size: 13px !important;
    line-height: 1.35 !important;
    margin-bottom: 2px !important;
}

#toast-container .toast-message {
    color: var(--ink-1) !important;
    font-size: 13px !important;
    line-height: 1.45 !important;
    word-break: break-word;
}

/* A close button on an overlay is a control, not a decoration: it needs a hit area and a hover, and
   it must not be a white text-shadowed "x" floating over a panel that may be light. */
#toast-container .toast-close-button {
    position: absolute !important;
    top: 8px !important;
    right: 8px !important;
    width: 20px !important;
    height: 20px !important;
    padding: 0 !important;
    font-size: 16px !important;
    line-height: 18px !important;
    font-weight: 400 !important;
    color: var(--ink-2) !important;
    text-shadow: none !important;
    opacity: 1 !important;
    background: none !important;
    border: 0 !important;
    border-radius: 5px !important;
    transition: color .12s var(--btn-ease, ease), background-color .12s var(--btn-ease, ease);
}

#toast-container .toast-close-button:hover,
#toast-container .toast-close-button:focus {
    color: var(--ink-1) !important;
    background-color: var(--srf-3) !important;
    opacity: 1 !important;
}

/* The progress bar is the only place the role colour is allowed to run the full width — it is a
   clock, and a clock has to be readable at a glance. */
#toast-container .toast-progress {
    position: absolute !important;
    left: 0 !important;
    bottom: 0 !important;
    height: 2px !important;
    background-color: var(--t-accent) !important;
    opacity: .9 !important;
}

@media (prefers-reduced-motion: reduce) {
    #toast-container > div,
    #toast-container .toast-close-button { transition: none !important; }
}

/* -- THE PAGES TREE NODE — a <div> wearing `.btn` ---------------------------------------------------
   `NctTreeNodePanel.html` builds each row of the authoring panel's page tree as
   `<div class="btn btn-outline-light nct-node-main-btn" style="width:100%">` holding the page name
   and, after it, a `.btn-group` positioned with `float: right`. It is the ONLY <div> in the product
   carrying a bare `.btn` (32 divs carry a btn-* class; 31 of them are btn-group / btn-actions-pane /
   header-btn-lg, none of which is `.btn` itself), so it is a single identified case rather than a
   reason to weaken the geometry rule.

   WHAT BROKE. The geometry rule makes every `.btn` an `inline-flex` box centred on its content, and
   `float` IS INERT ON A FLEX ITEM. So the row stopped being a block with a floated action and became
   a centred flex row: measured on a 340px node, the group's left edge moved from the right edge to
   199px — the dropdown left the end of the row and parked itself immediately after the page name.

   The row is given the layout the float was emulating, done properly: name on the left, actions on
   the right, and the name allowed to shrink and ellipsize instead of pushing the actions off. */
.nct-node-main-btn {
    display: flex !important;
    align-items: center !important;
    justify-content: space-between !important;
    gap: 8px;
    height: auto !important;
    min-height: 33px !important;
    padding: 4px 6px 4px 12px !important;
    text-align: left !important;
}

/* A long page name has to give way to the control, not the other way round. `min-width: 0` is what
   lets a flex item shrink below its content width at all. */
.nct-node-main-btn > span {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nct-node-main-btn .btn-group {
    flex: 0 0 auto;
    float: none !important;
    margin: 0 !important;
}

.nct-node-main-btn .btn-group > .btn {
    float: none !important;
    margin: 0 !important;
    height: 26px !important;
    padding: 0 7px !important;
}

/* ================================================================================================
   WIDE TABLES — the TABLE scrolls sideways, the PAGE does not.
   ================================================================================================

   THE BUG. `crud.table.plugin`, `crud.tree.plugin` and `process.table.plugin` each render a bare
   `<table>` inside `.row > .card-body`. A table cannot shrink below the min-content width of its
   columns, so a CRUD with fifteen columns (or a tree indented four levels deep) came out wider than
   the viewport. Nothing on the way up clipped or scrolled it, so the overflow travelled to the
   document: the WHOLE shell scrolled — breadcrumb, page title, pager and the row actions with it.
   Reading the last column meant losing every control on the page, and the pager you wanted next was
   somewhere off to the left.

   THE FIX, and why it is two rules and not one. `.nct-table-scroll` (in the three plugins' markup,
   wrapped around the table ONLY — the heading and the pager stay put) turns the table into its own
   scroll box. That alone does nothing: `.card-body` is a flex item of `.row`, and a flex item's
   automatic minimum size is its MIN-CONTENT size — the table's width — so the item simply grew and
   the page scrolled exactly as before. `min-width: 0` is what lets it shrink to the row instead,
   and only then does the inner box have an overflow to scroll.

   THE MENU IS THE CATCH. A scroll box clips on BOTH axes — `overflow-x: auto` computes `overflow-y`
   to `auto` as well, there is no one-axis scroll container in CSS. The row-action `.dropdown-menu`
   lives inside the table, so it would be clipped by the very box that makes the columns scrollable
   (this is exactly why `ListFormControlPlugin` gave up and set `overflow: visible`, keeping the
   page-wide scroll). It is answered in `common/js/common-d.js`, which switches any dropdown inside
   a `.nct-table-scroll` to Popper's `fixed` strategy: the menu is then positioned against the
   viewport and no ancestor's overflow can clip it. Do not "fix" a clipped menu here by relaxing the
   overflow — that only brings the page-wide scroll back. */

/* The shell's main column is a flex item of `.app-main`, and ArchitectUI leaves it at the flex
   default `min-width: auto` — "never shrink below your content". Measured: with a 1583px table on a
   1280px viewport the column itself came out 1639px wide, so the scroll box inside it was handed
   1583px of room and had nothing to scroll; the page scrolled instead, exactly as before. This one
   declaration is what makes every rule below do anything at all. It is the FLEX ITEM that needs it —
   `.app-main__inner` is an ordinary block and simply inherits the width. */
.app-main .app-main__outer {
    min-width: 0;
}

/* Same story one level down: `.card-body` is a flex item of `.row` in all three plugins. */
.crud-table-plugin > .card-body,
.process-table-plugin > .card-body,
.crud-tree-plugin > .card-body {
    min-width: 0;
}

.nct-table-scroll {
    overflow-x: auto;
    /* A scroll box establishes a scrollable overflow region on both axes; nothing in these tables
       overflows vertically (the box is as tall as its table), so this stays a horizontal scroller. */
    max-width: 100%;
    /* Firefox has no ::-webkit scrollbar pseudo-elements; these two are the whole styling API it
       offers, and they must sit on the same element as the overflow. */
    scrollbar-width: thin;
    scrollbar-color: var(--comment, #adb5bd) transparent;
}

/* Themed to the skin like every other scrollbar in the product, and only 8px tall — it sits under
   the last row, where a default 15px OS bar reads as a second table border. */
.nct-table-scroll::-webkit-scrollbar {
    height: 8px;
}

.nct-table-scroll::-webkit-scrollbar-track {
    background: transparent;
}

.nct-table-scroll::-webkit-scrollbar-thumb {
    background: var(--comment, #adb5bd);
    border-radius: 4px;
}

.nct-table-scroll::-webkit-scrollbar-thumb:hover {
    background: var(--foreground, #6c757d);
}

/* ================================================================================================
   SIDEBAR QUICK LINKS — a long name wraps, it does not widen the sidebar.
   ================================================================================================

   ArchitectUI writes `white-space: nowrap` on every `.vertical-nav-menu li a` and pins the link to a
   fixed `height`/`line-height` of 2.4rem (2rem for nested levels). The sidebar itself is a fixed
   `min-width` flex item, and each nesting level of `site.kicker.plugin` adds 2rem of indent — so a
   third-level quick link with a real page name ("Procurement request approval") ran past the edge
   and the sidebar scrolled sideways. The user then drags a scrollbar to read a menu.

   The name is what has to give. It wraps onto a second line, the row grows to fit it (`height:auto`
   with `min-height` keeping the single-line rhythm untouched), and `overflow-wrap: anywhere` covers
   the one case wrapping alone cannot — a single unbroken token longer than the sidebar.

   `display: flex` + `align-items: center` is how the text stays vertically centred once the fixed
   `line-height` can no longer do it. The two `<i>` glyphs (`metismenu-icon`, `metismenu-state-icon`)
   are absolutely positioned and centred on `top: 50%`, so they follow the taller row on their own.
   The caret keeps its 30px of `padding-right` from the base theme, so wrapped text never runs under
   it, and `.metismenu-actions-btn` is absolute at `top: 0` — it stays on the FIRST line of a wrapped
   item, which is where the pointer expects it. */

.app-sidebar .vertical-nav-menu li a {
    white-space: normal;
    overflow-wrap: anywhere;
    height: auto;
    min-height: 2.4rem;
    line-height: 1.25;
    display: flex;
    align-items: center;
}

.app-sidebar .vertical-nav-menu ul > li > a {
    min-height: 2rem;
}

/* The section headings above each group of quick links ("OPS", the epic names) are nowrap for the
   same reason and widen the sidebar the same way — with the added twist that the heading is a flex
   row holding a drag grip and an actions button, so only the NAME may wrap. */
.app-sidebar .app-sidebar__heading {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* The name inside that heading is a flex item (PagesPanel.html lays the row out as grip / name /
   actions), and a flex item refuses to shrink below its own content unless told to. Without this
   the wrap above never gets a chance to happen — the item stays as wide as the word. */
.app-sidebar .app-sidebar__heading > span {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* THE COLLAPSED SIDEBAR IS THE EXCEPTION, and it has to be stated or the rules above break it.
   With the sidebar closed ArchitectUI keeps the labels in the DOM and pushes them out of sight with
   `text-indent: -99rem`. That trick only ever moves the FIRST line, and it does not apply at all to
   a flex container — so a wrapping, flex link would have shown its second line as a stray fragment
   next to the icons. While the sidebar is collapsed and not hovered the labels therefore go back to
   being single-line blocks; on hover it expands to full width, shows its labels, and the wrapping
   rules above are exactly what it wants again. */
.closed-sidebar .app-sidebar:not(:hover) .vertical-nav-menu li a {
    display: block;
    white-space: nowrap;
    height: 2.4rem;
    line-height: 2.4rem;
}

.closed-sidebar .app-sidebar:not(:hover) .vertical-nav-menu ul > li > a {
    height: 2rem;
    line-height: 2rem;
}

.closed-sidebar .app-sidebar:not(:hover) .app-sidebar__heading {
    white-space: nowrap;
}

/* ================================================================================================
   THE ENVIRONMENT MENU — all skins

   Which copy of this project you are in, and how to reach, create or promote the others. It sits
   beside the theme switch because both answer "which version of this am I looking at?", and both
   have to be answerable without leaving the page.

   ONE layer for every skin, like everything else in this file: the control is drawn entirely from
   the RUNG tokens (--surface-1, --srf-2, --ink-1, --ink-2, --edge-ui, --ink-positive,
   --ink-caution, --ink-negative), so a new skin gets it for free and no skin file mentions it. Every
   token carries a fallback, because this control also renders on the pages that load before a skin
   has been resolved.

   NOT `.dropdown-menu`. Two independent reasons, and both bite:
     * base.css turns EVERY .dropdown-menu fixed below the lg breakpoint and pins left/top on it,
       which is what forced the four `!important` offsets on .nct-theme-menu above;
     * ArchitectUI's js/app.js binds a click handler to every .dropdown-menu and ends it with
       stopPropagation(), which swallows delegated handlers inside it.
   Carrying its own class costs a dozen lines of positioning and avoids both.
   ================================================================================================ */

.nct-env-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
    margin-right: .5rem;
}

/* The button REPORTS as well as acts, so it carries a label. An icon alone cannot answer "am I in
   production?", and the cost of getting that wrong is editing live. */
.nct-env-toggle {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    max-width: 190px;
    padding: 5px 10px;
    border-radius: 999px;
    color: var(--ink-1, inherit);
    cursor: pointer;
    text-decoration: none;
    box-shadow: inset 0 0 0 1px var(--edge-ui, rgba(127, 127, 127, .32));
    transition: box-shadow .16s ease, background-color .16s ease;
}

.nct-env-toggle:hover {
    text-decoration: none;
    color: var(--ink-1, inherit);
    background: var(--srf-2, rgba(127, 127, 127, .1));
}

/* An <a> with no href gets neither a pointer nor a focus ring for free. */
.nct-env-toggle:focus-visible {
    outline: 2px solid var(--bs-primary, #3f6ad8);
    outline-offset: 2px;
}

.nct-env-toggle__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .8125rem;
    font-weight: 600;
}

/* Production is the one that matters, so it is the one that gets the loud colour. Every copy shares
   a single quieter colour rather than each getting its own: the point of the dot is the binary
   "live / not live", and a palette of environment colours would bury it. */
.nct-env-dot {
    flex: 0 0 auto;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    box-shadow: inset 0 0 0 1px rgba(127, 127, 127, .45);
}

.nct-env-dot--root {
    background: var(--ink-positive, #3ac47d);
}

.nct-env-dot--copy {
    background: var(--ink-caution, #f7b924);
}

.nct-env-caret {
    font-size: .75rem;
    opacity: .55;
}

/* The ring REPLACES the caret while something runs, so the button says one thing at a time. Driven
   by ImportProgressSupport — the animator writes stroke-dasharray/offset on .nct-imp-ring, so
   nothing here animates and the geometry is all that lives in CSS. */
.nct-env-ring {
    display: inline-flex;
    width: 16px;
    height: 16px;
}

.nct-env-ring svg {
    width: 16px;
    height: 16px;
    transform: rotate(-90deg);          /* 12 o'clock start, like every other progress ring here */
}

.nct-env-ring__track,
.nct-env-ring__value {
    fill: none;
    stroke-width: 2.5;
}

.nct-env-ring__track {
    stroke: var(--edge-ui, rgba(127, 127, 127, .3));
}

.nct-env-ring__value {
    stroke: var(--bs-primary, #3f6ad8);
    stroke-linecap: round;
}

/* --- the menu ------------------------------------------------------------------------------- */

.nct-env-menu {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 1021;                      /* above the header (1020 in base.css), below a modal */
    display: none;
    /* Sized to its own content: the wrapper is only as wide as the button, and an absolutely
       positioned box shrink-to-fits against its containing block — so `auto` would make the menu's
       width depend on the length of the current environment's name. */
    width: max-content;
    min-width: 260px;
    max-width: 340px;
    padding: 6px;
    /* Literal, and --srf-1 rather than --surface-1: both --surface-1 and --radius are defined by
       dark.css ALONE. On Dracula, Forest and Dark Blue the fallback would win — a white box whose
       text is --ink-1, i.e. near-white on white. --srf-1 and --srf-2 are the two surface tokens
       every skin defines. */
    border-radius: 10px;
    background: var(--srf-1, #fff);
    box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .18), inset 0 0 0 1px var(--edge-ui, rgba(127, 127, 127, .22));
    text-align: left;
}

.nct-env-menu.show {
    display: block;
}

.nct-env-menu__title {
    padding: 6px 10px 8px;
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .075em;
    text-transform: uppercase;
    color: var(--ink-2, inherit);
    opacity: .72;
}

.nct-env-menu__items {
    /* A family is small, but it is not bounded — an author may take a copy per feature branch. The
       list scrolls; the actions under it never do, so "Create environment" is always reachable. */
    max-height: 46vh;
    overflow-y: auto;
}

.nct-env-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 8px;
    color: var(--ink-1, inherit);
    text-decoration: none;
}

.nct-env-item:hover {
    background: var(--srf-2, rgba(127, 127, 127, .1));
    color: var(--ink-1, inherit);
    text-decoration: none;
}

.nct-env-item__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1 1 auto;
}

.nct-env-item__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .875rem;
    font-weight: 600;
}

.nct-env-item__role {
    font-size: .6875rem;
    color: var(--ink-2, inherit);
    opacity: .8;
}

.nct-env-item__check {
    flex: 0 0 auto;
    font-size: .75rem;
    opacity: 0;
    color: var(--ink-brand, var(--bs-primary, #3f6ad8));
}

/* The row you are on. A left rail AND a tick: the rail reads at a glance in a list of similar
   names, and the tick is what a screen reader's user finds when they walk the menu. */
.nct-env-item.is-current {
    box-shadow: inset 2px 0 0 0 var(--bs-primary, #3f6ad8);
}

.nct-env-item.is-current .nct-env-item__check {
    opacity: 1;
}

/* A row is the environment PLUS what can be done to it. The anchor and the delete button are
   siblings because a link inside a link is invalid HTML; the row is the flex context that puts them
   on one line and lets the name take the space that is left. */
.nct-env-row {
    display: flex;
    align-items: center;
}

.nct-env-row .nct-env-item {
    flex: 1 1 auto;
    /* Without this a long environment name refuses to shrink and pushes the delete button out of
       the menu: a flex item's default min-width is auto, i.e. its content. */
    min-width: 0;
}

/* Deliberately VISIBLE at rest, only quieter.

   The obvious treatment is opacity: 0 until the row is hovered, and it is wrong twice: the control
   stays a full-size hit target while invisible, so a click can land on a delete the author never
   saw, and a touch device has no hover at all, which would leave the whole feature unreachable
   there. Subdued-then-strengthened costs nothing and has neither problem. */
.nct-env-remove {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin-left: 2px;
    border-radius: 8px;
    color: var(--ink-2, inherit);
    text-decoration: none;
    opacity: .45;
    transition: opacity .12s ease, color .12s ease, background-color .12s ease;
}

.nct-env-row:hover .nct-env-remove,
.nct-env-remove:focus {
    opacity: 1;
}

/* Red only on the way in. Colouring it at rest would make every environment row read as a warning,
   which is the opposite of what a list of environments is for. */
.nct-env-remove:hover {
    background: var(--srf-2, rgba(127, 127, 127, .1));
    color: var(--ink-negative, #d92d20);
    text-decoration: none;
    opacity: 1;
}

/* A control that cannot act right now. Muted and inert, but still THERE and still readable: the
   author needs to see that Publish exists and will come back, which is exactly the case where a
   disabled control is right and hiding it would be wrong. pointer-events keeps the hover affordance
   from lying; Wicket has already stopped the callback server-side. */
.nct-env-action.is-disabled,
.nct-env-remove.is-disabled {
    opacity: .38;
    pointer-events: none;
    cursor: default;
}

.nct-env-menu__divider {
    height: 1px;
    margin: 6px 4px;
    background: var(--edge-ui, rgba(127, 127, 127, .22));
}

.nct-env-action {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 8px;
    color: var(--ink-1, inherit);
    cursor: pointer;
    text-decoration: none;
    font-size: .875rem;
}

.nct-env-action:hover {
    background: var(--srf-2, rgba(127, 127, 127, .1));
    color: var(--ink-1, inherit);
    text-decoration: none;
}

/* --- a job in flight ------------------------------------------------------------------------ */

.nct-env-job {
    margin: 4px 2px;
    padding: 9px 10px;
    border-radius: 8px;
    background: var(--srf-2, rgba(127, 127, 127, .08));
    box-shadow: inset 0 0 0 1px var(--edge-ui, rgba(127, 127, 127, .2));
}

.nct-env-job.is-failed {
    box-shadow: inset 0 0 0 1px var(--edge-negative, rgba(211, 47, 47, .5));
}

.nct-env-job.is-warned {
    box-shadow: inset 0 0 0 1px var(--edge-caution, rgba(247, 185, 36, .5));
}

.nct-env-job__head {
    display: flex;
    align-items: center;
    gap: 8px;
}

.nct-env-job__title {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .8125rem;
    font-weight: 600;
}

.nct-env-job__dismiss {
    flex: 0 0 auto;
    color: var(--ink-2, inherit);
    opacity: .6;
    cursor: pointer;
}

.nct-env-job__dismiss:hover {
    opacity: 1;
    color: var(--ink-1, inherit);
}

.nct-env-job__line {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-top: 2px;
}

.nct-env-job__stage {
    flex: 1 1 auto;
    font-size: .75rem;
    color: var(--ink-2, inherit);
}

.nct-env-job__percent {
    font-size: .75rem;
    font-variant-numeric: tabular-nums;   /* the number must not shuffle the line as it counts up */
    color: var(--ink-2, inherit);
}

.nct-env-job__track {
    height: 4px;
    margin-top: 6px;
    border-radius: 999px;
    background: var(--edge-ui, rgba(127, 127, 127, .25));
    overflow: hidden;
}

/* Width is written by the animator, not here. Starting at 0 matters: a bar with no width rule paints
   full for one frame before the first rAF tick. */
.nct-env-job__bar {
    display: block;
    width: 0;
    height: 100%;
    border-radius: 999px;
    background: var(--bs-primary, #3f6ad8);
}

.nct-env-job__detail,
.nct-env-job__error {
    margin-top: 5px;
    font-size: .6875rem;
    line-height: 1.35;
    /* The full text stays in the DOM — someone reading it out on a support call needs all of it —
       but only three lines are shown, so one long stack trace cannot push the actions off screen. */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.nct-env-job__detail {
    color: var(--ink-2, inherit);
    opacity: .85;
}

.nct-env-job__error {
    color: var(--ink-negative, #d92550);
}

/* --- the create dialog ----------------------------------------------------------------------- */

.nct-env-address {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 10px 12px;
    border-radius: 8px;
    background: var(--srf-2, rgba(127, 127, 127, .08));
}

.nct-env-address__label {
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--ink-2, inherit);
    opacity: .8;
}

.nct-env-address__value {
    overflow-x: auto;                    /* a long alias scrolls inside the box, never widens it */
    white-space: nowrap;
    font-size: .8125rem;
    color: var(--ink-1, inherit);
}

.nct-env-note {
    display: flex;
    gap: 10px;
    margin-top: 14px;
    font-size: .8125rem;
    line-height: 1.45;
    color: var(--ink-2, inherit);
}

.nct-env-note i {
    flex: 0 0 auto;
    margin-top: 1px;
    font-size: 1.1rem;
    opacity: .7;
}

@media (prefers-reduced-motion: reduce) {
    .nct-env-toggle {
        transition: none;
    }
}

/* ================================================================================================
   THE PUBLISH DIALOG — all skins

   Promoting one environment into another: choose a target, watch the comparison, review what would
   happen, read what did. One modal, four sections, exactly one visible — see PublishDialogPanel.

   ONE layer for every skin, like the environment menu above it, and drawn from the tokens EVERY skin
   defines: --srf-1, --srf-2, --srf-3, --ink-1, --ink-2, --edge-ui, --edge-caution, --edge-negative,
   --ink-positive, --ink-caution, --ink-negative, --ink-brand, --bs-primary. Verify with
   `grep -l -- "--token:" themes/*.css` before adding one: --surface-1 and --radius are defined by
   dark.css ALONE, and using them is what rendered the environment menu white-on-white on three
   skins. Every token carries a fallback anyway, because this dialog can open on a page that loaded
   before a skin was resolved.

   The few literal rgba() values below are deliberate and are NOT missing tokens: they are low-alpha
   tints laid OVER whatever surface the skin painted, so they read the same on a white card and on a
   near-black one. A tint has no token because it is not a colour, it is a modifier of one.
   ================================================================================================ */

.nct-pub {
    color: var(--ink-1, inherit);
}

.nct-pub-field {
    margin-bottom: 1rem;
}

.nct-pub-target__meta {
    color: var(--ink-2, inherit);
}

/* --- the actions row, shared by all four sections -------------------------------------------- */

/* Sticky, because the review scrolls and the primary button carries the count of what is ticked.
   A number that scrolls off the screen while the author is ticking is a number they stop reading.
   Sticky with no scrolling ancestor is simply normal flow, so the other three sections are
   unaffected. */
.nct-pub-actions {
    position: sticky;
    bottom: 0;
    z-index: 2;
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    margin-top: 1rem;
    padding: .75rem 0 .25rem;
    background: var(--srf-1, #fff);
    box-shadow: inset 0 1px 0 0 var(--edge-ui, rgba(127, 127, 127, .22));
}

.nct-pub-actions__count {
    font-variant-numeric: tabular-nums;   /* the count must not shuffle the button as it changes */
}

/* An AjaxSubmitLink is an <a> and has no `disabled` to set, so "nothing is selected" is expressed
   by taking the click away rather than by only looking inert. */
.nct-pub-actions__publish.is-disabled {
    pointer-events: none;
    opacity: .5;
}

/* --- state two: the job in flight ------------------------------------------------------------ */

.nct-pub-progress {
    padding: 1.25rem .25rem;
}

.nct-pub-progress__title {
    font-size: 1rem;
    font-weight: 600;
}

.nct-pub-progress__line {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-top: .4rem;
}

.nct-pub-progress__stage {
    flex: 1 1 auto;
    font-size: .8125rem;
    color: var(--ink-2, inherit);
}

.nct-pub-progress__percent {
    font-size: .8125rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-2, inherit);
}

.nct-pub-progress__track {
    height: 6px;
    margin-top: .5rem;
    border-radius: 999px;
    background: var(--edge-ui, rgba(127, 127, 127, .25));
    overflow: hidden;
}

/* Width is written by ImportProgressSupport's animator, never here. Starting at 0 matters: a bar
   with no width rule paints FULL for one frame before the first rAF tick, i.e. it reports "done". */
.nct-pub-progress__bar {
    display: block;
    width: 0;
    height: 100%;
    border-radius: 999px;
    background: var(--bs-primary, #3f6ad8);
}

.nct-pub-progress__detail {
    margin-top: .5rem;
    font-size: .75rem;
    color: var(--ink-2, inherit);
    opacity: .85;
}

.nct-pub-progress__error {
    margin-top: .5rem;
    font-size: .8125rem;
    line-height: 1.4;
    color: var(--ink-negative, #d92550);
}

/* --- state three: the review ----------------------------------------------------------------- */

/* The scroll container. Bounded here rather than left to the modal, because the actions row is
   sticky to THIS box: sticky positions against the nearest scrolling ancestor, so if the modal
   scrolled instead, the button would scroll away with the list. */
.nct-pub-review {
    display: block;
    max-height: 62vh;
    overflow-y: auto;
    padding-right: 4px;                 /* rows must not sit under the scrollbar */
}

.nct-pub-review__summary {
    margin-bottom: .75rem;
    font-size: .8125rem;
    color: var(--ink-2, inherit);
}

.nct-pub-warning {
    margin-bottom: .5rem;
    padding: .6rem .75rem;
    border-radius: 8px;
    background: var(--srf-2, rgba(127, 127, 127, .08));
    box-shadow: inset 3px 0 0 0 var(--edge-ui, rgba(127, 127, 127, .35));
    font-size: .8125rem;
}

/* The three severities of PublishWarning.Severity, and all three must be here: the panel writes
   `nct-pub-warning--` + severity.name().toLowerCase() unconditionally, so a severity with no rule
   renders with the neutral base rail and reads as an ordinary line rather than as a notice. INFO
   was the one that was missing. */
.nct-pub-warning--info {
    box-shadow: inset 3px 0 0 0 var(--ink-info, var(--bs-primary, #3f6ad8));
}

.nct-pub-warning--caution {
    box-shadow: inset 3px 0 0 0 var(--ink-caution, #f7b924);
}

.nct-pub-warning--blocking {
    box-shadow: inset 3px 0 0 0 var(--ink-negative, #d92550);
}

.nct-pub-warning__advice {
    margin-top: .25rem;
    color: var(--ink-2, inherit);
    opacity: .9;
}

.nct-pub-empty {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 1.5rem .75rem;
    color: var(--ink-2, inherit);
    text-align: left;
}

.nct-pub-empty i {
    font-size: 1.5rem;
    color: var(--ink-positive, #3ac47d);
}

/* --- a section ------------------------------------------------------------------------------- */

.nct-pub-section {
    margin-bottom: .75rem;
    border-radius: 10px;
    background: var(--srf-2, rgba(127, 127, 127, .06));
    box-shadow: inset 0 0 0 1px var(--edge-ui, rgba(127, 127, 127, .2));
    overflow: hidden;
}

.nct-pub-section__head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: .6rem .75rem;
}

.nct-pub-section__toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 1 1 auto;
    min-width: 0;
    color: var(--ink-1, inherit);
    cursor: pointer;
    text-decoration: none;
}

.nct-pub-section__toggle:hover,
.nct-pub-section__toggle:focus {
    color: var(--ink-1, inherit);
    text-decoration: none;
}

.nct-pub-section__chevron {
    font-size: .8125rem;
    opacity: .6;
    transition: transform .15s ease;
}

.nct-pub-section__title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .875rem;
    font-weight: 600;
}

.nct-pub-section__count {
    flex: 0 0 auto;
    font-size: .75rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-2, inherit);
}

.nct-pub-section__bulk {
    display: flex;
    flex: 0 0 auto;
    gap: .6rem;
    font-size: .75rem;
}

.nct-pub-section__bulk a {
    color: var(--ink-brand, var(--bs-primary, #3f6ad8));
    cursor: pointer;
    text-decoration: none;
}

.nct-pub-section__bulk a:hover {
    text-decoration: underline;
}

.nct-pub-section__body {
    padding: 0 .5rem .5rem;
}

.nct-pub-section.is-collapsed .nct-pub-section__body {
    display: none;
}

.nct-pub-section.is-collapsed .nct-pub-section__chevron {
    transform: rotate(-90deg);
}

/* The database and the deletions are the two sections that can take something away, so they say so
   before a single row is read. A rail rather than a fill: a whole tinted panel makes every row in it
   look equally dangerous, and most of them are not. */
.nct-pub-section--db {
    box-shadow: inset 0 0 0 1px var(--edge-caution, rgba(247, 185, 36, .5));
}

.nct-pub-section--remove {
    box-shadow: inset 0 0 0 1px var(--edge-negative, rgba(211, 47, 47, .45));
}

.nct-pub-group__name {
    padding: .5rem .5rem .25rem;
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .075em;
    text-transform: uppercase;
    color: var(--ink-2, inherit);
    opacity: .75;
}

/* --- a row ------------------------------------------------------------------------------------ */

/* The whole row is the label, so the click target in a list of two hundred is the row and not a
   13px square. */
.nct-pub-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    width: 100%;
    margin: 0;
    /* Wider than it looks it needs to be: the checkbox inside carries a 12px glow when checked, and
       the section this row sits in scrolls, so anything that reaches past the padding is cut off. */
    padding: .45rem .65rem;
    border-radius: 7px;
    cursor: pointer;
}

/* A neutral tint rather than a surface token: the row already sits on --srf-2, and which of --srf-1
   / --srf-3 is LIGHTER than it differs between the light and the dark skins — an alpha wash over
   whatever is underneath is visible on all five. */
.nct-pub-row:hover {
    background: rgba(127, 127, 127, .12);
}

/* An EXPLICIT box, and room around it.
   Two things went wrong without this. The row is a flex container and `flex: 0 0 auto` takes its
   basis from the `width` property — which a bare <input type="checkbox"> does not have, and which
   none of the five skins sets even though every one of them restyles the control (gradient
   background, 2px border, radius). And each skin draws `box-shadow: 0 0 12px` around a CHECKED box,
   which needs room that .5rem of row padding did not leave, so the glow — and with it the left edge
   of the control — was clipped by the list's scroll container.
   accent-color makes the checked fill follow the brand in the skins that do not override it. */
.nct-pub-row__check {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    min-width: 16px;
    margin-top: .2rem;
    margin-left: 2px;
    accent-color: var(--bs-primary, #3f6ad8);
}

.nct-pub-row__text {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-width: 0;                       /* without it a long name widens the modal instead of eliding */
}

.nct-pub-row__line {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
}

.nct-pub-row__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .875rem;
}

.nct-pub-row__detail,
.nct-pub-row__note {
    font-size: .75rem;
    line-height: 1.35;
    color: var(--ink-2, inherit);
}

.nct-pub-row__note {
    opacity: .85;
}

.nct-pub-row__action {
    flex: 0 0 auto;
    font-size: .6875rem;
    padding: .1rem .45rem;
    border-radius: 999px;
    color: var(--ink-2, inherit);
    box-shadow: inset 0 0 0 1px var(--edge-ui, rgba(127, 127, 127, .3));
    white-space: nowrap;
}

.nct-pub-row.is-destructive .nct-pub-row__action {
    color: var(--ink-negative, #d92550);
    box-shadow: inset 0 0 0 1px var(--edge-negative, rgba(211, 47, 47, .45));
}

/* THE ONE ROW THIS WHOLE FEATURE EXISTS FOR. Dropping an index is destructive and costs nothing;
   dropping a column empties it for everyone who has a row in that table. It gets a rail, a tint and
   a word — colour alone would say nothing to a reader who cannot see it. */
.nct-pub-row.is-loses-rows {
    background: rgba(217, 37, 80, .12);
    box-shadow: inset 3px 0 0 0 var(--ink-negative, #d92550);
}

.nct-pub-row.is-loses-rows:hover {
    background: rgba(217, 37, 80, .18);
}

.nct-pub-loses {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
    padding: .1rem .45rem;
    border-radius: 999px;
    font-size: .6875rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--ink-negative, #d92550);
    box-shadow: inset 0 0 0 1px var(--ink-negative, #d92550);
}

/* Shown, never applied. Greyed rather than hidden: an author who is not told the two sides differ
   here would believe they now match. */
.nct-pub-row.is-disabled {
    cursor: default;
    opacity: .55;
}

.nct-pub-row.is-disabled:hover {
    background: transparent;
}

/* --- a two-way choice ------------------------------------------------------------------------- */

.nct-pub-row--conflict {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    cursor: default;
}

.nct-pub-row--conflict:hover {
    background: transparent;
}

.nct-pub-choice {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem 1rem;
    padding-left: .1rem;
}

.nct-pub-choice__option {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    font-size: .8125rem;
    cursor: pointer;
}

/* --- state four: what happened ---------------------------------------------------------------- */

.nct-pub-result__summary {
    margin-bottom: .75rem;
    font-size: .875rem;
    font-weight: 600;
}

.nct-pub-result {
    max-height: 52vh;
    overflow-y: auto;
}

.nct-pub-result__row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px;
    padding: .4rem .5rem;
    border-radius: 7px;
    font-size: .8125rem;
}

.nct-pub-result__row.is-failed {
    background: rgba(217, 37, 80, .12);
    box-shadow: inset 3px 0 0 0 var(--ink-negative, #d92550);
}

.nct-pub-result__row .nct-pub-row__error {
    flex: 1 1 100%;
    font-size: .75rem;
    line-height: 1.35;
    color: var(--ink-negative, #d92550);
}

/* --- the role-group question ------------------------------------------------------------------ */

/* Rendered inside the page's MESSAGE modal, not inside this dialog, so it is styled at the top
   level. It names the groups and the people in them, because "3 people" is a number and "and Nune
   from finance" is a decision. */
.nct-pub-rolegroups {
    margin: .5rem 0 .75rem;
    padding-left: 1.1rem;
    font-size: .8125rem;
    line-height: 1.5;
}

.nct-pub-rolegroups strong {
    color: var(--ink-negative, #d92550);
}

@media (prefers-reduced-motion: reduce) {
    .nct-pub-section__chevron {
        transition: none;
    }
}
