/* ==========================================================================
   AL Dev Toolbox - CORE COMPONENT LIBRARY (step 2)
   Requires tokens.css. Tokens only - no raw hex anywhere in this file.
   Controls are BC-square (--r-control); containers keep --r / --r-lg.
   Every component exposes real :hover / :focus-visible / :active / [disabled]
   states, plus .is-hover / .is-focus / .is-active forced classes so the spec
   sheet can show them side by side. Ship the pseudo-classes; the .is-* forms
   are display aids and are safe to drop in the Blazor port.
   ========================================================================== */

/* Every component below sets an explicit `display`, which silently beats the
   user agent's `[hidden] { display: none }` and leaves the element on screen.
   One guard here keeps the HTML attribute working on all of them. Matches the
   HTML spec's own suggested rendering for [hidden]. */
[hidden] { display: none !important; }

/* ---------- BUTTONS  (maps to .btn / .btn--primary in base.css) ---------- */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
    height: var(--control-h); padding: 0 var(--space-3);
    border: 1px solid var(--border-input); border-radius: var(--r-control);
    background: var(--surface); color: var(--ink-2);
    font-family: var(--font-sans); font-size: var(--text-base); font-weight: var(--fw-medium);
    line-height: 1; white-space: nowrap; cursor: pointer; position: relative;
    text-decoration: none;  /* a .btn can be an <a>; it must never carry a link underline */
    transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast);
}
.btn:hover, .btn.is-hover { background: var(--surface-sunken); border-color: var(--border-strong); color: var(--ink); text-decoration: none; }
.btn:focus-visible, .btn.is-focus { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.btn:active, .btn.is-active { background: var(--surface-sunken); border-color: var(--ink-4); transform: translateY(1px); }
.btn[disabled], .btn.is-disabled { background: var(--surface-sunken); border-color: var(--border); color: var(--ink-4); cursor: not-allowed; transform: none; }
.btn__icon { flex: none; width: 16px; height: 16px; }

.btn--primary { background: var(--primary-strong); border-color: var(--primary-strong); color: var(--on-primary-strong); }
.btn--primary:hover, .btn--primary.is-hover { background: var(--primary-stronger); border-color: var(--primary-stronger); color: var(--on-primary-strong); }
.btn--primary:active, .btn--primary.is-active { background: var(--primary-stronger); border-color: var(--primary-stronger); }
.btn--primary[disabled], .btn--primary.is-disabled { background: var(--surface-sunken); border-color: var(--border); color: var(--ink-4); }

.btn--danger { background: var(--danger-strong); border-color: var(--danger-strong); color: var(--on-primary-strong); }
.btn--danger:hover, .btn--danger.is-hover { filter: brightness(.92); }
.btn--danger[disabled], .btn--danger.is-disabled { background: var(--surface-sunken); border-color: var(--border); color: var(--ink-4); filter: none; }

.btn--ghost { background: transparent; border-color: transparent; color: var(--primary-ink); }
.btn--ghost:hover, .btn--ghost.is-hover { background: var(--primary-weak); border-color: transparent; color: var(--primary-ink); }
.btn--ghost[disabled], .btn--ghost.is-disabled { background: transparent; border-color: transparent; color: var(--ink-4); }

.btn--sm { height: var(--control-h-sm); padding: 0 var(--space-2); font-size: var(--text-sm); }
.btn--sm .btn__icon { width: 14px; height: 14px; }
.btn--icon { width: var(--control-h); padding: 0; }
.btn--icon.btn--sm { width: var(--control-h-sm); }
/* Carried over from the app; the handoff has no large size. */
.btn--lg { height: var(--control-h-lg); padding: 0 var(--space-4); font-size: var(--text-md); }

/* File picker dressed as a button. The native control cannot be styled, so it
   is stretched invisibly over a <label class="btn btn--file">. Without this the
   input renders at full size INSIDE the button and the user sees two controls:
   our label and the browser's "Choose File / No file chosen". */
/* Native file input, kept visible. Use this where nothing echoes the chosen
   filename back -- a plain form post -- and .btn--file below where something
   does. The control's own button is drawn by the browser; all we can do is
   give the box the same height and border as its siblings. */
.input--file { padding: 0 var(--space-2); line-height: calc(var(--control-h) - 2px); }
.input--file::file-selector-button {
    height: var(--control-h-sm); margin-right: var(--space-2); padding: 0 var(--space-2);
    background: var(--surface); border: 1px solid var(--input-border); border-radius: var(--r-control);
    color: var(--ink-2); font-family: var(--font-sans); font-size: var(--text-sm); cursor: pointer;
}
.input--file::file-selector-button:hover { background: var(--surface-sunken); border-color: var(--border-strong); }

.btn--file { position: relative; overflow: hidden; }
.btn--file input[type="file"] { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
.btn--file:focus-within { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
/* Carried over from the app: some buttons are <a>s, which cannot take the
   [disabled] attribute, so they signal it with aria-disabled + a class. */
.btn--disabled, .btn--disabled:hover,
.btn[aria-disabled="true"], .btn[aria-disabled="true"]:hover {
    background: var(--surface-sunken); border-color: var(--border);
    color: var(--ink-4); cursor: not-allowed; pointer-events: none; transform: none;
}

/* Loading state. The handoff blanks the label and draws a bare ::after spinner;
   we additionally support a .btn__label-busy span ("Generating...") because a
   spinner alone does not say what is happening on a generate that runs for
   seconds. Both contracts work: markup WITH a busy label swaps to it, markup
   WITHOUT one (everything in the handoff's own sheets) falls back to the
   handoff's spinner exactly. JS toggles .btn--loading -- see generate.js. */
.btn__spinner,
.btn__label-busy { display: none; }
.btn--loading { cursor: wait; pointer-events: none; }
.btn--loading .btn__label { display: none; }
.btn--loading .btn__label-busy { display: inline-flex; align-items: center; gap: var(--space-1); }
.btn--loading .btn__spinner {
    display: inline-block; width: 12px; height: 12px;
    border-radius: var(--r-pill); border: 2px solid currentColor;
    border-right-color: transparent; margin-right: var(--space-1);
    animation: al-spin 800ms linear infinite;
}
/* No busy label in the markup -> behave exactly as the handoff specifies:
   the label is hidden but still occupies its space, so the button keeps its
   width and does not jump the moment it is clicked. */
.btn--loading:not(:has(.btn__label-busy)) { color: transparent; }
.btn--loading:not(:has(.btn__label-busy)) .btn__label { display: inline; visibility: hidden; }
.btn--loading:not(:has(.btn__label-busy))::after {
    content: ""; position: absolute; width: 14px; height: 14px;
    border-radius: var(--r-pill); border: 2px solid var(--ink-3);
    border-top-color: transparent; animation: al-spin 700ms linear infinite;
}
.btn--primary.btn--loading:not(:has(.btn__label-busy))::after,
.btn--danger.btn--loading:not(:has(.btn__label-busy))::after { border-color: var(--on-primary-strong); border-top-color: transparent; }

/* split button + kebab  (maps to .ra / row-actions) */
.ra { position: relative; display: inline-flex; }
.ra__main { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.ra__caret { width: var(--control-h); padding: 0; margin-left: -1px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.ra__caret.btn--primary { border-left-color: var(--primary-stronger); }
.ra__menu { position: absolute; top: calc(100% + var(--space-1)); right: 0; display: none; z-index: 30; }
/* A row near the foot of a scrolling list has no room below it, and the content
   column clips what escapes. .ra--up flips the menu above its button; the page's
   open handler sets it from the button's rect against the scroll container. */
.ra--up > .ra__menu { top: auto; bottom: calc(100% + var(--space-1)); }
.ra.is-open .ra__menu { display: block; }

/* ---------- DROPDOWN MENU ---------- */
.menu {
    min-width: 196px; padding: var(--space-1); background: var(--surface);
    border: 1px solid var(--border-strong); border-radius: var(--r-control);
    box-shadow: var(--shadow-lg);
}
.menu__item {
    display: flex; align-items: center; gap: var(--space-2); width: 100%;
    height: var(--control-h); padding: 0 var(--space-2);
    background: none; border: 0; border-radius: var(--r-control);
    color: var(--ink-2); font-family: var(--font-sans); font-size: var(--text-base);
    text-align: left; cursor: pointer; white-space: nowrap;
    /* A menu entry that navigates is an <a>, not a <button> - the system's own
       screens only show buttons, so nothing had turned the underline off. */
    text-decoration: none;
}
/* Items are fixed-height rows, so a wrapping label overflows its own row and
   the list loses its rhythm. The menu sizes to its longest command instead. */
.menu__item:hover, .menu__item.is-hover { background: var(--primary-weak); color: var(--primary-ink); }
.menu__item:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }
.menu__item[disabled] { color: var(--ink-4); cursor: not-allowed; background: none; }
.menu__item--danger { color: var(--danger-text); }
.menu__item--danger:hover { background: var(--danger-bg); color: var(--danger-text); }
.menu__sep { height: 1px; margin: var(--space-1) 0; background: var(--border); }
.menu__icon { width: 15px; height: 15px; flex: none; opacity: .8; }

/* ---------- FORM FIELDS ---------- */
.field { display: grid; gap: var(--space-1); }
.field__label { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--ink-2); }
.field__label .req { color: var(--danger-text); }
.field__hint { font-size: var(--text-xs); color: var(--ink-3); }
.field-error { display: flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); color: var(--danger-text); }
.field-error svg { width: 13px; height: 13px; flex: none; }
/* Additive: the success twin of .field-error. The handoff only ever shows a
   field failing, because a prototype has nothing to save to; pages that save
   one row at a time need to say when a row saved, in the same place and at the
   same size as when it did not. */
.field-ok { display: flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); color: var(--success-text); }
.field-ok svg { width: 13px; height: 13px; flex: none; }
/* Additive: the third twin, for a field whose value is allowed but worth a
   second look. .field-error is for a value the form will not accept, and using
   it for a warning says "you cannot do this" about something the user can in
   fact do - releasing outside a customer's update window is permitted, it is
   just recorded on the delivery. */
.field-warn { display: flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); color: var(--warning-text); }
.field-warn svg { width: 13px; height: 13px; flex: none; }

