/* ============================================================
 * Components — buttons, forms, cards, badges, tables, overlays
 * All metrics from design.md's component reference.
 * ============================================================ */

/* ── Buttons ────────────────────────────────────────────────
 * 14px Book, sm 36px / md 40px, radius 6px (rounded-xs).
 * .button          -> Secondary   (filled neutral)
 * .button--primary -> Primary     (primary-700, one per view)
 * .button--secondary -> Outline
 * .button--subtle  -> Ghost
 * .button--danger  -> Destructive
 * .button--compact -> size sm
 */

.button,
button,
input[type="submit"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-h-md);
  padding: 0 var(--space-4);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-xs);
  background: var(--bg-surface);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-book);
  line-height: 1;
  text-align: center;
  cursor: pointer;
  transition: background-color var(--motion-base) var(--ease-standard),
    border-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard),
    transform var(--motion-fast) var(--ease-out);
}

.button:hover,
button:hover,
input[type="submit"]:hover {
  background: var(--bg-subtle);
  border-color: var(--border-strong);
  text-decoration: none;
}

/* The display above beats the user agent's [hidden] rule, so a button hidden
   from JavaScript stays on screen without this. */
.button[hidden],
button[hidden],
input[type="submit"][hidden] {
  display: none;
}

/* Press: a scale, never a full-colour invert.
 *
 * 0.96 and not the 0.985 this started at. 1.5% is under the threshold where a
 * press reads as a press: the rule was written but the feedback was not there.
 * Below 0.95 it tips into exaggerated. */
.button:active,
button:active,
input[type="submit"]:active {
  transform: scale(0.96);
}

.button--compact,
input.button--compact,
button.button--compact {
  min-height: var(--control-h-sm);
  padding: 0 var(--space-3);
}

.button--primary,
input.button--primary,
button.button--primary {
  background: var(--primary-700);
  border-color: var(--primary-edge);
  color: var(--text-on-primary);
}

.button--primary:hover,
input.button--primary:hover,
button.button--primary:hover {
  background: var(--primary-hover);
  border-color: var(--primary-hover);
}

.button--secondary,
input.button--secondary,
button.button--secondary {
  background: transparent;
  border-color: var(--border-control);
  color: var(--text-primary);
}

.button--secondary:hover,
input.button--secondary:hover,
button.button--secondary:hover {
  background: var(--bg-subtle);
  border-color: var(--border-strong);
}

.button--subtle,
input.button--subtle,
button.button--subtle {
  background: transparent;
  border-color: transparent;
  color: var(--text-primary);
}

.button--subtle:hover,
input.button--subtle:hover,
button.button--subtle:hover {
  background: var(--bg-subtle);
  border-color: transparent;
}

.button--danger,
input.button--danger,
button.button--danger {
  color: var(--text-primary);
  border-color: var(--error-border);
}

.button--danger:hover,
input.button--danger:hover,
button.button--danger:hover {
  background: var(--error-bg);
  border-color: var(--error-500);
}

/* Ghost + destructive: a delete action should not out-shout the
 * workflow action sitting next to it, so the red only appears on
 * intent. Order matters — this has to win over .button--subtle. */
.button--subtle.button--danger,
input.button--subtle.button--danger,
button.button--subtle.button--danger {
  border-color: transparent;
}

.button--subtle.button--danger:hover,
input.button--subtle.button--danger:hover,
button.button--subtle.button--danger:hover {
  background: var(--error-bg);
  border-color: var(--error-border);
}

/* Disabled — a real state, not just reduced opacity: the fill drops
 * to neutral-100 and the cursor changes, so the control does not read
 * as pressable while JS is still booting.
 *
 * neutral-500 on neutral-100 measures 4.39:1, just under AA. WCAG
 * 1.4.3 exempts inactive controls, and the state is also carried by
 * the fill and the cursor rather than by colour alone, so this is a
 * deliberate exemption. Anything that is NOT disabled keeps
 * secondary text on surface (4.83:1) or canvas (4.63:1) - do not
 * put live secondary text on --bg-subtle. */
.button:disabled,
.button[aria-disabled="true"],
button:disabled,
input[type="submit"]:disabled,
input:disabled,
textarea:disabled,
select:disabled {
  background: var(--bg-subtle);
  border-color: var(--border-default);
  color: var(--text-secondary);
  cursor: not-allowed;
  transform: none;
}

.button:disabled:hover,
button:disabled:hover,
input[type="submit"]:disabled:hover {
  background: var(--bg-subtle);
  border-color: var(--border-default);
}

