/* ==========================================================================
   components.css — styles for the design system components
   --------------------------------------------------------------------------
   These are the styles for the real components (Button, Card, Toggle…)
   that appear in the "Examples" sections of each component page.

   Rules:
   1. Only use tokens from tokens.css — e.g. var(--space-4), never 16px.
   2. Prefix every class with "ds-" (design system) so component styles
      never clash with the documentation site's own styles in docs.css.
   3. One section per component, in the same alphabetical order as the nav.

   Example of what a component section will look like:

     .ds-button {
       padding: var(--space-2) var(--space-4);
       border-radius: var(--radius-md);
       background: var(--color-primary);
       color: var(--color-on-primary);
       font-size: var(--font-size-sm);
     }

   Components are placeholders for now, so the sections below are empty.
   ========================================================================== */


/* ==========================================================================
   Typography
   One class per text style. Sizes and line heights change automatically
   with screen width (see the responsive type scale in tokens.css).
   Use them on any element, e.g. <h2 class="ds-text-h1">.
   ========================================================================== */

/* Display */
.ds-text-display {
  font-size: var(--type-display-size);
  line-height: var(--type-display-line-height);
  font-weight: var(--type-display-weight);
  letter-spacing: var(--type-display-letter-spacing);
}

/* H1 */
.ds-text-h1 {
  font-size: var(--type-h1-size);
  line-height: var(--type-h1-line-height);
  font-weight: var(--type-h1-weight);
  letter-spacing: var(--type-h1-letter-spacing);
}

/* H2 */
.ds-text-h2 {
  font-size: var(--type-h2-size);
  line-height: var(--type-h2-line-height);
  font-weight: var(--type-h2-weight);
  letter-spacing: var(--type-h2-letter-spacing);
}

/* H3 */
.ds-text-h3 {
  font-size: var(--type-h3-size);
  line-height: var(--type-h3-line-height);
  font-weight: var(--type-h3-weight);
  letter-spacing: var(--type-h3-letter-spacing);
}

/* H4 */
.ds-text-h4 {
  font-size: var(--type-h4-size);
  line-height: var(--type-h4-line-height);
  font-weight: var(--type-h4-weight);
  letter-spacing: var(--type-h4-letter-spacing);
}

/* Subheadline */
.ds-text-subheadline {
  font-size: var(--type-subheadline-size);
  line-height: var(--type-subheadline-line-height);
  font-weight: var(--type-subheadline-weight);
  letter-spacing: var(--type-subheadline-letter-spacing);
}

/* Body large */
.ds-text-body-large {
  font-size: var(--type-body-large-size);
  line-height: var(--type-body-large-line-height);
  font-weight: var(--type-body-large-weight);
  letter-spacing: var(--type-body-large-letter-spacing);
}

/* Body */
.ds-text-body {
  font-size: var(--type-body-size);
  line-height: var(--type-body-line-height);
  font-weight: var(--type-body-weight);
  letter-spacing: var(--type-body-letter-spacing);
}

/* Body small */
.ds-text-body-small {
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  font-weight: var(--type-body-small-weight);
  letter-spacing: var(--type-body-small-letter-spacing);
}

/* Caption */
.ds-text-caption {
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
  font-weight: var(--type-caption-weight);
  letter-spacing: var(--type-caption-letter-spacing);
}

/* Label */
.ds-text-label {
  font-size: var(--type-label-size);
  line-height: var(--type-label-line-height);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-letter-spacing);
}

/* Screen readers only: text that screen readers read but the screen
   doesn't show, like the " more" in a multi-select's "+2" count. It's part
   of the components, so it works on any site that uses components.css. */
.ds-visually-hidden {
  position: absolute;
  width: var(--border-width-thin);
  height: var(--border-width-thin);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}


/* Accordion ---------------------------------------------------------------- */

/* A vertically stacked list of headers that show or hide their sections.
   One look ("Separated"): each item is its own white card with a grey-200
   border and 12px corners, 8px apart, with a grey-50 background when open.
   Markup (one item):
   <div class="ds-accordion" data-accordion>
     <div class="ds-accordion-item">                   (.is-open when expanded)
       <h3 class="ds-accordion-heading">               (h2–h6, to fit the page)
         <button type="button" class="ds-accordion-header" id="faq-1-header"
                 aria-expanded="false" aria-controls="faq-1-panel">
           …optional leading icon: <i data-lucide="…" class="ds-accordion-icon" aria-hidden="true"></i>
           <span class="ds-accordion-text">
             <span class="ds-accordion-title">Title</span>
             <span class="ds-accordion-subtext">Optional one-line subtext</span>
           </span>
           <i data-lucide="chevron-down" class="ds-accordion-chevron" aria-hidden="true"></i>
         </button>
       </h3>
       <div class="ds-accordion-panel" id="faq-1-panel" role="region" aria-labelledby="faq-1-header">
         <div class="ds-accordion-panel-inner">
           <div class="ds-accordion-content">…content…</div>
         </div>
       </div>
     </div>
   </div>
   Small size: --sm. data-accordion-single: opening one item closes the
   others. main.js opens and closes items, and keeps aria-expanded up to date. */
.ds-accordion {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);   /* 8px between items */
  color: var(--color-text);
  text-align: left;
  font-family: var(--font-body);
}

/* Each item: its own card. grey-50 when open. */
.ds-accordion .ds-accordion-item {
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  transition: background-color var(--duration-normal) var(--easing-standard);
}

.ds-accordion .ds-accordion-item.is-open {
  background: var(--color-surface-muted);
}

.ds-accordion .ds-accordion-item > .ds-accordion-heading {
  margin: 0;
  font: inherit;
  letter-spacing: normal;
}

/* Header: the whole row is one button. MD is 56px tall
   (16px padding + 24px title + 16px padding). Its corners follow the card's. */
.ds-accordion .ds-accordion-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-4);
  border: 0;
  border-radius: calc(var(--radius-container) - var(--border-width-thin));
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-normal) var(--easing-standard);
}

/* Hover: grey-50 background (grey-100 when the item is open and already grey-50) */
.ds-accordion .ds-accordion-header:hover:not(:disabled),
.ds-accordion .ds-accordion-header.is-hover {
  background: var(--color-surface-muted);
}

.ds-accordion .ds-accordion-item.is-open > .ds-accordion-heading .ds-accordion-header:hover:not(:disabled),
.ds-accordion .ds-accordion-item.is-open > .ds-accordion-heading .ds-accordion-header.is-hover {
  background: var(--color-hover-bg);
}

/* Focus: 2px pink-600 ring, drawn just inside the header so it follows the
   card's corners. (.is-focus shows it on the docs pages.) */
.ds-accordion .ds-accordion-header:focus-visible,
.ds-accordion .ds-accordion-header.is-focus {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: calc(-1 * var(--focus-ring-width));
}

/* Disabled: grey-400 text and icons, no pointer */
.ds-accordion .ds-accordion-header:disabled {
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.ds-accordion .ds-accordion-header:disabled .ds-accordion-subtext,
.ds-accordion .ds-accordion-header:disabled .ds-accordion-icon,
.ds-accordion .ds-accordion-header:disabled .ds-accordion-chevron {
  color: var(--color-text-disabled);
}

/* Title and optional subtext, 4px apart */
.ds-accordion .ds-accordion-text {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

/* Title: 16px / 24px semi-bold */
.ds-accordion .ds-accordion-title {
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-line-height);
}

/* Subtext: one line, 14px / 20px grey-600, 4px below the title.
   Padding stays the same, so with subtext the MD header is 80px:
   16px padding + 24px title + 4px gap + 20px subtext + 16px padding */
.ds-accordion .ds-accordion-subtext {
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-regular);
  line-height: var(--type-body-small-line-height);
}

/* Leading icon and chevron: 20px, grey-700 */
.ds-accordion .ds-accordion-icon,
.ds-accordion .ds-accordion-chevron {
  flex-shrink: 0;
  width: var(--space-5);
  height: var(--space-5);
  color: var(--color-text-secondary);
}

/* The chevron turns 180° when its item opens (only its own item, so an
   accordion inside an open one isn't affected) */
.ds-accordion .ds-accordion-chevron {
  transition: transform var(--duration-normal) var(--easing-standard);
}

.ds-accordion .ds-accordion-item.is-open > .ds-accordion-heading .ds-accordion-chevron {
  transform: rotate(180deg);
}

/* Panel: opens smoothly by growing from 0 to its full height (a grid row
   going from 0fr to 1fr). It always grows to fit its content, so long
   content makes the page scroll, never the panel. When closed it's hidden
   from screen readers and the keyboard too (visibility). */
.ds-accordion .ds-accordion-panel {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition:
    grid-template-rows var(--duration-normal) var(--easing-standard),
    visibility var(--duration-normal);
}

.ds-accordion .ds-accordion-item.is-open > .ds-accordion-panel {
  grid-template-rows: 1fr;
  visibility: visible;
}

.ds-accordion .ds-accordion-panel-inner {
  min-height: 0;
  overflow: hidden;
}

/* Content: 16px padding, body 16px / 24px grey-700 */
.ds-accordion .ds-accordion-content {
  padding: var(--space-4);
  color: var(--color-text-secondary);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line-height);
}

.ds-accordion .ds-accordion-content > * {
  margin: 0;
}

.ds-accordion .ds-accordion-content > * + * {
  margin-top: var(--space-3);
}

/* With a leading icon, the content lines up with the title
   (16px padding + 20px icon + 12px gap) */
.ds-accordion .ds-accordion-item:has(.ds-accordion-icon) .ds-accordion-content {
  padding-left: calc(var(--space-4) + var(--space-5) + var(--space-3));
}

/* SM: 44px header (12px padding + 20px title + 12px padding),
   14px semi-bold title, 16px chevron, 8px gaps. The panel uses the same
   12px padding as the header. */
.ds-accordion--sm .ds-accordion-header {
  gap: var(--space-2);
  padding: var(--space-3);
}

.ds-accordion--sm .ds-accordion-title {
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* SM subtext: 12px / 16px, so with subtext the SM header is 64px:
   12px padding + 20px title + 4px gap + 16px subtext + 12px padding */
.ds-accordion--sm .ds-accordion-subtext {
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
}

.ds-accordion--sm .ds-accordion-icon,
.ds-accordion--sm .ds-accordion-chevron {
  width: var(--space-4);
  height: var(--space-4);
}

.ds-accordion--sm .ds-accordion-content {
  padding: var(--space-3);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* People who ask for less motion: open and close instantly */
@media (prefers-reduced-motion: reduce) {
  .ds-accordion .ds-accordion-item,
  .ds-accordion .ds-accordion-header,
  .ds-accordion .ds-accordion-chevron,
  .ds-accordion .ds-accordion-panel {
    transition: none;
  }
}

/* Alert -------------------------------------------------------------------- */

/* An inline message about status, feedback or important information.
   Soft background, 1px border, coloured icon, dark text, 12px corners.
   Markup:
   <div class="ds-alert ds-alert--neutral" role="status">
     <i data-lucide="info" class="ds-alert-icon" aria-hidden="true"></i>
     <div class="ds-alert-content">
       <p class="ds-alert-title">Title</p>
       <p class="ds-alert-body">Optional description.</p>
     </div>
     <div class="ds-alert-controls">   (only if there is a control)
       …optional link (goes to another page): <a class="ds-alert-link">…
       …or an optional action (does something here): a ghost button,
          ds-button ds-button--tertiary ds-button--sm ds-alert-action…
       …optional close: ds-button ds-button--tertiary ds-button--icon ds-alert-close
          with data-alert-dismiss and aria-label="Dismiss"…
     </div>
   </div>
   Use role="alert" for errors and role="status" for everything else. */
.ds-alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  /* The 1px edge is an outline drawn just inside the alert, so it doesn't
     add to the size: 16px padding + 24px title + 16px padding = 56px.
     (Unlike a box-shadow, an outline still shows in Windows high
     contrast mode.) */
  outline: var(--border-width-thin) solid;
  outline-offset: calc(var(--border-width-thin) * -1);
  border-radius: var(--radius-container);
  color: var(--color-text-secondary);
  text-align: left;
  font-family: var(--font-body);
}

.ds-alert[hidden] {
  display: none;
}

/* 20px icon, lined up with the middle of the title's first line */
.ds-alert .ds-alert-icon {
  flex-shrink: 0;
  width: var(--space-5);
  height: var(--space-5);
  margin-top: calc((var(--type-body-line-height) - var(--space-5)) / 2);
}

.ds-alert-content {
  flex: 1;
  min-width: 0;
}

/* Title: 16px / 24px semi-bold */
.ds-alert .ds-alert-title {
  margin: 0;
  color: var(--color-text);
  font-family: var(--font-heading);
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-line-height);
}

/* Description: 14px / 20px regular, 4px below the title */
.ds-alert .ds-alert-body {
  margin: var(--space-1) 0 0;
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-regular);
  line-height: var(--type-body-small-line-height);
}

/* Right-side controls: the link or action, then the close button, 8px
   apart. The group is centred to the full height of the alert, while the
   icon and text on the left stay at the top. The -4px top and bottom
   margins let the 32px controls sit on a one-line (24px) alert without
   making it taller. */
.ds-alert .ds-alert-controls {
  display: flex;
  flex-shrink: 0;
  align-self: center;
  align-items: center;
  gap: var(--space-2);
  margin-block: calc((var(--type-body-line-height) - var(--size-control-compact)) / 2);
}

/* Link (goes to another page): underlined, medium weight, in the alert's
   own status colour (set per variant below). The underline thickens on hover. */
.ds-alert .ds-alert-link {
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--type-body-small-line-height);
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-thickness: var(--border-width-thin);
  text-underline-offset: var(--link-underline-offset);
}

.ds-alert .ds-alert-link:hover {
  text-decoration-thickness: var(--border-width-thick);
}

/* Action (does something here): a ghost button. grey-900 text, no border
   or background, 8px corners (Small), 32px tall. On hover it gets a white
   background, like the close button. */
.ds-alert .ds-alert-action {
  --button-height: var(--size-control-compact);   /* 32px */
  --button-radius: var(--radius-control-sm);      /* 8px */
  background: transparent;
  box-shadow: none;
  color: var(--color-text);
}

.ds-alert .ds-alert-action:hover:not(:disabled),
.ds-alert .ds-alert-action.is-hover {
  background: var(--color-surface);
}

/* Close button: a 20px grey-700 "x" in a 32 × 32px click area, with no
   border. On hover it gets a white background with 8px corners and the
   icon turns grey-900. (Keyboard users still see the focus ring when they
   Tab to it.) */
.ds-alert .ds-alert-close {
  --button-height: var(--size-control-compact);   /* 32px */
  --button-icon: var(--space-5);                    /* 20px */
  --button-radius: var(--radius-control-sm);        /* 8px */
  background: transparent;
  box-shadow: none;
  color: var(--color-text-secondary);
}

.ds-alert .ds-alert-close:hover:not(:disabled),
.ds-alert .ds-alert-close.is-hover {
  background: var(--color-surface);
  color: var(--color-text);
}

/* On phones the alert becomes a small grid:
     icon | text          | close
          | link / action |
   The close button stays top right, on the title's line, and the link or
   action wraps below the text, lined up with it. (The action starts 12px
   further left, so its label lines up with the text, not its padding.)
   CSS can't use var() in @media, so the breakpoint is written here. */
@media (max-width: 767px) {
  .ds-alert:has(.ds-alert-controls) {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-2) var(--space-3);
  }

  .ds-alert:has(.ds-alert-close) {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }

  /* The group "disappears" so its controls can be placed one by one */
  .ds-alert .ds-alert-controls {
    display: contents;
  }

  .ds-alert .ds-alert-icon {
    grid-column: 1;
    grid-row: 1;
  }

  .ds-alert .ds-alert-content {
    grid-column: 2;
    grid-row: 1;
  }

  .ds-alert .ds-alert-close {
    grid-column: 3;
    grid-row: 1;
    margin-block: calc((var(--type-body-line-height) - var(--size-control-compact)) / 2);
  }

  .ds-alert .ds-alert-link,
  .ds-alert .ds-alert-action {
    grid-column: 2;
    grid-row: 2;
    justify-self: start;
    white-space: normal;
  }

  .ds-alert .ds-alert-action {
    margin-left: calc(var(--space-3) * -1);
  }
}

/* Neutral: general information */
.ds-alert--neutral {
  outline-color: var(--color-info-border);
  background: var(--color-info-bg);
}

.ds-alert--neutral .ds-alert-icon,
.ds-alert--neutral .ds-alert-link {
  color: var(--color-info);
}

/* Positive: confirms something worked */
.ds-alert--positive {
  outline-color: var(--color-success-border);
  background: var(--color-success-bg);
}

.ds-alert--positive .ds-alert-icon,
.ds-alert--positive .ds-alert-link {
  color: var(--color-success);
}

/* Warning: something needs attention */
.ds-alert--warning {
  outline-color: var(--color-warning-border);
  background: var(--color-warning-bg);
}

.ds-alert--warning .ds-alert-icon,
.ds-alert--warning .ds-alert-link {
  color: var(--color-warning);
}

/* Error: something went wrong and needs fixing */
.ds-alert--error {
  outline-color: var(--color-error-border);
  background: var(--color-error-bg);
}

.ds-alert--error .ds-alert-icon,
.ds-alert--error .ds-alert-link {
  color: var(--color-error);
}


/* Avatar ------------------------------------------------------------------- */

/* A person or organisation, shown as a photo, initials or an icon. Always
   a circle. Markup:
   Initials: one letter, the first letter of the first name, medium weight
   (12px SM, 14px MD, 18px LG). Colour --1 to --6 is picked from the full
   name (see main.js), and the full name goes in the aria-label:
     <span class="ds-avatar ds-avatar--3" role="img" aria-label="Grace Hopper">
       <span class="ds-avatar-initials" aria-hidden="true">G</span>
     </span>
   Image (initials underneath, shown if the photo fails to load):
     <span class="ds-avatar ds-avatar--3" data-avatar-image>
       <span class="ds-avatar-initials" aria-hidden="true">G</span>
       <img class="ds-avatar-image" src="…" alt="Grace Hopper">
     </span>
   Placeholder (no name or photo):
     <span class="ds-avatar" role="img" aria-label="Unknown user">
       <i data-lucide="user" class="ds-avatar-icon" aria-hidden="true"></i>
     </span>
   Group: all avatars in one .ds-avatar-group; from the 5th on they have
   data-avatar-extra and hidden, followed by the "+N" button
   (data-avatar-more, aria-label="N more people"), which reveals them.
   Sizes: --sm (24px), MD (44px, the default), --lg (56px), on the avatar
   or on a group. Interactive avatars are <button>s; put them in a
   .ds-avatar-tip with a .ds-avatar-tooltip to show the full name. */
.ds-avatar {
  --avatar-size: var(--size-control-standard);   /* 44px */
  --avatar-text: var(--font-size-sm);     /* 14px */
  --avatar-icon: var(--space-5);          /* 20px */
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--avatar-size);
  height: var(--avatar-size);
  padding: 0;
  overflow: hidden;
  border: 0;
  border-radius: var(--radius-full);
  background: var(--color-surface-subtle);
  color: var(--color-text-muted);
  font-family: var(--font-body);
  font-size: var(--avatar-text);
  font-weight: var(--font-weight-medium);
  line-height: 1;
  vertical-align: middle;
}

/* Sizes, on an avatar or on a whole group */
.ds-avatar--sm,
.ds-avatar-group--sm .ds-avatar {
  --avatar-size: var(--space-6);          /* 24px */
  --avatar-text: var(--font-size-xs);     /* 12px */
  --avatar-icon: var(--space-4);          /* 16px */
}

.ds-avatar--lg,
.ds-avatar-group--lg .ds-avatar {
  --avatar-size: calc(var(--space-12) + var(--space-2));   /* 56px */
  --avatar-text: var(--type-body-large-size);              /* 18px */
  --avatar-icon: calc(var(--space-6) + var(--space-1));    /* 28px */
}

.ds-avatar .ds-avatar-initials {
  text-transform: uppercase;
  font-family: var(--font-body);
}

/* The photo fills the circle, cropped (never stretched) and centred */
.ds-avatar .ds-avatar-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.ds-avatar .ds-avatar-icon {
  width: var(--avatar-icon);
  height: var(--avatar-icon);
}

/* Icons are SVGs, which ignore the hidden attribute on their own */
.ds-avatar .ds-avatar-icon[hidden] {
  display: none;
}

/* Initials colours: a light data colour with its dark shade for the
   letters (every pair is 4.5:1 or more) */
.ds-avatar--1 { background: var(--avatar-1-bg); color: var(--avatar-1-text); }
.ds-avatar--2 { background: var(--avatar-2-bg); color: var(--avatar-2-text); }
.ds-avatar--3 { background: var(--avatar-3-bg); color: var(--avatar-3-text); }
.ds-avatar--4 { background: var(--avatar-4-bg); color: var(--avatar-4-text); }
.ds-avatar--5 { background: var(--avatar-5-bg); color: var(--avatar-5-text); }
.ds-avatar--6 { background: var(--avatar-6-bg); color: var(--avatar-6-text); }

/* Interactive avatars (buttons): pointer, and the pink focus ring */
button.ds-avatar {
  cursor: pointer;
}

.ds-avatar:focus-visible,
.ds-avatar.is-focus,
.ds-avatar-trigger:focus-visible,
.ds-avatar-trigger.is-focus {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* Group: overlapping avatars, each with a 2px white ring. Overlap: -4px
   (SM), -8px (MD), -12px (LG). At most 4 avatars, then a "+3". */
.ds-avatar-group {
  --avatar-overlap: calc(-1 * var(--space-2));   /* -8px */
  display: inline-flex;
  align-items: center;
}

.ds-avatar-group--sm {
  --avatar-overlap: calc(-1 * var(--space-1));   /* -4px */
}

.ds-avatar-group--lg {
  --avatar-overlap: calc(-1 * var(--space-3));   /* -12px */
}

.ds-avatar-group > * + * {
  margin-left: var(--avatar-overlap);
}

.ds-avatar-group .ds-avatar {
  box-shadow: 0 0 0 var(--border-width-thick) var(--color-surface);
}

/* The rest of the group stays hidden until "+3" is selected. Then they
   slide in, in the same row and the same overlapping stack, and "+3" hides.
   (main.js adds .is-expanded; clicking outside the group collapses it.) */
.ds-avatar-group > [hidden] {
  display: none;
}

.ds-avatar-group.is-expanded > [data-avatar-extra] {
  animation: ds-avatar-slide-in var(--duration-normal) var(--easing-standard);
}

@keyframes ds-avatar-slide-in {
  from {
    opacity: 0;
    transform: translateX(calc(-1 * var(--space-4)));
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* "+3": the rest of the group */
.ds-avatar.ds-avatar--overflow {
  background: var(--color-surface-subtle);
  color: var(--color-text-secondary);
}

button.ds-avatar--overflow:hover {
  background: var(--color-surface-strong);
}

/* In the list, the 4px gap already sits above and below the divider */
.ds-dropdown-list > .ds-dropdown-divider {
  margin: 0;
}

/* Tooltip with the full name, on hover and keyboard focus */
.ds-avatar-tip {
  position: relative;
  display: inline-flex;
  font-family: var(--font-body);
}

.ds-avatar-tooltip {
  position: absolute;
  bottom: calc(100% + var(--space-2));
  left: 50%;
  z-index: var(--layer-dropdown);
  /* The same look as the Tooltip component */
  padding: calc(var(--space-1) * 1.5) var(--space-3);   /* 6px 12px */
  border-radius: var(--radius-container);
  background: var(--color-surface-inverse);
  box-shadow: var(--shadow-md);
  color: var(--color-text-inverse);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-regular);
  line-height: var(--type-body-small-line-height);
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-50%);
  transition: opacity var(--duration-fast) var(--easing-standard), visibility var(--duration-fast);
}

.ds-avatar-tip:hover .ds-avatar-tooltip,
.ds-avatar-tip:focus-within .ds-avatar-tooltip,
.ds-avatar-tip.is-tooltip-open .ds-avatar-tooltip {
  opacity: 1;
  visibility: visible;
}

/* Escape hides it until the pointer or focus leaves (main.js) */
.ds-avatar-tip.is-tooltip-dismissed .ds-avatar-tooltip {
  opacity: 0;
  visibility: hidden;
}

/* With dropdown: the avatar plus a small chevron, for account menus */
.ds-avatar-menu {
  position: relative;
  display: inline-flex;
  font-family: var(--font-body);
}

.ds-avatar-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1);
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
}

.ds-avatar-trigger:hover,
.ds-avatar-trigger.is-hover {
  background: var(--color-hover-bg);
}

.ds-avatar-trigger .ds-avatar-chevron {
  width: var(--space-4);
  height: var(--space-4);
  margin-right: var(--space-1);
  transition: transform var(--duration-normal) var(--easing-standard);
}

.ds-avatar-trigger[aria-expanded="true"] .ds-avatar-chevron {
  transform: rotate(180deg);
}

/* The menu: a white card below the avatar, lined up with its right edge */
.ds-avatar-menu-list {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: var(--layer-dropdown);
  min-width: calc(var(--space-24) + var(--space-16));   /* 160px */
  margin: 0;
  padding: var(--space-1);
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
  list-style: none;
}

.ds-avatar-menu-list[hidden] {
  display: none;
}

.ds-avatar-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--size-control-compact);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.ds-avatar-menu-item svg,
.ds-avatar-menu-item i[data-lucide] {
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-text-muted);
}

.ds-avatar-menu-item:hover,
.ds-avatar-menu-item:focus-visible {
  background: var(--color-surface-muted);
  outline: none;
}

.ds-avatar-menu-item:focus-visible {
  box-shadow: inset 0 0 0 var(--focus-ring-width) var(--color-focus-ring);
}

/* People who ask for less motion */
@media (prefers-reduced-motion: reduce) {
  .ds-avatar-tooltip,
  .ds-avatar-trigger .ds-avatar-chevron {
    transition: none;
  }

  .ds-avatar-group.is-expanded > [data-avatar-extra] {
    animation: none;
  }
}

/* Badge -------------------------------------------------------------------- */

/* A small count or dot that shows something needs attention. Only existing
   tokens: red-600 (--color-error) with white text, grey-100 / grey-900 for
   standalone counts.
   Markup, on an icon button (the count is part of the button's name, so the
   badge itself is hidden from screen readers):
     <button class="ds-button ds-button--tertiary ds-button--icon"
             aria-label="Notifications, 3 unread">
       <span class="ds-badge-anchor">
         <i data-lucide="bell" aria-hidden="true"></i>
         <span class="ds-badge" aria-hidden="true">3</span>
       </span>
     </button>
   Dot (no number): <span class="ds-badge ds-badge--dot" aria-hidden="true"></span>
   Standalone, next to a label: <span class="ds-badge ds-badge--neutral">12</span>
   Show up to 99, then "99+". Hide it (hidden) when the count is 0. */

/* Count: 20px tall, 6px side padding, at least 20px wide, so one digit is a
   perfect circle and two or more stretch into a pill. 12px semi-bold. */
.ds-badge {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: var(--space-5);                 /* 20px */
  height: var(--space-5);                    /* 20px */
  padding: 0 calc(var(--space-3) / 2);       /* 6px */
  border-radius: var(--radius-full);
  background: var(--color-error);            /* red-600 */
  color: var(--color-text-inverse);
  font-family: var(--font-body);
  font-size: var(--font-size-xs);            /* 12px */
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  white-space: nowrap;
}

.ds-badge[hidden] {
  display: none;
}

/* Dot: 8px, no number, with a 2px white ring */
.ds-badge.ds-badge--dot {
  width: var(--space-2);
  min-width: var(--space-2);
  height: var(--space-2);
  padding: 0;
  box-shadow: 0 0 0 var(--border-width-thick) var(--color-surface);
}

/* Standalone, next to a label (e.g. "Inbox 12"): grey-100 with grey-900 text */
.ds-badge.ds-badge--neutral {
  background: var(--color-surface-subtle);
  color: var(--color-text);
}

/* On an icon (or avatar): the badge is centred on its top-right corner,
   with a 2px white ring that separates it from the icon. Icons are 24px. */
.ds-badge-anchor {
  --button-icon: var(--space-6);             /* 24px icon inside an icon button */
  position: relative;
  display: inline-flex;
}

.ds-badge-anchor > svg,
.ds-badge-anchor > i[data-lucide] {
  width: var(--space-6);
  height: var(--space-6);
}

.ds-badge-anchor > .ds-badge {
  position: absolute;
  top: 0;
  right: 0;
  translate: 50% -50%;
  box-shadow: 0 0 0 var(--border-width-thick) var(--color-surface);
}

/* On a round avatar: centred on the circle's edge at the top right
   (14.6% in from the corner of its square box) */
.ds-badge-anchor--round > .ds-badge {
  top: 14.6%;
  right: 14.6%;
}

/* When the count changes: a subtle pop-in (main.js adds .is-updated).
   Never a constant pulse. */
.ds-badge.is-updated {
  animation: ds-badge-pop var(--duration-normal) var(--easing-standard);
}

@keyframes ds-badge-pop {
  from {
    scale: 0.8;
  }
  to {
    scale: 1;
  }
}

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

/* Breadcrumb --------------------------------------------------------------- */

/* Shows where the current page sits in the site. Only existing tokens.
   Markup:
   <nav class="ds-breadcrumb" aria-label="Breadcrumb">
     <ol class="ds-breadcrumb-list">
       <li class="ds-breadcrumb-item">
         <a class="ds-breadcrumb-link" href="/">Home</a>
         <i data-lucide="chevron-right" class="ds-breadcrumb-separator" aria-hidden="true"></i>
       </li>
       …
       <li class="ds-breadcrumb-item">
         <span class="ds-breadcrumb-current" aria-current="page">Security</span>
       </li>
     </ol>
   </nav>
   Long trails (more than 4 levels): the middle levels get data-breadcrumb-hidden
   and hidden, after an item holding a "…" button
   (.ds-breadcrumb-overflow, data-breadcrumb-more), which reveals them (main.js).
   Page-level use: wrap the breadcrumb and a back link in
   .ds-breadcrumb-responsive, which shows the back link instead on mobile
   and keeps 24px above the page title. */
.ds-breadcrumb {
  max-width: 100%;   /* never wider than its container: long names shorten instead */
  font-family: var(--font-body);
  font-size: var(--type-body-small-size);      /* 14px */
  line-height: var(--type-body-small-line-height);   /* 20px */
}

/* One row, never wrapping: 8px between each link and separator */
.ds-breadcrumb .ds-breadcrumb-list {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ds-breadcrumb .ds-breadcrumb-item {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  margin: 0;
}

.ds-breadcrumb .ds-breadcrumb-item[hidden] {
  display: none;
}

/* Separator: a 16px grey-400 chevron (decorative) */
.ds-breadcrumb .ds-breadcrumb-separator {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-border-strong);
}

/* Links and the current page: a 24px tall click area (20px line + 2px top
   and bottom), shortened with an ellipsis after 200px */
