/* ============================================================================
   Cordis Suite — component layer v2.0
   Shell profile (CRM, MachineControlServer, Portal, HMI).

   Requires tokens.css to be loaded FIRST. Nothing in this file contains a
   literal colour, font size, radius or duration: every value is a --cordis-*
   token, so a change to the contract changes every application at once.

   Studio (the Modeler desktop application) loads cordis-studio.css after
   this file. No other application may.
   ========================================================================= */

/* ---------- base ---------------------------------------------------------- */

/* ---------- the brand typeface (decision D6 — shipped) --------------------
   IBM Plex Sans and IBM Plex Mono, SIL OFL 1.1, latin subset. --cordis-font has
   asked for these since v2.0 of the tokens; until now nothing shipped them, so
   every application in the suite silently rendered in system-ui and the token
   described something untrue. These two faces are what make it true.

   Both are VARIABLE fonts: one file covers the whole 400–700 range, which is
   why there is no separate Medium or SemiBold file to keep in sync. Declare the
   range on the face and the browser interpolates --cordis-fw-medium / -semibold /
   -bold out of it.

   `swap` rather than the icon font's `block`: text in a fallback face for one
   frame is readable, where an icon ligature in a fallback face renders as the
   literal word `chevron_right`. Different failure, different answer.

   The url() is relative to THIS file, so the same declaration serves a non-C#
   consumer reading repo-root fonts/ and a Blazor consumer reading
   _content/Cordis.UI.Components/fonts/. Never add a second copy under any
   project's own wwwroot/. */
@font-face {
  font-family: 'IBM Plex Sans';
  src: url('../fonts/IBMPlexSans-latin.woff2') format('woff2-variations');
  font-weight: 400 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('../fonts/IBMPlexMono-latin.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}

/* The box model for the whole suite. This sheet is the entire global layer of the SHELL profile —
   tokens.css + cordis.css is the documented two-stylesheet install — so this copy is load-bearing
   for every non-Blazor consumer and must never be "deduped" away. The Blazor/Studio component
   layer declares the same rule again in src/Cordis.UI.Components/wwwroot/app.css, on purpose: see
   the long note there for why both copies stay and what a consumer's contract test pins (#402,
   #893). Two copies is the invariant; changing the number is a documented change, not a tidy-up. */
*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--cordis-bg);
  color: var(--cordis-text);
  font-family: var(--cordis-font);
  font-size: var(--cordis-fs-body);
  line-height: var(--cordis-lh-body);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--cordis-primary-text); text-decoration: none; }
a:hover { color: var(--cordis-primary-active); text-decoration: underline; }

/* One focus ring for the whole suite. Never remove it without replacing it.

   The `border-radius` this rule used to carry is GONE (user report, 2026-08), and its
   absence is the fix, not an omission. A box-shadow ring already follows the focused
   element's own border-radius, so forcing `--cordis-radius-sm` here only ever did damage:
   it was a focus rule reaching in and changing the focused element's GEOMETRY, so a pill
   focused square and any control whose radius is not `sm` could never agree with its own
   ring. Measured on CordisFilterBox before the fix: a 23px inner <input> with radius
   forced to 6px, ringed inside a 36px control.

   The other half of that report — the ring landing on the INNER element of a composite
   control, because a universal rule can only ring whatever actually holds focus — cannot
   be fixed here. Composite controls move it to the box the user perceives; see
   `.cordis-filter` / `.cordis-copyfield` in the Blazor library's app.css. This rule is
   correct for everything that is a single element.

   The ring is deliberately more visible than before, not less: --cordis-focus-ring is now a
   solid blue-700 (5.9:1 on white) where it was a 30%-alpha wash measuring ~1.4:1. */
:focus-visible { outline: none; box-shadow: var(--cordis-focus-ring); }

/* A heading or a landmark is focused for ANNOUNCEMENT, never for action. Blazor's
   <FocusOnNavigate Selector="h1" /> is the common case — it puts tabindex="-1" on the new
   route's heading and focuses it so a screen reader says where it has arrived — and the same
   pattern serves skip-links and a dialog's own title. A ring there tells a sighted user
   "you are here, press something", which is not true, and it appears on every navigation.

   Scoped to headings and landmarks rather than to [tabindex="-1"] on purpose: a roving-tabindex
   composite (a tree, a menu, a tablist) parks -1 on its inactive items and then MOVES focus to
   them with the arrow keys, so those genuinely need the ring. This is the narrow case where
   nothing is actionable. */
:is(h1, h2, h3, h4, h5, h6, main, [role="main"]):focus-visible { box-shadow: none; }

/* ---------- the button UA reset ------------------------------------------- */

/* A <button> that has to look like something the asset layer already styles — the label inside a
   view pill, the × on a filter chip, a page tab, a pager page, an editable cell at rest. The
   browser gives every one of them a grey fill, a bevelled border, its own font and centred text,
   and none of those come from a token.

   This is declared HERE, above every component rule, and that position is the whole design. It is a
   class, so a component rule of equal specificity that comes later wins on document order: .cordis-tabs__item
   keeps its border-bottom and its muted colour, .cordis-pagination__page.is-current keeps its blue fill,
   .cordis-viewpill keeps its own font size. The reset is a floor, not a ceiling.

   That is what an inline style could not do. The five components that each carried their own inline
   copy of these declarations had to omit, one by one, whatever their own class needed to keep —
   CordisPageTabs left out `color`, CordisPagination left out `background` for the current page — and a
   reader had to be told why in a comment each time. Nothing has to be omitted from a class. */
.cordis-btn-reset { appearance: none; background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: inherit; cursor: pointer; }

/* For the ones whose content is a glyph beside a label. Separate because a reset that also laid out
   its content could not serve a full-width editable cell, where inline-flex changes how text wraps. */
.cordis-btn-reset--row { display: inline-flex; align-items: center; gap: var(--cordis-space-2); }

.cordis-mono { font-family: var(--cordis-font-mono); font-variant-numeric: tabular-nums; }
.cordis-num  { font-family: var(--cordis-font-mono); font-variant-numeric: tabular-nums; }

/* ---------- typography ---------------------------------------------------- */

.cordis-display { font-size: var(--cordis-fs-display); line-height: var(--cordis-lh-display); font-weight: var(--cordis-fw-semibold); letter-spacing: -0.02em; color: var(--cordis-text-strong); margin: 0; }
.cordis-h1 { font-size: var(--cordis-fs-h1); line-height: var(--cordis-lh-h1); font-weight: var(--cordis-fw-semibold); letter-spacing: -0.01em; color: var(--cordis-text-strong); margin: 0; }
.cordis-h2 { font-size: var(--cordis-fs-h2); line-height: var(--cordis-lh-h2); font-weight: var(--cordis-fw-semibold); color: var(--cordis-text-strong); margin: 0; }
.cordis-h3 { font-size: var(--cordis-fs-h3); line-height: var(--cordis-lh-h3); font-weight: var(--cordis-fw-semibold); color: var(--cordis-text-strong); margin: 0; }
.cordis-sm { font-size: var(--cordis-fs-sm); line-height: var(--cordis-lh-sm); }
.cordis-xs { font-size: var(--cordis-fs-xs); line-height: var(--cordis-lh-xs); }
.cordis-caption {
  font-size: var(--cordis-fs-caption); font-weight: var(--cordis-fw-semibold);
  letter-spacing: var(--cordis-ls-caption); text-transform: uppercase; color: var(--cordis-text-muted);
}
.cordis-muted { color: var(--cordis-text-muted); }

/* Prose: authored long-form text in plain markup — a help topic, an explanation in a drawer. Caption subheads
   (h6), paragraphs and lists on the body rhythm, strong in the strong ink, nothing trailing below the last block.
   .cordis-prose__caution marks the sentence about an action that affects a running machine or cannot be undone. */
.cordis-prose { color: var(--cordis-text); font-size: var(--cordis-fs-body); line-height: 1.55; }
.cordis-prose h6 { margin: var(--cordis-space-4) 0 var(--cordis-space-2); font-size: var(--cordis-fs-caption);
  font-weight: var(--cordis-fw-semibold); letter-spacing: var(--cordis-ls-caption); text-transform: uppercase;
  color: var(--cordis-text-muted); }
.cordis-prose h6:first-child { margin-top: 0; }
.cordis-prose :is(p, ul, ol) { margin: 0 0 var(--cordis-space-3); }
.cordis-prose :is(ul, ol) { padding-left: var(--cordis-space-5); }
.cordis-prose li { margin-bottom: var(--cordis-space-2); }
.cordis-prose > :last-child { margin-bottom: 0; }
.cordis-prose strong { color: var(--cordis-text-strong); }
.cordis-prose__caution { color: var(--cordis-warning-text); }

/* ---------- icons --------------------------------------------------------- */

/* Material Symbols Rounded, subsetted — the ONE icon set for both profiles
   (decision D8; Bootstrap Icons is retired). An icon is named, not numbered: the
   glyph is a ligature, so the markup carries the meaning and reads in a diff.

       <i class="cordis-icon">chevron_right</i>

   The url() is relative to THIS file, and that is what lets one font file serve
   two very different consumers. A non-C# application loads css/cordis.css from
   the repo root, where ../fonts/ is the repo-root fonts/. The Blazor library
   serves this same file from _content/Cordis.UI.Components/css/, where ../fonts/
   is the same asset republished at _content/Cordis.UI.Components/fonts/. Never
   make this path absolute or root-relative — either one breaks a consumer.

   font-display: block, not swap. A swapped icon font paints the ligature's NAME
   as fallback text first, so the interface briefly reads "chevron_right" instead
   of showing nothing. Blocking is the lesser evil for a 170 kB subset.

   Two src entries with the same URL are not a mistake: browsers that understand
   the variable-font syntax take the first, older ones fall back to the second
   and get the default instance (FILL 0, wght 400), which is the common case. */
/* The `?v=` is load-bearing, and it is the one thing to remember when you add a glyph.
   The subset is regenerated separately from a build (tools/Subset-IconFont.ps1, or the
   `guard:icon-font` CI job's artifact), so the filename never changes while the bytes do — and a
   font is the most aggressively cached resource a browser holds. Without a version token, everyone
   who has ever loaded the page keeps the old subset, and a newly registered name goes on rendering
   as the first 22px of its own letters, clipped square, for as long as their cache lives. It looks
   like an icon, so nobody reports it.

   IT IS NO LONGER A DISCIPLINE. The `sha256` below is the digest of the exact bytes this token
   belongs to, and IconCacheTokenTests recomputes it from fonts/MaterialSymbolsRounded.woff2 on
   every `dotnet test`: change the font without bumping ?v= and the build fails, naming the new
   number and the new hash. So the pair moves together or not at all. Both `src` lines carry the
   same token deliberately, and the test requires that too — one bumped and one not is a browser
   serving itself two different fonts under one family.
   Nothing in CI rewrites this file. A second writer of one value is how things go stale silently
   (see TokenOwnershipTests), so the job PRINTS what to paste and the test refuses the mismatch.

   Bump on every regeneration. v2 = 2026-08, the nine names the specification page's screen sections
   needed (more_vert, block, stop, task_alt, science, rule, fork_right, key_off, handshake).
   v3 = 2026-09, thumb_up and thumb_down. v4 = 2026-09, `sensors` for Cordis Server — the first
   glyph added through the per-consumer icons/ registers (issue #29).

   sha256 = df8cc869f53ce84a6a72165ff7453ddf9ac75accae2516c9ee4d2f5930093512 */
@font-face {
  font-family: 'Material Symbols';
  src: url('../fonts/MaterialSymbolsRounded.woff2?v=22') format('woff2 supports variations'),
       url('../fonts/MaterialSymbolsRounded.woff2?v=22') format('woff2-variations');
  font-weight: 100 700; font-style: normal; font-display: block;
}

/* ONE glyph rule, and now ONE NAME. This selector used to read `.cds-ic, .cordis-icon`
   — the Shell spelling and the Studio/Blazor one, kept identical by hand. Decision D10
   collapsed the two families, and the icon was the easiest of the eighteen: the same
   declaration was already serving both, so all that changed is that there is one name
   to write. app.css adds only what is genuinely Studio-specific (the readiness flag).

   The knobs are Material Symbols' variation axes, exposed as custom properties so
   a component can set one without restating the rule. Each used to be read twice —
   once under a --cds-icon-* name and once under the legacy --cordis-icon-* one,
   because the two families spelled them differently. One family now, so one read.
   Note that wght is deliberately NOT in font-variation-settings: it is driven by
   font-weight above, and listing it here would silently win over that. */
.cordis-icon {
  font-family: var(--cordis-icon-font, 'Material Symbols');
  font-weight: var(--cordis-icon-weight, normal);
  font-style: normal;
  font-variation-settings:
    'FILL' var(--cordis-icon-fill, 0),
    'GRAD' var(--cordis-icon-grade, 0),
    'opsz' var(--cordis-icon-opsz, 40);
  /* 'liga' is what turns the text "chevron_right" into one glyph. Without it the
     letters render as letters. */
  font-feature-settings: 'liga';
  /* The remaining declarations all exist to stop inherited typography from
     corrupting the ligature: a letter-spacing, a text-transform or a line break
     inside the name breaks the substitution and the raw text shows through. */
  direction: ltr;
  text-transform: none;
  letter-spacing: normal;
  overflow-wrap: normal;
  white-space: nowrap;
  line-height: 1;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  user-select: none;
  -webkit-user-select: none;
  /* A square 1em box centring the glyph, so a row of icons and labels aligns on
     one baseline regardless of which glyph it is. overflow keeps an oversized
     mark from pushing its neighbours. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 1em;
  min-inline-size: 1em;
  overflow: hidden;
}

/* A DRAWN icon (CordisIcons.MarkFor) -- inline SVG inside the very same .cordis-icon box, so it
   inherits that box's size, colour and centring rather than growing a parallel set of rules. It fills
   the box; the mark's own square viewBox is what decides how much of it the shape occupies. The
   stroke follows `currentColor`, which is where the icon's Color parameter has already landed, so a
   mark tints exactly like a glyph does -- including the green a referenced state machine carries.
   The parent sets `overflow: hidden`, so the vector is clipped to the slot like a glyph is. */
.cordis-icon > .cordis-icon__mark {
  inline-size: 100%;
  block-size: 100%;
  fill: none;
}

/* Icon-font sizes are deliberate literals, not typography. A glyph has to sit on
   the text baseline at a whole pixel, so these do not follow the --cordis-fs-* scale.
   They are larger than the Bootstrap Icons values they replace (16/14/19) because
   Material Symbols draws on a 24-unit grid and leaves optical padding inside the
   em, so a 16px Material glyph reads noticeably smaller than a 16px Bootstrap one.
   20/18/24 restores the previous optical weight.

   The vertical-align is Shell-only, and only because the shared rule above is a
   flex box. Studio puts every glyph inside a flex row, where vertical-align is
   ignored; Shell markup drops icons straight into running text, where a centred
   1em box floats a fraction above the baseline the words sit on. The retired set
   nudged it down the same way (Bootstrap's own .bi carried -0.125em).

   Under one name these three rules now reach Studio's glyphs too, where they are wrong:
   a workbench icon takes its size from the 26px row it sits in, not from a Shell
   literal. cordis-studio.css hands it back to `inherit` — a profile difference stated
   in the profile's own file, which is the arrangement rule 5 asks for. */
.cordis-icon { font-size: 20px; vertical-align: -0.125em; }
.cordis-icon--sm { font-size: 18px; }
.cordis-icon--lg { font-size: 24px; }

/* Material Symbols has no separate *-fill glyph: fill is an axis. This is the
   Shell equivalent of Studio's .cordis-glyph-filled — what used to be a second
   icon name (bi-house vs bi-house-door-fill) is now one name plus this class. */
.cordis-icon--filled { --cordis-icon-fill: 1; }

/* ---------- buttons ------------------------------------------------------- */

/* One button family for the whole suite (decision D10). The Blazor CordisButton renders these very
   classes and adds nothing that overlaps them — what its scoped sheet still owns is only the shapes
   and contexts an asset-layer rule cannot see (the <label> form and the in-row 20px actions).

   The order inside this block is load-bearing. Geometry-only modifiers (--icon, --sm) come FIRST and
   the four looks come after, all at specificity 0-1-0, so document order settles any pair. That is
   what makes the modifiers compose in either direction a caller writes them:
     .cordis-btn                            filled, the default action
     .cordis-btn--icon                      square and transparent — the chrome / toolbar button
     .cordis-btn--primary .cordis-btn--icon square and FILLED (the look wins, being later)
   Put --icon after the looks instead and a filled icon button becomes unexpressible. */
.cordis-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--cordis-space-2);
  font-family: inherit; font-size: var(--cordis-control-fs); font-weight: var(--cordis-fw-semibold);
  height: var(--cordis-control-h); padding: 0 var(--cordis-control-px);
  border-radius: var(--cordis-radius-sm); border: 1px solid transparent;
  cursor: pointer; white-space: nowrap;
  transition: background var(--cordis-dur-fast) var(--cordis-ease),
              border-color var(--cordis-dur-fast) var(--cordis-ease),
              color var(--cordis-dur-fast) var(--cordis-ease),
              box-shadow var(--cordis-dur-fast) var(--cordis-ease-out),
              transform var(--cordis-dur-fast) var(--cordis-ease-out);
  background: var(--cordis-primary); color: var(--cordis-text-on-fill); border-color: var(--cordis-primary);
}
/* Every hover / active below carries :not(:disabled, .is-busy) — a button that cannot be pressed
   must not answer the pointer at all. It used to be expressed the other way round, as an unguarded
   hover plus a rule that RESTORED the base fill on a disabled button, and that restore was written
   with the filled default in mind: at 0-2-0 it also outranked `.cordis-btn--icon`'s transparent
   background at 0-1-0, so a disabled ICON button under the pointer took the brand fill and became the
   loudest control in the row at the one moment it did nothing. Seen on the Problems panel's Re-run
   check, which disables itself under the pointer that just clicked it and then spun a solid blue
   square. Guarding the hover instead of patching it afterwards is what makes the geometry modifiers
   keep their own background — and the guard is ONE `:not()` taking a selector list, not two chained
   ones, which is what `selector-not-notation` asks for and is also the only form that keeps the
   arithmetic right: a list's specificity is its heaviest argument (0-1-0 here), so the base hover
   stays at 0-3-0 and the `--icon` hover below it, at the same weight and later in the file, still
   wins. Chained, the base would have reached 0-4-0 and filled every ENABLED icon button instead. */
.cordis-btn:hover:not(:disabled, .is-busy)  { background: var(--cordis-primary-hover); border-color: var(--cordis-primary-hover); }
.cordis-btn:active:not(:disabled, .is-busy) { background: var(--cordis-primary-active); border-color: var(--cordis-primary-active); }
.cordis-btn[disabled], .cordis-btn:disabled, .cordis-btn.is-busy { opacity: 0.6; cursor: default; }
/* A press gives: 2%, for as long as the button is held, released at the same speed. Every look
   inherits it, because being pressed is a property of a button rather than of its colour. Studio
   turns it off (cordis-studio.css) — a workbench's tool buttons are pressed hundreds of times an
   hour, and there the colour step alone is the answer. The box-shadow transition above is the
   focus ring, which then grows in instead of appearing. */
.cordis-btn:active:not(:disabled, .is-busy) { transform: scale(0.98); }

/* Geometry modifiers. Square on the control height, so the icon button matches the field beside it.

   ONE icon button (D16, second pass). Studio's toolbar-row button (`.pv-toolbtn`, CordisToolButton) was
   this same box — control-height square, transparent, radius-sm — with a different resting colour and
   hover, and it is gone: a toolbar row, an editor band and a panel header now all draw this rule.

   RESTING COLOUR: the muted glyph, --cordis-text on hover. Both earlier answers were in use side by side
   and that was the problem: the toolbar-row button and a dense header's tools were muted, the plain icon
   button inherited its surroundings' (usually strong) text colour, so a toolbar row under a header read
   one step quieter than the header above it. The muted glyph is the one the majority already had, and it
   is what an icon button is in a bar of them — chrome, read when looked for. A toned, framed or filled
   button sets its own colour later in this section, one class heavier, and is unaffected.

   HOVER IS NEUTRAL, everywhere (D16, decided by the owner). It was the brand wash (--cordis-primary-soft)
   here, and three contexts — Studio's explorer / inspector, a dense action bar, `.cordis-dense-tools` —
   each carried a rule whose only job was to put the neutral wash back. A wall of icon buttons that flashes
   blue under the pointer reads as an accent state, not as "this is a button"; the overrides were the
   tell that the base was wrong. Studio keeps its own, lighter value of the same wash (--cordis-ghost-hover
   in cordis-studio.css); that is a profile value, not a correction. */
.cordis-btn--icon {
  width: var(--cordis-control-h); padding: 0;
  background: transparent; color: var(--cordis-text-muted); border-color: transparent;
}
/* :not(:disabled) — a disabled control must not answer the pointer at all. Without it this rule
   was the ONLY icon-button hover left standing on a disabled button: every override below and in
   cordis-studio.css already carries the guard, so a disabled icon button fell through to the hover
   here while its enabled neighbours took another, and it read as the brightest button in the row
   precisely when it could not be pressed. Reported against the IDE's debug transport bar, where Step
   is the button that is disabled while the controller free-runs.
   The glyph's hover colour is its own rule, excluding the toned / framed / filled buttons: at 0-4-0 it
   would otherwise outrank their 0-2-0 resting colour and grey a red Stop under the pointer. */
.cordis-btn--icon:hover:not(:disabled, .is-busy) { background: var(--cordis-neutral-soft); border-color: transparent; }
.cordis-btn--icon:not(.cordis-btn--danger, .cordis-btn--success, .cordis-btn--warning, .cordis-btn--framed, .cordis-btn--filled):hover:not(:disabled, .is-busy) {
  color: var(--cordis-text); }

/* A toggle (CordisButton Pressed, tri-state; from the retired CordisToolButton.Active, issue #435). Only
   the PRESSED half is drawn — the brand wash under the accent text, which stays put under the pointer so
   the state does not blink off on hover. An OFF toggle looks like a plain command on purpose (the 2026-08
   report: an outlined off-state was the one bordered member of a toolbar read as a set, and read as an
   artefact); `.is-toggle` stays on the element as the hook, and aria-pressed announces the difference. */
.cordis-btn--icon.is-active,
.cordis-btn--icon.is-active:hover:not(:disabled, .is-busy) { background: var(--cordis-primary-soft); color: var(--cordis-primary-text); }

/* A button that OPENS A MENU (CordisButton Caret, cordis-ide#768). It stops being square — the caret
   needs its own room, and squeezing it inside the square would shrink the glyph that identifies the
   button. Width goes to auto with the square as a floor, so a menu button in a row of plain ones is a
   little wider and otherwise identical. The caret is faint until the button is hovered or pressed. */
.cordis-btn--icon.cordis-btn--menu { width: auto; min-width: var(--cordis-control-h); gap: 1px; padding: 0 3px 0 4px; }
.cordis-btn__caret { flex: 0 0 auto; color: var(--cordis-text-faint); }
.cordis-btn:hover:not(:disabled) .cordis-btn__caret,
.cordis-btn.is-active .cordis-btn__caret { color: inherit; }
.cordis-btn--sm { height: calc(var(--cordis-control-h) - 4px); font-size: var(--cordis-fs-sm); padding: 0 var(--cordis-space-3); }
.cordis-btn--icon.cordis-btn--sm { width: calc(var(--cordis-control-h) - 4px); padding: 0; }

/* The four looks. --primary is the base fill named explicitly, for a caller that layers a look onto
   a geometry modifier (and for CordisButton, whose Variant enum always names one). */
.cordis-btn--primary { background: var(--cordis-primary); color: var(--cordis-text-on-fill); border-color: var(--cordis-primary); }
.cordis-btn--primary:hover:not(:disabled, .is-busy) { background: var(--cordis-primary-hover); border-color: var(--cordis-primary-hover); }

.cordis-btn--secondary { background: var(--cordis-surface); color: var(--cordis-text-strong); border-color: var(--cordis-border-strong); }
.cordis-btn--secondary:hover:not(:disabled, .is-busy) { background: var(--cordis-surface-sunken); border-color: var(--cordis-border-strong); }

.cordis-btn--ghost { background: transparent; color: var(--cordis-primary-text); border-color: transparent; }
.cordis-btn--ghost:hover:not(:disabled, .is-busy) { background: var(--cordis-primary-soft); }

/* Destructive. Neutral at rest and red on hover, so a delete reads the same at every size — which is
   why it is a look a caller LAYERS (`--icon --danger`) rather than a Variant of its own. */
.cordis-btn--danger { background: var(--cordis-surface); color: var(--cordis-danger-text); border-color: var(--cordis-border-strong); }
.cordis-btn--danger:hover:not(:disabled, .is-busy) { background: var(--cordis-danger-soft); border-color: var(--cordis-danger-soft); }
/* The ICON rule takes the BASE token, not the -text one (cordis-ide#843). The -text ramp is the 700 --
   it exists to be read on a soft tinted ground, which is what the filled looks above give it. A
   transparent icon button has no such ground: it sits on plain chrome beside a canvas whose own
   vocabulary is the 500s (--cordis-st-true is #3BAA35, the same green-500 as --cordis-success), and the
   700 read two steps darker than everything it was sitting next to. Reported on the debug transport,
   where a maroon Stop sat a few pixels from the diagram's #E20717.

   Hover keeps the base colour on the soft tint rather than swapping to -text: 500-on-50 clears 3:1 for a
   glyph, and a colour that darkens under the pointer reads as the button being pressed already. */