/* ── Icon button ────────────────────────────────────────────
 * Ghost type, size sm (32px square for the sidebar's xs slot). */

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--sidebar-item-h);
  height: var(--sidebar-item-h);
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard);
}

.icon-button:hover {
  background: var(--bg-subtle);
  color: var(--text-primary);
  text-decoration: none;
}

/* ── Icons ──────────────────────────────────────────────────
 * Lucide, stroke 1.5px, 16 inline / 20 default / 24 emphasis. */

.icon {
  flex: none;
  width: 20px;
  height: 20px;
  stroke-width: 1.5;
  vertical-align: -0.15em;
}

.icon--sm {
  width: 16px;
  height: 16px;
}

.icon--lg {
  width: 24px;
  height: 24px;
}

/* The one place motion carries meaning: work is in flight. */
.icon--spin {
  animation: icon-spin 900ms linear infinite;
}

@keyframes icon-spin {
  to {
    transform: rotate(360deg);
  }
}

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

/* ── A frame that is fetching ───────────────────────────────
 * Turbo Drive's progress bar covers page navigations and NOTHING else: a
 * turbo-frame navigation never shows it. So the app's frames - the scenario
 * editor is the one that matters, where a click swaps KPIs, roadmap and a
 * twelve-column table under the reader - had no loading affordance at all.
 *
 * Nothing had to be built. Turbo already writes `busy` and `aria-busy="true"`
 * on the frame for exactly the length of the fetch; no stylesheet was
 * selecting either. `[aria-busy="true"]` and not `[busy]` because it is the
 * standard attribute, so this covers every frame the app grows rather than one
 * by id.
 *
 * The affordance is the stale content going quiet, not something new appearing
 * over it. Nothing is replaced, nothing resizes, nothing moves: the numbers
 * about to be swapped simply stop claiming to be the answer. A skeleton here
 * would be worse than nothing - swapping a 6,700px editor for placeholder
 * blocks and back inside a tenth of a second is a flash, not feedback.
 *
 * `pointer-events: none` is the other half: during the swap the old scenario's
 * controls still answered, and a second click queued a second navigation
 * against a frame already being replaced.
 *
 * THE DELAY IS THE POINT. Measured in a clean session, the fetch runs 77-105ms
 * locally - faster than the eye reads as waiting, and an affordance that
 * appeared instantly would strobe on every click. The 200ms delay means the
 * dimming never starts on a fetch that fast: it appears only when the wait is
 * real - a loaded server, a phone on mobile data, an editor big enough to take
 * time rendering - which is the only time it says anything. On the way back
 * there is no delay, so the content brightens the instant it is current again.
 *
 * Under prefers-reduced-motion --motion-base is already 0ms, so the fade
 * becomes a step; the delay stays, which is what keeps the step from flashing.
 */
turbo-frame {
  transition: opacity var(--motion-base) var(--ease-standard);
}

turbo-frame[aria-busy="true"] {
  opacity: 0.6;
  pointer-events: none;
  transition-delay: 200ms;
}

/* ── Turbo's progress bar ───────────────────────────────────
 * Turbo ships its own stylesheet with `background: #0076ff`, a blue that is
 * in no palette this project has - not design.md's #1445D8, not the marketing
 * system's #2D4EFF. It is hard to catch because the default delay is 500ms, so
 * it only appears on the slow navigations, which are the ones somebody is
 * watching.
 *
 * Turbo inserts its <style> as the first child of <head>, so this rule wins on
 * document order at the same specificity without needing !important.
 *
 * --primary-edge and not --primary-700: this is a two-pixel line that has to
 * read against whatever the canvas is, which is the same job that token does
 * for a button's own border - primary-700 on light, primary-500 on dark, where
 * primary-700 falls to 2.7:1 on black. */
.turbo-progress-bar {
  background-color: var(--primary-edge);
}

/* ── Forms ──────────────────────────────────────────────── */

.stack-form,
.field-group {
  display: grid;
  gap: var(--space-4);
}

.field-group {
  gap: var(--space-2);
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

label {
  display: block;
  color: var(--text-primary);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-book);
}

input,
textarea,
select,
button {
  font: inherit;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="url"],
input[type="number"],
textarea,
select {
  width: 100%;
  min-height: var(--control-h-md);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-xs);
  background: var(--bg-surface);
  color: var(--text-primary);
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  transition: border-color var(--motion-base) var(--ease-standard),
    box-shadow var(--motion-base) var(--ease-standard);
}

