/* A Rough Idea · design system v2 · components
 *
 * Every value here is a role from tokens.css. A component never holds a literal color, size,
 * space or corner (scripts/test-v2-literals.mjs fails the build if one appears outside a media
 * query). What each cue means:
 *   corners  — what you can do: square to read, --ds-radius-act to act, the switch to flip;
 *   fill     — how important an action is: the main action ink-filled, the next outlined;
 *   a box    — you can type here;
 *   mono     — what the model produced, and labels;
 *   color   — status and data only, never the interface.
 */

/* ── Layout: containers and the grid ─────────────────────────────────────── */
.ds-container {
  max-width: var(--ds-content);
  margin-inline: auto;
  padding-inline: var(--ds-margin);
}

.ds-grid {
  display: grid;
  grid-template-columns: repeat(var(--ds-columns), minmax(0, 1fr));
  column-gap: var(--ds-gutter);
}

.ds-grid > * { min-width: 0; }
.ds-span-2 { grid-column: span 2; }
.ds-span-3 { grid-column: span 3; }
.ds-span-4 { grid-column: span 4; }
.ds-span-6 { grid-column: span 6; }
.ds-span-8 { grid-column: span 8; }
.ds-span-9 { grid-column: span 9; }
.ds-span-12 { grid-column: 1 / -1; }

/* Equal columns: a row of like things (cards, screenshots, numbers), 1 to 6 of the same width,
   set by --ds-cols. The 12-column grid above keeps the uneven splits. */
.ds-cols {
  display: grid;
  grid-template-columns: repeat(var(--ds-cols, 2), minmax(0, 1fr));
  gap: var(--ds-gutter);
}

.ds-cols > * { min-width: 0; }

/* Below 640 px every span takes the full width, and equal columns are one. */
@media (width <= 639px) {
  .ds-grid { grid-template-columns: minmax(0, 1fr); }
  .ds-grid > * { grid-column: 1 / -1; }
  .ds-cols { grid-template-columns: minmax(0, 1fr); }
  .ds-container { padding-inline: var(--ds-space-5); }
}

.ds-measure { max-width: var(--ds-measure); }

/* ── A skip link: past the top bar to the page ─────────────────────────────────
   The first thing on every page, out of sight until it has focus; then it shows over the top
   bar's corner, fixed and above the page, so showing it moves nothing. */
.ds-skip {
  position: fixed;
  top: var(--ds-space-2);
  left: var(--ds-space-2);
  z-index: var(--ds-layer-float);
  display: inline-flex;
  align-items: center;
  padding: var(--ds-space-2) var(--ds-space-3);
  border-radius: var(--ds-radius-act);
  background: var(--ds-ink);
  color: var(--ds-on-ink);
  font-weight: var(--ds-weight-semibold);
  text-decoration: none;
  transform: translateY(calc(-100% - var(--ds-space-5)));
}

.ds-skip:focus { transform: none; }

/* ── The top bar: the mark, the app, the page; then sections and actions ──── */
.ds-topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ds-space-2) var(--ds-space-3);
  min-height: var(--ds-topbar-height);
  padding: var(--ds-space-2) var(--ds-margin);
  border-bottom: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-read);
  background: var(--ds-bg);
  font-size: var(--ds-type-ui);
}

.ds-topbar a { text-decoration: none; }

.ds-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-2);
  color: var(--ds-text);
  font-family: var(--ds-font-display);
  font-stretch: var(--ds-display-stretch);
  font-weight: var(--ds-weight-display);
}

.ds-mark {
  display: block;
  width: var(--ds-space-5);
  height: var(--ds-space-5);
  flex: none;
}

.ds-mark .ds-mark-paper {
  fill: var(--ds-bg);
  stroke: var(--ds-ink);
}
.ds-mark .ds-mark-ink { fill: var(--ds-ink); }

.ds-crumb-sep { color: var(--ds-text-3); }
.ds-crumb { color: var(--ds-text-2); }

.ds-crumb[aria-current="page"] {
  color: var(--ds-text);
  font-weight: var(--ds-weight-semibold);
}

.ds-topnav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-1) var(--ds-space-5);
  margin-left: auto;
}

.ds-topnav a { color: var(--ds-text-2); }
.ds-topnav a:hover { color: var(--ds-text); }

/* The current item: an underline in a horizontal list, a rule in the margin of a vertical one. */
.ds-topnav a[aria-current="page"],
.ds-topnav a[aria-current="true"] {
  color: var(--ds-text);
  font-weight: var(--ds-weight-semibold);
  box-shadow: inset 0 calc(-1 * var(--ds-line-strong)) 0 var(--ds-rule-strong);
}

.ds-tools {
  display: flex;
  align-items: center;
  gap: var(--ds-space-1);
}

/* On a phone the mark stands for the name (the link keeps it as its label), and the sections
   drop to a row of their own under the mark and the tools. */
@media (width <= 639px) {
  .ds-topbar { padding-inline: var(--ds-space-5); }
  .ds-brand-name { display: none; }
  .ds-tools { margin-left: auto; }

  .ds-topnav {
    order: 1;
    width: 100%;
    margin-left: 0;
  }
}

/* ── Labels: metadata, in the label face and case (--ds-font-label, --ds-case-label) ── */
.ds-label {
  color: var(--ds-text-2);
  font-family: var(--ds-font-label);
  font-size: var(--ds-type-label);
  font-weight: var(--ds-weight-regular);
  letter-spacing: var(--ds-tracking-label);
  line-height: var(--ds-leading-ui);
  text-transform: var(--ds-case-label);
}

/* The running head: a label row over a strong rule. */
.ds-running-head {
  display: flex;
  justify-content: space-between;
  gap: var(--ds-space-4);
  padding-bottom: var(--ds-space-2);
  border-bottom: var(--ds-line) solid var(--ds-rule-strong);
}

/* ── Buttons: act ────────────────────────────────────────────────────────── */
.ds-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-2);
  min-height: var(--ds-control-height);
  padding: 0 var(--ds-space-3);
  border: var(--ds-line) solid var(--ds-ink);
  border-radius: var(--ds-radius-act);
  background: var(--ds-bg);
  color: var(--ds-text);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-ui);
  font-weight: var(--ds-weight-semibold);
  line-height: var(--ds-leading-ui);
  text-decoration: none;
  cursor: pointer;
}

.ds-button:hover { background: var(--ds-hover); }

.ds-button--primary {
  background: var(--ds-ink);
  color: var(--ds-on-ink);
}

.ds-button--primary:hover {

  background: var(--ds-text-2);

  border-color: var(--ds-text-2);

}

.ds-button--ghost {
  border-color: transparent;
  background: transparent;
}

.ds-button:disabled,
.ds-button[aria-disabled="true"] {
  border-color: var(--ds-rule);
  background: var(--ds-surface-sunk);
  color: var(--ds-text-3);
  cursor: not-allowed;
}

/* ── A link button: an action that reads as a link ──────────────────────────────
   A <button> when it acts, an <a> when it goes somewhere, whatever it looks like. In a sentence
   it is the size of its words; standing alone, it is a plain button (.ds-button--ghost). */
.ds-link-button {
  display: inline;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ds-text);
  font: inherit;
  text-decoration: underline;
  text-decoration-thickness: var(--ds-line);
  text-underline-offset: var(--ds-underline-offset);
  cursor: pointer;
}

.ds-link-button:hover { text-decoration-thickness: var(--ds-line-strong); }

/* A small icon button: home, full screen, a toolbar toggle. */
.ds-icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--ds-control-height-sm);
  min-height: var(--ds-control-height-sm);
  padding: 0 var(--ds-space-1);
  border: var(--ds-line) solid var(--ds-field-border);
  border-radius: var(--ds-radius-act);
  background: var(--ds-bg);
  color: var(--ds-text);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-ui);
  text-decoration: none;
  cursor: pointer;
}

.ds-icon-button:hover { border-color: var(--ds-ink); }
.ds-icon-button svg { display: block; }

.ds-icon-button[aria-pressed="true"] {
  background: var(--ds-ink); border-color: var(--ds-ink);
  color: var(--ds-on-ink);
}

.ds-icon-button:disabled,
.ds-icon-button[aria-disabled="true"] {
  border-color: var(--ds-rule);
  color: var(--ds-text-3);
  cursor: not-allowed;
}

/* ── A toolbar: tools for one area ─────────────────────────────────────────────
   Undo, zoom, export: small icon buttons in a row, groups split by a rule. The WAI-ARIA toolbar,
   one tab stop with the arrow keys between its tools (v2/toolbar.js). A tool that cannot act
   now is aria-disabled, so it keeps its place in the row and in the keyboard's order. */
.ds-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ds-space-1);
}

/* A tool may be a word ("Undo"), so it takes room either side of it. */
.ds-toolbar > .ds-icon-button { padding: 0 var(--ds-space-2); }

.ds-toolbar[aria-orientation="vertical"] {
  flex-flow: column nowrap;
  align-items: start;
}

.ds-toolbar-sep {
  align-self: stretch;
  margin: 0 var(--ds-space-1);
  border-left: var(--ds-line) solid var(--ds-rule);
}

.ds-toolbar[aria-orientation="vertical"] > .ds-toolbar-sep {
  margin: var(--ds-space-1) 0;
  border-top: var(--ds-line) solid var(--ds-rule);
  border-left: 0;
}

/* ── Fields: a box means you can type here ───────────────────────────────── */
.ds-input,
.ds-textarea,
.ds-select {
  min-height: var(--ds-control-height);
  padding: var(--ds-space-1) var(--ds-space-3);
  border: var(--ds-line) solid var(--ds-field-border);
  border-radius: var(--ds-radius-act);
  background: var(--ds-bg);
  color: var(--ds-text);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
}

/* A text area holds lines, not one value: it keeps more room above and below them. */
.ds-textarea {
  padding-block: var(--ds-space-2);
  resize: vertical;
}

/* What the model reads and writes is mono. */
.ds-input--mono,
.ds-textarea--mono { font-family: var(--ds-font-mono); }

.ds-input:hover,
.ds-textarea:hover,
.ds-select:hover { border-color: var(--ds-ink); }

.ds-select-wrap {
  position: relative;
  display: inline-flex;
}

.ds-select {
  width: 100%;
  appearance: none;
  padding-right: var(--ds-space-6);
}

/* The caret: ▾ opens a menu. */
.ds-select-wrap::after {
  content: "▾";
  position: absolute;
  top: 50%;
  right: var(--ds-space-3);
  color: var(--ds-text-2);
  pointer-events: none;
  transform: translateY(-50%);
}

/* ── A field: a label, its control, and one place for its hint or its error ──
   The note is always there and two lines tall (--ds-slot-hint), so an error that takes the
   hint's place never moves the form. aria-invalid on the control shows the error: the border
   and the note turn --ds-bad, and the note's words say what is wrong. */
.ds-field {
  display: grid;
  align-content: start;
  gap: var(--ds-space-2);
}

.ds-field-note {
  min-height: var(--ds-slot-hint);
  margin: 0;
  color: var(--ds-text-2);
  font-size: var(--ds-type-small);
  line-height: var(--ds-leading-ui);
}

.ds-field:has([aria-invalid="true"]) .ds-field-note { color: var(--ds-bad); }

.ds-input[aria-invalid="true"],
.ds-textarea[aria-invalid="true"],
.ds-select[aria-invalid="true"] { border-color: var(--ds-bad); }