.ds-breadcrumb .ds-breadcrumb-link,
.ds-breadcrumb .ds-breadcrumb-current {
  display: block;
  max-width: calc(var(--space-24) * 2 + var(--space-2));   /* 200px */
  padding-block: calc((var(--size-target-minimum) - var(--type-body-small-line-height)) / 2);
  overflow: hidden;
  border-radius: var(--radius-sm);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Link: grey-600 */
.ds-breadcrumb .ds-breadcrumb-link {
  color: var(--color-text-muted);
  text-decoration: none;
}

/* Hover: grey-900, underlined */
.ds-breadcrumb .ds-breadcrumb-link:hover,
.ds-breadcrumb .ds-breadcrumb-link.is-hover {
  color: var(--color-text);
  text-decoration: underline;
  text-decoration-thickness: var(--border-width-thin);
  text-underline-offset: var(--link-underline-offset);
}

/* Pressed: grey-900 */
.ds-breadcrumb .ds-breadcrumb-link:active,
.ds-breadcrumb .ds-breadcrumb-link.is-pressed {
  color: var(--color-text);
  text-decoration: none;
}

/* Focus: 2px pink-600 ring */
.ds-breadcrumb .ds-breadcrumb-link:focus-visible,
.ds-breadcrumb .ds-breadcrumb-link.is-focus,
.ds-breadcrumb .ds-breadcrumb-overflow:focus-visible,
.ds-breadcrumb .ds-breadcrumb-overflow.is-focus,
.ds-breadcrumb-back:focus-visible,
.ds-breadcrumb-back.is-focus {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* Current page: grey-900 semi-bold, plain text (not a link, no hover) */
.ds-breadcrumb .ds-breadcrumb-current {
  color: var(--color-text);
  font-weight: var(--font-weight-semibold);
}

/* "…": the hidden middle levels. A button with the same look as a link. */
.ds-breadcrumb .ds-breadcrumb-overflow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--size-target-minimum);
  min-height: var(--size-target-minimum);
  padding: 0 var(--space-1);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text-muted);
  font: inherit;
  cursor: pointer;
}

.ds-breadcrumb .ds-breadcrumb-overflow:hover,
.ds-breadcrumb .ds-breadcrumb-overflow.is-hover {
  background: var(--color-hover-bg);
  color: var(--color-text);
}

/* Full name of a shortened page, on hover and keyboard focus */
.ds-breadcrumb .ds-breadcrumb-tooltip {
  position: absolute;
  bottom: calc(100% + var(--space-2));
  left: 0;
  z-index: var(--layer-dropdown);
  /* The same look as the Tooltip component */
  padding: calc(var(--space-1) * 1.5) var(--space-3);   /* 6px 12px */
  border-radius: var(--radius-container);
  background: var(--color-surface-inverse);
  box-shadow: var(--shadow-md);
  color: var(--color-text-inverse);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-regular);
  line-height: var(--type-body-small-line-height);
  width: max-content;
  max-width: calc(var(--space-24) * 2 + var(--space-2));   /* 200px, then it wraps */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--easing-standard), visibility var(--duration-fast);
}

.ds-breadcrumb .ds-breadcrumb-item:hover > .ds-breadcrumb-tooltip,
.ds-breadcrumb .ds-breadcrumb-item:focus-within > .ds-breadcrumb-tooltip {
  opacity: 1;
  visibility: visible;
}

/* Escape hides it until the pointer or focus leaves (main.js) */
.ds-breadcrumb .ds-breadcrumb-item.is-tooltip-dismissed > .ds-breadcrumb-tooltip {
  opacity: 0;
  visibility: hidden;
}

/* Back link, for mobile: a 16px chevron-left and "Back to …", 44px tall */
.ds-breadcrumb-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: var(--size-control-standard);   /* 44px */
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font-family: var(--font-body);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--type-body-small-line-height);
  text-decoration: none;
}

.ds-breadcrumb-back svg,
.ds-breadcrumb-back i[data-lucide] {
  width: var(--space-4);
  height: var(--space-4);
}

.ds-breadcrumb-back:hover,
.ds-breadcrumb-back.is-hover {
  color: var(--color-text);
  text-decoration: underline;
  text-decoration-thickness: var(--border-width-thin);
  text-underline-offset: var(--link-underline-offset);
}

/* On a page: the breadcrumb, 24px above the page title. On mobile the back
   link shows instead. (CSS can't use var() in @media, so the breakpoint is
   written here.) */
.ds-breadcrumb-responsive {
  margin-bottom: var(--space-6);
}

.ds-breadcrumb-responsive .ds-breadcrumb-back {
  display: none;
}

@media (max-width: 767px) {
  .ds-breadcrumb-responsive .ds-breadcrumb {
    display: none;
  }

  .ds-breadcrumb-responsive .ds-breadcrumb-back {
    display: inline-flex;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ds-breadcrumb .ds-breadcrumb-tooltip {
    transition: none;
  }
}

/* Button ------------------------------------------------------------------- */

/* Base button: MD size (44px tall, the default), 12px corners.
   Corners scale with the size (XS 6px, SM 8px, MD and LG 12px), matching
   fields and dropdowns of the same height.
   Hover and pressed get darker with each step.
   Add a variant class for colour, e.g. class="ds-button ds-button--primary".
   Heights are min-heights, so a button can grow if text is enlarged. */
.ds-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* Each size sets these two; the rest of the button uses them */
  --button-height: var(--size-control-standard);   /* 44px */
  --button-icon: var(--space-5);                     /* 20px */
  --button-radius: var(--radius-control);         /* 12px */
  min-height: var(--button-height);
  padding: 0 var(--space-4);
  border: 0;
  border-radius: var(--button-radius);
  background: none;
  font-family: var(--font-body);
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--type-body-line-height);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--easing-standard),
    color var(--duration-fast) var(--easing-standard),
    box-shadow var(--duration-fast) var(--easing-standard);
}

/* Icons inside a button (Lucide <i data-lucide> becomes an <svg>) */
.ds-button svg,
.ds-button i[data-lucide] {
  flex-shrink: 0;
  width: var(--button-icon);
  height: var(--button-icon);
}

/* Focus: 2px pink-600 ring, 2px away from the button.
   (.is-focus shows the same ring on the docs pages.) */
.ds-button:focus-visible,
.ds-button.is-focus {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* Sizes ------------------------------------------------------------------
   MD is the default (no class needed). All values come from existing tokens.
   Size   Height  Sides  Text           Icon  Gap   Corners
   XS     24px    8px    12px medium    12px  4px   6px
   SM     32px    12px   14px medium    16px  8px   8px
   MD     44px    16px   16px medium    20px  8px   12px
   LG     56px    24px   18px medium    24px  8px   12px  */

.ds-button--xs {
  --button-height: var(--size-target-minimum);   /* 24px */
  --button-icon: var(--space-3);                   /* 12px */
  --button-radius: var(--radius-control-xs);       /* 6px */
  gap: var(--space-1);
  padding: 0 var(--space-2);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
}

.ds-button--sm {
  --button-height: var(--size-control-compact);  /* 32px */
  --button-icon: var(--space-4);                   /* 16px */
  --button-radius: var(--radius-control-sm);       /* 8px */
  padding: 0 var(--space-3);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-button--lg {
  --button-height: calc(var(--space-12) + var(--space-2));  /* 48 + 8 = 56px */
  --button-icon: var(--space-6);                               /* 24px */
  --button-radius: var(--radius-control);                   /* 12px */
  padding: 0 var(--space-6);
  font-size: var(--type-body-large-size);
  line-height: var(--type-body-large-line-height);
}

/* Icon only: a square the height of its size. Always give it an aria-label. */
.ds-button--icon {
  min-width: var(--button-height);
  padding: 0;
}

/* Variants ---------------------------------------------------------------
   Borders are inset shadows, so they don't add to a button's size.
   The .is-hover / .is-pressed classes show those states on the docs pages. */

/* Primary: the main action. One per area. */
.ds-button--primary {
  background: var(--button-primary-bg);
  color: var(--button-primary-text);
}

.ds-button--primary:hover:not(:disabled),
.ds-button--primary.is-hover {
  background: var(--button-primary-bg-hover);
}

.ds-button--primary:active:not(:disabled),
.ds-button--primary.is-pressed {
  background: var(--button-primary-bg-pressed);
}

/* Secondary: supporting actions. White with a grey border. */
.ds-button--secondary {
  background: var(--color-surface);
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--color-border-strong);
  color: var(--color-text);
}

.ds-button--secondary:hover:not(:disabled),
.ds-button--secondary.is-hover {
  background: var(--color-surface-muted);
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--color-border-strong-hover);
}

.ds-button--secondary:active:not(:disabled),
.ds-button--secondary.is-pressed {
  background: var(--color-hover-bg);
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--color-border-strong-hover);
}

/* Tertiary: low-emphasis actions. No background until hovered. */
.ds-button--tertiary {
  background: transparent;
  color: var(--color-text);
}

.ds-button--tertiary:hover:not(:disabled),
.ds-button--tertiary.is-hover {
  background: var(--color-hover-bg);
}

.ds-button--tertiary:active:not(:disabled),
.ds-button--tertiary.is-pressed {
  background: var(--color-pressed-bg);
}

/* Outline: for busy backgrounds */
.ds-button--outline {
  background: transparent;
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--color-primary);
  color: var(--color-primary-text);
}

.ds-button--outline:hover:not(:disabled),
.ds-button--outline.is-hover {
  background: var(--color-primary-subtle);
}

.ds-button--outline:active:not(:disabled),
.ds-button--outline.is-pressed {
  background: var(--color-primary-subtle-pressed);
}

/* Destructive: the default for delete and other irreversible actions.
   White with a red border and red text. */
.ds-button--destructive {
  background: var(--color-surface);
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--color-danger);
  color: var(--color-danger);
}

.ds-button--destructive:hover:not(:disabled),
.ds-button--destructive.is-hover {
  background: var(--color-danger-bg);
}

/* Pressed: a thicker border. (A red-200 background would drop the text to
   3.34:1, so the background stays red-50.) */
.ds-button--destructive:active:not(:disabled),
.ds-button--destructive.is-pressed {
  background: var(--color-danger-bg);
  box-shadow: inset 0 0 0 var(--border-width-thick) var(--color-danger);
}

/* Destructive filled: ONLY for the final "yes, delete it" step in a
   confirmation modal. Hover adds a 2px red-200 ring around the button. */
.ds-button--destructive-filled {
  background: var(--color-danger);
  color: var(--color-on-primary);
}

.ds-button--destructive-filled:hover:not(:disabled),
.ds-button--destructive-filled.is-hover,
.ds-button--destructive-filled:active:not(:disabled),
.ds-button--destructive-filled.is-pressed {
  box-shadow: 0 0 0 var(--border-width-thick) var(--color-danger-border);
}

/* Destructive ghost: red-600 text and icon, no outline or background, like
   Tertiary. For delete next to other Ghost buttons, e.g. in a table's batch
   action bar, where an outlined button would be too loud. Hover red-50
   (5.89:1); pressed a little deeper (4.99:1). It still asks for
   confirmation like any delete. */
.ds-button--destructive-ghost {
  background: transparent;
  color: var(--color-danger);
}

.ds-button--destructive-ghost:hover:not(:disabled),
.ds-button--destructive-ghost.is-hover {
  background: var(--color-danger-bg);
}

.ds-button--destructive-ghost:active:not(:disabled),
.ds-button--destructive-ghost.is-pressed {
  background: color-mix(in srgb, var(--color-danger-border) 30%, var(--color-danger-bg));
}

/* Link button: light actions like "Show more". Keeps the 44px height so
   it's still easy to tap. */
/* Looks like a standalone link (see Link): pink-600, underlined on hover
   (2px, pink-700), pink-800 when pressed */
.ds-button--link {
  padding: 0;
  background: none;
  color: var(--color-primary);
  text-underline-offset: var(--link-underline-offset);
}

.ds-button--link:hover:not(:disabled),
.ds-button--link.is-hover {
  color: var(--color-primary-hover);
  text-decoration: underline;
  text-decoration-thickness: var(--border-width-thick);
}

.ds-button--link:active:not(:disabled),
.ds-button--link.is-pressed {
  color: var(--color-primary-pressed);
  text-decoration: underline;
  text-decoration-thickness: var(--border-width-thick);
}

/* States -------------------------------------------------------------------- */

/* Disabled: grey for every variant. Explain nearby why it's unavailable.
   (A loading button is aria-disabled too, but keeps its normal look.) */
.ds-button:disabled,
.ds-button[aria-disabled="true"]:not(.is-loading) {
  background: var(--color-disabled-bg);
  box-shadow: none;
  color: var(--color-text-subtle);
  cursor: not-allowed;
}

.ds-button--link:disabled,
.ds-button--link[aria-disabled="true"] {
  background: none;
}

/* Loading: a spinner takes the icon's place, so the width doesn't change */
.ds-button.is-loading {
  cursor: progress;
}

.ds-button-spinner {
  animation: ds-spin var(--duration-spin) linear infinite;
}

/* Text-only button loading: the label goes invisible but keeps its space
   (so the width doesn't change, and screen readers still hear it), and the
   spinner sits centred on top. Markup:
   <button class="ds-button … is-loading" aria-busy="true">
     <span class="ds-button-label">Save</span>
     <i data-lucide="loader-circle" class="ds-button-spinner"></i>
   </button> */
.ds-button.is-loading .ds-button-label {
  opacity: 0;
}

.ds-button.is-loading .ds-button-label ~ .ds-button-spinner {
  position: absolute;
}

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

/* People who ask for less motion get a slower spinner */
@media (prefers-reduced-motion: reduce) {
  .ds-button-spinner {
    animation-duration: calc(var(--duration-spin) * 4);
  }
}

/* On a dark (grey-900) background ------------------------------------------
   Colours adjusted to keep at least 4.5:1 for text and 3:1 for borders. */
.ds-surface-dark {
  background: var(--color-surface-inverse-strong);
}

.ds-surface-dark .ds-button--tertiary {
  color: var(--color-text-inverse);          /* on grey-900: 17.70:1 */
}

.ds-surface-dark .ds-button--tertiary:hover:not(:disabled),
.ds-surface-dark .ds-button--tertiary.is-hover {
  background: var(--color-hover-bg-on-dark);   /* 13.48:1 */
}

.ds-surface-dark .ds-button--tertiary:active:not(:disabled),
.ds-surface-dark .ds-button--tertiary.is-pressed {
  background: var(--color-pressed-bg-on-dark);   /* 9.48:1 */
}

.ds-surface-dark .ds-button--outline {
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--color-primary-on-dark);  /* 6.95:1 */
  color: var(--color-primary-text-on-dark);                                              /* 10.11:1 */
}

.ds-surface-dark .ds-button--outline:hover:not(:disabled),
.ds-surface-dark .ds-button--outline.is-hover {
  background: var(--color-hover-bg-on-dark);   /* pink-200 text: 7.69:1 */
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--color-primary-on-dark);
}

.ds-surface-dark .ds-button--outline:active:not(:disabled),
.ds-surface-dark .ds-button--outline.is-pressed {
  background: var(--color-pressed-bg-on-dark);   /* 5.42:1 */
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--color-primary-on-dark);
}

/* pink-600 on grey-900 is only 3.2:1, so the focus ring is pink-300 (6.95:1) */
.ds-surface-dark .ds-button:focus-visible,
.ds-surface-dark .ds-button.is-focus {
  outline-color: var(--color-focus-ring-on-dark);
}

/* (a loading button is aria-disabled too, but keeps its normal look) */
.ds-surface-dark .ds-button:disabled:not(.is-loading),
.ds-surface-dark .ds-button[aria-disabled="true"]:not(.is-loading) {
  background: var(--color-disabled-bg);
  box-shadow: none;
  color: var(--color-text-subtle);
}

/* Groups ------------------------------------------------------------------- */

/* A row of buttons, 8px apart */
.ds-button-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Order: primary first (left, or top when stacked), then secondary, then
   tertiary. A destructive button goes last, with 24px before it:
   the usual 8px gap plus 16px. (Only when other actions come before it,
   and it's the last button in the group.) */
.ds-button-group > .ds-button:not(.ds-button--destructive) ~ .ds-button--destructive:last-of-type {
  margin-left: var(--space-4);
}

/* Stacked: one per row, all the same width */
.ds-button-group--stacked {
  flex-direction: column;
  align-items: stretch;
}

/* Stacked: the extra 16px goes above the destructive button instead */
.ds-button-group--stacked > .ds-button:not(.ds-button--destructive) ~ .ds-button--destructive:last-of-type {
  margin-top: var(--space-4);
  margin-left: 0;
}


/* Card --------------------------------------------------------------------- */

/* A container for content about one subject. Every card has a 16:9 image
   and a title. White, 1px grey-200 border, 12px corners, a subtle shadow
   on hover. Markup (top to bottom; tags, secondary text and the footer are
   optional):
   <article class="ds-card">
     <div class="ds-card-media"><img src="…" alt="…"></div>      16:9, edge to edge
     <div class="ds-card-body">                                   16px padding
       <ul class="ds-card-tags"><li><span class="ds-tag">…</span></li></ul>   up to 3 Medium tags (the Tag component)
       <h3 class="ds-card-title">Title</h3>                       max 2 lines
       <p class="ds-card-secondary">Secondary text</p>            4px below the title
       <p class="ds-card-text">Supporting text</p>               12px below, max 3 lines
       <div class="ds-card-footer">…</div>                         16px below
     </div>
   </article>
   Footer: an author (.ds-card-author: 32px avatar, name, date or read
   time) or actions (.ds-card-actions: up to 2 buttons, Primary + Ghost).
   Clickable card: put <a class="ds-card-link" href="…"> inside the title.
   It stretches over the whole card, so the card is one link. Don't add
   buttons or a separate "Read more" link to a clickable card. */
.ds-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  margin: 0;
  overflow: hidden;   /* the image's top corners follow the card's */
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  color: var(--color-text-muted);
  text-align: left;
  transition: box-shadow var(--duration-normal) var(--easing-standard);
  font-family: var(--font-body);
}

/* Image: 16:9, full width, no padding */
.ds-card .ds-card-media {
  flex-shrink: 0;
  aspect-ratio: 16 / 9;
  background: var(--color-surface-subtle);
}

.ds-card .ds-card-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Body: 16px padding on all sides. It fills the card, so the footer can
   sit at the bottom and cards in a row line up. */
.ds-card .ds-card-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  padding: var(--space-4);
}

/* Tags: up to 3, 8px apart, 12px above the title */
.ds-card .ds-card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
}

/* Title: 20px semi-bold, line height 1.3, at most 2 lines */
.ds-card .ds-card-title {
  display: -webkit-box;
  margin: 0;
  overflow: hidden;
  color: var(--color-text);
  font-family: var(--font-heading);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  letter-spacing: normal;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

/* Secondary text (optional): 14px muted, 4px below the title */
.ds-card .ds-card-secondary {
  margin: var(--space-1) 0 0;
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* Supporting text: 14px muted, 12px below the title block, at most 3 lines */
.ds-card .ds-card-text {
  display: -webkit-box;
  margin: var(--space-3) 0 0;
  overflow: hidden;
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
}

/* Footer: tags, author or actions, at least 16px below the supporting text
   and pushed to the bottom of the card */
.ds-card .ds-card-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-4);
}

/* Author: a 32px avatar, the name (14px semi-bold) and the date or read
   time (12px muted), 8px apart */
.ds-card .ds-card-author {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.ds-card .ds-card-author .ds-avatar {
  --avatar-size: var(--size-control-compact);   /* 32px */
  --avatar-text: var(--font-size-xs);
}

.ds-card .ds-card-author-name {
  display: block;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

.ds-card .ds-card-author-meta {
  display: block;
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
}

/* Actions: up to 2 buttons (Primary + Ghost), on the left, 8px apart */
.ds-card .ds-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* Clickable card: the title link covers the whole card */
.ds-card .ds-card-link {
  color: inherit;
  text-decoration: none;
}

.ds-card .ds-card-link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.ds-card .ds-card-link:focus-visible {
  outline: none;   /* the ring goes round the whole card instead (below) */
}

/* Hover: a subtle shadow. Pressed: back to none. */
.ds-card:has(.ds-card-link):hover,
.ds-card.is-hover {
  box-shadow: var(--shadow-md);
}

.ds-card:has(.ds-card-link:active),
.ds-card.is-pressed {
  box-shadow: none;
}

/* Focus: a 2px pink-600 ring round the whole card */
.ds-card:has(.ds-card-link:focus-visible),
.ds-card.is-focus {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* Card grid: 3 columns with 24px gaps on desktop, 2 on tablet, 1 on
   mobile. Cards in a row stretch to the same height. (CSS can't use var()
   in @media, so the breakpoints are written here.) */
.ds-card-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-6);
}

@media (max-width: 1023px) {
  .ds-card-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 767px) {
  .ds-card-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

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

/* Drawer ------------------------------------------------------------------- */

/* A panel that slides in from the edge of the screen, over a dimmed page.
   Put it at the end of <body>, and open it from a button with
   data-drawer-open="its id" (main.js does the rest):
   <div class="ds-drawer" id="project-drawer" data-drawer hidden>
     <div class="ds-drawer-overlay" data-drawer-close></div>
     <div class="ds-drawer-panel" role="dialog" aria-modal="true"
          aria-labelledby="project-drawer-title">
       <div class="ds-drawer-header">
         <button class="ds-button ds-button--tertiary ds-button--icon ds-drawer-back"
                 aria-label="Close" data-drawer-close>…arrow-left…</button>   phones only
         <h2 class="ds-drawer-title" id="project-drawer-title">Project details</h2>
         <div class="ds-drawer-actions">…icon buttons (optional)…</div>
         <button class="ds-button ds-button--tertiary ds-button--icon ds-drawer-close"
                 aria-label="Close" data-drawer-close>…x…</button>
       </div>
       <div class="ds-drawer-content">…groups, 16px apart…</div>     scrolls
       <div class="ds-drawer-footer">…Primary + Ghost (optional)…</div>
     </div>
   </div>
   Sizes: --sm (360px), MD (480px, the default, no class), --lg (720px).
   --left slides in from the left. Under 1024px a drawer is at most 80% of
   the screen; under 768px it fills the screen, with no overlay, and the
   close button becomes a back arrow on the left. */
.ds-drawer {
  position: fixed;
  inset: 0;
  z-index: var(--layer-drawer);
  display: flex;
  justify-content: flex-end;
  font-family: var(--font-body);
}

.ds-drawer[hidden] {
  display: none;
}

.ds-drawer--sm { --drawer-width: calc(var(--space-10) * 9); }   /* 360px */
.ds-drawer--lg { --drawer-width: calc(var(--space-20) * 9); }   /* 720px */

.ds-drawer--left {
  justify-content: flex-start;
}

/* The dimmed page: grey-900 at 40% */
.ds-drawer-overlay {
  position: absolute;
  inset: 0;
  background: var(--color-overlay);
  opacity: 0;
  transition: opacity var(--duration-normal) var(--easing-standard);
}

/* The panel: full height, white. It starts just off the screen and slides
   in when the drawer gets .is-open. */
.ds-drawer-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: var(--drawer-width, calc(var(--space-24) * 5));   /* 480px by default */
  max-width: 100%;
  height: 100%;
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
  color: var(--color-text);
  transform: translateX(100%);
  transition: transform var(--duration-normal) var(--easing-standard);
}

.ds-drawer--left .ds-drawer-panel {
  transform: translateX(-100%);
}

.ds-drawer.is-open .ds-drawer-overlay {
  opacity: 1;
}

.ds-drawer.is-open .ds-drawer-panel {
  transform: none;
}

/* Header: 16px 24px padding and a 1px grey-200 line below. It never
   scrolls. Title, then any actions, then the close button. */
.ds-drawer-header {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-6);
  border-bottom: var(--border-width-thin) solid var(--color-border);
}

/* Title: 20px semi-bold. Keep it to one line; if it's longer it wraps
   rather than being cut off. */
.ds-drawer-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  color: var(--color-text);
  font-family: var(--font-heading);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  letter-spacing: normal;
  overflow-wrap: anywhere;
}

/* Header actions (e.g. edit, more), 4px apart */
.ds-drawer-actions {
  display: flex;
  flex-shrink: 0;
  gap: var(--space-1);
}

/* The close button, back arrow and header actions: a 20px grey-700 icon
   in a 32 × 32 click area, no border. Grey-100 on hover (the tertiary
   button's hover), because the header is white. */
.ds-drawer-header .ds-button--icon {
  --button-height: var(--size-control-compact);   /* 32px */
  --button-icon: var(--space-5);                    /* 20px */
  --button-radius: var(--radius-control-sm);        /* 8px */
  flex-shrink: 0;
  color: var(--color-text-secondary);
}

.ds-drawer-header .ds-button--icon:hover:not(:disabled),
.ds-drawer-header .ds-button--icon.is-hover {
  color: var(--color-text);
}

/* The back arrow only shows on phones (see below) */
.ds-drawer-header .ds-drawer-back {
  display: none;
}

/* Content: 24px padding, 16px between groups. The only part that scrolls. */
.ds-drawer-content {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-4);
  min-height: 0;
  padding: var(--space-6);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Footer (optional): 16px 24px padding and a 1px grey-200 line above.
   Primary first, then Ghost, 8px apart. It never scrolls. */
.ds-drawer-footer {
  display: flex;
  flex-shrink: 0;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-6);
  border-top: var(--border-width-thin) solid var(--color-border);
}

/* "Discard unsaved changes?" takes the footer's place when someone tries
   to close a drawer with unsaved changes (main.js shows it) */
.ds-drawer-confirm {
  display: flex;
  flex: 1;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
}

.ds-drawer-confirm[hidden],
.ds-drawer-footer-actions[hidden] {
  display: none;
}