input::placeholder,
textarea::placeholder {
  color: var(--text-secondary);
}

/* The one control in the product still drawing the operating system's chrome.
 * The rule above already gives <select> the input's border, radius, surface,
 * height and type - and every one of them was being ignored, because a select
 * at `appearance: auto` paints the platform widget over the box: its own
 * corner radius, its own grey fill, its own triangle. Next to the text input
 * it shares a row with in the composer, and next to the primary button on the
 * sign-in screen - a page with exactly two controls, one of them this - it
 * read as a control from a different application.
 *
 * `appearance: none` turns the box back over to this stylesheet. It takes
 * nothing away that matters: the element stays a <select>, keeps its role and
 * its accessible name, still opens with Space, Enter and Alt+Down, and still
 * takes --focus-ring from the :focus rule below. The option list itself is
 * drawn by the operating system and cannot be styled at all - which is fine,
 * because `color-scheme` is declared on :root in 01_tokens.css, so the popup
 * already follows the theme.
 *
 * The caret is --select-caret, Lucide's chevron-down, one per theme. Reserved
 * space rather than overlap: padding-inline-end is the caret's 16px plus a
 * --space-3 on each side of it, so a long option cannot run underneath it.
 * `background-color` and not `background`, or the shorthand would drop the
 * image every time a variant restates the fill. */
select {
  appearance: none;
  padding-inline-end: calc(16px + 2 * var(--space-3));
  background-color: var(--bg-surface);
  background-image: var(--select-caret);
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 16px 16px;
}

/* The disabled rule above uses the `background` shorthand, which wipes the
 * caret and leaves a select with no affordance at all. Restated here: the
 * fill is the disabled one, the chevron stays. */
select:disabled {
  background-color: var(--bg-subtle);
  background-image: var(--select-caret);
}

/* A select in a data cell is a different control from a select in a form, and
 * the 40px the rule above reserves is most of it. The scenario editor's table
 * has six of them per row inside a twelve-column grid: measured at 1440, the
 * Puntos select is 54px wide and the Riesgo one 72px, so a 40px reserve plus
 * the 12px on the other side left ten pixels of text and "1" rendered as a
 * clipped stroke. Responsable, at 168px, ran "Lo elige el schedule" under the
 * chevron.
 *
 * Same chevron, smaller and closer in: 12px at --space-1 from the edge, with a
 * --space-2 between it and the text, and the leading padding down to --space-2
 * to match. That is 22px of text room in the Puntos cell and 136px in
 * Responsable, both of which fit their longest option - and both of which are
 * more than the platform widget left before any of this, since its own arrow
 * took about 16px it never declared.
 *
 * By cell and not by table class: what makes a control compact here is that it
 * sits in a row of data, which is true of any table this product grows. */
td select,
th select {
  padding-inline: var(--space-2) calc(12px + var(--space-1) + var(--space-2));
  background-position: right var(--space-1) center;
  background-size: 12px 12px;
}

input[type="text"]:hover,
input[type="email"]:hover,
input[type="password"]:hover,
textarea:hover,
select:hover {
  border-color: var(--border-strong);
}

/* Focus: primary-500 border plus ring, per the Input spec. */
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="search"]:focus,
input[type="url"]:focus,
input[type="number"]:focus,
textarea:focus,
select:focus {
  outline: none;
  border-color: var(--primary-500);
  box-shadow: 0 0 0 3px var(--focus-halo);
}

textarea {
  min-height: 6rem;
  padding: var(--space-3);
  resize: vertical;
}

/* WCAG 2.5.8 minimum target size. */
input[type="checkbox"],
input[type="radio"] {
  width: 1.5rem;
  height: 1.5rem;
  margin: 0;
  accent-color: var(--primary-edge);
  cursor: pointer;
}

/* Escrita antes de tener consumidor y estrenada por el editor de contexto del
   proyecto, que es la primera pantalla de la app que valida un campo suelto:
   hasta entonces el único canal de error era el toast global, que no dice qué
   campo rechazó ni por qué. Va debajo del control que nombra, con el ícono
   circle-alert y el borde de error sobre el propio input - el estado se lleva
   dos veces, forma y color, nunca solo color. */
.field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: 0;
  color: var(--text-primary);
  font-size: var(--text-body-sm);
}

/* ── Card ───────────────────────────────────────────────────
 * Radius 8px (rounded-sm), padding 32px (space-8), flat: the
 * boundary is the neutral-200 stroke, not a shadow. */