.cordis-btn--icon.cordis-btn--danger { background: transparent; border-color: transparent; color: var(--cordis-danger); }
/* Same guard as the plain icon hover above, for the same reason. */
.cordis-btn--icon.cordis-btn--danger:hover:not(:disabled, .is-busy),
.cordis-btn--icon.cordis-btn--danger:focus-visible { background: var(--cordis-danger-soft); }

/* The other two tones (cordis-ide#843). Same shape as --danger above and deliberately so: the treatment
   was already right, it was just only available in red. Reached through CordisButton's Tone parameter
   rather than by writing the class, which is what keeps the set spelled correctly.

   Why the ICON rules and not the filled ones carry the weight: a tone earns its place in a bar a reader
   SCANS -- the debug transport, where run / hot-reload / stop are three same-family glyphs at 16px and
   the colour is the only thing that separates them at a glance. A filled green button is a different and
   much louder claim, and nothing has asked for one, so these stay honest about being glyph colour. */
.cordis-btn--success { background: var(--cordis-surface); color: var(--cordis-success-text); border-color: var(--cordis-success-soft); }
.cordis-btn--success:hover:not(:disabled, .is-busy) { background: var(--cordis-success-soft); border-color: var(--cordis-success-soft); }
.cordis-btn--icon.cordis-btn--success { background: transparent; border-color: transparent; color: var(--cordis-success); }
.cordis-btn--icon.cordis-btn--success:hover:not(:disabled, .is-busy),
.cordis-btn--icon.cordis-btn--success:focus-visible { background: var(--cordis-success-soft); }

.cordis-btn--warning { background: var(--cordis-surface); color: var(--cordis-warning-text); border-color: var(--cordis-warning-soft); }
.cordis-btn--warning:hover:not(:disabled, .is-busy) { background: var(--cordis-warning-soft); border-color: var(--cordis-warning-soft); }
.cordis-btn--icon.cordis-btn--warning { background: transparent; border-color: transparent; color: var(--cordis-warning); }
.cordis-btn--icon.cordis-btn--warning:hover:not(:disabled, .is-busy),
.cordis-btn--icon.cordis-btn--warning:focus-visible { background: var(--cordis-warning-soft); }

/* Framed icon buttons (CordisButton Frame, Cordis Server #4025): an icon button that must not read as one
   of the quiet tools beside it -- a machine command's run button among a panel header's search and
   expand tools, or in a list row. Framed at rest (the secondary look's surface and edge) with a neutral
   glyph until a Tone tints it, the sunken ground under the pointer. `--framed-danger` is the emergency
   stop: a soft red square at rest and a solid red one under the pointer, also while it runs. After the
   tone rules and one class heavier than them, so a frame keeps its ground whatever the tone. */
.cordis-btn--icon.cordis-btn--framed { background: var(--cordis-surface); border-color: var(--cordis-border-strong); }
.cordis-btn--icon.cordis-btn--framed:not(.cordis-btn--danger, .cordis-btn--success, .cordis-btn--warning) { color: var(--cordis-text); }
.cordis-btn--icon.cordis-btn--framed:hover:not(:disabled, .is-busy) { background: var(--cordis-surface-sunken); border-color: var(--cordis-border-strong); }
.cordis-btn--icon.cordis-btn--framed.cordis-btn--framed-danger { background: var(--cordis-danger-soft); border-color: var(--cordis-danger); color: var(--cordis-danger); }
.cordis-btn--icon.cordis-btn--framed.cordis-btn--framed-danger:hover:not(:disabled, .is-busy) {
  background: var(--cordis-danger); border-color: var(--cordis-danger); color: var(--cordis-text-on-fill); }

/* A dialog's or a form's commit row (Cancel + Save, Delete + Add): the buttons share one minimum width,
   so a short "Add" is not a stub beside a longer "Cancel". CordisDialogFooter EvenButtons="true" puts the
   class on its band; any other footer (a drawer's, a modal editor's, a page header's Save / Discard)
   wraps its buttons in an element carrying it, which stays out of the host row's layout. */
.cordis-even-actions:not(.cordis-dialog__footer) { display: contents; }
.cordis-even-actions > .cordis-btn { min-width: 6rem; justify-content: center; }

/* Dense tool buttons outside a CordisActionBar Dense (whose own rule is the same look, in its scoped
   sheet, plus the fold): a row of tools that never folds -- a tab strip's trailing tools, a panel's own
   tool cluster. Each icon button is Studio's small tool square: the dense control height. Its colours
   are the plain icon button's own since D16 (muted glyph, neutral hover), so only the size is here. */
.cordis-dense-tools .cordis-btn--icon { width: var(--cordis-dense-control-h); height: var(--cordis-dense-control-h);
  min-height: 0; padding: 0; }
.cordis-dense-tools .cordis-btn--icon.cordis-btn--menu { width: auto; min-width: var(--cordis-dense-control-h); padding: 0 3px 0 4px; }

/* Controls at an editor toolbar's compact height, outside a CordisEditorToolbar (whose band sets the same
   thing): a format bar, a log's filter row, a chart header's range picker. Every control inside that
   follows --cordis-control-h (a button, an input, a select, a segmented toggle, a filter box) takes it. */
.cordis-toolbar-controls { --cordis-control-h: var(--cordis-toolbar-control-h); }

/* A DISABLED button has no tone, in any of the three. Green says "you can start this"; on a control that
   cannot be pressed that is a claim the reader has to test by clicking to disprove. Written after all
   three looks and, at equal specificity, later than each of them, so it wins on order. It restores
   `inherit` rather than a grey because the opacity rule near the top of this section is what actually
   greys a disabled button --
   this only has to stop the hue from surviving it. */
.cordis-btn--success:disabled,
.cordis-btn--warning:disabled,
.cordis-btn--danger:disabled { color: inherit; border-color: transparent; }

/* Fill (CordisButtonFill): the whole button in one solid colour, for a command whose colour an HMI author
   picked. Not a tone (a tone keeps the neutral chrome, see above); after every look and tone so it wins on
   order, and its hover brightens the fill instead of swapping in the brand hover. Each fill names its own pair
   through two local knobs, the fill and the ink legible on it in both themes. */
.cordis-btn--filled,
.cordis-btn--filled:hover:not(:disabled, .is-busy),
.cordis-btn--filled:active:not(:disabled, .is-busy) {
  background: var(--cordis-btn-fill, var(--cordis-primary));
  border-color: var(--cordis-btn-edge, var(--cordis-btn-fill, var(--cordis-primary)));
  color: var(--cordis-btn-ink, var(--cordis-text-on-fill)); }
.cordis-btn--filled:hover:not(:disabled, .is-busy)  { filter: brightness(1.08); }
.cordis-btn--filled:active:not(:disabled, .is-busy) { filter: brightness(0.92); }
.cordis-btn--fill-primary   { --cordis-btn-fill: var(--cordis-primary); --cordis-btn-ink: var(--cordis-text-on-fill); }
.cordis-btn--fill-secondary { --cordis-btn-fill: var(--cordis-surface-sunken); --cordis-btn-edge: var(--cordis-border-strong); --cordis-btn-ink: var(--cordis-text-strong); }
/* Success and info take the one shade deeper that holds white ink in both themes: the dark theme's own
   --cordis-success / --cordis-info are light enough for dark text, which a fill with white ink cannot use. */
.cordis-btn--fill-success   { --cordis-btn-fill: var(--cordis-green-600); --cordis-btn-ink: var(--cordis-text-on-fill); }
.cordis-btn--fill-danger    { --cordis-btn-fill: var(--cordis-danger); --cordis-btn-ink: var(--cordis-text-on-fill); }
/* White on amber is not legible in either theme; the darkest neutral is. */
.cordis-btn--fill-warning   { --cordis-btn-fill: var(--cordis-warning); --cordis-btn-ink: var(--cordis-n-900); }
.cordis-btn--fill-info      { --cordis-btn-fill: var(--cordis-blue-600); --cordis-btn-ink: var(--cordis-text-on-fill); }
.cordis-btn--fill-light     { --cordis-btn-fill: var(--cordis-surface-raised); --cordis-btn-edge: var(--cordis-border); --cordis-btn-ink: var(--cordis-text-strong); }
.cordis-btn--fill-dark      { --cordis-btn-fill: var(--cordis-inverse-bg); --cordis-btn-ink: var(--cordis-inverse-text); }
/* A disabled filled-primary ICON button is an ordinary disabled tool (Cordis Server #4025, carried over from the
   retired `.pv-toolbtn--primary` by D16): a band's commit action faded to a pale blue tile read as "something
   blue" rather than as "off", and no other disabled tool in a toolbar keeps a fill. */
.cordis-btn--icon.cordis-btn--fill-primary:disabled { background: transparent; border-color: transparent; color: var(--cordis-text-muted); }

/* An anchor brings its own underline and inherits none of the above. */
a.cordis-btn { text-decoration: none; }

/* ---------- fields -------------------------------------------------------- */

/* One labelled form row (decision D10). Two naming conventions used to describe it — the sibling
   classes a plain consumer writes (`.cordis-label`, `.cordis-hint`, `.cordis-error` beside the field)
   and CordisFormField's BEM elements — so both are declared here, on the same rules. The BEM half is
   not redundant: a label that is a CHILD of the row can be styled without the caller remembering to
   add a second class, which is the only form a component can emit reliably. */
.cordis-field { display: flex; flex-direction: column; gap: var(--cordis-space-2); min-width: 0; }
.cordis-label, .cordis-field__label { font-size: var(--cordis-fs-sm); font-weight: var(--cordis-fw-semibold); color: var(--cordis-text); user-select: none; }
.cordis-hint, .cordis-field__hint  { font-size: var(--cordis-fs-xs); color: var(--cordis-text-muted); }
/* The label and whatever the caller hangs beside it (a help icon, a badge) share one baseline row.
   Always emitted, suffix or not, so a field with one and a field without still put their labels on
   the same line — the row adds no height of its own. */
.cordis-field__labelrow { display: flex; align-items: center; gap: 4px; min-width: 0; }
/* The required marker is aria-hidden in the markup — it repeats what the control's own `required`
   attribute already tells a screen reader, and "Name star" is not what a user needs to hear. */
.cordis-field__required { margin-left: 2px; color: var(--cordis-danger-text); }
/* A flex row, so a field, a unit suffix and a browse button sit on one line without the call site
   laying them out. It is also what makes `flex: 1` on the input primitive mean anything. */
.cordis-field__control { display: flex; min-width: 0; }

/* The layout a form writes around its fields (moved in from the Cordis Server's editors, Mantis #4025).
   .cordis-fields is a responsive grid of .cordis-field rows: as many columns as fit, one when narrow; the
   `min(100%, …)` keeps a single column from overflowing a pane narrower than the minimum. --compact is the
   denser grid of a properties pane (10rem columns, tighter rows, controls top-aligned so a field with a
   hint does not stretch its neighbour). __wide spans every column. */
.cordis-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  gap: var(--cordis-space-4); min-width: 0; }
.cordis-fields--compact { grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
  gap: var(--cordis-space-3) var(--cordis-space-4); align-items: start; }
.cordis-fields__wide { grid-column: 1 / -1; }
/* A lone checkbox as a grid cell (a .cordis-field holding a .cordis-choice and no label): it sits on the
   controls' line, not the labels'. 18px is the .cordis-check box. */
.cordis-field--check { justify-content: flex-end; align-self: stretch; min-height: var(--cordis-control-h);
  padding-bottom: calc((var(--cordis-control-h) - 18px) / 2); }

/* A checkbox or radio with its label: <label class="cordis-choice"><input class="cordis-check"> text</label>.
   The label is the hit area. A row of them wraps (.cordis-choices; --stack puts one per line). --described
   adds a hint under the label: .cordis-choice__text > .cordis-choice__label + .cordis-hint, the box on the
   label's first line. For a bound checkbox outside an EditForm, CordisCheckbox is the component. */
.cordis-choices { display: flex; flex-wrap: wrap; gap: var(--cordis-space-2) var(--cordis-space-5); min-width: 0; }
.cordis-choices--stack { flex-direction: column; flex-wrap: nowrap; }
.cordis-choice { display: inline-flex; align-items: center; gap: var(--cordis-space-2);
  font-size: var(--cordis-fs-sm); color: var(--cordis-text); cursor: pointer; }
.cordis-choice:has(> input:disabled) { color: var(--cordis-text-muted); cursor: not-allowed; }
.cordis-choice--described { display: flex; align-items: flex-start; gap: var(--cordis-space-3); }
.cordis-choice--described > input { margin-top: 1px; }
.cordis-choice__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cordis-choice__label { font-weight: var(--cordis-fw-semibold); }

/* Several controls on one line of a field (a checkbox beside a colour, a unit beside a number). Inside a
   CordisFormField it is the flex item of .cordis-field__control, so it claims the width itself. (Not
   .cordis-inline, which is the resting box of an inline editor.) */
.cordis-controlrow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--cordis-space-2);
  flex: 1 1 auto; min-width: 0; }

/* A row list in a form (moved in from the Cordis Server's widget editors, Mantis #4025): the entries of one
   setting (graph series, table rows, zones, nav buttons), each a bordered row with a head (a title that
   ellipsizes, its actions) and, while it is being edited, a body with its fields. Not CordisMemberList,
   which is Studio's flat 26px member list with hover-revealed actions: these rows are few, carry a name
   over what they are bound to, and keep their actions visible.
     .cordis-rowlist > .cordis-rowlist__row[.is-editing] > .cordis-rowlist__head
         > .cordis-rowlist__title (> glyph / __swatch, __stack > (__text | an input) + __meta) + __actions
       + .cordis-rowlist__body
   --compact is the one-line entry list (40px heads) whose entries open in a dialog. __empty is the one
   muted line an empty list shows. */
.cordis-rowlist { display: flex; flex-direction: column; gap: var(--cordis-space-2); min-width: 0; }
.cordis-rowlist__row { min-width: 0; border: 1px solid var(--cordis-border); border-radius: var(--cordis-radius-sm);
  background: var(--cordis-surface); }
.cordis-rowlist__head { display: flex; align-items: center; gap: var(--cordis-space-2); min-height: 44px;
  padding: var(--cordis-space-1) var(--cordis-space-2) var(--cordis-space-1) var(--cordis-space-3); }
.cordis-rowlist--compact .cordis-rowlist__head { min-height: 40px; }
.cordis-rowlist__row.is-editing > .cordis-rowlist__head { border-bottom: 1px solid var(--cordis-border);
  background: var(--cordis-surface-sunken); }
.cordis-rowlist__title { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: var(--cordis-space-2);
  font-size: var(--cordis-fs-sm); font-weight: var(--cordis-fw-semibold); color: var(--cordis-text-strong); }
