/* Portal forms.
 *
 * The problem this file exists to solve: every portal form was one flat column
 * of full-width controls. "Add a kiosk" was fourteen fields and 1989px tall,
 * the settings page twenty-one and 3564px, and nothing in either said which
 * fields belonged together or which mattered. An operator scrolled a wall.
 *
 * Three rules do most of the work.
 *
 * A field is as wide as the answer it wants. A countdown in seconds is two
 * digits and was given a 580px box, which is not generous, it is misleading:
 * the width of an input is the strongest hint a form gives about the size of
 * the expected answer. `--field-w` carries that per field.
 *
 * Related fields sit in a titled group. Identity, behaviour, money and notes
 * are four decisions, not fourteen, and a legend costs one line to say so.
 *
 * Short fields pair up on a wide window. Two 8em boxes stacked leave a metre of
 * empty grey to their right and push everything below them down a row for no
 * reason.
 *
 * Loaded after components.css, so it wins where the two disagree. Everything
 * here is tokens; no raw colour.
 */

/* --- groups --------------------------------------------------------------- */

/* The section a form is divided into. `fieldset` rather than a div because the
 * grouping is semantic: a screen reader announces the legend with each control
 * inside it, so "Behaviour, Countdown seconds" is self-locating in a way that
 * "Countdown seconds" alone is not. */
.form-group {
  margin: 0 0 26px;
  padding: 0;
  border: 0;
  border-top: 1px solid var(--border);
  padding-top: 18px;
}

/* The first group has the page heading directly above it, so a rule there would
   be a second line under a line. */
.form-group:first-of-type {
  border-top: 0;
  padding-top: 0;
}

.form-group > legend {
  padding: 0;
  margin-bottom: 2px;
  color: var(--text);
  font-size: 15px;
  font-weight: 650;
  letter-spacing: -0.01em;
}

/* One sentence under the legend saying what the group decides, for the groups
   where the title alone is not enough. Capped at a readable measure rather
   than the width of the form. */
.form-group > .group-help {
  max-width: 72ch;
  margin: 0 0 14px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.55;
}

/* --- the row -------------------------------------------------------------- */

/* Fields flow in a row and wrap. A field's width comes from `--field-w`, which
 * the markup sets per field, defaulting to a full line. `flex-basis` rather
 * than a grid column because the widths are deliberately unequal: a grid would
 * make a 7em number box share a track with a 40ch name. */
.form-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0 18px;
  align-items: flex-start;
}

.form-row > .field {
  flex: 1 1 var(--field-w, 100%);
  min-width: min(var(--field-w, 100%), 100%);
  max-width: 100%;
}

/* Timing and motion: six equal cells, two rows of three, so a help sentence
 * on one field cannot stretch its neighbour or leave a hole. */
.form-group--dense .form-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px 16px;
  align-items: start;
}

.form-group--dense .form-row > .field {
  flex: none;
  min-width: 0;
  max-width: none;
  width: auto;
  margin-bottom: 0;
}

.form-group--dense .form-row > .field > :is(input, select, textarea) {
  width: 100%;
  max-width: none;
}

@media (max-width: 720px) {
  .form-group--dense .form-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Two equal columns, for a person and a phone, a city and a country, a
 * currency and a cap. Flex wrap with mixed `field-large` and `field-medium`
 * bases cannot do this: 22em plus 16em plus the gap is sometimes one row and
 * sometimes two, so the same form reads as a pair on a 1280 window and as a
 * leftover on a 1100 one, and a checkbox in the same flow sits wherever the
 * last wrap left a hole.
 *
 * A two-track grid makes the pairing a statement rather than an accident of
 * remaining space. A field that asked for no size, and any checkbox, spans
 * both columns, so a name, an address line or "this place is open" still
 * takes the whole measure. */
.form-group--pair .form-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 18px;
  align-items: start;
}

.form-group--pair .form-row > .field {
  flex: none;
  width: auto;
  min-width: 0;
  max-width: none;
  margin-bottom: 12px;
}

