/* ============================================================================
   Quercia viva — forms
   ============================================================================
   Everything that makes a page you type into: the field itself, the single
   column it lives in, the boxes a choice is drawn as, the message when it is
   wrong, and the bar that saves it.

   The rules are written down in docs/brand/forms.md; the Symfony form theme in
   templates/form/quercia_layout.html.twig is what makes them the default
   rendering rather than something a template has to remember. Change one and
   the other two have to agree.

   Split out of components.css, which had grown to 1865 lines and held four
   unrelated input recipes.
   ============================================================================ */

:root {
  /* Masked, not drawn: the mask takes the alpha and the colour comes from
     `currentColor`, so one file serves the error message in both themes. */
  --qc-icon-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 8v4'/%3E%3Cpath d='M12 16h.01'/%3E%3C/svg%3E");
}

/* ================================================================== *
 * §6 — Inputs / forms
 * ================================================================== */
.form-control,
.form-select,
.input-group-text {
  background-color: var(--qc-field-bg);
  border-color: var(--qc-border);
  border-radius: var(--qc-radius-input);
  color: var(--qc-text-primary);
}
.form-control::placeholder { color: var(--qc-text-tertiary); }
.form-label { color: var(--qc-text-secondary); font-weight: 600; }

/* Mandatory-field marker: Tabler draws the asterisk after any `.required`
   label (Symfony adds that class to required fields) — retint it through the
   token layer so it stays contrast-safe in both themes. */
.required::after { color: var(--qc-error-text); font-weight: 700; }

/* ...but a group is mandatory once, not once per option. Symfony marks every
   child of a required expanded choice as required too, so a six-option radio
   group came out with six asterisks — one on the legend and one beside each
   name, where it reads as though each option were separately compulsory. The
   legend keeps its marker; the options drop theirs.

   Scoped to a fieldset on purpose: that is what the row of a compound field is
   rendered as, and it is the case where a legend is there to carry the marker.
   A lone required checkbox — terms, consent — is not in one, and keeps its
   asterisk because there is nothing else to hold it. */
fieldset .form-check-label.required::after { content: none; }

/* ------------------------------------------------------------------ *
 * Forms — one column, and a measure to read it by
 * ------------------------------------------------------------------ */

/* A form page, and how wide it is allowed to get.
 *
 * The column is capped at the measure; the page around it was not, so a form
 * card stretched to the window and left the fields hugging its left edge with
 * half a monitor of nothing beside them. The settings hub never had the problem
 * because the rail eats the difference. A page with no rail needs to say it
 * itself — wide enough for a canvas that has to be wide (the addresses, a line
 * editor), never wide enough to strand the column. */
.qc-formpage { max-width: 64rem; }

/* A single column, because a two-column form makes the eye choose between
   reading down and reading across, and the fields on the right are the ones
   people skip. The measure matters as much as the count: past ~40rem a label
   and its input drift apart horizontally and stop reading as a pair, which is
   what a full-width form on a wide monitor does.
 *
   The measure belongs to what is read, not to the container. Capping .qc-form
   itself also capped the bar that closes it, so the save button stopped at the
   edge of the column instead of the edge of the card — on a page whose card is
   wider than 42rem that reads as a button someone forgot to align. The cap
   moves to the children; the bar, the summary and the danger zone opt out, and
   `.qc-form__wide` is the valve for anything else that has to be wide. */
.qc-form { --qc-form-measure: 42rem; max-width: none; }
.qc-form > * { max-width: var(--qc-form-measure); }
.qc-form > .qc-form__actions,
.qc-form > .qc-form__summary,
.qc-form > .qc-form__danger,
.qc-form > .qc-form__wide { max-width: none; }

.qc-form__group + .qc-form__group {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--qc-divider);
}
/* A group that is not a sibling of the others — it sits outside the column
   because it holds a canvas — still needs the rule that separates it. */
.qc-form__group--ruled {
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--qc-divider);
}
.qc-form__grouptitle {
  font-family: var(--qc-font-display);
  font-size: 1rem;
  font-weight: 700;
  margin: 0 0 .125rem;
}
.qc-form__grouplede {
  color: var(--qc-text-secondary);
  font-size: .875rem;
  margin: 0 0 1rem;
}
/* The theme writes `mb-3 qc-form__row` on every row. `mb-3` is what spaces a
   row on the pages still laid out in Bootstrap columns, where a row is an only
   child with no sibling to space against; inside the column the rhythm below
   takes over, so the Bootstrap margin is handed back. */