.input, .textarea, .select {
    width: 100%; height: var(--control-h); padding: 0 var(--space-2);
    background: var(--input-bg); color: var(--ink);
    border: 1px solid var(--input-border); border-radius: var(--r-control);
    font-family: var(--font-sans); font-size: var(--text-base);
    transition: border-color var(--transition-fast), background var(--transition-fast);
}
.textarea { height: auto; min-height: 76px; padding: var(--space-2); line-height: var(--leading-normal); resize: vertical; }
.input::placeholder, .textarea::placeholder { color: var(--ink-4); }
.input:hover, .textarea:hover, .select:hover { border-color: var(--input-border-hover); }
.input:focus-visible, .textarea:focus-visible, .select:focus-visible,
.input.is-focus, .textarea.is-focus, .select.is-focus {
    outline: var(--focus-ring); outline-offset: var(--focus-offset); border-color: var(--input-border-focus);
}
.input[disabled], .textarea[disabled], .select[disabled] { background: var(--input-bg-disabled); border-color: var(--border); color: var(--ink-4); cursor: not-allowed; }
.input[readonly], .input--ro { background: var(--input-bg-readonly); border-color: transparent; color: var(--ink-2); }
.input--num { text-align: right; font-variant-numeric: tabular-nums; }
/* Small variant, to sit inside a data-table row without overpowering it. The
   handoff sizes buttons three ways but only ever draws inputs at full height;
   an in-row role picker needs to match .btn--sm, not tower over it. */
.input--sm, .select--sm { height: var(--control-h-sm); font-size: var(--text-sm); }
/* Mono content in a textarea. The system already sets these three properties
   in two places -- .folder-editor__file-content and .code-editor .cm-editor --
   but both are BEM-scoped to their own block, so a JSON override field or a
   recipe file body had to restate them privately. Same values, as a modifier
   anything can take. */
.textarea--code { font-family: var(--font-mono); font-size: var(--text-sm); line-height: var(--leading-normal); }
.field--invalid .input, .field--invalid .textarea, .field--invalid .select, .input--invalid { border-color: var(--danger-strong); background: var(--danger-bg); }
.select { appearance: none; padding-right: var(--space-6); background-image: none; }
.select-wrap { position: relative; display: block; }
.select-wrap__caret { position: absolute; right: var(--space-2); top: 50%; transform: translateY(-50%); width: 15px; height: 15px; color: var(--ink-3); pointer-events: none; }

/* lookup / assist-edit, BC style */
.input-group { display: flex; }
.input-group .input { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.input-group__btn { width: var(--control-h); flex: none; margin-left: -1px; border-top-left-radius: 0; border-bottom-left-radius: 0; padding: 0; }

/* search */
.search { position: relative; display: block; }
.search__icon { position: absolute; left: var(--space-2); top: 50%; transform: translateY(-50%); width: 15px; height: 15px; color: var(--ink-3); pointer-events: none; }
.search .input { padding-left: calc(var(--space-2) * 2 + 15px); }
.search__clear { position: absolute; right: var(--space-1); top: 50%; transform: translateY(-50%); }

/* checkbox / radio */
.check { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-base); color: var(--ink-2); cursor: pointer; }
.check__box, .check__dot {
    width: 16px; height: 16px; flex: none; display: inline-grid; place-items: center;
    background: var(--input-bg); border: 1px solid var(--input-border); border-radius: var(--r-control);
    transition: background var(--transition-fast), border-color var(--transition-fast);
}
.check__dot { border-radius: var(--r-pill); }
/* Additive: the handoff's checkbox is a static mock, so nothing hides the real
   <input> a form needs. Collapse it rather than display:none — it has to stay
   focusable for the :focus-visible rule below, and clickable via the label. */