/* ── A search field: a field that filters what is shown ────────────────────────
   type="search" in a .ds-field: a visible label, and a note that says how many match
   (role="status"). Its Clear button sits inside the field's end, shown only when there is
   something to clear, so it moves nothing; the browser's own clear mark is hidden, so there is
   one. v2/search.js shows it, clears and puts focus back. */
.ds-search {
  position: relative;
  display: grid;
}

.ds-search > input { padding-right: var(--ds-space-8); }
.ds-search > input::-webkit-search-cancel-button { appearance: none; }

.ds-search-clear {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  padding: 0 var(--ds-space-3);
  border: 0;
  background: none;
  color: var(--ds-text-2);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-small);
  cursor: pointer;
}

.ds-search-clear:hover { color: var(--ds-text); }

/* ── A combobox: a field that suggests as you type ─────────────────────────────
   The WAI-ARIA combobox with a list. Typing filters the list and opens it under the field, out
   of the flow, so it moves nothing; the arrow keys move the active option while focus stays in
   the field; Enter or a click picks; Escape closes. v2/combobox.js runs it. */
.ds-combobox {
  position: relative;
  display: grid;
}

.ds-listbox {
  position: absolute;
  top: calc(100% + var(--ds-space-1));
  right: 0;
  left: 0;
  z-index: var(--ds-layer-float);
  max-height: calc(var(--ds-measure) / 2);
  margin: 0;
  padding: var(--ds-space-1) 0;
  overflow-y: auto;
  border: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-act);
  background: var(--ds-surface);
  box-shadow: var(--ds-shadow-float);
  list-style: none;
}

.ds-listbox > [role="option"] {
  display: flex;
  align-items: center;
  min-height: var(--ds-control-height-sm);
  padding: 0 var(--ds-space-3);
  color: var(--ds-text);
  font-size: var(--ds-type-ui);
  cursor: pointer;
}

.ds-listbox > [role="option"]:hover { background: var(--ds-hover); }

.ds-listbox > [role="option"][aria-selected="true"] {
  background: var(--ds-hover);
  box-shadow: inset var(--ds-line-strong) 0 0 var(--ds-ink);
}

/* Date and time: the browser's own inputs and pickers, as fields. The picker follows light and
   dark through color-scheme. */
.ds-input[type="date"],
.ds-input[type="time"],
.ds-input[type="datetime-local"] { min-width: 0; }

.ds-input::-webkit-calendar-picker-indicator { cursor: pointer; }

/* ── A slider: a value on a range, its number beside it ─────────────────────────
   The browser's own range input, in ink: its keyboard, its announcement and its forced colors
   come with it. The label and the number share a row above it; the number is at the row's end,
   so a longer one moves nothing. v2/slider.js keeps the number in step. */
.ds-slider {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: var(--ds-space-2) var(--ds-space-3);
}

.ds-slider > label { justify-self: start; }

.ds-slider > input[type="range"] {
  grid-column: 1 / -1;
  width: 100%;
  min-height: var(--ds-control-height-sm);
  margin: 0;
  accent-color: var(--ds-ink);
  cursor: pointer;
}

.ds-slider > output {
  color: var(--ds-text);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-ui);
}

/* ── An inspector: a tool's settings, one row each ─────────────────────────────
   A model's settings, or the selected thing's, in a narrow panel. Each row is a setting's name,
   then its control, and a slider's number at the row's end. The names share a column as wide as
   the longest (two fifths at most), so the controls line up after it. A field's note, when it has
   one, sits under its control. Checkboxes or radio buttons are a row too (role="group"), their
   choices one under another after the name: a fieldset cannot share the inspector's columns. A
   heading, or anything else, spans the inspector. Compact is where it belongs: rows a control's
   height, 8 px apart. */
.ds-inspector {
  display: grid;
  grid-template-columns: fit-content(40%) minmax(0, 1fr) auto;
  gap: var(--ds-space-2) var(--ds-space-3);
}

.ds-inspector > * { grid-column: 1 / -1; }

.ds-inspector > :is(.ds-field, .ds-slider, .ds-inspector-row) {
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  row-gap: var(--ds-space-1);
}

/* Everything after the name runs to the row's end. */
.ds-inspector > :is(.ds-field, .ds-slider, .ds-inspector-row) > * {
  grid-column: 2 / -1;
  min-width: 0;
}

/* The name: at the top of its row and a control's height, so it sits by the first control or the
   first choice, however many follow. */
.ds-inspector > :is(.ds-field, .ds-slider, .ds-inspector-row) > :first-child {
  display: flex;
  grid-column: 1;
  align-self: start;
  align-items: center;
  min-height: var(--ds-control-height);
}

.ds-inspector > :is(.ds-field, .ds-slider, .ds-inspector-row) > :is(.ds-segmented, .ds-switch, .ds-button) { justify-self: start; }

.ds-inspector > .ds-slider > input[type="range"] { grid-area: 1 / 2; }

.ds-inspector > .ds-slider > output {
  grid-area: 1 / 3;
  text-align: end;
}

/* A text area's name stays by its first line. */
.ds-inspector > .ds-field:has(> .ds-textarea) > :first-child {
  align-items: start;
  padding-top: var(--ds-space-2);
}

/* ── A drag handle: a row you move by dragging, and three other ways ────────────
   An <ol class="ds-reorder"> whose rows each start with a .ds-drag-handle: drag it, press it for
   Move up and Move down, or Alt+ArrowUp and Alt+ArrowDown (v2/reorder.js). The row being dragged
   takes a ground and a rule at its start, drawn inside, as a list's current row does. */
.ds-reorder {
  margin: 0;
  padding: 0;
  border-top: var(--ds-line) solid var(--ds-rule-soft);
  list-style: none;
}

.ds-reorder > li {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  min-height: var(--ds-control-height);
  padding: 0 var(--ds-space-2);
  border-bottom: var(--ds-line) solid var(--ds-rule-soft);
  font-size: var(--ds-type-ui);
}

.ds-reorder > li[data-ds-dragging] {
  background: var(--ds-hover);
  box-shadow: inset var(--ds-line-strong) 0 0 var(--ds-ink);
}

.ds-drag-handle {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--ds-control-height-sm);
  height: var(--ds-control-height-sm);
  padding: 0;
  border: 0;
  border-radius: var(--ds-radius-act);
  background: none;
  color: var(--ds-text-2);
  cursor: grab;
  touch-action: none;
}

.ds-drag-handle:hover {
  background: var(--ds-hover);
  color: var(--ds-text);
}

.ds-drag-handle svg { display: block; }
.ds-reorder > li[data-ds-dragging] .ds-drag-handle { cursor: grabbing; }

/* ── A number field: a number with its limits and step ─────────────────────────
   The browser's number input between − and + for a pointer (v2/number.js). The input's own
   arrow keys step it too, so the buttons are out of the Tab order, and the browser's small
   spinner is hidden, so there is one place to click. */
.ds-number {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--ds-space-1);
  max-width: calc(var(--ds-measure) / 3);
}

.ds-number > input[type="number"] {
  appearance: textfield;
  text-align: center;
}

.ds-number > input[type="number"]::-webkit-inner-spin-button,
.ds-number > input[type="number"]::-webkit-outer-spin-button {
  margin: 0;
  appearance: none;
}

.ds-number > .ds-icon-button { min-width: var(--ds-control-height); }

/* ── A color field: a color as a swatch, with its value beside it ───────────────
   The browser's own color input and picker; its value in mono, as something a machine reads
   (v2/slider.js keeps it in step). The swatch is the value, so it keeps its color in forced
   colors. */
.ds-color {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--ds-space-2) var(--ds-space-3);
}

.ds-color > label {
  grid-column: 1 / -1;
  justify-self: start;
}

.ds-color > input[type="color"] {
  width: var(--ds-control-height);
  height: var(--ds-control-height);
  margin: 0;
  padding: var(--ds-space-1);
  border: var(--ds-line) solid var(--ds-field-border);
  border-radius: var(--ds-radius-act);
  background: var(--ds-bg);
  cursor: pointer;
}

.ds-color > input[type="color"]:hover { border-color: var(--ds-ink); }
.ds-color > input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.ds-color > input[type="color"]::-webkit-color-swatch { border: 0; }
.ds-color > input[type="color"]::-moz-color-swatch { border: 0; }

.ds-color > output {
  justify-self: start;
  color: var(--ds-text);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-ui);
}

/* ── A file drop: add a file by dropping, pasting or choosing it ────────────────
   A real control to choose a file, so dropping is never the only way: the native file input,
   visually hidden, under a label drawn as a button, which shows the input's focus. Its status
   line is always there and says what was added, or why not. While a file is dragged over, the
   edge turns solid ink, at the same width. v2/file-drop.js takes a dropped or pasted file. */
.ds-file-drop {
  display: grid;
  justify-items: start;
  gap: var(--ds-space-2);
  padding: var(--ds-space-5);
  border: var(--ds-line) dashed var(--ds-field-border);
  border-radius: var(--ds-radius-act);
  color: var(--ds-text-2);
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
}

.ds-file-drop > p { margin: 0; }

.ds-file-drop[data-ds-over] {
  border-style: solid;
  border-color: var(--ds-ink);
  background: var(--ds-hover);
}

.ds-file-drop:has(input:focus-visible) > label {
  outline: var(--ds-focus-width) solid var(--ds-focus);
  outline-offset: var(--ds-focus-offset);
}

/* One line, always there. A file's name is the reader's own and can be any length, so a long
   one ends in an ellipsis rather than taking a second line; a screen reader reads it whole. */
.ds-file-drop > [role="status"] {
  justify-self: stretch;
  min-height: var(--ds-slot-status);
  overflow: hidden;
  color: var(--ds-text);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Checkboxes and radio buttons ───────────────────────────────────────────────
   The browser's own inputs, inside their labels, in ink: their keyboard (Space; the arrow keys
   between radio buttons), their announcement and their forced colors come with them. The whole
   label is the target. A list sits in a fieldset with a legend (.ds-choices). */
.ds-choices {
  display: grid;
  justify-items: start;
  gap: var(--ds-space-1);
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.ds-choices > legend {
  margin-bottom: var(--ds-space-2);
  padding: 0;
}

.ds-checkbox,
.ds-radio {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-2);
  min-height: var(--ds-control-height-sm);
  color: var(--ds-text);
  font-size: var(--ds-type-ui);
  cursor: pointer;
}

.ds-checkbox > input,
.ds-radio > input {
  flex: none;
  width: var(--ds-space-4);
  height: var(--ds-space-4);
  margin: 0;
  accent-color: var(--ds-ink);
  cursor: inherit;
}

.ds-checkbox:has(input:disabled),
.ds-radio:has(input:disabled) {
  color: var(--ds-text-2);
  cursor: default;
}

/* ── A segmented control: one of a few ───────────────────────────────────── */
.ds-segmented {
  display: inline-flex;
  overflow: hidden;
  border: var(--ds-line) solid var(--ds-ink);
  border-radius: var(--ds-radius-act);
}

/* Its buttons sit inside its 1 px edge, so the whole is a control's height. */
.ds-segmented > button {
  min-height: calc(var(--ds-control-height) - 2 * var(--ds-line));
  padding: 0 var(--ds-space-4);
  border: 0;
  background: var(--ds-bg);
  color: var(--ds-text);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-small);
  font-weight: var(--ds-weight-semibold);
  cursor: pointer;
}