.ds-drawer-confirm-text {
  margin: 0;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

.ds-drawer-footer-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* The page behind can't scroll while a drawer is open */
html.has-drawer-open {
  overflow: hidden;
}

/* Tablet (768–1023px): at most 80% of the screen, overlay stays.
   (CSS can't use var() in @media, so the breakpoints are written here.) */
@media (max-width: 1023px) {
  .ds-drawer-panel {
    max-width: 80%;
  }
}

/* Mobile (under 768px): full screen, no overlay, and a back arrow on the
   left instead of the close button. The footer stays at the bottom. */
@media (max-width: 767px) {
  .ds-drawer-panel {
    width: 100%;
    max-width: 100%;
    box-shadow: none;
  }

  .ds-drawer-overlay {
    display: none;
  }

  .ds-drawer-header .ds-drawer-back {
    display: inline-flex;
  }

  .ds-drawer-header .ds-drawer-close {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ds-drawer-overlay,
  .ds-drawer-panel {
    transition: none;
  }
}

/* Dropdown ----------------------------------------------------------------- */

/* A list of options in a menu that opens from a trigger.
   Dropdown field (choose a value in a form):
   <div class="ds-dropdown" data-dropdown>
     <span class="ds-dropdown-label" id="country-label">Country</span>
     <div class="ds-dropdown-control">
       <button type="button" class="ds-dropdown-field" role="combobox"
               aria-haspopup="listbox" aria-expanded="false"
               aria-controls="country-list" aria-labelledby="country-label country-value">
         <span class="ds-dropdown-value is-placeholder" id="country-value">Select a country</span>
         <i data-lucide="chevron-down" class="ds-dropdown-chevron" aria-hidden="true"></i>
       </button>
       <div class="ds-dropdown-menu" hidden>
         …optional .ds-dropdown-search…
         <ul class="ds-dropdown-list" role="listbox" id="country-list" aria-labelledby="country-label">
           <li class="ds-dropdown-item" role="option" id="…" aria-selected="false">
             <span class="ds-dropdown-item-label">France</span>
             <i data-lucide="check" class="ds-dropdown-check" aria-hidden="true"></i>
           </li>
         </ul>
       </div>
     </div>
     <p class="ds-dropdown-helper">Helper text</p>
   </div>
   Dropdown menu (actions or navigation), from a button, icon button or
   avatar: add .ds-dropdown--menu; the trigger gets aria-haspopup="menu",
   the menu role="menu" and its items role="menuitem".
   Sizes match the buttons: --sm (32px), MD (44px, the default, no class),
   --lg (56px). Menu items are the same height as the field.
   main.js (section 24) opens, closes and moves through the options. */
.ds-dropdown {
  --dropdown-height: var(--size-control-standard);   /* 44px */
  --dropdown-radius: var(--radius-control);         /* 12px, like a Medium button */
  --dropdown-pad: var(--space-4);                      /* 16px */
  --dropdown-text: var(--type-body-size);
  --dropdown-line: var(--type-body-line-height);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--dropdown-text);
  line-height: var(--dropdown-line);
  text-align: left;
}

.ds-dropdown--sm {
  --dropdown-height: var(--size-control-compact);    /* 32px */
  --dropdown-radius: var(--radius-control-sm);         /* 8px */
  --dropdown-pad: var(--space-3);                      /* 12px */
  --dropdown-text: var(--type-body-small-size);
  --dropdown-line: var(--type-body-small-line-height);
}

.ds-dropdown--lg {
  --dropdown-height: calc(var(--space-12) + var(--space-2));   /* 56px */
  --dropdown-radius: var(--radius-control);                   /* 12px */
  --dropdown-text: var(--type-body-large-size);
  --dropdown-line: var(--type-body-large-line-height);
}

/* Menus from a button or avatar sit inline, at the trigger's size */
.ds-dropdown--menu {
  display: inline-flex;
}

/* Label: 14px semi-bold, 8px above the field */
.ds-dropdown-label {
  align-self: flex-start;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

/* The field and its menu: the menu is placed from here */
.ds-dropdown-control {
  position: relative;
}

/* Field: 1px --field-border (grey-500, 4.8:1, WCAG 1.4.11), 8 / 12 / 12px corners by size: the shared field style. The value
   or placeholder fills it, then an 8px gap and the chevron. */
.ds-dropdown-field {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--dropdown-height);
  margin: 0;
  padding: 0 var(--dropdown-pad);
  border: var(--border-width-thin) solid var(--field-border);   /* the shared field style */
  border-radius: var(--dropdown-radius);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.ds-dropdown-value {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* With icons: the chosen option's 16px icon at the start of the field */
.ds-dropdown-field-icon {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-text-secondary);
}

/* Placeholder: grey-500 (4.8:1) */
.ds-dropdown-value.is-placeholder {
  color: var(--color-text-subtle);
}

.ds-dropdown-chevron {
  flex-shrink: 0;
  width: var(--space-5);    /* 20px */
  height: var(--space-5);
  color: var(--color-text-secondary);
  transition: transform var(--duration-normal) var(--easing-standard);
}

/* Hover: a darker border */
.ds-dropdown-field:hover,
.ds-dropdown-field.is-hover {
  border-color: var(--field-border-hover);
}

/* Focus (keyboard): a pink-600 border and a 3px pink-200 ring, like every field */
.ds-dropdown-field:focus-visible,
.ds-dropdown-field.is-focus {
  border-color: var(--color-primary);
  outline: none;
  box-shadow: 0 0 0 calc(var(--border-width-thin) * 3) var(--field-focus-ring);
}

/* Open: grey-900 border, chevron points up */
.ds-dropdown.is-open .ds-dropdown-field {
  border-color: var(--field-border-active);
}

.ds-dropdown.is-open .ds-dropdown-chevron {
  transform: rotate(180deg);
}

/* Disabled: grey-50 fill, grey-300 border, can't be opened (like every field) */
.ds-dropdown-field:disabled {
  border-color: var(--color-border-disabled);
  background: var(--color-surface-muted);
  color: var(--color-text-subtle);
  cursor: not-allowed;
}

.ds-dropdown-field:disabled .ds-dropdown-chevron {
  color: var(--color-text-disabled);
}

/* Read-only: the value can be read and focused, but not changed. No chevron. */
.ds-dropdown-field[aria-readonly="true"] {
  border-color: var(--color-surface-muted);   /* no visible border, like every read-only field */
  background: var(--color-surface-muted);
  cursor: default;
}

.ds-dropdown-field[aria-readonly="true"] .ds-dropdown-chevron {
  display: none;
}

/* Error and warning: the border and the message take the status colour.
   The message also has an icon, so colour isn't the only sign. */
.ds-dropdown--error .ds-dropdown-field {
  border-color: var(--color-error);
}

.ds-dropdown--warning .ds-dropdown-field {
  border-color: var(--color-warning);
}

/* Helper or error text: 14px, 8px below the field */
.ds-dropdown-helper {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-dropdown-helper svg,
.ds-dropdown-helper i[data-lucide] {
  flex-shrink: 0;
  width: var(--space-4);    /* 16px */
  height: var(--space-4);
  margin-top: calc((var(--type-body-small-line-height) - var(--space-4)) / 2);
}

.ds-dropdown--error .ds-dropdown-helper {
  color: var(--color-error);
}

.ds-dropdown--warning .ds-dropdown-helper {
  color: var(--color-warning);
}

/* Menu: 8px below the field, at least as wide as it, 4px padding inside,
   white, 12px corners. Taller than 320px, it scrolls. */
.ds-dropdown-menu {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: var(--layer-dropdown);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  width: 100%;
  max-height: calc(var(--space-20) * 4);   /* 320px */
  padding: var(--space-1);
  overflow-y: auto;
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.ds-dropdown-menu[hidden] {
  display: none;
}

/* Menus from a button or avatar: as wide as their longest option (at
   least 192px) */
.ds-dropdown--menu .ds-dropdown-menu {
  width: max-content;
  min-width: calc(var(--space-24) * 2);   /* 192px */
  max-width: calc(var(--space-24) * 3);   /* 288px */
}

/* No room below: the menu opens above, still 8px from the field. Add
   data-placement="top" to a .ds-dropdown to always open it above (used in
   table pagination and other bottom bars). Near the right of the screen:
   the menu lines up with the trigger's right edge. (main.js adds these.) */
.ds-dropdown--up .ds-dropdown-menu {
  top: auto;
  bottom: calc(100% + var(--space-2));
}

.ds-dropdown--end .ds-dropdown-menu {
  right: 0;
  left: auto;
}

/* Items are 4px apart */
.ds-dropdown-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Menu item: the same height as the field. Its side padding is 4px less
   than the field's, so with the menu's 4px padding the text lines up with
   the field's text. Icon (16px) and check mark 8px from the label. */
.ds-dropdown-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--dropdown-height);
  margin: 0;
  padding: var(--space-1) calc(var(--dropdown-pad) - var(--space-1));
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.ds-dropdown-item-label {
  flex: 1;
  min-width: 0;
}

.ds-dropdown-item-icon,
.ds-dropdown-check {
  flex-shrink: 0;
  width: var(--space-4);    /* 16px */
  height: var(--space-4);
}

.ds-dropdown-item-icon {
  color: var(--color-text-secondary);
}

/* The check mark only shows on the selected option */
.ds-dropdown-check {
  visibility: hidden;
}

.ds-dropdown-item[aria-selected="true"] .ds-dropdown-check {
  visibility: visible;
}

/* Hover: grey-100. The option the arrow keys are on (.is-active, only
   while the keyboard is in use: .is-keyboard, set by main.js) looks the same. */
.ds-dropdown-item:hover,
.ds-dropdown-item.is-hover,
.ds-dropdown.is-keyboard .ds-dropdown-item.is-active {
  background: var(--color-hover-bg);
}

/* Focus ring: 2px pink-600, inside the item, on keyboard focus only
   (never after a mouse click) */
.ds-dropdown.is-keyboard .ds-dropdown-item.is-active,
.ds-dropdown-item:focus-visible,
.ds-dropdown-item.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: calc(var(--focus-ring-width) * -1);
}

/* Selected (single choice): pink-50 background and a pink-700 check mark;
   the label stays grey-900. Selection wins over hover. Multi-select lists
   don't get the background: their pink checkbox already shows it. */
.ds-dropdown-item[aria-selected="true"] {
  font-weight: var(--font-weight-medium);
}

[role="listbox"]:not([aria-multiselectable="true"]) > .ds-dropdown-item[aria-selected="true"],
[role="listbox"]:not([aria-multiselectable="true"]) > .ds-dropdown-item[aria-selected="true"]:hover {
  background: var(--color-selected-bg);
}

.ds-dropdown-check {
  color: var(--color-selected-text);
}

/* Disabled: grey-500, can't be chosen */
.ds-dropdown-item[aria-disabled="true"] {
  background: transparent;
  color: var(--color-text-subtle);
  cursor: not-allowed;
}

.ds-dropdown-item[aria-disabled="true"] .ds-dropdown-item-icon {
  color: var(--color-text-disabled);
}

/* Destructive (e.g. Delete, Log out): red-600, last, after a divider */
.ds-dropdown-item--destructive,
.ds-dropdown-item--destructive .ds-dropdown-item-icon {
  color: var(--color-error);
}

.ds-dropdown-item--destructive:hover,
.ds-dropdown-item--destructive.is-hover,
.ds-dropdown-item--destructive.is-active {
  background: var(--color-error-bg);
}

/* Multi-select: a Small checkbox before each label. It's the Checkbox
   component's box as a picture (.ds-checkbox-box.ds-checkbox-box--sm, see
   Checkbox below): an option can't hold a real input, so the option itself
   is selected (aria-selected) and the box follows it. Ticked when chosen; a
   dash on "Select all" when only some are chosen. */
.ds-dropdown-item[aria-selected="true"] .ds-checkbox-box,
.ds-dropdown-item.is-mixed .ds-checkbox-box {
  border-color: var(--color-primary);
  background: var(--color-primary);
}

.ds-dropdown-item[aria-selected="true"] .ds-checkbox-box .ds-checkbox-check,
.ds-dropdown-item.is-mixed:not([aria-selected="true"]) .ds-checkbox-box .ds-checkbox-dash {
  display: block;
}

/* Divider: a 1px grey-200 line, 4px above and below */
.ds-dropdown-divider {
  flex-shrink: 0;
  height: var(--border-width-thin);
  margin: var(--space-1) 0;
  border: 0;
  background: var(--color-border);
}

/* Group heading: 12px semi-bold muted */
.ds-dropdown-group {
  margin: 0;
  padding: var(--space-2) calc(var(--dropdown-pad) - var(--space-1)) var(--space-1);
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-caption-line-height);
}

/* Search at the top of the menu, with a 16px search icon */
.ds-dropdown-search {
  position: relative;
  flex-shrink: 0;
  margin-bottom: var(--space-1);
}

.ds-dropdown-search svg,
.ds-dropdown-search i[data-lucide] {
  position: absolute;
  top: 50%;
  left: calc(var(--dropdown-pad) - var(--space-1));
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-text-secondary);
  transform: translateY(-50%);
  pointer-events: none;
}

.ds-dropdown-search-input {
  display: block;
  box-sizing: border-box;
  width: 100%;
  min-height: var(--dropdown-height);
  margin: 0;
  padding: 0 calc(var(--dropdown-pad) - var(--space-1)) 0 calc(var(--dropdown-pad) + var(--space-4) + var(--space-1));
  border: var(--border-width-thin) solid var(--field-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
}

.ds-dropdown-search-input::placeholder {
  color: var(--color-text-subtle);
}

.ds-dropdown-search-input:hover {
  border-color: var(--field-border-hover);
}

.ds-dropdown-search-input:focus,
.ds-dropdown-search-input.is-focus {
  border-color: var(--color-primary);
  outline: none;
  box-shadow: 0 0 0 calc(var(--border-width-thin) * 3) var(--field-focus-ring);
}

/* The typed text, in bold, in each matching option */
.ds-dropdown-match {
  background: transparent;
  color: inherit;
  font-weight: var(--font-weight-bold);
}

.ds-dropdown-empty {
  margin: 0;
  padding: var(--space-2) calc(var(--dropdown-pad) - var(--space-1));
  color: var(--color-text-muted);
}

/* Profile menu: a 32px avatar as the trigger, and a header with the name
   and email */
.ds-dropdown-avatar {
  display: inline-flex;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  cursor: pointer;
}

.ds-dropdown-avatar .ds-avatar {
  --avatar-size: var(--size-control-compact);   /* 32px */
  --avatar-text: var(--font-size-xs);
}

.ds-dropdown-avatar:focus-visible,
.ds-dropdown-avatar.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

.ds-dropdown-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--space-2) calc(var(--dropdown-pad) - var(--space-1));
}

.ds-dropdown-header-name {
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

.ds-dropdown-header-email {
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
}

/* Mobile sheet: long or searchable lists open full screen, with the
   label and a close button at the top (main.js adds .is-sheet on phones) */
.ds-dropdown-sheet-header {
  display: none;
}

.ds-dropdown.is-sheet .ds-dropdown-menu {
  position: fixed;
  inset: 0;
  z-index: var(--layer-drawer);
  width: auto;
  max-width: none;
  max-height: none;
  padding: var(--space-4);
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.ds-dropdown.is-sheet .ds-dropdown-sheet-header {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.ds-dropdown-sheet-title {
  margin: 0;
  color: var(--color-text);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
}

.ds-dropdown-sheet-header .ds-button--icon {
  --button-height: var(--size-control-compact);   /* 32px */
  --button-icon: var(--space-5);                    /* 20px */
  --button-radius: var(--radius-control-sm);        /* 8px */
  color: var(--color-text-secondary);
}

@media (prefers-reduced-motion: reduce) {
  .ds-dropdown-chevron {
    transition: none;
  }
}

/* Link --------------------------------------------------------------------- */

/* Links go somewhere; buttons do something. Always a real <a href>.
   Inline (default), inside a sentence:
     <a class="ds-link" href="…">accessibility guide</a>
     pink-600, always underlined (1px, 3px below the text).
   Standalone (+ .ds-link--standalone), on its own, e.g. "View all
     components": no underline until hover, and at least 24px tall to click.
   External (+ .ds-link--external): a 16px arrow-up-right icon after the
     text, 4px away, and it opens in a new tab:
     <a class="ds-link ds-link--external" href="…" target="_blank" rel="noopener noreferrer">
       Lucide<span class="ds-visually-hidden"> (opens in a new tab)</span><i data-lucide="arrow-up-right" class="ds-link-icon" aria-hidden="true"></i></a>
   With icon: a standalone link with a .ds-link-icon before the text.
   Subtle (+ .ds-link--subtle): grey-700, underlined. For footers, captions
     and metadata. Inverse (+ .ds-link--inverse): white, underlined, on dark
     backgrounds. Both stay underlined even when standalone.
   Links take the size of the text around them. Icons are 1em, so they
   follow it (14, 16 or 18px).
   Hover: pink-700 and a 2px underline. Pressed: pink-800. Focus (keyboard
   only): a 2px pink-600 ring, 2px out, 4px corners. No disabled state: if the
   destination isn't available, remove the link or show plain text.
   (.is-hover / .is-focus / .is-pressed show the states on the docs pages.) */
.ds-link {
  border-radius: var(--radius-sm);   /* the focus ring's corners */
  color: var(--color-primary);
  text-decoration: underline;
  text-decoration-thickness: var(--border-width-thin);
  text-underline-offset: var(--link-underline-offset);
  cursor: pointer;
}

.ds-link:hover,
.ds-link.is-hover {
  color: var(--color-primary-hover);
  text-decoration-thickness: var(--border-width-thick);
}

.ds-link:active,
.ds-link.is-pressed {
  color: var(--color-primary-pressed);
}

.ds-link:focus-visible,
.ds-link.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

/* Standalone: no underline at rest, underlined on hover. At least 24px
   tall (WCAG 2.5.8), with icons 4px from the text. */
.ds-link--standalone {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: var(--size-target-minimum);
  text-decoration: none;
}

.ds-link--standalone:hover,
.ds-link--standalone.is-hover {
  text-decoration: underline;
  text-decoration-thickness: var(--border-width-thick);
}

/* Icons: the size of the text (1em), never underlined */
.ds-link-icon {
  display: inline-block;
  flex-shrink: 0;
  width: 1em;
  height: 1em;
  vertical-align: text-bottom;
}

/* External inside a sentence: 4px between the text and the icon */
.ds-link--external:not(.ds-link--standalone) .ds-link-icon {
  margin-left: var(--space-1);
}

/* Subtle: grey-700, always underlined */
.ds-link.ds-link--subtle {
  color: var(--color-text-secondary);
  text-decoration-line: underline;
  text-decoration-thickness: var(--border-width-thin);
}

.ds-link.ds-link--subtle:hover,
.ds-link.ds-link--subtle.is-hover,
.ds-link.ds-link--subtle:active,
.ds-link.ds-link--subtle.is-pressed {
  color: var(--color-text);
  text-decoration-thickness: var(--border-width-thick);
}

/* Inverse: white, always underlined, on dark backgrounds (e.g. pink-900).
   The focus ring is white too, so it shows on the dark background. */
.ds-link.ds-link--inverse {
  color: var(--color-text-inverse);
  text-decoration-line: underline;
  text-decoration-thickness: var(--border-width-thin);
}

.ds-link.ds-link--inverse:hover,
.ds-link.ds-link--inverse.is-hover {
  color: var(--color-text-inverse);
  text-decoration-thickness: var(--border-width-thick);
}

.ds-link.ds-link--inverse:active,
.ds-link.ds-link--inverse.is-pressed {
  color: var(--color-text-inverse-pressed);
}

.ds-link.ds-link--inverse:focus-visible,
.ds-link.ds-link--inverse.is-focus {
  outline-color: var(--color-focus-ring-inverse);
}

/* Modal -------------------------------------------------------------------- */

/* A dialog on top of the page that needs a response. Built on the native
   <dialog> element, opened with showModal(): the browser puts it on top,
   makes the page behind inert and closes it with Esc. main.js (section 26)
   does the rest. Markup:
   <dialog class="ds-modal" id="publish-modal" data-modal
           aria-labelledby="publish-modal-title" aria-describedby="publish-modal-message">
     <div class="ds-modal-header">
       …optional <i data-lucide="circle-alert" class="ds-modal-icon">…
       <h2 class="ds-modal-title" id="publish-modal-title">Publish changes?</h2>
       <button class="ds-button ds-button--tertiary ds-button--icon ds-modal-close"
               aria-label="Close" data-modal-close>…x…</button>
     </div>
     <div class="ds-modal-body"><p id="publish-modal-message">…</p></div>
     <div class="ds-modal-footer">Cancel (Ghost), then the primary action</div>
   </dialog>
   Open it from a button with data-modal-open="its id".
   Destructive modals use role="alertdialog" and a filled Danger button.
   Sizes: --sm (400px), MD (560px, the default), --lg (800px).
   At most 80% of the screen tall; then the body scrolls between the fixed
   header and footer. Tablet: at most 90% wide. Mobile: Small and Medium are
   full width with a 16px margin and stacked buttons (primary on top);
   Large fills the screen. */
.ds-modal {
  --modal-width: calc(var(--space-20) * 7);   /* 560px */
  box-sizing: border-box;
  flex-direction: column;
  width: var(--modal-width);
  max-width: calc(100% - var(--space-8));
  max-height: 80dvh;
  margin: auto;
  padding: 0;
  overflow: visible;   /* never hidden: dropdown menus in a form open past the edge */
  border: 0;
  border-radius: var(--radius-container);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
  color: var(--color-text);
  font-family: var(--font-body);
}

.ds-modal--sm { --modal-width: calc(var(--space-20) * 5); }    /* 400px */
.ds-modal--lg { --modal-width: calc(var(--space-20) * 10); }   /* 800px */

/* Open: a column (header, body, footer). Fades and scales in from 96%. */
.ds-modal[open] {
  display: flex;
  animation: ds-modal-in var(--duration-normal) var(--easing-standard);
}

@keyframes ds-modal-in {
  from {
    opacity: 0;
    transform: scale(var(--modal-enter-scale));
  }
}

/* The dimmed page behind: grey-900 at 40% */
.ds-modal::backdrop {
  background: var(--color-overlay);
  animation: ds-modal-fade var(--duration-normal) var(--easing-standard);
}

@keyframes ds-modal-fade {
  from {
    opacity: 0;
  }
}

/* Header: 24px from the top and sides. Optional icon, the title, and the
   close button in the top-right corner. */
.ds-modal-header {
  position: relative;
  display: flex;
  flex-shrink: 0;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-6) var(--space-6) 0;
}

/* Title: 20px semi-bold. Room on the right for the close button.
   (Two classes, so page heading styles never change it.) */
.ds-modal .ds-modal-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding-right: var(--space-8);
  color: var(--color-text);
  font-family: var(--font-heading);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  letter-spacing: normal;
}

/* Icon before the title (e.g. destructive): 24px, the height of the title */
.ds-modal-icon {
  flex-shrink: 0;
  width: var(--space-6);
  height: var(--space-6);
  margin-top: calc((var(--font-size-lg) * var(--line-height-snug) - var(--space-6)) / 2);
}

.ds-modal--destructive .ds-modal-icon {
  color: var(--color-error);
}

/* Step indicator under the title ("Step 2 of 3") */
.ds-modal-step {
  flex-basis: 100%;
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* Close: a 20px grey-700 icon in a 44px click area, centred on the title's
   first line, its icon lined up with the 24px padding */
.ds-modal-close {
  --button-icon: var(--space-5);   /* 20px */
  position: absolute;
  top: calc(var(--space-6) + (var(--font-size-lg) * var(--line-height-snug) - var(--size-control-standard)) / 2);
  right: calc(var(--space-6) - (var(--size-control-standard) - var(--space-5)) / 2);
  color: var(--color-text-secondary);
}

/* Body: 8px below the title, 24px at the sides. Scrolls if the modal is
   taller than 80% of the screen. */
.ds-modal-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-4);
  min-height: 0;
  padding: var(--space-2) var(--space-6) 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.ds-modal-body p {
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line-height);
}

/* No footer (Passive): 24px below the body instead */
.ds-modal-body:last-child {
  padding-bottom: var(--space-6);
}

/* Forms: no scrolling, so dropdown menus can open past the body */
.ds-modal--form .ds-modal-body {
  overflow: visible;
}

/* Footer: 24px below the body and from the edges. Buttons on the right,
   8px apart, primary last. Multi-step: Back on the left. */
.ds-modal-footer {
  display: flex;
  flex-shrink: 0;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-6);
}

.ds-modal-footer .ds-modal-back {
  margin-right: auto;
}

/* Scrolling content: once the body scrolls, a 1px grey-200 line under the
   header and above the footer (main.js adds .is-scrolled-top / -bottom) */
.ds-modal--scrolling .ds-modal-header {
  padding-bottom: var(--space-4);
  border-bottom: var(--border-width-thin) solid transparent;
}

.ds-modal--scrolling .ds-modal-body {
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
}

.ds-modal--scrolling .ds-modal-footer {
  border-top: var(--border-width-thin) solid transparent;
}

.ds-modal.is-scrolled-top .ds-modal-header {
  border-bottom-color: var(--color-border);
}

.ds-modal.is-scrolled-bottom .ds-modal-footer {
  border-top-color: var(--color-border);
}

/* "Discard unsaved changes?" in place of the footer buttons (forms) */
.ds-modal-confirm {
  display: flex;
  flex: 1;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2) var(--space-4);
}

.ds-modal-confirm[hidden],
.ds-modal-footer-actions[hidden],
.ds-modal-step-panel[hidden] {
  display: none;
}

.ds-modal-confirm-text {
  margin: 0 auto 0 0;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

.ds-modal-footer-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* The page behind can't scroll while a modal is open */
html.has-modal-open {
  overflow: hidden;
}

/* Tablet: at most 90% of the screen wide.
   (CSS can't use var() in @media, so the breakpoints are written here.) */
@media (max-width: 1023px) {
  .ds-modal {
    max-width: 90%;
  }
}

/* Mobile: Small and Medium full width with a 16px margin, buttons stacked
   full width with the primary on top. Large fills the screen. */
@media (max-width: 767px) {
  .ds-modal {
    max-width: calc(100% - var(--space-8));
  }

  .ds-modal:not(.ds-modal--lg) {
    width: calc(100% - var(--space-8));
  }

  .ds-modal-footer,
  .ds-modal-footer-actions {
    flex-direction: column-reverse;
    align-items: stretch;
  }

  .ds-modal-footer .ds-modal-back {
    margin-right: 0;
  }

  .ds-modal.ds-modal--lg {
    width: 100%;
    max-width: 100%;
    height: 100%;
    max-height: 100%;
    border-radius: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ds-modal[open],
  .ds-modal::backdrop {
    animation: none;
  }
}

/* Pagination --------------------------------------------------------------- */

/* Moves through a long list one page at a time. Page links go somewhere
   (?page=3), so they're real links. Markup (Numbered):
   <nav class="ds-pagination ds-pagination--numbered" aria-label="Pagination">
     <ul class="ds-pagination-list">
       <li class="ds-pagination-item"><a class="ds-pagination-step" href="?page=5">…chevron-left…<span class="ds-pagination-step-label">Previous</span></a></li>
       <li class="ds-pagination-item ds-pagination-item--page"><a class="ds-pagination-page" href="?page=1" aria-label="Page 1">1</a></li>
       <li class="ds-pagination-item ds-pagination-item--page"><span class="ds-pagination-ellipsis" aria-hidden="true">…</span></li>
       <li class="ds-pagination-item ds-pagination-item--page"><a class="ds-pagination-page" href="?page=6" aria-label="Page 6" aria-current="page">6</a></li>
       …
       <li class="ds-pagination-item ds-pagination-item--status"><span class="ds-pagination-status">Page 6 of 12</span></li>
       <li class="ds-pagination-item"><a class="ds-pagination-step" href="?page=7"><span class="ds-pagination-step-label">Next</span>…chevron-right…</a></li>
     </ul>
   </nav>
   Compact (--compact): Previous, "Page 3 of 12", Next. Simple (--simple):
   Previous and Next only. On the first or last page the step is a link with
   no href and aria-disabled="true" (it stays in place, greyed out).
   Table pagination (.ds-pagination-bar): items per page and the item range
   on the left, "Page 1 of 12" and previous / next icon buttons on the right.
   Sizes match the buttons: MD 44px (default), --sm 32px. Page numbers are
   square with 8px corners, 4px apart; Previous and Next sit 8px away.
   Numbered shows at most 7 slots (5 on tablets; main.js works them out)
   and turns into Compact on phones. */
.ds-pagination {
  --pagination-size: var(--size-control-standard);   /* 44px */
  --pagination-radius: var(--radius-control);      /* 12px */
  --pagination-icon: var(--space-5);                   /* 20px */
  --pagination-text: var(--type-body-size);
  --pagination-line: var(--type-body-line-height);
  font-family: var(--font-body);
  font-size: var(--pagination-text);
  line-height: var(--pagination-line);
}

.ds-pagination--sm {
  --pagination-size: var(--size-control-compact);    /* 32px */
  --pagination-radius: var(--radius-control-sm);      /* 8px */
  --pagination-icon: var(--space-4);                   /* 16px */
  --pagination-text: var(--type-body-small-size);
  --pagination-line: var(--type-body-small-line-height);
}

.ds-pagination-list {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.ds-pagination-item {
  display: flex;
}

/* Page numbers: square, with button corners (8px Small, 12px Medium) */
.ds-pagination-page,
.ds-pagination-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--pagination-size);
  min-height: var(--pagination-size);
  border-radius: var(--pagination-radius);
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}

.ds-pagination-page {
  padding: 0 var(--space-1);
}

/* Previous and Next: icon and word, 8px from the page numbers */
.ds-pagination-step {
  gap: var(--space-1);
  padding: 0 var(--space-3);
  font-weight: var(--font-weight-medium);
}

.ds-pagination-item:first-child {
  margin-right: var(--space-1);
}

.ds-pagination-item:last-child {
  margin-left: var(--space-1);
}

.ds-pagination-step svg,
.ds-pagination-step i[data-lucide] {
  flex-shrink: 0;
  width: var(--pagination-icon);
  height: var(--pagination-icon);
}

/* Hover: grey-100 */
.ds-pagination-page:hover,
.ds-pagination-page.is-hover,
.ds-pagination-step[href]:hover,
.ds-pagination-step.is-hover {
  background: var(--color-hover-bg);
}

/* Current page: the selected style, pink-50 with pink-700 medium text.
   It keeps it on hover. Shown by background and weight, not colour alone. */
.ds-pagination-page[aria-current="page"],
.ds-pagination-page[aria-current="page"]:hover {
  background: var(--color-primary-subtle);
  color: var(--color-selected-text);
  font-weight: var(--font-weight-medium);
}

/* Focus (keyboard only): the 2px pink-600 ring */
.ds-pagination-page:focus-visible,
.ds-pagination-page.is-focus,
.ds-pagination-step:focus-visible,
.ds-pagination-step.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

/* Disabled (Previous on the first page, Next on the last): grey-500, no hover */
.ds-pagination-step[aria-disabled="true"] {
  color: var(--color-text-subtle);
  cursor: not-allowed;
}

/* "…": not clickable */
.ds-pagination-ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--pagination-size);
  min-height: var(--pagination-size);
  color: var(--color-text-secondary);
}

/* "Page 3 of 12" (Compact, and Numbered on phones) */
.ds-pagination-status {
  display: inline-flex;
  align-items: center;
  min-height: var(--pagination-size);
  padding: 0 var(--space-2);
  color: var(--color-text);
  white-space: nowrap;
}

.ds-pagination--numbered .ds-pagination-item--status {
  display: none;
}

/* Table pagination: a full-width bar under a table, 16px padding and a
   1px grey-200 line on top */
.ds-pagination-bar {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3) var(--space-6);
  padding: var(--space-4);
  border-top: var(--border-width-thin) solid var(--color-border);
  color: var(--color-text-secondary);
  font-family: var(--font-body);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-pagination-bar-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-6);
}

.ds-pagination-control {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* The dropdowns in the bar: Small, just wide enough for their numbers */
.ds-pagination-bar .ds-dropdown {
  width: var(--space-20);   /* 80px */
}

.ds-pagination-steps {
  display: flex;
  gap: var(--space-1);
}

/* Phones: Numbered becomes Compact (page numbers hidden, "Page 3 of 12"
   shown), and the table bar drops "Items per page" */
@media (max-width: 767px) {
  .ds-pagination--numbered .ds-pagination-item--page {
    display: none;
  }

  .ds-pagination--numbered .ds-pagination-item--status {
    display: flex;
  }

  .ds-pagination-bar .ds-pagination-per-page {
    display: none;
  }
}

/* Popover ------------------------------------------------------------------ */

/* A small panel that opens next to its trigger, without blocking the page.
   It uses the native popover attribute (popover="manual"), so it sits on
   top of everything and is never cut off by its container. main.js
   (section 28) positions it, flips it and shifts it to stay on screen.
   <button type="button" class="ds-button ds-button--secondary ds-button--sm ds-popover-trigger"
           aria-expanded="false" aria-controls="tip" data-popover-trigger="tip"
           data-popover-placement="bottom">What's new</button>
   <div class="ds-popover" id="tip" popover="manual" role="dialog" aria-labelledby="tip-title">
     <span class="ds-popover-caret" aria-hidden="true"></span>
     …optional <div class="ds-popover-media"><img …></div> (16:9)…
     <div class="ds-popover-body">
       <div class="ds-popover-header">
         <p class="ds-popover-title" id="tip-title">New: saved views</p>
         …optional close: ds-button ds-button--tertiary ds-button--icon ds-popover-close, aria-label="Close"…
       </div>
       <p class="ds-popover-message">…</p>
       <div class="ds-popover-actions">…a link, or up to 2 buttons (Primary + Ghost)…</div>
     </div>
   </div>
   Placements: top | right | bottom (default) | left, each with -start or
   -end. Hover popovers (data-popover-hover on the trigger) are linked with
   aria-describedby and hold short text only. --wide for Rich popovers.
   --no-caret: no arrow, still 8px from the trigger.
   Triggers are Secondary Small buttons; while open they keep that look,
   with aria-expanded="true". White, 1px grey-200 border, --shadow-md. */
.ds-popover {
  --popover-gap: var(--space-2);          /* 8px from the trigger to the caret tip */
  --popover-caret: var(--space-2);        /* the caret sticks out 8px */
  --caret-offset: 50%;                    /* where the caret sits along the edge (main.js) */
  position: fixed;
  inset: auto;
  box-sizing: border-box;
  min-width: calc(var(--space-20) * 2);   /* 160px */
  max-width: min(calc(var(--space-20) * 4), calc(100vw - var(--space-8)));   /* 320px */
  margin: 0;
  padding: 0;
  overflow: visible;
  border: var(--border-width-thin) solid var(--color-border);   /* with the shadow, separates it from the page */
  border-radius: var(--radius-container);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-popover--wide {
  max-width: min(calc(var(--space-24) * 5), calc(100vw - var(--space-8)));   /* 480px */
}

/* Fades in over 150ms (none with reduced motion) */
.ds-popover:popover-open {
  animation: ds-popover-in var(--duration-fade) var(--easing-standard);
}

@keyframes ds-popover-in {
  from {
    opacity: 0;
  }
}

/* Image (Rich): 16:9, edge to edge at the top, following the corners */
.ds-popover-media {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: calc(var(--radius-container) - var(--border-width-thin)) calc(var(--radius-container) - var(--border-width-thin)) 0 0;
  background: var(--color-surface-subtle);
}

.ds-popover-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Body: 16px padding. Title, 4px, message, 12px, actions. */
.ds-popover-body {
  display: flex;
  flex-direction: column;
  padding: var(--space-4);
}

.ds-popover-header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
}

.ds-popover .ds-popover-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  color: var(--color-text);
  font-family: var(--font-heading);
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-line-height);
}

/* The message: 4px below the title */
.ds-popover .ds-popover-message {
  margin: 0;
  color: var(--color-text-secondary);
}

.ds-popover-header + .ds-popover-message {
  margin-top: var(--space-1);
}

/* Actions: 12px below the message, 8px apart */
.ds-popover-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

/* Close: a 20px grey-700 icon in a 32 × 32 click area, no border,
   grey-100 on hover (the popover is white). Pulled up and right so its
   icon lines up with the padding. */
