/*
 * OmniusNET 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; }
.ds-brand img { height: 96px; width: 96px; }
.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 {
  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;
}

/* --- divider / separator (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: linear-gradient(90deg, transparent, var(--ds-accent), transparent);
}
.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-diamond {
  width: 8px; height: 8px;
  border: 1px solid var(--ds-accent-light);
  transform: rotate(45deg);
  flex-shrink: 0;
}

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

/* --- panel (frame with cut-corner brackets, see components.js <ds-panel>) --- */
@keyframes ds-rise {
  0% { opacity: 0; transform: translateY(24px); }
  100% { opacity: 1; transform: translateY(0); }
}
.ds-panel {
  display: block;
  background-color: var(--ds-panel);
  background-image:
    linear-gradient(var(--ds-panel-border), var(--ds-panel-border)),
    linear-gradient(var(--ds-panel-border), var(--ds-panel-border)),
    linear-gradient(var(--ds-panel-border), var(--ds-panel-border)),
    linear-gradient(var(--ds-panel-border), var(--ds-panel-border));
  background-repeat: no-repeat;
  background-size: calc(100% - 28px) 1px, calc(100% - 28px) 1px, 1px calc(100% - 28px), 1px calc(100% - 28px);
  background-position: 14px 0, 14px 100%, 0 14px, 100% 14px;
  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 {
  position: absolute;
  width: 8px; height: 8px;
  border: 1px solid var(--ds-accent-light);
  transform: rotate(45deg);
  pointer-events: none;
}
.ds-panel .ds-corner.tl { top: -4px; left: -4px; }
.ds-panel .ds-corner.tr { top: -4px; right: -4px; }
.ds-panel .ds-corner.bl { bottom: -4px; left: -4px; }
.ds-panel .ds-corner.br { bottom: -4px; right: -4px; }
.ds-panel h2 {
  font-size: 0.8rem;
  letter-spacing: var(--ds-tracking-wide);
  color: var(--ds-accent-light);
  margin: 0 0 var(--ds-space-4);
}

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

/* --- 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.55rem 0.65rem;
  font-family: inherit;
  font-size: 0.85rem;
  border-radius: var(--ds-radius);
}
.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 #C9A84C 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.55rem 2.2rem 0.55rem 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); }

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

/* --- 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;
}
.ds-auth-card img { height: 180px; 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 {
  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; }
.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. */
.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: var(--ds-radius);
  background-color: var(--ds-input-bg);
  background-image: linear-gradient(var(--ds-text-dim), var(--ds-text-dim));
  background-repeat: no-repeat;
  background-size: 0.7rem 0.7rem;
  background-position: 0.15rem 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: linear-gradient(var(--ds-accent-light), var(--ds-accent-light));
  background-position: calc(100% - 0.15rem) 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; }

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

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

/* --- angled panel ---
   The holding page's sheared frame. A different silhouette from the
   bracket-cornered .ds-panel, for hero/standalone blocks rather than
   stacked content. clip-path would slice the corner diamonds off, so
   this variant hides them and uses inset L-brackets instead. */
.ds-panel.ds-angled {
  background-image: none;
  background-color: color-mix(in srgb, var(--ds-accent-light) 2%, transparent);
  border: 1px solid color-mix(in srgb, var(--ds-accent-light) 20%, transparent);
  clip-path: polygon(20px 0%, 100% 0%, calc(100% - 20px) 100%, 0% 100%);
  padding: var(--ds-space-5) var(--ds-space-5);
}
.ds-panel.ds-angled .ds-corner { display: none; }
.ds-panel.ds-angled::before, .ds-panel.ds-angled::after {
  content: '';
  position: absolute;
  width: 10px; height: 10px;
  opacity: 0.5;
}
.ds-panel.ds-angled::before {
  top: 6px; left: 24px;
  border-top: 1px solid var(--ds-accent-light);
  border-left: 1px solid var(--ds-accent-light);
}
.ds-panel.ds-angled::after {
  bottom: 6px; right: 24px;
  border-bottom: 1px solid var(--ds-accent-light);
  border-right: 1px solid var(--ds-accent-light);
}

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