.cordis-rowlist__stack { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.cordis-rowlist__text, .cordis-rowlist__meta { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cordis-rowlist__meta { font-size: var(--cordis-fs-xs); font-weight: var(--cordis-fw-regular); color: var(--cordis-text-muted); }
.cordis-rowlist__actions { flex: 0 0 auto; display: flex; align-items: center; gap: var(--cordis-space-1); }
.cordis-rowlist__body { display: flex; flex-direction: column; gap: var(--cordis-space-4); padding: var(--cordis-space-3); }
.cordis-rowlist__swatch { flex: 0 0 auto; width: 12px; height: 12px; border: 1px solid var(--cordis-border-strong); border-radius: 3px; }
.cordis-rowlist__empty { margin: 0; padding: var(--cordis-space-1) 0; font-size: var(--cordis-fs-sm); color: var(--cordis-text-muted); }

/* One text-entry family (decision D10). CordisTextInput renders `.cordis-input` on both a text <input>
   and a <textarea>, and a plain consumer writes the same class by hand — so, as with the button, the
   asset layer is the single author and the component's scoped sheet keeps nothing that overlaps.
   `.cordis-textarea` remains for the same element written WITHOUT the component: the two selectors
   are interchangeable on a <textarea> and `textarea.cordis-input` below carries the shape either way.

   `.cds-select` USED to ride along in these three selector lists, and decision D10's seventh family is
   closed by deleting it rather than renaming it. It was never a second implementation of anything: it
   declared no property of its own, appearing only here, on :focus, and in the 640px min-height rule —
   so a native <select class="cordis-input"> already got every one of its rules. Meanwhile the merged
   name `.cordis-select` is owned by CordisSelect, which is a listbox built out of a button and a
   popup and shares no DOM with a native <select> at all. Keeping both would have meant two names for
   the text-entry shape and a third control answering to the more obvious one. */
.cordis-input, .cordis-textarea {
  font-family: inherit; font-size: var(--cordis-control-fs); color: var(--cordis-text);
  background: var(--cordis-surface); border: 1px solid var(--cordis-border-strong);
  border-radius: var(--cordis-radius-sm); height: var(--cordis-control-h);
  padding: 0 var(--cordis-control-px); outline: none; width: 100%;
  transition: border-color var(--cordis-dur-fast) var(--cordis-ease);
}
.cordis-textarea, textarea.cordis-input {
  height: auto; padding: var(--cordis-space-2) var(--cordis-space-3);
  line-height: var(--cordis-lh-body); resize: vertical;
}
/* A native <select> is the plain-HTML answer for a short, fixed option list — an inline cell editor
   in a table, mostly. It wears `.cordis-input` (decision D10 deleted `.cds-select` precisely because
   it needed no rules of its own), and the comment above was true about every declaration in this
   family and still left one thing undrawn: the UA's own dropdown arrow. So a page that did everything
   right got Cordis's border and height with Windows's chevron inside it — and that chevron is the one
   part of the control a reader looks at to know it is a select at all.

   The caret is two gradients rather than an SVG because `currentColor` is the only way to draw it
   without writing a colour down: an inline `data:` background-image cannot read a custom property, so
   it would have had to hardcode one of the greys and would then have stayed that grey in dark. That
   buys a solid triangle, which is deliberately NOT the `chevron_down` glyph CordisSelect's trigger
   carries — a hollow chevron out of gradients needs knockout layers painted in the surface colour, and
   they stop matching the moment the control sits on anything but `--cordis-surface`. Two different
   controls, and the triangle is the one every platform already uses for this one. */
select.cordis-input {
  appearance: none;
  padding-right: calc(var(--cordis-control-px) + 12px);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(-45deg, transparent 50%, currentColor 50%);
  background-size: 5px 5px;
  background-position:
    right calc(var(--cordis-control-px) + 4px) center,
    right var(--cordis-control-px) center;
  background-repeat: no-repeat;
}

.cordis-input::placeholder, .cordis-textarea::placeholder { color: var(--cordis-text-muted); }
/* Hover is a real affordance on a field: it is the only resting state that says "this is editable"
   before the caret is in it. */
.cordis-input:hover:not([disabled]), .cordis-textarea:hover:not([disabled]) { border-color: var(--cordis-text-muted); }
.cordis-input:focus, .cordis-textarea:focus { border-color: var(--cordis-accent); box-shadow: var(--cordis-focus-ring); }
.cordis-input[disabled], .cordis-textarea[disabled] {
  background: var(--cordis-surface-sunken); color: var(--cordis-text-muted); cursor: not-allowed;
}

.cordis-input--invalid, .cordis-input.is-invalid,
.cordis-input.is-invalid:focus { border-color: var(--cordis-danger); }
.cordis-error, .cordis-field__error { display: flex; align-items: center; gap: var(--cordis-space-2); font-size: var(--cordis-fs-xs); color: var(--cordis-danger-text); }

/* value fields carry a format: mono, tabular, so columns line up */
.cordis-input--code, .cordis-input--number, .cordis-input--date { font-family: var(--cordis-font-mono); font-variant-numeric: tabular-nums; font-size: var(--cordis-fs-sm); }

/* EDIT IN PLACE (`CordisTextInput Inline="true"`, user report 2026-08). The same field, swapped in for a
   label that is already sitting in a dense row -- a tree row, a member row, the name inside a diagram
   node. Everything about the family that makes a good FORM field is wrong here, and both halves of that
   were reported as bugs from a running app:

   - `height: var(--cordis-control-h)` is a FIXED height, so the field cannot be shrunk to the row it
     replaces a label in. A consumer that trims `padding` and `font-size` (the tree row and the two
     diagram rename bars all did) still gets a full-height control, and the row grows and shifts the
     moment editing starts. `height: auto` is the fix: the geometry follows the text, which is what a
     label does, so nothing moves.
   - `box-shadow: var(--cordis-focus-ring)` paints a solid ring OUTSIDE the border. On a form field with
     space around it that ring is the point; in a 26px row it is a halo over the neighbouring rows, and
     inside a node header clipped by `overflow: hidden` it is a blue smear on one edge. The accent
     border-colour the family already switches to on `:focus` is the indicator here, and it costs no
     geometry -- which is also exactly what this control looked like before the design system landed.

   Everything else is inherited from the label it replaces (font, weight, line-height, alignment), so a
   consumer no longer has to restate the typography of its own row just to keep the swap invisible. The
   resting border and background STAY: "this is now editable" is worth a 1px line, and losing it would be
   a drift of its own. */
.cordis-input--inline {
  height: auto;
  padding: 1px 5px;
  font: inherit;
  line-height: inherit;
  text-align: inherit;
}
.cordis-input--inline:focus { box-shadow: none; }
/* --quiet, on --inline: a name edited in place that reads as the name until it is pointed at (a row list's
   entry label, whose automatic name is the placeholder). No resting edge or fill; the edge comes back on
   hover and turns accent on focus, like the rest of the family. The negative start margin is the padding
   plus the edge, so the text lines up with the line under it. The placeholder is the automatic NAME, so it
   is drawn as text, not as a hint, until the field has focus. */
.cordis-input--quiet { width: 100%; min-width: 0; margin: 0 0 0 -6px; border-color: transparent; background: transparent;
  color: inherit; text-overflow: ellipsis; }
.cordis-input--quiet::placeholder { color: var(--cordis-text-strong); opacity: .8; }
.cordis-input--quiet:hover:not([disabled]) { border-color: var(--cordis-border); }
.cordis-input--quiet:focus { border-color: var(--cordis-accent); background: var(--cordis-surface); }
.cordis-input--quiet:focus::placeholder { color: var(--cordis-text-muted); opacity: 1; }

/* ---- the inline-editable cell's resting box (CordisInlineEdit) ---------------------------------- */

/* An inline-editable value shows a LABEL at rest and an `.cordis-input` while editing, and those two
   are not the same height: a line of text is ~21px where the control is `--cordis-control-h`. Left to
   itself the cell therefore grows the moment the editor opens, and in a table every row below it moves
   — the reader clicks a role and the row they were aiming at is no longer under the pointer
   (cordis-ide#910).

   So the RESTING box reserves the editor's height, and both resting shapes wear this class: the
   editable one (a <button>, for the keyboard) and the read-only one. That second one matters as much as
   the first — `Editable="false"` exists so a row the caller may not edit looks identical to one it may,
   and it cannot look identical while being 5px shorter.

   `min-height` rather than `height`, so a cell whose Display fragment is genuinely taller (an avatar and
   two lines) is not clipped. `display: flex` centres the value against the neighbouring cells instead
   of parking it on the top edge; the numeric override below is the price of that, since a single
   anonymous flex item ignores the `text-align: right` a number column inherits. */
.cordis-inline {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--cordis-control-h);
}
.cordis-table .cordis-num > .cordis-inline,
.cordis-table td.cordis-right > .cordis-inline { justify-content: flex-end; }

.cordis-inputgroup { display: flex; align-items: center; height: var(--cordis-control-h);
  border: 1px solid var(--cordis-border-strong); border-radius: var(--cordis-radius-sm); background: var(--cordis-surface); overflow: hidden; }
.cordis-inputgroup > .cordis-input { border: 0; box-shadow: none; height: 100%; }
.cordis-inputgroup__prefix { display: flex; align-items: center; gap: var(--cordis-space-1); height: 100%; padding: 0 var(--cordis-space-3);
  background: var(--cordis-surface-sunken); border-right: 1px solid var(--cordis-border); color: var(--cordis-text-muted);
  font-family: var(--cordis-font-mono); font-size: var(--cordis-fs-xs); }

/* ---------- checkbox, radio, switch --------------------------------------- */

.cordis-check, .cordis-radio { appearance: none; margin: 0; width: 18px; height: 18px; flex: 0 0 18px;
  border: 1px solid var(--cordis-border-strong); background: var(--cordis-surface); cursor: pointer; position: relative; }
.cordis-check { border-radius: var(--cordis-radius-sm); }
.cordis-radio { border-radius: var(--cordis-radius-pill); }
.cordis-check:checked, .cordis-radio:checked { background: var(--cordis-primary); border-color: var(--cordis-primary); }
.cordis-check:checked::after {
  content: ''; position: absolute; left: 5px; top: 2px; width: 5px; height: 9px;
  border: solid var(--cordis-text-on-fill); border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.cordis-check:indeterminate { background: var(--cordis-primary); border-color: var(--cordis-primary); }
.cordis-check:indeterminate::after {
  content: ''; position: absolute; left: 3px; top: 7px; width: 10px; height: 2px;
  border-radius: 1px; background: var(--cordis-text-on-fill); border: 0; transform: none;
}
.cordis-radio:checked::after {
  content: ''; position: absolute; inset: 3px; border-radius: var(--cordis-radius-pill); background: var(--cordis-surface);
}

/* A switch takes effect immediately — it never sits next to a Save button. */
/* The plain half of the switch family: ONE bare <input type="checkbox">, thumb drawn with ::after.
   Renamed from `.cds-switch` to a modifier rather than merged away, because the component cannot
   replace it — a switch that carries a label and an optional glyph needs elements to hang them on,
   and a plain-HTML consumer writing a settings list wants the input and nothing else. The `--plain`
   suffix says "the version without the label slot", not "a second switch".

   No border, so the inset that centres the thumb is exactly half the leftover height, and the
   travel is the rest. Both are computed rather than declared, which is what lets the three tokens
   above be the only thing a profile overrides. */
.cordis-switch--plain { --cordis-switch-inset: calc((var(--cordis-switch-h) - var(--cordis-switch-thumb)) / 2);
  appearance: none; margin: 0;
  width: var(--cordis-switch-w); height: var(--cordis-switch-h);
  flex: 0 0 var(--cordis-switch-w);
  border-radius: var(--cordis-radius-pill); background: var(--cordis-switch-off); position: relative; cursor: pointer;
  transition: background-color var(--cordis-dur-fast) var(--cordis-ease); }
.cordis-switch--plain::after { content: ''; position: absolute;
  top: var(--cordis-switch-inset); left: var(--cordis-switch-inset);
  width: var(--cordis-switch-thumb); height: var(--cordis-switch-thumb);
  border-radius: var(--cordis-radius-pill); background: var(--cordis-switch-knob);
  transition: left var(--cordis-dur-fast) var(--cordis-ease); }
.cordis-switch--plain:checked { background: var(--cordis-switch-on); }
.cordis-switch--plain:checked::after {
  left: calc(var(--cordis-switch-w) - var(--cordis-switch-thumb) - var(--cordis-switch-inset)); }

/* ---------- status and category ------------------------------------------- */
/* Two shapes on purpose: a status is a pill with a dot, a category is a soft
   rectangle without one. Colour is meaning; five statuses, no sixth.          */

/* Height from --cordis-tag-h rather than from block padding plus whatever the font
   does, so the pill keeps its air in a 26px Studio row as well as a 44px Shell one.  */
.cordis-status {
  display: inline-flex; align-items: center; gap: var(--cordis-space-2);
  height: var(--cordis-tag-h); box-sizing: border-box; white-space: nowrap;
  /* middle, not the inline default: a tag has a filled ground, so sitting its text on
     the surrounding baseline hangs the whole shape below the line it belongs to. */
  vertical-align: middle;
  padding: 0 var(--cordis-space-3); border-radius: var(--cordis-radius-pill);
  font-size: var(--cordis-fs-xs); font-weight: var(--cordis-fw-semibold);
}
.cordis-status::before { content: ''; width: 6px; height: 6px; border-radius: var(--cordis-radius-pill); background: currentColor; }
.cordis-status--ok      { background: var(--cordis-success-soft); color: var(--cordis-success-text); }
.cordis-status--warn    { background: var(--cordis-warning-soft); color: var(--cordis-warning-text); }
.cordis-status--danger  { background: var(--cordis-danger-soft);  color: var(--cordis-danger-text); }
.cordis-status--info    { background: var(--cordis-info-soft);    color: var(--cordis-info-text); }
.cordis-status--neutral { background: var(--cordis-neutral-soft); color: var(--cordis-neutral-text); }
/* A linked pill (CordisStatusPill Href): the same shape as an <a>. Hover draws a hairline in the tone's own
   ink rather than shading the ground, so it reads the same on all five tones in both themes. */
a.cordis-status { text-decoration: none; cursor: pointer;
  transition: box-shadow var(--cordis-dur-fast) var(--cordis-ease); }
a.cordis-status:hover { text-decoration: none; box-shadow: inset 0 0 0 1px currentColor; }
a.cordis-status:focus-visible { outline: none; box-shadow: var(--cordis-focus-ring); }

/* A status as a glyph alone (CordisStatusIcon), for a header or a row with no room for a pill: the same five
   tones, in the readable text colour. .is-busy breathes while the state is on its way somewhere; the reduced-
   motion rule at the foot of this file holds it still. */
.cordis-status-icon { display: inline-flex; align-items: center; vertical-align: middle; line-height: 1; }
.cordis-status-icon--ok      { color: var(--cordis-success-text); }
.cordis-status-icon--warn    { color: var(--cordis-warning-text); }
.cordis-status-icon--danger  { color: var(--cordis-danger-text); }
.cordis-status-icon--info    { color: var(--cordis-info-text); }
.cordis-status-icon--neutral { color: var(--cordis-neutral-text); }
.cordis-status-icon.is-busy  { animation: cordis-breathe 1.2s var(--cordis-ease) infinite; }
@keyframes cordis-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

.cordis-cat {
  display: inline-flex; align-items: center;
  height: var(--cordis-tag-h); box-sizing: border-box; white-space: nowrap;
  vertical-align: middle;
  padding: 0 var(--cordis-space-3); border-radius: var(--cordis-radius-sm); font-size: var(--cordis-fs-xs); font-weight: var(--cordis-fw-semibold);
}
.cordis-cat--blue   { background: var(--cordis-cat-blue-soft);   color: var(--cordis-cat-blue); }
.cordis-cat--violet { background: var(--cordis-cat-violet-soft); color: var(--cordis-cat-violet); }
.cordis-cat--amber  { background: var(--cordis-cat-amber-soft);  color: var(--cordis-cat-amber); }
.cordis-cat--orange { background: var(--cordis-cat-orange-soft); color: var(--cordis-cat-orange); }
.cordis-cat--green  { background: var(--cordis-cat-green-soft);  color: var(--cordis-cat-green); }
.cordis-cat--grey   { background: var(--cordis-cat-grey-soft);   color: var(--cordis-cat-grey); }
.cordis-cat--red    { background: var(--cordis-cat-red-soft);    color: var(--cordis-cat-red); }

/* A person (CordisPersonChip): the category chip's colour, a pill's shape. It is emitted WITH .cordis-cat and
   .cordis-cat--<hue> and only reshapes them — a kind is a small-radius rectangle, a person a pill — so the
   hue rules above stay the one copy. A long name ellipsizes in its own span, so the "(you)" suffix keeps
   its place; max-width:100% bounds it by whatever it sits in, and min-width:0 lets a flex parent shrink it. */
.cordis-person { max-width: 100%; min-width: 0; overflow: hidden; border-radius: var(--cordis-radius-pill);
  padding: 0 var(--cordis-space-3); }
.cordis-person__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cordis-person__suffix { flex: 0 0 auto; margin-inline-start: var(--cordis-space-1); white-space: nowrap; }
/* No name: the absence of a person, so no hue — the avatar's --empty look, the same neutral. */
.cordis-person--unknown { background: var(--cordis-neutral-soft); color: var(--cordis-neutral-text); }

/* A value in a set (CordisChip): a neutral pill that can be a toggle (pressed = in the set, the selected tint the
   filter chip wears), lead with the colour the value gives, and carry a ×. A row of them is .cordis-chips; a
   .cordis-input in that row (another value to add) takes the chip height, pill and a dashed edge. */
.cordis-chips { display: flex; flex-wrap: wrap; align-items: center; gap: var(--cordis-space-2); }
.cordis-chip {
  display: inline-flex; align-items: center; gap: var(--cordis-space-1);
  height: var(--cordis-tag-h); box-sizing: border-box; white-space: nowrap; vertical-align: middle;
  padding: 0 var(--cordis-space-2); border: 1px solid var(--cordis-border-strong);
  border-radius: var(--cordis-radius-pill); background: var(--cordis-surface);
  color: var(--cordis-text-muted); font-size: var(--cordis-fs-sm);
  transition: background-color var(--cordis-dur-fast) var(--cordis-ease), border-color var(--cordis-dur-fast) var(--cordis-ease);
}
.cordis-chip.is-on { border-color: var(--cordis-primary-soft-bd); background: var(--cordis-primary-soft);
  color: var(--cordis-primary-text); font-weight: var(--cordis-fw-semibold); }
.cordis-chip--toggle { padding: 0; }
.cordis-chip__toggle { display: inline-flex; align-items: center; gap: var(--cordis-space-1); height: 100%;
  padding: 0 var(--cordis-space-2); border-radius: var(--cordis-radius-pill); color: inherit; font: inherit; cursor: pointer; }
.cordis-chip__remove { display: inline-flex; align-items: center; justify-content: center; border-radius: var(--cordis-radius-pill);
  color: var(--cordis-text-muted); cursor: pointer; }
.cordis-chip--toggle .cordis-chip__remove { margin: 0 var(--cordis-space-1) 0 calc(-1 * var(--cordis-space-1)); }
.cordis-chip__remove:hover { color: var(--cordis-text-strong); }
.cordis-chip__toggle:focus-visible, .cordis-chip__remove:focus-visible { outline: none; box-shadow: var(--cordis-focus-ring); }
.cordis-chip__dot { flex: none; width: 10px; height: 10px; border-radius: var(--cordis-radius-pill);
  box-shadow: inset 0 0 0 1px var(--cordis-border-strong); }
.cordis-chips > .cordis-input { width: 7rem; height: var(--cordis-tag-h); padding: 0 var(--cordis-space-2);
  border-style: dashed; border-radius: var(--cordis-radius-pill); font-size: var(--cordis-fs-sm); }

/* ---------- value change ---------------------------------------------------- */
/* old -> new, read the way a code diff taught everyone to: the removed value struck
   through on the danger wash, the added one on the success wash. Washes, not fills,
   so a column of changes stays calm; the mono face so 10 and 10.0 do not look alike.
   A side may be absent (an addition or a removal), and an EMPTY side is a value too --
   `__empty` quietens its placeholder so a dash does not read as data.            */
.cordis-change { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--cordis-space-1);
  min-width: 0; vertical-align: middle; font-size: var(--cordis-fs-sm); line-height: var(--cordis-lh-sm); }
.cordis-change__label { margin-right: var(--cordis-space-1); color: var(--cordis-text-muted); font-size: var(--cordis-fs-xs); white-space: nowrap; }
.cordis-change__old,
.cordis-change__new { padding: 0 var(--cordis-space-1); border-radius: var(--cordis-radius-sm);
  font-family: var(--cordis-font-mono); font-size: var(--cordis-fs-xs); overflow-wrap: anywhere; }
.cordis-change__old { background: var(--cordis-danger-soft); color: var(--cordis-danger-text);
  text-decoration: line-through; text-decoration-thickness: 1px; }
.cordis-change__new { background: var(--cordis-success-soft); color: var(--cordis-success-text); text-decoration: none; }
.cordis-change__empty { font-family: inherit; font-style: italic; opacity: 0.8; }
.cordis-change__arrow { display: inline-flex; color: var(--cordis-text-muted); }

/* ---------- avatar -------------------------------------------------------- */

.cordis-avatar { display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: var(--cordis-radius-pill);
  background: var(--cordis-primary); color: var(--cordis-text-on-fill);
  font-size: var(--cordis-fs-caption); font-weight: var(--cordis-fw-semibold); flex: 0 0 auto; }
/* 10px is below the type scale on purpose: two initials have to fit inside a
   24px circle. It is a fit constraint, not a text size. */
.cordis-avatar--sm { width: 24px; height: 24px; font-size: 10px; }
.cordis-avatar--lg { width: 36px; height: 36px; font-size: var(--cordis-fs-sm); }
.cordis-avatar--empty { background: var(--cordis-neutral-soft); color: var(--cordis-neutral-text); }
/* 18px: a presence marker inside a row or on a box corner that already has a
   height of its own. 9px type for the same fit reason as --sm's 10px. */
.cordis-avatar--xs { width: 18px; height: 18px; font-size: 9px; }
/* Present but idle. Faded, never re-coloured: the hue is how they are recognised. */
.cordis-avatar--muted { opacity: 0.55; }
.cordis-avatarstack { display: inline-flex; align-items: center; }
/* The ring is the colour of whatever the stack sits ON, so the overlap reads as a
   cut-out. A surface is the default; a stack in the dark chrome sets
   --cordis-avatarstack-ring to the chrome's own background. */
.cordis-avatarstack > .cordis-avatar { border: 2px solid var(--cordis-avatarstack-ring, var(--cordis-surface)); }
.cordis-avatarstack > .cordis-avatar + .cordis-avatar { margin-left: -8px; }
.cordis-avatarstack > .cordis-avatar--xs + .cordis-avatar--xs { margin-left: -6px; }
.cordis-avatarstack > .cordis-avatar--xs { border-width: 1.5px; }
/* The whole stack as one control (CordisAvatarStack with OnClick): a roster, not
   a row of separate faces — per-face targets would be 8px crescents. */
.cordis-avatarstack--button { padding: 2px 4px; border: 0; border-radius: var(--cordis-radius-sm);
  background: transparent; cursor: pointer; font: inherit; color: inherit; }
.cordis-avatarstack--button:hover { background: var(--cordis-avatarstack-hover, var(--cordis-neutral-soft)); }
.cordis-avatarstack--button:focus-visible { outline: none; box-shadow: var(--cordis-focus-ring); }

/* ---------- surfaces ------------------------------------------------------ */

.cordis-card { background: var(--cordis-surface); border: 1px solid var(--cordis-border);
  border-radius: var(--cordis-radius-md); box-shadow: var(--cordis-shadow-sm); }
.cordis-card__body { padding: var(--cordis-card-p); }
.cordis-card__head { display: flex; align-items: center; gap: var(--cordis-space-3);
  padding: var(--cordis-space-3) var(--cordis-card-p); border-bottom: 1px solid var(--cordis-border); }
/* A flush card holding a table (CordisCard Padded="false" around a CordisDataTable) clips the table to
   its rounded corners: without it the sunken header band and the last row's hover paint square corners
   over the card's radius. `overflow-x: auto` rather than `hidden`, so a table that still cannot fit
   (its columns already stepped aside through HideBelow) scrolls as a last resort instead of losing its
   right edge. Keyed on a table as a DIRECT child, so a card of form fields keeps `overflow: visible`
   and a focus ring or a select's popup at its edge is not cut off. Five copies of this rule lived in
   Cordis Server's admin pages (#4025). */
.cordis-card:has(> .cordis-table) { overflow-x: auto; }

/* A dense card (CordisCard Dense): a panel that fills the pane or cell it was given, with a slim inset, and
   lays out a CordisPanelHeader, an optional __search and a __scroll body that scrolls on its own. Lists in it
   are scanned, not read: 28px rows, small type, and the table's grow/fit column sizing. The tree row is set
   too because tokens.css derives it from the row height on :root, where this override cannot reach it. */
.cordis-card--dense { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; min-height: 0;
  overflow: hidden; padding: var(--cordis-space-2); color: var(--cordis-text);
  --cordis-row-h: 28px; --cordis-tree-row-h: 28px; --cordis-tag-h: 20px; }
.cordis-card--dense .cordis-card__search { flex: none; height: 28px; margin-bottom: var(--cordis-space-1);
  font-size: var(--cordis-fs-sm); }
/* A row's controls (a command's Run button, an output's editor) size off the control height: they keep the row. */
.cordis-card--dense .cordis-card__scroll { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
  overflow: auto; --cordis-control-h: 24px; }
.cordis-card--dense .cordis-card__scroll > .cordis-table { flex: none; }
/* The table's own density is CordisDataTable Dense (.cordis-table--dense), not the card's: a dense card used to
   restate the dense cells here for raw tables, and every table in one now goes through the component (#4025). */
/* The empty state is the band centred in the panel's free height, never a box at the top of it. */
.cordis-card--dense .cordis-card__scroll > .cordis-empty { flex: 1 1 auto; }

/* A panel's title band (CordisPanelHeader): the glyph, the title and a dense action bar on a sunken band. The
   title keeps its width while the tools can still fold, and only shrinks once the bar is down to its floor. */
.cordis-panelhead { display: flex; align-items: center; gap: var(--cordis-space-2); flex: none; min-height: 32px;
  margin-bottom: var(--cordis-space-1); padding: 0 var(--cordis-space-1) 0 var(--cordis-space-3);
  border: 1px solid transparent; border-radius: var(--cordis-radius-sm); background: var(--cordis-surface-sunken); }
.cordis-panelhead__icon { flex: none; }
.cordis-panelhead__title { flex: 0 1 auto; min-width: 0; margin: 0; overflow: hidden; white-space: nowrap;
  text-overflow: ellipsis; font-size: var(--cordis-fs-sm); font-weight: var(--cordis-fw-semibold);
  color: var(--cordis-text-strong); }
.cordis-panelhead__actions { display: flex; align-items: center; justify-content: flex-end; flex: 1 1 0; }
.cordis-panelhead--danger { background: var(--cordis-danger-soft); border-color: var(--cordis-danger); }
.cordis-panelhead--danger .cordis-panelhead__title { color: var(--cordis-danger-text); }
.cordis-panelhead--warn { background: var(--cordis-warning-soft); border-color: var(--cordis-warning); }
.cordis-panelhead--warn .cordis-panelhead__title { color: var(--cordis-warning-text); }
.cordis-panelhead--ok { background: var(--cordis-success-soft); }
.cordis-panelhead--ok .cordis-panelhead__title { color: var(--cordis-success-text); }
.cordis-panelhead--info { background: var(--cordis-info-soft); }
.cordis-panelhead--info .cordis-panelhead__title { color: var(--cordis-info-text); }
/* A page card's head drawn by CordisPanelHeader Look="Divider" (which every titled CordisCard renders, D16):
   the panel header's structure on the card's divider row above. The compound selector undoes exactly what the
   band adds — its ground, its edge, its radius, its 32px floor and its gap below — so the result is the plain
   .cordis-card__head a hand-written card draws, with a body-size title. */
.cordis-panelhead.cordis-card__head { gap: var(--cordis-space-3); min-height: 0; margin-bottom: 0;
  padding: var(--cordis-space-3) var(--cordis-card-p); border: 0; border-bottom: 1px solid var(--cordis-border);
  border-radius: 0; background: none; }
.cordis-card__head > .cordis-panelhead__title { font-size: var(--cordis-fs-body); }
.cordis-card__head > .cordis-panelhead__actions { flex: 1 0 auto; gap: var(--cordis-space-2); }

.cordis-page { padding: var(--cordis-space-5); }
/* Wraps: on a phone the actions drop below the title (still right-aligned by their auto margin) instead
   of squeezing the title into a one-word column and pushing the last button off the screen. */
.cordis-pagehead { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--cordis-space-4);
  padding-bottom: var(--cordis-space-4); border-bottom: 1px solid var(--cordis-border); margin-bottom: var(--cordis-section-gap); }
.cordis-pagehead__title { font-size: var(--cordis-fs-h2); font-weight: var(--cordis-fw-semibold);
  letter-spacing: -0.01em; color: var(--cordis-text-strong); }
.cordis-pagehead__sub { font-size: var(--cordis-fs-sm); color: var(--cordis-text-muted); margin-top: 2px; }
/* Actions of different heights share one centre line. Stretched instead (the flex default), a shorter
   control's track grew to the tallest button while its fixed-height content stayed at the top: a
   .cordis-segmented next to 36px icon buttons drew its segments 2px from the top and 8px from the bottom. */
.cordis-pagehead__actions { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--cordis-space-2); }

.cordis-stat { background: var(--cordis-surface); border: 1px solid var(--cordis-border);
  border-radius: var(--cordis-radius-md); box-shadow: var(--cordis-shadow-sm); padding: var(--cordis-space-3) var(--cordis-space-4); }
.cordis-stat__label { display: flex; align-items: flex-start; gap: var(--cordis-space-2); }
.cordis-stat__value { margin-top: var(--cordis-space-2); font-size: var(--cordis-fs-display); line-height: 1;
  font-weight: var(--cordis-fw-semibold); color: var(--cordis-primary); font-variant-numeric: tabular-nums; }
.cordis-stat__value--danger { color: var(--cordis-danger-text); }
.cordis-stat__value--ok { color: var(--cordis-success-text); }
.cordis-stat__foot { margin-top: var(--cordis-space-2); font-size: var(--cordis-fs-xs); color: var(--cordis-text-muted); }

/* The movement since the last period. Its own band rather than a second figure in the footnote — see
   CordisStat's remarks. Muted by default because a component cannot know whether up is good news; the
   two tone modifiers are the call site saying so. Reported by a consumer (2026-08, item 9). */
.cordis-stat__delta { display: flex; align-items: center; gap: 2px; margin-top: var(--cordis-space-2);
  font-size: var(--cordis-fs-xs); font-weight: var(--cordis-fw-semibold); color: var(--cordis-text-muted);
  font-variant-numeric: tabular-nums; }
.cordis-stat__delta--danger { color: var(--cordis-danger-text); }
.cordis-stat__delta--ok { color: var(--cordis-success-text); }
.cordis-stat__delta-glyph { display: inline-flex; }

/* A tile that opens the list behind it. The element is a real <a> or <button> (see the component), so
   this only has to undo the UA's control styling and say the tile is live — the focus ring comes from the
   universal :focus-visible rule near the top of this file, which is the whole reason the affordance
   belongs here rather than in each dashboard. `text-align: left` matters: a <button> centres its content
   and would centre the label and figure with it. */
.cordis-stat--action { appearance: none; display: block; width: 100%; text-align: left;
  font-family: inherit; color: inherit; text-decoration: none; cursor: pointer;
  transition: border-color var(--cordis-dur-fast) var(--cordis-ease),
              box-shadow var(--cordis-dur-fast) var(--cordis-ease); }
.cordis-stat--action:hover { border-color: var(--cordis-border-strong); box-shadow: var(--cordis-shadow-md); }

/* ---------- activity grid (CordisActivityGrid) ----------------------------- */
/* A contribution calendar: one column per week, one row per weekday, the label track in row 1 and
   column 1. The component writes each cell's place as inline grid-column / grid-row and the week count as
   --cordis-activity-weeks; --cordis-activity-cell is the one size knob. The plot keeps its own width and
   the root scrolls sideways, so a narrow card shows the newest weeks rather than squashing every cell.
   The levels are the chart ramp read from faint (4) to emphatic (1), which is the right way round in
   both themes because each theme orders the ramp for its own background. */
.cordis-activity { --cordis-activity-cell: 12px; display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--cordis-space-2); max-width: 100%; min-width: 0; overflow-x: auto; }
.cordis-activity__plot { --cordis-activity-weeks: 26; display: grid; gap: 3px;
  grid-template-columns: auto repeat(var(--cordis-activity-weeks), var(--cordis-activity-cell));
  grid-template-rows: auto repeat(7, var(--cordis-activity-cell)); }
.cordis-activity__month, .cordis-activity__weekday { font-size: var(--cordis-fs-xs); line-height: 1;
  color: var(--cordis-text-muted); white-space: nowrap; }
.cordis-activity__month { grid-row: 1; padding-bottom: var(--cordis-space-1); }
.cordis-activity__weekday { grid-column: 1; align-self: center; padding-right: var(--cordis-space-1); }
.cordis-activity__cell { display: block; width: var(--cordis-activity-cell); height: var(--cordis-activity-cell);
  border-radius: calc(var(--cordis-radius-sm) / 3); background: var(--cordis-surface-sunken);
  box-shadow: inset 0 0 0 1px var(--cordis-border); }
.cordis-activity__cell[data-level="1"] { background: var(--cordis-chart-4); box-shadow: none; }
.cordis-activity__cell[data-level="2"] { background: var(--cordis-chart-3); box-shadow: none; }
.cordis-activity__cell[data-level="3"] { background: var(--cordis-chart-2); box-shadow: none; }
.cordis-activity__cell[data-level="4"] { background: var(--cordis-chart-1); box-shadow: none; }
.cordis-activity__legend { display: flex; align-items: center; gap: 3px; align-self: flex-end;
  font-size: var(--cordis-fs-xs); color: var(--cordis-text-muted); }
.cordis-activity__legend > span:first-child { margin-right: var(--cordis-space-1); }
.cordis-activity__legend > span:last-child { margin-left: var(--cordis-space-1); }

/* ---------- uptime bar (CordisUptimeBar) ----------------------------------- */
/* A status page's strip: one bar per day, the newest on the right, over an axis that reads
   "90 days ago ——— 99.46 % uptime ——— Today". The bars share the width, so the strip fits any card;
   --cordis-uptime-h is the one size knob. The colours are the status tokens, and a day nothing is
   known about is the sunken surface with a hairline — never green, since unknown is not good. */
.cordis-uptime { --cordis-uptime-h: 32px; display: flex; flex-direction: column; gap: var(--cordis-space-2);
  min-width: 0; }