.ds-popover-header .ds-popover-close {
  --button-height: var(--size-control-compact);   /* 32px */
  --button-icon: var(--space-5);                    /* 20px */
  flex-shrink: 0;
  margin: calc(var(--space-1) * -1) calc((var(--size-control-compact) - var(--space-5)) / -2) calc(var(--space-1) * -1) 0;
  --button-radius: var(--radius-control-sm);        /* 8px */
  color: var(--color-text-secondary);
}

.ds-popover-header .ds-popover-close:hover:not(:disabled),
.ds-popover-header .ds-popover-close.is-hover {
  color: var(--color-text);
}

/* The caret: an 8px arrow pointing at the trigger, with the same grey-200
   border so it blends in. Its tip is 8px from the trigger.
   [data-side] (set by main.js) says which side of the trigger it's on. */
.ds-popover-caret {
  position: absolute;
  width: 0;
  height: 0;
}

.ds-popover-caret::before,
.ds-popover-caret::after {
  content: "";
  position: absolute;
  border: var(--popover-caret) solid transparent;
}

/* Below the trigger: the caret on top, pointing up */
.ds-popover[data-side="bottom"] .ds-popover-caret {
  top: calc(var(--border-width-thin) * -1);   /* on the outer edge of the border */
  left: var(--caret-offset);
}

.ds-popover[data-side="bottom"] .ds-popover-caret::before {
  bottom: 0;
  left: calc(var(--popover-caret) * -1);
  border-bottom-color: var(--color-border);
}

.ds-popover[data-side="bottom"] .ds-popover-caret::after {
  bottom: calc(var(--border-width-thin) * -1);
  left: calc(var(--popover-caret) * -1);
  border-bottom-color: var(--color-surface);
}

/* Above the trigger: the caret at the bottom, pointing down */
.ds-popover[data-side="top"] .ds-popover-caret {
  bottom: calc(var(--border-width-thin) * -1);
  left: var(--caret-offset);
}

.ds-popover[data-side="top"] .ds-popover-caret::before {
  top: 0;
  left: calc(var(--popover-caret) * -1);
  border-top-color: var(--color-border);
}

.ds-popover[data-side="top"] .ds-popover-caret::after {
  top: calc(var(--border-width-thin) * -1);
  left: calc(var(--popover-caret) * -1);
  border-top-color: var(--color-surface);
}

/* Right of the trigger: the caret on the left, pointing left */
.ds-popover[data-side="right"] .ds-popover-caret {
  top: var(--caret-offset);
  left: calc(var(--border-width-thin) * -1);
}

.ds-popover[data-side="right"] .ds-popover-caret::before {
  top: calc(var(--popover-caret) * -1);
  right: 0;
  border-right-color: var(--color-border);
}

.ds-popover[data-side="right"] .ds-popover-caret::after {
  top: calc(var(--popover-caret) * -1);
  right: calc(var(--border-width-thin) * -1);
  border-right-color: var(--color-surface);
}

/* Left of the trigger: the caret on the right, pointing right */
.ds-popover[data-side="left"] .ds-popover-caret {
  top: var(--caret-offset);
  right: calc(var(--border-width-thin) * -1);
}

.ds-popover[data-side="left"] .ds-popover-caret::before {
  top: calc(var(--popover-caret) * -1);
  left: 0;
  border-left-color: var(--color-border);
}

.ds-popover[data-side="left"] .ds-popover-caret::after {
  top: calc(var(--popover-caret) * -1);
  left: calc(var(--border-width-thin) * -1);
  border-left-color: var(--color-surface);
}

.ds-popover--no-caret .ds-popover-caret {
  display: none;
}

/* Mobile bottom sheet (Rich popovers on phones): full width at the bottom,
   no caret (main.js adds .is-sheet) */
.ds-popover.is-sheet {
  inset: auto 0 0;
  width: 100%;
  max-width: none;
  border-radius: var(--radius-container) var(--radius-container) 0 0;
}

.ds-popover.is-sheet .ds-popover-caret {
  display: none;
}

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

/* Progress ----------------------------------------------------------------- */
/* Two forms, sharing the same colours and states:

   Progress bar (.ds-progress): label and value on top, the bar, helper text
   below.
     <div class="ds-progress" style="--progress-value: 25">
       <span class="ds-progress-label" id="upload-label">Uploading files…</span>
       <span class="ds-progress-status">
         <i data-lucide="circle-check" class="ds-progress-icon ds-progress-icon--success" aria-hidden="true"></i>
         <i data-lucide="circle-x" class="ds-progress-icon ds-progress-icon--error" aria-hidden="true"></i>
         <span class="ds-progress-value">25%</span>
       </span>
       <div class="ds-progress-track" role="progressbar" aria-labelledby="upload-label"
            aria-valuemin="0" aria-valuemax="100" aria-valuenow="25" aria-valuetext="3 of 12 files">
         <div class="ds-progress-fill"></div>
       </div>
       <p class="ds-progress-helper">Uploading 3 of 12 files</p>
     </div>
   Modifiers: --sm (4px bar; MD 8px is the default), --inline (label, bar
   and value on one line, no helper), --indeterminate (a moving segment, no
   value), --success, --error.

   Progress circle (.ds-progress-circle): an SVG ring, with an optional label.
     <div class="ds-progress-circle ds-progress-circle--lg" style="--progress-value: 25">
       <div class="ds-progress-ring" role="progressbar" aria-labelledby="…"
            aria-valuemin="0" aria-valuemax="100" aria-valuenow="25">
         <svg class="ds-progress-ring-svg" aria-hidden="true">
           <circle class="ds-progress-ring-track" cx="50%" cy="50%" pathLength="100"></circle>
           <circle class="ds-progress-ring-fill" cx="50%" cy="50%" pathLength="100"></circle>
         </svg>
         <span class="ds-progress-ring-value">25%</span>   (Large only; plus the two status icons)
       </div>
       <span class="ds-progress-circle-label">Uploading…</span>   (optional)
     </div>
   Sizes: --sm 16px (2px ring), MD 24px (3px, default), --lg 64px (6px, value
   in the centre). Modifiers: --inline (label beside it), --spinner (a turning
   arc, no value), --success, --error.

   --progress-value is a number from 0 to 100. main.js (section 29) keeps it,
   the text and the ARIA values in step. */

.ds-progress,
.ds-progress-circle {
  --progress-value: 0;
  --progress-fill: var(--color-primary);           /* pink-600 */
  --progress-track: var(--color-border);           /* grey-200 */
  --progress-loop: var(--duration-feedback);       /* 1.5s: one loop of the moving parts */
  font-family: var(--font-body);
}

/* Success and error: green or red, with an icon and text (never colour alone) */
.ds-progress--success,
.ds-progress-circle--success {
  --progress-fill: var(--color-success);
}

.ds-progress--error,
.ds-progress-circle--error {
  --progress-fill: var(--color-error);
}

/* The status icons are always in the markup, and shown only in their state */
.ds-progress-icon {
  display: none;
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
}

.ds-progress--success .ds-progress-icon--success,
.ds-progress--error .ds-progress-icon--error,
.ds-progress-circle--success .ds-progress-icon--success,
.ds-progress-circle--error .ds-progress-icon--error {
  display: inline-block;
}

/* ---------- Progress bar ---------- */
/* Label and value share the top line; the bar and the helper text sit
   below, 8px apart. Fills its container, at most 640px. */
.ds-progress {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "label  status"
    "track  track"
    "helper helper";
  column-gap: var(--space-4);
  row-gap: var(--space-2);
  align-items: center;
  width: 100%;
  max-width: calc(var(--space-20) * 8);   /* 640px */
}

.ds-progress-label {
  grid-area: label;
  min-width: 0;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

/* The value (or "Done"), with the status icon before it */
.ds-progress-status {
  grid-area: status;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  font-variant-numeric: tabular-nums;   /* the numbers don't jiggle as they change */
  white-space: nowrap;
}

/* The track (role="progressbar"): grey-200, fully rounded ends */
.ds-progress-track {
  position: relative;
  grid-area: track;
  height: var(--space-2);   /* 8px (MD) */
  overflow: hidden;         /* keeps the fill inside the rounded ends */
  border-radius: var(--radius-full);
  background: var(--progress-track);
}

/* The fill: grows smoothly (200ms) to the value */
.ds-progress-fill {
  width: calc(var(--progress-value) * 1%);
  height: 100%;
  border-radius: inherit;
  background: var(--progress-fill);
  transition: width var(--duration-normal) var(--easing-standard);
}

.ds-progress-helper {
  grid-area: helper;
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-progress--success .ds-progress-status {
  color: var(--color-success);
}

.ds-progress--error .ds-progress-status,
.ds-progress--error .ds-progress-helper {
  color: var(--color-error);
}

/* Small: a 4px bar */
.ds-progress--sm .ds-progress-track {
  height: var(--space-1);
}

/* Inline: label, bar and value on one line, 12px apart. No helper text. */
.ds-progress--inline {
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "label track status";
  column-gap: var(--space-3);
}

.ds-progress--inline .ds-progress-helper {
  display: none;
}

/* Indeterminate: a 40% segment slides across, every 1.5s. No value. */
.ds-progress--indeterminate .ds-progress-status {
  display: none;
}

.ds-progress--indeterminate .ds-progress-fill {
  width: 40%;
  transition: none;
  animation: ds-progress-slide var(--progress-loop) var(--easing-standard) infinite;
}

@keyframes ds-progress-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(250%); }
}

/* ---------- Progress circle ---------- */
.ds-progress-circle {
  --ring-size: var(--space-6);                               /* 24px (MD) */
  --ring-stroke: calc(var(--border-width-thin) * 3);         /* 3px */
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  vertical-align: middle;
}

.ds-progress-circle--sm {
  --ring-size: var(--space-4);                               /* 16px */
  --ring-stroke: var(--border-width-thick);                  /* 2px */
}

.ds-progress-circle--lg {
  --ring-size: var(--space-16);                              /* 64px */
  --ring-stroke: calc(var(--border-width-thick) * 3);        /* 6px */
}

/* The ring (role="progressbar"): the value or status icon sits in its middle */
.ds-progress-ring {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--ring-size);
  height: var(--ring-size);
}

/* Turned a quarter, so the fill starts at the top and goes clockwise */
.ds-progress-ring-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  transform: rotate(-90deg);
}

.ds-progress-ring-svg circle {
  r: calc(50% - var(--ring-stroke) / 2);   /* the stroke sits just inside the edge */
  fill: none;
  stroke-width: var(--ring-stroke);
}

.ds-progress-ring-track {
  stroke: var(--progress-track);
}

/* pathLength="100" makes the ring 100 units round, so the value is simply
   the length of the dash */
.ds-progress-ring-fill {
  stroke: var(--progress-fill);
  stroke-dasharray: 100 100;
  stroke-dashoffset: calc(100 - var(--progress-value));
  transition: stroke-dashoffset var(--duration-normal) var(--easing-standard);
}

/* The value in the middle of a Large ring */
.ds-progress-ring-value {
  position: relative;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
  font-variant-numeric: tabular-nums;
}

/* Large rings: a 24px status icon replaces the value */
.ds-progress-ring .ds-progress-icon {
  position: relative;
  width: var(--space-6);
  height: var(--space-6);
}

.ds-progress-circle--success .ds-progress-ring-value,
.ds-progress-circle--error .ds-progress-ring-value {
  display: none;
}

.ds-progress-circle--success .ds-progress-icon--success {
  color: var(--color-success);
}

.ds-progress-circle--error .ds-progress-icon--error {
  color: var(--color-error);
}

/* Optional label below the ring */
.ds-progress-circle-label {
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  text-align: center;
}

.ds-progress-circle--success .ds-progress-circle-label {
  color: var(--color-success);
}

.ds-progress-circle--error .ds-progress-circle-label {
  color: var(--color-error);
}

/* Inline: the label beside the ring, 8px away, like "Validating… 25%" */
.ds-progress-circle--inline {
  flex-direction: row;
}

.ds-progress-circle--inline .ds-progress-circle-label {
  color: var(--color-text);
  text-align: left;
  font-variant-numeric: tabular-nums;
}

/* Spinner: a quarter of the ring turns once every 1.5s. No value. */
.ds-progress-circle--spinner .ds-progress-ring-fill {
  stroke-dashoffset: 75;
  transition: none;
}

.ds-progress-circle--spinner .ds-progress-ring-svg {
  animation: ds-progress-spin var(--progress-loop) linear infinite;
}

.ds-progress-circle--spinner .ds-progress-ring-value {
  display: none;
}

@keyframes ds-progress-spin {
  from { transform: rotate(-90deg); }
  to   { transform: rotate(270deg); }
}

/* Reduced motion: no sliding or turning. The fill changes straight away,
   and the moving parts gently fade in and out instead. */
@keyframes ds-progress-fade {
  from { opacity: 1; }
  to   { opacity: var(--opacity-decorative); }
}

@media (prefers-reduced-motion: reduce) {
  .ds-progress-fill,
  .ds-progress-ring-fill {
    transition: none;
  }

  .ds-progress--indeterminate .ds-progress-fill {
    width: 100%;
    transform: none;
    animation: ds-progress-fade calc(var(--progress-loop) * 2) var(--easing-standard) infinite alternate;
  }

  .ds-progress-circle--spinner .ds-progress-ring-svg {
    animation: ds-progress-fade calc(var(--progress-loop) * 2) var(--easing-standard) infinite alternate;
  }
}


/* Skeleton ----------------------------------------------------------------- */
/* Grey placeholders in the shape of the content that's loading.

   Shapes (.ds-skeleton + one of):
     --text     a 12px line, full width (add --w80, --w60, --w40 or --w25)
     --heading  a 20px line, 40% wide
     --avatar   a 44px circle (MD avatar); --avatar-sm 32px (e.g. card authors)
     --image    a 16:9 block with a grey image icon in the middle
     --button   a Medium button (44px tall); --button-sm 32px
     --pill     a 24px tag
   Lines and blocks have 4px corners, avatars are circles.
   Groups: .ds-skeleton-lines (lines 8px apart), .ds-skeleton-field (label
   line + 44px field), and the patterns .ds-skeleton-card, -list, -table
   and -article, which match the real Weave components.

   Shimmer (default): a lighter band slides across every 1.5s.
   .ds-skeleton-static on any parent: no movement. .ds-skeleton-pulse: a
   gentle fade between grey-100 and grey-200 (also what reduced motion gets).
   Real content that replaces a skeleton gets .ds-skeleton-loaded: it fades
   in over 150ms.

   Accessibility: the loading area has aria-busy="true" (false once loaded)
   and a visually hidden "Loading…"; the shapes themselves are aria-hidden.
   main.js (section 30) swaps them: setSkeletonLoading(region, true|false). */

.ds-skeleton {
  position: relative;
  display: block;
  overflow: hidden;   /* keeps the shimmer band inside the shape */
  border-radius: var(--radius-sm);
  background: var(--skeleton-base);
}

/* The shimmer: a lighter band sliding left to right */
.ds-skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, var(--skeleton-base), var(--skeleton-highlight), var(--skeleton-base));
  transform: translateX(-100%);
  animation: ds-skeleton-shimmer var(--duration-feedback) var(--easing-standard) infinite;   /* 1.5s */
}

@keyframes ds-skeleton-shimmer {
  to { transform: translateX(100%); }
}

/* ---------- Shapes ---------- */
.ds-skeleton--text {
  width: 100%;
  height: var(--space-3);   /* 12px: body text */
}

.ds-skeleton--heading {
  width: 40%;
  height: var(--space-5);   /* 20px: titles */
}

/* Widths for lines: the last line of a paragraph is the short one (60%) */
.ds-skeleton--w80 { width: 80%; }
.ds-skeleton--w60 { width: 60%; }
.ds-skeleton--w40 { width: 40%; }
.ds-skeleton--w25 { width: 25%; }

.ds-skeleton--avatar {
  flex-shrink: 0;
  width: var(--size-control-standard);    /* 44px, the MD avatar */
  height: var(--size-control-standard);
  border-radius: var(--radius-full);
}

.ds-skeleton--avatar-sm {
  width: var(--size-control-compact);     /* 32px, like card authors */
  height: var(--size-control-compact);
}

.ds-skeleton--image {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 16 / 9;
  color: var(--skeleton-icon);
}

.ds-skeleton--image svg,
.ds-skeleton--image i[data-lucide] {
  width: var(--space-8);
  height: var(--space-8);
}

/* A button: as tall as a Medium (44px) or Small (32px) button. Put the
   real button's label inside in a .ds-skeleton-sizer: it's invisible, but
   with the button's own padding and text size it makes the block exactly
   as wide as the real button. Without one: 96px (MD) or 80px (SM). */
.ds-skeleton--button {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  width: var(--space-24);                 /* 96px */
  height: var(--size-control-standard);   /* 44px, a Medium button */
  padding: 0 var(--space-4);
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--type-body-line-height);
  white-space: nowrap;
}

.ds-skeleton--button-sm {
  width: var(--space-20);                 /* 80px */
  height: var(--size-control-compact);    /* 32px, a Small button */
  padding: 0 var(--space-3);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-skeleton--button:has(.ds-skeleton-sizer) {
  width: auto;
}

.ds-skeleton-sizer {
  visibility: hidden;
}

/* A tag: a 24px pill */
.ds-skeleton--pill {
  flex-shrink: 0;
  width: var(--space-16);
  height: var(--space-6);
  border-radius: var(--radius-full);
}

/* ---------- Groups ---------- */
/* Lines of text. Each 12px line sits in a 20px slot (the line height of
   body text), so lines are 8px apart and the skeleton is exactly as tall as
   the text it stands for: nothing moves when the content arrives. */
.ds-skeleton-lines {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.ds-skeleton-lines > .ds-skeleton--text,
.ds-skeleton-table-row > .ds-skeleton--text {
  margin-block: var(--space-1);
}

/* An author's date or read time (12px text): a 16px slot */
.ds-skeleton-row > .ds-skeleton-lines > .ds-skeleton--text:last-child {
  margin-block: calc(var(--space-1) / 2);
}

/* Card titles: 20px lines in 26px slots (20px text at 1.3 line height) */
.ds-skeleton-title > .ds-skeleton--heading {
  margin-block: calc(var(--space-1) * 0.75);
}

/* Shapes in a row (tags, an avatar and its lines) */
.ds-skeleton-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* A form field: a short label line, 8px above a 44px field */
.ds-skeleton-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.ds-skeleton-field .ds-skeleton--text {
  width: 30%;
}

.ds-skeleton--field {
  width: 100%;
  height: var(--size-control-standard);
}

/* ---------- Patterns ---------- */
/* Card: like .ds-card. The image runs edge to edge and its top corners
   follow the card's 12px; then 16px padding with the tags, title, two text
   lines and the author (32px avatar), spaced like the real card. */
.ds-skeleton-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
}

.ds-skeleton-card > .ds-skeleton--image {
  border-radius: 0;
}

.ds-skeleton-card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);       /* 12px: tags, title and text */
  padding: var(--space-4);   /* 16px */
}

.ds-skeleton-card-body > .ds-skeleton-row:last-child {
  margin-top: var(--space-1);   /* the footer sits 16px below the text */
}

/* List: 44px avatars with two lines beside them, 16px between rows */
.ds-skeleton-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.ds-skeleton-list-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.ds-skeleton-list-item > .ds-skeleton-lines,
.ds-skeleton-row > .ds-skeleton-lines {
  flex: 1;
}

/* Table: a header row and body rows (44px each), columns of different widths */
.ds-skeleton-table {
  overflow: hidden;
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
}

.ds-skeleton-table-row {
  display: grid;
  grid-template-columns: 3fr 2fr 2fr 1fr;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);   /* 12px 16px: 44px rows */
  border-bottom: var(--border-width-thin) solid var(--color-border);
}

.ds-skeleton-table-row:last-child {
  border-bottom: 0;
}

.ds-skeleton-table-row--head {
  background: var(--color-surface-muted);
}

/* Article: a 20px heading, 16px to the image, 24px to the paragraph */
.ds-skeleton-article {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.ds-skeleton-article > .ds-skeleton-lines {
  margin-top: var(--space-2);
}

.ds-skeleton-article .ds-skeleton--heading {
  width: 60%;
}

/* ---------- States ---------- */
/* Static: no movement */
.ds-skeleton-static .ds-skeleton::after {
  display: none;
}

/* Pulse: no movement, a gentle fade between grey-100 and grey-200 */
.ds-skeleton-pulse .ds-skeleton {
  animation: ds-skeleton-pulse var(--duration-feedback) var(--easing-standard) infinite alternate;
}

.ds-skeleton-pulse .ds-skeleton::after {
  display: none;
}

@keyframes ds-skeleton-pulse {
  from { background-color: var(--skeleton-base); }
  to   { background-color: var(--skeleton-pulse); }
}

/* Loaded: the real content fades in over 150ms */
.ds-skeleton-loaded {
  animation: ds-skeleton-fade-in var(--duration-fade) var(--easing-standard);
}

@keyframes ds-skeleton-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Reduced motion: the shimmer becomes the gentle fade (static stays still) */
@media (prefers-reduced-motion: reduce) {
  .ds-skeleton::after {
    display: none;
  }

  .ds-skeleton {
    animation: ds-skeleton-pulse calc(var(--duration-feedback) * 2) var(--easing-standard) infinite alternate;
  }

  .ds-skeleton-static .ds-skeleton {
    animation: none;
  }

  .ds-skeleton-loaded {
    animation: none;
  }
}


/* Spinner ------------------------------------------------------------------ */
/* A turning ring for short waits when you don't know how long it'll take.
     <span class="ds-spinner" role="status">
       <svg class="ds-spinner-ring" aria-hidden="true">
         <circle class="ds-spinner-track" cx="50%" cy="50%"></circle>
         <circle class="ds-spinner-indicator" cx="50%" cy="50%" pathLength="100"></circle>
       </svg>
       <span class="ds-spinner-label">Loading results…</span>
       (or, with no visible label: <span class="ds-spinner-label ds-spinner-label--hidden">Loading</span>)
     </span>
   Sizes: --sm 16px (2px ring, inline and in buttons), MD 24px (3px, the
   default, next to text), --lg 32px (3px, sections and panels).
   Colours: pink-600 indicator on a grey-200 track; --neutral grey-600;
   --inverse white on dark backgrounds (automatic inside Primary buttons).
   --stacked puts the label below (the Section variant). The label is 8px
   from the ring.
   In a button: add <span class="ds-spinner ds-spinner--sm" aria-hidden="true">
   (no role: the button has aria-busy) before the label; main.js
   setButtonLoading() swaps the icon for it and keeps the button's width.
   One turn every 0.8s (--duration-spin); reduced motion: a slow pulse. */

.ds-spinner {
  --spinner-size: var(--space-6);                        /* 24px (MD) */
  --spinner-stroke: calc(var(--border-width-thin) * 3);  /* 3px */
  --spinner-indicator: var(--color-primary);             /* pink-600 */
  --spinner-track: var(--color-border);                  /* grey-200 */
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);   /* 8px to the label */
  color: var(--color-text-muted);
  font-family: var(--font-body);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  vertical-align: middle;
}

.ds-spinner--sm {
  --spinner-size: var(--space-4);                        /* 16px */
  --spinner-stroke: var(--border-width-thick);           /* 2px */
}

.ds-spinner--lg {
  --spinner-size: var(--space-8);                        /* 32px */
}

/* Section: the label below the ring, both centred */
.ds-spinner--stacked {
  flex-direction: column;
  text-align: center;
}

.ds-spinner--neutral {
  --spinner-indicator: var(--color-text-muted);          /* grey-600 */
}

/* White on dark backgrounds, and in Primary and filled destructive buttons */
.ds-spinner--inverse,
.ds-button--primary .ds-spinner,
.ds-button--destructive-filled .ds-spinner {
  --spinner-indicator: var(--color-on-primary);
  --spinner-track: color-mix(in srgb, var(--color-on-primary) 30%, transparent);
}

.ds-spinner--inverse {
  color: var(--color-on-primary);
}

/* The ring: turned so the indicator starts at the top, then spinning */
.ds-spinner-ring {
  flex-shrink: 0;
  width: var(--spinner-size);
  height: var(--spinner-size);
  overflow: visible;
  transform: rotate(-90deg);
  animation: ds-spinner-spin var(--duration-spin) linear infinite;
}

.ds-spinner-ring circle {
  r: calc(50% - var(--spinner-stroke) / 2);   /* the stroke sits just inside the edge */
  fill: none;
  stroke-width: var(--spinner-stroke);
}

.ds-spinner-track {
  stroke: var(--spinner-track);
}

/* A quarter of the ring (pathLength="100") */
.ds-spinner-indicator {
  stroke: var(--spinner-indicator);
  stroke-dasharray: 25 75;
  stroke-linecap: round;
}

@keyframes ds-spinner-spin {
  from { transform: rotate(-90deg); }
  to   { transform: rotate(270deg); }
}

/* No visible label: read by screen readers only */
.ds-spinner-label--hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* In a button: the spinner takes the icon's place while loading */
.ds-button > .ds-spinner {
  display: none;
}

.ds-button.is-loading > .ds-spinner {
  display: inline-flex;
}

.ds-button.is-loading > svg.lucide:not(.ds-button-spinner),   /* not the text-only loading spinner */
.ds-button.is-loading > i[data-lucide] {
  display: none;
}

/* …and the label stays visible ("Saving…"). The Button page's text-only
   loading hides it instead, with a spinner on top. */
.ds-button.is-loading:has(> .ds-spinner) .ds-button-label {
  opacity: 1;
}

/* Reduced motion: no turning, a slow pulse instead (also .ds-spinner--pulse) */
@keyframes ds-spinner-pulse {
  from { opacity: 1; }
  to   { opacity: var(--opacity-decorative); }
}

.ds-spinner--pulse .ds-spinner-ring {
  animation: ds-spinner-pulse var(--duration-feedback) var(--easing-standard) infinite alternate;
}

@media (prefers-reduced-motion: reduce) {
  .ds-spinner-ring {
    animation: ds-spinner-pulse var(--duration-feedback) var(--easing-standard) infinite alternate;
  }
}


/* Table -------------------------------------------------------------------- */

/* Data in rows and columns. A real <table> inside a bordered box, with
   optional bars above and below it. Markup (top to bottom; everything but
   the table is optional):
   <div class="ds-table-container">                  1px grey-200 border, 12px corners
     <div class="ds-table-toolbar">…</div>            search left, buttons right
     <div class="ds-table-batchbar" hidden>…</div>    replaces the toolbar while rows are selected
     <div class="ds-table-filters">…</div>            active filters as removable tags + Clear all
     <div class="ds-table-scroll">                    scrolls sideways when the table is wider
       <table class="ds-table">
         <caption class="ds-visually-hidden">Projects</caption>
         <thead><tr><th scope="col">Name</th>…</tr></thead>
         <tbody><tr><td>…</td>…</tr></tbody>
       </table>
     </div>
     <div class="ds-pagination-bar">…</div>           the Pagination component's table bar
   </div>
   Rows: 44px (MD, default), --compact 32px, --relaxed 56px. In a table a
   cell's height is a minimum, so rows still grow if text wraps. Header
   row: always 44px, grey-50, 14px medium. Cells: 16px side padding, 14px
   text. Columns: .ds-table-cell--number (right-aligned, digits line up),
   --checkbox, --action and --expand (48px wide each).
   Rows: hover grey-50, selected (.is-selected) pink-50 with its checkbox
   ticked, focus (keyboard only) a 2px pink ring inside the row, disabled
   (.is-disabled + aria-disabled) grey-400 text.
   Sortable headers: <th aria-sort> holding a .ds-table-sort button (4px
   corners, focus ring 2px outside it) with all three icons (--none,
   --asc, --desc); CSS shows the right one.
   Groups: one <tbody class="ds-table-group"> per group, starting with a
   .ds-table-group-row (a button with aria-expanded); .is-collapsed hides
   the rest of the group. Expandable rows: a .ds-table-expand button
   (aria-expanded, aria-controls) and a .ds-table-details row below.
   Empty, no results and error: one .ds-table-empty-row spanning every
   column. Loading: rows of Skeleton text shapes. */

.ds-table-container {
  --table-radius: calc(var(--radius-container) - var(--border-width-thin));   /* corners just inside the border */
  min-width: 0;
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: var(--font-body);
}

/* The table area scrolls sideways when the columns don't fit. Its corners
   follow the box's when it's at the top or bottom, so header and selected
   row colours don't poke out past the round corners. */
.ds-table-scroll {
  position: relative;   /* keeps hidden labels and menus inside it */
  container-type: inline-size;   /* lets the empty state fit the visible width on phones */
  max-width: 100%;
  overflow: auto;
}

.ds-table-container > .ds-table-scroll:first-child {
  border-top-left-radius: var(--table-radius);
  border-top-right-radius: var(--table-radius);
}

.ds-table-container > .ds-table-scroll:last-child {
  border-bottom-left-radius: var(--table-radius);
  border-bottom-right-radius: var(--table-radius);
}

/* Keyboard users can focus a scrolling table to move it with the arrow keys */
/* (Drawn on the box's edge, not inside it, so the fixed first column
   can't cover it) */
.ds-table-scroll:focus-visible,
.ds-table-scroll.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: 0;
}