.qc-form .qc-form__row { margin-bottom: 0; }
.qc-form__row + .qc-form__row { margin-top: 1rem; }
/* A column of checkboxes reads as one list, not as a stack of separate fields,
   so it closes up. */
.qc-form__row--tight + .qc-form__row--tight { margin-top: .375rem; }

/* An input as wide as what goes into it. A three-character abbreviation in a
   42rem field tells the reader to write an essay; the width is the first hint
   anybody gets about what is expected. */
.qc-form__row--tiny  :is(.form-control, .form-select, .ts-wrapper) { max-width: 7rem; }
.qc-form__row--short :is(.form-control, .form-select, .ts-wrapper) { max-width: 16rem; }

/* And where the input type already says how much goes in, no class is needed:
   thirteen of the settings are counts and thresholds, and each of them was a
   42rem box holding the number 12. */
.qc-form input[type="number"] { max-width: 8rem; }
.qc-form input[type="time"] { max-width: 8rem; }
.qc-form input[type="date"] { max-width: 11rem; }

/* One option per row, drawn as a card: the same shape the modules picker uses,
   because they are two tabs of the same form. The <label> is the card, so the
   whole row is the hit area, and the radio inside it stays a real radio. */
.qc-form__pick { display: flex; flex-direction: column; gap: .5rem; }

.qc-form__option {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin: 0;
  padding: .75rem 1rem;
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-card);
  background: var(--qc-surface-light);
  cursor: pointer;
  transition: border-color var(--qc-dur-standard) var(--qc-ease-standard),
              background-color var(--qc-dur-standard) var(--qc-ease-standard);
}
.qc-form__option:hover { border-color: var(--qc-primary); }
.qc-form__option:has(input:checked) {
  border-color: var(--qc-primary);
  background: color-mix(in srgb, var(--qc-primary) 8%, var(--qc-surface-light));
}
/* The focus ring belongs to the card, not to the radio buried on its right. */
.qc-form__option:has(input:focus-visible) {
  outline: 2px solid var(--qc-primary);
  outline-offset: 2px;
}
.qc-form__optionicon {
  display: flex;
  font-size: 1.25rem;
  color: var(--qc-text-secondary);
}
.qc-form__option:has(input:checked) .qc-form__optionicon { color: var(--qc-primary); }
.qc-form__optionname { display: block; font-weight: 600; }
.qc-form__optionlede { display: block; font-size: .875rem; color: var(--qc-text-secondary); }

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

/* ------------------------------------------------------------------ *
 * Hints
 * ------------------------------------------------------------------
 * `.form-hint` was emitted by thirteen templates and styled by none of
 * them, so it fell through to whatever the Tabler CDN happened to say.
 * Symfony's bootstrap_5 theme spells the same thing `.form-text`. Both
 * are drawn here: the house theme emits `.form-hint`, and a template
 * that still writes the other one does not silently lose its styling.
 * ------------------------------------------------------------------ */
.form-hint,
.form-text {
  display: block;
  margin-top: .35rem;
  margin-bottom: 0;
  font-size: .875rem;
  line-height: 1.4;
  color: var(--qc-text-tertiary);
}

/* ------------------------------------------------------------------ *
 * Validation
 * ------------------------------------------------------------------
 * Nothing in the project styled `.is-invalid`, so an invalid field came
 * out in Bootstrap's own red (#d63939) — not a Quercia colour, and as
 * text it does not clear AA on the warm-white canvas. The vivid
 * --qc-error is right for a border or a fill; --qc-error-text is the
 * darkened variant the token layer keeps for anything readable.
 *
 * Keyed off `.is-invalid` and never off `:invalid`, because `:invalid`
 * is true for a required field nobody has reached yet — which is how a
 * form ends up shouting at somebody who has not done anything wrong.
 * ------------------------------------------------------------------ */
.form-control.is-invalid,
.form-select.is-invalid,
textarea.is-invalid,
.ts-wrapper.is-invalid .ts-control {
  border-color: var(--qc-error) !important;
  background-color: color-mix(in srgb, var(--qc-error) 8%, var(--qc-field-bg)) !important;
  /* Bootstrap paints a warning glyph inside the field. The message below
     carries the icon instead, where it sits beside the words it explains. */
  background-image: none !important;
  padding-right: .75rem;
}
.form-control.is-invalid:focus,
.form-select.is-invalid:focus,
textarea.is-invalid:focus,
.ts-wrapper.is-invalid.focus .ts-control {
  border-color: var(--qc-error) !important;
  box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--qc-error) 30%, transparent) !important;
}