.form-group--pair .form-row > .field:not(.field-tiny):not(.field-small):not(.field-medium):not(.field-large),
.form-group--pair .form-row > .field-check {
  grid-column: 1 / -1;
}

.form-group--pair .form-row > .field:not(.field-check) > :is(input, select, textarea) {
  width: 100%;
  max-width: 100%;
}

.field textarea.mono {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: 13px;
  line-height: 1.45;
}

@media (max-width: 720px) {
  .form-group--pair .form-row {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* A field that stated a width keeps it.
 *
 * With `flex-grow: 1` the basis was only a starting point, so the spend cap,
 * alone on its row and asked to be 11em, grew to the full 700px of the card and
 * was indistinguishable from the fields that had asked for the whole line. A
 * declared width has to survive being the only thing on its row, otherwise the
 * declaration means "at least this wide", which is not a useful thing to say
 * about a two-digit number. */
.form-row > .field-tiny,
.form-row > .field-small,
.form-row > .field-medium,
.form-row > .field-large {
  flex-grow: 0;
}

/* The sizes, named for the answer rather than for a number of pixels, so the
 * markup reads as a statement about the data.
 *
 * `--field-w` is the flex basis and the control fills it, so a field never
 * needs a width of its own and two of them never disagree. */
.field-tiny {
  --field-w: 6.5em;
}

.field-small {
  --field-w: 10.5em;
}

.field-medium {
  --field-w: 16em;
}

.field-large {
  --field-w: 22em;
}

/* --- fields --------------------------------------------------------------- */

.field {
  margin-bottom: 16px;
}

.field > label {
  display: block;
  margin-bottom: 5px;
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
}

/* Required, marked in two ways because a colour is not a marker.
   The asterisk is decorative here; the partial also emits a visually hidden
   "(required)" so a screen reader is told rather than shown. */
.field-required {
  margin-left: 3px;
  color: var(--err);
  font-weight: 700;
}

/* Help under the control, capped at a readable measure.
 *
 * It used to wrap at roughly 45 characters under a full-width input, so the
 * prose and the box it explained had two different widths and the pairing
 * looked accidental. 70ch is inside the 65-75 the guideline asks for and is
 * wider than any field, so short fields get one line rather than four. */
.field .help {
  max-width: 70ch;
  margin-top: 5px;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.55;
}

/* A narrow field that carries help gets a column wide enough to read it in,
 * while the control itself stays narrow.
 *
 * The two widths are different things and conflating them was the bug. The
 * control's width is a statement about the answer: a spend cap in cents is a
 * short number and a wide box for it misleads. The column's width is a measure
 * for prose, and a sentence set in 10.5em is five lines of two words, so a row
 * of those was a ragged grey block with the inputs lost inside it.
 *
 * Splitting them costs one token. `--field-col` is what the row lays out, and
 * it only differs from `--field-w` where there is prose to make room for. */
.form-row > .field {
  flex-basis: var(--field-col, var(--field-w, 100%));
}

.field-tiny:has(.help),
.field-small:has(.help) {
  --field-col: max(var(--field-w), 23ch);
}

/* The control no longer fills its column, so it has to be told the width the
   column was named for. Only for the sized fields: a full-width field's control
   should still fill the line. */
.field-tiny > :is(input, select, textarea),
.field-small > :is(input, select, textarea),
.field-medium > :is(input, select, textarea),
.field-large > :is(input, select, textarea) {
  width: var(--field-w);
  max-width: 100%;
}

.field > input[type="file"] {
  max-width: 100%;
}

/* --- checkboxes and radios ------------------------------------------------ */

/* A tick is the one control that goes before its label.
 *
 * Rendered like the others it was a 13px native square under a bold heading
 * with help text beneath, which reads as a broken widget. `field-check` puts
 * the box and its label on one line, and the box is drawn rather than left to
 * the platform so it matches the inputs above it on a dark surface. */
.field-check {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 4px 10px;
}

/* A tick is a sentence, not a short answer, so it never shares a flex row.
 *
 * Left in the wrap it sat in whatever hole the last pair left: "This place is
 * open" next to "Description", "Payment enabled" under a leftover currency
 * box. The help under it then set the height of that row and the pairing
 * looked like a layout fault rather than a choice. */
.form-row > .field-check {
  flex: 1 1 100%;
  max-width: 100%;
}

.field-check > input[type="checkbox"] {
  grid-column: 1;
  grid-row: 1;
  margin: 1px 0 0;
}

.field-check > label {
  grid-column: 2;
  grid-row: 1;
  margin: 0;
  font-weight: 500;
  cursor: pointer;
}

.field-check > .help,
.field-check > [id$="-error"] {
  grid-column: 2;
  margin-top: 2px;
}

/* The box itself. 18px rather than the platform's 13: it is a click target on a
 * dense page, and a 13px square next to 13px text is a target smaller than the
 * word beside it. */
input[type="checkbox"],
input[type="radio"] {
  appearance: none;
  flex: none;
  width: 18px;
  height: 18px;
  background: var(--surface-sunk);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-small);
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out);
}

