/*
 * Omneus Design System — components
 * Generalized from Solidos. All classes are ds-prefixed to avoid
 * collisions when this loads alongside a site's own CSS.
 * Requires tokens.css to be loaded first.
 */

* { box-sizing: border-box; }

body.ds-root {
  margin: 0;
  min-height: 100vh;
  background: radial-gradient(circle at 50% 20%, var(--ds-bg-radial) 0%, var(--ds-bg) 65%);
  color: var(--ds-text);
  font-family: var(--ds-font-display);
  letter-spacing: 0.02em;
}

/* Default link colour. Wrapped in :where() so it weighs (0,0,1) — a bare
   element default that any component class can override. As `.ds-root a` it
   weighed (0,1,1) and quietly beat every single-class rule that colours an
   anchor: .ds-link-back's dimmed resting state never applied to a real link,
   which is the only element it is for, and .ds-btn only looked correct
   because it happens to ask for this same colour. Adding a component that
   wants a different link colour must not require a specificity fight. */
:where(.ds-root) a { color: var(--ds-accent-light); }

/* --- layout: topbar --- */
.ds-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--ds-space-3) var(--ds-space-5);
}
.ds-brand { display: flex; align-items: center; gap: 0.75rem; }
/* width is auto, not a second 56px: the lockup is a wide horizontal
   asset (~4.7:1) and a fixed square would squash it. The slot is sized
   for omneus-mark.svg — use the mark here and let the adjacent h1 carry
   the name, rather than putting the lockup's own wordmark next to it.
   56px sits about 1.5x the h1 + .ds-app-name block beside it, which is
   enough to anchor the bar without the mark becoming the loudest thing
   on the page. This was 96px, inherited from the era of a lighter PNG
   logo; the mark is a solid filled frame and reads far heavier at the
   same height, which made the topbar 128px of mostly icon. */
.ds-brand img { height: 56px; width: auto; }
.ds-brand h1 {
  font-size: 1.1rem;
  font-weight: 800;
  letter-spacing: var(--ds-tracking-wide);
  margin: 0;
  color: var(--ds-accent-light);
}
.ds-brand .ds-app-name {
  color: var(--ds-text-dim);
  font-size: 0.75rem;
  letter-spacing: var(--ds-tracking-wider);
}

.ds-container {
  max-width: 1100px;
  margin: 0 auto;
  padding: var(--ds-space-5);
}
/* Panels cascade in rather than all firing at once. Solidos stopped at 3,
   which silently dropped the cascade on any page with a 4th panel — the
   styleguide itself now has more than that. Capped at 8 so a long page
   doesn't spend a full second staggering; panels past 8 share the last
   delay and come in together, which is the sane end state anyway. */
.ds-container .ds-panel:nth-of-type(1) { animation-delay: 0.05s; }
.ds-container .ds-panel:nth-of-type(2) { animation-delay: 0.15s; }
.ds-container .ds-panel:nth-of-type(3) { animation-delay: 0.25s; }
.ds-container .ds-panel:nth-of-type(4) { animation-delay: 0.35s; }
.ds-container .ds-panel:nth-of-type(5) { animation-delay: 0.45s; }
.ds-container .ds-panel:nth-of-type(6) { animation-delay: 0.55s; }
.ds-container .ds-panel:nth-of-type(7) { animation-delay: 0.65s; }
.ds-container .ds-panel:nth-of-type(n+8) { animation-delay: 0.75s; }

/* padding modifier for a .ds-separator sitting directly under .ds-topbar */
.ds-topbar-separator { padding: 0 var(--ds-space-5) var(--ds-space-4); }

/* --- footer ---
   Bottom-of-page chrome, the counterpart to .ds-topbar: a single quiet line
   of small tracked-out caps, dimmed well below the accent so it reads as a
   sign-off rather than as content. Arrived here from the holding page and
   Solidos independently writing the same six declarations.

   This styles the footer, not where it sits — placement stays with the page,
   the same way .ds-auth-card is separate from .ds-auth-wrap. Both known
   layouts:

   1. A page that can be shorter than the viewport wants a column, so the
      footer lands at the bottom instead of floating under the last section:
      `display: flex; flex-direction: column` on the body (.ds-root already
      supplies min-height: 100vh) plus `flex: 1` on the content element.
   2. A page built on .ds-auth-wrap can't use that column — the wrap is
      min-height: 100vh, so a footer after it pushes past the viewport and
      adds a scrollbar to a screen that otherwise never scrolls. Put the
      footer inside the wrap and pin it (`position: absolute; bottom: 0;
      left: 0; right: 0`), which the wrap's own position: relative supports.

   If a third project writes either of those, promote it too.

   .ds-footer-brand is the "Designed and built by" attribution every EMAX
   site already carries in its own hand-rolled footer — pulled into the
   shared component so a new app gets it via markup instead of re-writing
   the same link+logo CSS again. It's full opacity and its own (non-caps,
   non-tracked) type, deliberately not styled like the dimmed sign-off
   text beside it: the logo is a link being pointed at, not a quiet aside,
   and matches how those existing apps already present it. */
.ds-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--ds-space-2) var(--ds-space-4);
  text-align: center;
  padding: var(--ds-space-3) var(--ds-space-3) var(--ds-space-4);
  font-size: 0.65rem;
  letter-spacing: var(--ds-tracking-wide);
  color: color-mix(in srgb, var(--ds-accent-light) 35%, transparent);
  text-transform: uppercase;
}
.ds-footer-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-2);
  color: var(--ds-text-dim);
  text-transform: none;
  letter-spacing: normal;
  text-decoration: none;
  transition: color var(--ds-transition);
}
.ds-footer-brand:hover { color: var(--ds-text); }
.ds-footer-brand img {
  height: 22px;
  width: auto;
  opacity: 1;
  transition: opacity var(--ds-transition);
}
.ds-footer-brand:hover img { opacity: 0.8; }

/* --- divider / separator ---
   Omneus fades these lines to transparent at both ends and marks the
   header/section rule with a rotated-square diamond. Neither fits this
   skin: the fade reads as a gradient accent this palette doesn't otherwise
   use, and the diamond is the same HUD motif already dropped from
   .ds-panel. Both are now a flat line in --ds-panel-border (matching every
   other border in the system), and the separator's center mark is the
   EMAX logo's "A" (brand/emax-mark.svg) instead of a diamond. */
.ds-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: var(--ds-space-4) 0;
  color: var(--ds-accent);
}
.ds-divider::before, .ds-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--ds-panel-border);
}
.ds-divider span { font-size: 0.7rem; }

.ds-separator {
  display: flex;
  align-items: center;
  gap: 14px;
}
.ds-separator .ds-sep-line { flex: 1; height: 1px; background: var(--ds-panel-border); }
.ds-separator .ds-sep-mark {
  width: 14px; height: 14px;
  flex-shrink: 0;
  display: block;
}