.cordis-uptime__bars { display: flex; align-items: stretch; gap: 2px; height: var(--cordis-uptime-h); }
.cordis-uptime__day { flex: 1 1 0; min-width: 1px; border-radius: calc(var(--cordis-radius-sm) / 3); background: var(--cordis-surface-sunken);
  box-shadow: inset 0 0 0 1px var(--cordis-border);
  transition: opacity var(--cordis-dur-fast) var(--cordis-ease); }
.cordis-uptime__day[data-level="up"] { background: var(--cordis-success); box-shadow: none; }
.cordis-uptime__day[data-level="degraded"] { background: var(--cordis-warning); box-shadow: none; }
.cordis-uptime__day[data-level="down"] { background: var(--cordis-danger); box-shadow: none; }
.cordis-uptime__bars:hover .cordis-uptime__day:not(:hover) { opacity: 0.6; }
.cordis-uptime__axis { display: flex; align-items: center; gap: var(--cordis-space-2);
  font-size: var(--cordis-fs-xs); color: var(--cordis-text-muted); white-space: nowrap; }
.cordis-uptime__rule { flex: 1 1 0; min-width: var(--cordis-space-2); border-top: 1px solid var(--cordis-border); }
.cordis-uptime__summary { color: var(--cordis-text); }

/* ---------- dashboard grid (CordisDashboardGrid) --------------------------- */
/* A tile grid a user arranges: tiles sit on whole cells of a fixed column count and a fixed row
   height, so a layout saved at one window width comes back the same shape at another. The component
   writes each tile's cells as inline grid-column / grid-row and the two sizes as the custom
   properties below; the defaults here are what a grid without them looks like.

   The gestures are cordis-ui.js's (cordisUi.canvasGestures, the ones an HMI canvas uses too): a press
   selects, a drag moves the selection, the eight .cdg-resize zones of a selected tile resize it, and
   while a tile is in the hand it follows the pointer while a .cdg-target shows the cells it will land
   on. Only the cells go back to .NET; once its render lands every tile glides to its new cells. A
   target rather than moving the real tile through the grid on every cell, because re-placing a live
   chart forty times in one drag re-lays it out forty times. */
.cordis-dashgrid { --cordis-dashgrid-cols: 24; --cordis-dashgrid-row: 40px;
  position: relative; display: grid;
  grid-template-columns: repeat(var(--cordis-dashgrid-cols), minmax(0, 1fr));
  grid-auto-rows: var(--cordis-dashgrid-row); gap: var(--cordis-space-3); }
.cordis-dashgrid__item { --cordis-dashgrid-h: 1; position: relative; min-width: 0; min-height: 0;
  display: flex; flex-direction: column; }
/* The tile's content fills it: a card or a chart inside stretches to the cells it was given. */
.cordis-dashgrid__item > * { flex: 1 1 auto; min-height: 0; }
/* A card tile (the usual one) passes the height on to its body, so a chart in the body can fill it. */
.cordis-dashgrid__item > .cordis-card { display: flex; flex-direction: column; overflow: hidden; }
.cordis-dashgrid__item > .cordis-card > .cordis-card__body { flex: 1 1 auto; min-height: 0; }
.cordis-dashgrid.is-editable .cordis-dashgrid__item { cursor: grab; touch-action: none;
  border-radius: var(--cordis-radius-md); }
.cordis-dashgrid.is-editable .cordis-dashgrid__item:focus-visible { outline: none; box-shadow: var(--cordis-focus-ring); }
/* A selected tile (or one a marquee is over) wears the canvas's one emphasis ring, as a selected HMI
   widget does. */
.cordis-dashgrid.is-editable .cordis-dashgrid__item.is-selected,
.cordis-dashgrid.is-editable .cordis-dashgrid__item[data-cordis-marquee-hit] { box-shadow: var(--cdg-emph-box); }
/* A tile in the hand floats over the others, over the target that shows where it will land; while it
   glides onto its cells it keeps that layer, so it never slides under a tile it pushed. */
.cordis-dashgrid__item[data-cordis-gesture] { z-index: 2; }
/* As specific as the edit mode's grab, or that would win. */
.cordis-dashgrid.is-editable .cordis-dashgrid__item[data-cordis-gesture="move"] { opacity: 0.85; cursor: grabbing; }
.cordis-dashgrid__item[data-cordis-settling] { z-index: 2; }

/* ---------- table --------------------------------------------------------- */

.cordis-table { width: 100%; border-collapse: collapse; background: var(--cordis-surface); font-size: var(--cordis-fs-body); }
.cordis-table th {
  height: 34px; padding: 0 var(--cordis-space-4); text-align: left; white-space: nowrap;
  background: var(--cordis-surface-sunken); border-bottom: 1px solid var(--cordis-border);
  font-size: var(--cordis-fs-caption); font-weight: var(--cordis-fw-semibold);
  letter-spacing: var(--cordis-ls-caption); text-transform: uppercase; color: var(--cordis-text-muted);
}
.cordis-table th.is-sorted { color: var(--cordis-primary-text); }
/* A table inside its own scroll container keeps its header row in view. The header cell
   already paints --cordis-surface-sunken, so it covers the rows scrolling under it; the
   z-index lifts it over a cell's own positioned content (an inline editor, a pill). */
.cordis-table--sticky thead th { position: sticky; top: 0; z-index: 1; }
.cordis-table td { height: var(--cordis-row-h); padding: 0 var(--cordis-space-4); border-bottom: 1px solid var(--cordis-border); }
/* The hover travels between rows like a pane list's: CordisDataTable declares the glide on the <table>, whose
   indicator then paints the hover and the row's own stands down (--cordis-glide-row-hover). A plain-HTML table
   without the script keeps instant in, soft out: the hover arrives with the pointer and fades behind it. */
.cordis-table tbody tr { transition: background-color var(--cordis-dur) var(--cordis-ease); }
.cordis-table tbody tr:hover { background: var(--cordis-glide-row-hover, var(--cordis-surface-sunken)); transition-duration: 0s; }
.cordis-table tbody tr.is-selected { background: var(--cordis-primary-soft); }
.cordis-table .cordis-num, .cordis-table td.cordis-right { text-align: right; }
.cordis-table__actions { display: flex; gap: var(--cordis-space-2); justify-content: flex-end; opacity: 0; }
/* Column sizing (CordisColumnSize, Cordis Server #4025). The growing column takes the free width and
   ellipsizes first; a fitting column is as wide as its widest cell. `max-width: 0` is what lets the
   automatic table layout shrink the growing column below its content (its min-content counts as 0),
   and `width: 1%` against its `100%` is what shrinks a fitting column to its content. */