.check input[type="checkbox"], .check input[type="radio"] { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.check__box svg { width: 12px; height: 12px; color: var(--on-primary-strong); opacity: 0; }
.check.is-checked .check__box { background: var(--primary-strong); border-color: var(--primary-strong); }
.check.is-checked .check__box svg { opacity: 1; }
/* The handoff drives the tick off an `is-checked` class, which needs something
   watching the input. Plain server-rendered forms have nothing watching, and
   the real input is hidden - so without this the box never ticks. :has() reads
   the input's own state, and agrees with the class wherever both are present. */
.check:has(input:checked) .check__box { background: var(--primary-strong); border-color: var(--primary-strong); }
.check:has(input:checked) .check__box svg { opacity: 1; }
.check:has(input:checked) .check__dot { border-color: var(--primary-strong); border-width: 5px; }
/* After the :has(:checked) pair, not before it: a ticked-and-disabled box drawn
   in the primary colour reads as "this is going to happen". */
/* Additive: a tick that confirms something irreversible. The system has .check
   and it has .alert--danger, but nothing that is both - and a bare tick reading
   "I understand this installs into the live Production environment" is one more
   line of body text, which is exactly the weight such a thing must not have.
   Two callers: the release dialog and the release-pipeline editor. */
.check--ack {
    align-items: flex-start;
    padding: var(--space-3);
    border: 1px solid var(--danger);
    border-radius: var(--r-control);
    background: var(--danger-bg);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
}

.check:has(input:disabled) { color: var(--ink-4); cursor: not-allowed; }
.check:has(input:disabled) .check__box, .check:has(input:disabled) .check__dot { background: var(--input-bg-disabled); border-color: var(--border); }
.check:has(input:disabled):has(input:checked) .check__box { background: var(--ink-4); border-color: var(--ink-4); }
.check.is-indeterminate .check__box { background: var(--primary-strong); border-color: var(--primary-strong); }
.check.is-indeterminate .check__box::after { content: ""; width: 8px; height: 2px; background: var(--on-primary-strong); }
.check.is-checked .check__dot { border-color: var(--primary-strong); border-width: 5px; }
.check:hover .check__box, .check:hover .check__dot { border-color: var(--input-border-hover); }
.check.is-focus .check__box, .check.is-focus .check__dot, .check input:focus-visible + .check__box { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.check.is-disabled { color: var(--ink-4); cursor: not-allowed; }
.check.is-disabled .check__box, .check.is-disabled .check__dot { background: var(--input-bg-disabled); border-color: var(--border); }
.check.is-disabled.is-checked .check__box { background: var(--ink-4); border-color: var(--ink-4); }

/* switch */
.switch { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-base); color: var(--ink-2); cursor: pointer; }
.switch__track { width: 34px; height: 18px; flex: none; padding: 2px; background: var(--surface-sunken); border: 1px solid var(--input-border); border-radius: var(--r-pill); transition: background var(--transition-fast), border-color var(--transition-fast); }
.switch__knob { display: block; width: 12px; height: 12px; border-radius: var(--r-pill); background: var(--ink-4); transition: transform var(--transition-fast), background var(--transition-fast); }
.switch.is-on .switch__track { background: var(--primary-strong); border-color: var(--primary-strong); }
.switch.is-on .switch__knob { transform: translateX(16px); background: var(--on-primary-strong); }
.switch.is-focus .switch__track { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.switch.is-disabled { color: var(--ink-4); cursor: not-allowed; }
.switch.is-disabled .switch__track { background: var(--input-bg-disabled); border-color: var(--border); }
/* Additive, and the exact pair already added to .check above — same reason.
   The handoff's switch is a static mock driven by an `is-on` class, so it has
   no <input> and nothing a form could post. Ours wraps a real checkbox, hidden
   the same way, and reads its state with :has() so a plain server-rendered
   form needs nothing watching it. Both selectors agree where both apply. */
.switch input[type="checkbox"] { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.switch:has(input:checked) .switch__track { background: var(--primary-strong); border-color: var(--primary-strong); }
.switch:has(input:checked) .switch__knob { transform: translateX(16px); background: var(--on-primary-strong); }
.switch:has(input:focus-visible) .switch__track { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
/* After the :has(:checked) pair, so an on-and-disabled switch reads as locked
   rather than as primary-coloured "this is about to happen". */
.switch:has(input:disabled) { color: var(--ink-4); cursor: not-allowed; }
.switch:has(input:disabled) .switch__track { background: var(--input-bg-disabled); border-color: var(--border); }
.switch:has(input:disabled:checked) .switch__track { background: var(--ink-4); border-color: var(--ink-4); }

/* form layouts */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4) var(--space-5); }
.form-grid .field--full { grid-column: 1 / -1; }
@media (max-width: 700px) { .form-grid { grid-template-columns: 1fr; } }
.form-row { display: grid; grid-template-columns: 168px 1fr; align-items: center; gap: var(--space-2) var(--space-3); }
.form-row__label { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-base); color: var(--ink-2); white-space: nowrap; overflow: hidden; }
.form-row__label::after { content: ""; flex: 1; border-bottom: 1px dotted var(--label-leader); transform: translateY(-3px); }

/* ---------- CARDS ---------- */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); box-shadow: var(--shadow-xs); }
.card__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
.card__title { margin: 0; font-size: var(--text-md); font-weight: var(--fw-semibold); color: var(--ink); }
.card__sub { margin: 2px 0 0; font-size: var(--text-sm); color: var(--ink-3); }
.card__body { padding: var(--space-4); }
.card__foot { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--r) var(--r); }
/* Collapsible: the card's head is a <summary>, so a reference panel can be
   folded away. The handoff's cards are always open, which is right for a card
   showing a result and wrong for one holding forty rows of lookup table. */
