/* Runo — components.
 *
 * Rules that hold across this file:
 * - every interactive control is at least `--tap-min` in its smallest
 *   dimension, because most listening happens on a phone;
 * - no component defines its own focus ring (base.css owns that globally);
 * - no component invents a colour.
 */

@layer components {
  /* --- buttons --------------------------------------------------------- */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--tap-min);
    padding: 0 var(--space-4);
    background: var(--surface-raised);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    font-size: var(--text-sm);
    font-weight: 550;
    line-height: 1;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--duration-fast) var(--ease),
                border-color var(--duration-fast) var(--ease),
                color var(--duration-fast) var(--ease);
  }
  .btn:hover:not(:disabled) {
    background: color-mix(in oklab, var(--surface-raised) 80%, var(--text) 8%);
    border-color: var(--text-faint);
    color: var(--text);
  }
  .btn:active:not(:disabled) { transform: translateY(0.5px); }
  .btn:disabled,
  .btn[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  .btn-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-contrast);
  }
  .btn-primary:hover:not(:disabled) {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    color: var(--accent-contrast);
  }

  .btn-danger {
    background: var(--danger);
    border-color: var(--danger);
    color: var(--text-inverse);
  }
  .btn-danger:hover:not(:disabled) {
    background: color-mix(in oklab, var(--danger) 85%, black);
    border-color: color-mix(in oklab, var(--danger) 85%, black);
  }

  /* Destructive but not the main event — reads as quiet until hovered. */
  .btn-quiet-danger {
    background: transparent;
    border-color: var(--border-strong);
    color: var(--danger);
  }
  .btn-quiet-danger:hover:not(:disabled) {
    background: var(--danger-subtle);
    border-color: var(--danger);
    color: var(--danger);
  }

  .btn-ghost {
    background: transparent;
    border-color: transparent;
  }
  .btn-ghost:hover:not(:disabled) {
    background: var(--surface-raised);
    border-color: var(--border);
  }

  .btn-sm {
    min-height: 2rem;
    padding: 0 var(--space-3);
    font-size: var(--text-xs);
  }
  /* Small controls inside a dense table still need a real touch target on
   * a phone, where the table has become a stack of cards anyway. */
  @media (max-width: 48rem) {
    .btn-sm { min-height: var(--tap-min); font-size: var(--text-sm); }
  }

  .btn-block { width: 100%; }

  /* Icon-only button. Requires an aria-label; `title` alone is not an
   * accessible name — it is unreliable in AT and absent on touch. */
  .btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap-min);
    height: var(--tap-min);
    padding: 0;
    background: transparent;
    color: var(--text-muted);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: background var(--duration-fast) var(--ease),
                color var(--duration-fast) var(--ease);
  }
  .btn-icon:hover:not(:disabled) {
    background: var(--surface-raised);
    color: var(--text);
  }
  /* The tap target is `--tap-min` either way; this is the *glyph*, which at
   * 1.125rem read as decoration inside a 2.75rem button and was hard to
   * aim at by eye. The button does not grow — only the mark inside it. */
  .btn-icon svg { width: 1.5rem; height: 1.5rem; }

  .btn-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
  }

  /* --- badges ---------------------------------------------------------- */
  .badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 0.15rem var(--space-2);
    border-radius: var(--radius-full);
    background: var(--surface-sunken);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.01em;
    white-space: nowrap;
  }
  .badge-done { background: var(--ok-subtle); color: var(--ok); }
  .badge-narrating,
  .badge-running { background: var(--accent-subtle); color: var(--accent); }
  .badge-queued { background: var(--warn-subtle); color: var(--warn); }
  .badge-error { background: var(--danger-subtle); color: var(--danger); }
  .badge-pending { background: var(--surface-sunken); color: var(--text-faint); }

  /* Sidecar/worker health, admin-only. Token-based so both themes work —
   * the legacy versions were hardcoded for dark and became unreadable on
   * paper. */
  .badge-worker-down { background: var(--danger-subtle); color: var(--danger); }

  /* A dot that carries meaning needs a text equivalent beside it; this only
   * ever decorates a label. */
  .badge-dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: var(--radius-full);
    background: currentColor;
    flex: none;
  }

  /* --- forms ----------------------------------------------------------- */
  .field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }

  .label {
    font-size: var(--text-sm);
    font-weight: 550;
    color: var(--text);
  }

  .field-hint {
    color: var(--text-muted);
    font-size: var(--text-xs);
  }

  .field-error {
    color: var(--danger);
    font-size: var(--text-xs);
  }

  .input,
  .select,
  .textarea {
    width: 100%;
    min-height: var(--tap-min);
    padding: var(--space-2) var(--space-3);
    background: var(--surface-sunken);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    font-size: var(--text-base);
    transition: border-color var(--duration-fast) var(--ease);
  }
  .input::placeholder,
  .textarea::placeholder { color: var(--text-faint); }
  .input:hover,
  .select:hover,
  .textarea:hover { border-color: var(--text-faint); }
  .input:disabled,
  .select:disabled { opacity: 0.6; cursor: not-allowed; }
  .input[aria-invalid="true"] { border-color: var(--danger); }

  .textarea { min-height: 6rem; resize: vertical; }

  .select {
    appearance: none;
    padding-inline-end: var(--space-6);
    /* Chevron drawn with a gradient pair so there is no image request and
     * no colour literal. */
    background-image:
      linear-gradient(45deg, transparent 50%, currentColor 50%),
      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position:
      calc(100% - 1.05rem) calc(50% + 0.12rem),
      calc(100% - 0.65rem) calc(50% + 0.12rem);
    background-size: 0.4rem 0.4rem, 0.4rem 0.4rem;
    background-repeat: no-repeat;
    cursor: pointer;
  }

  .input-file {
    width: 100%;
    padding: var(--space-3);
    background: var(--surface-sunken);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text-muted);
    font-size: var(--text-sm);
    cursor: pointer;
  }
  .input-file:hover { border-color: var(--accent); }
  .input-file::file-selector-button {
    margin-inline-end: var(--space-3);
    padding: var(--space-2) var(--space-3);
    background: var(--surface-raised);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    font: inherit;
    font-size: var(--text-sm);
    cursor: pointer;
  }

  /* Horizontal form row that stacks on small screens. */
  .form-row {
    display: flex;
    align-items: flex-end;
    gap: var(--space-3);
    flex-wrap: wrap;
  }
  .form-row > .field { flex: 1 1 12rem; min-width: 0; }
  @media (max-width: 48rem) {
    .form-row { flex-direction: column; align-items: stretch; }
    /* `flex: 1 1 12rem` above is a *width* basis, which is what it means
       while this is a row. Turning the container into a column silently
       reinterprets it as a **height**: every field became at least 12rem
       tall and then grew to fill what was left, so labels and inputs sat
       hundreds of pixels apart on a phone (Toni, 2026-08-06).
       A basis chosen for one axis has to be released when the axis flips. */
    .form-row > .field { flex: 0 0 auto; }
    .form-row > .btn { width: 100%; }
  }

  /* Inline label + control, for toolbar-style settings. */
  .control {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-muted);
    white-space: nowrap;
  }
  .control .select { width: auto; min-width: 8rem; }

  /* --- tables ---------------------------------------------------------- */
  .table-wrap {
    overflow-x: auto;
    /* Without this a wide table stretches its grid/flex parent instead of
       scrolling, and the page — not the table — is what ends up sideways. */
    max-width: 100%;
    min-width: 0;
  }

  .table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
  }
  .table th,
  .table td {
    text-align: left;
    padding: var(--space-3);
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
  }
  .table thead th {
    position: sticky;
    /* 0, **not** --topbar-h. `.table-wrap` sets overflow-x: auto, which
     * forces overflow-y to compute to auto as well, so the wrap — not the
     * page — is the sticky scroll container. Offsetting by the topbar
     * height then pushed the header 56px down from the wrap's own top and
     * parked it on top of the first row, which simply vanished. Page-level
     * stickiness is not available to a table inside an overflow container;
     * this pins it to the wrap, which is the only thing that can scroll. */
    top: 0;
    z-index: 1;
    background: var(--surface);
    color: var(--text-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    white-space: nowrap;
  }
  .table tbody tr:last-child td { border-bottom: none; }
  .table tbody tr:hover { background: var(--surface-raised); }
  .table .num { text-align: right; font-variant-numeric: tabular-nums; }

  /* A table of numbers is unreadable at phone width and scrolling sideways
   * to compare rows is worse. `.table-stack` turns each row into a card and
   * labels each figure from data-label, so the header row is not needed.
   * Opt-in: only tables whose rows are self-contained records qualify. */
  @media (max-width: 48rem) {
    .table-stack,
    .table-stack tbody,
    .table-stack tr,
    .table-stack td,
    .table-stack th { display: block; width: 100%; }
    .table-stack thead { display: none; }
    .table-stack tbody tr {
      margin-bottom: var(--space-3);
      padding: var(--space-3) var(--space-4);
      background: var(--surface-raised);
      border: 1px solid var(--border);
      border-radius: var(--radius-md);
    }
    .table-stack tbody tr:last-child { margin-bottom: 0; }
    .table-stack tbody tr:hover { background: var(--surface-raised); }
    .table-stack td,
    .table-stack th {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: var(--space-3);
      padding: var(--space-1) 0;
      border: none;
      text-align: left;
    }
    .table-stack tbody th[scope="row"] {
      padding-bottom: var(--space-2);
      margin-bottom: var(--space-2);
      border-bottom: 1px solid var(--border);
      font-size: var(--text-md);
      font-weight: 600;
    }
    .table-stack td[data-label]::before {
      content: attr(data-label);
      color: var(--text-muted);
      font-size: var(--text-xs);
      text-transform: uppercase;
      letter-spacing: 0.05em;
    }
    .table-stack .num { text-align: right; }
  }

  /* A heading *inside* a card, for sections that belong together but are
     distinct actions — the admin console's invite / invitations / create
     grouping. Deliberately not `.section-title`, which marketing.css owns
     for the landing page and which the admin page never loads. */
  .card-subtitle {
    margin: var(--space-5) 0 var(--space-2);
    font-size: var(--text-sm);
    font-weight: 650;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
  }
  .card-subtitle:first-of-type { margin-top: var(--space-2); }

  /* The one-time invite link. Monospace because it is going to be copied,
     and full width because truncating a credential invites a partial copy. */
  .invite-link {
    margin-top: var(--space-2);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs);
  }

  /* --- progress --------------------------------------------------------
   * The bar is decorative; the accessible value lives on the wrapper via
   * role="progressbar" and aria-valuenow, set by the template or JS. A
   * screen-reader user previously got nothing at all from narration
   * progress, which is the app's primary feedback loop.
   */
  .progress {
    display: block;
    width: 100%;
    height: 0.375rem;
    background: var(--surface-sunken);
    border-radius: var(--radius-full);
    overflow: hidden;
  }
  .progress-fill {
    display: block;
    height: 100%;
    width: 0;
    background: var(--accent);
    border-radius: inherit;
    transition: width var(--duration-base) var(--ease);
  }
  .progress-done .progress-fill { background: var(--ok); }
  .progress-error .progress-fill { background: var(--danger); }

  /* Total unknown — "not started" and "no idea how long" must not look the
   * same, because 0% is what a hang looks like. */
  .progress-indeterminate .progress-fill {
    width: 35%;
    animation: progress-slide 1.4s var(--ease) infinite;
  }
  @keyframes progress-slide {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(340%); }
  }

  /* --- alerts ---------------------------------------------------------- */
  .alert {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
    font-size: var(--text-sm);
  }
  .alert-error {
    background: var(--danger-subtle);
    border-color: color-mix(in oklab, var(--danger) 40%, transparent);
    color: var(--text);
  }
  .alert-ok {
    background: var(--ok-subtle);
    border-color: color-mix(in oklab, var(--ok) 40%, transparent);
  }
  .alert-warn {
    background: var(--accent-subtle);
    border-color: color-mix(in oklab, var(--accent) 40%, transparent);
  }

  /* --- dialog ----------------------------------------------------------
   * Replaces window.confirm for destructive actions. Native <dialog> gives
   * focus trapping, Escape and inertness for free — hand-rolling those is
   * where accessible modals usually go wrong.
   */
  /* A closed <dialog> is display:none by UA stylesheet — but any author
   * `display` beats that, so `.dialog-wide { display: flex }` left the box
   * rendering in normal flow after close(): the backdrop vanished and an
   * empty card stayed behind on every page. Restated here, and every
   * display rule below is scoped to [open].
   *
   * Specificity: dialog:not([open]) is (0,1,1) and beats .dialog-wide
   * (0,1,0), so the guard holds regardless of source order. */
  dialog:not([open]) { display: none; }

  .dialog {
    max-width: 28rem;
    width: calc(100vw - var(--space-6));
    padding: var(--space-5);
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
  }
  .dialog::backdrop {
    background: rgb(0 0 0 / 0.55);
    backdrop-filter: blur(2px);
  }
  .dialog-title {
    font-family: var(--font-serif);
    font-size: var(--text-lg);
    margin-bottom: var(--space-2);
  }
  .dialog-body {
    color: var(--text-muted);
    font-size: var(--text-sm);
    margin-bottom: var(--space-5);
  }
  .dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
  }
  @media (max-width: 48rem) {
    .dialog-actions { flex-direction: column-reverse; }
    .dialog-actions .btn { width: 100%; }
  }

  /* --- toast ----------------------------------------------------------- */
  .toast-region {
    position: fixed;
    left: 50%;
    bottom: var(--space-5);
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    transform: translateX(-50%);
    pointer-events: none;
  }
  .toast {
    padding: var(--space-3) var(--space-4);
    background: var(--surface-raised);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    font-size: var(--text-sm);
    pointer-events: auto;
  }
  .toast-error { border-color: var(--danger); }

  /* --- misc ------------------------------------------------------------ */
  .hint { color: var(--text-muted); font-size: var(--text-sm); }
  .muted { color: var(--text-muted); }
  .tabular { font-variant-numeric: tabular-nums; }
}