/* --- button --- */
.ds-btn {
  font-family: inherit;
  background: color-mix(in srgb, var(--ds-accent) 8%, transparent);
  border: 1px solid var(--ds-accent);
  color: var(--ds-accent-light);
  padding: 0.6rem 1.4rem;
  font-size: 0.7rem;
  letter-spacing: var(--ds-tracking-wide);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  display: inline-block;
  cursor: pointer;
  border-radius: var(--ds-radius);
  transition: background var(--ds-transition), border-color var(--ds-transition), color var(--ds-transition);
}
.ds-btn:hover {
  background: color-mix(in srgb, var(--ds-accent-light) 16%, transparent);
  border-color: var(--ds-accent-light);
  color: var(--ds-accent-pale);
}
.ds-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.ds-btn.ds-primary {
  background: color-mix(in srgb, var(--ds-accent-light) 14%, transparent);
  border-color: var(--ds-accent-light);
  color: var(--ds-accent-pale);
}
.ds-btn.ds-primary:hover { background: color-mix(in srgb, var(--ds-accent-light) 26%, transparent); }

/* --- icon button (small square action button) ---
   For a row of per-item actions (confirm/delete on a list row, edit) where
   full .ds-btn text buttons ("Select" "Delete") don't fit side by side in
   a table cell without wrapping onto their own lines. Its own component,
   not a .ds-btn variant: .ds-btn's padding/font-size is tuned for a text
   label, not a bare icon at a fixed square size. No Omneus equivalent —
   added here, same as .ds-tabs/.ds-slider below. .ds-icon-btn-confirm/
   -danger tint the two most common cases (green/red); plain .ds-icon-btn
   is the neutral case (e.g. edit). Pair with an inline SVG glyph sized to
   fill most of the box — see the styleguide for markup. */
.ds-icon-btn-row {
  display: flex;
  gap: 8px;
}
.ds-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.2rem;
  height: 2.2rem;
  padding: 0;
  border-radius: var(--ds-radius);
  border: 1px solid var(--ds-panel-border);
  background: var(--ds-panel);
  color: var(--ds-text-dim);
  cursor: pointer;
  transition: background var(--ds-transition), border-color var(--ds-transition), color var(--ds-transition);
}
.ds-icon-btn:hover {
  color: var(--ds-text);
  border-color: var(--ds-accent);
}
.ds-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.ds-icon-btn-confirm {
  border-color: color-mix(in srgb, var(--ds-ok) 45%, transparent);
  color: var(--ds-ok);
}
.ds-icon-btn-confirm:hover {
  background: color-mix(in srgb, var(--ds-ok) 14%, transparent);
  border-color: var(--ds-ok);
  color: var(--ds-ok);
}
.ds-icon-btn-danger {
  border-color: color-mix(in srgb, var(--ds-danger) 45%, transparent);
  color: var(--ds-danger);
}
.ds-icon-btn-danger:hover {
  background: color-mix(in srgb, var(--ds-danger) 14%, transparent);
  border-color: var(--ds-danger);
  color: var(--ds-danger);
}

/* --- panel (see components.js <ds-panel>) ---
   Omneus draws this as a broken-line frame with a diamond bracket bridging
   each corner gap. That motif is HUD-specific and doesn't fit this skin's
   flat, rounded EMAX surfaces, so here the frame is a plain solid border
   with the shared radius token, and the injected .ds-corner diamonds
   (still present in the DOM via components.js) are hidden rather than
   removed there, so this file stays the only thing that diverges. */
@keyframes ds-rise {
  0% { opacity: 0; transform: translateY(24px); }
  100% { opacity: 1; transform: translateY(0); }
}
.ds-panel {
  display: block;
  background-color: var(--ds-panel);
  border: 1px solid var(--ds-panel-border);
  border-radius: var(--ds-radius);
  padding: var(--ds-space-4);
  margin-bottom: var(--ds-space-4);
  position: relative;
  animation: ds-rise 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.ds-panel .ds-corner { display: none; }
.ds-panel h2 {
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: var(--ds-tracking-wide);
  color: var(--ds-accent-light);
  margin: 0 0 var(--ds-space-4);
}

/* --- details (collapsible section) ---
   Plain <details>/<summary> for a section that should stay collapsed
   until asked for (a "set up once, glance at occasionally" panel, say) —
   free keyboard/touch support and state that survives a re-render, which
   a JS-driven show/hide doesn't get without extra work. <summary> reads
   as a section title matching .ds-panel h2 above (same size/weight/
   tracking/colour, since this replaces a h2 when used inside a panel),
   with a chevron that points down while collapsed ("there's more below")
   and flips to point up once open ("click to collapse"). Usage:
     <ds-panel>
       <details class="ds-details">
         <summary>Section title</summary>
         ...content...
       </details>
     </ds-panel>
   Nothing but CSS — no accompanying JS, the browser's own <details>
   toggle behaviour is all this needs. */
.ds-details > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: var(--ds-tracking-wide);
  color: var(--ds-accent-light);
}
.ds-details > summary::-webkit-details-marker { display: none; }
/* ::after, not ::before — the chevron sits at the row's right edge
   (justify-content:space-between above), same side a native <select>'s
   own chevron reads on, rather than leading the title on the left. */
.ds-details > summary::after {
  content: '';
  width: 7px;
  height: 7px;
  flex-shrink: 0;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg); /* down chevron — collapsed, "more below" */
  transition: transform var(--ds-transition);
}
.ds-details[open] > summary { margin-bottom: var(--ds-space-4); }
.ds-details[open] > summary::after {
  transform: rotate(-135deg); /* up chevron — open, "click to collapse" */
}

/* --- breadcrumbs --- */
.ds-breadcrumbs {
  font-size: 0.8rem;
  color: var(--ds-text-dim);
  margin-bottom: 0.75rem;
  word-break: break-all;
}
.ds-breadcrumbs button {
  background: none;
  border: none;
  color: var(--ds-accent-light);
  cursor: pointer;
  font-family: inherit;
  font-size: 0.8rem;
  padding: 0;
}