.card--collapsible > summary { list-style: none; cursor: pointer; user-select: none; }
.card--collapsible > summary::-webkit-details-marker { display: none; }
.card--collapsible > summary:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }
.card--collapsible > summary::after { content: ""; flex: none; width: 8px; height: 8px; border-right: 1.5px solid var(--ink-4); border-bottom: 1.5px solid var(--ink-4); transform: rotate(45deg) translate(-2px, -2px); transition: transform var(--transition-fast); }
.card--collapsible[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.card--collapsible:not([open]) > summary { border-bottom: 0; }

/* Danger zone: a card whose actions destroy or overwrite something. The border
   carries the warning so the heading does not have to shout it. */
.card--danger { border-color: var(--danger); }
.card--danger > .card__head { border-bottom-color: var(--danger); }
.card--danger > .card__head .card__title { color: var(--danger-text); }

/* A card whose content is itself a framed container. `.data-table`,
   `.sub-rows` and `.run-list` each carry their own surface, border and radius -
   they are containers, not contents - so putting one inside a padded
   `.card__body` draws a box inside a box, which is why the pipeline detail page
   originally titled them with a bare heading instead and skipped the card.

   Rendered next to real cards that was worse: the maintainer read four sections
   on one page with three different heading treatments as unconsidered, and it
   is. This is the third option neither had - the card frames the section, the
   container gives its own frame up, and one border is drawn where there were
   nearly two. Their background is already `--surface`, the same as the card's,
   so nothing has to be redrawn at the corners; only the border and radius go.

   No `overflow: hidden` here on purpose. That is what would clip a `.ra__menu`
   escaping a row, which divergence 6 exists to prevent - and a table inside a
   card is exactly where a row kebab turns up. */
.card--flush > .data-table,
.card--flush > .sub-rows,
.card--flush > .run-list { border: 0; box-shadow: none; background: transparent; border-radius: 0; }
.card--flush > .data-table thead th:first-child,
.card--flush > .data-table thead th:last-child { border-radius: 0; }
/* Give the card's bottom corners back to whatever sits in them. Without this
   the last row paints a square corner over the card's rounded one - and with
   `.data-table--edge` it is the 4px status keyline doing it, which is 4px of
   saturated colour cutting the corner off square. `.sub-rows` and `.run-list`
   clip their own children (they carry `overflow: hidden`), so the radius on
   the container is enough; a `.data-table` is `border-collapse: separate`, so
   the corner has to go on the cells themselves. */
.card--flush > :last-child { border-bottom-left-radius: var(--r); border-bottom-right-radius: var(--r); }
.card--flush > .data-table:last-child tbody tr:last-child td:first-child { border-bottom-left-radius: var(--r); }
.card--flush > .data-table:last-child tbody tr:last-child td:last-child { border-bottom-right-radius: var(--r); }

/* selectable card (maps to .module-card) */
.module-card { position: relative; display: grid; gap: var(--space-1); padding: var(--space-3) var(--space-4); padding-right: var(--space-7); text-align: left; width: 100%; background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--r-control); cursor: pointer; transition: background var(--transition-fast), border-color var(--transition-fast); }
.module-card__title { font-size: var(--text-base); font-weight: var(--fw-semibold); color: var(--ink); }
.module-card__text { font-size: var(--text-sm); color: var(--ink-3); }
.module-card__check { position: absolute; top: var(--space-3); right: var(--space-3); width: 16px; height: 16px; display: grid; place-items: center; border: 1px solid var(--input-border); border-radius: var(--r-control); color: transparent; }
.module-card__check svg { width: 12px; height: 12px; }
.module-card:hover, .module-card.is-hover { border-color: var(--primary); background: var(--surface-2); }
.module-card:focus-visible, .module-card.is-focus { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.module-card.is-selected { background: var(--primary-weak); border-color: var(--primary); }
.module-card.is-selected .module-card__check { background: var(--primary-strong); border-color: var(--primary-strong); color: var(--on-primary-strong); }
.module-card.is-disabled { background: var(--surface-sunken); border-color: var(--border); color: var(--ink-4); cursor: not-allowed; }
.module-card.is-disabled .module-card__title, .module-card.is-disabled .module-card__text { color: var(--ink-4); }
/* Ours is a <label> wrapping a real checkbox, because a form has to POST its
   selections and a toggle has to be reachable by keyboard. The handoff's is a
   <button role="checkbox">, which would post nothing - so hide the input rather
   than drop it, and move the focus ring onto the card that stands in for it. */
.module-card input[type="checkbox"], .module-card input[type="radio"] { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.module-card:focus-within { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
/* Marks a card whose CONTENT is curated elsewhere: the tick opts in or out,
   but the thing itself is edited on another page. Sits to the LEFT of the tick
   on the same line -- these cards are often two lines tall, so stacking it
   under the tick put the two glyphs on top of each other. */
.module-card__lock { position: absolute; top: var(--space-3); right: calc(var(--space-3) + 22px); display: inline-flex; color: var(--ink-4); }
.module-card__lock svg { width: 12px; height: 12px; }
.module-card:has(.module-card__lock) { padding-right: calc(var(--space-7) + 20px); }

/* ---------- TABS ---------- */
.pill-tabs { display: inline-flex; gap: var(--space-1); padding: 3px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--r-control); }
/* inline-flex rather than the handoff's default box: our pills carry a leading
   icon (they are route navigation as often as a toggle), and a plain inline
   button neither centres its label in the fixed height nor spaces the glyph. */
.pill-tab { display: inline-flex; align-items: center; gap: var(--space-1); height: var(--control-h-sm); padding: 0 var(--space-3); background: none; border: 1px solid transparent; border-radius: var(--r-control); color: var(--ink-3); font-family: var(--font-sans); font-size: var(--text-sm); font-weight: var(--fw-medium); cursor: pointer; transition: background var(--transition-fast), color var(--transition-fast); }
.pill-tab svg { width: 14px; height: 14px; flex: none; }
.pill-tab:hover, .pill-tab.is-hover { color: var(--ink); background: var(--surface); }
.pill-tab:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.pill-tab.is-active { background: var(--surface); border-color: var(--border-strong); color: var(--primary-ink); box-shadow: var(--shadow-xs); }
.pill-tab[disabled] { color: var(--ink-4); cursor: not-allowed; background: none; }
/* No margin: .pill-tab is a flex row and its gap already spaces the count. */
/* Nudged down a pixel. The count is 11px mono beside a 13px sans label and the
   tab centres their BOXES, which is not the same as aligning their baselines:
   the mono face's tall ascent lifts the digits, and "Microsoft 6" reads as
   Microsoft-to-the-sixth. Line-height does not move it — the fonts differ, so
   only an optical correction does. */
.pill-tab__count { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--ink-4); position: relative; top: 1px; }
/* Route-navigation variant: the pill is an <a> rather than a <button>. */
a.pill-tab, a.pill-tab:hover { text-decoration: none; }
/* Equal-width row: every tab takes the same share so labels of different
   lengths do not give the tabs different widths. */
.pill-tabs--equal { display: flex; }
.pill-tabs--equal .pill-tab { flex: 1 1 0; justify-content: center; }

.header-tabs { display: flex; gap: var(--space-4); border-bottom: 1px solid var(--border); }
.header-tab { position: relative; height: 38px; padding: 0 2px; background: none; border: 0; color: var(--ink-3); font-family: var(--font-sans); font-size: var(--text-base); font-weight: var(--fw-medium); cursor: pointer; }
.header-tab::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: transparent; }
.header-tab:hover, .header-tab.is-hover { color: var(--ink); }
.header-tab:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.header-tab.is-active { color: var(--primary-ink); }
.header-tab.is-active::after { background: var(--primary); }
.header-tab[disabled] { color: var(--ink-4); cursor: not-allowed; }
/* Route-navigation variant, matching the one .pill-tab already has: the tab is
   an <a> so the sub-page stays bookmarkable. A <button> is a flex container at
   38px tall and centres its own label; an <a> is inline and would sit its text
   on the baseline, so it has to opt in. */
a.header-tab { display: inline-flex; align-items: center; }
a.header-tab, a.header-tab:hover { text-decoration: none; }

/* ---------- STATUS PILLS / BADGES ----------
   One status language across the system: status is always carried by a coloured
   KEYLINE in a --bar-* colour. Rows take it on the right edge (.data-table--edge);
   pills take it on the left. So the pill is a squared tag with a 3px left keyline
   and a tinted fill - not a rounded outlined lozenge, which was the only fully
   round, fully outlined object in an otherwise square BC-native UI.
   Pills are for cards, detail headers and inline text; never for table rows.     */
.status-pill {
    display: inline-flex; align-items: center; gap: var(--space-1);
    height: 20px; padding: 0 var(--space-2);
    background: var(--surface-sunken);
    border: 0; border-left: 3px solid var(--bar-queued);
    border-radius: 0 var(--r-badge) var(--r-badge) 0;
    color: var(--ink-2); font-size: var(--text-xs); font-weight: var(--fw-medium);
    white-space: nowrap;
}
.status-pill svg { width: 12px; height: 12px; flex: none; }
/* The dot is now optional emphasis, not the status carrier - kept for --live. */
.status-pill__dot { display: none; }
.status-pill--live .status-pill__dot,
.status-pill--running .status-pill__dot { display: block; width: 6px; height: 6px; flex: none; border-radius: var(--r-pill); background: currentColor; animation: al-pulse 1.6s var(--ease) infinite; }
.status-pill--running .status-pill__dot { animation-duration: 1.1s; }
@keyframes al-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

.status-pill--muted   { background: var(--surface-sunken); border-left-color: var(--bar-queued);       color: var(--ink-3); }
.status-pill--success { background: var(--success-bg);      border-left-color: var(--bar-succeeded);   color: var(--success-text); }
.status-pill--warn    { background: var(--warning-bg);      border-left-color: var(--bar-draft);       color: var(--warning-text); }
.status-pill--danger  { background: var(--danger-bg);       border-left-color: var(--bar-failed);      color: var(--danger-text); }
.status-pill--info    { background: var(--info-bg);         border-left-color: var(--bar-running);     color: var(--info-text); }
/* XLIFF */
.status-pill--untrans { background: var(--st-untrans-bg);   border-left-color: var(--bar-untranslated); color: var(--st-untrans-text); }
.status-pill--fuzzy   { background: var(--st-fuzzy-bg);     border-left-color: var(--bar-fuzzy);       color: var(--st-fuzzy-text); }
.status-pill--trans   { background: var(--st-trans-bg);     border-left-color: var(--bar-translated);  color: var(--st-trans-text); }
.status-pill--final   { background: var(--st-final-bg);     border-left-color: var(--bar-final);       color: var(--st-final-text); }
/* build / delivery */
.status-pill--queued    { background: var(--surface-sunken); border-left-color: var(--bar-queued);     color: var(--ink-3); }
.status-pill--running   { background: var(--info-bg);        border-left-color: var(--bar-running);    color: var(--info-text); }
.status-pill--succeeded { background: var(--success-bg);     border-left-color: var(--bar-succeeded);  color: var(--success-text); }
.status-pill--failed    { background: var(--danger-bg);      border-left-color: var(--bar-failed);     color: var(--danger-text); }