@layer components {
  /* --- quota panel -----------------------------------------------------
   * The allowance, on both the account and plans pages. One component so
   * the two cannot describe the same number differently.
   */
  .quota-panel {
    display: grid;
    gap: var(--space-2);
    padding: var(--space-4);
    background: var(--surface-sunken);
    border-radius: var(--radius-md);
  }
  .quota-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
  }
  .quota-figures { font-size: var(--text-md); font-weight: 600; }
  .quota-plan { color: var(--text-muted); font-size: var(--text-sm); }
  .quota-plan strong { color: var(--text); }

  /* --- wide dialog (chapter text) -------------------------------------- */
  .dialog-wide {
    max-width: 46rem;
    width: min(46rem, calc(100vw - var(--space-6)));
    max-height: min(44rem, 85svh);
  }
  /* Scoped to [open] so it cannot resurrect a closed dialog. */
  .dialog-wide[open] { display: flex; flex-direction: column; }
  .dialog-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
  }
  .dialog-head .dialog-title { margin-bottom: 0; }
  .dialog-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding-right: var(--space-2);
    color: var(--text-muted);
    font-size: var(--text-base);
    line-height: var(--leading-prose);
    white-space: pre-wrap;
  }
  @media (max-width: 48rem) {
    .dialog-wide { max-height: 88svh; padding: var(--space-4); }
  }
}

@layer components {
  /* The acceptance page's list of outstanding documents. In components
     rather than marketing.css: /policies renders in the *app* shell, which
     never loads the marketing stylesheet. */
  .policy-list {
    margin: var(--space-3) 0 var(--space-5);
    padding-left: var(--space-5);
  }
  .policy-list li { margin-bottom: var(--space-2); }
  .policy-list .muted { margin-left: var(--space-2); font-size: var(--text-sm); }
}