.cordis-table .cordis-col-grow { width: 100%; max-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cordis-table .cordis-col-fit { width: 1%; white-space: nowrap; }
/* A sharing column (CordisColumnSize.Share, a wrapping or a dragged column): as wide as its content while there
   is room, and when there is not, every sharing column gives way in proportion to its content. The cell's grid
   track does it: minmax(0, max-content) reports a min-content of 0 and a max-content of the text to the table's
   automatic layout, which then shares between those columns by their max-content. A dragged column's width is on
   its header. A number keeps its right edge (`end`). */
.cordis-table .cordis-col-share { overflow: hidden; }
.cordis-cell-clip { display: grid; grid-template-columns: minmax(0, max-content); }
.cordis-num > .cordis-cell-clip { justify-content: end; }
.cordis-cell-clip__content { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* CordisColumn.Wrap: more lines instead of an ellipsis (a long word breaks only when it cannot fit), also in a
   dense table, whose rows then grow. */
.cordis-table td.cordis-col-wrap { white-space: normal; }
.cordis-col-wrap .cordis-cell-clip__content { white-space: normal; overflow-wrap: break-word; }
.cordis-table--dense td.cordis-col-wrap { padding-block: var(--cordis-space-1); }
/* Column resizing, the splitter's look: a grab strip on the header's trailing edge whose line turns brand blue on
   hover, and while dragging the line runs down the whole column. Each cell draws its piece reaching 1px over its
   top and bottom: collapsed borders paint over a cell's background and lie half outside its padding box, so a
   background line or a clipped one breaks at every row border. Hence `overflow: visible` while dragging (a
   sharing cell's content is clipped by its own wrapper; a grow cell's text is not, so it keeps its clip until
   the first move makes it share). ew-resize, not col-resize: WebView2 draws no col-resize cursor (see
   CordisSplitter). The !important beats the headers' inline pointer cursor mid-drag. */
.cordis-table__resize { position: absolute; top: 0; right: 0; bottom: 0; width: 7px; z-index: 1;
  cursor: ew-resize; touch-action: none; }
/* At rest the handle shows where to grab: the toolbar's separator (.cordis-ptoolbar__sep -- 1px, 18px, the
   border colour), centred on the edge, so a reader sees the column boundaries without hunting for the cursor.
   The line is one element in three states, and it MOVES between them rather than swapping (D15): hover keeps the
   divider's own length and only thickens it and inks it blue, a little longer, so the affordance answers the
   pointer without outgrowing the divider it belongs to; the press stretches it from its middle to the header's
   full height, and the column below then fades in under it. The ends are inset (not `height`), because
   `height: auto` cannot be interpolated and that was the sudden full-height snap. */
.cordis-table__resize::after { content: ""; position: absolute; right: 0; width: 1px;
  top: calc(50% - min(9px, 30%)); bottom: calc(50% - min(9px, 30%));
  border-radius: var(--cordis-radius-pill); background: var(--cordis-border);
  transition: top var(--cordis-dur) var(--cordis-ease-out), bottom var(--cordis-dur) var(--cordis-ease-out),
    width var(--cordis-dur-fast) var(--cordis-ease), background-color var(--cordis-dur-fast) var(--cordis-ease); }
.cordis-table__resize:hover::after {
  top: calc(50% - min(11px, 36%)); bottom: calc(50% - min(11px, 36%));
  width: 2px; background: var(--cordis-primary); }
.cordis-table th.is-resizing .cordis-table__resize::after {
  top: 0; bottom: 0; width: 2px; border-radius: 0; background: var(--cordis-primary); }
.cordis-table td.is-resizing { position: relative; }
.cordis-table td.is-resizing:not(.cordis-col-grow) { overflow: visible; }
/* The column's line draws DOWN from the header as one stroke rather than every row's piece appearing at once.
   One progress value, --cordis-resize-sweep (0..1), is animated on the table and inherited by every cell; each
   cell knows its row and the row count (inline, from CordisDataTable, only while dragging) and fills its own
   piece by the part of the sweep that has reached it -- so the pieces join into one eased line. Registered so it
   interpolates; where @property is unsupported the sweep is just its initial 1 and the line is simply there.
   It waits for the header's line to stretch first, so the stroke reads as continuing from it. */
@property --cordis-resize-sweep { syntax: "<number>"; inherits: true; initial-value: 1; }
@keyframes cordis-col-sweep { from { --cordis-resize-sweep: 0; } to { --cordis-resize-sweep: 1; } }
.cordis-table.is-col-resizing { animation: cordis-col-sweep var(--cordis-dur-slow) var(--cordis-ease-out)
  var(--cordis-dur-fast) backwards; }
.cordis-table td.is-resizing::after { content: ""; position: absolute; top: -1px; bottom: -1px; right: 0; width: 2px;
  background: var(--cordis-primary); pointer-events: none; transform-origin: top;
  transform: scaleY(clamp(0, var(--cordis-resize-sweep) * var(--cordis-resize-rows, 1) - var(--cordis-resize-row, 0), 1)); }
/* The last row's piece stops at the table's edge: 1px past it made the table's scroll container scroll. */
.cordis-table tr:last-child td.is-resizing::after { bottom: 0; }
.cordis-table.is-col-resizing, .cordis-table.is-col-resizing * { cursor: ew-resize !important; user-select: none; }
/* A table that adapts to its own width (CordisColumn.HideBelow, Cordis Server #4025). The host -- the card or
   panel the table sits in -- is an inline-size container, so a column steps aside when the TABLE is narrow,
   whatever made it so: a narrow window, the expanded rail, a splitter pane. A viewport breakpoint cannot see
   the rail, and at 1201-1440px the expanded rail is what squeezed the Server's Controllers table sideways.
   The growing column keeps a floor there: with every fitting column at its content width, `max-width: 0`
   alone let it shrink to two letters of the name it exists to show. */
.cordis-table-host { container: cordis-table / inline-size; --cordis-table-grow-min: 12rem; }
.cordis-table-host .cordis-table .cordis-col-grow { min-width: var(--cordis-table-grow-min); }
@container cordis-table (max-width: 1119.98px) { .cordis-table .cordis-col-hide-below-xl { display: none; } }
@container cordis-table (max-width: 959.98px) { .cordis-table .cordis-col-hide-below-lg { display: none; } }
@container cordis-table (max-width: 799.98px) { .cordis-table .cordis-col-hide-below-md { display: none; } }
@container cordis-table (max-width: 639.98px) { .cordis-table .cordis-col-hide-below-sm { display: none; } }
/* The empty band fills a scroll container that has a height to give (a dashboard panel) and centres in
   it; in page flow the percentage resolves to nothing and it is just the band. */
.cordis-table-empty { min-height: 100%; box-sizing: border-box; }
/* Dense (CordisDataTable Dense="true"): a list that is scanned, not read -- a dashboard panel's commands,
   observers and settings, a message list. The dense row, small type, a half inset, and one line per row.
   The table sets the profile metrics from the dense scale (tokens.css) on ITSELF, so what sits in a row
   (a run button, an inline editor, a status pill) takes the row's size without its host restating it.
   The header row is as tall as a body row. Four hosts in Cordis Server carried their own copy at 26, 28
   and 30px (#4025). */
.cordis-table--dense { font-size: var(--cordis-fs-sm);
  --cordis-row-h: var(--cordis-dense-row-h); --cordis-control-h: var(--cordis-dense-control-h);
  --cordis-tag-h: var(--cordis-dense-tag-h); }
.cordis-table--dense th { height: var(--cordis-dense-row-h); padding: 0 var(--cordis-space-2); }
.cordis-table--dense td { padding: 0 var(--cordis-space-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A name cell with a muted second line (a controller and its machine, an address and its MCP): the pair
   stacks and ellipsizes inside the growing column instead of widening the row. The first line is
   whatever the call site puts first (a <strong>); `__sub` is the quiet line under it. */
.cordis-cell-stack { display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding: var(--cordis-space-2) 0; }
.cordis-cell-stack > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cordis-cell-stack__sub { font-size: var(--cordis-fs-xs); color: var(--cordis-text-muted); }
/* A who-and-when line (CordisMetaLine): "Edited by admin · 3 days ago" in small muted type after a 14px glyph.
   Inline is one line that ellipsizes (an editor's footer, a card); --stacked is two short lines for a narrow
   "Last change" column, the time in ink over "Edited by admin", indented past the glyph so they read as one
   entry; --full is one block entry of a history (created, then last edited) that may wrap, the name in
   semibold ink, and consecutive entries stack. */
.cordis-metaline { display: inline-flex; align-items: center; gap: var(--cordis-space-1); min-width: 0;
  max-width: 100%; font-size: var(--cordis-fs-xs); line-height: var(--cordis-lh-xs); color: var(--cordis-text-muted); }
.cordis-metaline > .cordis-icon { flex: none; }
.cordis-metaline__text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cordis-metaline--stacked { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 2px;
  white-space: nowrap; }
.cordis-metaline--stacked .cordis-metaline__when { display: inline-flex; align-items: center;
  gap: var(--cordis-space-1); color: var(--cordis-text); }
.cordis-metaline--stacked .cordis-metaline__by { padding-left: calc(14px + var(--cordis-space-1)); }
.cordis-metaline--full { display: flex; flex-wrap: wrap; }
.cordis-metaline--full + .cordis-metaline--full { margin-top: var(--cordis-space-1); }
.cordis-metaline__name { color: var(--cordis-text); font-weight: var(--cordis-fw-semibold); }
/* A path read for its leaf (CordisPathLabel): "Machine/Drum/" muted, "MaxSpeed" in semibold strong ink. The path
   gives way first (ellipsis), the leaf never does while the label fits at all; at most 52ch of its column. */
.cordis-pathlabel { display: inline-flex; max-width: min(52ch, 100%); min-width: 0; white-space: nowrap;
  vertical-align: middle; }
.cordis-pathlabel__path { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  color: var(--cordis-text-muted); }
.cordis-pathlabel__leaf { flex: 0 0 auto; color: var(--cordis-text-strong); font-weight: var(--cordis-fw-semibold); }
.cordis-table tbody tr:hover .cordis-table__actions,
.cordis-table tbody tr:focus-within .cordis-table__actions { opacity: 1; }
/* CordisDataTable.ShowRowActions: the actions at rest on every row (cordis-ide#1362). */
.cordis-table--actions-shown .cordis-table__actions { opacity: 1; }

/* Reveal-on-hover has no reveal on a touch device: there is no hover, a tap on the row is
   already the row's own click, and :focus-within only arrives once something inside is
   focused -- which cannot happen while the control is invisible. So on a pointer-less device
   the actions were permanently hidden AND still taking a full cell of every card. Show them
   outright there rather than inventing a touch affordance.

   Keyed on (hover: none) rather than on the 640px card breakpoint on purpose: a tablet and a
   touchscreen HMI panel are wide enough to keep the table a table and still have no hover. */
@media (hover: none) {
  .cordis-table__actions { opacity: 1; }
}

/* The selection bar REPLACES the toolbar; the two never show together. */
.cordis-selectionbar { display: flex; align-items: center; gap: var(--cordis-space-3);
  padding: var(--cordis-space-2) var(--cordis-space-4); background: var(--cordis-primary-soft);
  border-bottom: 1px solid var(--cordis-primary-soft-bd); color: var(--cordis-primary-text);
  font-size: var(--cordis-fs-sm); font-weight: var(--cordis-fw-semibold); }
.cordis-selectionbar__actions { margin-left: auto; display: flex; gap: var(--cordis-space-2); }

.cordis-pagination { display: flex; align-items: center; gap: var(--cordis-space-3);
  padding: var(--cordis-space-2) var(--cordis-space-4); font-size: var(--cordis-fs-xs); color: var(--cordis-text-muted); }
.cordis-pagination__pages { margin-left: auto; display: flex; gap: var(--cordis-space-1); }
.cordis-pagination__page { display: flex; align-items: center; justify-content: center; min-width: 30px; height: 30px;
  padding: 0 var(--cordis-space-2); border: 1px solid var(--cordis-border); border-radius: var(--cordis-radius-sm);
  color: var(--cordis-text); font-size: var(--cordis-fs-sm); cursor: pointer; }
.cordis-pagination__page.is-current { background: var(--cordis-primary); border-color: var(--cordis-primary);
  color: var(--cordis-text-on-fill); font-weight: var(--cordis-fw-semibold); }
/* The unavailable end of the pager. A hand-written pager has the same first and last page, so this
   belongs here rather than inline in the Blazor component. [disabled] covers a <button>;
   .is-disabled covers an <a> that cannot carry the attribute. */
.cordis-pagination__page[disabled], .cordis-pagination__page.is-disabled { opacity: 0.5; cursor: default; }

/* ---------- list toolbar -------------------------------------------------- */

.cordis-toolbar { display: flex; align-items: center; gap: var(--cordis-space-2); flex-wrap: wrap;
  padding: var(--cordis-space-2) var(--cordis-space-3); background: var(--cordis-surface-sunken); }
.cordis-viewpill { display: inline-flex; align-items: center; gap: var(--cordis-space-2); height: 28px; padding: 0 var(--cordis-space-3);
  border-radius: var(--cordis-radius-pill); border: 1px solid var(--cordis-border); background: var(--cordis-surface);
  color: var(--cordis-text); font-size: var(--cordis-fs-xs); cursor: pointer; }
.cordis-viewpill.is-active { background: var(--cordis-primary); border-color: var(--cordis-primary);
  color: var(--cordis-text-on-fill); font-weight: var(--cordis-fw-semibold); }
.cordis-filterchip { display: inline-flex; align-items: center; gap: var(--cordis-space-2); height: 32px;
  padding: 0 var(--cordis-space-3); border: 1px solid var(--cordis-primary-soft-bd); background: var(--cordis-primary-soft);
  border-radius: var(--cordis-radius-sm); color: var(--cordis-primary-text);
  font-size: var(--cordis-fs-xs); font-weight: var(--cordis-fw-semibold); }

.cordis-segmented { display: inline-flex; padding: 2px; gap: 2px;
  background: var(--cordis-neutral-soft); border-radius: var(--cordis-radius-sm); }
/* A segment is always a <button> — there is no route-per-segment case the way .cordis-tabs__item has
   one — so the UA reset is folded in here rather than being a second class the author has to know
   about. It was not, and the gap was invisible from this file: the rule styled the box and left
   `appearance`, `border`, `background` and the font family to the browser, so a hand-written
   segmented control drew the grey button chrome INSIDE the track and nothing in the markup or the
   class name suggested that the fix was `.cordis-btn-reset` as well. Reported by a consumer who hit
   exactly that (2026-08, item 6) — from the neighbouring .cordis-tabs__item comment, which documents
   the same hazard for the one control that does need the second class.
   Note the longhands rather than `font: inherit`: this rule declares its own size and weight, and the
   shorthand would reset them depending on where in the block it sat. */
.cordis-segmented__item { appearance: none; background: none; border: 0; margin: 0;
  display: flex; align-items: center; justify-content: center; height: 26px;
  padding: 0 var(--cordis-space-3); border-radius: var(--cordis-radius-sm); color: var(--cordis-text-muted);
  font-family: inherit; font-size: var(--cordis-fs-xs); font-weight: var(--cordis-fw-semibold); cursor: pointer; }
/* surface-raised: white in light as before; in dark --cordis-surface is the track's own shade and the active
   segment could not be told apart (Cordis Server, M4025). */
.cordis-segmented__item.is-active { background: var(--cordis-surface-raised); color: var(--cordis-text-strong); box-shadow: var(--cordis-shadow-sm); }

.cordis-tabs { display: flex; gap: var(--cordis-space-1); border-bottom: 1px solid var(--cordis-border); }
/* An item is an <a> when the tabs are routes and a <button> when they swap a region in place, so the
   rule has to work for both: text-decoration for the link, and the row layout its icon+label+count
   needs either way. The <button> takes .cordis-btn-reset for the rest of the UA gap. */
.cordis-tabs__item { display: inline-flex; align-items: center; gap: var(--cordis-space-2);
  padding: var(--cordis-space-2) var(--cordis-space-4); margin-bottom: -1px; text-decoration: none;
  border-bottom: 2px solid transparent; color: var(--cordis-text-muted); font-size: var(--cordis-fs-body); cursor: pointer; }
/* The hover is a rounded tile that stops short of the underline — the same gesture as a CordisTabStrip
   tab's, so the two tab families answer the pointer alike.
   With the script (CordisPageTabs) it is ONE tile that TRAVELS: the strip's hover-glide layer
   (data-cordis-hover-glide, mechanism under "glide" below) slides it from tab to tab under the pointer,
   the way the rails' hover does, instead of each tab fading in and out on its own. Its box is the hovered
   tab's, pulled in 3px at the top and 5px at the bottom so it never touches the underline.
   Without the script (plain HTML) each tab keeps a tile of its own on its ::after — instant in, soft out.
   `isolation` makes the item a stacking context, so either tile's z-index: -1 lands behind the label and
   not behind whatever card the strip sits on. */
.cordis-tabs__item { position: relative; isolation: isolate; }
/* text-decoration again here: the global a:hover underline is more specific than .cordis-tabs__item, so a route tab
   showed it on hover (Cordis Server, Mantis #4025). */
.cordis-tabs__item:hover { color: var(--cordis-text-strong); text-decoration: none; }
.cordis-tabs:not([data-cordis-hover-glide]) .cordis-tabs__item::after { content: ''; position: absolute;
  inset: 3px 0 5px; z-index: -1; border-radius: var(--cordis-radius-sm); background: transparent;
  pointer-events: none; transition: background-color var(--cordis-dur) var(--cordis-ease); }
/* The tile is a WASH of the ink colour, as the joined sheet's hover is, not a solid swatch: a strip sits on
   whatever ground its host gives it (a card, a panel bar, the page), and a solid --cordis-neutral-soft is two
   shades off a dark card, i.e. invisible. A translucent wash is the same step above any ground, dark ink on
   light and light ink on dark. (Not --cordis-hover: that one is declared only by the compat shim.) */
.cordis-tabs { --cordis-tabs-hover: color-mix(in srgb, var(--cordis-text) 7%, transparent); }
.cordis-tabs:not([data-cordis-hover-glide]) .cordis-tabs__item:hover::after {
  background: var(--cordis-tabs-hover); transition-duration: 0s; }
.cordis-tabs[data-cordis-hover-glide]::after { background: var(--cordis-tabs-hover);
  height: calc(var(--hover-glide-h) - 8px);
  transform: translate(var(--hover-glide-x), calc(var(--hover-glide-y) + 3px)); }
/* No dead space between tabs under a travelling hover. The pointer crossing a gap is over no tab, which the
   glide reads as "left the strip": the tile vanished and reappeared on the next tab instead of sliding to
   it. So the strip's gap becomes padding INSIDE each tab (half a gap a side), and the tile and the underline
   are pulled in by the same half, which keeps them exactly where the gap used to put them. */
.cordis-tabs[data-cordis-hover-glide]:not(.cordis-tabs--panel) { gap: 0; }
.cordis-tabs[data-cordis-hover-glide]:not(.cordis-tabs--panel) .cordis-tabs__item {
  padding-inline: calc(var(--cordis-space-4) + var(--cordis-space-1) / 2); }
.cordis-tabs[data-cordis-hover-glide]:not(.cordis-tabs--panel) .cordis-tabs__more {
  width: calc(36px + var(--cordis-space-1)); }
.cordis-tabs[data-cordis-hover-glide]:not(.cordis-tabs--panel)::after {
  width: calc(var(--hover-glide-w) - var(--cordis-space-1));
  transform: translate(calc(var(--hover-glide-x) + var(--cordis-space-1) / 2), calc(var(--hover-glide-y) + 3px)); }
/* The selected label turns semibold, and a bolder face is a wider one: left alone, every tab after it would
   move along by a few pixels on each selection (the panel look is semibold throughout, so it never did). The
   label reserves its semibold width up front: a hidden zero-height copy of its own text, in the semibold
   weight, shares the label's one grid column, so the column is always the bold width and the visible text
   is centred in it. The copy comes from data-label; a label without one (hand-written markup) just keeps
   the old behaviour. visibility: hidden keeps the copy out of the accessibility tree as well as off screen. */
.cordis-tabs__label { white-space: nowrap; display: inline-grid; justify-items: center; }
.cordis-tabs__label[data-label]::after { content: attr(data-label); height: 0; overflow: hidden; visibility: hidden;
  font-weight: var(--cordis-fw-semibold); pointer-events: none; user-select: none; }
/* Folding (CordisPageTabs; cordisUi.pageTabs). A strip that folds opts in with [data-cordis-overflow]: its
   items keep their natural width — a squeezed tab would be measured as fitting — and the tabs that do not fit
   are hidden by the script and listed behind the "…", which shows only while something is hidden. A strip
   without the attribute (plain HTML, no script) is untouched. */
.cordis-tabs[data-cordis-overflow] { min-width: 0; }
.cordis-tabs[data-cordis-overflow] .cordis-tabs__item { flex: none; white-space: nowrap; }
.cordis-tabs__more { display: none; position: relative; isolation: isolate; align-items: center; justify-content: center;
  flex: none; width: 36px; margin-bottom: -1px; border-bottom: 2px solid transparent;
  color: var(--cordis-text-muted); cursor: pointer; }
.cordis-tabs.has-overflow .cordis-tabs__more { display: inline-flex; }
.cordis-tabs__more:hover { color: var(--cordis-text-strong); }
.cordis-tabs__item.is-active { border-bottom-color: var(--cordis-primary); color: var(--cordis-text-strong); font-weight: var(--cordis-fw-semibold); }
/* With the glide running (CordisPageTabs; see "glide" below) the underline is ONE bar that moves to the
   tab it lands on — the indicator's bottom 2px, sized to that tab — and the tab's own border stands down. */
.cordis-tabs[data-cordis-glide]::before { height: 2px; border-radius: 0; background: var(--cordis-primary);
  transform: translate(var(--glide-x), calc(var(--glide-y) + var(--glide-h) - 2px)); }
.cordis-tabs.glide-on .cordis-tabs__item.is-active { border-bottom-color: transparent; }
.cordis-tabs[data-cordis-hover-glide]:not(.cordis-tabs--panel)[data-cordis-glide]::before {
  width: calc(var(--glide-w) - var(--cordis-space-1));
  transform: translate(calc(var(--glide-x) + var(--cordis-space-1) / 2), calc(var(--glide-y) + var(--glide-h) - 2px)); }

/* The PANEL look (CordisPageTabs Look="Panel"; was CordisPanelTabs): the view switcher in the header bar of
   a docked panel — Problems / Output / Terminal, Specify / Documentation. Same strip, same underline, same
   travelling hover; what changes is the place. It stretches to the bar's height and draws no rule of its
   own (the bar has one), the labels are small caps, and a count is the accent pill, because there it is the
   number that says "open me". The bar sizes it; the strip clips a partial tab until the script folds it. */
.cordis-tabs--panel { align-self: stretch; align-items: stretch; flex: 1 1 auto; min-width: 0; overflow: hidden;
  gap: 0; border-bottom: 0; }
.cordis-tabs--panel .cordis-tabs__item { gap: 6px; padding: 0 var(--cordis-space-3); margin-bottom: 0;
  font-size: var(--cordis-fs-caption); font-weight: var(--cordis-fw-semibold);
  letter-spacing: var(--cordis-ls-caption); text-transform: uppercase; }
.cordis-tabs--panel .cordis-tabs__item:hover,
.cordis-tabs--panel .cordis-tabs__item.is-active { color: var(--cordis-text); }
.cordis-tabs--panel .cordis-tabs__more { width: 30px; margin-bottom: 0; }

/* ---------- messages ------------------------------------------------------ */

.cordis-notice { display: flex; gap: var(--cordis-space-3); padding: var(--cordis-space-3) var(--cordis-space-4);
  border-radius: var(--cordis-radius-sm); border: 1px solid var(--cordis-info-soft-bd); background: var(--cordis-info-soft); }
.cordis-notice::before { content: ''; width: 4px; border-radius: 2px; background: var(--cordis-info); flex: 0 0 4px; }
.cordis-notice__title { font-size: var(--cordis-fs-sm); font-weight: var(--cordis-fw-semibold); color: var(--cordis-info-text); }
.cordis-notice__body  { font-size: var(--cordis-fs-sm); color: var(--cordis-text-muted); margin-top: 2px; }
.cordis-notice--warn   { background: var(--cordis-warning-soft); border-color: var(--cordis-warning-soft); }
.cordis-notice--warn::before { background: var(--cordis-warning); }
.cordis-notice--warn .cordis-notice__title { color: var(--cordis-warning-text); }
.cordis-notice--danger { background: var(--cordis-danger-soft); border-color: var(--cordis-danger-soft); }
.cordis-notice--danger::before { background: var(--cordis-danger); }
.cordis-notice--danger .cordis-notice__title { color: var(--cordis-danger-text); }
.cordis-notice--ok { background: var(--cordis-success-soft); border-color: var(--cordis-success-soft); }
.cordis-notice--ok::before { background: var(--cordis-success); }
.cordis-notice--ok .cordis-notice__title { color: var(--cordis-success-text); }

/* ---------- log view (CordisLogView) ------------------------------------- */
/* A console: a monospace scroller on the sunken surface that takes the height its host gives it (flex: 1)
   and never less than a dozen lines. The lines are the caller's markup; the stick-to-bottom behaviour is
   cordisUi.stickToBottom. Focusable, so the keyboard can scroll it (the global focus ring applies). */
.cordis-logview { flex: 1; min-height: 12rem; overflow: auto; padding: var(--cordis-space-2) 0;
  background: var(--cordis-surface-sunken); border: 1px solid var(--cordis-border); border-radius: var(--cordis-radius-md);
  font-family: var(--cordis-font-mono); font-size: var(--cordis-fs-xs); line-height: 1.5; color: var(--cordis-text); }

/* ---------- toast --------------------------------------------------------- */
/* A toast confirms what the user just did. Anything that happened while they were elsewhere is a
   notification, not a toast.

   D10 family merge #4. There is ONE toast service (CordisToastService) and both profiles use it, so
   the family it renders is the one below: a light panel with a severity stripe, a glyph, a summary,
   an optional detail line, an action and a close. Severity is information — a failed build and a
   finished deploy must not be the same grey chip — and the four values are already the suite's own
   status vocabulary. A toast that can carry a detail is what stops a caller putting the detail in
   the summary and having it truncate.

   These rules were CordisToastHost.razor.css, and everything hardcoded in it (13px, 12px, a 10px
   radius, three literal shadow stacks, --cordis-panel-bg / -hover / -text-dim from the compat
   vocabulary) is a token here. That is also the profile fix: Studio's toast now takes Studio's
   radius ladder and its flatter surfaces, which it could not while the values were literal.

   The stack is fixed to the bottom-right of the window because the host is mounted once for the
   whole document; `pointer-events: none` on the stack and `auto` on each toast is what stops the
   empty column swallowing clicks on the page behind it. --cordis-space-7 clears the status bar in
   BOTH profiles (48px Shell, 32px Studio against a 26px bar); the literal 40px it replaced had been
   measured against Studio's bar alone. */
.cordis-toasts { position: fixed; right: var(--cordis-space-4); bottom: var(--cordis-space-7);
  z-index: 1000; display: flex; flex-direction: column-reverse; gap: var(--cordis-space-2);
  pointer-events: none; }

.cordis-toast { display: flex; align-items: flex-start; gap: var(--cordis-space-2);
  width: 320px; max-width: 320px;
  padding: var(--cordis-space-2) var(--cordis-space-3);
  border: 1px solid var(--cordis-border); border-radius: var(--cordis-radius-md);
  background: var(--cordis-surface-raised);   /* box-shadow is on the stripe rule below */
  pointer-events: auto;
  animation: cordis-toast-in var(--cordis-dur) var(--cordis-ease-out); }
@keyframes cordis-toast-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to   { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .cordis-toast { animation: none; } }

/* The exit (CordisToastHost). A toast used to vanish on dismiss and the stack jumped to close the hole;
   it now fades, sinks the 8px it rose on arrival, and its slot closes up so the toasts above it glide
   down into place. The slot exists for the collapse: a one-row grid track animates 1fr → 0fr where a
   flex box of unknown height cannot, and `overflow: hidden` on it clips the toast's own padding once
   the track is shorter than it. The negative margins give back the stack's gap, half each side, so a
   closed slot is not an 8px hole either. A hand-written stack needs no slot — a toast without one
   simply has no exit, which is where every toast was before. */
.cordis-toasts__slot { display: grid; grid-template-rows: 1fr; }
.cordis-toasts__slot > .cordis-toast { min-height: 0; }
.cordis-toasts__slot.is-leaving { overflow: hidden; pointer-events: none;
  animation: cordis-toast-collapse var(--cordis-dur) var(--cordis-ease) forwards; }
.cordis-toasts__slot.is-leaving > .cordis-toast {
  animation: cordis-toast-out var(--cordis-dur-fast) var(--cordis-ease) forwards; }
@keyframes cordis-toast-out { to { opacity: 0; transform: translateY(8px); } }
@keyframes cordis-toast-collapse {
  to { grid-template-rows: 0fr; margin-block: calc(var(--cordis-space-2) / -2); } }
@media (prefers-reduced-motion: reduce) { .cordis-toasts__slot.is-leaving { display: none; } }

.cordis-toast__icon { flex: 0 0 auto; margin-top: 1px; }
.cordis-toast__body { flex: 1; min-width: 0; }
.cordis-toast__summary { font-size: var(--cordis-fs-sm); font-weight: var(--cordis-fw-semibold);
  color: var(--cordis-text); overflow-wrap: break-word; }
.cordis-toast__detail { margin-top: 2px; font-size: var(--cordis-fs-xs);
  color: var(--cordis-text-muted); overflow-wrap: break-word; }
/* The determinate bar of a sticky progress toast (CordisToastService.ShowProgress). Since D16 it is a
   CordisProgressBar (`.cordis-progress--thin`, the 3px rule further down) carrying this class, not
   markup of its own: the ARIA, the clamping and the width live in the one progress primitive, and only
   the toast's colouring and spacing live here. A rule rather than the 6px capsule: it sits inside a box
   that already carries a radius and a stripe, and a second rounded shape at this size reads as a smudge.

   It takes its colour from the same --cordis-toast-stripe custom property the severity modifiers
   below set, so a progress toast raised as a Warning needs no rule of its own — and the track is
   that colour at low alpha rather than a surface token, which is what keeps it visible in both
   themes without a second variable. Declared above that property and resolving correctly anyway:
   custom properties are substituted at computed-value time, not in source order.

   The fill transitions its width so a coarse report (n of m, m small) glides rather than steps — at
   the toast's quicker --cordis-dur rather than the page bar's --cordis-dur-slow, because a toast
   reports in steps a reader is waiting on — and does not when the user asked for reduced motion: a
   bar creeping across a corner is exactly the continuous movement that preference is about. */
/* Compounded with .cordis-progress because that rule sits further down this file at the same
   specificity and would otherwise win the track's background back. */
.cordis-progress.cordis-toast__progress { margin-top: var(--cordis-space-2);
  background: color-mix(in srgb, var(--cordis-toast-stripe) 22%, transparent); }
.cordis-toast__progress .cordis-progress__bar { background: var(--cordis-toast-stripe);
  transition-duration: var(--cordis-dur); }
@media (prefers-reduced-motion: reduce) {
  .cordis-toast__progress .cordis-progress__bar { transition: none; } }

.cordis-toast__action { display: inline-block; margin-top: var(--cordis-space-1);
  padding: 0; border: 0; background: transparent; color: var(--cordis-primary-text);
  font-size: var(--cordis-fs-xs); font-weight: var(--cordis-fw-semibold); cursor: pointer; }
.cordis-toast__action:hover { text-decoration: underline; }
.cordis-toast__close { flex: 0 0 auto; display: inline-flex; align-items: center;
  justify-content: center; width: 20px; height: 20px; margin: -2px -4px 0 0; padding: 0;
  border: 0; border-radius: var(--cordis-radius-pill); background: transparent;
  color: var(--cordis-text-muted); cursor: pointer;
  transition: background var(--cordis-dur-fast) var(--cordis-ease),
              color var(--cordis-dur-fast) var(--cordis-ease); }
.cordis-toast__close:hover { background: var(--cordis-surface-sunken);
  color: var(--cordis-text); }

/* The severity stripe: an inset 2px edge rather than a border, so it cannot change the box's
   geometry and cannot fight the border above it. Declared through a custom property so a variant
   is one declaration and the three-layer shadow is written once — the previous version restated
   the whole stack four times, which is how one of the four had drifted. */
.cordis-toast { --cordis-toast-stripe: transparent;
  box-shadow: inset 2px 0 0 var(--cordis-toast-stripe), var(--cordis-shadow-lg); }
.cordis-toast--success { --cordis-toast-stripe: var(--cordis-success); }
.cordis-toast--warning { --cordis-toast-stripe: var(--cordis-warning); }
.cordis-toast--error   { --cordis-toast-stripe: var(--cordis-danger); }
.cordis-toast--info    { --cordis-toast-stripe: var(--cordis-info); }

/* The Shell pill, kept rather than deleted. It is the Material snackbar and it is not what the
   service renders, but it is the right shape for the one-line "Deployed to Plant-01 · Undo"
   confirmation a Shell page raises inline, so a Shell consumer may opt into it. What it is NOT any
   more is an inverse surface faked out of the text ramp: it reads --cordis-inverse-bg / -text,
   which flip with the theme, so the version that was invisible in dark mode is gone.

   A modifier and not a second family, so `grep cordis-toast` finds every toast in the suite. */
.cordis-toast--pill { --cordis-toast-stripe: transparent;
  align-items: center; width: auto; max-width: min(480px, 92vw);
  padding: var(--cordis-space-3) var(--cordis-space-4);
  border-color: transparent; border-radius: var(--cordis-radius-sm);
  background: var(--cordis-inverse-bg); color: var(--cordis-inverse-text);
  font-size: var(--cordis-fs-sm); }
.cordis-toast--pill .cordis-toast__summary,
.cordis-toast--pill .cordis-toast__detail { color: inherit; }
.cordis-toast--pill .cordis-toast__action { margin: 0 0 0 auto; color: inherit;
  font-size: inherit; }

/* ---------- empty state --------------------------------------------------- */
/* D10 family merge #5: one family, two variants, and neither of the two halves was wrong — they
   were answering different questions. A workbench panel that is empty must not draw a box inside a
   box; the pane already IS the box, so the default fills its host and centres in it. A PAGE with
   three sections, one of them empty, needs the opposite: an edge, so the absence reads as a
   placeholder rather than as a rendering fault. That is --boxed.

   The default is the component's band (CordisEmptyState.razor.css until the merge, moved here for
   the same #218 reason the danger zone was) and the boxed variant is the retired `.cds-empty`.
   Filling the host means the host must be a flex column with a height to give; every bottom-panel
   view in Studio is one.

   The three hardcoded sizes the component carried (13.5px, 12.5px, and 24/40px padding) are tokens
   now, which is what makes this respond to the profile switch at all. 40px was not on the 4px
   grid — the nearest step is --cordis-space-6, 32px — and the vertical stays the smaller value
   because vertical is the constrained axis: the bottom dock is short. */
.cordis-empty { flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--cordis-space-2);
  padding: var(--cordis-space-5) var(--cordis-space-6);
  /* --cordis-text-muted, not the legacy --cordis-text-dim the scoped sheet read: only the compat
     shim declares that name, and an unresolvable var() drops the whole declaration silently, so a
     plain-HTML consumer got an empty state in body-text colour. Same value either way. */
  text-align: center; color: var(--cordis-text-muted);
  /* The band is prose wherever it sits: an empty log console or code view must not hand it its mono. */
  font-family: var(--cordis-font); }

/* Fill (CordisEmptyState Fill="true"): the band takes the whole free height of a scroll container and
   centres in it -- a dashboard panel's body, a picker's tree, a table's frame -- whether the host is a
   flex column (the band's own `flex: 1`) or a plain block (the percentage). The table's built-in empty
   state is the same thing (`.cordis-table-empty`, above). */
.cordis-empty--fill { min-height: 100%; box-sizing: border-box; }

/* Compact (CordisEmptyState Compact="true"): the absence inside a card or a form section -- "No zones
   yet", "Please select an argument" -- where there is no pane to fill. It keeps its content height, a
   small inset and small type, so it reads as a quiet line in the section rather than a pane's band.
   Written `.cordis-empty.cordis-empty--compact` to outrank the band's padding and gap regardless of
   order. Cordis Server had grown five local copies of this line (#4025). */
.cordis-empty.cordis-empty--compact { flex: 0 0 auto; gap: var(--cordis-space-1);
  padding: var(--cordis-space-2) var(--cordis-space-3); font-size: var(--cordis-fs-sm); }

/* The variant for page flow: an edge and a sunken ground, sized by its content rather than filling
   a pane. `flex: 0 1 auto` undoes the default's `flex: 1` — a boxed empty state inside a flex
   column would otherwise eat the whole column. */
.cordis-empty--boxed { flex: 0 1 auto;
  border: 1px dashed var(--cordis-border-strong); border-radius: var(--cordis-radius-md);
  background: var(--cordis-surface-sunken); }

/* A measure, not a pixel width. The three copies CordisEmptyState replaced had drifted to
   52ch / 46ch / 360px. */
.cordis-empty p { margin: 0; font-size: var(--cordis-fs-sm); max-width: 52ch; }
.cordis-empty strong { color: var(--cordis-text); }

/* Both written `.cordis-empty .cordis-empty__x` rather than bare, and that is not decoration:
   `.cordis-empty p` above is 0-1-1 and would beat a bare 0-1-0 class on the <p> that carries the
   title. Same trap as any element-plus-class pair in a shared family. */
.cordis-empty .cordis-empty__title { font-size: var(--cordis-fs-body);
  font-weight: var(--cordis-fw-semibold); color: var(--cordis-text-strong); }
.cordis-empty .cordis-empty__body { max-width: 40ch; color: var(--cordis-text-muted);
  text-wrap: pretty; }

/* An inline link-button continuing the sentence above it ("…or Configure deployment"), not a
   toolbar control — which is why it is deliberately not a .cordis-btn. No local focus treatment:
   this is a single <button>, so the suite's universal :focus-visible ring is already the right ring
   on the right box, and the rule that used to be here drew a second one.

   --cordis-primary-text, not the bare --cordis-blue the scoped sheet read: this is a text-sized
   link, and blue-500 on a sunken ground is the 3.1:1 case D1-D5 settled by giving link and label
   text its own semantic token. The fill colour is unchanged; only text moved. */
.cordis-empty__action { padding: 0; border: 0; background: transparent;
  color: var(--cordis-primary-text); font: inherit; font-size: var(--cordis-fs-xs);
  font-weight: var(--cordis-fw-semibold); cursor: pointer; }
.cordis-empty__action:hover { text-decoration: underline; }

/* ---------- overlays ------------------------------------------------------ */

/* The z-index is what puts a modal or a drawer above the off-canvas rail, which
   is z-index 40 in the small-screen block at the foot of this file. Without it a
   drawer opened on a phone renders *behind* the nav drawer that opened it. */
.cordis-scrim { position: fixed; inset: 0; z-index: var(--cordis-z-scrim); background: var(--cordis-overlay);
  animation: cordis-fade-in var(--cordis-dur) var(--cordis-ease-out); }
/* The backdrop is the keyboard host of what it covers (it takes focus so Escape reaches it); the ring
   belongs on the control inside, never around the whole viewport. */
.cordis-scrim:focus, .cordis-scrim:focus-visible { outline: none; box-shadow: none; }
/* The two Blazor service layers (decision D16, CordisOverlay): the same backdrop one rung up. A dialog
   from CordisDialogService sits over a page's own modal or drawer; a prompt or confirm sits
   over an open dialog, because a dialog asks its own questions. */
.cordis-scrim--dialog { z-index: var(--cordis-z-dialog); }
.cordis-scrim--prompt { z-index: var(--cordis-z-prompt); }
/* --end docks the panel against the right edge instead of centring it, so a
   right-side drawer and a centred modal share one backdrop rule. */
.cordis-scrim--end { display: flex; justify-content: flex-end; }
/* --clear: no dimming (CordisDrawer Dim="false"); the host emphasises what the panel is about itself. */
.cordis-scrim--clear { background: transparent; }
/* --center is the modal's backdrop (CordisModal): the panel centred, with a gutter
   so it never touches the viewport edge. */
.cordis-scrim--center { display: flex; align-items: center; justify-content: center;
  padding: var(--cordis-space-4); }
/* The width is a knob (--cordis-modal-w) for the same reason as the drawer's: an
   inline width would beat the phone rule below. The panel is a column capped to the
   viewport: head and foot keep their height and only the body scrolls, so a long form
   cannot push its buttons off-screen and the scrollbar runs between head and foot,
   not past the foot. (The panel's own overflow is the fallback for unpadded content
   that brings no scrolling body of its own.) */
.cordis-modal { --cordis-modal-w: 520px;
  display: flex; flex-direction: column;
  background: var(--cordis-surface); border-radius: var(--cordis-radius-lg);
  box-shadow: var(--cordis-shadow-lg); max-width: var(--cordis-modal-w); width: 100%;
  max-height: 100%; overflow-y: auto;
  animation: cordis-modal-in var(--cordis-dur) var(--cordis-ease-out); }
.cordis-modal__head { display: flex; flex: none; align-items: center; gap: var(--cordis-space-3);
  padding: var(--cordis-space-4); border-bottom: 1px solid var(--cordis-border); }
.cordis-modal__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--cordis-space-4); }
.cordis-modal__foot { display: flex; flex: none; gap: var(--cordis-space-2); justify-content: flex-end;
  padding: var(--cordis-space-4); border-top: 1px solid var(--cordis-border); }
/* An unpadded modal whose content is a form (an EditForm around its own head, body and
   foot, so Save can be a submit button): the form joins the column, so its body is the
   part that scrolls. A wrapper of the consumer's own between the form and the three
   parts takes the same four declarations. */
.cordis-modal > form { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
/* CordisModalFrame: an editor's own head / body / foot inside an unpadded CordisModal. The frame is a flex column
   that fills the panel (the panel is a flex column itself), and the BODY is the only scroll box: the scrollbar
   runs between head and foot and never past them, and a long form never pushes Save off-screen. No sticky
   bands: a sticky foot sits inside the scroll box, so its scrollbar spanned the foot too. The editor's EditForm
   <form> between the panel and the frame fills the same way. The body is a column with the form rhythm; a
   CordisPageSection in it drops its page margin (the gap spaces sections) and spaces its own content the same
   way. Three classes: the section's scoped rule is two. */
.cordis-modalframe, form:has(> .cordis-modalframe) { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.cordis-modalframe__head, .cordis-modalframe__foot { flex: none; }
.cordis-modalframe__foot { align-items: center; }
.cordis-modalframe__title { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cordis-modalframe__body { flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--cordis-space-4); }
.cordis-modal__body.cordis-modalframe__body .cordis-page-section,
.cordis-drawer__body.cordis-modalframe__body .cordis-page-section { display: flex; flex-direction: column;
  gap: var(--cordis-space-4); margin-block-end: 0; }
.cordis-modal__body.cordis-modalframe__body .cordis-page-section__head,
.cordis-drawer__body.cordis-modalframe__body .cordis-page-section__head { margin: 0; }
/* The foot's start (an audit line) takes the free width and gives way first. */
.cordis-modalframe__meta { flex: 1; min-width: 0; overflow: hidden; }
@keyframes cordis-modal-in { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: none; } }
/* A backdrop dims the page in, rather than dropping on it — under a modal, a drawer, a service dialog. */
@keyframes cordis-fade-in { from { opacity: 0; } }

/* A popup ARRIVES (the motion pass, /decisions/motion): a menu, a select's list, a picker fades in over
   --cordis-dur-fast while settling 4px toward the side it opened from — down from a trigger above it,
   up when the placement pass flipped it above (which writes data-placement="above"). Its close stays
   instant: a dismissed menu that lingers reads as slow, and native menus close at once.

   MARGIN, NOT TRANSFORM, and that is load-bearing rather than a preference. A transformed element is the
   containing block for its position:fixed descendants, and a menu's flyouts are exactly that — so a
   transform held for even one entrance would re-base every coordinate placeMenuFlyouts writes while it
   runs, and a context menu opened under the pointer hovers its first row in the first frame. That is the
   2026-08 flicker, documented at cordisUi.placeContextMenu. A 4px margin on a fixed or absolute panel
   moves its box and re-bases nothing. */
@keyframes cordis-pop-down { from { opacity: 0; margin-top: -4px; } }
@keyframes cordis-pop-up   { from { opacity: 0; margin-top: 4px; } }
/* The handoff (cordisUi.menuHandoff): a submenu or menu-bar dropdown that replaces its sibling skips the
   entrance above and starts where the sibling was — the script writes the offset as --handoff-dx / -dy —
   then slides into place, already opaque. A margin, for the same containing-block reason. */
:where([data-cordis-handoff]) { --handoff-dx: 0px; --handoff-dy: 0px; }
@keyframes cordis-handoff { from { margin-left: var(--handoff-dx); margin-top: var(--handoff-dy); } }

/* ---------- drawer -------------------------------------------------------- */
/* The right-side slide-over: a record opened over the list it came from, so the
   list stays on screen and closing the drawer is a return to where you were
   rather than a navigation. The most-used chrome element in the CRM this whole
   profile was distilled from.

   Deliberately not a variant of .cordis-modal. A modal is a centred question that
   interrupts; a drawer is a second surface beside the first. They differ in
   geometry, in entry direction, and in scrolling — a drawer is its own scroll
   box with a sticky head, because the thing it holds is a long form. */
/* The width is a declared knob rather than a literal so a caller can widen one
   drawer with an inline `--cordis-drawer-w` and the phone rule at the foot of this
   file still wins over it — an inline `width` would not lose to a media query. */
.cordis-drawer { --cordis-drawer-w: min(620px, 94vw);
  display: flex; flex-direction: column; width: var(--cordis-drawer-w); height: 100%;
  background: var(--cordis-surface); box-shadow: var(--cordis-shadow-lg); overflow-y: auto;
  animation: cordis-drawer-in var(--cordis-dur) var(--cordis-ease-out); }
@keyframes cordis-drawer-in { from { opacity: 0; transform: translateX(var(--cordis-space-5)); } to { opacity: 1; transform: none; } }

/* Sticky, not fixed: the head belongs to the panel's own scroll box, so the title
   of the record stays put while its form scrolls under it. */
.cordis-drawer__head { position: sticky; top: 0; z-index: 1; display: flex; align-items: flex-start;
  gap: var(--cordis-space-3); padding: var(--cordis-space-4);
  background: var(--cordis-surface); border-bottom: 1px solid var(--cordis-border); }
.cordis-drawer__title { font-size: var(--cordis-fs-h3); line-height: var(--cordis-lh-h3);
  font-weight: var(--cordis-fw-semibold); color: var(--cordis-text-strong); margin: 0; }
.cordis-drawer__sub { font-size: var(--cordis-fs-sm); color: var(--cordis-text-muted); margin: 0; }
/* The title block (CordisDrawer's own head): the title line — the title and its badge, wrapping — over the
   subtitle. min-width 0 lets a long title give way to the close button instead of pushing it off the panel. */
.cordis-drawer__heading { min-width: 0; }
.cordis-drawer__titleline { display: flex; align-items: center; gap: var(--cordis-space-2); flex-wrap: wrap; }
.cordis-drawer__close { display: inline-flex; align-items: center; justify-content: center; flex: none;
  margin-left: auto; width: var(--cordis-control-h); height: var(--cordis-control-h);
  appearance: none; border: 0; border-radius: var(--cordis-radius-sm);
  background: none; color: var(--cordis-text-muted); cursor: pointer; }
.cordis-drawer__close:hover { background: var(--cordis-surface-sunken); color: var(--cordis-text-strong); }
/* flex: 1 rather than auto height, so a short body still pushes the foot to the
   bottom of the panel instead of leaving it floating mid-panel. */
.cordis-drawer__body { flex: 1; padding: var(--cordis-space-4);
  display: flex; flex-direction: column; gap: var(--cordis-section-gap); }
/* Fill (CordisDrawer.Fill): the panel does not scroll; the body is exactly the height between head and foot,
   and its content scrolls inside it where it chooses. */
.cordis-drawer--fill { overflow: hidden; }
.cordis-drawer--fill > .cordis-drawer__body { min-height: 0; overflow: hidden; }
/* Framed (CordisDrawer Padded=false + a CordisModalFrame): the panel does not scroll, the frame's body does, so the
   scrollbar runs between head and foot as in a modal; the body keeps the modal's form rhythm, and the head centres
   the frame's glyph on the title. */
.cordis-drawer--framed { overflow: hidden; }
.cordis-drawer__body.cordis-modalframe__body { gap: var(--cordis-space-4); }
.cordis-drawer__head.cordis-modalframe__head { align-items: center; }
.cordis-drawer__foot { position: sticky; bottom: 0; display: flex; gap: var(--cordis-space-2);
  justify-content: flex-end; padding: var(--cordis-space-4);
  background: var(--cordis-surface); border-top: 1px solid var(--cordis-border); }

/* ---------- menu panel ---------------------------------------------------- */
/* D10 family merge #3, and it is the one merge that was refused. `.cds-menu` became
   `.cordis-menupanel` — a DIFFERENT WORD for a different thing — rather than folding onto the
   component's `.cordis-menu`, which is the same answer `shell` got.

   What is below is a menu's APPEARANCE: panel, row, separator, shortcut slot, and nothing else. A
   menu is not a look. It is dismissal, roving focus shared between keyboard and pointer, viewport
   fitting, unbounded nesting, and a flyout that has to escape its own scroll box — all of which
   live in CordisMenuFlyout and cordisUi.placeMenuFlyouts, and none of which any static rule can
   carry. Merging the names would invite someone to reach for the CSS and ship something that traps
   focus.

   The rename was not optional either. Both halves would otherwise own `.cordis-menu__item`, and the
   component's copy survives only because Blazor's scoped CSS appends an attribute selector to it —
   a merge that is correct only by specificity accident, which breaks the day someone lifts a rule
   into app.css. That is precisely the D10 trap.

   Who this is for: a plain-HTML consumer with a short, static list of actions — an HMI panel with
   four buttons — that cannot run a Blazor component. If you are in Blazor, use CordisContextMenu.
   `position` is deliberately not set here, because the only thing that knows where a panel goes is
   whatever anchors it. */
.cordis-menupanel { min-width: 200px; padding: var(--cordis-space-1) 0;
  background: var(--cordis-surface); border: 1px solid var(--cordis-border);
  border-radius: var(--cordis-radius-md); box-shadow: var(--cordis-shadow-lg); }
.cordis-menupanel__item { display: flex; align-items: center; gap: var(--cordis-space-3);
  padding: var(--cordis-space-2) var(--cordis-space-3); font-size: var(--cordis-fs-sm);
  color: var(--cordis-text); cursor: pointer; }
.cordis-menupanel__item:hover { background: var(--cordis-surface-sunken); }
.cordis-menupanel__item[aria-disabled='true'] { color: var(--cordis-n-300); cursor: default; }
.cordis-menupanel__item--danger { color: var(--cordis-danger-text); }
.cordis-menupanel__item--danger:hover { background: var(--cordis-danger-soft); }
.cordis-menupanel__sep { height: 1px; margin: var(--cordis-space-1) 0;
  background: var(--cordis-border); }
/* A labelled group inside a panel, and it is NOT __sep with words in it. __sep is a 1px rule; giving
   it text collapses the label into a hairline and overlaps the row beneath. Two shapes because they
   answer different questions: a rule says "these are unrelated", a group says "these are the same
   KIND". A grouped panel needs the second — a notification tray splits Today / Earlier, a command
   palette splits Companies / Deals / Actions, and both were writing their own label because this
   was missing. */
.cordis-menupanel__group { padding: var(--cordis-space-2) var(--cordis-space-3) var(--cordis-space-1);
  font-size: var(--cordis-fs-caption); letter-spacing: var(--cordis-ls-caption);
  text-transform: uppercase; font-weight: var(--cordis-fw-semibold);
  color: var(--cordis-text-muted); }
/* A row that has not been read: a dot in the gutter and the faintest accent wash. Read rows keep
   their place and lose both, so the list never reflows as you read it. */
.cordis-menupanel__item--unread { background: var(--cordis-primary-soft); }
.cordis-menupanel__dot { flex: 0 0 auto; width: 6px; height: 6px;
  border-radius: var(--cordis-radius-pill); background: var(--cordis-primary); }
/* `inherit`, not `visible`: a declared `visible` on a descendant overrides an ancestor's `hidden`, and
   this panel is a raised overlay whose host may render it hidden while it measures itself (the flicker
   documented at the icon FOUT guard in app.css). `inherit` still reverses the `--spent` default below. */
.cordis-menupanel__item--unread .cordis-menupanel__dot { visibility: inherit; }
.cordis-menupanel__dot--spent { visibility: hidden; }
/* The shortcut slot. Its keys are .cordis-kbd caps (below), one per key; plain text in it still
   renders in the caps' mono voice for markup written before the caps existed. */
.cordis-menupanel__key { margin-left: auto; display: inline-flex; align-items: center;
  font-family: var(--cordis-font-mono); font-size: var(--cordis-fs-caption);
  color: var(--cordis-text-muted); }

/* ---------- pop-out motion (.cordis-popover) ------------------------------ */
/* ONE entrance and ONE exit for the small surfaces that pop out of a trigger and close through
   CordisPopoverState: the user menu, the select's list, the colour picker, the date pickers.

   The entrance is the motion pass's (cordis-pop-down / -up above: margin, never a transform, for the
   containing-block reason given there), keyed on the data-placement the placement pass writes, so a list
   that flipped above its button rises instead of dropping.

   The exit is `.is-closing`, which CordisPopoverState puts on the surface for
   CordisPopoverState.ExitDuration before the component unmounts it (Cordis Server, Mantis #4025: the
   motion pass left an exit fade open, and the Server's pickers asked for one). It is a fade only, shorter
   than the entrance, and stops taking the pointer at once, so a click during it lands on the page
   underneath. Reduced motion needs nothing here: the rule at the foot of this file collapses both to an
   instant, and `both` keeps the exit's end state. The context menu and the menu bar are not on it: their
   Hide is synchronous, so they keep the motion pass's instant close. */
.cordis-popover { animation: cordis-pop-down var(--cordis-dur-fast) var(--cordis-ease-out); }
.cordis-popover[data-placement="above"] { animation-name: cordis-pop-up; }
.cordis-popover.is-closing { pointer-events: none;
  animation: cordis-popover-out var(--cordis-dur-fast) var(--cordis-ease) both; }
@keyframes cordis-popover-out { to { opacity: 0; } }

/* ---------- tooltip ------------------------------------------------------- */
/* Only for icon-only controls and truncated text — never to carry information that is not on the
   screen.

   D10 family merge #6, and the two halves really were one chip drawn twice. The version that
   survived is the LIVE one: it was in the RCL's app.css, where nothing about it belonged — the
   engine is `cordisUi` delegating on the `title` attribute in cordis-ui.js, and an HMI panel
   loading the asset layer used to get the class and never the engine's chrome. The deleted half
   (`.cds-tooltip`) also had the toast's defect: an inverse surface built out of the TEXT ramp,
   which is white-on-white in dark mode.

   There is no class at the call site. You write `title="…"`, the engine stashes it so the OS
   bubble does not fire as well, and it moves ONE shared element per document. Which is why this is
   `position: fixed` with a z-index above every overlay: it is a sibling of <body>'s content, not a
   child of the thing it describes. */
.cordis-tooltip { position: fixed; z-index: 3000; top: 0; left: 0; pointer-events: none;
  /* TWO SLOTS, a label and an optional key combo, laid out the way a menu row lays them out — the
     combo beside the label in its own muted voice, never "Add Submachine reference (6)" inside the
     sentence. `baseline` rather than `center` so the small combo sits on the label's last line, which
     is what the eye expects when the label wraps to two. The engine builds both spans (cordis-ui.js);
     a chip with no combo is one span and looks exactly as it always did. */
  display: flex; align-items: baseline; gap: 12px;
  /* cordis-ui.js raises the chip into the top layer with popover="manual" so it paints above
     the menus and dropdowns that are raised the same way (z-index alone always loses to the
     top layer). These two undo the only UA popover defaults that would show: `margin: auto`
     against our written top/left, and `overflow: auto`, which would put a scrollbar in a chip
     that the max-width plus overflow-wrap below already wraps. The chip declares its own
     padding/border/background, so it needs none of the rest of the .cordis-overlay reset. */
  margin: 0; overflow: visible;
  /* Wide enough for a real Windows model path on two or three lines; the JS placement pass clamps
     to an 8px viewport margin, so this is a ceiling and never the actual width. */
  max-width: 460px; padding: 5px 9px;
  font-family: var(--cordis-font); font-size: var(--cordis-fs-xs); line-height: 1.45;
  font-weight: var(--cordis-fw-medium); letter-spacing: 0.1px;
  color: var(--cordis-tooltip-fg); background: var(--cordis-tooltip-bg);
  border: 1px solid var(--cordis-tooltip-border); border-radius: var(--cordis-radius-sm);
  /* --cordis-shadow-lg rather than the two literal stacks this carried: it is the same overlay
     elevation .cordis-modal and .cordis-drawer take, and D9 keeps a shadow on overlays in both
     profiles because an overlay has to read as being above what it covers. */
  box-shadow: var(--cordis-shadow-lg);
  white-space: pre-line;          /* honour the few "line1\nline2" titles, e.g. a path */
  /* A Windows path offers NO break opportunity — `\` is not one, and neither is a long folder
     name — so without this the text ran out of the chip and was clipped mid-segment. `anywhere`
     rather than `break-word` so the break may also shrink the box's intrinsic min-content width,
     not only wrap inside the max-width. */
  overflow-wrap: anywhere;
  opacity: 0; transform: translateY(3px) scale(0.97); transform-origin: top center;
  transition: opacity var(--cordis-dur) var(--cordis-ease-out),
              transform var(--cordis-dur) var(--cordis-ease-out);
  will-change: opacity, transform; }
/* Flip the rise direction when the chip sits above its target. */
.cordis-tooltip[data-placement='top'] { transform: translateY(-3px) scale(0.97);
  transform-origin: bottom center; }
.cordis-tooltip.is-visible { opacity: 1; transform: translateY(0) scale(1); }
/* The label half. It carries the wrapping the chip declares, because a flex ITEM does not inherit its
   container's `white-space` behaviour for layout purposes: min-width: 0 is what lets a long path shrink
   and wrap inside the chip's max-width instead of forcing the box wider than it. */
.cordis-tooltip__label { min-width: 0; }
/* A keyboard hint inside a chip, e.g. "Toggle the terminal  Ctrl+`". Same keycaps as a menu row's
   shortcut column (.cordis-menu__shortcut) — so a modeller reads the two surfaces as one vocabulary —
   re-inked for the chip, which is dark in both themes. It never wraps and never shrinks; the label
   gives way first. */
.cordis-tooltip__kbd { flex: 0 0 auto; display: inline-flex; align-items: center;
  white-space: nowrap; }
.cordis-tooltip__kbd .cordis-kbd { color: var(--cordis-tooltip-kbd);
  background: transparent; border-color: var(--cordis-tooltip-kbd-border); }

/* ---------- keycap (.cordis-kbd) ------------------------------------------ */
/* One key of a shortcut, drawn as a bordered cap: <kbd class="cordis-kbd">Ctrl</kbd>. A combo is one
   cap per key, never "Ctrl+Shift+P" as one run of text — the caps are what make a hint read as keys
   at a glance, in a menu row, a tooltip chip or a palette footer ("Close [Esc]").

   The "+" between keys and the space between a chord's strokes stay in the DOM as .cordis-kbd-sep,
   visually hidden, so the text a screen reader announces (and a copy picks up) is still the binding:
   CordisMenuFlyout and the tooltip engine both emit them. Hand-written markup may leave them out.
   The spacing between caps belongs to the CAP (its left margin), never to the container: 6px between
   the keys of one stroke, 10px before a chord's second stroke. A container that adds a flex `gap` of
   its own puts its gap on top of that, which is how a menu row, a tooltip and a demo once showed
   three different spacings for one combo — so a container holding caps declares no gap. */
/* A whole combo (CordisKbd's root): the caps on one line, centred on whatever row holds them. */
.cordis-kbd-combo { display: inline-flex; align-items: center; flex: 0 0 auto; white-space: nowrap; }
.cordis-kbd { display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; min-width: 18px; height: 18px; padding: 0 4px; margin: 0;
  font-family: var(--cordis-font-mono); font-size: var(--cordis-fs-caption);
  font-weight: var(--cordis-fw-medium); font-style: normal; line-height: 1; white-space: nowrap;
  color: var(--cordis-text-muted); background: var(--cordis-surface);
  border: 1px solid var(--cordis-border-strong); border-radius: calc(var(--cordis-radius-sm) - 2px); }
.cordis-kbd + .cordis-kbd,
.cordis-kbd-sep + .cordis-kbd { margin-left: 6px; }
.cordis-kbd-sep--chord + .cordis-kbd { margin-left: 10px; }
.cordis-kbd-sep { position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; }
/* ---- The drag ghost (issue #657) ----------------------------------------------------------------
   The thing that follows the cursor while something is being dragged. It exists because the drag is
   OURS now rather than the browser's: HTML5 drag and drop is inert in the desktop head (the WinUI 3
   host never enters the OS drag loop, so `dragstart` is followed straight by `dragend`), so
   cordis-ui.js drives the gesture from pointer events and there is no native drag image to inherit.

   JS-owned and global on purpose, like the tooltip chip above: the element is a CLONE of whatever is
   being dragged, appended to <body>, so it already carries the source component's scoped-CSS
   attribute and needs nothing here but the card and the lift. Which is why neither rule below sets a
   TEXT colour — a dragged tree row must keep looking like a tree row, and a dragged dialog row like a
   dialog row.

   Two rules, and the split is the point (issue #23): .cordis-drag-carried is WHAT THE CARRIED ITEM
   LOOKS LIKE and .cordis-drag-ghost is WHERE THE TRAVELLING COPY OF IT IS. The travelling clone wears
   both; a tree's open slot wears the first alone, because a snapped item is the same object drawn in
   a different place. */
/* THE CARRIED ITEM'S APPEARANCE (issue #23) - one rule set, both states.
   The item being dragged has ONE representation with two states (issue #20): it either follows the
   cursor or it snaps into the list it would land in. Those are two PLACES, not two things, so they
   are drawn from this one rule and nothing else may restate it. Before #23 the snapped state wore
   issue #19's placeholder treatment - a blue dashed outline over a pale blue fill - which is a
   picture of "a slot" rather than of the object in the hand; the maintainer's words on seeing both
   were "I'd rather not use the blue dotted one though for placement, just use the same styling as
   for the dragged image".

   Worn by cordis-ui.js's travelling clone (added beside .cordis-drag-ghost, which carries only the
   TRANSPORT half below) and by CordisTreeNode's open slot. Deliberately global rather than scoped:
   two components in two projects render it, which is exactly the shape SharedCssContractTests
   protects. Colours are the only thing it does NOT set beyond the card itself - the travelling image
   is a clone that must keep its source's own text colour.

   --cordis-shadow-md and not -lg, which is what the travelling image alone used to carry, and the
   one property where "in the list" and "over the page" genuinely differ. In Studio -lg is
   `0 6px 16px` - under a 26px row that washes the whole row beneath the slot, so the card reads as
   hovering ABOVE the list rather than sitting in it, which is the opposite of what the snapped state
   says. -md (`0 2px 6px` in Studio) still lifts the card clear of the row band, and the 1px border
   is what makes the edge legible at every indent depth in both themes rather than leaving the
   shadow to do that work alone. */
.cordis-drag-carried {
  box-sizing: border-box;
  opacity: 0.9;
  border: 1px solid var(--cordis-border);
  border-radius: var(--cordis-radius-sm);
  background: var(--cordis-surface-raised);
  box-shadow: var(--cordis-shadow-md); }

/* The TRANSPORT half: where the travelling image is, never what it looks like. Everything visual is
   .cordis-drag-carried above, which the snapped row wears too. */
.cordis-drag-ghost { position: fixed; left: 0; top: 0; z-index: 4000;
  /* Never a hit-test target: the emulator reads what is UNDER the cursor with elementFromPoint on
     every move, and a ghost that could be hit would answer every one of those with itself. */
  pointer-events: none;
  margin: 0;
  /* A slight lift so the ghost reads as being off the surface rather than as a duplicated row. */
  transform: scale(1.02); transform-origin: top left;
  /* Fades rather than blinks when the item snaps into a list and back out again (issue #20). --fast,
     because this is a state change the user is causing with their own hand: anything slower and the two
     states overlap, which is momentarily the double-draw this replaced. */
  transition: opacity var(--cordis-dur-fast) var(--cordis-ease); }

/* THE ITEM HAS SNAPPED INTO A LIST (issue #20), so the list is drawing it and the hand is empty.
   One item, one representation, in two states - near a valid insertion point the surface draws the row in
   place at the depth and position it would land at, away from one this travelling image is the only copy.
   Drawing both is what issue #19 shipped and what this rule ends: the same row in two places at once,
   with the reader left to work out that they are one thing.

   `opacity: 0` and not `display: none`: the transport writes left/top onto this element on every pointer
   move, and it has to keep being the same element with the same box when the item comes free again -
   removing it from the layout would make the return a re-layout rather than a fade. It is already
   `pointer-events: none`, so an invisible ghost hit-tests as nothing either way.

   NOTHING replaces it under the cursor - see cordis-ui.js's setSnapped for that decision written out. */
.cordis-drag-ghost--snapped { opacity: 0; }

/* A TREE ROW travels at depth 0 (cordis-studio#593). The image is a clone of the lifted row, so it carries the
   row's depth indent, which CordisTreeNode writes inline: a deep row travelled with a wide empty band in front
   of its glyph. Off the tree a depth means nothing; the 8px is the indent of a row at depth 0 (its markup's
   `8 + Depth * 14`). !important because the indent is an inline style. */
.cordis-drag-ghost.cordis-tree-node__row { padding-left: 8px !important; }

/* WHERE IT WOULD LAND, named on the travelling image (cordis-studio#593) when that is not the thing under
   the pointer - a tree whose host drops a member into its class while the pointer is on another member.
   Accent TEXT, not an accent surface: it is read, not clicked. While a caption shows, the clone drops its
   source row's fixed width so the caption is never clipped (cordis-ui.js's setCaption). */
.cordis-drag-ghost__caption { margin-left: var(--cordis-space-2); padding-right: var(--cordis-space-2);
  color: var(--cordis-primary-text); white-space: nowrap; }

/* The overlay layer CordisDragDropHost mounts (issue #18), and the parent cordis-ui.js appends the ghost
   into when one exists. Inert and empty: it is a stacking context the design system owns, so the
   travelling image paints above every panel by construction instead of wherever the consumer's <body>
   happens to sit. `inset: 0` rather than a size, because the ghost inside it is `position: fixed` and
   positions itself against the viewport either way. It is deliberately a GLOBAL rule and not a scoped
   one: JavaScript finds it by class name, and a scoped sheet's rewritten selector would still match the
   element but the querySelector would be the thing that has to keep working. */
.cordis-dragdrop-layer { position: fixed; inset: 0; z-index: 4000; pointer-events: none; }

/* No text selection while a drag is live. Its cursor (the closed fist, or no-drop over a target that has
   declared it will not take the kind) is the gesture-cursor vocabulary's: cordis-ui.js holds
   `data-cordis-cursor` on <body> for the drag (#58, the block at the end of this section). */
body.cordis-dragging { user-select: none; -webkit-user-select: none; }

/* The other two cursors of a drag (issue #18). Between them and the `grabbing` rule above, all three
   states a drag has are drawn, and none of them was drawn at all once the native transport went away:
   the browser used to supply every one of these and no longer does, so a pointer drag was a gesture with
   no feedback but the ghost.

   `grab` at rest keys off the SAME selector the emulator picks sources with — `[draggable="true"]` is the
   opt-in marker, so an element that offers a drag says so and one that does not is untouched. Which also
   means the two can never drift: an element that shows the hand is exactly one the emulator will pick up.
   The exclusions mirror the emulator's EXCLUDE list for the same reason — a Monaco selection drag is the
   browser's, not ours, and must not advertise itself as one of ours. */
[draggable="true"] { cursor: grab; }
.monaco-editor [draggable="true"], [data-cordis-no-drag] [draggable="true"] { cursor: auto; }
/* ...and a text field INSIDE a draggable never arms a drag (issue #62: the emulator skips any press its typing
   predicate claims), so it must not show the hand either. `cursor` is inherited, so without this a row's name
   box showed the row's own cursor over a press that selects text. */
[draggable="true"] :is(input, textarea, select, [contenteditable="true"], [contenteditable="plaintext-only"]) { cursor: auto; }

/* THE GESTURE CURSORS (#58). While a gesture runs, the cursor is a property of the GESTURE, not of whatever
   the pointer happens to be over: every row, box, grip and button under it declares its own cursor, and a
   running gesture has to beat all of them. The drag transport learned that first (#18); then a diagram's
   row drag and its armed create tool each wrote their own rule, with their own precedence, and the refused
   placement's not-allowed sat on one element, so any descendant with a cursor of its own showed that
   cursor over a spot that would refuse the click.

   So one attribute names the gesture, and this block is the only place that turns it into a cursor:
     data-cordis-cursor="grabbing | no-drop | refused | place | move | busy
                         | resize-ns | resize-ew | resize-nwse | resize-nesw"
   - On a SURFACE (a canvas host, a panel) it covers that element and every descendant, `!important`, so no
     per-element rule underneath can show through. C# renders it there (CordisGestureCursor in
     Cordis.UI.Abstractions names the values).
   - On <body> it covers the document and WINS OVER every surface: `body[…] *` outranks `[…] *` by one
     type selector, and both are `!important`. A drag that crosses a canvas with a refusing tool armed shows
     the drag's cursor. Script never writes the body attribute directly; it holds it through
     cordisUi.gestureCursor.hold, so two gestures cannot clear each other's.
   - Surfaces do not nest: two surface attributes on one path tie on specificity and source order decides.
     Put it on the outermost element that owns the gesture.
   - The value is what the gesture MEANS, never a CSS keyword, so `refused` stays one word if not-allowed
     ever has to become a drawn cursor. An unknown value matches nothing and paints nothing, silently:
     GestureCursorContractTests holds this block, CordisGestureCursor.All and the script to one set.
   - Desktop: WebView2 maps a cursor to WinUI's system shapes, which have no closed hand, so `grabbing`
     falls back to the arrow there (WebView2Feedback #2766). Every other value has a system shape.

   `no-drop` is held while the pointer is over a target that has DECLARED it does not take the dragged kind
   (`data-cordis-drop-kinds`); absent whenever nothing under the cursor declares anything, because most
   draggable markup predates the contract and a cursor that read "refused" over every unconverted target
   would be worse than no cursor. */
[data-cordis-cursor="grabbing"], [data-cordis-cursor="grabbing"] * { cursor: grabbing !important; }
[data-cordis-cursor="no-drop"], [data-cordis-cursor="no-drop"] * { cursor: no-drop !important; }
[data-cordis-cursor="refused"], [data-cordis-cursor="refused"] * { cursor: not-allowed !important; }
[data-cordis-cursor="place"], [data-cordis-cursor="place"] * { cursor: crosshair !important; }
[data-cordis-cursor="move"], [data-cordis-cursor="move"] * { cursor: move !important; }
[data-cordis-cursor="busy"], [data-cordis-cursor="busy"] * { cursor: wait !important; }
[data-cordis-cursor="resize-ns"], [data-cordis-cursor="resize-ns"] * { cursor: ns-resize !important; }
[data-cordis-cursor="resize-ew"], [data-cordis-cursor="resize-ew"] * { cursor: ew-resize !important; }
[data-cordis-cursor="resize-nwse"], [data-cordis-cursor="resize-nwse"] * { cursor: nwse-resize !important; }
[data-cordis-cursor="resize-nesw"], [data-cordis-cursor="resize-nesw"] * { cursor: nesw-resize !important; }

body[data-cordis-cursor="grabbing"], body[data-cordis-cursor="grabbing"] * { cursor: grabbing !important; }
body[data-cordis-cursor="no-drop"], body[data-cordis-cursor="no-drop"] * { cursor: no-drop !important; }
body[data-cordis-cursor="refused"], body[data-cordis-cursor="refused"] * { cursor: not-allowed !important; }
body[data-cordis-cursor="place"], body[data-cordis-cursor="place"] * { cursor: crosshair !important; }
body[data-cordis-cursor="move"], body[data-cordis-cursor="move"] * { cursor: move !important; }
body[data-cordis-cursor="busy"], body[data-cordis-cursor="busy"] * { cursor: wait !important; }
body[data-cordis-cursor="resize-ns"], body[data-cordis-cursor="resize-ns"] * { cursor: ns-resize !important; }
body[data-cordis-cursor="resize-ew"], body[data-cordis-cursor="resize-ew"] * { cursor: ew-resize !important; }
body[data-cordis-cursor="resize-nwse"], body[data-cordis-cursor="resize-nwse"] * { cursor: nwse-resize !important; }
body[data-cordis-cursor="resize-nesw"], body[data-cordis-cursor="resize-nesw"] * { cursor: nesw-resize !important; }

@media (prefers-reduced-motion: reduce) {
  .cordis-tooltip,
  .cordis-tooltip[data-placement='top'],
  .cordis-tooltip.is-visible { transform: none; }
  .cordis-tooltip { transition: opacity var(--cordis-dur-fast) var(--cordis-ease); }
}

/* ---------- full screen (CordisFullscreenToggle) ------------------------- */

/* cordisUi.fullscreen marks the element it can take full screen. The Fullscreen API paints a black backdrop
   behind it by default; the app's own ground instead. Where the browser refuses (or never answers), the element
   fills the window: fixed over the app's rail and top bar (40), under modals and drawers (the scrim, 50), so a
   dialog opened from inside it still shows. */
.cordis-fullscreen-host:fullscreen { background-color: var(--cordis-bg); }
.cordis-fullscreen-host.is-window-filled { position: fixed !important; inset: 0; z-index: 45; width: auto !important;
  height: auto !important; padding: var(--cordis-space-3); background-color: var(--cordis-bg); }

/* ---------- danger zone --------------------------------------------------- */

/* D10 family merge #7: the asset layer adopted the COMPONENT wholesale, so the old
   `.cds-dangerzone` -- a title over a body on a solid --cordis-danger-soft ground -- is DELETED
   rather than renamed. It had no action slot at all, so it could only describe a danger, and a
   block that describes a danger is a .cordis-notice--danger, which exists two sections up. The
   component is a structural superset: a variant glyph, an action slot, and an armed state.

   These rules were CordisDangerZone.razor.css until the merge. Moving them out of a scoped sheet
   and into the asset layer is the SAFE direction for the #218 hazard -- the one this repository
   keeps re-learning is that a shared class styled only by a scoped sheet silently unstyles every
   other renderer -- and it is what lets an HMI panel write the markup by hand and get the real
   block. Every ::deep the scoped copy needed is simply gone: a global rule reaches a child
   component's DOM without ceremony.

   The chrome deliberately matches .cordis-notice / CordisBanner -- leading variant glyph, accent
   edge, tinted wash -- so a zone and a notice read as one family. What differs is structural: the
   action comes FIRST and the hint second, because you should meet the control before the
   consequence, and the action carries no width ceiling. */
/* One accent per variant, consumed by the edge, the wash AND the glyph, so a variant is a single
   declaration rather than three.

   Named --cordis-dangerzone-accent and not the bare --accent the scoped sheet used, and that is a
   direct consequence of going global. CordisBanner declares --accent on .cordis-banner for the
   same purpose; while both were scoped the two could not see each other, but a global rule reading
   an inherited --accent would pick up a banner's amber for a zone nested inside it. A custom
   property in the asset layer needs the family in its name. */
/* The four token names in this block used to be legacy ones — --cordis-red, --cordis-amber,
   --cordis-body-bg and --cordis-text-dim — carried over verbatim when CordisDangerZone's scoped
   sheet moved down here in the D10 merge. In a scoped sheet that was fine: a Blazor consumer loads
   the compat shim, which is the only file that declares them. In THIS file it was a quiet breach of
   the contract css/CLAUDE.md states — "nothing in tokens.css or cordis.css reads a legacy name", so
   a plain-HTML consumer needs no shim — and it fails the way unresolvable var() always does, by
   discarding the whole declaration with no error: no accent stripe, no wash, no hint colour. The
   canonical names are the same values (--cordis-danger IS --cordis-red-500). */
.cordis-dangerzone { --cordis-dangerzone-accent: var(--cordis-danger);
  display: flex;
  /* Centres the glyph against the WHOLE block rather than just the action row. The grid version of
     this silently did nothing: `grid-row: 1 / -1` counts back from the EXPLICIT grid, and with no
     declared rows -1 resolves to line 1, so the span collapsed. A flex row needs no span. */
  align-items: center; gap: var(--cordis-space-2);
  padding: var(--cordis-space-2) var(--cordis-space-3);
  border: 1px solid var(--cordis-border);
  border-left: 3px solid var(--cordis-dangerzone-accent);
  border-radius: var(--cordis-radius-sm);
  background: color-mix(in srgb, var(--cordis-dangerzone-accent) 6%, transparent); }
.cordis-dangerzone--warning { --cordis-dangerzone-accent: var(--cordis-warning); }
.cordis-dangerzone--danger  { --cordis-dangerzone-accent: var(--cordis-danger); }

/* Armed = the dangerous option is currently on: the accent takes the whole border and the wash
   doubles, so it is obvious at a glance in a pane full of routine toggles. */
.cordis-dangerzone.is-armed { border-color: var(--cordis-dangerzone-accent);
  background: color-mix(in srgb, var(--cordis-dangerzone-accent) 12%, transparent); }

/* Matched on the explicit __glyph class rather than `.cordis-icon`: the loose selector would also
   catch glyphs INSIDE the action and hint slots -- a destructive button's own delete icon -- and
   recolour them. */
.cordis-dangerzone__glyph { flex: none; color: var(--cordis-dangerzone-accent); }

.cordis-dangerzone__body { display: flex; flex-direction: column; gap: var(--cordis-space-1);
  min-width: 0; flex: 1 1 auto; }
/* A flex row so a zone can carry more than one control without the call site laying it out. */
.cordis-dangerzone__action { display: flex; align-items: center; gap: var(--cordis-space-2);
  min-width: 0; }

/* A secondary button on a tinted ground reads as a label, and the control this makes visible is
   the destructive one, so it is the worst version of that bug. --cordis-shadow-sm rather than the
   literal the scoped sheet held, which also makes it correct per profile: Studio nulls that token
   because a workbench separates surfaces with borders, and Studio's buttons have one. */
.cordis-dangerzone__action .cordis-btn--secondary { background: var(--cordis-surface);
  box-shadow: var(--cordis-shadow-sm); }
.cordis-dangerzone__action .cordis-btn--secondary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--cordis-text) 7%, var(--cordis-surface));
  border-color: var(--cordis-border-strong); }