/* Badges are counts, not statuses: square, quiet, no keyline. */
/* Carried over from the app: spacing utility for a pill that trails inline text. */
.status-pill--inline { margin-left: var(--space-1); }

/* ---------- FRESHNESS ----------
   For a page whose data is read from an outside system and cached: says when
   the copy was taken and offers the only Refresh. Exactly one per page, above
   the content it describes - repeating it per section is what made the old
   environment panel unreadable. --stale escalates past the age at which a
   consultant should stop acting on the numbers; .is-refreshing is the in-flight
   state, and the button carries .btn--loading with it. */
.freshness { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; padding: var(--space-2) var(--space-3); background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--border-strong); border-radius: 0 var(--r) var(--r) 0; }
.freshness__icon { width: 15px; height: 15px; flex: none; color: var(--ink-3); }
.freshness__text { font-size: var(--text-sm); color: var(--ink-2); }
.freshness__age { font-weight: var(--fw-medium); color: var(--ink); font-variant-numeric: tabular-nums; }
.freshness__sep { width: 1px; height: 14px; background: var(--border-strong); flex: none; }
.freshness__note { font-size: var(--text-sm); color: var(--ink-3); }
.freshness__spacer { flex: 1; min-width: var(--space-3); }
.freshness--stale { background: var(--warning-bg); border-color: var(--warning); border-left-color: var(--warning); }
.freshness--stale .freshness__icon, .freshness--stale .freshness__text, .freshness--stale .freshness__age, .freshness--stale .freshness__note { color: var(--warning-text); }
.freshness--stale .freshness__sep { background: var(--warning); }
.freshness.is-refreshing .freshness__text, .freshness.is-refreshing .freshness__age { color: var(--ink-3); }

/* ---------- COMMAND BAR (actionable list) ----------
   A persistent bar above a table whose actions run across selected rows -
   Business Central's ribbon, rebuilt on this token contract rather than its
   styling. One row, read left to right the way the work goes: narrow the list
   (filters), find a row (search), then act on what is ticked (commands). It
   composes .btn, .ra/.menu, .search and .select-wrap; it owns no button of its
   own, and selection-dependent commands are plain disabled buttons, as they are
   in BC. Sticky, because the point of a bulk action is to still be reachable
   after scrolling to the last row you wanted. */
.cmdbar { position: sticky; top: var(--sticky-head, 0px); z-index: 6; display: grid; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); box-shadow: var(--shadow-xs); }
/* The scroll container has top padding, and a pinned bar only paints itself -
   rows would scroll through the gutter above it. Same trick as
   .page-head--sticky: carry enough background to cover that gutter, here as a
   pseudo-element so the bar's own box and resting position are untouched. */
.cmdbar::before { content: ""; position: absolute; left: -1px; right: -1px; bottom: 100%; height: var(--space-5); background: var(--bg); }
.cmdbar__row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; padding: var(--space-2) var(--space-3); min-height: 46px; }
.cmdbar__group { display: flex; align-items: center; gap: var(--space-1); }
/* The trailing command group is pushed right by an auto margin rather than a
   spacer element. Flex decides line breaks from hypothetical sizes before any
   shrink applies, so every extra item on the row - a spacer, a separator and
   their gaps - is width the bar has to find before it will stay on one line. */
.cmdbar__row > .cmdbar__group:last-child { margin-left: auto; }
.cmdbar__note { font-size: var(--text-2xs); color: var(--ink-4); }
/* Basis, not grow: the spacer is the only thing that should absorb slack, so
   the search takes its width from flex-basis and merely shrinks on narrow rows.
   With flex-basis 0 the two competed for the same free space and the search
   stalled on its min-width with the placeholder clipped. */
.cmdbar__search { flex: 0 1 300px; min-width: 180px; }

/* Two-line table cell: a value over its qualifier - "Production" over
   "Sandbox", "28.4" over its date. Stacked rather than inline because inline
   the second part starts at a different x on every row, and the eye cannot
   scan a column whose secondary text keeps moving. */
.cell-stack { display: grid; gap: 1px; min-width: 0; padding: var(--space-1) 0; }
.cell-stack__main { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cell-stack__sub { font-size: var(--text-sm); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.badge { display: inline-flex; align-items: center; min-width: 18px; height: 18px; padding: 0 5px; justify-content: center; border-radius: var(--r-badge); background: var(--primary-weak); color: var(--primary-ink); font-family: var(--font-mono); font-size: var(--text-2xs); font-weight: var(--fw-medium); }
.badge--solid { background: var(--primary-strong); color: var(--on-primary-strong); }
.badge--danger { background: var(--danger-bg); color: var(--danger-text); }

/* BC-style CUE tiles: solid fill, big number, rule, drill-down chevron.
   Attention is expressed by the FILL (red) rather than a warning pill, exactly
   like the BC activity indicators. .cue is a link; the whole tile is the target. */
/* Column count is chosen from the GRID'S OWN width, not the viewport: these tiles
   are often inside a half-width panel or a scaled shell frame, where a viewport
   media query would force six columns into a few hundred pixels. Counts are fixed
   (6 / 3 / 2) rather than auto-fit so a row never strands one tile beside empties.
   Requires an inline-size container ancestor; without one the 3-column base holds. */
.cue-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
@container (min-width: 620px) { .cue-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container (min-width: 1120px) { .cue-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
.cue {
    position: relative; display: grid; grid-template-rows: auto 1fr auto auto; gap: var(--space-2);
    min-height: 132px; padding: var(--space-3) var(--space-4) var(--space-2);
    background: var(--cue-bg); color: var(--cue-ink);
    border: 1px solid transparent; border-radius: var(--r);
    text-decoration: none; box-shadow: var(--shadow-xs);
    transition: background var(--transition-fast), box-shadow var(--transition-fast);
}
.cue__label { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--cue-ink); text-wrap: pretty; }
.cue__value { align-self: end; font-size: var(--text-3xl); font-weight: var(--fw-regular); line-height: 1; color: var(--cue-ink); font-variant-numeric: tabular-nums; }
.cue__rule { height: 2px; background: var(--cue-rule); opacity: .5; }
.cue__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); min-height: 18px; font-size: var(--text-2xs); color: var(--cue-ink-dim); }
.cue__drill { width: 15px; height: 15px; flex: none; opacity: .85; }
.cue:hover, .cue.is-hover { background: var(--cue-bg-hover); box-shadow: var(--shadow-sm); }
.cue:hover .cue__rule { opacity: 1; }
.cue:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.cue--attention { background: var(--cue-attn-bg); }
.cue--attention:hover { background: var(--cue-attn-bg-hover); }
.cue--attention .cue__rule { opacity: 1; height: 3px; }

/* ---------- DATA TABLE ---------- */
/* No `overflow: hidden` here, deliberately. It was only clipping the header fill
   to the table's own 2px radius, and it also clipped the row-actions menu this
   sheet puts INSIDE the table (.data-table__actions holding a .ra kebab) — the
   last rows of any table lost the bottom of their menu. Rounding the header
   cells directly buys the same corners and lets the popup escape. */
.data-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); font-size: var(--text-base); }
.data-table thead th:first-child { border-top-left-radius: var(--r); }
.data-table thead th:last-child { border-top-right-radius: var(--r); }
.data-table th, .data-table td { padding: 0 var(--cell-pad-x); height: var(--row-h); text-align: left; color: var(--ink-2); border-bottom: 1px solid var(--border); }
.data-table thead th { height: var(--row-h-head); background: var(--surface-sunken); color: var(--ink-3); font-size: var(--text-xs); font-weight: var(--fw-semibold); text-transform: uppercase; letter-spacing: .05em; border-bottom: 1px solid var(--border-strong); white-space: nowrap; }
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr:hover td, .data-table tbody tr.is-hover td { background: var(--surface-2); }
.data-table tbody tr.is-selected td { background: var(--primary-weak); }
.data-table tbody tr.is-selected td:first-child { box-shadow: inset 2px 0 0 var(--primary); }
.data-table--zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
.data-table--zebra tbody tr:hover td { background: var(--surface-sunken); }
/* Scoped to `.data-table` so these four beat `.data-table th, .data-table td`
   above, which sets `text-align: left` at (0,1,1) — a bare modifier is (0,1,0)
   and loses, so none of them had ever taken effect. Two classes is (0,2,0),
   which wins on the class count regardless of the element in the other rule. */