.ds-segmented > button + button { border-left: var(--ds-line) solid var(--ds-ink); }

.ds-segmented > button[aria-pressed="true"] {
  background: var(--ds-ink);
  color: var(--ds-on-ink);
}

/* ── Tabs: views of one thing, one shown at a time ─────────────────────────────
   The current tab is underlined; its color and rule change, never its weight, so the row holds
   still. v2/tabs.js gives them the keyboard. */
.ds-tabs {
  display: grid;
  align-content: start;
}

.ds-tablist {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--ds-space-5);
  border-bottom: var(--ds-line) solid var(--ds-rule);
}

.ds-tab {
  min-height: var(--ds-control-height);
  margin-bottom: calc(-1 * var(--ds-line));
  padding: 0;
  border: 0;
  border-bottom: var(--ds-line-strong) solid transparent;
  background: none;
  color: var(--ds-text-2);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-ui);
  white-space: nowrap;
  cursor: pointer;
}

.ds-tab:hover { color: var(--ds-text); }

.ds-tab[aria-selected="true"] {
  border-bottom-color: var(--ds-ink);
  color: var(--ds-text);
}

.ds-tabpanel { padding-top: var(--ds-space-4); }

/* ── Tags (status: read) and chips (filters: act) ────────────────────────── */
.ds-tag {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--ds-space-2);
  border: var(--ds-line) solid currentColor;
  border-radius: var(--ds-radius-tag);
  color: var(--ds-text-2);
  font-family: var(--ds-font-label);
  font-size: var(--ds-type-label);
  letter-spacing: var(--ds-tracking-label);
  line-height: var(--ds-leading-ui);
  text-transform: var(--ds-case-label);
  white-space: nowrap;
}

.ds-tag--ok { color: var(--ds-ok); }
.ds-tag--warn { color: var(--ds-warn); }
.ds-tag--bad { color: var(--ds-bad); }

/* ── A count: a number on a thing ──────────────────────────────────────────────
   Small and gray in the text face, read with what it counts: beside its word ("Members 12"),
   or with the word in .ds-visually-hidden where only the number shows. */
.ds-count {
  margin-left: var(--ds-space-1);
  color: var(--ds-text-2);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-label);
  font-weight: var(--ds-weight-regular);
}

/* ── An avatar: a person's picture or initials, beside their name ──────────────
   Always beside the name in words (.ds-person), so it is decorative: alt="" on a picture,
   aria-hidden on initials. Square, as what you read is. Its size is set, so a picture that loads
   late, or fails and leaves the initials, moves nothing. */
.ds-person {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-2);
}

.ds-avatar {
  display: inline-grid;
  flex: none;
  place-items: center;
  width: var(--ds-control-height-sm);
  height: var(--ds-control-height-sm);
  overflow: hidden;
  border: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-read);
  background: var(--ds-surface-sunk);
  color: var(--ds-text-2);
  font-family: var(--ds-font-label);
  font-size: var(--ds-type-label);
  letter-spacing: var(--ds-tracking-label);
  text-transform: var(--ds-case-label);
  object-fit: cover;
}

/* ── A notice: a message in the page ─────────────────────────────────────────
   Read, so square and flat. Its first word and a rule down its left say the kind; the color
   only repeats the word. */
.ds-notice {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-2) var(--ds-space-4);
  padding: var(--ds-space-2) 0 var(--ds-space-2) var(--ds-space-3);
  border-left: var(--ds-line-strong) solid var(--ds-rule-strong);
  border-radius: var(--ds-radius-read);
  color: var(--ds-text);
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
}

.ds-notice > p {
  flex: 1 1 auto;
  margin: 0;
}

.ds-notice-kind { font-weight: var(--ds-weight-semibold); }

.ds-notice--ok { border-left-color: var(--ds-ok); }
.ds-notice--warn { border-left-color: var(--ds-warn); }
.ds-notice--bad { border-left-color: var(--ds-bad); }
.ds-notice--ok .ds-notice-kind { color: var(--ds-ok); }
.ds-notice--warn .ds-notice-kind { color: var(--ds-warn); }
.ds-notice--bad .ds-notice-kind { color: var(--ds-bad); }

/* ── Progress: a bar when how much is known, a busy ring when it is not ───────
   Ink, since it is the interface, not a value. Words always go with it. */
.ds-progress {
  display: grid;
  gap: var(--ds-space-1);
}

.ds-progress-head {
  display: flex;
  justify-content: space-between;
  gap: var(--ds-space-3);
  color: var(--ds-text-2);
  font-size: var(--ds-type-small);
  line-height: var(--ds-leading-ui);
}

.ds-progress-bar {
  display: block;
  width: 100%;
  height: var(--ds-space-1);
  border: 0;
  border-radius: var(--ds-radius-read);
  background: var(--ds-data-track);
  appearance: none;
}

.ds-progress-bar::-webkit-progress-bar { background: var(--ds-data-track); }
.ds-progress-bar::-webkit-progress-value { background: var(--ds-ink); }
.ds-progress-bar::-moz-progress-bar { background: var(--ds-ink); }

/* The busy line: a ring and words, in a role="status" line that is always there. */
.ds-busy {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  min-height: var(--ds-slot-status);
  margin: 0;
  color: var(--ds-text-2);
  font-size: var(--ds-type-small);
  line-height: var(--ds-leading-ui);
}

.ds-busy-ring {
  flex: none;
  width: var(--ds-icon-size);
  height: var(--ds-icon-size);
  border: var(--ds-line-strong) solid var(--ds-rule);
  border-top-color: var(--ds-ink);
  border-radius: var(--ds-radius-note);
  animation: ds-spin var(--ds-spin) linear infinite;
}

/* When the work ends the ring hides but keeps its place, so the words never move. */
.ds-busy > .ds-busy-ring[hidden] {
  display: block !important;
  visibility: hidden;
}

@keyframes ds-spin {
  to { transform: rotate(1turn); }
}

@media (prefers-reduced-motion: reduce) {
  .ds-busy-ring { animation: none; }
}

/* ── A skeleton: the shape of what is loading, held in place ────────────────────
   A gray bar for each line of text to come, each exactly one line tall in the text around it,
   so the words take its place and nothing moves. The region that loads says aria-busy="true"
   and, in words, what is loading; the skeleton is aria-hidden. It pulses slowly, and holds
   still for reduced motion. */
.ds-skeleton {
  display: grid;
  animation: ds-pulse calc(var(--ds-spin) * 2) ease-in-out infinite alternate;
}

.ds-skeleton > * {
  height: var(--ds-slot-line);
  padding-block: var(--ds-space-1);
  background: var(--ds-surface-sunk);
  background-clip: content-box;
}

.ds-skeleton > :last-child:not(:first-child) { width: 60%; }

@keyframes ds-pulse {
  to { opacity: 0.5; }
}

@media (prefers-reduced-motion: reduce) {
  .ds-skeleton { animation: none; }
}

/* ── Steps: an order of steps, each saying its state ───────────────────────────
   Numbered instructions, or an agent chain's progress. Each step's state is a word (a tag) on its
   own line under its name, so a word that changes moves nothing. Its circle repeats the word: a
   ring to do, an ink ring for the current step (aria-current="step"), filled when done, filled in
   the bad color when it failed. */
.ds-steps {
  display: grid;
  gap: var(--ds-space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.ds-steps > li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--ds-space-1) var(--ds-space-3);
  color: var(--ds-text-2);
  font-size: var(--ds-type-ui);
}

.ds-steps > li > :first-child {
  display: inline-flex;
  grid-row: span 2;
  align-items: center;
  justify-content: center;
  width: var(--ds-space-5);
  height: var(--ds-space-5);
  border: var(--ds-line-strong) solid var(--ds-rule);
  border-radius: var(--ds-radius-note);
  background: var(--ds-bg);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-label);
}

.ds-steps > li > .ds-tag { justify-self: start; }
.ds-steps > li:is([aria-current="step"], [data-state="done"], [data-state="failed"]) { color: var(--ds-text); }
.ds-steps > li[aria-current="step"] > :first-child { border-color: var(--ds-ink); }

.ds-steps > li[data-state="done"] > :first-child {
  border-color: var(--ds-ink);
  background: var(--ds-ink);
  color: var(--ds-on-ink);
}

.ds-steps > li[data-state="failed"] > :first-child {
  border-color: var(--ds-bad);
  background: var(--ds-bad);
  color: var(--ds-on-ink);
}

.ds-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  min-height: var(--ds-control-height-sm);
  padding: 0 var(--ds-space-3);
  border: var(--ds-line) solid var(--ds-field-border);
  border-radius: var(--ds-radius-chip);
  background: var(--ds-bg);
  color: var(--ds-text-2);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-small);
  cursor: pointer;
}

/* On is ink, as every selected state is. The weight never changes: a semibold label is wider,
   and every chip after it in the row would move. */
.ds-chip[aria-pressed="true"] {
  border-color: var(--ds-ink);
  background: var(--ds-ink);
  color: var(--ds-on-ink);
}

/* ── A held place: things that take turns in one spot ────────────────────────
   As wide and as tall as the largest thing in it, shown or hidden, so a label that changes
   (Copy, Copied), a tag's word, or a "Saving…" that comes and goes moves nothing around it.
   A hidden child keeps its space and leaves the accessibility tree. */
.ds-hold {
  display: inline-grid;
  justify-items: start;
}

.ds-hold > * { grid-area: 1 / 1; }

/* For a number: the shown form ends where the widest would, so its last figure stays put. */
.ds-hold--end { justify-items: end; }

.ds-hold > [hidden] {
  display: block !important;
  visibility: hidden;
}

.ds-button > .ds-hold,
.ds-chip > .ds-hold,
.ds-tag > .ds-hold { justify-items: center; }

/* ── The switch: flip ─────────────────────────────────────────────────────── */
.ds-switch {
  display: inline-flex;
  align-items: center;
  width: var(--ds-space-7);
  height: var(--ds-space-5);
  padding: var(--ds-line-strong);
  border: 0;
  border-radius: var(--ds-radius-switch);
  background: var(--ds-field-border);
  cursor: pointer;
}

.ds-switch::before {
  content: "";
  width: calc(var(--ds-space-5) - 2 * var(--ds-line-strong));
  height: calc(var(--ds-space-5) - 2 * var(--ds-line-strong));
  border-radius: var(--ds-radius-knob);
  background: var(--ds-bg);
}

.ds-switch[aria-checked="true"] {
  justify-content: flex-end;
  background: var(--ds-ink);
}

/* ── A menu: actions behind a button ───────────────────────────────────────────
   The WAI-ARIA menu button on the native popover attribute: in the top layer under its button,
   closed by a click outside or Escape. v2/menu.js gives it the keyboard. An item shows hover
   and focus with a ground, never a change of size. */
.ds-menu {
  min-width: calc(var(--ds-measure) / 4);
  margin: 0;
  padding: var(--ds-space-1) 0;
  border: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-act);
  background: var(--ds-surface);
  color: var(--ds-text);
  box-shadow: var(--ds-shadow-float);
}

.ds-menu > [role="menuitem"] {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--ds-control-height-sm);
  padding: 0 var(--ds-space-3);
  border: 0;
  background: none;
  color: inherit;
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-ui);
  text-align: start;
  cursor: pointer;
}