/* Sits in the body column, so it aligns under the action's left edge by construction. Both
   hand-rolled copies this component replaced faked that with `margin-left: 22px` -- a magic number
   measured off a checkbox, wrong for any other control. */
.cordis-dangerzone__hint { margin: 0; font-size: var(--cordis-fs-xs);
  line-height: var(--cordis-lh-xs); color: var(--cordis-text-muted); }
.cordis-dangerzone__hint strong { color: var(--cordis-text);
  font-weight: var(--cordis-fw-semibold); }

/* ---------- loading ------------------------------------------------------- */

.cordis-skeleton { height: 12px; border-radius: var(--cordis-radius-sm);
  background: linear-gradient(90deg, var(--cordis-surface-sunken) 0%, var(--cordis-border) 40%, var(--cordis-surface-sunken) 80%);
  background-size: 200% 100%; animation: cordis-shimmer 1.2s linear infinite; }
@keyframes cordis-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }

/* TWO spinners, on purpose, and which one you want is a question about the surface rather
   than a preference. `.cordis-spinner` is a shape of its own: a 16px ring with a coloured
   arc, for a page or a card that is waiting on something and has room to say so.
   `.cordis-spin` is not a spinner at all — it is a rotation applied to something that is
   already there, almost always the `progress_activity` glyph, and it inherits currentColor.
   That is the one Cordis Studio uses: the panel tab strip, the deploy panel and the SCM
   history all spin a 13-15px glyph in the row's own text colour, because a workbench row
   has no room for a second shape and a coloured dot in a dense list reads as a status.
   Hence the Studio override in cordis-studio.css, which de-colours the ring to match.

   `.cordis-spin` is declared HERE and not in the RCL's app.css, where it lived at 1s while
   this file's ring ran at 0.7s — the same animation name at two speeds, so which one you
   got depended on which spinner you reached for. One duration, one owner. */