.data-table .data-table__num { text-align: right; font-variant-numeric: tabular-nums; }
.data-table .data-table__actions { width: 1%; text-align: right; white-space: nowrap; }
.data-table__sort { display: inline-flex; align-items: center; gap: var(--space-1); background: none; border: 0; padding: 0; color: inherit; font: inherit; text-transform: inherit; letter-spacing: inherit; cursor: pointer; }
.data-table__sort svg { width: 12px; height: 12px; opacity: .35; }
.data-table__sort:hover { color: var(--ink); }
.data-table__sort:hover svg { opacity: .7; }
.data-table__sort:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }
.data-table__sort.is-sorted { color: var(--primary-ink); }
.data-table__sort.is-sorted svg { opacity: 1; color: var(--primary); }
.data-table td .code { font-size: var(--text-sm); }
/* Row status as a 4px right edge bar - the table equivalent of .run-row--*.
   Use INSTEAD of a status pill column when the table is a run/delivery history;
   keep the pill when status is one attribute among many. Always keep the state
   in text or aria-label too - colour is never the only signal. */
.data-table--edge tbody td:last-child { border-right: 4px solid transparent; }
.data-table tr.is-succeeded td:last-child { border-right-color: var(--bar-succeeded); }
.data-table tr.is-failed td:last-child    { border-right-color: var(--bar-failed); }
.data-table tr.is-running td:last-child   { border-right-color: var(--bar-running); }
.data-table tr.is-queued td:last-child    { border-right-color: var(--bar-queued); }
.data-table tr.is-cancelled td:last-child { border-right-color: var(--bar-cancelled); }
.data-table tr.is-new td:last-child       { border-right-color: var(--bar-new); }
.data-table tr.is-modified td:last-child  { border-right-color: var(--bar-modified); }
.data-table tr.is-unchanged td:last-child { border-right-color: var(--bar-unchanged); }
.data-table tr.is-removed td:last-child   { border-right-color: var(--bar-removed); }
.data-table tr.is-untranslated td:last-child { border-right-color: var(--bar-untranslated); }
.data-table tr.is-fuzzy td:last-child        { border-right-color: var(--bar-fuzzy); }
.data-table tr.is-translated td:last-child   { border-right-color: var(--bar-translated); }
.data-table tr.is-final td:last-child        { border-right-color: var(--bar-final); }
.data-table tr.is-published td:last-child { border-right-color: var(--bar-published); }
.data-table tr.is-draft td:last-child     { border-right-color: var(--bar-draft); }
.data-table tr.is-archived td:last-child  { border-right-color: var(--bar-archived); }
.data-table__state { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); font-weight: var(--fw-medium); color: var(--ink-3); white-space: nowrap; }
.data-table__state svg { width: 13px; height: 13px; flex: none; }
/* Icon-only variant: the edge bar carries the status, the glyph disambiguates it
   without colour, and the accessible name lives in aria-label + title. */
.data-table__state--icon { gap: 0; }
.data-table__state--icon svg { width: 15px; height: 15px; }
.data-table__state--spin svg { animation: al-spin 1.1s linear infinite; }
.data-table .data-table__col-state { width: 1%; text-align: center; }
.data-table__col-state .data-table__state { justify-content: center; }
/* Leading select-the-row column. Same shrink-to-content sizing as the state
   column, but it holds a .check rather than a glyph. */
.data-table .data-table__col-check { width: 1%; text-align: center; }
.data-table__col-check .check { vertical-align: middle; }
/* A row that opens an editor underneath its parent - rename, change email. It
   is a continuation of the row above, so it drops the zebra and hover. */
.data-table tr.is-subrow td { background: var(--surface-sunken); padding-top: var(--space-2); padding-bottom: var(--space-2); height: auto; }
.data-table tr.is-subrow:hover td { background: var(--surface-sunken); }
/* .input is width: 100%, so in the editor's flex row it would take the whole
   cell and squeeze the buttons below their labels. */
.data-table tr.is-subrow .input { max-width: 280px; }
.data-table tr.is-subrow .btn { flex: none; }
/* Several small actions instead of the handoff's single kebab: keep them
   right-aligned the way the plain cell is. */
.data-table__actions .u-row { justify-content: flex-end; }
tr.is-succeeded .data-table__state { color: var(--success-text); }
tr.is-failed .data-table__state    { color: var(--danger-text); }
tr.is-running .data-table__state   { color: var(--info-text); }
tr.is-published .data-table__state { color: var(--success-text); }
tr.is-draft .data-table__state     { color: var(--warning-text); }
tr.is-new .data-table__state      { color: var(--success-text); }
tr.is-modified .data-table__state { color: var(--warning-text); }
tr.is-removed .data-table__state  { color: var(--danger-text); }
tr.is-untranslated .data-table__state { color: var(--st-untrans-text); }
tr.is-fuzzy .data-table__state        { color: var(--st-fuzzy-text); }
tr.is-translated .data-table__state   { color: var(--st-trans-text); }
tr.is-final .data-table__state        { color: var(--st-final-text); }

.data-table--compact { font-size: var(--text-sm); }
.data-table--compact th, .data-table--compact td { height: 28px; padding: 0 var(--space-2); }
.data-table--compact thead th { height: 26px; }

/* ---------- OVERLAYS ---------- */
.modal-backdrop { position: absolute; inset: 0; background: color-mix(in oklab, var(--ink) 45%, transparent); border-radius: var(--r); }
.confirm-dialog { position: relative; width: min(420px, 100%); background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--r); box-shadow: var(--shadow-lg); }
.confirm-dialog__head { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-4) var(--space-4) 0; }
.confirm-dialog__icon { width: 32px; height: 32px; flex: none; display: grid; place-items: center; border-radius: var(--r-control); background: var(--primary-weak); color: var(--primary-ink); }
.confirm-dialog--danger .confirm-dialog__icon { background: var(--danger-bg); color: var(--danger-text); }
.confirm-dialog__icon svg { width: 17px; height: 17px; }
.confirm-dialog__title { margin: 0; font-size: var(--text-md); font-weight: var(--fw-semibold); color: var(--ink); }
.confirm-dialog__body { padding: var(--space-2) var(--space-4) var(--space-4); font-size: var(--text-sm); color: var(--ink-2); }
.confirm-dialog__body > p { margin: 0 0 var(--space-3); }
.confirm-dialog__body > p:last-child { margin-bottom: 0; }
.confirm-dialog__actions { display: flex; justify-content: flex-end; gap: var(--space-2); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--r) var(--r); }
/* Wider panel for a dialog that carries a list or picker rather than a yes/no
   prompt - an extension checklist, a release picker. Same chrome, more room. */
.confirm-dialog--wide { width: min(560px, 100%); }

/* The viewport layer a dialog is centred in. The system's own screens have no
   counterpart: the prototype demonstrates .confirm-dialog inside a review frame
   (.overlay-demo), so .modal-backdrop is absolute and rounded to that frame. A
   real app needs a fixed, full-cover parent for the backdrop to resolve
   against, and a full-viewport backdrop must not round its corners. */
