﻿/* ============================================================================
   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;
}

*, *::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); }

/* ---------- 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 BY HAND (tools/Subset-IconFont.ps1), 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.

   Bump this every time you regenerate the woff2. 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). */
@font-face {
  font-family: 'Material Symbols';
  src: url('../fonts/MaterialSymbolsRounded.woff2?v=2') format('woff2 supports variations'),
       url('../fonts/MaterialSymbolsRounded.woff2?v=2') 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;
}

/* 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 <a>/<label> forms, the in-row 20px actions, the
   Studio explorer/inspector hover context).

   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);
  background: var(--cordis-primary); color: var(--cordis-text-on-fill); border-color: var(--cordis-primary);
}
.cordis-btn:hover  { background: var(--cordis-primary-hover); border-color: var(--cordis-primary-hover); }
.cordis-btn:active { 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; }
.cordis-btn[disabled]:hover, .cordis-btn:disabled:hover {
  background: var(--cordis-primary); border-color: var(--cordis-primary);
}

/* Geometry modifiers. Square on the control height, so the icon button matches the field beside it. */
.cordis-btn--icon {
  width: var(--cordis-control-h); padding: 0;
  background: transparent; color: inherit; border-color: transparent;
}
.cordis-btn--icon:hover { background: var(--cordis-primary-soft); border-color: transparent; }
.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 { 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 { 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 { 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-danger-soft); }
.cordis-btn--danger:hover { background: var(--cordis-danger-soft); border-color: var(--cordis-danger-soft); }
.cordis-btn--icon.cordis-btn--danger { background: transparent; border-color: transparent; color: var(--cordis-danger-text); }
.cordis-btn--icon.cordis-btn--danger:hover,
.cordis-btn--icon.cordis-btn--danger:focus-visible { background: var(--cordis-danger-soft); }

/* 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 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; }

/* 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;
}
.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; }

.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-border-strong); 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-surface);
  transition: left var(--cordis-dur-fast) var(--cordis-ease); }
.cordis-switch--plain:checked { background: var(--cordis-primary); }
.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); }

.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); }

/* ---------- 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); }
.cordis-avatarstack { display: inline-flex; align-items: center; }
.cordis-avatarstack > .cordis-avatar { border: 2px solid var(--cordis-surface); }
.cordis-avatarstack > .cordis-avatar + .cordis-avatar { margin-left: -8px; }

/* ---------- 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); }

.cordis-page { padding: var(--cordis-space-5); }
.cordis-pagehead { display: flex; 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; }
.cordis-pagehead__actions { margin-left: auto; display: flex; 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); }

/* ---------- 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); }
.cordis-table td { height: var(--cordis-row-h); padding: 0 var(--cordis-space-4); border-bottom: 1px solid var(--cordis-border); }
.cordis-table tbody tr:hover { background: var(--cordis-surface-sunken); }
.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; }
.cordis-table tbody tr:hover .cordis-table__actions,
.cordis-table tbody tr:focus-within .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; }
.cordis-segmented__item.is-active { background: var(--cordis-surface); 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; }
.cordis-tabs__item.is-active { border-bottom-color: var(--cordis-primary); color: var(--cordis-text-strong); font-weight: var(--cordis-fw-semibold); }

/* ---------- 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); }

/* ---------- 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; } }

.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; }
.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 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: 50; background: var(--cordis-overlay); }
/* --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; }
.cordis-modal { background: var(--cordis-surface); border-radius: var(--cordis-radius-lg);
  box-shadow: var(--cordis-shadow-lg); max-width: 520px; width: 100%;
  animation: cordis-modal-in var(--cordis-dur) var(--cordis-ease-out); }
.cordis-modal__head { display: flex; align-items: center; gap: var(--cordis-space-3);
  padding: var(--cordis-space-4); border-bottom: 1px solid var(--cordis-border); }
.cordis-modal__body { padding: var(--cordis-space-4); }
.cordis-modal__foot { display: flex; gap: var(--cordis-space-2); justify-content: flex-end;
  padding: var(--cordis-space-4); border-top: 1px solid var(--cordis-border); }
@keyframes cordis-modal-in { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: none; } }

/* ---------- 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; }
.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); }
.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); }
.cordis-menupanel__item--unread .cordis-menupanel__dot { visibility: visible; }
.cordis-menupanel__dot--spent { visibility: hidden; }
.cordis-menupanel__key { margin-left: auto; font-family: var(--cordis-font-mono);
  font-size: var(--cordis-fs-caption); color: var(--cordis-text-muted); }

/* ---------- 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;
  /* 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); }
/* A keyboard hint inside a chip, e.g. "Toggle the terminal  Ctrl+`". */
.cordis-tooltip__kbd { color: var(--cordis-tooltip-kbd); }
@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); }
}

/* ---------- 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); } }

/* ---------- 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. */
.cordis-appshell { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
.cordis-appshell.is-collapsed { grid-template-columns: 60px 1fr; }
.cordis-appshell__main { min-width: 0; background: var(--cordis-bg); }
/* The off-canvas rail's backdrop. Only exists below 900px, where the rail is a
   drawer over the content; 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); }
.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); }
/* 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; }
.cordis-rail__item:hover { text-decoration: none; }
.cordis-rail__label { overflow: hidden; text-overflow: ellipsis; }
.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); }
.cordis-appshell.is-collapsed .cordis-rail__item { justify-content: center; padding: 0; }
.cordis-appshell.is-collapsed .cordis-rail__item > .cordis-rail__label,
.cordis-appshell.is-collapsed .cordis-rail__group { display: none; }

/* 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); }
.cordis-appshell.is-collapsed .cordis-rail__badge { display: none; }

/* ---------- 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__group, .cordis-appshell .cordis-rail__badge { display: none; }
  .cordis-appshell .cordis-rail__item { justify-content: center; padding: 0; } }

@media (max-width: 900px) {
  .cordis-appshell { 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__group,
  .cordis-appshell .cordis-rail__badge { display: revert; }
  /* 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 .cordis-rail__item,
  .cordis-appshell.is-collapsed .cordis-rail__item { justify-content: flex-start; padding: 0 var(--cordis-space-3); }
  .cordis-appshell.is-collapsed .cordis-rail__item > .cordis-rail__label,
  .cordis-appshell.is-collapsed .cordis-rail__group,
  .cordis-appshell.is-collapsed .cordis-rail__badge { display: revert; }
}

@media (max-width: 640px) {
  .cordis-btn, .cordis-input { 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%; }
  .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; }
  /* 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. */
  .cordis-table--responsive td::before { content: attr(data-label); width: 40%;
    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; }
}

/* 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); }
/* 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 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; }

/* ---------- 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; }
}