/* The message, under the field it belongs to — never only at the top of
   the page. The icon is not decoration: colour is not a signal every
   reader receives, so the state is always said twice. */
.qc-form__error {
  display: flex;
  align-items: flex-start;
  gap: .35rem;
  margin-top: .35rem;
  font-size: .875rem;
  line-height: 1.4;
  color: var(--qc-error-text);
}
.qc-form__error::before {
  content: "";
  flex: none;
  width: 1rem;
  height: 1rem;
  margin-top: .1rem;
  background-color: currentColor;
  -webkit-mask: var(--qc-icon-alert) center / contain no-repeat;
          mask: var(--qc-icon-alert) center / contain no-repeat;
}

/* Bootstrap's own class, still emitted by work_schedule_controller.js.
   Re-tinted only — its display stays Bootstrap's business, so an empty
   one cannot appear. */
.invalid-feedback { color: var(--qc-error-text); font-size: .875rem; }

/* The summary a long form gets, once, above the fields (docs/brand/forms.md
   §5.3). Twelve templates hand-rolled an `alert alert-danger` for this; this
   is the one recipe that replaces them. */
.qc-form__summary {
  margin-bottom: 1.25rem;
  padding: .875rem 1rem;
  border: 1px solid color-mix(in srgb, var(--qc-error) 40%, transparent);
  border-left-width: 3px;
  border-radius: var(--qc-radius-card);
  background: color-mix(in srgb, var(--qc-error) 10%, var(--qc-card-bg));
  color: var(--qc-text-primary);
}
.qc-form__summary h2 {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: 0 0 .35rem;
  font-family: var(--qc-font-display);
  font-size: 1rem;
  font-weight: 700;
  color: var(--qc-error-text);
}
.qc-form__summary ul { margin: 0; padding-left: 1.1rem; font-size: .9375rem; }
.qc-form__summary a { color: var(--qc-error-text); }

/* ------------------------------------------------------------------ *
 * Checkboxes, switches, and controls nobody can change
 * ------------------------------------------------------------------ */
.form-check-input {
  background-color: var(--qc-field-bg);
  border-color: var(--qc-border);
}
.form-check-input:checked {
  background-color: var(--qc-primary);
  border-color: var(--qc-primary);
}
.form-check-label { color: var(--qc-text-primary); }

/* Dim the field, never the text — quercia-viva §9.7. `opacity: 1` is there
   to undo Bootstrap, which dims the whole control including its label. */
.form-check-input:disabled {
  opacity: 1;
  background-color: var(--qc-surface-light);
  border-color: var(--qc-border);
}
.form-check-input:disabled ~ .form-check-label { color: var(--qc-text-tertiary); }
.form-control:disabled,
.form-control[readonly],
.form-select:disabled {
  opacity: 1;
  background-color: var(--qc-surface-light);
  color: var(--qc-text-secondary);
}
.input-group-text { color: var(--qc-text-secondary); }

/* The small round "what is this?" beside a label, opening the field's help
   modal. It was three inline styles and a 14px inline SVG, repeated per use;
   the icon comes from the webfont now and the size from here. */
.qc-form__helpbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.1rem;
  height: 1.1rem;
  padding: 0;
  border: 0;
  font-size: .9rem;
  line-height: 1;
  color: var(--qc-text-tertiary);
  background: none;
}
.qc-form__helpbtn:hover { color: var(--qc-primary); background: none; }

/* ------------------------------------------------------------------ *
 * The one sanctioned two-column row
 * ------------------------------------------------------------------
 * Two inputs that are a single datum — a start and an end, a quantity
 * and its unit. The test is not "both are short"; it is "reading one
 * without the other is meaningless".
 * ------------------------------------------------------------------ */
.qc-form__pair {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  align-items: flex-start;
}
.qc-form__pair > * { flex: 1 1 12rem; min-width: 0; }
/* The halves of one datum are rarely the same size: a street is not a house
   number, a town is not a postcode. */
.qc-form__pair > .is-wide { flex-grow: 3; }
.qc-form__pair > .is-narrow { flex: 0 1 8rem; }
@media (max-width: 575.98px) {
  .qc-form__pair > *,
  .qc-form__pair > .is-narrow { flex: 1 1 100%; }
}