.modal-layer { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: var(--space-5); }
.modal-layer > .modal-backdrop { border-radius: 0; }

.tip { position: relative; display: inline-flex; }
.tip__bubble { position: absolute; bottom: calc(100% + var(--space-2)); left: 50%; transform: translateX(-50%); padding: var(--space-1) var(--space-2); background: var(--ink); color: var(--surface); border-radius: var(--r-control); font-size: var(--text-xs); white-space: nowrap; box-shadow: var(--shadow-sm); opacity: 0; pointer-events: none; transition: opacity var(--transition-fast); }
.tip:hover .tip__bubble, .tip:focus-within .tip__bubble, .tip.is-shown .tip__bubble { opacity: 1; }

.toast { display: flex; align-items: flex-start; gap: var(--space-3); width: min(400px, 100%); padding: var(--space-3); background: var(--surface); border: 1px solid var(--border-strong); border-left: 3px solid var(--primary); border-radius: var(--r-control); box-shadow: var(--shadow); }
.toast__icon { width: 17px; height: 17px; flex: none; color: var(--primary-ink); margin-top: 1px; }
.toast__body { flex: 1; display: grid; gap: 2px; }
.toast__title { font-size: var(--text-base); font-weight: var(--fw-semibold); color: var(--ink); }
.toast__text { font-size: var(--text-sm); color: var(--ink-3); }
.toast--success { border-left-color: var(--success); } .toast--success .toast__icon { color: var(--success-text); }
.toast--danger  { border-left-color: var(--danger);  } .toast--danger .toast__icon  { color: var(--danger-text); }
.toast--warn    { border-left-color: var(--warning); } .toast--warn .toast__icon    { color: var(--warning-text); }
.toast.is-dismissed { display: none; }

/* inline alert */
.alert { display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--r-control); background: var(--surface-sunken); font-size: var(--text-sm); color: var(--ink-2); }
.alert svg { width: 16px; height: 16px; flex: none; margin-top: 1px; }
.alert--danger { background: var(--danger-bg); border-color: var(--danger); color: var(--danger-text); }
.alert--warn { background: var(--warning-bg); border-color: var(--warning); color: var(--warning-text); }
.alert--info { background: var(--info-bg); border-color: var(--info); color: var(--info-text); }
.alert--success { background: var(--success-bg); border-color: var(--success); color: var(--success-text); }

/* ---------- CROSS-CUTTING STATES ---------- */
.spinner { width: 16px; height: 16px; flex: none; border-radius: var(--r-pill); border: 2px solid var(--border-strong); border-top-color: var(--primary); animation: al-spin 700ms linear infinite; }
.spinner--lg { width: 26px; height: 26px; border-width: 3px; }
.loading-block { display: grid; justify-items: center; gap: var(--space-3); padding: var(--space-8) var(--space-4); color: var(--ink-3); font-size: var(--text-sm); }
/* Additive: the list archetype pairs a skeleton table with a caption underneath,
   which needs the block's own padding trimmed. PageList.dc.html does this with an
   inline style; a modifier keeps it out of every list page's markup. */
.loading-block--under-table { padding: var(--space-3) 0 0; }
/* Skeleton: solid fill plus a highlight swept with transform, so the animation is
   composited. Animating background-position on a gradient repaints every frame and
   does not scale - a loading table holds dozens of these at once. */
.skeleton { position: relative; height: 12px; border-radius: var(--r-control); background: var(--surface-sunken); overflow: hidden; }
.skeleton::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, var(--surface-2), transparent); transform: translateX(-100%); animation: al-shimmer 1.4s linear infinite; will-change: transform; }
@keyframes al-shimmer { to { transform: translateX(100%); } }
.skeleton-rows { display: grid; gap: var(--space-3); padding: var(--space-4); }

/* Determinate <progress>. The system drew one only as .job-list__progress, a
   name BEM-scoped to the job list -- so an upload bar in an alert, or any other
   surface wanting the same 8px track, had to either borrow a job-list class or
   write its own. Same values, no block in the name; the job list composes it.
   The value transition is what makes a percentage bar read as progress rather
   than as a series of jumps. */
.progress { width: 100%; height: 8px; appearance: none; border: 0; border-radius: var(--r-pill); overflow: hidden; background: var(--surface-sunken); }
.progress::-webkit-progress-bar { background: var(--surface-sunken); border-radius: var(--r-pill); }
.progress::-webkit-progress-value { background: var(--primary); border-radius: var(--r-pill); transition: width var(--transition-fast); }
.progress::-moz-progress-bar { background: var(--primary); border-radius: var(--r-pill); }
/* A <progress> with no value attribute is ALREADY indeterminate, so there is no
   modifier here to keep in sync -- script drops the attribute and the sweep
   starts. Without this the appearance:none track just sits empty, which reads
   as "0%, stuck" rather than "working". The webkit bar has to go transparent or
   it paints over the gradient. Unlike .skeleton this animates
   background-position rather than a transform: a replaced element has no
   pseudo-element to slide, and a page carries one of these, not dozens. */
.progress:indeterminate { background-image: linear-gradient(90deg, transparent, var(--primary), transparent); background-size: 40% 100%; background-repeat: no-repeat; animation: al-progress-sweep 1.3s var(--ease) infinite alternate; }
.progress:indeterminate::-webkit-progress-bar { background: transparent; }
/* 0% to 100% BACKGROUND-position keeps the gradient inside the track at both
   ends rather than travelling past them, and `alternate` turns the return trip
   into the other half of the cycle. A sweep that runs off the end leaves the
   bar completely empty for part of every loop, which on an 8px track reads as
   stalled - the one thing an indeterminate bar exists to deny. */
@keyframes al-progress-sweep { from { background-position: 0 0; } to { background-position: 100% 0; } }

.empty-state { display: grid; justify-items: center; gap: var(--space-2); padding: var(--space-8) var(--space-4); text-align: center; }
.empty-state__icon { width: 42px; height: 42px; display: grid; place-items: center; border-radius: var(--r-control); background: var(--primary-weak); color: var(--primary-ink); margin-bottom: var(--space-1); }
.empty-state__icon svg { width: 21px; height: 21px; }
.empty-state__title { font-size: var(--text-md); font-weight: var(--fw-semibold); color: var(--ink); }
.empty-state__text { max-width: 46ch; font-size: var(--text-sm); color: var(--ink-3); text-wrap: pretty; }
.empty-state__action { margin-top: var(--space-2); }
/* An empty state that IS the whole page - a 404, a "we couldn't find that" -
   wants its title to be the document's heading, so focus management and the
   outline have somewhere to land. The grid already spaces the rows. */
h1.empty-state__title { margin: 0; }
/* Quiet variant, for a SECONDARY section that is empty most of the time. The
   full treatment teaches a first-time user what a page is for, which is right
   once per page; three stacked on one page push the thing the user came for
   below the fold. One line, no icon, no duplicate call to action. */
.empty-state--quiet { padding: var(--space-5) var(--space-4); }
.empty-state--quiet .empty-state__icon { display: none; }
.empty-state--quiet .empty-state__title { font-size: var(--text-base); font-weight: var(--fw-regular); color: var(--ink-3); }

.error-block { display: grid; justify-items: center; gap: var(--space-2); padding: var(--space-7) var(--space-4); text-align: center; }
.error-block__icon { width: 42px; height: 42px; display: grid; place-items: center; border-radius: var(--r-control); background: var(--danger-bg); color: var(--danger-text); }
.error-block__icon svg { width: 21px; height: 21px; }
.error-block__title { font-size: var(--text-md); font-weight: var(--fw-semibold); color: var(--ink); }
.error-block__text { max-width: 52ch; font-size: var(--text-sm); color: var(--ink-3); }
.error-block__detail { margin-top: var(--space-1); font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--ink-4); }