.ds-menu > [role="menuitem"]:hover,
.ds-menu > [role="menuitem"]:focus { background: var(--ds-hover); }
.ds-menu > [role="menuitem"]:focus-visible { outline-offset: calc(-1 * var(--ds-focus-width)); }

/* An item that cannot act now (Move up on the first row) stays in its place, lighter. */
.ds-menu > [role="menuitem"]:disabled {
  background: none;
  color: var(--ds-text-3);
  cursor: not-allowed;
}

.ds-menu > hr {
  margin: var(--ds-space-1) 0;
  border: 0;
  border-top: var(--ds-line) solid var(--ds-rule-soft);
}

/* ── Over the page: the popover and the dialog ────────────────────────────────
   Both sit in the browser's top layer, so opening one moves nothing beneath it, and both carry
   the one shadow v2 allows. */
.ds-popover,
.ds-dialog {
  border: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-panel);
  background: var(--ds-surface);
  color: var(--ds-text);
  box-shadow: var(--ds-shadow-float);
}

/* A small panel tied to a button. Where anchor positioning works it opens under its button;
   elsewhere it keeps the browser's place for it, the middle of the window. */
.ds-popover {
  max-width: min(calc(var(--ds-measure) / 2), calc(100% - 2 * var(--ds-space-5)));
  padding: var(--ds-space-4);
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
}

.ds-popover > :first-child { margin-top: 0; }
.ds-popover > :last-child { margin-bottom: 0; }

@supports (position-area: block-end) {
  .ds-popover,
  .ds-menu {
    inset: auto;
    margin: var(--ds-space-1) 0;
    position-area: block-end span-inline-end;
    position-try-fallbacks: flip-block, flip-inline;
  }
}

/* A window over the page, opened with showModal(). */
.ds-dialog {
  width: min(var(--ds-measure), calc(100% - 2 * var(--ds-space-5)));
  max-height: calc(100% - 2 * var(--ds-space-6));
  padding: var(--ds-space-6);
}

.ds-dialog::backdrop { background: var(--ds-scrim); }

.ds-dialog > h2 {
  margin: 0 0 var(--ds-space-3);
  font-size: var(--ds-type-h3);
}

.ds-dialog > p { margin: 0 0 var(--ds-space-5); }

.ds-dialog-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-3);
  margin: 0;
}

/* A drawer: a dialog at the window's edge, the whole height, opened with showModal() like any
   dialog. --start puts it at the left. */
.ds-drawer {
  width: min(calc(var(--ds-measure) / 2), calc(100% - var(--ds-space-7)));
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  margin: 0 0 0 auto;
  padding: var(--ds-space-5);
  border: 0;
  border-left: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-read);
  background: var(--ds-surface);
  color: var(--ds-text);
  box-shadow: var(--ds-shadow-float);
}

.ds-drawer--start {
  margin: 0 auto 0 0;
  border-right: var(--ds-line) solid var(--ds-rule);
  border-left: 0;
}

.ds-drawer::backdrop { background: var(--ds-scrim); }

.ds-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-3);
  margin-bottom: var(--ds-space-5);
}

.ds-drawer-head > h2 {
  margin: 0;
  font-size: var(--ds-type-h3);
}

.ds-drawer-head > form { margin: 0; }

.ds-drawer > .ds-field + .ds-field,
.ds-drawer > .ds-slider { margin-top: var(--ds-space-4); }

/* A tooltip: a short name or value, on hover and on keyboard focus. v2/tooltip.js opens it in
   the top layer above its control, inside the window, so it moves nothing; Escape hides it.
   Its text is always in the page, so a screen reader has it whether it shows or not. */
.ds-tooltip-wrap { display: inline-flex; }

.ds-tooltip {
  inset: auto;
  max-width: calc(var(--ds-measure) / 3);
  margin: 0;
  padding: var(--ds-space-1) var(--ds-space-2);
  border: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-read);
  background: var(--ds-surface);
  color: var(--ds-text);
  box-shadow: var(--ds-shadow-float);
  font-size: var(--ds-type-label);
  line-height: var(--ds-leading-ui);
}

/* ── A disclosure: a section that opens and closes ─────────────────────────────
   Native <details> and <summary>. The summary's words never change; its marker turns.
   Opening it may move what follows: a view someone asked for. */
.ds-disclosure { border-top: var(--ds-line) solid var(--ds-rule); }

.ds-disclosure > summary {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  min-height: var(--ds-control-height);
  font-weight: var(--ds-weight-semibold);
  list-style: none;
  cursor: pointer;
}

.ds-disclosure > summary::-webkit-details-marker { display: none; }

.ds-disclosure > summary::before {
  content: "›";
  flex: none;
  width: var(--ds-icon-size);
  color: var(--ds-text-2);
  text-align: center;
}

.ds-disclosure[open] > summary::before { transform: rotate(0.25turn); }

.ds-disclosure > :not(summary) {
  margin: 0 0 var(--ds-space-4);
  padding-left: var(--ds-space-5);
}

/* ── Visually hidden: read by a screen reader, never shown ─────────────────── */
.ds-visually-hidden {
  position: absolute;
  width: var(--ds-line);
  height: var(--ds-line);
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

/* ── A card: one thing to open or pick ─────────────────────────────────────────
   Its title's link or button covers the whole card: one tab stop, one target. Hover and the
   focus ring go round the card. A picked card (aria-pressed or aria-current on that link or
   button) is edged in ink, drawn inside so it does not grow. */
.ds-card {
  position: relative;
  display: grid;
  align-content: start;
  justify-items: start;
  gap: var(--ds-space-2);
  padding: var(--ds-space-4);
  border: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-panel);
  background: var(--ds-surface);
}

.ds-card > * { margin: 0; }
.ds-card:hover { border-color: var(--ds-ink); }
.ds-card-title { font-size: var(--ds-type-h3); }

.ds-card-title > a,
.ds-card-title > button {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
  text-decoration: none;
  cursor: pointer;
}

.ds-card-title > a::after,
.ds-card-title > button::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--ds-radius-panel);
}

.ds-card-title > :focus-visible { outline: none; }

.ds-card:has(.ds-card-title > :focus-visible) {
  outline: var(--ds-focus-width) solid var(--ds-focus);
  outline-offset: var(--ds-focus-offset);
}

.ds-card:has(.ds-card-title > [aria-pressed="true"], .ds-card-title > [aria-current]:not([aria-current="false"])) {
  border-color: var(--ds-ink);
  box-shadow: inset 0 0 0 var(--ds-line) var(--ds-ink);
}

/* ── A code block: code, a command, a model's raw answer ───────────────────────
   Mono on the sunk ground. It wraps, or (--scroll) keeps its lines and scrolls sideways inside
   itself; the page never scrolls. */
.ds-code {
  min-width: 0;
  margin: 0;
  padding: var(--ds-space-3) var(--ds-space-4);
  border-radius: var(--ds-radius-read);
  background: var(--ds-surface-sunk);
  color: var(--ds-text);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-small);
  line-height: var(--ds-leading-ui);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

.ds-code > code { font: inherit; }

.ds-code--scroll {
  overflow-x: auto;
  overflow-wrap: normal;
  white-space: pre;
}

/* ── A keyboard hint: a shortcut, written as keys ──────────────────────────────
   <kbd class="ds-kbd"> round the shortcut and a <kbd> for each key, joined by " + ". Each key is
   small, in mono, edged like a key and a little heavier below; read, so its corners are a
   tag's. Its edges sit inside the line, so it never makes the line taller. */
.ds-kbd {
  color: var(--ds-text-2);
  font-family: var(--ds-font-text);
  white-space: nowrap;
}

.ds-kbd > kbd {
  padding: 0 var(--ds-space-1);
  border: var(--ds-line) solid var(--ds-rule);
  border-bottom-width: var(--ds-line-strong);
  border-radius: var(--ds-radius-tag);
  background: var(--ds-surface);
  color: var(--ds-text);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-label);
}

/* ── A legend: the key to a figure ─────────────────────────────────────────────
   A swatch and its words for each mark; the swatch's data color is set as its color. A legend
   that filters is a group of toggle buttons: a series turned off has a hollow swatch and gray
   words, and nothing changes size. */
.ds-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-1) var(--ds-space-4);
  margin: 0;
  padding: 0;
  color: var(--ds-text);
  font-size: var(--ds-type-small);
  line-height: var(--ds-leading-ui);
  list-style: none;
}

.ds-legend > li,
.ds-legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-2);
}

.ds-legend-item {
  min-height: var(--ds-control-height-sm);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ds-text);
  font: inherit;
  cursor: pointer;
}

.ds-legend-item[aria-pressed="false"] { color: var(--ds-text-2); }

.ds-legend-swatch {
  flex: none;
  width: var(--ds-space-3);
  height: var(--ds-space-3);
  border: var(--ds-line) solid;
  background: currentColor;
}

.ds-legend-item[aria-pressed="false"] > .ds-legend-swatch { background: transparent; }

/* ── Facts: name and value pairs ───────────────────────────────────────────────
   A <dl>, so each value is read with its name: names as small gray labels, values in the
   text face, side by side with a hairline above each row; stacked on a phone. */
.ds-facts {
  display: grid;
  grid-template-columns: minmax(min-content, max-content) 1fr;
  gap: 0 var(--ds-space-4);
  margin: 0;
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
}

.ds-facts > dt,
.ds-facts > dd {
  margin: 0;
  padding: var(--ds-space-2) 0;
  border-top: var(--ds-line) solid var(--ds-rule-soft);
}

.ds-facts > dt {
  color: var(--ds-text-2);
  font-family: var(--ds-font-label);
  font-size: var(--ds-type-label);
}

.ds-facts > dd { color: var(--ds-text); }

/* ── Stats: a number large, its name small ─────────────────────────────────────
   A <dl>, so each number is read with its name: the name a small gray label above, the number
   in the text face, whose figures are one width. Each stat has its own column, so a number
   that gains a digit moves nothing beside it. */
.ds-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(calc(var(--ds-measure) / 5), 1fr));
  gap: var(--ds-space-4);
  margin: 0;
}

.ds-stats > div {
  display: grid;
  align-content: start;
  gap: var(--ds-space-1);
  padding-top: var(--ds-space-2);
  border-top: var(--ds-line) solid var(--ds-rule-strong);
}

.ds-stats dt {
  color: var(--ds-text-2);
  font-family: var(--ds-font-label);
  font-size: var(--ds-type-label);
  letter-spacing: var(--ds-tracking-label);
  text-transform: var(--ds-case-label);
}

.ds-stats dd {
  margin: 0;
  color: var(--ds-text);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-h2);
  font-variant-numeric: tabular-nums;
  font-weight: var(--ds-weight-semibold);
  line-height: var(--ds-leading-tight);
  white-space: nowrap;
}

.ds-stats .ds-stat-note {
  color: var(--ds-text-2);
  font-size: var(--ds-type-small);
  font-weight: var(--ds-weight-regular);
  line-height: var(--ds-leading-ui);
  white-space: normal;
}

/* ── An empty state: what a place says before it has anything ──────────────────
   What will appear and how to start, in the place the content will fill. Read, so square; a
   dashed hairline marks a place waiting to be filled. */