.cordis-spinner { width: 16px; height: 16px; border-radius: var(--cordis-radius-pill);
  border: 2px solid var(--cordis-border-strong); border-top-color: var(--cordis-primary);
  animation: cordis-spin 0.7s linear infinite; }
.cordis-spin { animation: cordis-spin 0.7s linear infinite; }
@keyframes cordis-spin { to { transform: rotate(360deg); } }

/* A bar for a wait that can be measured (`CordisProgressBar`): a sunken track, filled in the primary colour from the
   start. Without a value it is indeterminate, a third of the track sliding across it, so a bar whose total arrives a
   moment later changes nothing but its fill. The width eases so a download that reports in chunks still moves. */
.cordis-progress { position: relative; height: 6px; border-radius: var(--cordis-radius-pill); overflow: hidden;
  background: var(--cordis-border); }
.cordis-progress__bar { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: var(--cordis-primary);
  transition: width var(--cordis-dur-slow) var(--cordis-ease-out); }
.cordis-progress--indeterminate .cordis-progress__bar { width: 33%; animation: cordis-progress-slide 1.2s var(--cordis-ease-out) infinite; }
@keyframes cordis-progress-slide { from { left: -33%; } to { left: 100%; } }
/* `Thin`: a 3px rule for a bar that sits inside another surface rather than on the page — the progress toast's
   foot is the one that asked for it. Same track, fill, ARIA and motion; only the height drops. */
.cordis-progress--thin { height: 3px; }

/* ---------- navigation ---------------------------------------------------- */

/* `appshell`, not `shell`, and the awkward name is the point (decision D10). Studio's window is
   `.cordis-shell` — a three-row 100vh grid that owns its own scrolling, set by the IDE's own layout
   and by `.cordis-shell.cordis-dark`, which is how Studio still toggles dark. Two DIFFERENT layouts
   were reaching for one word: this grid is a rail beside a page in a browser tab, that one is a
   title bar over a workbench over a status bar. Unifying the prefix would have collided them, and
   the collision is worse than either name — the Studio grid's `overflow: hidden` on a Shell page
   silently kills the page's scrolling. Studio's name is the one that cannot move: it is written in
   application code outside this repository. */
/* Collapsing and expanding the rail is motion, not a jump: the track animates between its two widths
   and the labels, group headings and badges fade and fold with it (see the rail items below). */
.cordis-appshell { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh;
  transition: grid-template-columns var(--cordis-dur-slow) var(--cordis-ease-out); }
.cordis-appshell.is-collapsed { grid-template-columns: 60px 1fr; }
.cordis-appshell__main { min-width: 0; background: var(--cordis-bg); }
/* The open rail's backdrop. Only exists up to 1200px, where the open rail lies over the content (the
   phone drawer, or the icon column opened over the page); above that the rail is a column of the grid and
   a backdrop over it would dim the page for nothing. Sits one rung UNDER the rail (z-index 40 below) so
   the rail stays lit while everything else dims. */
.cordis-appshell__scrim { display: none; z-index: 39; }
/* The drawer trigger (CordisRailToggle), hidden wherever the rail is a grid column rather than a
   drawer. Declared here, after .cordis-btn, on purpose: the toggle wears the ghost icon-button classes
   so it needs no look of its own, and `display: none` is 0-1-0 against .cordis-btn's own
   `display: inline-flex`, so only document order stops the button winning and the hamburger showing on
   a desktop. It comes back at the 900px block below. */
.cordis-railtoggle { display: none; }

.cordis-rail { display: flex; flex-direction: column; background: var(--cordis-nav-bg); color: var(--cordis-nav-text); }
.cordis-rail__brand { display: flex; align-items: center; gap: var(--cordis-space-3);
  padding: var(--cordis-space-3) var(--cordis-space-4); border-bottom: 1px solid var(--cordis-nav-divider); }
/* The brand image's size lives HERE, not in each consumer's markup. The logo SVGs carry a 40px-tall
   intrinsic size of their own now (reference/index.html#logo), but this band's height must not depend
   on which file a consumer picked or on anyone remembering a height attribute — before the SVGs
   declared one, an unsized SVG-as-<img> had NO intrinsic size, each browser guessed its own, and iOS
   Safari's guess painted the mark over an entire page. 22px is a literal like the icon sizes above:
   a fit constraint of this band, not a metric a profile retunes — the rail is Shell-only furniture.
   max-width + contain keep the lock-up inside the 60px collapsed column instead of overflowing it. */
.cordis-rail__brand img { height: 22px; width: auto; max-width: 100%; object-fit: contain; }
/* A lock-up that collapses to its mark (CordisBrandLogo CollapseToMark, the Cordis Server's rail, M4025): the band
   takes no inset of its own and clips the lock-up's overflow; the clip, the motion and the inset that centres
   the mark in the 60px column are the component's (CordisBrandLogo.razor.css), since they read its measurements. */
.cordis-rail__brand:has(.cordis-brand-logo--collapse) { overflow: hidden; padding-inline-start: 0; }
.cordis-rail__group { padding: var(--cordis-space-3) var(--cordis-space-4) var(--cordis-space-1);
  font-size: var(--cordis-fs-caption); font-weight: var(--cordis-fw-semibold);
  letter-spacing: var(--cordis-ls-caption); text-transform: uppercase; color: var(--cordis-nav-text-muted);
  white-space: nowrap; overflow: hidden; max-height: 4em;
  transition: color var(--cordis-dur) var(--cordis-ease-out), max-height var(--cordis-dur-slow) var(--cordis-ease-out),
    padding var(--cordis-dur-slow) var(--cordis-ease-out); }
/* The rail is three bands: the brand, a scrolling list of items, and a foot pinned
   to the bottom for the collapse toggle. min-height:0 on the middle band is what
   lets it scroll instead of growing and pushing the foot off the viewport. */
.cordis-rail__nav { flex: 1; min-height: 0; overflow-y: auto; padding: var(--cordis-space-2) 0; }
.cordis-rail__foot { flex: none; padding: var(--cordis-space-2) 0; border-top: 1px solid var(--cordis-nav-divider); }

/* A rail item is a <button> (an action) or an <a> (a route) — never a div, because
   navigation has to be reachable from the keyboard. The reset half of this rule is
   what makes the two spellings render identically; `a { text-decoration }` at the
   top of this file would otherwise underline half the rail on hover. */
.cordis-rail__item { display: flex; align-items: center; gap: var(--cordis-space-3);
  height: var(--cordis-nav-item-h); margin: 0 var(--cordis-space-2); padding: 0 var(--cordis-space-3);
  border-radius: var(--cordis-radius-sm); color: var(--cordis-nav-text); font-size: var(--cordis-fs-body);
  cursor: pointer; white-space: nowrap;
  width: calc(100% - 2 * var(--cordis-space-2)); appearance: none; border: 0; background: none;
  font-family: inherit; text-align: left; text-decoration: none; overflow: hidden; }
.cordis-rail__item:hover { text-decoration: none; }
.cordis-rail__label { overflow: hidden; text-overflow: ellipsis;
  transition: opacity var(--cordis-dur) var(--cordis-ease-out); }
.cordis-rail__item:hover { background: var(--cordis-nav-hover); color: var(--cordis-nav-text-on); }
.cordis-rail__item.is-active { background: var(--cordis-nav-active); color: var(--cordis-nav-text-on); font-weight: var(--cordis-fw-semibold); }
/* One active highlight for the band, placed by cordisUi.navRail from the active item's measured box, so a new page
   slides it there (the Studio activity bar's indicator, for the Shell). Without the script the item's own
   background above still marks it. isolate + z-index -1 keep it under the items and over the rail's paint; left /
   right match the items' margin, so it narrows with them as the rail collapses. */
.cordis-rail__nav { position: relative; isolation: isolate; --rail-active-y: 0px; --rail-active-h: 0px; }
.cordis-rail__nav.has-active-indicator::before { content: ""; position: absolute; top: 0;
  left: var(--cordis-space-2); right: var(--cordis-space-2); z-index: -1; height: var(--rail-active-h);
  transform: translateY(var(--rail-active-y)); border-radius: var(--cordis-radius-sm);
  background: var(--cordis-nav-active); pointer-events: none; }
.cordis-rail__nav.active-motion-ready::before {
  transition: transform var(--cordis-dur-slow) var(--cordis-ease-out), height var(--cordis-dur-slow) var(--cordis-ease-out); }
.cordis-rail__nav.is-following::before { transition: none; }
.cordis-rail__nav.has-active-indicator .cordis-rail__item.is-active { background: transparent; }
/* Hover travels too (the glide's hover layer, data-cordis-hover-glide on the band and the foot): one tile
   slides from item to item as the pointer moves down the rail, where each item used to light up on its own.
   Under the active highlight (z-index -2 against its -1), so crossing the current page it passes beneath. */
.cordis-rail__nav[data-cordis-hover-glide]::after,
.cordis-rail__foot[data-cordis-hover-glide]::after { z-index: -2; left: 0; background: var(--cordis-nav-hover); }
.cordis-rail__nav.hover-glide-on .cordis-rail__item:hover,
.cordis-rail__foot.hover-glide-on .cordis-rail__item:hover { background: transparent; }
.cordis-rail__nav.hover-glide-on .cordis-rail__item.is-active:hover { background: transparent; }
@media (prefers-reduced-motion: reduce) { .cordis-rail__nav.active-motion-ready::before { transition: none; } }
/* Collapsed, an item keeps its padding: in the 60px track its 44px box puts the 20px glyph exactly in the
   middle, so the glyph never moves while the rail animates; the label fades and is clipped by the item. A
   group heading fades and folds into a short divider in its place, so the runs of icons stay told apart
   (reported by the Cordis Server, M4025) and the items below close up smoothly instead of jumping. The first
   group has nothing above it to divide from. */
.cordis-appshell.is-collapsed .cordis-rail__item > .cordis-rail__label { opacity: 0; }
.cordis-appshell.is-collapsed .cordis-rail__group { color: transparent; max-height: var(--cordis-space-4); padding-block: 0;
    background: linear-gradient(var(--cordis-nav-divider), var(--cordis-nav-divider)) center / calc(100% - 2 * var(--cordis-space-4)) 1px no-repeat; }
.cordis-appshell.is-collapsed .cordis-rail__nav > :first-child > .cordis-rail__group:first-child { max-height: 0; background: none; }

/* An item with sub-items (CordisNavItem ChildContent, a Cordis Server plugin and its pages, Mantis #2936): a button
   that opens them below it, each indented so its glyph sits under the parent's label. Closed over the current page
   the parent carries the highlight, since the active sub-item is hidden (cordisUi.navRail skips a hidden one). As an
   icon column the rail shows every sub-item, unindented and without the parent's chevron, so each page stays one
   click away; the active sub-item then carries the highlight and the parent drops it. */
.cordis-rail__chevron { margin-left: auto;
  transition: transform var(--cordis-dur) var(--cordis-ease-out), opacity var(--cordis-dur) var(--cordis-ease-out); }
.cordis-rail__item[aria-expanded="true"] > .cordis-rail__chevron { transform: rotate(180deg); }
.cordis-rail__children { display: none; }
.cordis-rail__children.is-open { display: block; }
.cordis-rail__children .cordis-rail__item { padding-inline-start: calc(var(--cordis-space-3) + var(--cordis-space-6)); }
.cordis-rail__item.has-active-child { background: var(--cordis-nav-active); color: var(--cordis-nav-text-on); font-weight: var(--cordis-fw-semibold); }
.cordis-rail__nav.hover-glide-on .cordis-rail__item.has-active-child:hover { background: var(--cordis-nav-active); }
.cordis-appshell.is-collapsed .cordis-rail__chevron { opacity: 0; }
.cordis-appshell.is-collapsed .cordis-rail__children { display: block; }
.cordis-appshell.is-collapsed .cordis-rail__children .cordis-rail__item { padding-inline-start: var(--cordis-space-3); }
.cordis-appshell.is-collapsed .cordis-rail__item.has-active-child { background: none; color: var(--cordis-nav-text); font-weight: var(--cordis-fw-regular); }

/* A count beside a rail item — overdue leads, open tasks. Pushed right so the
   counts line up down the rail, and dropped with the label when the rail
   collapses, because a bare number in a 60px column reads as the item's name.
   --cordis-nav-hover rather than a status colour: the count says "there are some",
   and only the consumer knows whether some is bad. */
.cordis-rail__badge { margin-left: auto; padding: 0 var(--cordis-space-2);
  border-radius: var(--cordis-radius-pill); background: var(--cordis-nav-hover);
  color: var(--cordis-nav-text-on); font-size: var(--cordis-fs-caption); font-weight: var(--cordis-fw-semibold);
  transition: opacity var(--cordis-dur) var(--cordis-ease-out); }
/* CordisNavItem renders the count as a CordisBadge, whose own scoped rules size and colour it; its tone
   colours are re-pointed here (three classes outrank the badge's tone rule) to the rail's on-dark pair. */