/* ------------------------------------------------------------------ *
 * The bar that closes the form
 * ------------------------------------------------------------------
 * It closes the *form*, not the column: it spans the card it sits in, so
 * the primary lands on the same edge as everything else on the page. A bar
 * capped at the reading measure puts the save button in the middle of a
 * wide card, which reads as an alignment somebody missed.
 *
 * `.qc-settings__actions` was the settings pages' private name for the
 * same thing and is gone; both templates that used it now say
 * `.qc-form__actions`.
 * ------------------------------------------------------------------ */
.qc-form__actions {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--qc-divider);
}
/* The primary sits right, and whatever else is in the bar stays left of it.
   The selector is deliberately a child selector: a bar whose buttons somebody
   wraps in a `.btn-list` stops matching and loses the alignment silently, so
   the buttons are the bar's own children everywhere. */
.qc-form__actions > [type="submit"],
.qc-form__actions > .btn-primary { margin-left: auto; }

/* When the bar already sits in something that separates it — a card-footer,
   a modal footer — it brings no rule and no gap of its own. */
.qc-form__actions--bare {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* Past a screenful the bar follows: a save button you have to go looking
   for is one people stop trusting.
 *
 * It sits on the page rather than in a card — a form several cards long has no
 * single card for it to close — so it takes the page's own ground and pulls the
 * card gutter back on both sides, otherwise the content it is covering shows
 * past its edges as it scrolls under. The shadow points upward, which is where
 * the thing it is covering is. */
.qc-form__actions--sticky {
  position: sticky;
  bottom: 0;
  z-index: 3;
  margin-top: 1.5rem;
  padding: .875rem 1.25rem;
  border-radius: var(--qc-radius-card) var(--qc-radius-card) 0 0;
  background: var(--qc-surface);
  box-shadow: 0 -8px 24px -12px var(--qc-shadow-color);
}

/* Facts a form does not decide.
 *
 * The username somebody signs in with, the rung they stand on, the sigla the
 * planner writes on a job: an administrator's decisions, shown on the account
 * page beside the four fields that are the person's own. A disabled input would
 * have said the same thing and invited them to try anyway; a definition list
 * says it is a fact and stops. */
.qc-facts { margin: 0; }
.qc-facts__row {
  display: flex;
  gap: 1rem;
  align-items: baseline;
  padding: .5rem 0;
}
.qc-facts__row + .qc-facts__row { border-top: 1px solid var(--qc-divider); }
.qc-facts dt {
  flex: 0 0 12rem;
  font-weight: 600;
  font-size: .8125rem;
  color: var(--qc-text-secondary);
}
.qc-facts dd { margin: 0; color: var(--qc-text-primary); }
@media (max-width: 575.98px) {
  .qc-facts__row { flex-direction: column; gap: .125rem; }
  .qc-facts dt { flex: none; }
}

/* The attachment bar: what is already pinned to a note, and one tile to add
 * more.
 *
 * The tiles were sized by an inline `style` on every one of them — 80px twice,
 * a dashed border with an `!important` to beat Bootstrap's, and a remove button
 * eighteen pixels square built out of `position-absolute top-0 end-0 p-0` and
 * three more declarations. A hex never leaked in only because nothing here was
 * coloured; the sizes still belonged in a stylesheet. */
.qc-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: .25rem;
}
.qc-attachment {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 5rem;
  height: 5rem;
  padding: .25rem;
  overflow: hidden;
  cursor: pointer;
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-btn);
  background: var(--qc-field-bg);
}
.qc-attachment img {
  max-width: 100%;
  max-height: 100%;
  object-fit: cover;
  border-radius: calc(var(--qc-radius-btn) - .25rem);
}
.qc-attachment--add {
  border-style: dashed;
  color: var(--qc-text-tertiary);
}
.qc-attachment--add:hover {
  border-color: var(--qc-primary);
  color: var(--qc-primary);
}
/* The × is small because it is on top of the thing it removes. It gets the
   error tint rather than a filled red button, which at 18px was a red dot with
   no readable glyph in it. */
.qc-attachment__remove {
  position: absolute;
  top: .125rem;
  right: .125rem;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  padding: 0;
  font-size: .75rem;
  line-height: 1;
  border: 0;
  border-radius: 999px;
  color: var(--qc-error-text);
  background: color-mix(in srgb, var(--qc-error) 22%, var(--qc-card-bg));
}
.qc-attachment__remove:hover {
  color: var(--qc-on-primary);
  background: var(--qc-error);
}