.ds-empty {
  display: grid;
  justify-items: start;
  gap: var(--ds-space-2);
  padding: var(--ds-space-6);
  border: var(--ds-line) dashed var(--ds-rule);
  border-radius: var(--ds-radius-read);
  color: var(--ds-text-2);
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
}

.ds-empty > * { margin: 0; }

.ds-empty-title {
  color: var(--ds-text);
  font-weight: var(--ds-weight-semibold);
}

.ds-empty > .ds-button { margin-top: var(--ds-space-2); }

/* ── A chat thread: turns between a person and a model, with a composer ─────────
   It fills a place of fixed height that the page gives it: the log scrolls inside and the
   composer stays put, so a reply streaming in moves nothing around it. The model's words are
   in mono, as everything a model produced is; the person's in the text face. */
.ds-chat {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--ds-space-3);
  height: 100%;
  min-height: 0;
}

.ds-chat-log {
  display: grid;
  align-content: start;
  min-height: 0;
  overflow-y: auto;
}

.ds-turn {
  display: grid;
  gap: var(--ds-space-1);
  padding: var(--ds-space-3) 0;
  border-top: var(--ds-line) solid var(--ds-rule-soft);
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
}

.ds-turn > * { margin: 0; }

.ds-turn-who {
  color: var(--ds-text-2);
  font-family: var(--ds-font-label);
  font-size: var(--ds-type-label);
}

.ds-turn--person > :not(.ds-turn-who) { font-family: var(--ds-font-text); }
.ds-turn--model > :not(.ds-turn-who) { font-family: var(--ds-font-mono); }
.ds-turn--failed > .ds-turn-who { color: var(--ds-bad); }

.ds-chat-composer {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: end;
  gap: var(--ds-space-2) var(--ds-space-3);
  margin: 0;
}

.ds-chat-composer > label { grid-column: 1 / -1; }

/* ── Toasts: word that something finished elsewhere on the page ─────────────────
   Notices that float at the window's foot, fixed, so they move nothing, in a live region that
   is there before the first one. v2/toast.js adds them and keeps each long enough to read. */
.ds-toasts {
  position: fixed;
  right: var(--ds-space-5);
  bottom: var(--ds-space-5);
  z-index: var(--ds-layer-float);
  display: grid;
  gap: var(--ds-space-2);
  width: min(calc(var(--ds-measure) / 2), calc(100% - 2 * var(--ds-space-5)));
  pointer-events: none;
}

.ds-toasts > .ds-notice {
  padding: var(--ds-space-3) var(--ds-space-4);
  border-top: var(--ds-line) solid var(--ds-rule);
  border-right: var(--ds-line) solid var(--ds-rule);
  border-bottom: var(--ds-line) solid var(--ds-rule);
  background: var(--ds-surface);
  box-shadow: var(--ds-shadow-float);
  flex-wrap: nowrap;
  pointer-events: auto;
}

/* In a toast the words wrap beside Dismiss, not above it. */
.ds-toasts > .ds-notice > p {
  flex: 1 1 0;
  min-width: 0;
}

/* ── Panels and figures: read ─────────────────────────────────────────────── */
.ds-panel {
  padding: var(--ds-space-4);
  border: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-panel);
  background: var(--ds-surface);
}

.ds-figure {
  margin: 0;
  padding-top: var(--ds-space-3);
  border-top: var(--ds-line-strong) solid var(--ds-rule-strong);
}

.ds-figure-head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--ds-space-1) var(--ds-space-4);
  margin-bottom: var(--ds-space-4);
}

.ds-caption {
  margin-top: var(--ds-space-4);
  padding-top: var(--ds-space-3);
  border-top: var(--ds-line) solid var(--ds-rule);
  color: var(--ds-text-2);
  font-size: var(--ds-type-caption);
  line-height: var(--ds-leading-ui);
}

/* A bar for an ordered value: its color is data, its track is not. The number stays beside it,
   because the light end of viridis is pale on white. */
.ds-bar {
  flex: 1;
  height: var(--ds-space-3);
  background: var(--ds-data-track);
}

.ds-bar > span {
  display: block;
  height: 100%;
}

/* A row of a bar chart: what the bar measures, the bar, and its number, a hairline below. */
.ds-bar-row {
  display: flex;
  align-items: center;
  gap: var(--ds-space-4);
  min-height: var(--ds-space-6);
  border-bottom: var(--ds-line) solid var(--ds-rule-soft);
}

.ds-bar-row > :first-child {
  width: var(--ds-space-9);
  font-family: var(--ds-font-mono);
}

.ds-bar-row > :last-child {
  min-width: var(--ds-space-8);
  text-align: right;
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-ui);
  font-variant-numeric: tabular-nums;
}

/* ── Tables: read ─────────────────────────────────────────────────────────── */
.ds-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--ds-type-ui);
}

/* A wide table scrolls sideways inside this on a narrow screen, never the page. It takes
   tabindex="0", role="region" and a label, so a keyboard can reach and scroll it. */
.ds-table-wrap {
  max-width: 100%;
  overflow-x: auto;
}

.ds-table th {
  padding: var(--ds-space-2) var(--ds-space-3);
  border-bottom: var(--ds-line) solid var(--ds-rule-strong);
  color: var(--ds-text-2);
  font-family: var(--ds-font-label);
  font-size: var(--ds-type-label);
  font-weight: var(--ds-weight-regular);
  letter-spacing: var(--ds-tracking-label);
  text-align: left;
  text-transform: var(--ds-case-label);
}

.ds-table td {
  padding: var(--ds-space-3);
  border-bottom: var(--ds-line) solid var(--ds-rule-soft);
  vertical-align: top;
}

.ds-table th:first-child,
.ds-table td:first-child { padding-left: 0; }

.ds-table tbody tr:last-child td { border-bottom-color: var(--ds-rule-strong); }

/* A sortable table: each sortable header holds a button (WAI-ARIA). The sorted column's header
   has aria-sort and shows its arrow, turned over for descending; the others show theirs on hover
   and focus. The arrow's place is always kept, so sorting moves no header. v2/sort.js sorts a
   plain table. */
.ds-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-align: inherit;
  text-transform: inherit;
  cursor: pointer;
}

.ds-sort:hover,
.ds-table th:is([aria-sort="ascending"], [aria-sort="descending"]) { color: var(--ds-text); }

.ds-sort svg {
  flex: none;
  width: var(--ds-type-label);
  height: var(--ds-type-label);
  visibility: hidden;
}

.ds-sort:hover svg,
.ds-sort:focus-visible svg,
th:is([aria-sort="ascending"], [aria-sort="descending"]) > .ds-sort svg { visibility: visible; }

th[aria-sort="descending"] > .ds-sort svg { scale: 1 -1; }

/* A table you pick from: layers, runs, files. Each row's name is a button (.ds-pick) that makes
   its row current (aria-current="true"), and the app shows that row's settings beside or below.
   Other cells may hold their own controls, such as a Show box. v2/pick.js lets a click anywhere
   else on a row press its name. The current row takes a ground and a rule at its start, drawn
   inside, as a selectable list's does, in the same weight, so it neither grows nor rewraps. Every
   control is its own Tab stop; the rows take none (TJ, October 30). */
.ds-table[data-ds-pick] tbody tr { cursor: pointer; }

/* A row's controls and words share a middle, so a Show box sits level with its name. */
.ds-table[data-ds-pick] td { vertical-align: middle; }

.ds-table[data-ds-pick] :is(th, td):first-child { padding-left: var(--ds-space-3); }

.ds-table[data-ds-pick] tbody tr:hover > *,
.ds-table[data-ds-pick] tbody tr:has(.ds-pick[aria-current="true"]) > * { background: var(--ds-hover); }

.ds-table[data-ds-pick] tbody tr:has(.ds-pick[aria-current="true"]) > :first-child { box-shadow: inset var(--ds-line-strong) 0 0 var(--ds-ink); }

.ds-pick {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
  cursor: pointer;
}

/* ── Lists with a current item: the rail ──────────────────────────────────── */
.ds-rail {
  display: flex;
  flex-direction: column;
  gap: var(--ds-line-strong);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
}

.ds-rail a {
  display: block;
  padding: var(--ds-space-1) 0 var(--ds-space-1) var(--ds-space-3);
  border-left: var(--ds-line-strong) solid transparent;
  color: var(--ds-text-2);
  text-decoration: none;
}

.ds-rail a:hover { color: var(--ds-text); }

.ds-rail a[aria-current="page"],
.ds-rail a[aria-current="true"] {
  border-left-color: var(--ds-rule-strong);
  color: var(--ds-text);
  font-weight: var(--ds-weight-semibold);
}

/* ── A selectable list: rows you pick from ─────────────────────────────────────
   Runs, revisions, sessions. Each row is one link or button: the thing's name, then an optional
   .ds-list-meta line, then an optional tag or count at its end. The current row (aria-current)
   takes a ground and a rule at its start, as a combobox's active option does, drawn inside so
   it does not grow and in the same weight, so it does not rewrap. */
.ds-list {
  margin: 0;
  padding: 0;
  border-top: var(--ds-line) solid var(--ds-rule-soft);
  list-style: none;
}

.ds-list > li { border-bottom: var(--ds-line) solid var(--ds-rule-soft); }

.ds-list > li > :is(a, button) {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--ds-space-3);
  width: 100%;
  min-height: var(--ds-control-height);
  padding: var(--ds-space-2) var(--ds-space-3);
  border: 0;
  background: none;
  color: var(--ds-text);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-ui);
  line-height: var(--ds-leading-ui);
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}

.ds-list > li > :is(a, button) > * { grid-column: 1; }

.ds-list > li > :is(a, button) > :is(.ds-tag, .ds-count) {
  grid-row: 1 / span 2;
  grid-column: 2;
}

.ds-list > li > :is(a, button):hover { background: var(--ds-hover); }
.ds-list > li > :is(a, button):focus-visible { outline-offset: calc(-1 * var(--ds-focus-width)); }

.ds-list > li > [aria-current]:not([aria-current="false"]) {
  background: var(--ds-hover);
  box-shadow: inset var(--ds-line-strong) 0 0 var(--ds-ink);
}

.ds-list-meta {
  color: var(--ds-text-2);
  font-size: var(--ds-type-small);
}

/* ── A tree: nested items that open and close ──────────────────────────────────
   The WAI-ARIA tree view, run by v2/tree.js: a scene's nodes, a JSON document. Each item's first
   child is its row. A parent's row starts with the marker a disclosure has, turning as it opens;
   a leaf's row keeps the same room empty, so names line up and opening one moves nothing in its
   row. Focus and the selected item show on the row, never round the item and its children. */
.ds-tree,
.ds-tree [role="group"] {
  margin: 0;
  padding: 0;
  list-style: none;
}

.ds-tree [role="group"] { padding-left: var(--ds-space-4); }
.ds-tree [role="treeitem"] { outline: none; }
.ds-tree [role="treeitem"][aria-expanded="false"] > [role="group"] { display: none; }

.ds-tree-row {
  display: flex;
  align-items: center;
  gap: var(--ds-space-1);
  min-height: var(--ds-control-height-sm);
  padding-right: var(--ds-space-2);
  color: var(--ds-text);
  font-size: var(--ds-type-ui);
  cursor: pointer;
}

.ds-tree-row::before {
  content: "";
  flex: none;
  width: var(--ds-icon-size);
  color: var(--ds-text-2);
  text-align: center;
}