/* grid-template-columns: minmax(0, 1fr), not the implicit single auto
 * column display:grid gets on its own: an auto track's minimum is its
 * widest child's min-content, so any card holding a long unbroken line
 * (a stat row, a wrapped-but-still-wide flex row) would otherwise grow
 * the card - and the page - to fit it instead of wrapping or scrolling
 * inside its own bounds. Same fix as .app-layout's mobile breakpoint,
 * one level down. */
.card {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  padding: var(--space-8);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--bg-surface);
}

/* ── Badge ──────────────────────────────────────────────────
 * 20px tall, 10px/2px padding, 12px Book. */

.badge {
  display: inline-flex;
  align-items: center;
  min-height: var(--badge-h);
  padding: 2px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  background: var(--bg-subtle);
  color: var(--text-primary);
  font-size: var(--text-caption);
  font-weight: var(--weight-book);
  line-height: 1;
  white-space: nowrap;
}

.badge--outline {
  background: transparent;
  border-color: var(--border-default);
  color: var(--text-secondary);
}

.badge--info {
  background: var(--info-bg);
  border-color: var(--info-border);
  color: var(--text-primary);
}

/* ── Alert (banner) ─────────────────────────────────────────
 * 16px padding, radius 8px (rounded-sm). Hue lives in the border
 * and fill; the copy stays at full text contrast. */

.alert {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--bg-subtle);
  color: var(--text-primary);
}

.alert--destructive {
  border-color: var(--error-border);
  background: var(--error-bg);
}

.alert--warning {
  border-color: var(--warning-border);
  background: var(--warning-bg);
}

.alert--info {
  border-color: var(--info-border);
  background: var(--info-bg);
}

.alert__title {
  margin: 0;
  font-size: var(--text-body-sm);
}

.error-message,
.inline-error {
  margin: 0;
  color: var(--text-primary);
}

/* ── Toast ──────────────────────────────────────────────────
 * 388px wide, 24px padding, radius 6px (rounded-xs).
 *
 * --toast-top lo escribe flash_toasts_controller: el borde de abajo del banner
 * de saldo (o de la barra de arriba en un teléfono), para que la pila no tape
 * su acción. Sin nada que esquivar vuelve al margen de siempre. */

.toast-stack {
  position: fixed;
  top: var(--toast-top, var(--space-4));
  right: var(--space-4);
  z-index: 1000;
  display: grid;
  gap: var(--space-3);
  width: min(calc(100vw - 2 * var(--space-4)), var(--toast-w));
}

.toast {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-2);
  padding: var(--space-6);
  border: 1px solid var(--border-default);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius-xs);
  background: var(--bg-surface);
  color: var(--text-primary);
  font-size: var(--text-body-sm);
  box-shadow: var(--shadow-popover);
  animation: toast-in var(--motion-base) var(--ease-out);
}

.toast__message {
  margin: 0;
  align-self: center;
}

/* La × lleva el target de 40px aunque el ícono sea de 16: el margen negativo
 * la come del padding del aviso, así el alto del aviso no crece por ella. */
.toast__close {
  width: var(--nav-item-h);
  height: var(--nav-item-h);
  margin: calc(-1 * var(--space-3)) calc(-1 * var(--space-3)) calc(-1 * var(--space-3)) 0;
}

.toast--notice {
  border-left-color: var(--success-500);
}

.toast--alert {
  border-left-color: var(--error-500);
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ── Overlays: scrim, dialog, popover ───────────────────────
 * Dialog 423px / radius 8px. Alert Dialog 512px / 24px / radius 6px.
 * Modal dimming per the brand: flat black at 60%, no decorative blur. */

.app-popover-scrim {
  position: fixed;
  inset: 0;
  z-index: 1100;
  background: var(--scrim);
  animation: fade-in var(--motion-base) var(--ease-standard);
}

.app-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 1200;
  width: min(calc(100vw - 2 * var(--space-4)), var(--alert-dialog-w));
  max-height: min(90vh, 46rem);
  overflow-y: auto;
  transform: translate(-50%, -50%);
  padding: var(--space-6);
  /* The overlay pair rather than the card pair. On light they resolve to the
   * same two values a card gets; on dark they are the step up that lets this
   * read as elevated without a wide shadow doing the work. See the
   * --bg-overlay note in 01_tokens.css. */
  border: 1px solid var(--border-overlay);
  border-radius: var(--radius-sm);
  background: var(--bg-overlay);
  box-shadow: var(--shadow-dialog);
  animation: dialog-in var(--motion-base) var(--ease-out);
}