/* --- tabs (view switcher) ---
   For switching between views in an app shell, not for paging within a
   document. A row of separate bordered-and-gapped pills read as buttons,
   not as a tab bar, so instead these sit adjacent — borders collapsed
   edge-to-edge via negative margin, the classic folder-tab construction —
   sharing one baseline (.ds-tabs's own border-bottom) rather than each
   tab drawing all four of its own sides. The active tab erases its span
   of that baseline (background + border-bottom-color both set to
   --ds-panel) and overlaps 4px into each neighbour so it visually sits a
   layer above them, connected to the panel content below rather than
   just another item in the row. .ds-tab-panel toggles exactly like
   Bootstrap/ARIA tabpanels do: plain display none/block, no transition —
   switching views shouldn't fight a panel's own entrance animation if its
   content is a .ds-panel. Markup and wiring are the consumer's; this is
   styling only, same as every other control here.

   --ds-panel is hardcoded rather than tokenized for "whatever's behind
   it": .ds-tabs is meant to sit inside a .ds-panel (see the styleguide),
   so erasing the baseline against that specific surface is the case that
   matters. Used directly on the page background instead, the active tab
   would show a faint seam against --ds-bg — acceptable for now, revisit
   if that usage comes up. */
.ds-tabs {
  display: flex;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--ds-panel-border);
  margin-bottom: var(--ds-space-4);
}
.ds-tab {
  position: relative;
  font-family: inherit;
  background: transparent;
  border: 1px solid var(--ds-panel-border);
  border-bottom: none;
  border-radius: var(--ds-radius) var(--ds-radius) 0 0;
  color: var(--ds-text-dim);
  padding: 0.5rem 1.1rem;
  font-size: 0.8rem;
  font-weight: 600;
  text-decoration: none;
  margin-bottom: -1px;
  cursor: pointer;
  transition: background var(--ds-transition), border-color var(--ds-transition), color var(--ds-transition);
}
.ds-tab + .ds-tab { margin-left: -1px; }
.ds-tab:hover { color: var(--ds-text); z-index: 1; }
.ds-tab:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ds-accent) 35%, transparent);
  z-index: 2;
}
.ds-tab.ds-active {
  background: var(--ds-panel);
  border-color: var(--ds-panel-border);
  border-bottom-color: var(--ds-panel);
  color: var(--ds-accent-light);
  margin-left: -4px;
  margin-right: -4px;
  padding-left: calc(1.1rem + 4px);
  padding-right: calc(1.1rem + 4px);
  z-index: 2;
}
/* The -4px margin above overlaps the active tab's border into a
   neighbouring tab on each side — correct when there is one, but the
   first tab in a row has nothing on its left to overlap, so activating it
   shifted its whole bordered box left of wherever the row actually
   starts (and the mirror case on the last tab's right edge). Reset the
   margin/padding pair on whichever end has no neighbour instead of
   dropping the overlap technique everywhere. */
.ds-tabs > .ds-tab:first-child.ds-active {
  margin-left: 0;
  padding-left: 1.1rem;
}
.ds-tabs > .ds-tab:last-child.ds-active {
  margin-right: 0;
  padding-right: 1.1rem;
}
.ds-tab:disabled { opacity: 0.35; cursor: not-allowed; }

.ds-tab-panel { display: none; }
.ds-tab-panel.ds-active { display: block; }

/* --- ticker (scrolling notice bar) ---
   For a persistent, glance-from-across-the-room notice — e.g. a pitlane
   speed limit or safety car rules on a garage display, where there's no
   screen space for a static paragraph and no one has time to scroll a
   page. Not for anything that needs to be read carefully or referred
   back to — an alert (.ds-alert) is the right component for that.
   <ds-ticker label="NOTICE">your text</ds-ticker> — see components.js for
   the custom element that builds the track/content structure below and
   measures scroll duration so long and short messages move at the same
   visual speed rather than the same fixed duration reading faster or
   slower depending on length. --ds-warn, not --ds-info/--ds-panel-border:
   this is meant to read as "notice," the loudest non-danger tone already
   in the palette, not as neutral chrome. */
.ds-ticker {
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  padding: 0.5rem var(--ds-space-4);
  background: color-mix(in srgb, var(--ds-warn) 14%, var(--ds-bg));
  border-top: 1px solid color-mix(in srgb, var(--ds-warn) 45%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--ds-warn) 45%, transparent);
  overflow: hidden;
}
.ds-ticker-label {
  flex-shrink: 0;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: var(--ds-tracking-wide);
  text-transform: uppercase;
  color: var(--ds-warn);
  padding-right: var(--ds-space-3);
  border-right: 1px solid color-mix(in srgb, var(--ds-warn) 45%, transparent);
}
.ds-ticker-track {
  flex: 1;
  overflow: hidden;
  white-space: nowrap;
  position: relative;
}
.ds-ticker-content {
  display: inline-block;
  padding-left: 100%;
  font-size: 0.85rem;
  color: var(--ds-text);
  animation: ds-ticker-scroll linear infinite;
}
@keyframes ds-ticker-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-100%); }
}
/* A message no one can keep still long enough to read defeats the whole
   point of a notice — respect the OS-level request to stop it moving
   rather than only slowing it down. */
@media (prefers-reduced-motion: reduce) {
  .ds-ticker-content { animation: none; padding-left: 0; white-space: normal; }
  .ds-ticker-track { white-space: normal; }
}

/* --- form fields --- */
.ds-field { text-align: left; margin-bottom: var(--ds-space-3); }
.ds-field label {
  display: block;
  font-size: 0.7rem;
  color: var(--ds-text-dim);
  margin-bottom: 0.35rem;
  letter-spacing: 0.1em;
}
/* The descendant element selectors are wrapped in :where() for the same
   reason as the `a` rule above: as `.ds-field input` this weighed (0,1,1)
   and beat every single-class control component placed inside a field.
   `.ds-select` lost its chevron and its chevron padding — visible in this
   repo's own styleguide, which marks a select up exactly that way — and a
   `.ds-checkbox` inside a field was stretched to width:100%. At (0,1,0)
   these are a default that later component classes override on source
   order, which is what they were always meant to be. */
.ds-field :where(input, select, textarea), .ds-input {
  width: 100%;
  background: var(--ds-input-bg);
  border: 1px solid var(--ds-panel-border);
  color: var(--ds-text);
  padding: 0.77rem 0.65rem;
  font-family: inherit;
  font-size: 0.85rem;
  border-radius: var(--ds-radius);
}
/* Single-line controls get a hard height on top of the padding above,
   not just the padding alone. Earlier versions relied on 0.77rem padding
   (and, for date/time inputs specifically, a separately-tuned 0.69rem)
   to *result in* a 2.6rem border-box height — this assumes a fixed
   line-height for the field's text, but a native control's intrinsic
   line-height for the same font-size isn't actually fixed across
   engines (confirmed: Chromium and Firefox disagree, and a multi-segment
   control like date/time needs more of it than plain text does in the
   same engine). An explicit height is a hard cap as well as a floor —
   box-sizing:border-box (set globally, top of file) keeps the padding
   above inset inside it rather than adding to it — so every single-line
   field lands on exactly 2.6rem regardless of what height its own
   content would otherwise have computed to. This is also why
   .ds-controls-row .ds-btn and .ds-file (below) use height, not
   min-height: same reasoning, same reference height, so a field, a file
   input and a button never mismatch when they share a row. Textarea is
   deliberately excluded — its height is rows-driven and must never be
   capped to one line. */