/* A tile whose file is still going up. The ring is the brand's loading mark at
   the only size that fits inside an 80px tile. */
.qc-attachment--pending { cursor: default; }
.qc-attachment__pending {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  border: 2px solid color-mix(in srgb, var(--qc-primary) 25%, transparent);
  border-top-color: var(--qc-primary);
  animation: qc-attachment-spin 900ms linear infinite;
}
@keyframes qc-attachment-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .qc-attachment__pending { animation: none; border-top-color: var(--qc-primary); }
}

/* A delete never shares a row with a save. It goes to the bottom, in its
   own zone, and it asks through QcDialog before it happens. */
.qc-form__danger {
  margin-top: 2rem;
  padding: 1rem 1.25rem;
  border: 1px solid color-mix(in srgb, var(--qc-error) 35%, transparent);
  border-radius: var(--qc-radius-card);
  background: color-mix(in srgb, var(--qc-error) 6%, var(--qc-card-bg));
}
.qc-form__danger h3 {
  margin: 0 0 .125rem;
  font-family: var(--qc-font-display);
  font-size: 1rem;
  font-weight: 700;
  color: var(--qc-error-text);
}
.qc-form__danger p {
  margin: 0 0 .75rem;
  font-size: .875rem;
  color: var(--qc-text-secondary);
}

/* Bootstrap makes a compound field's label a <legend class="col-form-label">,
   which floats and takes the full width. Inside the column it is a field label
   like any other. */
.qc-form fieldset > legend {
  float: none;
  width: auto;
  padding: 0;
  font-size: 1rem;
}

/* Segmented control (all-day vs time-range) */
.qc-seg {
    display: inline-flex; padding: 3px; gap: 3px; margin-top: .25rem;
    background: var(--qc-surface-light); border: 1px solid var(--qc-border);
    border-radius: var(--qc-radius-pill);
}
.qc-seg__btn {
    appearance: none; border: 0; background: transparent; cursor: pointer;
    color: var(--qc-text-secondary); font: 600 .85rem/1 var(--qc-font-body);
    padding: .45rem .95rem; border-radius: var(--qc-radius-pill);
    transition: background-color var(--qc-dur-fast) var(--qc-ease-fast),
                color var(--qc-dur-fast) var(--qc-ease-fast),
                box-shadow var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-seg__btn:hover { color: var(--qc-text-primary); }
.qc-seg__btn.is-active { background: var(--qc-card-bg); color: var(--qc-text-primary); box-shadow: var(--qc-shadow-sm); }
.qc-seg__btn:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 2px; }
/* A segment that navigates instead of submitting. The site pivot's axis is
   drawn server-side, so choosing one is a link — and a link left to itself
   arrives underlined and baseline-aligned inside the pill. */
a.qc-seg__btn { display: inline-flex; align-items: center; text-decoration: none; }

/* Inline start→end time range + live duration */
.qc-sched__times { display: flex; align-items: flex-end; flex-wrap: wrap; gap: .5rem .75rem; }
.qc-sched__times > div { display: flex; flex-direction: column; }
.qc-sched__times .form-control { max-width: 7rem; }
.qc-sched__arrow { align-self: center; color: var(--qc-text-tertiary); font-size: 1.15rem; padding-bottom: .4rem; }
.qc-sched__dur { align-self: center; color: var(--qc-text-secondary); font-weight: 600; padding-bottom: .5rem; white-space: nowrap; }

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

/* ------------------------------------------------------------------ *
 * Segmented control, on real radios
 * ------------------------------------------------------------------
 * `.qc-seg` above is button-based and driven by JavaScript, so it can
 * only carry role="group" — which tells assistive technology that these
 * things belong together and nothing at all about which one is chosen.
 * A choice field renders as this instead: the same pill, built from the
 * radios Symfony already emits, so the arrow keys, the selected state
 * and the screen reader come for free. New segmented controls use this;
 * the button-based one survives only in templates/work/edit.html.twig.
 * ------------------------------------------------------------------ */
.qc-seg__seg {
  position: relative;
  display: inline-flex;
  margin: 0;
}
/* Out of sight, not removed: it keeps focus and arrow-key behaviour. */
.qc-seg__seg input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
  pointer-events: none;
}
.qc-seg__face {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--qc-text-secondary);
  font: 600 .85rem/1 var(--qc-font-body);
  padding: .45rem .95rem;
  border-radius: var(--qc-radius-pill);
  transition: background-color var(--qc-dur-fast) var(--qc-ease-fast),
              color var(--qc-dur-fast) var(--qc-ease-fast),
              box-shadow var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-seg__seg:hover .qc-seg__face { color: var(--qc-text-primary); }
