/* bo-field-states.css — validation, help text and the checkbox row.
 *
 * Split out of admin-fields.css purely for the ≤200 LoC gate, and split HERE
 * because this is the seam: admin-fields.css draws the field at rest, this file
 * draws it when it has something to say. Must load AFTER admin-fields.css — the
 * `.check` rules below deliberately outrank the wrapping-label rules there, and
 * a checkbox styled as a stacked field is a box on its own line above its own
 * sentence.
 *
 * Read admin-fields.css's header first: it explains the two label idioms both
 * files select on. */

/* ---- invalid -------------------------------------------------------------
 * Red ring plus a 400 ms shake. The shake is what makes a rejected field
 * findable on a long form without hunting for red — movement is caught
 * peripherally. Inert under prefers-reduced-motion (admin-motion.css). */
/* The float rule in admin-fields.css paints the label primary-blue at (0,2,3).
 * At (0,2,1) this lost, so a REJECTED field kept a blue label and never went
 * red — the one moment the colour is carrying information. Matching the float
 * rule's shape takes it to (0,2,3) and, tying, it wins on load order. */
.field:has([aria-invalid="true"]) > label:not(:has(:is(input, select, textarea))),
.field:has([aria-invalid="true"]) > label { color: var(--sn-error-text); }
/* Both idioms, because the ring has to land on whichever element is the BOX —
 * see admin-fields.css. Aimed only at the control, this reached the 9 bare
 * fields and silently missed the 50 that wrap theirs in an .input-group, where
 * the control has no border to colour. */
.field :is(input, select, textarea):not(.input-group *)[aria-invalid="true"],
.field > .input-group:has([aria-invalid="true"]) {
  border-color: var(--sn-error);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--sn-error) 16%, transparent);
  animation: bo-shake var(--sn-motion-duration-long) var(--sn-motion-ease);
}

.field-error {
  display: block;
  margin-top: 0.375rem;
  color: var(--sn-error-text);
  font-size: var(--sn-type-caption-size);
  animation: bo-pop var(--sn-motion-duration-short) var(--sn-motion-ease);
}
.field-hint,
.field > .hint {
  display: block;
  margin-top: 0.375rem;
  color: var(--sn-on-surface-variant);
  font-size: var(--sn-type-caption-size);
  letter-spacing: normal;
  text-transform: none;
  font-weight: 400;
}

/* ---- checkboxes ----------------------------------------------------------
 * `.check` is a wrapping label too, but it is a ROW, not a stacked field: the
 * box sits beside its sentence. Declared after idiom B so it wins. */
.field > label.check,
label.check {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--sn-type-body-2-size);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--sn-on-surface);
  cursor: pointer;
}
.check input[type="checkbox"] {
  min-height: 0;
  accent-color: var(--sn-primary);
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  transition: transform var(--sn-motion-duration-press) var(--sn-motion-ease-out);
}
.check input[type="checkbox"]:active { transform: scale(0.9); }

/* ---- a note grows with what is typed ------------------------------------- */
/* `field-sizing: content` makes the intrinsic size the CONTENT's, so the box
 * follows the text and never scrolls. A browser without support keeps today's
 * fixed box — a pure enhancement, with no JS fallback to go stale.
 *
 * It also stops `rows` from sizing anything, so the floor has to be restated or
 * an empty note opens one line tall instead of the three it opens as today. The
 * `+ 1rem + 2px` is NOT padding for the text: min-height is a BORDER-box
 * measurement here, so three rows only survives if it also covers the field's
 * own 0.5rem×2 padding and 1px×2 border. Measured: 75px, the same box as before.
 * `.textarea` (tickets) needs no floor — forms.css already gives it 6rem. */
.field textarea,
.textarea { field-sizing: content; }
/* The `:not()` chain is what makes this reach, the same way admin-fields.css:98
 * reaches: a bare `.field textarea` is (0,1,1) and loses to that file's
 * `.field :is(input, select, textarea):not(...)` at (0,3,1), which pins every
 * field to 48px. Repeating the chain wins at (0,4,1), on specificity rather than
 * on load order. Without it this rule computed 48px and never applied. */
.field textarea:not([type="checkbox"]):not([type="radio"]):not(.input-group *) {
  min-height: calc(3lh + 1rem + 2px);
}

/* ---- the six-digit code, and the step it lives in ------------------------
 *
 * It lives in THIS file rather than admin-fields.css because it is a field that
 * says what to put in it before the label is read, which is this file's subject.
 *
 * Every duration and curve is a --sn-motion-* token; nothing here is a literal.
 * All of it is switched off by admin-motion.css's blanket reduced-motion reset,
 * and none of it needs an entry in that file's explicit restore list — the
 * `from` state exists only inside the keyframe, never as a static opacity: 0, so
 * `animation: none` leaves an ordinary visible element. */
.otp-code {
  font-size: var(--sn-type-h5-size);
  letter-spacing: 0.5em;
  text-align: center;
  /* Offsets the trailing letter-space, which otherwise pushes the optically
   * centred value half a character to the right. */
  text-indent: 0.5em;
  font-variant-numeric: tabular-nums;
}

/* The swap target's contents, arriving. bo-row-in and not bo-dialog-in: this is
 * content landing in place, not a surface replacing the page. */
.otp-step {
  display: grid;
  gap: 0.875rem;
  animation: bo-row-in var(--sn-motion-duration-enter) var(--sn-motion-ease-out);
}

.otp-cooldown {
  color: var(--sn-on-surface-variant);
  font-size: var(--sn-type-caption-size);
}