.cordis-rail .cordis-rail__item .cordis-rail__badge {
  --cordis-badge-bg: var(--cordis-nav-hover); --cordis-badge-fg: var(--cordis-nav-text-on); }
.cordis-appshell.is-collapsed .cordis-rail__badge { opacity: 0; }

/* ---------- timeline ------------------------------------------------------ */

.cordis-timeline { display: flex; flex-direction: column; }
.cordis-timeline__row { display: grid; grid-template-columns: 22px 1fr; gap: var(--cordis-space-3); }
/* The marker column. It is a flex column rather than two more grid cells so the
   connector's `flex: 1` below has something to grow inside — the line has to fill
   whatever height the entry's own content turns out to need. */
.cordis-timeline__rail { display: flex; flex-direction: column; align-items: center; }
.cordis-timeline__dot { width: 9px; height: 9px; margin-top: 5px; border-radius: var(--cordis-radius-pill);
  background: var(--cordis-n-400); justify-self: center; flex: none; }
/* The newest entry's dot takes the brand accent — unless the entry claims a tone of
   its own below, which is the more specific statement about it. Written as a :not()
   rather than as ten override rules further down: an override chain that has to
   out-specify a positional rule is the kind of thing nobody dares touch later. */
.cordis-timeline__row:first-child .cordis-timeline__dot:not(.cordis-timeline__dot--ok, .cordis-timeline__dot--warn,
  .cordis-timeline__dot--danger, .cordis-timeline__dot--info, .cordis-timeline__dot--neutral) { background: var(--cordis-primary); }
.cordis-timeline__line { width: 1px; flex: 1; margin: var(--cordis-space-1) 0; background: var(--cordis-border); justify-self: center; }
/* The connector joins one entry to the NEXT one, so the last entry must not draw
   it — a line hanging below the final dot reads as a feed that is still loading. */
.cordis-timeline__row:last-child .cordis-timeline__line { display: none; }
.cordis-timeline__content { min-width: 0; padding-bottom: var(--cordis-space-4); }
.cordis-timeline__row:last-child .cordis-timeline__content { padding-bottom: 0; }
.cordis-timeline__title { font-size: var(--cordis-fs-sm); font-weight: var(--cordis-fw-semibold); color: var(--cordis-text-strong); }
/* pre-wrap because the body is usually something a person typed into a box, and
   the line breaks they put in it are part of what they wrote. */
.cordis-timeline__body { margin-top: var(--cordis-space-1); font-size: var(--cordis-fs-sm);
  color: var(--cordis-text); white-space: pre-wrap; text-wrap: pretty; }
.cordis-timeline__when { margin-top: 2px; font-size: var(--cordis-fs-caption); color: var(--cordis-text-muted); }

/* A dot big enough to hold a glyph, for a feed whose entries differ in kind. The
   plain 9px dot stays the default: a timeline of one kind of thing does not need
   the same icon repeated down its whole length. */
.cordis-timeline__dot--icon { width: 22px; height: 22px; margin-top: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--cordis-surface-sunken); color: var(--cordis-text-muted); }

/* Tone. The plain dot takes the solid colour; the icon dot takes the soft fill
   plus the readable text colour, which is the same pairing .cordis-status uses — so
   an entry's dot and the status pill inside it cannot disagree. */
.cordis-timeline__dot--ok { background: var(--cordis-success); }
.cordis-timeline__dot--warn { background: var(--cordis-warning); }
.cordis-timeline__dot--danger { background: var(--cordis-danger); }
.cordis-timeline__dot--info { background: var(--cordis-info); }
.cordis-timeline__dot--neutral { background: var(--cordis-n-400); }
.cordis-timeline__dot--icon.cordis-timeline__dot--ok { background: var(--cordis-success-soft); color: var(--cordis-success-text); }
.cordis-timeline__dot--icon.cordis-timeline__dot--warn { background: var(--cordis-warning-soft); color: var(--cordis-warning-text); }
.cordis-timeline__dot--icon.cordis-timeline__dot--danger { background: var(--cordis-danger-soft); color: var(--cordis-danger-text); }
.cordis-timeline__dot--icon.cordis-timeline__dot--info { background: var(--cordis-info-soft); color: var(--cordis-info-text); }
.cordis-timeline__dot--icon.cordis-timeline__dot--neutral { background: var(--cordis-neutral-soft); color: var(--cordis-neutral-text); }
/* An untoned icon dot at the top of the feed is still the newest entry, so it keeps
   the brand accent — in its soft form, because the glyph has to stay legible on it. */
.cordis-timeline__row:first-child .cordis-timeline__dot--icon:not(.cordis-timeline__dot--ok, .cordis-timeline__dot--warn,
  .cordis-timeline__dot--danger, .cordis-timeline__dot--info, .cordis-timeline__dot--neutral) {
  background: var(--cordis-primary-soft); color: var(--cordis-primary-text); }

/* ---------- density: retired (decision D11, 2026-08) ----------------------
   There was a [data-density="compact"] axis here and in tokens.css. It moved
   five metrics to roughly Studio's values, which is exactly what made Studio
   read as "compact Shell" and hid the fact that the two profiles are two kinds
   of application rather than one at two sizes. Profile is the axis. Two of them,
   four combinations with theme — not three axes and eight.

   Nothing replaces it. A Shell application that wants dense rows sets its own
   row height; a workbench is Studio. */

/* ---------- small screens ------------------------------------------------- */

@media (max-width: 1200px) { .cordis-appshell { grid-template-columns: 60px 1fr; }
  .cordis-appshell .cordis-rail__label, .cordis-appshell .cordis-rail__badge, .cordis-appshell .cordis-rail__chevron { opacity: 0; }
  .cordis-appshell .cordis-rail__group { color: transparent; max-height: var(--cordis-space-4); padding-block: 0;
    background: linear-gradient(var(--cordis-nav-divider), var(--cordis-nav-divider)) center / calc(100% - 2 * var(--cordis-space-4)) 1px no-repeat; }
  .cordis-appshell .cordis-rail__nav > :first-child > .cordis-rail__group:first-child { max-height: 0; background: none; }
  .cordis-appshell .cordis-rail__children { display: block; }
  .cordis-appshell .cordis-rail__children .cordis-rail__item { padding-inline-start: var(--cordis-space-3); }
  .cordis-appshell .cordis-rail__item.has-active-child { background: none; color: var(--cordis-nav-text); font-weight: var(--cordis-fw-regular); } }

/* Between 901 and 1200px the foot toggle opens the icon column OVER the page (RailOpen), as the phone drawer does:
   the auto-collapse is there to give the page that room, and a toggle that did nothing at this width looked broken
   (reported by the Cordis Server, M4025). The rail keeps its 60px track and grows past it, so the page never
   moves; the labels come back at the specificity the collapsed rules carry (0-4-0 and 0-7-0 against 0-3-0 / 0-6-0). */
@media (min-width: 901px) and (max-width: 1200px) {
  .cordis-appshell .cordis-rail { position: relative; z-index: 40; width: 60px;
    transition: width var(--cordis-dur-slow) var(--cordis-ease-out), box-shadow var(--cordis-dur-slow) var(--cordis-ease-out); }
  .cordis-appshell .cordis-rail.is-open { width: 240px; box-shadow: var(--cordis-shadow-lg); }
  .cordis-appshell .cordis-rail.is-open .cordis-rail__label,
  .cordis-appshell .cordis-rail.is-open .cordis-rail__badge,
  .cordis-appshell .cordis-rail.is-open .cordis-rail__chevron { opacity: 1; }
  .cordis-appshell .cordis-rail.is-open .cordis-rail__group,
  .cordis-appshell .cordis-rail.is-open .cordis-rail__nav > :first-child > .cordis-rail__group:first-child {
    color: var(--cordis-nav-text-muted); background: none; max-height: 4em;
    padding-block: var(--cordis-space-3) var(--cordis-space-1); }
  .cordis-appshell .cordis-rail.is-open .cordis-rail__children:not(.is-open) { display: none; }
  .cordis-appshell .cordis-rail.is-open .cordis-rail__children .cordis-rail__item {
    padding-inline-start: calc(var(--cordis-space-3) + var(--cordis-space-6)); }
  .cordis-appshell .cordis-rail.is-open .cordis-rail__item.has-active-child {
    background: var(--cordis-nav-active); color: var(--cordis-nav-text-on); font-weight: var(--cordis-fw-semibold); }
  /* The toggle's chevron follows the reader's Collapsed preference, which this width overrides: shut, it opens. */
  .cordis-appshell:not(.is-collapsed) .cordis-rail:not(.is-open) .cordis-rail__toggle > .cordis-icon { transform: scaleX(-1); }
  .cordis-appshell__scrim { display: block; }
}

@media (max-width: 900px) {
  /* Both spellings, for the specificity reason the `.is-collapsed` item rules below spell out: the bare
     selector (0-1-0) loses to `.cordis-appshell.is-collapsed` (0-2-0) above, so a rail the reader had
     collapsed on a wide screen kept its 60px grid track here — and since the rail is `position: fixed`
     at this width, the PAGE fell into that 60px column (reported by the Cordis Server, M4025). */
  .cordis-appshell,
  .cordis-appshell.is-collapsed { grid-template-columns: 1fr; }
  .cordis-rail { position: fixed; inset: 0 auto 0 0; width: 260px; z-index: 40; transform: translateX(-100%);
    transition: transform var(--cordis-dur-slow) var(--cordis-ease-out); }
  .cordis-rail.is-open { transform: none; }
  /* The rail is a 260px drawer here, so the labels come back — and this has to be spelt out at the
     0-2-0 the auto-collapse block above carries, for the same reason the `.is-collapsed` rules below
     spell out 0-3-0. It was `.cordis-rail__label` (0-1-0) and therefore LOST to the 1200px rule at
     every width under 900: a 260px drawer of unlabelled icons — and where a consumer's entries carry
     no icon, a drawer of blank rows. The revert never fired at ANY viewport width, so nothing looked
     broken until a consumer had a long list of iconless destinations and opened it on a narrow one. */
  .cordis-appshell .cordis-rail__label,
  .cordis-appshell .cordis-rail__badge,
  .cordis-appshell .cordis-rail__chevron { opacity: 1; }
  .cordis-appshell .cordis-rail__group,
  .cordis-appshell .cordis-rail__nav > :first-child > .cordis-rail__group:first-child { color: var(--cordis-nav-text-muted);
    background: none; max-height: 4em; padding-block: var(--cordis-space-3) var(--cordis-space-1); }
  /* A drawer has the labels, so a parent's sub-items open and close again (the icon-column rules above showed them). */
  .cordis-appshell .cordis-rail__children:not(.is-open),
  .cordis-appshell.is-collapsed .cordis-rail__children:not(.is-open) { display: none; }
  .cordis-appshell .cordis-rail__children .cordis-rail__item,
  .cordis-appshell.is-collapsed .cordis-rail__children .cordis-rail__item {
    padding-inline-start: calc(var(--cordis-space-3) + var(--cordis-space-6)); }
  .cordis-appshell .cordis-rail__item.has-active-child,
  .cordis-appshell.is-collapsed .cordis-rail__item.has-active-child {
    background: var(--cordis-nav-active); color: var(--cordis-nav-text-on); font-weight: var(--cordis-fw-semibold); }
  /* The rail is a drawer here, so it needs a backdrop; above this width it has none. */
  .cordis-appshell__scrim { display: block; }
  /* …and something has to open it. This is the only width where the trigger means anything. */
  .cordis-railtoggle { display: inline-flex; }
  /* Collapse is meaningless here — the rail is a 260px drawer, not a grid column —
     so `.is-collapsed` is undone rather than left to fight the rules above it. It
     has to be spelt out at the same specificity the collapsed rules carry (0-3-0),
     which is why this is four selectors and not one. */
  .cordis-appshell.is-collapsed .cordis-rail__item > .cordis-rail__label,
  .cordis-appshell.is-collapsed .cordis-rail__badge,
  .cordis-appshell.is-collapsed .cordis-rail__chevron { opacity: 1; }
  .cordis-appshell.is-collapsed .cordis-rail__group,
  .cordis-appshell.is-collapsed .cordis-rail__nav > :first-child > .cordis-rail__group:first-child {
    color: var(--cordis-nav-text-muted); background: none; max-height: 4em;
    padding-block: var(--cordis-space-3) var(--cordis-space-1); }
  /* A column a reader can do without at this width steps aside (CordisColumn.HideAt, Cordis Server #4025),
     so the columns that matter stay whole instead of the table scrolling sideways. */
  .cordis-table .cordis-col-hide-narrow { display: none; }
}

@media (max-width: 640px) {
  /* The touch floor is the WHOLE control family or it is a defect. It used to name two members,
     .cordis-btn and .cordis-input, so a narrow viewport lifted the text field and the buttons to 44px
     and left the numeric input, the select trigger, the copy field and the filter box at
     --cordis-control-h beside them: measured live at 44px against 26px in one SPECIFY row (2026-08).
     A floor that reaches some of a row is worse than no floor at all -- it does not make the form
     touchable, it just makes it ragged. Studio opts out wholesale in css/cordis-studio.css; a
     workbench narrowed to 640px is still a workbench (D13). */
  .cordis-btn, .cordis-input, .cordis-textarea, .cordis-numeric,
  .cordis-select__trigger, .cordis-copyfield, .cordis-filter { min-height: 44px; }
  .cordis-modal { position: fixed; inset: auto 0 0 0; max-width: none; border-radius: var(--cordis-radius-lg) var(--cordis-radius-lg) 0 0; }
  /* A drawer takes the whole phone screen. The 94vw cap exists to keep the list
     visible behind it, and on a phone there is no useful list left to see. */
  .cordis-drawer { width: 100%; }
  /* A dashboard is one tile per row on a phone: 24 columns of 14px say nothing. The tile keeps the
     height it was given (--cordis-dashgrid-h rows), and its inline cells are overruled rather than
     rewritten, so the saved layout is untouched for the next wide screen. */
  .cordis-dashgrid { display: flex; flex-direction: column; }
  .cordis-dashgrid__item { height: calc(var(--cordis-dashgrid-h) * var(--cordis-dashgrid-row)); }
  .cordis-table--responsive thead { display: none; }
  .cordis-table--responsive tr { display: block; margin-bottom: var(--cordis-space-2);
    border: 1px solid var(--cordis-border); border-radius: var(--cordis-radius-md); background: var(--cordis-surface); }
  .cordis-table--responsive td { display: flex; height: auto; padding: var(--cordis-space-2) var(--cordis-space-3); border: 0; }
  /* Each row is a card on a phone, so the table and a card around it hand their frame to the rows: a card of
     cards read as a panel in a panel. */
  .cordis-table--responsive { background: none; }
  .cordis-card:has(> .cordis-table--responsive) { background: none; border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
  /* The label is prose, so it takes the UI font explicitly: on a .cordis-num cell the ::before
     would otherwise inherit the cell's monospace and "Amount" would come out in mono beside
     four labels that are not. The tabular figures the class exists for stay on the value. It also
     starts its own line: a .cordis-num cell's right alignment pushed the label against its value
     ("Widgets0"). */
  .cordis-table--responsive td::before { content: attr(data-label); flex: none; width: 40%;
    padding-inline-end: var(--cordis-space-3); text-align: start;
    color: var(--cordis-text-muted); font-family: var(--cordis-font); font-variant-numeric: normal; }
  /* A cell with no label -- the row-action column -- gets no label column either, or it spends
     40% of the card on nothing and pushes its buttons into the remainder. */
  .cordis-table--responsive td:not([data-label])::before { content: none; }
  /* A card's actions are its trailing buttons, at rest: a card has no row to hover, and hidden they
     left an empty band at the foot of every card on a desktop pointer. */
  .cordis-table--responsive .cordis-table__actions { flex: 1; opacity: 1; }
  /* A card has no columns to share: the sizing rules above would squeeze a value to nothing. */
  .cordis-table--responsive td.cordis-col-grow,
  .cordis-table--responsive td.cordis-col-fit,
  .cordis-table--responsive td.cordis-col-share { width: auto; max-width: none; white-space: normal; }
  /* The phone step of CordisColumn.HideAt (the narrow step above also holds here): in a card as in a row. */
  .cordis-table .cordis-col-hide-phone { display: none; }
}

/* minmax(0, 1fr) and not 1fr: `1fr` is minmax(AUTO, 1fr), and an auto minimum is the
   column's min-content — so the moment __label below stopped wrapping, one long label
   pushed its own column past a fifth of the bar and stole the width from the other
   four. The ellipsis can only do its job in a column that is allowed to be narrower
   than its text. */
.cordis-tabbar { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  border-top: 1px solid var(--cordis-border); background: var(--cordis-surface);
  padding-bottom: env(safe-area-inset-bottom, 0);
  isolation: isolate; }   /* the active pill paints at z-index -1 — behind the glyph, above this background */
/* 10px is below the type scale on purpose: the label sits under a 24px icon in a
   44px target and has to stay on one line at five columns — which __label below is
   what actually enforces. This comment claimed it for a while and nothing delivered
   it: the label was an unclassed <span> with white-space: normal, so a tab called
   "Rather a long label" wrapped onto three lines and grew the whole bar, because a
   grid row is as tall as its tallest cell. A tab label is a word. */
/* A tab is a <button> or an <a>, same reasoning as .cordis-rail__item: the reset half
   of the rule is what stops the two spellings drifting apart. */
.cordis-tabbar__item { display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: var(--cordis-space-2) 0; color: var(--cordis-text-muted); font-size: 10px; min-height: 44px;
  appearance: none; border: 0; background: none; font-family: inherit;
  cursor: pointer; text-decoration: none; }
.cordis-tabbar__item:hover { text-decoration: none; }
/* One line, clipped with an ellipsis. max-width rather than width so a short label
   still centres under its glyph; the item is a column flex box, so 100% is the tab's
   own fifth of the bar. */
.cordis-tabbar__label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cordis-tabbar__item.is-active { color: var(--cordis-primary-text); font-weight: var(--cordis-fw-semibold); }

/* The active tab wears a soft PILL behind its glyph — the Shell rail's soft active fill, at phone scale —
   and when the tab changes the pill slides to the new one (the glide, /decisions/motion). Two spellings of
   one shape: the glyph draws its own pill statically, which is all a page without cordis-ui.js ever sees
   (reference/index.html); once the glide runs, the bar's ::before is the pill and the static one steps
   aside, so there are never two. Both are the glyph's box grown 16px each side and 4px top and bottom. */
.cordis-tabbar__item.is-active .cordis-tabbar__ic::before,
.cordis-tabbar[data-cordis-glide]::before {
  background: var(--cordis-primary-soft); border-radius: var(--cordis-radius-pill); }
.cordis-tabbar__item.is-active .cordis-tabbar__ic::before {
  content: ""; position: absolute; inset: calc(-1 * var(--cordis-space-1)) calc(-1 * var(--cordis-space-4)); z-index: -1; }
.cordis-tabbar.glide-on .cordis-tabbar__item.is-active .cordis-tabbar__ic::before { content: none; }
.cordis-tabbar[data-cordis-glide]::before {
  width: calc(var(--glide-w) + 2 * var(--cordis-space-4));
  height: calc(var(--glide-h) + 2 * var(--cordis-space-1));
  transform: translate(calc(var(--glide-x) - var(--cordis-space-4)), calc(var(--glide-y) - var(--cordis-space-1))); }

/* The count rides on the glyph rather than sitting beside the label: five columns
   leave no horizontal room, and a number next to a 10px label wraps the label. */
.cordis-tabbar__ic { position: relative; display: inline-flex; }
.cordis-tabbar__badge { position: absolute; top: calc(-1 * var(--cordis-space-1)); left: 60%;
  min-width: var(--cordis-space-4); padding: 0 var(--cordis-space-1); border-radius: var(--cordis-radius-pill);
  background: var(--cordis-danger); color: var(--cordis-text-on-fill);
  font-size: var(--cordis-fs-caption); font-weight: var(--cordis-fw-semibold);
  line-height: var(--cordis-space-4); text-align: center; }

/* ---------- glide --------------------------------------------------------- */
/* ONE highlight per container that travels between items rather than snapping (the motion pass,
   /decisions/motion): the hover row in a menu or a select, a tab strip's underline. The container says
   data-cordis-glide="hover|selection" and data-glide-active="<selector of the highlighted item>";
   cordisUi.glide (the component library's script) measures that item and writes its box here as
   --glide-x/-y/-w/-h, then sets .glide-on (there is a target) and .glide-ready (it may travel — held off
   for the first appearance, a re-render and a long jump, so those snap instead of sliding across).

   The indicator is the container's ::before, behind the items (isolation + z-index -1), so an item's
   own text and icons sit on top of it. It is PROGRESSIVE: a component keeps its per-item highlight and
   drops it only under .glide-on, so a page without the script still shows which row is active.
   Hover follows the pointer, so it is the fast token and the arriving curve; selection is a
   deliberate change of place and takes the standard pair — the activity rail's timing. */
/* :where() so the host rule weighs nothing: a menu panel that is position:fixed stays fixed — any
   positioned value makes it the indicator's containing block, and only a static host needs the default. */
:where([data-cordis-glide]) { --glide-x: 0px; --glide-y: 0px; --glide-w: 0px; --glide-h: 0px;
  position: relative; isolation: isolate; }
[data-cordis-glide]::before { content: ""; position: absolute; left: 0; top: 0; z-index: -1;
  width: var(--glide-w); height: var(--glide-h); transform: translate(var(--glide-x), var(--glide-y));
  opacity: 0; pointer-events: none; border-radius: var(--cordis-radius-sm);
  transition: opacity var(--cordis-dur-fast) var(--cordis-ease-out); }
[data-cordis-glide="hover"]::before { background: var(--cordis-surface-sunken); }
[data-cordis-glide].glide-on::before { opacity: 1; }
[data-cordis-glide="hover"].glide-ready::before {
  transition: opacity var(--cordis-dur-fast) var(--cordis-ease-out),
    transform var(--cordis-dur-fast) var(--cordis-ease-out),
    width var(--cordis-dur-fast) var(--cordis-ease-out), height var(--cordis-dur-fast) var(--cordis-ease-out); }
[data-cordis-glide="selection"].glide-ready::before {
  transition: opacity var(--cordis-dur-fast) var(--cordis-ease-out),
    transform var(--cordis-dur) var(--cordis-ease),
    width var(--cordis-dur) var(--cordis-ease), height var(--cordis-dur) var(--cordis-ease); }

/* A row's OWN hover stands down under a running hover glide, because the travelling indicator is the
   hover there. A row opts in by painting its hover through this property —
   `background: var(--cordis-glide-row-hover, var(--cordis-hover))` — which a running hover glide sets to
   transparent for everything inside it. Inherited, so a row reads it through any nesting (a member list
   inside the absorbing SPECIFY body), and a plain value, so it weighs nothing against a row's selected
   or drop-target band. Without the script nothing sets it and the row paints its own. */
[data-cordis-glide="hover"].glide-on { --cordis-glide-row-hover: transparent; }

/* A PANE that is one surface. A pane holding several row lists with headers between them (SPECIFY: a
   section header, its member list, the next header) would otherwise run one glide per list, and the
   highlight would fade out at the end of one list and in at the next. The pane declares
   data-glide-absorb next to its own hover glide, with a data-glide-active that names every row it holds
   (CordisGlide.PaneRows for the library's lists, plus its own headers); a nested list that marks itself
   data-glide-yield then draws nothing, while still running, so its .glide-on keeps its rows' own hover
   off. Only the four row lists yield: a select's options or a menu inside the pane keep their own. */
[data-glide-absorb] [data-glide-yield]::before { content: none; }

/* The hover LAYER, for a container whose ::before is already a selection indicator (the two rails). Same
   mechanism and the hover timing above, on ::after and its own --hover-glide-* box, so it never touches
   the selection layer's state. Painted behind the items like the first layer; a rail puts it under its
   active highlight, so passing over the selected item the tile slides beneath it rather than on top. */
:where([data-cordis-hover-glide]) { --hover-glide-x: 0px; --hover-glide-y: 0px; --hover-glide-w: 0px;
  --hover-glide-h: 0px; position: relative; isolation: isolate; }
[data-cordis-hover-glide]::after { content: ""; position: absolute; left: 0; top: 0; z-index: -1;
  width: var(--hover-glide-w); height: var(--hover-glide-h);
  transform: translate(var(--hover-glide-x), var(--hover-glide-y));
  opacity: 0; pointer-events: none; border-radius: var(--cordis-radius-sm);
  transition: opacity var(--cordis-dur-fast) var(--cordis-ease-out); }
[data-cordis-hover-glide].hover-glide-on::after { opacity: 1; }
[data-cordis-hover-glide].hover-glide-ready::after {
  transition: opacity var(--cordis-dur-fast) var(--cordis-ease-out),
    transform var(--cordis-dur-fast) var(--cordis-ease-out),
    width var(--cordis-dur-fast) var(--cordis-ease-out), height var(--cordis-dur-fast) var(--cordis-ease-out); }

/* ---------- reduced motion ------------------------------------------------ */
/* State still changes — it just changes instantly. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important; }
}