.app-modal__header {
  display: grid;
  gap: var(--space-2);
  margin-bottom: var(--space-6);
}

/* The <details> that presents a modal: the trigger's positioning context, and
 * nothing else - the scrim and the dialog inside it are both `position: fixed`
 * and place themselves against the viewport. */
.app-modal-disclosure {
  position: relative;
}

.app-modal-disclosure > summary {
  list-style: none;
}

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

/* An inline trigger is a real button in the page body, so it sizes itself; the
 * disclosure around it must not stretch it across the row it sits in. */
.app-modal-disclosure--inline {
  display: inline-block;
}

.app-modal__actions,
.app-inline-popover__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
}

.app-inline-popover {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: 1000;
  width: min(20rem, calc(100vw - 3rem));
  padding: var(--space-4);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-surface);
  box-shadow: var(--shadow-popover);
  animation: fade-in var(--motion-fast) var(--ease-out);
}

.app-inline-popover__header {
  display: grid;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

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

@keyframes dialog-in {
  from {
    opacity: 0;
    transform: translate(-50%, calc(-50% + 8px));
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%);
  }
}

/* ── Disclosure (details/summary) ───────────────────────── */

summary {
  cursor: pointer;
}

summary::-webkit-details-marker {
  display: none;
}

.disclosure > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  color: var(--link-color);
  font-size: var(--text-body-sm);
}

.disclosure > summary:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.disclosure > summary .icon {
  transition: transform var(--motion-base) var(--ease-standard);
}

.disclosure[open] > summary .icon--chevron {
  transform: rotate(90deg);
}

/* ── Toggle / Toggle Group ──────────────────────────────────
 * design.md: Toggle size sm, Type=Text, Outlined; Toggle Group
 * Type=Theme for the Light/Dark/System selector. Pressed is carried
 * by aria-pressed so the state is one thing, not two. */

/* The track sits on the canvas colour, not --bg-subtle: an unpressed
 * label is --text-secondary, which needs a background at least as
 * light as the canvas to clear 4.5:1 (on --bg-subtle it is 4.39). */
.toggle-group {
  display: inline-flex;
  padding: 2px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-xs);
  background: var(--bg-canvas);
}

.toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: 26px;
  padding: 0 var(--space-2);
  border: 1px solid transparent;
  border-radius: 4px;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-caption);
  line-height: 1;
  cursor: pointer;
  transition: background-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard);
}

.toggle:hover {
  color: var(--text-primary);
}

.toggle[aria-pressed="true"] {
  background: var(--bg-subtle);
  border-color: var(--border-control);
  color: var(--text-primary);
}

.toggle:active {
  transform: none;
}

/* Inset ring: the toggles sit 3px from the group's own border, so the
 * base +2px offset would push the outline past it and get clipped. */
.toggle:focus-visible {
  outline-offset: -2px;
}

.toggle-group--theme {
  width: 100%;
}

.toggle-group--theme .toggle {
  flex: 1;
}

/* ── Locale switcher ────────────────────────────────────────
 * A small labelled row: eyebrow label, compact select, and a submit
 * button Stimulus hides once it is running, since changing the select
 * already submits. The select itself needs no rules of its own beyond
 * the width: it is covered by the shared control, hover and focus
 * rules above. */
.locale-switcher {
  display: grid;
  gap: var(--space-2);
  justify-items: start;
}

.locale-switcher__label {
  margin: 0;
}

.locale-switcher__select {
  width: auto;
  min-width: 8rem;
}

/* ── Settings menu ──────────────────────────────────────────
 * Theme and language, which somebody sets once, folded behind one row so
 * they stop holding a fixed position above the fold on every page. A
 * <details> rather than a scripted menu: both controls have to survive
 * with JavaScript off, and the language form's no-JS submit button is the
 * clearest case of it. */
.app-settings-menu {
  display: grid;
  gap: var(--space-3);
}

/* A <details> with an overridden `display` keeps the UA slot that holds its
 * contents as an empty grid item while closed, so the gap above was being paid
 * for a panel that is not on screen - 12px under "Ajustes" on every page.
 * Measured: the closed toggle is 32px and this element was 44px. */
.app-settings-menu:not([open]) {
  gap: 0;
}

.app-settings-menu__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  width: fit-content;
  min-height: var(--sidebar-item-h);
  padding: 0 var(--space-2);
  border-radius: var(--radius-xs);
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
  list-style: none;
  cursor: pointer;
  transition: background-color var(--motion-base) var(--ease-standard),
    color var(--motion-base) var(--ease-standard);
}