input[type="radio"] {
  border-radius: 50%;
}

input[type="checkbox"]:hover,
input[type="radio"]:hover {
  border-color: var(--primary);
}

input[type="checkbox"]:checked,
input[type="radio"]:checked {
  background-color: var(--primary);
  border-color: var(--primary);
}

/* The tick is drawn with a border rather than a glyph, so it needs no font and
   cannot arrive late or missing. */
input[type="checkbox"]:checked::after {
  content: "";
  display: block;
  width: 5px;
  height: 9px;
  margin: 1px auto 0;
  border: solid var(--primary-text);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

input[type="radio"]:checked::after {
  content: "";
  display: block;
  width: 6px;
  height: 6px;
  margin: 5px auto;
  background: var(--primary-text);
  border-radius: 50%;
}

input[type="checkbox"]:disabled,
input[type="radio"]:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* --- a list of tick boxes ------------------------------------------------- */

/* `CheckboxSelectMultiple` and `RadioSelect`. Three languages came out as three
 * unstyled rows of native squares taking a third of the group's height. As a
 * row of chips they read as one set of choices, each one a target rather than a
 * pixel, and the group is one line instead of three.
 *
 * Addressed through the `field-choices` class the partial adds, not through
 * Django's markup, which is a `<div>` per option inside a `<div>` carrying the
 * field id and has changed shape before. */
.field-choices > div {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 0;
}

.field-choices > div > div {
  margin: 0;
}

.field-choices > div label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 7px 12px 7px 10px;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition:
    border-color var(--dur-micro) var(--ease-out),
    background-color var(--dur-micro) var(--ease-out);
}

.field-choices > div label:hover {
  border-color: var(--border-strong);
}

/* `:has` so the whole chip reflects the box inside it. Supported everywhere the
   portal runs, and where it is not the box itself still shows the state. */
.field-choices > div label:has(input:checked) {
  background: var(--primary-soft, var(--surface));
  border-color: var(--primary);
}

/* --- actions -------------------------------------------------------------- */

/* Save stays on screen.
 *
 * On the settings page it was 3564px down, so the answer to "did that save"
 * required scrolling to the bottom to find out, and a form that long is
 * abandoned rather than submitted. Sticky to the bottom of the viewport with
 * the page's own ground behind it, so rows scroll under rather than through. */
.form-actions {
  position: sticky;
  /* Flush with the bottom edge of the card rather than 16px above it, so the
     card's own padding is not a transparent strip under the bar through which
     a row of fields is visible sliding past. */
  bottom: calc(-1 * var(--card-pad, 16px));
  z-index: var(--z-raised);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  /* Full width of the card, undoing the padding on both sides, so the rule
     above the bar reaches the edges like the group rules do. */
  margin: 24px calc(-1 * var(--card-pad, 16px)) 0;
  padding: 13px var(--card-pad, 16px);
  /* Opaque, not a fade. A translucent bar over a form is two sets of text in
     the same place, and the one you want is the one behind. */
  background: var(--form-actions-bg, var(--bg));
  border-top: 1px solid var(--border);
  border-bottom-left-radius: var(--radius-panel);
  border-bottom-right-radius: var(--radius-panel);
}