.ds-field :where(input, select), .ds-input {
  height: 2.6rem;
}
.ds-field :where(input, select):focus, .ds-input:focus {
  outline: none;
  border-color: var(--ds-accent);
}
/* The chevron is drawn from two gradient strokes rather than an SVG data
   URI. A data URI can't read a custom property, so the old version baked
   in a fixed gold hex and kept drawing a gold arrow on projects that had overridden
   the accent (RaceOps in EMAX red, say) — the one thing tokens.css
   promises is re-skinnable. <select> is a replaced element, so a ::after
   chevron is not an option either (same Firefox limit as the checkbox).
   Each layer is a 5px box holding one diagonal band: 45deg leans "\",
   135deg leans "/", and side by side they meet as a "v". */
.ds-select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  /* width is restated here rather than inherited from the .ds-field rule
     above, which no longer reaches a classed control (see its comment). */
  width: 100%;
  background-color: var(--ds-input-bg);
  background-image:
    linear-gradient(45deg, transparent calc(50% - 0.7px), var(--ds-accent-light) calc(50% - 0.7px) calc(50% + 0.7px), transparent calc(50% + 0.7px)),
    linear-gradient(135deg, transparent calc(50% - 0.7px), var(--ds-accent-light) calc(50% - 0.7px) calc(50% + 0.7px), transparent calc(50% + 0.7px));
  background-size: 5px 5px;
  background-position: right calc(0.85rem + 5px) center, right 0.85rem center;
  background-repeat: no-repeat;
  border: 1px solid var(--ds-panel-border);
  color: var(--ds-text);
  padding: 0.77rem 2.2rem 0.77rem 0.65rem;
  font-family: inherit;
  font-size: 0.85rem;
}
.ds-error { color: var(--ds-danger); font-size: 0.75rem; margin-bottom: var(--ds-space-3); }

/* --- file input ---
   <input type="file"> renders an unstyled OS button by default, and the
   themed ::file-selector-button this used to be (still visible in git
   history) never reached a stable cross-engine fix: its button and
   filename text live in the input's UA shadow tree, and how that shadow
   content responds to padding/flex is engine-specific. A height tuned to
   land it flush with a sibling field in Chromium (see the removed rule's
   comment for the full trail — display:flex did nothing there because
   host layout properties don't reach shadow content; padding alone did,
   but only by empirical measurement) did NOT hold in Firefox, whose file
   input uses its own internal flex layout with different intrinsic
   sizing — same symptom (button sitting low/short against its row), new
   engine, no CSS-only number that fixes both at once. Rather than keep
   chasing a per-engine target, the native input is visually hidden and a
   <label class="ds-btn"> + a plain <span class="ds-file-name"> stand in
   for it — ordinary elements this stylesheet already lays out
   predictably, so their height and alignment are exactly as reliable as
   any other .ds-controls-row content, on every engine, because none of
   it depends on shadow-tree internals. Markup:

     <div class="ds-field">
       <label>File</label>
       <div class="ds-file">
         <label class="ds-btn">
           Browse…
           <input type="file">
         </label>
         <span class="ds-file-name">No file chosen</span>
       </div>
     </div>

   The label wraps the input rather than using for="id" — clicking
   anywhere on the label (button face or text) opens the native picker
   via ordinary label/control association, no id coupling or JS required
   for that part. components.js delegates a document-level 'change'
   listener that updates the sibling .ds-file-name text (multiple files:
   "N files selected"; none: back to whatever text the span started
   with) — the only behaviour this component needs.

   The button sits beside the filename field, not inside a shared
   bordered box wrapping both (2026-09-30, after review) — nesting a
   bordered/backgrounded .ds-btn inside another bordered/backgrounded box
   read as the button sitting on top of the field rather than next to it,
   and looked worse than a plain field-input pairing wherever the
   surrounding layout gave the row less width than expected. Each of the
   two children now owns its field-equivalent height independently
   (height, not min-height — same cross-engine reasoning as
   .ds-controls-row .ds-btn above), so nothing needs a shared container
   height to line up. */
.ds-file {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
}
.ds-file-name {
  flex: 1 1 auto;
  height: 2.6rem;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  padding: 0 0.65rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: var(--ds-input-bg);
  border: 1px solid var(--ds-panel-border);
  border-radius: var(--ds-radius);
  color: var(--ds-text-dim);
  font-size: 0.8rem;
}
.ds-file label.ds-btn {
  flex: 0 0 auto;
  height: 2.6rem;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: 0;
}
/* Visually hidden but still focusable/keyboard-operable (the standard
   "sr-only" clip pattern) — NOT display:none, which would also drop it
   from the tab order and break the label's click-to-open association in
   some engines. */
.ds-file input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- checkbox / radio ---
   Custom-drawn (not native accent-color) because the shared shape here is
   the diamond used by .ds-corner and .ds-sep-diamond, which accent-color
   can't produce. The diamond is the input's own border turned 45deg —
   deliberately NOT a ::before/::after on the input, because Firefox does
   not generate pseudo-elements on <input> (replaced element), so a
   pseudo-drawn tick renders in Chrome and silently vanishes in Firefox.
   Checkbox and radio share the frame, so the checked state carries the
   one-vs-many distinction: checkbox fills solid, radio keeps a gap and
   shows a concentric pip. */
.ds-checkbox, .ds-radio {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  /* the 45deg turn leaves the layout box at 0.85rem while the visual grows
     to ~1.2rem across, which is why .ds-check-row's gap is wider than it
     looks it needs to be */
  width: 0.85rem; height: 0.85rem;
  border: 1px solid var(--ds-panel-border);
  border-radius: var(--ds-radius);
  background: var(--ds-input-bg);
  display: inline-block;
  vertical-align: middle;
  position: relative;
  flex-shrink: 0;
  margin: 0;
  cursor: pointer;
  transform: rotate(45deg);
  transition: border-color var(--ds-transition), background-color var(--ds-transition), box-shadow var(--ds-transition);
}
.ds-checkbox:hover, .ds-radio:hover { border-color: var(--ds-accent); }
.ds-checkbox:checked, .ds-radio:checked,
.ds-checkbox:indeterminate {
  border-color: var(--ds-accent-light);
  background: var(--ds-accent-light);
}
/* radio: hollow the centre so it reads as a pip rather than a full fill */
.ds-radio:checked { box-shadow: inset 0 0 0 3px var(--ds-input-bg); }
/* partial selection (e.g. a select-all header box over a mixed table).
   -45deg cancels the element's own 45deg turn, so the hard stop lands as a
   level split on screen and the diamond reads half-filled; `to top` here
   would split it corner-to-corner instead. */