.ds-tree [aria-expanded] > .ds-tree-row::before { content: "›" / ""; }
.ds-tree [aria-expanded="true"] > .ds-tree-row::before { transform: rotate(0.25turn); }
.ds-tree-row:hover { background: var(--ds-hover); }

.ds-tree [aria-selected="true"] > .ds-tree-row {
  background: var(--ds-hover);
  box-shadow: inset var(--ds-line-strong) 0 0 var(--ds-ink);
}

.ds-tree [role="treeitem"]:focus-visible > .ds-tree-row {
  outline: var(--ds-focus-width) solid var(--ds-focus);
  outline-offset: calc(-1 * var(--ds-focus-width));
}

/* ── A sequence: previous and next, each naming where it goes ─────────────── */
.ds-seq {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--ds-space-4);
  padding-top: var(--ds-space-3);
  border-top: var(--ds-line) solid var(--ds-rule-strong);
}

.ds-seq a {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
  color: var(--ds-text);
  font-family: var(--ds-font-display);
  font-size: var(--ds-type-lede);
  font-stretch: var(--ds-display-stretch);
  font-weight: var(--ds-weight-display);
  line-height: var(--ds-leading-tight);
  text-decoration: none;
}

.ds-seq a[rel="next"] {
  align-items: flex-end;
  margin-left: auto;
  text-align: right;
}

/* ── Pagination: the pages of a long list ──────────────────────────────────────
   Where you are in the middle, the ways back and on at the ends. At the first or last page the
   way past it stays, disabled, so nothing moves when it comes back; the middle is centered and
   on one line, so a page number that gains a digit moves only its own words. */
.ds-pagination {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--ds-space-3);
}

.ds-pagination > :first-child { justify-self: start; }
.ds-pagination > :last-child { justify-self: end; }

.ds-pagination-status {
  color: var(--ds-text-2);
  font-size: var(--ds-type-ui);
  white-space: nowrap;
}

/* ── A breadcrumb: where a page sits ────────────────────────────────────────────
   Area / Page, in a named nav: a trail above a page's title, deeper than the top bar's crumb goes.
   The separators are drawn, never read; the current page is the last link, aria-current="page". */
.ds-breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin: 0;
  padding: 0;
  color: var(--ds-text-2);
  font-size: var(--ds-type-ui);
  list-style: none;
}

.ds-breadcrumb > li + li::before {
  content: "/" / "";
  margin: 0 var(--ds-space-2);
  color: var(--ds-text-3);
}

.ds-breadcrumb a {
  color: inherit;
  text-decoration: none;
}

.ds-breadcrumb a:hover {
  color: var(--ds-text);
  text-decoration: underline;
}

.ds-breadcrumb a[aria-current="page"] {
  color: var(--ds-text);
  font-weight: var(--ds-weight-semibold);
}

/* ── Notes on a drawing: numbered circles ──────────────────────────────────────
   Filled in ink, or a ring: an ink edge round paper, lighter on a busy or dark drawing, where a
   solid disc would hide what it marks. The two mean the same; one drawing uses one. */
.ds-note-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--ds-space-5);
  height: var(--ds-space-5);
  border-radius: var(--ds-radius-note);
  background: var(--ds-ink);
  color: var(--ds-on-ink);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-type-label);
}

.ds-note-number--ring {
  border: var(--ds-line-strong) solid var(--ds-ink);
  background: var(--ds-bg);
  color: var(--ds-text);
}

/* ── The slide canvas (v2/canvas.js): a deck's slide, laid out at 1280 × 720 and scaled to the screen ──
   The headline at the top, the body, and the footer zone pinned to the foot (.ds-slide-foot). A slide
   that holds more than the canvas scrolls inside it, and a real deck splits it. On a phone held
   upright the canvas is off and the slide reflows. */
.ds-slide-canvas {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-5);
  width: var(--ds-slide-width);
  height: var(--ds-slide-height);
  margin: auto;
  padding: var(--ds-space-8) var(--ds-slide-margin) var(--ds-space-5);
  overflow: auto;
}

/* The gap is the only space between a slide's parts, so a paragraph or a list adds no margin to it. */
.ds-slide-canvas > :is(p, ol, ul) { margin-block: 0; }

@media (width <= 639px) {
  .ds-slide-canvas {
    width: auto;
    height: auto;
    padding: 0;
    overflow: visible;
  }
}

/* While a deck is shown, the canvas lays its slide out as a column whatever the page styles a section
   as (a site's grid, the centered layout), and its footer runs the slide's width. */
[data-ds-slides].ds-slides-on > .ds-slide-canvas[data-ds-canvas="fit"] {
  display: flex;
  flex-direction: column;
}

/* A column has no centered grid to keep a centered title slide's parts in the middle, so it does it here. */
[data-ds-slides].ds-slides-on > .ds-slide-canvas.ds-centered[data-ds-canvas="fit"] { align-items: center; }

/* Its footer zone, added by present.js while a canvas deck is shown: the running footer and the
   page number, small and gray, as the Reposar Method asks. */
.ds-slide-foot {
  display: flex;
  align-self: stretch;
  gap: var(--ds-space-4);
  margin-top: auto;
  padding-top: var(--ds-space-4);
  color: var(--ds-text-2);
  font-size: var(--ds-type-small);
  text-align: start;
}

.ds-slide-footer-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-slide-page { margin-left: auto; }

/* A canvas deck being shown: the window is the frame, the slide centered on it and the sides left on
   the slide's own ground. On a phone held upright it is shown as any deck is. */
@media (width >= 640px) {
  [data-ds-slides].ds-slides-on.ds-slides-canvas {
    display: grid;
    padding: 0;
    overflow: hidden;
  }
  [data-ds-slides].ds-slides-on.ds-slides-canvas > .ds-slide-current { max-width: none; }
}

/* ── Full-bleed slides: the picture fills the slide, words in front of it on solid grounds ──────────
   (TJ, 2026-10-27.) On a fitted canvas the picture fills the width from the top, and what runs past
   the foot is cut off at the slide's edge; a band runs across the foot (or the top) with the slide's
   headline at the body size, bold, its credit and the footer zone; a box sits in a corner. Bands
   and boxes are never see-through. Elsewhere (not presenting, a phone held upright) the picture is
   a picture in the flow and its words follow it. */
.ds-slide-picture {
  display: block;
  width: 100%;
  height: auto;
}

.ds-slide-band,
.ds-slide-box {
  padding: var(--ds-space-5);
  background: var(--ds-band);
  color: var(--ds-text);
}

.ds-slide-band h2,
.ds-slide-box h2 {
  margin: 0;
  font-size: var(--ds-type-body);
  font-weight: var(--ds-weight-semibold);
}

.ds-slide-box > :first-child { margin-top: 0; }
.ds-slide-box > :last-child { margin-bottom: 0; }

.ds-slide-credit {
  margin: var(--ds-space-2) 0 0;
  color: var(--ds-text-2);
  font-size: var(--ds-type-small);
}

/* A band's last line holds the credit and, beside it, the footer zone, so the band stays about a
   sixth of the slide tall, as on TJ's slides. */