/* ---------- PAGE CHROME ---------- */
.page-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); padding-bottom: var(--space-3); border-bottom: 1px solid var(--border); }
.page-head__crumbs { display: flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); color: var(--ink-3); margin-bottom: var(--space-1); }
.page-head__crumbs a { color: var(--ink-3); text-decoration: none; }
.page-head__crumbs a:hover { color: var(--primary-ink); text-decoration: underline; }
.page-head__crumbs svg { width: 12px; height: 12px; opacity: .6; }
.page-head__title { margin: 0; font-size: var(--text-xl); font-weight: var(--fw-semibold); color: var(--ink); letter-spacing: -.01em; }
.page-head__sub { margin: var(--space-1) 0 0; font-size: var(--text-sm); color: var(--ink-3); }
.page-head__actions { display: flex; align-items: center; gap: var(--space-2); flex: none; }
.section-label { font-size: var(--text-2xs); font-weight: var(--fw-medium); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-4); }

.stat-card { display: grid; gap: var(--space-1); padding: var(--space-3) var(--space-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); box-shadow: var(--shadow-xs); }
.stat-card__label { font-size: var(--text-2xs); font-weight: var(--fw-medium); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-4); }
.stat-card__value { font-size: var(--text-2xl); font-weight: var(--fw-semibold); color: var(--ink); line-height: var(--leading-tight); font-variant-numeric: tabular-nums; }
.stat-card__delta { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); font-weight: var(--fw-medium); color: var(--ink-3); }
.stat-card__delta svg { width: 13px; height: 13px; }
.stat-card__delta--up { color: var(--success-text); }
.stat-card__delta--down { color: var(--danger-text); }
.stat-card--accent { border-left: 3px solid var(--primary); }

/* ---------- TAG ----------
   A topic label: a keyword, a category, a subject. Shaped like .code and sized
   like it, but set in the UI face and free to wrap, because it is prose about
   the thing rather than a name from the thing. Reach for .code when the text is
   an identifier the user could type into an editor; reach for .tag when it is a
   word someone chose to describe it.
   As an <a> it becomes a filter link: same box, plus a hover and a focus ring
   so it reads as somewhere to go. Squared like everything else -- --r-pill is
   reserved for status pills, avatars and progress bars. */
.tag { display: inline-flex; align-items: center; font-size: var(--text-xs); padding: 1px 7px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--r-badge); color: var(--ink-3); }
a.tag { text-decoration: none; transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); }
a.tag:hover { background: var(--primary-weak); border-color: var(--primary-weak-strong); color: var(--primary-ink); }
a.tag:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-offset); }

/* ---------- CODE AFFORDANCES ---------- */
.code { font-family: var(--font-mono); font-size: var(--text-xs); padding: 1px 5px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--r-badge); color: var(--ink-2); white-space: nowrap; }
/* The static code block -- one of them, since #587. The design layer shipped
   two components one letter apart: .code-block here and .codeblock in
   pages-content.css, with different surfaces, different bar chrome, and two
   unrelated ways to tint the code inside. Nothing about their call sites
   explained the split -- both are "here is some code, read it" -- so a snippet
   changed appearance depending on which page you met it on.

   Merged onto this name and this sheet, because a code block is a component
   and pages-content.css is the page-archetype sheet. The bar chrome and the
   highlighting are the other one's, which were the better half.

   The SURFACE is this one's, though, and that was measured rather than
   assumed. --code-bg is the token named for a code canvas, which sounds like
   the right answer until you put numbers on it: in light it resolves to
   --surface, so the bar (--surface-2) comes out DARKER than the code, while in
   dark it resolves to --surface-sunken and the bar comes out lighter. The
   chrome/code relationship inverted between themes. --surface-sunken is the
   same value as --code-bg in dark, so nothing there changes, and in light it
   keeps the bar lighter than the code in both. It also keeps the block from
   vanishing: /templates/extension puts one on a white card, where --code-bg
   left it separated from its ground by a single border pixel.

   The bar keeps justify-content: space-between AND offers a __spacer, so both
   markup shapes work. Two children sit at the ends by themselves; a three-item
   bar (lang, name, button) puts the spacer between name and button, which eats
   the free space and leaves space-between nothing to distribute.

   No overflow: hidden, which .codeblock had. At --r: 2px it buys two pixels of
   corner fill, and this system has already lost a row-actions menu to exactly
   that property once (.data-table, divergence 6). */
.code-block { position: relative; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--r); }
.code-block__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); height: 30px; padding: 0 var(--space-2) 0 var(--space-3); background: var(--surface-2); border-bottom: 1px solid var(--border); }
.code-block__lang { font-family: var(--font-mono); font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: .07em; color: var(--ink-4); }
.code-block__name { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.code-block__spacer { flex: 1; }
.code-block pre { margin: 0; padding: var(--space-3); overflow-x: auto; font-family: var(--font-mono); font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--ink-2); }
/* The one highlight vocabulary for static code: the same six classes .codev
   uses in pages-power.css, so this merge did not have to invent a naming
   style. They replace `pre b` / `pre i` -- two elements can only ever express
   two kinds, which is what pushed the Cookbook's server-side highlighter into
   a third vocabulary of its own (#565). Emitted by
   Services/Cookbook/AlSyntaxHighlighter.cs; punctuation and plain text carry
   no class and inherit the pre's own colour. */
.code-block pre .k { color: var(--code-key); }
.code-block pre .t { color: var(--code-type); }
.code-block pre .n { color: var(--code-num); }
.code-block pre .s { color: var(--code-str); }
.code-block pre .c { color: var(--code-com); font-style: italic; }
.code-block pre .o { color: var(--code-obj); }
/* A block whose whole content is one short line -- a URL, a command. The bar
   folds into the row beside the code instead of sitting above it. */
.code-block--inlinebar { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-2); padding-right: var(--space-2); }
.code-block--inlinebar pre { padding: var(--space-2) var(--space-3); white-space: nowrap; }
/* copied confirmation: the button swaps label in place, no toast for something
   this small - the feedback belongs where the click happened. The fill comes
   from pages-content.css's copy of this rule, which said the same thing with
   one more property; #587 folded the two together. */
.copy-btn.is-copied { background: var(--success-bg); border-color: var(--success); color: var(--success-text); }

/* ---------- UTILITIES ---------- */
.u-row { display: flex; align-items: center; gap: var(--space-2); }
/* Vertical counterpart, at the page's own rhythm. Wanted whenever something
   that is not a .page has to stack .page-level blocks -- a <form> wrapping
   several cards, most often. */
.u-stack { display: grid; gap: var(--space-5); align-content: start; }
.u-between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.u-muted { color: var(--ink-3); }
.u-num { font-variant-numeric: tabular-nums; }
/* Additive: the sixth utility. The set has .u-row / .u-stack / .u-between /
   .u-muted / .u-num / .u-sr and no way to say "do not break this" - which a
   file name, a version or a `.app` beside a word needs more often than
   tabular numerals do. */
.u-nowrap { white-space: nowrap; }
.u-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

/* Link colour, at --primary-ink rather than the --primary-strong the hand-off
   shipped: measured in the running app, --primary-strong is 4.4:1 on --bg and
   4.26:1 on --primary-weak, both under AA. --primary-ink is 6.44 / 6.25.
   base.css still owns links for the app (it loads later and wins); this exists
   so the component layer is complete on its own — a spec sheet rendered against
   components.css alone should not have unstyled links. Retire the base.css copy
   with base.css itself. */
a { color: var(--primary-ink); }
a:hover { color: var(--primary-stronger); }