/* ---------- The table ---------- */
.ds-table {
  --table-row-height: var(--size-control-standard);   /* 44px (default) */
  --table-cell-pad-y: var(--space-2);                  /* used only when text wraps */
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* Row density */
.ds-table--compact {
  --table-row-height: var(--size-control-compact);     /* 32px */
  --table-cell-pad-y: calc(var(--space-1) / 2);        /* 2px, so a 24px tag fits */
}

.ds-table--relaxed {
  --table-row-height: calc(var(--space-12) + var(--space-2));   /* 56px */
  --table-cell-pad-y: var(--space-3);
}

.ds-table th,
.ds-table td {
  box-sizing: border-box;
  height: var(--table-row-height);   /* a minimum in tables: rows still grow */
  padding: var(--table-cell-pad-y) var(--space-4);
  border-bottom: var(--border-width-thin) solid var(--color-border);
  text-align: left;
  vertical-align: middle;
}

/* Header row: 44px whatever the density, grey-50, 14px medium. Headers
   never wrap: keep them short. */
.ds-table thead th {
  height: var(--size-control-standard);
  padding-top: 0;
  padding-bottom: 0;
  background: var(--color-surface-muted);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

/* Row headers (e.g. the Name column in a wide table) look like other cells */
.ds-table tbody th {
  font-weight: inherit;
}

/* No line under the last row: the box's border (or the pagination bar's
   top line) is there instead */
.ds-table > tbody:last-of-type > tr:last-child > * {
  border-bottom: 0;
}

/* ---------- Columns ---------- */

/* Numbers and money: right-aligned, and every digit the same width, so
   values line up and are easy to compare. Their header lines up too. */
.ds-table .ds-table-cell--number {
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Checkbox, action and expand columns: 48px wide */
.ds-table .ds-table-cell--checkbox,
.ds-table .ds-table-cell--action,
.ds-table .ds-table-cell--expand {
  width: var(--space-12);
  white-space: nowrap;
}

/* No padding above or below: the row height centres the checkbox or
   button (a 32px button plus padding would make the row taller) */
.ds-table .ds-table-cell--checkbox,
.ds-table .ds-table-cell--action,
.ds-table .ds-table-cell--expand {
  padding-top: 0;
  padding-bottom: 0;
}

/* 16px box with 16px either side */
.ds-table .ds-table-cell--checkbox {
  padding-right: 0;
}

.ds-table .ds-table-cell--checkbox .ds-checkbox {
  vertical-align: middle;
}

/* A 32px icon button with 8px either side */
.ds-table .ds-table-cell--action,
.ds-table .ds-table-cell--expand {
  padding-right: var(--space-2);
  padding-left: var(--space-2);
}

.ds-table .ds-table-cell--action {
  text-align: right;
}

/* Compact rows are 32px including their 1px line, so the Small icon
   buttons in them (row menus, expand) give up that 1px to fit */
.ds-table--compact .ds-table-cell--action .ds-button,
.ds-table--compact .ds-table-cell--expand .ds-button {
  --button-height: calc(var(--size-control-compact) - var(--border-width-thin));
}

/* Text cells that should stay on one line (e.g. dates) */
.ds-table .ds-table-cell--nowrap {
  white-space: nowrap;
}

/* ---------- Row states ---------- */

/* Hover: grey-50. Not on group headers, details or empty-state rows. */
.ds-table tbody tr:not(.ds-table-group-row, .ds-table-details, .ds-table-empty-row, .is-disabled):hover > *,
.ds-table tbody tr.is-hover > * {
  background: var(--color-surface-muted);
}

/* Selected: pink-50, the Weave selected style. The ticked checkbox shows
   it too, so it never relies on colour alone. */
.ds-table tbody tr.is-selected > *,
.ds-table tbody tr.is-selected:hover > * {
  background: var(--color-primary-subtle);
}

/* Focus (keyboard only, e.g. a row that opens a page): a solid 2px pink
   ring drawn inside the row's edges (like an outline with a -2px offset),
   so nothing around it can clip it */
.ds-table tbody tr:focus-visible {
  outline: none;
}

.ds-table tbody tr:focus-visible > *,
.ds-table tbody tr.is-focus > * {
  box-shadow:
    inset 0 var(--focus-ring-width) 0 var(--color-primary),
    inset 0 calc(-1 * var(--focus-ring-width)) 0 var(--color-primary);
}

.ds-table tbody tr:focus-visible > :first-child,
.ds-table tbody tr.is-focus > :first-child {
  box-shadow:
    inset var(--focus-ring-width) 0 0 var(--color-primary),
    inset 0 var(--focus-ring-width) 0 var(--color-primary),
    inset 0 calc(-1 * var(--focus-ring-width)) 0 var(--color-primary);
}

.ds-table tbody tr:focus-visible > :last-child,
.ds-table tbody tr.is-focus > :last-child {
  box-shadow:
    inset calc(-1 * var(--focus-ring-width)) 0 0 var(--color-primary),
    inset 0 var(--focus-ring-width) 0 var(--color-primary),
    inset 0 calc(-1 * var(--focus-ring-width)) 0 var(--color-primary);
}

/* Cells in the box's corners share its round corners, so a row's
   background and its focus ring (drawn inside the row) curve with the box
   instead of being cut off by it. "First row" means the first row of the
   first row group (header or body). */
.ds-table-container > .ds-table-scroll:first-child > .ds-table > :is(thead, tbody):not(thead ~ *, tbody ~ *) > tr:first-child > :first-child {
  border-top-left-radius: var(--table-radius);
}

.ds-table-container > .ds-table-scroll:first-child > .ds-table > :is(thead, tbody):not(thead ~ *, tbody ~ *) > tr:first-child > :last-child {
  border-top-right-radius: var(--table-radius);
}

.ds-table-container > .ds-table-scroll:last-child > .ds-table > tbody:last-of-type > tr:last-child > :first-child {
  border-bottom-left-radius: var(--table-radius);
}

.ds-table-container > .ds-table-scroll:last-child > .ds-table > tbody:last-of-type > tr:last-child > :last-child {
  border-bottom-right-radius: var(--table-radius);
}

/* Disabled: grey-400 text and a grey tag (the row can't be selected) */
.ds-table tbody tr.is-disabled > * {
  color: var(--color-text-disabled);
}

.ds-table tbody tr.is-disabled .ds-tag {
  background: var(--color-disabled-bg);
  color: var(--color-text-disabled);
}

/* ---------- Sortable headers ---------- */

/* The button sits inside the header cell: 28px tall (4px 8px padding),
   4px corners. A negative 8px margin keeps its text lined up with the
   column. Its focus ring goes 2px outside it and still fits inside the
   cell, so the table's rounded box never clips it. */
.ds-table-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);              /* 4px text to icon */
  box-sizing: border-box;
  margin: 0 calc(-1 * var(--space-2));
  padding: var(--space-1) var(--space-2);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--easing-standard);
}

/* Number columns: the icon goes before the text, so the text lines up
   with the numbers below */
.ds-table .ds-table-cell--number .ds-table-sort {
  flex-direction: row-reverse;
}

.ds-table-sort:hover,
.ds-table-sort.is-hover {
  background: var(--color-hover-bg);
}

.ds-table-sort:focus-visible,
.ds-table-sort.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);   /* 2px outside the button */
}

/* 16px icon: grey when not sorted, dark when it's the sorted column */
.ds-table-sort-icon {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-text-muted);
}

.ds-table-sort-icon--asc,
.ds-table-sort-icon--desc,
[aria-sort="ascending"] .ds-table-sort-icon--none,
[aria-sort="descending"] .ds-table-sort-icon--none {
  display: none;
}

[aria-sort="ascending"] .ds-table-sort-icon--asc,
[aria-sort="descending"] .ds-table-sort-icon--desc {
  display: block;
  color: var(--color-text);
}

/* ---------- Bars above the table ---------- */

/* Toolbar: search on the left (up to 280px), buttons on the right, 8px
   apart, 12px 16px padding. Wraps on narrow screens.
   Controls inside tables and toolbars use the Small size (32px buttons,
   icon buttons and search, 16px icons), so they match the 14px table text:
   add ds-button--sm and ds-input--sm. With Relaxed rows, use Medium
   (44px) controls instead: leave the --sm classes off. */
.ds-table-toolbar,
.ds-table-batchbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-width-thin) solid var(--color-border);
}

/* (Two classes, so this wins over the text field's own 384px limit) */
.ds-search.ds-table-search {
  flex: 1 1 calc(var(--space-24) * 2);    /* grows from 192px… */
  max-width: calc(var(--space-20) * 3.5); /* …up to 280px */
}

/* Search with a results count beside it ("12 results"), 12px apart */
.ds-table-search-group {
  display: flex;
  flex: 1 1 calc(var(--space-24) * 2);
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  min-width: 0;
}

.ds-table-results {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  white-space: nowrap;
}

.ds-table-results[hidden] {
  display: none;
}

.ds-table-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Batch action bar: same height as the toolbar, so nothing jumps when it
   swaps in. "3 selected" on the left, the actions on the right. */
.ds-table-batchbar {
  border-top-left-radius: var(--table-radius);
  border-top-right-radius: var(--table-radius);
  background: var(--color-surface-muted);
}

.ds-table-batchbar[hidden],
.ds-table-toolbar[hidden] {
  display: none;
}

.ds-table-selection {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Batch actions, all Small Ghost buttons with no outline: Export (Ghost,
   with its icon), Delete (Destructive ghost: red text and icon, red-50 on
   hover), 4px apart; then a separator with 8px on each side; then Cancel
   (Ghost, no icon) at the far right, quieter than the actions.
   <div class="ds-table-batch-actions">Export · Delete
     <span class="ds-table-separator" aria-hidden="true"></span> Cancel</div> */
.ds-table-batch-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);   /* 4px between the buttons */
}

/* 1px grey-200 line, 20px tall: 4px margin + the 4px gap = 8px each side */
.ds-table-separator {
  flex-shrink: 0;
  width: var(--border-width-thin);
  height: var(--space-5);
  margin: 0 var(--space-1);
  background: var(--color-border);
}

.ds-table-selected-count {
  margin: 0;
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

/* Active filters: removable tags, 8px apart, then Clear all */
.ds-table-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-width-thin) solid var(--color-border);
}

.ds-table-filters[hidden] {
  display: none;
}

/* ---------- People ----------
   Uses the shared Avatar component (.ds-avatar, as in the card author),
   Small (24px). Use Relaxed rows (56px) so two-line cells fit. */

/* Owner: avatar, 8px, name */
.ds-table-person {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  vertical-align: middle;
}

/* Two lines: 14px grey-900, then 12px grey-600, 2px apart */
.ds-table-two-line {
  display: flex;
  flex-direction: column;
  gap: calc(var(--space-1) / 2);   /* 2px */
}

/* 8px above and below (not Relaxed's 12px), so 8 + 38 + 8 stays inside
   the 56px row */
.ds-table td:has(> .ds-table-two-line) {
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
}

.ds-table-two-line-main {
  color: var(--color-text);
}

.ds-table-two-line-sub {
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
}

/* Team: the avatar group (-8px overlap, 2px white ring) with Small avatars.
   The ring turns grey-50 on a hovered row and pink-50 on a selected one,
   so it always matches the row. */
.ds-table-team {
  vertical-align: middle;
}

.ds-table tbody tr:hover .ds-table-team .ds-avatar,
.ds-table tbody tr.is-hover .ds-table-team .ds-avatar {
  box-shadow: 0 0 0 var(--border-width-thick) var(--color-surface-muted);
}

.ds-table tbody tr.is-selected .ds-table-team .ds-avatar {
  box-shadow: 0 0 0 var(--border-width-thick) var(--color-primary-subtle);
}

/* ---------- Menus and popovers from the table ----------
   They match the table's Small controls. Row menus add .ds-dropdown--sm
   (32px items, 14px text, 16px icons 8px from the label) and
   data-align="end"; here they get a 160px minimum width and open 4px from
   their trigger, lined up with its right edge. */
.ds-table-container .ds-dropdown--menu .ds-dropdown-menu {
  top: calc(100% + var(--space-1));
  min-width: calc(var(--space-20) * 2);   /* 160px */
}

.ds-table-container .ds-dropdown--menu.ds-dropdown--up .ds-dropdown-menu {
  top: auto;
  bottom: calc(100% + var(--space-1));
}

/* The Filter popover (.ds-table-popover on a .ds-popover): 14px text,
   12px padding, Small checkboxes 8px apart */
.ds-table-popover {
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-table-popover .ds-popover-body {
  padding: var(--space-3);
}

.ds-popover.ds-table-popover .ds-popover-title {
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-table-popover .ds-checkbox-options {
  gap: var(--space-2);
}

/* ---------- Row grouping ---------- */

/* Group header: grey-50, semi-bold, the full row is one button with a
   chevron, the group's name and its count */
.ds-table .ds-table-group-row > th {
  padding: 0;
  background: var(--color-surface-muted);
  font-weight: var(--font-weight-semibold);
}

.ds-table-group-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  box-sizing: border-box;
  width: 100%;
  min-height: var(--table-row-height, var(--size-control-standard));
  margin: 0;
  padding: 0 var(--space-4);
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--easing-standard);
}

.ds-table-group-toggle:hover,
.ds-table-group-toggle.is-hover {
  background: var(--color-hover-bg);
}

.ds-table-group-toggle:focus-visible,
.ds-table-group-toggle.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: calc(-1 * var(--focus-ring-width));
}

/* 16px chevron: down when open, right when closed */
.ds-table-group-chevron {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-text-muted);
  transition: transform var(--duration-normal) var(--easing-standard);
}

.ds-table-group-toggle[aria-expanded="false"] .ds-table-group-chevron {
  transform: rotate(-90deg);
}

/* The count: the neutral badge's colours */
.ds-table-group-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--space-5);
  min-height: var(--space-5);
  padding: 0 var(--space-2);
  box-sizing: border-box;
  border-radius: var(--radius-full);
  background: var(--color-surface-strong);
  font-size: var(--type-caption-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-caption-line-height);
}

.ds-table-group.is-collapsed > tr:not(.ds-table-group-row) {
  display: none;
}

/* ---------- Expandable rows ---------- */

/* Chevron: right when closed, down when open */
.ds-table-expand svg,
.ds-table-expand i[data-lucide] {
  transition: transform var(--duration-normal) var(--easing-standard);
}

.ds-table-expand[aria-expanded="true"] svg,
.ds-table-expand[aria-expanded="true"] i[data-lucide] {
  transform: rotate(90deg);
}

/* The open row stays white, with no line between it and its details */
.ds-table tbody tr.is-expanded > * {
  border-bottom-color: transparent;
}

/* The details row holds one inset box. The box starts where the Name
   column starts: after the 48px chevron column, or after the checkbox and
   chevron columns (96px) in selectable tables. 4px above it, 16px below
   (then the row's line), 16px from the table's right edge. */
.ds-table .ds-table-details {
  --table-details-lead: var(--space-12);   /* 48px: the chevron column */
}

.ds-table:has(> thead .ds-table-cell--checkbox) .ds-table-details {
  --table-details-lead: calc(var(--space-12) * 2);   /* 96px: checkbox + chevron */
}

.ds-table .ds-table-details > td {
  height: auto;
  padding: var(--space-1) var(--space-4) var(--space-4) var(--table-details-lead);
}

.ds-table-details[hidden] {
  display: none;
}

/* The inset box: grey-50, 1px grey-200 border, 8px corners, 12px 16px padding */
.ds-table-details-box {
  box-sizing: border-box;
  padding: var(--space-3) var(--space-4);
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-muted);
}

/* Label and value pairs in 3 columns, 12px apart. The description gets
   its own full-width line below them (.ds-table-details-wide). */
.ds-table-details-list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0;
}

.ds-table-details-wide {
  grid-column: 1 / -1;
}

/* Label 12px grey-600, value 14px grey-900, 2px apart */
.ds-table-details-list dt {
  margin-bottom: calc(var(--space-1) / 2);   /* 2px */
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
}

.ds-table-details-list dd {
  margin: 0;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* ---------- Deleting ----------
   A deleted row (and its details row) fades out over 150ms before it's
   removed. No fade with reduced motion: main.js removes it straight away. */
.ds-table tr.is-removing {
  animation: ds-table-fade-out var(--duration-fade) var(--easing-standard) forwards;
  pointer-events: none;
}

@keyframes ds-table-fade-out {
  to { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ds-table tr.is-removing {
    animation: none;
  }
}

/* ---------- Empty, no results and error ---------- */

/* One cell across every column: an icon, a title, a line of text and an
   action, centred */
.ds-table .ds-table-empty-row > td {
  height: auto;
  padding: var(--space-12) var(--space-4);
  text-align: center;
}

.ds-table-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  max-width: calc(var(--space-24) * 4);   /* 384px */
  margin: 0 auto;
}

/* 24px icon in a 48px grey-100 circle, 16px above the title */
.ds-table-empty-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--space-12);
  height: var(--space-12);
  margin-bottom: var(--space-2);
  border-radius: var(--radius-full);
  background: var(--color-surface-subtle);
  color: var(--color-text-muted);
}

.ds-table-empty-icon svg,
.ds-table-empty-icon i[data-lucide] {
  width: var(--space-6);
  height: var(--space-6);
}

.ds-table-empty .ds-table-empty-title {
  margin: 0;
  color: var(--color-text);
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-line-height);
}

.ds-table-empty .ds-table-empty-text {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* The action sits 16px below the text */
.ds-table-empty-action {
  margin-top: var(--space-2);
}

/* Error: a red icon on red-50 */
.ds-table-empty--error .ds-table-empty-icon {
  background: var(--color-error-bg);
  color: var(--color-error);
}

/* ---------- Loading ---------- */

/* Skeleton text (12px) sits in a 20px slot, like a line of cell text, so
   rows are the same height as the real ones */
.ds-table .ds-skeleton--text {
  margin: var(--space-1) 0;
}

.ds-table .ds-table-cell--number .ds-skeleton {
  margin-left: auto;
}

/* ---------- Scrolling: sticky header and first column ---------- */

/* Wide tables keep every cell on one line and scroll sideways */
.ds-table--nowrap th,
.ds-table--nowrap td {
  white-space: nowrap;
}

/* The header row stays at the top of the scrolling box */
.ds-table--sticky-header thead th {
  position: sticky;
  top: 0;
  z-index: calc(var(--layer-sticky) + 1);   /* above the sticky first column */
}

/* The first column stays on the left, with a line on its right edge */
.ds-table--sticky-column > * > tr > :first-child {
  position: sticky;
  left: 0;
  z-index: var(--layer-sticky);
  border-right: var(--border-width-thin) solid var(--color-border);
  background: var(--color-surface);
}

.ds-table--sticky-column > thead > tr > :first-child {
  z-index: calc(var(--layer-sticky) + 2);   /* the corner sits above both */
  background: var(--color-surface-muted);
}

/* ---------- Phones ---------- */
@media (max-width: 767px) {
  /* Search goes full width; the buttons wrap below it */
  .ds-search.ds-table-search {
    flex-basis: 100%;
    max-width: none;
  }

  /* Cells stay on one line and the table scrolls sideways, rather than
     squeezing names onto three lines (details and empty states still wrap) */
  .ds-table th,
  .ds-table td:not(.ds-table-details > td, .ds-table-empty-row > td) {
    white-space: nowrap;
  }

  /* Expandable rows: the details box stays in view at the width of the
     box (after the lead columns), while the columns behind it scroll, and
     its pairs stack in one column */
  .ds-table-details-box {
    position: sticky;
    left: var(--table-details-lead);
    width: calc(100cqi - var(--table-details-lead) - var(--space-4));
  }

  .ds-table-details-list {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Empty, no results and error: the message stays in view, at the width
     of the box, while the columns behind it scroll */
  .ds-table-empty {
    position: sticky;
    left: var(--space-4);
    width: calc(100cqi - var(--space-8));
    margin: 0;
  }
}

/* Tabs --------------------------------------------------------------------- */

/* Split related content into sections on one page; one section shows at a
   time.
     <div class="ds-tabs" data-tabs>
       <div class="ds-tabs-list" role="tablist" aria-label="Project">
         <button type="button" class="ds-tab" role="tab" id="t-1"
                 aria-selected="true" aria-controls="p-1">
           <i data-lucide="layout-grid" aria-hidden="true"></i>   (optional, 16px)
           <span class="ds-tab-text">
             <span class="ds-tab-label" data-label="Overview">Overview</span>
             <span class="ds-tab-count">12</span>             (optional)
           </span>
         </button>
         …more tabs (aria-selected="false" tabindex="-1")
       </div>
       <div class="ds-tabs-panels">
         <div class="ds-tabs-panel" role="tabpanel" id="p-1" aria-labelledby="t-1" tabindex="0">…</div>
         <div class="ds-tabs-panel" role="tabpanel" id="p-2" aria-labelledby="t-2" tabindex="0" hidden>…</div>
       </div>
     </div>
   Line (default): a 1px grey-200 line under the tabs; the selected tab has a
   2px pink-600 bar and pink-700 semibold text (6.54:1 on white).
   Variants: --contained (grey-50 bar, the selected tab white and joined to
   a bordered panel), --full (tabs share the width equally), --vertical
   (tabs on the left, sidebar style), --scroll (a .ds-tabs-bar with previous
   and next arrow buttons round the list, and a fade at the edges).
   Sizes: MD 44px / 16px text (default), --sm 32px / 14px text. 16px side
   padding, no gap between tabs, 8px icon to label, 4px label to count, the
   panel 24px below the list.
   States: hover grey-900 text and a grey-300 bar; focus (keyboard only) a
   2px pink ring drawn inside the tab, so a scrolling list never clips it;
   disabled (aria-disabled="true") grey-400. .is-hover / .is-focus show the
   states on the docs pages.
   The label's data-label holds the same text: an invisible semibold copy
   keeps the tab the same width when it's selected, so nothing shifts.
   Hidden panels keep their room (they share one grid cell), so the page
   doesn't jump when switching between panels of different heights.
   main.js (section 39) handles clicks, arrow keys, Home / End, the
   address (data-tabs-url), scrolling and the fade. */

.ds-tabs {
  --tab-height: var(--size-control-standard);   /* 44px */
  --tab-font-size: var(--type-body-size);        /* 16px */
  --tab-line-height: var(--type-body-line-height);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);   /* 24px from the tabs to the panel */
  min-width: 0;
  font-family: var(--font-body);
}

.ds-tabs--sm {
  --tab-height: var(--size-control-compact);     /* 32px */
  --tab-font-size: var(--type-body-small-size);  /* 14px */
  --tab-line-height: var(--type-body-small-line-height);
}

/* The row of tabs, with the 1px divider line along the bottom (an inset
   shadow, so it adds no height). It scrolls sideways when the tabs don't
   fit (no scrollbar; main.js adds the fade). */
.ds-tabs-list {
  display: flex;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  box-shadow: inset 0 calc(var(--border-width-thin) * -1) 0 var(--color-border);
  scrollbar-width: none;
}

.ds-tabs-list::-webkit-scrollbar {
  display: none;
}

/* A soft fade at an edge where there are more tabs to scroll to */
.ds-tabs-list.can-scroll-start {
  mask-image: linear-gradient(to right, transparent, var(--mask-solid) var(--space-8));
}

.ds-tabs-list.can-scroll-end {
  mask-image: linear-gradient(to left, transparent, var(--mask-solid) var(--space-8));
}

.ds-tabs-list.can-scroll-start.can-scroll-end {
  mask-image: linear-gradient(to right, transparent, var(--mask-solid) var(--space-8),
    var(--mask-solid) calc(100% - var(--space-8)), transparent);
}

/* One tab */
.ds-tab {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);   /* 8px icon to label */
  box-sizing: border-box;
  min-height: var(--tab-height);
  margin: 0;
  padding: 0 var(--space-4);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  color: var(--color-text-secondary);
  font: inherit;
  font-size: var(--tab-font-size);
  font-weight: var(--font-weight-regular);
  line-height: var(--tab-line-height);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--duration-fast) var(--easing-standard),
    background-color var(--duration-fast) var(--easing-standard);
}

.ds-tab svg,
.ds-tab i[data-lucide] {
  flex-shrink: 0;
  width: var(--space-4);    /* 16px */
  height: var(--space-4);
}

/* Label and count, 4px apart */
.ds-tab-text {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

/* The label, plus an invisible semibold copy of it underneath that sets
   its width, so the tab doesn't grow when it's selected (the copy isn't
   read out: the text after the "/" is its empty alternative) */
.ds-tab-label {
  display: inline-grid;
}

.ds-tab-label::after {
  content: attr(data-label) / "";
  height: 0;
  overflow: hidden;
  font-weight: var(--font-weight-semibold);
  visibility: hidden;
  pointer-events: none;
  user-select: none;
}

/* The count: a small grey pill, pink-700 on pink-50 on the selected tab */
.ds-tab-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: var(--space-5);    /* 20px */
  min-height: var(--space-5);
  padding: 0 calc(var(--space-1) * 1.5);   /* 6px */
  border-radius: var(--radius-full);
  background: var(--color-surface-subtle);
  color: var(--color-text-secondary);
  font-size: var(--type-label-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--type-label-line-height);
  font-variant-numeric: tabular-nums;
}

/* The bar along the bottom of the tab: none at rest, grey-300 on hover,
   pink-600 when selected. It covers the divider line. */
.ds-tab::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: var(--border-width-thick);   /* 2px */
  border-radius: var(--radius-full);
  background: transparent;
  transition: background-color var(--duration-fast) var(--easing-standard);
}

/* Hover */
.ds-tab:hover:not([aria-disabled="true"]):not([aria-selected="true"]),
.ds-tab.is-hover {
  color: var(--color-text);
}

.ds-tab:hover:not([aria-disabled="true"]):not([aria-selected="true"])::after,
.ds-tab.is-hover::after {
  background: var(--color-border-medium);
}

/* Selected */
.ds-tab[aria-selected="true"] {
  color: var(--color-primary-hover);   /* pink-700 */
  font-weight: var(--font-weight-semibold);
}

.ds-tab[aria-selected="true"]::after {
  background: var(--color-primary);    /* pink-600 */
}

.ds-tab[aria-selected="true"] .ds-tab-count {
  background: var(--color-primary-subtle);   /* pink-50 */
  color: var(--color-primary-hover);
}

/* Focus (keyboard only): a 2px pink ring 2px inside the tab's edge, so a
   scrolling list or a card never cuts it off */
.ds-tab:focus-visible,
.ds-tab.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: calc((var(--focus-ring-width) + var(--focus-ring-offset)) * -1);
}

/* Disabled: grey-400, can't be chosen. It stays focusable, so its tooltip
   (why it's disabled) can be read. */
.ds-tab[aria-disabled="true"] {
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.ds-tab[aria-disabled="true"] .ds-tab-count {
  background: var(--color-surface-muted);
  color: var(--color-text-disabled);
}

/* The panels share one grid cell, so the area is always as tall as the
   tallest panel: switching never makes the page jump. Hidden panels keep
   their room but can't be seen, focused or read. */
.ds-tabs-panels {
  display: grid;
  min-width: 0;
}

.ds-tabs-panel {
  grid-area: 1 / 1;
  min-width: 0;
  border-radius: var(--radius-md);
}

.ds-tabs-panel[hidden] {
  display: block;
  visibility: hidden;
}

.ds-tabs-panel:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

/* Contained: the tabs sit in a grey-50 bar; the selected tab is white with
   a grey-200 border and opens into the white panel below */
.ds-tabs--contained {
  gap: 0;
}

.ds-tabs--contained .ds-tabs-list {
  gap: var(--space-1);
  padding: var(--space-1) var(--space-1) 0;
  border: var(--border-width-thin) solid var(--color-border);
  border-bottom: 0;
  border-radius: var(--radius-container) var(--radius-container) 0 0;
  /* the grey-50 bar, with its 1px bottom line drawn as part of the
     background, so the selected tab's white covers it */
  background:
    linear-gradient(var(--color-border), var(--color-border)) bottom / 100% var(--border-width-thin) no-repeat,
    var(--color-surface-muted);
  box-shadow: none;
}

.ds-tabs--contained .ds-tab {
  border: var(--border-width-thin) solid transparent;
  border-bottom: 0;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}

.ds-tabs--contained .ds-tab::after {
  display: none;
}

.ds-tabs--contained .ds-tab:hover:not([aria-disabled="true"]):not([aria-selected="true"]),
.ds-tabs--contained .ds-tab.is-hover {
  background: var(--color-hover-bg);
}

.ds-tabs--contained .ds-tab[aria-selected="true"] {
  border-color: var(--color-border);
  background: var(--color-surface);
}

/* The panel: white, bordered on three sides, 24px padding */
.ds-tabs--contained .ds-tabs-panels {
  padding: var(--space-6);
  border: var(--border-width-thin) solid var(--color-border);
  border-top: 0;
  border-radius: 0 0 var(--radius-container) var(--radius-container);
  background: var(--color-surface);
}

/* Full width: 2 or 3 tabs share the width equally */
.ds-tabs--full .ds-tab {
  flex: 1 1 0;
  min-width: 0;
}

/* Navigation item: ONE shared look for the site's sidebar links
   (.ds-nav-item, current page = aria-current="page") and Vertical tabs
   (selected = aria-selected="true"), so the two always match.
   14px text, 8px 12px padding, no gap between items; square on the left,
   6px corners on the right. Selected: pink-50, pink-700 semibold text
   (6.54:1) and a 3px pink-600 bar down the left edge (an inset shadow, so
   it runs the full height and never moves the text): selection doesn't
   rely on colour alone. Hover: grey-100 only (no bar, normal weight), so it
   never looks selected. Focus (keyboard only): a 2px pink-600 ring drawn
   just inside the item, so a scrolling sidebar never cuts it off. */
.ds-nav-item,
.ds-tabs--vertical .ds-tab {
  min-height: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: 0 calc(var(--radius-sm) * 1.5) calc(var(--radius-sm) * 1.5) 0;   /* 6px on the right */
  color: var(--color-text-nav);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-normal);
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--easing-standard);
}

.ds-nav-item:hover,
.ds-tabs--vertical .ds-tab:hover:not([aria-disabled="true"]):not([aria-selected="true"]),
.ds-tabs--vertical .ds-tab.is-hover {
  background: var(--color-hover-bg);
  color: var(--color-text);
}

/* (after :hover, so the selected item keeps its look when hovered) */
.ds-nav-item[aria-current="page"],
.ds-tabs--vertical .ds-tab[aria-selected="true"] {
  background: var(--color-primary-subtle);
  box-shadow: inset calc(var(--border-width-thin) * 3) 0 0 var(--color-primary);   /* the 3px bar */
  color: var(--color-primary-hover);   /* pink-700 */
  font-weight: var(--font-weight-semibold);
}

.ds-nav-item:focus-visible,
.ds-tabs--vertical .ds-tab:focus-visible,
.ds-tabs--vertical .ds-tab.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: calc(var(--focus-ring-width) * -1);
}

@media (prefers-reduced-motion: reduce) {
  .ds-nav-item {
    transition: none;
  }
}

/* Vertical: tabs stacked in a 200px list on the left, with a 1px grey-200
   line between them and the panel (24px further on). Each tab is a
   navigation item (above), so it looks exactly like the sidebar. */
.ds-tabs--vertical {
  display: grid;
  grid-template-columns: calc(var(--space-20) * 2.5) minmax(0, 1fr);   /* 200px list */
  align-items: start;
}

.ds-tabs--vertical .ds-tabs-list {
  flex-direction: column;
  align-self: stretch;
  padding-right: var(--space-2);
  overflow: visible;
  box-shadow: inset calc(var(--border-width-thin) * -1) 0 0 var(--color-border);
}

.ds-tabs--vertical .ds-tab {
  justify-content: flex-start;
  text-align: left;
  white-space: normal;
}

/* No bottom bar: the selected bar is on the left */
.ds-tabs--vertical .ds-tab::after {
  display: none;
}

.ds-tabs--vertical .ds-tab[aria-disabled="true"] {
  color: var(--color-text-disabled);
}

/* Scrollable: previous and next arrow buttons on either side of the list.
   The divider runs under the whole bar. main.js hides an arrow when
   there's nothing more that way. */
.ds-tabs-bar {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
  box-shadow: inset 0 calc(var(--border-width-thin) * -1) 0 var(--color-border);
}

.ds-tabs-bar .ds-tabs-list {
  flex: 1;
  box-shadow: none;
}

.ds-tabs-arrow {
  flex-shrink: 0;
}

.ds-tabs-arrow[hidden] {
  display: inline-flex;
  visibility: hidden;   /* keeps its room, so the tabs don't move */
}

@media (prefers-reduced-motion: reduce) {
  .ds-tab,
  .ds-tab::after {
    transition: none;
  }
}

/* Tag ---------------------------------------------------------------------- */