.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-band {
  position: absolute;
  inset: auto 0 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  column-gap: var(--ds-space-5);
  padding: var(--ds-space-5) var(--ds-slide-margin);
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-band > :not(.ds-slide-credit, .ds-slide-foot) {
  grid-column: 1 / -1;
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-band .ds-slide-foot {
  grid-column: 2;
  margin: 0;
  padding: 0;
}

/* The bar and the slide's footer take turns at the foot: while the bar shows (it carries the same
   page number), the footer is hidden under it rather than cut in half. */
.ds-slides-bar-up .ds-slide-foot {
  visibility: hidden;
}

.ds-slide-canvas[data-ds-canvas="fit"].ds-slide--bleed {
  position: relative;
  padding: 0;
  overflow: hidden;
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-picture {
  position: absolute;
  inset: 0 0 auto;
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-picture--whole {
  inset: 0;
  height: 100%;
  object-fit: contain;
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-band--top { inset: 0 0 auto; }

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-box {
  position: absolute;
  max-width: var(--ds-measure);
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-box--top-left {
  top: var(--ds-space-7);
  left: var(--ds-space-7);
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-box--top-right {
  top: var(--ds-space-7);
  right: var(--ds-space-7);
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-box--bottom-left {
  bottom: var(--ds-space-7);
  left: var(--ds-space-7);
}

.ds-slide-canvas[data-ds-canvas="fit"] .ds-slide-box--bottom-right {
  bottom: var(--ds-space-7);
  right: var(--ds-space-7);
}

/* A full-bleed slide with no band at its foot: the footer zone takes the band's ground itself. */
.ds-slide-canvas[data-ds-canvas="fit"].ds-slide--bleed > .ds-slide-foot {
  position: absolute;
  inset: auto 0 0;
  padding: var(--ds-space-2) var(--ds-slide-margin);
  background: var(--ds-band);
}

/* ── Presentation mode (v2/present.js): one section at a time, as a slide ── */
.ds-presenting { overflow: hidden; }

[data-ds-slides].ds-slides-on {
  position: fixed;
  inset: 0;
  z-index: var(--ds-layer-present);
  display: block;
  max-width: none;
  margin: 0;
  padding: var(--ds-space-8) var(--ds-slide-margin) calc(var(--ds-touch) + var(--ds-space-8));
  overflow: auto;
  background: var(--ds-bg);
}

[data-ds-slides].ds-slides-on > :not(.ds-slide-current) { display: none; }

[data-ds-slides].ds-slides-on > .ds-slide-current {
  max-width: var(--ds-content);
  margin-inline: auto;
}

/* The bar: previous and next as plain arrows, the caption, n / N, full screen and Exit. */
.ds-present-bar {
  position: fixed;
  inset: auto 0 0;
  z-index: var(--ds-layer-present-bar);
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  min-height: var(--ds-touch);
  padding: var(--ds-space-1) var(--ds-space-5);
  border-top: var(--ds-line) solid var(--ds-rule);
  background: var(--ds-bg);
  color: var(--ds-text-2);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-chrome);
}

.ds-present-bar .ds-button,
.ds-present-bar .ds-icon-button { font-size: var(--ds-type-chrome); }

/* On a canvas deck the bar floats over the foot of the slide, where it is fixed already, and hides
   when idle (.ds-present-bar--float, --idle, set by present.js). It comes and goes at once, with no
   fade, and nothing on the slide moves. */
.ds-present-bar--idle {
  opacity: 0;
  pointer-events: none;
}

.ds-present-arrow {
  min-width: var(--ds-touch);
  min-height: var(--ds-touch);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ds-text);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-arrow);
  line-height: var(--ds-leading-tight);
  cursor: pointer;
}

.ds-present-arrow:disabled {
  color: var(--ds-field-border);
  cursor: default;
}

.ds-present-caption {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  color: var(--ds-text);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ds-present-count {
  font-family: var(--ds-font-mono);
  font-variant-numeric: tabular-nums;
}

@media (width <= 639px) {
  [data-ds-slides].ds-slides-on { padding-inline: var(--ds-space-5); }

  .ds-present-bar {
    gap: var(--ds-space-1);
    padding-inline: var(--ds-space-1);
  }

  .ds-present-caption {
    display: -webkit-box;
    min-height: var(--ds-slot-caption);
    white-space: normal;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
  }
}

/* ── The full lockup: the mark, a bar, the name in spaced capitals ─────────
   Only for big moments: the top of aroughidea.com, a print cover, a deck's title slide.
   Top bars use the mark and the name (.ds-brand). */
.ds-lockup {
  display: flex;
  align-items: center;
  gap: var(--ds-space-5);
  margin: 0;
}

.ds-lockup .ds-mark {
  width: var(--ds-lockup-mark);
  height: var(--ds-lockup-mark);
}

.ds-lockup-bar {
  align-self: stretch;
  width: calc(var(--ds-line-strong) * 2);
  margin-block: var(--ds-space-3);
  background: var(--ds-ink);
}

.ds-lockup-words {
  display: grid;
  gap: var(--ds-space-1);
}

.ds-lockup-name {
  color: var(--ds-text);
  font-family: var(--ds-font-display);
  font-size: var(--ds-type-h2);
  font-weight: var(--ds-weight-display);
  line-height: var(--ds-leading-tight);
  letter-spacing: var(--ds-tracking-wordmark);
  text-transform: uppercase;
}

/* The stacked lockup: mark above name, for the centered layout. The bar only reads side by side. */
.ds-lockup--stacked {
  flex-direction: column;
  gap: var(--ds-space-4);
}

.ds-lockup--stacked .ds-lockup-bar { display: none; }

/* ── The centered layout: everything centered on the reading column ────────
   For a page with one thing to say: a home page, a title slide, an exhibit's opening screen. */
.ds-centered {
  display: grid;
  justify-items: center;
  gap: var(--ds-space-5);
  max-inline-size: var(--ds-measure);
  margin-inline: auto;
  text-align: center;
}

.ds-centered > .ds-figure { width: 100%; }

.ds-centered svg,
.ds-centered img { margin-inline: auto; }

/* ── A split pane: two panes with a divider you drag ───────────────────────────
   The divider is a role="separator" whose value is the first pane's share; v2/split.js moves it
   by drag, by the arrow keys, and by a click that steps it for a pointer that cannot drag. The
   split has a height of its own and its panes scroll, so moving the divider moves nothing
   outside it. On a phone the panes stack and the divider goes. */
.ds-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  height: calc(var(--ds-measure) / 2);
  border: var(--ds-line) solid var(--ds-rule);
}

.ds-split > :not([role="separator"]) {
  min-width: 0;
  overflow: auto;
  padding: var(--ds-space-4);
}

.ds-split > [role="separator"] {
  position: relative;
  width: var(--ds-space-3);
  cursor: col-resize;
  touch-action: none;
  user-select: none;
}

.ds-split > [role="separator"]::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 50%;
  border-left: var(--ds-line) solid var(--ds-rule);
}

.ds-split > [role="separator"]:hover::before,
.ds-split > [role="separator"]:focus-visible::before { border-left: var(--ds-line-strong) solid var(--ds-ink); }
.ds-split > [role="separator"]:focus-visible { outline-offset: calc(-1 * var(--ds-focus-width)); }

@media (width <= 639px) {
  .ds-split {
    grid-template-columns: minmax(0, 1fr) !important;
    height: auto;
  }

  .ds-split > [role="separator"] { display: none; }
  .ds-split > :last-child { border-top: var(--ds-line) solid var(--ds-rule); }
}

/* ── A workbench: a tool page that fills the window ─────────────────────────────
   data-ds-page="tool" on <html> makes the window the frame: the top bar, a split of two panes,
   and a status bar at the foot. The page never scrolls, so it keeps no room for a scrollbar; the
   panes scroll. The split fills its row edge to edge, with no border and no padding in its panes,
   so docks sit flush and a canvas reaches its pane's edges. The frame needs the split directly in
   <body>: a tool page laid out any other way (panels that scroll) keeps its flow, and the
   attribute gives it only its ground, as it does on any other element. On a phone none of this
   applies: the panes stack and the page scrolls, as any phone page does. */
@media (width >= 640px) {
  html[data-ds-page="tool"]:has(> body > .ds-split) {
    height: 100%;
    scrollbar-gutter: auto;
  }

  html[data-ds-page="tool"] > body:has(> .ds-split) {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    height: 100%;
  }

  html[data-ds-page="tool"] > body > .ds-split {
    height: auto;
    border: 0;
  }

  html[data-ds-page="tool"] > body > .ds-split > :not([role="separator"]) { padding: 0; }
}

/* ── A dock: a tool's panel, flush in its pane ─────────────────────────────────
   In a workbench's pane, panels sit edge to edge with a hairline between them, not as cards with
   corners and room around them. Each has a title row that carries its actions (Add, Remove,
   Load), then its body. Paper on the tool's ground, flat. */
.ds-dock {
  border-bottom: var(--ds-line) solid var(--ds-rule);
  background: var(--ds-surface);
}

.ds-dock-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ds-space-1) var(--ds-space-2);
  padding: var(--ds-space-1) var(--ds-space-2) var(--ds-space-1) var(--ds-space-4);
}

/* The title is a heading for the outline, set as the interface's semibold, not as a page's. */
.ds-dock-head > :is(h2, h3, h4) {
  margin: 0;
  margin-inline-end: auto;
  color: var(--ds-text);
  font-family: var(--ds-font-text);
  font-size: var(--ds-type-ui);
  font-stretch: normal;
  font-weight: var(--ds-weight-semibold);
  line-height: var(--ds-leading-ui);
}

.ds-dock-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-1);
}

.ds-dock-body { padding: 0 var(--ds-space-4) var(--ds-space-4); }

/* ── A status bar: the window's foot ────────────────────────────────────────────
   Across a workbench's window, under its panes: one line, always there, for what just happened
   (added, removed, saved), then the facts of what is open as names and values. The line keeps
   its place and stays one line, so a message moves nothing; write it in a few words. */
.ds-statusbar {
  display: flex;
  align-items: center;
  gap: var(--ds-space-1) var(--ds-space-5);
  min-height: var(--ds-control-height);
  padding: var(--ds-space-1) var(--ds-margin);
  border-top: var(--ds-line) solid var(--ds-rule);
  background: var(--ds-bg);
  color: var(--ds-text-2);
  font-size: var(--ds-type-small);
  line-height: var(--ds-leading-ui);
}

.ds-statusbar > [role="status"] {
  flex: 1;
  min-width: 0;
  min-height: var(--ds-slot-status);
  margin: 0;
  overflow: hidden;
  color: var(--ds-text);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ds-statusbar > dl {
  display: flex;
  flex: none;
  gap: var(--ds-space-4);
  margin: 0;
}

.ds-statusbar > dl > div {
  display: flex;
  gap: var(--ds-space-1);
}

.ds-statusbar dd {
  margin: 0;
  color: var(--ds-text);
  font-variant-numeric: tabular-nums;
}

@media (width <= 639px) {
  .ds-statusbar {
    flex-wrap: wrap;
    padding-inline: var(--ds-space-5);
  }

  .ds-statusbar > [role="status"] {
    flex-basis: 100%;
    white-space: normal;
  }
}

@media (width <= 639px) {
  .ds-lockup .ds-mark {
    width: var(--ds-lockup-mark-sm);
    height: var(--ds-lockup-mark-sm);
  }

  .ds-lockup-name { font-size: var(--ds-type-lede); }
}

/* The two blocks below come last, so they win over every component above them, whatever
   order the components were written in. */

/* ── Compact: a tool's panels (data-ds-density="compact") ───────────────────────
   The roles do most of it (tokens.css): controls 24 px, text 12 px throughout, one
   line held under a field. Here, the room around the parts: a panel's inside 12 px, a field's
   label and note 4 px from its control, and a table's or a list's row a control's height. For a
   mouse, as the roles are. */
@media (pointer: fine) {
  .ds-panel:is([data-ds-density="compact"], [data-ds-density="compact"] *) { padding: var(--ds-space-3); }

  :is(.ds-field, .ds-slider):is([data-ds-density="compact"], [data-ds-density="compact"] *) { row-gap: var(--ds-space-1); }

  /* A field's words sit in its 24 px without padding above and below; a text area keeps its room. */
  :is(.ds-input, .ds-select):is([data-ds-density="compact"], [data-ds-density="compact"] *) { padding-block: 0; }

  .ds-choices:is([data-ds-density="compact"], [data-ds-density="compact"] *) > legend { margin-bottom: var(--ds-space-1); }

  .ds-table:is([data-ds-density="compact"], [data-ds-density="compact"] *) { line-height: var(--ds-leading-ui); }

  /* A row is a control's height, its words centered in it. */
  .ds-table:is([data-ds-density="compact"], [data-ds-density="compact"] *) :is(th, td) {
    height: var(--ds-control-height);
    padding-block: 0;
    vertical-align: middle;
  }

  /* Half the smallest gap above and below, so a row with a tag is still a control's height. */
  .ds-list:is([data-ds-density="compact"], [data-ds-density="compact"] *) > li > :is(a, button) { padding-block: calc(var(--ds-space-1) / 2); }

  .ds-dock-head:is([data-ds-density="compact"], [data-ds-density="compact"] *) { padding-left: var(--ds-space-3); }
  .ds-dock-body:is([data-ds-density="compact"], [data-ds-density="compact"] *) { padding: 0 var(--ds-space-3) var(--ds-space-3); }
}

/* ── A 3D view: a picture you turn, with one row along its foot ────────────────
   A scene, a cloud of points, a network: the app's own library draws it and moves it (three.js
   in every one of TJ's views), and v2 frames it. The drawing (.ds-view-canvas) takes focus with
   Tab as an image named for what it shows, then "Use the mouse or the arrow keys to move.", and
   shows the focus ring inside its edge, where the frame cannot clip it. One row along the foot
   (.ds-view-bar): the help button at the left (.ds-view-help), the view's buttons at the right, in
   the order TJ decided (Show axes, Stop orbit or Start orbit, Reset view, Full screen). A picked
   point's details sit beside it (.ds-view-beside), the same names for every point, so picking
   another changes only the values. Issue #46, October 30, 2026. */
.ds-view {
  display: grid;
  grid-template-rows: auto auto;
  min-width: 0;
  overflow: hidden;
  border: var(--ds-line) solid var(--ds-rule);
  background: var(--ds-surface-sunk);
}

.ds-view-canvas {
  display: block;
  inline-size: 100%;
  aspect-ratio: 16 / 10;
  cursor: grab;
  touch-action: none;
}

.ds-view-canvas:active { cursor: grabbing; }
.ds-view-canvas:focus-visible { outline-offset: calc(-1 * var(--ds-focus-width)); }

/* In a pane that has a height, or full screen, the drawing takes all the room the row leaves. */
.ds-view--fill,
.ds-view:fullscreen {
  block-size: 100%;
  grid-template-rows: minmax(0, 1fr) auto;
  border: 0;
}

.ds-view--fill > .ds-view-canvas,
.ds-view:fullscreen > .ds-view-canvas {
  block-size: 100%;
  aspect-ratio: auto;
}

.ds-view-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ds-space-1) var(--ds-space-3);
  padding: var(--ds-space-1) var(--ds-space-2) var(--ds-space-1) var(--ds-space-3);
  border-top: var(--ds-line) solid var(--ds-rule);
  background: var(--ds-surface);
}