.qc-seg__seg input:checked + .qc-seg__face {
  background: var(--qc-card-bg);
  color: var(--qc-text-primary);
  box-shadow: var(--qc-shadow-sm);
}
/* The ring goes on the face, which is what the eye is actually looking at. */
.qc-seg__seg input:focus-visible + .qc-seg__face {
  outline: 2px solid var(--qc-primary);
  outline-offset: 2px;
}

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

/* ---------------------------------------------------------------------------
   Swatch pickers — the status colour and the status icon
   ---------------------------------------------------------------------------
   Both choices are visual, so the control shows the result rather than its name
   (see system/settings/_swatches.html.twig). The radio is moved out of sight,
   never hidden: it keeps its place in the tab order and the arrow keys keep
   working, which `display: none` would take away.
*/
.qc-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
}

.qc-swatch {
  position: relative;
  display: inline-flex;
  cursor: pointer;
  border-radius: var(--qc-radius-btn);
}

.qc-swatch__input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}

.qc-swatch__face {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--qc-radius-btn);
  border: 1px solid var(--qc-border);
  background: var(--qc-surface);
  color: var(--qc-text-secondary);
  transition: box-shadow var(--qc-dur-fast) var(--qc-ease-fast),
              border-color var(--qc-dur-fast) var(--qc-ease-fast);
}

/* The colour swatch is the badge: the tint at the strength a chip wears it. */
.qc-swatches--color .qc-swatch__face {
  background: var(--qc-swatch);
  border-color: color-mix(in srgb, var(--qc-swatch) 60%, transparent);
}

.qc-swatch:hover .qc-swatch__face {
  border-color: var(--qc-text-tertiary);
}

/* The ring goes outside the swatch so the colour underneath stays the colour
   being chosen — an inset ring would eat into the sample. */
.qc-swatch__input:checked + .qc-swatch__face {
  border-color: var(--qc-primary);
  box-shadow: 0 0 0 2px var(--qc-primary);
}

.qc-swatches--icon .qc-swatch__input:checked + .qc-swatch__face {
  color: var(--qc-primary);
}

.qc-swatch__input:focus-visible + .qc-swatch__face {
  outline: 2px solid var(--qc-primary);
  outline-offset: 3px;
}

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

/* ------------------------------------------------------------------ */
/* Tick boxes in families — the permissions matrix (forms.md §4, §7)   */
/* ------------------------------------------------------------------ */

/* A long list of tick boxes in families. One fieldset per family, each
   item a label that carries its word and the line under it, closed up to
   the list rhythm. Not cards: forty-nine cards is a wall. */
.qc-form__checklist { display: flex; flex-direction: column; gap: 1.25rem; }
.qc-form__checkgroup { margin: 0; padding: 0; border: 0; min-width: 0; }
.qc-form__checkgrouptitle {
  float: none; width: auto; padding: 0; margin: 0 0 .25rem;
  font-size: .8125rem; font-weight: 600; color: var(--qc-text-secondary);
}
.qc-form__check {
  display: grid; grid-template-columns: auto 1fr; column-gap: .625rem; align-items: start;
  margin: 0; padding: .4rem 0; cursor: pointer;
}
.qc-form__check + .qc-form__check { border-top: 1px solid var(--qc-divider); }
.qc-form__check .form-check-input { margin-top: .2rem; }
.qc-form__checkname { display: block; font-weight: 500; }
.qc-form__checklede { display: block; font-size: .8125rem; color: var(--qc-text-secondary); }
/* Locked: the box says it cannot change, the note above the group says why. */
.qc-form__check:has(input:disabled) { cursor: default; }
.qc-form__check:has(input:disabled) .qc-form__checkname { color: var(--qc-text-secondary); }
/* The ring belongs to the row, not to the box. */
.qc-form__check:has(input:focus-visible) {
  outline: 2px solid var(--qc-primary); outline-offset: 2px; border-radius: var(--qc-radius-input, .5rem);
}

/* A swatch drawn as a webfont glyph sits like the SVG ones do. */
.qc-swatch__face > .ti { font-size: 1.25rem; line-height: 1; }