.ds-checkbox:indeterminate {
  background: linear-gradient(-45deg, var(--ds-accent-light) 50%, var(--ds-input-bg) 50%);
}
.ds-checkbox:focus-visible, .ds-radio:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ds-accent) 35%, transparent);
}
/* keep the pip when a checked radio also has focus (box-shadow doesn't stack
   across rules — both layers have to be restated together) */
.ds-radio:checked:focus-visible {
  box-shadow: inset 0 0 0 3px var(--ds-input-bg),
              0 0 0 2px color-mix(in srgb, var(--ds-accent) 35%, transparent);
}
.ds-checkbox:disabled, .ds-radio:disabled { opacity: 0.35; cursor: not-allowed; }

.ds-check-row {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  font-size: 0.85rem;
  margin-bottom: 0.5rem;
}
.ds-check-row label { cursor: pointer; color: var(--ds-text); }
.ds-check-row:last-child { margin-bottom: 0; }

/* --- list --- */
.ds-list {
  list-style: none;
  margin: 0; padding: 0;
  max-height: 320px;
  overflow-y: auto;
  border: 1px solid var(--ds-panel-border);
}
.ds-list li {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--ds-panel-border);
  cursor: pointer;
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.ds-list li:last-child { border-bottom: none; }
.ds-list li:hover { background: color-mix(in srgb, var(--ds-accent) 12%, var(--ds-panel)); }
.ds-list li.ds-selected {
  background: var(--ds-panel-border);
  color: var(--ds-accent-light);
}
.ds-list .ds-icon { color: var(--ds-accent); width: 1.2em; }

/* --- table --- */
.ds-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 0.8rem; }
.ds-table th, .ds-table td {
  text-align: left;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--ds-panel-border);
  overflow-wrap: anywhere;
}
.ds-table th { color: var(--ds-text-dim); font-weight: 600; letter-spacing: 0.05em; font-size: 0.7rem; }
.ds-status-ok { color: var(--ds-ok); }
.ds-status-fail { color: var(--ds-danger); }
/* row needing attention (e.g. an unmatched/failed item in a results table) */
.ds-table tr.ds-row-warn { color: var(--ds-text-dim); }
.ds-table tr.ds-row-warn td { color: var(--ds-danger); }

/* compact input for inline use inside table cells (vs the larger .ds-field input) */
.ds-input-compact {
  width: 100%;
  background: var(--ds-input-bg);
  border: 1px solid var(--ds-panel-border);
  color: var(--ds-text);
  padding: 0.4rem 0.5rem;
  font-family: inherit;
  font-size: 0.8rem;
  border-radius: var(--ds-radius);
}
.ds-input-compact:focus { outline: none; border-color: var(--ds-accent); }

/* --- progress --- */
.ds-progress-track {
  width: 100%; height: 8px;
  background: var(--ds-input-bg);
  border: 1px solid var(--ds-panel-border);
  margin-top: 0.5rem;
  overflow: hidden;
}
.ds-progress-fill { height: 100%; width: 0%; background: var(--ds-accent-light); transition: width 0.3s ease; }

.ds-empty { color: var(--ds-text-dim); font-size: 0.85rem; font-style: italic; }

/* --- controls row --- */
.ds-controls-row { display: flex; gap: var(--ds-space-3); align-items: flex-end; flex-wrap: wrap; margin-top: var(--ds-space-3); }
.ds-controls-row .ds-field { margin-bottom: 0; }
/* .ds-controls-row only ever adds margin-top on itself, never margin-
   bottom, so a submit button placed directly after one (rather than as a
   flex item inside it) sits flush against the row with nothing separating
   them. */
.ds-controls-row + .ds-btn {
  margin-top: var(--ds-space-3);
}
/* A .ds-btn beside a .ds-field in the same row doesn't line up with it
   height-wise by default — the button's own padding/font-size and the
   field's input padding don't resolve to the same height on their own.
   2.6rem is the shared reference height every row-sharing control is
   tuned to: .ds-field's own input/select height (above) and .ds-file's
   height (also above) are both set to land here too, so a field, a file
   input and a button never mismatch in the same row. Scoped to
   .ds-controls-row rather than every .ds-btn everywhere: a standalone
   button (a dialog action, a lone CTA) was never the problem and
   doesn't need to match a field it isn't sitting next to.

   `height`, not `min-height`: a button's own padding/font-size/line-
   height resolve to a *taller* natural height than 2.6rem in some
   engines (confirmed in Firefox — Chromium's is shorter and a
   min-height floor was enough there, which is how this shipped once
   already and still measured wrong when someone actually opened it in
   Firefox). `height` is a hard cap as well as a floor — box-sizing:
   border-box (set globally, top of file) keeps the button's own
   padding inside that cap rather than adding to it — so the label text
   is centred within exactly 2.6rem on every engine regardless of what
   height it would otherwise have chosen for itself. */
.ds-controls-row .ds-btn {
  height: 2.6rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* --- decorative rings ---
   Standalone background decoration (not login-specific — use anywhere:
   loading states, hero sections, empty states). Renders centered on
   its containing block: position:fixed for a viewport-wide backdrop
   (the original login usage), or drop it in a position:relative
   wrapper and add position:absolute inline to scope it to one section. */
.ds-ring {
  position: fixed;
  top: 50%; left: 50%;
  border: 1px solid color-mix(in srgb, var(--ds-accent-light) 8%, transparent);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.ds-ring::before {
  content: '';
  position: absolute;
  top: -2px; left: 50%;
  width: 4px; height: 4px;
  background: var(--ds-accent-light);
  border-radius: 50%;
  transform: translateX(-50%);
  box-shadow: 0 0 8px 2px color-mix(in srgb, var(--ds-accent-light) 50%, transparent);
}
.ds-ring.ds-ring-1 { width: 600px; height: 600px; animation: ds-spin 40s linear infinite; }
.ds-ring.ds-ring-2 {
  width: 420px; height: 420px;
  border-color: color-mix(in srgb, var(--ds-accent-light) 5%, transparent);
  animation: ds-spin 28s linear infinite reverse;
}
@keyframes ds-spin {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to { transform: translate(-50%, -50%) rotate(360deg); }
}

/* --- auth card (login/sign-in screens) --- */
.ds-auth-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; position: relative; }
.ds-auth-card {
  background: var(--ds-panel);
  border: 1px solid var(--ds-panel-border);
  padding: var(--ds-space-6) var(--ds-space-5);
  text-align: center;
  width: 320px;
}
/* Constrained on width rather than height: this slot takes the full
   lockup, which is wide and short, so a fixed height would overflow the
   320px card. max-width leaves the card's padding intact; height:auto
   keeps the aspect whether a project drops in the lockup or the mark. */
.ds-auth-card img {
  width: 100%;
  max-width: 240px;
  height: auto;
  margin-bottom: var(--ds-space-3);
}
.ds-auth-card h1 {
  font-size: 1rem;
  letter-spacing: var(--ds-tracking-wider);
  color: var(--ds-accent-light);
  margin: 0 0 var(--ds-space-4);
}

/* Bounds the full-screen auth layout so a styleguide can show it inline.
   .ds-auth-wrap is min-height:100vh and the rings are position:fixed, both
   of which need a containing block to be previewable on a page that is not
   itself the login screen. Documentation aid — real login pages use
   .ds-auth-wrap directly on the body. */
.ds-auth-preview {
  position: relative;
  overflow: hidden;
  height: 560px;
  border: 1px solid var(--ds-panel-border);
  background: var(--ds-bg);
}
.ds-auth-preview .ds-auth-wrap { min-height: 100%; }
.ds-auth-preview .ds-ring { position: absolute; }

/* --- typography helpers ---
   .ds-root sets the display face on everything, which is right for the
   HUD chrome and wrong for sentences. .ds-prose opts a block back into
   the body face; use it for anything longer than a label. */
.ds-prose {
  font-family: var(--ds-font-body);
  font-size: 0.85rem;
  line-height: 1.7;
  letter-spacing: 0.02em;
  color: var(--ds-text);
}
.ds-prose a { color: var(--ds-accent-light); }

/* small tracked-out kicker above a heading (from the holding page) */
.ds-eyebrow {
  font-size: 0.6rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ds-accent);
  margin: 0 0 var(--ds-space-2);
}