.app-settings-menu__toggle::-webkit-details-marker {
  display: none;
}

.app-settings-menu__toggle:hover {
  background: var(--bg-subtle);
  color: var(--text-primary);
}

.app-settings-menu__toggle .icon {
  transition: transform var(--motion-base) var(--ease-standard);
}

.app-settings-menu[open] > .app-settings-menu__toggle .icon {
  transform: rotate(180deg);
}

/* No indent of its own: the controls line up with the rest of the sidebar's
 * content column, and it is the toggle above that reads as the header. An
 * indent here would hold them a few pixels off every other row. */
.app-settings-menu__panel {
  display: grid;
  gap: var(--space-4);
}

.app-settings-menu__field {
  display: grid;
  gap: var(--space-2);
}

/* ── Model picker ───────────────────────────────────────────
 * Sits beside a submit button rather than in a column of fields, so it
 * has to stay compact: controls are full width by default, which is what
 * made it grow until it ran under the button next to it. Same shape as
 * the locale switcher above - only the width needs saying, since hover
 * and focus come from the shared control rules. The label is quieter
 * than a form label because it annotates an action row. */
.field-group--model {
  gap: var(--space-1);
  justify-items: start;
}

.field-group--model label {
  color: var(--text-secondary);
  font-size: var(--text-eyebrow);
}

.model-field__select,
.model-field__input {
  width: auto;
  min-width: 11rem;
}

/* ── Separator ──────────────────────────────────────────── */

.separator {
  height: 1px;
  border: 0;
  background: var(--border-default);
}

/* ── Table ──────────────────────────────────────────────────
 * Header 48px, row 56px, 16px horizontal padding. */

/* The two `local`-attached gradients cover the card's own background as far
 * as the wrapper has scrolled, so they vanish once there is nothing left in
 * that direction; the two `scroll`-attached ones sit fixed at each edge as a
 * constant hint. Without this a table with hidden columns gives no sign that
 * scrolling reveals more - `overflow-x: auto` alone renders no scrollbar on
 * the platforms this app runs on. */
.table-wrapper {
  overflow-x: auto;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background-color: var(--bg-surface);
  background-image:
    linear-gradient(to right, var(--bg-surface) 50%, transparent),
    linear-gradient(to left, var(--bg-surface) 50%, transparent),
    linear-gradient(to right, var(--border-strong), transparent),
    linear-gradient(to left, var(--border-strong), transparent);
  background-position: left, right, left, right;
  background-repeat: no-repeat;
  background-size: 24px 100%, 24px 100%, 8px 100%, 8px 100%;
  background-attachment: local, local, scroll, scroll;
}

.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
}

.data-table thead th {
  height: var(--table-header-h);
  padding: 0 var(--space-4);
  border-bottom: 1px solid var(--border-default);
  background: var(--bg-canvas);
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-book);
  text-align: left;
  white-space: nowrap;
}

.data-table tbody td,
.data-table tbody th {
  height: var(--table-row-h);
  padding: var(--space-4);
  border-bottom: 1px solid var(--border-default);
  font-weight: var(--weight-book);
  text-align: left;
  vertical-align: top;
}

.data-table tbody tr:last-child td,
.data-table tbody tr:last-child th {
  border-bottom: 0;
}

/* ── Skeleton ───────────────────────────────────────────── */

.skeleton {
  border-radius: var(--radius-xs);
  background: var(--bg-subtle);
  animation: skeleton-pulse 1.6s var(--ease-standard) infinite;
}

@keyframes skeleton-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.55;
  }
}

/* The reduced-motion block in 01_tokens.css zeroes --motion-fast/base/slow,
 * which switches off every animation and transition in this directory that is
 * timed by a token - and that is all of them but two. This one is 1.6s, a
 * literal, and it repeats forever: an indefinite pulse is the exact thing the
 * preference exists to stop, and nothing was turning it off.
 *
 * `animation: none` and not a slower pulse. What makes a skeleton legible is
 * its shape and its fill sitting where content will be; the pulse only says
 * "still loading", and a reader who asked for no motion is better served by a
 * block that holds still than by one that breathes more slowly.
 *
 * The other literal is .icon--spin at 900ms, guarded the same way beside its
 * own keyframes. The third animation in the directory, tabs-scroll-fade in
 * 05_features.css, has no duration at all - it runs on `animation-timeline:
 * scroll()`, so it only advances while the reader is already scrolling and
 * produces no motion of its own to suppress. */
@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
  }
}