/* A small label in a pill that names a category, status or attribute.
     <span class="ds-tag ds-tag--info"><span class="ds-tag-label">In progress</span></span>
   Sizes: --sm 20px (12px text, tables and dense lists), MD 24px (12px
   medium, the default, cards), --lg 32px (14px text, 12px side padding,
   filters and selectable tags). 8px side padding, full pill radius.
   Colours (soft background, dark text, all 4.5:1+; tokens --tag-*-bg /
   --tag-*-text in tokens.css):
     Status: Neutral (default), --info, --success, --warning, --error.
       Always mean the same thing.
     Categories: --pink, --purple, --blue, --teal, --green, --yellow,
       --orange, --red. The data visualisation set (so they match charts);
       they mean nothing on their own, only tell categories apart.
   Parts: a 12px Lucide icon or a 16px .ds-avatar before the text, 4px away.
   The text goes in .ds-tag-label, cut with "…" at 200px; a cut read-only
   tag gets tabindex="0" and data-tooltip-truncate, so its full text shows
   in a tooltip on hover and keyboard focus.
   Dismissible (.ds-tag--removable): a .ds-tag-remove x button 2px after the
   text, a 16px circle with a 24px click area, aria-label "Remove …"
   (data-tag-remove: main.js section 36 removes it and moves focus on).
   Selectable (.ds-tag--selectable): a <button aria-pressed> that turns on
   and off; selected = pink-50, pink-600 edge and a .ds-tag-check icon.
   Overflow (.ds-tag--overflow): a "+4" <button> with the rest of the names
   in its aria-label and data-tooltip (+ data-tooltip-named).
   Groups: .ds-tag-list, 8px apart, wrapping with 8px between rows
   (--scroll: one row that scrolls sideways on phones).
   Tag input field (.ds-tag-input): tags and a text input in one field.
   .is-hover / .is-focus show those states on the docs pages. */

.ds-tag {
  /* Each colour and size sets these; the rest of the tag uses them */
  --tag-bg: var(--tag-neutral-bg);
  --tag-text: var(--tag-neutral-text);
  --tag-height: var(--space-6);                    /* 24px (MD) */
  --tag-padding: var(--space-2);                   /* 8px */
  --tag-font-size: var(--type-caption-size);       /* 12px */
  --tag-line-height: var(--type-caption-line-height);
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);                             /* 4px icon or avatar to text */
  box-sizing: border-box;
  min-width: 0;
  max-width: calc(var(--space-20) * 2.5);          /* 200px, then "…" */
  min-height: var(--tag-height);
  margin: 0;
  padding: 0 var(--tag-padding);
  border: 0;
  border-radius: var(--radius-full);
  background: var(--tag-bg);
  color: var(--tag-text);
  font-family: var(--font-body);
  font-size: var(--tag-font-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--tag-line-height);
  white-space: nowrap;
  vertical-align: middle;
}

/* Sizes */
.ds-tag--sm {
  --tag-height: var(--space-5);                    /* 20px */
}

.ds-tag--lg {
  --tag-height: var(--size-control-compact);       /* 32px */
  --tag-padding: var(--space-3);                   /* 12px */
  --tag-font-size: var(--type-body-small-size);    /* 14px */
  --tag-line-height: var(--type-body-small-line-height);
}

/* Status colours */
.ds-tag--info    { --tag-bg: var(--tag-info-bg);    --tag-text: var(--tag-info-text); }
.ds-tag--success { --tag-bg: var(--tag-success-bg); --tag-text: var(--tag-success-text); }
.ds-tag--warning { --tag-bg: var(--tag-warning-bg); --tag-text: var(--tag-warning-text); }
.ds-tag--error   { --tag-bg: var(--tag-error-bg);   --tag-text: var(--tag-error-text); }

/* Category colours (data visualisation) */
.ds-tag--pink   { --tag-bg: var(--tag-pink-bg);   --tag-text: var(--tag-pink-text); }
.ds-tag--purple { --tag-bg: var(--tag-purple-bg); --tag-text: var(--tag-purple-text); }
.ds-tag--blue   { --tag-bg: var(--tag-blue-bg);   --tag-text: var(--tag-blue-text); }
.ds-tag--teal   { --tag-bg: var(--tag-teal-bg);   --tag-text: var(--tag-teal-text); }
.ds-tag--green  { --tag-bg: var(--tag-green-bg);  --tag-text: var(--tag-green-text); }
.ds-tag--yellow { --tag-bg: var(--tag-yellow-bg); --tag-text: var(--tag-yellow-text); }
.ds-tag--orange { --tag-bg: var(--tag-orange-bg); --tag-text: var(--tag-orange-text); }
.ds-tag--red    { --tag-bg: var(--tag-red-bg);    --tag-text: var(--tag-red-text); }

/* The text: cut with "…" when the tag reaches 200px */
.ds-tag-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-body);
}

/* A 12px icon before the text */
.ds-tag > svg,
.ds-tag > i[data-lucide] {
  flex-shrink: 0;
  width: var(--space-3);
  height: var(--space-3);
}

/* A 16px avatar before a name (the Avatar component, smaller) */
.ds-tag > .ds-avatar {
  --avatar-size: var(--space-4);                   /* 16px */
  --avatar-text: var(--font-size-xs);
  --avatar-icon: var(--space-3);
}

/* A cut read-only tag can be focused, to show its full text */
.ds-tag[tabindex]:focus-visible,
.ds-tag.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

/* Dismissible: the x is a 16px circle 2px after the text, with even room
   on its right; its click area is 24 × 24 even on a 20px tag */
.ds-tag--removable {
  padding-right: calc((var(--tag-height) - var(--space-4)) / 2);
}

.ds-tag-remove {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--space-4);                           /* 16px circle */
  height: var(--space-4);
  /* 2px after the text: the tag's 4px gap, less 2px */
  margin: 0 0 0 calc(var(--space-1) / -2);
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: none;
  color: inherit;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--easing-standard);
}

/* The 24 × 24 click area, centred on the circle */
.ds-tag-remove::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--size-target-minimum);
  height: var(--size-target-minimum);
  translate: -50% -50%;
}

.ds-tag-remove svg,
.ds-tag-remove i[data-lucide] {
  display: block;
  width: var(--space-3);                           /* 12px */
  height: var(--space-3);
}

/* Hover: a darker circle, made from the tag's own text colour */
.ds-tag-remove:hover,
.ds-tag-remove.is-hover {
  background: color-mix(in srgb, var(--tag-text) 16%, transparent);
}

/* (drawn on the circle's edge, inside it, so it never covers the text) */
.ds-tag-remove:focus-visible,
.ds-tag-remove.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: calc(var(--focus-ring-width) * -1);
}

/* Selectable: white with a grey edge; selected = pink-50, pink-600 edge,
   pink-700 text (6.54:1) and a check before the text. The edge is an
   inset shadow, so it adds no size. */
.ds-tag--selectable {
  --tag-bg: var(--color-surface);
  --tag-text: var(--color-text);
  --tag-edge: var(--color-border-medium);
  box-shadow: inset 0 0 0 var(--border-width-thin) var(--tag-edge);
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--easing-standard),
    box-shadow var(--duration-fast) var(--easing-standard);
}

.ds-tag--selectable:hover:not([aria-disabled="true"]):not(:disabled),
.ds-tag--selectable.is-hover {
  --tag-bg: var(--color-surface-muted);
  --tag-edge: var(--color-border-strong-hover);
}

.ds-tag--selectable[aria-pressed="true"] {
  --tag-bg: var(--color-primary-subtle);           /* pink-50 */
  --tag-text: var(--color-primary-hover);          /* pink-700 */
  --tag-edge: var(--color-primary);                /* pink-600 */
}

.ds-tag--selectable[aria-pressed="true"]:hover:not([aria-disabled="true"]):not(:disabled),
.ds-tag--selectable[aria-pressed="true"].is-hover {
  --tag-bg: var(--color-selected-bg-hover);
  --tag-edge: var(--color-primary-hover);
}

.ds-tag--selectable:focus-visible,
.ds-tag--selectable.is-focus,
.ds-tag--overflow:focus-visible,
.ds-tag--overflow.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

.ds-tag--selectable[aria-disabled="true"],
.ds-tag--selectable:disabled {
  --tag-bg: var(--color-surface-muted);
  --tag-text: var(--color-text-disabled);
  --tag-edge: var(--color-border);
  cursor: not-allowed;
}

/* The check: only on a selected tag */
.ds-tag > .ds-tag-check {
  display: none;
}

.ds-tag[aria-pressed="true"] > .ds-tag-check {
  display: block;
}

/* Overflow: "+4", a neutral tag that's a button (its tooltip lists the rest) */
.ds-tag--overflow {
  cursor: default;
}

.ds-tag--overflow:hover,
.ds-tag--overflow.is-hover {
  --tag-bg: var(--color-surface-strong);
}

/* A group of tags: 8px apart, wrapping onto new lines 8px apart */
.ds-tag-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Each item is just its tag (no extra line height), so rows are exactly
   8px apart */
.ds-tag-list > li {
  display: flex;
}

/* Phones: a row of selectable tags can scroll sideways instead, with a
   fade at an edge with more tags past it (main.js adds .can-scroll-*).
   The padding keeps the focus ring inside the scrolling box. */
@media (max-width: 767px) {
  .ds-tag-list--scroll {
    flex-wrap: nowrap;
    padding: var(--space-1);
    overflow-x: auto;
    scrollbar-width: none;
  }

  .ds-tag-list--scroll::-webkit-scrollbar {
    display: none;
  }

  .ds-tag-list--scroll > * {
    flex-shrink: 0;
  }

  .ds-tag-list--scroll.can-scroll-end {
    mask-image: linear-gradient(to left, transparent, var(--mask-solid) var(--space-8));
  }

  .ds-tag-list--scroll.can-scroll-start {
    mask-image: linear-gradient(to right, transparent, var(--mask-solid) var(--space-8));
  }

  .ds-tag-list--scroll.can-scroll-start.can-scroll-end {
    mask-image: linear-gradient(to right, transparent, var(--mask-solid) var(--space-8),
      var(--mask-solid) calc(100% - var(--space-8)), transparent);
  }
}

/* Tag input field: chosen values as dismissible tags, then a text input.
   The shared field style: grey-500 edge, grey-700 on hover, pink-600 with
   the 3px pink-200 ring while focused, 8px corners. At least 44px tall;
   it grows as the tags wrap.
     <div class="ds-tag-input" data-tag-input>
       <ul class="ds-tag-list" data-tag-fallback="the input's id">…tags…</ul>
       <input class="ds-tag-input-control" id="…" type="text">
     </div> */
.ds-tag-input {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  box-sizing: border-box;
  width: 100%;
  max-width: calc(var(--space-24) * 4);            /* 384px, like a text input */
  min-height: var(--size-control-standard);        /* 44px */
  padding: var(--space-2) var(--space-3);
  border: var(--border-width-thin) solid var(--field-border);
  border-radius: var(--radius-control);            /* 12px, a Medium field */
  background: var(--color-surface);
  cursor: text;
  transition: border-color var(--duration-fast) var(--easing-standard),
    box-shadow var(--duration-fast) var(--easing-standard);
  font-family: var(--font-body);
}

.ds-tag-input:hover {
  border-color: var(--field-border-hover);
}

.ds-tag-input:focus-within {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 calc(var(--border-width-thin) * 3) var(--field-focus-ring);
}

/* An empty list takes no room */
.ds-tag-input > .ds-tag-list:empty {
  display: none;
}

.ds-tag-input-control {
  flex: 1;
  min-width: var(--space-24);                      /* 96px, so there's always room to type */
  min-height: var(--space-6);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: var(--type-body-size);                /* 16px, so phones don't zoom */
  line-height: var(--type-body-line-height);
}

/* The field shows the focus, not the input */
.ds-tag-input-control:focus,
.ds-tag-input-control:focus-visible {
  outline: none;
}

.ds-tag-input-control::placeholder {
  color: var(--color-text-subtle);
}

@media (prefers-reduced-motion: reduce) {
  .ds-tag-remove,
  .ds-tag--selectable,
  .ds-tag-input {
    transition: none;
  }
}

/* Toast -------------------------------------------------------------------- */
/* A short message that appears at the edge of the screen after people do
   something, like "Project deleted", then disappears on its own.

   Show one from any page with the shared script (main.js, section 40):
     showToast({ message: 'Project deleted', variant: 'success',
                 action: { label: 'Undo', onClick: restore } });
   or from a button: <button data-toast="Changes saved"
     data-toast-variant="success" data-toast-action="Undo"
     data-toast-action-toast="Project restored">.
   The script builds the region and the toasts:
   <section class="ds-toast-region" aria-label="Notifications">
     <div class="ds-toast ds-toast--success">
       <div class="ds-toast-content">
         <i data-lucide="circle-check" class="ds-toast-icon" aria-hidden="true"></i>
         <p class="ds-toast-message">Changes saved</p>
       </div>
       <div class="ds-toast-controls">
         <button class="ds-toast-action">Undo</button>        (optional)
         <button class="ds-toast-close" aria-label="Close">x icon</button>
       </div>
     </div>
     + two hidden live regions that read the message out
   </section>

   Sizes: at least 48px tall, 280px to 480px wide, 12px 16px padding,
   12px corners, the subtle shadow. 20px icon, 12px to the message; 16px
   from the message to the action (or close button); 8px from the action
   to the close button. Messages wrap onto 2 lines at most. Solid grey-800
   with white 14px text (13.48:1), the same as tooltips. */
.ds-toast {
  display: flex;
  align-items: center;
  gap: var(--space-4);                              /* 16px: message to the controls */
  box-sizing: border-box;
  width: max-content;
  min-width: calc(var(--space-20) * 3.5);           /* 280px */
  max-width: calc(var(--space-24) * 5);             /* 480px */
  min-height: var(--space-12);                      /* 48px */
  padding: var(--space-3) var(--space-4);           /* 12px 16px */
  /* A 1px edge drawn just inside: invisible here, but it keeps the toast
     visible in Windows high contrast mode */
  outline: var(--border-width-thin) solid transparent;
  outline-offset: calc(var(--border-width-thin) * -1);
  border-radius: var(--radius-container);                  /* 12px */
  background: var(--toast-bg);
  box-shadow: var(--shadow-md);
  color: var(--toast-text);
  font-family: var(--font-body);
  font-size: var(--type-body-small-size);           /* 14px */
  font-weight: var(--font-weight-regular);
  line-height: var(--type-body-small-line-height);  /* 20px */
  text-align: left;
}

/* Keyboard focus on one of its buttons: a 2px pink ring round the whole
   toast too, so it's clear the toast is paused */
.ds-toast:has(:focus-visible),
.ds-toast.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

/* The icon and the message, 12px apart */
.ds-toast-content {
  display: flex;
  flex: 1;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}

/* Status icons: 20px, a light tint of the status colour (3:1+ on grey-800) */
.ds-toast-icon {
  flex-shrink: 0;
  width: var(--space-5);
  height: var(--space-5);
}

.ds-toast--success .ds-toast-icon { color: var(--toast-icon-success); }
.ds-toast--info .ds-toast-icon    { color: var(--toast-icon-info); }
.ds-toast--warning .ds-toast-icon { color: var(--toast-icon-warning); }
.ds-toast--error .ds-toast-icon   { color: var(--toast-icon-error); }

/* The message: 2 lines at most */
.ds-toast-message {
  display: -webkit-box;
  overflow: hidden;
  min-width: 0;
  margin: 0;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow-wrap: anywhere;
}

/* The action and the close button, 8px apart */
.ds-toast-controls {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: var(--space-2);
  /* the 32px buttons reach 4px into the padding, so a one-line toast
     stays 48px tall */
  margin-block: calc(var(--space-1) * -1);
}

/* The action: a pink-200 text button (7.69:1), 32px tall, one word */
.ds-toast-action {
  display: inline-flex;
  align-items: center;
  min-height: var(--size-control-compact);          /* 32px */
  padding: 0 var(--space-2);
  border: 0;
  border-radius: var(--radius-control-sm);          /* 8px, a Small button */
  background: transparent;
  color: var(--toast-action);
  font: inherit;
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--easing-standard), color var(--duration-fast) var(--easing-standard);
}

/* The close button: a 20px x in a 32px square */
.ds-toast-close {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--size-control-compact);               /* 32px */
  height: var(--size-control-compact);
  /* the x lines up 16px from the toast's right edge, like the padding */
  margin-inline-end: calc(var(--space-1) * -1.5);
  padding: 0;
  border: 0;
  border-radius: var(--radius-control-sm);          /* 8px, a Small button */
  background: transparent;
  color: var(--toast-close);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--easing-standard), color var(--duration-fast) var(--easing-standard);
}

.ds-toast-close svg,
.ds-toast-close i[data-lucide] {
  width: var(--space-5);
  height: var(--space-5);
}

/* Hover: a grey-700 square behind the button, lighter text */
.ds-toast-action:hover,
.ds-toast-action.is-hover {
  background: var(--toast-button-hover);
  color: var(--toast-action-hover);
}

.ds-toast-close:hover,
.ds-toast-close.is-hover {
  background: var(--toast-button-hover);
  color: var(--toast-text);
}

/* Focus (keyboard only): a 2px pink-200 ring, drawn inside the button so
   it shows on the dark toast */
.ds-toast-action:focus-visible,
.ds-toast-action.is-focus,
.ds-toast-close:focus-visible,
.ds-toast-close.is-focus {
  outline: var(--focus-ring-width) solid var(--toast-focus-ring);
  outline-offset: calc(var(--focus-ring-width) * -1);
}

/* Where the toasts appear: bottom left, 24px from the edges. Up to 3
   stack 8px apart, the newest at the bottom. */
.ds-toast-region {
  position: fixed;
  bottom: var(--space-6);
  left: var(--space-6);
  z-index: var(--layer-toast);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  margin: 0;
  pointer-events: none;                             /* the page stays clickable round the toasts */
}

.ds-toast-region .ds-toast {
  pointer-events: auto;
  /* Entering: slides up 8px and fades in over 200ms */
  animation: ds-toast-in var(--duration-normal) var(--easing-standard);
}

/* Leaving: fades out over 150ms (main.js adds .is-leaving, then removes it) */
.ds-toast-region .ds-toast.is-leaving {
  opacity: 0;
  transition: opacity var(--duration-fade) var(--easing-standard);
}

@keyframes ds-toast-in {
  from {
    opacity: 0;
    transform: translateY(var(--space-2));
  }
}

/* With reduced motion: it only fades, no sliding */
@keyframes ds-toast-fade-in {
  from { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ds-toast-region .ds-toast {
    animation-name: ds-toast-fade-in;
  }

  .ds-toast-action,
  .ds-toast-close {
    transition: none;
  }
}

/* The hidden live regions that read the message out */
.ds-toast-live {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Phones: along the bottom, the full width less 16px each side. A page
   with bottom navigation sets --toast-offset-bottom to its height, so
   toasts sit above it. */
@media (max-width: 767px) {
  .ds-toast-region {
    right: var(--space-4);
    bottom: calc(var(--space-4) + var(--toast-offset-bottom, 0px));
    left: var(--space-4);
    align-items: stretch;
  }

  .ds-toast-region .ds-toast {
    width: auto;
    min-width: 0;
    max-width: none;
  }
}

/* Toggle ------------------------------------------------------------------- */
/* Turns one setting on or off, straight away (no Save button). A native
   checkbox with role="switch", styled to pair with the checkbox and radio
   button. The <input> itself is the track (so clicking the track is clicking
   the input), with the thumb drawn on top of it. The whole label is the
   click area: at least 24px tall, 44px on touch screens.
     <label class="ds-toggle" for="email-notifications">
       <span class="ds-toggle-control">
         <input type="checkbox" role="switch" class="ds-toggle-input" id="email-notifications">
         <span class="ds-toggle-thumb" aria-hidden="true">
           <i data-lucide="check" class="ds-toggle-check"></i>
           <span class="ds-spinner ds-toggle-spinner">…spinner ring…</span>
         </span>
       </span>
       <span class="ds-toggle-label">Email notifications</span>
     </label>
   Helper text: wrap the label in a .ds-toggle-field and add a
   .ds-toggle-helper (linked with aria-describedby). --end puts the label on
   the left and the toggle at the right edge (settings lists). State text:
   a .ds-toggle-state (On / Off, aria-hidden) beside the toggle. Settings
   list: <ul class="ds-toggle-list"> of .ds-toggle-list-item rows, 16px top
   and bottom, 1px grey-200 lines between them.
   Sizes: MD 44 × 24px track, 20px thumb, 16px label (default); --sm 32 ×
   18px track, 14px thumb, 14px label. The thumb is 2px inside the track;
   12px from the toggle to the label; 4px from the label to helper text.
   Colours: off = the shared field border (grey-500, 3:1+ on white) with a
   white thumb; on = pink-600 with a white thumb holding a pink-600 check,
   so "on" never relies on colour alone. Hover: grey-700 / pink-700.
   Disabled: grey-200 track, grey-50 thumb, grey-400 label.
   Loading (.is-loading, set by main.js while a change saves): a spinner in
   the thumb, and clicks wait. The thumb slides over 150ms; with reduced
   motion it switches without sliding.
   main.js section 41: Enter flips it too, the state text, and saving
   (data-toggle-save) with Loading after 300ms and switching back on error. */

.ds-toggle {
  --toggle-width: calc(var(--space-10) + var(--space-1));     /* 44px track (MD) */
  --toggle-height: var(--space-6);                            /* 24px */
  --toggle-inset: calc(var(--space-1) / 2);                   /* 2px round the thumb */
  --toggle-thumb: calc(var(--toggle-height) - var(--toggle-inset) * 2);   /* 20px */
  --toggle-check: var(--space-3);                             /* 12px check */
  --toggle-line: var(--type-body-line-height);                /* 24px: the label's line */
  --toggle-on: var(--color-primary);                          /* pink-600 */
  position: relative;
  display: inline-flex;
  align-items: flex-start;
  gap: var(--space-3);                                        /* 12px toggle to label */
  min-height: var(--size-target-minimum);                     /* 24px click area */
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--type-body-size);
  line-height: var(--toggle-line);
  cursor: pointer;
}

.ds-toggle--sm {
  --toggle-width: var(--space-8);                             /* 32px */
  --toggle-height: calc(var(--space-4) + var(--space-1) / 2); /* 18px */
  --toggle-check: calc(var(--space-2) + var(--space-1) / 2);  /* 10px */
  --toggle-line: var(--type-body-small-line-height);          /* 20px */
  font-size: var(--type-body-small-size);
}

/* The track and the thumb share one spot, lined up with the label's first line */
.ds-toggle-control {
  display: inline-grid;
  flex-shrink: 0;
  margin-top: calc((var(--toggle-line) - var(--toggle-height)) / 2);
}

.ds-toggle-input,
.ds-toggle-thumb {
  grid-area: 1 / 1;
}

/* The track: the native input with its own look removed */
.ds-toggle-input {
  box-sizing: border-box;
  width: var(--toggle-width);
  height: var(--toggle-height);
  margin: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: var(--field-border);
  appearance: none;
  cursor: pointer;
  transition: background-color var(--duration-fade) var(--easing-standard);
}

/* The thumb: white, 2px inside the track. Clicks go through to the input. */
.ds-toggle-thumb {
  display: grid;
  place-items: center;
  align-self: center;
  justify-self: start;
  width: var(--toggle-thumb);
  height: var(--toggle-thumb);
  margin-left: var(--toggle-inset);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
  color: var(--toggle-on);
  pointer-events: none;
  transition: transform var(--duration-fade) var(--easing-standard);   /* slides over 150ms */
}

/* The check and the spinner share the middle of the thumb, both hidden at rest */
.ds-toggle-check,
.ds-toggle-thumb .ds-spinner {
  grid-area: 1 / 1;
  visibility: hidden;
}

.ds-toggle-check,
i.ds-toggle-check {
  width: var(--toggle-check);
  height: var(--toggle-check);
  stroke-width: 3;
}

.ds-toggle-thumb .ds-spinner {
  --spinner-size: calc(var(--toggle-thumb) - var(--space-1) * 1.5);   /* 14px (MD), 8px (SM) */
  --spinner-stroke: var(--border-width-thick);                        /* 2px */
}

/* On: a pink-600 track, the thumb at the right with a check in it */
.ds-toggle-input:checked {
  background: var(--toggle-on);
}

.ds-toggle-input:checked + .ds-toggle-thumb {
  transform: translateX(calc(var(--toggle-width) - var(--toggle-thumb) - var(--toggle-inset) * 2));
}

.ds-toggle-input:checked + .ds-toggle-thumb .ds-toggle-check {
  visibility: visible;
}

/* Hover (anywhere on the label): grey-700 off, pink-700 on */
.ds-toggle:hover .ds-toggle-input,
.ds-toggle.is-hover .ds-toggle-input {
  background: var(--field-border-hover);
}

.ds-toggle:hover,
.ds-toggle.is-hover {
  --toggle-on: var(--color-primary-hover);
}

.ds-toggle:hover .ds-toggle-input:checked,
.ds-toggle.is-hover .ds-toggle-input:checked {
  background: var(--toggle-on);
}

/* Focus ring: keyboard focus only, 2px out, round like the track */
.ds-toggle-input:focus-visible,
.ds-toggle.is-focus .ds-toggle-input {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

.ds-toggle-label {
  min-width: 0;
}

/* Disabled: grey-200 track, grey-50 thumb, grey-400 label (and check) */
.ds-toggle:has(.ds-toggle-input:disabled) {
  --toggle-on: var(--color-text-disabled);
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.ds-toggle-input:disabled,
.ds-toggle-input:disabled:checked,
.ds-toggle:hover .ds-toggle-input:disabled {
  background: var(--color-surface-strong);
  cursor: not-allowed;
}

.ds-toggle-input:disabled + .ds-toggle-thumb {
  background: var(--color-surface-muted);
  box-shadow: none;
}

/* Loading: a spinner in the thumb instead of the check, while the change
   saves. Clicks wait until it's done (main.js). */
.ds-toggle.is-loading {
  cursor: progress;
}

.ds-toggle.is-loading .ds-toggle-input {
  cursor: progress;
}

.ds-toggle.is-loading .ds-toggle-thumb .ds-spinner {
  visibility: visible;
}

/* The hidden spinner doesn't turn until it's needed */
.ds-toggle:not(.is-loading) .ds-toggle-thumb .ds-spinner-ring {
  animation: none;
}

.ds-toggle.is-loading .ds-toggle-input:checked + .ds-toggle-thumb .ds-toggle-check {
  visibility: hidden;
}

/* State text: "On" or "Off" beside the toggle, grey-600 14px. Both words
   share one spot, so the width never changes. */
.ds-toggle-state {
  display: inline-grid;
  flex-shrink: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--toggle-line);
}

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

.ds-toggle-state-on,
.ds-toggle:has(.ds-toggle-input:checked) .ds-toggle-state-off {
  visibility: hidden;
}

.ds-toggle:has(.ds-toggle-input:checked) .ds-toggle-state-on {
  visibility: visible;
}

/* Label on the left, toggle at the right edge (settings lists). The order
   on screen: label, state text, toggle. */
.ds-toggle--end {
  display: flex;
  width: 100%;
}

.ds-toggle--end .ds-toggle-label {
  flex: 1;
  order: -2;
}

.ds-toggle--end .ds-toggle-state {
  order: -1;
}

/* A toggle with helper text: stacked, with no extra line space under the
   label (so the helper text is exactly 4px below it) */
.ds-toggle-field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  font-family: var(--font-body);
}

/* Helper text: grey-600, under the label (lined up with it) */
.ds-toggle-helper {
  margin: var(--space-1) 0 0;
  padding-left: calc(var(--space-10) + var(--space-1) + var(--space-3));   /* track + gap: 56px */
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-toggle-field:has(> .ds-toggle--sm) .ds-toggle-helper {
  padding-left: calc(var(--space-8) + var(--space-3));        /* 44px */
}

/* Label on the left: the helper text starts at the left too, and stops
   short of the toggle */
.ds-toggle-field:has(> .ds-toggle--end) {
  align-items: stretch;
}

.ds-toggle-field:has(> .ds-toggle--end) .ds-toggle-helper {
  padding-right: calc(var(--space-10) + var(--space-1) + var(--space-3));
  padding-left: 0;
}

/* ---------- Settings list ---------- */
/* Rows with 16px top and bottom, 1px grey-200 lines between them; every
   toggle lines up at the right edge */
.ds-toggle-list {
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--font-body);
}

.ds-toggle-list-item {
  padding-block: var(--space-4);
}

.ds-toggle-list-item + .ds-toggle-list-item {
  border-top: var(--border-width-thin) solid var(--color-border);
}

/* Slides only without reduced motion */
@media (prefers-reduced-motion: reduce) {
  .ds-toggle-thumb,
  .ds-toggle-input {
    transition: none;
  }
}

/* Touch screens and phones: 44px click areas (the list rows keep their
   16px look: the taller click area makes up the difference) */
@media (pointer: coarse), (max-width: 767px) {
  .ds-toggle {
    min-height: var(--size-control-standard);
    padding-block: calc((var(--size-control-standard) - var(--toggle-line)) / 2);
  }

  .ds-toggle-list-item {
    padding-block: calc(var(--space-4) - (var(--size-control-standard) - var(--type-body-line-height)) / 2);
  }
}

/* Tooltip ------------------------------------------------------------------ */
/* A small label that appears on hover or keyboard focus. Never interactive.

   Any element gets one with data-tooltip="Copy link" (main.js, section 38
   builds it, places it and shows it). Options on the same element:
     data-tooltip-shortcut="Ctrl+B"      a key after the label
     data-tooltip-placement="bottom"     top (default) / bottom / left / right
     data-tooltip-variant="light"        white, for dark areas like the footer
     data-tooltip-truncate               only when the element's text is cut off;
                                         the tooltip shows its full text
   Definition: a term in running text, <button type="button"
   class="ds-tooltip-term" data-tooltip="…">Design tokens</button>, with a
   dotted underline; its tooltip holds a short definition.
   The tooltip itself:
   <div class="ds-tooltip" role="tooltip" popover="manual" data-side="top">
     <span class="ds-tooltip-caret" aria-hidden="true"></span>
     <span class="ds-tooltip-label">Bold</span>
     <kbd class="ds-tooltip-key">Ctrl+B</kbd>     (only with a shortcut)
   </div>
   It sits in the browser's top layer (popover="manual"), so it's never
   clipped and needs no stacking number.

   Sizes: 14px text (20px line height), 6px 12px padding, about 32px tall
   (12px 16px when the text wraps: .ds-tooltip--multiline, added by
   main.js), 12px corners, at most 280px wide. An 8px caret with a 2px
   rounded tip, 8px from the trigger in every placement (like popovers and
   menus). Solid grey-800 with white text (13.48:1) and the
   subtle shadow. */
.ds-tooltip {
  --tooltip-bg: var(--color-surface-inverse);                    /* solid: a softer dark than grey-900 */
  --tooltip-text: var(--color-text-inverse);
  --tooltip-edge: transparent;                      /* the 1px edge: invisible on Default */
  --tooltip-caret-size: var(--space-2);             /* 8px, base to tip */
  --tooltip-caret-tip: calc(var(--space-1) / 2);    /* 2px rounded tip */
  --tooltip-caret-overlap: 0px;                     /* Light: covers the 1px edge */
  --tooltip-caret: 50%;                             /* where the caret points (main.js moves it) */
  position: fixed;
  inset: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  box-sizing: border-box;
  width: max-content;
  max-width: calc(var(--space-20) * 3.5);         /* 280px */
  margin: 0;
  padding: calc(var(--space-1) * 1.5) var(--space-3);   /* 6px 12px: 32px tall */
  overflow: visible;
  border: 0;
  /* The edge is an outline drawn just inside, so it adds no size. It also
     keeps the tooltip visible in Windows high contrast mode. */
  outline: var(--border-width-thin) solid var(--tooltip-edge);
  outline-offset: calc(var(--border-width-thin) * -1);
  border-radius: var(--radius-container);                 /* 12px */
  background: var(--tooltip-bg);
  box-shadow: var(--shadow-md);
  color: var(--tooltip-text);
  font-family: var(--font-body);
  font-size: var(--type-body-small-size);          /* 14px */
  font-weight: var(--font-weight-regular);
  line-height: var(--type-body-small-line-height); /* 20px */
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Multi-line: more room around text that wraps (main.js adds the class
   when the text runs onto a second line) */
.ds-tooltip--multiline {
  padding: var(--space-3) var(--space-4);          /* 12px 16px */
}

/* The popover attribute hides it until main.js shows it */
.ds-tooltip[popover]:not(:popover-open) {
  display: none;
}

/* Fades in over 150ms; none with reduced motion */
.ds-tooltip:popover-open {
  animation: ds-tooltip-in var(--duration-fade) var(--easing-standard);
}

@keyframes ds-tooltip-in {
  from { opacity: 0; }
}

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

/* Light: white, a 1px grey-200 edge and the subtle shadow. For dark areas
   like the footer. */
.ds-tooltip--light {
  --tooltip-bg: var(--color-surface);
  --tooltip-text: var(--color-text);
  --tooltip-edge: var(--color-border);
  --tooltip-caret-overlap: var(--border-width-thin);
}

.ds-tooltip-label {
  min-width: 0;
}

/* Shortcut key: 12px medium on grey-600 (white text 6.61:1), 4px corners,
   2px 6px padding, 8px after the label */
.ds-tooltip-key {
  flex-shrink: 0;
  padding: calc(var(--space-1) / 2) calc(var(--space-1) * 1.5);   /* 2px 6px */
  border-radius: var(--radius-sm);
  background: var(--color-surface-inverse-raised);
  color: var(--color-text-inverse);
  font: inherit;
  font-size: var(--type-caption-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--type-caption-line-height);
  white-space: nowrap;
}

.ds-tooltip--light .ds-tooltip-key {
  background: var(--color-surface-subtle);
  color: var(--color-text-secondary);
}

/* Caret: an 8px triangle with a 2px rounded tip, the same colour as the
   tooltip. It's a square turned 45deg with one rounded corner (the tip);
   clip-path keeps only the half outside the tooltip. Its border draws the Light edge. */
.ds-tooltip-caret {
  position: absolute;
  box-sizing: border-box;
  /* the square's side: an 8px tall triangle (plus the 1px it overlaps on Light) */
  width: calc((var(--tooltip-caret-size) + var(--tooltip-caret-overlap)) * 1.4142);
  height: calc((var(--tooltip-caret-size) + var(--tooltip-caret-overlap)) * 1.4142);
  border: var(--border-width-thin) solid var(--tooltip-edge);
  border-bottom-right-radius: var(--tooltip-caret-tip);
  background: var(--tooltip-bg);
  clip-path: polygon(100% 0, 100% 100%, 0 100%);
  pointer-events: none;
}

/* Where the caret goes: centred on --tooltip-caret along the side facing the
   trigger, its base on the tooltip's edge, the tip pointing at the trigger */
.ds-tooltip-caret {
  left: var(--tooltip-caret);
  top: calc(100% - var(--tooltip-caret-overlap));
  transform: translate(-50%, -50%) rotate(45deg);
}

.ds-tooltip[data-side="bottom"] .ds-tooltip-caret {
  top: var(--tooltip-caret-overlap);
  transform: translate(-50%, -50%) rotate(225deg);
}

.ds-tooltip[data-side="left"] .ds-tooltip-caret {
  top: var(--tooltip-caret);
  left: calc(100% - var(--tooltip-caret-overlap));
  transform: translate(-50%, -50%) rotate(-45deg);
}

.ds-tooltip[data-side="right"] .ds-tooltip-caret {
  top: var(--tooltip-caret);
  left: var(--tooltip-caret-overlap);
  transform: translate(-50%, -50%) rotate(135deg);
}

/* Definition: a term in running text with a 1px dotted grey-500 underline.
   It's a button, so keyboard users can reach it; tapping it shows the
   definition too (no hover on touch screens). */
.ds-tooltip-term {
  display: inline;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
  text-decoration: underline dotted var(--color-text-subtle);
  text-decoration-thickness: var(--border-width-thin);
  text-underline-offset: var(--link-underline-offset);
  cursor: help;
}

.ds-tooltip-term:focus-visible,
.ds-tooltip-term.is-focus {
  border-radius: var(--radius-sm);
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}


/* ==========================================================================
   Form inputs
   ========================================================================== */

/* Checkbox ----------------------------------------------------------------- */
/* A native checkbox, styled. The <input> itself is the box (so clicking the
   box is clicking the input), with the tick or dash drawn on top of it. The
   whole label is the click area: at least 24px tall, 44px on touch screens.
     <div class="ds-checkbox-field">
       <label class="ds-checkbox" for="updates">
         <span class="ds-checkbox-control">
           <input type="checkbox" class="ds-checkbox-input" id="updates" aria-describedby="updates-help">
           <span class="ds-checkbox-icon" aria-hidden="true">
             <i data-lucide="check" class="ds-checkbox-check"></i>
             <i data-lucide="minus" class="ds-checkbox-dash"></i>
           </span>
         </span>
         <span class="ds-checkbox-label">Email me updates</span>
       </label>
       <p class="ds-checkbox-helper" id="updates-help">About once a month.</p>   (optional)
     </div>
   Groups: <fieldset class="ds-checkbox-group"> with a <legend
   class="ds-checkbox-legend"> (8px above the first checkbox), then the
   checkboxes in a .ds-checkbox-options, 12px apart; --horizontal puts 2 or 3 short ones in a row (stacked on
   phones). One .ds-checkbox-error for the group, linked with
   aria-describedby. Parent and child: [data-checkbox-parent] with the
   children in a .ds-checkbox-children list (main.js section 32 keeps them
   in step and sets the parent's indeterminate property).
   Selection card: .ds-checkbox-card, the whole card is the label.
   Sizes: MD 20px box and 16px label (default), --sm 16px box and 14px
   label (tables, dense lists, menus). 4px corners, 8px from box to label.
   States: hover (grey-700 border, or pink-700 fill), focus ring on keyboard
   focus only, disabled, read-only (aria-readonly="true": main.js stops it
   changing), error (.ds-checkbox-group--error or .ds-checkbox--error).
   .ds-checkbox-box is the same box as a picture (no input), e.g. in the
   dropdown multi-select menu: add .is-checked or .is-mixed. */

.ds-checkbox {
  --checkbox-size: var(--space-5);              /* 20px box (MD) */
  --checkbox-icon: calc(var(--space-3) + var(--border-width-thick));   /* 14px tick */
  --checkbox-line: var(--type-body-line-height); /* 24px: the label's line */
  position: relative;
  display: inline-flex;
  align-items: flex-start;
  gap: var(--space-2);                          /* 8px box to label */
  min-height: var(--size-target-minimum);       /* 24px click area */
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--type-body-size);
  line-height: var(--checkbox-line);
  cursor: pointer;
}

.ds-checkbox--sm,
.ds-checkbox-group--sm .ds-checkbox {
  --checkbox-size: var(--space-4);              /* 16px box */
  --checkbox-icon: var(--space-3);              /* 12px tick */
  --checkbox-line: var(--type-body-small-line-height);   /* 20px */
  font-size: var(--type-body-small-size);
}

/* The box and its icon share one spot, lined up with the label's first line */
.ds-checkbox-control {
  display: inline-grid;
  flex-shrink: 0;
  margin-top: calc((var(--checkbox-line) - var(--checkbox-size)) / 2);
}

.ds-checkbox-input,
.ds-checkbox-icon {
  grid-area: 1 / 1;
}

/* The box: the native input with its own look removed */
.ds-checkbox-input,
.ds-checkbox-box {
  box-sizing: border-box;
  width: var(--checkbox-size, var(--space-5));
  height: var(--checkbox-size, var(--space-5));
  margin: 0;
  border: var(--border-width-thin) solid var(--field-border);   /* 3:1+ on white */
  border-radius: var(--radius-sm);              /* 4px */
  background: var(--color-surface);
  appearance: none;
  cursor: pointer;
}

.ds-checkbox-box--sm {
  --checkbox-size: var(--space-4);
  --checkbox-icon: var(--space-3);
}

/* The tick or dash, white, centred on the box. Clicks go through to the input. */
.ds-checkbox-icon,
.ds-checkbox-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-on-primary);
}