/* --- badge (inline status pill) ---
   For the state of a thing in a list or table — Queued / Running /
   Failed. .ds-status-ok and .ds-status-fail stay the right choice for
   colouring bare text in a cell; this is for when it needs a chip. */
.ds-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--ds-panel-border);
  border-radius: var(--ds-radius);
  background: color-mix(in srgb, var(--ds-text-dim) 10%, transparent);
  color: var(--ds-text-dim);
  font-size: 0.65rem;
  letter-spacing: var(--ds-tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
}
/* the leading diamond is the shared motif at pip scale; it carries the
   status colour so the chip still parses at a glance in a dense table */
.ds-badge::before {
  content: '';
  width: 5px; height: 5px;
  background: currentColor;
  transform: rotate(45deg);
  flex-shrink: 0;
}
.ds-badge.ds-badge-ok { color: var(--ds-ok); border-color: color-mix(in srgb, var(--ds-ok) 45%, transparent); background: color-mix(in srgb, var(--ds-ok) 12%, transparent); }
.ds-badge.ds-badge-warn { color: var(--ds-warn); border-color: color-mix(in srgb, var(--ds-warn) 45%, transparent); background: color-mix(in srgb, var(--ds-warn) 12%, transparent); }
.ds-badge.ds-badge-fail { color: var(--ds-danger); border-color: color-mix(in srgb, var(--ds-danger) 45%, transparent); background: color-mix(in srgb, var(--ds-danger) 12%, transparent); }
.ds-badge.ds-badge-info { color: var(--ds-info); border-color: color-mix(in srgb, var(--ds-info) 45%, transparent); background: color-mix(in srgb, var(--ds-info) 12%, transparent); }

/* --- alert (persistent inline callout) ---
   .ds-error is a one-line string under a field; this is the bordered
   block for something that stays on screen and needs explaining. */
.ds-alert {
  position: relative;
  display: flex;
  gap: 0.75rem;
  padding: var(--ds-space-3);
  margin-bottom: var(--ds-space-3);
  border: 1px solid var(--ds-panel-border);
  border-left-width: 2px;
  background: color-mix(in srgb, var(--ds-text-dim) 7%, transparent);
  font-size: 0.8rem;
  line-height: 1.6;
}
.ds-alert .ds-alert-body { flex: 1; }
/* Extra right padding only when a dismiss button is actually present, so
   the title/body text doesn't run under it — a plain (non-dismissible)
   .ds-alert stays exactly as it was. */
.ds-alert:has(.ds-alert-dismiss) .ds-alert-body { padding-right: 1.6rem; }
/* Dismiss button (added 2026-09-30) — top-right "×", removes the alert
   from the DOM on click (components.js, one delegated listener). That's
   the whole of what this component owns: whether "dismissed" should
   persist (e.g. per-user, per-item, until some condition changes) is left
   entirely to the consuming app, since that's app state, not something a
   styling-only library should own. */
.ds-alert-dismiss {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 1.4rem;
  height: 1.4rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--ds-radius);
  color: var(--ds-text-dim);
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  transition: background var(--ds-transition), color var(--ds-transition);
}
.ds-alert-dismiss:hover {
  background: color-mix(in srgb, var(--ds-text-dim) 16%, transparent);
  color: var(--ds-text);
}
.ds-alert .ds-alert-title {
  display: block;
  font-size: 0.7rem;
  letter-spacing: var(--ds-tracking-wide);
  text-transform: uppercase;
  margin-bottom: 0.35rem;
}
/* body copy inside an alert is a sentence, so it takes the body face */
.ds-alert p { font-family: var(--ds-font-body); margin: 0; color: var(--ds-text); }
.ds-alert.ds-alert-ok { border-left-color: var(--ds-ok); background: color-mix(in srgb, var(--ds-ok) 8%, transparent); }
.ds-alert.ds-alert-ok .ds-alert-title { color: var(--ds-ok); }
.ds-alert.ds-alert-warn { border-left-color: var(--ds-warn); background: color-mix(in srgb, var(--ds-warn) 8%, transparent); }
.ds-alert.ds-alert-warn .ds-alert-title { color: var(--ds-warn); }
.ds-alert.ds-alert-fail { border-left-color: var(--ds-danger); background: color-mix(in srgb, var(--ds-danger) 8%, transparent); }
.ds-alert.ds-alert-fail .ds-alert-title { color: var(--ds-danger); }
.ds-alert.ds-alert-info { border-left-color: var(--ds-info); background: color-mix(in srgb, var(--ds-info) 8%, transparent); }
.ds-alert.ds-alert-info .ds-alert-title { color: var(--ds-info); }