/* The help: a button named "How to move" (v2's rotate-3d icon, Lucide's) and its panel, in place of
   a line of instructions that would be too faint to read or too loud to ignore (TJ, October 30).
   The panel (.ds-view-howto, popover="manual", role="tooltip") opens on hover, on keyboard focus
   and on a click or tap, in the top layer above the button, so opening it moves nothing; it stays
   while the pointer is on it, and Escape closes it (v2/view-help.js). */
.ds-view-help { display: inline-flex; }

/* It takes the view's own colors, dark in a dark view and light in a light one, a touch see-through
   with the picture blurred behind it, and an opaque edge. */
.ds-view-howto {
  position: fixed;
  inset: auto;
  max-inline-size: calc(var(--ds-measure) / 2);
  margin: 0;

  /* A popover clips what overflows it by default, and its bridge (below) lies outside it. */
  overflow: visible;
  padding: var(--ds-space-2) var(--ds-space-3);
  border: var(--ds-line) solid var(--ds-rule);
  border-radius: var(--ds-radius-panel);
  background: color-mix(in srgb, var(--ds-surface) 88%, transparent);
  box-shadow: var(--ds-shadow-float);
  color: var(--ds-text);
  font-size: var(--ds-type-small);
  line-height: var(--ds-leading-ui);
  backdrop-filter: blur(var(--ds-space-2));
}

/* A bridge the width of the panel over the gap to its button, above and below it, so the pointer
   stays on the panel as it crosses from the button. view-help.js keeps that gap. */
.ds-view-howto::before,
.ds-view-howto::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  block-size: var(--ds-space-1);
}

/* Measured from inside the panel's border, so each starts past it. */
.ds-view-howto::before { bottom: calc(100% + var(--ds-line)); }
.ds-view-howto::after { top: calc(100% + var(--ds-line)); }

.ds-view-howto-title {
  margin: 0 0 var(--ds-space-1);
  font-size: var(--ds-type-ui);
  font-weight: var(--ds-weight-semibold);
}

.ds-view-howto > dl {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--ds-space-1) var(--ds-space-3);
  margin: 0;
}

.ds-view-howto dt { color: var(--ds-text-2); }
.ds-view-howto dd { margin: 0; }

/* Solid for anyone who has asked their computer to reduce transparency. */
@media (prefers-reduced-transparency: reduce) {
  .ds-view-howto {
    background: var(--ds-surface);
    backdrop-filter: none;
  }
}

.ds-view-tools {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-1);
  margin-inline-start: auto;
}

.ds-view-beside {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--ds-gutter);
  align-items: start;
}

@media (width <= 639px) {
  .ds-view-beside { grid-template-columns: minmax(0, 1fr); }
  .ds-view-canvas { aspect-ratio: 4 / 3; }
}

/* ── Exhibit screens: one page, set up by its address (v2/exhibit.js) ─────────
   The Colloquy's display is the reference: ui=, interaction= and sessionReset= on the address.
   ui=guest, a visitor's screen: the picture first. The view's row lies over the foot of the
   picture, and it and anything marked data-ds-tuck stay off the picture until the pointer comes
   near the bottom edge (data-ds-reach), after a tap, or while Tab is in them; once the pointer rests
   (data-ds-rest) it hides, and they go with it. Like the slideshow's bar they come and go at once,
   with no fade, and nothing on the picture moves. They stay in the Tab order, so the keyboard can
   always reach them. ui=kiosk, an installed screen: no top bar, no row, no help, nothing tucked.
   interaction=none: the view takes no pointer, and "How to move" goes. */

/* An exhibit's page is the picture, edge to edge: no room kept for a scrollbar it never shows. */
html:is([data-ds-ui="guest"], [data-ds-ui="kiosk"]) { scrollbar-gutter: auto; }

[data-ds-ui="guest"] .ds-view { position: relative; }

[data-ds-ui="guest"] .ds-view-bar {
  position: absolute;
  inset: auto 0 0;
}

[data-ds-ui="guest"] :is(.ds-view-bar, [data-ds-tuck]) {
  opacity: 0;
  pointer-events: none;
}

[data-ds-ui="guest"][data-ds-reach]:not([data-ds-rest]) :is(.ds-view-bar, [data-ds-tuck]),
[data-ds-ui="guest"] :is(.ds-view-bar, [data-ds-tuck]):has(:focus-visible) {
  opacity: 1;
  pointer-events: auto;
}

/* "How to move" opens in the top layer, where its row's see-through does not reach: it goes with
   the row. */
[data-ds-ui="guest"]:is(:not([data-ds-reach]), [data-ds-rest]) .ds-view-bar:not(:has(:focus-visible)) .ds-view-howto { visibility: hidden; }

[data-ds-ui="kiosk"] :is(.ds-topbar, .ds-view-bar, .ds-view-help, [data-ds-tuck]),
[data-ds-interaction="none"] .ds-view-help { display: none; }

/* No hands: the view refuses the pointer, and the pointer does not show over it. */
[data-ds-interaction="none"] .ds-view-canvas { pointer-events: none; }
[data-ds-interaction="none"] .ds-view { cursor: none; }
[data-ds-interaction="none"] .ds-view-bar { cursor: auto; }

/* The pointer at rest, on a guest's screen or a kiosk: last, so it wins over every cursor above. */
:root[data-ds-rest],
:root[data-ds-rest] * { cursor: none; }

/* ── Forced colors (Windows high contrast) ────────────────────────────────────
   The system drops every fill there, so a state shown by fill vanishes, and the switch, drawn
   in fills, vanishes whole. Each takes the system's own colors instead: Highlight for on. */
@media (forced-colors: active) {
  /* Forced colors keep a background's transparency: the help panel goes solid. */
  .ds-view-howto {
    background: Canvas;
    backdrop-filter: none;
  }

  .ds-chip[aria-pressed="true"],
  .ds-segmented > button[aria-pressed="true"],
  .ds-icon-button[aria-pressed="true"] {
    forced-color-adjust: none;
    border-color: Highlight;
    background: Highlight;
    color: HighlightText;
  }

  .ds-switch {
    forced-color-adjust: none;
    border: var(--ds-line) solid ButtonText;
    background: Canvas;
  }

  .ds-switch::before { background: ButtonText; }

  .ds-switch[aria-checked="true"] {
    border-color: Highlight;
    background: Highlight;
  }

  .ds-switch[aria-checked="true"]::before { background: HighlightText; }

  /* The current tab: the system's highlight, where color and rule were. */
  .ds-tab[aria-selected="true"] {
    forced-color-adjust: none;
    border-bottom-color: Highlight;
    color: Highlight;
  }

  /* A bar's length is data: it keeps its colors. */
  .ds-bar { forced-color-adjust: none; }

  /* A listbox's active option: the system's highlight, where the ground and rule were. */
  .ds-listbox > [role="option"][aria-selected="true"] {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }

  /* A picked card: the system's highlight, where the ink edge was. */
  .ds-card:has(.ds-card-title > [aria-pressed="true"], .ds-card-title > [aria-current]:not([aria-current="false"])) {
    outline: var(--ds-line-strong) solid Highlight;
    outline-offset: calc(-1 * var(--ds-line-strong));
  }

  /* A tree's selected row: the system's highlight, where the ground and rule were. */
  .ds-tree [aria-selected="true"] > .ds-tree-row {
    outline: var(--ds-line-strong) solid Highlight;
    outline-offset: calc(-1 * var(--ds-line-strong));
  }

  /* A selectable list's current row: the system's highlight, where the ground and rule were. */
  .ds-list > li > [aria-current]:not([aria-current="false"]) {
    outline: var(--ds-line-strong) solid Highlight;
    outline-offset: calc(-1 * var(--ds-line-strong));
  }

  /* A color field's swatch is its value: it keeps its color, edged in the system's color. */
  .ds-color > input[type="color"] {
    forced-color-adjust: none;
    border-color: ButtonText;
    background: Canvas;
  }

  /* A note number keeps its circle: the disc in the system's text color, or the ring's edge. */
  .ds-note-number {
    forced-color-adjust: none;
    background: CanvasText;
    color: Canvas;
  }

  .ds-note-number--ring {
    border-color: CanvasText;
    background: Canvas;
    color: CanvasText;
  }

  /* The row being dragged: the system's highlight, where the ground and rule were. */
  .ds-reorder > li[data-ds-dragging] {
    outline: var(--ds-line-strong) solid Highlight;
    outline-offset: calc(-1 * var(--ds-line-strong));
  }

  /* A step done or failed keeps its filled circle; the current step's ring is the highlight. */
  .ds-steps > li:is([data-state="done"], [data-state="failed"]) > :first-child {
    forced-color-adjust: none;
    border-color: CanvasText;
    background: CanvasText;
    color: Canvas;
  }

  .ds-steps > li[aria-current="step"] > :first-child { border-color: Highlight; }

  /* A skeleton's lines: gray in the system's colors, where the fill was. */
  .ds-skeleton > * {
    forced-color-adjust: none;
    background-color: GrayText;
  }

  /* A legend's swatches are data: they keep their colors. */
  .ds-legend-swatch { forced-color-adjust: none; }

  /* Progress is drawn in fills too: the system's colors keep it. */
  .ds-progress-bar { forced-color-adjust: none; }
  .ds-progress-bar::-webkit-progress-bar { background: Canvas; }
  .ds-progress-bar::-webkit-progress-value { background: Highlight; }
  .ds-progress-bar::-moz-progress-bar { background: Highlight; }
}

/* ── Targets on a phone ──────────────────────────────────────────────────────
   Every control is at least a 44 px target (--ds-touch). The switch keeps its drawing and
   takes a taller target around it. */
@media (width <= 639px) {
  .ds-checkbox,
  .ds-radio,
  .ds-menu > [role="menuitem"],
  .ds-listbox > [role="option"],
  .ds-list > li > :is(a, button),
  .ds-tree-row,
  .ds-reorder > li { min-height: var(--ds-touch); }

  .ds-sort,
  .ds-pick {
    min-width: var(--ds-touch);
    min-height: var(--ds-touch);
  }

  .ds-table .ds-checkbox { min-width: var(--ds-touch); }

  .ds-facts { grid-template-columns: 1fr; }

  .ds-facts > dd {
    padding-top: 0;
    border-top: 0;
  }

  .ds-button,
  .ds-input,
  .ds-select,
  .ds-chip,
  .ds-segmented > button,
  .ds-tab,
  .ds-disclosure > summary,
  .ds-legend-item,
  .ds-slider > input[type="range"] { min-height: var(--ds-touch); }

  .ds-skip { min-height: var(--ds-touch); }

  .ds-inspector > :is(.ds-field, .ds-slider, .ds-inspector-row) > :first-child { min-height: var(--ds-touch); }

  .ds-icon-button,
  .ds-drag-handle,
  .ds-number > .ds-icon-button,
  .ds-color > input[type="color"] {
    min-width: var(--ds-touch);
    min-height: var(--ds-touch);
  }

  .ds-topnav a,
  .ds-crumb,
  .ds-breadcrumb a,
  .ds-brand {
    display: inline-flex;
    align-items: center;
    min-height: var(--ds-touch);
  }

  /* A short word ("Wall") is still a 44 px target. */
  .ds-brand,
  .ds-breadcrumb a,
  .ds-topnav a {
    justify-content: center;
    min-width: var(--ds-touch);
  }

  .ds-switch { position: relative; }

  .ds-switch::after {
    content: "";
    position: absolute;
    inset: calc((var(--ds-space-5) - var(--ds-touch)) / 2) 0;
  }
}