.ds-checkbox-icon {
  pointer-events: none;
}

.ds-checkbox-icon svg,
.ds-checkbox-icon i[data-lucide],
.ds-checkbox-box svg,
.ds-checkbox-box i[data-lucide] {
  display: none;
  width: var(--checkbox-icon, var(--space-3));
  height: var(--checkbox-icon, var(--space-3));
}

/* Checked and indeterminate: pink-600 fill */
.ds-checkbox-input:checked,
.ds-checkbox-input:indeterminate,
.ds-checkbox-box.is-checked,
.ds-checkbox-box.is-mixed {
  border-color: var(--color-primary);
  background: var(--color-primary);
}

.ds-checkbox-input:checked:not(:indeterminate) + .ds-checkbox-icon .ds-checkbox-check,
.ds-checkbox-input:indeterminate + .ds-checkbox-icon .ds-checkbox-dash,
.ds-checkbox-box.is-checked .ds-checkbox-check,
.ds-checkbox-box.is-mixed .ds-checkbox-dash {
  display: block;
}

/* Hover (anywhere on the label): a darker border, or a darker fill */
.ds-checkbox:hover .ds-checkbox-input:not(:checked):not(:indeterminate),
.ds-checkbox.is-hover .ds-checkbox-input:not(:checked):not(:indeterminate) {
  border-color: var(--field-border-hover);
}

.ds-checkbox:hover .ds-checkbox-input:is(:checked, :indeterminate),
.ds-checkbox.is-hover .ds-checkbox-input:is(:checked, :indeterminate) {
  border-color: var(--color-primary-hover);
  background: var(--color-primary-hover);
}

/* Focus ring: keyboard focus only */
.ds-checkbox-input:focus-visible,
.ds-checkbox.is-focus .ds-checkbox-input {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

.ds-checkbox-label {
  min-width: 0;
}

/* A checkbox with helper text: stacked, with no extra line space under the
   label (so the helper text is exactly 4px below it) */
.ds-checkbox-field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  font-family: var(--font-body);
}

/* Helper text: grey-600, under the label (lined up with it) */
.ds-checkbox-helper {
  margin: var(--space-1) 0 0;
  padding-left: calc(var(--space-5) + var(--space-2));   /* box + gap */
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-checkbox-group--sm .ds-checkbox-helper {
  padding-left: calc(var(--space-4) + var(--space-2));
}

/* Disabled: grey-100 fill, grey-300 border, grey-400 label */
.ds-checkbox:has(.ds-checkbox-input:disabled) {
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.ds-checkbox-input:disabled,
.ds-checkbox:hover .ds-checkbox-input:disabled {
  border-color: var(--color-border-disabled);
  background: var(--color-disabled-bg);
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.ds-checkbox-input:disabled + .ds-checkbox-icon {
  color: var(--color-text-disabled);
}

/* Read-only: shows its value, can't be changed. Quiet grey, no hover. */
.ds-checkbox:has(.ds-checkbox-input[aria-readonly="true"]) {
  cursor: default;
}

.ds-checkbox-input[aria-readonly="true"],
.ds-checkbox:hover .ds-checkbox-input[aria-readonly="true"]:not(:checked):not(:indeterminate) {
  border-color: var(--color-border-strong);
  background: var(--color-surface-muted);
  cursor: default;
}

.ds-checkbox-input[aria-readonly="true"]:is(:checked, :indeterminate),
.ds-checkbox:hover .ds-checkbox-input[aria-readonly="true"]:is(:checked, :indeterminate) {
  border-color: var(--color-text-muted);
  background: var(--color-text-muted);
}

/* Error: red border (red fill when ticked) */
.ds-checkbox--error .ds-checkbox-input,
.ds-checkbox-group--error .ds-checkbox-input,
.ds-checkbox--error:hover .ds-checkbox-input:not(:checked):not(:indeterminate),
.ds-checkbox-group--error .ds-checkbox:hover .ds-checkbox-input:not(:checked):not(:indeterminate) {
  border-color: var(--color-error);
}

.ds-checkbox--error .ds-checkbox-input:is(:checked, :indeterminate),
.ds-checkbox-group--error .ds-checkbox-input:is(:checked, :indeterminate) {
  background: var(--color-error);
}

/* One message for the group (or the single checkbox): icon and text */
.ds-checkbox-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: var(--space-2) 0 0;
  color: var(--color-error);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-checkbox-error svg,
.ds-checkbox-error i[data-lucide] {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  margin-top: calc(var(--space-1) / 2);
}

.ds-checkbox-error[hidden] {
  display: none;
}

/* ---------- Groups ---------- */
/* <fieldset class="ds-checkbox-group"> > <legend class="ds-checkbox-legend">,
   then the checkboxes in a .ds-checkbox-options, then the error message */
.ds-checkbox-group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  font-family: var(--font-body);
}

.ds-checkbox-options {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);       /* 12px between checkboxes */
}

/* The legend: 8px above the first checkbox */
.ds-checkbox-legend {
  margin-bottom: var(--space-2);
  padding: 0;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

/* Horizontal: 2 or 3 short options in a row, 24px apart */
.ds-checkbox-group--horizontal .ds-checkbox-options {
  flex-direction: row;
  flex-wrap: wrap;
  column-gap: var(--space-6);
}

/* Parent and child: the parent, then its children 12px below */
.ds-checkbox-nested {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Parent and child: the children indented under the parent's label */
.ds-checkbox-children {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0 0 0 calc(var(--space-5) + var(--space-2));
  list-style: none;
}

.ds-checkbox-children > li {
  display: flex;   /* no extra line space under the label */
}

/* ---------- Selection card ---------- */
/* The whole card is the label. 1px grey-200 border, 12px corners, 16px
   padding; selected: pink-600 border, pink-50 background. */
.ds-checkbox-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  cursor: pointer;
  font-family: var(--font-body);
}

.ds-checkbox-card:hover,
.ds-checkbox-card.is-hover {
  border-color: var(--color-border-strong);
}

.ds-checkbox-card:has(.ds-checkbox-input:checked) {
  border-color: var(--color-primary);
  background: var(--color-primary-subtle);
}

/* The card shows the focus ring (keyboard only), not the small box */
.ds-checkbox-card:has(.ds-checkbox-input:focus-visible),
.ds-checkbox-card.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

.ds-checkbox-card .ds-checkbox-input:focus-visible {
  outline: 0;
}

.ds-checkbox-card .ds-checkbox-control {
  --checkbox-size: var(--space-5);
  --checkbox-icon: calc(var(--space-3) + var(--border-width-thick));
  margin-top: calc((var(--type-body-line-height) - var(--space-5)) / 2);
}

.ds-checkbox-card-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.ds-checkbox-card-title {
  color: var(--color-text);
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-line-height);
}

.ds-checkbox-card-description {
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* Selection cards in a group: 8px apart */
.ds-checkbox-cards {
  display: grid;
  gap: var(--space-2);
}

/* Touch screens and phones: 44px click areas, horizontal groups stack */
@media (pointer: coarse), (max-width: 767px) {
  .ds-checkbox {
    min-height: var(--size-control-standard);
    padding-block: calc((var(--size-control-standard) - var(--checkbox-line)) / 2);
  }

  .ds-checkbox-options,
  .ds-checkbox-nested,
  .ds-checkbox-children {
    gap: 0;                  /* the 44px click areas space them out */
  }
}

@media (max-width: 767px) {
  .ds-checkbox-group--horizontal .ds-checkbox-options {
    flex-direction: column;
  }
}


/* Date picker -------------------------------------------------------------- */
/* Typing a date (DD/MM/YYYY) or choosing it from a calendar. UK format,
   weeks start on Monday.
     <div class="ds-datepicker" data-datepicker data-mode="single">
       <label class="ds-datepicker-label" for="due">Due date</label>
       <div class="ds-datepicker-control">
         <div class="ds-datepicker-field">
           <input class="ds-datepicker-input" id="due" placeholder="DD/MM/YYYY"
                  inputmode="numeric" autocomplete="off" aria-describedby="due-hint">
           <button type="button" class="ds-button ds-button--tertiary ds-button--icon ds-button--sm ds-datepicker-button"
                   aria-label="Choose date" aria-expanded="false" aria-controls="due-calendar">
             <i data-lucide="calendar" aria-hidden="true"></i></button>
         </div>
         <div class="ds-datepicker-popup" id="due-calendar" role="dialog" aria-label="Choose date" hidden>
           <div class="ds-calendar">…</div>
         </div>
       </div>
       <p class="ds-datepicker-hint" id="due-hint">For example, 21/03/2026</p>
     </div>
   data-mode: single, range (two fields, two months), presets (range with a
   list of presets and Cancel / Apply), month (a 12-month grid). Limits:
   data-min="today", data-no-weekends. Field sizes: --sm 32px, MD 44px,
   --lg 56px, like dropdowns and buttons.
   The calendar (.ds-calendar) is drawn by main.js (section 33): a header
   (Previous month, "March 2026", Next month), a weekday row (Mo–Su) and 6
   weeks of 40px days, 4px apart, 16px padding (one month 336px; two months
   664px with 24px between them). Days: today has a pink-600 dot; selected,
   range start and end are solid pink-600 (the date is the value); days in
   a range are pink-50; disabled days are grey-300 and struck through;
   days from other months are grey-400.
   The popup sits 8px below the field (1px grey-200 border, --shadow-md,
   12px corners) and flips above when there's no room. On phones it's a
   bottom sheet with 44px days. */

.ds-datepicker {
  --field-height: var(--size-control-standard);   /* 44px (MD) */
  --field-radius: var(--radius-control);       /* 12px */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  min-width: 0;
  font-family: var(--font-body);
}

.ds-datepicker--sm { --field-height: var(--size-control-compact); --field-radius: var(--radius-control-sm); }                 /* 32px, 8px corners */
.ds-datepicker--lg { --field-height: calc(var(--space-12) + var(--space-2)); --field-radius: var(--radius-control); }      /* 56px, 12px corners */

.ds-datepicker-label {
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

/* The field and its popup: the popup is placed from here */
.ds-datepicker-control {
  position: relative;
  width: 100%;
  max-width: calc(var(--space-24) * 3);   /* 288px, like dropdowns */
}

/* Range: the start and end fields side by side, 8px apart */
.ds-datepicker-range {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-2);
}

.ds-datepicker-range > .ds-datepicker-part {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-2);
  min-width: calc(var(--space-24) + var(--space-16));   /* 160px */
}

.ds-datepicker--range .ds-datepicker-control {
  max-width: none;
}

/* The field: the shared field style (1px --field-border, 8 / 12 / 12px corners by size); the calendar button inside */
.ds-datepicker-field {
  display: flex;
  align-items: center;
  box-sizing: border-box;
  width: 100%;
  min-height: var(--field-height);
  padding: 0 var(--space-1) 0 var(--space-3);
  border: var(--border-width-thin) solid var(--field-border);   /* the shared field style */
  border-radius: var(--field-radius);
  background: var(--color-surface);
}

.ds-datepicker--lg .ds-datepicker-field { padding-left: var(--space-4); }

.ds-datepicker-input {
  flex: 1;
  min-width: 0;
  height: calc(var(--field-height) - var(--border-width-thin) * 2);
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: var(--type-body-size);
  font-variant-numeric: tabular-nums;
  outline: none;
}

.ds-datepicker--sm .ds-datepicker-input { font-size: var(--type-body-small-size); }

.ds-datepicker-input::placeholder {
  color: var(--color-text-subtle);
}

.ds-datepicker-button {
  flex-shrink: 0;
}

.ds-datepicker--sm .ds-datepicker-button {
  --button-height: var(--size-target-minimum);   /* 24px inside a 32px field */
  --button-radius: var(--radius-control-xs);     /* 6px */
}

/* Field states */
.ds-datepicker-field:hover,
.ds-datepicker-field.is-hover {
  border-color: var(--field-border-hover);
}

.ds-datepicker-field:has(.ds-datepicker-input:focus),
.ds-datepicker-field.is-focus {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 calc(var(--border-width-thin) * 3) var(--field-focus-ring);
}

.ds-datepicker-field.is-open,
.ds-datepicker-field:has(.ds-datepicker-button[aria-expanded="true"]) {
  border-color: var(--field-border-active);
}

.ds-datepicker-field:has(.ds-datepicker-input:disabled) {
  border-color: var(--color-border-disabled);
  background: var(--color-surface-muted);
}

.ds-datepicker-input:disabled {
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.ds-datepicker-field:has(.ds-datepicker-input[readonly]) {
  border-color: var(--color-surface-muted);   /* no visible border */
  background: var(--color-surface-muted);
}

.ds-datepicker-field:has(.ds-datepicker-input[readonly]) .ds-datepicker-button {
  display: none;   /* read-only: no calendar */
}

.ds-datepicker--error .ds-datepicker-field,
.ds-datepicker-field.is-error {
  border-color: var(--color-error);
}

/* Format hint and error */
.ds-datepicker-hint {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-datepicker-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: 0;
  color: var(--color-error);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-datepicker-error svg,
.ds-datepicker-error i[data-lucide] {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  margin-top: calc(var(--space-1) / 2);
}

.ds-datepicker-error[hidden] {
  display: none;
}

/* ---------- The popup ---------- */
/* 8px below the field (or above, --up), grey-200 border, subtle shadow */
.ds-datepicker-popup {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: var(--layer-dropdown);
  overflow: visible;
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.ds-datepicker-popup[hidden] {
  display: none;
}

.ds-datepicker-popup--up {
  top: auto;
  bottom: calc(100% + var(--space-2));
}

/* Presets beside the calendar, the footer below */
.ds-datepicker-body {
  display: flex;
}

/* Presets: a 160px list on the left */
.ds-datepicker-presets {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  box-sizing: border-box;
  width: calc(var(--space-24) + var(--space-16));   /* 160px */
  padding: var(--space-4) var(--space-2);
  border-right: var(--border-width-thin) solid var(--color-border);
}

.ds-datepicker-preset {
  min-height: var(--size-control-compact);
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  text-align: left;
  cursor: pointer;
}

.ds-datepicker-preset:hover,
.ds-datepicker-preset.is-hover {
  background: var(--color-hover-bg);
}

/* The chosen preset: the selected style (pink-50, pink-700) */
.ds-datepicker-preset[aria-pressed="true"] {
  background: var(--color-primary-subtle);
  color: var(--color-selected-text);
  font-weight: var(--font-weight-medium);
}

.ds-datepicker-preset:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: calc(var(--focus-ring-width) * -1);
}

/* Footer: the range in words, then Cancel and Apply */
.ds-datepicker-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-top: var(--border-width-thin) solid var(--color-border);
}

.ds-datepicker-summary {
  margin-right: auto;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* ---------- The calendar ---------- */
.ds-calendar {
  --day-size: var(--space-10);            /* 40px */
  --day-gap: var(--space-1);              /* 4px */
  display: flex;
  gap: var(--space-6);                    /* 24px between two months */
  padding: var(--space-4);                /* 16px */
  font-family: var(--font-body);
}

/* Inline: on the page, with its own border */
.ds-calendar--inline {
  display: inline-flex;
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
}

.ds-calendar-month {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: calc(var(--day-size) * 7 + var(--day-gap) * 6);   /* 304px */
}

/* Header: Previous month, the month and year, Next month */
.ds-calendar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.ds-calendar-title {
  flex: 1;
  margin: 0;
  color: var(--color-text);
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-line-height);
  text-align: center;
}

/* In two months, only the outer buttons show */
.ds-calendar-header .is-spacer {
  visibility: hidden;
}

.ds-calendar-grid,
.ds-calendar-weeks {
  display: flex;
  flex-direction: column;
  gap: var(--day-gap);
}

.ds-calendar-weekdays,
.ds-calendar-week {
  display: grid;
  grid-template-columns: repeat(7, var(--day-size));
  gap: var(--day-gap);
}

.ds-calendar-weekday {
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--type-body-small-line-height);
  text-align: center;
}

/* A day: 40px square, 8px corners */
.ds-calendar-day {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--day-size);
  height: var(--day-size);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  user-select: none;
}

.ds-calendar-day:hover,
.ds-calendar-day.is-hover {
  background: var(--color-hover-bg);
}

/* Today: a 4px pink-600 dot under the number */
.ds-calendar-day[aria-current="date"]::after,
.ds-calendar-day.is-today::after {
  content: "";
  position: absolute;
  bottom: calc(var(--space-1) + var(--border-width-thin));
  left: 50%;
  width: var(--space-1);
  height: var(--space-1);
  border-radius: var(--radius-full);
  background: var(--color-primary);
  transform: translateX(-50%);
}

/* Days in a range: pink-50, joined across the 4px gaps */
.ds-calendar-day.is-in-range {
  border-radius: 0;
  background: var(--color-primary-subtle);
}

.ds-calendar-day.is-in-range:not(:first-child),
.ds-calendar-day.is-range-end:not(.is-range-start):not(:first-child) {
  box-shadow: calc(var(--day-gap) * -1) 0 0 var(--color-primary-subtle);
}

/* Selected, range start and end: solid pink-600, white text */
.ds-calendar-day[aria-selected="true"],
.ds-calendar-day.is-selected,
.ds-calendar-day.is-range-start,
.ds-calendar-day.is-range-end {
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-weight: var(--font-weight-semibold);
}

.ds-calendar-day[aria-selected="true"]::after,
.ds-calendar-day.is-selected::after,
.ds-calendar-day.is-range-start::after,
.ds-calendar-day.is-range-end::after {
  background: var(--color-on-primary);   /* today's dot turns white */
}

/* Days from the months either side */
.ds-calendar-day.is-outside {
  color: var(--color-text-faint);
}

/* Can't be chosen: grey-300, struck through (not colour alone) */
.ds-calendar-day[aria-disabled="true"] {
  background: none;
  box-shadow: none;
  color: var(--color-text-disabled-subtle);
  text-decoration: line-through;
  cursor: not-allowed;
}

/* Keyboard focus only */
.ds-calendar-day:focus {
  outline: none;
}

.ds-calendar-day:focus-visible,
.ds-calendar-day.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

/* Month and year: 12 months in a 3 × 4 grid */
.ds-calendar-months-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
}

.ds-calendar-months-grid .ds-calendar-day {
  width: auto;
}

/* Tablets and phones: a range shows one month (with both arrows) */
@media (max-width: 1023px) {
  .ds-datepicker-popup .ds-calendar-month + .ds-calendar-month {
    display: none;
  }

  .ds-datepicker-popup .ds-calendar-header .is-spacer {
    visibility: visible;
  }
}

/* ---------- Phones: a bottom sheet, one month, 44px days ---------- */
.ds-datepicker-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--layer-overlay);
  background: var(--color-overlay);
}

.ds-datepicker-overlay[hidden] {
  display: none;
}

@media (max-width: 767px) {
  .ds-datepicker-popup.is-sheet {
    position: fixed;
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: var(--layer-drawer);
    max-height: calc(var(--layout-full-height) * 0.9);
    overflow-y: auto;
    border-radius: var(--radius-container) var(--radius-container) 0 0;
  }

  .is-sheet .ds-calendar {
    --day-size: var(--size-control-standard);   /* 44px */
    --day-gap: var(--space-0);
    justify-content: center;
  }

  .is-sheet .ds-calendar-month + .ds-calendar-month {
    display: none;   /* one month */
  }

  .is-sheet .ds-calendar-header .is-spacer {
    visibility: visible;
  }

  /* Presets: a scrolling row above the calendar */
  .is-sheet .ds-datepicker-body {
    flex-direction: column;
  }

  .is-sheet .ds-datepicker-presets {
    flex-direction: row;
    width: auto;
    overflow-x: auto;
    border-right: 0;
    border-bottom: var(--border-width-thin) solid var(--color-border);
  }

  .is-sheet .ds-datepicker-preset {
    flex-shrink: 0;
    white-space: nowrap;
  }

  /* The summary, then Apply (full width, on top: stacked buttons put the
     primary first), then Cancel */
  .is-sheet .ds-datepicker-footer .ds-button,
  .is-sheet .ds-datepicker-summary {
    flex-basis: 100%;
  }

  .is-sheet .ds-datepicker-summary { order: -2; }
  .is-sheet .ds-datepicker-footer .ds-button--primary { order: -1; }

  .is-sheet .ds-datepicker-presets {
    scrollbar-width: thin;
  }
}


/* Radio button ------------------------------------------------------------- */
/* A native radio button, styled to pair with the checkbox. The <input> itself
   is the circle (so clicking the circle is clicking the input), with the dot
   drawn on top of it. The whole label is the click area: at least 24px tall,
   44px on touch screens.
     <fieldset class="ds-radio-group" aria-describedby="delivery-error">
       <legend class="ds-radio-legend">Delivery</legend>
       <div class="ds-radio-options">
         <label class="ds-radio" for="delivery-standard">
           <span class="ds-radio-control">
             <input type="radio" class="ds-radio-input" id="delivery-standard" name="delivery" value="standard">
             <span class="ds-radio-dot" aria-hidden="true"></span>
           </span>
           <span class="ds-radio-label">Standard</span>
         </label>
         …
       </div>
       <p class="ds-radio-error" id="delivery-error" hidden>…icon… Choose a delivery option</p>
     </fieldset>
   Helper text: wrap the label in a .ds-radio-field and add a
   .ds-radio-helper (linked with aria-describedby). Follow-up field: put a
   .ds-radio-followup (holding a Text input) in the same .ds-radio-field; it
   shows only while its option is chosen. --horizontal puts 2 or 3 short
   options in a row (stacked on phones). Selection card: .ds-radio-card, the
   whole card is the label.
   Sizes: MD 20px circle, 8px dot, 16px label (default); --sm 16px circle,
   6px dot, 14px label. 8px from circle to label, like the checkbox.
   States: hover (grey-700 border, or pink-700 when chosen), focus ring on
   keyboard focus only, disabled, read-only (.ds-radio-group--readonly with
   role="radiogroup" aria-readonly="true", or .ds-radio--readonly: main.js
   stops it changing), error (.ds-radio-group--error or .ds-radio--error).
   main.js section 35 runs read-only, validation and the follow-up field. */