/* --- switch (on/off setting) ---
   Distinct from .ds-checkbox on purpose: a checkbox selects an item, a
   switch flips a setting — and that distinction now carries the weight
   the shape used to, since checkbox and radio share the diamond.
   The knob is a background layer slid with background-position, not a
   ::after, for the same Firefox reason as the checkbox. Omneus draws that
   layer as a flat-cornered rectangle to match its sharp HUD frames; this
   skin's track is a pill (border-radius half its own height), so the knob
   is drawn as a radial-gradient circle instead — a square knob inside a
   pill track reads as broken, not just off-brand. `circle closest-side`
   sizes the circle to exactly fill the background-size box, so it stays a
   full circle rather than clipping into the corners. That box was 0.7rem
   against a 1.1rem track — the knob is mathematically centered either
   way ("50%" positioning scales with the leftover space, not a fixed
   offset), but at 0.7rem it only leaves ~2px of margin top and bottom, so
   the knob reads as oversized and the thin, easy-to-misjudge-by-eye gap
   reads as off-center even though it isn't. 0.55rem leaves closer to 3.5px
   each side. */
.ds-switch {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  width: 2.2rem; height: 1.1rem;
  flex-shrink: 0;
  margin: 0;
  border: 1px solid var(--ds-panel-border);
  border-radius: 999px;
  background-color: var(--ds-input-bg);
  background-image: radial-gradient(circle closest-side, var(--ds-text-dim) 100%, transparent 100%);
  background-repeat: no-repeat;
  background-size: 0.55rem 0.55rem;
  background-position: 0.2rem center;
  cursor: pointer;
  vertical-align: middle;
  transition: background-position var(--ds-transition), background-color var(--ds-transition), border-color var(--ds-transition);
}
.ds-switch:hover { border-color: var(--ds-accent); }
.ds-switch:checked {
  border-color: var(--ds-accent-light);
  background-color: color-mix(in srgb, var(--ds-accent) 25%, var(--ds-input-bg));
  background-image: radial-gradient(circle closest-side, var(--ds-accent-light) 100%, transparent 100%);
  background-position: calc(100% - 0.2rem) center;
}
.ds-switch:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ds-accent) 35%, transparent);
}
.ds-switch:disabled { opacity: 0.35; cursor: not-allowed; }

/* --- slider (range input) ---
   No filled-left-of-thumb track: Firefox has ::-moz-range-progress for
   that, but WebKit has no equivalent, and faking one there means a
   background-size kept in sync with the value on every `input` event —
   real JS a consumer would have to wire up themselves, not something this
   stylesheet can own. So the track stays one flat colour in both engines
   and the thumb alone shows the value, same trade Omneus makes everywhere
   else in this file (no JS beyond the two custom elements). The two
   pseudo-elements can't share a rule — a selector list where either half
   is invalid (`:is()` included) drops the whole rule in both engines. */
.ds-slider {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  width: 100%;
  height: 1.25rem;
  background: transparent;
  margin: 0;
  cursor: pointer;
}
.ds-slider::-webkit-slider-runnable-track {
  height: 4px;
  background: var(--ds-input-bg);
  border: 1px solid var(--ds-panel-border);
  border-radius: 999px;
}
.ds-slider::-moz-range-track {
  height: 4px;
  background: var(--ds-input-bg);
  border: 1px solid var(--ds-panel-border);
  border-radius: 999px;
}
.ds-slider::-moz-range-progress {
  height: 4px;
  background: var(--ds-accent-light);
  border-radius: 999px;
}
/* thumb centers on the 4px track via a negative margin-top: box-sizing is
   border-box (see the sheet-wide `*` rule), so the 2px border doesn't add
   to the 16px the centering math is done against. */
.ds-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px; height: 16px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--ds-accent-light);
  border: 2px solid var(--ds-panel);
  box-shadow: 0 0 0 1px var(--ds-accent-light);
  transition: box-shadow var(--ds-transition);
}
.ds-slider::-moz-range-thumb {
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--ds-accent-light);
  border: 2px solid var(--ds-panel);
  box-shadow: 0 0 0 1px var(--ds-accent-light);
  transition: box-shadow var(--ds-transition);
}
.ds-slider:hover::-webkit-slider-thumb, .ds-slider:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 1px var(--ds-accent-light), 0 0 0 4px color-mix(in srgb, var(--ds-accent) 35%, transparent);
}
.ds-slider:hover::-moz-range-thumb, .ds-slider:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 1px var(--ds-accent-light), 0 0 0 4px color-mix(in srgb, var(--ds-accent) 35%, transparent);
}
.ds-slider:focus-visible { outline: none; }
.ds-slider:disabled { cursor: not-allowed; }
.ds-slider:disabled::-webkit-slider-thumb { background: var(--ds-text-dim); box-shadow: none; }
.ds-slider:disabled::-moz-range-thumb { background: var(--ds-text-dim); box-shadow: none; }

/* --- spinner (indeterminate, compact) ---
   .ds-ring shrunk to component scale: the same faint circle with the same
   glowing bead orbiting it. Borrowing the ring motif rather than the
   diamond one is deliberate — a diamond spun on its own axis reads as a
   turning chevron, not as "working", and the diamond already carries the
   waiting state via .ds-pulse. */