/* Inside a card the bar stands on the card, not on the page. Reading `--bg`
   there drew a near-black strip across a lighter surface. */
.card > .form-actions,
.card .form-actions {
  --form-actions-bg: var(--surface);
}

/* A form short enough to see whole does not need a bar stuck to the window. */
.form-actions-inline {
  position: static;
  background: none;
  margin: 18px 0 0;
  padding: 14px 0 0;
  border-radius: 0;
}

/* Keyboard focus must not land underneath the bar.
 *
 * Content scrolling under a sticky footer is the point of one, and a mouse user
 * simply scrolls past it. Tabbing is different: the browser scrolls the focused
 * control only just into view, which for the last field above the bar means
 * exactly behind it. The operator then types into a box they cannot see.
 *
 * `scroll-padding-bottom` tells the browser the bottom of the viewport is not
 * all usable, so every scroll it performs itself stops short by that much. It
 * costs nothing when there is no bar.
 *
 * The reserve is the bar's own parts rather than a bare number: its two 13px
 * bands, the control between them, its 1px rule, and the ring the focused
 * field wants clear of it. Sixteen of that is given back by the negative
 * `bottom` above, so this errs long by design, and a field stopping slightly
 * high is the harmless direction to be wrong in. */
html {
  --form-actions-reserve: calc(13px * 2 + 40px + 1px + var(--ring-reach));

  scroll-padding-bottom: var(--form-actions-reserve);
}

@media (prefers-reduced-motion: reduce) {
  .field,
  input[type="checkbox"],
  input[type="radio"],
  .field-choices > div label {
    transition: none;
  }
}

/* --- narrow --------------------------------------------------------------- */

/* Every field takes the full line on a phone. Two 11em fields side by side in a
   390px column is two cramped boxes, and the pairing was only ever there to use
   up horizontal space that no longer exists. */
@media (max-width: 720px) {
  .form-row > .field {
    flex-basis: 100%;
    min-width: 0;
  }

  /* The seam between groups is a rule, 18px above it and 26px below: 44px per
     seam, which buys separation from the fields that pair up either side of it.
     Nothing pairs at this width, so the whole 44px is being spent on the rule
     alone. 28px still reads as a division and gives a five-group form 80px of
     its scroll back. */
  .form-group {
    margin-bottom: 16px;
    padding-top: 12px;
  }
}

/* Activity toolbar. One row: heading, then three pieces that must not wrap
   (Usage/Money, Pictures, Videos). `chart-stat` is a single string so the
   30px `.figure-value` card style cannot put the number on its own line.
   This file loads last, which is what beats `select { width: 100% }`. */
.activity-toolbar {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
  white-space: nowrap;
}

.activity-toolbar > h2 {
  margin: 0;
  font-size: 17px;
  flex: 0 0 auto;
}

.activity-meta {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 12px;
  margin-left: auto;
  width: max-content;
}

.activity-meta > .chart-totals {
  display: contents;
}

.activity-meta > .chart-totals > li {
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  list-style: none;
  white-space: nowrap;
}

.activity-meta > .chart-totals > li::before {
  content: "";
  flex: 0 0 8px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.activity-meta .chart-stat {
  font-size: 13px;
  font-weight: 550;
  line-height: 1.2;
}

.activity-meta > .chart-pick {
  display: inline-flex;
  flex: 0 0 auto;
  width: auto;
  margin: 0;
}

select.chart-show[name="chart-show"] {
  display: inline-block;
  width: auto;
  min-width: 0;
  max-width: 9em;
  flex: 0 0 auto;
  padding: 3px 8px;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.2;
  field-sizing: content;
}