.ds-radio {
  --radio-size: var(--space-5);                 /* 20px circle (MD) */
  --radio-dot: var(--space-2);                  /* 8px dot */
  --radio-line: var(--type-body-line-height);   /* 24px: the label's line */
  position: relative;
  display: inline-flex;
  align-items: flex-start;
  gap: var(--space-2);                          /* 8px circle to label */
  min-height: var(--size-target-minimum);       /* 24px click area */
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--type-body-size);
  line-height: var(--radio-line);
  cursor: pointer;
}

.ds-radio--sm,
.ds-radio-group--sm .ds-radio {
  --radio-size: var(--space-4);                 /* 16px circle */
  --radio-dot: calc(var(--space-1) + var(--border-width-thick));   /* 6px dot */
  --radio-line: var(--type-body-small-line-height);                /* 20px */
  font-size: var(--type-body-small-size);
}

/* The circle and its dot share one spot, lined up with the label's first line */
.ds-radio-control {
  display: inline-grid;
  flex-shrink: 0;
  margin-top: calc((var(--radio-line) - var(--radio-size)) / 2);
}

.ds-radio-input,
.ds-radio-dot {
  grid-area: 1 / 1;
}

/* The circle: the native input with its own look removed. Unselected: white,
   with the shared field border (grey-500, 3:1+ on white). */
.ds-radio-input {
  box-sizing: border-box;
  width: var(--radio-size);
  height: var(--radio-size);
  margin: 0;
  border: var(--border-width-thin) solid var(--field-border);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  appearance: none;
  cursor: pointer;
}

/* The dot, centred on the circle. Clicks go through to the input. */
.ds-radio-dot {
  place-self: center;
  width: var(--radio-dot);
  height: var(--radio-dot);
  border-radius: var(--radius-full);
  background: var(--color-primary);
  visibility: hidden;
  pointer-events: none;
}

/* Selected: a pink-600 ring with a pink-600 dot in the centre */
.ds-radio-input:checked {
  border-color: var(--color-primary);
}

.ds-radio-input:checked + .ds-radio-dot {
  visibility: visible;
}

/* Hover (anywhere on the label): a darker border, or a darker ring and dot */
.ds-radio:hover .ds-radio-input:not(:checked),
.ds-radio.is-hover .ds-radio-input:not(:checked) {
  border-color: var(--field-border-hover);
}

.ds-radio:hover .ds-radio-input:checked,
.ds-radio.is-hover .ds-radio-input:checked {
  border-color: var(--color-primary-hover);
}

.ds-radio:hover .ds-radio-input:checked + .ds-radio-dot,
.ds-radio.is-hover .ds-radio-input:checked + .ds-radio-dot {
  background: var(--color-primary-hover);
}

/* Focus ring: keyboard focus only, round like the circle */
.ds-radio-input:focus-visible,
.ds-radio.is-focus .ds-radio-input {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

.ds-radio-label {
  min-width: 0;
}

/* An option with helper text or a follow-up field: stacked, with no extra
   line space under the label (so the helper text is exactly 4px below it) */
.ds-radio-field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  font-family: var(--font-body);
}

/* Helper text: grey-600, under the label (lined up with it) */
.ds-radio-helper {
  margin: var(--space-1) 0 0;
  padding-left: calc(var(--space-5) + var(--space-2));   /* circle + gap: 28px */
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-radio-group--sm .ds-radio-helper {
  padding-left: calc(var(--space-4) + var(--space-2));
}

/* Follow-up field: a Text input under its option, indented 28px so it lines
   up with the label, 8px below it. Shows only while its option is chosen. */
.ds-radio-followup {
  align-self: stretch;
  margin-top: var(--space-2);
  padding-left: calc(var(--space-5) + var(--space-2));
}

.ds-radio-field:not(:has(> .ds-radio .ds-radio-input:checked)) > .ds-radio-followup {
  display: none;
}

/* Disabled: grey-100 fill, grey-300 border, grey-400 label (and dot) */
.ds-radio:has(.ds-radio-input:disabled) {
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.ds-radio-input:disabled,
.ds-radio:hover .ds-radio-input:disabled {
  border-color: var(--color-border-disabled);
  background: var(--color-disabled-bg);
  cursor: not-allowed;
}

.ds-radio-input:disabled + .ds-radio-dot,
.ds-radio:hover .ds-radio-input:disabled + .ds-radio-dot {
  background: var(--color-text-disabled);
}

/* Read-only: shows its value, can't be changed. Quiet grey, no hover. */
.ds-radio--readonly,
.ds-radio-group--readonly .ds-radio {
  cursor: default;
}

.ds-radio--readonly .ds-radio-input,
.ds-radio-group--readonly .ds-radio-input,
.ds-radio--readonly:hover .ds-radio-input:not(:checked),
.ds-radio-group--readonly .ds-radio:hover .ds-radio-input:not(:checked) {
  border-color: var(--color-border-strong);
  background: var(--color-surface-muted);
  cursor: default;
}

.ds-radio--readonly .ds-radio-input:checked,
.ds-radio-group--readonly .ds-radio-input:checked,
.ds-radio--readonly:hover .ds-radio-input:checked,
.ds-radio-group--readonly .ds-radio:hover .ds-radio-input:checked {
  border-color: var(--color-text-muted);
}

.ds-radio--readonly .ds-radio-dot,
.ds-radio-group--readonly .ds-radio-dot,
.ds-radio--readonly:hover .ds-radio-input:checked + .ds-radio-dot,
.ds-radio-group--readonly .ds-radio:hover .ds-radio-input:checked + .ds-radio-dot {
  background: var(--color-text-muted);
}

/* Error: red border (red ring and dot when chosen) */
.ds-radio--error .ds-radio-input,
.ds-radio-group--error .ds-radio-input,
.ds-radio--error:hover .ds-radio-input:not(:checked),
.ds-radio-group--error .ds-radio:hover .ds-radio-input:not(:checked) {
  border-color: var(--color-error);
}

.ds-radio--error .ds-radio-dot,
.ds-radio-group--error .ds-radio-dot {
  background: var(--color-error);
}

/* One message for the group: icon and text */
.ds-radio-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: var(--space-2) 0 0;
  color: var(--color-error);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-radio-error svg,
.ds-radio-error i[data-lucide] {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  margin-top: calc(var(--space-1) / 2);
}

.ds-radio-error[hidden] {
  display: none;
}

/* ---------- Groups ---------- */
/* <fieldset class="ds-radio-group"> > <legend class="ds-radio-legend">, then
   the options in a .ds-radio-options, then the error message */
.ds-radio-group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  font-family: var(--font-body);
}

.ds-radio-options {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);       /* 12px between options */
}

/* Options with a follow-up field stretch, so the field can be full width */
.ds-radio-options > .ds-radio-field:has(.ds-radio-followup) {
  align-self: stretch;
}

/* The legend: 8px above the first option */
.ds-radio-legend {
  margin-bottom: var(--space-2);
  padding: 0;
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

/* Horizontal: 2 or 3 short options in a row, 24px apart */
.ds-radio-group--horizontal .ds-radio-options {
  flex-direction: row;
  flex-wrap: wrap;
  column-gap: var(--space-6);
}

/* ---------- Selection card ---------- */
/* The whole card is the label. 1px grey-200 border, 12px corners, 16px
   padding; selected: pink-600 border, pink-50 background (like the checkbox
   selection card). */
.ds-radio-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  cursor: pointer;
  font-family: var(--font-body);
}

.ds-radio-card:hover,
.ds-radio-card.is-hover {
  border-color: var(--color-border-strong);
}

.ds-radio-card:has(.ds-radio-input:checked) {
  border-color: var(--color-primary);
  background: var(--color-primary-subtle);
}

/* The card shows the focus ring (keyboard only), not the small circle */
.ds-radio-card:has(.ds-radio-input:focus-visible),
.ds-radio-card.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: var(--focus-ring-offset);
}

.ds-radio-card .ds-radio-input:focus-visible {
  outline: 0;
}

.ds-radio-card .ds-radio-control {
  --radio-size: var(--space-5);
  --radio-dot: var(--space-2);
  margin-top: calc((var(--type-body-line-height) - var(--space-5)) / 2);
}

.ds-radio-card-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.ds-radio-card-title {
  color: var(--color-text);
  font-size: var(--type-body-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-line-height);
}

.ds-radio-card-description {
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* Selection cards in a group: 8px apart */
.ds-radio-cards {
  display: grid;
  gap: var(--space-2);
}

/* Touch screens and phones: 44px click areas, horizontal groups stack,
   selection cards go full width */
@media (pointer: coarse), (max-width: 767px) {
  .ds-radio {
    min-height: var(--size-control-standard);
    padding-block: calc((var(--size-control-standard) - var(--radio-line)) / 2);
  }

  .ds-radio-options {
    gap: 0;                  /* the 44px click areas space them out */
  }
}

@media (max-width: 767px) {
  .ds-radio-group--horizontal .ds-radio-options {
    flex-direction: column;
  }

  .ds-radio-cards {
    width: 100%;
  }
}

/* Search ------------------------------------------------------------------- */
/* Lets people find content by typing. Built from Weave's own parts: the Text
   input field (.ds-input), the Dropdown menu (.ds-dropdown-menu, items,
   group headings and bold matches), the Spinner and the Tooltip.
   Three kinds, set with data-search (main.js, section 42):
   - "filter": filters a list on the page as people type (after a 300ms
     pause), with a count like "12 results for 'brand'".
   - "suggest": a menu of suggestions after 2 characters, and recent
     searches when the empty field gets focus.
   - "site": the global search panel (.ds-search-dialog), opened from a
     .ds-search-trigger in the top bar or with "/" or Ctrl K.
     <div class="ds-search ds-dropdown" role="search" data-search="suggest">
       <label class="ds-search-label" for="find">Search projects</label>
       <div class="ds-dropdown-control">
         <div class="ds-input">
           <i data-lucide="search" class="ds-input-icon" aria-hidden="true"></i>
           <input class="ds-input-control ds-search-input" id="find" type="search"
                  placeholder="Search projects" autocomplete="off"
                  role="combobox" aria-expanded="false" aria-controls="find-list" aria-autocomplete="list">
           <span class="ds-search-spinner">…a Small spinner…</span>      (Loading)
           <button class="ds-button ds-button--tertiary ds-button--icon ds-button--xs ds-input-clear"
                   aria-label="Clear search" hidden>…x…</button>
         </div>
         <div class="ds-dropdown-menu ds-search-menu" hidden>
           <div role="listbox" id="find-list">…groups of .ds-dropdown-item options…</div>
         </div>
       </div>
       <p class="ds-search-live" role="status"></p>
     </div>
   Filter searches don't need the menu: drop "ds-dropdown" and the combobox
   attributes. Options: .ds-search-label--hidden (the label for screen
   readers only), .ds-search-scope (a Small Dropdown inside the field, like
   "All"), .ds-search-key (a shortcut hint, like "/"), --expandable (an icon
   button that opens into the field), --table (Small, up to 280px).
   Sizes: the field's, Small 32px / Medium 44px / Large 56px (.ds-input--sm,
   --lg), 16px icons 8px from the text, 12px side padding. The menu is 8px
   below the field, as wide as it, up to 360px tall, then it scrolls; its
   items match the field's size. */

.ds-search {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);                          /* label to field: 8px */
  width: 100%;
  max-width: calc(var(--space-24) * 4);         /* 384px, like a text input */
  min-width: 0;
  font-family: var(--font-body);
}

/* In a table toolbar: up to 280px */
.ds-search--table {
  max-width: calc(var(--space-20) * 3.5);       /* 280px */
}

.ds-search-label {
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

/* A label for screen readers only (when the placeholder and the icon make
   it clear on screen, like in a toolbar) */
.ds-search-label--hidden,
.ds-search-live {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The menu: up to 360px tall, then it scrolls */
.ds-search .ds-search-menu {
  max-height: calc(var(--space-20) * 4.5);      /* 360px */
}

.ds-search-menu [role="listbox"],
.ds-search-menu [role="group"] {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* A divider between groups, like the dropdown's */
.ds-search-menu [role="group"] + [role="group"] {
  margin-top: var(--space-1);
  padding-top: var(--space-1);
  border-top: var(--border-width-thin) solid var(--color-border);
}

/* The group heading with an action on its right ("Clear") */
.ds-search-group-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.ds-search-group-row .ds-dropdown-group {
  flex: 1;
}

.ds-search-group-row .ds-button {
  margin-right: var(--space-1);
}

/* Extra words on the right of an option (like the page's section) */
.ds-search-meta {
  flex-shrink: 0;
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
}

/* No results: what was searched, then a tip */
.ds-search-empty {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-2) calc(var(--dropdown-pad, var(--space-4)) - var(--space-1));
}

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

.ds-search-empty-tip {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* Loading: a Small spinner in place of the clear button, in the same 24px
   spot so nothing moves */
.ds-search-spinner {
  display: none;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--size-target-minimum);
  margin-right: calc(var(--space-1) * -1);
}

.ds-search.is-loading .ds-search-spinner {
  display: inline-flex;
}

.ds-search.is-loading .ds-input-clear {
  display: none;
}

/* Shortcut hint: 12px medium grey-700 on grey-100 (8.89:1), 4px corners.
   It hides once there's text. */
.ds-search-key {
  flex-shrink: 0;
  padding: calc(var(--space-1) / 2) calc(var(--space-1) * 1.5);   /* 2px 6px */
  border-radius: var(--radius-sm);
  background: var(--color-surface-subtle);
  color: var(--color-text-secondary);
  font-family: inherit;
  font-size: var(--type-caption-size);
  font-weight: var(--font-weight-medium);
  line-height: var(--type-caption-line-height);
  white-space: nowrap;
}

.ds-input:has(.ds-input-control:not(:placeholder-shown)) .ds-search-key {
  display: none;
}

/* ---------- Scope ---------- */
/* A Small Dropdown inside the field, on the left: "All", "Projects"… The
   field's own border does the work, so its trigger has none, only a 1px
   grey-200 line on its right. */
.ds-search-scope {
  flex-shrink: 0;
  align-self: stretch;
  margin-left: calc(var(--input-pad) * -1);
}

.ds-search-scope .ds-dropdown-control {
  height: 100%;
}

.ds-search-scope .ds-dropdown-field {
  gap: var(--space-1);
  height: 100%;
  min-height: 0;
  padding: 0 var(--space-2) 0 var(--input-pad);
  border: 0;
  border-right: var(--border-width-thin) solid var(--color-border);
  border-radius: calc(var(--input-radius) - var(--border-width-thin)) 0 0 calc(var(--input-radius) - var(--border-width-thin));
  background: var(--color-surface-muted);
  font-weight: var(--font-weight-medium);
}

.ds-search-scope .ds-dropdown-field:hover,
.ds-search-scope .ds-dropdown-field.is-hover {
  background: var(--color-hover-bg);
}

.ds-search-scope .ds-dropdown-field:focus-visible,
.ds-search-scope .ds-dropdown-field.is-focus {
  outline: var(--focus-ring-width) solid var(--color-primary);
  outline-offset: calc(var(--focus-ring-width) * -1);
  box-shadow: none;
}

.ds-search-scope .ds-dropdown-chevron {
  width: var(--space-4);
  height: var(--space-4);
}

.ds-search-scope .ds-dropdown-menu {
  width: max-content;
  min-width: calc(var(--space-20) * 2);         /* 160px */
}

/* ---------- Expandable ---------- */
/* An icon button (with a "Search" tooltip) that opens into the field, for
   toolbars with little space. The field grows to 280px over 200ms. */
.ds-search--expandable {
  max-width: calc(var(--space-20) * 3.5);       /* 280px */
}

/* The icon button sits at the start at its own size (the search is a
   column, so it would otherwise stretch to the full width) */
.ds-search-expand {
  align-self: flex-start;
}

.ds-search--expandable:not(.is-expanded) .ds-dropdown-control,
.ds-search--expandable:not(.is-expanded) > .ds-input,
.ds-search--expandable.is-expanded .ds-search-expand {
  display: none;
}

.ds-search--expandable.is-expanded .ds-input {
  animation: ds-search-grow var(--duration-normal) var(--easing-standard);
}

@keyframes ds-search-grow {
  from { width: var(--size-control-standard); }
}

/* ---------- Global search ---------- */
/* The trigger in the top bar: looks like a field (search icon, "Search",
   the shortcut hint), and opens the search panel. Under 1024px it shrinks
   to a 44px icon button. */
.ds-search-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  box-sizing: border-box;
  width: calc(var(--space-20) * 3.5);           /* 280px */
  min-height: var(--size-control-standard);     /* 44px */
  padding: 0 var(--space-3);
  border: var(--border-width-thin) solid var(--field-border);
  border-radius: var(--radius-control);      /* 12px, a Medium field */
  background: var(--color-surface);
  color: var(--color-text-subtle);
  font: inherit;
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
  text-align: left;
  cursor: pointer;
}

.ds-search-trigger:hover,
.ds-search-trigger.is-hover {
  border-color: var(--field-border-hover);
}

.ds-search-trigger:focus-visible,
.ds-search-trigger.is-focus {
  border-color: var(--color-primary);
  outline: none;
  box-shadow: 0 0 0 calc(var(--border-width-thin) * 3) var(--field-focus-ring);
}

.ds-search-trigger svg,
.ds-search-trigger i[data-lucide] {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-text-muted);
}

.ds-search-trigger-text {
  flex: 1;
  min-width: 0;
}

/* The panel: a native <dialog>, 640px wide, near the top of the screen,
   over a grey-900 40% overlay. The field at the top, suggestions below,
   key hints and the shortcut setting at the bottom. */
.ds-search-dialog {
  box-sizing: border-box;
  width: calc(100% - var(--space-8));
  max-width: calc(var(--space-20) * 8);          /* 640px */
  max-height: 80vh;
  margin: 10vh auto auto;
  padding: 0;
  overflow: hidden;
  border: var(--border-width-thin) solid var(--color-border);
  border-radius: var(--radius-container);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
  color: var(--color-text);
  font-family: var(--font-body);
}

.ds-search-dialog[open] {
  display: flex;
  flex-direction: column;
}

.ds-search-dialog::backdrop {
  background: var(--color-overlay);
}

.ds-search-dialog-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4);
  border-bottom: var(--border-width-thin) solid var(--color-border);
}

.ds-search-dialog-header .ds-dropdown-control {
  flex: 1;
}

/* No close button: the panel closes with Esc, a click outside, or by
   opening a result. Phones have no Esc key, so they get a Cancel button
   (Ghost, Small) next to the field instead. */
.ds-search-cancel {
  display: none;
  flex-shrink: 0;
}

/* The search fills the panel: field, suggestions, footer */
.ds-search-dialog .ds-search {
  flex: 1;
  gap: 0;
  max-width: none;
  min-height: 0;
}

/* The suggestions sit in the panel itself (no floating menu) */
.ds-search-dialog .ds-search-menu {
  position: static;
  max-height: none;
  padding: 0;
  overflow: visible;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.ds-search-dialog-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 0;
  padding: var(--space-2) var(--space-3);
  overflow-y: auto;
}

.ds-search-dialog-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-top: var(--border-width-thin) solid var(--color-border);
  color: var(--color-text-muted);
  font-size: var(--type-caption-size);
  line-height: var(--type-caption-line-height);
}

.ds-search-hints {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin: 0;
}

.ds-search-hints .ds-search-key {
  margin-right: var(--space-1);
}

@media (prefers-reduced-motion: reduce) {
  .ds-search--expandable.is-expanded .ds-input {
    animation: none;
  }
}

/* Tablets and phones: the trigger is a 44px icon button */
@media (max-width: 1023px) {
  .ds-search-trigger {
    justify-content: center;
    width: var(--size-control-standard);
    padding: 0;
    border-color: transparent;
    border-radius: var(--radius-control);   /* a Medium icon button */
    background: transparent;
    color: var(--color-text);
  }

  .ds-search-trigger:hover,
  .ds-search-trigger.is-hover {
    border-color: transparent;
    background: var(--color-hover-bg);
  }

  .ds-search-trigger svg,
  .ds-search-trigger i[data-lucide] {
    width: var(--space-5);
    height: var(--space-5);
    color: var(--color-text);
  }

  .ds-search-trigger-text,
  .ds-search-trigger .ds-search-key {
    display: none;
  }
}

/* Phones: page searches go full width; the panel fills the screen */
@media (max-width: 767px) {
  .ds-search,
  .ds-search--table,
  .ds-search--expandable {
    max-width: none;
  }

  .ds-search-dialog {
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    margin: 0;
    border: 0;
    border-radius: 0;
  }

  /* No key hints or shortcut setting on phones */
  .ds-search-dialog-footer {
    display: none;
  }

  .ds-search-cancel {
    display: inline-flex;
  }
}

/* Text input --------------------------------------------------------------- */
/* A native <input> (or <textarea>) in a field box, with a visible label.
     <div class="ds-field">
       <label class="ds-field-label" for="email">Email address
         <span class="ds-field-optional">(optional)</span></label>
       <div class="ds-input">
         <i data-lucide="mail" class="ds-input-icon" aria-hidden="true"></i>      (optional, leading)
         <span class="ds-input-affix">£</span>                                    (optional prefix)
         <input class="ds-input-control" id="email" type="email" autocomplete="email"
                aria-describedby="email-helper email-error">
         <span class="ds-input-affix">kg</span>                                   (optional suffix)
         <button class="ds-button ds-button--tertiary ds-button--icon ds-button--xs ds-input-clear"
                 aria-label="Clear search" hidden>…x…</button>                    (optional)
       </div>
       <div class="ds-field-footer">
         <p class="ds-field-helper" id="email-helper">We'll only use this to send receipts</p>
         <p class="ds-field-count" aria-hidden="true">24 / 100</p>                (optional)
       </div>
       <p class="ds-field-error" id="email-error" hidden>…icon… <span class="ds-field-error-text"></span></p>
     </div>
   Sizes: .ds-input--sm 32px, MD 44px (default), --lg 56px, like buttons,
   dropdowns and date pickers. 12px side padding (16px on Large), 16px icons
   8px from the text, 8px corners; the label is 8px above, helper text 4px
   below. Text is 16px (Small: 14px, but 16px on phones so they don't zoom).
   All fields share one style: a --field-border (grey-500) border at rest,
   grey-700 on hover, a pink-600 border with a 3px pink-200 ring on focus.
   States: disabled (grey-50, grey-300 border), read-only (grey-50, no
   border), error (.ds-field--error: red border, icon and message),
   success (.ds-field--success: a green check in the field).
   Also: .ds-input--textarea (a <textarea>, 4 lines, 12px padding, resizes
   down), .ds-input-group (a field joined to a button), .ds-input-reveal
   (Show / Hide for passwords). Widths: .ds-field--narrow (128px),
   --short (192px), default 384px, --wide (no limit); full width on phones.
   main.js (section 34) runs the clear, Show / Hide, the character count
   and validation. */

.ds-field {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: 100%;
  max-width: calc(var(--space-24) * 4);   /* 384px */
  min-width: 0;
  font-family: var(--font-body);
}

.ds-field--narrow { max-width: calc(var(--space-24) + var(--space-8)); }   /* 128px: a postcode */
.ds-field--short  { max-width: calc(var(--space-24) * 2); }                /* 192px */
.ds-field--wide   { max-width: none; }

.ds-field-label {
  margin-bottom: var(--space-2);   /* 8px to the field */
  color: var(--color-text);
  font-size: var(--type-body-small-size);
  font-weight: var(--font-weight-semibold);
  line-height: var(--type-body-small-line-height);
}

/* Weave marks optional fields, not required ones */
.ds-field-optional {
  color: var(--color-text-muted);
  font-weight: var(--font-weight-regular);
}

/* ---------- The field box ---------- */
.ds-input {
  --input-height: var(--size-control-standard);   /* 44px (MD) */
  --input-pad: var(--space-3);                     /* 12px */
  --input-radius: var(--radius-control);        /* 12px, like a Medium button */
  --input-ring: var(--field-focus-ring);
  display: flex;
  align-items: center;
  gap: var(--space-2);                             /* 8px icon to text */
  box-sizing: border-box;
  width: 100%;
  min-height: var(--input-height);
  padding: 0 var(--input-pad);
  border: var(--border-width-thin) solid var(--field-border);
  border-radius: var(--input-radius);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--type-body-size);
  line-height: var(--type-body-line-height);
}

.ds-input--sm {
  --input-height: var(--size-control-compact);     /* 32px */
  --input-radius: var(--radius-control-sm);        /* 8px */
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-input--lg {
  --input-height: calc(var(--space-12) + var(--space-2));   /* 56px */
  --input-pad: var(--space-4);                     /* 16px */
  --input-radius: var(--radius-control);        /* 12px */
}

.ds-input:hover,
.ds-input.is-hover {
  border-color: var(--field-border-hover);
}

/* Focus: a pink-600 border and a 3px pink-200 ring */
.ds-input:has(.ds-input-control:focus),
.ds-input.is-focus {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 calc(var(--border-width-thin) * 3) var(--input-ring);
}

.ds-input-control {
  flex: 1;
  min-width: 0;
  height: calc(var(--input-height) - var(--border-width-thin) * 2);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  outline: none;
}

/* No second outline: the field box shows focus (pink-600 border + pink-200
   ring, through :has(:focus) below), so the typing area itself never draws
   the browser's outline or the docs' general focus outline on top of it.
   Covers text inputs, textareas and date picker inputs. */
.ds-input-control:focus,
.ds-input-control:focus-visible,
.ds-datepicker-input:focus,
.ds-datepicker-input:focus-visible {
  outline: none;
}

.ds-input-control::placeholder {
  color: var(--color-text-subtle);
}

/* Native search fields: no browser clear button (ours is the one) */
.ds-input-control::-webkit-search-cancel-button {
  appearance: none;
}

/* A 16px icon in the field (leading, or the success check) */
.ds-input-icon {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-text-muted);
}

/* Prefix or suffix: a grey-50 box on the field's edge */
.ds-input-affix {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  align-self: stretch;
  padding: 0 var(--input-pad);
  background: var(--color-surface-muted);
  color: var(--color-text-muted);
  white-space: nowrap;
}

.ds-input-affix:first-child {
  margin-left: calc(var(--input-pad) * -1);
  border-right: var(--border-width-thin) solid var(--color-border);
  border-radius: calc(var(--input-radius) - var(--border-width-thin)) 0 0 calc(var(--input-radius) - var(--border-width-thin));
}

.ds-input-affix:last-child {
  margin-right: calc(var(--input-pad) * -1);
  border-left: var(--border-width-thin) solid var(--color-border);
  border-radius: 0 calc(var(--input-radius) - var(--border-width-thin)) calc(var(--input-radius) - var(--border-width-thin)) 0;
}

/* Buttons in the field (clear, Show): 24px, pulled in to the edge */
.ds-input > .ds-button {
  flex-shrink: 0;
  margin-right: calc(var(--space-1) * -1);
}

.ds-input > .ds-button[hidden] {
  display: none;
}

/* The clear button: a plain 16px grey-600 x (6.61:1 on white) in a 24px
   click area, with no box; grey-100 behind it only on hover. Hidden until
   there is text. aria-label="Clear search". */
.ds-input-clear {
  --button-icon: var(--space-4);   /* 16px */
  color: var(--color-text-muted);
}

.ds-input-clear:hover:not(:disabled),
.ds-input-clear:active:not(:disabled),
.ds-input-clear.is-hover {
  background: var(--color-hover-bg);
}

/* ---------- Below the field ---------- */
.ds-field-footer {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  margin-top: var(--space-1);      /* 4px below the field */
}

.ds-field-helper,
.ds-field-count {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

/* No extra space between the paragraphs here (pages may add some).
   Two classes, so it beats page rules like ".doc-section p + p". */
.ds-field-footer > .ds-field-helper,
.ds-field-footer > .ds-field-count {
  margin: 0;
}

.ds-field > .ds-field-error {
  margin-top: var(--space-1);
}

/* The count sits at the field's right edge, on the helper's line */
.ds-field-footer > .ds-field-count {
  flex-shrink: 0;
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}

.ds-field-count.is-over {
  color: var(--color-error);
}

/* Read by screen readers only (the polite character count) */
.ds-field-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.ds-field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: var(--space-1) 0 0;
  color: var(--color-error);
  font-size: var(--type-body-small-size);
  line-height: var(--type-body-small-line-height);
}

.ds-field-error svg,
.ds-field-error i[data-lucide] {
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  margin-top: calc(var(--space-1) / 2);
}

.ds-field-error[hidden] {
  display: none;
}

/* ---------- States ---------- */
/* Disabled: grey-50, grey-300 border, grey-400 text */
.ds-input:has(.ds-input-control:disabled),
.ds-input:hover:has(.ds-input-control:disabled) {
  border-color: var(--color-border-disabled);
  background: var(--color-surface-muted);
  color: var(--color-text-disabled);
}

.ds-input-control:disabled {
  cursor: not-allowed;
}

/* Read-only: grey-50, no border */
.ds-input:has(.ds-input-control[readonly]),
.ds-input:hover:has(.ds-input-control[readonly]) {
  border-color: var(--color-surface-muted);   /* no visible border */
  background: var(--color-surface-muted);
}

/* Error: a red border (and a red-200 ring on focus) */
.ds-field--error .ds-input,
.ds-field--error .ds-input:hover {
  --input-ring: var(--color-error-border);
  border-color: var(--color-error);
}

/* Success: a green check in the field, and the helper text in green */
.ds-input-icon--success {
  display: none;
  color: var(--color-success);
}

.ds-field--success .ds-input-icon--success {
  display: block;
}

.ds-field--success .ds-field-helper {
  color: var(--color-success);
}

/* ---------- Textarea ---------- */
/* 4 lines tall, 12px padding; people can make it taller */
.ds-input--textarea {
  align-items: stretch;
  padding: var(--space-3);
}

.ds-input--textarea .ds-input-control {
  height: auto;
  min-height: calc(var(--type-body-line-height) * 4);
  resize: vertical;
}

/* ---------- A field joined to a button ---------- */
.ds-input-group {
  display: flex;
  width: 100%;
}

.ds-input-group > .ds-input {
  min-width: 0;   /* the field gives way in narrow spaces, the button keeps its size */
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.ds-input-group > .ds-button {
  flex-shrink: 0;
  min-height: var(--size-control-standard);
  border-radius: 0 var(--radius-control) var(--radius-control) 0;
}

/* Phones: full width, 16px text (no zoom on focus), the button below */
@media (max-width: 767px) {
  .ds-field,
  .ds-field--narrow,
  .ds-field--short {
    max-width: none;
  }

  .ds-input--sm {
    font-size: var(--type-body-size);
  }

  .ds-input-group {
    flex-direction: column;
    gap: var(--space-2);
  }

  .ds-input-group > .ds-input,
  .ds-input-group > .ds-button {
    border-radius: var(--radius-control);
  }
}


/* Textarea ----------------------------------------------------------------- */