.ds-spinner {
  display: inline-block;
  position: relative;
  width: 1rem; height: 1rem;
  /* heavier than .ds-ring's 8%: that value is tuned for a 600px background
     element, and at 1rem in the foreground it disappears */
  border: 1px solid color-mix(in srgb, var(--ds-accent-light) 30%, transparent);
  border-radius: 50%;
  animation: ds-spinner-turn 1.1s linear infinite;
}
.ds-spinner::before {
  content: '';
  position: absolute;
  top: -2px; left: 50%;
  width: 4px; height: 4px;
  background: var(--ds-accent-light);
  border-radius: 50%;
  transform: translateX(-50%);
  box-shadow: 0 0 6px 1px color-mix(in srgb, var(--ds-accent-light) 50%, transparent);
}
@keyframes ds-spinner-turn {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* Breathing pulse for a status mark that is waiting rather than working
   (lifted from the holding page's construction icon).
   Opacity only, no scale. The holding page pulsed a 48px SVG, where a
   1 -> 1.06 scale is harmless; the marks here are ~12px with 1px borders
   and 1px cross bars, and sweeping their size across fractional device
   pixels makes those hairlines resample every frame and visibly crawl.
   Opacity composites without touching geometry, so it stays clean at any
   size — and it leaves `transform` free for the rotate(45deg) the
   diamonds already carry. */
@keyframes ds-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}
.ds-pulse { animation: ds-pulse 3s ease-in-out infinite; }
/* .ds-ornament is a diamond (rotated square) by default; the waiting mark
   drops that rotation to stay a plain square, in line with this skin
   dropping the diamond motif from .ds-panel's corners. */
.ds-ornament.ds-pulse { transform: none; }

/* --- indeterminate progress (scan sweep) ---
   The holding page's "Initialising / Stand by" bar, for work with no
   known percentage. It animated `left`, which relayouts every frame;
   translateX does the same thing on the compositor. */
.ds-progress-track.ds-indeterminate { position: relative; }
.ds-progress-track.ds-indeterminate .ds-progress-fill {
  width: 100%;
  background: linear-gradient(90deg, transparent, var(--ds-accent-light), transparent);
  animation: ds-scan 2.8s ease-in-out infinite;
}
@keyframes ds-scan {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}

/* --- dialog ---
   Built on native <dialog> opened with showModal(), which gets focus
   trapping, Esc-to-close, inertness of the page behind and top-layer
   stacking from the browser — all things a div-based modal has to
   reimplement badly. */
.ds-dialog {
  background: var(--ds-panel);
  border: 1px solid var(--ds-panel-border);
  border-radius: var(--ds-radius);
  color: var(--ds-text);
  font-family: var(--ds-font-display);
  padding: var(--ds-space-4);
  max-width: min(440px, calc(100vw - 2rem));
  position: relative;
  animation: ds-rise 0.35s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.ds-dialog::backdrop {
  /* ::backdrop does not inherit from the page, so custom properties are
     out of reach here and this one colour stays literal */
  background: rgba(12, 12, 12, 0.72);
}
.ds-dialog h2 {
  font-size: 0.8rem;
  letter-spacing: var(--ds-tracking-wide);
  color: var(--ds-accent-light);
  margin: 0 0 var(--ds-space-3);
}
.ds-dialog .ds-dialog-actions {
  display: flex;
  gap: var(--ds-space-3);
  justify-content: flex-end;
  margin-top: var(--ds-space-4);
}
/* Shows the dialog's styling inline on a page — a real <dialog> is
   display:none until opened, and ::backdrop only exists in the top layer,
   so a styleguide can't otherwise show either. Documentation aid, not a
   substitute for showModal(). */
.ds-dialog-preview {
  display: flex;
  justify-content: center;
  padding: var(--ds-space-5) var(--ds-space-3);
  background: rgba(12, 12, 12, 0.72);
  border: 1px solid var(--ds-panel-border);
  margin-bottom: var(--ds-space-3);
}
.ds-dialog-preview .ds-dialog { animation: none; }

/* --- atmosphere (holding-page background layers) ---
   Opt-in modifiers on the same <body class="ds-root"> element. Both are
   fixed, non-interactive overlays; .ds-glow sits under content, .ds-grain
   sits over it at low opacity to kill banding in the radial gradient. */
.ds-root.ds-glow::before {
  content: '';
  position: fixed;
  inset: 0;
  background: radial-gradient(ellipse 70% 60% at 50% 50%, color-mix(in srgb, var(--ds-accent-light) 10%, transparent) 0%, transparent 70%);
  pointer-events: none;
  z-index: 0;
}
.ds-root.ds-grain::after {
  content: '';
  position: fixed;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
  opacity: 0.4;
  pointer-events: none;
  z-index: 1;
}
/* Keep real content above both layers.
   .ds-ring is excluded: it is background decoration, not content, and it
   positions itself. Without the :not(), this rule's `position: relative`
   at (0,2,0) outranked .ds-ring's own `position: fixed` at (0,1,0), so on
   any page combining rings with .ds-glow or .ds-grain the rings lost
   viewport centring and were pushed down the page by their own `top: 50%`
   resolved against normal flow. Excluded rather than raised in specificity
   so the rings also keep `z-index: auto` and stay under the grain, which is
   where background decoration belongs. */
.ds-root.ds-glow > *:not(.ds-ring),
.ds-root.ds-grain > *:not(.ds-ring) { position: relative; z-index: 2; }

/* --- ornament (separator diamond with a cross) ---
   The holding page's richer separator mark. Drop-in replacement for
   .ds-sep-diamond inside a .ds-separator. */
.ds-ornament {
  /* inline-block, not the default inline: width/height do not apply to a
     non-replaced inline box, so as a bare <span> in normal flow this
     collapsed to a 2px-wide, line-height-tall sliver. It only looked right
     inside .ds-separator and .ds-controls-row because flex blockifies its
     items. */
  display: inline-block;
  width: 10px; height: 10px;
  border: 1px solid var(--ds-accent-light);
  transform: rotate(45deg);
  flex-shrink: 0;
  position: relative;
}
.ds-ornament::before, .ds-ornament::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  background: var(--ds-accent-light);
  transform: translate(-50%, -50%);
}
.ds-ornament::before { width: 4px; height: 1px; }
.ds-ornament::after { width: 1px; height: 4px; }

/* separator that draws itself outward on load */
.ds-separator.ds-reveal { animation: ds-expand 1.6s 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; }
@keyframes ds-expand {
  from { transform: scaleX(0); opacity: 0; }
  to { transform: scaleX(1); opacity: 1; }
}

/* --- ring variant: tick marks instead of the glowing bead --- */
.ds-ring.ds-ticks::before {
  width: 1px; height: 12px;
  background: var(--ds-accent-light);
  border-radius: 0;
  opacity: 0.4;
  box-shadow: none;
  top: -1px;
}
.ds-ring.ds-ticks::after {
  content: '';
  position: absolute;
  bottom: -1px; left: 50%;
  width: 1px; height: 12px;
  background: var(--ds-accent-light);
  opacity: 0.2;
  transform: translateX(-50%);
}

/* --- highlight panel ---
   Formerly .ds-angled: Omneus shears this panel with clip-path and adds
   inset L-brackets to compensate (a plain border would show the shear's
   cut corners as sharp, un-rounded points). This skin doesn't shear it —
   a sheared silhouette reads as another HUD angle, same objection as the
   diamonds — so it's a plain square panel like any other .ds-panel and
   needs none of that; only the accent-tinted background and border carry
   over, which is the whole point of this variant (a hero/standalone block
   that reads as "featured" without a second border-radius or shape). */
.ds-panel.ds-highlight {
  background-color: color-mix(in srgb, var(--ds-accent-light) 2%, transparent);
  border-color: color-mix(in srgb, var(--ds-accent-light) 20%, transparent);
}

/* --- back link (arrow slides on hover) --- */
.ds-link-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: color-mix(in srgb, var(--ds-accent-light) 55%, transparent);
  font-size: 0.6rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  transition: color var(--ds-transition);
}
.ds-link-back:hover { color: var(--ds-accent-light); }
.ds-link-back svg { width: 12px; height: 12px; transition: transform var(--ds-transition); }
.ds-link-back:hover svg { transform: translateX(-3px); }

/* --- reduced motion ---
   This system leans on motion everywhere: every panel rises, the rings
   and spinner turn forever, the scan bar sweeps. Honour the OS setting.
   Durations collapse instead of animations being removed, so anything
   using `both` fill (.ds-panel) still lands on its final frame rather
   than being stuck at opacity 0. */
@media (prefers-reduced-motion: reduce) {
  .ds-root *, .ds-root *::before, .ds-root *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
